monorepo最佳实践

monorepo最佳实践

1. 什么是 Monorepo

Monorepo(Monolithic Repository)是一种项目管理策略,将多个相关但独立的项目代码存储在同一个代码仓库中。与之相对的是Multi-repo(多仓库)策略,即每个项目都有自己独立的代码仓库。

1.2 优势与劣势

优势

  • 代码共享和复用:在同一仓库中的代码更容易共享,组件库、工具函数等可以方便地在多个项目间复用
  • 原子提交:可以在一次提交中同时更新多个相关项目,保持一致性
  • 统一的工作流:所有项目使用相同的构建、测试和部署流程
  • 依赖管理优化:统一管理依赖版本,避免版本冲突
  • 大规模代码重构更容易:可以在一次操作中修改所有相关项目
  • 跨项目协作更便捷:团队成员可以轻松查看、修改其他相关项目的代码

劣势

  • 仓库体积增大:随着项目增多,仓库体积可能变得非常大
  • 权限控制复杂:不同团队/项目的权限管理变得更复杂
  • 构建和CI/CD挑战:需要更智能的构建系统,避免不必要的构建
  • 学习曲线:需要团队成员熟悉更多项目结构和工具

1.3 适合 Monorepo的项目类型

  • 紧密关联的多个应用:前后端分离但紧密协作的应用
  • 共享组件和库的生态系统:如组件库与使用它的多个应用
  • 微前端架构:多个团队开发的前端应用需要集成
  • 大型组织内部相关项目:如 Google、Facebook 等大公司的内部项目
  • 开源项目及其生态:如 Babel、React 等及其插件体系
  • 需要统一工具链和配置的项目群:共享构建配置、类型定义、测试设置等

2. pnpm 作为 Monorepo 解决方案

2.1 pnpm 基本介绍

pnpm(全称:Performant NPM)是一个快速、高效、节省磁盘空间的 Node.js 包管理工具。

pnpmnpmyarn 的比较:

特性 npm yarn pnpm
安装方式 复制包到 node_modules 复制包到 node_modules 使用硬链接和符号链接
磁盘空间 占用最多 占用较多 占用最少
安装速度 较慢 较快 最快
依赖管理 平铺或嵌套结构 平铺结构 基于内容寻址的存储
多项目共享缓存 不支持 不支持 支持全局缓存共享
Monorepo 支持 需要额外工具 需要额外工具 原生支持

2.2 pnpm 的核心特点

1. 硬链接(symlink)技术

