Monorepo实战指南:提升前端项目管理效率的关键技术
我的 Monorepo 写法,亲测靠谱
我折腾 Monorepo 已经快三年了,从一开始用 Lerna 硬扛,到后来迁移到 pnpm + workspace,踩过的坑能写半本《前端避雷手册》。现在项目里跑得稳的基本都是下面这套组合拳,虽然不是最炫的,但绝对是最省心的。
首先,我坚决不用 Lerna 了。不是它不好,是太重了。依赖安装慢、命令复杂、缓存机制反人类。现在我一律用 pnpm + workspace,轻量、快、依赖扁平还干净。初始化就一行:
pnpm init
然后在根目录加个 pnpm-workspace.yaml:
packages:
- 'packages/*'
- 'apps/*'
这样 packages/ 里放共享库(比如 UI 组件、工具函数),apps/ 里放具体应用(比如后台、H5、小程序)。结构清晰,新人接手一眼就懂。
最关键的是依赖管理。我见过太多人把所有依赖都装在根目录,结果子项目一升级就崩。我的做法是:公共依赖只放根,业务依赖各自管。
比如 react、lodash 这种通用库,放在根 package.json 里;而 axios、dayjs 这种某个 app 才用的,就只装在那个 app 的目录下。这样避免污染,也方便按需升级。
另外,脚本统一用 pnpm -r 跑。比如:
pnpm -r build
它会自动遍历所有子包,按依赖顺序执行 build。比 Lerna 的 run 快多了,而且不会漏掉。
这几种错误写法,别再踩坑了
我见过太多反面教材,这里挑几个高频雷区说说。
错误一:所有子项目共用 tsconfig.json
有人为了省事,在根目录写一个 tsconfig.json,然后所有子项目继承它。乍一看挺优雅,但实际问题一堆:不同项目可能需要不同的 target(比如 Node 服务用 ES2020,前端用 ES2017),或者不同的 jsx 配置(React vs Vue)。结果就是改一个配置,三个项目炸。
我的做法是:每个子项目有自己的 tsconfig.json,但通过 extends 引用一个基础配置。比如根目录建个 tsconfig.base.json:
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"paths": {
"@myorg/utils": ["packages/utils/src"]
}
}
}
然后子项目里:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"jsx": "react-jsx"
},
"include": ["src"]
}
这样既复用又灵活,改基础配置不影响各项目特有设置。
错误二:用相对路径 import 兄弟包
比如在 apps/web 里这么写:
import { formatDate } from '../../packages/utils/src/date';
短期看没问题,但一旦目录结构调整,或者工具链不支持(比如某些打包器对跨 workspace 的相对路径处理异常),直接报错。更糟的是,TypeScript 的路径映射和这个对不上,类型检查可能失效。
正确做法是:**一律通过包名引用**。在 packages/utils/package.json 里定义好 name:
{
"name": "@myorg/utils"
}
然后在代码里直接:
import { formatDate } from '@myorg/utils';
配合前面说的 tsconfig.paths,开发时走源码,构建时走打包产物,无缝切换。
错误三:忽略 .gitignore 和 .npmignore
很多人以为 Monorepo 里子项目不会发包,就随便提交 node_modules、dist。结果 Git 历史膨胀得飞快,CI 跑得慢如蜗牛。更惨的是,如果某天要单独发布某个包(比如给第三方用),没配 .npmignore,把测试文件、源码全发出去,安全风险拉满。
我的习惯是:每个子项目都配 .npmignore,根目录配全局 .gitignore。比如:
# .gitignore
node_modules/
dist/
coverage/
*.log
# packages/utils/.npmignore
src/
tests/
.eslintrc.js
README.md
这样干净又安全。
实际项目中的坑
Monorepo 看似美好,但落地时总有意外。我总结几个实战中必须注意的点。
版本号管理是个大坑。如果你的子包之间互相依赖(比如 app-web 依赖 @myorg/ui),每次改 ui 都要手动 bump 版本再更新 app-web 的依赖?太痛苦了。我试过 changesets,但学习成本高;最后还是用 pnpm 的 workspace: 协议:
{
"dependencies": {
"@myorg/ui": "workspace:*"
}
}
这样无论 ui 怎么改,app-web 永远用最新本地版本,不用改版本号。发包时 pnpm 会自动替换成真实版本。省事到哭。
CI 构建策略要精细化。别一上来就全量 build。我用 pnpm --filter=... 只构建变更的包。比如结合 GitHub Actions:
# 只构建受影响的 app
pnpm --filter=...[origin/main] run build
配合缓存 node_modules,CI 时间从 8 分钟降到 2 分钟。不过要注意,如果子包有 breaking change,得手动触发全量测试,否则可能漏掉集成问题。
IDE 支持有时抽风。VS Code 对 Monorepo 的 TypeScript 路径映射偶尔失灵,表现为跳转失败或类型提示错乱。这时候删掉 .vscode 目录、重启 TS Server 基本能解决。实在不行,就在子项目根目录加个空的 tsconfig.json 强制识别。
还有个小细节:别在子项目里放 node_modules。虽然 pnpm 默认不会生成,但万一有人手误 npm install,就乱套了。我在根目录的 .gitignore 里加了 **/node_modules 双保险。
结尾碎碎念
Monorepo 不是银弹,小团队三两个项目可能反而增加复杂度。但如果你像我一样,维护着 5+ 个相互关联的前端项目,那这套方案真的能救命。它让我少写了 60% 的重复配置,CI 稳定性提升一大截,新人上手也快多了。
当然,这套方案也不是完美的。比如 pnpm 的生态工具不如 npm 成熟,有些冷门插件不兼容。但权衡下来,利远大于弊。
以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流——毕竟前端这行,谁还没被工具链折磨过呢?

暂无评论