使用 pnpm + Workspaces 构建 Monorepo 的完整指南

一、核心概念: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 不会被意外发布
  • 可以移除 maintest等字段,因为根目录通常不包含业务代码

步骤 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.jsonworkspaces字段

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 的目录

两种方式的优先级与兼容性

  1. pnpm 的读取顺序

    • 优先读取 pnpm-workspace.yaml
    • 如果不存在,则读取 package.json中的 workspaces字段
    • 如果两者都存在,pnpm-workspace.yaml优先级更高
  2. 推荐使用 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-appadmin-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 的主要优势包括:

  1. 依赖共享:所有包共享同一份依赖,节省磁盘空间和安装时间
  2. 原子提交:跨包变更可以一次性提交,保持一致性
  3. 本地链接 :使用 workspace:*协议,本地开发无需发布到 npm
  4. 精细控制:支持按包过滤的命令执行
  5. 统一管理:集中的 CI/CD 和代码质量检查

这种架构特别适合中大型项目、微前端架构、组件库开发等场景,能显著提升开发效率和代码复用性。

相关推荐
翔云 OCR API6 小时前
API让文档信息“活”起来:通用文档识别接口-开发者文字识别API
前端·数据库·人工智能·mysql·ocr
cypking6 小时前
Web常见安全漏洞全解析(含案例+前后端实操防御方案)
前端
shuaijie05186 小时前
两个表格进行相互联动
前端·javascript·vue.js
JS_GGbond6 小时前
让代码学会“等外卖”:JavaScript异步编程趣谈
前端·javascript
一点晖光6 小时前
小程序中web-view加载uni-app H5如何使用postMessage方法的解决方案
前端·小程序·uni-app
随风一样自由6 小时前
React编码时,什么时候用js文件,什么时候用jsx文件?
开发语言·javascript·react.js
AI分享猿6 小时前
雷池 WAF vs React 高危漏洞:1 毫秒检测延迟,护住全栈业务安全
前端·安全·react.js
开发者小天6 小时前
react中todolist小案例
前端·react.js·前端框架
MQliferecord6 小时前
如何实现倒计时工具
前端