在单体仓库(Monorepo)的工程实践中,一个仓库统一管理多个包的源码、依赖和构建流程,逐渐成为中大型前端项目以及全栈 Node.js 项目的标准选择。相比多仓库(Multirepo),Monorepo 更容易统一项目配置、共享工具链、进行跨包重构,并且能有效避免“小包多仓库”带来的版本碎片化问题。
本节将重点介绍 Node.js 生态中目前最主流的两种 Monorepo 方案:pnpm workspace 和 Lerna,以及它们在实际项目中的配合与演进。
15.5.1 为什么是 pnpm workspace?
在 15.1 节基础上我们了解了 pnpm 的硬链接与软链接机制,这本身就是它为 Monorepo 量身打造的天然优势。pnpm workspace 是 pnpm 内置的第一公民功能,无需额外安装任何工具,通过简单的配置即可将一个普通仓库升级为 workspace。
配置方式
(1)在根目录声明 workspace 结构
// pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
- "!**/test/**"
这一文件明确告诉 pnpm 哪些目录下的子目录是独立的包,可以通过 workspace 协议相互引用。
(2)在根 package.json 声明公共脚本
{
"private": true,
"scripts": {
"dev": "pnpm --parallel -r run dev",
"build": "pnpm -r run build",
"test": "pnpm -r run test"
},
"devDependencies": {
"typescript": "^5.0.0",
"eslint": "^8.0.0"
}
}
-r 表示递归执行所有包中匹配的脚本,--parallel 可并行执行,极大加快本地开发。
(3)包内使用 workspace 协议声明依赖
// packages/core/package.json
{
"name": "@my-project/core",
"version": "1.0.0",
"dependencies": {
"@my-project/utils": "workspace:*"
}
}
workspace:* 表示使用本地工作区中的对应包,发布时会被替换成实际版本号。pnpm 会自动创建符号链接,使得 node_modules 中的 @my-project/utils 直接指向 packages/utils,修改后实时生效,无需重新安装。
核心优势
- 安装速度极快:依赖全局存储 + 硬链接,多包复用同一份物理文件。
- 幽灵依赖问题得到控制:pnpm 的严格模式确保每个包只能访问自己声明过的依赖,不会意外依赖其他包的提升依赖。
- 轻量启动:一个
pnpm-workspace.yaml加上几行命令即可构建 Monorepo,学习成本极低。
pnpm workspace 更适合包之间的依赖关系清晰、以代码共享为主的场景,比如类库集合、工具函数库、前端组件库等。但它本身缺少版本管理、发布流水线等更高级的能力。
15.5.2 Lerna:老牌 Monorepo 管理工具
Lerna 是历史最久的 JavaScript Monorepo 工具之一,曾一度是“标准答案”。它解决的核心问题是多包的版本管理、脚本批量执行和自动化发布。
基本使用模式
Lerna 提供两种模式,在使用 lerna init 初始化时就需要选定:
- Fixed mode(固定模式):所有包使用同一个版本号,一次
lerna publish会统一提升所有包的版本。适合相互紧密耦合的项目,如 Babel。 - Independent mode(独立模式):每个包拥有自己的版本号,发布时单独提升。适合松耦合的组件或服务。
// lerna.json(固定模式)
{
"version": "1.0.0",
"npmClient": "pnpm",
"useWorkspaces": true
}
注意最新版本的 Lerna(v7+)已经重新架构,内部使用 Nx 的任务编排,并建议搭配 pnpm workspace 或 npm workspaces 使用。
典型工作流
- 批量执行脚本:
lerna run build会按照拓扑依赖顺序依次执行每个包的 build 脚本,确保依赖包先构建。 - 版本与发布:
lerna version可以基于 Conventional Commits 自动推断本次应该升级的版本号,生成 CHANGELOG,并打上 git tag。lerna publish则负责将包发布到 npm registry。 - 差异检测:
lerna changed可以列出自上一个 tag 以来发生了变更的包,以便只对这些包执行测试或 lint,节省 CI 时间。
Lerna 当前定位
随着 npm/yarn/pnpm 原生 workspace 功能的完善,Lerna 的依赖管理、符号链接等能力逐渐被取代,但其版本管理与发布自动化能力依然不可或缺。在大型开源项目(如 Jest、Vue、React 生态工具链)中,Lerna 仍是版本发布的核心工具。值得注意的是,从 Lerna 6 起,它已经与 Nx 深度集成,可以享受到增量构建、缓存等能力。
15.5.3 真实世界的最佳搭配:pnpm workspace + Lerna(轻量发布)
现代 Monorepo 项目更倾向于用 pnpm workspace 解决依赖与本地链接问题,用 Lerna 解决版本与发布问题,各取所长,避免重复造轮子。
组合配置示例
// 根 package.json
{
"private": true,
"scripts": {
"dev": "pnpm --parallel -r run dev",
"build": "pnpm -r run build",
"test": "pnpm -r run test",
"release": "lerna publish"
},
"devDependencies": {
"lerna": "^7.0.0"
}
}
# pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
// lerna.json
{
"version": "independent",
"npmClient": "pnpm",
"command": {
"publish": {
"registry": "https://registry.npmjs.org"
}
}
}
这一组合的工作流程为:
- 日常开发中,
pnpm install安装所有依赖,pnpm -r run dev并行启动服务。 - 代码提交前,
pnpm -r run lint和pnpm -r run test确保所有包质量。 - 准备发版时,运行
lerna version,Lerna 会检测自上次发布以来哪些包有变动,询问要升级的版本,自动更新package.json版本号、生成 CHANGELOG、提交并打 tag。 lerna publish from-git仅发布 git 中有新 tag 的包,安全精确。
常见问题与应对
- 跨包引用版本失效
使用 workspace:* 协议时,务必在发布前通过 pnpm publish 或 lerna publish 将其替换为实际版本。Lerna + pnpm 组合会自动处理这一替换。
- 构建顺序问题
lerna run build 天然支持拓扑顺序,配合 --stream 可以实时输出每个包的构建日志。也可以在 lerna.json 中配置 "stream": true 作为默认行为。
- CI 中的增量处理
结合 lerna changed 和 lerna run test --since origin/main 等命令,可以在 CI 中仅对变更的包执行测试,极大节省构建时间。
15.5.4 选型建议
| 场景 | 推荐方案 |
|------|---------|
| 小型项目、学习型 Monorepo | pnpm workspace(无需额外工具) |
| 前端组件库、工具函数集 | pnpm workspace + Changesets(轻量版本管理) |
| 多包应用、需严格发布流水线 | pnpm workspace + Lerna(或 Nx) |
| 已有大量历史 Lerna 项目 | 升级到 Lerna v7+,配合 pnpm workspace 使用 |
| 极端重视构建性能 | Nx 或 Turborepo(更专业的任务编排与缓存) |
无论选择哪种方案,核心目标都是让跨包协作像在同一个代码库中修改文件一样自然。在 Node.js 的全栈项目中,Monorepo 的良好实践可以极大减少不同服务、工具库之间的版本不一致和构建重复开销,是工程化走向成熟的标志之一。