[TOC]

原文地址:https://martinfowler.com/articles/micro-frontends.html

做好前端开发是困难的。扩展前端开发,多个前端团队同时开发一个大而复杂的产品,就更困难了。在本文中,我们将介绍一种新的趋势,将巨石应用拆解成多个更小、更易管理小应用。以及解释这种架构是如何提升团队处理前端代码的有效性和效率的。除了介绍各种优劣势,我们还将介绍一些可用的实施选项,并且我们将深入介绍一个使用了此技术的完整示例。

近年来,微服务大受欢迎,很多团队使用此架构来规避单个巨石后端应用的局限。即使用微服务构建的软件已经很多,大量公司仍然继续在与巨石前端代码库做斗争。也许你想构建一个渐进式的或响应式的网页应用,但不知从何下手将这些功能整合到现有代码中。也许你计划开始使用 JavaScript 的新特性(或是可编译成 JavaScript 的无数语言之一),但是你不能将必要的构建工具安装到你已有的构建进程中。又或者你仅仅是想要扩展你的应用,以支持多团队同时开发一个产品,但是现有的耦合、复杂的单体应用会彼此之间互踩脚趾(相互干扰)。这些问题不利于,你高效的为客户提供高质量的用户体验。

近来我们看到越来越多的人关注复杂、现代的 Web 开发所必须的整体架构和组织结构。特别是,我们看到前端巨石应用被分解成更小、更简单 Chunk 的模式兴起,它们可以独立开发、测试、部署,但客户看到的依然是一个整体的产品。我们称这种技术为微前端,我们将其定义为:

“一种由多个独立可交付的前端应用组合而成更庞大整体的架构方式”

在2016年11月出版的 ThoughtWorks 技术雷达上,我们指出微前端需作为组织应该评估的一种技术。后来我们促其被评审,最后被采纳,即我们认为它已经被证明在需要时你可以使用它。

【译】微前端 - 图1
图 1:微前端已多次出现在技术雷达上

我们所看到的微前端关键优势:

  • 更小、更有凝聚力和可维护的代码库
  • 更易解耦组织依赖,形成独立团队
  • 比以往更容易升级、更新或重写部分前端代码

这些优势都是由微服务带来的,这不是巧合。

当然,天下没有免费的午餐 —— 于软件架构而言,是有代价的。一些微前端的实现会导致重复依赖,从而增加用户加载更多字节数。此外单个团队自主的增加,会割裂各团队间的工作。尽管如此,我们依然相信这些风险是可控的,并且微前端的优势是大于劣势的。

优势

对于微前端的定义,相较于其特定的技术实现细节,我们更注重其所拥有的属性和所带来的优势。

增量式升级

对于很多组织来说,这是他们开始微前端之旅的理由。原前端巨石应用被过时的技术栈所拖累,或仅在交付压力下编写代码,并且重写已经达到很诱人的程度。为了避免完全重写,我们更喜欢将老应用一点一点的干掉,并同时继续为我们的客户提供新的功能,而不被巨石拖累。

此时经常引导往微前端架构发展。一旦有一个团队拥有了对旧世界(老代码)做很小的修改的同时新增功能到生产环境的经验,那么其他团队也会想要加入。现有代码仍然需要维护,在某些情况下,对其增加新功能也是有意义的,但现在有了选择。

这里的结局是,我们有了对产品中独立部分的架构、依赖和用户体验是否采用增量升级的决策权。如果我们主框架发生了重大的突破性变化,每个微前端可依据自身需要进行升级,而不用停下所有并一次性做好升级。如果我们想尝试新的技术,或新的交互,我们可以比以往更独立的方式来实现。

简单、解耦的代码库

根据定义,每个微前端的源码要比一个单独的巨石应用小得多。这些更小的代码库对于开发者而言会更简单、更容易。特别是避免了,组件间无意识、不恰当的耦合所产生的复杂性。通过在应用内绘制更清晰的上下文界限,使得意外耦合更不易发生。

当然,一个高阶架构决策(例如“开始微前端”),不会用于替代好的老式的清洁代码。我们并不是试图避免对提升代码质量的思考。相反的,我们试图让我们自己处于更容易做出好决策的环境中。例如,共享跨越有界上下文的领域模型更为麻烦,故而开发者更不愿如此做。类似的,微前端促使你明确谨慎的思考数据和事件如何在应用的不同部分之间流转,这是些我们必须做的事情!

独立部署

