微前端究竟好在哪?

阅读数:1471 2019 年 10 月 30 日 19:44

微前端究竟好在哪?

微前端架构是一种设计方法,其中,前端应用被分解为多个松散而协同工作的半独立“微应用”。微前端的思想来源于微服务,其名称也遵循了微服务的命名方式。那么,微前端的优势和好处在哪?让我们一起通过这篇微前端教程来了解。

微前端模式的好处包括:

  1. 微前端架构可能更简洁,因此更易于推理和管理。
  2. 多个独立的开发团队更容易协同开发单个前端应用。
  3. 微前端模式可以让“新”应用与 " 旧 " 应用并行工作,从而提供了一种迁移手段。

尽管微前端最近在业内引发了很多关注,但到目前为止并没有出现一种绝对主流的实现方式,也没有公认“最佳”的微前端框架。实际上,根据目标和需求的不同,可行的方法有很多。要了解其中一些比较知名的实现,请参见附注。

本文将提供一份微前端教程,重点在于具体的实现,主要介绍微前端架构中的重要问题及其可能的解决方案。

我们的实现称为 Yumcha。“Yum cha”在粤语中的字面意思是“喝茶”,但其日常含义是“出去吃点心”。这里的想法是,宏应用(我们将其称为组合式顶级应用)中的各个单独的微应用类似于外出野餐时带的许多大小不一的篮子。

微前端究竟好在哪?

有时我们将 Yumcha 称为“微前端框架”。如今,“框架”这个术语通常用来指代 Angular、React、Vue.js 或其他类似的 Web 应用上层架构,但我们所说的完全不是这种框架。我们称 Yumcha 为框架只是为了方便起见:实际上,它是一组工具和一些薄层,用于构建基于微前端的应用。

微前端教程第一步:为组合应用作标记

首先我们来深入探讨,如何定义一款 " 宏应用 " 以及组成它的众多微应用。标记(Markup)一直是 Web 的核心要素,因此只需使用以下标记即可定义我们的宏应用:

复制代码
<html>
<head>
<script src="/yumcha.js"></script>
</head>
<body>
<h1>Hello, micro-frontend app.</h1>
<!-- HERE ARE THE MICROAPPS! -->
<yumcha-portal name="microapp1" src="https://microapp1.example.com"></yumcha-portal>
<yumcha-portal name="microapp2" src="https://microapp2.example.com"></yumcha-portal>
</body>
</html>

使用标记定义宏应用后,我们就可以充分利用 HTML 和 CSS 的功能来布局和管理我们的微应用。例如,一个微应用可以处在另一个微应用的上方,或者位于其侧面,或者位于页面的一角,或者位于折叠面板的一个格子内,或者可以一直隐藏起来直到发生某种情况再现身,或者永久地保留在背景中 。

我们将用于微应用的自定义元素命名为yumcha-portal,因为“portal”是 portal 提案中针对微应用推荐的术语,这一提案是为微前端定义标准 HTML 元素的早期尝试。

实现自定义元素

我们应该如何实现呢?因为它是自定义元素,所以当然是作为 Web 组件来实现了!我们有许多高水平的方案可供挑选,帮助我们编写和编译微前端 Web 组件。在本文中,我们将使用 Polymer 项目的最新版本 LitElement。LitElement 支持基于 TypeScript 的语法糖,为我们处理了大多数自定义元素样板。为了使yumcha-portal在我们的页面上可用,我们必须将相关代码作为script包括在页面内,之前的代码就是这样做的。

但是yumcha-portal到底是做什么的呢?首先它可以使用指定的源创建一个 iframe:

复制代码
render() {
return html`<iframe src=${this.src}></iframe>`;
}

这里,render 是标准的 LitElement 渲染 hook,使用它的 html 标记模板字面量。对于某些普通的场景来说,这种初级功能可能就够用了。

在 iframe 中嵌入微应用

大家对 iframe 这个 HTML 元素又爱又恨,但实际上它们提供了极其有用的、坚如磐石的沙箱行为。但在使用 iframe 时还有很多问题需要注意,这些问题可能会对我们应用的行为和功能产生影响:

  • 首先,iframe 在大小控制和自身布局方面有一些众所周知的怪癖。
  • 当然,CSS 会与 iframe完全隔离,这有利有弊。
  • 浏览器的“后退”按钮将正常工作,但是 iframe 的 当前导航状态不会反映在页面的 URL 中,所以我们既不能通过剪切和粘贴 URL 来使微应用状态与宏应用保持一致,也无法对其进行深层链接。
  • 根据我们的 CORS 设置,从外部与 iframe 通信 可能需要通过 postMessage 协议
  • 必须处理 跨 iframe 边界的身份验证 事宜。
  • 一些屏幕阅读器可能会在 iframe 边界出错,或者需要 iframe 具有可以向用户告知的标题。

如果不使用 iframe 的话,上面的某些问题也可以避免或减轻,我们稍后会讨论这种替代方法。

从正面的角度来说,iframe 有自己独立的 Content-Security-Policy(内容安全政策,CSP)。另外,如果 iframe 指向的微应用使用了 Service Worker 或实现了服务端渲染,那么一切都会按照预期正常工作。我们还可以为 iframe 指定各种沙箱选项以限制其能力,例如禁止导航到顶部框架等。

一些浏览器已经支持,或正在计划为 iframe 支持 loading=lazy 属性,该属性会推迟加载非首屏的 iframe,直到用户将页面滚动到它们附近才开始加载,但这并未提供对延迟加载的细粒度控制,这种控制才是我们想要的。

iframe 的真正问题在于,它们的内容需要多个网络请求才能获取。首先接收顶级的 index.html,然后加载它的脚本,接下来解析它的 HTML——但是随后,浏览器必须为 iframe 的 HTML 发送另一个请求,等待接收它,解析并加载其脚本,然后渲染 iframe 的内容 。在许多情况下,iframe 的 JavaScript 也得单独加载,调用自己的 API;等到这些 API 调用返回,为视图准备的数据处理完毕之后才能显示出有意义的数据。

这可能会导致我们不希望看到的延迟和渲染伪像,尤其在涉及多个微应用时更容易出现这类问题。如果 iframe 的应用实现了 SSR 还能好一些,但还是不能避免额外的往返行程。

因此,我们在设计门户实现时面临的一项主要挑战就是处理往返问题。我们的目标是,只用单个网络请求就获取整个页面和页面上的所有微应用(包括每个微应用会填充的内容)。这个问题的解决方案就是 Yumcha 服务器。

Yumcha 服务器

本文介绍的微前端解决方案的关键要素,就是设置一个专用服务器来处理微应用组合。这个服务器会代理对托管各个微应用的服务器发出的请求。当然,我们需要花些功夫来设置和管理这个服务器。一些微前端方法(例如 single-spa)为了简化部署和配置工作,会尽量精简这种特殊服务器的设置任务。

但是建立这种反向代理的成本会被我们从中获得的收益抵消。实际上,如果没有这种反向代理,我们基于微前端的应用就有一些重要的行为无法实现。有很多商业和免费的方案可以用来设置这种反向代理服务器。

反向代理除了将微应用请求路由到合适的服务器外,还会将宏应用请求路由到宏应用服务器上。这个宏应用服务器以特殊方式为组合应用提供 HTML。它会通过代理服务器的 URL 从浏览器收到对 index.html 的请求,然后获取 index.html,对其进行简单但重要的转换后返回它。

具体来说,HTML 将为yumcha-portal标记解析,从 Node.js 生态系统中找一款合适的 HTML 解析器即可。使用yumcha-portal的 src 属性可以联系上运行微应用的服务器,并获取其 index.html——包括服务端渲染的内容(如果存在)。结果将作为scripttemplate标记插入 HTML 响应中,这样就不会被浏览器执行。

微前端究竟好在哪?

这套设置的优点包括:首先,在对组合页面的 index.html 发出请求时,服务器可以从各个微应用服务器中整体检索各个页面,包括 SSR 渲染的内容(如果存在) ——并向浏览器提供一个完整的页面(包括可用于填充 iframe 的内容),而无需额外的服务器往返(使用不多的 srcdoc 属性)。代理服务器还可以完全掩盖微应用来源的细节。最后,由于所有应用请求都来自同一来源,因此它简化了 CORS 问题。

