当前位置: 首页 > news >正文

设计系统搭建与 Token 管理体系:从原子变量到跨端一致性的工程实践

设计系统搭建与 Token 管理体系:从原子变量到跨端一致性的工程实践

一、设计系统的"散装"困境:为什么有了组件库还不够

很多团队认为"有了组件库就等于有了设计系统"。实际情况是:组件库只是设计系统的冰山一角。水面之下,还有 Design Token 管理、主题切换机制、跨平台同步、版本发布策略、废弃迁移流程——这些才是决定设计系统是否"可用"的关键。

典型的"散装"症状:设计师在 Figma 中定义了primary-500#3B82F6,开发者在代码中写了#3B82F6,运营在落地页中硬编码了#3B82F6。当品牌升级需要将主色改为#2563EB时,需要逐个文件搜索替换——遗漏一处,就是视觉不一致。

Design Token 是解决这个问题的核心机制:将所有设计决策抽象为命名变量,通过单一数据源驱动所有平台的样式输出。

二、Design Token 的分层架构:从原始值到组件级 Token

2.1 三层 Token 模型

flowchart TD A[Global Token<br/>全局原始值] --> B[Alias Token<br/>语义别名] B --> C[Component Token<br/>组件级映射] 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?: Record<string, 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"] end

3.2 Token 编译器实现

// Token 编译器:将 JSON Token 编译为多平台输出 class TokenCompiler { private tokens: Map<string, DesignToken> = new Map(); // 加载 Token 文件 async loadTokenFiles(globPattern: string): Promise<void> { 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 Map<string, TokenValue>(); const resolving = new Set<string>(); // 检测循环引用 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: Record<string, 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 ): Promise<MigrationReport> { 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 转化为多平台输出,版本管理确保变更可控,废弃迁移确保平滑过渡。

落地路线建议:

  1. 建立 Token 的三层架构,从 Global 原始值到 Alias 语义映射再到 Component 组件级。
  2. Token 以 JSON 格式存储,通过编译管线输出为 CSS 自定义属性、JS 对象、移动端常量。
  3. 主题切换基于 CSS 自定义属性 + data 属性,尊重系统偏好并支持手动覆盖。
  4. Token 版本遵循语义化版本控制,废弃 Token 标记 deprecated 并提供自动迁移脚本。
  5. Component Token 只为高频定制的组件定义,避免 Token 粒度过细导致维护成本失控。
  6. 限制主题数量在 3 个以内,超出时考虑动态计算方案。
http://www.jsqmd.com/news/1091858/

相关文章:

  • 【FPGA】Questasim仿真环境搭建与波形调试实战指南
  • Gemmini:开源全栈DNN加速器如何重塑系统级协同设计
  • CANoe CAPL实战:Message对象从声明到总线交互的完整指南
  • AI设计进阶:从路径查找器到扩展外观,解锁矢量图形高效编辑
  • 如何3分钟搞定macOS微信防撤回:终极完整安装指南
  • 软件测试还有前景吗?2026年行业发展趋势解析,零基础还有机会进入吗?
  • Rusted PackFile Manager:全面战争MOD开发的架构深度解析与技术实现
  • List、Set、Map
  • 架构选型与规划
  • JMeter WebSocket插件实战:从功能到性能的完整测试方案
  • Win11Debloat:3分钟完成Windows系统优化,彻底清理臃肿应用
  • 如何进入状态
  • 3分钟上手FunClip:如何用AI智能剪辑让视频处理效率提升10倍?
  • 五脏养生别瞎补!老中医总结的先后顺序,照着养少走弯路
  • 【goal命令技术解析】Claude Code与Codex目标驱动自主执行机制全景解析
  • 如何永久激活IDM?开源脚本的终极解决方案
  • Electron 如何调用 Windows 原生 API
  • Go 高性能网络服务:从 TCP 参数调优到连接池工程实践
  • 深入解析TSB41BA3D PHY-LLC状态传输机制:实时事件通知与串行总线协同设计
  • QEMU安全配置:虚拟机隔离、权限控制与安全最佳实践
  • 豆包LaTeX公式转Word全攻略:AI导出鸭助你一键搞定
  • 从IO 500双登顶出发,中国存储领跑AI新周期
  • 【共创季稿事节】鸿蒙 ArkTS 安全区布局完全指南:SafeArea、expandSafeArea 与 Web 适配实战
  • 02 如何解决粘包问题
  • Metasploit实战入门:从Auxiliary侦察到Meterpreter后渗透完整指南
  • 【机器学习300问】早停法(Early Stopping):从损失曲线到实战调参的防过拟合指南
  • 联想小新休眠黑屏无法唤醒?聊聊低温锡 CPU 虚焊故障现象
  • 2026年银行全员营销新变局:当任务完成率统计成为“硬指标”,哪套系统真正能落地?
  • TI TPIC7710评估板实战指南:从硬件解析到软件调试的汽车电机控制验证
  • 2026年排盘精准度与底层逻辑:哪家八字排盘app排盘最标准、操作简单、功能齐全且能保存命盘