查看原文
其他

【第3033期】@钟正楷:基于微模块构建大型 Web 前端应用

@w3c 中国 前端早读课 2023-08-22

前言

本文主要内容是介绍了微模块的概念和实践。微模块是一种支持模块独立开发、独立部署,并在多个项目间共享的技术方案。相比于传统的 NPM 共享方式,微模块能够提供更高效的模块分发效率、分离编译和构建的提速、以及减小宿主包体积的优势。

还介绍了微模块的发展历程,从使用 CDN 和 UMD 的方式开始,到模块联邦的概念的提出,以及将微模块 SDK 化的方案。同时,文章还探讨了微模块在大型 Web 前端项目设计和架构中的应用,包括基于模块的拆分层级、开发和运行模式、平台化和纯 CDN 架构等方面。

总的来说,主要强调了微模块作为一种新的模块化开发方式的优势和实践,以及其在大型前端项目中的应用。

今日前端早读课文章由 @钟正楷分享,公号:W3C 中国授权(@W3C 资讯)

正文从这开始~~

对基于微模块构建⼤型 Web 前端应⽤进行了介绍,包括微模块是什么、模块联邦的困境与解法,以及基于 hel 微模块的⼤型 Web 前端项⽬设计与架构。

【第2839期】模块联邦在微前端架构中的实践

大家好,我是来自腾讯 PCG 的钟振凯,然后也是一个开源社区的活跃者,我在我们公司同时也担任那个微前端领域的一个接口负责人。那微前端我们现在主要分容器方向、模块方向和小程序方向,那今天我主要是带来这个模块方向的一个实践,然后希望会后有兴趣一起讨论,同学可以接触更多的了解。

目录主要分两个,我们会说到微模块的进化,然后会说我们基于我们的一个现有的一个成品,就还有微模块的一个项目设计和架构。微模块我们把它定义一下,其实就是一种支持模块独立开发、独立部署,然后并在多个项目间运行式共享的一种技术方案。那相比我们的 NPM 共享,其实 NPM 我们可以看到有个安装和构建的过程,然后在微模块的这个体系下,我们可以看到我们这个黄色的这一块是一个远程模块,它在项目里它可能只是一个占位符,它不参与构建,那其实它会带来更高效的一个模块分发效力,然后分离编译和构建提速,然后宿主的包体积减小,那这三个是很大的益处。

那分发效率这一块可以点一下,就是就是我们一相当于是只有一次发版行为,然后所有的引用方其实就是一个免安装的过程,所以你相当于是只发版一次,然后是全局就可以更新了。那我们再说一下这三种模块方式,在我们模块联邦之前,那个微模块有一个就是 CDN 的 UMD 的方式,那其实那排除 NPM 左侧我们看到的这个安装和构建的这个流程,我们看到我们可其实很早就在用微模块这种方式了,那在构建体系工具下其实就是一个模块,然后你配一个 external,然后指向 CDN。那 external 呢?无论什么什么工具现在都支持,那 webpack white 或者其他的构建工具,那都有这种特性。

那在这种模式下,其实我们就已经在把微模块用起来了,但它带来的一个问题就是它这个模块的抽象的复杂度会很高,然后你的这个模块的独立性必须是很独立,然后你要你从你项目里有一块代码要共享的时候,你要单独的把它分离到另一个项目里,然后走构建工具,然后发版。

同时它有个依赖时序问题,那比如我们有一个 UI 库,它前置依赖是 moment 或者对接时,然后你必须在首屏上,你需要 script 相应 moment 在意你的这个 UI 库,那否则你会很容易报错。那所以在两年前 we 派股就提出了一个联邦模块,联邦那个概念,并且对它做了一个实践,就是相当于是我们任意一个应用都是一个容器,那我可以把我应用里的一部分模块直接共享出去,我也可以引入另一个应用里的一部分模块,那其实它就是任何一个应用都有一个,它既是消费者又是提供者的这样一个角色。

