打破最后的单体架构——微前端
介绍
介绍
JsFoo 2019,班加罗尔
在过去的十年里,AngularJS 一直是“最酷的孩子”之一,它弥补了基于 jQuery 的 Web 开发解决方案所带来的诸多缺陷。许多企业非常乐意使用 AngularJS 构建其现代化的 Web 门户。随着 Web 业务的迅猛发展,AngularJS 也让许多开发人员的工作变得更加轻松。
在 Web 开发的世界里,每个人都很快乐,但是这种快乐并没有持续多久。
JavaScript 和大泥球
尽管人们将大量注意力集中在高级软件架构模式上,但实际上,事实上的标准软件架构却很少被讨论。本文探讨了最常部署的软件架构:大泥球。大泥球是一种随意甚至杂乱的结构化系统。它的组织方式(如果可以这样称呼的话)更多是出于权宜之计而非设计。然而,它经久不衰的流行并不仅仅表明人们对架构的普遍漠视。- http://www.laputan.org/mud/
我们于 2015 年 11 月开始开发 Innovaccer 的医疗保健数据平台;当时,我们的应用程序架构是一个单页应用程序,用 Angular1.3 编写,因为当时 React 在构建复杂的医疗保健产品方面相对较新,团队中的每个人都更精通 AngularJs。此外,当时找到熟悉这项技术的开发人员也相对容易。
我们的应用程序在创建之初就拥有明确的架构。任何卓越的系统都会面临无休止的调整需求冲击,而这些需求会逐步破坏其结构。曾经井然有序的系统最终变得臃肿不堪,因为零碎的增长逐渐导致系统元素以失控的方式蔓延。
这个系统开始显露出明显的无节制增长和反复权宜之计的迹象。
就像我们日渐衰败的系统一样,它陷入了恶性循环。由于系统变得越来越难以理解,维护成本也越来越高,难度也越来越大。遏制软件包熵增的方法是重构。持续的重构可以防止系统陷入混乱状态。
如果这种蔓延持续加剧,系统结构将严重受损,最终不得不被废弃。
对于我们来说,用 React 或 Vue 重写整个前端不是一个选择,特别是在现代 JavaScript 生态系统中,它高度不稳定且趋势不断变化,我们希望创建一个架构,该架构可以不受特定团队用来构建其 Web 界面的前端框架的影响,并提供一个脚手架来包含任何现有的前端框架,或者如果有更好的框架出现,则无需完全破坏现有的应用程序。
为了将现有的单页单体重构为更优雅、更高效、更灵活的架构,我们最终创建了 UI Engine,它解决了构建大规模 JavaScript 应用程序的复杂性。此外,它还灵活而严格地执行了某些基本规则,这些规则是构建高弹性 Web 应用程序的先决条件,这些应用程序对于医疗保健等关键业务部门来说至关重要,并且更易于测试、维护、更改和保护。
简而言之:近年来,得益于 Node.js 的出现,JavaScript 已成为 Web 前端和后端应用程序的“通用语言”。这催生了 Angular、React 和 Vue 等众多优秀项目,它们提升了开发人员的生产力,并支持构建快速、可测试且可扩展的前端应用程序。然而,尽管 Node(以及服务器端 JavaScript)已经存在大量优秀的库、辅助程序和工具,但它们都无法有效解决主要问题——架构。1
该引擎是一个控制反转容器,解决了大规模复杂 JavaScript 应用程序的架构问题。
松散耦合允许您对一个模块进行更改而不影响其他模块。
编写 JavaScript 非常容易——几乎任何人都可以学习并开始使用 JavaScript 或 jQuery、AngularJS、React、Vue 等开发用户界面,然而,困难的部分是编写可维护的 JavaScript。
我们通过将每个 AngularJS 应用程序迁移为 UI Engine 提供的 App Shell 架构内的小型微前端来部署重构的前端应用程序,并且所有网络调用(最初都是从浏览器跨域触发到我们的后端服务)现在都通过在 UI Engine 中注册的 API 网关进行代理。
需要进行更多调整,以及在 Nginx 上启用 HTTP/2 并在 node.js 层使用压缩中间件来压缩所有 JSON 和静态资源;下面是我们在 2018 年 4 月在 staging.innovaccer.com 上进行的首次部署的一些屏幕截图,与我们在 qa.innovaccer.com 上的传统 SinglePage AngularJS 应用程序相比。
微前端
软件架构永远不应该是最终目标,而应该成为达到目的的手段
如今,经济由字节驱动,而在字节经济中,重点是快速将产品推向市场。
在竞争激烈、充满颠覆性的创业十年中,我们看到软件公司逐渐跻身全球市值最高的公司之列,而初创公司也每天都在涌现和消亡。为了生存,我们必须维持并占据相当大的市场份额,我们希望软件工厂能够以最快的速度运转。这些工厂由一群拥有感知能力的人类程序员组成,他们孜孜不倦地开发一个又一个功能,以交付用户故事,而用户故事是软件产品整体结构的重要组成部分。
一开始...
我们拥有古老的单体系统,所有组件都捆绑在一个可部署的单元中。
这很可能是大多数行业所依赖的。然而,需要注意的是,单体系统虽然设计快速,部署简单,但其敏捷性有限,因为即使是微小的更改也需要完全重新部署。此外,我们知道,由于软件系统会随着时间的推移而不断发展,单体系统通常会陷入某种泥潭。例如,许多单体系统采用分层设计,而分层架构相对容易被滥用(例如,绕过某个服务直接访问存储库/数据访问层)。
我们正在开发的应用程序是一个面向公众的大型 Web 应用程序。自产品最初构思以来,我们已确定了一些独立的功能,并创建了微服务来提供这些功能。我们已精简了提供用户界面(即面向公众的 Web 前端)的基本功能。该微服务仅具有一项功能,即提供用户界面。它可以扩展和部署,并独立于复合后端服务。
如果您无法构建整体,那么您凭什么认为微服务就是答案?
从技术角度来说,微服务在今天已经变得非常便宜,计算、存储和网络成本还在快速下降。这种趋势催生了微型、独立的全栈软件的发展,如果运用得当,这可以说是轻量级面向服务架构的演进。
微服务复兴了构建更小、松耦合、可复用的软件这一古老理念,该软件专注于做好一件事,并强调最短的上市时间和最低的成本。同样,需要注意的是,如果做得好,基于服务的架构将为您带来极大的灵活性和敏捷性,因为每个服务都可以单独开发、测试、部署、扩展、升级和重写,尤其是在服务单元通过异步电子消息传递进行解耦的情况下。缺点是软件的复杂性会增加,因为移动元素比滚石还要多。
复杂性仍然存在,你只是将它移到了其他地方。
因此,同样的旧概念只是用远程网络调用取代了所有内存中的函数调用或共享库调用;现在我们可以独立地构建、更改、部署和扩展它们,而独立的团队不必被迫了解不同团队的存在。
当你拥有一个庞大的单体前端,无法简单拆分时,你必须考虑将其缩小。你可以将前端分解成几个独立的部分,由完全不同的团队分别开发。
在实施微服务架构时,您需要保持服务规模的精简。这也适用于前端。如果不这样做,您只能在后端服务上享受微服务带来的好处。一个简单的解决方案是将应用程序拆分为多个独立的前端。
我们有多个团队负责开发不同的应用程序。然而,您还没有完全实现这一点;前端仍然是一个横跨不同后端的单体应用。这意味着,在前端,您仍然会遇到一些在切换到微服务之前遇到的问题。下图展示了当前架构的简化图。
如果没有前端的更新,后端团队就无法交付业务价值,因为没有用户界面的 API 作用不大。后端团队的增多意味着前端团队拥有了更多新的选择,因此集成新功能的压力也更大。
为了弥补这一点,可以扩大前端团队规模,或者让多个团队共同开发同一个项目。由于前端仍然需要一次性部署,团队无法独立工作。变更必须集成到同一个项目中,而且由于变更可能会破坏其他功能,因此需要对整个项目进行测试。这实际上意味着团队不再独立工作。
使用单体式前端,你永远无法获得微服务架构所保证的跨组扩展的灵活性。除了无法扩展之外,还存在后端和前端团队分离的典型开销。每当某个服务的 API 发生重大变更时,前端就必须更新——尤其是在服务中添加功能时,前端也必须更新以确保客户能够使用该功能。
如果你的前端足够小,可以由一个团队维护,并且该团队还负责一个或多个与前端耦合的服务,那么跨团队沟通就不会有开销。但是,由于前端和后端无法独立运行,所以你实际上并没有在实施微服务。
如果您的平台上有多个团队,同时还有多个较小的前端应用程序,那就没有问题了。每个前端都充当一个或多个服务的接口。每个服务都有自己的持久层。这被称为垂直分解。
现在,用前端实现这种架构的主要问题是用户体验。
如今,现代应用程序产品的最终用户认为一家公司就意味着一个网站。
然而,正如我们上面所讨论的,这种方法会成为开发瓶颈,并且无法有效扩展。
我们将讨论一些最流行的前端垂直分解方法,以实现以下目标:
- 团队所有权
- 独立开发
- 独立运行
- 技术不可知论者
- 快速加载
- 原生支持
- 共享基础知识
- 模块化的
- 企业形象
- 流畅的用户交互
基于 Nginx 的硬核路由
如果我们想要将单体前端单页应用程序拆分成多个独立的单页应用程序,并在 Nginx 后面独立运行,我们该怎么做呢?
我们可以将不同的应用程序超链接起来,但是,每个应用程序都需要在其代码中维护类似的基础应用程序模板,以实现品牌标识。
正如您所见,这种方法一开始是没问题的,但是,其中四个非常关键的案例在这里失败了。
已通过 | 失败的 |
---|---|
团队所有权 | 共享基础知识 |
独立开发 | 模块化的 |
独立运行 | 企业形象 |
技术不可知论者 | 流畅的用户界面 |
快速加载 | |
原生支持 |
那么,我们还有什么其他选择?
服务器端包含
我们可以使用另一种有趣的方法来实现这一点,最广为人知的是 Edge Side Includes ESI。
Edge Side Includes 或 ESI 是一种用于边缘级动态 Web 内容组装的小型标记语言。
经过 | 失败的 |
---|---|
团队所有权 | 快速加载 |
独立开发 | 原生支持 |
独立运行 | 流畅的用户界面 |
技术不可知论者 | |
共享基础知识 | |
模块化的 | |
企业形象 |
代码级集成
嗯,这就是我们现有的前端整体的工作方式,我们将多个角度模块进行代码级集成到最终的 SPA 构建中。
经过 | 失败的 |
---|---|
团队所有权 | 快速加载 |
独立开发 | 技术不可知论者 |
原生支持 | 独立运行 |
流畅的用户界面 | |
共享基础知识 | |
模块化的 | |
企业形象 |
显然,我们有一些可以提供帮助的解决方法,但这种方法从长远来看也是不可持续的。
应用外壳
让我们稍微改变一下视角,跳出思维定式
这里有关于这种方法的很好的介绍,它应该设置这个概念的背景。
应用“外壳”是用户界面所需的精简 HTML、CSS 和 JavaScript,离线缓存后可确保用户重复访问时获得即时、可靠的良好性能。这意味着应用外壳无需在用户每次访问时都从网络加载;只需从网络加载必要的内容即可。
这种方法使得我们在第一次访问时能够立即加载应用程序外壳,并且所需的最少量的静态资源会缓存在浏览器上。
现在,我们可以根据用户需求或意图将称为微前端的独立单页应用程序延迟加载到我们的 shell 中。
我们可以通过为每个微前端提供路由信息来实现这一点。
然后为每个微前端提供清单 JSON。
一旦我们加载了应用程序所需的所有资源,我们就可以按照以下方式初始化微前端应用程序:
如果我们在测试用例中评估这种方法:
经过 | 挑战 |
---|---|
团队所有权 | 模块化的 |
独立开发 | 技术不可知论者 |
原生支持 | 共享基础知识 |
流畅的用户界面 | 独立运行 |
超快速加载 | |
企业形象 |
有了这个,App Shell 似乎是解决我们前端问题最合适的方法。
该引擎的设计从一开始就充分利用了应用程序外壳架构。我们通过在浏览器和 Nodejs 层引入称为控制反转(IOC)的设计模式来实现这一点,这有助于我们的应用程序进行依赖注入,而不是直接导入源代码;这种模式有助于我们构建低耦合、高内聚的应用程序。
因此,借助 UI Engine,开发人员可以构建自己的微前端,并且每个应用程序都可以与提供视图级 RESTful API 的服务器部分耦合,或通过 API 网关公开某些下游服务,为在 App Shell 中注册的应用程序提供支持。
UI引擎
松散耦合允许您对一个模块进行更改而不影响其他模块。
Engine 是一个可插入的基于组件的应用程序组合层;它提供了一个明确定义的地方,用于创建、配置和非侵入式地连接应用程序的组件或应用程序的各个部分。
Web 应用程序模块- 一个独立的功能单元,是 Web 应用程序整体结构的一部分。
使用 Engine,您可以专注于编写组件的应用逻辑,并让 Engine 处理引导和连接组件的粘合工作。您只需编写简单的声明式 JavaScript 模块来描述组件的组合方式,Wire 会加载、配置和连接这些组件以创建应用程序,并在稍后进行清理。
Engine 旨在处理现有流行框架之间的连接点,并解决在设计大型复杂 JavaScript Web 应用程序时出现的常见集成问题,从而将整个应用程序与每个应用程序垂直的实现细节分离,从而可以自由地从 Angular、React、Vue、Mithril 等中选择 UI 堆栈。
特征
引擎提供:
- 简单的声明式依赖注入
- 灵活、非侵入式的连接基础设施
- 应用程序生命周期管理
- 强大的核心工具和插件架构,用于集成流行框架和现有代码
- 应用程序外壳架构和可插拔微前端
- 支持浏览器和服务器环境
使用 Engine 构建的应用程序:
- 具有高度的模块化
- 可以轻松进行单元测试,因为它们本质上将应用程序逻辑与应用程序组成分开
- 允许独立于应用程序逻辑重构应用程序结构
- 对 DOM Ready、DOM 查询引擎或 DOM 事件库没有明确的依赖
- 旨在为您提供一种快速且有组织的方式,开始在 PWA shell 中开发微前端
- 鼓励构建更小、松散耦合、可重复使用的软件这一古老理念,该软件只做一件事,并且做好这件事,以加快上市时间并降低变更成本
- 引擎软件包系统允许开发者创建模块化代码,提供其他引擎开发者可以使用的实用工具。这些软件包发布后即可即插即用,使用方式与传统的 npm 软件包非常相似。
- 引擎包系统将所有包集成到引擎项目中,就像代码是引擎本身的一部分一样,并为开发人员提供将其包集成到主机项目所需的所有必要工具
- 安装程序可以展开以作为分布式前端架构运行。
Engine 提供了开箱即用的应用程序架构,可以轻松创建高度可测试、可扩展、松散耦合且易于维护的大型前端 Web 应用程序作为一组独立的微前端。
Engine 被开发为一个非常轻量且优雅的层,这使得我们将现有的前端单体应用(Angular1.x)迁移到可单独安装的包中。现在,每个包都可以单独安装到 Engine 中;每个包都可以为该引擎应用程序提供完整的前端以及 Rest-API,从而形成一个即插即用的应用程序框架。
如果 Engine 中的任何模块依赖于 Engine 中的任何其他功能模块,那么就不会存在明确的源代码级别依赖关系,但我们利用依赖注入来使用特定模块公开的功能。
下面附加的代码片段描述了如何在 Engine 中定义包。
浏览器
import { skeletonEngine } from 'skeletonpwa';
const mdrun = function(dashboard, router) {
router.addRoute({
action: () => dashboard('dashboardspage', 'dashboards', app),
path: '/dashboards',
name: 'ingraph'
});
};
skeletonEngine.shell('datashop').defineConfig(mdrun,['dashboard', 'router']);
Node.js
const engine = require('engine-core');
const Module = engine.Module;
const Dashboards = new Module('ingraph');// Defining the Package
const ESI = require('nodesi').middleware;
/*
* All engine packages require registration
* Dependency injection is used to define required modules
*/
Dashboards.register((app, datastore, database, gateway, admin, sources, worksets) => {
app.use(ESI(config.esiSettings));
Dashboards.menus.add({
title: 'Dashboards',
link: '/app/dashboards/main#/home',
weight: 19,
name: 'dashboards',
menu: 'care'
});
Dasboards.routes(app, datastore, database, admin);
return Dashboards;
});
引擎为我们提供了进行垂直分解的能力,而无需完全放弃现有系统,而不是提高现有 Angular 应用程序的性能,同时还能够开发新功能并将现有功能重写为更现代、更注重性能的引擎库,例如 React、Preact、Vue、Svelte 等。
引擎测试用例
已通过 | 失败的 |
---|---|
团队所有权 | 独立运行 |
独立开发 | |
原生支持 | |
流畅的用户界面 | |
超快速加载 | |
企业形象 | |
共享基础知识 | |
模块化的 | |
共享基础知识 | |
技术不可知论者 |
Engine 为每个 JavaScript 开发人员提供了一个良好且熟悉的生态系统,他们可以使用原生提供的 NPM cli 工具以真正的即插即用格式构建、发布和安装他们的微前端到任何基于引擎的项目中。
为 Engine 创建的所有应用程序以及任何需要重复使用或即插即用的 JavaScript 模块都发布到我们网络内托管的私有 NPM 注册表。
灵活、强大而又简单的架构
到目前为止,我们已经能够将大型遗留 UI 单体应用拆分成独立的微应用,这些微应用可以像传统的 npm 包一样使用,因为每个引擎包本身就是一个 Web 应用中间件。UI Engine 提供的应用程序外壳充当了缝合层,它由各个包组成无缝的 UI,并为该 UI 发布一个 Docker 化的镜像。
为了将每个引擎包作为独立的微应用程序运行,从而以分布式方式展开,我们需要了解满足下面所述的微前端架构基本要求的主要组件。
客户端
- 编排
- 路由
- 微应用隔离
- 应用程序到应用程序通信
- 微应用程序UI之间的一致性
服务器端
- 服务器端渲染
- 路由
- 依赖管理
为了满足客户端的需求,我们提供了UI引擎的四个基本结构:PWAManager、Loader、Router和UI Engine Store。
Pwa管理器
PwaManager 是客户端微应用编排的核心。PwaManager 的主要功能是创建依赖关系树。一旦微应用的所有依赖关系都解析完毕,PwaManager 就会启动该微应用。
装载机
加载器是 UI Engine 提供的客户端解决方案中最重要的部分之一。加载器负责从服务器获取尚未解析的微应用程序。
路由器
为了解决客户端路由问题,UI Engine 提供了一个路由器;该路由器主要用于解析微应用,它处理每个应用的顶层路由,并将后续处理委托给相应的微应用。假设我们有一个 URL 为 的应用,/sources/view/123
以及一个名为 SourcesApp 的应用。在这种情况下,UI Engine 路由器将解析 URL/sources/*
并调用 SourcesApp 函数/view/123
。
店铺
该 store 用于解决客户端多个应用程序之间的通信问题;这个 store 是按照 Redux 的思路建模的。
微型应用服务器
微应用服务器负责初始化和服务微应用。
每当微应用服务器启动时,它首先会调用 StitchingServer 提供的注册端点,并传入应用清单,其中定义了依赖项、类型和 URL 模式。
拼接服务器
StitchingServer 为 MicroAppServer 提供了一个注册钩子。一旦 MicroAppServer 将自己注册到 StitchingServer,StitchingServer 就会记录该 MicroAppServer 的清单。
之后,StitchingServer 使用清单声明从请求的统一资源定位器解析 MicroAppServers。
解析后,MicroAppServer 及其所有依赖项,以及 CSS、JS 和超文本标记语言中的所有相关方法都将添加前缀,并与 MicroAppServer 公共统一资源定位符连接。下一步是在 CSS 选择器中添加 MicroAppServer 的单个符号作为前缀,以防止客户端微应用程序之间发生冲突。
然后,StitchingServer 的主要职责就登场了——从所有收集的组件中组成并返回无缝的超文本标记语言页面。
结论
微前端是一个相对较新的术语,于 2016 年才被提出;然而,已经有很多大公司尝试解决类似的问题,比如 Facebook 的BigPipe。
Zalando 开源了其名为Project Mosaic的解决方案。
目前已经有一个框架,称为single-spa。
微前端的话题正在被广泛讨论;基于 Web 组件的开发策略已经获得了巨大的发展势头,我相信随着时间的推移,这个话题将会被更频繁地讨论。
在未来的几年里,我希望这将成为大型团队事实上的开发方式。
资源
读者应该阅读一下Nicholas Zakas 的演讲,他一直是 Engine 背后的灵感和动力。
文章来源:https://dev.to/aregee/writing-down-the-last-monolith-micro-frontends-hd4