就像微服务,微前端的可独立部署性是关键。减少给定部署范围,从而降低关联风险。无论你的前端代码以何种形式在哪里进行托管,每个微前端都需要专属于自己的连续发布管道,构建、测试和发布到生产环境。我们需要能部署每个微前端,而无需顾虑其他代码库或发布管道的状态。不用关心,原巨石应用是固定的、手动的还是周期性的发布,亦或者是隔壁团队把半完成的或有损的功能推送到了他们的主干上。一个特定微前端是否准备好投入生产,这应该由构建和维护它的团队来决定。

【译】微前端 - 图2
图 2: 每个微前端都被独立的部署到生产环境

自主团队

作为高阶优势,相较于解耦代码库和周期发布,拥有完全独立的团队需要走更长的路,其可以承担产品中某一个部分的完善和超越。团队拥有为用户提供价值所需的权力,这使他们可以快速有效的行动。为实现这一目标,我们团队需要形成基于业务功能的垂直切片,而非基于技术能力。一个简单的方式是基于用户最终看到的产品形态进行分割,所以每个微前端囊括一个单页面应用,并由一个团队端到端的拥有。这个比基于技术或者横向关系(如样式、表单或验证)形成的团队,带来更高的内聚性。

【译】微前端 - 图3
图 3: 每个应用应该被单一团队所拥有

简而言之

简言之,微前端是将大而可怕的应用分割成更小更容易管理的应用,并明确他们之间关系的技术。我们的技术选型、代码库、团队和发布过程都需要能独立的操作和发展,而无需过度协调。

示例

想想一个网站,顾客可以订餐并配送到家。从表面上看,这是一个相当简单的概念,但如果要实现它,你会发现有惊人多的细节:

  • 应该有个首页,用于顾客浏览和搜索餐厅。餐厅列表需要能够搜索和通过众多属性的过滤,如价格、风味或者顾客之前购买过的食物。
  • 每个餐厅需要专属页面展示它的菜单项,并允许顾客选择他们想要吃什么,包括折扣、优惠券及特殊需求。
  • 顾客需要一个配置页面,可以看到他们的订单历史,跟踪配送并配置付款方式。

【译】微前端 - 图4
图 4: 一个送餐网站可能有多个相当复杂的页面

每个页面都足够复杂,我们很容易证明每个页面都需要专门的独立于其他团队的团队来支持。他们需要能给开发、测试、部署和维护他们的代码,而不需要为与其他团队的冲突与协调担心。然后,我们的用户依然可以看到一个单一的无缝的网站。在文章的其余部分,我们将在任何需要使用样例代码或场景中使用此示例应用。

整合方法

鉴于前面相当松散的定义,有很多被称为微前端的合理方法。在本章节中,我们将展示一些示例,并讨论他们的利弊。这是一个相当自然的架构——通常应用中的每个页面就是一个微前端,还是一个单容器应用,其中:

  • 渲染通用的页面元素,例如页头和页尾
  • 解决诸如认证和导航等交叉关系问题
  • 将各种微前端放在页面上,并告诉每个微前端何时何地渲染自己

【译】微前端 - 图5
图 5: 您通常可以从页面的视觉结构中推导出架构

服务端模板构成

我们的前端开发始于一种绝对不新颖的方法——在服务端输出多模板或片段渲染HTML。我们有一个包含了通用页面元素的 index.html 文件,然后使用服务端的 include 插入包含了特定页面内容的 HTML 片段文件。

<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Feed me</title>
  </head>
  <body>
    <h1>🍽 Feed me</h1>
    <!--# include file="$PAGE.html" -->
  </body>
</html>

我们使用Nginx作为服务,配置 $PAGE 变量,根据请求的 URL 进行匹配。

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
图 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>

类似服务端的 include 选项,从 iframe 构建一个页面也不是新技术,或许这并不令人兴奋。但是我们回顾前面罗列的微前端的优势,iframe 符合大部分要求,只要我们谨慎拆分应用和团队组织结构。

我们经常看到很多人不情愿选择 iframe。虽然有些不情愿源于对 iframe 有点“恶心”的直觉,但可以通过一些好的理由规避它们。前面提到的简单隔离相比其他选项确实缺失了一些灵活性。很难在应用程序的不同部分之间构建集成,它们使得路由、历史、深度链接更加复杂,并且对页面的响应提出了额外的挑战。

通过 JavaScript 在执行态集成

我们描述的下一种方法可能是最灵活,也是最常见的一个。每个微前端都使用