双重角色,那我们可以看到,嗯,在这种模式下模块联邦其实相对于微模块那个概念,其实它就多了一层,它不只是在应用间共享,而是直接跨应用共享。就是我可以基于现有的应用直接拎一部分出去,然后随着主项目一起部署,然后一起发版,不需要单独去搞一个工程,然后单独发 NPM 或者发别的。那所以它这个模块的供需关系会很便捷。那你到一定的时候,你再抽到独立的包都可以,然后它的模块的依赖会更稳定。其实它内部的编译的那个有一个依赖图谱,它就锁定了我依赖哪些东西,就不需要你 external,你自己去维护那些,我依赖谁依赖谁。但是模块联邦它真的很完美。就是虽然社区现在大家都很火,所以我们看两个灵魂拷问,它现在有个困境,就是它会借助插件去实现,那无论是 white 的对应的 federation plugin,或者说 white hope 的 modifaceration,或者其他的对等的实现有很多,然后都会有一份这样一个配置文件,那就声明你要暴露的一个对象目录或者你要引用的远程模块在哪里。

然后其实它就产生了一个工具链强绑定的问题,然后有一个构建是远程生命模块的一个不灵活的问题,那如果你遇到一些更灵活的低代码场景,你需要动态的根据后台配置拉不同的模块聚合,其实这个啊,插件这一层就已经锁定了,不能这么搞。同时如果你的老项目是 WEX 4 Web X3,那你的整个公司很多项目你都要面临着升级,你要去强升到 WEPEC 5,那如果有些老的新的项目它用的是 white 构建的,那其实 Wifi 5 和 white 之间的模块怎么互通也是个问题,那所以解决这个困境的方法那也比较简单,就是我们将它 SDK 化,那我们内部有个实践 health Micro,那我们就是把它定义为一种与构建工具无关,然后基于模块构建产物元数据提取而实现的这个模块联邦 SDK 化的一个方案。

那么在这个方案上我们看到模块消费方,那他只需要引 SDK,然后拉模块,然后模块提供方,我们只需要用引一个 SDK 暴露模块,然后模块你可以部署在 CDN,也可以部署在其他任何地方,无所谓,然后在我们的元数据提取这个流程其实主要就是把你本地构建的产物通过有两个入口,一个是你的所有的编译产物,一个是 HTMO 首屏的产物。那首屏的产物和变异产物全部提取出来,然后会再加上一些其他的变量,比如你的流水线,谁提交的等等。那些是后置工作核心还是前面两个,一个是本地所有产物提取、递归提取,一个是入口提取,会得到一个 Meta data,那这个 data 你可以推到 CDN,比如 unpack 自动有,或者推到我们自定义平台。

在这个模式下,我们看到上构建关系是这样的,就是我们的代码,我们通过 Wepeg 构建,然后我们构建的包构建的源数据,那推到我们的自定义平台或者 CDN,那然后前端浏览器加载时候先加载元数据,根据元数据描述再加载模块。那同时我们也那个基于 roll up 再构建一份类型入口文件,那类型入口,那推到 NPMO,那你可以使用方,可以安装。

安装后我们可以看到右侧你引的这个 hello Dash,那只是一个代理对象,那其实如果,嗯,你我们不提前注入代,对,不提前注入那个运行时代码的话,这个地方会报错。那这个后面我们会说那个代码注入,这只是可以看到一个效果,那其实它就完美的解决了类型提示问题,跟 NPM,而且无缝集成。嗯, federation 本身社区也在做这个类型,怎么融合起来的一个问题,那其实我们基于 NPM 是我是无缝衔接的,嗯,刚才一直强调了元数据,那我们看元数据是什么?其实元数据很简单,我们可以看到一个元数据包含一个应用和一个版本,那版本每次构建我们可能会都产生一个新的版本。那下面有一个 head set list, body asset list 就代表的我的首屏铺在 HTM 上的入口文件,h,t,m, l 或者那个 CSS 或 GS。那其实我们还有一个 trunk 的和一个所有的,会记录所有的 CSS,所有的 GS,这个就是够递归那个 dist 目录得到的所有的产物。

