基于 MF 的组件化共享工作流

2022/11/10 18:00
阅读数 1.5K

前端研发资源沉淀以及共享,是每一个前端技术团队都需要做的事情。针对目前跨团队之间协作、进行研发资源共享的痛点问题,我们提出了一个基于 Webpack Module Federation 的前端组件化共享方案。通过打造一个前端组件共享工作流,实现前端组件资源共享以及中心化管理。

1. 业务研发背景

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

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

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

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

2. 组件共享方案

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

2.1 CV 大法

  • 直接从一个项目复制到另一个项目,速度非常快;
  • 可维护性低,各个项目各自多一套代码;
  • 当需求发生变更时需要各自更新。

2.2 NPM

  • 简单易上手;
  • 各个项目引用的时候都会打包构建一次;
  • 更新链路很长。

2.3 CDN + Webpack externals

  • 可以去抽离一些公共库,但无法做到按需加载;
  • 一般使用 unpkg 的方式,和 NPM 共享类似。

2.4 Webpack Module Federation

  • 依赖的共享资源不需要重复构建;
  • 可以实现依赖共享;
  • webpack 5 新特性,需升级。

3. Module Federation(MF)

The motivation for Module Federation is developing one or more applications with multiple teams.        —— Zack Jackson,MF 主要开发者。那 Module Federation 它是什么?以及它是如何进行资源共享的?其实,MF 的设计动机就是为了让多个团队可以共同开发一个或者多个应用,简而言之,就是使应用之间能共享组件开发资源。

3.1 Single Build

首先来看下传统的 Single Build 方式。

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

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

3.1 Module Federation 共享

MF 的共享模式,如果运营平台将水印组件以 MF 的方式暴露出去,羚珑前台直接通过异步 chunk 的方式去动态加载后台的组件资源,同时会共享这些组件所依赖的 React,然后它会把这个 React 打包成一个 shared chunk。

3.2 MF Container

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

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

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

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

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

那如何使用呢?羚珑前台在加载的时候,也是需要新增 webpack 5 内置的 ModuleFederationPlugin,然后配置 remote 远程资源地址,也就是运营平台的 remoteEntry.js

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

3.3 MF VS NPM

那 MF 相对于 NPM 共享方式,有哪些优势?

  • 组件更新链路更短,实时更新
  • 可以实现依赖共享,在运行时动态去判断加载哪一方的依赖
  • 不需要重复编译,因为本身 MF 资源是已经编译过的代码

3.4 MF 应用场景

  • 组件共享

    动态加载其他应用的组件,实现不同应用之间的组件共享。

  • Umi MFSU

    MF 可作为一个编译速度提升的方案。目前前端社区里的 UmiJS ,推出了 MFSU 的一个新功能,这个新功能其实就是当你第一次编译过后,后面再重新编译它就会非常快。怎么做到的?就是通过 Module Federation 来做到的。因为之前编译好的那些组件它已经把它 MF 化了,后面就不需要再重复编译,只需要编译你修改的那些文件就可以了。

  • 微前端

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

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

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

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

4.1 组件共享平台

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

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

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

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

4.2 导出组件库

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

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

# 导出组件库
mf export
# 添加发版信息
mf export -m 'xxx'

执行 mf export 的时候主要是做了三件事:

  • 首先是执行用户配置的 build 命令,打包编译当前项目;
  • 基于导出配置去生成一个 TS 声明文件,用于开发时导入组件库类型声明;
  • 最后编译 Storybook 或是 Markdown 文档,用于组件库文档展示。

把这三者结合起来就生成 MF 静态资源,然后将之上传至 MF 平台,完成组件库的发布。

4.3 使用组件库

# 导入 xxx 组件库
mf import xxx

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

4.4 组件文档

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

4.5 发布控制

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

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

4.5 线上实践

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

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

5. 总结与展望

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

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

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

当然,目前该平台还存在一些问题:

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

最后,目前整套工作流是比较通用化的,如果我们对开发组件时加入一些开发上约定的规范,并且实现一套通用的组件加载机制,这样其实就已经隐约摸索到了一套微组件的运行模式。微组件对于某些多人参与以及协同的大型应用是一个非常不错的架构方向。


本文分享自微信公众号 - 凹凸实验室(AOTULabs)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部