回到客户端,yumcha-portal标记被实例化并找到服务器放置在响应文档中的内容,并在适当的时间渲染 iframe,然后将内容分配给其 srcdoc 属性。如果我们不使用 iframe(请参见下文),则与yumcha-portal标签对应的内容(如果我们正在使用的话)会插入自定义元素的 shadow DOM 中,或者直接内联到文档中。

至此,我们已经有了一个基于微前端应用的半成品。

这只是 Yumcha 服务器众多有趣功能的冰山一角。比如说,我们想要添加一些功能来控制来自微应用服务器的 HTTP 错误响应的处理方式,或者控制响应速度非常慢的微应用的应对策略——如果一个微应用没有响应,我们可不想永远等下去!

这套 Yumcha 宏应用 index.html 转换逻辑很容易用无服务器 lambda 函数的方式实现,也可以作为服务端框架(如 Express 或 Koa)的中间件来实现。

基于存根的微应用控件

回到客户端,在我们实现微应用的过程中,要做到高效、延迟加载和无垃圾的渲染还有一个重要的层面需要关注。我们可以使用 src 属性(发出另一个网络请求),或使用 srcdoc 属性(由服务器为我们填充内容)来为每个微应用生成 iframe 标记。但是在这两种情况下,这个 iframe 中的代码都会立即启动,包括加载其所有脚本和链接标签、引导程序、任何初始 API 调用以及相关的数据处理——即使这个微应用根本没有被用户访问过。

我们针对此问题的解决方案是,一开始将页面上的微应用表示为较小的未激活存根,等待将来激活。可以使用未充分利用的 Intersection-Observer API,通过微应用的视图区域来激活,或者更常见的是通过外部发送的预先通知来激活。当然,我们也可以指定立即激活微应用。

不管是哪种方式,只有在激活了微应用后 iframe 才会真正渲染,其代码才会被加载和执行。就我们使用 LitElement 的实现而言,假设激活状态由一个 activated 的实例变量表示,我们将得到以下内容:

复制代码
render() {
if (!this.activated) return html`{this.placeholder}`;
else return html`
<iframe srcdoc="${this.content}" @load="${this.markLoaded}"></iframe>`;
}

微应用间通信

尽管根据定义,构成宏应用的多个微应用是松散耦合的,但它们仍需要能够相互通信。例如,导航微应用需要发出通知,告知用户刚刚选择的某个微应用应被激活,并且要激活的应用需要能够接收此类通知。

为了符合我们的极简主义思想,我们希望避免引入很多传递消息的机制。相反,基于 Web 组件的精神,我们将使用 DOM 事件。我们提供了一个简单的广播 API,该 API 会预先通知所有存根,告诉它们即将发生哪个事件,然后等待该事件类型下面所有请求激活的事件激活完毕,最后将事件分派给文档,这样任何微应用都可以侦听到它。假设我们所有的 iframe 都来自同一来源,我们就可以从 iframe 一路来到页面,找出引发事件的元素,反之亦然。

路由

在当今时代,我们所有人都希望 SPA 中的 URL 栏代表应用的视图状态,这样我们就可以剪切、粘贴、发送邮件、记录文本和链接到这个 URL,从而直接跳转到应用内的页面。但在微前端应用中,应用状态实际上是很多状态的组合,每个微应用对应一个状态。我们如何表示和控制这一切呢?

解决方案是将每个微应用的状态编码为单个复合 URL,并使用小型的宏应用路由,该路由知道如何将这些复合 URL 组合在一起或者拆分开来。遗憾的是,这要求每个微应用都有特定于 Yumcha 的逻辑:从宏应用路由接收消息并更新微应用的状态,反之则是通知宏应用路由这个状态的变化,以更新复合 URL。例如,特定于 Angular 的可能是 YumchaLocationStrategy,或特定于 React 的元素。

微前端究竟好在哪?

非 iframe 情况

如上所述,在 iframe 中托管微应用确实存在一些缺点。有两种替代方案:直接将它们直接内嵌在页面的 HTML 中,或者将它们放置在 shadow DOM 中。两种选择都在某种程度上抵消了 iframe 的优缺点,但具体的方式有所不同。

