Skip to content
📖预计阅读时长:0 分钟字数:0

话题 5:工程化落地方案

本文是「设计系统与 Tailwind CSS 深度实践」系列的第五篇,聚焦 Token 的存储、构建、分发以及 Monorepo 架构设计。


一、Token 的存储格式选择

1.1 格式对比

格式优点缺点适用场景
JSON通用、工具支持好、与 Figma 插件兼容无法写注释、无法复用标准选择
JSON5支持注释、尾逗号需要额外解析器需要注释时
YAML可读性好、支持注释缩进敏感、易出错配置文件偏好者
JavaScript/TypeScript类型安全、可计算、可复用与设计工具同步困难纯开发场景

1.2 我的推荐

阶段一(现在):直接用 JavaScript/TypeScript

  • 简单直接,无需构建工具
  • 适合个人项目快速启动

阶段二(成熟后):JSON + Style Dictionary

  • 与 Figma Tokens Studio 同步
  • 生成多平台产物
个人项目演进路径:

Phase 1: JS/TS 直接定义
├── tokens.ts → tailwind.config.js 直接引用
└── 简单、快速、够用

Phase 2: JSON + 构建
├── tokens.json → Style Dictionary → tokens.ts
├── 支持 Figma 同步
└── 支持多平台输出

二、阶段一方案:直接 JS/TS 定义

2.1 目录结构

src/
├── tokens/
│   ├── index.ts           # 统一导出
│   ├── colors.ts          # 颜色 Token
│   ├── spacing.ts         # 间距 Token
│   ├── typography.ts      # 字体 Token
│   └── semantic.ts        # 语义 Token(含主题)
├── styles/
│   └── globals.css        # CSS 变量定义
├── components/
└── tailwind.config.ts

2.2 Token 定义文件

typescript
// src/tokens/colors.ts
/**
 * Primitive Colors - 原始调色板
 * 这些值不应直接在组件中使用,而是通过 semantic tokens 引用
 */
export const primitiveColors = {
  // Brand Blue
  blue: {
    50: '#EFF6FF',
    100: '#DBEAFE',
    200: '#BFDBFE',
    300: '#93C5FD',
    400: '#60A5FA',
    500: '#3B82F6',
    600: '#2563EB',
    700: '#1D4ED8',
    800: '#1E40AF',
    900: '#1E3A8A',
    950: '#172554',
  },
  // Neutral Gray
  gray: {
    50: '#F9FAFB',
    100: '#F3F4F6',
    200: '#E5E7EB',
    300: '#D1D5DB',
    400: '#9CA3AF',
    500: '#6B7280',
    600: '#4B5563',
    700: '#374151',
    800: '#1F2937',
    900: '#111827',
    950: '#030712',
  },
  // Status Colors
  red: {
    50: '#FEF2F2',
    500: '#EF4444',
    600: '#DC2626',
    700: '#B91C1C',
  },
  green: {
    50: '#F0FDF4',
    500: '#22C55E',
    600: '#16A34A',
    700: '#15803D',
  },
  yellow: {
    50: '#FEFCE8',
    500: '#EAB308',
    600: '#CA8A04',
  },
  // Base
  white: '#FFFFFF',
  black: '#000000',
  transparent: 'transparent',
  current: 'currentColor',
} as const;

// 导出类型
export type PrimitiveColor = typeof primitiveColors;
typescript
// src/tokens/spacing.ts
/**
 * Spacing Scale - 间距刻度
 * 基于 4px 基准单位
 */
export const spacing = {
  0: '0',
  px: '1px',
  0.5: '2px',
  1: '4px',
  1.5: '6px',
  2: '8px',
  2.5: '10px',
  3: '12px',
  3.5: '14px',
  4: '16px',
  5: '20px',
  6: '24px',
  7: '28px',
  8: '32px',
  9: '36px',
  10: '40px',
  11: '44px',
  12: '48px',
  14: '56px',
  16: '64px',
  20: '80px',
  24: '96px',
  28: '112px',
  32: '128px',
  36: '144px',
  40: '160px',
  44: '176px',
  48: '192px',
  52: '208px',
  56: '224px',
  60: '240px',
  64: '256px',
  72: '288px',
  80: '320px',
  96: '384px',
} as const;

