关于前端:微前端在得物客服域的技术实践-那么多微前端框架为啥我们选Qiankun-MF

12次阅读

共计 9873 个字符,预计需要花费 25 分钟才能阅读完成。

一、业务背景

以后客服一站式工作台蕴含 在线服务、电话、工单和工具类 四大性能,页面的根本构造如下:

每个业务模块绝对独立,各有独立的业务体系,单个模块体积较大,我的项目整体采纳 SPA + iframe 的架构模式,其中的工单零碎就是通过 iframe 嵌套的。在客服业务一直迭代的过程中,SPA + iframe 的架构模式暴露出了很多问题,次要问题如下:

  • 问题一:SPA 架构模式下,因为各个模块集中于一个架构下,导致首屏加载资源过多,首屏加载速度较慢;SPA 只有入口文件,所以须要对各个模块做业务模式兼容,导致入口文件代码条件语句较多,代码错乱,呈现线上问题的时候,排查较为艰难,如果有新的同学参加开发,梳理业务也较为艰难,甚至有的时候难以了解。
  • 问题二:我的项目中嵌套大量的 iframe,iframe 也会连累页面的加载速度,iframe 应用 postMessage 通信时也会带来数据提早,数据失落等各种问题,客服应用工夫较长的时候,当切换 iframe 中的页面时,前一个页面中的无奈被齐全开释,导致浏览器所占的内存不停的飙升,最终导致浏览器解体。

基于下面两个问题,咱们用微前端技术对一站式工作台做了业务上的拆分,本文次要论述在拆分过程中遇到的问题和挑战。

二、技术计划调研

通过对微前端技术计划的调研,能够晓得:微前端是一种相似于微服务的架构,它将微服务的理念利用于浏览器端,行将单页面前端利用由繁多的单体利用转变为多个小型前端利用聚合为一的利用,具备以下几个外围价值:

  • 技术栈无关: 主框架不限度接入利用的技术栈,微利用具备齐全自主权
  • 独立 开发 、独立部署: 微利用仓库独立,前后端可独立开发,部署实现后主框架主动实现同步更新
  • 增量降级: 在面对各种简单场景时,咱们通常很难对一个曾经存在的零碎做全量的技术栈降级或重构,而微前端是一种十分好的施行渐进式重构的伎俩和策略
  • 独立运行时: 每个微利用之间状态隔离,运行时状态不共享

通过对开源社区相干微前端技术的调研,现今支流的微前端解决方案次要包含以下这些:

  • 技术框架: iframe、single-spa、qiankun、icestark、Garfish、microApp、ESM、EMP
  • 技术亮点: js Entry、html Entry、沙箱隔离、款式隔离、web Component、ESM、ModuleFederation
##### 解决方案 ##### 起源 ##### 特点 ##### 毛病
iframe 天生隔离款式与脚本、多页 窗口大小不好管制,隔离性无奈被冲破,导致利用间上下文无奈被共享,随之带来开发体验、产品体验等问题无奈做到单页导致许多性能无奈失常在主利用中展现
single-spa 国外 Js Entry, 主利用重写 window.addEventListener 拦挡监听路由的工夫,执行外部的 reroute 逻辑,加载子利用 基于 reroute,对于须要缓存,加载多利用的场景不适宜
qiankun 蚂蚁金服 基于 single-spa,减少了 html-entry,sandbox, globalSate, 资源预加载等外围性能 须要编译为 umd 形式,对于 AMD,systemJs 反对不敌对,且官网没有公开反对 vite 构建
icestark 阿里 把大部分配置通过 cache 写进 window[‘icestark’]全局变量 只对 React 反对,跨框架反对不敌对
Garfish 字节 对现有 MFE 框架的增强版,VM 沙箱
microApp 京东 基于 web Component 的实现 存在兼容性问题,微前端方面的摸索不够成熟
ESM 微模块,通过构建工具编译为 js,近程加载模块,无技术栈限度,跟页面路由无关,能够随处挂载 无奈兼容所有浏览器(但能够通过编译工具解决),需手动隔离款式(可通过 css module 解决),利用通信不敌对
EMP 欢聚时代 基于 Module Federation、去中心化、跨利用状态共享、跨框架组件调用、近程拉取 ts 申明文件、动静更新微利用、第三方依赖的共享等能力 目前无奈涵盖所有框架

