什么是 Monorepo ? 如何实现?

1. 定义

多个项目(或包)的代码放在 同一个版本控制仓库 中进行管理。

Ps:想象一个大房子(一个大的代码仓库),里面住着好几个相关的项目(比如前端的网页 App、后端的服务器程序、共享的工具库),它们不再各自住单独的小房子(单独的仓库),而是共享同一个大空间、同一个地址簿(版本管理)、同一个工具箱(构建工具)。这个大房子就是 Monorepo。


2. Monorepo 的核心概念

  1. 单一仓库

所有相关项目的源代码都存放在一个 Git 仓库里。git clone 一次,所有代码都到手。

  1. 原子提交

修改可以跨多个项目/包,并作为一个 整体提交。

想象你同时改了客厅的灯(前端)和厨房的水管(后端),提交时记录的是"升级智能家居系统"这一整个改动,而不是分开提交"修灯"和"修水管"。

  1. 共享依赖

房子里的所有项目共享一个"中央工具箱"(node_modules 或其他依赖目录)。

如果它们都需要同一个锤子(比如 lodash 库),只需要在房子中央放一把,大家共用(符号链接或提升依赖),不需要每个房间(项目)都买一把重复的。这解决了 "依赖地狱" 和 版本不一致 问题。

  1. 标准化工具链

整个大房子使用同一套建筑规范。

同样的代码风格检查工具(如 ESLint)、同样的打包工具(如 Webpack, Vite)、同样的测试框架(如 Jest)、同样的 CI/CD 流程。保证大家干活方式一致,效率高。

  1. 跨项目重构与协作

因为所有代码都在一个地方,改一个共享的工具库,可以立刻看到哪些前端和后端项目在用它,方便统一修改和测试。不同团队(前端、后端、基础架构)更容易看到彼此代码,协作更紧密。


3. 与传统 Polyrepo (多仓库) 的对比

对比维度 Monorepo 🏢 Polyrepo 🏠
代码组织 所有项目共享一个仓库 每个项目独立仓库
依赖管理 ✅ 优势: • 共享依赖(提升/符号链接) • 统一版本,避免冲突 • 跨项目依赖实时生效 ⚠️ 劣势: • 重复安装依赖(磁盘冗余) • 跨仓库版本易冲突 • 需手动发布和升级依赖
跨项目变更 ✅ 优势: • 原子提交(一次提交修改多个项目) • 重构和影响分析高效 • 统一 CI/CD 流程 ⚠️ 劣势: • 需跨仓库提交和 PR • 依赖发布流程繁琐 • 协调成本高
协作与可见性 ✅ 优势: • 代码透明,跨团队无缝协作 • 统一代码规范与工具链 • 集中问题追踪 ⚠️ 劣势: • 团队间存在信息壁垒 • 规范易碎片化 • 上下文切换频繁
工具链一致性 ✅ 优势: • 统一构建、测试、Lint 配置 • 标准化开发环境 ⚠️ 劣势: • 工具配置易分散 • 维护多套流水线成本高
权限控制 ⚠️ 挑战: • 需复杂目录级权限管理(如 Git 子模块/定制方案) ✅ 优势: • 天然仓库级权限隔离 • 精细控制更简单
仓库性能 ⚠️ 挑战: • 仓库体积膨胀(Clone/Status 慢) • 依赖增量构建工具优化(如 Turborepo/Nx) ✅ 优势: • 各仓库轻量 • Git 操作快速
构建/测试速度 ✅ 优势(需工具支持): • 增量构建(仅改动的包) • 并行任务 + 分布式缓存 ⚠️ 劣势: • 独立构建无全局优化 • 缓存难以共享
新成员上手 ⚠️ 挑战: • 初始学习曲线陡峭 • 需理解整体架构 ✅ 优势: • 聚焦单个仓库 • 认知负担低
适用场景 ✅ 适合: • 强关联项目(如微服务全家桶) • 高频共享代码库 • 追求统一基建的重型团队 ✅ 适合: • 松散耦合的独立项目 • 开源生态库 • 权限敏感型组织

4. Monorepo 的实现:Yarn Workspaces (+ Lerna)

Yarn Workspaces

Yarn Workspaces 是 Yarn 包管理器提供的一个功能,旨在 简化在单一代码仓库(Monorepo)中管理多个相互关联的 JavaScript/TypeScript 包 的过程。它允许你将一个大的项目根目录(通常是一个 Git 仓库)划分为多个子包(子项目),这些子包可以独立管理自己的依赖,但又能方便地相互引用和共享代码,并且 Yarn 会智能地优化依赖安装。

如何工作?

  1. 项目结构:

    1. 创建一个根目录作为 Monorepo 的根。
    2. 在根目录下创建一个 package.json 文件。
    3. 在这个根 package.json 中设置 "private": true(因为根目录本身通常不是一个要发布的包)。
    4. 在根 package.json 中添加 "workspaces" 字段。这个字段告诉 Yarn 哪些子目录是工作区(包)。
    5. 子包通常放在 packages/apps/ 等目录下(结构可自定义),每个子目录都是一个独立的包,有自己的 package.json
