1. 首页
  2. 生活百科
  3. 正文

微前端架构的实现方式

  想象一下,有一个网站,顾客可以在那里订购食物并送货。从表面上看,这是一个相当简单的网站,但如果你想把它做好,其中有大量令人惊讶的细节:

  应该有一个登录页,在这个页面,客户可以浏览和搜索餐厅。餐厅应该可以根据价格、菜肴或客户之前订购的内容进行搜索和过滤。

  每家餐厅都需要有自己的页面来显示菜单,并允许客户选择他们想吃的东西,包括折扣、餐饮优惠和特殊要求。

  客户应该有一个个人资料页面,在该页面上他们可以查看订单历史记录、跟踪交货情况并自定义付款选项。


  图1:食品配送网站可能有几个相当复杂的页面

  由于每个页面都有足够的复杂性,所以我们可以为每个页面指定一个专门的团队负责,并且每个团队的工作都应该能够独立于其他团队。他们应该能够开发、测试、部署和维护自己的代码,而不必担心与其他团队的代码产生冲突或需要协调。然而最终我们的客户看到的仍然是一个单一、无缝的网站。

  在本文的其余部分中,我们将使用此示例做说明。

  微前端实现的几种集成方法

  鉴于上述定义相当宽松,有许多方法可以称为微前端。在本节中,我们将展示一些示例并讨论开发中如何进行权衡。所有方法都有一个相当自然的体系结构——通常应用程序中的每个页面都是一个微前端,这些页面有一个统一的容器,该容器可以:

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

  解决身份验证和导航等交叉问题

  将各种微前端模块组合到页面上,并告诉每个模块何时何地进行渲染


  图2:您通常可以从页面的视觉结构派生出您的架构

  服务器端模板组合

  我们从一种不太新颖的前端开发方法开始——在服务器上用多个模板或片段渲染HTML。我们有一个index.html,它包含任何常见的页面元素,然后在服务器端把包含特定页面内容的片段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提供此html文件,通过匹配所请求的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页面。这里没有显示这些不同的HTML文件如何最终到达web服务器,但假设它们都有自己的部署管道,因此部署其中一个页面不会影响任何其他页面。

        为了获得更大的独立性,可以有一个单独的服务器负责呈现和服务每个微前端,前端有一个服务器向其他服务器发出请求。

        这个例子说明了微前端不一定是一种新技术,也不必很复杂。只要我们仔细考虑我们的设计决策如何影响我们的代码库和团队的自主性,无论我们的技术堆栈如何,我们都可以获得许多相同的好处。

        构建时集成

        我们有时会看到一种方法:将每个微前端发布为一个包,并用一个页面作为容器包含所有依赖库。以下是该容器页面的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包,允许我们从各种应用程序中消除重复的常见依赖项。然而,这种方法意味着一旦其中一个产品的任何单个部分发生更改,需要发布时,我们就必须重新编译和发布每个微前端。就像微服务一样,我们已经看到了这样一个锁步释放过程所带来的痛苦,我们强烈建议不要使用这种方法来处理微前端。

        我们已经解决了将应用程序划分为可以独立开发和测试的离散代码库的所有问题,我们不要在发布阶段重新介绍所有这些耦合。我们应该找到一种在运行时而不是在构建时集成微前端的方法。

        通过iframes进行运行时集成

        在浏览器中组合应用程序的最简单方法之一是简陋的iframe。iframes的本质是可以很容易地用独立的子页面构建页面。它们在样式和全局变量方面也提供了良好的隔离度,不会相互干扰。

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

        就像服务器端的includes选项一样,用iframe构建页面并不是一种新技术,可能看起来也不那么令人兴奋。但是,如果我们重新审视前面列出的微前端的主要好处,iFrame基本上符合要求,只要我们仔细考虑如何划分应用程序和构建团队。

        Iframe的缺点是各子页面之间相互通信麻烦,并且它们会使路由、历史记录和深度链接更加复杂,因此会给页面的响应带来一些额外的挑战。

        通过JavaScript进行运行时集成

        我们即将描述的这种方法可能是最灵活的方法,也是我们看到的团队最常采用的方法。每个微前端都使用  

      <html><head><title>Feed me!</title></head><body><h1>Welcome to Feed me!</h1>

      <!--These scripts don't render anything immediately --><!--Instead they attach entry-point functions to `window` --><script src="https://browse.example.com/bundle.js"></script><script src="https://order.example.com/bundle.js"></script><script src="https://profile.example.com/bundle.js"></script>

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

      <script type="text/javascript">// These global functions are attached to window by the above scriptsconst microFrontendsByRoute = { '/': window.renderBrowseRestaurants, '/order-food': window.renderOrderFood, '/user-profile': window.renderUserProfile,};const renderFunction = microFrontendsByRoute[window.location.pathname];

      // Having determined the entry-point function, we now call it,// giving it the ID of the element where it should render itselfrenderFunction('micro-frontend-root');</script></body></html>

        以上显然是一个原始示例,但它演示了基本技术。与构建时集成不同,我们可以独立部署每个bundle.js文件。与iFrame不同的是,我们有充分的灵活性,可以根据自己的喜好在微前端之间构建集成。我们可以通过多种方式扩展上述代码,例如仅根据需要下载每个JavaScript包,或者在呈现微前端时传入和传出数据。

        这种方法的灵活性,再加上独立的可部署性,使其成为我们的默认选择,也是我们经常看到的选择。在我们做完整的示例时,我们将更详细地探讨它。

        通过Web组件进行运行时集成

        与前一种方法不同的是,每个微前端都要定义一个HTML自定义元素为容器进行实例化,而不是定义一个全局函数来供容器调用。

      <html><head><title>Feed me!</title></head><body><h1>Welcome to Feed me!</h1>

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

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

      <scripttype="text/javascript">// These element types are defined by the above scriptsconst webComponentsByRoute = { '/': 'micro-frontend-browse-restaurants', '/order-food': 'micro-frontend-order-food', '/user-profile': 'micro-frontend-user-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 documentconst root = document.getElementById('micro-frontend-root');const webComponent = document.createElement(webComponentType);root.appendChild(webComponent);</script></body></html>

        这里的最终结果与前面的示例非常相似,主要区别在于您选择了“web组件方式”。如果您喜欢web组件规范,并且喜欢使用浏览器提供的功能,那么这是一个很好的选择。

        样式设置

        CSS作为一种语言,本质上是全局的、继承的和级联的,传统上没有模块系统、名称空间或封装。其中一些功能现在确实存在,但通常缺乏浏览器支持。在微前端领域,许多问题都在加剧。例如,如果一个团队的微前端有一个样式表,上面写着h2 { color: black; },另一个是h2 { color: blue; },而且这两个选择器都应用于同一个页面,那么后面的样式会覆盖前面的。更糟糕的是,这些选择器是由不同的团队在不同的时间编写的,而且代码可能被分割到不同的存储库中,因此更难发现。

        多年来,人们发明了许多方法来提高CSS的可管理性。有些人选择使用严格的命名约定,如BEM,以确保选择器仅适用于预期的位置。其他人则不喜欢单独依赖开发人员规程,而是使用诸如SASS之类的预处理器,其选择器嵌套可以用作名称空间的一种形式。一种较新的方法是使用CSS modules或其中一种CSS-in-JS库,它可以确保样式仅在开发人员想要的地方直接应用。或者使用更基于平台的方法,比如shadow DOM,它也提供样式隔离。

        选择哪种方法并不重要,只要您找到一种方法来确保开发人员可以彼此独立地编写自己的样式,并且相信他们的代码在组合到单个应用程序中时的行为是可预测的。

        共享组件库

        我们前面提到,跨微前端的视觉一致性很重要,实现这一点的方法之一是开发一个共享的、可重用的UI组件库。总的来说,我们认为这是一个好主意,尽管很难做好。创建这样一个库的主要好处是通过重用代码和视觉一致性来减少工作量。此外,您的组件库可以作为一个活生生的样式指南,它可以成为开发人员和设计人员之间的一个重要协作点。

        最容易出错的事情之一就是过早地创建太多组件。创建一个所有应用程序通用的视觉效果的基础框架很有吸引力。然而,经验告诉我们,在您实际使用组件之前,很难精确确定组件的API应该是什么,这会导致组件早期的大量改动。出于这个原因,我们更愿意让团队根据需要在其代码库中创建自己的组件,即使这样最初会导致一些重复工作。一旦组件的API明显通用,您就可以将重复的代码收集到共享库中。

        共享组件库一般包含图标、标签和按钮等最基本的元素。还可以共享包含大量UI逻辑的复杂组件,例如自动完成的下拉搜索字段。或可排序、可筛选、分页的表。但是,要小心确保共享组件只包含UI逻辑,而不包含业务逻辑。当业务逻辑被放入共享库时,它会在应用程序之间产生高度耦合,并增加维护的难度。例如,您通常不应该尝试共享ProductTable,因为ProductTable包含关于“产品”到底是什么以及应该如何行为的各种假设。此类领域建模和业务逻辑属于微前端的应用程序代码,而不是共享库。

        与任何内部共享库一样,围绕其所有权和管理存在一些棘手的问题。一种模式是说,作为共享资产,“每个人”都拥有它,尽管在实践中这通常意味着没有人拥有它。它可能很快成为一个不一致代码的大杂烩,没有明确的约定或技术愿景。另一个极端是,如果共享库的开发完全集中化,那么创建组件的人和使用组件的人之间就会出现很大的脱节。我们见过的最好的模式是任何人都可以为共享库贡献代码,但有一个管理者(一个人或一个团队)负责确保这些代码的质量、一致性和有效性。维护共享库的工作需要强大的技术技能,但也需要培养跨多个团队协作所需的人际技能。

        跨应用程序通信

        关于微前端,最常见的问题之一是如何让它们相互通信。一般来说,我们建议让它们尽可能少地交流,因为这通常需要重新引入我们最开始要避免的那种不适当的耦合。

        也就是说,通常需要某种程度的跨应用通信。自定义事件允许微前端进行间接通信,这是一种将直接耦合降至最低的好方法。或者,向下传递回调和数据的React模型(在本例中是从容器向下传递到微前端)也是一个很好的解决方案。第三种选择是使用地址栏作为通信机制,我们将在后面进行更详细的探讨。

        如果您使用的是redux,通常的方法是为整个应用程序提供一个单一的、全局的共享存储。然而,如果每个微前端都是独立的应用程序,那么每个微前端都应该有自己的redux存储。redux文档甚至提到“将redux应用程序作为更大应用程序中的一个组件进行隔离”,以此作为拥有多个store的有效理由。

        无论我们选择何种方式,我们都希望微前端通过相互发送消息或事件进行通信,并避免共享任何状态。就像跨微服务共享数据库一样,一旦我们共享数据结构和域模型,我们就会产生大量的耦合,并且很难进行更改。

        后端通信

        如果我们有独立的团队处理每个前端应用,那么后端开发呢?我们坚信全栈团队的价值,他们拥有自己的应用程序开发,从可视化代码到API开发,再到数据库和基础架构代码。这里有一个有用的模式是BFF模式,其中每个前端应用都有一个相应的后端,其目的只是满足该前端的需要。虽然BFF模式最初可能意味着每个前端通道(web、移动等)都有专用的后端,但它可以很容易地扩展为每个微前端的后端。

        这里有很多因素需要考虑。BFF可能是自包含的,有自己的业务逻辑和数据库,也可能只是下游服务的聚合器。


        图4:有许多不同的方法来构建前端/后端关系

        测试

        在测试方面,微前端与其它前端构建方式没有多大区别。每个微前端也应该有自己全面的自动化测试套件,以确保代码的质量和正确性。

        然后,明显的差别将是各种微前端与容器应用程序的集成测试。这可以使用您首选的功能/端到端测试工具(如Selenium或Cypress)完成。使用单元测试来覆盖底层业务逻辑和呈现逻辑,然后使用功能测试来验证页面是否正确组装。

        功能测试的重点是验证前端的集成,而不是验证每个微型前端的内部业务逻辑,单元测试应该已经涵盖了这些逻辑。

        延伸阅读

      • 推广引流方法有哪些,裂变营销什么意思

        推广引流方法有哪些,裂变营销什么意思除了各公域平台,另一个比较重要的引流场景,就是在微信中。一方面做信社交性强,对于身边用友的链接更紧密,微信上也会以群、公众号的形式聚集一群有...

      • 小红书引流推广怎么做,小红书引流的最快方法是什么

        做小红书要九浅一深,为什么你的小红书没有流量呢?因为你很有可能被判为营销号,不要以为只有个人号才会判定你为营销号,企业号也会这样子。原因很简单,小红书必竟是一个内容分享平台,是...

      • 外贸英文网站建设怎么做?

        1、规划和设计确定网站的目标、目标受众和关键信息。设计网站结构和页面布局,包括主页、产品展示页面、联系方式等。以纺织服装行业为例,考虑到时尚和审美特点,英文网站建设设计风格应该...

      • SEO套餐的生产与加工

        任何一件事情都可以独立分割成一个完整的体系,运用程序化和步骤化加工的方法实现快速无限复制和粘贴今天我们就来分解SEO套餐在“流水线”上的生产与加工:一、前端/页编人员二、内容编...

      • 2024网络营销怎么做?ai自媒体矩阵助力企业“降本增效”

        在2024年,随着人工智能技术的快速发展,网络营销正在经历着前所未有的变革。企业需要寻找新的营销方式来提高营销效果,降低营销成本。AI自媒体矩阵成为了企业“降本增效”的新选择。...

      本文来自投稿,不代表本人立场,如若转载,请注明出处:http://www.lnbdc.com/article/8593.html

      (function(){ var src = (document.location.protocol == "http:") ? "http://js.passport.qihucdn.com/11.0.1.js?1d7dde81dc0903e04d3ac0b9599444f6":"https://jspassport.ssl.qhimg.com/11.0.1.js?1d7dde81dc0903e04d3ac0b9599444f6"; document.write('<\/mip-script>'); })(); (function(){ var bp = document.createElement('script'); var curProtocol = window.location.protocol.split(':')[0]; if (curProtocol === 'https') { bp.src = 'https://zz.bdstatic.com/linksubmit/push.js'; } else { bp.src = 'http://push.zhanzhang.baidu.com/push.js'; } var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(bp, s); })();