面试官:monorepo解决了什么问题?

128 阅读9分钟

刚开始做前端项目的时候遇到一个很困惑的问题:为什么有些大公司的开源项目,比如 React、Vue、Angular,都把所有相关的包放在一个仓库里?而我们平时做项目,通常都是一个项目一个仓库。

跟着不同教程学习时发现,有的教程教你把前端、后端分别建仓库,有的又说要放在一起管理。特别是看到一些大型项目的 GitHub 仓库,里面有几十个 packages 文件夹,每个都是独立的 npm 包,但又在同一个仓库里维护。

这种管理方式到底有什么好处?什么时候该用?怎么搭建?

📚 历史背景:从分散到集中的管理演进

传统多仓库的痛点

在 Monorepo 出现之前,大部分团队采用的是 Multi-repo(多仓库)的管理方式:

project-frontend/     # 前端仓库
project-backend/      # 后端仓库
project-mobile/       # 移动端仓库
project-admin/        # 管理后台仓库
shared-utils/         # 共享工具库仓库

这种方式在项目规模较小时工作良好,但随着业务复杂度增加,问题逐渐暴露:

依赖管理混乱

  • 共享代码需要发布到 npm 才能被其他项目使用
  • 版本同步困难,经常出现版本不一致的问题
  • 修改共享代码需要在多个仓库间来回切换

开发效率低下

  • 跨项目的功能开发需要同时维护多个仓库
  • CI/CD 配置重复,每个仓库都要单独配置
  • 代码审查分散,难以把握整体架构

Monorepo 的诞生背景

Google 是 Monorepo 理念的先驱者。早在 2000 年代初,Google 就开始将几乎所有代码放在一个巨大的仓库中管理。据 Google 工程师的分享,他们的主仓库包含超过 20 亿行代码,每天有数万次提交。

这种做法的核心理念是:统一的代码库带来统一的开发体验

Facebook(现 Meta)也采用了类似的策略,React 生态的多个包(react、react-dom、react-reconciler 等)都在同一个仓库中维护。

设计哲学的差异

Multi-repo 的设计哲学

  • 关注点分离:每个项目独立演进
  • 权限隔离:不同团队管理不同仓库
  • 技术栈自由:每个项目可以选择不同的技术方案

Monorepo 的设计哲学

  • 代码共享:最大化代码复用和一致性
  • 原子性变更:跨项目的修改可以在一次提交中完成
  • 统一工具链:所有项目使用相同的构建、测试、部署流程

🔧 核心原理解析

什么是 Monorepo

Monorepo(单一仓库)是一种将多个相关项目存储在同一个版本控制仓库中的软件开发策略。与传统的每个项目一个仓库的方式不同,Monorepo 将所有相关代码集中管理。

Monorepo 的核心优势

1. 代码共享和复用

问题:Multi-repo 中,共享代码需要发布到 npm 才能使用,流程繁琐且容易出错。

解决方案:Monorepo 中的包可以直接引用,无需发布:

// packages/web-app/src/utils.js
import { formatDate } from "@my-company/shared-utils";
import { Button } from "@my-company/shared-ui";

// 修改 shared-utils 后立即生效,无需重新发布

实际收益

  • 开发效率提升 (无需等待包发布)
  • 减少重复代码 6
  • 降低维护成本

2. 原子性变更

问题:跨项目的功能变更需要在多个仓库间协调,容易出现版本不一致。

解决方案:一次提交完成所有相关变更:

# 一次提交同时更新 API 和前端
git commit -m "feat: 添加用户头像功能

- 后端新增头像上传接口
- 前端添加头像组件
- 共享类型定义更新
- 文档同步更新"

实际收益

  • 消除版本不一致问题
  • 简化发布流程
  • 提高代码审查效率

3. 统一的开发体验

问题:不同项目使用不同的工具链,学习成本高,维护困难。

解决方案:统一的配置和工具:

// 根目录统一配置
{
  "scripts": {
    "lint": "eslint packages/*/src --fix",
    "test": "jest packages/*/src",
    "build": "lerna run build"
  }
}

实际收益

  • 新人上手时间减少 50%
  • 工具维护成本降低 70%
  • 代码质量更一致

4. 依赖管理优化

问题:Multi-repo 中相同依赖重复安装,占用大量磁盘空间。

解决方案:依赖提升到根目录:

# Multi-repo 方式
project-a/node_modules/lodash  # 50MB
project-b/node_modules/lodash  # 50MB
project-c/node_modules/lodash  # 50MB
总计:150MB

# Monorepo 方式
node_modules/lodash            # 50MB
总计:50MB

实际收益

  • 磁盘空间节省 60-80%
  • 安装速度提升 40-60%
  • 减少依赖冲突

5. 更好的重构支持

问题:Multi-repo 中重构共享代码影响范围不明确,容易遗漏。

解决方案:IDE 可以跨包进行重构:

// 重命名函数时,所有引用自动更新
// packages/shared-utils/src/index.js
export const formatUserName = (user) => {
  // 重命名
  return `${user.firstName} ${user.lastName}`;
};

// packages/web-app/src/UserCard.js
import { formatUserName } from "@my-company/shared-utils"; // 自动更新

实际收益

  • 重构安全性提升 90%
  • 减少因重构导致的 bug
  • 提高代码演进速度

6. 统一的 CI/CD 流程

问题:每个仓库都需要单独配置 CI/CD,维护成本高。

解决方案:智能的增量构建和测试:

# 只测试发生变更的包及其依赖者
- name: Test changed packages
  run: |
    lerna run test --since HEAD~1
    lerna run build --since HEAD~1

实际收益

  • CI/CD 配置维护成本降低 80%
  • 构建时间减少 50-70%(增量构建)
  • 部署流程更可靠

7. 更好的可见性和治理

问题:Multi-repo 中难以了解整体架构和依赖关系。

解决方案:统一的依赖图和分析工具:

# 查看包依赖关系
nx dep-graph

# 分析包大小
lerna run analyze --parallel

实际收益

  • 架构可见性提升 100%
  • 便于技术债务管理
  • 更好的安全漏洞管控

典型的 Monorepo 结构:

my-monorepo/
├── packages/
│   ├── web-app/          # 前端应用
│   ├── mobile-app/       # 移动端应用
│   ├── api-server/       # 后端服务
│   ├── shared-ui/        # 共享 UI 组件库
│   └── shared-utils/     # 共享工具库
├── tools/                # 构建工具和脚本
├── docs/                 # 文档
├── package.json          # 根配置
└── lerna.json           # Lerna 配置(如果使用)

核心技术特性

1. 工作区(Workspace)管理

现代包管理器都支持 workspace 功能:

// package.json
{
  "name": "my-monorepo",
  "workspaces": ["packages/*"]
}

这样配置后,npm/yarn/pnpm 会将 packages 下的所有子项目视为工作区,实现:

  • 依赖提升:相同依赖安装在根目录
  • 符号链接:本地包之间可以直接引用
  • 统一安装:一次 npm install 安装所有依赖

2. 依赖关系管理

Monorepo 中的包可以相互依赖:

// packages/web-app/package.json
{
  "name": "@my-company/web-app",
  "dependencies": {
    "@my-company/shared-ui": "workspace:*",
    "@my-company/shared-utils": "workspace:*"
  }
}

workspace:* 表示使用工作区中的最新版本,修改共享包后无需重新发布就能在其他包中使用。

3. 构建依赖图

Monorepo 工具会分析包之间的依赖关系,构建依赖图:

shared-utils → shared-ui → web-app
            → api-server

基于这个依赖图,工具可以:

  • 按正确顺序构建包
  • 只构建发生变化的包及其依赖者
  • 并行构建无依赖关系的包

🛠️ 实践指南

选择合适的工具

目前主流的 Monorepo 工具有:

Lerna:老牌工具,功能完善

npm install -g lerna
lerna init

