Module Federation在客服工单业务中的最佳实践

1,036 阅读13分钟

Module Federation: 是模块联邦的意思,在webpack 5中流行起来的,也属于一种微前端方案。

一、背景

1、客服高频工作场景

一线客服: 基于一站式工作台中的在线工作台及电话工作台,根据用户进线反馈的问题,查看当前用户相关的工单详情或订单详情,并根据实际情况决定是否创建新的工单。

二线客服: 根据各种渠道反馈的工单(其中一个主要来源是一线客服的反馈),根据工单内容,联系用户或其他相关方沟通处理并完结工单。

2、使用iframe的背景

在上述场景中我们可以知道:

  • 一线客服和二线客服不是同一批同学,分属不同的部门;
  • 由于业务上的一些区别,所以IM与工单是两个不同的前端项目,也由不同的前端同学开发和维护;
  • 即使使用微前端拆分后,IM、电话和工单也属于不同的微应用,工单页面在IM和电话子应用中没法直接引入工单详情页面

改造之前 IM 和电话工作台中使用到的工单 详情 页面都是使用iframe嵌套的,这在前期是最简便,兼容性最好的方案。

二、存在的问题

1、iframe问题的扩大化

我们知道,单个iframe是比较占 内存 的,打开比较慢,只是偶尔使用的时候,这些可能并不是什么大问题,但在一线客服中有这么些场景会急剧扩大这些问题,具体如下:

  • 一线客服与单个用户对话过程中,打开多个该用户的工单详情或订单详情
  • 一线客服对接多个用户咨询
  • 一线客服频繁且快速的切换不同的用户及不同的工单/订单详情
  • 由于客服对切换不同会话的响应有要求,IM侧对不同的会话页面做了缓存(如keep-alive)

这会导致 内存 消耗急剧升高,切换响应速度慢,甚至会导致浏览器崩溃,客服使用 体验 差,最终导致客服效能的降低

2、内存不断升高

模拟用户在IM中打开一个新客户的消息,点击创建工单,点击工单详情,再打开一个新的客户消息重复上述动作的页面文档数,内存占用及页面节点数的趋势图,如下:

由于页面缓存的存在,可见 内存 占用是越来越高的,在一分多钟的时间内从100多MB到500多MB,而且可以判断的是当用户的这类操作,其内存占有率将持续走高,虽然在大多数情况下浏览器的垃圾回收机制会启动,但由于:

  • 页面级的缓存使垃圾回收不那么有效
  • 在客服的长时的工作下,内存还是会持续走高

最终应用会越来越慢,甚至导致崩溃,并已经有一些客服在反馈崩溃的问题了:

3、响应速度慢

先看段工单详情打开的视频,如下:

视频中打开工单详情并非首屏打开,也需要2.8秒,当首屏打开的时候甚至需要7s!

注意: 此处及后续的数据都是在测试环境采集的,由于是内部实现的新开标签页,对LCP( Largest Contentful Paint, 最大内容渲染 ), TTI( Time to Interactive, 可交互时间)等不能很好采集,主要是根据谷歌性能工具中截屏中的时间数据来获取的,该值更接近于TTI。

三、选型及对应策略

根据当下的情况,已经不能再继续使用iframe了,那有什么方案可以满足我们目前的情形呢,首先梳理我们目前的诉求,这个诉求主要有两方面:

  • 一方面当然是解决上述问题,提升性能,提升客服使用体验
  • 另一方面它需要对前端来说要足够友好,能实现,有良好兼容性,也要保持前端的目前低成本维护

摆在我们面前的常用的方案大概有这么两个,npm包和以qiankun为代表的微前端

方案性能维护
npm好,与本地组件无异,性能是最好的差,一旦工单有发布,IM也得跟随发版,不可接受
微前端较好好,项目解耦,IM侧无需关心工单侧的迭代及具体技术

1、npm包适合么

先看下我们需要引用的页面本身,如下图所示:

它是一个复杂度高的,业务关联性高,更新频次高的三高页面级组件,完全不适合做成低频的npm包了

2、微前端合适么

大家可能以为我要选qiankun为主的微前端方案了,但并不是,因为qiankun或其他类似的微前端方案在我们这样的具体场景下也有一些问题:

2.1、对嵌套场景的支持度不高

在我们一站式工作台的整体方案中,已经使用qiankun将工单、IM、电话拆成了三个子应用,如果IM再引用工单子应用就会形成应用之间的嵌套,如下图所示:

搜寻微前端嵌套方案:

结论 无论是webpack还是vite,以沙箱隔离为主要解决方案的微前端在嵌套场景上支持度不高 并且如果以后出现更多,更深,甚至循环嵌套的情况的话,那就更棘手了。

2.2、场景不适合

以沙箱隔离的微前端方案,还是会在每次引用时将项目本身初始化所需的文件都引用并且解析加载,即使你引用一个小组件他也会这么做,还是会影响组件加载的速度。所以它主要是解决以项目维度的引用,隔离及性能问题。在以模块或组件为维度的情形不适合,它还是有点重了。

那么是否存在这样一个方案,可以像npm包这样灵巧,只加载对应的组件代码,又可以像微前端这样直接引用,与项目无关呢,答案是有的,那就是这次的主角——Module Federation(模块联邦)。

四、Module Federation模块联邦

模块联邦,它首先是在webpack 5中流行开来的,可见文档:Module Federation | webpack 中文文档,而且它也属于一种微前端方案。

1、我们关注下它能干什么

我感觉一句话概括就是:它能允许一个线上部署的项目在运行时加载其他线上部署项目中的组件

其中关键几点如下:

  • 它没有沙箱隔离方案,与宿主环境共用一个window环境
  • 加载时只加载目标组件及其相关的内容
  • 无需改变组件或页面在之前的项目目录位置或结构,直接根据对应目录层级引用即可
  • 使用时与本项目组件使用方式一致,因为它就是一个组件,只是远程加载而已

2、基本原理

设想一下,在webpack中可以将一些后续加载的模块打成chunk包,然后在使用到这个模块的时候再懒加载,这种情形一般是在同一个项目中进行。

那么我们是否也可以在A项目中把一个组件及其的依赖打包成chunk包,而在B项目中按约定的地址异步import刚才那个A项目的chunk包,并运行使用呢?答案就是Module Federation。

2.1、一些概念

  • 远程组件提供方称为remote端,远程组件使用方称为host端。
  • 一个项目既可以为remote端,也可以为host端,可以使用数个不同其他项目的组件,也可以为数个其他项目提供不同的组件。

据此,我们甚至可以打造一个去中心化的应用集群,大概这样:

2.2、社区应用

而且此项技术已在多个知名厂商中应用:

2.3、vite中的Module Federation方案

vite插件vite-plugin-federation提供了对标webpack的能力,只是加载的方式改成了ESM,其插件接口设计与webpack的参数设计几乎是一样的。vite-plugin-federation也是vite官方推荐和收录的插件,已收录在社区插件列表GitHub - vitejs/awesome-vite: ⚡️ A curated list of awesome things related to Vite.js

2.4、具体到我们的场景中

工单工作台应用,作为remote端,它可以提供工单详情,订单详情和创建工单,赔付模块等多个页面级组件。

IM 或电话工作台应用,作为host端,可以使用上述组件。

3、具体实践

由于我们两个项目都使用的是vue3 + vite方案,所以使用vite-plugin-federation插件是我们最佳的选择。首先需要在host端和remote端安装插件,如下命令:

yarn add @originjs/vite-plugin-federation -D

注意: 此处为了展示方便使用了官方插件,实际由于项目相关的原因,我们对官方插件进行了一些改动。

3.1、工单工作台作为remote端的配置

(1) 在插件内注册需要提供的组件

并且提供需要共享的三方依赖

(2) 配置完成后打包

在remote端打包后会生成对应的入口文件,chunk文件及依赖包文件:

3.2、IM和电话工作台作为host端的配置

(1) 插件配置,设置远程包地址

(2) 引用及注册组件 main.ts文件 如下

这时需要注意,ticket-share为host侧在vite插件中注册的包名,Detail为remote端暴露(exposes字段)声明的组件名。 除了在全局注册,还可以在任意需要引用的地方引用。

