微前端框架qiankun体验

2,901 阅读6分钟

前言

本文主要是对于qiankun微前端框架的简单体验,并且对于其微应用挂载、应用通信、应用隔离等方面的技术实现进行了了解,方便日后对巨石应用进行微前端改造,代码demo在这里 qiankun_experience

实践过程

创建主应用

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

const AppsInfo = [
  {
    name: 'react-app', // app name registered
    entry: '//localhost:3000',
    container: '#app-qiankun',
    activeRule: '/react',
  },
  {
    name: 'vue-app', // app name registered
    entry: '//localhost:3001',
    container: '#app-qiankun',
    activeRule: '/vue',
  }
]

创建微应用

微应用主要是需要暴露出三个生命周期函数,bootstrap用于标识初始化应用,然后mountunmount用于挂载和卸载微应用。然后按照官网修改打包方式为umd,并更改打包

同时为了dev环境下无障碍还需要解决跨域问题,如果是基于webpack打包的话,只需要在DevServer里增加允许跨域的header即可。 然后为了路由兼容,还需要在微应用的router控制中,增加一个baseURL(对应activeRule)来实现匹配到activeRule之后,路由由子应用进行接管

# header配置
devServer: {
    headers: {
    "Access-Control-Allow-Origin": "*",
  },
}

# router控制
## react-router-dom
<BrowserRouter basename={baseURL} >
</BrowserRouter>
## vueRouter
new VueRouter({
  base: baseURL,
  routes,
  mode: 'history',
})

这里还要注意一个细节,如果报这样的错误 Application died in status NOT_MOUNTED: Target container with #container not existed after xxx mounted! 说明主应用和子应用如果挂载时,render函数选择的挂载节点是相同的,官方给出的解决方案是修改id查询范围,

技术实现

应用的隔离和切换

这里对应到上边说的主子应用路由配置的问题,当匹配到子应用的路由时,加载子应用的内容,然后将路由控制交给子应用(activeWhen如果匹配成功再卸载当前应用,挂载新应用) qiankun在这里直接使用了社区技术方案 Single-SPA解决这个问题,这里我们去看一下single-SPA的关于这部分的重点代码: 主要有两个部分:

  1. 通过劫持路由事件,来调用reroute方法
  2. 在reroute方法中对APP进行调度,判断根据路由判断shouldActive,然后进行load和mount,并对部分app进行unmounted
# 1. 劫持路由变化事件 如hashchange pushstate等,监听后执行reroute
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
 .......
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
 .......
# 2、 reroute函数,主要调度函数,start()后执行的函数
function reroute() {
    ... 
    if (isStarted()) { // 如果主应用开启
      appChangeUnderway = true;
      appsThatChanged = appsToUnload.concat(appsToLoad, appsToUnmount, appsToMount);
      return performAppChanges(); // 展示应用
    } else {
      appsThatChanged = appsToLoad;
      return loadApps(); // 加载应用
    }
    function loadApps() { // 应用加载
      ......
    }
    function performAppChanges() {
      return Promise.resolve().then(function () {
       ......
        var unloadPromises = appsToUnload.map(toUnloadPromise);
        var unmountUnloadPromises = appsToUnmount.map(toUnmountPromise).map(function (unmountPromise) {
          return unmountPromise.then(toUnloadPromise);
        });
        var allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
        var unmountAllPromise = Promise.all(allUnmountPromises);
        var loadThenMountPromises = appsToLoad.map(function (app) {
          return toLoadPromise(app).then(function (app) {
            return tryToBootstrapAndMount(app, unmountAllPromise);
            // 遍历所有需要load的APP,查看是否有activeRule能够匹配,能匹配则load
          });
        });
        var mountPromises = appsToMount.filter(function (appToMount) {
          return appsToLoad.indexOf(appToMount) < 0;
        }).map(function (appToMount) {
          return tryToBootstrapAndMount(appToMount, unmountAllPromise);
          // 遍历所有需要挂载的APP,
        });
        return unmountAllPromise.catch(function (err) { // 卸载APP
          callAllEventListeners();
          throw err;
        }).then(function () {
          ......
        });
      });
    }
  }

主子应用隔离

样式隔离

