微前端:在微服务的世界中重塑UI

842 阅读8分钟

我们惊讶于软件行业的发展方式!过去,软件就是简单的程序。最早出现的软件如阿波罗任务着陆模块和曼彻斯特宝贝都只是最基本的存储过程。软件的主要目的是用于研究和数学。

个人电脑的发明和英特网的的兴起改变了软件世界。文字处理器、电子表格以及游戏逐渐兴起。网站也逐渐出现,此时网页只是作为静态文档给人阅读。到1990年代中期,随着Netscape引入客户端脚本语言,JAvascript和Marcromedia引入了Flash,此时的浏览器已经更强大,网站也变得更加精彩和更具交互性。尽管如此,这个发展已经应用仍然是简单的。工程师在构建它们也没有足够重视。

云计算和大数据等颠覆性技术的出现,为更复杂的Web和原生移动应用铺平了道路。从电子商务和视频流应用 到 社交媒体和照片编辑,软件应用逐渐承担了最复杂的数据处理和存储任务。传统的单体架构在可伸缩性、团队协作和集成/部署方面面临着巨大的挑战,并且会导致代码库庞大混乱。

为了解决这个问题,逐渐出现了各种面向服务端的架构。其中最有希望的就是微服务架构-将一个应该用拆解成可以独立开发、部署和测试的独立工作的模块。它的可扩展性和同时被多团队发布的特性证明它是解决大部分架构问题的利器。一些面向客户端的前端架构也逐渐出现,例如MVC、MVVM、Web Components等等,但是他们并没有完全能够获得微服务的益处。

微前端:概念

微前端最早出现在ThoughtWorks Technology Radar,他们尝试,验证并且最终采用了微前端。它是一种Web开发的微服务方法,它将独立运行的前端应用程序 组合成为一个整体!

借助微服务的思想,微前端打破了前端单体架构,为Web应用程序创建了完整的微架构设计模式。它完全由松散耦合的业务功能垂直模块组成,而不是水平模块。这些垂直模块被称为称为“微应用”。这个概念并不新鲜,在 Scaling with Microservices and Vertical Decomposition中第一次提出每个垂直模块负责一个单独的业务,并且提出了表示层,持久层和单独的数据库等概念。从开发的角度来看,每个垂直领域都由一个团队实现,不同系统之间不共享代码。

微前端的优势

与单体架构相比,微前端具有诸多优势。

  • 易于升级

    微前端在应用中构建了严格的有界的上下文。独立的应用以更加隔离和增长的方式更新,你不必担心会破坏应用程序中的某些部分。

  • 可扩展性

    对于微前端来说,水平扩展是极其容易的。每一个微前端都保证是无状态的,以便扩展。

  • 易于部署

    每个微前端都有自己的 CI/CD 管道,可以构建、测试并将其部署到生产环境中。因此,其他团队是否正在开发某个功能并推动了错误修复,或者是否正在进行切换或重构都无关紧要。只要保证一个团队负责一个微前端,在微前端的更改就应该没有风险。

  • 团队协作和所有权

    Scrum 指南说“最佳开发团队规模小到足以保持敏捷,大到足以在 Sprint 中完成重要工作”。微前端非常适合可以完全拥有堆栈的多个跨职能团队(微前端)从用户体验到数据库设计的应用程序。如果是电子商务网站,产品团队和支付团队可以同时在应用程序上工作,而不会互相踩踏。

微前端集成的方法

有很多可以实现微前端,但是建议无论什么方法都采用运行时集成,而不是构建时集成。因为前者必须在每个微前端上进行重新编译和发布,才能发布任何微前端的更改。

接下来,我们通过构建一个简单的宠物电子商务酒店来学习微前端的突出方法。这个网站具有页或搜索、购物车、结帐、产品和联系我们等微应用。我们只演示网站的微前端部分。假设每个微应用都有一个服务它的微服务。您可以查看项目演示代码库。每种集成方式在 repo 代码中都有一个分支,您可以检出以查看。

单页前端

实现微前端最简单(但不是最优雅)的方法是将每个微前端视为一个页面。

micro-frontend.html


!DOCTYPE html>

<html lang="zxx">

<head>

	<title>The MicroFrontend - eCommerce Template</title>

</head>

<body>

  <header class="header-section header-normal">

    <!-- Header is repeated in each frontend which is difficult to maintain -->

    ....

    ....

  </header

  <main>

  </main>

  <footer

    <!-- Footer is repeated in each frontend which means we have to multiple changes across all frontends-->

  </footer>

  <script>

    <!-- Cross Cutting features like notification, authentication are all replicated in all frontends-->

  </script>

</body>

这是做微前端最纯粹的方法之一,因为没有容器或拼接元素将前端绑定到应用程序中。每个微前端都是一个独立的应用程序,每个依赖项都封装在其中,并且没有耦合。这种方法的缺点是页面都有很多重复(如页眉和页脚),这增加了冗余和维护负担。

JavaScript 渲染组件(或 Web 组件、自定义元素)-

正如我们在上面看到的,单页微前端架构有其缺点。为了克服这些,我们应该选择一个架构,它有一个容器元素来构建应用程序的上下文和横切关注点(如身份验证),并将所有微前端拼接在一起以创建一个有凝聚力的应用程序。

MicroFrontend.js


// A virtual class from which all micro-frontends would extend

class MicroFrontend {

  

  beforeMount() {

    // do things before the micro front-end mounts

  }

  onChange() {

    // do things when the attributes of a micro front-end changes

  }