(3) 组件的使用

在需要的地方引入使用,并根据业务情况传递props,props的使用方式与普通组件无异。

此时我们已经完成了全部配置,此后我们只要打包上线运行即可。

五、重构之后的效果

1、加载速度

再来看一段视频

大家可以与最开始的视频对比一下,可以看出非常直观的差异。

首屏二次(非首屏)
iframe7076ms2594ms
模块联邦1279ms428ms

注意:上述数据由测试环境进行十次平均得到,测试值更接近TTI。

从数据中可以看到,在首屏环境(即无任何缓存)下模块联邦加载速度提高了5.5倍。而在二次加载场景中(此场景是客服使用频率最高的场景),速度提高了6倍!

2、内存

我们依旧根据开始的场景,模拟客服打开多个用户多个工单/订单详情的情形,如上图,首先可以发现:

  • 没有内存泄漏!
  • 内存占用在相同的时间内,最高只达到了55MB,并回落。
  • 单个工单详情打开内存增幅从10MB降低到不足1MB。

因为除了模块联邦带来的直接收益,我们还享受到了它的附加收益,由于模块联邦本身引用的是一个组件,所以利用vue组件本身的特性,多个工单详情我们可以复用同一个实例!

3、客服反馈

上线之后,我们再也没有收到相关的任何负面反馈,例如打开工单详情慢,或者说浏览器崩溃的反馈没有了!

六、后续规划

1、模块联邦的缺点:维护共享库

它并不是没有任何缺点,在配置过程中我们可以看到需要配置share字段,即不同项目需要共享的三方库,例如A项目使用vue,B项目也使用vue,那么就需要将vue声明出来,插件会将vue自动分割出来,这样A,B项目可以共享使用了,在remote端和host端都需要声明,声明如下:

shared: { 
  vue: { 
    // remote端该字段为requiredVersion 
    version: '3.1.5' 
  }, 
  axios: { 
    version: '0.21.1' 
  }, 
  vuex: { 
    version: '4.0.0' 
  }, 
  'vue-router': { 
    version: '4.0.8' 
  } 
 } 

但是,这里对开发同学有非常不友好的点:

  • 需要将组件共享的包都手动找出来
  • 共享有时是强制的,如果漏了某些包,可能会导致页面报错或崩溃,想象一下页面同时有两个vue
  • 同一个库,有时需要维护在约定的版本范围内,有时一个包太老或太新也会在加载时报错

简而言之,就是对共享包的维护成本很大!

2、解决思路

  • 需要建立日常及生产构建时hook
  • 然后自建服务监听这些hook,对比三方库的包差异及其版本差异, 并抛出相应警告
  • 引入巡检,构建完成后对远程模块部分进行检查,因为不是每个改动都会使测试回归,但每个改动都可能引起页面崩溃。
  • 对插件进一步封装,使开发者无需关心三方依赖的管理。

3、与去应用化assets发布的冲突

  • 由于assets发布天然需要引入版本号,因此每个迭代所有的资源的地址都会带入版本号
  • 远程组件引用需要按约定确定一个remote端的入口文件地址,该地址基本是固定的

所以,目前remote端项目还不能使用assets发布,接下来一是可以用接口的方式代替,二是需要团队之间协商处理这个问题。

4、更大方向的畅想

还记得“可以打造一个去中心化的应用集群”那张图吗?组件的引用不仅仅出现在团队内部,也可能出现在团队间共享,例如创建工单这个模块就会被情报系统这样的团队引用。

即各个团队间,或团队内部的组件或模块互相引用的,目前主要依靠iframe解决的,都可以使用模块联邦解决。

而公司内部也可以有这么个平台,集合模块联邦的团队,业务模块展示管理,共享依赖包的校验,警告或自动修补等等功能,来实现业务级别的团队内外高效协作。

扩展阅读:

【1】juejin.cn/post/685656…

【2】github.com/umijs/qiank…

【3】webpack.docschina.org/concepts/mo…

【4】juejin.cn/post/684490…

【5】juejin.cn/post/691149…

【6】github.com/vitejs/awes…

【7】github.com/vitejs/awes…

文/LIUZHOUCHANG

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