Material 3组件夹具封装教程 - 第1节

1.1 设计令牌(Design Tokens)——组件样式的原子单元

Material 3 的设计系统核心是 令牌(Tokens)。令牌是样式的最基本抽象层,它将颜色、字体、间距、圆角等设计决策定义为命名变量,使得所有组件都能通过引用这些变量获得统一的外观。令牌分为三个层级:

  • 全局令牌(Global Tokens):与品牌无关的原始值,如 md.ref.palette.primary0(纯黑)、md.ref.typeface.plain(正文字体族)。
  • 别名令牌(Alias Tokens):在全局令牌基础上映射到语义角色,如 md.sys.color.primary 引用某个调色板色值,md.sys.typescale.title-large 定义标题字号。
  • 组件令牌(Component Tokens):直接作用于具体组件的属性,如 md.comp.button.container-color、md.comp.card.shape。

在组件夹具封装中,我们通常将组件令牌映射为 CSS 自定义属性或 TypeScript 常量,实现运行时主题切换。

💡 关键实现:在 React 组件中,通过 useTheme Hook 读取令牌值,并注入到组件的 style 或 className 中。
示例:从主题上下文中获取按钮容器颜色并应用到根节点。

令牌与CSS自定义属性映射

/* tokens.css  —— 全局令牌定义 */
:root {
  --md-ref-palette-primary0: #000000;
  --md-ref-palette-primary10: #21005d;
  --md-ref-palette-primary100: #ffffff;
  /* … 更多全局色板 */
}

/* 别名令牌 —— 语义颜色 */
:root {
  --md-sys-color-primary: var(--md-ref-palette-primary40);
  --md-sys-color-on-primary: var(--md-ref-palette-primary100);
  --md-sys-color-surface: var(--md-ref-palette-neutral99);
}

/* 组件令牌 —— 按钮容器颜色 */
:root {
  --md-comp-button-container-color: var(--md-sys-color-primary);
  --md-comp-button-disabled-container-color: rgba(0,0,0,0.12);
}

TypeScript 类型定义(夹具封装常用)

// tokens.types.ts
export interface MaterialTokens {
  color: {
    primary: string;
    onPrimary: string;
    surface: string;
    error: string;
  };
  shape: {
    small: string;  // 圆角值,如"4px"
    medium: string; // "8px"
    large: string;  // "16px"
  };
  typography: {
    headlineLarge: string;
    titleMedium: string;
    bodyLarge: string;
  };
}

1.2 组件层次结构——从原子到页面

Material 3 组件遵循 原子-分子-有机体 的层次模型。在夹具封装中,每一层都应当保持职责清晰。

层次 定义 封装示例
原子 最小组件单元,不可再分割(如按钮、图标、文本输入)。 MdButton、MdIcon
分子 由多个原子组合而成的通用控件(如搜索栏、卡片)。 MdCard(内含按钮、图标、文本)
有机体 业务相关的复合组件,通常包含状态管理(如用户资料卡片)。 UserProfileCard(含数据加载、错误边界)

封装原则:原子组件只接收令牌和基本回调;分子组件负责布局和子组件传参;有机体组件负责业务逻辑与数据流。

1.3 组件状态——交互响应模型

Material 3 为每个可交互组件定义了 状态层叠:enabled → hovered → focused → pressed → disabled。夹具封装需要为每个状态提供对应的视觉令牌和覆盖层。

⚠️ 常见错误:只处理 enabled 和 disabled 状态,忽略 hover/focus/pressed 的动画过渡,导致组件交互生硬。
解决方法:在组件样式表中为每个状态定义独立的 CSS 变量,并使用 transition 属性。

状态枚举与映射(React 夹具)

// component-states.ts
export enum ComponentState {
  Enabled = 'enabled',
  Hovered = 'hovered',
  Focused = 'focused',
  Pressed = 'pressed',
  Disabled = 'disabled',
}

export function getStateToken(
  baseToken: string,
  state: ComponentState
): string {
  const suffixMap: Record<ComponentState, string> = {
    [ComponentState.Enabled]: '',
    [ComponentState.Hovered]: '-hover',
    [ComponentState.Focused]: '-focus',
    [ComponentState.Pressed]: '-pressed',
    [ComponentState.Disabled]: '-disabled',
  };
  return `${baseToken}${suffixMap[state]}`;
}

