还在为多个项目仓库切换而头疼?是时候拥抱一种更现代、更高效的管理方式了。
一、Monorepo 是个啥?
想象一下你正在管理一个大家庭的零食库。
-
传统方式(MultiRepo) :你有三个孩子,每个孩子都有自己的零食箱。老大一个,老二一个,老三一个。你想给所有人都发一种新零食,需要打开三个箱子,分别放进去。老大想尝尝老二的零食?得先问老二要,老二再从自己箱子里找出来给他。管理麻烦,共享更麻烦。
-
Monorepo方式 :你买了一个超大号的透明整理箱 ,里面用不同的收纳格分好。所有零食都放在这个大箱子里,但每个孩子有自己的专属格子。发新零食时,你直接往大箱子的"公共区"一放,所有人都能看见、能拿。老大想吃老二的零食?直接从老二的格子里拿就行(当然,得经过同意)。一目了然,共享便捷。 好的!这是一张详细的 Monorepo 和 MultiRepo 对比图介绍,包含了架构和对比表格。
css
MultiRepo (多仓库) 架构:
┌─────────────────┐ npm install ┌─────────────────┐
│ Project A │◄─────────────────│ Shared Lib 1 │
│ │ │ │
│ - package.json │ │ - package.json │
│ - src/ │ git clone │ - src/ │
│ - node_modules │ │ - node_modules │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ npm install ┌─────────────────┐
│ Project B │◄─────────────────│ Shared Lib 2 │
│ │ │ │
│ - package.json │ │ - package.json │
│ - src/ │ │ - src/ │
│ - node_modules │ │ - node_modules │
└─────────────────┘ └─────────────────┘
Monorepo (单仓库) 架构:
┌─────────────────────────────────────────────────────────┐
│ my-monorepo/ │
│ │
│ ├── packages/ │
│ │ ├── shared-lib-1/ ├── shared-lib-2/ │
│ │ │ - package.json │ - package.json │
│ │ │ - src/ │ - src/ │
│ │ └─────────────────┘ └─────────────────┘ │
│ │ │ │ │
│ ├── apps/ │ │ │
│ │ ├── project-a/ ├── project-b/ │
│ │ │ - package.json │ - package.json │
│ │ │ - src/ │ - src/ │
│ │ └─────────────────┘ └─────────────────┘ │
│ │ │ │ │
│ └── node_modules/ (提升到根目录,统一管理) │
│ - 所有共享依赖在这里 │
│ │
└─────────────────────────────────────────────────────────┘
核心差异对比表
特性 | MultiRepo (多仓库) | Monorepo (单仓库) |
---|---|---|
仓库数量 | 每个项目独立仓库 | 单个仓库包含所有项目 |
代码共享 | 通过 npm 包发布/安装,有版本延迟 | 直接内部引用,实时同步 |
依赖管理 | 各项目独立 node_modules ,可能重复 |
依赖提升,统一管理,减少冗余 |
版本控制 | 每个项目独立版本号 | 统一版本或独立版本(需工具支持) |
CI/CD | 每个仓库独立流水线 | 统一流水线,可增量构建 |
权限控制 | 按仓库精细控制 | 按目录控制,相对粗粒度 |
新人上手 | 需要 clone 多个仓库,环境复杂 | 一次 clone,完整开发环境 |
跨项目修改 | 需多个 PR,跨仓库协调 | 单次提交,原子性更改 |
适用场景 | 松散耦合的独立项目 | 紧密关联的家族项目、组件库 |
翻译成技术语言:
- MultiRepo(多仓库) :每个项目(比如一个独立的 npm 包、一个前端应用、一个后端服务)都有自己独立的 Git 仓库。它们之间通过
npm install
或者git submodule
来建立联系。 - Monorepo(单仓库) :把所有相关的项目都放在同一个 Git 仓库里进行管理。它们虽然是独立的,但都在你的眼皮子底下。
所以,Monorepo 的核心思想就是:把多个项目"物理"上放在一起,"逻辑"上保持独立。
二、为什么要用Monorepo?它的"香"在哪里?
Monorepo 不是银弹,但在特定场景下,它能让你和团队的效率飙升。
1. 依赖管理,天下大同
在 MultiRepo 中,如果 A 项目和 B 项目都依赖了 lodash
,但版本不同,你可能会遇到棘手的依赖冲突。在 Monorepo 中,可以通过工具(如 pnpm)将所有依赖提升到根目录统一管理,大大减少重复安装和版本不一致的问题。
2. 代码共享,轻而易举
你有一个自己写的 utils
工具函数库。在 MultiRepo 里,你需要先把它发布到 npm,然后在其他项目里 npm install
。在 Monorepo 里,你直接通过 "@project/utils"
这样的路径就能引用了,就像在同一个项目里引用不同文件夹一样简单,极大促进了代码复用。
3. 重构无忧,原子提交
当你修改了一个共享工具函数时,在 MultiRepo 中,你需要先在工具库提交,发布新版本,然后在所有依赖它的项目里更新版本并测试,提交多个仓库。在 Monorepo 中,你一次提交(原子提交) 就可以同时修改工具函数和所有依赖它的项目,并且 CI/CD 可以一次性跑所有项目的测试,确保你的修改没有破坏任何项目。
4. 开发环境,高度一致
所有项目都在一个仓库里,git clone
一次就能获得完整的开发环境。新同事 onboarding 时,再也不用为配置五六个项目环境而抓狂了。
适用场景:
- 技术栈统一的家族产品:比如公司有主站、后台、移动端H5,它们技术栈相同(都是 React + TypeScript),且共享组件和工具库。
- UI 组件库和业务项目:业务项目直接链接(link)到本地的组件库进行开发和调试。
- 全栈应用:前端 React 应用和后端 Node.js 服务放在一起,可以轻松地一起部署。
三、动手搭建:一个现代 Monorepo 实战
理论说再多,不如上手干。我们来搭建一个基于 pnpm + workspace 的现代 Monorepo 项目,这是目前最流行和高效的组合。
第一步:初始化项目
bash
# 创建一个新文件夹,并进入
mkdir my-monorepo
cd my-monorepo
# 初始化 package.json
pnpm init
第二步:配置核心 ------ pnpm-workspace.yaml
这个文件是 pnpm 的 workspace 功能的灵魂,它告诉 pnpm:"哪些文件夹是我要管理的子项目"。
在项目根目录创建 pnpm-workspace.yaml
文件:
yaml
packages:
# 所有在 packages/ 子目录下的项目
- 'packages/*'
# 所有在 apps/ 子目录下的项目 (比如你的Vue/React应用)
- 'apps/*'
现在,你的目录结构应该是:
go
my-monorepo/
├── package.json
└── pnpm-workspace.yaml
第三步:创建我们的子项目
让我们创建两个包和一个应用,模拟真实场景。
- 创建共享工具库
utils
bash
mkdir -p packages/utils
cd packages/utils
pnpm init
修改生成的 package.json
,给它起个带作用域的名字,这更专业:
json
{
"name": "@my-monorepo/utils",
"version": "1.0.0",
"main": "index.js",
"types": "index.d.ts", // 如果有TypeScript
"scripts": {}
}
创建一个简单的函数,在 index.js
中:
javascript
module.exports.sayHello = (name) => {
return `Hello, ${name} from shared utils!`;
};
- 创建共享UI组件库
ui-button
bash
# 在项目根目录执行
mkdir -p packages/ui-button
cd packages/ui-button
pnpm init
修改 package.json
:
json
{
"name": "@my-monorepo/ui-button",
"version": "1.0.0",
"main": "index.js",
"scripts": {},
"dependencies": {}
}
在 index.js
中创建一个按钮组件(假设是React):
javascript
import React from 'react';
export const MyButton = ({ children }) => {
return <button style={{ padding: '10px 20px' }}>{children}</button>;
};
- 创建业务应用
web-app
bash
# 在项目根目录执行
mkdir -p apps/web-app
cd apps/web-app
# 这里你可以用 Vite 或 Create-React-App 等脚手架初始化
pnpm init
修改 package.json
,并声明它对前面两个包的依赖:
json
{
"name": "@my-monorepo/web-app",
"version": "1.0.0",
"scripts": {
"dev": "vite" // 假设你用Vite
},
"dependencies": {
"@my-monorepo/utils": "workspace:*",
"@my-monorepo/ui-button": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
注意 "workspace:*"
,这个语法告诉 pnpm:"请直接链接到 workspace 内的那个包",而不是去 npm 上下载。
第四步:安装依赖与内部链接
在项目根目录执行神奇的命令:
bash
pnpm install
这一刻,pnpm 会:
- 读取
pnpm-workspace.yaml
,发现packages/*
和apps/*
下的所有项目。 - 分析所有子项目的依赖关系。
- 将
web-app
对@my-monorepo/utils
和@my-monorepo/ui-button
的依赖,直接 symlink(符号链接)到本地的这两个包。 - 将所有公共依赖(如
react
)提升到根目录的node_modules
中,避免重复安装。
现在,你可以在 apps/web-app
中愉快地使用本地包了:
javascript
// 在 apps/web-app/src/App.jsx 中
import { MyButton } from '@my-monorepo/ui-button';
import { sayHello } from '@my-monorepo/utils';
function App() {
console.log(sayHello('Developer'));
return (
<div>
<h1>My Monorepo App</h1>
<MyButton>Click Me!</MyButton>
</div>
);
}
第五步:使用脚本和过滤命令
Monorepo 的强大还在于可以统一运行命令。
在根目录的 package.json
中添加脚本:
json
{
"scripts": {
"dev": "pnpm --filter \"./apps/**\" dev",
"build": "pnpm -r run build",
"test": "pnpm -r run test"
}
}
pnpm --filter <package_name> <command>
: 只对某个特定的包执行命令。例如pnpm --filter @my-monorepo/utils test
。pnpm -r <command>
: 在所有子项目中运行该命令。-r
是--recursive
的缩写。
四、进阶与最佳实践
- 版本管理与发布 :对于需要发布的包,可以使用
changesets
来管理版本号和生成 CHANGELOG,它能智能地识别哪些包需要被发布。 - CI/CD 优化 :在 GitHub Actions 或 GitLab CI 中,可以利用
pnpm -r --filter
等命令,只对发生变化的项目进行构建和测试,而不是全部,极大加快流水线速度。 - 代码规范:在根目录配置统一的 ESLint、Prettier,确保所有子项目代码风格一致。
总结
Monorepo 就像是从"租单间"变成了"买下一整层楼",你把所有的"家人"(项目)都安置在一起,沟通成本极大降低,协作效率自然提升。
核心优势: 依赖管理简单、代码共享直接、重构安全、环境统一。
技术选型: pnpm workspace
是当前前端领域搭建 Monorepo 的首选,因其磁盘效率和依赖管理能力非常出色。
如果你的团队正被多个仓库间的依赖、调试和版本管理问题所困扰,不妨就从今天这个简单的例子开始,尝试一下 Monorepo,体验一下"真香"定律吧!
希望这篇文章对你有帮助!欢迎在评论区交流你的看法和实践经验。