Skip to content

微前端与 qiankun:起源、选型与实战

· 11 min

微前端不是新瓶装旧酒,它就是大前端长到一定体量后“不得不拆”的产物。团队多、业务多、上游节奏乱,纯粹一个巨石仓库(Monolith)压榨工程效率,联调一改动就牵一发动全身,发布还要“等齐人”。这时候,微前端登场:把一个大站拆成多个能独立开发、独立部署的小应用,最后在壳里拼一起,既能“各自飞”,也能“成体系”。

起源与目的:为了解耦和可持续演进#


技术栈横向对比:谁上场,谁当替补#

路线思路优点坑点/代价适用场景
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 起步:5 分钟能跑#

先装好依赖(主应用):

Terminal window
pnpm add qiankun
# 或 npm i qiankun

1) 主应用:注册并启动#

src/micro.ts
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(示意):

src/index-qiankun.tsx
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 设置公共路径(很关键):

src/public-path.js
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)来简化生命周期包装。


进阶:把细节拉满#

路由模式与激活规则#

资源预取与性能#

start({
prefetch: (apps) => {
// 返回要预取的应用列表
return apps.filter(a => a.name !== 'heavy-app')
}
})

沙箱与样式隔离#

主子通信(全局状态 + 定向 props)#

export async function mount(props: any) {
props.onGlobalStateChange((state, prev) => {
console.log('[react-app] state:', state)
}, true)
props.setGlobalState({ theme: 'dark' })
}

动态载入(不走注册清单)#

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()

适合“弹窗/抽屉里塞一个子应用”的场景。

依赖共享与“减肥”#

部署与路径“八股”#

兜底与监控#


和其它方案怎么配合?#


常见坑位(踩过就不想再踩第二次)#


一个落地清单(照这个来,基本不翻车)#


小结#

微前端不是“把系统撕裂”,是“让系统长得更好看、更耐用”。qiankun 这条路的优点是:上手快、技术栈包容、可一点点把老系统接进来;代价就是你得认真处理沙箱、样式隔离和构建路径这些“小事”。值不值?看你的用户、你的团队、你的节奏。大多数时候,值。