一、核心概念:pnpm Workspace
pnpm 内置了对 Monorepo(单一代码仓库)的原生支持,通过 Workspace(工作区) 机制实现。Workspace 允许你在一个仓库中管理多个相互关联但独立的项目(包),并智能地处理它们之间的依赖关系。
二、将普通仓库转变为 Monorepo 的步骤
步骤 1:初始化项目结构
perl
# 创建项目根目录
mkdir my-monorepo
cd my-monorepo
# 初始化根目录 package.json
pnpm init
步骤 2:配置根目录 package.json
修改根目录的 package.json,关键配置如下:
ruby
{
"name": "my-monorepo",
"version": "1.0.0",
"private": true, // 必须设置为 true,避免误发布到 npm
"scripts": {
"dev": "pnpm -r run dev", // -r 表示递归执行所有子包
"build": "pnpm -r run build",
"test": "pnpm -r run test"
},
"keywords": [],
"author": "",
"license": "ISC"
}
重要说明:
"private": true是必须的,确保整个 Monorepo 不会被意外发布- 可以移除
main、test等字段,因为根目录通常不包含业务代码
步骤 3:创建 pnpm-workspace.yaml 配置文件
在根目录创建 pnpm-workspace.yaml文件,这是 pnpm Workspace 的核心配置文件:
bash
# pnpm-workspace.yaml
packages:
# packages 目录下的所有直接子目录
- 'packages/*'
# apps 目录下的所有直接子目录
- 'apps/*'
# components 目录下的所有层级子目录
- 'components/**'
# 排除包含 test 的目录
- '!**/test/**'
配置说明:
packages/*:匹配packages目录下的所有一级子目录apps/*:匹配apps目录下的所有一级子目录components/**:匹配components目录下的所有层级子目录!**/test/**:排除所有包含test的目录
pnpm 中的两种配置方式
方式一:使用 package.json的 workspaces字段
json
// package.json
{
"name": "my-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"dev": "pnpm -r run dev"
}
}
方式二:使用 pnpm-workspace.yaml文件
bash
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- '!**/test/**' # 排除包含 test 的目录
两种方式的优先级与兼容性
-
pnpm 的读取顺序:
- 优先读取
pnpm-workspace.yaml - 如果不存在,则读取
package.json中的workspaces字段 - 如果两者都存在,
pnpm-workspace.yaml优先级更高
- 优先读取
-
推荐使用
pnpm-workspace.yaml的原因:- 更丰富的配置选项 :支持排除模式(
!**/test/**) - 更好的可读性:YAML 格式更适合复杂配置
- 工具兼容性:明确标识为 pnpm 工作区
- 未来扩展性:pnpm 的新功能会优先在 YAML 配置中支持
- 更丰富的配置选项 :支持排除模式(
步骤 4:创建子项目结构
典型的 Monorepo 目录结构如下:
csharp
my-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── pnpm-lock.yaml
├── packages/
│ ├── shared-utils/ # 共享工具库
│ │ ├── package.json
│ │ └── src/
│ ├── ui-components/ # UI 组件库
│ │ ├── package.json
│ │ └── src/
│ └── core-lib/ # 核心库
│ ├── package.json
│ └── src/
├── apps/
│ ├── web-app/ # 前端应用
│ │ ├── package.json
│ │ └── src/
│ └── mobile-app/ # 移动应用
│ ├── package.json
│ └── src/
└── docs/ # 文档
步骤 5:配置子项目的 package.json
每个子项目都需要有自己的 package.json,关键配置如下: 示例:共享工具库 (packages/shared-utils/package.json)
perl
{
"name": "@my-monorepo/shared-utils",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
示例:前端应用 (apps/web-app/package.json)
perl
{
"name": "web-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@my-monorepo/shared-utils": "workspace:*", // 关键:引用本地包
"@my-monorepo/ui-components": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"vite": "^5.0.0"
}
}
三、关键配置详解
1. workspace:* 协议
这是 pnpm Workspace 的核心特性,用于声明对本地其他包的依赖:
perl
{
"dependencies": {
"@my-monorepo/shared-utils": "workspace:*", // 使用最新版本
"@my-monorepo/ui-components": "workspace:^1.0.0", // 指定版本范围
"@my-monorepo/core-lib": "workspace:../packages/core-lib" // 相对路径
}
}
作用:
- 建立本地包之间的软链接,无需发布到 npm
- 修改本地包时,依赖它的项目能立即看到变化
- 确保所有包使用同一份依赖,避免重复安装
2. 依赖安装与管理
在根目录安装全局依赖(所有包共享)
csharp
# 安装到根目录,所有包共享
pnpm add typescript -w
# 或
pnpm add typescript --workspace-root
为特定包安装依赖
csharp
# 为 web-app 安装 react
pnpm add react --filter web-app
# 或
pnpm add react -F web-app
# 为多个包安装依赖
pnpm add axios --filter "web-app" --filter "mobile-app"
安装本地包依赖
sql
# 在 web-app 中安装 shared-utils
pnpm add @my-monorepo/shared-utils --filter web-app
3. 脚本执行
在所有包中执行相同脚本
bash
# 递归执行所有包的 build 脚本
pnpm -r run build
# 递归执行所有包的 test 脚本
pnpm -r run test
在特定包中执行脚本
bash
# 仅在 web-app 中执行 dev 脚本
pnpm --filter web-app run dev
# 使用包名(package.json 中的 name)
pnpm -F @my-monorepo/shared-utils run build
四、完整示例:Vue 项目 Monorepo
项目结构
lua
vue-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── packages/
│ ├── ui-lib/ # UI 组件库
│ │ ├── package.json
│ │ ├── src/
│ │ └── vite.config.ts
│ └── utils/ # 工具函数库
│ ├── package.json
│ └── src/
└── apps/
├── admin/ # 后台管理系统
│ ├── package.json
│ └── src/
└── portal/ # 门户网站
├── package.json
└── src/
pnpm-workspace.yaml
vbnet
packages:
- 'packages/*'
- 'apps/*'
根目录 package.json
json
{
"name": "vue-monorepo",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "pnpm -r run dev",
"build": "pnpm -r run build",
"lint": "pnpm -r run lint"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
包间依赖示例 (apps/admin/package.json)
perl
{
"name": "admin",
"version": "1.0.0",
"private": true,
"dependencies": {
"vue": "^3.3.0",
"@vue-monorepo/ui-lib": "workspace:*",
"@vue-monorepo/utils": "workspace:*"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"vite": "^5.0.0"
}
}
五、最佳实践与注意事项
1. 命名规范
- 根项目:使用项目总名称,如
my-monorepo - 子包:使用作用域名称,如
@my-monorepo/ui-components - 应用:使用描述性名称,如
web-app、admin-console
2. 依赖管理
- 公共依赖(如 TypeScript、ESLint)安装在根目录
- 业务依赖安装在各自包中
- 使用
pnpm-lock.yaml确保依赖一致性
3. 版本控制
- 提交
pnpm-lock.yaml到版本控制系统 - 考虑使用 Changesets 或 Lerna 进行版本管理和发布
4. 性能优化
- pnpm 使用硬链接和符号链接,节省磁盘空间
- 所有包共享同一份依赖,安装速度快
- 支持过滤命令,只构建需要的包
5. CI/CD 集成
yaml
# GitHub Actions 示例
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- run: pnpm install
- run: pnpm -r run build
- run: pnpm -r run test
六、常见问题解决
1. 幽灵依赖问题
确保所有依赖都在 package.json中明确声明,避免直接引用 node_modules中的未声明包。
2. 循环依赖检测
使用 pnpm why <package-name>检查依赖关系,避免包之间的循环依赖。
3. 包找不到错误
如果出现 no matches found错误,检查:
- 包名是否正确(包括作用域)
- 包是否在
pnpm-workspace.yaml配置的目录中 - 包是否有正确的
name字段
4. 跨包类型引用
对于 TypeScript 项目,配置 tsconfig.json中的 paths:
perl
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@my-monorepo/*": ["packages/*/src"]
}
}
}
总结
通过 pnpm + Workspaces 构建 Monorepo 的主要优势包括:
- 依赖共享:所有包共享同一份依赖,节省磁盘空间和安装时间
- 原子提交:跨包变更可以一次性提交,保持一致性
- 本地链接 :使用
workspace:*协议,本地开发无需发布到 npm - 精细控制:支持按包过滤的命令执行
- 统一管理:集中的 CI/CD 和代码质量检查
这种架构特别适合中大型项目、微前端架构、组件库开发等场景,能显著提升开发效率和代码复用性。