基于webcomponents的实现

什么是Micro Frontends?

Micro Frontends这个术语在2016年底首次出现在ThoughtWorks技术雷达中。它将微服务的概念扩展到前端世界。目前的趋势是构建一个功能丰富且功能强大的浏览器应用程序,即单页面应用程序,它位于微服务架构之上。随着时间的推移,前端层通常由一个单独的团队开发,并且变得越来越难以维护。这就是我们所说的Frontend Monolith

Micro Frontends背后的想法是将网站或Web应用程序视为独立团队拥有的功能组合。每个团队都有一个独特的业务任务****领域,它关注和专注。团队是跨职能的,从数据库到用户界面开发端到端的功能。

然而,这个想法并不新鲜,过去它的名称是垂直系统自包含系统的[前端集成]3。但Micro Frontends显然是一个更友好,更笨重的术语。

单片前端 单片<a href="https://www.w3cdoc.com" target="_blank" rel="noopener noreferrer">前端</a>

垂直组织 具有Micro Frontends的端到端团队

什么是现代Web应用程序?

在介绍中,我使用了“构建现代Web应用程序”这一短语。让我们定义与该术语相关的假设。

为了更广泛地看待这一点,Aral Balkan撰写了一篇关于他称之为Documents-to-Applications Continuum的博客文章。他提出了滑动比例的概念,其中一个由静态文档构建的站点,通过链接连接在左端,一个纯粹的行为驱动,无内容应用程序,如在线照片编辑器在右边

如果您将项目放在此频谱左侧,则在Web服务器级别进行集成是一个不错的选择。使用此模型,服务器从构成用户请求的页面的所有组件中收集和连接HTML字符串。通过从服务器重新加载页面或通过ajax替换部分页面来完成更新。Gustaf Nilsson Kotte撰写了一篇关于这一主题的综合文章

当您的用户界面必须提供即时反馈时,即使在不可靠的连接上,纯服务器渲染的站点也不再足够。要实现Optimistic UISkeleton Screens等技术,您还需要能够在设备上****更新UI 。Google的术语Progressive Web Apps恰当地描述了成为网络的良好公民(渐进增强)的平衡行为,同时还提供类似应用程序的性能。这种应用程序位于site-app-continuum中间的某个位置。这里仅基于服务器的解决方案已不再适用。我们要搬家了集成到浏览器,这是本文的重点。

Micro Frontends背后的核心理念

  • 技术不可知
    每个团队都应该能够选择并升级他们的筹码,而无需与其他团队协调。自定义元素是隐藏实现细节的好方法,同时为其他人提供中性界面。
  • 隔离团队代码
    即使所有团队使用相同的框架,也不要共享运行时。构建自包含的独立应用程序。不要依赖共享的状态或全局变量。
  • 建立团队前缀
    同意在无法实现隔离的命名约定。命名空间CSS,事件,本地存储和Cookie,以避免冲突并澄清所有权。
  • 支持自定义API上的本机浏览器功能
    使用[浏览器事件进行通信,]15而不是构建全局PubSub系统。如果您真的需要构建跨团队API,请尽量保持简单。
  • 构建弹性站点
    即使JavaScript失败或尚未执行,您的功能也应该很有用。使用通用渲染和渐进增强来提高感知性能。

DOM是API

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

但仅限定制元素并不是我们所有需求的解决方案。为了解决渐进增强,通用渲染或路由问题,我们需要额外的软件。

本页面分为两个主要区域。首先,我们将讨论页面组合 - 如何从不同团队拥有的组件中组装页面。之后,我们将展示实现客户端页面转换的示例。

页面组成

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

基础原型

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

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

示例0 - 产品页面 - Plain JS

[尝试在浏览器]21检查码

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

客户整合

在此示例中,页面被拆分为由三个团队拥有的单独组件/片段。Team Checkout(蓝色)现在负责购买流程的所有事项 - 即购买按钮迷你购物篮Team Inspire(绿色)管理此页面上的产品推荐。页面本身由Team Product(红色)拥有。

示例1 - 产品页面 - 组合

[尝试在浏览器]24检查码

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

如何创建自定义元素?

我们购买按钮为例。团队产品包括简单地添加到标记中所需位置的按钮。为此,Team Checkout必须blue-buy在页面上注册元素。

class BlueBuy extends HTMLElement {
  constructor() {
    super();
    this.innerHTML = `<button type="button">buy for 66,00 €</button>`;
  }
  disconnectedCallback() { ... }
}
window.customElements.define('blue-buy', BlueBuy);

现在,每次浏览器遇到新blue-buy标记时,都会调用构造函数。this是对自定义元素的根DOM节点的引用。所有属性和一个标准的DOM元素的方法等innerHTML或getAttribute()可被使用。

行动中的自定义元素

