前端组件模式Skill frontend-component-patterns

前端组件模式技能专注于使用设计模式如复合组件、渲染属性、自定义钩子和高阶组件来构建高效、可维护的前端组件。适用于React、Vue和Angular框架,帮助开发者创建可重用UI库、优化组件架构和提升开发效率。关键词:前端开发、组件设计、React模式、Vue组件、Angular架构、可重用UI、性能优化、无障碍访问。

前端开发 0 次安装 0 次浏览 更新于 3/18/2026

名称:前端组件模式 描述:构建可重用、可组合和可维护的React/Vue/Angular组件,遵循如复合组件、渲染属性、自定义钩子和高阶组件等设计模式。在创建组件库、实现组件组合、构建可重用UI元素、设计属性API、管理组件状态模式、实现受控与非受控组件、创建复合组件、使用渲染属性或函数作为子元素、构建自定义钩子或建立组件架构标准时使用。

前端组件模式 - 构建可重用的React组件

何时使用此技能

  • 创建可重用组件库
  • 实现组件组合模式
  • 构建灵活、可配置的UI组件
  • 设计直观的组件属性API
  • 使用模式管理组件状态
  • 实现受控与非受控组件
  • 创建复合组件(如标签页、手风琴)
  • 使用渲染属性或函数作为子元素
  • 构建自定义React钩子以共享逻辑
  • 实现高阶组件(HOC)
  • 建立组件架构标准
  • 创建可访问、键盘可导航的组件

何时使用此技能

  • 设计React组件架构、提高组件可重用性、管理状态或解决常见UI模式时。
  • 处理相关任务或功能时
  • 需要此专业知识的开发过程中

使用时机:设计React组件架构、提高组件可重用性、管理状态或解决常见UI模式时。

核心原则

  1. 组合优于继承 - 从简单组件构建复杂UI
  2. 单一职责 - 每个组件做好一件事
  3. 属性向下、事件向上 - 单向数据流
  4. 关注点分离 - 逻辑与表现分离
  5. 无障碍优先 - ARIA、键盘导航、语义化HTML

组件模式

1. 表现型与容器型组件

// ❌ 混合关注点 - 逻辑与表现在一起
function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser)
      .finally(() => setLoading(false));
  }, [userId]);
  
  if (loading) return <div>加载中...</div>;
  
  return (
    <div className="profile">
      <img src={user.avatar} alt={user.name} />
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

// ✅ 分离 - 容器处理逻辑
function UserProfileContainer({ userId }: { userId: string }) {
  const { data: user, isLoading } = useUser(userId);
  
  if (isLoading) return <LoadingSpinner />;
  if (!user) return <NotFound />;
  
  return <UserProfileView user={user} />;
}

// ✅ 表现型 - 纯展示组件
interface UserProfileViewProps {
  user: {
    avatar: string;
    name: string;
    bio: string;
  };
}

function UserProfileView({ user }: UserProfileViewProps) {
  return (
    <div className="profile">
      <img src={user.avatar} alt={user.name} />
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

2. 复合组件

// ✅ 灵活、可组合的API
interface TabsProps {
  children: React.ReactNode;
  defaultValue?: string;
}

interface TabsContextValue {
  activeTab: string;
  setActiveTab: (value: string) => void;
}

const TabsContext = React.createContext<TabsContextValue | null>(null);

function Tabs({ children, defaultValue }: TabsProps) {
  const [activeTab, setActiveTab] = useState(defaultValue || '');
  
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabsList({ children }: { children: React.ReactNode }) {
  return <div className="tabs-list" role="tablist">{children}</div>;
}

function TabsTrigger({ value, children }: { value: string; children: React.ReactNode }) {
  const context = useContext(TabsContext);
  if (!context) throw new Error('TabsTrigger必须在Tabs内部');
  
  const isActive = context.activeTab === value;
  
  return (
    <button
      role="tab"
      aria-selected={isActive}
      onClick={() => context.setActiveTab(value)}
      className={isActive ? 'active' : ''}
    >
      {children}
    </button>
  );
}

function TabsContent({ value, children }: { value: string; children: React.ReactNode }) {
  const context = useContext(TabsContext);
  if (!context) throw new Error('TabsContent必须在Tabs内部');
  
  if (context.activeTab !== value) return null;
  
  return <div role="tabpanel">{children}</div>;
}

// 作为复合组件导出
export { Tabs, TabsList, TabsTrigger, TabsContent };

// 用法 - 灵活直观
function App() {
  return (
    <Tabs defaultValue="account">
      <TabsList>
        <TabsTrigger value="account">账户</TabsTrigger>
        <TabsTrigger value="password">密码</TabsTrigger>
        <TabsTrigger value="notifications">通知</TabsTrigger>
      </TabsList>
      
      <TabsContent value="account">
        <AccountSettings />
      </TabsContent>
      
      <TabsContent value="password">
        <PasswordSettings />
      </TabsContent>
      
      <TabsContent value="notifications">
        <NotificationSettings />
      </TabsContent>
    </Tabs>
  );
}

3. 渲染属性模式

// ✅ 使用渲染属性实现灵活渲染
interface MousePositionProps {
  children: (position: { x: number; y: number }) => React.ReactNode;
}

function MousePosition({ children }: MousePositionProps) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    const handleMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);
  
  return <>{children(position)}</>;
}

// 用法 - 消费者控制渲染
function App() {
  return (
    <MousePosition>
      {({ x, y }) => (
        <div>
          鼠标位于 ({x}, {y})
        </div>
      )}
    </MousePosition>
  );
}

4. 自定义钩子(现代替代方案)

// ✅ 提取可重用逻辑为钩子
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    const handleMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);
  
  return position;
}

