可靠的企业战略,数字化转型,智能化转型和企业架构智库

【微前端架构】独立的微前端与SPA 库

我们最近在实用主义者的微前沿黑客马拉松上发表了什么。

 

什么是微前端?

它是将大型web应用程序设计为由小型子应用程序组成的概念。这个想法与微服务非常相似。区别在于微服务是独立的后端服务,而微前端,顾名思义,是独立的前端组件。如果将这两种体系结构模式组合在一起,就可以设计自包含的应用程序。这意味着我们可以将大型系统垂直地分割成独立的部分,在理想的情况下,每个部分都有自己的前端、后端服务和数据库(如果需要的话)。这听起来像是一个摆脱单一UI的完美方法,支持独立且可伸缩的组件!

简单真实的例子

假设我们要实现一个在线商店。它应该有一个可用产品的列表。客户可以将产品添加到购物车中。从购物车级别来看,应该可以下订单。可选地,推荐系统可以推荐可能引起客户兴趣的产品。

布局

在web页面上有两种定位子应用程序的方法:

  • 每个页面一个应用程序——每当URL更改时,都会加载一个新的应用程序

  • 每个页面有许多应用程序——一个URL可以访问显示在单个页面上的许多小应用程序。

我们如何实现它?

每个应用程序前端可以从不同的位置提供服务,并具有自己的API。为了实现它,我们需要一个模板,它将应用程序呈现在特定的屏幕位置。我们可以通过两种方式实现:服务器端或客户端。

对于服务器端,我们可以使用:

  • 启用ESI模块的清漆。有关详细信息,请参见https://varnish-cache.org/docs/3.0/tutorial/esi.html
  • NGINX作为启用SSI和ngx_pagespeed模块的反向代理。有关详细信息,请参见https://www.thoughtworks.com/talks/a-high- performance mansolution to microservice-ui-composition

对于客户端,我们可以使用:

服务器端和客户端都有各自的优缺点,但我更关注客户端。

微前端的优势

  1. 应用程序是自包含的——这意味着更高的内聚性,因为每个应用程序只有一个职责。
  2. 应用程序可以独立开发——开发人员可以专注于他们的工作并交付业务价值;不需要与其他团队进行技术同步。
  3. 应用程序可以独立部署——部署应用程序时可以自由选择,不会对系统的其他部分造成负面影响。
  4. 应用程序可以在不同的技术中实现——在前端技术快速发展的世界中,选择一个理想的JS框架是不可能的,这在未来两年内不会被视为遗留问题。如果我们可以使用一个新的框架来编写代码,而不用重写现有的系统,那不是很棒吗?使用微前端,我们可以组合许多框架。额外的好处是,我们可以选择我们喜欢的技术,或者与我们团队的技能相匹配的技术!

 

潜在的问题

  1. 一致的外观和感觉。我们如何执行它?我们可以使用共享的CSS样式表,但这意味着所有应用程序都依赖于一个公共资源。有更好的方法吗?答案是肯定的,也不是。没有完美的解决方案,但是我建议为每个应用程序使用单独的样式表。冗余导致用户获取更多数据,从而影响应用程序的加载时间。此外,组件必须至少实现一次,这将影响开发成本和一致性。这种方法的好处是独立性。这样,我们就可以避免团队在开发和部署期间的同步问题。有一个共同的风格指南,例如在Zeplin中设计的,有助于保持整个系统的外观和感觉一致(但不相同)。或者,我们可以使用每个应用程序包含的公共组件库。这种解决方案的缺点是,每当有人更改库时,他们必须确保不会破坏依赖的应用程序。它会引入一个巨大的惯性。此外,库在大多数情况下只能由一个框架使用。实现一个UI组件库没有简单的方法,Angular和React应用程序可以使用这个库。
  2. 缓慢的加载。如果我们要使用至少两个JS框架(例如,两个应用程序使用Angular,一个使用React),那么我们的web浏览器必须获取大量数据。
  3. 整体的复杂性。如何将不同技术和由不同团队创建的应用程序编排并组合成一个产品?