export type Spacing = typeof spacing;
typescript
// src/tokens/typography.ts
export const fontSize = {
  xs: ['0.75rem', { lineHeight: '1rem' }],
  sm: ['0.875rem', { lineHeight: '1.25rem' }],
  base: ['1rem', { lineHeight: '1.5rem' }],
  lg: ['1.125rem', { lineHeight: '1.75rem' }],
  xl: ['1.25rem', { lineHeight: '1.75rem' }],
  '2xl': ['1.5rem', { lineHeight: '2rem' }],
  '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
  '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
  '5xl': ['3rem', { lineHeight: '1' }],
  '6xl': ['3.75rem', { lineHeight: '1' }],
} as const;

export const fontWeight = {
  thin: '100',
  light: '300',
  normal: '400',
  medium: '500',
  semibold: '600',
  bold: '700',
  extrabold: '800',
  black: '900',
} as const;

export const fontFamily = {
  sans: [
    'Inter',
    'ui-sans-serif',
    'system-ui',
    '-apple-system',
    'sans-serif',
  ],
  mono: [
    'JetBrains Mono',
    'ui-monospace',
    'SFMono-Regular',
    'monospace',
  ],
} as const;

export const borderRadius = {
  none: '0',
  sm: '0.25rem',    // 4px
  DEFAULT: '0.375rem', // 6px
  md: '0.5rem',     // 8px
  lg: '0.75rem',    // 12px
  xl: '1rem',       // 16px
  '2xl': '1.25rem', // 20px
  '3xl': '1.5rem',  // 24px
  full: '9999px',
} as const;
typescript
// src/tokens/semantic.ts
/**
 * Semantic Tokens - 语义化 Token
 * 通过 CSS 变量实现,支持主题切换
 */

// 定义 CSS 变量名(用于 Tailwind 配置)
export const semanticColors = {
  background: {
    DEFAULT: 'rgb(var(--color-bg) / <alpha-value>)',
    secondary: 'rgb(var(--color-bg-secondary) / <alpha-value>)',
    tertiary: 'rgb(var(--color-bg-tertiary) / <alpha-value>)',
  },
  foreground: {
    DEFAULT: 'rgb(var(--color-text) / <alpha-value>)',
    muted: 'rgb(var(--color-text-muted) / <alpha-value>)',
    subtle: 'rgb(var(--color-text-subtle) / <alpha-value>)',
  },
  primary: {
    DEFAULT: 'rgb(var(--color-primary) / <alpha-value>)',
    foreground: 'rgb(var(--color-primary-foreground) / <alpha-value>)',
  },
  secondary: {
    DEFAULT: 'rgb(var(--color-secondary) / <alpha-value>)',
    foreground: 'rgb(var(--color-secondary-foreground) / <alpha-value>)',
  },
  muted: {
    DEFAULT: 'rgb(var(--color-muted) / <alpha-value>)',
    foreground: 'rgb(var(--color-muted-foreground) / <alpha-value>)',
  },
  accent: {
    DEFAULT: 'rgb(var(--color-accent) / <alpha-value>)',
    foreground: 'rgb(var(--color-accent-foreground) / <alpha-value>)',
  },
  destructive: {
    DEFAULT: 'rgb(var(--color-destructive) / <alpha-value>)',
    foreground: 'rgb(var(--color-destructive-foreground) / <alpha-value>)',
  },
  success: {
    DEFAULT: 'rgb(var(--color-success) / <alpha-value>)',
    foreground: 'rgb(var(--color-success-foreground) / <alpha-value>)',
  },
  warning: {
    DEFAULT: 'rgb(var(--color-warning) / <alpha-value>)',
    foreground: 'rgb(var(--color-warning-foreground) / <alpha-value>)',
  },
  border: {
    DEFAULT: 'rgb(var(--color-border) / <alpha-value>)',
    muted: 'rgb(var(--color-border-muted) / <alpha-value>)',
  },
  ring: 'rgb(var(--color-ring) / <alpha-value>)',
} as const;
typescript
// src/tokens/index.ts
export { primitiveColors } from './colors';
export { spacing } from './spacing';
export { fontSize, fontWeight, fontFamily, borderRadius } from './typography';
export { semanticColors } from './semantic';

2.3 Tailwind 配置

typescript
// tailwind.config.ts
import type { Config } from 'tailwindcss';
import {
  primitiveColors,
  spacing,
  fontSize,
  fontWeight,
  fontFamily,
  borderRadius,
  semanticColors,
} from './src/tokens';

const config: Config = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  darkMode: 'class',
  theme: {
    // 完全覆盖默认值
    colors: {
      ...primitiveColors,
      ...semanticColors,
    },
    spacing,
    fontSize,
    fontWeight,
    fontFamily,
    borderRadius,
    
    extend: {
      // 扩展默认值
      boxShadow: {
        sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
        DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
        md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
        lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
        xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
      },
      transitionDuration: {
        DEFAULT: '150ms',
        fast: '100ms',
        slow: '300ms',
      },
    },
  },
  plugins: [],
};

