本文的主要话题是京东玲珑平台基于 MF 的组件化共享工作流的成果分享,主要针对目前跨团队之间协作、进行研发资源共享的痛点问题,介绍基于 Webpack Module Federation 的前端组件化共享方案,分享如何打造一个前端组件共享工作流,最后实现前端组件资源共享以及中心化管理的实践过程。

# 1. 业务背景

目前在玲珑产品下有很多个平台,比如说可能有前台、运营平台以及编辑器等。这些平台分别是由不同的团队去进行开发的,并且每个团队都有沉淀自己的一些基础组件或者是业务组件。比如 Button、Select、Modal 等等,其实这些组件都有一些共通的逻辑是可以复用的。

img

基于这种情况,因为每个团队开发的组件资源沉淀在各自的项目当中,导致各个项目的这些组件资源沉淀无法进行集中维护和统一管理。同时,因为这些平台都同属一个产品下,所以有很多共同业务,这样就催生出来很多业务逻辑相同的组件。

img

如上图所示,有一些业务组件水印,需要在前台、运营、编辑器三个平台使用,但由于涉及不同的团队,团队之间共享这些组件变得比较困难和麻烦。可能有的时候求快,就直接把代码从其他团队的项目仓库里面复制粘贴过来,但是这样带来的问题就是可维护性非常差。其次,可以通过 npm 包的方式去进行共享,但是这种方式它的更新链路非常长。因此,我们希望打造一个统一的设计系统,统一整个业务所有平台的设计标准。在这种业务背景下,组件共享以及如何做到,变得尤为重要。

那如何实现?首先是我们需要去做一个组件资源沉淀的统一管理;其次是做一个组件共享;最后需要去统一前后台设计标准。这三个目标我们如何去实现?下文将逐一展开。

# 2. 组件共享方案

组件共享怎么做?这里先大致介绍一下组件共享的几种方案。

img

第一种是大家最熟悉的就是 CV 大法,直接从一个项目复制到另一个项目,速度非常快。但是问题也很明显,可维护性低,同时各个项目就各自多了一套代码,当需求发生变更时需要各自更新。

img

第二种是大家比较熟悉的 NPM 包共享的方式,这种方式简单易上手,也是目前最常见的。但问题是各个项目引用的时候都会打包构建一次,如果项目比较大,可能会导致构建时间很长。同时这种共享方式的更新链路很长,可能出现一个项目都已经发布上线一段时间了,另一个项目还保留着之前的版本。

img

第三种是通过 CDN + Webpack externals 的方式进行组件共享,这种方式其实和 npm 差不多。它有一个优点是可以去抽离一些公共库,但是无法做到按需加载,必须以 script 标签形式提前引入。一般我们使用这种 CDN 的方式,都是通过 on package 一个 npm 包的静态链接这样的形式去加载。其实还是需要发布一个 npm 包,所以它的更新链路和 npm 包的更新链路是一样的。

接下来就是本文的重点,Webpack Module Federation,它是 webpack 5 的一个新特性。

img

那 Module Federation(MF) 方案是如何共享的?如上图所示,这种方案可以让图中的玲珑前台去动态加载运营平台的原子组件,反之,运营平台也可以动态去加载玲珑前台的业务组件。这种方案的优势是依赖的共享资源不需要重复构建,同时可以实现依赖共享。因为 MF 是 webpack 5 的新特性,所以强依赖 webpack 5 。如果你是 webpack 4 及其以下版本,可能你需要先升级到 webpack 5。

# 3. Module Federation(MF)

那 Module Federation 它是什么?以及它是如何进行资源共享的?其实,MF 的设计动机就是为了让多个团队可以共同开发一个或者多个应用,简而言之,就是使应用之间能共享组件开发资源。

img

首先来看下传统的 Single Build 方式。假如 B 团队开发了运营后台,并开发了水印组件,同时 A 团队开发的前台项目也需要使用这些组件,那么,B 团队就先把这些组件打包成一个 npm 包来供 A 团队使用,他们只要安装这个包,然后在本地构建依赖打包、发布、上线就可以了。

img

如果使用 npm 的方式进行共享,它的组件更新传导链路就如上图所示。如果水印组件有优化更新,B 团队重新打包发布了一个新版,然后用在运营平台上,再重新打包,然后发布上线。同时这里需要手动通知 A 团队进行原子组件库 npm 包的更新,A 团队(前台)需要更新版本后再重新打包发布上线。可以看出,组件更新传导链路非常长,如果有更多项目引用到了这个包,那这条链路会继续增加。

