Monorepo 实践指南:Lerna 与 pnpm 的完美结合
2024/8/25大约 7 分钟
Monorepo 实践指南:Lerna 与 pnpm 的完美结合
什么是 Monorepo
Monorepo(单体仓库)是一种将多个相关项目存储在单一代码仓库中的开发策略。与传统的多仓库(Multi-repo)相比,Monorepo 将所有相关代码集中管理。
Monorepo vs Multi-repo
Monorepo 结构:
project/
├── packages/
│ ├── shared-utils/
│ ├── web-app/
│ ├── mobile-app/
│ └── api-server/
├── tools/
├── docs/
└── package.json
Multi-repo 结构:
├── shared-utils-repo/
├── web-app-repo/
├── mobile-app-repo/
└── api-server-repo/Monorepo 的优势
1. 代码共享和重用
// 在 Monorepo 中,共享组件变得简单
// packages/shared-ui/src/Button.js
export const Button = ({ children, onClick }) => {
return <button onClick={onClick}>{children}</button>;
};
// packages/web-app/src/App.js
import { Button } from '@company/shared-ui';
// packages/admin-panel/src/Dashboard.js
import { Button } from '@company/shared-ui';2. 统一的工具链和配置
// 根目录的 package.json
{
"scripts": {
"build": "lerna run build",
"test": "lerna run test",
"lint": "eslint packages/*/src/**/*.{js,ts,tsx}",
"format": "prettier --write packages/*/src/**/*.{js,ts,tsx}"
},
"devDependencies": {
"eslint": "^8.0.0",
"prettier": "^2.0.0",
"jest": "^29.0.0",
"@babel/core": "^7.0.0"
}
}3. 原子化提交
# 一次提交可以同时更新多个包
git commit -m "feat: add new authentication method
- Update auth-service API
- Update web-app login component
- Update mobile-app authentication flow
- Update shared-types interfaces"4. 简化依赖管理
// 所有包可以共享相同版本的依赖
// 根目录 package.json
{
"devDependencies": {
"react": "^18.0.0",
"typescript": "^5.0.0",
"webpack": "^5.0.0"
}
}
// 子包只需要声明特定依赖
// packages/web-app/package.json
{
"dependencies": {
"@company/shared-ui": "^1.0.0",
"react-router-dom": "^6.0.0"
}
}Lerna 简介
Lerna 是一个优化使用 git 和 npm 管理多包仓库的工作流程的工具。
Lerna 的核心功能
- 自动链接包之间的依赖
- 批量执行命令
- 版本管理和发布
- 变更检测
安装和初始化
# 全局安装 Lerna
npm install -g lerna
# 创建新的 Monorepo
mkdir my-monorepo
cd my-monorepo
git init
lerna init
# 或者初始化为独立版本模式
lerna init --independentLerna 配置文件
// lerna.json
{
"version": "independent",
"npmClient": "pnpm",
"command": {
"publish": {
"conventionalCommits": true,
"message": "chore(release): publish",
"registry": "https://registry.npmjs.org"
},
"bootstrap": {
"ignore": "component-*",
"npmClientArgs": ["--no-package-lock"]
}
},
"packages": [
"packages/*"
]
}pnpm 的优势
1. 磁盘空间效率
# pnpm 使用硬链接和符号链接,节省大量磁盘空间
# 传统 npm/yarn
node_modules/
├── lodash/ (4MB)
├── react/ (2MB)
└── other-packages/
# pnpm
.pnpm-store/
├── lodash@4.17.21/ (4MB) # 全局存储一份
└── react@18.2.0/ (2MB) # 全局存储一份
node_modules/
├── lodash -> .pnpm-store/lodash@4.17.21/ # 硬链接
└── react -> .pnpm-store/react@18.2.0/ # 硬链接2. 更严格的依赖管理
// pnpm 防止访问未声明的依赖
// package.json 中没有声明 lodash,但代码中使用了
import _ from 'lodash'; // ❌ pnpm 会报错,npm/yarn 可能正常工作
// 必须显式声明依赖
// package.json
{
"dependencies": {
"lodash": "^4.17.21"
}
}3. 更快的安装速度
# 性能比较(安装 React 应用的依赖)
npm install # ~45s
yarn install # ~35s
pnpm install # ~25sLerna + pnpm 最佳实践
1. 项目结构设置
# 创建项目结构
mkdir my-monorepo && cd my-monorepo
git init
# 初始化 pnpm workspace
cat > pnpm-workspace.yaml << EOF
packages:
- 'packages/*'
- 'apps/*'
- 'tools/*'
EOF
# 初始化 Lerna
lerna init --independent2. 根目录配置
// package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"build": "lerna run build",
"test": "lerna run test",
"clean": "lerna run clean",
"bootstrap": "lerna bootstrap",
"publish": "lerna publish",
"version": "lerna version",
"dev": "lerna run dev --parallel",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"format": "prettier --write ."
},
"devDependencies": {
"lerna": "^6.0.0",
"@changesets/cli": "^2.26.0",
"eslint": "^8.0.0",
"prettier": "^2.8.0",
"typescript": "^5.0.0"
}
}3. 创建共享包
# 创建共享工具包
mkdir -p packages/shared-utils
cd packages/shared-utils
cat > package.json << EOF
{
"name": "@company/shared-utils",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "jest"
},
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/lodash": "^4.14.0"
}
}
EOF4. 创建应用包
# 创建 Web 应用
mkdir -p apps/web-app
cd apps/web-app
cat > package.json << EOF
{
"name": "@company/web-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "jest"
},
"dependencies": {
"@company/shared-utils": "workspace:*",
"@company/shared-ui": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"vite": "^4.0.0",
"@vitejs/plugin-react": "^3.0.0"
}
}
EOF常用 Lerna 命令
1. 包管理命令
# 安装所有依赖并链接本地包
lerna bootstrap
# 清理所有 node_modules
lerna clean
# 在所有包中运行脚本
lerna run build
lerna run test
lerna run dev --parallel
# 在特定包中运行脚本
lerna run build --scope=@company/web-app
# 在多个包中运行脚本
lerna run build --scope=@company/web-app --scope=@company/api2. 依赖管理命令
# 给所有包添加依赖
lerna add lodash
# 给特定包添加依赖
lerna add react --scope=@company/web-app
# 添加内部包依赖
lerna add @company/shared-utils --scope=@company/web-app
# 添加开发依赖
lerna add typescript --dev3. 版本管理命令
# 交互式版本升级
lerna version
# 自动升级补丁版本
lerna version patch
# 升级次版本
lerna version minor
# 升级主版本
lerna version major
# 预发布版本
lerna version prerelease --preid=beta4. 发布命令
# 发布到 npm
lerna publish
# 发布预发布版本
lerna publish --dist-tag=beta
# 从 git 标签发布
lerna publish from-git
# 从包内容发布
lerna publish from-packagepnpm Workspace 配置
1. 基础配置
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- 'tools/*'
- 'docs'2. 依赖管理
# 安装根级别依赖
pnpm add -w typescript
# 给特定包安装依赖
pnpm add react --filter @company/web-app
# 安装工作区内的包
pnpm add @company/shared-utils --filter @company/web-app
# 安装所有依赖
pnpm install
# 递归安装(包括子包)
pnpm install -r3. 脚本执行
# 在所有包中运行脚本
pnpm -r run build
# 在特定包中运行脚本
pnpm --filter @company/web-app run dev
# 并行运行脚本
pnpm -r --parallel run test
# 根据依赖关系顺序运行
pnpm -r run build --workspace-concurrency=1实际项目示例
1. React 组件库 Monorepo
my-design-system/
├── packages/
│ ├── tokens/ # 设计令牌
│ ├── icons/ # 图标库
│ ├── components/ # 组件库
│ └── utils/ # 工具函数
├── apps/
│ ├── storybook/ # 组件文档
│ ├── playground/ # 测试应用
│ └── website/ # 官网
├── tools/
│ ├── build-tools/ # 构建工具
│ └── eslint-config/ # ESLint 配置
└── docs/ # 文档2. 全栈应用 Monorepo
my-fullstack-app/
├── packages/
│ ├── shared-types/ # 共享类型定义
│ ├── shared-utils/ # 共享工具
│ ├── database/ # 数据库模型
│ └── api-client/ # API 客户端
├── apps/
│ ├── web/ # Web 前端
│ ├── mobile/ # 移动应用
│ ├── admin/ # 管理后台
│ └── api/ # 后端 API
└── tools/
├── scripts/ # 脚本工具
└── configs/ # 配置文件高级配置
1. 自定义发布流程
// tools/release.js
const { execSync } = require('child_process');
async function release() {
// 运行测试
console.log('Running tests...');
execSync('lerna run test', { stdio: 'inherit' });
// 构建所有包
console.log('Building packages...');
execSync('lerna run build', { stdio: 'inherit' });
// 发布
console.log('Publishing...');
execSync('lerna publish', { stdio: 'inherit' });
}
release().catch(console.error);2. 自动化 CI/CD
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
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 run lint
- run: pnpm run test
- run: pnpm run build3. 条件发布
// lerna.json
{
"command": {
"publish": {
"conventionalCommits": true,
"message": "chore(release): publish",
"ignoreChanges": [
"**/*.md",
"**/*.test.{js,ts}",
"**/stories/**"
]
}
}
}性能优化
1. 缓存策略
// .npmrc
# 启用 pnpm 缓存
store-dir=~/.pnpm-store
cache-dir=~/.pnpm-cache
# 启用构建缓存
enable-pre-post-scripts=true2. 并行构建
// package.json
{
"scripts": {
"build:parallel": "lerna run build --parallel",
"test:parallel": "lerna run test --parallel --",
"dev": "lerna run dev --parallel --stream"
}
}3. 增量构建
# 只构建有变更的包
lerna run build --since HEAD~1
# 只测试有变更的包
lerna run test --since origin/main故障排除
1. 依赖解析问题
# 清理并重新安装
pnpm store prune
rm -rf node_modules packages/*/node_modules
pnpm install
# 检查依赖关系
pnpm list --depth=0
pnpm why package-name2. 版本管理问题
# 检查包的发布状态
lerna ls --long
lerna diff
# 手动同步版本
lerna version --no-git-tag-version --no-push3. 构建顺序问题
// 在 package.json 中定义构建顺序
{
"scripts": {
"build": "lerna run build --stream --scope=@company/shared-* && lerna run build --stream --ignore=@company/shared-*"
}
}最佳实践总结
1. 项目结构
- 使用清晰的目录结构(packages/, apps/, tools/)
- 统一命名规范(@company/package-name)
- 合理划分包的职责
2. 依赖管理
- 优先使用 pnpm workspace
- 在根目录管理公共依赖
- 使用 workspace: 协议引用内部包
3. 版本管理
- 使用语义化版本
- 启用 conventional commits
- 考虑使用 changesets 替代 lerna version
4. 发布策略
- 自动化 CI/CD 流程
- 使用预发布版本测试
- 实施代码审查机制
5. 性能优化
- 启用并行构建和测试
- 使用增量构建
- 合理配置缓存策略
总结
Monorepo 结合 Lerna 和 pnpm 提供了强大的多包管理能力:
主要优势:
- 🚀 提升开发效率:统一工具链和依赖管理
- 🔄 简化代码共享:内部包之间轻松共享代码
- 📦 原子化变更:一次提交涉及多个相关包
- 🛠️ 统一配置:共享构建、测试和部署配置
适用场景:
- 大型前端项目
- 组件库和工具库
- 全栈应用开发
- 企业级应用系统
通过合理使用 Monorepo 架构,可以显著提升团队的开发效率和代码质量,是现代前端工程化的重要实践之一。
