微前端,将微服务理念扩展到前端开发

原创
04/14 14:09
阅读数 2W

多个可以独立发布功能的团队构建现代Web应用程序的技术,策略和方法。

什么是微前端?

微前端”一词最早于2016年底在ThoughtWorks Technology Radar中提出。它将微服务的概念扩展到了前端世界。当前的趋势是构建一个功能强大且功能强大的浏览器应用程序(也称为单页应用程序),该应用程序位于微服务架构之上。随着时间的流逝,通常由一个单独的团队开发的前端层会不断增长,并且变得更加难以维护。这就是我们所谓的Frontfront Monolith。

Micro Frontends背后的想法是将网站或Web应用程序视为由独立团队拥有的功能的组合。每个团队都有自己关心和专长的不同业务任务领域。一个团队是跨职能的,并且从数据库到用户界面,端到端地开发其功能。

但是,这个想法并不新鲜。它与“独立系统”概念有很多共同点。在过去,类似的方法被称为“垂直系统的前端集成”。但是Micro Frontends显然是一个更友好,更轻巧的术语。

单体前端

 

垂直组织

 

什么是现代Web应用程序?

在引言中,我使用了“构建现代的Web应用程序”一词。让我们定义与此术语相关的假设。

为了更广泛地理解这一点,Aral Balkan写了一篇博客文章,介绍了他所说的“文档到应用程序连续体”。他用滑尺的概念上来,其中一个网站,内置了静态的文档,通过链路连接,是在左侧端和纯粹的行为驱动的,无内容的应用就像一个在线照片编辑器右侧

如果您将项目放置在此范围左侧,则非常适合在Web服务器级别进行集成。使用此模型,服务器从构成用户请求页面的所有组件中收集并连接HTML字符串。更新是通过从服务器重新加载页面或通过ajax替换页面的一部分来完成的。古斯塔夫·尼尔森·科特(Gustaf Nilsson Kotte)撰写了有关该主题的综合文章。

当您的用户界面必须提供即时反馈(即使是在不可靠的连接上)时,仅由服务器呈现的站点已不再足够。要实现诸如“乐观UI”或“骨架屏幕”之类的技术,您还需要能够在设备本身上更新UI 。 Google的“渐进式Web应用程序”一词恰当地描述了成为网络良好公民(渐进式增强功能)的平衡行为,同时还提供了类似于应用程序的性能。这种应用程序位于site-app-continuum中间的某个位置。在这里,仅基于服务器的解决方案已不再足够。我们必须移动集成到浏览器中,这是本文的重点。

微型前端背后的核心思想


  • 不受技术影响每个团队都应该能够选择和升级其堆栈,而无需与其他团队进行协调。自定义元素是一种隐藏实现细节,同时为其他人提供中立接口的好方法。
  • 隔离团队代码
    即使所有团队都使用相同的框架,也不要共享运行时。构建自包含的独立应用程序。不要依赖共享状态或全局变量。
  • 建立团队前缀
    同意尚无法隔离的命名约定。命名空间CSS,事件,本地存储和Cookies,以避免冲突并阐明所有权。
  • 与自定义API相比,本机浏览器功能更受青睐
    使用浏览器事件进行通信,而不是构建全局的PubSub系统。如果您确实必须构建跨团队API,请尝试使其尽可能简单。
  • 建立弹性站点
    即使JavaScript失败或尚未执行,您的功能也应该很有用。使用通用渲染和渐进增强来改善可感知的性能。

DOM是API

Web组件规范中的互操作性方面,Custom Elements是在浏览器中集成的一个很好的原语。每个团队建立他们的组件使用他们所选择的网络技术,并把它包装自定义元素中(如<order-minicart></order-minicart>)。该特定元素(标记名,属性和事件)的DOM规范充当其他团队的合同或公共API。优点是他们可以使用组件及其功能,而无需了解实现。他们只需要能够与DOM交互即可。

但是,仅自定义元素并不能解决我们所有需求。为了解决渐进增强,通用渲染或路由问题,我们需要其他软件。

该页面分为两个主要区域。首先,我们将讨论页面组成-如何根据不同团队拥有的组件来组装页面。之后,我们将展示实现客户端Page Transition的示例。

页面组成

除了使用不同框架本身编写的代码的客户端服务器端集成之外,还应该讨论很多附带主题:隔离js的机制,避免css冲突,按需加载资源,在团队之间共享公共资源,处理数据获取并考虑为用户提供良好的加载状态。我们将一步一步地涉及这些主题。

基本原型

该型号拖拉机商店的产品页面将作为以下示例的基础。

