跳转到内容

pnpm workspace 与 monorepo 实战教程 2026

pnpm workspace 与 monorepo 架构

💡 什么是 Monorepo? Monorepo(单一仓库)是一种项目管理策略,将多个相关项目放在同一个仓库中管理。pnpm workspace 是实现 Monorepo 的最佳工具之一。

本文将带你从 0 到 1 掌握:

  • ✅ Monorepo 架构设计原则
  • ✅ pnpm workspace 配置详解
  • ✅ 依赖共享与版本管理
  • ✅ 性能优化策略
  • ✅ CI/CD 集成方案
  • ✅ 实际项目案例

一、为什么选择 pnpm workspace?

1.1 Monorepo vs Multi-repo

维度MonorepoMulti-repo
代码共享✅ 模块间轻松共享❌ 需要 npm 发布
依赖管理✅ 统一管理,版本一致❌ 重复安装,版本可能不一致
代码复用✅ 共享组件、工具函数❌ 重复造轮子
CI/CD✅ 增量构建,效率高❌ 各自构建,重复工作
学习成本✅ 一处学习,处处可用❌ 多个仓库,各自为政
仓库大小⚠️ 可能很大✅ 每个仓库较小
权限管理⚠️ 权限较粗粒度✅ 精细权限控制

1.2 pnpm vs npm/yarn

特性pnpmnpmyarn
磁盘空间⭐⭐⭐⭐⭐(硬链接共享)⭐⭐⭐⭐⭐⭐⭐
安装速度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
workspace 支持✅ 原生支持⚠️ 有限支持✅ 支持
安全性⭐⭐⭐⭐⭐(严格隔离)⭐⭐⭐⭐⭐⭐⭐⭐
缓存机制⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
社区支持⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

1.3 pnpm 的核心优势

bash
# pnpm 使用硬链接和符号链接来共享依赖
# 所有版本的相同包只存储一次
pnpm install

# 传统 npm/yarn 每个项目独立安装
npm install
# 或
yarn install

空间对比示例:

bash
# 10 个项目使用相同的 100 个依赖
# npm/yarn: 10 * 100 = 1000 份依赖副本
# pnpm: 100 份依赖(通过硬链接共享)

# 实际空间节省可达 80-90%

二、项目结构设计

2.1 推荐的 monorepo 结构

my-monorepo/
├── .git/                      # Git 仓库
├── .github/                   # GitHub Actions 配置
├── .vscode/                   # VS Code 配置
├── packages/                  # 工作区包目录
│   ├── app/                   # 主应用(可执行)
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   ├── components/            # 共享组件库
│   │   ├── src/
│   │   └── package.json
│   ├── utils/                 # 工具函数库
│   │   ├── src/
│   │   └── package.json
│   └── ui/                    # UI 组件库
│       ├── src/
│       └── package.json
├── apps/                      # 应用目录(可选)
│   └── admin/                 # 管理后台
│       ├── src/
│       └── package.json
├── tools/                     # 工具脚本
│   ├── scripts/
│   └── configs/
├── .gitignore
├── package.json               # 根 package.json
├── pnpm-workspace.yaml        # pnpm workspace 配置
├── tsconfig.json              # 根 TypeScript 配置
├── tsconfig.base.json         # TypeScript 基础配置
└── README.md

2.2 各目录职责说明

目录职责是否必须
packages/可复用的包(组件、工具、UI)
apps/独立运行的应用⚠️ 可选
tools/项目工具脚本和配置⚠️ 可选
pnpm-workspace.yamlworkspace 配置
tsconfig.base.json共享的 TypeScript 配置

三、pnpm workspace 配置详解

3.1 初始化 workspace

bash
# 1. 创建项目目录
mkdir my-monorepo && cd my-monorepo

# 2. 初始化 pnpm workspace
pnpm init

# 3. 创建 pnpm-workspace.yaml
touch pnpm-workspace.yaml

# 4. 创建 packages 目录
mkdir packages apps

3.2 pnpm-workspace.yaml 配置

yaml
# pnpm-workspace.yaml
packages:
  # 所有包目录
  - 'packages/**'
  # 应用目录
  - 'apps/**'
  # 可选:排除某些目录
  - '!**/node_modules'
  - '!**/.git'

# 配置 workspace 根目录的依赖安装
# 这些依赖会安装在根 node_modules
# 可被所有子包共享
# 注意:建议使用 workspace 协议代替

# 启用严格模式(推荐)
# strict: true

# 配置 peer dependencies 自动安装
# auto-install-peers: true

# 配置依赖注入(高级特性)
# inject:
#   - package: eslint
#     version: ^8.0.0

