8

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

 3 years ago
source link: https://my.oschina.net/taogang/blog/5018575
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

什么是微前端?

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

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

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

单体前端

6c79e5d071424cf58c589301e5c0cc76

垂直组织

f43ee9945f4d4ca1b64c5ebde1d0792a

什么是现代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冲突,按需加载资源,在团队之间共享公共资源,处理数据获取并考虑为用户提供良好的加载状态。我们将一步一步地涉及这些主题。

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

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

aec03b4c6d7447a084df8de51f8aae20

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

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

客户端整合

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

8b07ee53c5874bf39bb862ae901dad88

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

团队产品决定要包括的功能以及在布局中的位置。该页面包含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元素的所有属性和方法,例如innerHTML或getAttribute()。

3fea58ae016f4c57b0cd93523c212caf

命名元素时,规范定义的唯一要求是名称必须包含破折号(-),以保持与即将到来的新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),则将由算法自动完成。

b3601955c7134879ac1e0aa975a08124

为此,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机制,其中组件可以发布消息,而其他组件可以订阅特定主题。幸运的是,浏览器内置了此功能。这正是浏览器事件怎么样click,select或者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功能,upstream并location为每个小组添加了and块,以确保所有以开头的url都/blue将被路由到正确的应用程序(team_blue:3001)。此外,该/路线已映射到红色队,该红色队控制着主页/产品页面。

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

1328ccd5dad64eb386a7b97f0dbe1fe1

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

重新打开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" />将无法正常进行。

d721c9b4ab7440c6b56d0a1ec814924a

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

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

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

8c4c680fad0d49eab8b105704375d4b7

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK