如何用 Web Components + 服务端渲染实现微前端

微前端是一种使多个团队能够独立开发一个现代 web 应用的技术,策略或者方法。这项技术源自于微服务。

什么是微前端
2016年末微前端在 ThoughtWorks Technology Radar 被第一次提到,它将微服务的概念应用到前端。现在比较流行的方式是创建一个基于微服务的功能丰富并且功能强大的浏览器应用,它被称为单页应用。但是前端层面上,这个应用只由一个前端团队开发,随着不断迭代,维护变得越来越困难,这被我们称为前端巨石应用。

微前端背后的理念是将一个网站或者 web 应用分为多个功能,不同的团队开发他们各自的功能。每个团队都有自己关心和擅长的业务或任务领域。

但是这并不是一个新的概念,它与 Self-contained Systems 有很多共同之处。在过去,这种处理方法被称为垂直系统的前端集成。

单体前端
如何用 Web Components + 服务端渲染实现微前端_第1张图片
垂直划分
如何用 Web Components + 服务端渲染实现微前端_第2张图片
微前端背后的核心理念
技术无关
每个团队可以选择或者更新他们自己的技术栈,并且不对其他团队造成影响。

隔离团队代码
不共享运行时,即便使用相同的框架。应用程序独立构建,并且不依赖共享状态和全局变量。

建立团队前缀
在不可能实现绝对隔离的情况下,使用团队前缀达成隔离的目的。例如:通过给 CSS、Events、Local Storage 和 Cookies 加前缀的方式避免冲突和明确所有权。

使用浏览器特性而非自定义 API
使用浏览器自定义事件进行通信,而非建立全局的发布订阅系统。如果不得不建立一个跨团队的 API,尽量保持简单。

建立一个有弹性的网站
即便 JS 出错或者禁止执行,也要让你的功能是可用的。

这篇文章分为两个主要部分。首先我们讨论页面合成,即:怎么用不同团队开发组件组成一个页面。之后,我们将演示实现客户端转换的例子。

页面合成
除了整合用不同框架写的客户端,服务端代码之外,我们需要商讨如何隔离 JS、避免 CSS 冲突、加载应用所需的资源、共享公共资源以及处理数据抓取,同时我们还需要考虑如何在加载状态给用户一个良好的体验。

基本例子
下面将使用拖拉机商品店的产品页作为例子。这里的功能是在三个不同的拖拉机模型中进行切换,并且每一次切换都会改变拖拉机的图片、名称、价格和与这个拖拉机相关的推荐商品。另外这儿还有一个购买按钮,点击购买按钮会将选择的商品添加到购物车,同时顶部的购物车中的商品数量也会得到相应更新。

在第一版代码中所有的 HTML 都使用 JS 和 ES6 模版字符串生成,每一次切换拖拉机会重新渲染整个 HTML,代码被写在一个 js/css 文件中,所有的功能由同一个团队开发。

功能划分
在拖拉机商品店的产品页的例子中,我们将整个页面的功能分为由三个不同的团队完成。团队 A (blue) 负责购买流程,即:蓝色虚线的部分,团队 B(green) 负责这个页面的商品推荐区域,即:绿色虚线的部分,这个页面本身由团队 C(red) 负责,即:红色虚线的区域。
如何用 Web Components + 服务端渲染实现微前端_第3张图片
团队 C (red) 决定这个页面应该包含那些功能以及页面布局。这个页面中包含团队 C 自己负责的内容,例如:产品图片、名称和可选模型,同时也包含由其他团队负责的内容。

在这个例子中,我们使用 Custom Elements 创建组件。

创建自定义元素
在这里以购买按钮为例,团队 C(red) 将 添加到页面的指定位置,为了让按钮能够被正常使用,团队 A(blue) 必须在页面上注册 blue-buy 。

class BlueBuy extends HTMLElement {
connectedCallback() {
this.innerHTML = ;
}

disconnectedCallback() { … }
}
window.customElements.define(‘blue-buy’, BlueBuy);
复制代码
现在每当浏览器遇到 blue-buy 标签, connectedCallback 就会调用,并且 this 为自定义元素的根节点,它可以使用浏览器内置 DOM 元素的所有方式和属性,例如: innerHTML , getAttribute() 。
自定义元素的元素名中必须包含一个短横线(-),在接下来的例子中,我们将自定义元素的命名约定为:团队名-功能。团队的命名空间可以防止冲突,并且只需查看 DOM 就能知道这个功能由哪个团队负责。

通信
有两种实现通信的方式,第一种方式是通过向下传递属性的方式进行通信,另一种方式是使用订阅-发布机制进行通信。

向下传递属性的方式进行通信
当用户切换拖拉机模型时,购买按钮也必须做出相应的更新。为了实现这个功能,团队 C(red) 可以用一个新的元素替换掉已经存在的元素。

/* container.innerHTML;
= … */
container.innerHTML = ‘’;
复制代码
被移除的元素的 disconnectedCallback 会被同步触发,在 disconnectedCallback 中可以做一些清理工作,例如移除事件监听。在那之后新创建的 t_fendt 元素的 connectedCallback 将会被调用。

