Skip to content

pnpm + Turborepo 通俗实战指南

我们将抛开晦涩的术语,用**"乐高积木""中央厨房"**的比喻,带你从零开始理解并搭建一个现代化的 Monorepo(单体仓库)。


第一部分:核心概念通俗版

1. 为什么需要 Monorepo(单体仓库)?

想象你开了一家连锁餐厅(你的公司),你有:

  • APP A(网页版点餐)
  • APP B(小程序点餐)
  • 共享组件库(按钮、菜单样式、Logo)
  • 共享工具库(计算价格、验证用户)

传统做法(多仓库): 你把"按钮"代码复制一份到 APP A,再复制一份到 APP B。

  • 痛点:如果你要改按钮颜色,得去两个地方改,改漏了一个就出 Bug。而且依赖包(如 React)在每个项目里都下载一遍,硬盘爆炸。

Monorepo 做法: 所有代码放在一个大仓库里。

  • 优势:按钮代码只写一次,A 和 B 直接引用。改一处,全生效。

2. pnpm:超级省空间的"仓库管理员"

npm/yarn 的做法: 每个项目都在自己的 node_modules 里把依赖包完整拷贝一份。

  • 🏠 项目 A:存了一份 React (100MB)
  • 🏠 项目 B:存了一份 React (100MB)
  • 结果:浪费空间,安装慢。

pnpm 的做法(硬链接 + 符号链接): pnpm 在全局建了一个"中央仓库",所有包只存一份

  • 🏢 中央仓库:存了一份 React (100MB)
  • 🏠 项目 A:放了一个"快捷方式"指向中央仓库。
  • 🏠 项目 B:放了一个"快捷方式"指向中央仓库。
  • 结果:速度极快,节省 50%+ 磁盘空间。而且它很严格,你没声明的依赖,它不让你用(防止"幽灵依赖")。

3. Turborepo:聪明的"施工队长"

当你运行 build(构建)命令时:

普通做法: 队长傻傻地按顺序来:先建 A,等 A 完了,再建 B,再建 C... 哪怕 B 和 C 跟 A 没关系,也得排队。

Turborepo 的做法: 队长手里有一张地图(依赖图)

  1. 并行:它发现 A 和 B 互不影响,于是大喊:"A 队和 B 队,同时开工!"
  2. 缓存:它记得"上次 A 队的代码没变过",于是直接拿出上次的成品:"A 队休息,直接用旧成果!"
  3. 依赖感知:它知道 C 依赖 A,所以会等 A 完工后,再让 C 开工。

第二部分:手把手实战教程

我们假设你要做一个项目:包含一个 官网 (Web) 和一个 共享 UI 库 (UI)

步骤 1:初始化项目(一键生成)

打开终端,运行官方提供的创建命令(这是最快最稳的方式):

bash
# 使用 pnpm 创建名为 "my-awesome-app" 的项目
pnpm create turbo@latest my-awesome-app

# 过程中会让你选择包管理器,选 pnpm
# 还会问你要什么模板,选 "Default" (包含 Next.js 示例) 或 "Basic"

进入目录看看结构:

text
my-awesome-app/
├── apps/              # 【应用层】具体的网站或 App
│   ├── web/           # 例如:你的主官网
│   └── docs/          # 例如:文档站
├── packages/          # 【库层】共享的代码
│   ├── ui/            # 共享的按钮、输入框等组件
│   └── eslint-config/ # 共享的代码规范
├── turbo.json         # 【核心】告诉 Turborepo 怎么干活
├── pnpm-workspace.yaml# 【核心】告诉 pnpm 哪些文件夹是一伙的
└── package.json       # 根配置文件

步骤 2:理解"工作区" (Workspace)

打开 pnpm-workspace.yaml,你会看到:

yaml
packages:
  - 'apps/*'
  - 'packages/*'

人话翻译

"pnpm 听好了,apps 文件夹里的所有子文件夹,和 packages 文件夹里的所有子文件夹,都是我的自家人。它们之间可以互相引用,不用发布到 npm 网上也能用。"

步骤 3:如何让"应用"引用"共享库"?

这是 Monorepo 最核心的操作。假设你想在 apps/web 里使用 packages/ui 里的按钮。

  1. 找到 apps/web/package.json

  2. 添加依赖

    json
    {
      "name": "web",
      "dependencies": {
        "@my-org/ui": "workspace:*"
      }
    }

    关键点workspace:* 这个写法非常特殊。

    • 它的意思是:"别去 npm 网上下载 @my-org/ui 了,直接去我们本地仓库里找那个叫 ui 的包链接过来!"
    • 这样你在 packages/ui 里改一行代码,apps/web 立马就能感觉到(开发模式下)。
  3. 安装依赖: 回到项目根目录,运行:

    bash
    pnpm install

    pnpm 会自动把本地链接打通。

