转发:大前端时代下的微前端架构:实现增量升级、代码解耦、独立部署


大前端时代下的微前端架构:实现增量升级、代码解耦、独立部署

想做好前端很难,做出可扩展的前端,从而让多个团队可以同时投身于一项复杂的大型产品项目就更难了。本文将介绍前端领域最近的一项变革:单体前端架构正在过渡到许多较小、较易管理的前端架构。我们还会展示这种新的体系结构怎样提升前端团队的效率和表现。除了讨论这种新趋势的好处与代价外,我们还将介绍一些可行的实现方案,并深入分析一个完整的微前端应用案例。

 

微服务近年来大受欢迎,许多组织转向了微服务以克服大型单体后端架构的局限。但虽然微服务在服务端很流行,很多企业在前端代码库上仍然在沿用问题多多的单体架构。

 

也许你想构建一个渐进式或响应式的 Web 应用,但却找不到一种将这些功能集成进现有代码中的简单途径;也许你想尝试 JavaScript 语言的新功能(或者是其他可以编译为 JS 的某种语言),但你却无法将关键的构建工具融入已有的构建流程;或者你只是想扩展开发流程,让多个团队可以同时开发一种产品,但现有单体架构中的耦合度与复杂性让团队间的合作变得磕磕绊绊。这些都是很现实的问题,都会影响你们向客户交付高质量体验的能力。

 

微前端的定义

最近业界越来越关注复杂的现代化 Web 开发需要怎样的整体架构和组织结构这个问题。于是我们开始看到单体前端正在分解为更小、更简单的模块,这些模块可以各自独立开发、测试和部署,而它们组合在一起仍然对客户表现为一件单一完整的产品。我们将这种技术称为微前端,其定义为:

 

“微前端是一种架构风格,其中众多独立交付的前端应用组合成一个大型整体。”

 

在 ThoughtWorks TechRadar 2016 年第 11 期中,我们将微前端列入了“组织应评估的技术”之列;不久后我们将其提升为“可尝试”级别,最后将其列入“应采用”之列。换句话说,我们认为它是经受住了考验的方法,企业在需要时就应该采用它。详情请点击此处链接。

 

 

图 1:TechRadar 多次提到了微前端

 

我们认为微前端的主要好处有:

 

  •  

  • 更小,更紧密且更易维护的代码库。

  •  

  •  

  • 组织更具扩展能力,其团队更加独立自治。

  •  

  •  

  • 能够以更加增量式的风格来升级、更新前端,甚至重写部分前端代码。

  •  

 

这些核心优势与微服务的优势基本一致,这也不是什么巧合。

 

当然,软件架构领域没有免费的午餐——一切都要付出代价。一些微前端实现可能导致重复依赖,使用户不得不下载更多内容。此外,大幅提升的团队自治水平可能会让各个团队的工作愈加分裂。只不过我们认为这些风险都能控制在合理水平上,微前端终究还是利大于弊的。

 

好处

我们不会从具体的技术方法或实施细节角度来定义微前端,而是重点关注它的属性和好处。

 

增量升级

对于许多组织来说,追求增量升级就是他们迈向微前端的第一步。对他们来说,老式的大型单体前端要么是用老旧的技术栈打造的,要么就充斥着匆忙写成的代码,已经到了该重写整个前端的时候了。一次性重写整个系统风险很大,我们更倾向一点一点换掉老的应用,同时在不受单体架构拖累的前提下为客户不断提供新功能。

 

为了做到这一点,解决方案往往就是微前端架构了。一旦某个团队掌握了在几乎不影响旧世界的同时为生产环境引入新功能的诀窍,其他团队就会纷纷效仿。现有代码仍然需要继续维护下去,但在某些情况下还要继续添加新功能,现在总算有了解决方案。

 

到最后,我们就能更随心所欲地改动产品的各个部分,并逐渐升级我们的架构、依赖关系和用户体验。当主框架发生重大变化时每个微前端模块都可以按需升级,不需要整体下线或一次性升级所有内容。如果我们想要尝试新的技术或互动模式,也能在隔离度更好的环境下做试验。

 

简洁、解耦的代码库