export default config;

2.4 CSS 变量文件

css
/* src/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    /* ========================================
       Light Theme
       ======================================== */
    --color-bg: 255 255 255;
    --color-bg-secondary: 249 250 251;
    --color-bg-tertiary: 243 244 246;
    
    --color-text: 17 24 39;
    --color-text-muted: 107 114 128;
    --color-text-subtle: 156 163 175;
    
    --color-primary: 59 130 246;
    --color-primary-foreground: 255 255 255;
    
    --color-secondary: 243 244 246;
    --color-secondary-foreground: 17 24 39;
    
    --color-muted: 243 244 246;
    --color-muted-foreground: 107 114 128;
    
    --color-accent: 243 244 246;
    --color-accent-foreground: 17 24 39;
    
    --color-destructive: 239 68 68;
    --color-destructive-foreground: 255 255 255;
    
    --color-success: 34 197 94;
    --color-success-foreground: 255 255 255;
    
    --color-warning: 234 179 8;
    --color-warning-foreground: 255 255 255;
    
    --color-border: 229 231 235;
    --color-border-muted: 243 244 246;
    
    --color-ring: 59 130 246;
  }

  .dark {
    /* ========================================
       Dark Theme
       ======================================== */
    --color-bg: 3 7 18;
    --color-bg-secondary: 17 24 39;
    --color-bg-tertiary: 31 41 55;
    
    --color-text: 249 250 251;
    --color-text-muted: 156 163 175;
    --color-text-subtle: 107 114 128;
    
    --color-primary: 96 165 250;
    --color-primary-foreground: 3 7 18;
    
    --color-secondary: 31 41 55;
    --color-secondary-foreground: 249 250 251;
    
    --color-muted: 31 41 55;
    --color-muted-foreground: 156 163 175;
    
    --color-accent: 31 41 55;
    --color-accent-foreground: 249 250 251;
    
    --color-destructive: 248 113 113;
    --color-destructive-foreground: 3 7 18;
    
    --color-success: 74 222 128;
    --color-success-foreground: 3 7 18;
    
    --color-warning: 250 204 21;
    --color-warning-foreground: 3 7 18;
    
    --color-border: 55 65 81;
    --color-border-muted: 31 41 55;
    
    --color-ring: 96 165 250;
  }

  /* 基础样式重置 */
  * {
    @apply border-border;
  }
  
  body {
    @apply bg-background text-foreground;
    font-feature-settings: "rlig" 1, "calt" 1;
  }
}

三、阶段二方案:Style Dictionary 构建流程

3.1 什么是 Style Dictionary?

Style Dictionary 是 Amazon 开源的设计 Token 构建工具,它能:

  • 读取 JSON/YAML 格式的 Token 源文件
  • 解析 Token 间的引用关系
  • 转换输出为多种格式(CSS、JS、iOS、Android 等)
Token 源文件 (JSON)


  Style Dictionary

       ├──→ CSS Variables
       ├──→ JavaScript Module
       ├──→ TypeScript Types
       ├──→ SCSS Variables
       ├──→ Swift Constants
       └──→ Kotlin Constants

3.2 目录结构

packages/
├── tokens/                    # Token 包
│   ├── src/
│   │   ├── primitive/
│   │   │   ├── colors.json
│   │   │   ├── spacing.json
│   │   │   └── typography.json
│   │   └── semantic/
│   │       ├── light.json
│   │       └── dark.json
│   ├── dist/                  # 构建产物
│   │   ├── css/
│   │   │   └── variables.css
│   │   ├── js/
│   │   │   ├── tokens.js
│   │   │   └── tokens.d.ts
│   │   └── tailwind/
│   │       └── theme.js
│   ├── style-dictionary.config.js
│   └── package.json

├── ui/                        # 组件库包
│   ├── src/
│   │   └── components/
│   ├── tailwind.config.js     # 消费 @scope/tokens
│   └── package.json

└── package.json               # Monorepo 根配置

3.3 Token 源文件示例

