0%

微前端

一、什么是微前端?

微前端的概念由我司(ThoughtWorks)在 2016 年十一月份的技术雷达中被列为组织应评估的技术,后来又被提升为试用版,最后将其推广到 Adopt 中。

微前端是一种类似于微服务的架构。它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发,独立部署,这样我们也就实现了应用的自治。而划分的团队之间也可以自治,独立定义自己的命名规则、代码规范、技术栈、开发流程等。

二、为什么需要微前端?

一个大型组织的组织架构、软件架构是在不断地发生变化的。其业务的不断发展会导致组织的应用不断地膨胀,进一步映射到软件架构上。当一个组织的部门已经过于庞大的时候,就会进一步将它细化。同理,软件的不同部分又被拆分到不同的部门之下。随着不同部门的业务发展,技术栈也会因此变得越来越难以统一,出现多样化。

而对于用户来说,会越来越厌倦同一家公司的软件出现在不同的应用上。用户厌倦之后,应用的获客成本就会变得越来越高,在这样的背景下,应用又再一次走向了聚合。最近几年,移动应用就出现了这样的趋势,我们拿支付宝来举例。

一家大的商业公司,都会提供一系列的应用,这些应用从某种程度上也可以反映这家公司的组织架构。但是在用户眼里,他们就是一家公司,他们就只应该有一个应用。就像支付宝,我们肯定希望只在一个 App 中进行各种操作,比如收付款、转账、理财、缴纳水电,以及现在的小程序等,当然不希望下载多个 App 来完成这些事情。

相似的,这种趋势也在桌面 Web 出现,聚合成为客户端的一个技术趋势,实现前端聚合的就是微前端架构。

现在,绝大多数的公司都已经采用前后端分离的架构和开发模式。分离降低了系统的复杂度,同时也提高了软件的开发效率。而随着业务的不断扩张,需求也不断扩大,应用又开始变得臃肿起来。既然应用变大了,我们就得继续拆分,拆分成更小的单元。所以,微前端的架构模式和理念也是基于此诞生。

如果你经历过一个遗留系统的项目开发,应该会有很深的体会。产品代码是使用一个老旧的前端框架搭建而成,当你想把新的框架应用到项目中的时候,却发现新技术栈的某些编译配置在当前代码库中无法使用;想在的产品代码库中构建一个响应式 Web 应用程序,却找不到一个合适的入口去进行代码集成。代码越来越臃肿,前进的脚步被往日堆积的技术债所阻碍。你可能会想到重写,但是对于一个遗留系统而言,在没有很清楚地弄明白它的业务细节之前,加上无法得到重写的 support 之前,是万万不敢轻举妄动的,也是无法进行的。解决遗留系统,是采用微前端方案最重要的原因。

三、微前端的技术拆分方式

从技术实践上来说,微前端可以采用以下几种方式进行:

  1. 路由分发式。通过 HTTP 服务器的反向代理功能,将请求路由到对应的应用上。
  2. 前端微服务化。在不同的框架之上设计通信和加载机制,以在一个页面内加载对应的应用。
  3. 微应用。通过软件工程的方式,在部署构建环境之中,把多个独立的应用组合成一个单体应用。
  4. 微件化。开发一个新的构建系统,将部分业务功能构建成一个独立的 chunk 代码,使用时只需要远程加载即可。
  5. 前端容器化。将 iframe 作为容器来容纳其他前端应用。
  6. 应用组件化。借助于 Web Components 技术,来构建跨框架的前端应用。

实施的方式虽然多,但都是依据场景而采用的。在某些场景下,可能没有合适的方式;而在某些场景下,则可以同时使用多种方案。下面我们看看上面这六种方式的具体实现。

(1) 路由分发式

路由分发式微前端,即通过路由将不同的业务分发到不同的独立前端应用上。通常是通过 HTTP 服务器的反向代理来实现,也可以通过应用框架自带的路由来解决。

路由分发式

(图片来自网络)


