1. 定义
多个项目(或包)的代码放在 同一个版本控制仓库 中进行管理。
Ps:想象一个大房子(一个大的代码仓库),里面住着好几个相关的项目(比如前端的网页 App、后端的服务器程序、共享的工具库),它们不再各自住单独的小房子(单独的仓库),而是共享同一个大空间、同一个地址簿(版本管理)、同一个工具箱(构建工具)。这个大房子就是 Monorepo。
2. Monorepo 的核心概念
- 单一仓库
所有相关项目的源代码都存放在一个 Git 仓库里。git clone
一次,所有代码都到手。
- 原子提交
修改可以跨多个项目/包,并作为一个 整体提交。
想象你同时改了客厅的灯(前端)和厨房的水管(后端),提交时记录的是"升级智能家居系统"这一整个改动,而不是分开提交"修灯"和"修水管"。
- 共享依赖
房子里的所有项目共享一个"中央工具箱"(node_modules
或其他依赖目录)。
如果它们都需要同一个锤子(比如 lodash
库),只需要在房子中央放一把,大家共用(符号链接或提升依赖),不需要每个房间(项目)都买一把重复的。这解决了 "依赖地狱" 和 版本不一致 问题。
- 标准化工具链
整个大房子使用同一套建筑规范。
同样的代码风格检查工具(如 ESLint)、同样的打包工具(如 Webpack, Vite)、同样的测试框架(如 Jest)、同样的 CI/CD 流程。保证大家干活方式一致,效率高。
- 跨项目重构与协作
因为所有代码都在一个地方,改一个共享的工具库,可以立刻看到哪些前端和后端项目在用它,方便统一修改和测试。不同团队(前端、后端、基础架构)更容易看到彼此代码,协作更紧密。
3. 与传统 Polyrepo (多仓库) 的对比
对比维度 | Monorepo 🏢 | Polyrepo 🏠 |
---|---|---|
代码组织 | 所有项目共享一个仓库 | 每个项目独立仓库 |
依赖管理 | ✅ 优势: • 共享依赖(提升/符号链接) • 统一版本,避免冲突 • 跨项目依赖实时生效 | ⚠️ 劣势: • 重复安装依赖(磁盘冗余) • 跨仓库版本易冲突 • 需手动发布和升级依赖 |
跨项目变更 | ✅ 优势: • 原子提交(一次提交修改多个项目) • 重构和影响分析高效 • 统一 CI/CD 流程 | ⚠️ 劣势: • 需跨仓库提交和 PR • 依赖发布流程繁琐 • 协调成本高 |
协作与可见性 | ✅ 优势: • 代码透明,跨团队无缝协作 • 统一代码规范与工具链 • 集中问题追踪 | ⚠️ 劣势: • 团队间存在信息壁垒 • 规范易碎片化 • 上下文切换频繁 |
工具链一致性 | ✅ 优势: • 统一构建、测试、Lint 配置 • 标准化开发环境 | ⚠️ 劣势: • 工具配置易分散 • 维护多套流水线成本高 |
权限控制 | ⚠️ 挑战: • 需复杂目录级权限管理(如 Git 子模块/定制方案) | ✅ 优势: • 天然仓库级权限隔离 • 精细控制更简单 |
仓库性能 | ⚠️ 挑战: • 仓库体积膨胀(Clone/Status 慢) • 依赖增量构建工具优化(如 Turborepo/Nx) | ✅ 优势: • 各仓库轻量 • Git 操作快速 |
构建/测试速度 | ✅ 优势(需工具支持): • 增量构建(仅改动的包) • 并行任务 + 分布式缓存 | ⚠️ 劣势: • 独立构建无全局优化 • 缓存难以共享 |
新成员上手 | ⚠️ 挑战: • 初始学习曲线陡峭 • 需理解整体架构 | ✅ 优势: • 聚焦单个仓库 • 认知负担低 |
适用场景 | ✅ 适合: • 强关联项目(如微服务全家桶) • 高频共享代码库 • 追求统一基建的重型团队 | ✅ 适合: • 松散耦合的独立项目 • 开源生态库 • 权限敏感型组织 |
4. Monorepo 的实现:Yarn Workspaces (+ Lerna)
Yarn Workspaces
Yarn Workspaces 是 Yarn 包管理器提供的一个功能,旨在 简化在单一代码仓库(Monorepo)中管理多个相互关联的 JavaScript/TypeScript 包 的过程。它允许你将一个大的项目根目录(通常是一个 Git 仓库)划分为多个子包(子项目),这些子包可以独立管理自己的依赖,但又能方便地相互引用和共享代码,并且 Yarn 会智能地优化依赖安装。
如何工作?
-
项目结构:
- 创建一个根目录作为 Monorepo 的根。
- 在根目录下创建一个
package.json
文件。 - 在这个根
package.json
中设置"private": true
(因为根目录本身通常不是一个要发布的包)。 - 在根
package.json
中添加"workspaces"
字段。这个字段告诉 Yarn 哪些子目录是工作区(包)。 - 子包通常放在
packages/
或apps/
等目录下(结构可自定义),每个子目录都是一个独立的包,有自己的package.json
。
text
my-monorepo/ (根目录)
├── package.json (根 package.json, 包含 "workspaces")
├── yarn.lock (根 lockfile,由 Yarn 管理)
├── node_modules/ (根 node_modules,存放提升的依赖)
│
├── packages/ (通常存放库包)
│ ├── shared-utils/ (包 A)
│ │ ├── package.json (name: "@my-project/shared-utils")
│ │ ├── src/
│ │ └── ...
│ │
│ └── component-library/ (包 B)
│ ├── package.json (name: "@my-project/component-library")
│ ├── src/
│ └── ...
│
└── apps/ (通常存放应用)
└── web-app/ (包 C)
├── package.json (name: "@my-project/web-app")
├── src/
└── ...
json
// 根 package.json
{
"name": "my-monorepo",
"private": true, // 必须为 true
"workspaces": [
"packages/*", // 匹配 packages 目录下的所有子目录
"apps/*" // 匹配 apps 目录下的所有子目录
],
"scripts": {
// 可以在这里添加根级别的脚本,利用 `yarn workspaces foreach` 或 `yarn workspace <pkg> <cmd>`
},
"devDependencies": {
// 根目录可能有一些共享的开发工具,如 TypeScript, Jest, ESLint, Prettier 的全局配置
}
}
-
依赖安装 (
yarn install
):-
在根目录运行
yarn install
-
Yarn 会:
- 提升依赖 (Hoisting): 分析所有工作区包的依赖关系。如果一个依赖(如
lodash@^4.17.21
)被多个工作区包需要,并且版本范围兼容,Yarn 会尝试将这个依赖安装在根目录的node_modules
中。这样,磁盘上只保留一份副本。 - 处理内部依赖: 如果包 B 的
package.json
中声明依赖包 A ("@my-project/shared-utils": "1.0.0"
),Yarn 会识别出@my-project/shared-utils
是工作区内的另一个包,并自动创建一个符号链接 (symlink),从包 B 的node_modules/@my-project/shared-utils
指向包 A 的实际目录。这使得在开发时修改包 A 能立即反映在依赖它的包 B 中,无需手动link
或发布。 - 生成单个
yarn.lock
: 所有工作区包的依赖关系都记录在根目录下的单个yarn.lock
文件中。这保证了整个 Monorepo 使用的依赖版本完全一致,避免了冲突。
- 提升依赖 (Hoisting): 分析所有工作区包的依赖关系。如果一个依赖(如
-

- 关键命令与工作流:
-
安装所有依赖: 在根目录运行
yarn install
。 -
为特定工作区添加依赖:
- 全局依赖 (会添加到根
package.json
):yarn add -W <package>
(-W
代表--ignore-workspace-root-check
)。 - 给某个工作区添加生产依赖:
yarn workspace <workspace-package-name> add <package>
- 给某个工作区添加开发依赖:
yarn workspace <workspace-package-name> add -D <package>
- 示例:
yarn workspace @my-project/web-app add react-router-dom
- 全局依赖 (会添加到根
-
移除工作区的依赖:
yarn workspace <workspace-package-name> remove <package>
-
运行工作区中的脚本:
- 在特定工作区运行其
package.json
中的脚本:yarn workspace <workspace-package-name> run <script-name>
- 示例:
yarn workspace @my-project/web-app run dev
- 在特定工作区运行其
-
在所有工作区运行同一个脚本:
yarn workspaces foreach run <script-name>
- 示例:
yarn workspaces foreach run lint
(假设每个包都有lint
脚本) - 可以添加
-p
(并行) 或-i
(交互式) 或-v
(详细) 等选项。 - 示例 (并行构建):
yarn workspaces foreach -p run build
- 示例:
-
列出所有工作区:
yarn workspaces list
-
在依赖树中查看为什么安装了某个包:
yarn why <package>
(在根目录运行)
主要优势
-
依赖优化 (提升): 显著减少磁盘空间占用和
yarn install
时间,避免重复安装相同的包。 -
无缝本地链接: 工作区内的包可以像从 npm registry 安装的一样相互引用,修改一个包能立即在依赖它的包中生效,极大简化开发和调试。
-
单一锁文件: 根目录的单个
yarn.lock
确保所有工作区使用完全一致的依赖版本,提升稳定性和可重现性。 -
统一命令入口: 通过
yarn workspace
和yarn workspaces foreach
命令,可以方便地在特定包或所有包上执行操作(安装、运行脚本、构建、测试、发布等)。 -
代码共享与原子提交: 促进组件、工具函数、配置等的共享。相关修改可以跨多个包一起提交,保证原子性。
-
简化工具链配置: 更容易在根目录设置统一的构建、测试、代码格式化、Lint 等工具配置。
需要注意的点/缺点
-
依赖提升的复杂性: 虽然提升节省空间,但也可能导致依赖冲突更难调试("幽灵依赖" - 某个包使用了被提升到根
node_modules
的依赖,但这个依赖并没有明确声明在该包的package.json
中)。Yarn 的 Plug'n'Play (PnP) 模式是另一种解决依赖问题的方案,但 Workspaces 与 PnP 的结合需要额外配置。 -
工具链支持: 虽然主流工具(TypeScript, Jest, ESLint, Babel, Webpack, Vite 等)对 Workspaces 支持良好,但有时可能需要特定配置(如 TypeScript 的项目引用
references
)才能完美处理跨包的类型和构建。 -
IDE/编辑器支持: 现代 IDE (VSCode, WebStorm) 对 Workspaces 支持很好,但有时需要正确配置工作区或 TypeScript 项目引用以实现准确的代码跳转和类型检查。
-
规模管理: 当 Monorepo 变得非常庞大时,即使有 Workspaces,构建、测试和安装时间也可能变长,需要更复杂的工具(如 Nx, Turborepo)进行增量构建和任务调度优化。
-
发布策略: 发布多个相互依赖的包需要协调版本号。工具如 Lerna (常与 Yarn Workspaces 结合使用) 或 Yarn 自己的 version 和 release 工作流 可以自动化此过程。
Lerna
Lerna 是一个用于管理 JavaScript 项目 Monorepo(单仓库多包)的经典工具,它通过自动化解决多包管理中的依赖联动、版本发布和跨包操作等复杂问题。
Lerna 提供命令行工具,自动化完成包之间的版本号同步、依赖安装、测试和发布:
- 跨包依赖链接 (
bootstrap
)
包A依赖包B时,需将B构建后发布到 npm,再在A中安装,开发调试极其繁琐。
Lerna 将仓库内所有包的依赖(包括内部包)安装到根目录 node_modules
(依赖提升)。
通过 符号链接(Symlinks) 将本地相互依赖的包直接链接到彼此 node_modules
中(例如 @project/ui/node_modules/@project/utils
→ 指向本地代码)。
效果:修改工具包代码,依赖它的应用包 实时生效,无需手动发布安装。
- 统一版本发布 (
publish
)
当多个包存在依赖关系时,手动协调版本号和发布顺序易出错。
Lerna 方案:
- 固定模式(Fixed):所有包强制共用同一版本号(如从
1.0.0
统一升级到1.1.0
),适合强关联包。 - 独立模式(Independent):每个包可独立升级版本(如
[email protected]
+[email protected]
),通过交互式提示选择新版本。 - 自动关联发布:若包A依赖包B,且B有更新,发布时会自动升级A的依赖版本。
- 批量执行脚本 (
run
)
在所有包中执行相同命令(如测试、构建):
bash
lerna run build # 为每个包执行 `npm run build`
lerna run test --since main # 仅测试自 main 分支有变更的包
- 智能包变更检测 (
changed
)
列出自上次发布以来代码有修改的包,避免全量发布。
bash
# 典型工作流示例
# 1. 初始化 Monorepo
lerna init
# 2. 创建两个包
lerna create utils
lerna create ui
# 3. 在 ui 包中声明依赖 utils
cd packages/ui
npm install @project/utils # Lerna 会自动识别本地依赖
# 4. 链接所有依赖(关键步骤!)
lerna bootstrap
# 5. 修改 utils 代码后,统一发布新版本
lerna publish
# 交互式选择版本 → 更新依赖链 → 发布到 npm
Lerna 的局限性及现代替代方案:
问题 | Lerna 方案 | 现代工具优化 |
---|---|---|
构建速度慢 | 无内置优化 | Turborepo/Nx 增量构建 + 缓存 |
依赖安装冗余 | 依赖提升(hoisting) | pnpm 硬链接节省磁盘 + 严格依赖隔离 |
版本管理灵活性不足 | 支持独立模式但配置复杂 | Changesets 精细控制版本生成 |
缺乏任务调度 | 仅顺序执行命令 | Turborepo 并行执行 + 依赖拓扑排序 |
- 新项目推荐 Turborepo + pnpm + Changesets 组合(速度更快、功能更现代)。
- 旧 Monorepo 迁移可保留 Lerna 发布流程,用 Turborepo 替代
lerna run
提升构建性能。