微前端的优势和好处
已发表: 2022-03-11微前端架构是一种设计方法,其中前端应用程序被分解成单独的、半独立的“微应用程序”,松散地一起工作。 微前端概念模糊地受到微服务的启发并以微服务命名。
微前端模式的好处包括:
- 微前端架构可能更简单,因此更容易推理和管理。
- 独立开发团队可以更轻松地在前端应用程序上进行协作。
- 他们可以通过同时运行“新”应用程序来提供从“旧”应用程序迁移的方法。
尽管微前端最近受到了很多关注,但到目前为止还没有单一的主导实现,也没有明确的“最佳”微前端框架。 事实上,根据目标和要求,有多种方法。 请参阅参考书目以了解一些更知名的实现。
在本文中,我们将跳过微前端的大部分理论。 以下是我们不会涉及的内容:
- 将应用“切片”成微应用
- 部署问题,包括微前端如何适应 CI/CD 模型
- 测试
- 微应用是否应该与后端的微服务一对一对齐
- 对微前端概念的批评
- 微前端和普通的旧组件架构之间的区别
相反,我们将提供一个专注于具体实现的微前端教程,重点介绍微前端架构中的重要问题及其可能的解决方案。
我们的实现称为 Yumcha。 粤语“yum cha”的字面意思是“喝茶”,但日常的意思是“出去吃点心”。 这里的想法是,宏应用程序中的各个微应用程序(我们将称之为组合的顶级应用程序)类似于点心午餐时带出的各种一口大小的篮子。
我们有时会将 Yumcha 称为“微前端框架”。 在当今世界,术语“框架”通常用于指代 Angular、React、Vue.js 或其他类似的 Web 应用程序上层结构。 我们根本不是在谈论那种意义上的框架。 我们称 Yumcha 为框架只是为了方便:它实际上更像是一组工具和一些用于构建基于微前端的应用程序的薄层。
微前端教程第一步:组合应用程序的标记
让我们深入思考如何定义宏应用程序和构成它的微应用程序。 标记一直是网络的核心。 因此,我们的宏应用程序将通过以下标记来指定:
<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>
,因为“门户”是门户提案中使用的微应用的一个有前途的术语,这是定义用于微前端的标准 HTML 元素的早期尝试。
实现<yumcha-portal>
自定义元素
我们应该如何实现<yumcha-portal>
? 既然它是一个自定义元素,当然是作为一个 Web 组件! 我们可以从众多强大的竞争者中选择编写和编译微前端 Web 组件; 在这里,我们将使用聚合物项目的最新版本 LitElement。 LitElement 支持基于 TypeScript 的语法糖,它为我们处理大部分自定义元素样板。 为了使<yumcha-portal>
可用于我们的页面,我们必须将相关代码包含为<script>
,就像我们在上面所做的那样。
但是<yumcha-portal>
实际上做了什么? 第一个近似值是它只创建一个具有指定源的iframe
:
render() { return html`<iframe src=${this.src}></iframe>`; }
…其中render
是标准的 LitElement 渲染钩子,使用其html
标记的模板字面量。 对于一些琐碎的用例来说,这个最小的功能可能已经足够了。
在iframe
中嵌入微应用
iframe
是每个人都喜欢讨厌的 HTML 元素,但实际上它们提供了非常有用的、坚如磐石的沙盒行为。 但是,在使用iframe
时仍然需要注意一长串问题,这些问题可能会对我们的应用程序的行为和功能产生影响:
- 首先,
iframe
在大小和布局方面有众所周知的怪癖。 - CSS 当然会完全隔离到
iframe
,无论好坏。 - 浏览器的“后退”按钮可以正常工作,尽管
iframe
的当前导航状态不会反映在页面的 URL中,因此我们既不能剪切和粘贴 URL 以达到组合应用程序的相同状态,也不能进行深度链接给他们。 - 根据我们的 CORS 设置,从外部与
iframe
的通信可能需要通过postMessage
协议。 - 必须为跨
iframe
边界的身份验证做出安排。 - 一些屏幕阅读器可能会在
iframe
边界处绊倒,或者需要iframe
有一个可以向用户宣布的标题。
不使用iframe
可以避免或减轻其中一些问题,我们将在本文后面讨论另一种选择。
从好的方面来说, iframe
将拥有自己独立Content-Security-Policy
(CSP)。 此外,如果iframe
指向的微应用使用服务工作者或实现服务器端渲染,则一切都会按预期工作。 我们还可以为iframe
指定各种沙盒选项以限制其功能,例如能够导航到顶部框架。
一些浏览器已经发布或计划为iframe
发布loading=lazy
属性,该属性会延迟加载首屏iframe
直到用户滚动到它们附近,但这并不能提供对延迟加载的细粒度控制我们想要。
iframe
的真正问题是iframe
的内容需要多个网络请求才能检索。 顶级index.html
被接收,它的脚本被加载,它的 HTML 被解析——但是浏览器必须发起另一个对iframe
的 HTML 的请求,等待接收它,解析和加载它的脚本,然后渲染iframe
的内容。 在许多情况下, iframe
的 JavaScript 仍然需要启动,进行自己的 API 调用,并且只有在这些 API 调用返回并处理数据以供查看后才显示有意义的数据。
这可能会导致不希望的延迟和渲染伪影,尤其是在涉及多个微应用时。 如果iframe
的应用程序实现了 SSR,这将有所帮助,但仍不能避免额外往返的必要性。
因此,我们在设计门户实施时面临的主要挑战之一是如何处理这种往返问题。 我们的目标是单个网络请求应该关闭整个页面及其所有微应用,包括每个微应用能够预填充的任何内容。 这个问题的解决方案在于 Yumcha 服务器。
Yumcha 服务器
此处介绍的微前端解决方案的一个关键要素是设置专用服务器来处理微应用组合。 该服务器将请求代理到托管每个微应用的服务器。 当然,设置和管理此服务器需要付出一些努力。 一些微前端方法(例如,single-spa)试图以易于部署和配置的名义免除对此类特殊服务器设置的需求。
然而,建立这个反向代理的成本被我们获得的收益所抵消; 事实上,没有它我们根本无法实现基于微前端的应用程序的一些重要行为。 设置这种反向代理有许多商业和免费的替代方案。
反向代理除了将微应用请求路由到适当的服务器外,还将宏应用请求路由到宏应用服务器。 该服务器以一种特殊的方式为组合应用程序提供 HTML。 在通过代理服务器在诸如http://macroapp.example.com
之类的 URL 处从浏览器接收到对index.html
的请求后,它会检索index.html
,然后在返回之前对其进行简单但关键的转换它。
具体来说,HTML 被解析为<yumcha-portal>
标记,这可以通过 Node.js 生态系统中可用的胜任 HTML 解析器之一轻松完成。 使用<yumcha-portal>
的src
属性,联系运行微应用的服务器并检索其index.html
— 包括服务器端呈现的内容(如果有)。 结果作为<script>
或<template>
标记插入到 HTML 响应中,以免被浏览器执行。