路由分发式的架构应该是采用最多最容易的微前端方法。但是这种方式也有一个缺点:看上去更像是多个前端应用的聚合 —— 我们只是将这些不同的前端应用拼凑在一起,使它们看起来像一个完整的整体,但它们并非是一个整体。每当用户从 A 应用切换到 B 应用的时候,往往需要刷新一下页面,重新加载资源文件。

在这个架构中,我们只需要关注应用间的数据传递方式。通常,我们只需要将当前的用户状态从 A 应用传递到 B 应用即可。如果两个应用在同一个域里运行,就更加方便了,它们可以通过 LocalStorageCookiesIndexedDB 等方式共享数据。

这里需要注意的是:在采用这种应用时,缺少了对应用状态的处理,需要用户重新登录,这种体验对用户来说相当不友好。

(2) 前端微服务化

前端微服务化,是微服务架构在前端的实施,每个前端应用都是完全独立的(技术栈、开发、部署、构建)、自主运行的,最后通过模块化的方式组合出完整的前端应用,架构图如下:

前端微服务化

(图片来自网络)


采用这种方式,就意味着一个页面上同时存在两个及以上的前端应用在运行,而前面所提到的路由分发方案则是页面页面只有唯一一个应用。

当我们单击指向某个应用的路由时,会加载、运行对应的应用。而原有的一个或多个应用,仍然可以在页面上保持运行的状态。同时,这些应用可以使用的不同的技术栈来开发,如页面上可以同时运行 ReactAngular 和 Vue 框架开发的应用。

能够这样实施的原因是:不论是基于 Web ComponentsAngular,还是 VirtualDOMReact,现有的前端框架都离不开基本的 HTML 元素 DOM 。所以我们只需要做到如下两点:

  • 在页面合适的地方引入或者创建 DOM
  • 用户操作时,加载对应的应用(触发应用的启动),并能卸载应用。

创建 DOM 比较容易解决,第二点却比较麻烦,特别是移除 DOM 和相应应用的监听。当我们拥有一个不同的技术栈时,我们需要有针对性地设计出一套这样的逻辑。同时,我们还需要保证应用间的第三方依赖之间不发生冲突。

(3) 组合式集成:微应用化

微应用化是指,在开发时应用都是以单一的、微小应用的形式存在的,而在运行时,则通过构建系统合并这些应用,并组合成一个新的应用,其架构如下图:

微应用化

微应用化大都是以软件工程的方式来完成前端应用的开发的,因此又可以称之为组合式集成。对于一个大型的前端应用来说,采用的架构方式往往是可以通过业务作为主目录的,然后在业务目录中放置相关的组件,同时拥有一些通用的共享模板,例如:

|—— account
|—— dashboard
|—— reports
|—— ...
|__ shared

当我们开发一个这样的应用时,从目录结构上看,业务本身已经被拆分了。我们所要做的是:让每个模块都成为一个单独的项目,如将仪表盘功能提取出来,加上共享部分的代码、应用的基本脚手架,便可以成为一个单独的应用。拆分出每个模块之后,便只需要在构建的时候复制所有的模块到一个项目中,再进行集成构建。

微应用化与前端微服务化类似,在开发时都是独立应用的,在构建时又可以按照需要单独加载。如果以微前端的单独开发、单独部署、运行时聚合的基本思想来看,微应用化就是微前端的一种实践,只是使用微应用化意味着我们只用使用唯一的一种前端框架。大团队通常是不会同时支持多个前端框架的。

(4) 微件化

微件(widget),指的是一段可以直接嵌入在应用上运行的代码,它由开发人员预先编译好,在加载时不需要再做任何修改或者编译。而微前端下的微件化则指的是,每个业务团队编写自己的业务代码,并将编译好的代码部署(上传或者放置)到指定的服务器上,在运行时,我们只需要加载相应的业务模块即可。对应的,在更新代码的时候,我们只需要更新对应的模块即可。下图便是微件化的架构示意图:

微件化

(图片来自网络)


