跳到主要内容
EN

组件化架构设计

10 分钟阅读

组件设计原则

单一职责

一个组件只做一件事。判断标准:如果一个组件有超过一个理由去改变,它就承担了过多职责。

// 差:UserProfile 既展示信息又编辑信息
function UserProfile({ user, onSave }) { /* ... */ }

// 好:拆分展示与编辑
function UserProfileCard({ user }) { /* 只读展示 */ }
function UserProfileForm({ user, onSave }) { /* 编辑表单 */ }

接口契约

组件的 Props 就是它的 API。好的接口设计遵循:

  • 最小化:只传必要的数据,不传整个对象
  • 明确性:用 TypeScript 定义 Props 类型
  • 默认值:为可选 Props 提供合理默认值
interface ButtonProps {
  variant: "primary" | "secondary" | "danger"; // 联合类型限制选项
  size?: "sm" | "md" | "lg";                   // 可选,有默认值
  disabled?: boolean;
  onClick?: () => void;
  children: React.ReactNode;
}

受控与非受控

// 受控组件:状态由外部控制
function Input({ value, onChange }) {
  return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}

// 非受控组件:状态由内部管理
function Input({ defaultValue }) {
  const [value, setValue] = useState(defaultValue);
  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

选择原则:表单数据需要实时校验/联动/提交 → 受控;简单输入不需要外部感知 → 非受控。也可以设计为同时支持两种模式:

function Input({ value: controlledValue, defaultValue, onChange }) {
  const isControlled = controlledValue !== undefined;
  const [internalValue, setInternalValue] = useState(defaultValue);
  const value = isControlled ? controlledValue : internalValue;

  return (
    <input
      value={value}
      onChange={(e) => {
        if (!isControlled) setInternalValue(e.target.value);
        onChange?.(e.target.value);
      }}
    />
  );
}

组件通信模式

graph TD
    A["组件通信"] --> B["父子"]
    A --> C["跨层级"]
    A --> D["兄弟/无关系"]
    
    B --> B1["Props 传递"]
    B --> B2["回调函数"]
    B --> B3["Ref 转发"]
    
    C --> C1["Context / Provide-Inject"]
    C --> C2["事件总线"]
    
    D --> D1["状态管理库"]
    D --> D2["事件总线"]
模式 适用场景 特点
Props / Events 父子 最直接,数据流清晰
Provide / Inject 祖先后代 跨层级传递,避免逐层 Props
Ref + ImperativeHandle 父调子方法 暴露命令式 API(如 focus、scroll)
状态管理 全局共享 单一数据源,跨组件同步
事件总线 任意组件 解耦但难追踪,谨慎使用
// Ref 转发:父组件调用子组件方法
const Input = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ""; },
  }));
  return <input ref={inputRef} />;
});

// 父组件
const inputRef = useRef();
inputRef.current.focus(); // 命令式调用

复用模式演进

高阶组件(HOC)

// 包装组件注入额外能力
function withAuth(WrappedComponent) {
  return function AuthComponent(props) {
    const { user } = useAuth();
    if (!user) return <Redirect to="/login" />;
    return <WrappedComponent {...props} user={user} />;
  };
}
const ProtectedPage = withAuth(Dashboard);

问题:嵌套地狱(withA(withB(withC(Component))))、Props 来源不明、Ref 丢失。

Render Props

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  // ...监听鼠标移动
  return render(position);
}

<MouseTracker render={({ x, y }) => <Cursor x={x} y={y} />} />

问题:回调嵌套难读、无法在 shouldComponentUpdate 中优化。

Hooks 复用

function useAuth() {
  const [user, setUser] = useState(null);
  const login = useCallback(async (credentials) => {
    const data = await api.login(credentials);
    setUser(data);
  }, []);
  return { user, login, isLoggedIn: !!user };
}

// 使用:逻辑与状态天然聚合
function Dashboard() {
  const { user, isLoggedIn } = useAuth();
  if (!isLoggedIn) return <Navigate to="/login" />;
  return <h1>欢迎, {user.name}</h1>;
}

Hooks 成为现代主流复用模式:无嵌套、来源清晰、类型安全。

组件库设计

Design Token

Design Token 是设计系统的原子变量——颜色、间距、字号、阴影等:

:root {
  /* 颜色 */
  --color-primary-500: #3b82f6;
  --color-danger-500: #ef4444;
  /* 间距 */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  /* 字号 */
  --font-size-sm: 12px;
  --font-size-md: 14px;
  --font-size-lg: 18px;
  /* 圆角 */
  --radius-sm: 4px;
  --radius-md: 8px;
}

主题系统

// 运行时主题切换
const themes = {
  light: { "--color-bg": "#ffffff", "--color-text": "#1f2328" },
  dark: { "--color-bg": "#0d1117", "--color-text": "#e6edf3" },
};

function ThemeProvider({ theme, children }) {
  return (
    <div style={themes[theme]}>
      {children}
    </div>
  );
}

无障碍(A11y)

function Dialog({ open, onClose, title, children }) {
  return (
    <div role="dialog" aria-modal="true" aria-labelledby="dialog-title"
         hidden={!open}>
      <h2 id="dialog-title">{title}</h2>
      {children}
      <button onClick={onClose} aria-label="关闭对话框"></button>
    </div>
  );
}

关键原则:语义化 HTML、ARIA 属性、键盘可操作、颜色对比度达标。

Storybook 驱动的组件文档与测试

Storybook 为组件提供隔离的开发环境:

// Button.stories.jsx
export default {
  title: "Components/Button",
  component: Button,
  argTypes: {
    variant: { control: "select", options: ["primary", "secondary", "danger"] },
  },
};

export const Primary = {
  args: { variant: "primary", children: "点击我" },
};

export const Disabled = {
  args: { variant: "primary", disabled: true, children: "已禁用" },
};

Storybook 的价值:

  • 可视化文档:每个 Story 就是组件的一种用法
  • 交互测试:结合 Playwright 做交互测试
  • 视觉回归:Chromatic 截图对比,防止样式意外变更
  • 开发隔离:不受应用状态影响,专注组件本身
编辑此页

评论