微前端架构(二):封装与实现

一、前言

很荣幸由我设计并实现了公司自研的微前端框架,目前该框架已在多个业务项目中广泛应用。

本文是我结合内部培训、产品文档及相关实践梳理而成的总结,旨在对微前端技术体系进行系统性的整理与沉淀。

本篇是系列文章的第二篇,分享一些关于微前端的封装和实践。

二、产品简介

XX微前端是基于qiankun和公司权限管理系统实现的微前端架构解决方案,旨在帮助大家能更简单、无痛地构建一个生产可用微前端架构系统。

它在XX基础模板的基础上,兼容适配了XX已有的用户认证、权限控制、埋点功能,并集成了一系列微前端能力,包括应用加载、应用通信、应用隔离、应用缓存等。

三、核心封装与实现

3.1 应用缓存

在主应用中使用v-show指令控制子应用容器,以实现子应用的切换与缓存。

xml 复制代码
<!-- 子应用容器 -->
<template v-for="item in apps">
  <div
    class="app-container"
    v-show="currentAppName && currentAppName === item.name"
    :id="`container-${item.name}`"
    :key="item.name"
  ></div>
</template>

3.2 应用加载

采用qiankun的loadMicroApp实现多标签页场景下的应用加载。

核心逻辑说明:监听路由变化=》根据路由匹配子应用=》loadMicroApp手动加载子应用,给挂载到指定容器。

vue 复制代码
// 1. 监听路由变化
watch: {
    $route: {
      immediate: false,
      handler(route) {
        this.openMicroApp(route);
      }
    }
  },
// 2. 根据路由信息获取菜单数据及子应用信息
const resource = getMenuByRoute(route);
subApp = resource && this.getAppById(resource.rootResourceId);
// 2.1 菜单资源数据映射关系
{
  id: item.resourceId, // web应用资源id
  name: item.resourceCode, // web应用资源编码
  entry: item.resourceUrl // web应用基础路径
}
// 3. 调用loadMicroApp加载子应用
const { name, entry } = subApp;
const appInstances = loadMicroApp({
  name,
  entry,
  container: `#container-${name}`,
  props: {}
});

以上代码是简化后的示意代码,实际实现中还包含有对容器节点、路由、loading等细节的处理。

3.3 应用通信

3.3.1 基于props

qiankun的loadMicroApp 方法支持通过props配置选项,传递数据或者方法给子应用。

javascript 复制代码
// 1. 主应用定义并通过props.data传递
const mainAppName = 'app-main'
const appInstances = loadMicroApp({
  name,
  entry,
  container: `#container-${name}`,
  props: {
    data: {
      mainAppName
    }
  }
});

// 2. 子应用接收
export async function mount(props) {
  render(props);
}
function render(props) {
  const { data = {}, container } = props || {};
  console.log(data.mainAppName)
}

3.3.2 基于window

通过挂载全局属性和方法,供其他应用使用。

scss 复制代码
// 1. 主应用定义
window.$mainAxxxxxxx = initAxxxxxxx(router);

// 2. 子应用通过window调用
window.$mainAxxxxxxx.xxx();

3.3.3 基于事件

框架选用Node.js的EventEmitter,由主应用集成并对外提供event-bus基于事件总线的应用间通信能力。包含事件的注册、派发和移除功能。

javascript 复制代码
// 1. 主应用实例化事件对象
import { EventEmitter } from 'events';
const eventBus = new EventEmitter();
// 2. 监听事件
eventBus.on('event1', (params1, params2) => {
  // ...do some things
});
// 3. 传递实例给子应用
loadMicroApp({
  props: {
    data: {
      eventBus
    }
  }
});

// 4. 子应用接收并派发事件
function render(props) {
  const { data = {}, container } = props || {};
  if (data.eventBus) {
    data.eventBus.emit('event1', params1, params2);
  }
}
// 5. 子应用也可以 监听其他应用派发的事件
data.eventBus.on('xxx',xxx)

注意事项:

  • 在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信。虽然在短期内往往是最简单的解决方案,但从长期来看,它维护起来总是令人头疼。可以用,但不建议大量使用。
  • 删除在其他地方添加的监听器是不好的做法,特别是当 EventEmitter 实例是由其他组件或模块(例如套接字或文件流)创建时。存在较大风险隐患。

3.3.4 基于vuex

通过共享主应用store, 可以快速将基座的vuex注册到微应用自己的vuex实例上。

kotlin 复制代码
// 1. 主应用定义并通过props传递store
import qiankunCommonStore from '@/store/modules/common';
loadMicroApp({
  name,
  entry,
  container: `#container-${name}`,
  props: {
    data: {
      store: qiankunCommonStore,
    }
  }
});
// 2. 子应用store注册到自己的vuex实例上
function render(props) {
  const { data = {}, container } = props || {};
  if (store && store.hasModule && data.store) {
    store.registerModule('mainAppStore', data.store);
  }
}
// 3.1 使用:取值
const userInfo = this.$store.state.mainAppStore.userInfo
// 3.2 使用:赋值
this.$store.commit('mainAppStore/setUserInfo', { ...this.userInfo, newData: 'home-new-data' });