// 用法 - 比渲染属性更简洁
function App() {
  const { x, y } = useMousePosition();
  
  return (
    <div>
      鼠标位于 ({x}, {y})
    </div>
  );
}

// ✅ 更多自定义钩子示例
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return debouncedValue;
}

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });
  
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  
  return [value, setValue] as const;
}

5. 高阶组件(传统模式)

// ✅ 用于添加功能的HOC
function withLoading<P extends object>(
  Component: React.ComponentType<P>
) {
  return function WithLoadingComponent(
    props: P & { isLoading: boolean }
  ) {
    if (props.isLoading) {
      return <LoadingSpinner />;
    }
    
    return <Component {...props} />;
  };
}

// 用法
const UserListWithLoading = withLoading(UserList);

<UserListWithLoading users={users} isLoading={loading} />

// 注意:现在自定义钩子比HOC更受青睐

状态管理模式

1. 属性与状态

// ✅ 属性 - 从父组件传递
interface ButtonProps {
  label: string;        // 显示文本
  onClick: () => void;  // 回调
  disabled?: boolean;   // 可选配置
}

function Button({ label, onClick, disabled }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

// ✅ 状态 - 组件内部
function Counter() {
  const [count, setCount] = useState(0); // 本地状态
  
  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

// ✅ 受控与非受控
// 受控 - 值来自属性(父组件控制)
function ControlledInput({ value, onChange }: { 
  value: string; 
  onChange: (value: string) => void; 
}) {
  return (
    <input 
      value={value} 
      onChange={(e) => onChange(e.target.value)} 
    />
  );
}

// 非受控 - 内部状态(组件控制)
function UncontrolledInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  
  const handleSubmit = () => {
    console.log(inputRef.current?.value);
  };
  
  return (
    <>
      <input ref={inputRef} defaultValue="初始值" />
      <button onClick={handleSubmit}>提交</button>
    </>
  );
}

2. 状态提升

// ❌ 重复状态 - 兄弟组件无法通信
function ParentBad() {
  return (
    <>
      <SearchBox /> {/* 有自己的搜索状态 */}
      <ResultsList /> {/* 有自己的搜索状态 */}
    </>
  );
}

// ✅ 在父组件中共享状态
function ParentGood() {
  const [searchQuery, setSearchQuery] = useState('');
  
  return (
    <>
      <SearchBox query={searchQuery} onQueryChange={setSearchQuery} />
      <ResultsList query={searchQuery} />
    </>
  );
}

3. 上下文用于深层属性

// ❌ 属性传递 - 穿过多个层级
function App() {
  const [theme, setTheme] = useState('light');
  return <Layout theme={theme} setTheme={setTheme} />;
}

function Layout({ theme, setTheme }) {
  return <Sidebar theme={theme} setTheme={setTheme} />;
}

function Sidebar({ theme, setTheme }) {
  return <ThemeToggle theme={theme} setTheme={setTheme} />;
}

// ✅ 上下文 - 在任何层级直接访问
interface ThemeContextValue {
  theme: string;
  setTheme: (theme: string) => void;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme必须在ThemeProvider内部');
  return context;
}

// 用法 - 无需属性传递
function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      切换 {theme} 主题
    </button>
  );
}

性能优化

1. React.memo - 防止重新渲染

// ❌ 每次父组件渲染时都重新渲染
function ExpensiveComponent({ data }: { data: string }) {
  console.log('渲染中...');
  return <div>{data}</div>;
}

// ✅ 仅在属性更改时重新渲染
const ExpensiveComponent = memo(function ExpensiveComponent({ 
  data 
}: { 
  data: string 
}) {
  console.log('渲染中...');
  return <div>{data}</div>;
});

// ✅ 自定义比较函数
const ExpensiveList = memo(
  function ExpensiveList({ items }: { items: Item[] }) {
    return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
  },
  (prevProps, nextProps) => {
    // 仅在项目数组长度更改时重新渲染
    return prevProps.items.length === nextProps.items.length;
  }
);