3.3 根 package.json 配置

json
{
  "name": "@my-monorepo/root",
  "private": true,
  "version": "1.0.0",
  "description": "My Monorepo",
  "scripts": {
    "build": "pnpm --filter \"./packages/**\" build",
    "dev": "pnpm --filter @my-monorepo/app dev",
    "test": "pnpm --filter \"./packages/**\" test",
    "lint": "pnpm --filter \"./packages/**\" lint",
    "clean": "pnpm --filter \"./packages/**\" clean",
    "publish": "pnpm --filter \"./packages/**\" publish --access public"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "eslint": "^8.0.0",
    "prettier": "^3.0.0"
  },
  "pnpm": {
    "overrides": {
      "lodash": "^4.17.21",
      "react": "^18.0.0"
    }
  }
}

3.4 子包 package.json 配置

json
// packages/components/package.json
{
  "name": "@my-monorepo/components",
  "version": "1.0.0",
  "description": "共享组件库",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "test": "vitest run",
    "lint": "eslint ."
  },
  "dependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.0",
    "@types/react-dom": "^18.0.0",
    "typescript": "^5.0.0"
  }
}

四、依赖管理策略

4.1 workspace 协议(推荐)

json
// packages/app/package.json
{
  "dependencies": {
    "@my-monorepo/components": "workspace:^",
    "@my-monorepo/utils": "workspace:^",
    "@my-monorepo/ui": "workspace:1.0.0"
  }
}

workspace 协议版本说明:

版本格式含义
workspace:*任意版本
workspace:^兼容版本(^1.0.0)
workspace:~补丁版本(~1.0.0)
workspace:1.0.0精确版本
workspace:../utils相对路径

4.2 安装依赖

bash
# 安装所有依赖(根目录执行)
pnpm install

# 为特定包安装依赖
pnpm --filter @my-monorepo/app install

# 在特定包目录下安装
cd packages/app
pnpm install

# 添加新依赖到特定包
pnpm --filter @my-monorepo/app add lodash

# 添加开发依赖到根目录
pnpm add -Dw typescript eslint

# 添加共享依赖(根目录)
pnpm add -w react react-dom

# 从特定包移除依赖
pnpm --filter @my-monorepo/app remove lodash

# 更新所有依赖
pnpm update

# 更新特定包的依赖
pnpm --filter @my-monorepo/components update

# 检查过时依赖
pnpm outdated

# 清理未使用的依赖
pnpm prune

4.3 依赖安装策略

bash
# 策略 1:根目录安装共享依赖
pnpm add -w react react-dom typescript

# 策略 2:特定包安装专属依赖
pnpm --filter @my-monorepo/app add lodash

# 策略 3:使用 workspace 协议引用内部包
pnpm --filter @my-monorepo/app add @my-monorepo/components@workspace

# 策略 4:安装所有子包的依赖
pnpm install

# 策略 5:只安装生产依赖
pnpm install --prod

# 策略 6:使用 lockfile 精确安装
pnpm install --frozen-lockfile

4.4 依赖缓存优化

bash
# 查看 pnpm 存储路径
pnpm config get store-dir
# 输出: /Users/username/Library/pnpm/store/v3

# 设置自定义存储路径
pnpm config set store-dir /path/to/custom/store

# 清理缓存
pnpm store prune

# 查看缓存大小
pnpm store status

# 验证缓存完整性
pnpm store verify

# 从缓存中删除特定包
pnpm store rm lodash

五、TypeScript 配置

5.1 根 tsconfig.json

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "lib": ["ES2020", "DOM"],
    "baseUrl": ".",
    "paths": {
      "@my-monorepo/*": ["packages/*/src"]
    },
    "types": ["node"]
  },
  "files": [],
  "references": [
    { "path": "./packages/components" },
    { "path": "./packages/utils" },
    { "path": "./packages/ui" }
  ]
}

5.2 子包 tsconfig.json

json
// packages/components/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

5.3 路径别名配置

json
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@my-monorepo/components": ["packages/components/src"],
      "@my-monorepo/utils": ["packages/utils/src"],
      "@my-monorepo/ui": ["packages/ui/src"],
      "@my-monorepo/app/*": ["packages/app/src/*"]
    }
  }
}

使用示例:

typescript
// packages/app/src/App.tsx
import { Button } from '@my-monorepo/components';
import { formatDate } from '@my-monorepo/utils';
import { Card } from '@my-monorepo/ui';

function App() {
  return (
    <Card>
      <Button onClick={() => console.log(formatDate(new Date()))}>
        Click Me
      </Button>
    </Card>
  );
}

六、脚本管理与执行

6.1 根目录脚本