3.3.5 基于initGlobalState

qiankun提供的基于全局状态的通信方式。模板中未集成,如需使用,可参看qiankun文档自行扩展。

未集成的原因是,作者并不推荐该API并计划在未来版本中移除。

globalState 不是合理的微前端通信方案,会加剧应用之间的耦合.。 -- qiankun官方issues

3.4 应用跳转

3.4.1 应用内跳转

调用自身路由router的方法

javascript 复制代码
onJump(path) {
  this.$router.push({
    path,
    query
  });
},

3.4.2 应用间跳转

使用主应用路由方法

javascript 复制代码
onJumpOther(path) {
  this.$root.mainAppRouter.push({
    path,
    query
  });
},

3.5 应用隔离

3.5.1 js沙箱

众所周知javaScript的全局作用域存在命名冲突、变量污染等风险,为了解决该问题,qiankun借助 ES6 的 proxy ,创建js代理沙箱,实现了应用间环境的隔离。

qiankun已有实现,默认为开启状态。

参考资料:

3.5.2 样式隔离

为了确保不同组件或模块之间的样式不会相互影响,从而提高代码的可维护性和可复用性。qiankun提供有两种样式隔离方案,strictStyleIsolation和experimentalStyleIsolation。

qiankun默认开启strictStyleIsolation严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。

参考资料:

3.5.3 本地存储隔离

为了防止各微应用间操作本地存储时,数据覆盖、误删除等问题,框架封装并集成了本地存储功能模块。原理是在基座运行时,覆写localStorage、sessionStorage方法,给本地存储的key 统一加上前缀。

我们对此封装了一个组件,代码 和API就不赘述了,这里仅分享,在去覆写的时候遇到的一个卡点问题和解决方案。

问题描述:修改Storage接口的原型方法后,乾坤无法隔离, 导致所有应用(包括主应用)的相关方法均被修改。

解决方案:在引入qiankun之前删除window.localStorage,随后再重新声明,此时的window.localStorage是隔离的,在主子应用分别定义,互不影响。

javascript 复制代码
// 主应用
window.originStorage = window.localStorage;
delete window.localStorage;
window.localStorage = {
 getItem(...ags) {
   console.log("this", this);
   return window.originStorage.getItem(...ags);
},
};

// 子应用
window.localStorage = {
 getItem(key, ...ags) {
   console.log("insub");
   return window.originStorage.getItem(`app-vue${key}`, ...ags);
},
};

原理是qiankun的沙箱(proxySandBox),会从window对象拷贝不可配置的属性并filter掉,不走proxy代理,比如location、localStorage等。现在咱们在qiankun沙箱逻辑执行前,把localStorage属性移除,它拷贝不到,后面咱们再加上的localStorage就会走proxy然后被隔离起来。

3.6 权限控制

整体策略是,各微应用分别进行权限控制,谁的页面谁管控。

在微前端场景下,由于各微应用的路由监听的是同一url,当url切换时,所有微应用的路由守卫都会触发。需要对不是自己应用路由放行。

vbnet 复制代码
// 微前端运行时的路由守卫
function microAppHook(to, from, next){
  const isAppNameMatched = to.meta.appName ? to.meta.appName === appName : true,
    isNotMyRoute = !to.path || !to.name || !isAppNameMatched;
  if (isNotMyRoute) {
    // 非当前应用路由,跳过处理
    next();
    return;
  }
  // 权限控制:白名单、角色菜单权限判断
  if (hasPermission(to) || hasPermission(to, false)) {
    next();
  }else{
    next({path: '/404', replace: true});
  }
}

四、后续改进

  1. 性能提升

    1. 内存占用高:在低版本火狐浏览器内存占用经常性超过2G,页面卡顿明显
    2. 加载速度慢:超过5M的子应用资源,请求和加载长达3-5秒
  2. 简化路由、菜单资源的处理

    1. 当前关于菜单资源的处理较复杂且不灵活,有较大提升空间
相关推荐
瘦的可以下饭了3 小时前
2 数组 递归 复杂度 字符串
前端·javascript
Kellen3 小时前
ReactDOM.preload
前端·react.js
岭子笑笑3 小时前
vant 4 之loading组件源码阅读
前端
hxmmm4 小时前
自定义封装 vue多页项目新增项目脚手架
前端·javascript·node.js
ETA84 小时前
JS执行机制揭秘:你以为的“顺序执行”,其实是V8引擎在背后搞事情!
前端·javascript
鹏北海-RemHusband4 小时前
微前端实现方式:HTML Entry 与 JS Entry 的区别
前端·javascript·html
瘦的可以下饭了4 小时前
3 链表 二叉树
前端·javascript
我那工具都齐_明早我过来上班4 小时前
WebODM生成3DTiles模型在Cesium地图上会垂直显示问题解决(y-up-to-z-up)
前端·gis
粉末的沉淀4 小时前
jeecgboot:electron桌面应用打包
前端·javascript·electron