• home > webfront > engineer > Architecture >

    微前端学习笔记(4):从微前端到微模块之EMP与hel-micro方案探索

    Author:zhoulujun Date:

    页面级别的微前端粒度太粗,有时候需要更细粒度的微前端,例如:组件、函数级别的。这种场景,就可以使用远程模块,来实现微模块的效果。

    ModuleFederation是啥?

    Module Federation就是一个JavaScript远程模块加载架构,即:Module federation allows a JavaScript application to dynamically run code from another bundle/build, on both client and server。


    它允许将一个应用程序的某些模块打包为一个独立的、可远程加载的 bundle,并在运行时动态地加载这些模块。这样,在另一个应用程序中就可以通过远程容器加载这些模块,并直接使用它们。这种方式可以避免重复打包和加载相同的模块或库,提高了应用程序的性能和效率。

    复习一下核心概念:

    • module:每一个源码js文件其实都可以看成一个module

    • chunk:每一个打包落地的js文件其实都是一个chunk,每个chunk都包含很多module

    • 本地模块即为普通模块,是当前构建的一部分;

    • 远程模块不属于当前构建,并在运行时从所谓的容器加载。

    建议读一下:《探索webpack5新特性Module federation在腾讯文档的应用》、《一文通透讲解webpack5 module federation》、《一文看透 Module Federation 下

    image.png

    mf提供的能力就是把每个应用中可以共用的component或library提取出来,部署到应用集群中,每个应用可以引用其他应用暴露出来的模块,自身也能被其他应用引用,这就实现了一个去中心化的应用部署集群


    Module Federation 模块联邦



    在 webpack 的构建中,每个构建结果其实都是隔离的,那么它是如何打破这个隔离,实现应用间共享依赖呢?

    关键在于 sharedScope,共享作用域,在 Host 和 Remote 应用之间建立一个可共享的 sharedScope,它包含了所有可共享的依赖,大家都按一定规则往 sharedScope 里获取对应的依赖。

    webpack5将ModuleFederationPlugin这个插件封装在内部



    Module Federation 基于 webpack 的远程容器特性,详见:https://www.webpackjs.com/concepts/module-federation/#dynamic-remote-containers



    ModuleFederation为何而生?

    webpack和npm几乎形成了完美搭档的状态,但前端原本从cdn获取的资源改由打包工具合并到一个包体里带来了致命的更新和部署效率问题。

    在某些需要需要动态更新的场景,这种all in one的打包机制让包体的部署效率大打折扣,这本不是webpack和npm的问题,而是人们天生对web环境需要快速迭代、快递实验的高要求带来的典型场景需求。

    注:externals 本身不能彻底解决动态更新的诉求,只适合于将底层公关依赖包体外链到cdn

    同时webpack随着项目体积日趋庞大,新的问题诸如开发体验差(热更新慢)、包体加大、构建速度慢(node_modules黑洞)等问题也诞生了,此时新生代的开发工具snow和vite以不打包的名义开始蚕食webpack的市场。

    4.png

    他们都利用了浏览器的原生模块化能力esm,跳过webpack的需要的依赖分析和打包流程,在此设计下做到了毫秒级的调试启动。

    5.png

    模块联邦因此诞生了,它的伟大之处在于保持当前前端开发模块化、组件化、工程化的高效率体系下,允许模块独立开发、独立部署,通过 CDN 直接共享,从而挣脱npm包体无法动态更新的桎梏,从而推动整个前端界开发和运行体验上升到一个新高度

    只要有越多的模块能提升到联邦里,本地启动速度将越快!

    远程模块可以作为微模块(模块级别的微前端),是页面级别的微前端的一种补充,因为页面级别的微前端,如 qiankun、无界等,它们的粒度太粗了,有时候需要更细粒度的微前端,例如:组件、函数级别的。这种场景,就可以使用远程模块,来实现微模块的效果。

    容器型微前端

    我们把以single-spa为代表的这一类方案统称为微容器,在single-spa走红之后市面很多基于single-spa二次封装的库如雨后春笋般涌出,典型的代表作如阿里的qiankun,意在解决一些single-spa未解决的问题并让其更适合企业级开发,同时也诞生了很多非singlespa系的框架,如京东的micro-app、腾讯的wujie等,它们的细节实现各有差异,包含js沙箱隔离、css隔离、iframe编排、启用web-component、window代理、接入过程等各个地方的细节也各有千秋,但它们都一个很显著的特点,对应的模块粒度是整个应用,做出的产品可以理解为一种以宏观态的方式来组合多个应用交付给用户使用

    试想一下,你不会极端到以运行时隔离的方式去渲染多个按钮吧?

    模块型微前端

    相较于微容器宏观态的组合应用方式,微模块则可以形容为微观态的组合方式,它的粒度更小,小到可以是一个函数,一个基础的组件,对于开发者来说,引入微模块和引入一个普通的js包没有任何区别,他们在使用上也并无任何区别,但恰恰是这一点!是它和微容器最大差异之一,微模块的使用方式回归到了js语法本身。

    v2-242f0b111242c6300cb96d5bb4527a0f_r.jpg

    这样,微前端就变为:管好webpack生成的模块id,某种意义上来说,hel经过统一的插件处理与加工并提取构建产物的元数据,相当于是对webpack的模块id做了统一管理,但是如何管理好并形成pass服务,这个从工程层面如何解决?

    而且联邦模块天生具有双重身份,即可以是模块消费者,也可以是模块提供者,这让模块联邦应用之间形成了天然的网格关系,模块分发效率、部署效率、共享效率都得到了前所未有的提升!

    7.png

    远程模块可以作为微模块(模块级别的微前端),是页面级别的微前端的一种补充,因为页面级别的微前端,如 qiankun、无界等,它们的粒度太粗了,有时候需要更细粒度的微前端,例如:组件、函数级别的。这种场景,就可以使用远程模块,来实现微模块的效果。


    方案微的定义微前端的定义技术实现使用场景
    MF模块由多个互相独立的模块聚合而成的应用模块本质上是JS代码片段,这种代码片段一般称为chunk。因此,模块的聚合,实际上是chunk的聚合。是一种技术升级的创造性工作,有一定成本,目的是为了让系统具备更强大的能力。
    qiankun/wujie/garfish等应用由多个互相独立的应用聚合而成的应用应用本质上是HTML,而在SPA中,HTML又是main.js进行填充的。因此,应用的聚合,实际上是main.js的聚合。是一种维持现状的保守性工作,成本极小,目的是为了让系统拥有更长久的生命力。



    模块联邦优势

    1. 单体拆分的新解决方案,更小的加载体积,当前子系统已经下载的chunk可以被共享,如果可以复用,下一个子系统将不会再次下载。

      1. 降维打击了 systemjs, 而且可以在运行时拉远程模块和二、三方包,所以,systemjs 应该可以退场了。

      2. mf的远程模块是以webpack打包后的组件形式提供,可以按需在代码任意地方引用,像script标签的引用只适应在全局引用。

        1. 使用script的引用,只适应整个模块的共享,例如一个按钮组件,使用script的话就得单独为改组件分配一个共享域名,而使用mf可以在同一个域名内对一个应用的任意模块进行共享。

    2. 相比过去, externals 无法多版本共存,dll 无法共享模块,MF 完美解决。

    3. 解决了从前用 NPM 公共包方式共享的不便利。不过换来的是本地启动的巨量的 lib server 。

    模块联邦之痛

    webpack 5或者其他工具带来的模块联邦实现真的完美了吗?

    它的确解决了免构建、动态更新、跨项目共享模块的问题,但基于现有的编译时插件化机制去实现,无法规避工具链强绑定,编译时确定才能远程模块消费关系的难题

    • MF虽然能做到依赖共享,但是被共享的lib不能做tree-shaken,也就是说如果共享了一个lodash,那么整个lodash库都会被打包到shared-chunk中。

    • MF需升级到webpack5,就项目改造成本大,且webpack为了支持加载remote模块对runtime做了大量改造,在运行时要做的事情也因此陡然增加,可能会对我们页面的运行时性能造成负面影响。

      • 因此如果要使用mf,就必须对当前的构建工具进行升级,并把现有应用可共享的模块进行提炼,工作量比较大

    • 运行时共享也是一把双刃剑,如何去做版本控制以及控制共享模块的影响是需要去考虑的问题

    • 针对使用react、vue这一类开发框架的应用,使用mf进行模块共享时还需考虑框架的版本,如果是应用处于不同的大版本,那么就有可能导致运行上下文的版本不一致,这也是将应用接入微前端的难点之一。

    image.png

    你需要使用模块联邦这么技术,需要做的前置条件有多重,需要升级整个工具链!而且不同工具链之前的联邦模块是互相不通的!模块的流通性绑定在了你选择的工具链上。

    EMP

    https://github.com/empjs/emp

    EMP 通过 module federation 实现依赖共享,使得依赖不会重新重复(依赖变成全局变量,相同依赖只会留下一个),所以体积会相对 qiankun 更小。

    1.png

    • 跨框架调用实现。qiankun 通过 dom 隔离的方式,使得跨框架实现十分容易,但是不能互相调用,粒度只能渲染在规定的 dom 区域。EMP 实现的跨框架调用粒度到了 function ,而且使用十分方便。

    • 体积方面。qiankun 因为是通过 dom 隔离方式实现,所以依赖共享并不完善,需要依赖于 systemjs,而且共享不方便,导致依赖可能会出现重复,使得出现体积变大。EMP 通过 module federation 实现依赖共享,使得依赖不会重新重复(依赖变成全局变量,相同依赖只会留下一个),所以体积会相对 qiankun 更小。

    2.png

    但是EMP的具体背后原理,暂时没有时间去看源码


    hel-micro 

    hel-micro 自称是:业内首个以sdk的方式支持模块联邦技术的方案,它脱离了工具链的枷锁,回归到js语言本身,接入快速、简单、灵活,极大的降低了模块联邦技术的接入门槛,让不同工具链间的联邦模块可以互认互通,提高了模块的流通性.


    sdk化后,任何技术栈、任何工具链均可无损、无痛接入模块联邦技术。

    1.png

    运行时的模块消费关系

    从工具链回归到js语言本身,意味着模块消费关系从编译时提升到运行时,将极大提高动态载入远程模块的灵活性,为更复杂的业务赋能。

    2.png

    对比依赖工具插件实现的模块联邦,hel-micro从语言层面的实现将对其他模块联邦实现造成降维打击。

    2.png

    hel-micro 优势如下:


    基于 sdk 的远程加载能力,我们可以搭配公共cdn部署远程模块(sdk默认指向unpkg),用户也可以轻松定制自己的模块管控平台,然后重置sdk的请求模块元数据接口即可。

    基于核心层提供的远程加载能力,我们规划了更多的上层框架远程加载适配器,例如 远程web component组件,远程angular组件、远程vue组件、远程react组件(已实现为hel-micro-react,提供钩子函数加载远程react组件)等。

    3.png


    hel-micro实现原理

    通常我们都会在头文件使用import关键字静态导入其他模块,但其实import可以作为函数调用,异步的导入一个模块,并返回一个promise对象

     const mod = await import('./some-mod');

    所以我们可以通过微调模块的加载顺序,来达到为一个模块被其他模块静态导入之前能够为它注入新代码的效果

    5.png

    而这个异步import带来的提前注入效果成为了hel-micro为模块代理对象注入远程运行时代码的关键实现点,让hel-micro可以位用户提供懒加载和预加载两种加载方式。

    4.png

    上图里两个核心接口:libReady接口负责暴露模块,preFetchLib接口负责拉取模块,通过调用接口的行为让每一个模块都表现为提供方或者消费者。运行时依赖分析

    1.jpg


    1. 当调用 helMicro.preFetchLib 时,先拉取元数据,从元数据中获取到入口脚本的 url,然后拉取远程模块入口并执行,最后 helMicro.preFetchLib 将模块返回,代码中就可以直接使用了。

    2. import 代理模块,实际上是从远程模块的缓存中读取模块。因此,必须要等待helMicro.preFetchLib拉取完成后,import 的代理模块才能够获取到远程模块

    hel 的默认拉取元数据的方式,是根据远程模块名称,到 unpkg CDN 对应的 npm 包下,获取元数据 meta_data.json 文件。这个拉取元数据的过程也可以开发者自定义。

    hel-micro通过内部维护的事件总线、模块池、样式池、元数据池四个数据结构,让有多级依赖层次的远程模块得以高效并安全有序的加载。

    7.png


    其中模块池能保证模块不被重复加载并被上层各方调用者重复使用。

    10.png

    元数据-模块的灵魂

    模块的实质是构建产物文件的集合,hel-micro通过提供构建时的插件,收集好产物的网络路径并按sdk规定的协议存储起来,得以后续可以在网络让sdk可以下载并执行所有的远程模块。

    11.png

    双构建机制

    hel-micro使用rollup打包本地可静态导入的代理文件,使用webpack打包远程注入的实际运行代码,来达成可以本地静态导入node_modules里的代理模块对象得到完整的类型提示,让用户能得到像使用本地模块一样地使用远程模块的极致开发体验

    2..jpg

    • 远程模块发布 CDN,在浏览器运行时,调用helMicro.preFetchLib真正拉取代码

    • 代理模块用于开发时的类型提示,上传到 npm。开发时安装并使用该 npm 包,可以获得 TS 类型提示

    • 元数据是一份 json 配置清单,是在远程模块构建完成后,从构架产物中提取生成的。它记录了远程模块的名称、入口脚本路径等信息

    12.png


    内定了4个目录hel_dist,hel_proxy,hel_proxy_es,hel_bundle来承载不同的产物,供package.json配置不同的入口。

    其中hel_proxy,hel_proxy_es目录下的文件是就是我们说到的模块代理对象的入口文件,我们可以看到该文件近乎一个空壳,所以它对模块使用方的打包体积大小影响几乎可以省略不计。



    参考文章:

    hel-micro 模块联邦新革命 https://juejin.cn/post/7138792768234586148

    腾讯开源的 hel 提供了加载远程模块的能力,谈谈它的实现原理 https://zhuanlan.zhihu.com/p/591621582

    关于module Federation的思考 https://geocld.github.io/2021/09/15/module-federation/




    转载本站文章《微前端学习笔记(4):从微前端到微模块之EMP与hel-micro方案探索》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/engineer/Architecture/9063.html