这种设置的优势首先在于,在第一次请求组合页面的index.html
时,服务器可以从各个微应用服务器中完整地检索各个页面——包括 SSR 渲染的内容,如果任何——并向浏览器提供一个完整的页面,包括可用于填充iframe
的内容,无需额外的服务器往返(使用未充分利用的srcdoc
属性)。 代理服务器还确保任何有关微应用程序从何处提供服务的细节都不会被窥探。 最后,它简化了 CORS 问题,因为应用程序请求都来自同一个来源。
回到客户端, <yumcha-portal>
标签被实例化并找到服务器在响应文档中放置的内容,并在适当的时间呈现iframe
并将内容分配给它的srcdoc
属性。 如果我们不使用iframe
(见下文),那么对应于该<yumcha-portal>
标记的内容将插入到自定义元素的影子 DOM 中,如果我们正在使用它,或者直接内联到文档中。
至此,我们已经有了一个部分功能的基于微前端的应用程序。
这只是 Yumcha 服务器有趣功能的冰山一角。 例如,我们希望添加一些功能来控制如何处理来自微应用服务器的 HTTP 错误响应,或者如何处理响应非常缓慢的微应用——如果一个微应用没有,我们不想永远等待页面提供服务回应! 这些和其他主题我们将留给另一篇文章。
Yumcha 宏应用index.html
转换逻辑可以轻松地以无服务器 lambda 函数方式实现,或者作为 Express 或 Koa 等服务器框架的中间件。
基于存根的微应用控制
回到客户端,我们如何实现微应用还有另一个方面对效率、延迟加载和无卡顿渲染很重要。 我们可以为每个微应用生成iframe
标签,或者使用src
属性(发出另一个网络请求)或使用srcdoc
属性填充服务器为我们填充的内容。 但在这两种情况下,该iframe
中的代码将立即启动,包括加载其所有脚本和链接标签、引导以及任何初始 API 调用和相关数据处理——即使用户甚至从未访问过有问题的微应用。
我们对这个问题的解决方案是最初将页面上的微应用表示为微小的未激活存根,然后可以将其激活。 激活可以由进入视野的微应用区域驱动,使用未充分使用的IntersectionObserver
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,它预先通知所有存根即将发生的事件,等待任何已请求激活的事件类型被激活,然后针对文档分派事件,任何微应用都可以在该文档上侦听它。 鉴于我们所有的iframe
都是同源的,我们可以从iframe
到达页面,反之亦然,以找到触发事件的元素。
路由
在这个时代,我们都开始期望 SPA 中的 URL 栏代表应用程序的视图状态,因此我们可以剪切、粘贴、邮寄、文本和链接到它,以直接跳转到应用程序内的页面。 然而,在微前端应用程序中,应用程序状态实际上是状态的组合,每个微应用程序一个状态。 我们如何表现和控制这一点?
解决方案是将每个微应用的状态编码为单个复合 URL,并使用一个小型宏应用路由器,该路由器知道如何将该复合 URL 放在一起并将其分开。 不幸的是,这需要每个微应用中特定于 Yumcha 的逻辑:从宏应用路由器接收消息并更新微应用的状态,相反,通知宏应用路由器该状态的变化,以便可以更新复合 URL。 例如,可以想象 Angular 的YumchaLocationStrategy
或 React 的<YumchaRouter>
元素。
非iframe
案例
如上所述,在iframe
中托管微应用确实有一些缺点。 有两种选择:将它们直接内联到页面的 HTML 中,或者将它们放置在 shadow DOM 中。 这两种选择都在一定程度上反映了iframe
的优缺点,但有时方式不同。
例如,必须以某种方式合并各个微应用 CSP 策略。 屏幕阅读器等辅助技术应该比iframe
更好地工作,假设它们支持 shadow DOM(目前还不是全部支持)。 使用“范围”的服务工作人员概念来安排注册微应用的服务工作人员应该很简单,尽管应用程序必须确保其服务工作人员是在应用程序的名称下注册的,而不是"/"
。 与iframe
相关的布局问题都不适用于内联或影子 DOM 方法。
但是,使用 Angular 和 React 等框架构建的应用程序可能会不满意内联或影子 DOM。 对于那些,我们可能会想要使用iframe
。
就 CSS 而言,内联和影子 DOM 方法有所不同。 CSS 将被干净地封装在 shadow DOM 中。 如果出于某种原因我们确实想与 shadow DOM 共享外部 CSS,我们将不得不使用可构造的样式表或类似的东西。 使用内联微应用,所有 CSS 将在整个页面中共享。
最后,在<yumcha-portal>
中实现内联和影子 DOM 微应用的逻辑很简单。 我们检索给定微应用的内容,从服务器逻辑将其作为 HTML <template>
元素插入到页面中,克隆它,然后将其附加到 LitElement 调用的renderRoot
中,这通常是元素的影子 DOM,但可以对于内联(非影子 DOM)情况,也可以设置为元素本身( this
)。
可是等等! 微应用服务器提供的内容是一个完整的 HTML 页面。 我们不能将带有html
、 head
和body
标签的微应用的 HTML 页面插入到宏应用的中间,可以吗?
我们通过利用template
标签的一个怪癖来解决这个问题,其中包装了从微应用服务器检索到的微应用内容。 事实证明,当现代浏览器遇到template
标签时,虽然它们不会“执行”它,但会解析它,并在这样做时删除无效内容,例如<html>
、 <head>
和<body>
标签,同时保留其内在内容。 因此<head>
中的<script>
和<link>
标记以及<body>
的内容都被保留了。 这正是我们想要将微应用内容插入页面的目的。
微前端架构:细节中的魔鬼
如果(a)它们被证明是一种更好的架构方法,并且(b)我们可以弄清楚如何以满足当今网络无数实际需求的方式实现它们,那么微前端将在 webapp 生态系统中扎根。
关于第一个问题,没有人声称微前端是所有用例的正确架构。 特别是,单个团队的绿地开发几乎没有理由采用微前端。 我将把什么类型的应用程序在什么类型的上下文中最能从微前端模式中受益的问题留给其他评论员。
在实施和可行性方面,我们已经看到有很多细节需要关注,其中包括一些在本文中甚至没有提到的细节——特别是身份验证和安全、代码复制和 SEO。 尽管如此,我希望本文为微前端提供了一种基本的实现方法,经过进一步的改进,可以满足现实世界的需求。
参考书目
- 微前端 — 角度风格 — 第 1 部分
- 微前端 — 角度风格 — 第 2 部分
- 使用微前端开发 AngularJS 应用程序
- 微前端
- UI 微服务——逆转反模式(微前端)
- UI 微服务——一种反模式?
- 使用微前端的页面构建采用类似于 Yumcha 的反向代理和 SSI 方法,我强烈推荐。
- 微前端资源
- 讲台
- 我不了解微前端。 这是对微前端架构类型和用例的一个很好的概述。
- 使用 Vue.js、AWS Lambda 和 Hypernova 的无服务器微前端
- 微前端:一个伟大的、全面的概述。