⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

3.2.6 积木该多大才合适——组件设计

一句话破题

好的组件像乐高积木——足够小可以灵活组合,足够完整可以独立使用。

核心价值

组件设计决定了代码的可维护性。设计不当的组件要么过于庞大难以修改,要么过于碎片化难以理解。

单一职责原则

一个组件应该只有一个变更的理由。

tsx
// 错误:一个组件承担太多职责
function UserDashboard() {
  const [user, setUser] = useState(null)
  const [posts, setPosts] = useState([])
  const [notifications, setNotifications] = useState([])
  
  // 获取用户数据
  // 获取帖子数据
  // 获取通知数据
  // 处理用户操作
  // 渲染所有内容...
  
  return (/* 数百行 JSX */)
}

// 正确:拆分为多个专注的组件
function UserDashboard() {
  return (
    <div>
      <UserProfile />
      <UserPosts />
      <NotificationPanel />
    </div>
  )
}

展示组件 vs 容器组件

类型职责特点
展示组件渲染 UI接收 Props,无状态逻辑
容器组件管理数据处理 state、effects、数据获取
tsx
// 展示组件:只负责渲染
interface UserCardProps {
  name: string
  avatar: string
  email: string
}

function UserCard({ name, avatar, email }: UserCardProps) {
  return (
    <div className="card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
}

// 容器组件:负责数据
function UserCardContainer({ userId }: { userId: string }) {
  const { data: user, loading } = useFetch(`/api/users/${userId}`)
  
  if (loading) return <Skeleton />
  return <UserCard {...user} />
}

组合模式

通过 children 和插槽实现灵活组合:

tsx
// 基础 Card 组件
interface CardProps {
  children: React.ReactNode
  header?: React.ReactNode
  footer?: React.ReactNode
}

function Card({ children, header, footer }: CardProps) {
  return (
    <div className="card">
      {header && <div className="card-header">{header}</div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  )
}

// 灵活使用
<Card
  header={<h2>用户信息</h2>}
  footer={<button>编辑</button>}
>
  <p>姓名:张三</p>
  <p>邮箱:zhang@example.com</p>
</Card>

复合组件模式

对于复杂 UI,将相关组件组合在一起:

tsx
// 定义复合组件
const Tabs = ({ children, defaultValue }: TabsProps) => {
  const [activeTab, setActiveTab] = useState(defaultValue)
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  )
}

Tabs.List = function TabsList({ children }) {
  return <div className="tabs-list">{children}</div>
}

Tabs.Tab = function Tab({ value, children }) {
  const { activeTab, setActiveTab } = useTabsContext()
  return (
    <button 
      className={activeTab === value ? 'active' : ''}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  )
}

Tabs.Panel = function TabPanel({ value, children }) {
  const { activeTab } = useTabsContext()
  if (activeTab !== value) return null
  return <div>{children}</div>
}

// 使用
<Tabs defaultValue="tab1">
  <Tabs.List>
    <Tabs.Tab value="tab1">标签1</Tabs.Tab>
    <Tabs.Tab value="tab2">标签2</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="tab1">内容1</Tabs.Panel>
  <Tabs.Panel value="tab2">内容2</Tabs.Panel>
</Tabs>

组件拆分时机

需要拆分的信号:

  1. 组件超过 200 行代码
  2. 有明显独立的 UI 区块
  3. 逻辑可以被复用
  4. 需要独立测试某部分
  5. 多人协作时需要减少冲突

不需要拆分的情况:

  1. 拆分后只在一处使用
  2. 拆分导致 Props 传递更复杂
  3. 拆分后反而难以理解

文件组织

components/
├── ui/                    # 通用 UI 组件
│   ├── Button.tsx
│   ├── Card.tsx
│   └── Input.tsx
├── features/              # 业务组件
│   ├── UserProfile/
│   │   ├── index.tsx
│   │   ├── UserAvatar.tsx
│   │   └── UserStats.tsx
│   └── PostList/
│       ├── index.tsx
│       ├── PostCard.tsx
│       └── PostActions.tsx
└── layouts/               # 布局组件
    ├── Header.tsx
    └── Sidebar.tsx

AI 协作指南

核心意图:让 AI 帮你分析和重构组件结构。

需求定义公式

  • 功能描述:这个组件目前 [做了什么]
  • 交互方式:其中 [哪些部分] 是独立的
  • 预期效果:希望拆分后 [达到什么效果]

关键术语:单一职责、展示组件、容器组件、复合组件、组合模式

交互策略

  1. 把现有大组件展示给 AI
  2. 让它分析可拆分的边界
  3. 讨论拆分方案的利弊
  4. 逐步重构

避坑指南

  1. 不要为了拆分而拆分:过度拆分增加理解成本
  2. 保持命名一致UserCardUserCardContaineruseUserCard
  3. 考虑组件的消费者:API 设计要符合直觉
  4. 避免 Props 爆炸:超过 7-8 个 Props 考虑重新设计

验收清单

  • [ ] 每个组件有清晰的单一职责
  • [ ] 展示逻辑和业务逻辑适当分离
  • [ ] 使用组合模式实现灵活性
  • [ ] 文件组织清晰合理