前端工程化 / 闯关模式
pnpm + monorepo + Turborepo 管理多项目
理解 monorepo、pnpm workspace、workspace:*、--filter、Turborepo 任务调度和缓存。
一句话:monorepo 解决“多个项目怎么放在一起”,pnpm workspace 解决“这些项目怎么共享本地包”,Turborepo 解决“这些项目的 build、lint、typecheck 怎么更快、更有顺序地跑”。
本篇学完你会什么:能分清 pnpm、monorepo、Turborepo 各自负责什么,知道它们是不是必须一起用,也能判断一个已有项目该不该迁移成根级 workspace 和 Turbo 管理。
1. 先回答最容易混淆的 3 个问题
问题 1:pnpm、monorepo、Turborepo 必须一起用吗
不必须。
它们是三层东西:
| 层级 | 名字 | 负责什么 |
|---|---|---|
| 组织方式 | monorepo | 多个项目放在一个 Git 仓库里 |
| 包管理工具 | pnpm workspace | 安装依赖、本地包互相引用 |
| 任务调度工具 | Turborepo | 跑 build、dev、lint、typecheck,并做缓存 |
所以可以有这些组合:
| 组合 | 可以吗 | 适合场景 |
|---|---|---|
| 只用 pnpm | 可以 | 单个项目 |
| pnpm + workspace | 可以 | 多个项目要共享本地包 |
| pnpm + workspace + Turborepo | 可以 | 项目多了,构建和检查任务开始变慢 |
| Turborepo 单独用 | 不推荐 | 它最强的能力依赖 workspace 关系 |
先记住一句话:
pnpm 管依赖。
monorepo 管项目放在哪里。
Turborepo 管任务怎么跑。问题 2:多个项目不在同一个仓库,Turborepo 怎么工作
不好工作。
Turborepo 最擅长的是这种结构:
one-repo/
apps/admin/
apps/website/
packages/utils/
pnpm-workspace.yaml
turbo.json它能在同一个仓库里看到:
- 哪些项目存在
- 哪些包互相依赖
- 哪些任务要先跑
- 哪些任务可以跳过
如果项目分散在多个仓库:
repo-admin/
repo-website/
repo-utils/Turbo 就很难自然知道这些仓库之间的关系。你可以写脚本强行调度,但那更像是在用 shell 或 CI 编排多个仓库,不是 Turborepo 最顺手的用法。
问题 3:我直接把多个项目放一个文件夹,不就行了吗
可以,但那只是“放得近”,不一定是 monorepo。
比如:
projects/
admin/
website/
ai/如果每个项目都有自己的:
node_modulespnpm-lock.yaml- 独立安装命令
- 独立公共代码副本
那它们只是住在同一个大文件夹里,彼此并没有真正建立关系。
真正有用的 monorepo 通常还会有:
root/
apps/
packages/
package.json
pnpm-workspace.yaml
pnpm-lock.yaml它不只是“放一起”,而是能做到:
- 统一安装依赖
- 共享本地包
- 统一脚本命令
- 一次提交同时改多个相关项目
- 后续可以接入 Turborepo 做任务缓存
2. 三个词分别是什么
可以用一个“公司办公室”的类比:
monorepo = 一栋办公楼
pnpm workspace = 楼里的通讯录和仓库钥匙
Turborepo = 楼里的任务调度员办公楼让不同团队在一个地方办公。
通讯录和仓库钥匙让大家能找到彼此,也能共享内部工具。
任务调度员负责安排工作顺序:谁先做,谁后做,谁这次不用重复做。