微前端体系下,每个小模块的代码库要比一个单体前端的代码库小很多。对开发者来说这些较小的代码库处理起来更简单方便。而且微前端还能避免无关组件之间不必要的耦合,让代码更简洁。我们可以在应用的限界上下文处划出更明显的界限,更好地避免无意间造成的这类耦合问题。

 

当然,只靠架构更迭本身(比如说“我们改成微前端吧”)并不能自动为以往的优质代码生成替代品。我们要做的是设法让糟糕的决策难以露头,而让正确的决策畅通无阻,从而进入迈向成功的良性循环。例如,现在很难跨越限界上下文共享域模型,所以开发者就不太可能这样做了。类似地,微前端会让开发者更审慎地把握数据和事件在应用的各个部分之间流动的方式,其实就算没有微前端我们本来也应该这样做的!

 

独立部署

就像微服务一样,微前端的一大优势就是可独立部署的能力。这种能力会缩减每次部署涉及的范围,从而降低了风险。不管你的前端代码是在哪里托管,怎样托管,各个微前端都应该有自己的持续交付管道;这些管道可以将微前端构建、测试并部署到生产环境中。我们在部署各个微前端时几乎不用考虑其他代码库或管道的状态;就算旧的单体架构采用了固定、手动的按季发布周期,或者隔壁的团队在他们的主分支里塞进了一个半成品或失败的功能,也不影响我们的工作。如果某个微前端已准备好投入生产,那么它就能顺利变为产品,且这一过程完全由开发和维护它的团队主导。

 

 

图 2:各个微前端都能独立部署到生产环境中

 

自治团队

解藕代码库、分离发布周期还能带来一个高层次的好处,那就是大幅提升团队的独立性;一支独立的团队可以自主完成从产品构思到最终发布的完整流程,有足够的能力独立向客户交付价值,从而可以更快、更高效地工作。为了实现这一目标需要围绕垂直业务功能,而非技术功能来打造团队。一种简单的方法是根据最终用户将看到的内容来划分产品模块,让每个微前端都封装应用的某个页面,并分配给一个团队完整负责。相比围绕技术或“横向”问题(如样式、表单或验证)打造的团队相比,这种团队能有更高的凝聚力。

 

 

图 3:每个应用都由一个团队完整负责

 

小结

简而言之,微前端是将庞大复杂的整体分割为更小、更易于管理的模块,然后明确它们之间的依赖关系。我们的技术决策、代码库、团队和发布流程都应该彼此独立,无需过多协调工作就能自主运行并发展。

 

案例

假设要做一个食品外卖的网站。乍一看这种网站好像很好做,但想要做好需要在诸多细节上下足功夫:

 

  •  

  • 应该有一个引导页面,让顾客浏览并搜索餐馆。顾客应该能按照一系列参数(包括价格、菜品或订购历史等)来搜索并过滤餐馆。

  •  

  •  

  • 每家餐馆都要有自己的页面,页面中要展示菜单,允许客户自主选餐,还要有折扣、套餐和特殊要求选项。

  •  

  •  

  • 顾客应该有自己的主页,可以用来查看订单历史、跟踪外卖进度并自定义付款选项

  •  

 

 

图 4:外卖网站可能有几个相当复杂的页面

 

每个页面都非常复杂,都应该分配一个专门团队来负责,并且每个团队都应该有足够的独立性。各个团队都应该能独立开发、测试、部署和维护自己的代码,而不会与其他团队发生冲突或需要其他团队配合。但在客户这里,整个网站仍然应该是一个无缝的整体。

 

下面我们就会围绕这个案例来展示代码与场景示例。

 

集成方法

前文对微前端的定义相当松散,所以有很多方法都可以划入这个范畴。本节将展示一些示例并讨论它们的优劣。这些方法在架构上有共通之处——通常应用中的每个页面都有一个微前端,还有一个容器应用,它有以下功能:

 

  •  

  • 呈现常见的页面元素,如页眉和页脚。

  •  

  •  

  • 解决了身份认证和跳转等跨领域问题。

  •  

  •  

  • 在页面上集成多个微前端,并告诉各个微前端该何时何地呈现自己。

  •  

 

 

图 5:一般来说可以从页面的可视结构出发来建立架构

 

服务器端模板组合