json
{
  "scripts": {
    // 构建所有包
    "build": "pnpm --filter \"./packages/**\" build",
    
    // 构建特定包及其依赖
    "build:app": "pnpm --filter @my-monorepo/app... build",
    
    // 并行构建
    "build:parallel": "pnpm --parallel --filter \"./packages/**\" build",
    
    // 开发模式
    "dev": "pnpm --filter @my-monorepo/app dev",
    
    // 并行开发多个包
    "dev:all": "pnpm --parallel --filter \"./packages/**\" dev",
    
    // 测试
    "test": "pnpm --filter \"./packages/**\" test",
    
    // 测试特定包
    "test:components": "pnpm --filter @my-monorepo/components test",
    
    // 代码检查
    "lint": "pnpm --filter \"./packages/**\" lint",
    
    // 格式化
    "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json}\"",
    
    // 清理构建产物
    "clean": "pnpm --filter \"./packages/**\" clean",
    
    // 发布所有包
    "publish": "pnpm --filter \"./packages/**\" publish --access public",
    
    // 版本管理
    "version:major": "pnpm --filter \"./packages/**\" version major",
    "version:minor": "pnpm --filter \"./packages/**\" version minor",
    "version:patch": "pnpm --filter \"./packages/**\" version patch"
  }
}

6.2 执行脚本的方式

bash
# 执行根目录脚本
pnpm run build

# 执行特定包的脚本
pnpm --filter @my-monorepo/app run dev

# 在包目录下直接执行
cd packages/app
pnpm run dev

# 并行执行多个脚本
pnpm --parallel --filter @my-monorepo/components --filter @my-monorepo/utils run dev

# 执行依赖包的脚本(会先执行依赖构建)
pnpm --filter @my-monorepo/app... run build

# 跳过缓存执行
pnpm --filter @my-monorepo/app run build --no-cache

6.3 脚本执行顺序控制

bash
# 使用 --filter 控制执行顺序
pnpm --filter @my-monorepo/utils build
pnpm --filter @my-monorepo/components build
pnpm --filter @my-monorepo/ui build
pnpm --filter @my-monorepo/app build

# 使用 ... 自动解析依赖顺序
pnpm --filter @my-monorepo/app... build

# 使用 workspace 协议时,pnpm 会自动处理依赖顺序

七、性能优化策略

7.1 缓存策略

bash
# 启用构建缓存(vite/webpack 等)
# vite.config.ts
export default {
  build: {
    cacheDir: '../../node_modules/.vite'
  }
}

# 设置 pnpm 缓存大小限制
pnpm config set store-max-size 10GB

# 清理旧缓存
pnpm store prune

# 使用 faster 模式(跳过某些验证)
pnpm install --faster

# 启用预构建(pnpm 8+)
pnpm install --prefer-offline

7.2 增量构建

bash
# 使用 turbo 进行增量构建
npm install turbo --save-dev

# turbo.json 配置
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    }
  }
}

# 运行 turbo 构建
npx turbo build

# 运行 turbo 测试
npx turbo test

# 强制重新构建
npx turbo build --force

7.3 内存优化

bash
# 设置 Node.js 内存限制
export NODE_OPTIONS="--max-old-space-size=4096"

# 优化 tsconfig 以减少内存使用
{
  "compilerOptions": {
    "skipLibCheck": true,
    "noEmit": true,
    "incremental": true
  }
}

# 使用 swc 替代 tsc 进行更快的编译
pnpm add -D @swc/cli @swc/core

7.4 依赖分析

bash
# 分析依赖树
pnpm ls

# 分析特定包的依赖
pnpm ls --filter @my-monorepo/app

# 查看重复依赖
pnpm dedupe --list

# 执行依赖去重
pnpm dedupe

# 检查未使用的依赖
pnpm install --check

# 查看依赖大小
pnpm why lodash

# 查看依赖统计
pnpm stats

八、CI/CD 集成

8.1 GitHub Actions 配置

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      
      - name: Install pnpm
        run: npm install -g pnpm
      
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      
      - name: Build packages
        run: pnpm build
      
      - name: Run tests
        run: pnpm test
      
      - name: Run lint
        run: pnpm lint

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      
      - name: Install pnpm
        run: npm install -g pnpm
      
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      
      - name: Build and deploy
        run: pnpm deploy

8.2 缓存策略优化

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

- name: Cache build outputs
  uses: actions/cache@v4
  with:
    path: |
      packages/**/dist
      apps/**/dist
    key: ${{ runner.os }}-build-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-build-

8.3 增量 CI 配置

yaml
- name: Run turbo build
  run: npx turbo build --cache-dir=.turbo
  env:
    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

