pnpm Workspaces:前端 monorepo 实战
monorepo 在前端圈越来越流行。pnpm 的 workspaces 功能是目前最轻量的 monorepo 方案,不需要学 Nx 或 Turborepo 那套复杂的工具链。
为什么选 monorepo
在前端项目里,monorepo 的典型场景:
- 前端应用 + 后端 API 在同一个仓库
- 多个应用共享组件库、工具库
- 微前端项目,多个子应用共享依赖
好处:
- 代码共享方便:直接 import,不用发包
- 依赖版本统一:所有子项目用同一个 React 版本
- 重构安全:改了共享库,所有使用方一起改,不会遗漏
- CI/CD 统一:一个仓库一套流水线
pnpm workspace 基础配置
目录结构
my-project/
pnpm-workspace.yaml # workspace 配置
package.json # 根 package.json
packages/
apps/
web/ # 前端应用
admin/ # 管理后台
libs/
ui/ # UI 组件库
utils/ # 工具函数
types/ # 类型定义
pnpm-workspace.yaml
packages:
- 'packages/*'
- 'packages/apps/*'
- 'packages/libs/*'
这告诉 pnpm 哪些目录是 workspace 的成员。
根 package.json
{
"name": "my-monorepo",
"private": true,
"scripts": {
"dev:web": "pnpm --filter web dev",
"dev:admin": "pnpm --filter admin dev",
"build": "pnpm -r build",
"lint": "pnpm -r lint",
"test": "pnpm -r test"
},
"devDependencies": {
"typescript": "^5.0.0",
"eslint": "^9.0.0"
}
}
--filter 指定在哪个子项目运行命令。-r 表示递归,在所有子项目运行。
工作区依赖
引用 workspace 包
// packages/apps/web/package.json
{
"dependencies": {
"@my/ui": "workspace:*",
"@my/utils": "workspace:*",
"@my/types": "workspace:*"
}
}
workspace:* 表示使用 workspace 里的最新版本。pnpm 会创建符号链接,不用真正安装。
引用 workspace 包的类型
TypeScript 配置:
// tsconfig.json(根配置)
{
"references": [
{ "path": "packages/libs/ui" },
{ "path": "packages/libs/utils" },
{ "path": "packages/libs/types" }
]
}
// packages/libs/ui/tsconfig.json
{
"compilerOptions": {
"composite": true, // 启用项目引用
"outDir": "./dist"
}
}
这样跨包引用时,TypeScript 可以正确推导类型。
常用命令
# 在 web 应用启动开发服务器
pnpm --filter web dev
# 在所有子项目安装依赖
pnpm install
# 在所有子项目运行 build
pnpm -r build
# 按拓扑顺序构建(依赖先构建)
pnpm -r --topological-sort build
# 给指定包添加依赖
pnpm --filter web add react
# 给所有子项目添加依赖
pnpm -r add eslint -D
# 查看 workspace 的依赖关系
pnpm list --depth 0 -r
依赖提升和幽灵依赖
pnpm 使用内容寻址存储和符号链接。每个包只能看到自己声明的依赖(和 npm/yarn 不同)。
// 如果 web 包没有声明 lodash,即使它安装在了 node_modules 里也无法引入
import _ from 'lodash'; // 报错:Cannot find module 'lodash'
这是好事,避免了"幽灵依赖"问题。你需要什么就在 package.json 里声明什么。
如果确实需要在 workspace 里共享一个没有在 package.json 里声明的依赖(比如构建工具),可以用 .npmrc:
# .npmrc
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
版本管理
monorepo 里的包版本管理可以用 changeset:
# 安装
pnpm add -Dw @changesets/cli
# 初始化
pnpm changeset init
# 创建变更记录
pnpm changeset
# 交互式选择:哪些包变了,变更是 major/minor/patch
# 版本更新
pnpm changeset version
# 生成 changelog
pnpm changeset publish
构建缓存
monorepo 最大的痛点是构建速度。pnpm 本身没有提供构建缓存,但可以配合 Turborepo 使用:
# 安装 Turborepo
pnpm add -Dw turbo
# turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Turborepo 会缓存构建结果,如果源码没变就跳过构建。在大型 monorepo 里效果很明显。
不需要 monorepo 的情况
monorepo 有成本——配置复杂、CI 变慢、git 历史混乱。以下情况不建议用:
- 单项目:一个前端应用,不需要拆分
- 团队很小:2-3 人的团队,monorepo 的管理成本大于收益
- 项目之间没有关联:只是图方便放在一起
monorepo 适合的是有一定规模、多个关联项目、需要共享代码的团队。如果只是个人博客或者简单的前端项目,单仓库就够了。
pnpm workspace 是最简单的 monorepo 方案,比 Lerna 和 Nx 轻量很多。对于中小型前端 monorepo 来说,pnpm workspace + changeset 就够了。