微前端与 qiankun 学习笔记
一、微前端概述
微前端(Micro Frontends)是一种将前端应用拆分成多个独立、可部署的部分的架构模式,每个部分可以由不同的团队、技术独立开发、测试、部署和维护。这种架构类似于后端的微服务,是为了应对前端项目复杂难维护、技术栈多种多样、多团队协同冲突等问题提出的。
二、核心思想
微前端的核心思想是将大型单体前端应用拆分为多个独立、自治、可独立开发部署的小型子应用(微应用),再通过主应用(基座)统一整合,最终对用户呈现为一个完整、无感知的整体。
三、基本要素
- 技术栈无关:主应用不强制限制子应用技术栈,Vue、React、Angular、jQuery 等应用可以在同一基座中共存。
- 独立开发部署:子应用可以拥有独立仓库、独立构建流程和独立发布节奏。
- 运行时集成:主应用负责路由分发、资源加载、生命周期调度和公共能力注入。
- 应用隔离:子应用之间应尽量隔离样式、全局变量、事件监听、定时器和全局状态,避免互相污染。
- 通信可控:主应用与子应用之间通过 props、事件总线、状态管理或约定 API 通信,避免随意读写全局对象。
四、qiankun 概述
在中大型后台管理系统迭代中,老项目技术栈老旧、新业务快速迭代、多团队并行开发是常态,qiankun 作为蚂蚁开源微前端方案,凭借 JS/CSS 沙箱、多技术栈兼容、独立部署特性成为行业首选。
qiankun是一个基于single-spa封装的微前端框架,用于帮助开发者构建生产可用的微前端架构。它可以把多个独立开发、独立部署的前端应用聚合到同一个主应用中,并通过路由规则控制子应用的加载、挂载和卸载。
注意:qiankun 子应用需要支持独立运行。如果子应用依赖主应用传入的 props、全局状态、用户信息或鉴权信息,需要提供默认值、降级逻辑或 mock 数据,避免独立启动时报错。
五、qiankun 的核心设计理念
- 简单接入:qiankun 提供
registerMicroApps、start、loadMicroApp等 API,主应用只需要配置子应用名称、入口地址、挂载容器和激活规则,就可以完成基本接入。 - 技术栈无关:子应用可以使用 Vue、React、Angular、jQuery 等不同技术栈,只要按 qiankun 约定导出生命周期即可接入。
- 独立开发部署:子应用可以独立开发、独立构建、独立发布,主应用通过入口地址加载子应用资源。
- 运行时隔离:qiankun 通过 JS 沙箱、样式隔离和生命周期管理,降低子应用之间互相污染的风险。
六、qiankun 的特性
- 基于 single-spa:在 single-spa 的生命周期调度能力之上,提供更易用的微前端接入方式。
- HTML Entry:主应用可以通过子应用入口 HTML 自动解析和加载 JS、CSS 等资源,降低接入成本。
- JS 沙箱:隔离子应用对全局对象的修改,降低全局变量污染风险。
- 样式隔离:通过样式作用域或 Shadow DOM 等方式减少 CSS 冲突,具体能力与 qiankun 版本和配置有关。
- 资源预加载:可在浏览器空闲时预加载子应用资源,提高后续访问速度。
- 独立部署:子应用可以独立构建和发布,主应用通过入口地址加载最新子应用。
七、qiankun 快速上手
一个最小 qiankun 项目通常包含:
qiankun-demo/ ├── main-base/ # 主应用,也叫基座应用 ├── vue2-sub/ # Vue2 子应用 └── vue3-sub/ # Vue3 子应用基本流程:
- 主应用安装 qiankun。
- 主应用注册子应用入口、挂载容器和激活规则。
- 子应用开启跨域访问。
- 子应用导出 bootstrap、mount、unmount 生命周期。
- 子应用配置打包格式和资源 publicPath。
- 分别启动主应用和子应用,通过主应用路由访问子应用。
八、主应用配置(基座项目)
8.1 安装 qiankun 依赖
cdmain-basenpminstallqiankun@^2--save8.2 注册子应用(src/main.js)
import{createApp}from'vue'importAppfrom'./App.vue'import{registerMicroApps,start}from'qiankun'// 注册两个子应用registerMicroApps([{name:'vue2-sub',// 子应用唯一标识entry:'http://localhost:8081',// 本地开发地址container:'#micro-container',// 挂载DOM容器IDactiveRule:'/app-vue2',// 路由匹配规则,访问该路由加载子应用},{name:'vue3-sub',entry:'http://localhost:8082',container:'#micro-container',activeRule:'/app-vue3',}])// 启动qiankunstart()createApp(App).mount('#app')8.3 增加子应用挂载容器
主应用注册代码:
// src/main.jsimport{createApp}from'vue';importAppfrom'./App.vue';importrouterfrom'./router';import{registerMicroApps,start}from'qiankun';registerMicroApps([{name:'vue2-sub',entry:'http://localhost:8081',container:'#micro-container',activeRule:'/app-vue2',props:{token:'mock-token',from:'main-app'}},{name:'vue3-sub',entry:'http://localhost:8082',container:'#micro-container',activeRule:'/app-vue3',props:{token:'mock-token',from:'main-app'}}],{beforeLoad:app=>{console.log('[qiankun] before load:',app.name);returnPromise.resolve();},beforeMount:app=>{console.log('[qiankun] before mount:',app.name);returnPromise.resolve();},afterUnmount:app=>{console.log('[qiankun] after unmount:',app.name);returnPromise.resolve();}});start({prefetch:true,sandbox:{experimentalStyleIsolation:true}});constapp=createApp(App);app.use(router);app.mount('#app');App.vue挂载容器:
<template> <div class="main-wrap"> <!-- 主应用菜单跳转 --> <router-link to="/app-vue2">进入 Vue2 子应用</router-link> | <router-link to="/app-vue3">进入 Vue3 子应用</router-link> <!-- 子应用挂载节点 --> <div id="micro-container"></div> <router-view /> </div> </template>九、子应用改造
接入 qiankun 的子应用通常需要完成以下改造:
- 开启跨域访问,确保主应用可以加载子应用 HTML、JS、CSS 等资源。
- 导出
bootstrap、mount、unmount生命周期。 - 在独立运行时自行渲染,在 qiankun 环境中等待主应用调用
mount。 - 设置运行时 publicPath,避免异步 chunk、图片、字体等资源路径错误。
- 如果子应用使用 history 路由,需要让子应用路由 base 与主应用
activeRule保持一致。 - Webpack 子应用需要配置 UMD 输出格式。
- Vite 子应用需要使用对应的 qiankun 适配方案。
- 在
unmount中销毁应用实例、事件监听、定时器、订阅和全局副作用。
Vue2 Webpack 子应用接入代码
Vue2 子应用入口:
import'./public-path';importVuefrom'vue';importAppfrom'./App.vue';importrouterfrom'./router';Vue.config.productionTip=false;letinstance=null;functionrender(props={}){const{container}=props;constmountNode=container?container.querySelector('#app'):document.querySelector('#app');if(!mountNode){thrownewError('[vue2-sub] mount node #app not found');}instance=newVue({router,render:h=>h(App)}).$mount(mountNode);}if(!window.__POWERED_BY_QIANKUN__){render();}exportasyncfunctionbootstrap(){console.log('[vue2-sub] bootstrap');}exportasyncfunctionmount(props){console.log('[vue2-sub] mount',props);render(props);}exportasyncfunctionunmount(){console.log('[vue2-sub] unmount');if(instance){instance.$destroy();if(instance.$el){instance.$el.innerHTML='';}instance=null;}}Vue2 子应用public/index.html:
<divid="app"></div>Vue CLI / Webpack 子应用通常需要配置 UMD 输出。
constpackageName=require('./package.json').name;module.exports={devServer:{port:8081,headers:{'Access-Control-Allow-Origin':'*'}},configureWebpack:{output:{library:`${packageName}-[name]`,libraryTarget:'umd',chunkLoadingGlobal:`webpackJsonp_${packageName}`}}};注意:jsonpFunction多见于 Webpack 4,chunkLoadingGlobal多见于 Webpack 5。
子应用被主应用加载时,异步 chunk、图片、字体等资源路径容易错,所以要配置运行时 publicPath。
Vue CLI 子应用可以新增:
// src/public-path.jsif(window.__POWERED_BY_QIANKUN__){// eslint-disable-next-line no-undef__webpack_public_path__=window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;}然后在入口文件最顶部引入(注意必须放在其他业务 import 之前):
import'./public-path';路由 base:
// RouterimportVuefrom'vue';importVueRouterfrom'vue-router';Vue.use(VueRouter);constrouter=newVueRouter({mode:'history',base:window.__POWERED_BY_QIANKUN__?'/app-vue2':'/',routes:[{path:'/',component:()=>import('./views/Home.vue')}]});exportdefaultrouter;Vue3 子应用接入代码
Vue3 子应用入口:
import{createApp}from'vue';importAppfrom'./App.vue';importrouterfrom'./router';letapp=null;letmountNode=null;functionrender(props={}){const{container}=props;mountNode=container?container.querySelector('#app'):document.querySelector('#app');if(!mountNode){thrownewError('[vue3-sub] mount node #app not found');}app=createApp(App);app.use(router);app.mount(mountNode);}if(!window.__POWERED_BY_QIANKUN__){render();}exportasyncfunctionbootstrap(){console.log('[vue3-sub] bootstrap');}exportasyncfunctionmount(props){console.log('[vue3-sub] mount',props);render(props);}exportasyncfunctionunmount(){console.log('[vue3-sub] unmount');if(app){app.unmount();app=null;}if(mountNode){mountNode.innerHTML='';mountNode=null;}}如果项目使用 Vue CLI 4 / Webpack 4,使用jsonpFunction;如果项目使用 Webpack 5,使用chunkLoadingGlobal。
示例:
// vite.config.jsimport{defineConfig}from'vite';importvuefrom'@vitejs/plugin-vue';importqiankunfrom'vite-plugin-qiankun';exportdefaultdefineConfig({plugins:[vue(),qiankun('vue3-sub',{useDevMode:true})],server:{port:8082,cors:true}});入口文件示例:
import{createApp}from'vue';import{renderWithQiankun,qiankunWindow}from'vite-plugin-qiankun/dist/helper';importAppfrom'./App.vue';letapp=null;functionrender(props={}){const{container}=props;app=createApp(App);app.mount(container?container.querySelector('#app'):'#app');}renderWithQiankun({bootstrap(){console.log('[vite-sub] bootstrap');},mount(props){console.log('[vite-sub] mount',props);render(props);},unmount(){console.log('[vite-sub] unmount');app.unmount();app=null;}});if(!qiankunWindow.__POWERED_BY_QIANKUN__){render();}路由 base:
// Routerimport{createRouter,createWebHistory}from'vue-router';constrouter=createRouter({history:createWebHistory(window.__POWERED_BY_QIANKUN__?'/app-vue3':'/'),routes:[{path:'/',component:()=>import('./views/Home.vue')}]});exportdefaultrouter;十、常见问题
1.__POWERED_BY_QIANKUN__是什么?为什么有时是 undefined?
window.__POWERED_BY_QIANKUN__是 qiankun 加载子应用时注入的运行时标识,用来判断当前子应用是否运行在 qiankun 主应用中。
它为undefined通常有几种情况:
- 子应用是独立运行的,此时没有被 qiankun 加载,属于正常情况。
- 子应用入口代码执行时机不正确,判断发生在 qiankun 注入之前。
- TypeScript 项目没有声明该全局变量,导致类型报错。
- 子应用构建格式或生命周期导出错误,导致 qiankun 没有正确识别子应用。
- Vite 或 Webpack 项目没有按对应接入方式配置。
TypeScript 类型声明:
// src/types/qiankun.d.tsexport{};declareglobal{interfaceWindow{__POWERED_BY_QIANKUN__?:boolean;__INJECTED_PUBLIC_PATH_BY_QIANKUN__?:string;}}2. 子应用资源 404 怎么办?
常见原因是子应用被主应用加载后,异步 chunk、图片、字体等资源仍然按照子应用独立运行时的相对路径加载,导致路径错误。
解决方案:
- Webpack 子应用配置运行时 publicPath。
- Vite 子应用使用对应 qiankun 适配插件。
- 静态资源尽量使用绝对路径或 CDN 地址。
- 检查子应用部署路径是否和入口地址一致。
Webpack 解决代码:
if(window.__POWERED_BY_QIANKUN__){// eslint-disable-next-line no-undef__webpack_public_path__=window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;}3. 子应用样式污染怎么办?
样式污染通常来自全局选择器,例如body、html、.button、.container等。解决方案包括:
- 开启 qiankun 样式隔离配置。
- 子应用样式增加命名空间。
- 避免在子应用中直接修改
body、html的全局样式。 - UI 组件库主题样式要避免互相覆盖。
主应用配置:
start({sandbox:{experimentalStyleIsolation:true}});子应用样式命名空间示例:
.vue2-sub-app { .button { color: #f00; } .page-container { padding: 16px; } }子应用根节点:
<template> <div class="vue2-sub-app"> <router-view /> </div> </template>4. 子应用切走后仍然有定时器或事件怎么办?
原因通常是unmount中没有清理全局副作用。子应用被卸载时,除了销毁 Vue/React 实例,还要清理事件监听、定时器、订阅、全局状态等。
示例:
lettimer=null;functionhandleResize(){console.log('resize');}exportasyncfunctionmount(props){window.addEventListener('resize',handleResize);timer=setInterval(()=>{console.log('running');},1000);render(props);}exportasyncfunctionunmount(){window.removeEventListener('resize',handleResize);if(timer){clearInterval(timer);timer=null;}instance.$destroy();instance.$el.innerHTML='';instance=null;}十一、 高频面试题
1. 什么是微前端?解决什么业务痛点?
微前端是把大型单体应用拆分成多个独立子应用,由主应用统一调度;
解决:
- 多团队并行开发、技术栈异构(Vue2、Vue3、React通用);
- 项目体量过大打包慢、迭代耦合、牵一发动全身;
- 老旧项目存量改造,不用一次性全量重构。
2. 微前端常见实现方案有什么区别?
- iframe:隔离最强,天然隔离 JS、CSS 和运行环境;缺点是路由同步、通信、性能、弹窗层级、用户体验都比较麻烦。
- Web Components:基于浏览器原生组件模型,可结合 Shadow DOM 做样式隔离;缺点是工程生态、团队使用习惯和复杂应用集成成本需要评估。
- 运行时加载方案:例如 qiankun,主应用在运行时加载子应用资源,并通过生命周期、沙箱、样式隔离完成应用集成。
- 模块联邦方案:例如 Webpack Module Federation / EMP,更偏向构建和运行时模块共享,适合跨应用复用模块或组件。
3. qiankun 核心原理是什么?
- HTML Entry:主应用拉取子应用入口 HTML,解析其中的
script、link、style等资源,再由 qiankun 加载并执行子应用。 - 生命周期调度:qiankun 基于 single-spa 管理子应用的
bootstrap、mount、unmount、update。 - JS 沙箱:通过代理或快照机制隔离子应用对全局对象的修改,降低全局变量污染风险。
- 样式隔离:通过样式作用域、动态样式处理或 Shadow DOM 等方式减少 CSS 冲突。
- 应用通信:主应用可以通过 props 或全局状态机制向子应用传递数据和方法。
4. qiankun 四个生命周期各自作用
bootstrap:子应用初始化,只在首次加载时执行一次,适合做不会频繁变化的初始化工作。mount:子应用被激活时执行,用于创建应用实例并挂载 DOM。unmount:子应用失活时执行,用于销毁实例、解绑事件、清理定时器、取消订阅、清空 DOM。update:主应用手动触发子应用更新时执行,使用频率较低。