pnpm 使用硬链接和符号链接来管理依赖:

  • 所有包都存储在全局存储中(通常在 ~/.pnpm-store
  • 项目中的 node_modules 通过硬链接指向全局存储
  • 依赖树通过符号链接组织

2. 高效缓存机制

  • 下载的包会被保存在全局存储中
  • 即使删除项目,包仍然保留在全局存储中
  • 新项目可以直接从缓存中获取已下载的包

3. 极速安装

  • 安装相同依赖时几乎不需要下载
  • 即使首次安装,速度也通常比 npm/yarn
  • 多个项目共享同一份依赖,节省时间和空间

4. 严格的依赖结构

  • 防止幽灵依赖(使用未在 package.json 中声明的依赖)
  • 确保代码在不同环境中的一致行为

3. pnpm Monorepo 基础搭建

3.1. 初始化项目

首先,创建一个新项目并初始化 pnpm

json 复制代码
mkdir my-monorepo
cd my-monorepo
pnpm init

这会生成一个基本的 package.json 文件。

json 复制代码
{
  "name": "my-monorepo",
  "version": "1.0.0",
  "private": true,
  "description": "My pnpm monorepo project",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

关键配置说明:

  • "private": true:防止根包被意外发布到npm

3.2. pnpm workspace 简介

pnpm workspacepnpm提供的 monorepo解决方案,它允许:

  • 集中管理依赖:在一个仓库中管理多个相互关联的项目
  • 共享代码:各个子项目之间可以方便地共享代码
  • 统一版本:确保所有项目使用相同版本的依赖
  • 提高效率:通过共享node_modules减少磁盘占用和安装时间
  • 简化操作:支持一次性对多个包执行命令

3.3. 如何配置 pnpm-workspace.yaml

在项目根目录创建 pnpm-workspace.yaml 文件,定义工作区:

json 复制代码
packages:
  # 所有直接在packages目录下的包
  - 'packages/*'
  # 所有直接在apps目录下的应用
  - 'apps/*'
  # 所有在tools目录及其子目录下的包
  - 'tools/**'
  # 排除在test目录下的包
  - '!**/test/**'

配置说明:

  • 使用glob模式指定包含的目录
  • 匹配单级目录,*匹配多级目录
  • 前缀!表示排除匹配的目录

3.4. 项目结构示例

一个典型的monorepo项目结构如下:

json 复制代码
my-monorepo/
├── pnpm-workspace.yaml      # workspace配置
├── package.json             # 根项目配置
├── pnpm-lock.yaml          # 锁文件
├── apps/                    # 应用程序
│   ├── web/                 # Web前端应用
│   │   ├── package.json
│   │   └── src/
│   └── api/                 # API服务
│       ├── package.json
│       └── src/
├── packages/                # 共享包
│   ├── ui/                  # UI组件库
│   │   ├── package.json
│   │   └── src/
│   ├── utils/               # 工具函数库
│   │   ├── package.json
│   │   └── src/
│   └── config/              # 共享配置
│       ├── package.json
│       └── src/
└── tools/                   # 工具和脚本
    ├── eslint-config/       # ESLint配置
    │   ├── package.json
    │   └── index.js
    └── ts-config/           # TypeScript配置
        ├── package.json
        └── tsconfig.json

这种结构有以下几个优点:

  • 清晰的分层:apps 包含最终产品,packages 包含可复用的库
  • 职责分离:每个包都有明确的职责和边界
  • 依赖关系清晰:通常 apps 依赖 packages,packages 之间也可能有依赖关系
  • 独立性与共享性的平衡:既保持了各个项目的独立性,又能方便地共享代码

3.5. 子包配置package.json配置注意事项和规范

📦 Monorepo子包 package.json 必备配置项

字段 是否必须 说明 示例
name 必须 包名,全局唯一,用来标识子包,也用于 workspace 依赖和 filter "name": "utils""name": "@my-scope/utils"
version 必须(内部版本控制或发布用) 即使不发布,保持一致也有利于管理 "version": "1.0.0"
private 建议加(看用途) 如果是纯内部包,不对外发布,加 "private": true 避免误发布 "private": true
dependencies 必须根据实际情况 声明本包运行时需要的依赖 "dependencies": { "lodash": "^4.17.21" }
devDependencies 建议 开发时需要的依赖,比如测试库、打包工具等 "devDependencies": { "vitest": "^1.0.0" }
peerDependencies 如果是库,就必须加 指明由使用方提供的依赖,比如 React 组件库 "peerDependencies": { "react": "^18.0.0" }
engines 可选但推荐 限制 Node.jspnpm 版本,提高一致性 "engines": { "node": ">=18", "pnpm": ">=8" }
main / module / types 如果是可被引用的包,必须配置 指定入口文件、模块文件、类型文件路径 "main": "dist/index.js", "types": "dist/index.d.ts"
exports 高级配置(可选) 精细控制对外暴露哪些模块(用于 ESM/CJS兼容) 见下文详细示例

🎯 一个标准示例(开发内部 utils 包)

packages/utils/package.json

json 复制代码
{
  "name": "@my-scope/utils",
  "version": "1.0.0",
  "private": true,
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "require": "./dist/index.js",
      "import": "./dist/index.mjs",
      "types": "./dist/index.d.ts"
    }
  },
  "dependencies": {
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "vitest": "^1.0.0"
  },
  "engines": {
    "node": ">=18",
    "pnpm": ">=8"
  }
}

🚀 重点解释

重点项 为什么重要?
name 唯一性 Monorepo内必须唯一,不然 workspace 软链接、filter 可能混乱。最好统一加个作用域,比如 @你的组织名/包名
private 纯内部开发时加上,避免 npm publish 出错(防止误发!)
exports 现代 Node 项目、打包工具(如 ViteESBuild)会优先读 exports 字段,控制暴露点
peerDependencies 组件库、工具库如果依赖外部环境,应该用 peerDependencies 声明,防止版本冲突
types 如果包有 TypeScript 支持,一定记得加 "types",否则使用方提示不出类型

📋 小Tips

  • 子包不需要自己安装 pnpm,本身由根目录控制。
  • 子包内部可以有自己的 tsconfig.json(配合 tsconfig.base.json 统一继承)
  • 子包如果要被其他子包引用,记得在根目录 pnpm-workspace.yaml 声明路径。

例如你的 pnpm-workspace.yaml 要这样:

json 复制代码
packages:
  - "apps/*"
  - "packages/*"

这样 apps/admin 能正确引用 packages/utils

3.6. 依赖管理:共享与隔离

pnpmmonorepo中提供了强大而灵活的依赖管理能力:

依赖类型:

  1. 工作区依赖:工作区内部包之间的依赖关系
  2. 根依赖 :在根package.json中声明的依赖
  3. 子包依赖 :在各子包package.json中声明的依赖

安装依赖的方式

  1. 在根目录安装全局共享的依赖
json 复制代码
# 安装所有工作区共享的依赖(添加到根package.json)
pnpm add lodash -w

# 安装为开发依赖
pnpm add -wD typescript

# 安装子包
pnpm add -D @your-org/typescript-config@workspace:* -w

-w--workspace-root 标志表示在工作区根目录安装依赖。

  1. 在根目录为某个子包安装依赖

使用 pnpm --filter 命令可以为特定子包安装依赖:

json 复制代码
# 语法: pnpm --filter <target-package> add <dependency>
pnpm --filter "your-subpackage-name" add @your-org/typescript-config@workspace:*

例如,如果您有一个名为 "my-nextjs-app" 的子包,想为它安装 typescript-config:

json 复制代码
pnpm --filter "my-nextjs-app" add @your-org/typescript-config@workspace:*

@workspace:* 表示使用工作区内的包版本。

  1. 在根目录为多个子包安装依赖

使用 pnpm --filter 配合通配符 ... 可以为多个子包安装依赖:

json 复制代码
# 语法: pnpm --filter "<pattern>" add <dependency>
pnpm --filter "./apps/*" add lodash

例如,如果您的项目中有多个子应用(如 apps/webapps/admin),可以用这种方式同时为它们安装 lodash

  1. 只在当前子包安装依赖

如果你在某个子包的目录下操作,可以直接用 pnpm add,不需要 --filter

json 复制代码
# 语法: 在子包目录下直接执行
pnpm add axios
  1. 安装开发依赖到特定子包

加上 -D(或 --save-dev)参数,将依赖作为开发依赖安装到指定子包:

json 复制代码
# 语法: pnpm --filter <target-package> add -D <dependency>
pnpm --filter "my-nextjs-app" add -D eslint

例如,想只在 my-nextjs-app 里安装 eslint 作为 devDependency,可以这样写。

  1. 在子包中安装工作区内其他包作为依赖

可以使用 @workspace:* 直接安装工作区的其他本地包:

json 复制代码
# 语法: pnpm --filter <target-package> add <workspace-package>@workspace:*
pnpm --filter "apps/web" add @your-org/ui@workspace:*

比如把 @your-org/ui 这个内部组件库作为依赖加到 apps/web 里。

  1. package.json 中手动添加

也可以直接编辑子包的 package.json 文件,添加依赖项

json 复制代码
{
  "dependencies": {
    "@your-org/typescript-config": "workspace:*"
  }
  // 或者如果是开发依赖
  "devDependencies": {
    "@your-org/typescript-config": "workspace:*"
  }
}

然后在根目录执行 pnpm install 来安装所有依赖。

共享与隔离机制

pnpm 默认严格隔离依赖,同时提供优化的共享机制:

共享机制:

  1. 全局 store:所有依赖包存储在一个全局缓存中
  2. 硬链接共享:所有项目共享相同的依赖包实例
  3. 根依赖共享:根依赖可供所有子包使用(如开发工具)

隔离机制:

  1. 严格的 node_modules 结构:每个包只能访问自己声明的依赖
  2. 符号链接树:通过复杂的链接结构实现依赖隔离
  3. 防止幽灵依赖:防止使用未声明的依赖

常用命令

json 复制代码
# 安装所有工作区依赖
pnpm install

# 在所有工作区运行build命令
pnpm -r run build

# 在指定工作区运行dev命令
pnpm --filter web-app run dev

# 查看工作区依赖图
pnpm m ls

4. 日常操作

4.1 在 Monorepo 中新增包/项目

monorepo中添加新的包或项目有两种方法:

方法一:手动创建

  • 在适当的目录(如packages/apps/)创建新的子目录
  • 初始化package.json
  • 添加必要的配置和依赖
