设计系统搭建与 Token 管理体系:从原子变量到跨端一致性的工程实践
设计系统搭建与 Token 管理体系从原子变量到跨端一致性的工程实践一、设计系统的散装困境为什么有了组件库还不够很多团队认为有了组件库就等于有了设计系统。实际情况是组件库只是设计系统的冰山一角。水面之下还有 Design Token 管理、主题切换机制、跨平台同步、版本发布策略、废弃迁移流程——这些才是决定设计系统是否可用的关键。典型的散装症状设计师在 Figma 中定义了primary-500为#3B82F6开发者在代码中写了#3B82F6运营在落地页中硬编码了#3B82F6。当品牌升级需要将主色改为#2563EB时需要逐个文件搜索替换——遗漏一处就是视觉不一致。Design Token 是解决这个问题的核心机制将所有设计决策抽象为命名变量通过单一数据源驱动所有平台的样式输出。二、Design Token 的分层架构从原始值到组件级 Token2.1 三层 Token 模型flowchart TD A[Global Tokenbr/全局原始值] -- B[Alias Tokenbr/语义别名] B -- C[Component Tokenbr/组件级映射] subgraph 第一层Global Token A1[blue-500: #3B82F6] A2[spacing-4: 16px] A3[radius-md: 8px] A4[font-size-sm: 14px] end subgraph 第二层Alias Token B1[color-primary: {blue-500}] B2[spacing-container: {spacing-4}] B3[radius-button: {radius-md}] B4[font-size-body: {font-size-sm}] end subgraph 第三层Component Token C1[button-bg: {color-primary}] C2[button-padding: {spacing-container}] C3[button-radius: {radius-button}] C4[button-font-size: {font-size-body}] end style A fill:#e8f5e9 style B fill:#e3f2fd style C fill:#fff3e0三层模型的核心价值当品牌升级时只需修改第二层的 Alias Token 映射所有组件自动跟随变化。第一层的原始值不变第三层的组件引用不变。2.2 Token 的完整类型定义// Design Token 的类型系统 type TokenValue string | number; interface DesignToken { // Token 唯一标识 name: string; // Token 值可以是原始值或引用其他 Token value: TokenValue | {${string}}; // Token 类型 type: color | dimension | fontFamily | fontWeight | duration | cubicBezier | number | shadow; // 描述 description: string; // 所属层级 tier: global | alias | component; // 主题变体暗色模式等 themes?: Recordstring, TokenValue; // 是否已废弃 deprecated?: boolean; // 替代 Token replacedBy?: string; // 标签用于分组和检索 tags?: string[]; } // 完整的 Token 集合 interface TokenCollection { // 集合元信息 meta: { name: string; version: string; lastModified: string; }; // Token 列表 tokens: DesignToken[]; }2.3 Token 文件组织结构tokens/ ├── global/ │ ├── colors.json # 全局颜色原始值 │ ├── spacing.json # 全局间距原始值 │ ├── typography.json # 全局排版原始值 │ ├── radius.json # 全局圆角原始值 │ ├── shadows.json # 全局阴影原始值 │ └── motion.json # 全局动效原始值 ├── alias/ │ ├── colors.json # 语义颜色别名 │ ├── spacing.json # 语义间距别名 │ └── typography.json # 语义排版别名 ├── component/ │ ├── button.json # 按钮组件 Token │ ├── input.json # 输入框组件 Token │ ├── card.json # 卡片组件 Token │ └── modal.json # 模态框组件 Token └── themes/ ├── light.json # 亮色主题覆盖 └── dark.json # 暗色主题覆盖三、Token 编译管线从 JSON 到多平台输出3.1 编译管线架构flowchart LR A[Token JSON 源文件] -- B[解析与引用展开] B -- C[主题合并] C -- D[平台编译] D -- E1[CSS 自定义属性] D -- E2[SCSS 变量] D -- E3[JavaScript 对象] D -- E4[Swift/Kotlin 常量] D -- E5[Figma 变量] subgraph 引用展开 B -- B1[{blue-500} → #3B82F6] B -- B2[{color-primary} → {blue-500} → #3B82F6] end subgraph 主题合并 C -- C1[亮色: color-bg → #FFFFFF] C -- C2[暗色: color-bg → #1A1A1A] end3.2 Token 编译器实现// Token 编译器将 JSON Token 编译为多平台输出 class TokenCompiler { private tokens: Mapstring, DesignToken new Map(); // 加载 Token 文件 async loadTokenFiles(globPattern: string): Promisevoid { const files await glob(globPattern); for (const file of files) { const content await fs.readFile(file, utf-8); const collection: TokenCollection JSON.parse(content); for (const token of collection.tokens) { this.tokens.set(token.name, token); } } } // 解析引用将 {token-name} 替换为实际值 resolveReferences(): void { const resolved new Mapstring, TokenValue(); const resolving new Setstring(); // 检测循环引用 const resolve (name: string): TokenValue { // 已解析的值直接返回 if (resolved.has(name)) { return resolved.get(name)!; } // 检测循环引用 if (resolving.has(name)) { throw new Error(检测到循环引用: ${name}); } const token this.tokens.get(name); if (!token) { throw new Error(Token 不存在: ${name}); } resolving.add(name); // 如果值是引用递归解析 if (typeof token.value string token.value.startsWith({)) { const refName token.value.slice(1, -1); const resolvedValue resolve(refName); resolved.set(name, resolvedValue); resolving.delete(name); return resolvedValue; } resolved.set(name, token.value); resolving.delete(name); return token.value; }; // 解析所有 Token for (const name of this.tokens.keys()) { resolve(name); } // 将解析后的值写回 Token for (const [name, value] of resolved) { const token this.tokens.get(name)!; token.value value; } } // 编译为 CSS 自定义属性 compileToCSS(theme?: string): string { const lines: string[] [ /* Design Token - 自动生成请勿手动修改 */, :root {, ]; for (const [name, token] of this.tokens) { if (token.deprecated) continue; const value theme token.themes?.[theme] ? token.themes[theme] : token.value; // 将 Token 名转换为 CSS 自定义属性名 const cssName --${name.replace(/\./g, -)}; lines.push( ${cssName}: ${value};); } lines.push(}); return lines.join(\n); } // 编译为暗色主题 compileDarkThemeCSS(): string { const lines: string[] [ media (prefers-color-scheme: dark) {, :root {, ]; for (const [name, token] of this.tokens) { if (token.deprecated) continue; if (!token.themes?.dark) continue; const cssName --${name.replace(/\./g, -)}; lines.push( ${cssName}: ${token.themes.dark};); } lines.push( }); lines.push(}); return lines.join(\n); } // 编译为 JavaScript 对象 compileToJS(): string { const obj: Recordstring, TokenValue {}; for (const [name, token] of this.tokens) { if (token.deprecated) continue; obj[name.replace(/\./g, _)] token.value; } return export const tokens ${JSON.stringify(obj, null, 2)} as const;; } }3.3 主题切换机制// 主题切换基于 data 属性 CSS 自定义属性 class ThemeManager { private currentTheme: light | dark light; private mediaQuery: MediaQueryList; constructor() { this.mediaQuery window.matchMedia((prefers-color-scheme: dark)); // 监听系统主题变化 this.mediaQuery.addEventListener(change, (e) { if (!this.hasManualOverride()) { this.applyTheme(e.matches ? dark : light); } }); // 读取用户偏好 const saved localStorage.getItem(theme) as light | dark | null; if (saved) { this.applyTheme(saved); } else { this.applyTheme(this.mediaQuery.matches ? dark : light); } } // 应用主题 applyTheme(theme: light | dark): void { this.currentTheme theme; document.documentElement.setAttribute(data-theme, theme); // CSS 自定义属性会自动根据>// Token 版本变更规范 interface TokenVersionChange { // MAJOR删除 Token 或改变 Token 的语义 major: string[]; // MINOR新增 Token 或新增主题变体 minor: string[]; // PATCH修改 Token 值但不改变语义 patch: string[]; } // 示例v2.0.0 的变更日志 const v2Changelog: TokenVersionChange { major: [ 删除 color-brand-legacy已迁移至 color-primary, spacing-base 语义变更从 4px 改为 8px 基准, ], minor: [ 新增 color-surface-elevated Token, 新增 dark 主题下 color-surface 的值, 新增 motion-spring-* 弹簧动效 Token 系列, ], patch: [ color-primary 从 #3B82F6 调整为 #2563EB, radius-lg 从 12px 调整为 16px, ], };4.2 废弃 Token 的自动迁移// Token 迁移脚本自动替换废弃 Token async function migrateDeprecatedTokens( projectRoot: string, tokenRegistry: TokenCollection ): PromiseMigrationReport { const deprecatedTokens tokenRegistry.tokens.filter( (t) t.deprecated t.replacedBy ); const report: MigrationReport { filesScanned: 0, replacements: [], errors: [], }; // 扫描项目中的所有样式文件 const styleFiles await glob(**/*.{css,scss,less,tsx,jsx,ts,js}, { cwd: projectRoot, ignore: [**/node_modules/**, **/dist/**], }); report.filesScanned styleFiles.length; for (const file of styleFiles) { const filePath path.join(projectRoot, file); let content await fs.readFile(filePath, utf-8); let modified false; for (const token of deprecatedTokens) { const oldName --${token.name.replace(/\./g, -)}; const newName --${token.replacedBy!.replace(/\./g, -)}; if (content.includes(oldName)) { content content.replaceAll(oldName, newName); modified true; report.replacements.push({ file, oldToken: oldName, newToken: newName, }); } } if (modified) { await fs.writeFile(filePath, content); } } return report; }五、设计系统的边界与 Token 管理的权衡5.1 Token 粒度的两难Token 粒度过细每个组件属性都是 Token维护成本极高一个按钮就有 20 Token。粒度过粗只有全局颜色和间距组件级别的定制能力不足。建议的平衡点Global Token 覆盖所有原始值Alias Token 覆盖语义映射Component Token 只为高频定制的组件定义。5.2 跨平台同步的延迟Token 从 JSON 编译到 CSS、Swift、Kotlin、Figma 变量各平台的发布节奏不同。Web 端可以实时更新移动端需要发版Figma 需要手动同步插件。这种延迟会导致短期内各平台样式不一致。5.3 主题数量的膨胀每新增一个主题所有 Alias Token 和 Component Token 都需要定义主题变体。5 个主题意味着 5 倍的维护量。建议限制主题数量在 3 个以内亮色、暗色、高对比度超出时考虑动态计算而非手动定义。5.4 编译管线的构建时间大型设计系统的 Token 编译可能需要 10-30 秒。在开发阶段每次修改 Token 都等待编译会降低效率。建议开发模式使用 Token 源文件直接引用生产模式使用编译后的输出。五、总结设计系统的核心不是组件库而是 Token 管理体系。三层 Token 模型Global → Alias → Component将设计决策分层抽象编译管线将 Token 转化为多平台输出版本管理确保变更可控废弃迁移确保平滑过渡。落地路线建议建立 Token 的三层架构从 Global 原始值到 Alias 语义映射再到 Component 组件级。Token 以 JSON 格式存储通过编译管线输出为 CSS 自定义属性、JS 对象、移动端常量。主题切换基于 CSS 自定义属性 data 属性尊重系统偏好并支持手动覆盖。Token 版本遵循语义化版本控制废弃 Token 标记 deprecated 并提供自动迁移脚本。Component Token 只为高频定制的组件定义避免 Token 粒度过细导致维护成本失控。限制主题数量在 3 个以内超出时考虑动态计算方案。