一提到样式隔离,我们其实马上就可以想到使用web Component,开启shadow Dom,这种方案完全样式隔离,但是存在很大的局限性,因为目前大部分组件库对于弹窗类的组件都是挂在document.body下的,这样就导致微应用的弹窗溢出到主应用中从而丢失样式。

这里就需要了解到qiankun获取资源列表是通过运行时获取用html entry,通过import-html-entry拉取应用,子应用项目通过html为入口,运行自己内部的css,js

所以更好的是使用别的方法,qiankun对于微应用之间的样式隔离采用的是动态样式表的方式,所以对于不同的微应用qiankun只需要在微应用容器中卸载旧的html,挂载新的html,就可以自动实现单例微应用间的样式隔离。 但是这样还是没有解决主子应用的样式隔离,这里一般还是采用工程化的思路,给微应用的class增加前缀后缀,或者使用属性选择器的方式,这块就需要自己团队进行实践了。

js隔离

qiankun对于js隔离采用了两种方式

  1. 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
  2. 基于 Proxy 实现的沙箱 基于diff快照实现的方式,实际上都是在window上进行的操作,只是每个沙箱都有一个mount时的快照windowSnapshot和一份修改记录modifyPropsMap, 在激活时windowSnapshot记录下当前window的全部状态,然后将之前的modifyPropsMap在window上进行更新 在退出的时候将修改在modifyPropsMap中进行记录,通过windowSnapshot对window进行还原
key: "active",
    value: function active() {
      var _this = this;
      // 记录当前快照
      this.windowSnapshot = {};
      iter(window, function (prop) {
        _this.windowSnapshot[prop] = window[prop];
      }); // 恢复之前的变更
      Object.keys(this.modifyPropsMap).forEach(function (p) {
        window[p] = _this.modifyPropsMap[p];
      });
      this.sandboxRunning = true;
    }
  }, {
    key: "inactive",
    value: function inactive() {
      var _this2 = this;
      this.modifyPropsMap = {};
      iter(window, function (prop) {
        if (window[prop] !== _this2.windowSnapshot[prop]) {
          // 记录变更,恢复环境
          _this2.modifyPropsMap[prop] = window[prop];
          window[prop] = _this2.windowSnapshot[prop];
        }
      });
      this.sandboxRunning = false;
    }
  }]);

基于proxy实现的沙箱,是通过复制一份fakewindow,然后通过proxy设置set和get,让fakewindow只存在于对应的微应用下,每个微应用下一份全局变量,这样就完全避免了变量污染,代码大意如下

var _createFakeWindow = createFakeWindow(rawWindow),
        fakeWindow = _createFakeWindow.fakeWindow,
var proxy = new Proxy(fakeWindow, {
      set: function set(target, p, value) {
            1. 判断是否在激活状态
          2. 是否可写
          3. 将更新添加到更新Map中
          ……
          return true;
        }
        return true;
      },
      get: function get(target, p) {
        ...
      }

微应用之间如何共享数据

官方提供了基于发布订阅模式的action操作,在主应用中对state进行注册,然后自动注入到微应用中,但是并没有getGlobalState,并且qiankun源码对onGlobalStateChange做了限制,每个应用只能有一个onGlobalStateChange,所以个人感觉这个全局状态管理API并不是特别好用。 个人尝试的时候还是使用了redux,一方面没有技术栈限制,另一方面可以以更小细粒度管理状态,且subscribe没有个数限制,只需要在props里传入一个store即可,然后在微应用中获取并使用。当然本质上还是通过主应用对数据进行下发的。

# 主应用
const initState = {
    ...
};
// 创建reducer
const reducer = (state = inistate, action) => {
    ...
}
// 根据reducer创建store
const store = createStore(reducer)
// 将store传入props中
{
  name: 'vue-app', // app name registered
  entry: '//localhost:3001',
  container: '#app-qiankun',
  activeRule: '/vue',
  props: {
    store
  }
}
# 微应用
const { store } = props // 从props中获取store
store.subscribe(...)
store.dispatch(...)

总结

这里只是对qiankun的大体体验,和对自己觉得不太明白的地方做了一下深入了解,关于应用的部署以及静态资源相关的没有进行尝试,感觉官方文档基本都有涉及,这里就不多赘述了。总的来说对于To B的巨石应用来讲,如果要使用微前端解决方案的话,qiankun确实可以做到开箱即用,对子应用基本无侵入。以上为个人浅薄见解,还望大佬多多指教。