微前端与自研组件库
qiankun 微前端选型与坑、样式隔离、JS 沙箱边界、通信与公共依赖;自研组件库的 Design Token、按需引入、tree-shaking 与防回退。
微前端与自研组件库
前端工程化的两个实战方向:把单体拆成微前端、把重复 UI 收敛成组件库。重心不在概念,而在决策链、坑、效益、代价。
第一部分 · 微前端(qiankun)
为什么要拆:把痛点变成数字
答这题千万别说「因为微前端很流行」。先把旧架构问题量化:
| 维度 | 单体痛在哪 |
|---|---|
| 构建时长 | 全量构建 21 分钟,改一行也要等 |
| 发布节奏 | 统一发版,一个团队没准备好全员卡住 |
| 技术栈 | 锁定旧版本,升级牵一发动全身 |
| 故障半径 | 任一模块报错可能白屏全站 |
| 组织 | 4 团队改同一份路由表,必冲突 |
一句话:单体的瓶颈不在性能,在组织协作——微前端解决的首要问题是解耦发布权,其次才是技术栈自由度。
选型决策表
| 方案 | 隔离能力 | 体验 | 改造成本 | 适用 |
|---|---|---|---|---|
| iframe | 最强 | 差(路由不同步/弹窗被裁) | 低 | 异构老系统嵌入 |
| qiankun | 好(JS 沙箱+样式隔离) | 好 | 中 | 存量 Vue/React 渐进迁移 |
| Module Federation | 弱(自行处理隔离) | 好 | 中高 | 新建项目/同栈运行时共享 |
| wujie | 很强(iframe+Web Components) | 好 | 中 | 需要强隔离 |
上微前端的门槛是「多团队 + 独立发布诉求」,不是「代码多」。
迁移六阶段
盘点 → 划界 → 基座 → 试点 → 批量 → 收尾。
- 划界按业务域拆,不按技术分层拆(错的:header-app/table-app;对的:权限中心/资产管理)。主应用只留横切能力(登录态/权限/菜单/路由/埋点),绝不放业务组件——这条守不住,主应用半年后又变第二个单体。
- 基座:主应用持顶层路由表;token 存主应用,通过 props 下发;权限双层(路由级能不能进 + 按钮级能不能点)。
- 试点:选最小最独立模块先迁,产出《子应用接入 SOP》。
- 批量:路由级灰度(新旧路由并行、配置开关切流、出问题一键切回)。
- 收尾:清老路由/老配置,上子应用维度监控 + CI 门禁。
关键设计
样式隔离(弹窗是重灾区)
| 策略 | 原理 | 代价 |
|---|---|---|
strictStyleIsolation | Shadow DOM 真隔离 | 弹窗挂 body 丢样式、三方库不兼容 |
experimentalStyleIsolation | 选择器加前缀 | 挂 body 仍逃逸 |
| 构建期加前缀(PostCSS) | 构建时给选择器加子应用前缀 | 最稳、无运行时开销 |
一句话:样式隔离的难点不是隔离子应用内部样式,是隔离挂到 body 上的那部分样式(弹窗、抽屉、下拉全在 body 上)。解法:统一 getPopupContainer + 构建期前缀 + experimentalStyleIsolation 兜底。
JS 沙箱边界(区分度所在)
qiankun 的 ProxySandbox 给每个子应用一个 fakeWindow,读写都走代理——但它只拦 window.xxx = ... 属性操作,拦不住三类副作用:
- 事件监听:
window.addEventListener沙箱不管,卸载后回调仍在跑 - 定时器:
setInterval没清,切走还在跑 - 全局副作用:改
document.body的 class、往 document 挂节点、prototype 污染
解法:自研副作用收集器,mount 时注册、unmount 时统一清理——把「记得清理」从人的纪律变成框架的默认行为。
通信:三类场景三种手段
| 场景 | 手段 |
|---|---|
| 主 → 子:传上下文 | props(登录态/request 实例,最简单可靠) |
| 状态共享(广播) | initGlobalState + onGlobalStateChange |
| 事件通知(一次性) | 事件总线(基于 CustomEvent,成对 on/off 防泄漏) |
原则:能用 props 就别用全局状态(隐式依赖等于把单体耦合搬了个地方)。
公共依赖共享
Vue/UI 库/axios 走 externals 统一共享(大且版本能统一);业务工具库各自打包。公共依赖升版本必须主应用先升 + 全子应用回归。
坑清单(15 条核心)
| # | 坑 | 一句话根因 |
|---|---|---|
| 1 | 弹窗/抽屉样式全丢 | 挂到 body,跑出隔离范围 |
| 2 | 子应用样式污染全局 | 全局 CSS 未加前缀 |
| 3 | 定时器越跑越多 | setInterval 未清、监听未解绑,沙箱不拦 |
| 4 | 打包后资源 404 | publicPath 写死 |
| 5 | 主子发版不同步状态错乱 | 全局状态/事件契约无版本兼容 |
| 6 | Vue/UI 库加载两份 | 公共依赖未 external |
| 7 | 子应用本地开发困难 | 未做独立运行入口 |
| 8 | 刷新 404 | history 模式 base 与 fallback 未配 |
| 9 | 首屏并发风暴 | prefetch: all 与首屏抢带宽 |
| 10 | mount 拿不到全局状态 | 通信时序问题 |
| 11 | 发版后用户仍老页面 | HTML Entry 被缓存 |
| 12 | 三方库沙箱报错 | 依赖 window 真实性 |
| 13 | 版本矩阵爆炸 | 子应用独立发版组合态失控 |
| 14 | 主题/国际化不生效 | 全局变量读不到或读快照 |
| 15 | 埋点/错误归属混乱 | 上报归属规则未定 |
代价与边界:什么时候不该上
- 只有一个团队/一个业务域 → 拆了收益为零
- 模块间耦合极深 → 改造前必须先解耦
- 应用体量不大(20 页面内)→ 升级构建工具+代码分割就够
- 团队没有自动化测试和监控 → 微前端把编译期错误变运行期错误,没兜底就是裸奔
金句:微前端是「组织架构的镜像」——解决多团队独立发布的问题。技术问题用技术解,组织问题才用架构解。
第二部分 · 自研组件库(qdma-ui)
为什么自研:先回答「这不是造轮子吗」
先说什么时候不该自研:只需要通用基础组件、团队 <5 人、没有明确业务组件诉求。
再说不自研解决不了的:
- 三库并存契约不统一(同一业务 3 种按钮、2 套弹窗)——换库只是换一家,多套在契约就永远不统一
- 体积失控(三方库大量组件用不上却打不进 tree-shaking)
- 业务定制被卡(地址级联、扫码上传三方库改不动)
一句话:自研的不是「基础组件」,是「业务组件契约 + 体积控制权」。
Design Token 三层(顺序不能反)
1. 原始值(palette):--qd-color-red-6: #e54d42
2. 语义层(semantic):--qd-color-primary: var(--qd-color-red-6) ← 业务只认这层
3. 组件级(component):--qd-button-height: 40px
业务只用语义层,换主题 = 覆盖语义层变量;组件内不写死任何颜色值(用 stylelint 卡住硬编码)。
按需引入与 tree-shaking(最经典坑)
「写了 ESM」不等于「能 tree-shake」。体积不降反升的根因:index.ts 里 export * 全量导出,构建工具因无法确定副作用不敢裁。
解法五件套:
package.json加"sideEffects": ["*.css"](明确只有样式有副作用)- 提供多入口(
@qdma/ui/es/button按路径引入) babel-plugin-import兜底老构建链路external与peerDependencies成对(Vue 必须 external,否则双 Vue 实例响应式失效)- CI 体积对比门禁
坑清单(12 条核心)
| # | 坑 | 根因 |
|---|---|---|
| 1 | 打包进了 Vue,双实例 | external 没配全(带路径引用没匹配上) |
| 2 | 体积没降反升 | 全量导出,tree-shaking 失效 |
| 3 | 样式顺序错乱 | CSS 产物顺序不可控 |
| 4 | TS 没类型提示 | 未产出 .d.ts |
| 5 | 按需引入配置复杂 | 没用 ESM 多入口 |
| 6 | 组件 API 一改下游全红 | 破坏性变更未走 major |
| 7 | Storybook 和产物不一致 | 文档跑源码、业务用产物 |
| 8 | 主题切换不彻底 | 组件内硬编码色值 |
| 9 | 覆盖率高仍有 bug | 只测函数没测交互路径 |
| 10 | 构建产物残缺 | 组件间循环依赖 |
| 11 | 业务装不上 | 版本冲突/peerDependencies 没声明 |
| 12 | 体积反弹 | 没依赖准入与体积门禁 |
长效防回退三件套
- 体积门禁:PR 对比产物体积,增长 <3% 通过、3~10% 警告、>10% fail
- 依赖准入:新增运行时依赖必须评审(有无现成能力/体积多大/能否按需引入)
- 性能预算:Lighthouse/体积阈值写进 CI,性能回归一票否决
金句:性能优化不是一次性战役,是要把它变成流水线的一道门禁。没有门禁的优化,三个月后一定回退。