那其实元数据你记录了首屏的和所有的,同时还记录了你的 index 文件, index 点 html 的有一个入口文件,所以有也有个 g HTM 入口,那我们看在元数据下,因为我们有了所有的这些资源,那其实很容易就做这种样式隔离。那比如我们可以用 mutation observer 这种可以监听非首屏加载的 CSS 资源,那如果我们有一些组件是需要做样式隔离的,那这个时候我们就很容易地在 SDK 层判断。

诶我是不是 shadow 加载?是的话,那铺在这个 document 上的这个资源,我们看到右上角,诶,这能指吗?对,右上角有个红色的点,红色那个就是 1011080 chunk 的 CSS 已经标记了 disable 的,其实它就它的样式就不会再对外面影响。

那内部我们 shadow DOM 那个 shadow root 下面样式已经转移到内部了,所以我们无法控制 webpack 它 runtime 那个文件的对 CSS 的加载行为,但是我们可以通过 mutation observer 拦截住它标记 disable,那同时我们可以把它转移到 shadow DOM 内部,那这些都是有元数据的一个前置条件,之后我们可以做很多的事情,那我们看一下它的加载方式。

刚才说到了一个 Hellodash 加载,那其实我们看到两种,一种是懒加载和预加载,那对应着懒加载的话,其实就是你我们通过 SDK 直接拿这个包,那拉,通过 SDK 拉这个包之后,然后我们再调它的方法,那这个就是你用的时候再拉。那当然我们拉的时候也支持传版本号,那默认你不传就 latest,那传了的话就具体的版本,那就是做到了一个运行时锁定版本的效果。然后我们懒加载的,我们可以看到如果用这种方式做的话,就是我们提前在入口文件这 prefresh 一下,然后再把整个应用的入口文件下沉一层,就是 await load APP,之后就才是你的应用环境的入口,那这个时候你就可以在任用环节任意地方,就像调本地模块一样,调远程模块你不用感知了。

而且这个地方相当于是安了一个 NPM,然后带你对象给你提供那些,那对于接入方是这样的,那对于消费方我们可以看到就是我只需要那个对于消费方,刚才这个是接入方,那对于接入方我们可以看到我们需要暴露模块,我们也用 SDK 来暴露,可以看到有一个 library 那发射模块的事件。然后我们还提供一个 is must APP 这样一个函工具,函数判断你是不是数组,那如果你是数组,你自身加载,比如你调试自己的时候,你自身是自己,那你就加载你的主应用,就相当于你自己也有个入口。那上面我们注掉的一段话就是如果你依赖了其他模块,那你也可以去引其他模块,那这样就会形成左侧的一个关系,就是我们在模块加载的时候,运行时就会是形成一个依赖图谱,那其实就是我看到模块一依赖模块 2,然后加载了模块 3,模块 4,那如果模块一也依赖模块 3,因为在模块一向下那个深转向下转的那个加载路径上已经加载个模块 3 了,所以模块 3 就缓存做了。

原通最接入元数。通过元数据我们还可以做更多的比如构建元义层面的事情,那是所何谓构建元语?就是我们可以通过 script 上标一些我们自定义的一些属性,来控制这个细粒度的控制资源加载的时机。那比如我们通过 data helpend,那表示这是一个非独立加载的时禁用的资源。就是我是主应用,我自己运行,那我可以加载,但是我是子应用,被别人加载我就不加载。

或者说我们通过这个 Hive 1X 这种控制这种可延迟加载的共享模块,那 1X 就是 external,那就表示什么呢?就是我有一个资源,比如 moment,我标记了 1X moment,那我加载这个资源的时候,我先去检查一道 window 下或者全局里有没有,那没有我再加载,有的话就忽略。