通过调研以及联合咱们的业务现状,采纳了 qiankun + Module Federation 作为咱们微前端的技术框架,依照性能拆分,将利用拆分为 4 个独立的零碎,能够独立 开发 ,独立部署,可依据权限配置接入基座;我的项目中波及到 依赖其余模块的中央采纳近程组件的形式加载依赖组件,例如:IM,电话中会依赖工单中的工单创立,赔付,工单详情,订单详情等组件,工具箱目前会依赖 IM 中的会话记录组件,所以 IM,工单能够作为 remote 端,IM、电话,工具箱能够作为 host 端,提供更敌对的组件复用办法,勾销了以前的 iframe 加载形式,也不须要利用 qiankun 加载多个微利用的形式去实现,防止大量资源的反复加载,进步页面的响应速度。

1、一站式工作台微前端架构图

2、MF 近程组件规划图

三、计划具体实现

后面咱们曾经通过调研和联合我的项目理论,采纳 qiankun 作为业务利用拆分的微前端框架,模块联邦作为不同利用之间共享近程组件的框架,造成了初步的框架体系,在此框架体系下,咱们面临很多的技术挑战,如下:

  • 微利用须要具备缓存 (keep-alive) 能力,利用切换状态不能失落
  • 须要具备同一时刻加载多个微利用
  • 沙箱隔离和引入第三方资源
  • 基座 - 微利用,微利用 - 微利用之间如何进行通信
  • 如何接入近程组件
  • 款式隔离

基座 - 微利用连贯示意图

1、微利用缓存能力的实现

qiankun 为咱们提供了两个注册办法:registerMicroAppsloadMicroApp

  • registerMicroApps(apps, lifeCycles?):实用于 route-based 场景,路由扭转会帮咱们主动注册微利用和销毁上一个微利用,对于不须要做缓存的利用来说,举荐应用这个办法,简略易用,只须要给微利用设置一个独立的路由匹配规定即可。

上面是 qiankun 官网的一段 demo 示例:

import {registerMicroApps} from 'qiankun';

registerMicroApps(
  [
    {
      name: 'app1',
      entry: '//localhost:8080',
      container: '#container',
      activeRule: '/react',
      props: {name: 'kuitos',},
    },
    {
      name: 'app2',
      entry: '//localhost:8081',
      container: '#container',
      activeRule: '/vue',
      props: {name: 'Tom',},
    },
  ],
  {beforeLoad: (app) => console.log('before load', app.name),
    beforeMount: [(app) => console.log('before mount', app.name)],
  },
);
  • loadMicroApp(app, configuration?):实用于须要手动 加载 / 卸载 一个微利用的场景。对于咱们来说,须要实现缓存和同时加载多个微利用,这个办法更实用。

论断:

qiankun2.0 之后官网为咱们提供 loadMicroApp API 给咱们带来手动管制利用加载 / 卸载的能力,且不是基于 routeBase 加载资源,所以咱们不必放心在切换菜单的时候,导致前一个微利用被被动卸载。

基于 loadMicroApp 手动管制加载微利用的个性,想要实现 keep-alive 能力,能够 基座和微利用设置适合 keep-alive 缓存策略, 而后 通过“display: none”的形式去管制切换的显示和暗藏 (DOM 从新渲染会导致历史状态失落), 在基座中为每个微利用设置挂载点,利用切换的时候就不会导致前一个微利用 DOM 被卸载。

在基座中的逻辑

当咱们检测到路由变动的时候,手动的去调用 loadMicroAppFn 去加载对应的微利用,对于须要同时加载多个的场景,能够循环去调用加载(vite 构建下加载多个微利用可能会失败,倡议采纳 webpack 构建)。

