先说本地环境踩了一遍坑的总结:
原项目中是使用iframe
嵌入不同的微应用,iframe
虽然有挺多坑,但是用着其实还算顺手,萌发想换qiankun
是因为Chrome80后的版本会出现跨站不发送Cookies的问题,本想尝试qiankun
看看能不能解决这个问题的同时,也能在实际项目中测试实践微前端,但是Cookies这个问题其实qiankun
也存在。
还是先说说风险吧,以下为本地成功尝试完qiankun
后总结的一些存在的风险:(如有误,欢迎指正)
-
首先微应用挂载过一次后,qiankun会在内存中缓存相关的js和css资源,也就是说,卸载后再次挂载这个微应用的时候,不需要再去请求这些资源。这样的做法有利有弊:有利是再挂载进入微应用的速度大大提升,弊处是,如果微应用太重,会占用太多内存。(希望后面qiankun能提供
destroy
接口让使用者自行决定是销毁还是缓存,qiankun作者有提到一嘴715。) -
内存泄露:浏览器刷新页面,之前在内存中缓存的微应用资源没有全部清理干净,存在内存泄露。例如,这部分微应用js资源是5M,刷新一次,重新加载这5M,但是之前在内存中缓存的5M没有被清理干净。现在变成了5 * 2 = 10M。
复现可以参考我提的issues1073。这个问题我后来找到临时的解决方案:在主项目入口文件引入
zone.js
,可以暂时修复这个问题。 -
跨站不发送Cookies的问题。 Chrome80后的版本中默认屏蔽了第三方的 Cookie,即SameSite的默认值从
None
(无论是否跨站都会发送 Cookie)改为Lax
(允许部分第三方请求携带 Cookie),也就是说Chrome80后的版本主应用是无法发送Cookie到微应用的。但是这个问题有解决方案,具体的问题描述和解决方案,查看这两篇文章。
项目背景介绍
主应用和微应用都是基于vue
(如果你不是,可能下面内容对你参考价值不大)。微应用统一挂载在主应用的子路由/service
下,如微应用1为/service/app1
,微应用2为/service/app2
。主应用tab切换,微应用组件状态保持。
微应用接入指南:
1. 微应用的静态资源和接口必须允许主应用跨域,通过设置header允许跨域,例如:
Access-Control-Allow-Origin: *
以下为go的后端设置作为参考:
beego.InsertFilter("*", beego.BeforeStatic, cors.Allow(&cors.Options{
AllowOrigins: []string{"http://localhost:9931", "localhost:9931"},
AllowCredentials: true,
}))
beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{
AllowOrigins: []string{"http://localhost:9931", "localhost:9931"},
AllowCredentials: true,
}))
说明:这里需要理解一个点,为何微应用接口需要允许主应用跨域,即使微应用并未前后端分离?微应用的前端资源是qiankun 通过fetch去拿到的,但是拿到之后其实是在主应用跑的,所以例如你的主应用是localhost:9931,微应用是localhost:8080,微应用的前端调了一个后端接口,实际这个请求是从localhost:9931发出的,所以存在跨域问题,这也是为何微应用接口需要允许主应用跨域。
2. 微应用前端接入qiankun步骤:
1)导出相应的生命周期钩子:
微应用需要在自己的入口 js 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用,以vue为例:
if (window.__POWERED_BY_QIANKUN__) {
// 动态设置 webpack publicPath,防止静态资源加载出错
// eslint-disable-next-line
const path = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__.includes('page/jump/')
? window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__.split('page/jump/')[0]
: window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
// eslint-disable-next-line
__webpack_public_path__ = `${path}static/`; // 正确设置静态资源路径(以上根据自己的业务调整哈)
}
import Vue from 'vue';
import Router from 'vue-router';
import appRouter from './router.js';
import store from './store';
import App from './App';
Vue.use(Component);
Vue.use(Router);
let instance = null;
let router = null;
function render() {
router = new Router(appRouter);
router.beforeEach((to, from, next) => {
// 主应用内路由切换不触发微应用路由
if (window.__POWERED_BY_QIANKUN__ && !location.pathname.startsWith('/service/appname')) {
return;
}
if(!to.matched || to.matched.length === 0) {
next('/404');
return;
}
next();
});
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount('#appname-app');
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
}
export async function mount(props) {
render(props);
}
export async function unmount() {
instance.$destroy();
instance = null;
router = null;
}
2)配置微应用的打包工具,webpack如下:
module.exports = {
// ...
output: {
library: 'appname',
libraryTarget: 'umd',
jsonpFunction: 'webpackJsonp_appname',
},
}
3)因为项目中主应用将微应用挂载在子路由/service
下,所以微应用路由需要配置基路径,以vue为例:
const router = {
base: window.__POWERED_BY_QIANKUN__ ? '/service/appname/' : '/',
routes: [
...
]
};
3. 如果希望带登录信息的cookie到微应用后端,需要做以下两步:
1) Origin不能设置为*
,应该设置指定的地址并且要同时设置允许跨域携带cookie,如:
Access-Control-Allow-Origin: http://localhost:9931
Access-Control-Allow-Credentials: true
2) 前端调用接口设置带上cookie, 例如:
// 例一:axios
axios.create({
withCredentials: true
});
// 例二:原生fetch
fetch('https://example.com', {
credentials: 'include'
})
4. 经过以上步骤,已经实现了将微应用嵌入主应用中,但是如果想实现主应用tab切换,回到微应用的时候,微应用原组件状态保持,则需要:
1)在router-view加入
<keep-alive>
<router-view></router-view>
</keep-alive>
<!-- 可能不希望在微应用独立运行时缓存组件状态,可以这么写 -->
<keep-alive v-if="isQianKun">
<router-view></router-view>
</keep-alive>
<router-view v-else></router-view>
export default {
computed: {
isQianKun() {
return window.__POWERED_BY_QIANKUN__;
}
}
}
2)微应用内的路由切换不缓存组件状态,如果微应用所有组件你都按照规范,在组件内部定义了name
属性,你可以按【法一】即vue官方提供的include
属性去实现,但是如果微应用有很多路由,之前也没有给每个组件添加name
属性,不想要逐个去添加name
属性,可以使用【法二】即在进入组件前通过条件判断,去决定是否使用缓存的组件状态。
法一:基本思路就是进入子路由先缓存,等离开跳转到下个路由的时候再视情况决定是否将缓存移除。
<keep-alive :include="keepAlive">
<router-view></router-view>
</keep-alive>
import {componentList} from './router'; // 所有路由的组件名的数组
export default {
watch: {
$route(to, from) {
const isFromAppRoute = componentList.includes(from.name) && from.matched.length !== 0,
isToAppRoute = componentList.includes(to.name) && to.matched.length !== 0;
if (window.__POWERED_BY_QIANKUN__) { // 微应用内跳转或者非qiankun内嵌时,组件不缓存
if (!this.keepAlive.includes(to.name) && isToAppRoute) {
this.keepAlive.push(to.name);
}
if (!isFromAppRoute || !isToAppRoute) return;
//
const fromIndex = this.keepAlive.findIndex(i => i === from.name);
if (fromIndex === -1) return;
this.keepAlive.splice(fromIndex, 1);
}
}
},
data() {
return {
keepAlive: []
}
},
}
法二:基本思路和法一是一样的。
在入口main.js
注册路由守卫,在离开组件时调用。
import qiankunMixin from './qiankun-cache';
Vue.mixin(qiankunMixin);
// qiankun-cache
export default {
beforeRouteLeave: function(to, from, next) {
if (!window.__POWERED_BY_QIANKUN__) {
next();
return;
}
// 根据自己的业务更改此处的判断逻辑,酌情决定是否摧毁本层缓存。默认为只缓存qiankun微应用通过tab切换的组件
const isFromAppRoute = from.path.startsWith('/basepath/') && from.matched.length !== 0,
isToAppRoute = to.path.startsWith('/basepath/') && to.matched.length !== 0;
if (isFromAppRoute && isToAppRoute) {
if (this.$vnode && this.$vnode.data.keepAlive) {
if (this.$vnode.parent && this.$vnode.parent.componentInstance && this.$vnode.parent.componentInstance.cache) {
if (this.$vnode.componentOptions) {
const key = this.$vnode.key === null
? this.$vnode.componentOptions.Ctor.cid + (this.$vnode.componentOptions.tag ? `::${this.$vnode.componentOptions.tag}` : '')
: this.$vnode.key,
cache = this.$vnode.parent.componentInstance.cache,
keys = this.$vnode.parent.componentInstance.keys;
if (cache[key]) {
if (keys.length) {
const index = keys.indexOf(key);
if (index > -1) {
keys.splice(index, 1);
}
}
delete cache[key];
}
}
}
}
this.$destroy();
}
next();
}
};
微应用中前端踩过的坑和解决方案:
1. babel重复问题
问题描述: babel6主应用和微应用都使用垫片(babel-polyfill)出现only one instance of babel-polyfill is allowed
报错
解决方案:
步骤1)删除主应用和微应用的入口js中的import 'babel-polyfill';
步骤2)将polyfill
提取为js资源在index.html中作为js资源加载,主应用:<script src="/path/polyfill.min.js"></script>
,微应用:<script ignore src="/path/polyfill.min.js"></script>
*说明:微应用的ignore
为qiankun自定义的属性,表示子项目需要复用主项目该js依赖,有了该属性,qiankun 便不会再去加载这个 js,而子项目独立运行,这些 js仍能被加载。*
2. 发送给后端的请求浏览器自动发起OPTIONS请求
问题描述: 微应用调用接口属于跨域,而当发起跨域请求时,由于安全原因,触发一定条件时浏览器会在正式请求之前自动先发起OPTIONS请求,即CORS预检请求,服务器若接受该跨域请求,浏览器才继续发起正式请求。
原因:
1)关于 OPTIONS MDN提到:
规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。
2) 跨域请求时,OPTIONS请求触发条件,满足其中之一则会触发:
- 使用了下面任一HTTP 方法:PUT/DELETE/CONNECT/OPTIONS/TRACE/PATCH;
- 人为设置了以下集合之外首部字段:Accept/Accept-Language/Content-Language/Content-Type/DPR/Downlink/Save-Data/Viewport-Width/Width;
- Content-Type 的值不属于下列之一: application/x-www-form-urlencoded、multipart/form-data、text/plain
解决方案:
法一:查看【原因2的触发条件】进行调整,尽量避免触发OPTIONS请求;
法二:后端接口处理OPTIONS请求,同时使用Access-Control-Max-Age
缓存预检请求结果。
3. qiankun中避免使用通配符配置404路由
问题描述: 为了实现微应用组件状态keep-alive
,通过loadMicroApp
挂载微应用后,如果不是关闭页签则不卸载微应用,但是这样带来的问题就是,从微应用切到主应用的时候,如果微应用的路由采用通配符*
处理404,则微应用最后离开所在的页面其实是404页面。
解决方案: 将404组件路径注册为/404
,注册一个全局前置守卫beforeEach
,当未匹配到的时候跳转到/404
。
4. 微应用卸载后再挂载,微应用内路由跳转出现错误。
render
函数应该new
一个路由对象,确保每次重新挂载,路由对象被正确加载。
import appRouter from './router.js';
function render() {
// 解决方案
router = new Router(appRouter);
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount('#appName-app');
}
主应用中前端踩过的坑和解决方案:
1. IE11兼容问题
1)新增依赖core-js@3.6.5
,custom-event-polyfill@1.0.7
和whatwg-fetch@3.4.1
;
2)主应用入口文件引入如下:
// 以下为qiankun兼容ie11的一些插件
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';
2. vue路由修改为history
模式后,需要调整如下:
(1)如果 URL 匹配不到任何静态资源,则后端应该默认返回index.html
(2)webpack的publicPath
改为绝对路径。如,publicPath: '/static/'
。(否则可能出现加载部分chunk 404的问题);
3. 主应用请求微应用静态资源未携带cookie,通过自定义fetch
解决
const app = loadMicroApp(this.microList[appnname], {
fetch(url, data) {
const fetchConfig = {
...data,
credentials: 'include'
};
return window.fetch(url, fetchConfig);
}
});
【参考资料】