主题
01 · Monorepo:为什么拆、怎么拆、怎么管
目标:说清 monorepo vs polyrepo 的取舍;能搭一个
apps + packages最小 workspace;对照 Journal 仓库讲清依赖边界与脚本约定。
1. 背景与目标
你已经在 Journal 里用过 pnpm --filter client dev:h5,但可能说不清:为什么要把 client / admin / backend 放在同一仓库,以及这和「每个项目一个 Git 仓库」差在哪。
| 你现在的痛点 | Monorepo 想解决什么 |
|---|---|
| 多端共享类型、工具函数 | 改一处,各应用同步引用 |
| 版本对不齐(API 改了前端没跟上) | 一次 PR 改全链路 |
| 各仓重复 ESLint / tsconfig | 抽到 packages/* 统一维护 |
| CI 要分别拉四个仓库 | 根目录一条流水线跑全量 |
不是银弹:仓库变大、权限粒度变粗、新人 clone 更慢。中小团队「一个产品多应用」通常 monorepo 更划算;完全独立商业线、不同发布节奏,polyrepo 仍合理。
2. 核心概念
2.1 Monorepo vs Polyrepo
| 维度 | Monorepo | Polyrepo |
|---|---|---|
| 代码位置 | 一个 Git 仓,多 package | 一应用一仓 |
| 依赖关系 | workspace 协议 workspace:* | npm 发包 + semver |
| 原子提交 | 前后端同 PR | 多仓协调、版本矩阵 |
| 工具 | pnpm / yarn / npm workspaces;可选 Turborepo | 各仓各自配置 |
2.2 典型目录骨架
text
my-monorepo/
├── package.json # 根脚本、devDependencies
├── pnpm-workspace.yaml # 声明哪些目录是 workspace 成员
├── apps/
│ ├── web/ # 可独立 dev/build 的应用
│ └── admin/
└── packages/
├── shared-types/ # 纯类型 / 工具,被 apps 引用
└── eslint-config/1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Journal 对照(扁平 workspace,无 apps/ 前缀):
text
journal/
├── pnpm-workspace.yaml # client, backend, admin, docs
├── package.json # pnpm --filter 聚合 dev/test
├── client/ # uni-app C 端
├── admin/ # React 管理后台
├── backend/ # package 名 schedule-management-backend
└── docs/ # journal-docs 文档站1
2
3
4
5
6
7
2
3
4
5
6
7
根 package.json 用 pnpm --filter client --filter admin ... --parallel run dev 并行起多应用——这是 编排层,不是把代码硬塞进一个 bundle。
2.3 包边界三原则
- apps 不互相 import:
client不应import自admin源码;共享逻辑进packages/*。 - 显式声明依赖:
package.json的dependencies里写"@my/shared": "workspace:*",不靠「碰巧能 resolve」。 - 共享配置向上抽:
tsconfig.base.json、eslint-config放 packages,各 appextends。
3. 最小实践:从零搭 workspace
bash
mkdir my-monorepo && cd my-monorepo
pnpm init1
2
2
pnpm-workspace.yaml:
yaml
packages:
- apps/*
- packages/*1
2
3
2
3
根 package.json:
json
{
"private": true,
"scripts": {
"dev": "pnpm --filter web dev",
"build": "pnpm -r build"
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
创建共享包 packages/shared-utils/package.json:
json
{
"name": "@my/shared-utils",
"version": "0.0.0",
"main": "index.js",
"exports": { ".": "./index.js" }
}1
2
3
4
5
6
2
3
4
5
6
apps/web/package.json 引用:
json
{
"name": "web",
"dependencies": {
"@my/shared-utils": "workspace:*"
}
}1
2
3
4
5
6
2
3
4
5
6
根目录执行 pnpm install,在 apps/web 里 import { formatDate } from '@my/shared-utils' 即可。验收:pnpm --filter web dev 能跑。
可选加速:加 Turborepo 做任务缓存与依赖图编排——Journal 当前未用,小仓不必强上。
4. 踩坑与取舍
| 坑 | 现象 | 默认做法 |
|---|---|---|
| 幽灵依赖 | 代码能 import 到未声明的包(靠 hoist 蹭到) | 开启 pnpm 严格模式;CI 跑 pnpm install --frozen-lockfile;禁止跨包深路径 import |
| 包边界泄露 | client 直接引 backend/src/xxx | 共享类型单独 package;API 契约用 OpenAPI / 生成类型 |
| 版本对齐 | 子包各自锁不同 major 的 vue | 根 pnpm.overrides 或 catalog(pnpm 9+)统一关键依赖 |
| 脚本分散 | 新人不知道进哪个目录跑命令 | 根 package.json 暴露 dev:client 等别名;文档写清 filter 名 |
| 发布差异 | 本地 workspace:* 上线要换成真实版本 | 内部包走私有 registry;或用 changesets 管版本 |
Journal 实例:根脚本统一 test、e2e:client;pre-commit 在根层跑 manifest 检查——质量门禁放根上,子包只关心自己的 test / build。
5. 验收清单
- [ ] 能向他人 30 秒说清 monorepo vs polyrepo,并举 Journal 一个例子
- [ ] 本地搭好
apps/web+packages/shared-utils,workspace:*引用成功 - [ ] 能解释什么是幽灵依赖,以及 pnpm 为何比 npm 扁平 node_modules 更严
- [ ] 能画出 Journal 四包关系:client / admin / backend / docs 各负责什么
- [ ] 根目录一条命令能触发至少一个子包的
dev或test
