3、目录结构设计:组件库的目录规范、src目录划分、构建输出目录规划

说实话,目录结构这事儿,看着不起眼,但踩过的坑是真不少。

我记得刚带团队那会儿,有个同事把组件和工具函数全塞在一个文件夹里。三个月后,项目膨胀到连他自己都找不到文件在哪儿。嗯,那场面,挺尴尬的。

所以这一章,我想跟你聊聊我这些年沉淀下来的目录设计思路。说白了,就是让你少走弯路。

3.1 顶层目录:一眼看清项目骨架

一个组件库的根目录,应该像人的骨架一样清晰。我个人习惯这样组织:

my-component-library/
├── packages/          # 多包管理(如果采用monorepo)
├── src/               # 源码目录
├── dist/              # 构建输出
├── docs/              # 文档
├── scripts/           # 构建脚本
├── tests/             # 测试
├── .eslintrc.js       # 代码规范
├── .prettierrc        # 格式化配置
├── tsconfig.json      # TypeScript配置
├── package.json
└── README.md

你可能会问:为什么要把 scripts 单独拎出来?

我在项目中遇到过,有人把构建逻辑写在 package.json 里,结果脚本越来越长,最后根本没法维护。单独放一个文件夹,每个脚本职责清晰,想改哪里直接定位。

我的小习惯: 根目录下只放配置文件,业务代码全部进子目录。这样你打开项目第一眼,看到的就是配置,而不是一堆代码。

3.2 src目录划分:组件库的心脏

src 目录是组件库的核心。怎么划分,直接决定了后续的可维护性。

我推荐这种结构:

src/
├── components/        # 组件目录
│   ├── Button/
│   ├── Input/
│   └── index.ts       # 统一导出
├── hooks/             # 自定义hooks
├── utils/             # 工具函数
├── styles/            # 全局样式
├── types/             # 类型定义
├── locale/            # 国际化
└── index.ts           # 入口文件

3.2.1 components:每个组件一个文件夹

为什么每个组件要单独一个文件夹?

你想想看,一个成熟的组件,除了核心代码,还有样式、测试、类型、文档。如果全堆在一个文件里,那画面太美我不敢看。

以 Button 组件为例:

components/Button/
├── index.tsx          # 组件实现
├── style.ts           # 样式(如果用CSS-in-JS)
├── Button.test.tsx    # 单元测试
├── index.md           # 组件文档
└── types.ts           # 类型定义
核心原则: 每个组件文件夹,就是一个独立的模块。它可以被单独引用、单独测试、单独发布。

3.2.2 hooks:逻辑复用的秘密武器

我见过很多新手,把逻辑直接写在组件里。结果一个组件上千行,改一个功能要翻半天。

其实,把可复用的逻辑抽成 hooks,是性价比最高的优化。比如:

hooks/
├── useClickOutside.ts   # 点击外部检测
├── useDebounce.ts       # 防抖
├── useEventListener.ts  # 事件监听
└── index.ts

我曾经在一个项目中,把表单校验逻辑抽成 useFormValidator,结果后面三个页面直接复用,省了至少两天工时。

3.2.3 utils:纯函数的天堂

工具函数,说白了就是那些不依赖 React 的纯函数。比如日期格式化、classNames 合并、DOM 操作辅助。

utils/
├── classNames.ts      # 类名合并
├── dom.ts             # DOM操作
├── format.ts          # 格式化
└── index.ts
注意: 不要把业务逻辑塞进 utils。我曾经见过有人把 API 请求也放进来,结果 utils 变成了垃圾堆。保持 utils 的纯粹性,它只做一件事:输入 -> 处理 -> 输出。

3.3 构建输出目录规划:让用户用得舒服

构建输出,是用户直接使用的部分。规划得好,用户开心;规划得乱,用户骂娘。

我建议的输出结构:

dist/
├── es/                # ES Module 格式
│   ├── Button/
│   ├── Input/
│   └── index.js
├── lib/               # CommonJS 格式
│   ├── Button/
│   ├── Input/
│   └── index.js
├── types/             # TypeScript 类型声明
│   ├── Button/
│   └── index.d.ts
└── index.css          # 全局样式

3.3.1 为什么需要两种模块格式?

这个问题我经常被问到。

简单说:es 给现代打包工具(如 Vite、Webpack 5)用,支持 tree-shaking;lib 给 Node.js 环境或旧版打包工具用。

package.json 中这样配置:

{
  "main": "dist/lib/index.js",
  "module": "dist/es/index.js",
  "types": "dist/types/index.d.ts"
}

3.3.2 按需加载:别让用户全量引入

我记得有个用户反馈,说引入一个 Button,结果打包出来 200KB。一查,原来是全量引入了所有组件。

解决方案很简单:每个组件单独导出,同时提供 es 目录下的路径。

// 用户按需引入
import Button from 'my-lib/es/Button'
避坑指南: 我曾经因为忘记在 package.json 中配置 sideEffects,导致样式被 tree-shaking 干掉。记得加上 "sideEffects": ["*.css"]

3.4 目录规范总结

说了这么多,我整理了一张表,方便你对照:

目录 用途 注意事项
src/components 组件实现 每个组件独立文件夹
src/hooks 逻辑复用 保持单一职责
src/utils 工具函数 不要混入业务逻辑
dist/es ES Module 输出 支持 tree-shaking
dist/lib CommonJS 输出 兼容 Node.js
dist/types 类型声明 必须与源码同步

嗯,目录结构这块,其实没有绝对的标准。但有一条铁律:让新人看一眼就能找到文件,让机器一眼就能识别格式。

你按照这个思路去设计,至少能保证项目在半年后,你自己还能看得懂。

最后说一句: 目录结构是组件库的「基础设施」。前期花半小时规划好,后期能省下无数个加班的夜晚。别问我怎么知道的,都是泪。