🚀 实战:搭建 Monorepo 项目
步骤 1:初始化项目
1 2 3 4 5
| mkdir my-monorepo && cd my-monorepo
pnpm init
|
步骤 2:配置 pnpm workspace
pnpm-workspace.yaml 中的 packages 配置是告诉 pnpm 哪些目录下的子目录是 workspace 的成员包。
简单说:这个配置定义了 Monorepo 的边界,告诉 pnpm 哪些目录是”包”。
1 2 3 4
| packages: - "packages/*" - "apps/*"
|
步骤 3:创建目录结构
1
| mkdir -p packages/shared packages/ui apps/web apps/admin
|
1 2 3 4 5 6 7 8 9 10
| my-monorepo/ ├── apps/ │ ├── web/ # 主应用,被识别为 @myorg/web │ └── admin/ # 管理后台,被识别为 @myorg/admin ├── packages/ │ ├── shared/ # 共享工具,被识别为 @myorg/shared │ └── ui/ # UI 组件,被识别为 @myorg/ui ├── scripts/ ❌ 不在 workspace 中 ├── package.json └── pnpm-workspace.yaml
|
步骤 4:初始化子包
packages/shared/package.json
1 2 3 4 5 6
| { "name": "@myorg/shared", "version": "1.0.0", "main": "src/index.ts", "types": "src/index.ts" }
|
packages/ui/package.json
1 2 3 4 5 6 7 8 9
| { "name": "@myorg/ui", "version": "1.0.0", "main": "src/index.ts", "types": "src/index.ts", "dependencies": { "@myorg/shared": "workspace:*" } }
|
apps/web/package.json
1 2 3 4 5 6 7 8
| { "name": "@myorg/web", "version": "1.0.0", "dependencies": { "@myorg/shared": "workspace:*", "@myorg/ui": "workspace:*" } }
|
步骤 5:安装依赖
1 2 3 4 5 6 7 8
| pnpm install
pnpm add lodash --filter @myorg/shared
pnpm add -D typescript -w
|
步骤 6:配置 Turborepo
turbo.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| { "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "dev": { "cache": false, "persistent": true }, "lint": {}, "test": { "dependsOn": ["build"] } } }
|
package.json(根目录)
1 2 3 4 5 6 7 8
| { "scripts": { "dev": "turbo run dev", "build": "turbo run build", "lint": "turbo run lint", "test": "turbo run test" } }
|
步骤 7:运行命令
1 2 3 4 5 6 7 8 9 10 11
| pnpm dev
pnpm build
pnpm build --filter @myorg/web
pnpm build --filter @myorg/web...
|
📦 共享代码的最佳实践
1. 内部包引用
使用 workspace:* 协议引用内部包:
1 2 3 4 5 6
| { "dependencies": { "@myorg/shared": "workspace:*", "@myorg/ui": "workspace:^1.0.0" } }
|
2. TypeScript 路径映射
主要解决两个问题:
1. 让 IDE 正确识别源码
在开发阶段,内部包还没有构建(没有 dist 目录),但你希望:
1 2
| // apps/web/src/index.ts import { utils } from '@myorg/shared'; // IDE 能跳转到源码
|
2. 开发时直接引用源码
@myorg/shared 的解析路径:
1 2 3 4 5
| 开发时(通过 paths): import '@myorg/shared' → packages/shared/src/index.ts ✅
生产时(通过 package.json): import '@myorg/shared' → packages/shared/dist/index.js ✅
|
tsconfig.json(根目录)
1 2 3 4 5 6 7 8 9
| { "compilerOptions": { "baseUrl": ".", "paths": { "@myorg/shared": ["packages/shared/src"], "@myorg/ui": ["packages/ui/src"] } } }
|
3. 共享配置
统一的 ESLint 配置:
1 2 3 4 5 6
| packages/ └── eslint-config/ ├── package.json ├── base.js ├── react.js └── node.js
|
1 2 3 4 5
| module.exports = { extends: ["./base.js", "plugin:react/recommended"], };
|
1 2 3 4
| { "extends": ["@myorg/eslint-config/react"] }
|
🚀 Monorepo 发布指南
Monorepo 中发布包到 npm 需要解决几个核心问题:版本管理、变更追踪、依赖更新。推荐使用 Changesets 工具。
发布流程概览
1 2 3 4 5
| 代码开发 → 记录变更 → 更新版本 → 构建 → 发布 npm │ │ │ │ │ └── PR ────┼── merge ─┼── CI ──┼── CI ──┘ │ │ │ changeset version build & publish
|
1. 安装 Changesets
1 2 3 4 5
| pnpm add -D @changesets/cli -w
pnpm changeset init
|
初始化后会创建 .changeset 目录:
1 2 3
| .changeset/ ├── config.json # 配置文件 └── README.md
|
2. 配置 Changesets
.changeset/config.json
1 2 3 4 5 6 7 8 9 10 11
| { "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": ["@myorg/web", "@myorg/admin"] }
|
| 配置项 |
说明 |
access |
public 发布公开包,restricted 发布私有包 |
baseBranch |
主分支名称 |
updateInternalDependencies |
内部依赖更新时的版本策略 |
ignore |
忽略不需要发布的包(如应用项目) |
fixed |
固定版本的包组(同步升级) |
linked |
关联版本的包组 |
3. 记录变更(开发者操作)
每次完成功能开发或 bug 修复后,记录变更:
交互式选择:
1 2 3 4 5 6 7 8 9
| 🦋 Which packages would you like to include? ◯ @myorg/shared ◉ @myorg/ui # 选择修改的包
🦋 Which packages should have a major bump? # 选择 major/minor/patch
🦋 Summary: # 输入变更描述
|
生成变更文件:
1 2 3 4 5
| ## <!-- .changeset/happy-panda-123.md -->
## "@myorg/ui": minor
feat: 新增 DatePicker 组件,支持日期范围选择
|
4. 更新版本(CI 或手动)
当变更合并到主分支后,执行版本更新:
这个命令会:
- ✅ 读取所有 changeset 文件
- ✅ 更新相关包的
package.json 版本号
- ✅ 更新依赖该包的其他包版本
- ✅ 生成
CHANGELOG.md
- ✅ 删除已处理的 changeset 文件
生成的 CHANGELOG.md
1 2 3 4 5 6 7
| # @myorg/ui
## 1.1.0
### Minor Changes
- feat: 新增 DatePicker 组件,支持日期范围选择
|
5. 发布到 npm
1 2 3 4 5
| pnpm build
pnpm changeset publish
|
6. 完整的 package.json 脚本
根目录 package.json
1 2 3 4 5 6 7 8
| { "scripts": { "changeset": "changeset", "version": "changeset version", "release": "pnpm build && changeset publish", "prepublishOnly": "pnpm build" } }
|
7. GitHub Actions 自动发布
.github/workflows/release.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| name: Release
on: push: branches: - main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4
- name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 8
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: "pnpm"
- name: Install dependencies run: pnpm install
- name: Create Release Pull Request or Publish id: changesets uses: changesets/action@v1 with: publish: pnpm release version: pnpm version title: "chore: version packages" commit: "chore: version packages" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
8. 自动化发布流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| ┌─────────────────────────────────────────────────────────────┐ │ 开发者工作流 │ ├─────────────────────────────────────────────────────────────┤ │ 1. 开发功能/修复 bug │ │ 2. pnpm changeset → 记录变更 │ │ 3. git commit & push │ │ 4. 创建 PR │ └─────────────────────────────────────────────────────────────┘ │ ▼ merge ┌─────────────────────────────────────────────────────────────┐ │ CI 自动处理 │ ├─────────────────────────────────────────────────────────────┤ │ 5. Changesets Action 检测到 changeset 文件 │ │ 6. 自动创建 "Version Packages" PR │ │ - 更新版本号 │ │ - 生成 CHANGELOG │ └─────────────────────────────────────────────────────────────┘ │ ▼ merge Version PR ┌─────────────────────────────────────────────────────────────┐ │ 自动发布 │ ├─────────────────────────────────────────────────────────────┤ │ 7. CI 执行 pnpm release │ │ 8. 构建所有包 │ │ 9. 发布到 npm │ │ 10. 创建 GitHub Release │ └─────────────────────────────────────────────────────────────┘
|
9. 私有包发布(内部 npm 仓库)
如果发布到私有仓库:
.npmrc
1 2
| @myorg:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${NPM_TOKEN}
|
package.json
1 2 3 4 5 6
| { "publishConfig": { "registry": "https://npm.pkg.github.com", "access": "restricted" } }
|
10. 预发布版本(Beta/Alpha)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| pnpm changeset pre enter beta
pnpm changeset
pnpm changeset version
pnpm changeset publish --tag beta
pnpm changeset pre exit
|
发布前检查清单
| 检查项 |
说明 |
| ✅ 构建通过 |
pnpm build 无错误 |
| ✅ 测试通过 |
pnpm test 无失败 |
| ✅ Lint 通过 |
pnpm lint 无错误 |
| ✅ 版本正确 |
changeset 选择了正确的版本类型 |
| ✅ CHANGELOG |
变更描述清晰准确 |
| ✅ 依赖更新 |
内部依赖版本同步更新 |
⚡ 性能优化
1. Turborepo 远程缓存
1 2 3 4 5 6 7 8
| npx turbo login
npx turbo link
pnpm build
|
2. 增量构建
Turborepo 会自动检测变更,只构建受影响的包:
3. 并行执行
1 2 3 4 5 6 7 8
| { "pipeline": { "lint": { "outputs": [] } } }
|
📊 目录结构最佳实践
推荐结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| my-monorepo/ ├── apps/ # 应用项目 │ ├── web/ # Web 应用 │ ├── admin/ # 管理后台 │ └── docs/ # 文档站点 ├── packages/ # 共享包 │ ├── ui/ # UI 组件库 │ ├── shared/ # 共享工具 │ ├── config/ # 共享配置 │ │ ├── eslint-config/ │ │ ├── tsconfig/ │ │ └── prettier-config/ │ └── types/ # 共享类型定义 ├── .changeset/ # Changesets 配置 ├── .github/ # GitHub Actions │ └── workflows/ ├── turbo.json # Turborepo 配置 ├── pnpm-workspace.yaml # pnpm workspace 配置 ├── package.json # 根 package.json └── README.md
|
🔧 常见问题解决
1. 依赖提升导致的问题
1 2 3
| shamefully-hoist=false strict-peer-dependencies=false
|
2. TypeScript 找不到模块
1 2 3 4 5 6 7 8 9 10
| { "compilerOptions": { "moduleResolution": "node", "paths": { "@myorg/*": ["packages/*/src"] } }, "references": [{ "path": "packages/shared" }, { "path": "packages/ui" }] }
|
3. ESLint 配置不生效
1 2
| pnpm add -D @myorg/eslint-config --filter @myorg/web
|