  render() {

    // html of the micro frontend

    return '<div></div>';

  }

  onDismount() {

    // do things after the micro front-end dismounts 

  }

}

Cart.js


class Cart extends MicroFrontend {

  beforeMount() {

    // get previously saved cart from backend

  }

  render() {

    return `<!-- Page -->

    <div class="page-area cart-page spad">

      <div class="container">

        <div class="cart-table">

          <table>

            <thead>

            .....

            

     `

  }

  addItemToCart(){

    ...

  }

    

  deleteItemFromCart () {

    ...

  }

  applyCouponToCart() {

    ...

  }

    

  onDismount() {

    // save Cart for the user to get back to afterwards

  }

}

Product.js


class Product extends MicroFrontend {

  static get productDetails() {

    return {

      '1': {

        name: 'Cat Table',

        img: 'img/product/cat-table.jpg'

      },

      '2': {

        name: 'Dog House Sofa',

        img: 'img/product/doghousesofa.jpg'

      },

    }

  }

  getProductDetails() {

    var urlParams = new URLSearchParams(window.location.search);

    const productId = urlParams.get('productId');

    return this.constructor.productDetails[productId];

  }

  render() {

    const product = this.getProductDetails();

    return `	<!-- Page -->

    <div class="page-area product-page spad">

      <div class="container">

        <div class="row">

          <div class="col-lg-6">

            <figure>

              <img class="product-big-img" src="${product.img}" alt="">`

  }

  selectProductColor(color) {}

  selectProductSize(size) {}

 

  addToCart() {

    // delegate call to MicroFrontend Cart.addToCart function

  }

  

}

index.html


<!DOCTYPE html>

<html lang="zxx">

<head>

	<title>PetStore - because Pets love pampering</title>

	<meta charset="UTF-8

  <link rel="stylesheet" href="css/style.css"/>

</head>

<body>

	<!-- Header section -->

	<header class="header-section">

  ....

  </header>

	<!-- Header section end -->

	<main id='microfrontend'>

    <!-- This is where the Micro-frontend gets rendered by utility renderMicroFrontend.js-->

	</main>

                                <!-- Header section -->

	<footer class="header-section">

  ....

  </footer>

	<!-- Footer section end -->

  	<script src="frontends/MicroFrontend.js"></script>

	<script src="frontends/Home.js"></script>

	<script src="frontends/Cart.js"></script>

	<script src="frontends/Checkout.js"></script>

	<script src="frontends/Product.js"></script>

	<script src="frontends/Contact.js"></script>

	<script src="routes.js"></script>

	<script src="renderMicroFrontend.js"></script>

renderMicroFrontend.js


function renderMicroFrontend(pathname) {

  const microFrontend = routes[pathname || window.location.hash];

  const root = document.getElementById('microfrontend');

  root.innerHTML = microFrontend ? new microFrontend().render(): new Home().render();

  $(window).scrollTop(0);

}

$(window).bind( 'hashchange', function(e) { renderFrontend(window.location.hash); });

renderFrontend(window.location.hash);

utility routes.js (A map of the hash route to the Microfrontend class)

const routes = {

  '#': Home,

  '': Home,

  '#home': Home,

  '#cart': Cart,

  '#checkout': Checkout,

  '#product': Product,

  '#contact': Contact,

};

这种方法非常简洁,并且为微前端封装了一个单独的类。所有其他微前端均由此扩展。请注意与微应用相关的所有功能是如何封装在相应的微前端中的。这确保微前端上的并发工作不会弄乱其他一些微前端。

当涉及到 Web 组件和自定义元素时,一切都将在类似的范例中工作。

React

由于客户端 JavaScript 框架非常流行,因此不可能将 React 从任何前端讨论中排除。React 是一个基于组件的 JS 库,上面讨论的大部分内容也适用于 React。我将讨论关于 React 微前端的一些技术细节和挑战。

样式

由于在任何微前端之间应该有最少的代码共享,考虑到 CSS 的全局和级联性质,对 React 组件进行样式设置可能具有挑战性。我们应该确保样式针对特定的微前端,而不会溢出到其他微前端。inline CSSRadium ,CSS in JS 的代表 RadiumCSS Modules可以与 React 一起使用。

Redux

在当今的前端世界中,将 React 与 Redux 结合使用是一种规范。约定是使用 Redux 作为整个应用程序的单个全局存储,用于跨应用程序通信。微前端应该是独立的,没有依赖关系。因此,每个微前端都应该有自己的 Redux 存储,朝着多 Redux 存储架构发展。

其他值得注意的集成方法

  • 服务器端渲染

    可以使用服务器组装微前端模板,然后将其渲染到浏览器。也可以使用 SSI 技术。

  • iframes

    每个微前端都可以是一个 iframe。它们还在样式方面提供了很好的隔离度,并且全局变量不会相互干扰。

概括

借助微服务,微前端有望在构建复杂应用程序以及开发、部署和维护等方面带来许多好处。

有一句名言:“there is no one-size-fits-all approach that anyone can offer you. The same hot water that softens a carrot hardens an egg”。微前段好,但他并不能解决所有架构问题,它自身也存在着缺点。随着更多的存储库,工具,构建/部署,服务器需要去维护。微前端也可能增加应用程序的复杂性。它难以在跨应用之间建立通信,并且依赖项的重复,无限放大了程序的体积。

是否采用微前端架构取决于许多因素。例如组织的规模和应用的复杂度。无论是新项目还是遗留大代码库,建议随着时间的推移逐渐应用该技术,并且检查其是否有效解决问题。