前言
微前端方案中我们最终选择了 qiankun ,上篇文章 qiankun 微前端方案实践及总结 也总结了 qiankun 很多的“坑点”和解决方法,这篇文章是上一篇的补充,本想直接在上一篇文章上编辑,没想到触发了掘金的文章内容最大长度限制:
所以只好重新整理下,纪录一些踩过的坑和调研,希望能对大家有所帮助。本文内容基于 qiankun 的 2.x 版本。
潜在的坑及解决方案
有一些问题是自己发现并解决的,有一些是在 qiankun 的仓库 issue 区看到的。对于这些问题,我尽可能的讲清楚产生原因及解决思路和方法。
子项目字体文件加载失败
qiankun 对于子项目的 js/css 的处理
之前讲到:qiankun 请求到子项目的 index.html 之后,会先用正则匹配到其中的 js/css 相关标签,然后替换掉,它需要自己加载 js/css 并运行,接着去掉 html/head/body 等标签,剩下的内容原样插入到子项目的容器中 :
对于 js ( <script> 标签)的处理
内联 js 的内容会直接记录到一个对象中,外链 js 则会使用 fetch 请到到内容(字符串),然后记录到这个对象中。
if (isInlineCode(script)) {
return getInlineCode(script);
} else {
return fetchScript(script);
}
const fetchScript = scriptUrl => scriptCache[scriptUrl] ||
(scriptCache[scriptUrl] = fetch(scriptUrl).then(response => response.text()));
运行子项目时,执行这些 js 即可:
//内联js
eval(`;(function(window){;${inlineScript}\n}).bind(window.proxy)(window.proxy);`)
//外链js
eval(`;(function(window){;${downloadedScriptText}\n}).bind(window.proxy)(window.proxy);`))
加载并运行外链 js 这里有一个难点就是,如何保证 js 的正确执行顺序?
<script> 标签的 async 和 defer 属性:
defer: 等价于将外链的js放在了页面底部async: 相对于页面的其余部分异步地执行,加载好了就执行。常用于Google Analytics
所以说外链 js 只要区分有无async,有async的 <script> 使用 promise 异步加载,加载完再执行即可,无async 属性的按顺序执行。
假设HTML中有如下几个 js 标签:
<script src="./a.js" onload="console.log('a load')"></script>
<script src="./b.js" onload="console.log('b load')"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
<script src="./c.js" onload="console.log('c load')"></script>
浏览器正常的加载及执行逻辑是并行加载,但是按顺序执行,但是只要第一个加载好了,就会立马执行第一个。如果第三个没加载完成,第四个即使加载完成,也不会先执行。
如图,我将网速限制到 5kb/s,第三个 js 还未加载完成,但是第四个js已经加载完成了,但不会执行。
qiankun 则是并行加载,但是等所有的 js 都加载完成了,再按顺序执行。与浏览器的原生加载执行顺序有一点点出入,但是效果一样。
这里有一点优化的空间:只要它前面的 js 都加载执行完了,那么它加载好了就可以立即执行,而不用等它后面的 js 加载完成。
对于 css ( <style> 和 <link> 标签)的处理
加载逻辑还是一样的:内联 css ( <style> 标签)的内容会直接记录到一个对象中,外链 css ( <link> 标签)则会使用 fetch 请到到内容(字符串),然后记录到这个对象中。
但是执行时,也和 js “类似的”:内容放到 <style> 标签,然后插入到页面,子项目卸载移除这些 <style> 标签。
这样会把外链的 css 变成内联 css ,好处就是切换子系统,不用重复请求,直接应用 css 样式,让子项目加载得更快。
但是会带来一个隐藏的坑,css 中如果使用了字体文件,并且是相对路径,原本是link外链样式,相对路径就是相对于这个外链 css 的路径,现在变成了内联样式,相对路径则变成了相对于 index.html 的路径,就会导致字体文件404。
更坑的是开发模式没有这个问题,开发模式下这个路径会被注入 publicPath,打包之后会有这个问题。
如何解决子项目字体文件加载失败的问题
虽然说,是由于 qiankun 将子项目的 <link> 改成 <style> 执行 ,导致了这个问题,但是它这么做似乎也没问题,并且是合理的。
根本原因在于字体文件虽然经过了 webpack 处理,但是没有被注入路径前缀。所以修改 webpack 的配置,让字体文件经过 url-loader 的处理,打包成 base64 ,就可以解决这个问题了。
子项目的 webpack 配置中加上如下内容即可:
module.exports = {
chainWebpack: (config) => {
config.module
.rule('fonts')
.test(/.(ttf|otf|eot|woff|woff2)$/)
.use('url-loader')
.loader('url-loader')
.options({})
.end()
},
}
查看源码发现 vue-cli4 也是这样处理的,但是它限制了4kb以内的字体打包成base64。
子项目部署在二级目录
首先,一个 vue 项目要想部署到二级目录,必须配置 publicPath,vue-cli3 官网描述:
然后需要注意的点就是,注册子项目时 入口地址 entry 的填写。
假设子项目部署在 app-vue-hash 目录下,entry 直接写 http://localhost/app-vue-hash 会导致 qiankun 取 publicPath 错误。子项目入口地址 http://localhost/app-vue-hash 的相对路径是 http://localhost,而我们希望的子项目相对路径是 http://localhost/app-vue-hash,这时我们只需要写成 http://localhost/app-vue-hash/ 即可,最后面的 / 不可省略。
qiankun 取 publicPath 源码:
function defaultGetPublicPath(url) {
try {
// URL 构造函数不支持使用 // 前缀的 url
const { origin, pathname } = new URL(url.startsWith('//') ? `${location.protocol}${url}` : url, location.href);
const paths = pathname.split('/');
// 移除最后一个元素
paths.pop();
return `${origin}${paths.join('/')}/`;
} catch (e) {
console.warn(e);
return '';
}
}
通过测试我们可以发现 http://localhost/app 和 http://localhost/app/ 两个不同路径的 server, 同一个 html,然后在 html 里引入一个相对路径的资源。浏览器解析的地址分别为:
说明 qiankun 取 publicPath 的处理是正确的。
IE11 的兼容性
qiankun 在加载子项目时,使用了 fetch,所以针对 IE11,需要引入 fetch 的 polyfill,然而引入了fetch-polyfill,在 IE11 下任然报错。
直观上是 single-spa.min.js 报错,但是这个代码是是压缩后的代码,不太好排查。
找到 node_modules\single-spa\package.json,找到 "module": "lib/esm/single-spa.min.js" 这一行,改成 "module": "lib/esm/single-spa.dev.js",然后重启项目。
发现 single-spa 报错原因是 APP 加载失败,代码如下:
function handleAppError(err, app, newStatus) {
var transformedErr = transformErr(err, app, newStatus);
if (errorHandlers.length) {
errorHandlers.forEach(function (handler) {
return handler(transformedErr);
});
} else {
setTimeout(function () {
throw transformedErr; // 这一行抛出的错误,也就是控制台显示的错误信息
});
}
}
打印了一下APP加载出错的信息,如下:
经排查是 fetch 的 polyfill 的问题,是它报的错。
然而只引入 fetch-polyfill,然后在项目中使用 fetch,不会报这个错。
同时引入 qiankun 和 fetch-polyfill,就会报这个错:
import 'fetch-polyfill';
import { registerMicroApps, start } from 'qiankun';
console.log(fetch);
console.log(fetch("http://localhost:1111"));
排查了好久这两个插件不兼容的原因。最终,qiankun 作者的解决方法是使用 whatwg-fetch,然后显示的列出 promise、symbol等的 polyfill。
在主项目入口文件引入以下内容即可(记得安装依赖):
import 'whatwg-fetch';
// import 'custom-event-polyfill'; // 如果还报错,需要引入这个
import 'core-js/stable/promise';
import 'core-js/stable/symbol';
import 'core-js/stable/string/starts-with';
import 'core-js/web/url';
试了下,IE11 可以完美运行,但是会偶现一些网络相关的报错,不影响页面运行,应该是 websocket 导致的,生产环境不会报错。
qiankun 作者的文章:2020 如何优雅的兼容 IE
vue子项目内存泄露问题
这个问题挺难发现的,是在 qiankun 的 issue 区看到的: github.com/umijs/qiank… ,排查过程我就不发了,解决方案挺简单。
子项目销毁时清空 dom 即可:
export async function unmount() {
instance.$destroy();
+ instance.$el.innerHTML = ""; //新增这一行代码
instance = null;
router = null;
}
但是其实,来回切换子项目并不会使内存不断增加。也就是说,即使卸载子项目时,子项目占用的内存没有被释放,但是下次加载时会复用这块内存,那这样的话,子项目会不会加载更快?(还未考证)
特殊需求探索与思考
在满足基本的微前端使用后,调研了一些优化和特殊需求,方便更好的使用。
keep-alive 需求
子项目 keep-alive 其实就是想在子项目切换时不卸载掉,仅仅是样式上的隐藏(display: none),这样下次打开就会更快。
keep-alive 需要谨慎使用,同时加载并运行多个子项目,这会增加 js/css 污染的风险。
虽然 qiankun 的 proxy 方式的 js 沙箱可以完美支持多项目运行,但是别忘了 IE11 这个毒瘤,IE11 下沙箱使用的是 diff 方法,这会让多个项目共用一个沙箱,这等于没有沙箱。路由之间也可能存在冲突。
多项目运行的 css 沙箱也没有特别好的处理方式,目前比较靠谱的是 class 命名空间 + css-scoped。
实现 keep-alive 需求有多种方式,推荐使用方案一。
方案一:借助 loadMicroApp 函数
尝试使用其已有 API 来实现 keep-alive 需求:借助 loadMicroApp 函数来实现手动加载和卸载子项目,一般有 keep-alive 需求的就是 tab 页,新增一个 tab 页时就加载这个子项目,关闭 tab 页时卸载这个子项目。
由于 demo 中没有 tab 页,我就直接加载所有子项目,然后看效果,改动并不是很多。
主项目 APP.vue 文件:
<template>
<div id="app">
<header>
<router-link to="/app-vue-hash/">app-vue-hash</router-link>
<router-link to="/app-vue-history/">app-vue-history</router-link>
<router-link to="/about">about</router-link>
</header>
<div id="appContainer1" v-show="$route.path.startsWith('/app-vue-hash/')"></div>
<div id="appContainer2" v-show="$route.path.startsWith('/app-vue-history/')"></div>
<router-view></router-view>
</div>
</template>
<script>
import { loadMicroApp } from 'qiankun';
const apps = [
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: '#appContainer1',
props: { data : { store, router } }
},
{
name: 'app-vue-history',
entry: 'http://localhost:2222',
container: '#appContainer2',
props: { data : store }
}
]
export default {
mounted() {
// 优先加载当前的子项目
const path = this.$route.path;
const currentAppIndex = apps.findIndex(item => path.includes(item.name));
if(currentAppIndex !== -1){
const currApp = apps.splice(currentAppIndex, 1)[0];
apps.unshift(currApp);
}
// loadMicroApp 返回值是 app 的生命周期函数数组
const loadApps = apps.map(item => loadMicroApp(item))
// 当 tab 页关闭时,调用 loadApps 中 app 的 unmount 函数即可
},
}
</script>
切换子项目,子项目的DOM没有被清空:
方案二:修改 qiankun 源码
实现起来也比较简单: 子系统卸载不清空容器里的 dom 也不卸载 vue 实例,用 display: none 来隐藏。子系统加载时先判断下容器有无内容,已存在就不重新插入子系统的HTML。
主要分4个步骤:
- 修改子项目的
render函数,不重复实例化vue
function render() {
if(!instance){
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/app-vue-history' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount('#appVueHistory');
}
}
- 修改子项目的
unmount生命周期,子项目unmount时不卸载vue实例
export async function unmount() {
// instance.$destroy();
// instance = null;
// router = null;
}
- 修改主项目中子项目的注册及容器,每个子项目单独放一个容器(当然你也可以放到一个容器,处理起来麻烦点)。然后就是切换子系统隐藏其他的
<div id="appContainer1" v-show="$route.path && $route.path.startsWith('/app-vue-hash')"></div>
<div id="appContainer2" v-show="$route.path && $route.path.startsWith('/app-vue-history')"></div>
registerMicroApps([
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: '#appContainer1',
activeRule: '/app-vue-hash',
props: { data : { store, router } }
},
{
name: 'app-vue-history',
entry: 'http://localhost:2222',
container: '#appContainer2',
activeRule: '/app-vue-history',
props: { data : store }
},
]);
- 修改
qiankun的源码:子项目加载前先判断有无内容,有内容则不处理;子系统卸载时不清空dom
这里刚好可以用到patch-package插件,直接修改qiankun/es/loader.js,改动如下:
修改 qiankun/es/sandbox/patchers/dynamicHeadAppend.js:
至此,就可以实现切换子项目不清空,下次进入秒加载的效果:
这个方案仅供学习参考,加深对 qiankun 的理解。
方案三:缓存子项目的 dom
方案来源:qiankun 仓库的 issue
这个方案比较麻烦,大致原理就是缓存 vue 实例的 dom ,子项目的入口文件修改:
let instance = null;
let router = null;
function render() {
// 这里必须要new一个新的路由实例,否则无法响应URL的变化。
router = new VueRouter({
mode: 'hash',
base: process.env.BASE_URL,
routes
});
if (window.__POWERED_BY_QIANKUN__ && window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__) {
const cachedInstance = window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__;
// 从最初的Vue实例上获得_vnode
const cachedNode =
// (cachedInstance.cachedInstance && cachedInstance.cachedInstance._vnode) ||
cachedInstance._vnode;
// 让当前路由在最初的Vue实例上可用
router.apps.push(...cachedInstance.catchRoute.apps);
instance = new Vue({
router,
store,
render: () => cachedNode
});
// 缓存最初的Vue实例
instance.cachedInstance = cachedInstance;
router.onReady(() => {
const { path } = router.currentRoute;
const { path: oldPath } = cachedInstance.$router.currentRoute;
// 当前路由和上一次卸载时不一致,则切换至新路由
if (path !== oldPath) {
cachedInstance.$router.push(path);
}
});
instance.$mount('#appVueHash');
} else {
console.log('正常实例化');
// 正常实例化
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount('#appVueHash');
}
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
console.log('[vue] props from main framework', props);
render();
}
export async function unmount() {
console.log('[vue] vue app unmount');
const cachedInstance = instance.cachedInstance || instance;
window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ = cachedInstance;
const cachedNode = cachedInstance._vnode;
if (!cachedNode.data.keepAlive) cachedNode.data.keepAlive = true;
cachedInstance.catchRoute = {
apps: [...instance.$router.apps]
}
instance.$destroy();
router = null;
instance.$router.apps = [];
}
复用公共依赖(方案)
子项目要想复用公共依赖,配置 webpack 的 externals 是必须的,而配置了这个之后,子项目独立运行时,这些依赖的来源有且仅有 index.html 中的外链 script 标签。
有两种情况:
- 子项目之间的依赖“复用”
这个很好办,你只需要保证依赖的 url 一致即可。比如说子项目A 使用了 vue,子项目B 也使用了同版本的 vue,如果两个项目使用了同一份 CND 文件,加载时会先从缓存读取:
const fetchScript = scriptUrl => scriptCache[scriptUrl] ||
(scriptCache[scriptUrl] = fetch(scriptUrl).then(response => response.text()));
- 子项目复用主项目的依赖
只需要给子项目 index.html 中公共依赖的 script 和 link 标签加上 ignore 属性(这是自定义的属性,非标准属性)。
有了这个属性,qiankun 便不会再去加载这个 js/css,而子项目独立运行,这些 js/css 仍能被加载,如此,便实现了“子项目复用主项目的依赖”。
<link ignore rel="stylesheet" href="//cnd.com/antd.css">
<script ignore src="//cnd.com/antd.js"></script>
需要注意的是:主项目使用externals 后,子项目可以复用它的依赖,但是不复用依赖的子项目会报错。
看了下 qiankun 官网,有写这个问题:Vue Router 报错 Uncaught TypeError: Cannot redefine property: $router
报错原因
子项目不配置 externals 时,项目中的 Vue是全局变量,但是 不属于 window ,所以子项目独立运行时这个 if 判断不生效:
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
而 qiankun 在运行这个子项目时,先找子项目的 window,再找父项目的 window,然后在window 上找到了 vue。if判断会生效,然后 window 上父项目的 Vue 安装了 VueRouter,子项目自己的全局的 vue没有安装,导致报错。
解决方案一:加载子项目之前处理下全局变量
假设 app-vue-hash 子项目复用主项目依赖,app-vue-history子项目不复用主项目依赖。
在主项目中注册子项目时新增如下代码解决:
registerMicroApps(apps,
{
beforeLoad(app){
if(app.name === 'app-vue-hash'){
// 如果直接在 app-vue-hash 子项目刷新页面,此时 window.Vue2 是 undefined
// 所以先判断下 window.Vue2 是否存在
if(window.Vue2){
window.Vue = window.Vue2;
window.Vue2 = undefined;
}
}else if(app.name === 'app-vue-history'){
window.Vue2 = window.Vue;
window.Vue = undefined
}
},
});
解决方案二:通过 props 传递依赖
上面的兼容性问题,可以考虑 主项目通过props把依赖传给子项目,不配置 externals 来解决。
主项目注册时,将依赖传递给子项目(省略了一些不必要的代码):
import VueRouter from 'vue-router'
registerMicroApps([
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: '#appContainer',
activeRule: '/app-vue-hash',
props: { data : { VueRouter } }
},
]);
子项目配置 externals 并且外链依赖加上ignore 属性:
function render(parent = {}) {
if(!instance){
// 当它独立运行时,使用自己的外链依赖 window.VueRoute
const VueRouter = parent.VueRouter || window.VueRouter;
Vue.use(VueRouter);
router = new VueRouter({
routes,
});
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount('#appVueHash');
}
}
export async function mount(props) {
render(props.data);
}
解决方案三:修改主项目和子项目的依赖名称
主应用和子应用复用的依赖改个名称,这样就不会影响其他不复用依赖的子应用。具体改的有:
- 修改子应用和主应用的
externals配置,修改依赖的名称,不使用Vue
externals: {
'vue': 'Vue2' , // 这个的意思是告诉 webpack 去把 winodw.Vue2 当做 vue 这个模块
}
- 在主应用导入外链
vue.js之后,将名称改成Vue2
<script src="https://unpkg.com/vue@2.5.16/dist/vue.runtime.min.js"></script>
<script>
window.Vue2 = winow.Vue;
window.Vue = undefined;
</script>
三个方案中,推荐方案三,更加简洁方便。
主项目路由的 hash 与 history 之争
之前的写一些内容不太全面,重新梳理了一下。分为三种情况讨论:
主项目路由用 history 模式
主项目使用 history 模式,则需要使用 location.pathname 来区分不同的子项目,这也是 qiankun 推荐的一种形式。注册子项目时, activeRule 只需要写路径即可:
registerMicroApps([
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: '#appContainer',
activeRule: '/app-vue-hash',
},
])
优点:
- 子项目可以使用
history模式,也可以使用hash模式 。这样旧项目就都可以直接接入,兼容性强。 - 对
hash模式子项目无影响,不需要做任何修改
缺点:
history模式路由需要设置base- 子项目之间的跳转需要使用父项目的
router对象(不用<a>链接直接跳转的原因是<a>链接会刷新页面)。
其实不传递 router 对象,用原生的 history 对象跳转也行: history.pushState(null, 'name', '/app-vue-hash/#/about'),同样不会刷新页面。
不管是父项目的 router 对象,还是原生的 history 对象,跳转都是 js 的方式。这里有一个小小的用户体验问题:标签(<router-link> 和 <a>)形式的跳转是支持浏览器默认的右键菜单的,js 方式则没有:
主项目路由用 hash 模式且子项目没有history 模式路由
也就是说主项目和所有子项目都是 hash 模式,这种情况下也有两种做法:
- 用
path来区分子项目
做法就不赘述了
优点:无需修改子项目内部代码
缺点:项目之间的跳转,都得靠原生的 history 对象
- 用
hash来区分子项目
这样做主项目和子项目会共同接管路由,举个栗子:
-
/#/vue/home: 会加载vue子项目的home页面,但是其实,单独访问这个子项目的home页面的完整路由就是/#/vue/home -
/#/react/about: 会加载react子项目的about页面,同样,单独访问这个子项目的about页面的完整路由就是/#/react/about -
/#/about: 会加载主项目的about页面
做法就是自定义 activeRule :
const getActiveRule = hash => location => location.hash.startsWith(hash);
registerMicroApps([
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: '#appContainer',
activeRule: getActiveRule('#/app-vue-hash'),
},
])
然后需要在子项目的所有路由前加上这个前缀,或者将子项目的根路由设置为这个前缀。
const routes = [
{
path: '/app-vue-hash',
name: 'Home',
component: Home,
children: [
// 其他的路由都写到这里
]
}
]
如果子项目是新项目还好,如果是旧项目,则影响还是比较大,子项目里面的路由跳转(<router-link>、router.push()、router.repace())如果使用的是 path ,则需要修改,得加上这个前缀,如果使用的是 name跳转,则无需改动:router.push({ name: 'user'})。
优点: 所有项目之间的跳转都可以直接使用自己的 router 对象或者 <router-link>,不需要借助父项目的路由对象或者原生的 history 对象
缺点: 对子项目是入侵式修改,如果是全新项目,则无影响。
主项目路由用 hash 模式且子项目有history 模式路由
主项目是hash 模式,子项目间的跳转就只能借助原生的 history 对象了,我们既可以用 path 也可以用 hash 来区分子项目:
- 用
path来区分子项目
与主项目是 history 没有太大的差异,优缺点也一样。
/vue-hash/#/home: 会加载vue子项目的home页面/vue-history/about: 会加载vue-history子项目的about页面/#/about: 会加载主项目的about页面
- 用
hash来区分子项目
这样做其实不太好,有点反常规,但是也可以用:
/home/#/vue: 会加载vue子项目的home页面/#/vue-hash/about: 会加载vue-hash子项目的about页面/#/about: 会加载主项目的about页面
优点:无
缺点: 对 hash 子项目是入侵式修改,如果是全新项目,则无影响。
总结
主项目路由的 hash 与 history 模式都可以使用,各有优劣,看情况取舍。
项目间的组件共享
组件共享,优先推荐 npm 包方式,但是如果没有部署私有 npm ,项目又涉及隐私不能放到 GitHub,或者是带数据的业务组件共享,可以考虑下面的几种方式。
父子项目间的组件共享
因为主项目会先加载,然后才会加载子项目,所以一般是子项目复用主项目的组件(主项目复用子项目的情况下面再讨论)。
做法也很简单,主项目加载时,将组件挂载到 window 上,子项目直接注册即可。
主项目入口文件:
import HelloWorld from '@/components/HelloWorld.vue'
window.commonComponent = { HelloWorld };
子项目直接使用:
components: {
HelloWorld: window.__POWERED_BY_QIANKUN__ ? window.commonComponent.HelloWorld :
import('@/components/HelloWorld.vue'))
}
子项目间的组件共享(弱依赖)
什么是弱依赖呢?就是子项目本身自己也有这个组件,当别的子项目已经加载过了,他就复用别人的组件,如果别的子项目未加载,就使用自己的这个组件。
适用场景就是避免组件的重复加载,这个组件可能并不是全局的,只是某个页面使用。做法分三步:
-
由于子项目之间的全局变量不共享,主项目提供一个全局变量,用来存放组件,通过
props传给需要共享组件的子项目。 -
子项目拿到这个变量挂载到
window上
export async function mount(props) {
window.commonComponent = props.data.commonComponent;
render(props.data);
}
- 子项目中的共享组件写成异步组件
components: {
HelloWorld: () => {
if(!window.commonComponent){
// 独立运行时
window.commonComponent = {};
}
const HelloWorld = window.commonComponent.HelloWorld;
return HelloWorld || (window.commonComponent.HelloWorld =
import('@/components/HelloWorld.vue'));
}
}
这里有个 bug :来回切换时,共享组件的样式不加载。感觉这个 bug 和“子项目跳转到主项目页面,主项目的样式不加载”是同一个 bug,暂时没有很好的解决方法。
另一个子项目也这样写就行,只要某个子项目加载过一次,另一个项目就可以直接复用,不用重复加载。
子项目间的组件共享(强依赖)
与弱依赖不同的是,子项目本身没有这个组件,所以另一个子项目一定先加载,然后他才能拿到这个组件。
那么如何保证子项目间的加载顺序呢?解决方案:在子项目使用这个组件前,手动加载另一个子项目,保证一定能拿到这组件(在有权限的情况下,无权限会加载失败)
qiankun 也可以使用 loadMicroApp 来手动加载子项目。基本步骤如下:
- 同样,由于子项目之间的全局变量不共享,主项目提供一个全局变量,用来存放组件,通过
props传给使用组件的子项目,同时还要将loadMicroApp函数传过去。
const commonComponent = {};
registerMicroApps(
[
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: '#appContainer',
activeRule: '/app-vue-hash',
props: { data : { loadMicroApp, commonComponent } }
},
],
)
- 子项目将加载函数和公共变量挂载到全局
export async function mount(props) {
window.commonComponent = props.data.commonComponent;
window.loadMicroApp = props.data.loadMicroApp;
render();
}
- 子项目在需要使用组件的页面,手动加载提供组件的子项目,等它加载完成,就可以拿到组件。
这里需要注意的是:由于提供组件的子项目是通过 loadMicroApp 加载的,所以存放组件的公共变量必须由 loadMicroApp 传递过去。另外:加载子项目需要提供容器,在 APP.vue 提供一个隐藏的容器就行。
const app = window.loadMicroApp({
name: 'app-vue-history',
entry: 'http://localhost:2222',
container: '#appContainer2',
props: { data: { commonComponent: window.commonComponent } }
})
await app.mountPromise;
由于组件强依赖,子项目这里就没法独立运行,这里注册组件有两个选择:
- 一是使用异步组件:
components: {
HelloWorld: async () => {
// 这个 app 最好写成当前页的全局变量
// 因为卸载当前页时需要调用他的 `unmount` 来卸载子项目
const app = window.loadMicroApp({
name: 'app-vue-history',
entry: 'http://localhost:2222',
container: '#appContainer2',
props: { data: { commonComponent: window.commonComponent } }
})
await app.mountPromise;
return window.commonComponent.HelloWorld
}
}
- 二是动态注册,先使用
v-if来隐藏标签:
<HelloWorld v-if="loadingEnd"/>
async created() {
const app = window.loadMicroApp({
name: 'app-vue-history',
entry: 'http://localhost:2222',
container: '#appContainer2',
props: { data: { commonComponent: window.commonComponent } }
})
await app.mountPromise;
Vue.component('HelloWorld', window.commonComponent.HelloWorld)
this.loadingEnd = true;
},
vue 文档有写如何处理异步组件的加载状态: 处理加载状态
- 另一个子项目共享组件,需要注意的是
loadMicroApp与路由无关,所以共享的组件必须在入口文件挂载到公共变量上,而不能在路由页挂载。
如果共享组件及其子组件不依赖 store 和 i18n 等全局的插件,这里有一个投机的处理:不实例化Vue,仅挂载组件。
export async function mount(props) {
// 如果有commonComponent变量,说明是另一个子项目通过 loadMicroApp 加载的
// 他此时只需要挂载组件
if(props.data.commonComponent){
props.data.commonComponent.HelloWorld = HelloWorld;
}else{
// 没有 commonComponent 变量,说明是主项目通过 registerMicroApps 加载的
// 当让这里只是一个简单的判断,也可以传递其他参数判断
render();
}
}
如果这个组件及其子组件依赖 store 和 i18n 等全局的插件,则需要返回一个函数,调用时,直接函数执行:
export async function mount(props) {
// 如果有commonComponent变量,说明是另一个子项目通过 loadMicroApp 加载的
// 他此时只需要挂载组件
if(props.data.commonComponent){
const createHelloWorld = container => new Vue({
el: container,
store,
i18n,
render: h => h(HelloWorld)
});
props.data.commonComponent.createHelloWorld = createHelloWorld;
}else{
// 没有 commonComponent 变量,说明是主项目通过 registerMicroApps 加载的
// 当让这里只是一个简单的判断,也可以传递其他参数判断
render();
}
}
父项目复用子项目的组件也适用于子项目间的组件共享这个情况。如果想实现主项目使用子项目的“小部件”(某些带数据图表),则需要使用强依赖方案
qiankun 的嵌套
首先,将 qiankun 项目按照子项目的要求做修改,然后才能被接入,基本改动如下:
- 修改打包配置,允许跨域及
umd - 修改根id,不使用
#app - 修改路由文件,在入口文件实例化路由
- 修改
public-path的文件 - 修改入口文件,将
qiankun需要的生命周期暴露出去
方案一:子项目自己运行一个 qiankun 实例
存在的问题:
- 子项目无法根据已有信息判断是独立运行还是被集成
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
由于子项目本身也是一个qiankun 项目,所以独立运行时 window.__POWERED_BY_QIANKUN__ 为 true,被集成时,还是 true。
解决办法:在主项目的入口文件另外定义一个全局变量window.__POWERED_BY_QIANKUN_PARENT__ = true;,用这个变量来区分是被集成还是独立运行
- 子项目入口文件的修改
主要有以下几点注意的地方:
- 切换子项目时,避免重复注册孙子项目,
- 由于子项目会被注入一个前缀,那么孙子项目的路由也要加上这个前缀
- 注意容器的冲突,子项目和孙子项目使用不同的容器
let router = null;
let instance = null;
let flag = false;
function render() {
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun' : '/',
mode: 'history',
routes,
});
const childRoute = ['/app-vue-hash','/app-vue-history'];
const isChildRoute = path => childRoute.some(item => path.startsWith(item))
const rawAppendChild = HTMLHeadElement.prototype.appendChild;
const rawAddEventListener = window.addEventListener;
router.beforeEach((to, from, next) => {
// 从子项目跳转到主项目
if(isChildRoute(from.path) && !isChildRoute(to.path)){
HTMLHeadElement.prototype.appendChild = rawAppendChild;
window.addEventListener = rawAddEventListener;
}
next();
});
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount('#appQiankun');
if(!flag){
registerMicroApps([
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: '#appContainer',
activeRule: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun/app-vue-hash' : '/app-vue-hash',
props: { data : { store, router } }
},
{
name: 'app-vue-history',
entry: 'http://localhost:2222',
container: '#appContainer',
activeRule: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun/app-vue-history' : '/app-vue-history',
props: { data : store }
},
]);
start();
flag = true
}
}
if (!window.__POWERED_BY_QIANKUN_PARENT__) {
render();
}
export async function bootstrap() {
console.log('vue app bootstraped');
}
export async function mount(props) {
render();
}
export async function unmount() {
instance.$destroy();
instance = null;
router = null;
}
history模式路由的孙子项目的base修改
base: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun/app-vue-history' :
(window.__POWERED_BY_QIANKUN__ ? '/app-vue-history' : '/'),
- 打包配置的修改
以上操作完成后,在主项目中可以把这个 qiankun 子项目加载出来,但是点击其孙子项目,报错,生命周期找不到。
修改一下孙子项目的打包配置:
- library: `${name}-[name]`,
+ library: `${name}`,
然后重启就可以了。
原因是 qiankun 取子项目的生命周期,优先取子项目运行时最后一个挂载到 window 上的变量,如果这个不是生命周期函数,再根据 appName 取。让 webpack 的 library 值对应 appName 即可。
方案二:主项目将 qiankun 的注册函数传递给子项目
基本步骤同上,但是这里有bug:孙子项目不加载。路由/app-qiankun加载qiankun子项目,孙子项目注册为主项目的子项目/app-qiankun/app-vue-hash,但是其qiankun子项目可以正常加载,孙子项目不加载也不报错,感觉这是qiankun的一个bug,两个项目共用了一部分路由前缀,路径长的一个不加载。
如果孙子项目不和qiankun子项目共用路由前缀,则可以正常加载,所以这个实用场景趋向于:将嵌套的子项目都注册为同级子项目,直接用主项目的容器,共用了主项目的注册函数,这些孙子项目本身就是主项目的子项目。
qiankun 子项目注册子项目时的代码如下:
if(!flag){
let registerMicroApps = parentData.registerMicroApps;
let start = parentData.start;
if(!window.__POWERED_BY_QIANKUN_PARENT__){
const model = await import('qiankun');
registerMicroApps = model.registerMicroApps;
start = model.start;
}
registerMicroApps([
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: window.__POWERED_BY_QIANKUN_PARENT__ ? '#appContainerParent' : '#appContainer',
activeRule: '/app-vue-hash',
props: { data : { store, router } }
},
{
name: 'app-vue-history',
entry: 'http://localhost:2222',
container: window.__POWERED_BY_QIANKUN_PARENT__ ? '#appContainerParent' : '#appContainer',
activeRule: '/app-vue-history',
props: { data : store }
},
]);
start();
flag = true;
}
qiankun 使用总结
- 出现最多的问题: 偶现刷新页面报错,容器找不到。
解决方案1:在组件 mounted 周期注册并启动 qiankun
解决方案2:new Vue() 之后,等 DOM 加载好了再注册并启动 qiankun
const vueApp = new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
vueApp.$nextTick(() => {
//在这里注册并启动 qiankun
})
- 我之前的担心:所有的
js脚本和css文件都在内存中缓存起来,子项目过多会不会导致浏览器卡死?
看到了 issue 区作者的回复:
复用了主项目的依赖之后,一个子项目的 js 和 css 体积在 2M - 5M 左右,所以基本上不用担心。
qiankun多应用同时运行js沙箱的处理
两个子应用同时存在, 又添加了两个全局变量 window.a, 如何保证这两个能同时运行但互不干扰?
采用了 proxy 代理之后,所有子应用的全局变量变更都是在闭包中产生的,不会真正回写到 window 上,这样就能避免多实例之间的污染了。
结尾
如果文章有什么问题或者错误,欢迎指出,谢谢!