实施微前端——发展遗留代码库的途径下一步是什么?

2025-06-07

实现微前端——遗留代码库的演进之路

下一步是什么?

微前端是扩展大型客户端应用程序的现在和未来。为什么要实现这种架构风格?让我们深入探讨一下我和我的团队采用它的原因。

大规模工作是现代软件工程的主要目标之一。微前端是一种架构风格,也是许多组织为解决这一问题而采用的技术。一般来说,使用这种方法的动机与我们在“后端”领域选择微服务的动机基本相同:将单体代码库分解成多个微应用,每个微应用由独立的团队开发,并负责项目这部分内容的整个生命周期。

这种方法的主要优势在于改进开发流程和开发者体验,因为每个团队都专注于较小的范围,并且可以独立部署新功能,并快速适应变化。换句话说,微前端继承了微服务众所周知的优势,例如开发敏捷性、技术自由度和精准部署。

由于自动化是成功实施的关键要求之一,我们为这种高度封装的业务规则和每个团队的高度灵活性付出了更多努力,从而增加了基础设施的投入。现代云技术极大地简化了这项工作,可以被视为这种架构风格的赋能技术。尽管如此,分布式系统本质上比单体系统更复杂,因为我们必须考虑编排、分布式故障、可观察性和自动化部署等方面。

目标与原则

就本文而言,我们可以将初始架构视为一个经典的客户端-服务器应用程序,其中 PHP 服务器负责生成 HTML 页面,JavaScript 客户端代码负责使这些页面具有交互性。我们需要改进用户体验,使用户界面响应更快、更像应用程序。我们还需要提升开发人员的体验,因为维护这个遗留代码库已经变得非常繁重,而且团队无法快速适应不断变化的需求。此次架构迁移的最终目标是提升用户体验、实现用户界面的现代化并加快开发流程,我们将通过以下目标来实现这一目标:

  • 独立构件:团队应该能够独立部署其前端应用程序,并将对其他服务的影响降至最低。其他团队应该将其他微前端视为黑匣子。
  • 更易于维护:每个微前端模块都应该独立开发,开发人员在开发功能或调试时认知负担较低。每个微前端都有独立且解耦的代码库,每个微前端都代表一个业务子域。因此,开发人员更容易成为(子)领域专家。
  • 自治团队:每个团队应负责为其模块做出正确的选择。
  • 技术独立性:遗留框架和工具应该与现代技术共存,从而可以轻松地发展代码库。
  • 可扩展开发:每个微前端应该彼此独立,以便每个团队可以按照自己的节奏部署,从而提高开发速度。

挑战

正如软件工程中经常发生的那样,每个选择都需要付出代价,或者至少需要考虑一些方面。选择微前端架构需要面对这些挑战。

  • 父子集成:微前端架构涉及加载不同的模块来组成应用程序的 UI。这需要定义已加载微前端之间的通信方式,以确保性能、与单体应用预期一致的用户体验,以及模块之间的低耦合度。许多实现策略都涉及创建一个应用外壳 (App Shell),作为微前端的协调器。
  • 运营复杂性:微前端应用需要为所有团队创建和管理独立的基础设施。自动化是实现系统可扩展性的关键环节。
  • 一致的用户体验:子应用应使用与父应用相同的设计系统。这在旧版应用的升级过程中尤其困难,因为无法共享相同的 UI 组件和 CSS 库。

执行

首先,让我们简单描述一下应用领域,这有助于理解我们的选择。

Nuvola 是一款网络应用程序,提供一系列服务,全面满足学校从教学到行政管理的需求。教师使用该软件进行教学管理,添加课程主题、作业、成绩、缺勤记录等。家长和学生的主要目的是查看教师输入的数据。最后,学校秘书处使用它来处理行政事务,例如文件登记、沟通、人事管理等。

其核心思想是运用“绞杀模式”,通过将每个子域替换为微前端,逐步淘汰原有的应用程序。结合微前端架构,该模式可以让前端团队独立部署每个应用程序模块,并提高开发的并行度。

图片描述

我们遵循微前端决策框架 作为顶层架构选择的指南。以下是我们决策的简要总结。

  1. 定义:垂直分割。每个页面显示一个微前端,并有一个负责切换微前端的编排层。我们为每个用户类型指定一个子域名,因此每个微前端都是一个服务于单一类型用户的微应用。
  2. 组成:客户端。一小层 JavaScript 代码负责在页面上加载微前端。在我们的架构中,微前端之间的切换属于剩余用例,因为它只能发生在具有不同角色的用户身上,例如同一所学校的老师和家长。
  3. 路由:客户端。由于我们在客户端构建微前端,因此将路由责任委托给相同的 JavaScript 代码是最佳选择。
  4. 通信:本地存储。之前的选择旨在加强封装,并最大限度地减少微前端之间的通信需求,因此我们唯一需要共享的就是身份验证。在未来的开发中,如果模块需要共享更多内容,我们可能会依赖一个松散的通信系统,例如事件发射器。

图片描述

我们选择通过 JavaScript 的运行时集成来连接父级和子级,因为这是最灵活的方法之一。每个微前端都公开一个入口点和一个渲染函数。应用外壳知道可用微前端的入口点列表,并负责将它们加载到页面上。

