设计 Token 的跨团队协作治理:命名空间、所有权与变更管理
设计 Token 的跨团队协作治理命名空间、所有权与变更管理一、那个 colors-primary 被三个团队同时修改的黑色星期一公司设计系统的 Token 仓库有 3 个 Core Maintainer、来自 6 个业务线的 28 个 Contributors。某个周一--color-primary的值从#3B82F6变成了#2563EB——因为产品 A 的设计师在 Figma 中更新了主色导出的 Token JSON 覆盖了全局文件。产品 B 和 C 的页面在一夜之间从亮蓝变成了深蓝用户打开 App 以为自己点错了产品。这不是技术 Bug——这是 Token 所有权的治理缺位。当所有团队共享一份 Token 文件时谁有权修改什么、修改了如何通知受影响的消费者、如何追溯变更是谁批准的——这三个问题没有答案Token 的共享就变成了 Token 的共毁。二、Token 治理的三层模型flowchart TD A[Token 治理架构] -- B[命名空间层br/Who can see what] A -- C[所有权层br/Who can change what] A -- D[变更管理层br/How changes propagate] B -- B1[global: 全局 Tokenbr/所有团队可见] B -- B2[team-A: 团队 A 专用br/仅团队 A 可见] B -- B3[internal: 内部实现br/不对外暴露] C -- C1[Design Tokens → 设计团队br/拥有最终审定权] C -- C2[Semantic Tokens → 设计系统团队br/拥有映射规则] C -- C3[Component Tokens → 组件库团队br/拥有具体绑定] D -- D1[PR 级CODEOWNERS 自动指定审批人] D -- D2[通知级变更影响面自动 mention 消费者] D -- D3[版本级Token Break Change 走 MAJOR 语义化版本]三、Token 治理的实现/** * Token 治理系统 * * 核心机制 * 1. 命名空间global / team-* / internal 三级隔离 * 2. 所有权CODEOWNERS 映射 文件级审批规则 * 3. 变更传播Token Change → 影响面分析 → 自动通知 → 版本号升级 */ // Token 命名空间 interface TokenNamespace { prefix: string; // 命名空间前缀 global, team-marketing visibility: public | team | internal; owners: string[]; // GitHub Team 名称 consumers: string[]; // 依赖此命名空间的团队列表 description: string; } const NAMESPACES: TokenNamespace[] [ { prefix: global, visibility: public, owners: [design-system-core], consumers: [team-marketing, team-payments, team-admin, team-partner], description: 所有产品线共享的基础 Token颜色、间距、字体、阴影, }, { prefix: team-marketing, visibility: team, owners: [team-marketing], consumers: [team-marketing], description: 营销线专用 Token活动主题色、促销标签、倒计时组件, }, { prefix: internal, visibility: internal, owners: [design-system-core], consumers: [], description: 内部实现 Token不向业务团队暴露如骨架屏动画参数, }, ]; /** * Token 文件结构示例 * * tokens/ * ├── global/ * │ ├── colors.json → global/colors (所有团队的公共色板) * │ ├── spacing.json → global/spacing * │ └── typography.json → global/typography * ├── team-marketing/ * │ ├── campaign.json → 营销活动专用 Token * │ └── seasonal.json → 季节主题 Token * ├── internal/ * │ ├── skeleton.json → 骨架屏动画参数 * │ └── z-index.json → 内部 z-index 管理 * └── semantic/ * ├── light.json → 亮色模式语义映射 * └── dark.json → 暗色模式语义映射 */ /** * .github/CODEOWNERS 自动化规则 * 每个 Token 目录/文件指定审批团队 */ const CODEOWNERS_RULES # Global Tokens → 设计系统核心团队审批 tokens/global/ design-system-core # Semantic Tokens → 设计系统团队 至少一名设计师审批 tokens/semantic/ design-system-core design-team # Marketing Tokens → 营销团队自主管理 tokens/team-marketing/ team-marketing # Internal Tokens → 核心团队独立管理不允许外部 PR tokens/internal/ design-system-core ; /** * Token 变更通知系统 * * 工作流Token 文件变更 → 解析 diff → 查询消费者 → 自动 mention */ interface TokenChange { file: string; // 变更文件路径 tokenName: string; // 变更的 Token 名称 changeType: added | removed | modified; oldValue?: string; newValue?: string; namespace: string; } function analyzeTokenDiff(diff: string): TokenChange[] { // 解析 git diff提取 Token 级变更 // diff 格式 // - --color-primary: #3B82F6, // --color-primary: #2563EB, const changes: TokenChange[] []; const lines diff.split(\n); for (let i 0; i lines.length; i) { const line lines[i]; if (line.startsWith(- ) || line.startsWith( )) { // 提取 Token 名称和值 const match line.match(/(--[\w-]):\s*([^])/); if (match) { changes.push({ file: , tokenName: match[1], changeType: line.startsWith(-) ? removed : line.startsWith() ? added : modified, oldValue: line.startsWith(-) ? match[2] : undefined, newValue: line.startsWith() ? match[2] : undefined, namespace: match[1].startsWith(--team-) ? team- match[1].split(-)[1] : global, }); } } } return changes; } /** * 查找受 Token 变更影响的消费者 */ function findAffectedConsumers(changes: TokenChange[]): string[] { const affectedTeams new Setstring(); for (const change of changes) { const ns NAMESPACES.find((n) change.namespace.startsWith(n.prefix)); if (ns) { ns.consumers.forEach((team) affectedTeams.add(team)); } } return [...affectedTeams]; } /** * 生成 PR 通知评论 */ function generateChangeNotification(changes: TokenChange[], consumers: string[]): string { const lines [ ## Token 变更通知, , 以下 Token 发生了变更可能影响您的产品, , | Token | 变更类型 | 旧值 | 新值 |, |-------|---------|------|------|, ]; for (const c of changes) { lines.push(| \${c.tokenName}\ | ${c.changeType} | ${c.oldValue || -} | ${c.newValue || -} |); } lines.push(, ### 受影响的团队, ...consumers.map((c) - ${c})); lines.push(, 请在 3 个工作日内检查此变更对您产品的影响如有问题在此 PR 中评论。); return lines.join(\n); } export { NAMESPACES, analyzeTokenDiff, findAffectedConsumers, generateChangeNotification }; export type { TokenNamespace, TokenChange };四、Token 治理的两个关键权衡集中 vs 自治。将globalToken 的所有权完全集中到核心团队好处是不会有人乱改主色代价是核心团队成为瓶颈——一个团队需要的活动主色如果必须走核心团队的审批流程响应速度从分钟级降到天级。权衡方案全局 Token 走集中审批CODEOWNERS 锁定团队专用 Token 走自治CODEOWNERS 放权但必须在 PR 描述中链接到使用了哪些全局 Token 作为基础——为后续的变更影响分析留痕迹。语义稳定性 vs 迭代速度。Token 的版本号应遵循语义化版本——删除 Token 或 Token 语义变更--color-primary从蓝变绿 MAJOR新增 Token MINOR修正 Token 值中的精度误差如 HEX 大小写统一 PATCH。但问题是如果每次新增 Token都要走 MINOR 版本发布流程产品团队的迭代速度会被拖慢。折中方案是Batch Release——每周固定一个 Token 发布窗口所有当周累积的非 BREAKING Token 变更打包在同一个 MINOR 版本中发布。五、总结Token 治理的三层模型命名空间隔离可见性 所有权锁定修改权限 变更管理自动通知消费者。global→ 所有团队可见team-*→ 团队内部可见internal→ 仅核心维护者可见。CODEOWNERS 文件是 Token 访问控制的自动化关卡——谁修改了不该改的文件PR 无法合并。Token 变更通知的核心链路解析 git diff → 找到变更 Token → 查询消费者列表 → mention 团队。全局 Token 走集中审批团队 Token 走自治——但必须在 PR 中声明使用了哪些全局 Token 作为基础。Token 版本语义删除 MAJOR、新增 MINOR、值修正 PATCH。Batch Release 策略每周一个发布窗口当周的 Token 变更打包在同一个版本号中。Token 变更的影响面包括CSSvar() 引用、JStoken 导出对象、FigmaDesign Token 同步。命名空间的前缀约定global/team-*/internal应在团队成立第一天就定好——后改的成本极高。Token 治理的目标是让正确的团队在正确的时间修改正确的 Token并让所有消费者在合并前知道。