我的文档库

Monorepo 基础工具包设计:构建稳定、无业务耦合的公共底座

介绍 Monorepo 底层基础工具包,要求零业务耦合、单向依赖。拆分为 types、utils、http、storage 等多个子包,明确各包职责与边界,强调纯函数、强类型、Tree‑Shaking,规范目录、测试、构建发布,给出新增工具包的判定标准。

基础工具包位于共享依赖体系的底层,目标是提供跨应用、跨业务域都可以复用的运行时能力。

本文将基础工具包理解为“公共底座”,不讨论具体业务实现。

1. 基础工具包解决什么问题

随着前端项目规模扩大,日期处理、类型工具、请求封装、日志、存储、Cookie 等能力会不断重复出现。如果这些能力分散在不同应用中,通常会产生三个问题:

  • 相同问题出现多套实现;
  • 不同实现的行为和边界不一致;
  • 公共能力修改后无法同步到所有应用。

基础工具包的职责,就是把这些无业务耦合的通用能力沉淀为稳定的共享 API。

原方案将基础工具包定义为 Monorepo 依赖层最底层的运行时共享包,并强调零业务耦合、单向依赖、纯函数优先、强类型和 Tree Shaking。fileciteturn1file0L14-L33

2. 基础工具包的五项设计原则

2.1 零业务耦合

基础工具不应该知道“用户”“订单”“支付”等业务概念。

例如:

// 可以
formatDate(date, 'YYYY-MM-DD')

// 不应该出现在基础 utils 中
formatUserRegistrationDate(user)

后者已经包含业务语义,应由业务域包承担。

2.2 单向依赖

推荐依赖关系:

types

constants

utils

hooks

上层可以依赖下层,下层不能反向依赖业务包或应用。原方案采用 @types → @constants → @utils → @hooks 的基础依赖链。fileciteturn1file0L28-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/hooksReact 通用 Hooks
@org/logger日志能力
@org/encryptor加解密与编码
@org/storageWeb Storage 封装
@org/cookieCookie / SSR Cookie
@org/httpHTTP 请求抽象

原方案除了 types、constants、utils、hooks,还将 logger、encryptor、storage、cookie、http 等能力列为基础共享包。fileciteturn1file0L195-L209

4. types:统一类型契约

types 包的价值是避免各个项目重复定义通用结构。

典型内容包括:

  • PartialDeepRequiredByPartialBy 等工具类型;
  • IDictionary
  • IResponse
  • 分页结构;
  • HTTP 方法联合类型。

例如:

export interface IDictionary<T = unknown> {
  [key: string]: T
}

export interface IResponse<T = unknown> {
  code: number
  data: T
  message: string
  success: boolean
}

这些类型属于跨业务的技术结构。如果一个类型只描述某个业务领域,例如 UserOrder,就不应该放在 types 包,而应放到对应业务域包中。原方案明确区分通用结构和业务实体边界。fileciteturn1file7L314-L344

5. constants:消除魔法值

常量包适合沉淀真正跨业务共享的稳定值:

  • HTTP 状态码;
  • 通用错误码;
  • 默认分页大小;
  • 日期格式;
  • 通用状态。

需要特别注意:业务状态码、业务产品配置和领域枚举不应该为了“复用”而全部塞进 constants。

Enum 还是 Union Type

如果只需要编译期类型约束,优先使用联合类型:

type CommonStatus = 0 | 1

如果确实需要运行时对象或反向映射,再考虑 enum。原方案也指出联合类型没有运行时成本,而 enum 会产生运行时对象。fileciteturn0file1L184-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,例如 useCurrentUserusePaymentStatus。原方案明确要求基础 Hooks 只能依赖基础工具和 React,不能依赖业务包。fileciteturn0file1L209-L235

8. logger:统一日志出口

日志包的核心不是“打印日志”,而是统一日志结构。

建议至少提供:

logger.debug()
logger.info()
logger.warn()
logger.error()

以及模块化 logger:

const logger = createLogger('auth')

这样可以统一:

  • 日志级别;
  • 环境过滤;
  • 时间戳;
  • scope;
  • 上下文;
  • 远程错误上报。

如果日志可能包含用户信息、Token 等敏感字段,应在日志入口进行过滤或脱敏。原方案明确禁止直接打印明文 Token、手机号、身份证等敏感信息。fileciteturn0file1L236-L250

9. encryptor:统一加密与编码能力

加解密包可以提供统一的:

  • AES;
  • RSA;
  • SHA;
  • MD5;
  • Base64;
  • Hex;
  • 签名与验签。

关键原则是算法封装 ≠ 密钥管理

密钥、IV、私钥等敏感材料不能硬编码到共享包源码中,应由运行环境、配置系统或安全密钥服务提供。原方案也特别强调密钥和 IV 不应硬编码。fileciteturn1file0L251-L264

10. storagecookie

Storage

统一封装:

storage.set(key, value)
storage.get(key)
storage.remove(key)
storage.clear()

可以进一步提供:

  • JSON 序列化;
  • TTL;
  • 加密存储;
  • 浏览器兼容处理。

但 Storage 并不适合存储大文件或大量 Base64 数据。原方案建议将此类场景交给 IndexedDB 或服务端。fileciteturn0file1L265-L280

Cookie 包需要同时考虑浏览器与 SSR:

setCookie()
getCookie()
removeCookie()

并支持 expiresmaxAgedomainpathsecurehttpOnlysameSite 等属性。敏感鉴权数据不应放进可被前端 JavaScript 读取的 Cookie。fileciteturn0file1L281-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 的底层依赖,因此测试应该比业务应用更加严格。

推荐:

  1. 公共 API 全覆盖;
  2. 边界条件优先;
  3. 异常输入必须测试;
  4. 浏览器能力需要考虑 SSR 环境;
  5. 核心基础包设置较高覆盖率目标。

原维护体系要求每个共享包都具备单元测试,并建议核心包覆盖率达到 80% 以上。fileciteturn1file2L83-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 明确入口。fileciteturn0file0L235-L247

Monorepo 内部依赖可以使用:

{
  "dependencies": {
    "@org/types": "workspace:*"
  }
}

这样可以让工作区内的依赖关系保持稳定,并由版本工具在发布阶段处理实际版本关系。

15. 依赖管理原则

严格区分:

  • dependencies:运行时需要;
  • devDependencies:开发、测试、构建需要;
  • peerDependencies:由宿主环境提供。

同时避免幽灵依赖:一个包实际使用的依赖必须在自己的 package.json 中声明,而不是依赖根目录提升带来的“碰巧可用”。原维护体系将 pnpm 的严格依赖能力作为防止幽灵依赖的重要手段。fileciteturn1file2L249-L258

16. 基础工具包的判断标准

当你准备新增一个工具时,可以问四个问题:

  1. 是否不包含具体业务语义?
  2. 是否至少被多个独立模块复用?
  3. 是否可以定义稳定、清晰的公共 API?
  4. 是否能够独立测试?

如果答案大部分是否定的,那么它很可能不应该成为基础工具包。

17. 总结

基础工具包的目标不是“把所有公共代码都收进去”,而是建立一个足够小、足够稳定、足够通用的技术底座。

越靠近底层,越应该克制;越通用的 API,越需要稳定。

本页目录