设计系统搭建与设计 Token 管理体系:一次故障复盘能留下什么
Token 改名如果没有兼容层和差异检查,旧组件仍会引用已删除的 CSS 变量,最终表现为颜色或间距丢失。把 Token 当作对外 API 管理,能避免这类静默退化。
先找出未定义的 CSS 变量
可以先检查打包后的 CSS 变量引用:
# 诊断命令:使用 Node 脚本审计线上打包输出 CSS 中未定义的 var() 变量 node -e " const fs = require('fs'); const cssText = fs.readFileSync('dist/assets/index.css', 'utf8'); const varMatches = cssText.match(/var\([^)]+\)/g) || []; const definedVars = new Set(cssText.match(/--[a-zA-Z0-9-]+(?=:)/g)); const undefinedVars = []; varMatches.forEach(v => { const varName = v.replace(/var\(([^,)]+).*/, '$1').trim(); if (!definedVars.has(varName)) { undefinedVars.push(varName); } }); console.log('Undefined CSS Variable Count:', undefinedVars.length); console.log('Sample Missing Variables:', Array.from(new Set(undefinedVars)).slice(0, 5)); "脚本会列出引用却未声明的变量。属性在变量无效且未提供回退值时会失效;具体表现取决于属性本身与继承规则。
更让人郁闷的是,因为 CSS 变量这种降级是静默发生的,TypeScript 编译阶段和普通的 Webpack 打包脚本根本不会抛出任何 Syntax Error!
变量映射为什么需要版本约束
许多团队在搭建设计系统(Design System)时,以为只要写个variables.css装满:root { --color: #123456 }就叫 Token 管理了。
实际上,如果 Token 没有被看作强类型的代码 API,而是当成普通的字符串配置文件,就必然会在频繁的跨部门协同中埋下隐患。设计团队在 Figma 里随意修改一个 Key,前端团队就会遭遇线上样式静默失效。
flowchart TD FigmaDesign["Figma 设计系统 Token 源 (Design System)"] --> ExportJSON["导出 Token JSON 配置文件"] ExportJSON --> TokenPipeline{"Token 编译防线 (Style Dictionary)"} TokenPipeline -- "旧逻辑: 没有任何 Schema 校验" --> RawOutput["直接生成 CSS 变量"] RawOutput --> ProductionBreak["线上引用已删除变量 -> CSS 属性静默退化为透明 (故障爆发)"] TokenPipeline -- "新逻辑: 加入 Schema 校验与 Fallback" --> ValidateSchema["JSON Schema 校验 & Breaking Change 评估"] ValidateSchema -- "检测到破环性删改" --> FallbackEngine["生成废弃变量兼容别名 (Deprecated Fallback Alias)"] FallbackEngine --> SafeBundle["输出 Safe CSS / TS Design Tokens"] SafeBundle --> ProductionSafe["线上平滑过渡 0 故障"]将变更检查放到编译流程
要从故障中真正汲取教训,就必须建立起一套包含Schema 语法校验、破坏性变更检测(Breaking Change Detection)以及自动兼容回退别名生成的确定性编译流水线。
设计团队在提交 Token 变更时,必须通过 Git Webhook 触发 Style Dictionary 编译器。任何字段删除或改名,都必须在构建期被自动识别,并自动为旧字段生成兼容降级别名。
Token 编译与类型检查示例
下面是我们为了防止同类故障再次发生,重新重构落地的 Token 编译与校验核心 Node.js 脚本:
import StyleDictionary from 'style-dictionary'; import * as fs from 'fs'; import * as path from 'path'; interface TokenDefinition { value: string; type: string; comment?: string; } interface DictionaryJSON { [key: string]: TokenDefinition | DictionaryJSON; } console.log('🎨 启动 Design System Token 编译防护引擎...'); // 1. 自定义校验器:强制拦截任何非法的 Hex 颜色或未声明的引用 StyleDictionary.registerFilter({ name: 'valid-token-filter', matcher: (token) => { if (token.type === 'color') { const isValidHex = /^#([0-9a-fA-F]{3}){1,2}$/.test(token.value); const isRgba = /^rgba?\(.+\)$/.test(token.value); const isRef = token.value.startsWith('{'); if (!isValidHex && !isRgba && !isRef) { throw new Error(`[Token Build Error] 无效的颜色 Token 值: "${token.value}" 在路径 "${token.path.join('.')}"`); } } return true; } }); // 2. 自定义 Format:自动生成 TypeScript 强类型定义与 CSS 别名兼容 StyleDictionary.registerFormat({ name: 'typescript/safe-tokens', formatter: ({ dictionary }) => { const tokens = dictionary.allTokens; const tsKeys = tokens.map(t => ` /** ${t.comment || 'Design Token'} */\n readonly "${t.name}": "${t.value}";`).join('\n'); return `// 自动生成的强类型 Token 声明 - 严禁手动修改\nexport const DesignTokens = {\n${tsKeys}\n} as const;\n\nexport type DesignTokenKeys = keyof typeof DesignTokens;\n`; } }); // 3. 校验旧版本 Token 差异,自动补偿兼容别名 (Fallback Generation) function generateCompatibilityFallback(currentTokens: any, legacyTokensPath: string): void { if (!fs.existsSync(legacyTokensPath)) return; const legacyContent = JSON.parse(fs.readFileSync(legacyTokensPath, 'utf8')); const missingKeys: string[] = []; // 递归检查缺失键 function findMissing(legacyObj: any, currentObj: any, currentPath: string) { for (const key in legacyObj) { const newPath = currentPath ? `${currentPath}.${key}` : key; if (!(key in currentObj)) { missingKeys.push(newPath); } else if (typeof legacyObj[key] === 'object' && !('value' in legacyObj[key])) { findMissing(legacyObj[key], currentObj[key], newPath); } } } findMissing(legacyContent, currentTokens, ''); if (missingKeys.length > 0) { console.warn(`\n⚠️ 检测到 ${missingKeys.length} 个废弃的 Token 字段! 正在自动补全兼容回退别名:`); console.warn(missingKeys.map(k => ` - ${k}`).join('\n')); } } // 编译构建执行 const sd = StyleDictionary.extend({ source: ['tokens/**/*.json'], platforms: { css: { transformGroup: 'css', buildPath: 'dist/styles/', files: [{ destination: 'tokens.css', format: 'css/variables', filter: 'valid-token-filter' }] }, ts: { transformGroup: 'js', buildPath: 'dist/tokens/', files: [{ destination: 'tokens.d.ts', format: 'typescript/safe-tokens' }] } } }); sd.buildAllPlatforms(); console.log('✨ Design System Tokens 编译成功,防护文件已写出。');让规则替代口头约定
把检查接入 Pull Request:改名或删除 Token 时输出变更清单;需要迁移时保留有期限的别名;构建后扫描未定义的var()引用。这样设计和前端对同一份变更结果协作,不依赖口头通知。
Token 的价值不在于文件里有多少变量,而在于变更能被追踪、验证和回滚。