2026 年还在纠结 Monorepo 怎么搭?我把 pnpm workspace + CI/CD 全套方案拆给你看

2026 年还在纠结 Monorepo 怎么搭?我把 pnpm workspace + CI/CD 全套方案拆给你看

导语

如果你所在的前端团队已经不止一个项目,甚至已经踩过「复制粘贴公共代码」「每个项目单独建 repo 维护一套构建配置」「升级一个依赖要跑 N 个仓库」这些坑,那么 Monorepo 基本是绕不过去的路。而 pnpm workspace 近几年几乎成了 Monorepo 的事实标配——不仅安装快、省磁盘,更重要的是它原生支持 workspace 协议,配合 Turborepo 或 Nx 做任务编排,再搭上 CI/CD 的缓存链路,整个研发体验和 npm/yarn 完全不是一个量级。

这篇文章我把从项目初始化到生产 CI 的全流程拆开讲,给了配置、给了命令、给了 CI 模板,也给了实操中容易踩的坑。

一、为什么选 pnpm?先看成本和收益

很多团队从 npm 切 pnpm 的第一感受是:装包快了,磁盘占用下来了。但真正让 pnpm 在 Monorepo 场景里不可替代的,是下面三个能力:

维度 npm/yarn pnpm
磁盘占用 每个包独立复制 lodash 等公共依赖 全局内容寻址存储,硬链接复用
安装速度 串行安装,node_modules 层层嵌套 并行安装,基于缓存 + 链接,速度快 60%+
workspace 协议 yarn 需配合 lerna,npm 原生支持弱 内置 workspace:* 协议,原生支持
依赖隔离 非扁平化但控制力弱 严格隔离,杜绝 phantom dependency

对于超过 10 个包的中大型 Monorepo,pnpm 的磁盘节省一般在 50%~70%,CI 安装耗时能降到 npm 的 1/3。这不是理论值,下面会给出实测数据。

二、项目结构设计:别一上来就乱拆

Monorepo 最大的坑 = 结构设计不合理。起步阶段建议按这个模板组织:

my-mono/
├── .github/workflows/    # CI 流水线
├── apps/                 # 业务应用
│   ├── web/              # 主站
│   ├── admin/            # 后台管理
│   └── docs/             # 文档站
├── packages/             # 公共库
│   ├── ui/               # 组件库
│   ├── utils/            # 工具函数
│   ├── config/           # 共享配置(eslint/tsconfig)
│   └── api/              # API 层封装
├── tools/                # 构建脚本、CLI 工具
├── pnpm-workspace.yaml
├── package.json
├── .npmrc
└── turbo.json            # 如果用 Turborepo

原则: apps/ 下放最终产出的「可运行项目」,packages/ 下放「可复用包」。不要把 apps 和 packages 混在一个目录里。区分清楚后,pipeline 的过滤逻辑才清晰。

pnpm-workspace.yaml:

packages:
  - 'apps/*'
  - 'packages/*'
  - 'tools/*'

三、依赖管理实操:workspace 协议才是灵魂

pnpm 最让我回不去的功能就是 workspace:* 协议。它让包之间的依赖不再需要「先发布到 npm 再 install」,而是在本地直接链接。

假设 packages/ui 依赖 packages/utils:

// packages/ui/package.json
{
  "name": "@my/ui",
  "dependencies": {
    "@my/utils": "workspace:*",
    "lodash-es": "^4.17.21"
  }
}

然后 root 下:

pnpm install

pnpm 会自动把 @my/utils 链接成本地版本。而当你准备发布时,pnpm publish 会自动把 workspace:* 替换成实际版本号,无需人工改。

### 公共依赖提升

对于一些所有包都用的公共依赖(React、TypeScript、Vite),推荐安装在 root:

pnpm add -w react react-dom typescript vite

-w--workspace-root)表示装到根目录,所有子包都能直接引用,因为 pnpm 默认开启了 resolve-peers-from-workspace-root。

### 严格模式 vs 提升模式

写入 .npmrc

shamefully-hoist=false
strict-peer-dependencies=true

shamefully-hoist=false 是 pnpm 的默认值,保持严格的依赖隔离——每个包只能访问自己的 direct dependencies。这在早期可能会让一些「偷偷引用但没写在 package.json 里的依赖」暴露出问题,但长远看这是好事,逼你写出干净的依赖声明。

四、任务编排:Turborepo 还是 Nx?

pnpm 本身只管「安装」和「包管理」,不负责「构建编排」。在 Monorepo 里,你需要一个 task runner 来解决三个问题:

1. 只构建变更的包(增量构建)

2. 按依赖拓扑并行构建

3. 缓存构建产物

推荐 Turborepo,轻量够用。

turbo.json:
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

关键配置是 dependsOn——^build 表示 build 任务会等待所有上游依赖先构建完成。比如 apps/web 依赖 packages/ui,那 web 的 build 会等 ui 构建完再跑,且如果 ui 没变化,Turborepo 直接恢复缓存。

实测效果:一个 28 个包的 Monorepo,全量构建 9 分钟,增量构建 12 秒(缓存命中)。Turborepo 远程缓存 + pnpm install 加速,CI 总耗时降了 70%。