例如,必须以某种方式合并独立的微应用 CSP 策略。假设屏幕阅读器等辅助技术支持 shadow DOM(目前还不是全部平台都支持),它们不用 iframe 的效果会更好。使用 Service Worker 的“作用域”概念注册一个微应用的 Service Worker 应该是很简单的,尽管该应用必须确保其 Service Worker 以该应用的名称而不是 "/”注册。与 iframe 相关的布局问题均不适合内联或 shadow DOM 方法。

但是,使用诸如 Angular 和 React 之类的框架构建的应用可能不适合内联或 shadow DOM。对于这些情况,我们可能还是会使用 iframe。

内联和 shadow DOM 方法在 CSS 层面有所不同。CSS 会被完全封装在 shadow DOM 中。如果由于某种原因我们确实想与 shadow DOM 共享外部 CSS,则必须使用可构造的样式表之类的东西。对于内联的微应用来说,所有 CSS 将在整个页面中共享。

最后,在yumcha-portal中实现内联和 shadow DOM 微应用的逻辑很简单。我们获取给定的微应用的内容(这个微应用由服务器逻辑作为 HTMLtemplate元素插入页面),克隆它,然后将它附加到 LitElement 中称为 renderRoot 的位置(一般来说是元素的 shadow DOM,至于内联,也就是非 shadow DOM 的情况也可以设置到元素自身,也就是 this)。

可是等等!宏应用服务器提供的内容是整个 HTML 页面。我们不能将带有 html、head 和 body 标签的微应用 HTML 页面插入到宏应用的某个 HTML 页面中,显然不行。

我们利用 template 标签的怪癖来解决这个问题,在这个标签中包装了从宏应用服务器获取到的宏应用内容。事实上,当现代浏览器遇到 template 标记时,尽管它们没有“执行”它,但还是会 " 解析 " 它,这样就移除了诸如htmlheadbody标记之类的无效内容,同时保留其内部内容。因此head中的scriptlink标记以及body的内容都会留下来,这正是我们想要的,这样就可以将微应用内容正常插入页面了。

微前端架构:细节决定成败

如果(a)事实证明微前端是一种更好的架构方法,并且(b)我们可以弄清楚如何在实现它们的过程中满足当今 Web 领域的众多实际需求,微前端就可以在 Web 应用的生态系统中打下基础。

关于第一个问题,没有人声称微前端是适用于所有用例的架构。特别是,单个团队没有什么理由采用微前端来开发新内容。下面这个问题留给读者思考:在哪些环境中,哪些类型的应用采用微前端模式比其他方法效果更好?

在实现和可行性方面,我们已经意识到有很多细节需要关注,其中包括本文没有提到的一些细节,尤其是身份验证和安全性、代码重复和 SEO 等。尽管如此,我希望本文为微前端提供一种基本的实现方法,这种方法进一步完善后就可以满足实际需求。

基础知识

什么是微前端?

微前端是一种新模式,其中 Web 应用程序 UI(前端)由一些半独立的片段组成,可以由不同的团队使用不同的技术来构建。微前端架构类似一种后端架构,其中后端由一些半独立的微服务组成。

什么是微前端架构?

微前端架构为微前端框架的结构元素提供了方法。它还定义了它们之间的关系,控制 UI 片段的组装和通信方式,以实现最佳的开发人员和用户体验。

微服务可以有 UI 吗?

答案是肯定的。微前端模式通常采用的方法是将微前端的一个片段(可能实现为微前端 Web 组件)与微服务配对以提供其 UI。

如何定义微服务?

一个微服务是体系结构中的一个元素,在该体系结构中,应用程序被构造为一些互操作服务的集合。如果前端采用微前端模式,则一个微服务可以与一个微前端配对。

微前端和 Web 组件之间有什么关系?

微型前端和 Web 组件(自定义元素)可能有几种关系。Web 组件提供了一种基于自然标记的方式来描述组成微前端应用的微应用。微前端应用中的各个微应用本身可以使用 Web 组件构建。

附注

作者介绍:
Bob Myers:前端架构师,曾在教育、出版和金融行业参与过众多大规模项目。

原文链接:
https://www.toptal.com/front-end/micro-frontends-strengths-benefits

评论

发布