它具有一个变体选择器,可以在三种不同的拖拉机型号之间进行切换。更改产品图片时,名称,价格和建议会更新。还有一个“购买”按钮,将选择的变体添加到购物篮中,并在顶部增加一个迷你购物篮,并相应地进行更新。

 

在浏览器中尝试并检查代码

所有HTML都是使用纯JavaScript没有依赖项的ES6模板字符串在客户端生成的。该代码使用简单的状态/标记分离,并在每次更改时重新呈现整个HTML客户端-无需花哨的DOM扩散,并且目前还没有通用呈现。也没有团队分离-代码被编写在一个js / css文件中。

客户端整合

在此示例中,页面分为三个团队拥有的单独的组件/片段。Team Checkout(蓝色)现在负责与购买过程有关的所有事情,即“购买”按钮迷你购物篮Team Inspire(绿色)在此页面上管理产品推荐。该页面本身归团队产品(红色)所有。

 

在浏览器中尝试并检查代码

团队产品决定要包括的功能以及在布局中的位置。该页面包含Team Product本身可以提供的信息,例如产品名称,图像和可用的变体。但是它也包括其他团队的片段(“自定义元素”)。

如何创建自定义元素?

让我们以“购买”按钮为例。团队产品包括仅添加<blue-buy sku="t_porsche"></blue-buy>到标记中所需位置的按钮。为此,Team Checkout必须blue-buy在页面上注册该元素。