json 复制代码
# 创建新的库包
mkdir -p packages/new-lib
cd packages/new-lib
pnpm init

# 创建新的应用
mkdir -p apps/new-app
cd apps/new-app
pnpm init

方法二:使用工具自动创建

使用脚手架或自定义脚本创建标准化的新包:

json 复制代码
# 使用自定义脚本(如果有)
pnpm run create-package my-new-package

# 或使用现有工具(如plop)
pnpm plop package

新包配置示例:

json 复制代码
{
  "name": "@my-scope/new-lib",
  "version": "0.1.0",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "test": "jest"
  },
  "dependencies": {},
  "devDependencies": {
    "typescript": "^5.0.4"
  }
}

新增包后需要运行pnpm install以更新整个工作区的依赖。

4.2 在 Monorepo 内部包之间互相引用

monorepo最强大的功能之一是包之间可以方便地相互引用。这通过workspace协议实现:

workspace协议语法:

makefile 复制代码
workspace:<版本范围>

版本范围可以是:

  • *:任何版本
  • ^1.0.0:兼容语义版本范围
  • ~1.0.0:次版本范围
  • 1.0.0:精确版本

package.json中使用workspace协议:

json 复制代码
{
  "dependencies": {
    "@my-scope/ui": "workspace:*",
    "@my-scope/utils": "workspace:^0.1.0"
  }
}

在代码中导入工作区包:

json 复制代码
// 在JavaScript/TypeScript中导入
import { Button } from '@my-scope/ui';
import { formatDate } from '@my-scope/utils';

安装工作区依赖:

json 复制代码
# 为web应用添加ui库作为依赖
pnpm add @my-scope/ui --filter @my-scope/web --workspace

workspace 协议的优势

  1. 本地开发:更改会立即反映,无需发布
  2. 版本同步:始终使用项目中的最新版本
  3. 简化测试:可以同时测试库和使用它的应用
  4. 统一更新:便于统一升级所有相互依赖的包

4.3 升级/管理不同项目的依赖

monorepo中管理依赖版本是一项重要工作,pnpm提供了多种工具:

查看过时依赖:

json 复制代码
# 查看所有项目的过时依赖
pnpm -r outdated

# 查看特定项目的过时依赖
pnpm --filter @my-scope/web outdated

更新依赖:

json 复制代码
# 更新根依赖
pnpm update

# 更新特定包的所有依赖
pnpm update --filter @my-scope/web

# 更新特定依赖
pnpm update react react-dom --filter @my-scope/web

# 更新所有项目的特定依赖
pnpm -r update typescript

管理依赖版本:

  1. 固定版本:使用精确版本号("react": "18.2.0")
  2. 范围版本:使用语义版本范围("react": "^18.2.0")
  3. 集中管理 :在根目录使用工具如npmrc或类似工具

处理依赖冲突:

使用pnpm的覆盖功能解决冲突

json 复制代码
{
  "pnpm": {
    "overrides": {
      "foo": "^1.0.0",
      "bar@^2.1.0": "3.0.0"
    }
  }
}

4.4 批量执行命令

pnpm提供了多种方式在工作区的多个包中执行命令:

使用 -r 或 --recursive:

json 复制代码
# 在所有包中执行build命令
pnpm -r build

# 在所有包中并行执行build命令
pnpm -r --parallel build

# 按拓扑顺序(依赖关系)执行命令
pnpm -r --topological build

使用 --filter 选择特定包:

json 复制代码
# 在单个包中执行命令
pnpm --filter @my-scope/web build

# 在多个包中执行命令
pnpm --filter "@my-scope/web" --filter "@my-scope/api" build

# 使用glob模式选择包
pnpm --filter "./packages/*" build

高级过滤选择:

json 复制代码
# 在某个包及其所有依赖项中执行命令
pnpm --filter @my-scope/web... build

# 在依赖某个包的所有包中执行命令
pnpm --filter ...@my-scope/utils build

# 根据package.json字段过滤
pnpm --filter "[private=true]" build

在根目录定义通用脚本:

json 复制代码
{
  "scripts": {
    "build": "pnpm -r build",
    "test": "pnpm -r test",
    "dev": "pnpm --filter @my-scope/web dev"
  }
}

5. 进阶内容

5.1 Turborepo/ Nx / Rush 等工具协助 Monorepo 任务管理

