设计系统搭建:从 Token 体系到组件库自动化管理的工程实践
设计系统搭建:从 Token 体系到组件库自动化管理的工程实践
一、设计一致性困境:当团队规模撞上样式碎片
项目初期,两三个开发者用 CSS 变量加一份共享样式文件就能维持界面一致性。但当团队扩展到十人以上、同时维护多个产品线时,问题开始爆发:不同项目各自定义了名为primary的颜色但色值不同,间距命名从sm/md/lg到4/8/12各有一套,组件 API 设计风格迥异导致跨项目复用几乎不可能。
设计系统(Design System)的搭建正是为了解决这类规模化一致性问题。它不仅仅是一份 UI 规范文档,而是一套从设计 Token 到组件代码、从版本发布到跨项目消费的完整工程体系。本文聚焦设计系统的工程化落地,探讨 Token 体系设计、组件库自动化管理以及跨项目同步的实践方案。
二、Design Token 体系与组件库自动化架构
Design Token 是设计系统的原子层,它将设计决策从具体的 CSS 属性中抽离出来,形成平台无关的中间表示。组件库则是在 Token 之上构建的分子层,消费 Token 并提供可复用的 UI 组件。
flowchart LR subgraph 设计层 A[Figma 设计源] --> B[Token 定义<br/>JSON/YAML] end subgraph 转换层 B --> C[Style Dictionary<br/>编译管线] C --> D1[CSS 变量] C --> D2[SCSS 变量] C --> D3[JS Token 对象] C --> D4[iOS/Android 原生] end subgraph 消费层 D1 --> E1[Web 组件库] D2 --> E1 D3 --> E2[JS 运行时主题切换] D4 --> E3[移动端组件库] end subgraph 自动化 F[CI Pipeline] -->|Token 变更触发| C G[Changesets] -->|版本管理| H[npm 发包] E1 --> H endToken 体系的设计需要分层:全局 Token(Global Token)定义最基础的值,别名 Token(Alias Token)赋予语义,组件 Token(Component Token)绑定具体组件。这种三层结构确保了修改的局部性——调整品牌色只需修改全局 Token,组件 Token 会通过别名链自动更新。
组件库的自动化管理涵盖三个维度:代码质量(Lint + 测试)、版本发布(Changesets + 自动化 Changelog)、跨项目同步(npm 包 + 语义化版本)。每一个维度的缺失都会导致组件库逐渐腐化,最终被项目团队弃用。
三、Token 编译管线与组件库自动化发布实现
3.1 基于 Style Dictionary 的 Token 编译
// token.config.ts —— Style Dictionary 编译配置 import StyleDictionary from 'style-dictionary'; import { fileURLToPath } from 'url'; import path from 'path'; // 自定义 Transform:将 Token 名称转为 CSS 变量命名规范 StyleDictionary.registerTransform({ name: 'name/kebab', type: 'name', transformer: (token) => { // color.brand.primary → --color-brand-primary return token.path.join('-'); }, }); // 自定义 Transform Group:Web 平台完整转换组 StyleDictionary.registerTransformGroup({ name: 'custom/web', transforms: [ 'attribute/cti', 'name/kebab', 'time/seconds', 'content/icon', 'size/rem', 'color/css', ], }); // 自定义 Format:生成带注释的 CSS 变量文件 StyleDictionary.registerFormat({ name: 'css/variables-with-comment', formatter: ({ dictionary }) => { const lines = dictionary.allTokens.map((token) => { const comment = token.comment ? ` /* ${token.comment} */` : ''; return ` --${token.name}: ${token.value};${comment}`; }); return `/* Design Token - Auto Generated */\n:root {\n${lines.join('\n')}\n}`; }, }); // 多平台编译配置 const config: StyleDictionary.Config = { source: ['tokens/**/*.json'], platforms: { css: { transformGroup: 'custom/web', buildPath: 'dist/css/', files: [{ destination: 'tokens.css', format: 'css/variables-with-comment', options: { outputReferences: true }, }], }, js: { transformGroup: 'custom/web', buildPath: 'dist/js/', files: [{ destination: 'tokens.js', format: 'javascript/es6', }], }, scss: { transformGroup: 'custom/web', buildPath: 'dist/scss/', files: [{ destination: '_tokens.scss', format: 'scss/variables', options: { outputReferences: true }, }], }, }, }; export default config;3.2 组件库自动化发布管线
// scripts/release.ts —— 基于 Changesets 的自动化发布脚本 import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; interface PackageInfo { name: string; version: string; private: boolean; } class ComponentLibRelease { private packagesDir: string; private dryRun: boolean; constructor(packagesDir: string, dryRun = false) { this.packagesDir = packagesDir; this.dryRun = dryRun; } /** 执行完整发布流程 */ async run(): Promise<void> { // Step 1: 代码质量检查 this.exec('npm run lint'); this.exec('npm run typecheck'); this.exec('npm run test -- --coverage'); // Step 2: 构建 Token 和组件 this.exec('npm run build:tokens'); this.exec('npm run build:components'); // Step 3: 检查是否有待发布的 Changeset const hasChangeset = this.hasPendingChangesets(); if (!hasChangeset) { console.log('没有待发布的 Changeset,跳过发布'); return; } // Step 4: 版本升级 this.exec('npx changeset version'); // Step 5: 校验版本号一致性(组件库对 peer 依赖版本敏感) this.validatePeerDependencies(); // Step 6: 发布 if (this.dryRun) { console.log('[Dry Run] 将执行 npm publish'); this.exec('npx changeset publish --dry-run'); } else { this.exec('npx changeset publish'); // 发布后创建 Git Tag this.exec('git push --follow-tags'); } } private hasPendingChangesets(): boolean { const dir = '.changeset'; if (!fs.existsSync(dir)) return false; const files = fs.readdirSync(dir) .filter(f => f !== 'config.json' && f.endsWith('.md')); return files.length > 0; } /** 校验组件包之间的 peer 依赖版本是否对齐 */ private validatePeerDependencies(): void { const packages = this.getPackages(); const versionMap = new Map<string, string>(); // 收集所有包的当前版本 for (const pkg of packages) { versionMap.set(pkg.name, pkg.version); } // 校验 peer 依赖引用的版本是否与实际发布版本一致 for (const pkg of packages) { const pkgJson = this.readPackageJson(pkg.name); const peers = pkgJson.peerDependencies ?? {}; for (const [dep, versionRange] of Object.entries(peers)) { const actualVersion = versionMap.get(dep); if (!actualVersion) continue; // 简化校验:检查主版本号是否匹配 const majorFromRange = (versionRange as string).replace(/[^0-9]/g, '').charAt(0); const majorActual = actualVersion.split('.')[0]; if (majorFromRange && majorFromRange !== majorActual) { throw new Error( `${pkg.name} 的 peer 依赖 ${dep}@${versionRange} ` + `与实际版本 ${actualVersion} 主版本号不匹配` ); } } } } private getPackages(): PackageInfo[] { const dirs = fs.readdirSync(this.packagesDir); return dirs .map(dir => { const pkgPath = path.join(this.packagesDir, dir, 'package.json'); if (!fs.existsSync(pkgPath)) return null; const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); return { name: pkg.name, version: pkg.version, private: pkg.private ?? false, }; }) .filter((p): p is PackageInfo => p !== null && !p.private); } private readPackageJson(name: string): any { const dirs = fs.readdirSync(this.packagesDir); for (const dir of dirs) { const pkgPath = path.join(this.packagesDir, dir, 'package.json'); if (!fs.existsSync(pkgPath)) continue; const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); if (pkg.name === name) return pkg; } throw new Error(`包 ${name} 未找到`); } private exec(command: string): void { console.log(`> ${command}`); execSync(command, { stdio: 'inherit' }); } } // 执行入口 const release = new ComponentLibRelease( path.resolve(process.cwd(), 'packages'), process.argv.includes('--dry-run') ); release.run().catch((err) => { console.error('发布失败:', err.message); process.exit(1); });四、设计系统落地的现实阻力与取舍
设计系统的搭建只是第一步,真正的挑战在于持续运营和跨团队推广。
Token 与代码的同步鸿沟:设计在 Figma 中更新了 Token,但代码中的 CSS 变量没有同步更新,导致设计与实现再次偏离。解决方案是将 Token 定义从 Figma 导出为 JSON,通过 CI 管线自动编译为各平台产物,消除人工搬运环节。但这要求设计团队接受"Token 是代码的一部分"这一理念,对设计工作流是一次重构。
组件库的版本碎片化:多个项目消费同一组件库的不同大版本,Bug 修复需要在多个分支上重复提交。语义化版本约束了兼容性,但无法消除碎片化本身。实践中需要在 Breaking Change 时提供 codemod 自动迁移工具,降低项目升级成本。
过度设计的风险:设计系统容易陷入"大而全"的陷阱——试图覆盖所有场景的 Token 和组件,结果维护成本远超收益。建议从核心场景(颜色、间距、排版、基础组件)起步,按需扩展。一个被 80% 项目使用的 20 个组件的库,远比一个被 10% 项目使用的 200 个组件的库更有价值。
五、总结
设计系统的工程化落地需要三个支点:分层的 Token 体系确保设计决策的可追溯性,Style Dictionary 编译管线消除设计与代码的同步鸿沟,Changesets 自动化发布保障组件库的持续交付。落地时务必克制过度设计的冲动,从核心场景起步,用数据驱动扩展决策——组件库的价值不在于数量,而在于被消费的频率。