那同时如果另一个应用也遵循这个原语规则打包的时候,那其实我们俩相当于是都复用了 moment,那刚才提到了源数据有个双入口加载机制,那在这个地方简单地展现一下,就是我们可以自己调试自己或者自己有个 html 入口,那如果我暴露一部分模块出去的时候,那就是我会把这部分模块分发到其他的站点,那其实也就是 Wepeg 强调的跨站点,那跨应用那个分享模块的效果,那其实就是你的应用里你可以共享一部分出去,那也可以就是自己用自己的。

那我们总结一下,其实终归到底就是插件和 SDK 的一个对决,那插这件插件其实我们就相当于是通过编译层面来达到模块流通,那 SDK 就是我们通过 GS 回归到 GS 本身的一个返璞归真的一个效果,来达到多应用间的模块流通,那其实它带来好处就显而易见了,就是我们接入很快,你的工具链不需要升级,你也不需要关心你是 white Wepeg 还是其他的 Parcel 等等工具。然后我们可以使用更灵活,你运行时锁定版本,然后可以感知到的元数据,可以,可以那个做更好的一些加载方式,然后模块流通性更好。

我们刚才也强调过,那就是相当于你不用感,你不用关心你到底用什么工具构建的,那当然 GSDK 我们可以做很多扩展,那这个是目前我们一个示例,那比如核心层我们提供了样式的管理,那资源的管理、参数的管理,那基于样式。其实大多数时候我们看上面那个例子,如果你不需要那个样式隔离,假设你是基于单一技术栈,或者说单一那个 UI 库构建的,没有样式隔离的话,你直接就引出来用。那只有需要样式隔离这些特性的时候,你可以向上封装,那我们现在已经实现了 react 封装,就相当于是你可以通过钩子函数加载一个远程组件,那这个远程组件其实就是一个基于 it 运行在 schedule DOM 里的远程组件,那分享完这个微模块的进化这一块,那我们下一章就会说基于 Hive 微模块的一个大型 Web 前端项目设计与架构。因为之前做的铺垫就相当于是为我们下面做思考,就是在微模块这个模式下,我们怎么去做微前端。

嗯,我们通常有一个新项目,我们会考虑着先要上一个,要聚合多少,然后项目周期很长,然后跨团队合作,有很多人想着就可能上来一个微前端,或者去找一个社区流行方案。但其实如果你是一个新项目,应该是基于西安模块在容器的原则去做,因为你 99% 你可能不需要搞 10 个技术栈去堆你的应用,你也不需要有那么多技术栈去分裂开,然后去维护你的一个长期迭代的一个大项目,我们只是需要按照模块去拆就好了。如果你万不得已你有一个新的别的团队的接进来的模块,你再用容器去套,那相当于是你在 document 上再铺一个容器,那容器的模式下顶多就是我们有一个相同的模块,它下载一次,然后多次解析,因为它有容器隔离,所以它就在两个容器里分别解析两次,然后运行时是独立的。

那我们在基于先模块原则下,我们模块拆分层级大概是这样一个关系,其实 external 它工作得很好,它特别适合管理这种底层的公共模块。然后我们 SDK 化微模块,这一块就适合管理业务公共模块。然后 NODE modules 本身它也可以管理它一些私应用,私有模块,你不需要把所有的都提升到 external hell,就按需提就好了,因为你全部提出去也是负担。

我们在 Hive 这一块,这个动态化模块,这一块其实我们分三层,就是库、接口,再往上就是组件,再往上就应用,那应用其实就是力度最大的模块了,那它会带自身的一个路由系统,区别就在这里,它会有多个页面。然后这个下面我们会继续说应用层级的划分。那在这种模式下,我们可以先考虑到我们的新业务,按左侧假设是有很多业务,我们可以有一个,先有个按一组多子的模式来搭,然后在第一步我们可能会有很多个仓库,那我们只需要接 SDK,然后把我们的跟老业务共享的这个模块抽出来,然后共享好就可以了。那第二步我们可以在我们现在时间是引入 PMPM Workspace,因为这个 PMPM 的这个磁盘复用率很高,所以就安装速度特别快,而且在这个大仓化下就是也特别容易复用代码。那所以在这一步在流水线这一层我们看到就是可能是一个仓库,但是我们在流水线是按目录编译就好了。