另一个性能更好的做法是:更新已有元素的 sku 属性值。

document.querySelector(‘blue-buy’).setAttribute(‘sku’, ‘t_fendt’);
复制代码
为了支持这个功能,团队 A (blue) 在实现自定义元素 blue-buy 时,需要给 blue-buy 定义 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 = ;
}
attributeChangedCallback(attr, oldValue, newValue) {
this.render();
}
disconnectedCallback() {…}
}
window.customElements.define(‘blue-buy’, BlueBuy);
复制代码
为了避免代码重复,我们定义了一个 render 方法,它是从 connectedCallback 和 attributeChangedCallback 中调用的。这个方法用于渲染出新的 DOM。

使用订阅-发布机制进行通信
通过向下传递属性的方式进行通信,在某些情况下效率太低。在我们的例子中,当用户点击了购买按钮, mini basket 组件应该更新。 mini basket 和购买按钮都由团队 A (blue) 开发,所以他们能够创建一些内部 API 让 mini basket 知道按钮被点击了,这要求组件之间相互了解,违反了隔离原则。

一个好的方式是使用订阅-发布机制,任何位置的组件都可以发布消息并且其他组件可以订阅这个消息,幸运的是,浏览器原生支持这个功能,我们可以使用 new CustomEvent(…) 创建自定义事件,它类似于浏览器的 click,select 和 mouseover 等事件。由于大多数原生事件都支持冒泡,这使得侦听 DOM 特定子树上的所有事件成为可能。如果你想监听页面上的所有事件,可以将事件监听器绑定到 window 上。接下来,在这个例子中我们创建一个 blue:basket:changed 事件。

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 = ;
}
disconnectedCallback() {
this.firstChild.removeEventListener(‘click’, this.addToCart);
}
}
复制代码
mini basket 组件现在可以在 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);
}
}
复制代码
在这个例子中 mini basket 向其作用域之外的 DOM 元素上添加事件监听器,在大多数情况下这样做没有问题,如果你不想这样做,你也可以让团队 C (red) 负责的页面本身去监听事件然后通知 mini basket 更新,如下所示:

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

$(‘blue-buy’)[0].addEventListener(‘blue:basket:changed’, function() {
$(‘blue-basket’)[0].refresh();
});
复制代码
服务端渲染
在浏览器中使用自定义元素集成组件是一种很好的方式。但是,当构建一个 web 可访问的站点时,初始加载性能很可能会影响到用户,在所有js下载并执行之前,用户会看到一个白屏。并且我们最好思考一下,如果 js 被阻止执行或者发生错误我们网站会怎么样。在服务端渲染出网站的核心内容可以解决 js 被阻止或执行错误的问题,但是 web component 规范没有谈到服务器渲染。无 js,无自定义元素。

web components + 服务端渲染
为了让服务器渲染能够工作,在这个例子中我们使用代码同构的方式。每一个团队都有他们自己的 express 服务器,并且需要满足每个自定义元素的 render() 方法可以通过 url 进行访问。

$ curl http://127.0.0.1:3000/blue-buy?sku=t_porsche

buy for 66,00 €
复制代码
自定义元素的标签名作为路径名,属性作为查询参数。这是一种服务端渲染每个组件内容的方式,结合 服务端渲染的实现方式,可以得到有一种通用的服务端渲染 Web Component 的方式。

复制代码 #include 属于服务器端包含(Server Side Includes)的一部分,这是大多数 web 服务器中可用的特性,这和在我们的网站上嵌入当前数据的技术是一样的,这儿也有一些可选的技术,例如:ESI、 nodesi 、 compoxure 和 tailor 。

在我们的例子中 Server Side Includes 是一种简单并且稳定的方案。在 web 服务器将完整的页面发送到浏览器之前,#include 部分会被 /blue-buy?sku=t_porsche 的内容替换。我们像如下这样配置 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; 表示开启 Server Side Includes 功能并且给每个团队都配置了 upstream 和 location 。
每次切换拖拉机模型都会让页面重新加载。右边的终端展示了如何将页面的请求路由到 team red 的过程,然后请求 team blue and green 的组件填充到它们相应的位置。

当开启浏览器的 JS 功能,在服务器日志信息中只能看到第一次的请求。之后切换拖拉机模型都由客户端处理,产品数据将通过 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 文件被映射到各个容器中,当您进行代码更改时,node 应用程序将重新启动。

修改 nginx 的配置文件之后,必须重新 docker-compose 配置才会生效。

数据获取和加载状态
使用 Server Side Includes 的缺点是,最慢的组件片段决定了整个页面的响应时间。将组件片段进行缓存是一个值得推荐的做法。将那些生产成本高并且难以被缓存的组件片段从初始化渲染中移除,在浏览器中异步加载它们,是一个很好的方式。

在我们的例子中 团队 B (green) 的 green-recos 用于展示每种拖拉机模型的推荐产品,我们可以将 green-recos 从初始化渲染中移除。做法如下:

before

复制代码 now


复制代码
可以添加前端学习群:1017810018 大家一起学习(群主会不定时更新学习资料,以及面试题文档)

你可能感兴趣的:(前端,前端·)