- name: Upload turbo cache
  uses: actions/upload-artifact@v4
  with:
    name: turbo-cache
    path: .turbo

九、常见问题与解决方案

Q1:workspace 协议找不到包?

bash
# 检查包名是否正确
pnpm --filter @my-monorepo/app ls

# 检查 workspace 配置
cat pnpm-workspace.yaml

# 确保包已在 workspace 中
pnpm list --filter "@my-monorepo/*"

# 重新安装依赖
pnpm install

# 检查 package.json 中的 workspace 协议
# 确保格式正确:workspace:^ 或 workspace:*

Q2:TypeScript 路径别名不生效?

json
// 确保配置了正确的路径别名
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@my-monorepo/*": ["packages/*/src"]
    }
  }
}

// 如果使用 Vite,需要配置 vite.config.ts
import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@my-monorepo': path.resolve(__dirname, './packages')
    }
  }
});

Q3:依赖版本冲突?

bash
# 使用 pnpm overrides 强制统一版本
{
  "pnpm": {
    "overrides": {
      "react": "^18.0.0",
      "react-dom": "^18.0.0",
      "lodash": "^4.17.21"
    }
  }
}

# 查看依赖树
pnpm ls --filter @my-monorepo/app

# 分析重复依赖
pnpm dedupe --list

# 执行去重
pnpm dedupe

Q4:构建速度慢?

bash
# 使用 turbo 增量构建
npx turbo build

# 启用缓存
pnpm install --prefer-offline

# 使用 swc 替代 tsc
pnpm add -D @swc/cli

# 减少内存使用
export NODE_OPTIONS="--max-old-space-size=8192"

# 并行构建
pnpm --parallel build

Q5:如何发布包?

bash
# 发布所有包
pnpm --filter "./packages/**" publish --access public

# 发布特定包
pnpm --filter @my-monorepo/components publish --access public

# 发布到 npm 私有仓库
pnpm --filter @my-monorepo/components publish --registry https://npm.pkg.github.com

# 使用 np 进行版本管理
pnpm add -D np
npx np --filter @my-monorepo/components

十、完整项目示例

10.1 项目初始化脚本

bash
#!/bin/bash
# init-monorepo.sh

# 创建目录结构
mkdir -p my-monorepo/{packages,apps,tools/scripts}
cd my-monorepo

# 初始化 pnpm
pnpm init -y

# 创建 pnpm-workspace.yaml
cat > pnpm-workspace.yaml << 'EOF'
packages:
  - 'packages/**'
  - 'apps/**'
EOF

# 创建根 package.json
cat > package.json << 'EOF'
{
  "name": "@my-monorepo/root",
  "private": true,
  "version": "1.0.0",
  "scripts": {
    "build": "pnpm --filter \"./packages/**\" build",
    "dev": "pnpm --filter @my-monorepo/app dev",
    "test": "pnpm --filter \"./packages/**\" test",
    "lint": "pnpm --filter \"./packages/**\" lint"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "eslint": "^8.0.0",
    "prettier": "^3.0.0"
  }
}
EOF

# 创建 tsconfig.json
cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@my-monorepo/*": ["packages/*/src"]
    }
  }
}
EOF

# 创建示例包
mkdir -p packages/components/src
cat > packages/components/package.json << 'EOF'
{
  "name": "@my-monorepo/components",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "test": "vitest run"
  },
  "dependencies": {
    "react": "^18.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.0",
    "typescript": "^5.0.0",
    "vitest": "^1.0.0"
  }
}
EOF

cat > packages/components/tsconfig.json << 'EOF'
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}
EOF

cat > packages/components/src/index.ts << 'EOF'
export const Button = ({ children }: { children: React.ReactNode }) => {
  return <button>{children}</button>;
};
EOF

echo "Monorepo 项目初始化完成!"

10.2 使用脚本

bash
# 运行初始化脚本
bash init-monorepo.sh

# 安装依赖
pnpm install

# 构建所有包
pnpm build

# 启动开发服务器
pnpm dev

结语

pnpm workspace 是构建大规模前端项目的利器,它不仅解决了依赖管理的痛点,还提供了优秀的性能和缓存机制。通过合理的架构设计和脚本管理,你可以轻松管理包含数十个包的复杂项目。

推荐阅读:


🚀 提示: 开始你的 monorepo 之旅吧!从一个小型项目开始,逐步扩展,你会发现管理多个包变得前所未有的轻松。


延伸阅读

免责声明

本文仅供技术交流和学习参考。涉及第三方服务的链接可能包含 sponsored 标记,请自行核实服务条款、价格和可用性,并遵守当地法律法规。