让我们开始一个概念项目的客户端证明

我们的目标是:

  1. 创建带有顶部标题和左侧导航栏的类似门户的应用程序,
  2. 导航栏有打开子应用程序的链接,
  3. 每个子应用程序都可以用不同的技术实现(比如Angular 5、React 15和React 16),
  4. 我们可以在一个页面上呈现一个子应用程序,
  5. 我们可以在一个页面上呈现许多子应用程序,
  6. 每个子应用程序都可以独立部署并托管在不同的服务器上,
  7. CSS样式表是独立的,
  8. 子应用程序之间的通信是可能的。

核心框架

单个SPA库对于引导项目非常有用。它的功能如下:

  1. 在同一页面上使用多个框架而不刷新页面,
  2. 改进初始加载时间的延迟加载代码,
  3. 顶级路由。

那听起来很棒!让我们继续!

概念项目结构证明

我们将创建3个模块:

  1. 子app- Angular - Angular 5应用,
  2. 子应用程序-反应物-反应应用程序,
  3. main-app - React 15应用;它是一个模板模块,将sub-app-angular和sub-app-react16聚合为一个“门户”应用程序。

我们可以使用create-反应物-app来创建新的React子app-react16和main-app应用程序,以及Angular CLI来创建子app- Angular。由于这些启动程序,我们能够跳过耗时的新项目设置。

 

部署

我们希望有3个运行的服务器:

  1. http://localhost:3000,服务于main-app应用程序。用户将使用它在web浏览器中打开门户。
  2. http://localhost:3001,提供子app-angular包。
  3. http://localhost:3002,服务于子app-react16 bundle。

这意味着,无论何时我们决定必须更新Angular应用程序(因为bug修复),我们都必须构建新的包,并将它们复制到http://localhost:3001上提供的文件夹中。

子应用等进行实现细节

为了将模块导出为子应用程序,我们需要从入口点JS文件导出3个生命周期函数:

  1. 引导-将被调用一次,就在注册的应用程序第一次挂载之前,
  2. 挂载-当已注册的应用程式挂载时,
  3. 卸载——将在卸载已注册的应用程序时调用。

幸运的是,我们不必手动实现这些函数,因为Single SPA为最常见的JS框架提供了默认实现。我们必须做的是在包中包含单spa-react和单spa-angular2库。json文件并编写一段代码。

React应用程序入口点文件可以是这样的:

React 16应用程序的入口点

我们必须为单spa反应提供以下特性:

  • rootComponent —用于呈现React应用程序的根组件,
  • domElementGetter ----函数,返回应用程序要呈现的DOM元素。

Angular应用程序入口点文件可以是这样的:

Angular 5应用程序的入口点

我们必须为单spa-angular2提供以下属性:

  • mainModule -应用根Angular模块,
  • domElementGetter——函数返回应用程序要呈现的DOM元素,
  • template- Angular模板。

您可能想知道主应用程序在从服务器获取引导、挂载和卸载函数后如何找到它们。答案是我们必须做一些假设。我们假设,在获取并执行所有Angular应用程序包之后,这些函数都可以在窗口中访问。angularApp对象。类似地,对于React应用程序,我们希望在窗口中有这些函数。reactApp对象。这意味着子应用程序和模板都需要知道单个SPA生命周期函数的全局名称空间中的位置。我们可以将它称为子应用程序与其容器之间的契约。

我们如何执行这些函数的目标位置?我们当然可以使用Webpack !为此,我们必须向module.exports添加2个条目。每个子应用程序的Webpack配置文件中的输出对象。

对于React应用程序,它可能是这样的:

将React应用程序导出为window.reactApp

对于Angular应用程序,它可能是这样的:

将Angular应用程序导出为window.angularApp

太酷了!现在,我们准备配置一个模板项目——主应用程序。我们要做的是渲染:

  • sub-app-react16 for /react or / in address bar
  • sub-app-angular for /angular or / in address bar

模板工程实现

我们必须为子应用程序准备DOM占位符元素:

子应用等进行容器

接下来,我们必须将single-spa库添加到包中。json文件。