具体起因可参考 issue:

  • 想问一下,将来是否思考反对 vite · Issue #1257 · umijs/qiankun
  • [[求答疑] 奴才利用均应用 vite 应用 loadMicroApp 无奈同时加载多个子利用 #1861](https://github.com/umijs/qian…)
// 手动加载微利用办法封装
const loadMicroAppFn = (microApp) => {
  const app = loadMicroApp(
    {
      ...microApp,
      props: {
        ...microApp.props,
        // 下发给微利用的数据
        microFn: (status) => setMicroStatus(status)
      },
    },
    {
      sandbox: true,
      singular: false
    }
  );
  
  return app;
}
// 为每个微利用提供一个挂载的容器节点:<template>
  <div class="tabs-view">
    <div class="tabs-view-content tabs-view-container">
      <template v-if="microApps && microApps.length">
        <div
          v-for="micro in microApps" :key="micro.name"
          :id="micro.id"
          v-show="currentPath && currentPath.startsWith(`${micro.key}`)"
        ></div>
      </template>
    </div>
  </div>
</template>

<script lang="ts">
import {defineComponent, computed, toRefs, watch, ref} from 'vue'
import {useRoute} from 'vue-router'
import {useStore} from 'vuex'

export default defineComponent({
  name: 'Micro-content',
  components: { },
  props: {
    currentMenu: {
      type: String,
      default: ''
    }
  },
  setup(props) {const route = useRoute()
    const store = useStore()
    // 微利用注册表
    const microApps = computed(() => store.getters.microAppsList).value
    const currentPath = ref(route.path)

    watch(() => route.path,
      (to, _) => {currentPath.value = route.path},
      {immediate: true}
    )

    return {
      route,
      spin,
      microApps,
      currentPath
    }
  }
})
</script>
<template>
  <div class="app-content">
    <a-config-provider :locale="zhCN" prefixCls="basic">
      <router-view v-if="isShowViews" v-slot="{Component}">
        <keep-alive v-if="isKeppAlive">
          <component :is="Component" />
        </keep-alive>
        <component :is="Component" v-else />
      </router-view>
    </a-config-provider>
  </div>
</template>

在子利用中的逻辑: 须要调用 qiankun 生命周期,入口文件设置适合的 keep-alive 缓存策略

import './public-path'
import {createApp} from 'vue'
import App from './App.vue'
import router, {setupRouter, destroyRoute} from '@/router'
import {setupStore} from '@/store'
import {isChildApp} from '@/utils/env'

let app: any = null
function render(props) {app = createApp(App)
  // 挂载 vuex 状态治理
  setupStore(app, props)
  // 挂载路由
  setupRouter(app)
  // 路由准备就绪后挂载 APP 实例
  router.isReady().then(() => {app.mount(document.getElementById('miro-app'))
  })
}

// 独立运行时
if (!isChildApp()) {render({})
}

// 裸露主利用生命周期钩子
export async function mount(props: any) {render(props)
}

export async function bootstrap() {console.log('vue app bootstraped')
}

// 销毁生命周期
export async function unmount(props: any) {app.unmount()
  app._container.innerHTML = ''
  destroyRoute()
  app = null
}
<template>
  <a-config-provider :locale="zhCN">
    <router-view v-slot="{Component}">
      <keep-alive v-if="isKeepAlive">
        <component :is="Component" />
      </keep-alive>
      <component :is="Component" v-else />
    </router-view>
  </a-config-provider>
</template>

微利用加载前后 performance 性能比照图:

  • 第一次激活各个微利用性能耗费:

  • 加载胜利之后切换微利用性能耗费:

\

通过微利用激活前后的性能比照可知:

  • 微利用初始化加载的时候,须要经验一次资源申请,页面渲染,会有一次大的性能开销;
  • 微利用加载胜利之后,在此切换回来,采纳“display: none”+keep-alive 形式解决 + 路由 过滤,尽管须要经验一次重流重绘,但也不会带来太大的性能开销

2、沙箱隔离和引入第三方资源资源

qiankun 外部的沙箱次要是通过是否反对 window.Proxy 分为 LegacySandbox 和 SnapshotSandbox 两种。对于通过 script 标签去加载的第三方资源,须要留神的是:要显示的申明一个全局变量并挂载到 window 上,这样能力在应用的时候获取到。

扩大浏览:多实例还有一种 ProxySandbox 沙箱,这种沙箱模式目前看来是最优计划。因为其体现与旧版本略有不同,所以临时只用于多实例模式。ProxySandbox 沙箱稳固之后可能会作为单实例沙箱应用。原文链接:https://segmentfault.com/a/11…

// 例如上面这个例子
// global.js 中定义一个全局变量
var globalMicroApp = 'micro-name'
// index.html 引入这个 global.js
<script src="global.js"></script>

// global.js 中定义一个全局变量
var globalMicroApp = 'micro-name'
window. globalMicroApp = globalMicroApp
// index.html 引入这个 global.js
<script src="global.js"></script>

案例 1 因为沙箱隔离,在应用的时候无奈获取到该全局变量,案例 2 才是正确的形式,如果有应用 jQuery,最好放在基座中加载,例如当应用 ajax jsonp 去跨域加载资源的时候,放在微利用中沙箱隔离的起因会导致无奈获取到 callbackName(没有显示的挂载到 window 上),对于 jsonp 跨域的申请,也须要非凡解决,否则 qiankun 会劫持该 jsonp 申请,将其转为 fetch 申请导致跨域失败。

const loadMicroAppFn = (microApp) => {
  const app = loadMicroApp(
    {
      ...microApp,
      props: {...microApp.props},
    },
    {
      sandbox: true,
      singular: false,
      // 指定局部非凡的动静加载的微利用资源(css/js) 不被 qiankun 劫持解决
      excludeAssetFilter: (url) => {return !!(url.indexOf("https://xxx.com/xxx") !== -1);
      },
    }
  );

  return app;
};

3、利用之间的通信

通信形式能够采纳:URL 携参,window,postMessage, qiankun 提供的 props, initGlobalState 等形式;在此只介绍 props, initGlobalState 这两种形式。

  • props 形式传递参数:

基座通过 qiankun loadMicroApp 办法下发一个 state 参数,这个 state 能够为一般类型,也能够为一个 callback,或者 vuex action 办法,微利用激活之后能够通过 qiankun 生命周期函数 mount 拿到 props 传递下来的 state,如果须要微利用更新数据到基座,能够下发一个 action 或者 callback,微利用在承受办法后保留到本人的 vuex store 中,须要更新数据的之后,间接调用缓存的 action 或者 callback。

props 通信示意图

  • initGlobalState 形式传递参数:

action 订阅 - 公布模式示意图

基座:

import {initGlobalState, MicroAppStateActions} from 'qiankun';

// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

微利用:

// 从生命周期 mount 中获取通信办法,应用形式和 master 统一
export function mount(props) {props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });

  props.setGlobalState(state);
}