在非单面应用时代,要实现微件化方案是一件特别容易的事。从远程加载来对应的 JavaScript 代码并在浏览器上执行,生成对应的组件嵌入到页面的相应部分。对于业务组件也是类似的,提前编写好我们的业务组件,当需要对应的组件时再响应和执行。在未来,我们也可以采用 WebComponent 技术来做这样的事情。

而在单页面应用时代,要实现微件化就没有那么容易了。为了支持微建化,我们需要做下面一些事情:

  • 持有一个完整的框架运行时及编译环境。这用于保证微件能征程使用,即可调用框架 API 等。
  • 性能受影响。应用由提前编译变成运行时才编译,会造成一些性能方面的影响——具体视组件的大小而定。
  • 提前规划依赖。如果一个新的微件想使用新的依赖,需要从上游编译引入。

此外,我们还需要一个支持上述功能的构建系统,它用于构建一个独立的微件模块。这个微件的形式如下:

  • 分包构建出来的独立代码,如 webpack 构建出来的 chunk
  • 使用 DSL 的方式编写出来的组件。

为了实现这种方式,我们需要对前端的应用的构建系统进行修改,如 webpack ,使它可以支持构建出单个的代码段。这种方式的实施成本比微应用化成本高。

(5) 前端容器:iframe

iframe 作为一个非常 “古老” 的,人人都觉得普通的技术,却一直很管用。它能有效地将另一个网页/单页面应用嵌入到当前页面中,两个页面间的 CSSJavaScript 是相互隔离的——除去 iframe 父子通信部分的代码,它们之间的代码是完全不相互干扰的。iframe 便相当于是创建了一个全新的独立的宿主环境,类似于沙箱隔离,它意味着前端应用之间可以相互独立运行。

当然,采用 iframe 有几个重要的前提:

  • 网站不需要 SEO 支持;
  • 设计相应的应用管理机制。
<html>
  <head>
    <title>Book Store</title>
  </head>
  <body>
    <h1>Welcome to Book Store!</h1>

    <iframe id="micro-frontend-container"></iframe>

    <script type="text/javascript">
      const microFrontendsByRoute = {
        '/browse': 'https://browse.micro-frontend-demo.com/index.html',
        '/order': 'https://order.micro-frontend-demo.com/index.html',
        '/profile': 'https://profile.micro-frontend-demo.com/index.html',
      };

      const iframe = document.getElementById('micro-frontend-container');
      iframe.src = microFrontendsByRoute[window.location.pathname];
    </script>
  </body>
</html>

(6) 结合 Web Components 构建

Web Components 是一套不同的技术,允许开发者创建可重用的定制元素(它们的功能封装在代码之外),并且在 web 应用中使用它们。

微件化

(图片来自网络)

<html>
  <head>
    <title>Book Store!</title>
  </head>
  <body>
    <h1>Welcome to Book Store!</h1>

    <!-- These scripts don't render anything immediately -->
    <!-- Instead they each define a custom element type -->
    <script src="https://browse.micro-frontend-demo.com/bundle.js"></script>
    <script src="https://order.micro-frontend-demo.com/bundle.js"></script>
    <script src="https://profile.micro-frontend-demo.com/bundle.js"></script>

    <div id="micro-frontend-root"></div>

    <script type="text/javascript">
      // These element types are defined by the above scripts
      const webComponentsByRoute = {
        '/': 'micro-frontend-browse-books',
        '/order': 'micro-frontend-order-book',
        '/profile': 'micro-frontend-profile',
      };
      const webComponentType = webComponentsByRoute[window.location.pathname];

      // Having determined the right web component custom element type,
      // we now create an instance of it and attach it to the document
      const root = document.getElementById('micro-frontend-root');
      const webComponent = document.createElement(webComponentType);
      root.appendChild(webComponent);
    </script>

  </body>
</html>

Web Component 于微前端中的应用,现在也正处于摸索阶段。

参考:

请我喝杯咖啡吧~
-------- 本文结束 感谢阅读 --------