本文概述
- 微前端教程的第一步:为组成的应用程序标记
- 实现<yumcha-portal>定制元素
- 在iframe中嵌入Microapp
- Yumcha服务器
- 基于存根的Microapp控件
- 微型应用间通信
- 路由
- 非iframe案例
- 微前端架构:细节决定成败
- 参考书目
微前端架构是一种设计方法, 其中, 前端应用程序被分解为松散地协同工作的各个半独立的”微型应用程序”。微前端概念受到微服务的启发, 并以微服务命名。
微前端模式的好处包括:
- 微前端架构可能更简单, 因此更易于推理和管理。
- 独立开发团队可以更轻松地在前端应用程序上进行协作。
- 通过让”新”应用程序并排运行, 它们可以提供从”旧”应用程序迁移的方法。
尽管微前端最近引起了很多关注, 但到目前为止, 还没有单一的主要实现方式, 也没有明确的”最佳”微前端框架。实际上, 有多种方法取决于目标和要求。有关某些更知名的实现, 请参见参考书目。
在本文中, 我们将跳过许多微观前端理论。这是我们将不介绍的内容:
- 将应用”切片”为微型应用
- 部署问题, 包括微前端如何适合CI / CD模型
- 测试中
- 微应用程序是否应与后端的微服务一一对应
- 对微前端概念的批评
- 微型前端和普通的旧组件体系结构之间的区别
取而代之的是, 我们将提供一个针对具体实现的微型前端教程, 重点介绍微型前端架构中的重要问题及其可能的解决方案。
我们的实现称为Yumcha。粤语中” yum cha”的字面意思是”喝茶”, 但日常含义是”出去喝点点心”。这里的想法是, 宏应用程序中的各个微型应用程序(我们将称为组合式顶级应用程序)类似于点心午餐中带出的各种不同大小的篮子。
有时我们将Yumcha称为”微前端框架”。在当今世界, 术语”框架”通常用于指代Angular, React, Vue.js或其他类似的Web应用程序上层结构。我们根本不是在那种意义上谈论框架。为了方便起见, 我们将Yumcha称为框架:实际上, 它是一组工具和一些薄层, 用于构建基于微型前端的应用程序。
微前端教程的第一步:为组成的应用程序标记
让我们深入思考如何定义宏应用程序以及组成它的微应用程序。标记一直是Web的核心。因此, 可以通过以下标记来指定我们的macroapp:
<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组件。在这里, 我们将使用Polymer项目的最新版本LitElement。 LitElement支持基于TypeScript的语法糖, 该糖为我们处理了大多数自定义元素样板。为了使<yumcha-portal>在我们的页面上可用, 我们必须像上面一样将相关代码作为<script>包括在内。
但是<yumcha-portal>实际上是做什么的?第一种近似方法是使用指定的源仅创建一个iframe:
render() {
return html`<iframe src=${this.src}></iframe>`;
}
…这里render是标准的LitElement渲染钩子, 使用它的html标记模板文字。对于某些琐碎的用例, 这种最小的功能可能就足够了。
在iframe中嵌入Microapp
iframe是每个人都讨厌的HTML元素, 但实际上, 它们提供了极为有用的坚如磐石的沙箱行为。但是, 在使用iframe时, 还有很多问题需要注意, 这可能会对我们的应用程序的行为和功能产生影响:
- 首先, iframe在大小和布局方面具有众所周知的怪癖。
- 当然, 无论好坏, CSS都将完全与iframe隔离。
- 浏览器的”后退”按钮将正常运行, 尽管iframe的当前导航状态不会反映在页面的URL中, 所以我们既不能剪切和粘贴URL来使内容与组成应用的状态相同, 也无法进行深层链接给他们。
- 根据我们的CORS设置, 从外部与iframe进行通信可能需要通过postMessage协议进行。
- 必须为跨iframe边界的身份验证进行安排。
- 一些屏幕阅读器可能会绊倒iframe边界, 或者需要iframe具有可以向用户宣布的标题。
通过不使用iframe, 可以避免或减轻其中的某些问题, 这是我们稍后在本文中讨论的替代方法。
从好的方面来说, iframe将具有自己的独立内容安全政策(CSP)。此外, 如果iframe指向的微型应用程序使用服务工作者或实现服务器端呈现, 则所有操作都将按预期工作。我们还可以为iframe指定各种沙箱选项以限制其功能, 例如能够导航到顶部框架。
一些浏览器已经发布或正在计划为iframe加载load = lazy属性, 该属性推迟将折页iframe加载, 直到用户在其附近滚动为止, 但这并未提供我们想要的对延迟加载的细粒度控制。
iframe的真正问题在于iframe的内容将需要多个网络请求才能检索。接收到顶级index.html, 加载了其脚本, 并对其HTML进行了解析-但是随后, 浏览器必须向iframe的HTML发起另一个请求, 等待接收它, 解析并加载其脚本, 并呈现iframe的内容。在很多情况下, 仅在这些API调用返回并处理了数据以供查看之后, iframe的JavaScript仍将不得不旋转, 进行自己的API调用并显示有意义的数据。
这可能会导致不希望的延迟和渲染伪像, 尤其是在涉及多个微型应用程序时。如果iframe的应用程序实现了SSR, 那将有所帮助, 但仍不能避免需要进行额外的往返行程。
因此, 我们在设计门户实现时面临的主要挑战之一就是如何处理往返问题。我们的目标是, 单个网络请求应将其所有微型应用程序(包括每个微型应用程序能够填充的任何内容)的整个页面都放下。这个问题的解决方案在于Yumcha服务器。
Yumcha服务器
本文介绍的微前端解决方案的关键要素是设置专用服务器来处理微应用程序组合。该服务器代理对托管每个微型应用程序的服务器的请求。当然, 这将需要一些努力来设置和管理该服务器。一些微前端方法(例如, 单spa)试图以易于部署和配置为名, 免除对此类特殊服务器设置的需求。
但是, 设置此反向代理的成本已被我们获得的收益所抵消;实际上, 没有微型前端的应用程序存在一些重要的行为, 而我们根本无法实现。设置这种反向代理有很多商业和免费的选择。
反向代理除了将microapp请求路由到适当的服务器外, 还将macroapp请求路由到macroapp服务器。该服务器以特殊方式为组合的应用程序提供HTML。在通过代理服务器通过URL(例如http://macroapp.example.com)从浏览器收到对index.html的请求后, 它将检索index.html, 然后对其进行简单但重要的转换, 然后返回它。
具体来说, 将为<yumcha-portal>标记解析HTML, 这可以通过Node.js生态系统中可用的合格HTML解析器之一轻松完成。通过将src属性用于<yumcha-portal>, 可以与运行微应用程序的服务器联系, 并检索其index.html-包括服务器端呈现的内容(如果有)。结果将作为<script>或<template>标记插入HTML响应中, 以便不被浏览器执行。
此设置的优点首先包括:首先, 在对组成页面的index.html进行首次请求时, 服务器可以从单个微型应用服务器中整体检索单个页面, 包括SSR呈现的内容(如果有)任何-并向浏览器提供一个完整的页面, 包括可用于填充iframe的内容, 而无需进行其他服务器往返(使用未充分利用的srcdoc属性)。代理服务器还可以确保从任何地方掩盖微应用程序的来源。最后, 由于所有应用程序请求都来自同一来源, 因此它简化了CORS问题。
回到客户端, <yumcha-portal>标记被实例化并找到服务器将其放置在响应文档中的内容, 并在适当的时间呈现iframe并将内容分配给其srcdoc属性。如果我们不使用iframe(请参见下文), 则与<yumcha-portal>标签相对应的内容(如果我们正在使用的话)插入自定义元素的影子DOM中, 或者直接内联到文档中。
至此, 我们已经有一个部分运行的基于微前端的应用程序。
就Yumcha服务器的有趣功能而言, 这只是冰山一角。例如, 我们想要添加功能来控制如何处理来自微应用程序服务器的HTTP错误响应, 或者如何处理响应速度非常慢的微应用程序-如果一个微应用程序没有运行, 我们不想永远等待服务页面回应!这些主题和其他主题我们将留给另一篇文章。
Yumcha macroapp index.html转换逻辑可以轻松地以无服务器lambda功能的方式实现, 也可以作为服务器框架(如Express或Koa)的中间件来实现。
基于存根的Microapp控件
回到客户端, 我们如何实现微应用程序还有另一个方面, 这对于效率, 延迟加载和无垃圾的呈现非常重要。我们可以为每个微型应用程序生成iframe标记, 可以使用src属性(发出另一个网络请求), 也可以使用srcdoc属性填充服务器为我们填充的内容。但是在这两种情况下, 该iframe中的代码都会立即启动, 包括加载其所有脚本和链接标签, 引导程序以及任何初始API调用和相关的数据处理-即使用户甚至从未访问所涉及的微应用程序。
我们对此问题的解决方案是, 最初将页面上的微型应用程序表示为微小的未激活的存根, 然后将其激活。可以通过使用未充分利用的IntersectionObserver API来查看微型应用程序的区域来驱动激活, 或者更常见的是通过外部发送的预先通知来驱动激活。当然, 我们还可以指定立即激活微应用程序。
无论如何, 只有在激活了微型应用程序后, iframe才会真正呈现, 并且其代码将被加载和执行。就我们使用LitElement的实现而言, 并假设激活状态由激活的实例变量表示, 我们将得到以下内容:
render() {
if (!this.activated) return html`{this.placeholder}`;
else return html`
<iframe srcdoc="${this.content}" @load="${this.markLoaded}"></iframe>`;
}
微型应用间通信
尽管根据定义, 构成宏应用程序的微应用程序是松散耦合的, 但它们仍需要能够相互通信。例如, 导航微应用程序将需要发出通知, 告知用户刚刚选择的某个其他微应用程序应被激活, 并且要激活的应用程序需要接收此类通知。
根据我们的极简主义思想, 我们希望避免引入很多传递消息的机制。相反, 本着Web组件的精神, 我们将使用DOM事件。我们提供了一个简单的广播API, 该API会向所有存根预先通知即将发生的事件, 等待激活该事件类型的所有已请求激活的事件, 然后将事件分派给文档, 任何microapp都可以侦听该文档它。假设我们所有的iframe都是同源的, 那么我们可以从iframe延伸到页面, 反之亦然, 以查找触发事件的元素。
路由
在当今时代, 我们所有人都希望SPA中的URL栏能够代表应用程序的视图状态, 因此我们可以剪切, 粘贴, 邮件, 文本和链接到它, 以直接跳转到应用程序内的页面。但是, 在微型前端应用程序中, 应用程序状态实际上是状态的组合, 每个微型应用程序对应一个状态。我们如何代表和控制这一点?
解决方案是将每个微型应用程序的状态编码为一个复合URL, 并使用小型的macroapp路由器, 该路由器知道如何将该复合URL放在一起并将其分开。不幸的是, 这要求每个微型应用程序都具有特定于Yumcha的逻辑:从宏应用程序路由器接收消息并更新微型应用程序的状态, 反之则是通知宏应用程序路由器该状态的变化, 以便可以更新复合URL。例如, 可以想象Angular的YumchaLocationStrategy或React的<YumchaRouter>元素。
非iframe案例
如上所述, 在iframe中托管微应用程序确实存在一些缺点。有两种选择:直接将它们直接内嵌在页面的HTML中, 或将它们放置在影子DOM中。两种选择都在某种程度上反映了iframe的利弊, 但有时以不同的方式。
例如, 必须以某种方式合并单个microapp CSP策略。假设屏幕阅读器等辅助技术支持影子DOM(目前还不是全部), 它们应该比iframe更好。使用应用程序工作人员”范围”概念注册微应用程序的服务工作人员应该很简单, 尽管该应用程序必须确保其服务人员是使用该应用程序的名称而不是” /”注册的。与iframe相关的布局问题均不适用于内联或阴影DOM方法。
然而, 使用诸如Angular和React之类的框架构建的应用程序可能不满意地生活在内联或影子DOM中。对于这些, 我们可能会想要使用iframe。
内联和影子DOM方法在CSS方面有所不同。 CSS将被完全封装在影子DOM中。如果由于某种原因我们确实想与影子DOM在外部CSS上共享, 则必须使用可构造的样式表或类似的样式表。使用内联的微型应用程序, 所有CSS将在整个页面中共享。
最后, 在<yumcha-portal>中实现内联和影子DOM微型应用程序的逻辑很简单。我们从服务器逻辑将其插入到页面中作为HTML <template>元素的位置检索给定微应用程序的内容, 将其克隆, 然后将其附加到LitElement所谓的renderRoot上, 后者通常是该元素的影子DOM, 但可以对于内联(非阴影DOM)情况, 也可以将其设置为元素本身(this)。
可是等等! microapp服务器提供的内容是整个HTML页面。我们不能将带有html, head和body标签的microapp的HTML页面插入到macroapp的HTML页面中, 可以吗?
我们利用模板标签的怪癖来解决此问题, 在模板标签中包装了从microapp服务器检索到的microapp内容。事实证明, 当现代浏览器遇到模板标记时, 尽管它们不”执行”模板, 但仍会对其进行解析, 从而删除了诸如<html>, <head>和<body>标记之类的无效内容。 , 同时保留其内部内容。因此, 将保留<head>中的<script>和<link>标记以及<body>的内容。这正是我们想要将microapp内容插入页面的目的。
微前端架构:细节决定成败
如果(a)事实证明微型前端是一种更好的架构方法, 并且(b)我们可以弄清楚如何以满足当今网络无数实际需求的方式实施它们, 微型前端将在Webapp生态系统中扎根。
关于第一个问题, 没有人声称微前端是适用于所有用例的架构。特别是, 一个团队没有什么理由采用微前端来进行绿地开发。我将问一个问题:在哪种类型的上下文中, 哪种类型的应用程序可以从微前端模式向其他评论者受益最大。
在实现和可行性方面, 我们已经看到了很多细节需要关注, 其中包括本文中甚至没有提到的一些细节, 特别是身份验证和安全性, 代码重复和SEO。尽管如此, 我希望本文为微前端提供一种基本的实现方法, 通过进一步完善, 可以满足实际需求。
参考书目
- 微型前端-做成角形-第1部分
- 微型前端—做角形—第二部分
- 使用微前端开发AngularJS应用程序
- 微前端
- UI微服务-反转反模式(微前端)
- UI微服务-”反模式”?
- 我强烈建议使用Micro-Frontends建立页面采用类似于Yumcha的反向代理和SSI方法。
- 微前端资源
- 讲台
- 我不了解微前端。这是对微前端体系结构和用例的很好的概述。
- 使用Vue.js, AWS Lambda和Hypernova的无服务器微前端
- 微型前端:一个很棒的, 全面的概述。