在上一篇中对 Monorepo 的基本概念进行了讲解,在本篇文章中,将会结合项目代码,对 Monorepo 的工程落地进行一个应用。大家可以结合实际代码在本地运行一下整个项目,对项目的目录结构、运行方式、函数、模块如何调用,以及在 Monorepo 仓库中的各个子包是如何关联调用起来的。
上一篇的概念梳理文章地址:深入理解 Monorepo:概念梳理新技术的的诞生一定是有它的技术背景的。新技术一定是适合公司、团队的技术吗?我看这倒未 - 掘金
GitHub 地址:wangyang-tpri/MyMonorepo: monorepo 单体化仓库
从0到1完整搭建一个可用的 monorepo 单体化仓库,并将工程治理工具(ESLint+Prettier+dependency-cruiser+CODEOWNERS+manypkg)全部配置到项目中了。
项目本地运行成功截图:可以看到 Monorepo 项目中的 web 应用分别使用了 ui-components 和 utils。
注意: 使用 pnpm + workspace 创建的 Monorepo 项目,不管是根目录下的依赖,还是子包中的依赖都不能使用 yarn 和 npm 安装和管理的,必须统一使用 pnpm 进行管理。
一、目录结构设计
1.1 常见布局
my-monorepo/
├── apps/ # 可部署的应用
│ ├── web/ # 前端web应用
├── packages/ # 可复用共享库
│ ├── ui-components/ # 组件库 @scope/ui
│ ├── utils/ # 工具函数 @scope/utils
│ ├── hooks/ # Vue Hooks @scope/hooks
│ └── config/ # 统一配置 @scope/config(eslint/构建)
├── tools/ # 工程脚本/CLI
├── .github/ # CI 工作流
├── pnpm-workspace.yaml # 工作区声明
├── turbo.json # 任务编排(可选)
├── package.json # 根 package.json
├── pnpm-lock.yaml # 统一锁文件
└── README.md
1.2 命名与规范要点
- 包名统一使用
@scope/包名,包目录名与包名保持一致(如packages/ui↔@scope/ui); - 根目录只放公共配置与脚本,不放业务代码;
- 公共配置(eslint、prettier、tsconfig、构建预设)抽到
packages/config或根配置,避免重复; - 用
package.json的private: true区分"可发布包"与"纯内部包"。
二、工程实践:从零搭建一套 Monorepo
2.1 第一步:pnpm + workspace 起步
# 初始化
npm i -g pnpm
mkdir my-monorepo && cd my-monorepo
pnpm init
创建 pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"
创建三个测试包,apps/web 依赖 packages/ui-components 依赖 packages/utils:
依赖关系为:utils -> ui-components -> app-web
// packages/ui-components(节选)
{
"name": "@my-monorepo/ui",
"version": "0.0.0",
"dependencies": {
"@my-monorepo/utils": "workspace:*"
}
}
安装并验证软链:
pnpm install
此时 packages/ui-components 下的 @my-monorepo/utils 会直接链接到本地 @my-monorepo/utils,改动即时生效。apps/app-web 也是同样的道理,会直接链接到本地的 @my-monorepo/uitls 和 @my-monorepo/ui
2.2 第二步:接入 Turborepo 做任务编排
pnpm add -D turbo
根目录 turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.js", "src/**/*.vue", "test/**/*.js"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
根 package.json scripts:
{
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint"
}
}
此后 pnpm build 会按依赖图并行、缓存化地构建所有包。
2.4 第五步:统一代码质量与边界
- 根目录统一
ESLint+Prettier配置; - 用
eslint-plugin-boundaries或dependency-cruiser(依赖关系)检查包间依赖是否越界; - 用
CODEOWNERS(代码评审)管理关键目录的评审负责人; - 引入
syncpack/manypkg(依赖版本)检查跨包依赖版本是否一致。 - 根目录管
全局和调度, 子包管自身和执行。
三、什么时候不该用 Monorepo
Monorepo 不是银弹,以下情况请慎重:
- 团队很小、项目彼此独立:共享代码很少,合并后徒增工具链成本;
- 不同团队技术栈/发布节奏严重割裂:强行统一会引发大量内耗;
- 没有工程化人力维护工具链:缓存、影响范围分析、依赖治理都做不好,Monorepo 反而拖慢所有人;
- 仓库文件巨大、二进制多:需要额外投入 Git LFS、浅克隆等基建。
一个务实的判断:从 pnpm workspace 起步,等痛点出现(构建太慢、需要缓存)再渐进引入 Turborepo/Nx,而不是一开始就上重型方案。
四、从 Multi-repo 迁移到 Monorepo
推荐渐进式迁移,避免"大爆炸式"合并:
五、总结
Monorepo 本质上是一种工程组织方式,它把"跨项目协作"的成本从"发布-安装-协调"转移到了"工具链建设"上:
- 适合:多项目强关联、频繁共享代码、需要原子变更与统一治理的团队;
- 不适合:项目彼此独立、追求极致自治、缺乏工程化投入的小团队;
- 选型路径:小型起步
pnpm workspace→ 需要任务编排加Turborepo→ 复杂企业级/需要精确影响范围分析选Nx→ 纯多包发布用Changesets。
工具会迭代,但底层理念稳定:共享、原子、统一、可缓存。理解这些机制,比追逐某个具体工具更重要。