在命名元素时,规范定义的唯一要求是名称必须**包含短划线( - )**以保持与即将推出的新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旧元素被同步调用提供的元素与收拾东西像事件侦听器的机会。之后,调用constructor新创建的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'];
  }
  constructor() {
    super();
    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()引入了一个从constructor和调用的方法attributeChangedCallback。此方法收集所需数据,innerHTML收集新标记。当决定在Custom Element中使用更复杂的模板引擎或框架时,这就是它的初始化代码所在的位置。

浏览器支持

上面的示例使用了Chrome,Safari和Opera目前支持的Custom Element V1 Spec 。但是使用document-register-element,可以在所有浏览器中使用轻量且经过实战考验的polyfill。在引擎盖下,它使用广泛支持的 Mutation Observer API,因此在后台没有看到hacky DOM树。

框架兼容性

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

Child-Parent或Siblings Communication / DOM Events

但传递属性对于所有交互来说都是不够的。在我们的示例中,当用户单击“购买”按钮时,迷你篮子应该刷新

这两个片段都由Team Checkout(蓝色)拥有,因此他们可以构建某种内部JavaScript API,让迷你篮子知道按下按钮的时间。但这需要组件实例相互了解,并且也会违反隔离。

更简洁的方法是使用PubSub机制,其中组件可以发布消息,而其他组件可以订阅特定主题。幸运的是,浏览器内置了此功能。这是浏览器究竟是如何的事件,如click,select或mouseover工作。除了本地事件之外,还可以创建更高级别的事件new CustomEvent(…)。事件始终与创建/分派的DOM节点相关联。大多数原生活动也有冒泡。这使得可以监听DOM的特定子树上的所有事件。如果要监听页面上的所有事件,请将事件侦听器附加到window元素。以下是blue🧺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🧺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🧺changed', this.refresh);
  }
  refresh() {
    // fetch new data and render it
  }
  disconnectedCallback() {
    window.removeEventListener('blue🧺changed', this.refresh);
  }
}

通过这种方法,迷你篮子片段为DOM元素添加了一个监听器,该元素超出了其范围(window)。这应该适用于许多应用程序,但如果您对此感到不舒服,您还可以实现一种方法,其中页面本身(Team Product)侦听事件并通过调用refresh()DOM元素通知迷你篮子。

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

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

命令式调用DOM方法非常罕见,但可以在视频元素api中找到。如果可能,应优先使用声明性方法(属性更改)。

Serverside渲染/通用渲染

自定义元素非常适合在浏览器中集成组件。但是,当构建可在Web上访问的站点时,初始加载性能很可能很重要,并且用户将看到白屏,直到所有js框架被下载并执行。此外,如果JavaScript失败或被阻止,最好考虑网站会发生什么。Jeremy Keith解释了他的电子书/播客弹性网页设计的重要性。因此,在服务器上呈现核心内容的能力是关键。遗憾的是,Web组件规范根本没有讨论服务器渲染。没有JavaScript,没有自定义元素:(

自定义元素+服务器端包含=❤️ {#custom-elements–server-side-includes–️}

要使服务器呈现工作,前面的示例将被重构。每个团队都有自己的快速服务器,render()也可以通过URL访问自定义元素的方法。

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

自定义元素标记名称用作路径名称 - 属性成为查询参数。现在有一种方法来服务器呈现每个组件的内容。与-Custom Elements 结合使用可以实现与Universal Web Component非常接近的东西:

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

该#include注释是Server Side Includes的一部分,这是大多数Web服务器中都可用的功能。是的,这与将当前日期嵌入我们网站的日子所使用的技术相同。还有一些替代技术,如ESInodesicompoxuretailor,但对于我们的项目,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;启用S​​SI功能,upstream并location为每个团队添加一个和块,以确保所有以其开头的URL /blue将路由到正确的应用程序(team_blue:3001)。此外,/路线映射到团队红色,这是控制主页/产品页面。

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

Serverside渲染 - 禁用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>

重要说明:自定义元素不能自动关闭,因此写入无法正常工作。

回流

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

有不同的选择可以避免像这样烦人的回流。控制页面的团队红色可以固定推荐容器的高度。在响应式网站上,确定高度通常很棘手,因为不同屏幕尺寸可能会有所不同。但更重要的问题是,这种团队间协议在红色和绿色团队之间产生紧密联系。如果团队绿色想要在reco元素中引入额外的子标题,则必须在新高度上与团队红色协调。两个团队都必须同时推出他们的更改,以避免布局中断。

更好的方法是使用称为Skeleton Screens的技术。红队留下green-recosSSI包含在标记中。另外,team green更改其片段的服务器端呈现方法,以便生成内容的原理图版本。该骷髅标记可以重用的实际内容的布局样式的部分。这样它就可以保留所需的空间,实际内容的填充不会导致跳跃。

骨架屏幕

骨架屏幕对于客户端渲染非常有用。当您的自定义元素由于用户操作而插入DOM时,它可以**立即呈现骨架,**直到它需要从服务器获取的数据到达。

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

原文:https://micro-frontends.org/



请遵守《互联网环境法规》文明发言,欢迎讨论问题
扫码反馈

扫一扫,反馈当前页面

咨询反馈
扫码关注
返回顶部