那 Module Federation 它是怎么做的?

img

MF 的共享模式,如果 B 团队的后台直接将水印组件以 MF 的方式暴露出去,玲珑前台会直接通过异步 chunk 的方式去动态加载后台的组件资源,同时会共享这些组件所依赖的 React,然后它会把这个 React 打包成一个 Shared Chunk。那 Module Federation 是怎样把这个水印组件打包出来的,以及它的具体形式如何?

img

首先 MF 应用可以导出一些组件,我们把它叫做一个 container。这个 container 它有一个入口文件,一般叫做 remoteEntry.js,并且 remoteEntry.js 是可以自定义的。它的核心内容主要是两个方法,一个是 get 方法,get 你可以理解为它是这个入口文件里面的组件配置表,MF 资源它导出了哪些组件,它就在 get 方法里面去进行配置,然后你就可以通过这个 get 方法拿到对应的组件资源。

另一个是 init 方法,它会去做 shared scope 对象的初始化,将你配置的共享依赖放到 share scope 对象里面去。如图所示,入口文件导出了两个组件,一个是 watermark 水印组件,一个是 button 组件。两个组件都会分别打包成一个 chunk.js,同时它会去依赖一个 react.js。当访问这个水印组件的时候,它就会把水印的 watemark.trunk 以及 react.trunk 一起加载过来。

具体的路径:首先是前台去加载水印组件,它会先去加载 remoteEntry.js,也就是 container 的入口文件。紧接着,它会去拿到调用 get 方法,再去拿水印组件具体的 chunk,同时进行 share scope 的创建,以及将 React 放到它本地的 share scope 里面去。

img

在这个时候,MF 会去比较原子组件所依赖的 React 版本和玲珑前台依赖的 React 版本是否一致,这里是通过 semver 版本工具库的方式去比较的。如果不一致,假如玲珑前台依赖的 React 版本高于水印组件所依赖的 React 版本,默认会使用版本号更高的那个 React。如果一致,它会优先使用水印组件依赖的 React 版本,因为它在执行 init 方法的时候,会去覆盖前台的 share scope,将水印组件依赖的 React 版本覆盖掉前台的资源,所以它加载的就是水印组件所依赖的 react.js。如果说你想要一个固定的版本的话,也可以在配置里面去配。

img

接下来,了解下对应的 webpack 配置。首先一体化平台这里需要新增一个 webpack 5 内置的 ModuleFederationPlugin,然后进行这个插件的配置。如上图右侧所示,name 就是导出的资源名称,filename 就是入口文件,exposes 配置是需要导出的组件以及对应的目录,最后再将需要共享依赖的库放到 shared 里去。因为多个版本 React 的运行时实例对于 React 来说是敏感的,一般情况下只能存在一个版本的 React ,所以这里需要将它放到共享依赖里去。图中此处是简写状态,如果想要让这个 React 是单例的形式去加载的话,后面还可以再加一个 single time 为 true 的配置。

img

那如何使用呢?玲珑前台在加载的时候,也是需要新增 webpack 5 内置的 ModuleFederationPlugin,然后配置 remote 远程资源地址,也就是玲珑运营平台的 remoteEntry.js,这里的 LingCore 可以自定义指定,后面 ling 下划线 core 这个名称对应一体化平台导出来的名称,@符号后再加上资源地址,最后再将 React 共享依赖放到 share 的配置里面去就可以了。水印组件的使用方式和平常使用的普通 npm 包类似,直接 import 进来就可以了,也可以通过 lazy import 的方式加载进来。

img

然后,来看下 MF 共享模式的组件更新传导链路。一体化平台更新了水印组件,只需要重新打包发布即可,玲珑前台因为加载了线上资源,所以会跟着一起更新,这样一来,链路短了很多。所以由此也能看出,我们使用 MF 做一个组件共享,最大的特点就是它能实现实时更新。

img

