微前端不是新瓶装旧酒,它就是大前端长到一定体量后“不得不拆”的产物。团队多、业务多、上游节奏乱,纯粹一个巨石仓库(Monolith)压榨工程效率,联调一改动就牵一发动全身,发布还要“等齐人”。这时候,微前端登场:把一个大站拆成多个能独立开发、独立部署的小应用,最后在壳里拼一起,既能“各自飞”,也能“成体系”。
起源与目的:为了解耦和可持续演进#
- 痛点三件套
- 团队协作:多人多线,代码冲突、依赖地狱、版本“卡壳”。
- 发布节奏:任一功能卡住,全站跟着等;线上回滚“一刀切”。
- 技术异构:老项目 Vue2,新项目 React/Vue3,硬统一要么伤筋动骨,要么寸步难行。
- 目标四个字:拆、合、稳、快
- 拆:子应用各自开发部署。
- 合:用户面前仍是一个完整网站(统一路由、统一导航、统一登录态)。
- 稳:出事只影响局部。
- 快:局部改动局部发,全链路更顺。
技术栈横向对比:谁上场,谁当替补#
路线 | 思路 | 优点 | 坑点/代价 | 适用场景 |
---|---|---|---|---|
iframe | 真隔离(DOM/CSS/JS) | 强隔离、简单粗暴 | 体验割裂、通信麻烦、路由/样式难统一 | 极端隔离诉求、异域系统嵌入 |
single-spa 家族 | (qiankun、micro-app、wujie、garfish)运行时装配,主应用调度子应用 | 易落地、技术栈可混搭、路由共享 | 需要做沙箱与样式隔离、资源路径/跨域细节多 | 大多数中大型前端站点 |
Web Components | 标准化自定义元素 | 技术中立、可组合 | 状态管理/构建/共享依赖还得自己补 | 组件级复用,非整应用编排 |
Module Federation(Webpack5) | 模块级运行时共享 | 真·复用依赖,减少重复打包 | 架构设计要稳,版本/兼容要控 | 单组织、统一构建链路 |
Import Maps + 原生 ESM | 浏览器原生加载映射 | 轻依赖、可控 | 老浏览器要补丁,生态配合度要求高 | 现代浏览器占比高的场景 |
一句话总结:如果你想“有组织地拼应用”,优先考虑 single-spa 系方案;想“在构建层把包揉开揉合”,看 Module Federation;隔离到“玻璃房”的,iframe 也不是不能用,但要忍痛割爱一部分体验。
选型建议(落地向)#
- 你的团队多、技术异构、需要快速把存量系统拼一起:优先 qiankun(single-spa 增强版)。
- 单组织、统一 Webpack,强调依赖共享:可以上 Module Federation,和微前端并不冲突,甚至能一起用。
- 极端隔离/安全:iframe,配合 PostMessage 通信,做好 Loading 与导航统一。
用 qiankun 起步:5 分钟能跑#
先装好依赖(主应用):
pnpm add qiankun# 或 npm i qiankun
1) 主应用:注册并启动#
import { registerMicroApps, start, initGlobalState, addGlobalUncaughtErrorHandler,} from 'qiankun'
registerMicroApps( [ { name: 'react-app', entry: 'http://localhost:7101', // 子应用入口(html) container: '#subapp-viewport', // 子应用要挂载的 DOM activeRule: '/react', // 命中这个路由就激活 props: { from: 'main' }, // 传参(登录态、埋点上下文等) }, { name: 'vue-app', entry: 'http://localhost:7102', container: '#subapp-viewport', activeRule: '/vue', }, ], { beforeLoad: [app => console.log('before load', app.name)], beforeMount: [app => console.log('before mount', app.name)], afterUnmount: [app => console.log('after unmount', app.name)], },)
const actions = initGlobalState({ user: null, theme: 'light' })actions.onGlobalStateChange((state, prev) => { console.log('[main] state change:', state, 'prev:', prev)})// actions.setGlobalState({ user: { id: 1, name: 'breeze' } })
addGlobalUncaughtErrorHandler((e) => { console.error('微应用异常', e)})
start({ prefetch: 'all', // 资源预取:all/true/false/函数 singular: false, // 是否同一时刻只挂一个应用 sandbox: { strictStyleIsolation: true, // Shadow DOM 强样式隔离 experimentalStyleIsolation: false, // 实验性样式隔离(属性选择器) },})
主应用 HTML 放个容器就行:
<div id="subapp-viewport"></div>
2) 子应用:导出生命周期(React/Vue 示例)#
React(示意):
import React from 'react'import ReactDOM from 'react-dom/client'import App from './App'
let root: ReactDOM.Root | null = null
function render(props: any = {}) { const { container } = props const el = container ? container.querySelector('#root') : document.getElementById('root') root = ReactDOM.createRoot(el!) root.render(<App />)}
export async function bootstrap() { // 初始化,只执行一次}export async function mount(props: any) { render(props)}export async function unmount() { root?.unmount() root = null}
Webpack 设置公共路径(很关键):
if (window.__POWERED_BY_QIANKUN__) { // @ts-ignore // eslint-disable-next-line no-undef __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__}
入口文件第一行引入它:
import './public-path'
React Router 建议加 basename
:
const basename = (window as any).__POWERED_BY_QIANKUN__ ? '/react' : '/'createBrowserRouter(routes, { basename })
Vue 2/3 类似:在 mount
时 new Vue/应用实例,并在 unmount
里销毁,路由 base
指向子路径。
Vite 项目可以配合社区插件(如 vite-plugin-qiankun
)来简化生命周期包装。
进阶:把细节拉满#
路由模式与激活规则#
activeRule
支持字符串或函数。字符串最常见:/react
、/vue
。- 主应用多用浏览器历史路由(history),子应用也尽量一致;服务端记得做子路径回退(避免刷新 404)。
资源预取与性能#
prefetch: 'all'
上来就把注册过的子应用资源全预热,切页不白屏;对首屏要求高、子应用不多的项目很香。- 想省带宽,就传函数,基于路由/条件预取:
start({ prefetch: (apps) => { // 返回要预取的应用列表 return apps.filter(a => a.name !== 'heavy-app') }})
沙箱与样式隔离#
strictStyleIsolation: true
:用 Shadow DOM,隔离强,少量第三方库可能不适配,需要测。experimentalStyleIsolation: true
:给样式加 data- 前缀选择器,隔离一般,但兼容性好。- 通用建议:基础样式(reset/vars)收敛到主应用;子应用组件尽量“内聚”样式,少用全局选择器。
主子通信(全局状态 + 定向 props)#
- 全局:
initGlobalState
一次创建,主子都能订阅/更新- 主应用:
const actions = initGlobalState({ ... })
- 子应用(来自 props):
- 主应用:
export async function mount(props: any) { props.onGlobalStateChange((state, prev) => { console.log('[react-app] state:', state) }, true) props.setGlobalState({ theme: 'dark' })}
- 定向:在注册/挂载时通过
props
传方法或数据,点对点效率高。
动态载入(不走注册清单)#
import { loadMicroApp } from 'qiankun'
const app = loadMicroApp({ name: 'report', entry: 'https://cdn.example.com/report/', container: '#slot', props: { token: 'xxx' },}, { sandbox: { experimentalStyleIsolation: true },})
// 需要时再卸载await app.unmount()
适合“弹窗/抽屉里塞一个子应用”的场景。
依赖共享与“减肥”#
- 大体积公共库(如 React、antd)可以外链到同一 CDN,主子都
externals
,避免重复打包。 - 也可以在构建层走 Module Federation 做运行时共享(更细粒度,但复杂度更高)。
部署与路径“八股”#
- 子应用打包
publicPath
不能写死根路径;Webpack 方案用__webpack_public_path__
动态注入最稳。 - 跨域要配
Access-Control-Allow-Origin
,开发期devServer.headers
放开即可。 - 刷新 404:服务端把子路径路由都回退到子应用入口(静态托管里做 rewrite)。
兜底与监控#
addGlobalUncaughtErrorHandler
能接住子应用脚本出错,打日志别省。- 业务层留一套“无法加载子应用的 Plan B”(比如文案和刷新按钮),别让用户干等。
和其它方案怎么配合?#
- 跟 Module Federation 不冲突:qiankun 负责“编排应用”,MF 负责“共享模块”,一个“搭积木”、一个“共用零件”,搭配使用更香。
- 跟 iframe:极端情况下某些系统不可控(跨域、老技术栈),可以 iframe 兜底;导航和面包屑样式可在壳层修饰,体验不至于太出戏。
常见坑位(踩过就不想再踩第二次)#
- 刷新 404/资源 404:十有八九是
publicPath
或服务端 rewrite 没配好。 - 样式串色:全局选择器“祸从口出”,或者子应用引了 reset。隔离+收敛两手都要硬。
- 双份依赖:主子都把 React 打进包,体积起飞。外链或共享,二选一。
- WebView:部分 X5/定制内核对 Shadow DOM 有历史包袱,遇到兼容问题就把
strictStyleIsolation
换成experimentalStyleIsolation
,必要时降级。
一个落地清单(照这个来,基本不翻车)#
- 定边界:哪些页面/模块做成子应用,路由前缀怎么分。
- 定规范:公共 UI、登录态、埋点、主题风格谁说了算,放主应用。
- 起脚手架:主应用先跑通两个子应用(React/Vue 各一个),打通路由/通信/部署。
- 做瘦身:公共依赖抽出去,不要“各自为政各自打”。
- 做预取:首屏轻,常用子应用预热,切页不白屏。
- 做监控:错误/加载时长/白屏率上报,真数据说话。
小结#
微前端不是“把系统撕裂”,是“让系统长得更好看、更耐用”。qiankun 这条路的优点是:上手快、技术栈包容、可一点点把老系统接进来;代价就是你得认真处理沙箱、样式隔离和构建路径这些“小事”。值不值?看你的用户、你的团队、你的节奏。大多数时候,值。