Design Token 单一真源:从 Figma 变量到代码的工程化同步
Design Token 单一真源:从 Figma 变量到代码的工程化同步
一、设计稿与代码的漂移:Token 治理的工程痛点
在多人协作的前端工程中,"设计稿与代码不一致"是高频出现的协作债务。设计师在 Figma 中定义了一组颜色变量(如color/brand/primary-500),开发者在代码中以硬编码方式(如#3B82F6)使用。当品牌升级需要调整主色时,设计师在 Figma 中改一次,开发者却需要在代码库中全局搜索替换,遗漏与不一致几乎不可避免。
这种漂移的根因是"设计源"与"代码源"分离。设计稿与代码各自维护一份"颜色、间距、字体"的真理,两者之间没有机器可校验的同步链路。Design Token 的提出正是为了消除这一分裂——它定义了一种与平台无关的中间表示,使设计决策可以从 Figma 单向流向前端、iOS、Android 等多端代码产物。
但 Design Token 落地的工程复杂度远超"把颜色写成变量"。它涉及 Token 的分层策略、命名规范、跨平台转译、版本管理与 CI 校验。本文聚焦 Figma 到前端代码的同步链路,讨论生产级 Token 体系的工程实现与权衡。
二、Token 分层与同步链路:从 Figma 变量到多平台产物
要理解 Design Token 的同步链路,需要先看 Token 的分层模型。W3C Design Tokens Format Module 定义了 Token 的标准结构,但实际工程中需要在标准之上做分层治理。
2.1 Token 的三层分层模型
生产级 Token 体系通常分为三层:原始 Token、语义 Token、组件 Token。
原始 Token 是"无意义的原子值",如color-blue-500: #3B82F6。它只描述"是什么",不描述"用于哪里"。语义 Token 描述"用途",如color-background-primary,它的值引用原始 Token。组件 Token 描述"具体组件的某个属性",如button-primary-bg,它的值引用语义 Token。三层之间的引用关系如下图所示。
[Figma Variables] [代码产物] +------------------+ +-------------------+ | 原始 Token | Style | CSS 变量 | | color-blue-500 | Dictionary | --color-blue-500 | | space-4 | --------------> | --space-4 | +------------------+ 转译 +-------------------+ | | v v +------------------+ +-------------------+ | 语义 Token | | CSS 变量(语义) | | color-bg-primary | 引用关系保留 | --color-bg-primary| | = color-blue-500| | = var(--color-blue-500) | +------------------+ +-------------------+ | | v v +------------------+ +-------------------+ | 组件 Token | | 组件级样式 | | button-bg | | .button { | | = color-bg-... | | background: | +------------------+ | var(--color-bg-primary)| | } | +-------------------+2.2 同步链路的关键节点
从 Figma 到代码的同步链路包含五个关键节点,每个节点都有明确的输入输出与校验职责。
| 节点 | 输入 | 输出 | 校验职责 |
|---|---|---|---|
| Figma Variables | 设计师定义 | .tokens.json(W3C 格式) | 命名规范、引用完整性 |
| Token 仓库 | .tokens.json | Style Dictionary 配置 | 分层结构、循环引用 |
| Style Dictionary | Token 加配置 | CSS、SCSS、TS、iOS、Android | 转译正确性 |
| 前端代码库 | 转译产物 | 组件样式 | Token 使用率 lint |
| CI 校验 | PR diff | 通过或阻断 | 禁止硬编码颜色 |
2.3 引用关系与循环检测
语义 Token 引用原始 Token,组件 Token 引用语义 Token,形成有向无环图(DAG)。Style Dictionary 在转译时会展开引用,将button-bg: {color-bg-primary}解析为最终的 CSS 值。但如果 Token 之间存在循环引用(如 A 引用 B,B 又引用 A),转译会陷入死循环。工程上需要在 Token 入库阶段做拓扑排序校验,发现环则拒绝入库。
三、Style Dictionary 流水线:生产级 Token 转译与校验实现
以下实现基于 Style Dictionary v4,它支持 W3C Design Tokens Format Module,并可通过插件扩展多平台输出。
3.1 Token 文件结构与命名规范
// tokens/primitive/color.json // 原始 Token 层,只包含无语义的原子值 // 命名规范:{category}-{item}-{variant} // 严禁在此层引入业务语义,否则会破坏分层治理 { "color": { "blue": { "500": { "value": "#3B82F6", "type": "color" }, "600": { "value": "#2563EB", "type": "color" } }, "gray": { "100": { "value": "#F3F4F6", "type": "color" }, "900": { "value": "#111827", "type": "color" } } }, "space": { "4": { "value": "16px", "type": "dimension" }, "8": { "value": "32px", "type": "dimension" } } }// tokens/semantic/color.json // 语义 Token 层,使用引用而非硬编码 // 引用语法 {path.to.token} 是 W3C 标准的一部分 // 关键约束:语义 Token 只能引用原始 Token,禁止跨语义层引用 { "color": { "background": { "primary": { "value": "{color.gray.100}", "type": "color" }, "inverse": { "value": "{color.gray.900}", "type": "color" } }, "brand": { "primary": { "value": "{color.blue.500}", "type": "color" }, "primary-hover":{ "value": "{color.blue.600}", "type": "color" } } } }3.2 Style Dictionary 配置与多平台转译
// style-dictionary.config.mjs // Style Dictionary v4 配置 // 关键设计: // 1. 按原始、语义、组件三层分别 include,确保引用顺序 // 2. 每个平台(web/css、web/ts)独立配置,避免产物耦合 // 3. 转译时保留引用关系(CSS 变量版),便于运行时主题切换 import StyleDictionary from 'style-dictionary'; import { promises as fs } from 'node:fs'; import path from 'node:path'; // 自定义格式:输出带 CSS 变量引用的产物 // 选择保留引用而非展开最终值,是为了支持运行时主题切换 // 展开值会导致主题切换时需要重新加载所有 CSS StyleDictionary.registerFormat({ name: 'css/variables-with-references', format: async ({ dictionary, file }) => { const lines = [ `/* Generated by Style Dictionary - do not edit */`, `:root {`, ]; for (const token of dictionary.allTokens) { // 原始 Token 输出值,语义 Token 输出 var() 引用 const value = token.original.value.startsWith('{') ? `var(--${token.path.join('-')})` : token.value; lines.push(` --${token.path.join('-')}: ${value};`); } lines.push('}'); return lines.join('\n'); }, }); const sd = new StyleDictionary({ // include 顺序决定引用解析,原始 Token 必须先于语义 Token include: [ 'tokens/primitive/**/*.json', 'tokens/semantic/**/*.json', 'tokens/component/**/*.json', ], platforms: { css: { transformGroup: 'css', buildPath: 'dist/css/', files: [ { destination: 'tokens.css', format: 'css/variables-with-references', }, ], }, ts: { transformGroup: 'ts', buildPath: 'dist/ts/', files: [ { destination: 'tokens.ts', format: 'javascript/es6', // TS 产物用于组件库的类型校验,确保代码中使用合法 Token options: { type: 'module' }, }, ], }, }, }); // 构建前的循环引用检测 // 通过拓扑排序判断 Token 引用图是否存在环 // 环的存在会导致 Style Dictionary 转译时无限递归 async function detectCircularReferences(tokens) { const graph = new Map(); for (const token of tokens) { const refs = extractReferences(token.original.value); graph.set(token.path.join('.'), refs); } // 深度优先遍历检测环 const visited = new Set(); const stack = new Set(); for (const [node] of graph) { if (hasCycle(node, graph, visited, stack)) { throw new Error(`检测到循环引用,起始节点:${node}`); } } } function extractReferences(value) { if (typeof value !== 'string') return []; const matches = value.matchAll(/\{([^}]+)\}/g); return [...matches].map((m) => m[1]); } function hasCycle(node, graph, visited, stack) { if (stack.has(node)) return true; if (visited.has(node)) return false; visited.add(node); stack.add(node); for (const dep of graph.get(node) ?? []) { if (hasCycle(dep, graph, visited, stack)) return true; } stack.delete(node); return false; } try { // 先做循环检测,避免 Style Dictionary 进入死循环导致 CI 卡死 await detectCircularReferences(sd.tokens); await sd.cleanAllPlatforms(); await sd.buildAllPlatforms(); console.log('[tokens] 转译完成'); } catch (err) { console.error(`[tokens] 转译失败:${err.message}`); process.exit(1); }3.3 CI 校验与硬编码阻断
// scripts/lint-tokens-usage.js // 校验代码库中是否出现硬编码颜色或间距 // 阻断策略: // - 颜色十六进制值(如 #3B82F6)直接阻断 // - px 间距值(如 16px)记录警告,允许但不推荐 // - 例外:tailwind 配置、构建脚本本身可豁免 const { execSync } = require('node:child_process'); const IGNORE_PATTERNS = [ 'tailwind.config.js', 'scripts/lint-tokens-usage.js', 'style-dictionary.config.mjs', ]; // 获取本次 PR 修改的样式相关文件 const changedFiles = execSync( 'git diff --name-only --diff-filter=ACM origin/main...HEAD', { encoding: 'utf8' } ).split('\n').filter(Boolean); const violations = []; for (const file of changedFiles) { if (IGNORE_PATTERNS.some((p) => file.includes(p))) continue; if (!/\.(css|scss|vue|tsx|jsx)$/.test(file)) continue; const content = execSync(`git show HEAD:${file}`, { encoding: 'utf8' }); // 匹配十六进制颜色,但不匹配注释中的说明 const hexColorMatches = content.matchAll(/(?<!\/\/.*)#([0-9a-fA-F]{3,8})\b/g); for (const match of hexColorMatches) { violations.push({ file, line: content.slice(0, match.index).split('\n').length, value: match[0], }); } } if (violations.length > 0) { console.error('[lint] 发现硬编码颜色,应使用 Design Token:'); for (const v of violations) { console.error(` - ${v.file}:${v.line} 使用了 ${v.value}`); } process.exit(1); } console.log('[lint] 通过,未发现硬编码颜色');四、Token 体系的代价:治理成本与平台差异边界
Design Token 体系引入的治理成本与平台差异,需要在落地前充分评估。
4.1 治理成本与组织协作
Token 体系的引入会改变设计师与开发者的协作模式。设计师需要在 Figma 中严格使用 Variables 而非自由填色,这要求 Figma 协作规范的培训成本。开发者需要从"随手写颜色"切换到"查 Token 字典",初期开发效率会有所下降。根据生产项目的观测数据,接入 Token 体系后的前两周,组件开发耗时平均增加 15% 至 20%,但在第三周后回落到原有水平,长期看因减少返工而净收益为正。治理手段是引入 IDE 插件(如 VSCode 的 Design Token 自动补全),将 Token 查询的摩擦降到最低。
4.2 平台差异与转译损耗
不同平台的样式系统存在原生差异。CSS 变量是运行时可改的,而 iOS 的 UIColor 在编译期确定;Android 的资源系统对命名有约束(小写下划线)。Style Dictionary 的 transformGroup 会做平台适配,但某些复杂 Token(如带透明度的颜色、响应式间距)在转译到 iOS 时会丢失语义。生产实践中,对复杂 Token 需要为每个平台单独定义 transform,代价是配置文件膨胀,可维护性下降。
4.3 版本管理与兼容性
Token 体系作为独立 npm 包发布后,下游代码库依赖特定版本。Token 重命名或删除会构成破坏性变更,需要 Semver 主版本号升级。治理手段是引入@deprecated标记与别名机制,在 Token 仓库中保留旧名称一段时间,给予下游迁移窗口。代价是 Token 仓库会累积历史别名,需要定期做废弃清理,否则命名空间会逐渐污染。
4.4 适用边界与禁用场景
Token 体系不适用于以下场景。第一,营销活动页面,生命周期短(通常 1 至 2 周),引入 Token 治理的收益低于成本。第二,数据可视化场景(如图表),颜色由数据驱动而非设计系统定义,Token 化反而限制灵活性。第三,原型与 demo 代码,迭代频繁,Token 查询的摩擦会拖慢验证速度。第四,第三方主题完全由用户控制的应用,应在运行时切换 CSS 变量,而非通过 Token 体系构建多套产物。
结论
Design Token 单一真源的工程化落地,核心是建立"原始、语义、组件"三层分层模型,并通过 Style Dictionary 实现 Figma 到多端代码的自动转译。分层模型的价值在于隔离变化——品牌色调整只需改原始 Token,组件级样式自动跟随;语义层调整只需改语义 Token,原始层不受影响。
落地建议分四步推进。第一步,在 Figma 中固化 Variables 命名规范,导出 W3C 格式的 Token 文件作为唯一源。第二步,建立独立的 Token 仓库,配置 Style Dictionary 转译流水线,输出 CSS 变量与 TS 类型。第三步,在前端代码库接入硬编码 lint,阻断未经 Token 的颜色与间距使用。第四步,建立 Token 版本管理与废弃流程,确保破坏性变更有 Semver 信号与迁移窗口。
Token 体系不是一次性工程,而是持续的治理过程。工具链是骨架,命名规范与 lint 约束才是确保设计稿与代码长期一致的真正机制。