那 MF 相对于 NPM 共享方式,有哪些优势?首先组件更新链路更短,发布了就实时更新。其次 MF 可以实现依赖共享,在运行时动态去判断加载哪一方的依赖,然后它不需要重复编译,因为本身 MF 资源是已经编译过的代码。如果用 npm 包的话,那每一个用到这个 npm 包的项目,都需要重复构建重复编译。所以 MF 无需重复编译,这样也带来一定程度的编译速度提升。上图所示就是一个老项目在引入 MF 资源之后的编译时间测试,能看出有些许的速度提升。当然引用的 MF 资源越多,这个对比就会越明显。

最后是 MF 的应用场景。首先就是目前的场景,组件共享。其次是它可作为一个编译速度提升的方案。目前市面上 UmiJS ,算是 SSR 的一个框架,它推出了 MFSU 的一个新功能,这个新功能其实就是当你第一次编译过后,后面再重新编译它就会非常快。怎么做到的?就是通过 Module Federation 来做到的。因为之前编译好的那些组件它已经把它 MF 化了,后面就不需要再重复编译,只需要编译你修改的那些文件就可以了。

第三个应用场景就是微前端场景。MF 其实就是天然的一种微前端场景,可以去动态地加载其他的应用,但是它跟国内目前其他的一些框架有一点概念上的不同,比如说 qiankun、icestark 之类的微前端框架。比如 qiankun 这种框架,它是以应用级别去进行页面的聚合。MF 则是以 JS 模块这种方式,粒度更细。所以其实也可以去探索基于 MF 的一种微前端的场景。

# 4. 基于 MF 的组件共享工作流

介绍完 MF ,可以大致了解到它组件共享的模式。我们基于 MF 打造了一套组件共享工作流,做到组件共享与研发资源统一管理。

img

如果使用 MF 的方式来做项目间的组件共享,自然而然就会想到将每一个项目导出的 MF 共享资源集中到一个大池子里面,在这个大池子里面做中心化管理,于是我们就做了一个组件共享平台,让每一个独立的项目都可以发布它的研发资源到这个平台。基于组件共享平台的 MF 共享模式,后台去加载水印组件时就直接去访问了我们的共享平台的资源,前台也是如此,这里和之前的区别只是资源地址变了。另外,基于 MF 平台的组件更新传导链路,其实就是原子组件更新后发布到 MF 平台,依赖 MF 平台资源的两个项目就会实时更新。不再需要本地再重复构建。

img

这个组件共享平台体系由三个部分构成:首先是一个脚手架工具,我们称之为 MF-CLI,主要作用是用于本地开发时导入和导出组件库到我们的平台;然后是组件共享平台,主要是存放组件库以及做一些组件的中心化管理;然后是一个 NGINX 转发服务器,它主要做一些跨域处理、缓存处理以及请求转发的工作,这样可以保证目标网站加载资源时是最新的状态。

接下来,看一下组件共享平台是如何工作的。

img

首先发布一个组件库。用户可以在自己项目的根目录去执行一个 init 命令,然后我们会去生成 mf.config.js,这个 mf.config.js 需要去配置导出的那些组件,再配置一下 Webpack,然后去执行一个 mf export 命令,这样就可以去发布这个组件库。同时组件共享平台会去创建一个组件库,生成一个 OSS 资源,用户就可以通过 NGINX 服务器绑定的对应域名去访问到我们的资源。

那使用资源的时候是怎么做的?首先也是去用户项目的根目录去执行一个 init / import 这个组件库的名称,会拿到对应组件库的配置,之后回来生成 mf.config.js。然后再把远程的组件配置写到 mf.config.js 里面去,再进行一个 webpack 配置,最后你就可以在本地进行开发。当开发去访问这个远程资源的时候,NGINX 服务器在做跨域和缓存的处理,再把相关的资源返回给前端,这就是一个整体的流程。

接着分享下我们工作流里导出一个组件库的流程。

img

首先导出组件库在执行 init 命令的时候,它会去生成一个 mf.config.js。在这里需要去定义一个 MF 应用的名称,以及对应的 ModuleFederationPlugin 的配置,需要写在 MF 的字段里面。然后还需要配置本地的编译命令以及对应的输出目录,同时再修改一下 webpack 配置,添加一个 ModuleFederationPlugin 就可以了。

下一步就是去执行 mf export 命令。

img

当我们去执行 mf export 的时候主要是做了三件事,首先是执行配置的 build 命令,打包编译当前项目,然后基于导出配置去生成一个 TS 声明文件,最后生成 Storybook 或是 Markdown 文档。这个在执行 init 命令的时候,可以自由去选择。把这三者结合起来就生成 MF 静态资源,然后将之上传至 MF 平台。这就是导出组件库的一个流程。