按目录编译后,然后依然的老系统你可以不动共享,然后新系统我们把它全部搭起来,然后这些每一个这些应用是独立的,那它是开发和运行模式是怎样的?我们在开发模式下,我们看它每一个应用都是自带基座的,我们刚才提到了一个一组多子的一个概念,那在自带基座的模式下,我们可以看到开发者基座其实就很薄的一层了,就是管理导航或者登录、信息监控等等。那每一个应用带一个基座,它开发的时候它就自己运行着它自己的,一旦它上线后它不加载,因为它本身是作为子模块加载,那所以当然我们开发至开发的时候,可以把两个应用间联调,那上线后其实就是先阻后子路由激活的这个概念,那我们在大型项目里就是我们约定一级路由是拿来激活子应用的,那在这个子应用下面,我们每个子应用按约定在这个一级路由下注册其他二级路由或者三级路由的相关页面。

那这样的话按这个约定来,相当于说我们有一个应用访问的时候,后台会首先下发首页,然后加载完基座,然后根据你的路由系统,然后再命中具体的子应用,加载后子应用再根据路由系统面积命中它的二级路由或者其他页面。那刚才说到了开发时、运行时共享,那其实在运行时线上的时候,本身每一个应用间也可以共享一些模块,那比如我的应用一、应用 2、应用 3 之间可以相互共享,那其实就是模块联邦的那个概念,但是我们会做更好的抽象,那如果有些上下文是可以脱离这个应用的,所以只可以把它逐步地向下抽,那比如抽到 common comes、 common lives 或者 common types 这样一层层抽,那其实这个本身是和模块联盟不违背的,我们只是强调一个柔性的一个抽取过程。就说你一开始你可以直接在应用间相互共享,那当某一天你觉得它可以再抽一层,你再抽,所以就相比以前的 external n p m 方式,它就会更灵活。

那刚才说到了抽到,嗯,刚才这有一个抽到 common comes common Labs 的过程,其实它就随着基座一起下发了,那这就会涉及到一个首屏的可能会慢的问题,当你抽了越来越多的到基座上的时候,其实首屏下发前端我们的微模块有个换源数据,换了资源后再加载,然后解析模块的过程。

随着这个你的首屏的那个微模块过多之后,会遇到一个啊,前首屏加载慢,那这个时候我们会有一个解决方案,就是首屏你预埋源数据,那你可以在首屏下载 html,是直接把那些源数据就是一个 JSON 直接通后台去拉获取好埋上去。那你也可以预埋版本号,那版本号埋好之后,其实你前端只是把版本号去比一下你的 index DB 或者 local storage,你的版本对不对?如果对的话,直接用你的前端缓存你的那个源数据 JSON,如果,嗯,不对的话,那其实你就去再拉一下也无所谓,因为我们既然它是基础层的元数据,其实它不会变化,不发版频率不会很大,它不像业务一样。

那基于这个源数据我们还可以做这种类似的高频应用预热,那因为我们每一个应用其实都是路由激活加载的,其实你可以在它没有激活的时候,你可以提前的把它拉下来,然后执行模块解析,下载解析,一执行完,其实用户在切过去的时候就几乎秒开的效果就达到了。最后我们说一下平台化和纯 c 纯 CDN 架构,那我们刚才一直强调了源数据这个概念,那其实源数据的管理本身是一个问题,那我们有两种方式,你可以把你的源数据,我们内部会做一个源数据托管平台,那资源在 CDN,源数据在平台,那平台放平台的好处就是你可以做更细粒度的版本控制啊。