步骤 4:配置"施工队长" (turbo.json)

打开 turbo.json,这是大脑。让我们看懂默认配置:

json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {}
  }
}

逐行解读

  • "build" 任务
    • "dependsOn": ["^build"]:那个 ^ 符号很重要!意思是**"先构建我的上游依赖"**。
      • 如果你运行 web 的 build,因为它依赖 ui,Turborepo 会自动先运行 ui 的 build,然后再运行 web 的 build。你不需要手动分两步走。
    • "outputs":告诉 Turbo,构建完后生成的文件夹是 .nextdist。Turbo 会把这些文件夹打包存进缓存。下次代码没变,直接解压复用,秒完成。
  • "dev" 任务
    • "cache": false:开发模式不要缓存,我要实时看变化。
    • "persistent": true:这是一个长期运行的命令(服务器不会停),别把它当成一次性任务。

步骤 5:日常开发命令大全

所有命令都在根目录运行,Turbo 会帮你分发。

你想做什么命令发生了什么
启动所有项目pnpm dev同时启动 webdocs 等的本地服务器。占用多个端口,并行运行。
只启动官网pnpm dev --filter=web只启动 apps/web。适合只想调试某个项目时。
构建所有项目pnpm build智能顺序:先建 ui,再建 web。利用缓存加速。
检查代码规范pnpm lint并行检查所有项目的代码风格。
查看任务图谱pnpm turbo run build --graph神器! 它会生成一张图片,展示任务之间的依赖关系,帮你排查谁依赖谁。

第三部分:进阶技巧与避坑指南

1. 怎么发布我的共享库到 npm?

在 Monorepo 里发布多个包很麻烦(版本号要同步,Changelog 要写)。

推荐方案:Changesets

它是目前业界标准。

  1. 安装:pnpm add -D @changesets/cli
  2. 初始化:pnpm changeset init
  3. 每次改代码后,运行 pnpm changeset,它会问你改了哪个包,是 major/minor/patch 版本。
  4. 准备发布时,运行 pnpm changeset version(自动改版本号)-> pnpm changeset publish(自动发布到 npm)。

2. 常见报错与解决

  • 报错:ERR_PNPM_WORKSPACE_PKG_NOT_FOUND

    • 原因:你在 package.json 里写了 "@my-org/ui": "workspace:*",但 pnpm 找不到这个包。
    • 解决:检查 packages/ui/package.json 里的 name 字段是不是真的叫 @my-org/ui?检查 pnpm-workspace.yaml 有没有包含 packages/*
  • 报错:TypeScript 找不到类型

    • 原因:TS 编译器有时候比较笨,不知道本地链接的存在。
    • 解决:确保你的 tsconfig.json 使用了 references 或者配置了 paths。通常 Turborepo 模板里已经配好了 baseUrl: "."paths。如果不行,尝试在根目录运行 pnpm install 刷新一下链接。
  • 问题:我想加一个新的 React 包到 UI 库,会影响官网吗?

    • 答案:会。因为 web 依赖 ui
    • 优化:Turborepo 很聪明。如果你只改了 ui 里的一个文件,运行 pnpm build 时,它只会重新构建 ui 和依赖 uiweb。其他的 docs 如果没依赖 ui 或者代码没变,它根本不会动。

3. 目录命名建议

为了清晰,建议遵循以下约定:

  • apps/:放最终产物(能跑的服务器、网站、App)。
    • apps/web (Next.js)
    • apps/mobile (Expo/React Native)
  • packages/:放纯代码库(不能直接跑,只能被引用)。
    • packages/ui (React 组件)
    • packages/utils (纯函数工具)
    • packages/config (共享的 tsconfig, eslint 配置)

总结:为什么要坚持用这套组合?

  1. 开发体验爽:改一个组件,所有用到它的页面即时更新。
  2. 省钱省资源:pnpm 让你的 CI/CD 服务器磁盘压力减半,安装时间减半。
  3. 构建飞快:Turborepo 的缓存机制,让第二次构建通常在 1 秒内完成(只要代码没变)。
  4. 逻辑清晰:强制你把代码拆分成"应用"和"库",避免了 spaghetti code(面条代码)。

一句话口诀

大仓管理用 pnpm,构建加速靠 Turbo; > 内部引用 workspace,依赖顺序它做主。