2. useMemo - 缓存昂贵计算

function ProductList({ products, filters }: { products: Product[]; filters: Filters }) {
  // ❌ 每次渲染都重新计算
  const filteredProducts = products.filter(p => matchesFilters(p, filters));
  
  // ✅ 仅在依赖项更改时重新计算
  const filteredProducts = useMemo(() => {
    return products.filter(p => matchesFilters(p, filters));
  }, [products, filters]);
  
  return (
    <div>
      {filteredProducts.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}

3. useCallback - 稳定的函数引用

function Parent() {
  const [count, setCount] = useState(0);
  
  // ❌ 每次渲染都创建新函数(导致子组件重新渲染)
  const handleClick = () => {
    console.log('点击');
  };
  
  // ✅ 稳定的函数引用
  const handleClick = useCallback(() => {
    console.log('点击');
  }, []); // 无依赖项 - 永不更改
  
  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <MemoizedChild onClick={handleClick} />
    </>
  );
}

const MemoizedChild = memo(function Child({ onClick }: { onClick: () => void }) {
  console.log('子组件渲染');
  return <button onClick={onClick}>点击我</button>;
});

4. 代码拆分与懒加载

// ✅ 懒加载重型组件
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<HeavyChart />} />
        <Route path="/admin" element={<AdminPanel />} />
      </Routes>
    </Suspense>
  );
}

无障碍模式

1. 语义化HTML与ARIA

// ✅ 无障碍按钮
function AccessibleButton({ label, onClick }: { label: string; onClick: () => void }) {
  return (
    <button
      type="button"
      onClick={onClick}
      aria-label={label}
    >
      {label}
    </button>
  );
}

// ✅ 无障碍模态框
function Modal({ isOpen, onClose, children }: { 
  isOpen: boolean; 
  onClose: () => void; 
  children: React.ReactNode; 
}) {
  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = 'hidden';
      // 焦点陷阱、ESC键处理等。
    }
    return () => {
      document.body.style.overflow = '';
    };
  }, [isOpen]);
  
  if (!isOpen) return null;
  
  return (
    <div
      role="dialog"
      aria-modal="true"
      className="modal-overlay"
      onClick={onClose}
    >
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        {children}
        <button onClick={onClose} aria-label="关闭模态框">×</button>
      </div>
    </div>
  );
}

// ✅ 无障碍表单
function SignupForm() {
  return (
    <form>
      <label htmlFor="email">邮箱</label>
      <input
        id="email"
        type="email"
        required
        aria-required="true"
        aria-describedby="email-error"
      />
      <span id="email-error" role="alert" className="error">
        请输入有效邮箱
      </span>
      
      <button type="submit">注册</button>
    </form>
  );
}

2. 键盘导航

// ✅ 键盘可访问下拉框
function Dropdown({ options }: { options: string[] }) {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(0);
  
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' || e.key === ' ') {
      setIsOpen(!isOpen);
    } else if (e.key === 'ArrowDown') {
      setSelectedIndex((i) => Math.min(i + 1, options.length - 1));
    } else if (e.key === 'ArrowUp') {
      setSelectedIndex((i) => Math.max(i - 1, 0));
    } else if (e.key === 'Escape') {
      setIsOpen(false);
    }
  };
  
  return (
    <div
      role="combobox"
      aria-expanded={isOpen}
      aria-controls="dropdown-options"
      tabIndex={0}
      onKeyDown={handleKeyDown}
    >
      <button onClick={() => setIsOpen(!isOpen)}>
        选择选项
      </button>
      
      {isOpen && (
        <ul id="dropdown-options" role="listbox">
          {options.map((opt, i) => (
            <li
              key={opt}
              role="option"
              aria-selected={i === selectedIndex}
              className={i === selectedIndex ? 'selected' : ''}
            >
              {opt}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

组件设计检查清单

结构:
□ 每个组件单一职责
□ 表现型与容器型分离
□ 正确的属性类型(TypeScript)
□ 定义默认属性
□ 关键输入进行属性验证

状态管理:
□ 状态在最低必要层级
□ 需要共享时提升状态
□ 使用上下文处理深层属性传递
□ 不突变属性
□ 表单使用受控组件

性能:
□ 对昂贵组件使用memo()
□ 对昂贵计算使用useMemo()
□ 对稳定回调使用useCallback()
□ 大型组件代码拆分
□ 路由懒加载

无障碍:
□ 语义化HTML元素
□ ARIA标签和角色
□ 键盘导航支持
□ 焦点管理
□ 屏幕阅读器测试

可重用性:
□ 通过属性可配置
□ 可组合使用子元素
□ 无硬编码值
□ 清晰、文档化的API
□ 提供示例用法

资源


记住:优秀组件应简单、可重用、无障碍且高性能。从简单开始,仅在需要时添加复杂性。