基于vue的qiankun实践总结

4,654 阅读5分钟

先说本地环境踩了一遍坑的总结:

原项目中是使用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.7whatwg-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);
  }
});

【参考资料】

qiankun

什么时候会发送options请求

qiankun 微前端实践总结(二)