qiankun 微前端实践总结(二)
2020/7/22 11:03:54
本文主要是介绍qiankun 微前端实践总结(二),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
前言
微前端方案中我们最终选择了 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
处理,但是没有被注入路径前缀,所以是 loader
的问题,修改 webpack
的配置,让字体文件也经过 url-loader
的处理就可以解决这个问题了。
子项目的 webpack
配置中加上如下内容即可:
module.exports = { chainWebpack: (config) => { config.module .rule('fonts') .test(/.(ttf|otf|eot|woff|woff2)$/) .use('url-loader') .loader('url-loader') .tap(options => ({ name: '/fonts/[name].[hash:8].[ext]' })) .end() }, } 复制代码
查看源码发现 vue-cli4
是有这个处理的,但是打包之后的确没有生效,就很奇怪。
子项目部署在二级目录
首先,一个 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 '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
的理解。
复用公共依赖(方案)
子项目要想复用公共依赖,配置 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
模式都可以使用,各有优劣,看情况取舍。
子项目间的组件共享
这个需求不常见,仅学习研究用,做法也很简单:
由于子项目之间的全局变量不共享,主项目提供一个全局变量,用来存放组件,通过
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
,暂时没有很好的解决方法。
另一个子项目也这样写就行,只要某个子项目加载过一次,另一个项目就可以直接复用,不用重复加载。
公共组件还是放私有 npm
吧,既方便维护,又方便复用。
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
上,这样就能避免多实例之间的污染了。
结尾
如果文章有什么问题或者错误,欢迎指出,谢谢!
本文使用 mdnice 排版
这篇关于qiankun 微前端实践总结(二)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-24Vite多环境配置学习:新手入门教程
- 2024-11-23实现OSS直传,前端怎么实现?-icode9专业技术文章分享
- 2024-11-22在 HTML 中怎么实现当鼠标光标悬停在按钮上时显示提示文案?-icode9专业技术文章分享
- 2024-11-22html 自带属性有哪些?-icode9专业技术文章分享
- 2024-11-21Sass教程:新手入门及初级技巧
- 2024-11-21Sass学习:初学者必备的简单教程
- 2024-11-21Elmentplus入门:新手必看指南
- 2024-11-21Sass入门:初学者的简单教程
- 2024-11-21前端页面设计教程:新手入门指南
- 2024-11-21Elmentplus教程:初学者必备指南