json
// packages/tokens/src/primitive/colors.json
{
  "color": {
    "primitive": {
      "blue": {
        "50":  { "value": "#EFF6FF", "type": "color" },
        "100": { "value": "#DBEAFE", "type": "color" },
        "500": { "value": "#3B82F6", "type": "color" },
        "600": { "value": "#2563EB", "type": "color" },
        "900": { "value": "#1E3A8A", "type": "color" }
      },
      "gray": {
        "50":  { "value": "#F9FAFB", "type": "color" },
        "100": { "value": "#F3F4F6", "type": "color" },
        "500": { "value": "#6B7280", "type": "color" },
        "900": { "value": "#111827", "type": "color" }
      }
    }
  }
}
json
// packages/tokens/src/semantic/light.json
{
  "color": {
    "bg": {
      "value": "{color.primitive.gray.50}",
      "type": "color",
      "description": "Default background color"
    },
    "text": {
      "value": "{color.primitive.gray.900}",
      "type": "color",
      "description": "Default text color"
    },
    "primary": {
      "value": "{color.primitive.blue.500}",
      "type": "color",
      "description": "Primary brand color"
    }
  }
}
json
// packages/tokens/src/semantic/dark.json
{
  "color": {
    "bg": {
      "value": "#0F172A",
      "type": "color"
    },
    "text": {
      "value": "#F8FAFC",
      "type": "color"
    },
    "primary": {
      "value": "#60A5FA",
      "type": "color"
    }
  }
}

3.4 Style Dictionary 配置

javascript
// packages/tokens/style-dictionary.config.js
const StyleDictionary = require('style-dictionary');

// 自定义格式:生成 Tailwind 兼容的 theme 对象
StyleDictionary.registerFormat({
  name: 'tailwind/theme',
  formatter: function({ dictionary }) {
    const tokens = {};
    
    dictionary.allTokens.forEach(token => {
      const path = token.path.join('.');
      tokens[path] = token.value;
    });
    
    return `module.exports = ${JSON.stringify(tokens, null, 2)};`;
  }
});

// 自定义转换:将颜色转为 RGB 格式(支持 Tailwind 透明度)
StyleDictionary.registerTransform({
  name: 'color/rgb',
  type: 'value',
  matcher: (token) => token.type === 'color',
  transformer: (token) => {
    const hex = token.value;
    const r = parseInt(hex.slice(1, 3), 16);
    const g = parseInt(hex.slice(3, 5), 16);
    const b = parseInt(hex.slice(5, 7), 16);
    return `${r} ${g} ${b}`;
  }
});

module.exports = {
  source: ['src/**/*.json'],
  platforms: {
    // CSS 变量输出
    css: {
      transformGroup: 'css',
      buildPath: 'dist/css/',
      files: [{
        destination: 'variables.css',
        format: 'css/variables',
        options: {
          outputReferences: true
        }
      }]
    },
    
    // JavaScript 模块输出
    js: {
      transformGroup: 'js',
      buildPath: 'dist/js/',
      files: [{
        destination: 'tokens.js',
        format: 'javascript/es6'
      }]
    },
    
    // TypeScript 类型输出
    ts: {
      transformGroup: 'js',
      buildPath: 'dist/js/',
      files: [{
        destination: 'tokens.d.ts',
        format: 'typescript/es6-declarations'
      }]
    },
    
    // Tailwind theme 输出
    tailwind: {
      transforms: ['attribute/cti', 'name/cti/kebab', 'color/rgb'],
      buildPath: 'dist/tailwind/',
      files: [{
        destination: 'theme.js',
        format: 'tailwind/theme'
      }]
    }
  }
};

3.5 Package.json 配置

json
// packages/tokens/package.json
{
  "name": "@your-scope/tokens",
  "version": "1.0.0",
  "main": "dist/js/tokens.js",
  "types": "dist/js/tokens.d.ts",
  "exports": {
    ".": {
      "import": "./dist/js/tokens.js",
      "types": "./dist/js/tokens.d.ts"
    },
    "./css": "./dist/css/variables.css",
    "./tailwind": "./dist/tailwind/theme.js"
  },
  "scripts": {
    "build": "style-dictionary build",
    "clean": "rm -rf dist"
  },
  "devDependencies": {
    "style-dictionary": "^3.9.0"
  }
}

3.6 组件库消费 Token

javascript
// packages/ui/tailwind.config.js
const tokens = require('@your-scope/tokens/tailwind');

module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  darkMode: 'class',
  theme: {
    colors: {
      // 从 Token 包导入
      ...tokens.colors,
      // 语义颜色仍使用 CSS 变量
      background: 'rgb(var(--color-bg) / <alpha-value>)',
      foreground: 'rgb(var(--color-text) / <alpha-value>)',
      primary: {
        DEFAULT: 'rgb(var(--color-primary) / <alpha-value>)',
        foreground: 'rgb(var(--color-primary-foreground) / <alpha-value>)',
      },
    },
    spacing: tokens.spacing,
  },
};

