人人都会AI编程

Monorepo 方案:pnpm workspace、Lerna

更新时间:2026-07-11

在单体仓库(Monorepo)的工程实践中,一个仓库统一管理多个包的源码、依赖和构建流程,逐渐成为中大型前端项目以及全栈 Node.js 项目的标准选择。相比多仓库(Multirepo),Monorepo 更容易统一项目配置、共享工具链、进行跨包重构,并且能有效避免“小包多仓库”带来的版本碎片化问题。

本节将重点介绍 Node.js 生态中目前最主流的两种 Monorepo 方案:pnpm workspaceLerna,以及它们在实际项目中的配合与演进。

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 使用。

典型工作流

  1. 批量执行脚本lerna run build 会按照拓扑依赖顺序依次执行每个包的 build 脚本,确保依赖包先构建。
  2. 版本与发布lerna version 可以基于 Conventional Commits 自动推断本次应该升级的版本号,生成 CHANGELOG,并打上 git tag。lerna publish 则负责将包发布到 npm registry。
  3. 差异检测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 lintpnpm -r run test 确保所有包质量。
  • 准备发版时,运行 lerna version,Lerna 会检测自上次发布以来哪些包有变动,询问要升级的版本,自动更新 package.json 版本号、生成 CHANGELOG、提交并打 tag。
  • lerna publish from-git 仅发布 git 中有新 tag 的包,安全精确。

常见问题与应对

  1. 跨包引用版本失效

使用 workspace:* 协议时,务必在发布前通过 pnpm publishlerna publish 将其替换为实际版本。Lerna + pnpm 组合会自动处理这一替换。

  1. 构建顺序问题

lerna run build 天然支持拓扑顺序,配合 --stream 可以实时输出每个包的构建日志。也可以在 lerna.json 中配置 "stream": true 作为默认行为。

  1. CI 中的增量处理

结合 lerna changedlerna 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 的良好实践可以极大减少不同服务、工具库之间的版本不一致和构建重复开销,是工程化走向成熟的标志之一。