4、如何接入近程组件

近程组件采纳 webpack5 模块联邦去实现,在微前端实际中须要留神的事项:

// mian.ts 中只能导出 qiankun 生命周期
const {bootstrap, mount, unmount} = await import('./bootstrap')
export {bootstrap, mount, unmount}

须要将入口文件 (mian.ts) 转移到新的文件(bootstrap.ts),并在入口文件中导出 qiankun 生命周期,防止打包出两个入口文件,导致 qiankun 加载生命周期函数失败。

具体的接入办法能够参考这篇文章:Module Federation 在得物客服工单业务中的最佳实际

5、款式隔离

qiankun 官网 API 给咱们提供了很欠缺的 API,如下所示:

sandbox – boolean | {strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean}

  • 默认场景 sandbox: true, 只能保障单实例下的款式隔离,无奈保障多个微利用共存,基座 - 微利用之间的款式隔离;
  • 设置为 strictStyleIsolation: true;示意开启严格的款式隔离模式。这种模式下 qiankun 会为每个微利用的容器包裹上一个 shadow dom 节点,从而确保微利用的款式不会对全局造成影响;
  • qiankun 还提供了一个实验性的款式隔离个性,当 experimentalStyleIsolation 被设置为 true 时,qiankun 会改写子利用所增加的款式为所有款式规定减少一个非凡的选择器规定来限定其影响范畴,因而改写后的代码会表白相似为如下构造:
