Skip to content

Monorepo 概述与方案对比 ​

Monorepo 的本质是用仓库结构换取协作效率:跨包改动能一个 PR 提交、公共代码零成本复用。 代价是工具链复杂度和仓库膨胀 —— 所以要先想清楚「你的痛点是不是多仓带来的」。

一、该不该用 ​

你的情况选
一个前端应用 + 一个后端 + 一个共享类型包✅ 小型 Monorepo
多个业务应用共享组件库、工具库✅ 典型场景
每个包有独立团队、独立发布节奏、权限要求严格⚠️ 多仓更合适
只有一个应用❌ 单仓即可,别为拆而拆

优势 vs 劣势 ​

MonorepoMultiRepo
代码复用✅ 跨项目共享零成本❌ 要发 npm 包、走版本
原子提交✅ 跨包修改一个 PR❌ 多仓协同、顺序发布
依赖管理✅ 公共依赖提升、省磁盘❌ 重复安装
构建速度❌ 全量构建慢(靠缓存解决)✅ 各自独立构建最快
权限控制❌ 代码全可见✅ 可精确到仓库
工具链成本❌ 要配 workspace + 缓存 + 发布✅ 简单直接

二、方案对比 ​

方案包管理构建缓存任务编排适用
pnpm workspacepnpm无无(只有 -r 批量)小型 Monorepo
Turborepo任意✅ 本地 + 远程✅ 依赖图 + 并行中大型
Nx任意✅ 本地 + 远程✅ 依赖图 + 插件生态大型、多技术栈
Rush内置 pnpm✅✅微软生态
Lerna任意❌(已不维护构建)✅ 批量发布存量项目

建议:pnpm workspace 打底,需要缓存和任务编排就加 Turborepo。Nx 功能强但绑定深,小团队容易背不动。

三、pnpm workspace 最小配置 ​

yaml
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
json
// apps/web/package.json —— 用 workspace: 协议引用内部包
{
  "dependencies": {
    "@my/ui": "workspace:*",
    "@my/utils": "workspace:^"
  }
}
// apps/web/package.json —— 用 workspace: 协议引用内部包
{
  "dependencies": {
    "@my/ui": "workspace:*",
    "@my/utils": "workspace:^"
  }
}
bash
pnpm -r build                  # 所有包依次构建
pnpm --filter @my/web dev      # 只跑某个包
pnpm --filter @my/web... build # 该包及其依赖
pnpm --filter ...@my/ui build  # 依赖该包的所有包
pnpm -r build                  # 所有包依次构建
pnpm --filter @my/web dev      # 只跑某个包
pnpm --filter @my/web... build # 该包及其依赖
pnpm --filter ...@my/ui build  # 依赖该包的所有包

workspace: 协议的价值:不经过 npm registry,直接软链到本地源码,改完立即生效。

四、Turborepo:任务管道与缓存 ​

json
// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],        // 先构建依赖包
      "outputs": ["dist/**"]
    },
    "test": { "dependsOn": ["build"] },
    "lint": {},
    "dev": { "cache": false, "persistent": true }
  }
}
// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],        // 先构建依赖包
      "outputs": ["dist/**"]
    },
    "test": { "dependsOn": ["build"] },
    "lint": {},
    "dev": { "cache": false, "persistent": true }
  }
}
bash
turbo run build --filter=web...
turbo run build --filter=web...

缓存的关键:Turbo 用「输入文件 + 环境变量 + 依赖图」的哈希做 key。同样的输入直接复用上次产物(命中会显示 FULL TURBO)。

缓存类型说明
本地缓存默认,存在 .turbo/cache
远程缓存团队共享(Vercel / 自建),CI 直接命中

要注意:环境变量变了必须体现在哈希里(env / globalEnv 配置),否则会拿到「用旧变量构建的产物」—— 这是最隐蔽的缓存坑。

五、版本与发布:changesets ​

bash
pnpm add -Dw @changesets/cli
pnpm changeset            # 交互式选择改动的包与版本类型
pnpm changeset version    # 按 changeset 升版本、写 CHANGELOG
pnpm changeset publish    # 发布到 npm
pnpm add -Dw @changesets/cli
pnpm changeset            # 交互式选择改动的包与版本类型
pnpm changeset version    # 按 changeset 升版本、写 CHANGELOG
pnpm changeset publish    # 发布到 npm
版本策略说明适用
fixed(统一版本)所有包同版本号,一起发强关联的包(如 React 生态)
independent(独立版本)各包按需升版松耦合的工具库

六、四个常见坑 ​

1. 幽灵依赖(Phantom Dependency) ​

# ❌ npm/yarn 的提升(hoist)让没声明的包也能 require
import lodash from 'lodash'   // package.json 里根本没有它,但能跑

# ✅ pnpm 的严格 node_modules:没声明就解析不到
# ❌ npm/yarn 的提升(hoist)让没声明的包也能 require
import lodash from 'lodash'   // package.json 里根本没有它,但能跑

# ✅ pnpm 的严格 node_modules:没声明就解析不到

解法:用 pnpm 的严格模式;真的需要提升时显式写进 public-hoist-pattern。

2. 循环依赖 ​

packages/a 依赖 packages/b,b 又依赖 a —— 构建会死循环或产物不完整。

解法:把公共部分抽到第三个包(分层:utils ← ui ← apps)。

3. CI 全量构建 ​

每次 PR 都构建所有包,CI 越来越慢。

解法:Turborepo 远程缓存 + --filter=...[origin/main] 只构建受影响的包。

4. 发布到 npm 时带上了 workspace 协议 ​

"@my/ui": "workspace:*" 直接发布,外部用户装不上。

解法:用 pnpm publish(会自动替换 workspace 协议),或用 changesets 的 publish 流程。

七、选型决策树 ​

包之间有共享代码吗?
├─ 没有 → 多仓,别硬凑
└─ 有
   ├─ 包数量 < 5,只在本地开发 → pnpm workspace 足够
   ├─ 需要「只构建受影响的包」+ 团队缓存 → 加 Turborepo
   ├─ 多技术栈(前端 + Node 服务 + 移动端)+ 强约束 → Nx
   └─ 存量 Lerna 项目 → Lerna + pnpm,逐步迁 changesets
包之间有共享代码吗?
├─ 没有 → 多仓,别硬凑
└─ 有
   ├─ 包数量 < 5,只在本地开发 → pnpm workspace 足够
   ├─ 需要「只构建受影响的包」+ 团队缓存 → 加 Turborepo
   ├─ 多技术栈(前端 + Node 服务 + 移动端)+ 强约束 → Nx
   └─ 存量 Lerna 项目 → Lerna + pnpm,逐步迁 changesets

八、速查 ​

问题答案
什么时候该用 Monorepo包之间有共享代码、需要原子提交
什么时候别用单应用、团队间要强权限隔离
内部包怎么引用workspace:* 协议
只构建受影响的包Turbo 的 --filter=...[origin/main]
幽灵依赖怎么来的npm/yarn 的 hoist;pnpm 严格模式可避免
版本统一还是独立强关联用 fixed,松耦合用 independent
最容易踩的缓存坑环境变量没进哈希,拿到旧产物