设计系统搭建与 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 转化为多平台输出,版本管理确保变更可控,废弃迁移确保平滑过渡。

落地路线建议:

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

一站式 AI 云服务平台

更多推荐