Monorepo 基础工具包设计:构建稳定、无业务耦合的公共底座
介绍 Monorepo 底层基础工具包,要求零业务耦合、单向依赖。拆分为 types、utils、http、storage 等多个子包,明确各包职责与边界,强调纯函数、强类型、Tree‑Shaking,规范目录、测试、构建发布,给出新增工具包的判定标准。
基础工具包位于共享依赖体系的底层,目标是提供跨应用、跨业务域都可以复用的运行时能力。
本文将基础工具包理解为“公共底座”,不讨论具体业务实现。
1. 基础工具包解决什么问题
随着前端项目规模扩大,日期处理、类型工具、请求封装、日志、存储、Cookie 等能力会不断重复出现。如果这些能力分散在不同应用中,通常会产生三个问题:
- 相同问题出现多套实现;
- 不同实现的行为和边界不一致;
- 公共能力修改后无法同步到所有应用。
基础工具包的职责,就是把这些无业务耦合的通用能力沉淀为稳定的共享 API。
原方案将基础工具包定义为 Monorepo 依赖层最底层的运行时共享包,并强调零业务耦合、单向依赖、纯函数优先、强类型和 Tree Shaking。fileciteturn1file0L14-L33
2. 基础工具包的五项设计原则
2.1 零业务耦合
基础工具不应该知道“用户”“订单”“支付”等业务概念。
例如:
// 可以
formatDate(date, 'YYYY-MM-DD')
// 不应该出现在基础 utils 中
formatUserRegistrationDate(user)后者已经包含业务语义,应由业务域包承担。
2.2 单向依赖
推荐依赖关系:
types
↓
constants
↓
utils
↓
hooks上层可以依赖下层,下层不能反向依赖业务包或应用。原方案采用 @types → @constants → @utils → @hooks 的基础依赖链。fileciteturn1file0L28-L37
2.3 纯函数优先
工具函数尽量做到:
- 输入决定输出;
- 不修改输入参数;
- 没有隐式全局状态;
- 没有不可预测的副作用。
这样可以降低测试成本,也方便在浏览器、Node.js 和其他运行环境中复用。
2.4 类型优先
TypeScript 类型不是附属文档,而是工具包公共 API 的一部分。
推荐:
export function groupBy<T, K extends PropertyKey>(
list: T[],
getKey: (item: T) => K,
): Record<K, T[]> {
// ...
}而不是:
export function groupBy(list: any[], fn: any): any {
// ...
}2.5 Tree Shaking 友好
工具包通常被大量应用依赖,因此应该尽量避免无关代码进入最终产物:
- 使用 ESM;
- 具名导出;
- 避免模块级副作用;
- 合理设置
sideEffects; - 避免把多个大型第三方库绑定到一个入口。
3. 推荐的基础工具包分层
一个可维护的基础工具体系可以拆成多个职责明确的包:
| 包 | 职责 | 运行时 |
|---|---|---|
@org/types | 通用类型 | 无 |
@org/constants | 通用常量和枚举 | 少量 |
@org/utils | 通用函数 | 有 |
@org/hooks | React 通用 Hooks | 有 |
@org/logger | 日志能力 | 有 |
@org/encryptor | 加解密与编码 | 有 |
@org/storage | Web Storage 封装 | 有 |
@org/cookie | Cookie / SSR Cookie | 有 |
@org/http | HTTP 请求抽象 | 有 |
原方案除了 types、constants、utils、hooks,还将 logger、encryptor、storage、cookie、http 等能力列为基础共享包。fileciteturn1file0L195-L209
4. types:统一类型契约
types 包的价值是避免各个项目重复定义通用结构。
典型内容包括:
PartialDeep、RequiredBy、PartialBy等工具类型;IDictionary;IResponse;- 分页结构;
- HTTP 方法联合类型。
例如:
export interface IDictionary<T = unknown> {
[key: string]: T
}
export interface IResponse<T = unknown> {
code: number
data: T
message: string
success: boolean
}这些类型属于跨业务的技术结构。如果一个类型只描述某个业务领域,例如 User、Order,就不应该放在 types 包,而应放到对应业务域包中。原方案明确区分通用结构和业务实体边界。fileciteturn1file7L314-L344
5. constants:消除魔法值
常量包适合沉淀真正跨业务共享的稳定值:
- HTTP 状态码;
- 通用错误码;
- 默认分页大小;
- 日期格式;
- 通用状态。
需要特别注意:业务状态码、业务产品配置和领域枚举不应该为了“复用”而全部塞进 constants。
Enum 还是 Union Type
如果只需要编译期类型约束,优先使用联合类型:
type CommonStatus = 0 | 1如果确实需要运行时对象或反向映射,再考虑 enum。原方案也指出联合类型没有运行时成本,而 enum 会产生运行时对象。fileciteturn0file1L184-L193
6. utils:最通用的运行时能力
utils 是基础工具体系中复用范围最广的包,原则上不绑定 React 或具体业务框架。
可以包含:
- 日期处理;
- 对象与数组处理;
- 字符串处理;
- URL 与 QueryString;
- 数值处理;
- Promise 辅助;
- 浏览器能力检测;
- 数据转换。
一个重要边界是:工具函数应该描述“技术问题”,而不是“业务问题”。
例如 formatCurrency 是否应该属于 utils,需要根据团队定义判断。如果它包含特定产品的货币规则、业务状态或展示策略,则应该进一步上移到业务域。
7. hooks:只放真正通用的 React 逻辑
Hooks 与 utils 的边界非常重要。
适合放在 @org/hooks 的能力包括:
useToggle;useBoolean;useDebounce;useThrottle;useTimeout;useInterval;useLocalStorage;useElementSize;useClickOutside。
不应该放入业务域 Hooks,例如 useCurrentUser、usePaymentStatus。原方案明确要求基础 Hooks 只能依赖基础工具和 React,不能依赖业务包。fileciteturn0file1L209-L235
8. logger:统一日志出口
日志包的核心不是“打印日志”,而是统一日志结构。
建议至少提供:
logger.debug()
logger.info()
logger.warn()
logger.error()以及模块化 logger:
const logger = createLogger('auth')这样可以统一:
- 日志级别;
- 环境过滤;
- 时间戳;
- scope;
- 上下文;
- 远程错误上报。
如果日志可能包含用户信息、Token 等敏感字段,应在日志入口进行过滤或脱敏。原方案明确禁止直接打印明文 Token、手机号、身份证等敏感信息。fileciteturn0file1L236-L250
9. encryptor:统一加密与编码能力
加解密包可以提供统一的:
- AES;
- RSA;
- SHA;
- MD5;
- Base64;
- Hex;
- 签名与验签。
关键原则是算法封装 ≠ 密钥管理。
密钥、IV、私钥等敏感材料不能硬编码到共享包源码中,应由运行环境、配置系统或安全密钥服务提供。原方案也特别强调密钥和 IV 不应硬编码。fileciteturn1file0L251-L264
10. storage 与 cookie
Storage
统一封装:
storage.set(key, value)
storage.get(key)
storage.remove(key)
storage.clear()可以进一步提供:
- JSON 序列化;
- TTL;
- 加密存储;
- 浏览器兼容处理。
但 Storage 并不适合存储大文件或大量 Base64 数据。原方案建议将此类场景交给 IndexedDB 或服务端。fileciteturn0file1L265-L280
Cookie
Cookie 包需要同时考虑浏览器与 SSR:
setCookie()
getCookie()
removeCookie()并支持 expires、maxAge、domain、path、secure、httpOnly、sameSite 等属性。敏感鉴权数据不应放进可被前端 JavaScript 读取的 Cookie。fileciteturn0file1L281-L294
11. http:请求能力的边界
HTTP 包可以统一:
- 请求方法;
- 超时;
- 重试;
- 请求头;
- 响应解析;
- 错误结构;
- 拦截器。
但 HTTP 层不应该知道“登录接口”“订单接口”这些业务语义。
业务域只负责把 HTTP 能力组合成领域 API:
@org/http
↓
@org/user-domain
↓
Application这样可以让请求基础设施和业务接口保持独立。
12. 标准目录结构
packages/utils/
├── src/
│ ├── index.ts
│ ├── array/
│ ├── object/
│ ├── string/
│ ├── date/
│ └── __tests__/
├── package.json
├── tsconfig.json
├── tsdown.config.ts
└── README.md公共 API 只从 index.ts 导出,内部模块可以随时重构。
13. 测试策略
基础包是整个 Monorepo 的底层依赖,因此测试应该比业务应用更加严格。
推荐:
- 公共 API 全覆盖;
- 边界条件优先;
- 异常输入必须测试;
- 浏览器能力需要考虑 SSR 环境;
- 核心基础包设置较高覆盖率目标。
原维护体系要求每个共享包都具备单元测试,并建议核心包覆盖率达到 80% 以上。fileciteturn1file2L83-L91
14. 构建与发布
共享包推荐同时输出 ESM、CJS 和类型声明:
{
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"sideEffects": false
}原维护方案采用 ESM + CJS + DTS 的输出方式,并建议通过 exports 明确入口。fileciteturn0file0L235-L247
Monorepo 内部依赖可以使用:
{
"dependencies": {
"@org/types": "workspace:*"
}
}这样可以让工作区内的依赖关系保持稳定,并由版本工具在发布阶段处理实际版本关系。
15. 依赖管理原则
严格区分:
dependencies:运行时需要;devDependencies:开发、测试、构建需要;peerDependencies:由宿主环境提供。
同时避免幽灵依赖:一个包实际使用的依赖必须在自己的 package.json 中声明,而不是依赖根目录提升带来的“碰巧可用”。原维护体系将 pnpm 的严格依赖能力作为防止幽灵依赖的重要手段。fileciteturn1file2L249-L258
16. 基础工具包的判断标准
当你准备新增一个工具时,可以问四个问题:
- 是否不包含具体业务语义?
- 是否至少被多个独立模块复用?
- 是否可以定义稳定、清晰的公共 API?
- 是否能够独立测试?
如果答案大部分是否定的,那么它很可能不应该成为基础工具包。
17. 总结
基础工具包的目标不是“把所有公共代码都收进去”,而是建立一个足够小、足够稳定、足够通用的技术底座。
越靠近底层,越应该克制;越通用的 API,越需要稳定。