3、组件分类:基础组件、业务组件、布局组件、容器组件、高阶组件

做前端这么多年,我见过不少团队在组件化上栽跟头。问题出在哪?说白了,就是分类没做好。你想想看,一个项目里几百个组件,如果全混在一起,维护起来简直是一场噩梦。

我个人习惯,先把组件分成五类。这五类就像乐高积木的不同模块,各有各的职责。咱们一个一个来看。

3.1 基础组件

基础组件,也叫通用组件或原子组件。它们是系统里最底层、最通用的积木块。

特点:

  • 不依赖业务数据
  • 只通过 props 接收数据
  • 可在任何项目复用

举个例子,一个按钮组件:

// Button.jsx
function Button({ children, type = 'default', disabled = false, onClick }) {
  return (
    <button
      className={`btn btn-${type}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

我在项目中遇到过,有人把业务逻辑直接写进基础组件里。比如在 Button 里判断用户权限。嗯,这其实是个坑。基础组件就该保持纯粹,只做 UI 展示和交互反馈。

避坑指南:我曾经接手过一个项目,基础组件里混入了大量业务判断。结果换个项目想复用,发现根本拆不开。记住,基础组件应该是「无状态」的,或者只有 UI 状态。

3.2 业务组件

业务组件,就是和具体业务绑定的组件。它们通常由多个基础组件组合而成,并且会调用业务接口。

特点:

  • 依赖特定业务数据
  • 通常包含状态管理
  • 复用范围限于当前业务域

比如一个用户信息卡片:

// UserCard.jsx
function UserCard({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  if (!user) return <Loading />;

  return (
    <div className="user-card">
      <Avatar src={user.avatar} />
      <UserName name={user.name} />
      <UserRole role={user.role} />
    </div>
  );
}

你看,这里用了 Avatar、UserName 这些基础组件,但整体逻辑是围绕「用户」这个业务实体展开的。业务组件的好处是,同一个业务场景下,你不需要重复写数据获取和状态管理的代码。

我的建议:业务组件可以适当「胖」一点,但别太胖。如果一个业务组件超过 300 行,就该考虑拆分了。我一般会按「数据获取」、「数据处理」、「UI 渲染」三个维度来拆分。

3.3 布局组件

布局组件,负责页面的结构划分。它们不关心具体内容,只关心「东西放哪」。

常见布局组件:

  • Header / Footer
  • Sidebar / MainContent
  • Grid / FlexContainer
  • Card / Panel

举个例子:

// PageLayout.jsx
function PageLayout({ header, sidebar, main }) {
  return (
    <div className="page-layout">
      <header>{header}</header>
      <div className="content">
        <aside>{sidebar}</aside>
        <main>{main}</main>
      </div>
    </div>
  );
}

布局组件有个关键点:它们通常使用 children 或具名插槽来接收内容。这样布局和内容就彻底解耦了。

核心原则:布局组件只负责「怎么摆」,不负责「摆什么」。如果你在布局组件里看到了业务数据,那八成是设计出了问题。

3.4 容器组件

容器组件,也叫智能组件。它们负责数据管理和业务逻辑,然后把数据传给展示组件。

和展示组件的区别:

维度 容器组件 展示组件
数据来源 状态管理 / API props
交互逻辑 处理事件 触发回调
复用性
测试难度 较高 容易

来看个实际例子:

// UserListContainer.jsx
function UserListContainer() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUsers()
      .then(data => setUsers(data))
      .finally(() => setLoading(false));
  }, []);

  const handleDelete = async (id) => {
    await deleteUser(id);
    setUsers(prev => prev.filter(u => u.id !== id));
  };

  return (
    <UserList
      users={users}
      loading={loading}
      onDelete={handleDelete}
    />
  );
}

容器组件把数据获取和状态管理都包起来了,展示组件只管渲染。这种分离方式,我在大型项目中屡试不爽。测试起来也方便——容器组件测逻辑,展示组件测 UI。

3.5 高阶组件

高阶组件(HOC),本质上是一个函数。它接收一个组件,返回一个新组件。说白了,就是给组件「加料」的。

常见用途:

  • 权限校验
  • 日志记录
  • 数据预加载
  • 样式注入

举个权限校验的例子:

// withAuth.jsx
function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const { user } = useAuth();

    if (!user) {
      return <Redirect to="/login" />;
    }

    return <WrappedComponent {...props} user={user} />;
  };
}

// 使用
const AdminPanelWithAuth = withAuth(AdminPanel);

我记得刚开始用 HOC 时,总觉得它很玄乎。后来理解了,其实就是「组件工厂」——你给我一个组件,我加工一下,还你一个更强的组件。

注意:HOC 虽然强大,但别滥用。多层嵌套的 HOC 会让调试变得困难。我个人建议,超过三层嵌套就考虑用 hooks 替代。React 16.8 之后,很多 HOC 的场景都可以用自定义 hooks 实现,代码更清晰。

3.6 如何选择?

说了这么多,实际开发中怎么选?我一般按这个思路来:

  1. 先看复用范围:全局通用?选基础组件。某个业务域内通用?选业务组件。
  2. 再看职责:只负责结构?布局组件。负责数据和逻辑?容器组件。
  3. 最后看扩展方式:需要给现有组件加能力?考虑 HOC 或 hooks。

其实没有绝对的分类标准。同一个组件,在不同项目里可能分属不同类别。关键是团队内部达成共识,保持一致性。

一个小技巧:我习惯在组件文件头部加注释,标明组件类型。比如 // @category: base。这样后期维护时,一眼就能知道这个组件的定位。

组件分类这件事,说白了就是给代码「分家」。分得好,项目越做越清爽;分得乱,越做越痛苦。希望这五种分类方式,能帮你理清思路。