Monorepo 实战:pnpm workspace + Turborepo 的最佳实践

以 pnpm 的硬链接依赖管理与 Turborepo 的增量任务调度为核心,搭建一套可落地的 Monorepo 工程规范。

Monorepo 不是简单地把多个项目放进一个仓库,而是一套关于依赖管理、任务编排与代码边界的工程方法论。本文以最主流的 pnpm + Turborepo 组合为例,给出可直接复制的实践。

为什么选 pnpm

pnpm 相对 npm/yarn 有两个决定性优势:

  1. 硬链接存储:所有依赖统一存放在全局 store,项目通过硬链接引用,不重复占用磁盘,node_modules 结构也更为规范;
  2. 严格的幽灵依赖隔离:pnpm 默认按包隔离依赖,package.json 里没声明的包无法被 require 到,倒逼你把依赖写清楚。
# 初始化 workspace
pnpm init
# 新建 pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

Turborepo 的增量调度

Turborepo 解决的是「任务怎么跑得快」:它缓存每个任务的输入输出,只有依赖变化时才真正执行。

{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": { "dependsOn": ["build"] }
  }
}

^build 表示先构建所有被依赖的包,配合输出缓存,二次构建几乎瞬时完成。远程缓存(Remote Cache)还能让 CI 之间共享构建结果。

代码边界的设计原则

Monorepo 最怕的坑是「边界模糊」。建议:

  • packages/ 放可复用的库,明确语义化版本;
  • apps/ 放可部署的应用,彼此零依赖;
  • 用 ESLint 的 import/no-restricted-imports 禁止应用间互相引用;
  • 通过 changesets 管理版本发布,避免手动改版本号。

常见陷阱

  • 锁文件冲突:统一用 pnpm-lock.yaml,禁用 npm/yarn;
  • 幽灵依赖回潮:升级依赖时用 pnpm why 追踪来源;
  • 构建顺序混乱:依赖 dependsOn 而非手动编排脚本。

结语

Monorepo 的价值在于降低跨包改动的成本,而不是追求仓库规模的宏大。工具只是手段,清晰的边界与可预测的构建才是目的。