再用表格压缩一下:
| 名字 | 它是什么 | 它不是什么 |
|---|---|---|
| monorepo | 仓库组织方式 | 不是 npm 包,也不是构建工具 |
| pnpm workspace | pnpm 的多包管理能力 | 不是任务缓存工具 |
| Turborepo | 任务编排和缓存工具 | 不是包管理器 |
3. 只把项目放进一个文件夹,为什么还不算真正的 monorepo
假设你现在有 3 个前端:
frontend-admin/
frontend-ai/
frontend-website/如果它们只是这样放在同一个目录下:
zero-one/
frontend-admin/
package.json
pnpm-lock.yaml
frontend-ai/
package.json
pnpm-lock.yaml
frontend-website/
package.json
pnpm-lock.yaml好处只有一个:你找文件方便。
但它们之间没有真正的“项目关系”:
frontend-admin不知道frontend-ai存在- 公共工具函数不能自然共享
- 每个项目可能安装一套自己的依赖版本
- 根目录不能统一跑所有项目的检查
- 后续 Turbo 也没有完整任务图可以用
monorepo 的重点不是“文件夹挨着”,而是“仓库里有统一规则”。
比如:
zero-one/
apps/
admin/
ai/
website/
packages/
utils/
request/
ui/
package.json
pnpm-workspace.yaml
pnpm-lock.yaml这时根目录的 pnpm-workspace.yaml 会告诉 pnpm:
packages:
- "apps/*"
- "packages/*"意思是:
apps/ 下面都是应用。
packages/ 下面都是共享包。
这些目录共同组成一个 workspace。这才开始进入“真正可管理的 monorepo”。
4. pnpm workspace 到底帮你做了什么
pnpm 本身是包管理器,和 npm、yarn 属于同一类工具。
它最基础的工作是:
读取 package.json
安装 dependencies
生成 pnpm-lock.yaml
让项目可以 import 第三方包加上 workspace 之后,它多了一个能力:让本地多个 package 互相引用。
比如你有一个公共工具包:
packages/utils/
package.json
src/index.tspackages/utils/package.json:
{
"name": "@zero-one/utils",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts"
}然后 apps/website/package.json 可以这样写:
{
"dependencies": {
"@zero-one/utils": "workspace:*"
}
}workspace:* 的意思是:
不要去 npm 远程仓库找 @zero-one/utils。
直接使用当前 workspace 里的 packages/utils。业务代码里就可以像用普通 npm 包一样使用它:
import { formatDate } from "@zero-one/utils";
const text = formatDate(new Date());这就是 pnpm workspace 的核心价值:
共享代码不用复制。
共享代码也不用先发布到 npm。
本地改了共享包,业务项目能直接用。
5. Turborepo 到底帮你做了什么
pnpm workspace 管的是“包之间怎么引用”。
但是项目多了以后,你还会遇到另一个问题:命令怎么跑才聪明。
比如你有:
apps/admin # 后台
apps/website # 官网
packages/utils # 公共工具包
packages/ui # 公共 UI 包依赖关系是:
admin -> ui -> utils
website -> ui -> utils现在你执行:
pnpm build真正的问题不是“能不能跑”,而是:
- 应该先 build
utils还是先 buildadmin admin和website能不能并行 build- 如果
utils没变,能不能跳过它 - 如果只改了
website的页面,能不能不重新 buildadmin - CI 每次都全量构建会不会太慢
这些就是 Turborepo 负责的。
它会把任务看成一张图:
packages/utils build
↓
packages/ui build
↓
apps/admin build
apps/website build然后它会尽量做到:
| 能力 | 大白话解释 |
|---|---|
| 任务排序 | 先跑被依赖的包,再跑依赖它的应用 |
| 并行执行 | 互不影响的任务同时跑 |
| 本地缓存 | 文件没变,重复任务可以跳过 |
| 远程缓存 | CI 或同事电脑也能复用缓存,配置后才有 |
| 过滤执行 | 只跑某个项目,或者某个项目加它依赖的包 |
所以 Turborepo 的核心不是“共享代码”。
共享代码是 pnpm workspace 的事情。
Turborepo 的核心是:
同一个仓库里有很多任务。
我帮你判断哪些任务要跑、按什么顺序跑、哪些可以跳过。
6. pnpm workspace 和 Turborepo 的区别
这一节很重要。
很多人一开始会把它们混在一起,以为用了 monorepo 就必须马上用 Turbo。
其实不是。
| 问题 | pnpm workspace | Turborepo |
|---|---|---|
| 安装依赖 | 负责 | 不负责 |
| 本地包引用 | 负责 | 不负责 |
workspace:* | 负责 | 不负责 |
| 统一 lockfile | 负责 | 不负责 |
| 任务按依赖顺序执行 | 能做一部分,但不够专门 | 负责 |
| build 缓存 | 不负责 | 负责 |
| 只跑受影响任务 | 不擅长 | 擅长 |
| CI 加速 | 有限 | 擅长 |
更直观一点:
pnpm install这句话主要是 pnpm 的世界。
pnpm --filter website dev这句话还是 pnpm 的世界,它在筛选某个 workspace package。
pnpm turbo build这句话进入 Turbo 的世界,它在调度 build 任务。
如果你的项目现在只有 2 个应用,公共包也很少,先用 pnpm workspace 就够了。
当你开始感觉:
- 每次 build 都要等很久
- 不知道改了公共包后该重建哪些项目
- CI 总是全量跑
lint、typecheck、build越来越多
这时再加 Turborepo,收益会更明显。
7. 从一个最小例子看完整工作流
先创建一个根目录:
mkdir my-monorepo
cd my-monorepo
pnpm init创建目录:
my-monorepo/
apps/
website/
admin/
packages/
utils/创建 pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"创建 packages/utils/package.json:
{
"name": "@my-project/utils",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts"
}创建 packages/utils/src/index.ts:
export function formatTitle(title: string) {
return title.trim().replace(/\s+/g, " ");
}在 apps/website/package.json 引用它:
{
"name": "website",
"private": true,
"scripts": {
"dev": "vite --host 0.0.0.0 --port 3000",
"build": "vite build",
"lint": "eslint .",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@my-project/utils": "workspace:*"
}
}安装依赖:
pnpm install只启动官网:
pnpm --filter website dev对所有 workspace 包执行 build:
pnpm -r build-r 是 recursive,也就是递归执行所有 workspace package 里的同名脚本。
到这里为止,你还没有用 Turborepo,但已经是一个能共享本地包的 pnpm workspace 了。
接下来才加 Turbo:
pnpm add -D turbo -w-w 的意思是安装到 workspace 根目录。
创建 turbo.json:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".output/**", ".nuxt/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {},
"typecheck": {}
}
}这几个配置的意思:
| 配置 | 意思 |
|---|---|
build | 定义 build 任务 |
dependsOn: ["^build"] | 先构建当前包依赖的其他包 |
outputs | 告诉 Turbo 哪些文件是构建产物,可以缓存 |
dev.cache: false | 开发服务不要缓存 |
dev.persistent: true | dev 是长期运行的任务 |
根目录 package.json 可以写:
{
"private": true,
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"lint": "turbo lint",
"typecheck": "turbo typecheck"
},
"devDependencies": {
"turbo": "^2.0.0"
}
}以后你就可以在根目录跑:
pnpm build
pnpm lint
pnpm typecheck只构建官网:
pnpm turbo build --filter=website构建官网以及它依赖的包:
pnpm turbo build --filter=website...这里的 ... 可以理解为“把相关依赖也带上”。
8. 推荐目录结构
如果从干净项目开始,推荐这样组织:
my-monorepo/
apps/
website/
package.json
admin/
package.json
ai/
package.json
packages/
utils/
package.json
request/
package.json
ui/
package.json
package.json
pnpm-workspace.yaml
pnpm-lock.yaml
turbo.json每一层职责:
| 目录或文件 | 放什么 |
|---|---|
apps/ | 真正能运行、能部署的应用,比如官网、后台、AI 前端 |
packages/ | 被多个应用引用的共享包,比如工具函数、请求封装、UI 组件 |
根 package.json | 统一脚本和公共开发依赖 |
pnpm-workspace.yaml | 告诉 pnpm 哪些目录属于 workspace |
pnpm-lock.yaml | 锁定整个 workspace 的依赖版本 |
turbo.json | 告诉 Turbo 怎么调度任务和缓存产物 |
不要一开始就把所有东西抽成包。
更稳的做法是:
先放进 apps/
再统一 workspace
观察重复代码
稳定复用后再抽 packages/
任务变多后再接 Turbo9. 用这个项目理解迁移路线
以当前 zero-one 项目为例,仓库里已经有多个前端和后端:
zero-one/
frontend-website/ # 公开网站项目,也就是 /learn 展示文章的项目
frontend-zero-ai/ # AI 应用前端项目
frontend-zero-bi/ # BI 数据分析前端项目
frontend-react/ # React 版本前端项目或实验项目
frontend-vue/ # Vue 管理端项目
backend/ # 后端服务项目它已经有“多个项目在一个仓库里”的形态。
但目前更像是:
多个项目放在同一个仓库下,
每个前端项目自己管理一套 pnpm workspace。如果要整理成根级 workspace,不建议一口气大改。
第一步:只建立根级 workspace
先在根目录新增 pnpm-workspace.yaml:
packages:
- "frontend-website"
- "frontend-zero-ai"
- "frontend-zero-bi"
- "frontend-react"
- "frontend-vue"这个阶段只做一件事:让 pnpm 知道这些前端项目属于同一个 workspace。
不要急着抽包,也不要急着加 Turbo。
第二步:统一 package name 和 scripts
每个前端项目的 package.json 都要有稳定名字:
{
"name": "frontend-website",
"private": true,
"scripts": {
"dev": "...",
"build": "...",
"typecheck": "..."
}
}脚本名字尽量统一:
| 脚本 | 作用 |
|---|---|
dev | 本地启动 |
build | 生产构建 |
build:test | 测试环境构建 |
typecheck | 类型检查 |
lint | 代码检查 |
脚本名统一之后,后续 Turbo 才好调度。
第三步:抽真正重复的工具包
比如多个前端项目都需要:
- 时间格式化
- API 响应处理
- 权限判断
- 常量枚举
- Markdown 渲染工具
就可以抽:
packages/
shared/
utils/
request/判断标准很简单:
至少两个项目稳定复用,
而且复制维护已经开始烦了,
再抽成 package。第四步:谨慎抽 UI 包
UI 包最容易抽过头。
推荐先抽这些低业务属性组件:
- 按钮
- 表单项
- 空状态
- 弹窗
- 页面标题
- 数据卡片
不要一开始就抽复杂业务组件。
公共 UI 包可以叫:
packages/ui/业务项目引用:
{
"dependencies": {
"@zero-one/ui": "workspace:*"
}
}第五步:最后接入 Turborepo
等 workspace 跑顺了,再加 turbo.json。
原因很简单:
Turborepo 是加速器,不是地基。地基是:
目录清楚
项目名清楚
依赖关系清楚
脚本命令清楚这些没整理好,直接上 Turborepo 只会让问题更难排查。

