Monorepo实战指南:提升前端项目管理效率的关键技术

FSD-丽萍 工具 阅读 1,791
赞 12 收藏
二维码
手机扫码查看
反馈

我的 Monorepo 写法,亲测靠谱

我折腾 Monorepo 已经快三年了,从一开始用 Lerna 硬扛,到后来迁移到 pnpm + workspace,踩过的坑能写半本《前端避雷手册》。现在项目里跑得稳的基本都是下面这套组合拳,虽然不是最炫的,但绝对是最省心的。

Monorepo实战指南:提升前端项目管理效率的关键技术

首先,我坚决不用 Lerna 了。不是它不好,是太重了。依赖安装慢、命令复杂、缓存机制反人类。现在我一律用 pnpm + workspace,轻量、快、依赖扁平还干净。初始化就一行:

pnpm init

然后在根目录加个 pnpm-workspace.yaml

packages:
  - 'packages/*'
  - 'apps/*'

这样 packages/ 里放共享库(比如 UI 组件、工具函数),apps/ 里放具体应用(比如后台、H5、小程序)。结构清晰,新人接手一眼就懂。

最关键的是依赖管理。我见过太多人把所有依赖都装在根目录,结果子项目一升级就崩。我的做法是:公共依赖只放根,业务依赖各自管

比如 reactlodash 这种通用库,放在根 package.json 里;而 axiosdayjs 这种某个 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_modulesdist。结果 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 成熟,有些冷门插件不兼容。但权衡下来,利远大于弊。

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流——毕竟前端这行,谁还没被工具链折磨过呢?

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论