Skip to main content

JS 项目 Monorepo 怎么起步?从 npm workspaces 开始的最简路径

· 14 min read

Monorepo 不需要从 pnpm / Turborepo 开始——npm 7+ 自带的 workspaces 字段就能跑,是真正零成本的入门。

  1. 是什么:多项目放一个仓库,不强制任何工具
  2. 第 1 档:npm workspaces(npm 7+ 内置),根 package.json 加字段
  3. 第 2 档:yarn workspaces(早期方案,yarn 1 时代)
  4. 第 3 档:pnpm workspace(当前事实标准,磁盘节省 50%+)
  5. 第 4 档:Turborepo / Nx,只有跨包 build 依赖才需要
  6. 口诀:包数 <5 npm workspaces,<10 pnpm,10+ 才上 Turborepo

Monorepo 的字面意思比你想的朴素

按字面拆:Mono(单一)+ Repo(仓库)——把多个项目放在一个 git 仓库里。看云的一个总结说得很直接:

严格意义上说,只要将多个项目放在一个存储库里就算 monorepo。

这个朴素的定义很关键:monorepo 是一种组织策略,不是一种工具。后面要装的 Turborepo / Nx / pnpm 全是附加工具,不是"monorepo 本身"。

知乎上一篇 QCon 分享文 把最朴素的形态叫"刀耕火种:伪 Monorepo"——几个项目扔到一个文件夹里、各装各的依赖、npm link 手动连。严格来说也是 monorepo,只是工程化能力为零。

一个真正的 monorepo 至少解决两件事:

  1. 项目之间的依赖关系 自治 —— 不需要人肉按拓扑顺序 link
  2. 共享依赖 只装一次 —— 避免 React/Webpack 这种 singleton 冲突

工具链的本质就是把这两件事自动化。

Monorepo 解决什么问题

不是所有项目都适合 monorepo。综合两篇微信文章(通用扫盲深度对比),触发条件是这 5 个里至少中 2 个:

  1. 代码重复严重——同样的功能在多个项目里重复实现
  2. 版本同步困难——升一个库要在 5 个项目里各改一遍
  3. 开发环境复杂——新人 clone 5 个仓库、装 5 遍依赖、配 5 套环境
  4. 跨项目修改频繁——一次需求要同时改前端 + 后端 + 共享类型
  5. 团队协作不畅——跨仓库 PR review 延迟,集成经常出问题

Multirepo vs Monorepo 的核心对比:

维度MultirepoMonorepo
公共函数复用必须发 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 / yarnpnpm
同版本重复存储每项目一份硬链接共享,50%+ 节省
安装速度基准快 2-3 倍
幽灵依赖容忍严格禁止
workspace 协议支持原生支持 + workspace:* 协议

什么时候从 npm workspaces 升级到 pnpm?

  • 项目数 >5
  • 磁盘空间吃紧(pnpm 节省 50%+)
  • 遇到幽灵依赖引发的"在我机器上能跑"问题
  • 团队从零开始(没历史包袱)

第 4 档:+ Turborepo / Nx(按需上)

pnpm workspace 解决了"依赖管理"。但还有一类问题 pnpm 解决不了:

  1. 跨包构建依赖图——@web 依赖 @ui,必须先 build @ui 再 build @web
  2. 增量构建——改了 @ui 只想 rebuild 受影响的 @web,不想跑 10 个包
  3. 远程缓存共享——A 同事 build 过的产物,B 同事和 CI 直接命中缓存
  4. 统一 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 buildpnpm -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

  1. npm workspaces 官方文档 —— npm 7+ 内置 workspace
  2. w3cschool:npm 工作区 workspaces —— npm workspace 最小配置示例
  3. 博客园:掌握 npm、yarn、pnpm 的 workspaces 使用 —— 三个包管理器 workspace 对比
  4. pnpm workspaces 官方文档 —— 当前事实标准
  5. vercel/turborepo —— Turborepo 主仓库,Rust 写就
  6. nrwl/nx —— Nx 平台主仓库
  7. lerna/lerna —— Nx 团队维护,仍在活跃发布
  8. Changesets 文档 —— 版本与 CHANGELOG 管理
  9. 看云:monorepo · Web 前端工程化 —— "严格意义上只要把多个项目放一个仓库就是 monorepo"
  10. 知乎 QCon:Monorepo 的过去、现在、和未来 —— "刀耕火种:伪 Monorepo" 阶段定义
  11. 掘金:带你了解更全面的 Monorepo - 优劣、踩坑、选型 —— Multirepo → Monorepo 痛点对比
  12. 阿里云开发者:使用 pnpm workspace 构建 monorepo 项目实现组件共享 —— 国内 pnpm workspace 实践
  13. 奇舞团:JS monorepo 工程化最佳实践(PNPM + Turborepo + Changesets) —— 360 大前端深度配置拆解(pipeline / changesets config 字段)
  14. 一文读懂 monorepo 与 multirepo 优劣对比 —— 概念扫盲 + 工具横评 + 实操目录结构
  15. 带你了解更全面的 Monorepo:优劣、踩坑、选型(深度对比) —— Monorepo 4 大优势 + 包管理器 5 维度对比表
  16. pnpm 依赖解析机制和 npm/yarn 的区别? —— 同站 pnpm 机制详解