JS 项目 Monorepo 怎么起步?从 npm workspaces 开始的最简路径
Monorepo 不需要从 pnpm / Turborepo 开始——npm 7+ 自带的 workspaces 字段就能跑,是真正零成本的入门。
- 是什么:多项目放一个仓库,不强制任何工具
- 第 1 档:npm workspaces(npm 7+ 内置),根
package.json加字段 - 第 2 档:yarn workspaces(早期方案,yarn 1 时代)
- 第 3 档:pnpm workspace(当前事实标准,磁盘节省 50%+)
- 第 4 档:Turborepo / Nx,只有跨包 build 依赖才需要
- 口诀:包数 <5 npm workspaces,<10 pnpm,10+ 才上 Turborepo
Monorepo 的字面意思比你想的朴素
按字面拆:Mono(单一)+ Repo(仓库)——把多个项目放在一个 git 仓库里。看云的一个总结说得很直接:
严格意义上说,只要将多个项目放在一个存储库里就算 monorepo。
这个朴素的定义很关键:monorepo 是一种组织策略,不是一种工具。后面要装的 Turborepo / Nx / pnpm 全是附加工具,不是"monorepo 本身"。
知乎上一篇 QCon 分享文 把最朴素的形态叫"刀耕火种:伪 Monorepo"——几个项目扔到一个文件夹里、各装各的依赖、npm link 手动连。严格来说也是 monorepo,只是工程化能力为零。
一个真正的 monorepo 至少解决两件事:
- 项目之间的依赖关系 自治 —— 不需要人肉按拓扑顺序 link
- 共享依赖 只装一次 —— 避免 React/Webpack 这种 singleton 冲突
工具链的本质就是把这两件事自动化。
Monorepo 解决什么问题
不是所有项目都适合 monorepo。综合两篇微信文章(通用扫盲、深度对比),触发条件是这 5 个里至少中 2 个:
- 代码重复严重——同样的功能在多个项目里重复实现
- 版本同步困难——升一个库要在 5 个项目里各改一遍
- 开发环境复杂——新人 clone 5 个仓库、装 5 遍依赖、配 5 套环境
- 跨项目修改频繁——一次需求要同时改前端 + 后端 + 共享类型
- 团队协作不畅——跨仓库 PR review 延迟,集成经常出问题
Multirepo vs Monorepo 的核心对比:
| 维度 | Multirepo | Monorepo |
|---|---|---|
| 公共函数复用 | 必须发 npm 包,跨仓库 bump | 直接 import 源码 |
| 依赖版本 | 每仓库独立 lock,容易漂移 | 全仓共享一份 lock |
| 工具链配置 | 每仓库复制 ESLint/tsconfig | 根目录一份,子包继承 |
| 跨项目变更 | 跨仓库 PR,难以原子提交 | 一个 commit 搞定 |
| 仓库体积 | 小 | 大,CI 要靠缓存 |
只跑一个项目、或者多个项目完全独立,硬上 monorepo 只会得到一个又大又慢的仓库。
四档配置:从 npm workspaces 到 Turborepo
工具链不是 all-or-nothing,而是按"包数 + 痛点"分支选择:
对照原四档分级:
- npm workspaces(npm 7+ 内置):3-5 个包、无幽灵依赖痛点 → 零成本起步
- pnpm workspace:5-10 个包,或有幽灵依赖 / 磁盘吃紧 → 当前事实标准
- Turborepo:10+ 个包,有跨包 build 依赖,不需要 Affected 分析 → 30K star 构建系统
- Nx:10+ 个包 + 需要 Affected / AI 修复 PR → 29K star 平台
- Changesets:以上任意一种 + 对外发 npm 包 → 加装;否则跳过
第 1 档:npm workspaces(最朴素起步)
npm 7+(2020 年发布)开始内置 workspaces 功能。这意味着零额外工具就能跑 monorepo。
w3cschool 教程 给出的最小配置:
// 根 package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"]
}
目录结构:
my-monorepo/
├── package.json # 根包,加 "private": true 防止误发布
└── packages/
├── utils/ # 子包 1
│ └── package.json
└── ui/ # 子包 2
└── package.json
private: true 是关键——根目录只是个容器,不应该被 npm publish。
workspaces 字段是个 路径数组,支持 glob 通配符。packages/* 匹配 packages/ 下所有一级子目录。
跨包引用
子包之间互相引用:
// packages/ui/package.json
{
"name": "@myorg/ui",
"version": "1.0.0"
}
// packages/web/package.json
{
"name": "@myorg/web",
"dependencies": {
"@myorg/ui": "*"
}
}
在根目录跑 npm install:
my-monorepo/
├── node_modules/
│ └── @myorg/
│ └── ui -> ../../packages/ui # 符号链接到源码
├── package-lock.json
└── packages/
├── ui/
└── web/
@myorg/ui 被符号链接到 packages/ui 的源码。改 packages/ui 的代码,@myorg/web 立即生效,不需要任何 publish 步骤——这就是 monorepo 的核心便利。
跨包命令
# 在所有子包里跑 build
npm run build --workspaces
# 在指定子包里跑
npm run test -w @myorg/ui
# 给指定子包装依赖
npm install react -w @myorg/web
--workspaces / -w 是 npm 7+ 的语法。这些命令够覆盖 80% 的多包任务——很多人不知道 npm 本身就够了。
npm workspaces 的局限
- 幽灵依赖问题:和 npm 一样有 hoisting,子包可能访问到未声明的依赖
- 没有
workspace:*协议(要写*而不是workspace:*) - 跨包构建依赖图需要自己写脚本,没有
dependsOn: ["^build"]这种声明
如果项目数 <5、子包之间没有复杂的构建依赖、npm workspaces 就是终点。不要为了"显得专业"硬上 pnpm / Turborepo。
第 2 档:yarn workspaces(npm 的早期竞品)
yarn 1(2016 年发布)比 npm 7 早 4 年引入 workspaces,是 前端 monorepo 的开拓者。语法和 npm 类似:
// 根 package.json
{
"private": true,
"workspaces": {
"packages": ["packages/*"]
}
}
主要区别:
- yarn 1 支持嵌套数组(
"packages": ["packages/*", "docs/*"]) - yarn 的
workspaces协议语法是*,和 npm 一致 - yarn 1 跑命令用
yarn workspaces run build
但 yarn 1 已停止维护(2020 年后),yarn 2+(Berry)改用 PnP(Plug'n'Play),PnP 不适合 monorepo——它要求所有依赖都在一个全局存储里,子包不能独立安装依赖。
什么时候选 yarn workspaces? 几乎没有理由。除非你在维护 2018-2020 年的遗留项目。
第 3 档:pnpm workspace(当前主流)
pnpm workspace 是 npm/yarn workspaces 的 全面升级 —— 同样的语法,更干净的依赖管理。
配置文件从 package.json 挪出来
pnpm 把 workspace 配置从 package.json 移到独立文件:
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- '!**/test/**'
好处是配置文件更醒目、可读性更好。
workspace 协议升级到 workspace:*
pnpm 支持更明确的协议声明:
// packages/web/package.json
{
"name": "@myorg/web",
"dependencies": {
"@myorg/ui": "workspace:*"
}
}
workspace:* 明确告诉 pnpm:本地源码引用,永远用最新版本,不用管版本号。
pnpm 的硬优势
相比 npm/yarn,pnpm 在依赖层面有质的提升(同站之前写过详解):
| 维度 | npm / yarn | pnpm |
|---|---|---|
| 同版本重复存储 | 每项目一份 | 硬链接共享,50%+ 节省 |
| 安装速度 | 基准 | 快 2-3 倍 |
| 幽灵依赖 | 容忍 | 严格禁止 |
| workspace 协议 | 支持 | 原生支持 + workspace:* 协议 |
什么时候从 npm workspaces 升级到 pnpm?
- 项目数 >5
- 磁盘空间吃紧(pnpm 节省 50%+)
- 遇到幽灵依赖引发的"在我机器上能跑"问题
- 团队从零开始(没历史包袱)
第 4 档:+ Turborepo / Nx(按需上)
pnpm workspace 解决了"依赖管理"。但还有一类问题 pnpm 解决不了:
- 跨包构建依赖图——
@web依赖@ui,必须先 build@ui再 build@web - 增量构建——改了
@ui只想 rebuild 受影响的@web,不想跑 10 个包 - 远程缓存共享——A 同事 build 过的产物,B 同事和 CI 直接命中缓存
- 统一 lint / test 入口——
turbo run lint一行跑完所有包
这 4 个痛点 都中 才需要上 Turborepo。少一个都不需要。
Turborepo
vercel/turborepo 是 Vercel 维护的 Rust 写构建系统(30K+ star),核心是 turbo.json 描述任务图:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": { "dependsOn": ["^build"] },
"lint": {},
"dev": { "cache": false, "persistent": true }
}
}
三个关键字段:
^build——^前缀表示"先 build 我的依赖"outputs—— 声明产物路径,用于缓存复用cache: false——dev跑 watch 不缓存
杀手锏是 远程缓存 —— 开启 Vercel Remote Cache 后,CI 上一次构建的产物全队都能命中。
Nx
Nx 不只是构建系统——它是一个完整平台(29K star):
- Affected 分析:
nx affected:test自动找出你改的 commit 影响哪些包 - 生成器:
nx g component my-button脚手架 - AI 修复 PR:Nx Cloud 自动基于错误日志改 PR
- 强约束项目图:所有包之间依赖必须显式声明
什么时候选 Nx 而不是 Turborepo?
- 项目数 < 30:选 Turborepo,简单
- 项目数 30+,需要 Affected 分析 + 复杂 CI 编排:选 Nx
- 已有 Nx / Lerna 历史包袱:留在 Nx 生态
Lerna 现状
Nx 团队 2022 年接管了 Lerna,v9.0.7(2026-03)还在发版。中文圈常说"Lerna 已死"是过时的说法——它是被 Nx 团队收编,没死,但功能已被 Turborepo + Changesets 拆开超越。
Changesets 在什么场景下才需要
Changesets 解决 多包版本号 + CHANGELOG 管理(详细文档):
pnpm add -Dw @changesets/cli
pnpm changeset init
# 改完代码后描述这次改了什么
pnpm changeset
# 生成 .changeset/random-hash.md
# 发版前一键 bump + 生成 CHANGELOG
pnpm changeset version
pnpm changeset publish
但 如果你不发包到 npm registry —— 只是 monorepo 内部几个应用互相引用 —— Changesets 完全用不上。判断标准:你有没有"对外发包"的需求?没有就别装。
几点看法
1. 工具升级的决策是"规模"驱动,不是"哪个最好"驱动。
包数 <5 → npm workspaces;5-10 → pnpm;10+ 且有跨包 build 依赖 → + Turborepo;30+ 且要复杂 CI 编排 → Nx。一上来追求"主流栈"是把工具当目的,忘了最初为什么要 monorepo。
2. pnpm 的真正杀手锏 不是快,是禁幽灵依赖。
磁盘节省 50% / 安装快 2-3 倍听起来很爽,但日常开发最痛的是"在我机器上能跑"——幽灵依赖让子包能 import 任何 hoisted 上去的包。pnpm 严格禁止这个,这是 monorepo 友好性最高的特性——子包之间源码引用不会因为依赖漂浮导致运行时崩。
3. Turborepo 的真正价值是缓存复用,不是"快"。
冷启动 turbo run build 和 pnpm -r build 体感差别不大。第二次跑同样的命令、产物未变更时 Turborepo 直接命中缓存——这才是 CI 和日常开发的最大收益。配合 Vercel Remote Cache 把这份收益从单机扩展到全队/CI。
4. Monorepo 不是银弹,仓库物理集中 ≠ 架构清晰。
一个 30 个包、依赖混乱的 monorepo,比 10 个各管一摊的 multirepo 更难维护。Monorepo 把团队协作压力从"版本号对齐"转到"目录结构设计"——后者的设计成本不低。
5. 跨语言 monorepo 别用前端这套。
前端这套工具(npm/yarn/pnpm workspaces + Turborepo/Nx)本质是 JS/TS 生态。混 Go + Python + TS 的大 monorepo 应该看 Bazel(Google)或 Pants(Twitter)——它们的规则系统理解多语言构建语义。
怎么开始
起步阶段(2-3 个 package)
mkdir my-monorepo && cd my-monorepo
npm init -y
# 加 workspaces 字段
cat > package.json <<EOF
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"]
}
EOF
# 建两个子包
mkdir -p packages/utils packages/web
cd packages/utils && npm init -y && cd ../..
cd packages/web && npm init -y && cd ../..
# 跨包引用(web 依赖 utils)
cd packages/web
npm install @myorg/utils@*
# 在所有子包里跑 build
npm run build --workspaces
到此为止就是 monorepo 了。不需要 pnpm,不需要 Turborepo。
中等规模(5-10 个 package / 想优化依赖管理)
# 装 pnpm
npm install -g pnpm
# 把 package.json 的 workspaces 字段挪到 pnpm-workspace.yaml
cat > pnpm-workspace.yaml <<EOF
packages:
- 'apps/*'
- 'packages/*'
EOF
# 把跨包引用从 @* 改成 workspace:*
# "dependencies": { "@myorg/ui": "workspace:*" }
# 在所有子包里跑命令
pnpm -r build
# 只在某个子包及其依赖里跑
pnpm --filter @myorg/web... test
大规模(10+ package / 跨包构建依赖)
pnpm add -Dw turbo
cat > turbo.json <<'EOF'
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"test": { "dependsOn": ["^build"] },
"lint": {},
"dev": { "cache": false, "persistent": true }
}
}
EOF
# 替代 pnpm -r build 的是 turbo run build
turbo run build
老项目渐进迁移
不要一上来全量重构。从"抽公共 utils 到 packages/utils"开始,让团队先熟悉 workspaces 协议 + -r / --workspaces 命令,跑顺了再扩。
一句话总结
Monorepo 的入场券是 npm workspaces,不是 pnpm 或 Turborepo——后面那些是规模上去之后才需要的优化项。零成本起步,别被"主流栈"绑架。
References
- npm workspaces 官方文档 —— npm 7+ 内置 workspace
- w3cschool:npm 工作区 workspaces —— npm workspace 最小配置示例
- 博客园:掌握 npm、yarn、pnpm 的 workspaces 使用 —— 三个包管理器 workspace 对比
- pnpm workspaces 官方文档 —— 当前事实标准
- vercel/turborepo —— Turborepo 主仓库,Rust 写就
- nrwl/nx —— Nx 平台主仓库
- lerna/lerna —— Nx 团队维护,仍在活跃发布
- Changesets 文档 —— 版本与 CHANGELOG 管理
- 看云:monorepo · Web 前端工程化 —— "严格意义上只要把多个项目放一个仓库就是 monorepo"
- 知乎 QCon:Monorepo 的过去、现在、和未来 —— "刀耕火种:伪 Monorepo" 阶段定义
- 掘金:带你了解更全面的 Monorepo - 优劣、踩坑、选型 —— Multirepo → Monorepo 痛点对比
- 阿里云开发者:使用 pnpm workspace 构建 monorepo 项目实现组件共享 —— 国内 pnpm workspace 实践
- 奇舞团:JS monorepo 工程化最佳实践(PNPM + Turborepo + Changesets) —— 360 大前端深度配置拆解(pipeline / changesets config 字段)
- 一文读懂 monorepo 与 multirepo 优劣对比 —— 概念扫盲 + 工具横评 + 实操目录结构
- 带你了解更全面的 Monorepo:优劣、踩坑、选型(深度对比) —— Monorepo 4 大优势 + 包管理器 5 维度对比表
- pnpm 依赖解析机制和 npm/yarn 的区别? —— 同站 pnpm 机制详解