跳轉到內容

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 标记,请自行核实服务条款、价格和可用性,并遵守当地法律法规。

最後更新於: