3、组件库架构设计:目录结构规划、组件分类策略、按需加载设计、主题系统设计思路

好,咱们进入第三章节。说实话,很多同学做组件库,上来就写代码。结果写到一半,目录乱成一锅粥,组件之间互相依赖,改一个按钮要重新打包整个库。嗯,我早期踩过这个坑,后来才明白——架构设计才是组件库的骨架。骨架歪了,后面怎么长都是歪的。

这一章,我会把四个核心设计点掰开揉碎讲清楚:目录怎么规划、组件怎么分类、怎么实现按需加载、主题系统怎么设计。都是我在真实项目中沉淀下来的经验。

3.1 目录结构规划:别让文件找文件

我个人习惯,组件库的根目录一定要清晰。你想想看,一个库可能有几十上百个组件,如果目录结构混乱,新同事来了根本不知道往哪放代码。

我推荐这种分层结构:

my-component-library/
├── packages/              # 核心包
│   ├── components/        # 组件源码
│   │   ├── button/
│   │   │   ├── index.tsx
│   │   │   ├── style.css
│   │   │   └── types.ts
│   │   ├── input/
│   │   └── ...
│   ├── theme/             # 主题系统
│   │   ├── tokens.ts
│   │   └── mixins.ts
│   └── utils/             # 工具函数
├── docs/                  # 文档
├── scripts/               # 构建脚本
├── test/                  # 测试
└── package.json

这里有个关键点:每个组件独立一个文件夹。为什么?因为后续做按需加载时,每个组件就是一个独立的入口。我在项目中遇到过,有人把所有组件塞到一个文件里,结果按需加载根本没法做,只能全量引入。

核心原则:组件之间不要互相引用。如果两个组件有公共逻辑,抽到 utils 里。否则,你改一个组件,会牵连一片。

3.2 组件分类策略:分而治之

组件不是随便堆的。我一般把它们分成三类:

分类 说明 例子
基础组件 原子级,不依赖业务 Button、Input、Icon
复合组件 由基础组件组合而成 Form、Table、Modal
业务组件 带业务逻辑,可复用 UserPicker、ChartCard

为什么要这么分?说白了,基础组件要稳定,业务组件要灵活。我曾经在一个项目里,把业务逻辑直接写进了 Button 组件。结果换了个项目,Button 就废了,还得重写。嗯,从那以后,我坚持基础组件只做 UI,不做业务。

小技巧:基础组件的 props 尽量少,10 个以内最好。业务组件可以多一些,但要有默认值。

3.3 按需加载设计:别让用户全量引入

这是个大问题。很多组件库,用户只用了 Button 和 Input,结果打包出来几百 KB。为什么?因为全量引入了。

按需加载的核心思路是:每个组件独立打包,用户只引入需要的。我常用的方案有两种:

  1. ES Module 方式:每个组件导出独立的模块,配合 tree-shaking。
  2. babel-plugin-import:自动转换 import 语句,按需加载。

举个例子,用户这样写:

import { Button } from 'my-lib';

经过 babel 插件转换后,变成:

import Button from 'my-lib/es/button';
import 'my-lib/es/button/style.css';

你看,只加载了 Button 的代码和样式。其他组件根本没进包。

注意:按需加载要求每个组件必须有独立的样式文件。如果所有样式写在一个文件里,按需加载就失效了。我见过有人把样式全写在 index.css 里,结果按需加载后样式全乱了。

另外,组件之间不要有循环依赖。比如 A 组件引用了 B,B 又引用了 A。这种在按需加载时会导致死循环。我曾经排查过这种问题,花了两天时间才找到原因——两个工具函数互相调用了。

3.4 主题系统设计思路:让用户自定义

主题系统,说白了就是让用户能换皮肤。我常用的方案是 CSS 变量 + Token 系统

先定义 Token:

// tokens.ts
export const lightTokens = {
  colorPrimary: '#1890ff',
  colorBg: '#ffffff',
  fontSize: '14px',
  borderRadius: '4px',
};

export const darkTokens = {
  colorPrimary: '#177ddc',
  colorBg: '#141414',
  fontSize: '14px',
  borderRadius: '4px',
};

然后生成 CSS 变量:

// 生成 CSS 变量
:root {
  --color-primary: #1890ff;
  --color-bg: #ffffff;
  --font-size: 14px;
  --border-radius: 4px;
}

[data-theme="dark"] {
  --color-primary: #177ddc;
  --color-bg: #141414;
}

组件里直接用变量:

.button {
  background-color: var(--color-primary);
  font-size: var(--font-size);
  border-radius: var(--border-radius);
}

这样用户只需要切换 data-theme 属性,整个组件库的样式就变了。我在项目中遇到过,用户想自定义主题,但我们的组件库用了硬编码颜色。结果用户只能自己覆盖样式,维护起来特别痛苦。后来我改成 CSS 变量方案,用户只需要改几个 Token 值就行。

核心思路:Token 是设计语言,CSS 变量是技术实现。两者分离,用户改 Token 不影响组件逻辑。

另外,主题系统要支持嵌套。比如某个页面用暗色主题,其他页面用亮色主题。CSS 变量天然支持作用域,只要在对应 DOM 节点上设置 data-theme 就行。

3.5 总结一下

这一章的内容,说白了就是四个字:先想后做。目录结构、组件分类、按需加载、主题系统,这些都是在写第一行代码前就要想清楚的。我见过太多项目,前期图省事,后期重构到崩溃。

嗯,下一章我们会进入具体的组件开发环节。到时候我会手把手带你写一个 Button 组件,从设计到实现,再到测试。敬请期待。

课后思考:如果你的组件库要支持多个主题(比如 10 个以上),Token 系统该怎么设计?欢迎在评论区讨论。