企业级软件系统通常由几个跨职能团队实现的。为了使这些团队能够高效地提供新功能,减少它们之间的协调,这更符合中文的习惯表达。这就需要将系统模块化,使其垂直划分成低耦合的区域,每个团队可以独立负责它的部分,这样更明确。
有这样的高级模块(也称为垂直领域)。例如,它们可以使用相应的文件夹结构实现,或者在一个Monorepo中以多个库的形式实现这些模块。微前端则更进一步,为每个垂直领域独立创建一个应用。这种架构风格提供了多种优势,比如高度的团队自主性,但同时也带来了很多挑战。
本文的第一部分批判性地概述了微前端在单页应用程序应用领域的优缺点。第二部分讨论了如何通过社区项目 Native Federation 来实现这种架构,该项目基于 web 标准并与 Angular CLI 紧密集成。
微前端背后的原因是什么呢?就像微服务一样,微前端也承诺了多种优势,这些优势既包括技术方面的,也包括组织结构方面的。根据多个来源,微前端架构的应用会带来多个较小的应用,这使得测试、性能调整和隔离系统某部分故障变得更加容易。
然而,增强的团队自主性是我作为顾问参与多个项目时采用这种架构风格的主要原因。各个团队不会被其他团队的进度拖慢,可以随时独立部署代码。虽然在许多项目中这可能不是主要问题,但在企业环境中涉及多团队项目时,尤其是在沟通链长且决策过程繁琐的情况下,这种情况很快就会成为项目成功的关键。
团队也可以根据其目标在架构和技术层面上做出最佳决策。在同一应用程序中混合使用多个客户端框架被认为是反模式,应该避免。然而,这可以在长期内帮助过渡到新的技术栈。在企业环境中,我们更关心的是找到那些能比平均技术堆栈具有更长寿命的软件解决方案。
由于微前端技术导致了独立的构建流程,因此将它们与增量构建结合使用,只需重建被修改的应用程序,这种方法在构建时间上有很大的改进潜力。例如,知名的Nx构建系统提供了这一选项。有趣的是,即使不考虑其他因素,比如团队与单个应用的对齐或单独部署,也可以使用此功能。人们正在讨论,是否使用这一引人注目的选项会自动形成微前端架构。
由几个较小的应用程序组成的系统可以提供进一步的组织优势:新成员更容易加入,通过增加更多的微前端来扩展开发也更加容易。团队自主性还能加快发布周期。
要留意的挑战每个架构决策都会有相应的后果需要进行评估,微前端也不例外,也有一系列需要考虑的负面影响。除了上面提到的积极影响之外,也有一系列负面影响需要考虑。
例如,各自开发的微前端可能在界面和用户体验方面有所不同,导致外观上的不一致。此外,加载多个应用会增加需要下载的包的数量,进而拖慢加载速度并增加内存负担。
将应用程序拆分为低耦合的部分可能是常见的最佳实践。然而,通常很难明确界定各个垂直组件的边界,以便作为独立应用实现。此外,虽然一开始看多个小应用似乎简化了实施,但将它们集成到一个整体解决方案会增加额外的复杂性。
这引出了我实际工作中遇到的最大挑战之一:我们正从编译时集成转向运行时集成。这种变化带来严重的后果,因为我们难以预见独立开发和部署的应用程序在运行时相互作用时可能出现的问题。除了技术冲突外,我们还需要注意到,当前的SPA框架并未考虑到这种操作模式。
相反,现代SPA框架,特别是Angular,已被开发来专注于编译时优化。强大的编译器利用类型检查来识别技术上的冲突,并生成适合代码分割优化的高效源代码。此外,Angular领域的CLI提供了一个高度优化的构建流程。用于实现微前端的非标准的用法削弱了这些优化成果。
目前 Angular 并没有正式支持微前端由于上述原因,Angular 团队建议检查是否使用替代方案(如将各个垂直领域实现为单一 Monorepo 并一起编译)更合适。例如,谷歌多年前就采用了这种方法来管理其所有产品和库。
当然,也可以通过一些方法来弥补这里提到的缺点,其中一些方法,比如建立设计系统以帮助实现一致的UI/UX或按需加载各个系统部分,可能是必要的。更多此类补偿策略的细节可以在该调查中找到,该调查调查了超过150名微前端开发者。
所有的架构决策都有其利弊,在决定实施解决方案时,应考虑这些因素。如果评估显示微前端架构在实现你的具体目标上比其他选择更有优势,那么以下章节将为你提供一条清晰的路径,使用Angular实现这种架构模式。
微前端架构与联邦架构模块联邦 是一种流行的实现微前端和共享依赖的技术。它最初随 webpack 5 一起发布,自带工具中立的运行时,并提供在编译时与 rspack、rebuild 和 vite 的集成支持。除了可以使用 vite 开发服务器进行开发外,这些技术目前尚不被 Angular CLI 支持。不过,像 @ng-rsbuild/plugin-nx 和 AnalogJS 这样的社区解决方案很有前景,可以让它们与 Angular 一起使用。Nx 和我的 CLI 插件 提供了无缝的集成。
模块联邦允许一个应用按需加载其他单独构建和部署的应用的部分。这样的应用被称为宿主;集成的应用称为远程模块。
模块联邦,如果允许的话,可以将 Angular 或 RxJS 这样的依赖项在主应用和远程应用之间共享。有几种配置选项可以用来防止版本不一致。由于模块联邦只能在运行时决定共享哪些依赖项,因此无法对共享部分执行树摇。
为了告诉主机关于远程模块及其共享依赖的信息,模块联邦功能在构建过程中会创建一个元数据文件,也就是所谓的远程入口。需要将这个文件加载到主机。
原生联邦为了将联邦这一概念完全从特定的打包器中分离,几年前我开始了一个项目 原生联邦。其 API 接口与模块联邦非常相似。重点在于便携性以及 ECMAScript 模块和 Import Maps 等标准。它在编译时作为一个现有打包器的包装层。在与打包器通信的过程中,它使用可替换的适配器:
直接集成到Angular CLI中则直接利用Angular的ApplicationBuilder,它利用了快速打包工具esbuild,并成为当前一些特性(如部分渲染)的基础。由于其架构,Native Federation还可以移植到其他构建器或CLI未来可能提供的其他创新中。
为了集成使用 Angular 基于 webpack 构建的微前端应用,有一种桥接方案允许将这些远程模块加载到 Native Federation 主应用上。此方案支持逐步采用 CLI 新增的 ApplicationBuilder,并允许在两种类型的 Federation(原生 Federation 和模块 Federation)之间共享依赖项。最近新增的一个特性是支持SSR 和 Hydration,这对于性能关键的应用(如公共门户和网上商店)来说至关重要。
Angular 的原生联邦与 CLI 的 ApplicationBuilder 类似,但其共享依赖的编译模式有所不同。虽然它对符合 Angular 包格式 的包(所有使用 CLI 构建的库都符合这一格式)运行良好,但对于其他库来说可能会带来一些挑战,特别是那些仍然使用 CommonJS 或旧约定提供元数据的较旧库。
Angular 中的原生联邦在设置上,Native Federation(原生联邦)提供了一个示例图。
运行以下命令来添加@angular-architects/native-federation模块到mfe1项目中,并设置端口为4201及类型为remote:ng add @angular-architects/native-federation --project mfe1 --port 4201 --type remote
开关类型定义了应用程序的类型。可能的选项是 remote 、 host 和 dynamic-host 。后者是在应用程序启动时通过配置文件(清单文件)自定义的主机。清单会告知应用程序哪些是远程的位置,并且在部署过程中,该清单可以被另一个清单文件替换。
{
"mfe1" : "http://localhost:4201/remoteEntry.json"
}
在这种情形下,键 mfe1 是主机用来指代微前端的简短名称。值是指远程入口的位置,由上述元数据给出。或者,可以由一个服务来替代清单,该服务告知主机所有已部署远程的位置,并充当微前端注册表的角色。
该配置方案将 Native Federation 构建者委托给 ApplicationBuilder,并生成配置文件 federation.config.js。
const { withNativeFederation, shareAll }
= require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'mfe1',
exposes: {
'./Component': './projects/mfe1/src/app/app.component.ts',
},
shared: {
...shareAll({}),
},
skip: [
'rxjs/ajax',
'rxjs/fetch',
'rxjs/testing',
'rxjs/webSocket',
// 添加不需要在运行时的其他包,
]
});
配置为远程或主机分配一个独特的名称,并定义要共享哪些依赖项。配置使用辅助函数 _shareAll_
,自动添加项目中 _package.json_
指定的所有依赖项。排除列表(skip list)用于选择性地排除某些依赖项或它们的次要入口点,以便不共享它们。
远程代码还定义了可以加载到shell中的暴露的EcmaScript模块(如example中所示的./Component)。具体来说,_暴露节点将模块路径映射到短名称,例如./Component_。
该示意图还向 main.ts 添加了代码以初始化 Native Federation。对于主机,此代码指向 federation 配置文件。
import { initFederation } from '@angular-architects/native-federation'; // 导入初始化联邦的函数
initFederation('federation.manifest.json') // 初始化联邦配置,传入联邦配置文件路径
.catch(err => console.error(err)) // 捕获任何错误并输出到控制台
.then(_ => import('./bootstrap')) // 加载引导程序文件
.catch(err => console.error(err)); // 捕获任何错误并输出到控制台
初始化之后,由生成工具创建的文件 bootstrap.ts 会被加载。该文件包含启动 Angular 的常规代码,例如,当应用使用独立组件时,可以通过 bootstrapApplication 启动应用。
要加载通过远程服务暴露的组件或路由配置,传统的惰性加载与 Native Federation 的 loadRemoteModule 函数结合使用,如下:
import { loadRemoteModule } from '@angular-architects/native-federation';
export const APP_ROUTES: Routes = [
[...]
{
path: '飞行',
loadChildren: () =>
loadRemoteModule('mfe1', './Component').then((m) => m.AppComponent),
},
[...]
];
这里,mfe1 是在 manifest 中定义的键值,而 ./Component 指向远程联邦配置中的相应暴露模块。
更多关于 Native Federation 的信息可以在博客文章和README中找到,后者还提供了一个教程的链接。
最后微前端架构为企业级应用带来了显著的优势,例如提高了团队的自主性和独立部署能力。这些优势使得这种架构风格在多团队的企业环境中尤为吸引人,因为在这些情况下,顺畅的沟通和快速的开发周期变得尤为重要。此外,它们支持逐步迁移到新技术,并通过采用增量构建来优化构建效率。
然而,这些优势也伴随着权衡。微前端架构可能导致不一致的界面和用户体验、加载时间变长以及复杂的应用运行时集成。定义明确的垂直边界和管理跨应用的通信也增加了挑战。此外,像 Angular 这样的框架,设计用于编译时优化,在运行时集成场景中存在局限性。因此,Angular 团队建议采用其他方案,例如将应用程序拆分成多个库并在单个代码库中管理,这更符合 Angular 在类型安全和高效编译方面的优势所在。
模块联邦方案作为一种流行解决方案已出现,通过启用懒加载和依赖共享来应对一些挑战。原生模块联邦在此基础上更进一步,更加注重标准和跨平台性。它提供了一个平滑集成到 Angular CLI 中的功能,以及其高性能的基于 esbuild 的 ApplicationBuilder,并成为诸如 SSR 和服务器端渲染后的 DOM 初始化等高级功能的基础。
与_ANGULARarchitects.io_团队合作,曼弗雷德·施特尔帮助全球的公司建立可维护的Angular架构。他是一名培训师、顾问,并且是谷歌开发者专家,为O'Reilly、《德国Java杂志》、Windows开发者网站以及Heise Developer撰写文章。他还在各种会议上经常演讲。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章