由于本次开发项目需要嵌入之前的老项目,由于考虑到iframe速度慢、css/js需要额外请求、阻塞页面加载、浏览器前进/后退等缺点,遂打算踩坑qiankun,为了更早的爬坑,整理此文。
简介 qiankun 是一个基于 single-spa 的微前端
实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
官方提供的资源:
根据 qiankun官方文档 介绍,主要有以下七大特性:
📦 基于 single-spa 封装,提供了更加开箱即用的 API。
📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
🛡 样式隔离,确保微应用之间样式互相不干扰。
🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
行业内其他前端团队对微前端的看法和实践:
API介绍 此处只介绍api的简单功能描述,如想继续了解请移步官方文档
registerMicroApps(apps, lifeCycles?) 注册微应用的基础配置信息。当浏览器 url 发生变化时,会自动检查每一个微应用注册的 activeRule 规则,符合规则的应用将会被自动激活。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { registerMicroApps } from 'qiankun' ;registerMicroApps ( [ { name : 'apass-micro' , entry : 'localhost:8080' , container : '#apassMicroTemplateConfig' , activeRule : '/index/config/template/edit' , props : { name : 'kuitos' , routerPushFunc : (that ) => { that.$router .push ('/713/5f4f65fabcb7c173/fields' ) }, data : { store : microAppStore.getGlobalState }, } } ], { beforeLoad : app => console .log ('before load' , app.name ), beforeMount : [ app => console .log ('before mount' , app.name ), ], afterMount : [ app => console .log ('after mount' , app.name ), ], beforeUnmoun : [ app => console .log ('before unmount' , app.name ), ], afterUnmount : [ app => console .log ('after unmount' , app.name ), ] }, );
start(opts?) 启动 qiankun
1 2 3 import { start } from 'qiankun' ;start ();
setDefaultMountApp(appLink) 设置主应用启动后默认进入的微应用。
1 2 3 import { setDefaultMountApp } from 'qiankun' ;setDefaultMountApp ('/homeApp' );
runAfterFirstMounted(effect) 第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本。
1 2 3 4 5 6 import { runAfterFirstMounted } from 'qiankun' ;runAfterFirstMounted (() => { console .log ('第一个子应用加载完后,该方法被调用' ) this .otherFunction () })
loadMicroApp(app, configuration?) 适用于需要手动 加载/卸载 一个微应用的场景。
通常这种场景下微应用是一个不带路由的可独立运行的业务组件。 微应用不宜拆分过细,建议按照业务域来做拆分。业务关联紧密的功能单元应该做成一个微应用,反之关联不紧密的可以考虑拆分成多个微应用。 一个判断业务关联是否紧密的标准:看这个微应用与其他微应用是否有频繁的通信需求。如果有可能说明这两个微应用本身就是服务于同一个业务场景,合并成一个微应用可能会更合适。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { loadMicroApp } from 'qiankun' ;this .microApp = loadMicroApp ( { name : 'sub-vue' , entry : 'http://localhost:7777/subapp/sub-vue' , container : '#apassMicroTemplateConfig' , props : { routerBase : '/index/config/template/edit' , getGlobalState : microAppStore.getGlobalState , sheetId : '2133123123' } }, { sandbox : { strictStyleIsolation : true }, singular : true } ) private unmountMicroApp () { if (this .microApp ) { this .microApp .mountPromise .then (() => { this .microApp .unmount () }) } }
prefetchApps(apps, importEntryOpts?) 手动预加载指定的微应用静态资源。仅手动加载
微应用场景需要,基于路由自动激活场景直接配置 prefetch 属性即可。
1 2 3 import { prefetchApps } from 'qiankun' ;prefetchApps ([ { name : 'app1' , entry : '//locahost:7001' }, { name : 'app2' , entry : '//locahost:7002' } ])
主应用配置 安装qiankun
调整main.js 如果你需要在项目初始化的时候就加载这些子应用,那么需要修改main.js的一些配置;如果是在页面中手动加载可略过此步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 import Vue from "vue" import App from "./App.vue" import router from "./router" import { registerMicroApps, setDefaultMountApp, start } from "qiankun" Vue .config .productionTip = false let app = null ;function render ({ appContent, loading } = {} ) { if (!app) { app = new Vue ({ el : "#container" , router, data ( ) { return { content : appContent, loading }; }, render (h ) { return h (App , { props : { content : this .content , loading : this .loading } }); } }); } else { app.content = appContent; app.loading = loading; } } function genActiveRule (routerPrefix ) { return location => location.pathname .startsWith (routerPrefix); } function initApp ( ) { render ({ appContent : '' , loading : true }); } initApp ();let msg = { data : { auth : false }, fns : [ { name : "_LOGIN" , _LOGIN (data ) { console .log (`父应用返回信息${data} ` ); } } ] }; registerMicroApps ( [ { name : "sub-app-1" , entry : "//localhost:8091" , render, activeRule : genActiveRule ("/app1" ), props : msg }, { name : "sub-app-2" , entry : "//localhost:8092" , render, activeRule : genActiveRule ("/app2" ), } ], { beforeLoad : [ app => { console .log ("before load" , app); } ], beforeMount : [ app => { console .log ("before mount" , app); } ], afterUnmount : [ app => { console .log ("after unload" , app); } ] } ); setDefaultMountApp ("/app1" );start ();
修改App.vue中的id 或 增加渲染子应用的盒子 因为一个主应用可能会嵌套多个子应用,所以App.vue难免会重名,所以最好加一个自己项目名称的前缀来做区分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template > <div id ="main-root" > <div v-if ="loading" > loading</div > <div id ="root-view" class ="app-view-box" v-html ="content" > </div > </div > </template > <script > export default { name : "App" , props : { loading : Boolean , content : String } }; </script >
配置vue子应用 因为子应用本身就是一个单独的应用,所以不必安装qiankun,只需要暴露被当做子应用嵌入时,qiankun所需的3个生命周期即可。
配置maim.js 在支持被当做子应用嵌入的同时,需要支持项目独立运行,兼容之前配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import Vue from 'vue' ;import VueRouter from 'vue-router' ;import App from './App.vue' ;import routes from './router' ;import './public-path' ;Vue .config .productionTip = false ;let router = null ;let instance = null ;function render ( ) { router = new VueRouter ({ base : window .__POWERED_BY_QIANKUN__ ? '/app1' : '/' , mode : 'history' , routes, }); instance = new Vue ({ router, render : h => h (App ), beforeMount () { if (window .__POWERED_BY_QIANKUN__ ) { routerPushFunc (this ) AppModule .SET_CURRENT_ENV () } } }).$mount(container ? container.querySelector ('#templateConfig' ) : '#templateConfig' ); } if (!window .__POWERED_BY_QIANKUN__ ) { render (); } export async function bootstrap ( ) { console .log ('vue app bootstraped' ); } export async function mount (props ) { console .log ('props from main app' , props); render (); } export async function unmount ( ) { (instance as Vue ).$destroy(); (instance as Vue ).$el .innerHTML = '' ; instance = null ; router = null ; }
public-path.js 使用 webpack 静态 publicPath 配置:可以通过两种方式设置,一种是直接在 mian.js 中引入 public-path.js 文件,一种是在开发环境直接修改 vue.config.js
1 2 3 4 if (window .__POWERED_BY_QIANKUN__ ) { __webpack_public_path__ = window .__INJECTED_PUBLIC_PATH_BY_QIANKUN__ }
配置 vue.config.js 子应用必须支持跨域:由于 qiankun 是通过 fetch 去获取子应用的引入的静态资源的,所以必须要求这些静态资源支持跨域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 const path = require ('path' );const { name } = require ('./package' );function resolve (dir ) { return path.join (__dirname, dir); } const pagesMicro = { templateConfig : { entry : 'src/microPage/templateConfig/main.ts' , template : 'src/microPage/templateConfig/index.html' , chunks : ['runtime~templateConfig' , 'chunk-vendors' , 'chunk-common' , 'templateConfig' ] }, } const pagesMain = { index : { entry : 'src/main.ts' , template : '/index.html' } } const pages = process.env .VUE_APP_ENTRY === 'main' ? pagesMain : pagesMicrolet config = { outputDir : 'dist' , assetsDir : 'static' , filenameHashing : true , devServer : { hot : true , disableHostCheck : true , port, overlay : { warnings : false , errors : true , }, headers : { 'Access-Control-Allow-Origin' : '*' , }, }, configureWebpack : { resolve : { alias : { '@' : resolve ('src' ), }, }, output : { library : `${name} -[name]` , libraryTarget : 'umd' , jsonpFunction : `webpackJsonp_${name} ` , }, }, }; if (process.env .VUE_APP_ENTRY === 'micro' ) { config.pages = pagesMicro } module .exports = config
qiankun常见问题及解决方案 避免 css 污染 qiankun 只能解决子项目之间的样式相互污染,不能解决子项目的样式污染主项目的样式,技术与规范方面大约有这 5 种方案:
vue自带的scope
只能解决一部分页面内的样式污染,但一般不会有这个问题
BEM命名方式
css-in-js
css-loader
开启css-modules,类似于图片懒加载,替换attr
缺点:页面中需要把class写成css-modules的形式;样式多了之后都是hash的形式可读性不高;
postcss-loader
利用postcss-modules插件的getJson()函数将所有css文件中的class转为json对象;利用postcss-html把json对象渲染回html页面的class
缺点:利用新的gulp,意义不大;每次修改都要编译,很慢;
拿css-loader举例,开启css-modules,可参考以下文章:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 module .exports = { css : { extract : false , sourceMap : false , loaderOptions : { css : { modules : { exportGlobals : true , localIdentName : '[path][name]__[local]--[hash:base64:5]' }, localsConvention : 'asIs' } }, requireModuleExtension : true }, }
谨慎使用 position:fixed 在子项目中这个定位会出现问题,基本出现在模态框和抽屉的定位上,应尽量避免使用,确有相对于浏览器窗口定位需求,可以用 position: sticky
,但是会有兼容性问题(IE不支持)。如果定位使用的是 bottom 和 right,则问题不大。 还有个办法,位置可以写成动态绑定 style 的形式:
1 <div :style ="{ top: isQiankun ? '10px' : '0'}" >
给 body 、 document 等绑定的事件,请在 unmount 周期清除 js 沙箱只劫持了 window.addEventListener,使用 document.body.addEventListener 或者 document.body.onClick 添加的事件并不会被沙箱移除,会对其他的页面产生影响,请在 unmount 周期清除
报错:Uncaught Error application ‘xxx’ died in status LOADING_SOURCE_CODE: [qiankun] You need to export lifecycle functions in xxx entry 一般就是打包姿势不对,可能原因:未打包成umd格式;所需的js文件虽然被整体打包了但没被加载,需要利用runtimeChunk单独打包出来
现刷新页面报错,容器找不到 解决方案1:在组件 mounted 周期注册并启动 qiankun
解决方案2:new Vue() 之后,等 DOM 加载好了再注册并启动 qiankun
1 2 3 4 5 6 7 8 const vueApp = new Vue ({ router, store, render : h => h (App ) }).$mount("#app" ); vueApp.$nextTick(() => { })
主、子应用的路由,均可用 history 模式 因为vue-router的history模式是全匹配的,所以如果当前子应用是被qiankun嵌入时,需要在子应用的一级路由前加上主应用除了http://ip+port/
后的所有路由,即在主应用中初始子应用是定义的activeRule
。
1 2 3 4 5 6 7 router = new VueRouter ({ base : window .__POWERED_BY_QIANKUN__ ? '/templateConfig' : '/' , mode : 'history' , routes : [ { ... } ] })
history模式下,主、子应用的路由配置问题 如果主、子应用的vue-router都是history模式(即路由全匹配)时
主应用中的route信息的path属性需要改为’index/edit*’的形式,即模糊全匹配,而且子应用的跟路由需要改为’index/edit/‘的形式(上面说过了)。否则子应用改变路由后,主应用匹配不到当前页面,则会跳回登录页会调至404。
子应用中的route信息里最好不要有’’或者’*’之类的判空。否则主应用(从嵌入子应用的那个页面)跳转到其他页面后,会触发子应用的路由匹配规则,进而跳转至子应用的登录页,而且导致主应用的路由跳转失败(也不能叫失败,实际上是跳转出去了又被redirect重定向回来了)。
从一个子项目跳转到另一个子项目 在子项目里面如何跳转到另一个子项目/主项目页面呢,直接写 或者用 router.push/router.replace 是不行的,原因是这个 router 是子项目的路由,所有的跳转都会基于子项目的 base 。写 链接可以跳转过去,但是会刷新页面,用户体验不好。
解决办法也比较简单,在子项目注册时将主项目的路由实例对象传过去,子项目挂载到全局,用父项目的这个 router 跳转就可以了。
但是有一丢丢不完美,这样只能通过 js 来跳转,跳转的链接无法使用浏览器自带的右键菜单
图片资源报错404 最好改为绝对路径
1 2 3 <img src ="./img/logo.jpg" > <img src ="/img/logo.jpg" >
或者在主应用中配置nginx静态文件的代理(这里没有后台的nginx配置,所以拿webpack自带的proxyTable代理作示例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (item === '/index/config/template/edit/static' ) { proxyObj[item] = { target : 'http://localhost:8081' , ws : false , changeOrigin : true , pathRewrite : { '^/index/config/template/edit/static' : '/static' } } } else if (item === '/static/home' ) { proxyObj[item] = { target : 'http://localhost:8081' , ws : false , changeOrigin : true , pathRewrite : { '^/static/home' : '/static/home' } } }
手动加载子应用时,如果子应用的js文件太大会造成阻塞 如果是手动加载子应用,即loadMicroApp(),推荐在页面初始化的时候就预加载资源,即prefetchApps()。避免请求的pending时间太长阻塞加载
ts项目与js项目文件加载的问题 因为主项目是ts,默认加载的是ts文件;但子项目是js。所以在子项目中引入js文件的时候要标清楚后缀名,例如
1 2 3 4 5 // 会报错 Unknown custom element: <widget> - did you register the component correctly? For recursive components, make sure to provide the "name" option. import {widgetInRecord as widget} from '@/views/sheetConfig/fieldConfig/widget/widget' // 加上后缀名就不报错了 import {widgetInRecord as widget} from '@/views/sheetConfig/fieldConfig/widget/widget.js'
在一个页面内以不同的初始化数据加载同一子应用(如:左侧是列表,右侧的详情是qiankun嵌入的子应用) 重复加载问题、数据通信问题、请求响应问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ``` ## 主项目与子项目的数据通信 项目之间的不要有太多的数据依赖,毕竟项目还是要独立运行的。通信操作需要判断是否 qiankun 模式,做兼容处理。 通过 props 传递父项目的 Vuex ,如果子项目是 vue 技术栈,则会很好用。假如子项目是 jQuery/react/angular ,就不能很好的监听到数据的变化。 qiakun 提供了一个全局的 GlobalState 来共享数据。主项目初始化之后,子项目可以监听到这个数据的变化,也能提交这个数据。 ```js // 主项目初始化 import { initGlobalState } from 'qiankun'; const actions = initGlobalState(state); // 主项目项目监听和修改 actions.onGlobalStateChange((state, prev) => { // state: 变更后的状态; prev 变更前的状态 console.log(state, prev); }); actions.setGlobalState(state); // 子项目监听和修改 export function mount(props) { props.onGlobalStateChange((state, prev) => { // state: 变更后的状态; prev 变更前的状态 console.log(state, prev); }); props.setGlobalState(state); }
vue子项目内存泄露问题 这个问题挺难发现的,是在 qiankun 的 issue 区看到的: github.com/umijs/qiank… ,排查过程我就不发了,解决方案挺简单。
子项目销毁时清空 dom 即可:
1 2 3 4 5 6 export async function unmount ( ) { instance.$destroy(); + instance.$el .innerHTML = "" ; instance = null ; router = null ; }
但是其实,来回切换子项目并不会使内存不断增加。也就是说,即使卸载子项目时,子项目占用的内存没有被释放,但是下次加载时会复用这块内存,那这样的话,子项目会不会加载更快?(还未考证)
安全和性能的问题 qiankun 将每个子项目的 js/css 文件内容都记录在一个全局变量中,如果子项目过多,或者文件体积很大,可能会导致内存占用过多,导致页面卡顿。
另外,qiankun 运行子项目的 js,并不是通过 script 标签插入的,而是通过 eval 函数实现的,eval 函数的安全和性能是有一些争议的:MDN的eval介绍
祝君无Bug~