微前端--qiankun框架

9/27/2021 微前端

参考1 (opens new window)

qiankun 特性

  • 基于 single-spa 封装,提供了更加开箱即用的 API。
  • 技术栈无关,任意技术栈的应用均可使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  • HTML Entry 接入方式,让我们接入微应用像使用 iframe 一样简单。
  • 样式隔离,确保微应用之间样式互相不干扰。
  • JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  • 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
  • umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。

qiankun是基于基座模式的,所以它必然有一个基座应用(主应用),来管理各个子应用的加载和卸载。

# 微前端需要解决的问题

  • 应用的加载与切换( single-spa[路由问题、应用入口]、应用加载)

  • 应用的隔离与通信( js隔离、css样式隔离、应用间通信)

    • 路由问题
    • single-spa是监听hashChange和popState来检测路由变化的,劫持原生的pushState和replaceState事件
    • 应用入口
    • 应用入口必须暴露出bootstrap、mount、unmount三个生命周期钩子函数且返回Promise,以保证single-spa可以注册回调函数。
    • bootstrap--挂载前的准备, 在子应用的bootstrap钩子内手动创建这样一个节点并插入到基座应用
    • 采用快照策略实现js隔离

    • 在加载应用前,将window上的所有属性保存起来(即拍摄快照);等应用被卸载时,再恢复window上的所有属性。IE下的快照策略无法支持多实例模式。

      qiankun通过import-html-entry,可以对html入口进行解析,并获得执行脚本的方法execScripts。qiankun引入该接口后,首先为该应用生成一个window的代理对象,然后将代理对象作为参数传入接口,以保证应用内的js不会对全局window造成影响。

    • css样式隔离

    • 两种样式隔离方案,一种是基于shadowDom隔离,即为子应用的根节点创建一个shadow root。最终整个应用的所有DOM将形成一棵shadow tree。它内部所有节点的样式对树外面的节点无效。

      缺陷:弹出框直接挂载到document.body下,脱离了shadow tree,它的样式仍会对全局造成污染。

      sandbox: {
         strictStyleIsolation: true
         // experimentalStyleIsolation: true   // 实验性方案,scoped方式
      },
      
      1
      2
      3
      4
      • 另一种则是实验性的,思路类似于Vue中的scoped属性,给每个子应用的根节点添加一个特殊属性,用作对所有css选择器的约束。不支持@ keyframes,@ font-face,@ import,@ page(即不会被重写)
    • 应用间通信

    • 基于一个全局的globalState对象。这个对象由基座应用负责创建,内部包含一组用于通信的变量,以及两个分别用于修改变量值和监听变量变化的方法:setGlobalState和onGlobalStateChange。

      //  在基座应用中初始化
      import { initGlobalState, MicroAppStateActions } from 'qiankun';
      const initialState = {};
      const actions: MicroAppStateActions = initGlobalState(initialState);
      export default actions;
      
      // 加载子应用时通过props将actions传递到子应用内,
      actions.onGlobalStateChange(globalState, oldGlobalState){} // 监听全局状态变化
      actions.setGlobalState(...);   // 修改全局状态
      
      1
      2
      3
      4
      5
      6
      7
      8
      9

# qiankun实践

安装:$ yarn add qiankun # 或者 npm i qiankun -S

# 在主应用中注册微应用

//  微应用入口配置 micro-app.js
const microApps: Array<any> = [{
  name: 'module-app1',
  entry: 'https://app1.example.com',
  activeRule: '/app1',
  container: '#root-view',
  sandbox: { strictStyleIsolation: true }   // 开启样式隔离
}, {
  name: 'module-shop',
  entry: 'https://app2.example.com',
  activeRule: '/app2',
  container: '#root-view',
  sandbox: { strictStyleIsolation: true }   // 开启样式隔离
}, ...];
export default microApps;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。

如果微应用不是直接跟路由关联的时候,可手动加载微应用的方式:

import { loadMicroApp } from 'qiankun';
loadMicroApp({name:'app', entry:'//localhost:7100', container:'#yourContainer'});
1
2

现在希望在加载左侧菜单栏的时候去注册和启动qiankun

// menu.vue
<script>
import {registerMicroApps, start} from 'qiankun'; // 引入注册子应用和启动的接口函数
import microApps from './modules/micro-apps';   // 引入微应用入口配置
export default class PrimeMenu extends Vue {
  async created () {  			// 注册一些全局生命周期钩子,如进行日志打印,不需要可不传
    registerMicroApps(microApps, { beforeMount () {} });
    start({ prefetch: true });   // 启动qiankun,并开启预加载
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11

路由变化触发hashChange事件,而qiankun借助single-spa所绑定的监听事件,会侦测到地址变化而执行reroute函数;

随后qiankun通过import-html-entry提供的能力,去下载app1对应的html文件,并从中解析出所依赖的脚本和样式文件。

qiankun会将其封装在一个沙箱中,执行这些脚本并添加样式表,从而渲染出子应用。

window.history.pushState({}, '', '/app1/overview');
window.location.href = '/app1/overview';
1
2

# 微应用(以vue框架为例)

子应用内暴露出三个生命周期钩子函数:bootstrap、mount、unmount

let router = null;  let instance = null;
function render(props = {}) {
  const { container } = props;
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
    mode: 'history',
    routes,
  });
  instance = new Vue({router,store,render: (h) => h(App),})
            .$mount(container ? container.querySelector('#app') : '#app');
}

if (window.__POWERED_BY_QIANKUN__) {  // 在qiankun环境下,修正加载路径
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
} else { render(); }  // 独立运行时

export async function bootstrap() { console.log('[vue]vueapp bootstraped');}
export async function mount(props) {
  console.log('[vue]props from main', props);
  render(props);
}
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  router = null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 打包配置修改(vue.config.js)

const { name } = require('./package');
module.exports = {
  devServer: { headers: { 'Access-Control-Allow-Origin': '*' } },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};
1
2
3
4
5
6
7
8
9
10
11

umd格式的应用会向外暴露指定的生命周期钩子函数,便于single-spa解析。

jsonpFunction配置是webpack打包之后保存在window中的key,只需保证各个子应用不一致即可。