微前端基于webcomponents的实现

什么是Micro Frontends?

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

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

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

单片前端 单片前端

垂直组织 具有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上的本机浏览器功能
    使用浏览器事件进行通信,而不是构建全局PubSub系统。如果您真的需要构建跨团队API,请尽量保持简单。
  • 构建弹性站点
    即使JavaScript失败或尚未执行,您的功能也应该很有用。使用通用渲染和渐进增强来提高感知性能。

DOM是API

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

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

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

页面组成

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

基础原型

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

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

示例0 - 产品页面 - Plain JS

尝试在浏览器检查码

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

客户整合

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

示例1 - 产品页面 - 组合

尝试在浏览器检查码

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

如何创建自定义元素?

让我们以购买按钮为例。团队产品包括简单地添加<blue-buy sku="t_porsche"></blue-buy>到标记中所需位置的按钮。为此,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元素的方法等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旧元素被同步调用提供的元素与收拾东西像事件侦听器的机会。之后,调用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机制,其中组件可以发布消息,而其他组件可以订阅特定主题。幸运的是,浏览器内置了此功能。这是浏览器究竟是如何的事件,如clickselectmouseover工作。除了本地事件之外,还可以创建更高级别的事件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);
  }
}

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

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

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

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

Serverside渲染/通用渲染

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

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

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

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

自定义元素标记名称用作路径名称 – 属性成为查询参数。现在有一种方法来服务器呈现每个组件的内容。与<blue-buy>-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功能,upstreamlocation为每个团队添加一个和块,以确保所有以其开头的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>

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

回流

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

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

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

骨架屏幕

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

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

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

知识共享署名4.0国际许可协议,转载请保留出处; 部分内容来自网络,若有侵权请联系我:前端学堂 » 微前端基于webcomponents的实现

赞 (5) 打赏

评论 0

如果对您有帮助,别忘了打赏一下宝宝哦!

支付宝扫一扫打赏

微信扫一扫打赏