text 复制代码
my-monorepo/ (根目录)
├── package.json (根 package.json, 包含 "workspaces")
├── yarn.lock (根 lockfile,由 Yarn 管理)
├── node_modules/ (根 node_modules,存放提升的依赖)
│
├── packages/ (通常存放库包)
│   ├── shared-utils/ (包 A)
│   │   ├── package.json (name: "@my-project/shared-utils")
│   │   ├── src/
│   │   └── ...
│   │
│   └── component-library/ (包 B)
│       ├── package.json (name: "@my-project/component-library")
│       ├── src/
│       └── ...
│
└── apps/ (通常存放应用)
    └── web-app/ (包 C)
        ├── package.json (name: "@my-project/web-app")
        ├── src/
        └── ...
json 复制代码
// 根 package.json
{
  "name": "my-monorepo",
  "private": true, // 必须为 true
  "workspaces": [
    "packages/*", // 匹配 packages 目录下的所有子目录
    "apps/*"     // 匹配 apps 目录下的所有子目录
  ],
  "scripts": {
    // 可以在这里添加根级别的脚本,利用 `yarn workspaces foreach` 或 `yarn workspace <pkg> <cmd>`
  },
  "devDependencies": {
    // 根目录可能有一些共享的开发工具,如 TypeScript, Jest, ESLint, Prettier 的全局配置
  }
}  
  1. 依赖安装 ( yarn install ):

    1. 在根目录运行 yarn install

    2. Yarn 会:

      • 提升依赖 (Hoisting): 分析所有工作区包的依赖关系。如果一个依赖(如 lodash@^4.17.21)被多个工作区包需要,并且版本范围兼容,Yarn 会尝试将这个依赖安装在根目录的 node_modules 中。这样,磁盘上只保留一份副本。
      • 处理内部依赖: 如果包 B 的 package.json 中声明依赖包 A ("@my-project/shared-utils": "1.0.0"),Yarn 会识别出 @my-project/shared-utils 是工作区内的另一个包,并自动创建一个符号链接 (symlink),从包 B 的 node_modules/@my-project/shared-utils 指向包 A 的实际目录。这使得在开发时修改包 A 能立即反映在依赖它的包 B 中,无需手动 link 或发布。
      • 生成单个 yarn.lock: 所有工作区包的依赖关系都记录在根目录下的单个yarn.lock文件中。这保证了整个 Monorepo 使用的依赖版本完全一致,避免了冲突。
  1. 关键命令与工作流:
  • 安装所有依赖: 在根目录运行 yarn install

  • 为特定工作区添加依赖:

    • 全局依赖 (会添加到根 package.json):yarn add -W <package> (-W 代表 --ignore-workspace-root-check)。
    • 给某个工作区添加生产依赖:yarn workspace <workspace-package-name> add <package>
    • 给某个工作区添加开发依赖:yarn workspace <workspace-package-name> add -D <package>
    • 示例: yarn workspace @my-project/web-app add react-router-dom
  • 移除工作区的依赖: yarn workspace <workspace-package-name> remove <package>

  • 运行工作区中的脚本:

    • 在特定工作区运行其 package.json 中的脚本:yarn workspace <workspace-package-name> run <script-name>
    • 示例: yarn workspace @my-project/web-app run dev
  • 在所有工作区运行同一个脚本: yarn workspaces foreach run <script-name>

    • 示例: yarn workspaces foreach run lint (假设每个包都有 lint 脚本)
    • 可以添加 -p (并行) 或 -i (交互式) 或 -v (详细) 等选项。
    • 示例 (并行构建): yarn workspaces foreach -p run build
  • 列出所有工作区: yarn workspaces list

  • 在依赖树中查看为什么安装了某个包: yarn why <package> (在根目录运行)

主要优势

  1. 依赖优化 (提升): 显著减少磁盘空间占用和 yarn install 时间,避免重复安装相同的包。

  2. 无缝本地链接: 工作区内的包可以像从 npm registry 安装的一样相互引用,修改一个包能立即在依赖它的包中生效,极大简化开发和调试。

  3. 单一锁文件: 根目录的单个 yarn.lock 确保所有工作区使用完全一致的依赖版本,提升稳定性和可重现性。

  4. 统一命令入口: 通过 yarn workspaceyarn workspaces foreach 命令,可以方便地在特定包或所有包上执行操作(安装、运行脚本、构建、测试、发布等)。

  5. 代码共享与原子提交: 促进组件、工具函数、配置等的共享。相关修改可以跨多个包一起提交,保证原子性。

  6. 简化工具链配置: 更容易在根目录设置统一的构建、测试、代码格式化、Lint 等工具配置。

