每隔一段时间,前端圈就会冒出一个新的「工程化」概念。它在分享会上闪闪发光,在真实项目里却常常水土不服。过去三年,我们依次尝试了 Monorepo、微前端、模块联邦——有的留下了真实收益,有的只留下了债。
这篇文章不站队,只复盘:每个方案在什么规模下值得用,在什么规模下是过度工程。
工程化的初衷:解决问题还是制造问题
所有工程化方案都应该先回答一个问题——它解决的痛点,是否真的存在。下面这张表是我们每次评估新方案时的第一道筛子:
| 自检问题 | 该用 | 别用 |
|---|---|---|
| 当前痛点是否量化 | 有数字 | 只是"感觉乱" |
| 团队规模 | ≥ 5 人长期协作 | 1–2 人项目 |
| 方案的学习成本 | 一周内能上手 | 需要专人维护 |
| 退出成本 | 能逐步回退 | 一旦上马不可逆 |
这张表挡住了我们 60% 的「追新」冲动。剩下的 40%,再按具体方案的边界逐一评估。
工程化方案的收益,应该用「它解决了多少真实工单」来衡量,而不是"上了多先进的工具"。没有痛点数字背书的架构升级,大概率只是技术债换个名字。
先用尽简单方案,再上复杂方案。
— 工程化的第一原则
Monorepo:值不值的这笔账
Monorepo 是我们第一个真正落地的工程化方案。它的卖点是「共享代码、统一发版」,但成本藏在工具链里。
收益:真实的
我们有六个产品共享一套设计系统和工具函数。在多仓库时代,改一个 Token 要发六个包、升六个版本。切到 Monorepo 后:
成本:被低估的
// pnpm-workspace.yaml —— Monorepo 的入门门槛不高
packages:
- 'apps/*'
- 'packages/*'
// 但真正的成本在 CI 与缓存
// 一次提交可能触发跨包构建,缓存策略要重新设计
// "我只改了一个包" ≠ "只构建一个包"
真实的隐性成本清单:
- 构建缓存:跨包依赖让增量构建的判定变难,CI 时间反而上升。
- 权限边界:所有人都能改所有包,需要 CODEOWNERS 守住边界。
- 发版策略:Changesets / Lerna 的学习曲线,团队要专人掌握。
- IDE 性能:大型 Monorepo 让 TS Server 明显变慢,需要 project references。
| 规模 | 建议 |
|---|---|
| 1–2 个产品 / < 5 人 | 多仓库足够,别上 Monorepo |
| 3+ 个产品共享代码 / 5–20 人 | Monorepo 收益开始大于成本 |
| 10+ 产品 / 20+ 人 | Monorepo 几乎是刚需 |
微前端:拆分还是拆散
微前端是争议最大的一个。它的卖点是「独立部署、团队解耦」,但反面是「运行时拼装、体验割裂」。
什么时候真的需要
微前端的真正适用场景很窄——多个独立团队、技术栈异构、且必须统一为一个入口。比如收购合并后的多个系统、跨部门的大型平台。绝大多数中型项目,根本到不了这个规模。
如果你的"微前端"动机是"组件太多想拆分",你用错了工具。组件拆分用 Monorepo + 懒加载就够,微前端的成本是运行时拼装与样式隔离,与"代码多"无关。
真实的代价
| 问题 | 多仓单页 | 微前端 |
|---|---|---|
| 首屏 JS 体积 | 单 bundle | N 个运行时 |
| 样式隔离 | 天然 | 需 Shadow DOM / scope |
| 路由与状态共享 | 直接 | 跨域通信成本 |
| 独立部署 | 否 | 是 |
我们最终只在两个场景用了微前端:一是收购来的老系统嵌入新壳,二是异构技术栈(Vue + React)必须共存。其余场景,Monorepo + 懒加载 + 路由分块永远是更便宜的方案。
微前端解决的是组织问题,不是技术问题。
— 康威定律的延伸
模块联邦:理想与现实的落差
模块联邦(Module Federation)刚出来时被誉为"微前端的终局"。它的愿景很诱人——运行时按需加载另一个独立构建的模块,像 import 本地代码一样。
// webpack.config.js —— 暴露一个模块
new ModuleFederationPlugin({
name: 'shop',
filename: 'remoteEntry.js',
exposes: { './Cart': './src/Cart' },
shared: ['react', 'react-dom'],
});
// 消费方:像本地 import 一样用远程模块
const Cart = React.lazy(() => import('shop/Cart'));
理想里,它解决了 Monorepo 的"构建耦合"和微前端的"体验割裂"两个问题。现实是——它把两者的复杂度叠加了。
三道坎
- 共享依赖的版本对齐:React 版本不一致时,运行时会崩。需要严格的 shared 配置和单例约束。
- 类型安全断裂:跨构建边界后,TS 类型靠
@types/remote手动同步,容易漂移。 - 调试链路变长:一个报错可能跨三个独立构建,source map 要专门配置才能拼起来。
我们的结论——模块联邦适合"同一团队、多构建产物"的场景,比如一个 Monorepo 内把重型模块拆成独立构建以加速 CI。跨团队、跨技术栈用它,维护成本会吞噬收益。
评估模块联邦时,先问"构建是否真的太慢"。如果只是想"看起来更解耦",标准代码分割 + 懒加载的性价比远高于联邦。
边界感:什么时候止步
三年走下来,我们对每个方案给出了一条"该止步的红线":
| 方案 | 该止步的信号 |
|---|---|
| Monorepo | CI 超过 15 分钟、TS Server 明显卡顿 |
| 微前端 | 运行时拼装带来可见的交互延迟 |
| 模块联邦 | shared 依赖对齐消耗超过构建本身 |
工程化的本质不是"用上多先进的工具",而是用合适的复杂度解决真实的痛点。分享会上的架构图永远比生产环境漂亮,但真正决定项目寿命的,是那些没出现在图上的隐性成本。
把下面四句话贴到团队的墙上,比任何架构文档都管用:
- 先量化痛点,再选方案。没有数字的"感觉乱"不是理由。
- 先用尽简单方案。懒加载和路由分块能解决 80% 的"太大"问题。
- 评估退出成本。不可逆的方案要加倍谨慎。
- 定期复看是否还需要。痛点消失后,方案就该退役。
最后一句——工程化不是目的,是手段。把它当目的的人,最终会发现自己维护的不是产品,而是工具链本身。