设计系统搭建与 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 自定义属性会自动根据 data-theme 切换
// 因为暗色 Token 编译为 [data-theme="dark"] 选择器下的覆盖
}
// 手动切换主题
toggleTheme(): void {
const next = this.currentTheme === 'light' ? 'dark' : 'light';
this.applyTheme(next);
localStorage.setItem('theme', next);
}
// 是否有手动覆盖
private hasManualOverride(): boolean {
return localStorage.getItem('theme') !== null;
}
}
四、Token 的版本管理与废弃迁移
4.1 语义化版本控制
// 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 转化为多平台输出,版本管理确保变更可控,废弃迁移确保平滑过渡。
落地路线建议:
- 建立 Token 的三层架构,从 Global 原始值到 Alias 语义映射再到 Component 组件级。
- Token 以 JSON 格式存储,通过编译管线输出为 CSS 自定义属性、JS 对象、移动端常量。
- 主题切换基于 CSS 自定义属性 + data 属性,尊重系统偏好并支持手动覆盖。
- Token 版本遵循语义化版本控制,废弃 Token 标记 deprecated 并提供自动迁移脚本。
- Component Token 只为高频定制的组件定义,避免 Token 粒度过细导致维护成本失控。
- 限制主题数量在 3 个以内,超出时考虑动态计算方案。
更多推荐




所有评论(0)