代码共享与应用架构
多种代码共享方式对比
要理解它,我们需要能够将它与我们迄今为止所接触到的机制进行比较。要做到这一点,我们需要设想一个相当常见的场景。你有一个企业系统,在这个系统中,你有两个应用程序;第一个是面向内部的内容管理系统(CMS),它允许员工将产品信息格式化,以便在网站上显示。第二个应用程序是面向外部的网站,将产品信息显示给客户。内部的 CMS 系统具有预览功能,理想的情况是与客户在网站上看到的信息完全一致。这两个应用程序由两个独立的团队管理,管理层的要求是这两个应用程序共享相同的渲染代码,以便 CMS 应用程序中的预览功能始终与客户网站上的渲染代码同步。
NPM 包
到目前为止,Javascript 生态系统中最常见的分享代码的途径是通过一个 NPM(Node Package Manager)模块。在这种模式下,无论是 CMS 系统还是外部网站将把渲染代码提取到一个新的项目中(可能还有一个新的存储库)。然后,这两个应用程序将删除它们所拥有的任何渲染代码,并将其替换为 NPM 模块中的组件。下面的简单插图显示了这两个应用程序之间的联系和持有渲染组件的 NPM 模块。
这种方法的优势在于它的高度控制和版本化。要更新渲染组件,需要修改、批准代码并发布到 NPM。然后,两个应用程序都需要颠覆他们的依赖版本和自己的版本,然后重新测试和重新部署。这种方法的另一个优点是,使用这个库的两个应用都是反过来作为完整的单元进行版本化和部署的,可能是作为一个 Docker 镜像。如果不进行版本调整和重新部署,渲染组件是不可能改变的。
NPM 包 方式最大的缺点是,使用 npm 包的更新流程繁琐复杂。比如,开发三个管理后台应用项目,将相同的业务子模块抽离成 npm 包方式,这时候,如果要更新该业务子模块的逻辑时,那么需要做以下的流程操作:
- 更新 npm 包版本
- 更新 A 管理系统应用的 npm 包版本
- 发布部署 A 管理系统应用
- 对 B 和 C 管理系统应用循环 2 和 3 步骤
NPM 包 方式带来的第二个痛点,就是构建速度慢。假如一个大型管理应用系统,引入了 n 个可复用的业务子模块,在构建层面来说,相当于将 n 个可复用的业务子模块的代码“复制”到了项目中,构建的时候也需要同步去构建这些业务子模块,这样一来,要构建的体积就大大增加了,构建时长也因此延长,开发体验会越来越不友好,发布效率也会随之降低。
NPM 包 方式的第三的痛点,体现在这样一个场景中。比如我们多个后台管理配置系统,使用了一些相同的资源,比如:统一的 UI 风格、移动端适配功能、通用状态。这时候,如果使用了 npm 包方式,可能给抽离成 template 模板,然后执行命令或者手动去复制一个应用项目模板使用。但这种有个弊端是,如果我们对应用模板的内容更新了,需要同步到实际已经使用的项目的时候,就需要每个实际项目都去代码复制,甚至需要解决冲突之类的。这样一来,不便于我们后续的应用迭代。
微前端
在微前端(Micro-frontend,Micro-FE)模型中,渲染代码再次被提取到一个新的项目中,但代码在运行时要么在客户端,要么在服务器上使用。
为了实现这一点,Rendering Micro-FE 项目将其源代码打包为一个运行时包。流行的 Micro-FE Single SPA 将这些包称为 “包裹”。然后,这些代码会被部署到一个静态的托管服务中,比如亚马逊的 S3。CMS 应用程序和外部网站就会在运行时下载这些代码,并使用它来实现渲染内容。这种方法的优点是可以更新渲染的微前端代码。而不需要任何一个消费应用程序重新部署。此外,由于两个应用程序总是下载最新的版本,因此它们在渲染代码的方式上总是同步的。
具体理解就是,我们可以把复用的业务子模块,放在同一个基站应用之中,来管理和维护,并且暴露出去可以给多个管理系统应用使用。如果业务子模块需要更新逻辑的话,只需要发布部署基站应用,其他管理系统应用并不需要做什么操作,只需要访问时刷新,就可以立即拿到最新的业务子模块逻辑了。
缺点是,就像在 NPM 模型中一样,渲染代码需要被从原始应用程序中提取出来,以使其工作。它还需要 包装的方式,使其能与 Micro-FE 框架一起工作,这可能需要用到 实质性修改。最终,它可能不会像一个普通的工厂一样,最终看起来 React 组件。
另一个缺点是,对渲染代码的更新可能最终会破坏任何一个地方的应用。因此,需要增加额外的监测,以使之成为一种新的应用。这项工作。性能和代码重复是管理的挑战。由于任何一个消费者都可能 已经有 Micro-FE 使用的依赖性。这可能会大大降低负载和 执行时间,因为有更多的代码需要反复处理。由于每个应用程序都有启动开销,启动时间会给一个应用程序带来压力。机器实现第一有意义的涂鸦的能力(页面速度的重要指标)。
减少代码重复的一个变通方法是将常用的包外部化,由消费程序独立加载。然而,这可能会成为一个非常手动的过程,并且可能是危险的,因为有一个高度集中的依赖,常用代码总是在那里。这可能会违背 Micro-FE 的去中心化原则的目的。升级到较新发布的副本是具有挑战性的,因为所有 Micro-FE 需要在完全相同的时间兼容和部署。
Module Federation
通过 Module Federation,无论是 CMS 应用程序还是网站,都可以使用 Webpack 5 ModuleFederationPlugin 简单地暴露其渲染代码。而另一个应用程序则在运行时使用该插件来消耗这些代码。
在这种新的架构中,CMS 应用程序将外部网站指定为 “远程”。然后,它就像导入其他 React 组件一样导入这些组件,并像内部拥有代码一样调用它们。然而,它是在运行时进行的,因此当外部网站部署新版本时,CMS 应用程序会在下次刷新时消耗该新代码。分布式代码的部署 独立地进行,而无需与其他消耗了它。消耗的代码不必改变以使用新部署的分布式的编码。
该模式的优势如下:
- 代码保持原位,对于其中一个应用程序,渲染代码保持原位,不做修改。
- 无框架,只要两个应用程序使用相同的视图框架,就可以使用相同的代码。
- 没有代码加载器,Micro-FE 框架通常与代码加载器结合在一起,比如 SystemJS,它与工程师习惯的 babel 和 Webpack 导入并行工作。导入联合模块的工作就像任何普通的导入一样。它只是碰巧是远程的。与其他方法不同的是,Module Federation 不需要对现有的代码库进行任何改动。没有 Micro-FE 框架的学习曲线。
- 适用于任何 Javascript,当 Micro-FE 框架在 UI Components 上工作时,Module Federation 可以用于任何类型的 Javascript,UI Components,业务逻辑,i18n 字符串等。任何 Javascript 都可以共享。
- 适用于 Javascript 以外的内容,虽然许多框架都非常注重 Javascript 方面,但 Module Federation 可以处理 Webpack 目前能够处理的文件,如图像、JSON 和 CSS。如果你能要求它,它就能被联邦化。
- 通用,Module Federation 可以在任何使用 Javascript 运行时的平台上使用,如浏览器、Node、Electron 或 Web Worker。它也不需要特定的模块类型。许多框架需要使用 SystemJD 或 UMD。Module Federation 将与当前可用的任何类型一起工作,包括 AMD、UMD、CommonJS、SystemJS、window 变量等。
就像任何架构一样,有一些缺点,其中最主要的是链接的运行时性质。就像在 Micro-FE 的例子中,如果没有渲染代码,应用程序是不完整的,渲染代码是在运行时加载的。如果在加载代码时出现问题,那么就会导致中断。此外,共享代码中的错误很容易导致其中一个或两个消费应用程序被关闭。
应用架构
微前端的落地,需要考虑到产品研发与发布的完整生命周期;我们会关注如何保证各个团队的独立开发与灵活的技术栈选配,如何保证代码风格、代码规范的一致性,如何合并多个独立的前端应用,如何在运行时对多个应用进行有效治理,如何保障多应用的体验一致性,如何保障个应用的可测试与可依赖性等方面。通常应用中的每个页面都有一个微前端实例,还有一个容器应用,它有以下功能:呈现常见的页面元素,如页眉和页脚;解决了身份认证和跳转等跨领域问题;在页面上集成多个微前端,并告诉各个微前端该何时何地呈现自己。
实际上所有的微前端框架都面临这两大共性问题。当你解决了这两大问题之后,你的微前端框架的运行时,就已经基本可用了。
- 应用的加载与切换。包括路由的处理、应用加载的处理和应用入口的选择。
- 应用的隔离与通信。这是应用已经加载之后面临的问题,它们包括 JS 的隔离(也就是副作用的隔离)、样式的隔离、也包括父子应用和子子应用之间的通信问题。
具体而言,我们可能从应用组合、应用隔离、应用协调与治理、开发环境等几个方面进行考虑:
-
应用集成:
- 集成时机,在构建时组合,还是在运行时组合;是在服务端组合,还是在客户端组合;
- 应用路由,如何根据 URL 加载/导航到不同的页面,如何根据子应用界面的变化切换 URL;
- 应用加载,确定加载应用的版本,依赖于框架的加载机制,还是采用 AMD 或者 SystemJS 异步加载;
-
应用隔离:
- 应用容错,某个应用的崩溃不应影响到其他应用或容器应用;
- 样式隔离,避免 CSS 相互污染;
- DOM 隔离,避免子应用操作非自身作用域内的结点;
-
应用协调与治理:
- 统一配置与切换,主题,利用 CSS Variables 等方式动态换肤;
- 应用的生命周期,规范化子应用的生命周期,并且在不同生命周期中执行不同的操作;
- 数据共享,子应用间数据共享,父子、子子应用间通信;
- 服务共享,跨应用数据共享与服务调用;
- 组件共享,可能将某个纯界面组件或者业务组件以插件(Plugin)或者部件(Widget)的方式共享出去;提供某些业务逻辑的计算能力;
-
开发环境:
- 跨技术栈支持;
- 统一的构建流程与规范;
- 打桩、埋点与 Hijack;
此外值得一提的是,微前端化本身是为了保证系统的持续集成与快速迭代,那么对于各个子模块与系统本身的可用性与稳定性势必会带来挑战,这就要求我们在设计微前端解决方案时,考虑持续构建的时机与对应的测试方案;除了标准的单元测试、集成测试、端到端测试之外,我们还需要保证模块的依赖一致性与功能模块的可生成性;关于此部分的详细讨论参阅 Web 自动化测试概述。