前言
微前端方案中我们最终选择了 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
上,这样就能避免多实例之间的污染了。
结尾
如果文章有什么问题或者错误,欢迎指出,谢谢!