1.4 主题系统——暗色/亮色及自定义配色

Material 3 主题由三个核心维度构成:颜色方案(Color Scheme)、字体层级(Typography Scale)、形状体系(Shape Scale)。主题变量通常定义在 :root 或 [data-theme] 选择器下。

Material 3 主题系统架构 主题容器 颜色方案 字体层级 形状体系 颜色子令牌 primary / on-primary secondary / surface error / outline 暗色/亮色两套值 字体子令牌 display / headline title / label body / caption 字号/字重/行高 形状子令牌 small / medium / large extra-large / full 圆角半径值 对应组件形状

主题切换夹具实现步骤

  1. 定义全局 CSS 自定义属性(亮色/暗色两套)。
  2. 创建 ThemeProvider 组件,管理当前主题类型(light/dark)。
  3. 在 Provider 中将主题令牌注入到 data-theme 属性或 :root 选择器。
  4. 组件夹具通过 useContext 获取当前主题对象,并应用到组件令牌变量。

主题 Provider 核心代码(React)

// ThemeProvider.tsx
import React, { createContext, useContext, useState } from 'react';

interface ThemeContextValue {
  scheme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextValue>({
  scheme: 'light',
  toggleTheme: () => {},
});

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [scheme, setScheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    const next = scheme === 'light' ? 'dark' : 'light';
    setScheme(next);
    document.documentElement.setAttribute('data-theme', next);
  };

  return (
    <ThemeContext.Provider value={{ scheme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

亮色/暗色主题 CSS 示例

/* 亮色主题(默认) */
:root {
  --md-sys-color-primary: #6750a4;
  --md-sys-color-on-primary: #ffffff;
  --md-sys-color-surface: #fffbfe;
  --md-comp-button-container-color: var(--md-sys-color-primary);
  --md-comp-button-shape: 20px;
}

/* 暗色主题 */
[data-theme="dark"] {
  --md-sys-color-primary: #d0bcff;
  --md-sys-color-on-primary: #381e72;
  --md-sys-color-surface: #1c1b1f;
  --md-comp-button-container-color: var(--md-sys-color-primary);
  --md-comp-button-shape: 20px;
}
💡 调试技巧:在浏览器控制台使用 document.documentElement.setAttribute('data-theme', 'dark') 立即切换主题,验证所有组件是否正确响应令牌变化。

1.5 组件夹具的封装流程总览

结合本章知识点,一个完整的 Material 3 组件夹具封装流程如下:

步骤 操作 输出
1 定义全局令牌(颜色、字体、形状) tokens.css 或 JSON 文件
2 创建主题 Provider 与上下文 ThemeProvider.tsx + useTheme
3 实现基础原子组件(只依赖令牌) MdButton.tsx、MdIcon.tsx
4 添加状态覆盖层(hover/focus/pressed/disabled) CSS 伪类 + 状态令牌映射
5 组合分子组件(集成原子组件并处理布局) MdCard.tsx、MdNavigationBar.tsx
6 单元测试与视觉回归 Jest + Storybook 快照

1.6 常见错误与稳定性优化

  • 令牌名称不一致:确保组件使用的令牌名与主题定义完全匹配,建议使用 TypeScript 枚举或常量字符串(如 TOKENS.BUTTON.CONTAINER_COLOR)避免拼写错误。
  • 状态层叠顺序错误:CSS 中必须按 enabled → hover → focus → press → disabled 书写,否则后定义的状态可能被先定义的覆盖。
  • 主题切换性能:避免在每次渲染时重新计算 CSS 变量,使用 data-theme 属性批量切换,并利用 CSS transition 实现平滑过渡。
  • 形状单位不一致:所有圆角值统一使用 CSS 变量,避免直接写死 px 值,确保主题可定制。

稳定性检查清单(夹具发布前)

# 稳定性检查命令示例(基于 Playwright 视觉对比)
npx playwright test --update-snapshots   # 更新基准图
npx playwright test                       # 对比当前快照
# 检查主题切换后所有组件无样式泄漏
npx lighthouse --chrome-flags="--headless" http://localhost:6006