class BlueBuy extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<button type="button">buy for 66,00 €</button>`;
  }

  disconnectedCallback() { ... }
}
window.customElements.define('blue-buy', BlueBuy);

现在,每次浏览器遇到新blue-buy标签时,connectedCallback都会调用。this是对自定义元素的根DOM节点的引用。可以使用标准DOM元素的所有属性和方法,例如innerHTMLgetAttribute()

 

命名元素时,规范定义的唯一要求是名称必须包含破折号(-),以保持与即将到来的新HTML标记的兼容性。在接下来的示例中,将[team_color]-[feature]使用命名约定。团队名称空间可防止冲突,因此只需查看DOM,就可以清楚地了解功能的所有权。

亲子沟通/ DOM修改

当用户在变型选择器中选择另一台拖拉机时,必须相应地更新购买按钮。要实现此团队产品,只需从DOM中删除现有元素,然后插入一个新元素即可。

container.innerHTML;
// => <blue-buy sku="t_porsche">...</blue-buy>
container.innerHTML = '<blue-buy sku="t_fendt"></blue-buy>';

disconnectedCallback旧元素被同步调用提供的元素与收拾东西像事件侦听器的机会。之后,将调用connectedCallback新创建的t_fendt元素的。

另一个性能更高的选项是仅更新sku现有元素上的属性。

document.querySelector('blue-buy').setAttribute('sku', 't_fendt');

如果Team Product使用具有DOM差异功能的模板引擎(如React),则将由算法自动完成。

 

为此,Custom Element可以实现attributeChangedCallback并指定observedAttributes应为其触发该回调的列表。

const prices = {
  t_porsche: '66,00 €',
  t_fendt: '54,00 €',
  t_eicher: '58,00 €',
};

class BlueBuy extends HTMLElement {
  static get observedAttributes() {
    return ['sku'];
  }
  connectedCallback() {
    this.render();
  }
  render() {
    const sku = this.getAttribute('sku');
    const price = prices[sku];
    this.innerHTML = `<button type="button">buy for ${price}</button>`;
  }
  attributeChangedCallback(attr, oldValue, newValue) {
    this.render();
  }
  disconnectedCallback() {...}
}
window.customElements.define('blue-buy', BlueBuy);

为了避免重复,render()引入了一种方法,该方法从connectedCallback和中调用attributeChangedCallback。此方法收集所需的数据,并收集innerHTML的新标记。当决定在Custom Element中使用更复杂的模板引擎或框架时,这里就是初始化代码的地方。

浏览器支持

上面的示例使用了Chrome,Safari和Opera当前支持的Custom Element V1 Spec 。但是,通过文档注册元素,可以使用轻量级且经过战斗测试的polyfill来使其在所有浏览器中都能正常工作。在后台,它使用了广泛支持的Mutation Observer API,因此在后台不会出现任何骇人的DOM树。

框架相容性

由于自定义元素是一种网络标准,因此所有主要的JavaScript框架(例如Angular,React,Preact,Vue或Hyperapp)都支持它们。但是,当您进入细节时,某些框架中仍然存在一些实现问题。Rob Dodson在“无处不 在的自定义元素”中放出了一个兼容性测试套件,重点突出了未解决的问题。

亲子或兄弟姐妹交流/ DOM事件

但是,传递属性不足以进行所有交互。在我们的示例中,当用户单击“购买”按钮时,迷你购物篮应刷新

这两个片段都属于Team Checkout(蓝色)所有,因此它们可以构建某种内部JavaScript API,该API可使迷你购物篮知道何时按下按钮。但这将要求组件实例彼此了解,并且也将成为隔离冲突。

较干净的方法是使用PubSub机制,其中组件可以发布消息,而其他组件可以订阅特定主题。幸运的是,浏览器内置了此功能。这正是浏览器事件怎么样clickselect或者mouseover工作。除了本地事件,还可以使用创建更高级别的事件new CustomEvent(...)。事件始终与创建/调度事件的DOM节点相关。大多数本机事件还具有起泡功能。这样就可以侦听DOM特定子树上的所有事件。如果要侦听页面上的所有事件,请将事件侦听器附加到window元素。在blue:basket:changed示例中,-event的创建如下所示:

class BlueBuy extends HTMLElement {
  [...]
  connectedCallback() {
    [...]
    this.render();
    this.firstChild.addEventListener('click', this.addToCart);
  }
  addToCart() {
    // maybe talk to an api
    this.dispatchEvent(new CustomEvent('blue:basket:changed', {
      bubbles: true,
    }));
  }
  render() {
    this.innerHTML = `<button type="button">buy</button>`;
  }
  disconnectedCallback() {
    this.firstChild.removeEventListener('click', this.addToCart);
  }
}

迷你购物篮现在可以订阅此事件,window并在刷新数据时得到通知。

class BlueBasket extends HTMLElement {
  connectedCallback() {
    [...]
    window.addEventListener('blue:basket:changed', this.refresh);
  }
  refresh() {
    // fetch new data and render it
  }
  disconnectedCallback() {
    window.removeEventListener('blue:basket:changed', this.refresh);
  }
}

通过这种方法,迷你购物篮片段向其范围(window)之外的DOM元素添加了一个侦听器。对于许多应用程序这应该可以,但是如果您对此感到不舒服,则还可以实现一种方法,其中页面本身(团队产品)侦听事件并通过调用refresh()DOM元素通知迷你购物篮。

// page.js
const $ = document.getElementsByTagName;

$('blue-buy')[0].addEventListener('blue:basket:changed', function() {
  $('blue-basket')[0].refresh();
});

强制性地调用DOM方法并不常见,但是例如可以在video元素api中找到。如果可能,应首选使用声明式方法(属性更改)。

服务器端渲染/通用渲染

自定义元素非常适合在浏览器内部集成组件。但是,当构建一个可在Web上访问的站点时,初始负载性能很可能会变得很重要,并且在下载并执行所有js框架之前,用户将看到白屏。另外,最好考虑一下如果JavaScript失败或被阻止,站点将发生什么情况。杰里米·基思(Jeremy Keith)在他的电子书/播客“弹性Web设计”中解释了其重要性。因此,在服务器上呈现核心内容的能力是关键。可悲的是,Web组件规范根本没有涉及服务器渲染。没有JavaScript,没有自定义元素:(

自定义元素+服务器端包含=❤️

为了使服务器渲染正常工作,重构了前面的示例。每个团队都有自己的快递服务器,render()还可以通过url访问Custom Element的方法。

$ curl http://127.0.0.1:3000/blue-buy?sku=t_porsche
<button type="button">buy for 66,00 €</button>

“自定义元素”标记名称用作路径名-属性成为查询参数。现在,有一种方法可以通过服务器呈现每个组件的内容。与<blue-buy>-Custom Elements结合使用,可以实现与通用Web组件非常接近的功能:

<blue-buy sku="t_porsche">
  <!--#include virtual="/blue-buy?sku=t_porsche" -->
</blue-buy>

#include注释是“服务器端包含”的一部分,该功能在大多数Web服务器中都可用。是的,这与过去将当前日期嵌入到我们网站上的技术相同。还有像一些替代技术ESI,nodesi,compoxure和裁缝,但对于我们的项目SSI已经证明了自己作为一个简单且非常稳定的解决方案。

#include评论被替换的响应/blue-buy?sku=t_porsche之前,Web服务器发送完整的网页浏览器。nginx中的配置如下所示:

upstream team_blue {
  server team_blue:3001;
}
upstream team_green {
  server team_green:3002;
}
upstream team_red {
  server team_red:3003;
}

server {
  listen 3000;
  ssi on;

  location /blue {
    proxy_pass  http://team_blue;
  }
  location /green {
    proxy_pass  http://team_green;
  }
  location /red {
    proxy_pass  http://team_red;
  }
  location / {
    proxy_pass  http://team_red;
  }
}

该指令ssi: on;启用了SSI功能,upstreamlocation为每个小组添加了and块,以确保所有以开头的url都/blue将被路由到正确的应用程序(team_blue:3001)。此外,该/路线已映射到红色队,该红色队控制着主页/产品页面。

此动画显示了在禁用了JavaScript的浏览器中的拖拉机商店。

 

检查代码

现在,变体选择按钮是实际的链接,每次单击都会导致页面重新加载。右侧的终端说明了如何将页面请求路由到团队红色,该团队控制产品页面,然后标记由团队蓝色和绿色组成的片段进行补充。

重新打开JavaScript时,仅第一个请求的服务器日志消息将可见。像第一个示例一样,所有后续的拖拉机更改都在客户端进行处理。在后面的示例中,将根据需要从JavaScript中提取产品数据并通过REST API进行加载。

您可以在本地计算机上使用此示例代码。仅需要安装Docker Compose。

git clone https://github.com/neuland/micro-frontends.git
cd micro-frontends/2-composition-universal
docker-compose up --build

然后,Docker在端口3000上启动nginx并为每个团队构建node.js映像。在浏览器中打开http://127.0.0.1:3000/时,您应该会看到一个红色的拖拉机。结合在一起的日志docker-compose可以很容易地查看网络中发生了什么。遗憾的是,无法控制输出颜色,因此您必须忍受这样一个事实,即团队蓝色可能会以绿色突出显示:)

这些src文件被映射到各个容器中,并且当您更改代码时,节点应用程序将重新启动。更改nginx.conf要求重新启动docker-compose才能生效。因此,随时随处摆弄并提供反馈。

数据获取和加载状态

SSI / ESI方法的缺点是,最慢的片段确定整个页面的响应时间。因此,可以缓存片段的响应是很好的。对于生产成本高昂且难以缓存的片段,通常最好将其从初始渲染中排除。可以将它们异步加载到浏览器中。在我们的示例中green-recos,显示个性化推荐的片段就是这种选择。

一种可能的解决方案是,红色团队仅跳过SSI Include。

<green-recos sku="t_porsche">
  <!--#include virtual="/green-recos?sku=t_porsche" -->
</green-recos>

<green-recos sku="t_porsche"></green-recos>

重要说明:自定义元素不能自动关闭,因此编写<green-recos sku="t_porsche" />将无法正常进行。

 

渲染仅在浏览器中进行。但是,正如可以在动画中可以看出,这种变化现在推出了大幅回流的页面。推荐区域最初是空白的。团队果岭JavaScript已加载并执行。进行了用于获取个性化推荐的API调用。呈现推荐标记,并请求关联的图像。现在,该片段需要更多空间并推动页面布局。

有不同的选择来避免像这样令人讨厌的重排。控制页面的红色小组可以固定建议容器的高度。在响应式网站上,确定高度通常很棘手,因为对于不同的屏幕尺寸,高度可能会有所不同。但是更重要的问题是,这种团队间协议在红色和绿色团队之间建立了紧密的联系。如果绿色团队希望在reco元素中添加其他子标题,则必须与红色团队在新高度上进行协调。两个团队都必须同时推出自己的更改,以避免布局混乱。

更好的方法是使用一种称为“骨架屏幕”的技术。红色小组将green-recosSSI包括在标记中。另外,绿色团队更改了其片段的服务器端渲染方法,从而生成了内容示意图。该骷髅标记可以重用的实际内容的布局样式的部分。这样,它可以保留所需的空间,并且实际内容的填充不会导致跳转。

 

骨架屏幕对于客户端渲染非常有用。当您的自定义元素由于用户操作而被插入DOM时,它可以立即渲染框架,直到到达服务器所需的数据为止。

即使在诸如变量选择之类的属性更改上,您也可以决定切换到框架视图,直到新数据到达。这样,用户可以得到片段中正在发生某些事情的指示。但是,当您的端点快速响应时,新旧数据之间的短暂骨架闪烁也可能很烦人。保留旧数据或使用智能超时可以有所帮助。因此,请明智地使用此技术,并尝试获取用户反馈。

 

(本文由翻译自Michael Geers 的文章《Micro Frontends,extending the microservice idea to frontend development》,转载请注明出处,原文链接 https://micro-frontends.org/)

展开阅读全文
打赏
2
27 收藏
分享
加载中
打赏
5 评论
27 收藏
2
分享
返回顶部
顶部