设计系统组件:从 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 后组件,先基础后业务,用数据验证每个阶段的效果。设计系统是约束的工程化表达,约束越清晰,一致性越高。

Logo

一站式 AI 云服务平台

更多推荐