我们在使用组件库的时候也是去执行 init 或者 input 命令,拿到对应的组件库,然后去生成一个 mf.config.js,把对应的远程的配置帮你写进去,以及对应的 share 配置也会帮你写进去。再修改一下 webpack 配置,就可以在本地进行开发。所以整体的流程对用户项目的侵入性是非常低的。

img

再看一下 init 的时候,它中间还有一步可能会去下载和导入这个声明文件,以及生成一个初始文档。在导入声明文件以及生成这个初始文档的时候,会在本地项目增加一些文件。总体来说,这个方案是比较轻量的,不会对本地的项目有太多的侵入性。

关于组件文档生成,我们推荐使用 Storybook 来写组件使用文档以及组件预览,脚手架工具会自动根据本地 Storybook 配置去寻找导出组件的 Storybook 并打包编译。如果本地没有 Storybook 配置,你也可以选择使用 Markdown 作为组件文档。

img

因为 MF 更新是实时更新,只要发布了就会更新,我们也提供版本化的功能,类似 NPM 的版本流程,但区别是无需更新版本后重装依赖。有时线上的某个 MF 资源非常重要,为了确保发布之后不会出现问题,可以打开发布控制功能,它可以限制团队里的成员随意发布 MF 资源。

img img

如果说你的组件库打开了这个发布控制开关,在你发布这个组件库的时候,它不会直接去覆盖线上的资源,会先去生成一个测试的 remote,之后它会去消息通知到每一个使用组件库的使用方。使用方拿到这个测试的 remote 就可以根据本地的环境配置,去配置测试环境的 remote,如果测试成功,就可以回到平台进行点击发布上线的按钮,然后去覆盖线上的 remote。为了确保万无一失,我们在覆盖线上 remote 的时候会去生成一个发布记录,这个发布记录你可以将它回滚,如果你发上去后发现有问题,可以拿以前的发布记录再给它回滚一次,它就会把以前的发布内容回滚到线上。

我们已经基于这套工作流,成功的使用了 MF 。我们的前台项目使用了原子组件库、以及业务组件库,这些组件库都是非前台团队开发的。

img

从这个图里可以看出后台团队开发的原子组件库应用到了前台,以及这里的业务组件库用在了 3 个平台上,这都是通过访问组件共享平台的资源实现的。我们的前台、运营平台、编辑器都使用到了业务组件库的组件,然后前台和运营平台都使用了同一套的原子组件库。这样的话整个设计标准进行了统一。

# 5. 总结与展望

最后,我们打造了这样一套组件共享工作流,它对我们团队来说有些什么收益以及对未来的展望呢?

img

首先,通过实现基于 MF 的组件化共享工作流,我们搭建起一个组件共享平台,沉淀了不同团队的研发资源,同时组件研发负责人通过该平台进行中心化管理,可以做到组件检索、版本管理、发布控制、资源共享、权限控制等这些操作。同时基于这套工作流我们实现了高效的组件共享协作模式。

img

旧的流程我们可能直接 copy 代码或是用 npm 包的方式去引入,这样带来的问题就是组件更新链路非常长或者是非常难维护。新的流程我们就直接进到 MF 平台去寻找所需的组件,如果有的话直接 input 到本地的项目,就可以去进行开发和预览了。当它更新的时候,只需要更新对应的组件库就可以了。

当然,目前该平台还存在一些问题:首先,组件使用上,用户只能去平台进行检索查找,为了更加方便开发者去使用以及提升整个平台的生态,我们后续会开发一个 VSCode 插件,方便大家更好地使用平台组件。其次,目前 MF 是基于 webpack 5 这个脚手架工具实现的,之后会考虑其他打包工具的适配,因为有些项目可能是用 rollup 或者是 vite 去开发的,当然目前社区上已经有一些插件实了 rollup 或者是 vite 的 MF 的适配。最后就是我们也尝试在一个微前端项目中使用 MF 组件,不过也遇到了一些问题,大体上 MF 是可以替代目前的微前端框架的,我们也会尝试去探索基于 MF 的微前端框架,比如做一些像是沙箱隔离、样式隔离等。

最后一句话结束本文的分享,“用 MF 的最好时机,就是现在”。