四、Monorepo 架构设计

4.1 推荐的包结构

your-design-system/
├── packages/
│   ├── tokens/              # 设计 Token
│   │   ├── src/
│   │   ├── dist/
│   │   └── package.json
│   │
│   ├── ui/                  # React 组件库
│   │   ├── src/
│   │   │   ├── components/
│   │   │   │   ├── Button/
│   │   │   │   ├── Input/
│   │   │   │   └── ...
│   │   │   ├── hooks/
│   │   │   └── index.ts
│   │   ├── tailwind.config.js
│   │   └── package.json
│   │
│   ├── icons/               # 图标库(可选)
│   │   ├── src/
│   │   └── package.json
│   │
│   └── eslint-config/       # 共享 ESLint 配置(可选)
│       └── package.json

├── apps/                    # 应用(可选)
│   ├── docs/                # 文档站点
│   │   └── package.json
│   └── playground/          # 组件演示
│       └── package.json

├── package.json             # 根配置
├── pnpm-workspace.yaml      # pnpm 工作区配置
└── turbo.json               # Turborepo 配置(可选)

4.2 根配置文件

yaml
# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'
json
// package.json (根目录)
{
  "name": "your-design-system",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "clean": "turbo run clean"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  }
}
json
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {},
    "clean": {
      "cache": false
    }
  }
}

4.3 包间依赖关系

依赖关系图:

tokens (无依赖)


  ui (依赖 tokens)


 apps (依赖 ui, tokens)
json
// packages/ui/package.json
{
  "name": "@your-scope/ui",
  "version": "1.0.0",
  "dependencies": {
    "@your-scope/tokens": "workspace:*",
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}

五、Figma 同步(可选进阶)

5.1 工具选择

工具特点适用场景
Tokens Studio for Figma免费、功能全面、支持 GitHub 同步推荐
Figma VariablesFigma 原生、简单简单项目
Specify商业工具、全流程企业级

5.2 Tokens Studio 工作流

设计师在 Figma                    开发者在代码
     │                               │
     ▼                               │
Tokens Studio 插件                   │
定义/修改 Token                       │
     │                               │
     ▼                               │
推送到 GitHub                        │
(tokens.json)                        │
     │                               │
     └──────────► GitHub ◄───────────┘


              GitHub Action
              (Style Dictionary 构建)


               npm 发布
           @your-scope/tokens

5.3 GitHub Action 示例

yaml
# .github/workflows/build-tokens.yml
name: Build Tokens

on:
  push:
    paths:
      - 'packages/tokens/src/**'
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
          
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
          
      - name: Install dependencies
        run: pnpm install
        
      - name: Build tokens
        run: pnpm --filter @your-scope/tokens build
        
      - name: Commit built files
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add packages/tokens/dist
          git diff --staged --quiet || git commit -m "chore: build tokens"
          git push

六、本轮小结

推荐的演进路径

Phase 1: 简单启动(现在)
├── Token 直接用 TypeScript 定义
├── Tailwind config 直接引用
├── 手动维护 CSS 变量
└── 单仓库结构

Phase 2: 工程化提升(项目成熟后)
├── 引入 Style Dictionary
├── Token 源文件改为 JSON
├── 自动化构建流程
└── Monorepo 拆分包

Phase 3: 设计协作(团队协作时)
├── 引入 Tokens Studio for Figma
├── 设计师直接维护 Token
├── GitHub Action 自动同步
└── 版本化发布 npm 包

关键文件清单

Phase 1 需要的文件

src/
├── tokens/
│   ├── index.ts
│   ├── colors.ts
│   ├── spacing.ts
│   ├── typography.ts
│   └── semantic.ts
├── styles/
│   └── globals.css
└── tailwind.config.ts

Phase 2 需要的文件

packages/
├── tokens/
│   ├── src/**/*.json
│   ├── style-dictionary.config.js
│   └── package.json
├── ui/
│   ├── src/
│   ├── tailwind.config.js
│   └── package.json
├── pnpm-workspace.yaml
└── turbo.json

本轮思考问题

  1. 你现在想从 Phase 1 还是 Phase 2 开始? 我建议先从 Phase 1 开始,快速跑起来。

  2. 你的项目是否已经是 Monorepo 结构? 如果是,用的什么工具(pnpm/npm/yarn workspaces)?