话题 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.ts2.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 Constants3.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 Variables | Figma 原生、简单 | 简单项目 |
| Specify | 商业工具、全流程 | 企业级 |
5.2 Tokens Studio 工作流
设计师在 Figma 开发者在代码
│ │
▼ │
Tokens Studio 插件 │
定义/修改 Token │
│ │
▼ │
推送到 GitHub │
(tokens.json) │
│ │
└──────────► GitHub ◄───────────┘
│
▼
GitHub Action
(Style Dictionary 构建)
│
▼
npm 发布
@your-scope/tokens5.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.tsPhase 2 需要的文件:
packages/
├── tokens/
│ ├── src/**/*.json
│ ├── style-dictionary.config.js
│ └── package.json
├── ui/
│ ├── src/
│ ├── tailwind.config.js
│ └── package.json
├── pnpm-workspace.yaml
└── turbo.json本轮思考问题
你现在想从 Phase 1 还是 Phase 2 开始? 我建议先从 Phase 1 开始,快速跑起来。
你的项目是否已经是 Monorepo 结构? 如果是,用的什么工具(pnpm/npm/yarn workspaces)?