<html>
  <head>
    <title>Parent application</title>
  </head>
  <body>
    <script>
      const microFrontendEntries = {
        "/pupils-area": "https://pupils.frontend.madisoft.it/pupils.js",
        "/teachers-area": "https://teacher.frontend.madisoft.it/teacher.js",
        "/administrative-area":
          "https://administrative.frontend.madisoft.it/administrative.js",
      }
      const microFrontendScript = document.createElement("script")
      microFrontendScript.src = microFrontendEntries[window.location.pathname]
      document.body.appendChild(microFrontendScript)
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

此示例是应用外壳的基本实现。微前端在页面上加载后,会渲染相应的内容。在我们的配置中,应用外壳不渲染任何内容:它唯一的职责是加载和编排微前端。

完整披露:在撰写本文时,每个微前端都由单独的 HTML 页面提供服务,服务器负责将用户重定向到相应的微前端 HTML。未来的版本将采用之前描述的实现方式。

部署管道

AWS CodePipeline 协调管道步骤。

  1. 新的提交被推送到存储库主分支,触发部署。
  2. AWS CodeBuild 从存储库克隆最新版本的代码,并运行构建命令来安装依赖项、转译和压缩代码。构建步骤会生成静态资产:JS、SVG、CSS 和 HTML 文件。
  3. 构建输出被传输到 S3 存储桶。
  4. AWS CloudFront 为父应用程序提供静态资产。

图片描述

关于资源缓存的补充说明

一种用于服务静态资产的常用性能优化技术是设置较长的缓存过期时间,并通过包含内容哈希来生成动态文件名。当连接微前端时,它们的入口点必须是稳定的,不能使用内容哈希算法动态生成。因此,我们采用的解决方案是生成一个清单文件,将微前端名称与入口点的动态名称进行映射。清单文件绝不能被缓存,或者最多可以保存在共享缓存(CDN)中,我们可以在每次部署时将其失效。

缓存已在我们的 S3 源设置 Cache-Control 标头上进行了适当配置。

  • 清单文件:Cache-Control: max-age=0, s-max-age=2592000// 共享 1 个月
  • 所有其他资产:Cache-Control: max-age=31536000// 1年

Webpack 模块联合

2020年10月10日,Webpack 5 正式发布。我们已经基于之前提供的代码,发布了我们的第一个微前端解决方案版本。此版本附带了 Module Federation 插件,该插件以非常有效的方式抽象了父子组件的集成。作为 Wepack 插件,我们刚刚将 Webpack 升级到 5 版本,并准备使用它。

简而言之,模块联合允许为每个模块分配主机或远程的角色。

  • 远程模块可以被视为独立的,并公开一个或多个入口点。
  • 主机模块是知道远程模块入口点并因此可以使用它们的容器。

还有很多事情要做,但这是核心概念。例如,模块联合允许每个模块同时作为远程和主机,或者可以处理共享库,避免用户在页面上依赖多个微前端时多次下载共享库。

这是远程模块配置的示例。

const { ModuleFederationPlugin } = require("webpack").container
const deps = require("./package.json").dependencies

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "RemoteApp",
      filename: "remoteEntry.js",
      exposes: {
        "./renderMFE": "./src/index",
      },
      shared: {
        react: {
          requiredVersion: deps.react,
          singleton: true,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
  ],
}
Enter fullscreen mode Exit fullscreen mode

此配置为名为 RemoteApp 的模块声明了一个远程入口,该模块公开了一个 renderMFE 函数。此外,它还声明了两个共享库:react 和 react-dom。

在微前端之间共享某些内容似乎适得其反,相反,避免使用多框架方法可以使项目更具凝聚力和可维护性。然而,我们知道这是一个非常宝贵的可能性,因为在后续的迁移过程中,我们可能需要它来允许 jQuery 和 React 在短时间内共存,以完成代码库的迁移。

下一步是什么?

在撰写本文时,我们已经完成了家长和学生专区的迁移,并且我们注意到用户对此版块的使用率有所提升。对我们来说,这意味着此次更新受到了广泛好评,并且至少修复了一些导致其用户体验不佳的可用性问题。其次,由于采用了 API 模型,与在主服务器上生成 HTML 相比,我们的云架构成本相对降低。基础设施成本的绝对值增长主要有两个原因。首先,我们的用户群在不断增长,越来越多的学校选择使用 Nuvola 作为管理软件和电子注册系统。其次,新冠疫情的爆发也促进了我们软件的使用率的提升,该软件已成为远程学习期间与学校沟通的官方软件解决方案。

鉴于取得的优异成果,我们已经开始使用已实施的相同原则迁移软件的其他部分。Webpack 模块联合无疑将在此过程中发挥关键作用,并用于实现更简单、更直接的编排。

感谢您的帮助,我们将在未来的未来中实现这一目标。继续继续写博客。

作者:Mirco Bellagamba,软件工程师,前端专家。

文章来源:https://dev.to/mbellagamba/implementing-micro-frontends-a-path-to-evolve-legacy-codebases-4e37
PREV
React 测试库食谱 - 入门
NEXT
JavaScript 中的一切都是对象吗?