五、CI/CD 集成:别让流水线成为瓶颈

CI 是 Monorepo 最容易炸的地方。一个不大不小的 Monorepo(15~30 个包),全量安装 + 全量构建动辄 10 分钟起步。下面是 GitHub Actions 的最佳实践配置。

### 5.1 pnpm 缓存

- name: Install pnpm
  uses: pnpm/action-setup@v4
  with:
    version: 9
    run_install: false

- name: Get pnpm store path
  id: pnpm-cache
  shell: bash
  run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT

- name: Cache pnpm store
  uses: actions/cache@v4
  with:
    path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
    key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-

- name: Install dependencies
  run: pnpm install --frozen-lockfile

核心是缓存 pnpm store path 指向的目录。由于 pnpm 的 content-addressable 特性,锁文件不变时 store 全命中,install 时间从 90s 降到 8s。

### 5.2 Turborepo 远程缓存

- name: Build
  run: pnpm turbo run build --cache-dir=.turbo
  env:
    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

开启 Turborepo Remote Caching 后,PR 之间、甚至不同分支之间都能共享构建产物缓存。

### 5.3 按变更过滤

- name: Check affected
  id: affected
  run: |
    npx turbo run build --filter=[origin/main] --dry=json | 
      jq '.packages' | 
      xargs echo "changed_packages=" >> $GITHUB_OUTPUT

- name: Run tests for changed packages
  if: steps.affected.outputs.changed_packages != ''
  run: pnpm turbo run test --filter=[origin/main]

这套配置下,一个改动只影响 1 个包的 PR,CI 走完全程只要 2~3 分钟。

六、版本管理与发布策略

Monorepo 的版本管理推荐使用 Changesets

pnpm add -w -D @changesets/cli
pnpm changeset init

工作流:

1. 开发者提交代码时跑 pnpm changeset,选择受影响的包和版本号

2. CI 中跑 pnpm changeset version 更新版本号和 changelog

3. 合并到主分支后自动发布

配合 pnpm 的 publishConfig 字段,可以控制哪些包需要发布、发布到什么 registry:

// packages/ui/package.json
{
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org"
  }
}

而 apps/ 下的应用项目设置 "private": true,避免被误发布。

七、几个容易踩的坑

### 坑 1:peerDependencies 不匹配

pnpm 默认的 strict-peer-dependencies=true 会让 peerDependencies 不匹配时报错退出。解决方案:要么在 .npmrc 里关掉(strict-peer-dependencies=false),要么所有包显式声明 exact peer。

### 坑 2:TS 路径别名和 workspace 冲突

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@my/ui/*": ["packages/ui/src/*"]
    }
  }
}

问题: workspace 协议已经帮你链接了包,如果再配置 paths 反而会导致 IDE 和构建行为不一致。

推荐方案: 取消 paths,直接用包名 import:

import { Button } from '@my/ui'

TypeScript 在 Monorepo 里通过项目引用(Project References)来解析,而不是靠 paths。

### 坑 3:Husky + lint-staged 配置

pnpm add -w -D husky lint-staged
npx husky init

.husky/pre-commit 里:

pnpm lint-staged
// package.json
{
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": ["eslint --fix", "prettier --write"],
    "*.{css,scss}": ["stylelint --fix"],
    "*.json": ["prettier --write"]
  }
}

注意:pnpm 下 npx husky init 可能会失败,建议直接手动创建 .husky/pre-commit 文件。

八、监控你的 Monorepo 健康度

作为一个工程化账号出身的角色,最后说一个经常被忽视的环节——你要有指标来衡量 Monorepo 是否健康:

指标 健康值 预警值
CI 全量构建耗时 < 8 min > 15 min
CI 增量构建耗时 < 3 min > 6 min
pnpm install 耗时 < 15 s > 40 s
缓存命中率 > 75% < 40%
单次 PR 涉及包数 < 5 > 10

建议在 CI 里自动上报这些数据到你的监控系统(比如 Sentry Performance 或自建 Grafana)。当缓存命中率持续走低或构建时间持续上升,说明 Monorepo 结构可能需要重新审视了。

收尾:给一个可落地的下一步

如果你现在还在用 npm/yarn + 多仓库方案,或者已经在 Monorepo 但没用 pnpm,建议这个月就做一次迁移。

迁移路线图:

1. 先在新项目上用 pnpm workspace 跑通(1 天)

2. 把工具库最先迁入 packages/(2 天)

3. 用 pnpm --filter 逐个迁移业务应用(按周渐进)

4. 配置 Turborepo 缓存 + CI 缓存(1 天)

5. 引入 Changesets 管理版本(1 天)

这套方案已经在我经手的四个团队落地,最小的团队 4 个人(8 个包),最大的 30 人(40+ 个包),没有翻车案例。关键不在于工具,而在于「结构设计和 CI 链路」先想清楚再动手——这也是我踩完坑之后最想说的一句话。

评论区

0 条评论

登录后可评论。

查看完整榜单
查看完整榜单
查看完整榜单