.app-main {font-size: 14px;}

div[data-qiankun-react16] .app-main {font-size: 14px;}

这种试验个性(experimentalStyleIsolation)也能够通过 postcss 插件去实现,社区提供了一个插件postcss-plugin-namespace,应用起来也比较简单,配置如下:

postcss:{plugins:[require('postcss-plugin-namespace')('.basic-project',{ ignore: [ '*'] })]
}
.app-main {font-size: 14px;}

.basic-project .app-main {font-size: 14px;}

尽管官网提供了很欠缺的 API,但对于很多场景来说都不能很完满的解决款式抵触的问题,例如基座的全局款式会净化微利用的全局款式,如果你应用的是 antd/ant-design-vue,能够采纳如下的形式去更改 UI 库前缀,也是一个很好的解决方案:在入口文件 app.vue 中:ant-design-vue 提供了一个 prefixCls 能够帮忙咱们批改 class 前缀:

在 vue.config.js 中能够在 less/sass loader 中笼罩 ant-design-vue 的类名全局变量:

批改完之后的成果:

// 批改前
.ant-menu-item {
  text-align: center;
  padding: 10px;
}

// 批改后
.basic-menu-item {
  text-align: center;
  padding: 10px;
}

四、带来的功效

通过微前端技术对一站式工作台的革新,咱们对革新前和革新后做了比照:

项目名称 CR 效率 开发效率 班车公布制度 近程组件
革新前 较慢 我的项目较重,代码耦合性较高,开发难度大 利用较重,班车公布须要思考的问题较多 不反对
革新后 各子利用拆分,齐全解耦,可节俭 1 / 3 工夫 独立利用开发,业务逻辑解耦,开发效率更高 独立开发,独立公布,更轻便,班车公布,须要测试回归的内容较少,能更快的交付业务需要 反对

五、思考与总结

经验我的项目立项到实现整个过程,选定 qiankun 作为咱们的微前端框架,在整个开发过程中堪称是艰难曲折,第一个难关就是微利用缓存能力的实现,社区中只有简短的 demo,间隔真正落地到我的项目差的还很远;其次咱们的我的项目还须要思考刷新页面,在以后微利用重载其余微利用的场景;有些微利用须要依赖第三方的插件,这个插件可能会是一个 jQuery 插件,可能还会遇到 jsonp 跨域的场景;还须要思考微利用之间通用组件的复用问题;原始我的项目采纳 vite 构建,面对 qiankun 对 vite 反对不敌对的状况下,最终不得不抉择 webpack5。

在遭逢这一系列问题后,而后再到解决这些问题,对咱们来说,收益还是很大,也积攒了很多社区计划中短板的内容。通过这次我的项目之后我的思考是:任何技术框架都有其实用场景,对于特定的业务场景,可能原来的技术架构显得臃肿,但他可能是最合适的,微前端不是神话,正确的场景应用正确的技术才是最优选。

六、参考文档

  • qiankun 官网文档
  • 比照多种微前端计划
  • 万字长文 + 图文并茂 + 全面解析微前端框架 qiankun 源码 – qiankun 篇
  • 可能是你见过最欠缺的微前端解决方案 - 阿里云开发者社区
  • Module Federation 在得物客服工单业务中的最佳实际
  • webpack5 应用模块联邦,抛出异样,生命周期未加载 #1515
  • MicroApp

文 /CHENLONG

关注得物技术,做最潮技术人!

正文完
 0