设计系统组件:从 Token 治理到跨端一致性的工程化构建
设计系统组件:从 Token 治理到跨端一致性的工程化构建
一、设计系统不是组件库,是约束的工程化表达
很多团队把设计系统等同于组件库——写一批 Button、Input、Modal 就叫设计系统。但组件库只是设计系统的产出物,设计系统的本质是约束的工程化表达:色彩用什么 Token、间距遵循什么梯度、组件接口遵循什么规范。没有约束的组件库,只是代码的堆砌。
一个金融科技团队在构建设计系统时,先花了 3 个月定义 Token 体系和组件规范,再用 2 个月实现组件。结果:跨 4 个产品线的界面一致性从 52% 提升到 89%,新页面开发效率提升 40%。约束先行,实现后行——这是设计系统的正确构建顺序。
二、设计系统的分层架构与 Token 流转
设计系统分为四个层次:Design Token → 基础样式 → 基础组件 → 业务组件。Token 是整个系统的源头,所有视觉决策最终追溯到 Token。
graph TD
A[原始 Token] --> B[语义 Token]
B --> C[组件 Token]
C --> D[组件样式]
D --> E[基础组件]
E --> F[业务组件]
subgraph Token 层
A1[color-blue-500]
A2[spacing-4]
A3[radius-md]
end
subgraph 语义层
B1[color-primary → color-blue-500]
B2[spacing-input → spacing-4]
B3[radius-button → radius-md]
end
subgraph 组件层
C1[button-bg → color-primary]
C2[button-padding → spacing-input]
C3[button-radius → radius-button]
end
A --> B
B --> C
C --> D
style A fill:#f9f,stroke:#333
style B fill:#ff9,stroke:#333
style C fill:#9ff,stroke:#333
原始 Token(Raw Token)定义具体的色值、间距值、圆角值。语义 Token(Semantic Token)将原始 Token 映射到界面角色(primary、secondary、danger)。组件 Token(Component Token)将语义 Token 映射到组件属性(button-bg、input-border)。三层 Token 的流转确保了:修改一个原始 Token,所有使用它的组件自动更新。
三、生产级设计系统 Token 管理与组件实现
3.1 Token 定义与多平台输出
/**
* Design Token 管理器
* 统一定义 Token,多平台格式输出
*/
interface TokenSet {
colors: Record<string, string>;
spacing: Record<string, number>;
typography: Record<string, TypographyToken>;
radii: Record<string, number>;
shadows: Record<string, string>;
motion: Record<string, MotionToken>;
}
interface TypographyToken {
fontFamily: string;
fontSize: string;
fontWeight: number;
lineHeight: string;
letterSpacing: string;
}
interface MotionToken {
duration: string;
easing: string;
}
class TokenManager {
private tokens: TokenSet;
constructor(tokens: TokenSet) {
this.tokens = tokens;
}
/**
* 输出为 CSS 自定义属性
*/
toCSSVariables(): string {
const lines: string[] = [':root {'];
// 色彩 Token
for (const [name, value] of Object.entries(this.tokens.colors)) {
lines.push(` --color-${name}: ${value};`);
}
// 间距 Token
for (const [name, value] of Object.entries(this.tokens.spacing)) {
lines.push(` --spacing-${name}: ${value}px;`);
}
// 圆角 Token
for (const [name, value] of Object.entries(this.tokens.radii)) {
lines.push(` --radius-${name}: ${value}px;`);
}
// 阴影 Token
for (const [name, value] of Object.entries(this.tokens.shadows)) {
lines.push(` --shadow-${name}: ${value};`);
}
// 动效 Token
for (const [name, value] of Object.entries(this.tokens.motion)) {
lines.push(` --motion-${name}-duration: ${value.duration};`);
lines.push(` --motion-${name}-easing: ${value.easing};`);
}
lines.push('}');
return lines.join('\n');
}
/**
* 输出为 Tailwind 配置
*/
toTailwindConfig(): Record<string, unknown> {
return {
theme: {
colors: this.prefixKeys(this.tokens.colors, 'color-'),
spacing: this.tokens.spacing,
borderRadius: this.tokens.radii,
boxShadow: this.tokens.shadows,
transitionTimingFunction: Object.fromEntries(
Object.entries(this.tokens.motion).map(([name, { easing }]) => [
name, easing,
])
),
transitionDuration: Object.fromEntries(
Object.entries(this.tokens.motion).map(([name, { duration }]) => [
name, duration,
])
),
},
};
}
/**
* 输出为 Flutter ThemeData
*/
toFlutterTheme(): string {
const primary = this.tokens.colors['primary'] ?? '#3B82F6';
const bg = this.tokens.colors['bg'] ?? '#FFFFFF';
return `
import 'package:flutter/material.dart';
class AppTheme {
static ThemeData get light => ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF${primary.replace('#', '')}),
brightness: Brightness.light,
surface: const Color(0xFF${bg.replace('#', '')}),
),
useMaterial3: true,
);
}`;
}
private prefixKeys(
obj: Record<string, unknown>,
prefix: string
): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[`${prefix}${key}`] = value;
}
return result;
}
}
3.2 组件规范约束器
/**
* 组件规范约束器
* 在开发时校验组件是否符合设计系统规范
* 检测硬编码值、Token 使用率、接口一致性
*/
class ComponentSpecEnforcer {
private tokenValues: Set<string>;
constructor(tokenManager: TokenManager) {
// 收集所有 Token 的具体值,用于检测硬编码
this.tokenValues = new Set();
const cssVars = tokenManager.toCSSVariables();
const valuePattern = /:\s*([^;]+);/g;
let match;
while ((match = valuePattern.exec(cssVars)) !== null) {
this.tokenValues.add(match[1].trim());
}
}
/**
* 校验 CSS/SCSS 文件中的硬编码值
*/
checkHardcodedValues(sourceCode: string): HardcodeViolation[] {
const violations: HardcodeViolation[] = [];
// 检测硬编码色值
const hexPattern = /#([0-9a-fA-F]{3,8})\b/g;
let match;
while ((match = hexPattern.exec(sourceCode)) !== null) {
const fullMatch = match[0];
// 排除 CSS 变量定义中的色值
const lineStart = sourceCode.lastIndexOf('\n', match.index) + 1;
const line = sourceCode.slice(lineStart, sourceCode.indexOf('\n', match.index));
if (!line.includes('--')) {
violations.push({
type: 'color',
value: fullMatch,
position: match.index,
suggestion: '使用设计系统色彩 Token 替代',
});
}
}
// 检测硬编码间距值
const spacingPattern = /(?:margin|padding|gap):\s*(\d+)px/g;
while ((match = spacingPattern.exec(sourceCode)) !== null) {
const value = `${match[1]}px`;
if (!this.tokenValues.has(value)) {
violations.push({
type: 'spacing',
value,
position: match.index,
suggestion: '使用设计系统间距 Token 替代',
});
}
}
return violations;
}
/**
* 校验组件接口是否符合规范
*/
checkComponentInterface(
componentName: string,
props: Record<string, unknown>,
spec: ComponentSpec
): InterfaceViolation[] {
const violations: InterfaceViolation[] = [];
// 检查是否使用了未定义的 prop
for (const propName of Object.keys(props)) {
if (!spec.allowedProps.includes(propName)) {
violations.push({
type: 'unknown_prop',
component: componentName,
detail: `"${propName}" 不在组件规范允许的 props 中`,
});
}
}
// 检查必填 prop 是否缺失
for (const requiredProp of spec.requiredProps) {
if (!(requiredProp in props)) {
violations.push({
type: 'missing_required',
component: componentName,
detail: `缺少必填 prop "${requiredProp}"`,
});
}
}
// 检查变体值是否在允许范围内
for (const [propName, allowedValues] of Object.entries(spec.enumProps)) {
const value = props[propName];
if (value !== undefined && !allowedValues.includes(String(value))) {
violations.push({
type: 'invalid_variant',
component: componentName,
detail: `"${propName}" 的值 "${value}" 不在允许范围 [${allowedValues.join(', ')}] 中`,
});
}
}
return violations;
}
}
interface HardcodeViolation {
type: 'color' | 'spacing' | 'typography';
value: string;
position: number;
suggestion: string;
}
interface InterfaceViolation {
type: 'unknown_prop' | 'missing_required' | 'invalid_variant';
component: string;
detail: string;
}
interface ComponentSpec {
allowedProps: string[];
requiredProps: string[];
enumProps: Record<string, string[]>;
}
3.3 跨端样式同步机制
/**
* 跨端样式同步器
* 确保 Web、Flutter、RN 三端组件视觉一致
*/
class CrossPlatformStyleSync {
/**
* 将 CSS Token 转换为 Flutter 常量
*/
cssToFlutterConstants(cssTokens: string): string {
const lines = cssTokens.split('\n');
const dartLines: string[] = [
'class DesignTokens {',
' // 自动生成,请勿手动修改',
];
for (const line of lines) {
const match = line.match(/--([\w-]+):\s*([^;]+);/);
if (!match) continue;
const [, name, value] = match;
const dartName = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
if (value.startsWith('#')) {
dartLines.push(` static const int ${dartName} = 0xFF${value.replace('#', '')};`);
} else if (value.endsWith('px')) {
dartLines.push(` static const double ${dartName} = ${value.replace('px', '')};`);
} else {
dartLines.push(` static const String ${dartName} = '${value}';`);
}
}
dartLines.push('}');
return dartLines.join('\n');
}
/**
* 对比两端的 Token 一致性
*/
compareTokens(
webTokens: Record<string, string>,
flutterTokens: Record<string, string>
): TokenDiff[] {
const diffs: TokenDiff[] = [];
const allKeys = new Set([
...Object.keys(webTokens),
...Object.keys(flutterTokens),
]);
for (const key of allKeys) {
const webValue = webTokens[key];
const flutterValue = flutterTokens[key];
if (webValue === undefined) {
diffs.push({ key, type: 'missing_web', webValue: null, flutterValue });
} else if (flutterValue === undefined) {
diffs.push({ key, type: 'missing_flutter', webValue, flutterValue: null });
} else if (webValue !== flutterValue) {
diffs.push({ key, type: 'mismatch', webValue, flutterValue });
}
}
return diffs;
}
}
interface TokenDiff {
key: string;
type: 'missing_web' | 'missing_flutter' | 'mismatch';
webValue: string | null;
flutterValue: string | null;
}
四、设计系统的治理成本与渐进式落地
Token 治理的维护成本:三层 Token 体系(原始 → 语义 → 组件)增加了维护复杂度。每次品牌升级需要逐层更新,漏更新任何一层都会导致不一致。建议建立自动化校验管线,在 CI 中检测 Token 层级的引用完整性。
组件规范的执行成本:规范约束器能检测硬编码值和接口违规,但执行依赖团队自觉。建议将约束器集成到 CI 管线,硬编码值检测作为 lint 规则,接口校验作为单元测试。但初期不要设为阻断性规则,先用 warning 模式培养习惯。
跨端同步的精度损耗:CSS 和 Flutter 的色彩渲染存在差异,同一色值在两端可能呈现不同效果。特别是阴影和渐变,两端引擎的渲染算法不同。建议建立视觉回归测试,用截图对比检测跨端偏差。
渐进式落地的节奏:不要试图一次性构建完整的设计系统。建议分三个阶段:第一阶段只定义色彩和间距 Token,覆盖 80% 的视觉一致性问题;第二阶段实现基础组件(Button、Input、Modal),覆盖 60% 的组件复用需求;第三阶段扩展业务组件和跨端同步。每个阶段 2-3 个月,用数据验证效果后再推进下一阶段。
五、总结
设计系统的工程化核心是 Token 驱动的约束体系。三层 Token 架构(原始 → 语义 → 组件)确保视觉决策可追溯、可批量更新。Token 管理器统一输出 CSS 变量、Tailwind 配置和 Flutter 主题,实现跨端样式源头的统一。组件规范约束器在开发时检测硬编码值和接口违规,保障规范的执行。但设计系统有治理成本:Token 维护需要自动化校验,规范执行需要 CI 集成,跨端同步存在渲染精度损耗。渐进式落地是务实的路径——先 Token 后组件,先基础后业务,用数据验证每个阶段的效果。设计系统是约束的工程化表达,约束越清晰,一致性越高。
更多推荐


所有评论(0)