需要注意的点/缺点

  1. 依赖提升的复杂性: 虽然提升节省空间,但也可能导致依赖冲突更难调试("幽灵依赖" - 某个包使用了被提升到根 node_modules 的依赖,但这个依赖并没有明确声明在该包的 package.json 中)。Yarn 的 Plug'n'Play (PnP) 模式是另一种解决依赖问题的方案,但 Workspaces 与 PnP 的结合需要额外配置。

  2. 工具链支持: 虽然主流工具(TypeScript, Jest, ESLint, Babel, Webpack, Vite 等)对 Workspaces 支持良好,但有时可能需要特定配置(如 TypeScript 的项目引用 references)才能完美处理跨包的类型和构建。

  3. IDE/编辑器支持: 现代 IDE (VSCode, WebStorm) 对 Workspaces 支持很好,但有时需要正确配置工作区或 TypeScript 项目引用以实现准确的代码跳转和类型检查。

  4. 规模管理: 当 Monorepo 变得非常庞大时,即使有 Workspaces,构建、测试和安装时间也可能变长,需要更复杂的工具(如 Nx, Turborepo)进行增量构建和任务调度优化。

  5. 发布策略: 发布多个相互依赖的包需要协调版本号。工具如 Lerna (常与 Yarn Workspaces 结合使用) 或 Yarn 自己的 version 和 release 工作流 可以自动化此过程。

Lerna

Lerna 是一个用于管理 JavaScript 项目 Monorepo(单仓库多包)的经典工具,它通过自动化解决多包管理中的依赖联动、版本发布和跨包操作等复杂问题。

Lerna 提供命令行工具,自动化完成包之间的版本号同步、依赖安装、测试和发布:

  1. 跨包依赖链接bootstrap

包A依赖包B时,需将B构建后发布到 npm,再在A中安装,开发调试极其繁琐。

Lerna 将仓库内所有包的依赖(包括内部包)安装到根目录 node_modules(依赖提升)。

通过 符号链接(Symlinks) 将本地相互依赖的包直接链接到彼此 node_modules 中(例如 @project/ui/node_modules/@project/utils → 指向本地代码)。

效果:修改工具包代码,依赖它的应用包 实时生效,无需手动发布安装。

  1. 统一版本发布publish

当多个包存在依赖关系时,手动协调版本号和发布顺序易出错。

Lerna 方案:

  • 固定模式(Fixed):所有包强制共用同一版本号(如从 1.0.0 统一升级到 1.1.0),适合强关联包。
  • 独立模式(Independent):每个包可独立升级版本(如 [email protected] + [email protected]),通过交互式提示选择新版本。
  • 自动关联发布:若包A依赖包B,且B有更新,发布时会自动升级A的依赖版本。
  1. 批量执行脚本run

在所有包中执行相同命令(如测试、构建):

bash 复制代码
lerna run build  # 为每个包执行 `npm run build`
lerna run test --since main  # 仅测试自 main 分支有变更的包
  1. 智能包变更检测changed

列出自上次发布以来代码有修改的包,避免全量发布。

bash 复制代码
# 典型工作流示例

# 1. 初始化 Monorepo
lerna init

# 2. 创建两个包
lerna create utils
lerna create ui

# 3. 在 ui 包中声明依赖 utils
cd packages/ui
npm install @project/utils  # Lerna 会自动识别本地依赖

# 4. 链接所有依赖(关键步骤!)
lerna bootstrap

# 5. 修改 utils 代码后,统一发布新版本
lerna publish
# 交互式选择版本 → 更新依赖链 → 发布到 npm

Lerna 的局限性及现代替代方案:

问题 Lerna 方案 现代工具优化
构建速度慢 无内置优化 Turborepo/Nx 增量构建 + 缓存
依赖安装冗余 依赖提升(hoisting) pnpm 硬链接节省磁盘 + 严格依赖隔离
版本管理灵活性不足 支持独立模式但配置复杂 Changesets 精细控制版本生成
缺乏任务调度 仅顺序执行命令 Turborepo 并行执行 + 依赖拓扑排序
  • 新项目推荐 Turborepo + pnpm + Changesets 组合(速度更快、功能更现代)。
  • 旧 Monorepo 迁移可保留 Lerna 发布流程,用 Turborepo 替代 lerna run 提升构建性能。
相关推荐
小着3 小时前
vue项目页面最底部出现乱码
前端·javascript·vue.js·前端框架
啃火龙果的兔子8 小时前
前端八股文-react篇
前端·react.js·前端框架
自由鬼9 小时前
企业架构框架深入解析:TOGAF、Zachman Framework、FEAF与Gartner EA Framework
程序人生·架构
jiedaodezhuti9 小时前
EFK架构的数据安全性
架构
蓝色天空的银码星10 小时前
SpringCloud微服务架构下的日志可观测解决方案(EFK搭建)
spring cloud·微服务·架构
汪子熙11 小时前
在 Word 里编写 Visual Basic 调用 DeepSeek API
后端·算法·架构
灏瀚星空13 小时前
高频交易技术:订单簿分析与低延迟架构——从Level 2数据挖掘到FPGA硬件加速的全链路解决方案
人工智能·python·算法·信息可视化·fpga开发·架构·数据挖掘
沐森14 小时前
qiankun微前端
前端·架构
debug 小菜鸟14 小时前
MySQL 主从复制与一主多从架构实战详解
数据库·mysql·架构