现在,我们准备在单个SPA库中注册子应用程序。React子应用程序由registerReactApp函数调用注册,Angular子应用程序由registerAngularApp函数调用注册。最后,我们必须启动单个SPA库。

main-app入口文件

registerReactApp是这样的:

反应应用程序注册

registerAngularApp是这样的:

角应用程序注册

singleSpa的registerApplication方法需要:

  • 要注册的应用程序的逻辑名称,
  • 应用程序加载器,它是一个函数返回承诺,解析为引导,挂载和卸载子应用程序提供的函数,
  • 接受窗口的函数(谓词)。location作为第一个参数,并在应用程序应该为给定URL激活时返回true。

我们需要一些公用工具:

runScript函数获取外部JS脚本文件,并通过向文档添加一个新的脚本元素来运行它。

您可能会注意到子应用程序和模板项目之间的第二个约定:子应用程序和模板都需要知道子应用程序容器的DOM元素ID。

现在让我们看看我们取得了什么成果:

它完美地!

CSS样式表独立

在理想情况下,所有子应用程序都应该是独立的。这意味着不应该提供CSS选择器,这将影响其他子应用程序,例如:按钮,h1等。

因为我们的测试项目使用PostCSS,所以我们可以利用它提供的插件。其中之一是postcss-wrap。它将CSS规则封装在一个名称空间中,例如.selector{}被转换成.namespace .selector{}。在我们的示例中,每个子应用程序可能有自己的名称空间,仅限于其容器标识符。

这意味着对于使用ID React -app在DOM元素中呈现的React应用程序,所有CSS选择器都可以使用# React -app作为前缀。

为此,我们必须将postcss-wrap库添加到React应用程序依赖项中,然后在Webpack配置文件中配置它。它可以是这样的:

postcss-wrap Webpack配置的示例

最后,您可以看到引导样式表的作用域仅限于React应用程序:

带有名称空间的CSS选择器

这是一个非常简单和功能齐全的解决方案!

应用程序之间的通信

最后缺少的是应用程序间的通信。应用程序在理论上是独立的,但是它们可以对其他应用程序发送的一些事件作出响应。因为我们讨论的是事件,而不是直接同步调用,所以它不会在应用程序之间引入直接耦合。

您可能会注意到,React 16应用程序显示当前时间。我们可以实现的是从Angular应用程序发送的一个事件,它会停止或启动时钟。

我们如何在应用程序之间发送事件?最简单的方法是使用本地web浏览器事件机制,它不需要任何额外的库。

我们可以像这样从Angular应用程序发送一个事件:

发送toggleClock事件

此事件的接收者是React应用程序中的时钟组件。它可以在应用程序挂载时订阅,也可以在卸载时取消订阅:

现在通信已经准备好了。很简单的代码!

总结

我们所验证的是,基于客户端呈现的实现(由单个SPA库支持)可能是一个可行的选择。可能还有一些潜在的问题。

需要调查:

  • 如果应用程序使用相同的库(例如Lodash),但是版本不同,它会工作得很好吗?
  • 有没有办法卸载不再使用的应用程序的JS代码?

可以改进的地方:

  • 包缓存。下载自上次使用以来没有更改过的代码基没有任何意义。
  • 减少模板应用程序和子应用程序之间的耦合。目前,主应用程序已经硬编码了每个子应用程序包的列表。这个列表可以作为清单文件移动到子应用程序,并通过公共URL公开。这意味着每个子应用程序将确切地告诉模板应用程序为了运行应用程序必须获取什么。模板应用程序只知道每个应用程序的清单文件的URL。

概念验证代码可以在Github上找到:https://github.com/pragmatists/microfron。它还没有准备好投入生产,但这个想法是为了验证我们的假设,并在一天的黑客马拉松中消除我们的疑虑。

希望你喜欢这篇文章!再见。

 

原文:https://blog.pragmatists.com/independent-micro-frontends-with-single-spa-library-a829012dc5be

本文:http://pub.intelligentx.net/node/566

讨论:请加入知识星球或者小红圈【首席架构师圈】