设计系统 Token 工程化从定义到 CI 校验的完整链路一、Token 管理的混乱现状设计 Token 是设计系统的最小原子单元。颜色、间距、字体、圆角等都属于 Token。在团队规模扩大后Token 管理会出现三个典型问题定义分散。设计师在 Figma 定义了一套开发在代码里维护了另一套两边不同步。使用混乱。开发者直接写硬编码颜色值Token 形同虚设。变更无追溯。设计师改了主色开发不知情上线后发现色差。这些问题的根因是 Token 缺少一个单一的源Single Source of Truth和自动化校验机制。一个不成体系的 Token 管理方式在 3 人以下团队勉强可用。超过 5 人时维护成本指数级上升。flowchart TB subgraph 混乱状态 A1[Figma 本地样式] -- A2[手动导出] A2 -- A3[开发手动录入代码] A3 -- A4[各组件独立使用] A4 -- A5[不一致的设计呈现] end subgraph 理想状态 B1[Token 定义文件 - JSON/YAML] -- B2[CI 校验] B1 -- B3[生成 CSS 变量] B1 -- B4[生成 Figma 插件数据] B2 -- B5[阻止不合规代码合入] B3 -- B6[各组件统一引用] B4 -- B6 B6 -- B7[一致性设计呈现] end二、Token 的分层定义策略Token 的定义需要分层。单一层级的 Token 无法同时满足语义化和灵活性需求。采用三层 Token 体系原始 TokenPrimitive Token最底层的原子值不包含语义。例如#1a73e8、16px、0.5。语义 TokenSemantic Token将原始值赋予业务含义。例如color-primary: {primitive.blue.500}。组件 TokenComponent Token针对具体组件的 Token。例如button-primary-bg: {semantic.color-primary}。flowchart LR subgraph 原始层 P1[blue-500: #1a73e8] P2[spacing-4: 16px] P3[radius-md: 8px] end subgraph 语义层 S1[color-primary → blue-500] S2[spacing-component-gap → spacing-4] S3[radius-button → radius-md] end subgraph 组件层 C1[Button: bg → color-primary] C2[Card: padding → spacing-component-gap] C3[Input: radius → radius-button] end P1 -- S1 P2 -- S2 P3 -- S3 S1 -- C1 S2 -- C2 S3 -- C3定义文件推荐使用 JSON 格式便于程序化处理和 CI 解析{ primitive: { color: { blue: { 50: #e8f0fe, 100: #d2e3fc, 500: #1a73e8, 700: #1967d2 }, neutral: { 0: #ffffff, 100: #f8f9fa, 900: #202124 } }, spacing: { 1: 4px, 2: 8px, 4: 16px, 6: 24px, 8: 32px }, radius: { sm: 4px, md: 8px, lg: 12px, full: 9999px } }, semantic: { color: { primary: {primitive.color.blue.500}, primary-hover: {primitive.color.blue.700}, background: {primitive.color.neutral.0}, text-primary: {primitive.color.neutral.900} }, spacing: { component-gap: {primitive.spacing.4}, section-padding: {primitive.spacing.8} } }, component: { button: { bg-primary: {semantic.color.primary}, bg-primary-hover: {semantic.color.primary-hover}, radius: {primitive.radius.md} } } }三层分层的实际收益没有分层时如果需要将主色调从蓝色改为绿色需要修改所有引用了蓝色的组件 Token。有了三层分层只需修改语义 Token 的引用指向// 修改前 semantic: { color: { primary: {primitive.color.blue.500}, primary-hover: {primitive.color.blue.700} } } // 修改后只需改两行 semantic: { color: { primary: {primitive.color.green.500}, primary-hover: {primitive.color.green.700} } }所有组件自动继承新的主色无需逐个修改组件 Token。这对主题切换暗黑模式、品牌色切换场景尤其有价值。例如实现暗黑模式只需准备两套语义 Tokenlight 和 dark组件层无需任何修改。实际项目中的 Token 组织策略在超过 50 个组件的中型项目中Token 定义文件可能超过 500 行。推荐按模块拆分到多个文件tokens/ primitive.json # 原始 Token semantic.json # 语义 Token components/ button.json # 按钮组件 Token input.json # 输入框组件 Token card.json # 卡片组件 Token这种按文件拆分的策略让不同开发者可以并行修改不同组件的 Token避免 Git 冲突。三、CI 校验管道的实现Token 定义文件是整个系统的核心资产。任何对它的修改都必须经过自动化校验。校验规则设计引用完整性。语义 Token 引用的原始 Token 必须存在组件 Token 引用的语义 Token 必须存在。不允许悬空引用。值类型一致。色值引用不能指向间距值。例如button-bg引用的 Token 必须是颜色类型。命名规范。Token 名称必须符合[category]-[property]-[variant]格式kebab-case。禁止未使用的 Token。定义了但从未被任何组件引用的 Token 应标记为告警。校验规则的实际实现细节引用完整性校验是最关键的规则。实现时需要注意循环引用的问题语义 Token A 引用了语义 Token B而语义 Token B 又引用了语义 Token A。这种情况会导致解析时无限递归。需要在解析方法中加入 visited 集合来检测循环引用function resolveTokenValue( value: string, tokens: TokenDefinition, visited: Setstring new Set() ): string { const ref parseReference(value); if (!ref) return value; // 检测循环引用 if (visited.has(value)) { throw new Error(检测到循环引用: ${value}); } visited.add(value); if (ref.type primitive) { return resolvePrimitive(tokens.primitive, ref.path) ?? value; } if (ref.type semantic) { const semanticValue resolveSemantic(tokens.semantic, ref.path); if (!semanticValue) return value; return resolveTokenValue(semanticValue, tokens, visited); } return value; }值类型一致性校验需要为原始 Token 增加类型标注。修改 Token 定义文件的结构{ primitive: { color: { blue: { 500: { value: #1a73e8, type: color } } }, spacing: { 4: { value: 16px, type: spacing } } } }在校验时检查组件 Token 引用的语义 Token 的类型是否匹配。例如按钮的bg属性必须引用类型为color的 Token。未使用 Token 检测需要配合构建流程。在 CI 中先生成 CSS 变量文件然后使用 Stylelint 或 PostCSS 插件扫描所有组件的样式文件提取实际使用的 CSS 变量列表与生成的变量列表对比。未被引用的 Token 产生 warning 级别告警。function findUnusedTokens( tokens: TokenDefinition, usedVariables: Setstring ): ValidationError[] { const errors: ValidationError[] []; const allGeneratedVars generateCSSVariables(tokens); for (const cssVar of allGeneratedVars) { if (!usedVariables.has(cssVar)) { errors.push({ path: cssVar, message: Token ${cssVar} 未被任何组件使用, severity: warning, }); } } return errors; }三、CI 校验管道的实现Token 定义文件是整个系统的核心资产。任何对它的修改都必须经过自动化校验。校验规则设计引用完整性。语义 Token 引用的原始 Token 必须存在组件 Token 引用的语义 Token 必须存在。不允许悬空引用。值类型一致。色值引用不能指向间距值。例如button-bg引用的 Token 必须是颜色类型。命名规范。Token 名称必须符合[category]-[property]-[variant]格式kebab-case。禁止未使用的 Token。定义了但从未被任何组件引用的 Token 应标记为告警。import { readFileSync } from fs; interface TokenDefinition { primitive: Recordstring, Recordstring, Recordstring, string; semantic: Recordstring, Recordstring, string; component: Recordstring, Recordstring, string; } interface ValidationError { path: string; message: string; severity: error | warning; } function validateTokenReferences(tokens: TokenDefinition): ValidationError[] { const errors: ValidationError[] []; // 校验语义 Token 引用 for (const [category, values] of Object.entries(tokens.semantic)) { for (const [key, value] of Object.entries(values)) { const ref parseReference(value); if (!ref) continue; const resolved resolvePrimitive(tokens.primitive, ref.path); if (!resolved) { errors.push({ path: semantic.${category}.${key}, message: 引用的原始 Token ${ref.raw} 不存在, severity: error, }); } } } // 校验组件 Token 引用 for (const [component, values] of Object.entries(tokens.component)) { for (const [key, value] of Object.entries(values)) { const ref parseReference(value); if (!ref) { // 也允许直接使用原始值 continue; } if (ref.type semantic) { const resolved resolveSemantic(tokens.semantic, ref.path); if (!resolved) { errors.push({ path: component.${component}.${key}, message: 引用的语义 Token ${ref.raw} 不存在, severity: error, }); } } } } return errors; } function parseReference(value: string): { type: string; path: string; raw: string } | null { const match value.match(/^\{(\w)\.(.)\}$/); if (!match) return null; return { type: match[1], path: match[2], raw: value }; } function resolvePrimitive( primitives: TokenDefinition[primitive], path: string ): string | null { const parts path.split(.); let current: unknown primitives; for (const part of parts) { if (typeof current ! object || current null) return null; current (current as Recordstring, unknown)[part]; } return typeof current string ? current : null; } function resolveSemantic( semantics: TokenDefinition[semantic], path: string ): string | null { const parts path.split(.); let current: unknown semantics; for (const part of parts) { if (typeof current ! object || current null) return null; current (current as Recordstring, unknown)[part]; } return typeof current string ? current : null; } // 主入口 function main(): void { try { const raw readFileSync(tokens.json, utf-8); const tokens: TokenDefinition JSON.parse(raw); const errors validateTokenReferences(tokens); if (errors.filter(e e.severity error).length 0) { console.error(Token 校验失败:); errors.forEach(e console.error( [${e.severity}] ${e.path}: ${e.message})); process.exit(1); } const warnings errors.filter(e e.severity warning); if (warnings.length 0) { console.warn(Token 校验告警:); warnings.forEach(w console.warn( [${w.severity}] ${w.path}: ${w.message})); } console.log(Token 校验通过); } catch (error) { console.error(Token 文件解析失败:, error instanceof Error ? error.message : error); process.exit(1); } } main();在 CI 管线中集成# .github/workflows/token-check.yml name: Token Validation on: pull_request: paths: - tokens.json jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 - name: Validate Tokens run: npx tsx scripts/validate-tokens.ts四、生成产物与开发体验Token 定义文件校验通过后下一步是生成各平台可用的产物。CSS 变量生成function generateCSSVariables(tokens: TokenDefinition): string { const lines: string[] [:root {]; for (const [category, values] of Object.entries(tokens.semantic)) { for (const [key, value] of Object.entries(values)) { const cssVar --${category}-${key}; const resolved resolveTokenValue(value, tokens); lines.push( ${cssVar}: ${resolved};); } } lines.push(}); return lines.join(\n); } function resolveTokenValue(value: string, tokens: TokenDefinition): string { const ref parseReference(value); if (!ref) return value; if (ref.type primitive) { return resolvePrimitive(tokens.primitive, ref.path) ?? value; } if (ref.type semantic) { const semanticValue resolveSemantic(tokens.semantic, ref.path); if (!semanticValue) return value; return resolveTokenValue(semanticValue, tokens); } return value; }生成结果示例:root { --color-primary: #1a73e8; --color-primary-hover: #1967d2; --color-background: #ffffff; --color-text-primary: #202124; --spacing-component-gap: 16px; --spacing-section-padding: 32px; }生成的 CSS 变量文件作为构建产物输出前端代码中统一通过 CSS 变量引用而非直接使用硬编码值。这样在 Token 变更时只需重新生成 CSS 变量文件即可全局生效。为确保开发者不使用硬编码值可以配合 Stylelint 规则// stylelint.config.js module.exports { rules: { color-no-hex: [true, { severity: warning, }], declaration-property-value-disallowed-list: { /^color/: [/^#[0-9a-fA-F]{3,8}$/], /^background/: [/^#[0-9a-fA-F]{3,8}$/], }, }, };五、总结Token 工程化的核心是建立单一数据源和自动化校验链路。三层 Token 体系原始层、语义层、组件层兼顾了灵活性和语义化。CI 校验确保引用完整性和类型一致性防止悬空引用和类型错配。配合 CSS 变量生成和 Stylelint 规则可以强制开发者使用 Token 而非硬编码值。这套方案的投入约 2-3 天对 5 人以上团队的设计一致性有明显收益。