10. 常见错误和排查
错误 1:把 monorepo 当成工具
monorepo 不是工具。
它是“多个项目放进同一个 Git 仓库”的组织方式。
你可以不用 Turborepo,也能有 monorepo。
错误 2:以为用了 pnpm workspace 就必须用 Turborepo
不必须。
如果项目还小,可以先只用 pnpm workspace。
当你开始遇到这些问题时,再考虑 Turborepo:
- build 越来越慢
- 多个项目任务要按依赖顺序跑
- 想复用上次构建缓存
- 想在 CI 里只跑受影响项目
错误 3:以为放一个文件夹就是 monorepo
只是放在一个文件夹里,最多叫“目录集中”。
真正有管理价值的 monorepo 至少要能回答这些问题:
- 哪些目录属于 workspace
- 哪些项目是 app
- 哪些项目是 package
- 公共代码怎么被本地引用
- 根目录能不能统一安装和执行命令
错误 4:共享包忘记使用 workspace 协议
错误写法:
{
"dependencies": {
"@zero-one/utils": "^1.0.0"
}
}这会让包管理器去远程 npm 找。
本地 workspace 应该写:
{
"dependencies": {
"@zero-one/utils": "workspace:*"
}
}错误 5:turbo build 没有配置 outputs
如果 outputs 配错,Turborepo 不知道哪些文件是构建产物,缓存效果会变差。
常见前端产物:
{
"outputs": ["dist/**", ".output/**", ".nuxt/**"]
}不同框架产物不一样:
| 框架 | 常见产物 |
|---|---|
| Vite | dist/** |
| Nuxt | .output/, .nuxt/ |
| Next.js | .next/** |
错误 6:一开始就抽太多共享包
共享包不是越多越好。
抽包会带来新的维护成本:
- 包名要维护
- 入口要维护
- 构建产物要维护
- 版本或依赖关系要维护
- 改共享包会影响多个应用
更稳的判断是:
重复出现 2 次,可以先观察。
重复出现 3 次,而且改起来烦,再抽。错误 7:一次性重构整个仓库
这是最容易翻车的。
更稳的顺序是:
先 workspace
再统一 scripts
再抽共享包
再接 turbo
最后优化 CI每一步都能单独验证,出问题也容易回退。
11. 命令速查表
| 命令 | 作用 |
|---|---|
pnpm install | 安装整个 workspace 的依赖 |
pnpm -r build | 对所有 workspace package 执行 build |
pnpm --filter website dev | 只启动 website |
pnpm --filter zero-ai build | 只构建 zero-ai |
pnpm add lodash --filter website | 给 website 安装 lodash |
pnpm add @zero-one/utils --filter website | 给 website 添加本地共享包 |
pnpm add -D turbo -w | 在根目录安装 Turborepo |
pnpm turbo build | 用 Turborepo 构建所有相关项目 |
pnpm turbo build --filter=website | 只构建 website |
pnpm turbo build --filter=website... | 构建 website 以及它依赖的包 |
12. 推荐学习顺序
不要一上来就背配置。
按这个顺序学会更稳:
- 先理解为什么多个项目会需要放一起。
- 再理解“放一个文件夹”和“monorepo 管理”有什么区别。
- 再学
pnpm-workspace.yaml怎么声明 workspace。 - 再学
workspace:*怎么引用本地包。 - 再学
--filter怎么只操作某个项目。 - 最后学 Turborepo 的任务依赖、缓存和过滤执行。
如果你是在维护已有项目,推荐实践顺序是:
整理目录
统一项目名
统一 scripts
增加 pnpm-workspace.yaml
确认每个项目还能单独启动
抽第一个 packages/utils
再接入 turbo.json13. 最后总结
| 概念 | 简单意思 | 你要记住 |
|---|---|---|
| monorepo | 多项目放一个仓库 | 它是组织方式,不是工具 |
| pnpm workspace | 管多个包和本地依赖 | 它解决包之间怎么引用 |
| Turborepo | 管任务和缓存 | 它解决怎么更快、更有顺序地跑命令 |
workspace:* | 使用本地 workspace 包 | 不去远程 npm 找 |
--filter | 只操作指定项目 | 日常开发很常用 |
turbo.json | Turborepo 配置文件 | 定义任务关系和缓存产物 |
最重要的一句话:
先判断多个项目有没有共享和统一管理的需要。
有需要,再用 monorepo 把项目组织起来。
然后用 pnpm workspace 把依赖关系管清楚。
等任务变多、变慢,再用 Turborepo 把任务跑聪明。不要反过来。
工具是为项目服务的,不是项目为了工具服务。