虽然pnpm已经提供了不错的monorepo支持,但对于更大规模的项目,可能需要额外的工具来优化工作流:

Turborepo

Turborepo是一个高性能的构建系统,专为JavaScript/TypeScript monorepos设计:

主要特点

  • 增量构建:只重新构建已更改的部分
  • 远程缓存:团队共享构建缓存
  • 任务管道:定义任务之间的依赖关系
  • 并行执行:最大化CPU利用率
  • 智能任务调度:基于依赖图优化构建顺序

配置示例 (turbo.json):

json 复制代码
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "lint": {},
    "deploy": {
      "dependsOn": ["build", "test", "lint"]
    }
  }
}

5.2 发布 Monorepo 包(比如用 changesets 工具)

发布monorepo中的包需要特殊的工具和策略,changesets是最流行的选择之一:

Changesets的工作流程:

  1. 创建变更集:记录对特定包的更改
  2. 版本管理:根据变更集确定版本升级
  3. 生成变更日志 :自动创建CHANGELOG.md
  4. 发布包 :将包发布到npm

安装和配置Changesets

json 复制代码
# 安装
pnpm add -Dw @changesets/cli

# 初始化
pnpm changeset init

这会在项目根目录创建.changeset目录和配置文件。

基本使用流程:

  1. 添加变更集
json 复制代码
pnpm changeset
  1. 版本更新
json 复制代码
pnpm changeset version

这会根据变更集更新package.json中的版本并生成CHANGELOG.md

  1. 发布包
json 复制代码
pnpm changeset publish

这会发布所有已更新版本的包到npm

Changesets配置示例:

.changeset/config.json:

json 复制代码
{
  "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}

与CI/CD集成:

在GitHub Actions中自动化发布流程:

json 复制代码
name: Release

on:
  push:
    branches:
      - main

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16
      - run: pnpm install
      - name: Create Release Pull Request or Publish
        uses: changesets/action@v1
        with:
          publish: pnpm changeset publish
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

其他发布工具:

  1. Lerna :经典的monorepo管理工具,虽然现在与nx集成
  2. Release It:可配置的发布自动化工具
  3. Semantic Release:全自动版本管理和包发布

6. 实战练习

下面是一个从零开始搭建简单monorepo项目的步骤:

实战练习

总结

pnpm Monorepo是一种强大的项目组织方式,特别适合管理多个相关但独立的项目。通过本指南,我们学习了:

  • Monorepo的概念和适用场景
  • pnpm的优势和工作原理
  • 如何搭建和配置pnpm Monorepo
  • 项目结构和包管理最佳实践
  • 依赖管理和版本控制
  • 日常开发操作和命令
  • 进阶工具和性能优化
  • 实战练习

Monorepo不仅仅是一种代码组织方式,更是一种开发理念和工作流程。它鼓励代码共享和重用,促进团队协作,简化项目管理。随着项目规模的增长,合理使用Monorepo可以显著提高开发效率和代码质量。

  • 清晰的分层:apps 包含最终产品,packages 包含可复用的库
  • 职责分离:每个包都有明确的职责和边界
  • 依赖关系清晰:通常 apps 依赖 packages,packages 之间也可能有依赖关系
  • 独立性与共享性的平衡:既保持了各个项目的独立性,又能方便地共享代码
相关推荐
涛哥码咖10 分钟前
Rule.resourceQuery(通过路径参数指定loader匹配规则)
前端·webpack
夕水43 分钟前
这个提升效率宝藏级工具一定要收藏使用
前端·javascript·trae
会飞的鱼先生1 小时前
vue3 内置组件KeepAlive的使用
前端·javascript·vue.js
斯~内克1 小时前
前端浏览器窗口交互完全指南:从基础操作到高级控制
前端
Mike_jia2 小时前
Memos:知识工作者的理想开源笔记系统
前端
前端大白话2 小时前
前端崩溃瞬间救星!10 个 JavaScript 实战技巧大揭秘
前端·javascript
loveoobaby2 小时前
Shadertoy着色器移植到Three.js经验总结
前端
蓝易云2 小时前
在Linux、CentOS7中设置shell脚本开机自启动服务
前端·后端·centos
浩龙不eMo2 小时前
前端获取环境变量方式区分(Vite)
前端·vite
土豆骑士2 小时前
monorepo 实战练习
前端