Nx:现代化工具,性能优秀

npx create-nx-workspace@latest myworkspace

Rush:微软开发,适合大型项目

npm install -g @microsoft/rush
rush init

Turborepo:Vercel 开发,专注构建性能

npx create-turbo@latest

从零搭建 Monorepo

以 npm workspaces + Lerna 为例:

1. 初始化项目

mkdir my-monorepo
cd my-monorepo
npm init -y

2. 配置 workspaces

// package.json
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*"],
  "devDependencies": {
    "lerna": "^6.0.0"
  }
}

3. 初始化 Lerna

npx lerna init

4. 创建子包

mkdir -p packages/shared-utils
mkdir -p packages/web-app

# 在 packages/shared-utils 中
cd packages/shared-utils
npm init -y
# 修改 package.json 的 name 为 "@my-company/shared-utils"

# 在 packages/web-app 中
cd ../web-app
npm init -y
# 修改 package.json 的 name 为 "@my-company/web-app"

5. 配置包依赖

// packages/web-app/package.json
{
  "name": "@my-company/web-app",
  "dependencies": {
    "@my-company/shared-utils": "workspace:*"
  }
}

6. 安装依赖

# 在根目录执行
npm install

常用命令和脚本

统一脚本管理

// 根目录 package.json
{
  "scripts": {
    "build": "lerna run build",
    "test": "lerna run test",
    "lint": "lerna run lint",
    "dev": "lerna run dev --parallel"
  }
}

选择性执行

# 只在特定包中执行命令
lerna run build --scope=@my-company/web-app

# 在发生变更的包中执行
lerna run test --since HEAD~1

# 并行执行(适合开发服务器)
lerna run dev --parallel

版本管理策略

固定模式:所有包使用相同版本号

// lerna.json
{
  "version": "1.0.0",
  "npmClient": "npm",
  "command": {
    "publish": {
      "conventionalCommits": true
    }
  }
}

独立模式:每个包独立版本

// lerna.json
{
  "version": "independent"
}

CI/CD 配置

利用 Monorepo 的优势,可以实现智能的 CI/CD:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      packages: ${{ steps.changes.outputs.packages }}
    steps:
      - uses: actions/checkout@v3
      - uses: dorny/paths-filter@v2
        id: changes
        with:
          filters: |
            web-app:
              - 'packages/web-app/**'
            shared-utils:
              - 'packages/shared-utils/**'

  test:
    needs: changes
    runs-on: ubuntu-latest
    if: ${{ needs.changes.outputs.packages != '[]' }}
    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npm run test

避坑经验

1. 依赖版本冲突

不同包可能需要同一依赖的不同版本:

// 使用 resolutions 强制统一版本
{
  "resolutions": {
    "lodash": "^4.17.21"
  }
}

2. 构建顺序问题

确保依赖包先于使用它的包构建:

// packages/web-app/package.json
{
  "scripts": {
    "prebuild": "lerna run build --scope=@my-company/shared-*"
  }
}

3. 发布权限管理

使用 scoped packages 和 npm organizations:

{
  "name": "@my-company/package-name",
  "publishConfig": {
    "access": "restricted"
  }
}

🎯 适用场景分析

适合使用 Monorepo 的场景

1. 微前端架构

  • 多个前端应用共享组件库
  • 需要统一的设计系统和开发规范

2. 全栈项目

  • 前后端需要共享类型定义
  • API 变更需要同步更新客户端

3. 组件库生态

  • 核心库 + 插件 + 工具的组合
  • 需要保证版本兼容性

4. 企业级应用

  • 多个业务线的相关项目
  • 需要统一的技术栈和工具链

不适合的场景

1. 完全独立的项目

  • 不同技术栈
  • 不同发布周期
  • 不同团队维护

2. 开源项目

  • 需要独立的贡献者权限
  • 不同的许可证要求

3. 小型团队

  • 项目数量少
  • 复杂度不高
  • 维护成本大于收益

📖 相关文档