举个例子,我们在这个地方我们会看到,诶,我有一个模块,我线上的是一个版本,然后我可能灰度的是一个版本,我个性化的是另一个版本。那这样因为我们是 SDK 加载的,所以 SDK 那会感在前端会感知到当前用户是谁,当前登录站点是什么。比如你按地域区分模块,然后这个时候你就可以通过各种标记来知道你要加载什么版本,后台也可以控制我要下发什么版本,这就非常的容易来做到,那这是平台化。

当然我们如果不想搭这种平台,也可以走纯 CDN 架构,那走纯电商架构,就是你本地构建好你的模块,然后你就嗯 push 到 CDN 上,因为 CDN 除了带产物也带资源。以我们的 unpack 为例, unpack 它是自带那个语义化 CDN 的,你自动拉一个版本,它会自动拉最新版本,那这个版本里我们只要在 package json 里配上 unpack 的入口文件,是你的 Meta json 文件,那其实它自动会拉那个 Meta json,那 Meta JSON 1 拉,那其实你只要给一个 CDN 地址,它默认拉那个 Meta json 后,那就开始解析就加载模块了。

这也是我们在外网模式下,就是你如果是啊,走 CDN 架构就可以这样做,那当然你们还可以做多 CDN 容灾,你可以部署到多个 CDN,那你再配合上 Saturday try 这类的工具,你就可以保证你的模块比它。比如我的域名 a 的某一个 js 加载失败了,对,然后自动帮你切到域名 b,嗯,最后就是一个场景,对吧?就是特别简单,你安装一下 health Micro,然后就可以开始你的微模块之旅了,那这些场景其实都是可以做微模块实践的,那我们今天只是详细介绍了基于做微前端架构的,那其实你不考虑微前端,你任何事都可以接入模块来享受这些优势。比如你想做前后端分离,你想做那个构建提速或者模块热更新,那个性化版本下发这些可能是需要搭一下后台站点托管,因为我们会有一个 html 的一个美元数据里本身也包含一个 html 的一个字符串的那个记录。

谢谢大家,我们现在这套方案本身也开源了,左侧就是你可以扫码 git 了解一下,就是以我们无论是对内还是对外,还是 ToB to c 都有大量使项目在使用,已经是比较成熟的。嗯,有了解的小伙伴。嗯,可以关注一下。左侧是我们项目的二维码,右侧是我个人的二维码,谢谢。

是的,已经完全开源。就是你进这个扫码进去后我们有大量的实例,就是模块的组件的 Voe 的, react 等等,当然还有很多可以等待共建,比如 Anguna 的 Swift, Sony 的等等。就是它本身只提供了一个模块加载流程,你可以基于它可以加载任何远程模块,比如 Web component, Web symbling 都可以。对。

还有就是社区的开发者有没有什么反馈,以及后面有没有标准化的一些。

想法。先说这个反馈,就是可以记 issue,你看到就是有不少的应同事应用起来,然后那个有一些反馈题我们已经解决了,而且这个首页里有我们的一个群的,然后技术交流里面有很多同事,然后有了解更多详细的话也可以入群。嗯,用的人还很多。然后标准化的话,嗯,这个地方标准化我觉得现在考虑有点遥远,但是不妨可以畅想一下,就是如果这个这种方式被更多的人认可,那其实我觉得有一种可能,就是可以内嵌到 window 的标准 API 里。那其实我们只需要约定就是所有的模块的元数据按那个方式走,然后那我们直接可以。然后另一个问题是这个资源托管在哪?比如我们提供一个公共可信的 CDN,就是啊所有网厂商都认的 CDN,然后那这个时候我们可以直接可以在 window 上拉这个模块,然后默认去拉那个大家都认可的 CDN,那这样的话看起来是可以标准化的,当然这只是一个畅想。

最后:hel-micro github:https://github.com/tnfe/hel

关于本文
作者:@w3c 中国
原文:https://mp.weixin.qq.com/s/vCj3tsbYIGVX5O5pRGLk6Q

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存