先来介绍一种非常新颖的前端开发方法——就是在服务器上使用多个模板或片段呈现 HTML。首先我们要有一个 index.html,其中包含所有常见的页面元素;然后使用服务器端包含从 HTML 片段文件中插入的特定页面内容:

 

 
<html lang="en" dir="ltr">  <head>    <meta charset="utf-8">    <title>Feed metitle>  head>  <body>    <h1>?? Feed meh1>      body>html>
      复制代码  

 

我们使用 Nginx 提供此文件,通过匹配正在请求的 URL 来配置 $PAGE 变量:

 

 
server {    listen 8080;    server_name localhost;
root /usr/share/nginx/html; index index.html; ssi on;
# Redirect / to /browse rewrite ^/$ http://localhost:8080/browse redirect;
# Decide which HTML fragment to insert based on the URL location /browse { set $PAGE 'browse'; } location /order { set $PAGE 'order'; } location /profile { set $PAGE 'profile' }
# All locations should render through index.html error_page 404 /index.html;}
      复制代码  

 

这是相当标准的服务器端组合方法。它之所以可以算作微前端,是因为我们可以由此来分割代码,让每部分代码代表一个自包含的域概念,并由一个独立的团队负责。这里没有展示各个 HTML 片段文件最终如何在 Web 服务器上呈现,实际上它们都有自己的部署管道,改动某个页面并不会影响其他内容。

 

想要更高独立性的话,可以为每个微前端单独安排一个服务器负责呈现和服务,再安排一个服务器专门向其他服务器发出请求。如果能缓存好各个响应就不会增大延迟。

 

 

图 6:这些服务器都可以独立构建和部署

 

这个例子说明微前端不一定是一种新技术,也不一定很复杂。只要我们的设计决策能为代码库和团队赋予更多自主权,那么不管怎样的技术栈都能为我们带来类似的收益。

 

构建时集成

还有一种方法是将每个微前端作为一个包来发布,并让容器应用将它们全部作为库依赖包含进去。下面展示了容器的 package.json 查找本文示例应用的方法:

 

 
{  "name": "@feed-me/container",  "version": "1.0.0",  "description": "A food delivery web app",  "dependencies": {    "@feed-me/browse-restaurants": "^1.2.3",    "@feed-me/order-food": "^4.5.6",    "@feed-me/user-profile": "^7.8.9"  }}
      复制代码  

 

这种办法初看上去挺不错。它通常会生成一个可部署的 Javascript 包,允许我们从各种应用中删除常见的重复依赖。但这意味着我们修改产品的任何部分时都必须重新编译和发布所有微前端。这种齐步走的发布流程在微服务里已经够让我们好受了,所以我们强烈建议不要用它来实现微前端架构。我们好不容易在开发和测试阶段实现了解耦和独立,可别再在发布阶段又绕回去了。我们得在运行时中也集成微前端。

 

通过 iframe 在运行时集成

想要在浏览器中组合应用,一种最简单的方法就是用 iframe。iframe 可以轻松地用一系列独立的子页面构建整个页面。它们的样式和全局变量也能充分隔离,不会互相干扰。

 

 
<html>  <head>    <title>Feed me!title>  head>  <body>    <h1>Welcome to Feed me!h1>
<iframe id="micro-frontend-container">iframe>
<script type="text/javascript"> const microFrontendsByRoute = { '/': 'https://browse.example.com/index.html', '/order-food': 'https://order.example.com/index.html', '/user-profile': 'https://profile.example.com/index.html', };
const iframe = document.getElementById('micro-frontend-container'); iframe.src = microFrontendsByRoute[window.location.pathname]; script> body>html>
      复制代码  

 

就像前文提到的服务器端包含方法一样,用 iframe 构建页面并不是一种激动人心的新技术。但只要我们能精心分割好应用并组建好团队,那么用 iframe 就能实现前面提到的一系列好处。

 

很多人不喜欢 iframe,它也的确有一些缺陷。上面提到的简单隔离方式确实降低了它的灵活性。用 iframe 在应用的各个部分之间构建集成可能会很困难,从而让路由、历史记录和深层链接变得更加复杂;它还会影响页面的响应速度。

 

通过 JavaScript 在运行时集成

这个方法非常灵活,应用广泛。每个微前端都使用