微前端——功能團隊中缺失的一塊拼圖

在任何合法的前端開發團隊中,提高可擴展性和敏捷性很少會成爲頭等大事。在處理大型、複雜的產品時,如何確保快速、頻繁地交付同時包含後端和前端的功能?像後端那樣將前端單體分解成許多更小的部分似乎是答案。如果執行得當,微前端可以提高團隊的有效性和效率。就是這樣。

微前端背後的想法是將網站或 Web 應用程序視爲由獨立團隊擁有的功能的組合。每個團隊都有自己關心和擅長的不同業務領域或任務。團隊是跨職能的,從數據庫到用戶界面,端到端地開發其功能。

將較大的問題分解爲較小的問題以提高敏捷性、可重用性和可擴展性一直是 IT 的聖盃之一,過去二十年來該領域取得的進展令人震驚。但是,使用越來越大的代碼庫的新限制仍在不斷出現。業內最有頭腦的人一直在努力應對這一挑戰,試圖從技術和組織的角度來解決這個問題。迄今爲止,微服務架構肯定仍然最接近解決它。

在本文中,您將學習:

微服務架構和微前端——它們解決了什麼以及它們如何運作

微服務架構承諾:

解決方案是將大型系統拆分爲圍繞明確定義的業務能力組織的細粒度、鬆散耦合的服務。然後,每個服務都足夠小,可以由一個開發團隊開發,並且可以快速調整以適應新的業務需求並進行部署,而不會破壞系統的其他部分。

這種方法的危險在於系統將被分解成許多不相關的應用程序。儘管這對開發人員來說很好處理,但這並不是用戶對系統的期望;大多數人不喜歡使用大量的小型應用程序來完成他們的工作。因此,必須將爲此過程分解的內容重新組合到用戶界面中。

將碎片重新組合在一起

常見的方法是在用戶和微服務之間構建一個單一的前端層。對於每一個重要的系統,龐大的前端代碼庫都會威脅到微服務架構的好處。這就是微前端的用武之地。

微前端的基本思想非常簡單——將用戶界面由多個圍繞明確定義的業務能力組織起來的細粒度部分組成。然後每個部分都足夠小,可以由一個垂直結構的團隊開發和擁有。擁有前端和後端的團隊建立了一個真正自給自足的功能團隊。

然而不幸的是,微前端帶來了同樣的挑戰,使得微服務難以實現。此外,任何捷徑都可能以負面的方式影響用戶體驗。必須確保一致性、安全性、互操作性、性能、可擴展性和所有權,以確保無縫的用戶界面。

儘管不同的微前端方法解決了各種問題,但還沒有一個能夠涵蓋所有這些問題。但在我們深入研究這些不同方法的細節之前,讓我們先仔細看看選擇微前端的主要動機,以及與單體方法相比的優缺點。

 解釋微前端的 3 大優勢

微前端的優勢 #1:可擴展的團隊設置

3 種不同的前端方法及其對團隊組織的影響
選擇微前端方法有很多很好的理由,但最重要的(如果不是最重要的)之一來自可擴展的團隊設置。

至少可以確定影響開發團隊組織的三種可能的前端架構:單片前端和後端、帶有後端微服務的單片前端和微前端。下面,我們將描述每個團隊組織的後果。

單片前端和後端

構建需要前端和後端的解決方案的常用方法是水平拆分項目並通過 REST API 在這些層之間進行通信。

如果系統足夠小,可以供一個團隊開發,最好的選擇是保持架構簡單。您可以通過創建單頁應用程序 (SPA) 前端並通過 REST API 將其連接到後端來實現此目的。然後根據每一層所需的工作量調整您的團隊設置。

良好的做法是確保從一開始您的代碼就結構良好,並且當您的解決方案增長時,您可以引入另一個或兩個團隊,而無需重新構建它。

但是這種架構的侷限性是顯而易見的:團隊修改代碼庫的次數越多,就需要更多的協調和集成,這會導致產品開發癱瘓。

帶有後端微服務的單片前端

尤其是在大型企業中,對額外開發團隊的需求通常從後端開始。大多數系統需要大量的業務邏輯。例如,它們需要在瀏覽器端無法完成的處理,或者與遺留系統的複雜集成。在這些情況下,一個跨職能團隊已經不夠了。

大多數公司擴展其架構的第一步是垂直拆分後端代碼庫以解決複雜性。前端被分配給一個專門的前端團隊,而其餘的工作則分配給各個後端團隊。因此,積壓的項目被分解成塊,並由不同的團隊交付。然後,這些團隊要麼通過合同談判解決依賴關係,要麼——以更敏捷的方法——通過大量積壓計劃來解決依賴關係。

這種方法帶來的最大挑戰是管理這些依賴關係和同步發佈。測試也變得很麻煩,因爲您需要等待所有單獨的部分到達測試環境才能驗證它。

當解決方案必須嵌入來自不同產品 backlog 的功能時,它變得更加複雜(這種情況經常發生)。

在更大的範圍內,這種方法需要大量的管理。日常部署幾乎是不可能的。這就是爲什麼在具有複雜前端的大型企業中工作的開發人員和架構師尋求最終垂直擴展的解決方案,將前端添加到他們已經改變遊戲規則的微服務架構 - 微前端。

微前端

爲了快速開發、測試和發佈其功能,團隊需要能夠在不依賴其他團隊的情況下工作。微前端可以在用戶界面領域實現後端微服務的相同承諾,並且可以應用支持獨立團隊合作的相同原則。

在此設置中,前端和後端這兩個領域緊密耦合,因爲需求來自一個產品待辦列表。再一次,一個團隊可以在一個簡單的架構中交付整個功能。如果執行得當,這不會影響用戶體驗。

爲了很好地執行它,微前端帶來了許多後端微服務已知的類似問題,必須解決。否則,用戶可能仍將系統感知或體驗爲不同特徵的拼湊。

微前端的優勢 #2:技術選擇自由

除了創建可擴展且獨立的團隊設置外,微前端方法還有助於處理應用於前端的大量技術。每個季度都有關於如何開發面向用戶的系統部分的新想法。很多時候,新版本的框架甚至不向後兼容,這意味着每個版本實際上都是一個單獨的框架。

微服務架構的優勢之一是可以自由選擇所使用的技術。當用戶界面被拆分成獨立的模塊時,前端開發人員可以享有同樣的自由——至少在一定程度上。

微前端的優勢 #3:彈性

任何系統的實際成本都不能很好地體現在代碼庫的初始開發成本上,而是體現在維護上。代碼重構和系統重構的無休止螺旋的目的是保持與開始時相同的速度引入功能更改。

微前端架構通過引入以下約束使代碼庫更具彈性:

這些約束防止不受控制的依賴關係、限制代碼重用和強制服務邊界。

不再有不受控制的依賴

多年來開發的大型應用程序不可避免地充滿了難以跟蹤和維護的代碼依賴關係。開發人員在上市時間的壓力下工作,或者只是試圖優化他們的工作方式,會在代碼的不同部分之間產生許多不受控制的依賴關係。當引入新的依賴項時,重用一些業務邏輯、緩存數據或資源池似乎總是一個好主意。後來發生的事情是這種共享功能變化的不可預見的後果。

通過將代碼庫分成幾個不相連的部分,很容易避免這種依賴關係。由於兩者之間的自然邊界,一個微前端不可能重用其他微前端的現有功能。因此,更改的任何意外副作用僅限於一個微前端。

有限的代碼重用

普遍遵循的原則不要重複自己(DRY)的目的是限制代碼庫的大小,從而降低出錯的可能性。另一方面,開發成代碼的每個抽象都引入了依賴性。隨着時間的推移,抽象也經常出現必須根據特定的使用上下文進行調整。當您的微前端代碼庫僅限於幾個功能時,開發人員不太可能試圖創建這樣的抽象。相反,當他們找到重用代碼的機會時,他們只是複製並粘貼相關的片段,這通常比引入依賴項要好得多。

服務邊界執行

系統的架構通常受某些分析和設計決策的影響。然而,決定某事和遵守這些決定往往是不一樣的。在服務被拆分並就職責達成一致並記錄在案之後,漂移就開始了。缺乏明確的邊界或跨越邊界的能力可能導致並允許開發人員破壞先前商定的設計。有一些工具,例如在 CI/CD 管道中實現的代碼審查或依賴檢查,但使用單獨的微前端,不可能偏離約定的架構。

微前端的 6 個常見要求

爲了不失去微前端的任何潛在好處,這種架構的實際實現必須滿足一些共同的要求。以下六點很重要,不容忽視:

獨立部署

——使用微前端的最大風險是,當它們沒有正確實施時,它們將需要部署協調。除了將有意義的功能封裝在單個組件中並始終確保向後兼容性的良好設計之外,組件本身必須可以一個一個地部署,而無需任何協調。

熱部署

——開發某些應用程序片段的團隊必須能夠部署新版本而不會造成任何停機。必須考慮使用滾動更新或金絲雀部署等策略。使用帶有經過深思熟慮的路徑系統的高級 HTTP 路由機制可以提供很大幫助。

統一的樣式 / 組件

——將應用程序構建爲不兼容的拼貼塊可能會對用戶體驗產生破壞性影響。如何確保視覺和行爲一致性的問題必須從引入微前端的一開始就解決。該解決方案通常涵蓋技術(共享 UX 庫)和組織方面(共享 UX 團隊)。

身份驗證和授權

——顯然,用戶必須只進行一次身份驗證。授權上下文和規則必須由前端和後端的所有組件共享。

跨組件通信

——即使組件之間的通信引入了耦合並因此應該避免,但很難想象一個應用程序由完全分離的部分組成。特定的微前端必須能夠共享應用程序上下文(即用戶或其他資源標識)並相互通知內部狀態的變化。選擇的通信方法應該更喜歡基於事件或地址欄的間接通信,而不是直接使用其他組件 API。

搜索引擎優化

——這種需求的嚴重程度取決於具體的用例,但對於某些應用程序來說,它是要解決的第一類公民。

微前端技術——解耦前端的 3 種方法

微前端技術可以分爲三類,每一種技術都有一些優點和缺點。我們可以區分:

 下面我們通過使用像 Strava 或 Endomondo 這樣的健身追蹤應用程序的示例來仔細研究這三種方法中的每一種。這些應用程序中的每一個都具有相似的特性和功能,例如顯示運動員個人資料摘要、他們的最新活動、一些正在進行的挑戰等的儀表板。

構建時集成

解耦前端的第一種方法是將代碼庫組織在獨立的存儲庫中。通過構建時集成,每個微前端都作爲獨立包構建和發佈。完整的應用程序導入這些包並從包含的組件組成用戶界面。

這樣,在組織團隊和適當劃分團隊之間的功能上稍加努力,就可以實現合理的團隊獨立性。雖然它不會提供微前端的所有可能好處,例如技術多樣性或部署自主性,但它的優點是它不需要任何其他工具,除了開發人員已經使用的工具。

與其他方法相比,構建時集成簡化了一致性保證。這也是減少傳輸到用戶瀏覽器的數據量的最簡單和最有效的方法,因爲整個應用程序包在構建階段進行了優化。

在我們的示例中設計健身跟蹤應用程序時需要考慮的是使用組件之間的間接通信,這將減少耦合。

服務器端集成

第二種方法的總體思路如下圖所示。

瀏覽器對頁面 (1) 的請求來自 “佈局服務”,該服務首先爲頁面佈局 (2) 請求“頁面模板服務”。佈局包含 HTML 兼容標籤,其中包含要包含的頁面片段的 URL (3)。“佈局服務” 請求實現特定功能的所有包含部分調用服務的內容。佈局服務的更高級實現並行執行查詢 (4),支持故障轉移和快速響應流。

在檢索到所有部分 (5) 之後,準備好請求頁面的全部內容並將其返回到瀏覽器 (6)。

佈局服務在從用戶角度建立應用程序的一致行爲方面發揮着核心作用,但它由獨立開發和部署的部分組成。

當應用程序包含由許多獨立尾部組成的頁面時,服務器端集成非常有用,有些是用戶特定的,有些是用戶之間共享的,如電子商務網站通常具有的。

有很多技術可以應用這種模式,例如服務器端包含、邊緣端包含和 Zalando 的帶有 “片段” 標籤的項目 Mosaic。

服務器端包括

服務器端包含 (SSI) 是一種由 Web 服務器解釋的腳本語言,用於將一個或多個文件的內容包含到網頁中。語言語法基於放置在 HTML 註釋中的指令,這些指令由啓用 SSI 的 Web 服務器處理。

<!--#directive parameter=value parameter=value -->

最常用的指令是 “包含”,允許將一個文檔包含到另一個文檔中。包含的文檔可以通過相對於當前文件目錄的路徑來引用。例如:

<!--#include file="footer.html" -->

更強大和推薦的選項是使用虛擬參數,它可以指定任何資源,包括 CGI 腳本或遠程頁面:

<!--# include virtual="/athlete/768769879/details" -->

該語言還提供變量和條件子句,以在頁面上創建更多上下文感知內容。

SSI 受到流行的 Web 服務器(如 Apache 或 NGINX)的支持。

邊側包括

Edge Side Includes (ESI) 是一種用於邊緣級動態 Web 內容組裝的標記語言。ESI 的目標是解決 Web 基礎設施擴展和內容發佈的問題。與 SSI 類似,它通過 esi:include 標籤提供嵌入:

<esi:include src="http://befit.com/footer.html" />

條件執行命令與大量預定義變量相結合,可以在提供給用戶的頁面上實現真正的動態內容。

<esi:choose>
    <esi:when test="$(HTTP_COOKIE{group})=='Advanced'">
        <esi:include src="http://befit.com/advanced.html"/>
    </esi:when>
    <esi:when test="$(HTTP_COOKIE{group})=='Basic User'">
        <esi:include src="http://befit.com/basic.html"/>
    </esi:when>
    <esi:otherwise>
        <esi:include src="http://befit.com/new_user.html"/>
    </esi:otherwise>
</esi:choose>

緩存代理服務器(如 Varnish 或 Squid)支持 ESI。

Zalando 的馬賽克項目

Mosaic 內置於 Zalando,是首批成熟的、有目的的、用於實現服務器端集成的微前端框架之一。

Mosaic 架構的中心點是 “Tailor”,即在這種服務器端微前端架構中實現佈局服務。爲了在頁面中包含微前端,使用了“片段” 標籤:

<html>
<head>
    <script type="fragment" src="http://assets.befit.com"></script>
</head>
<body>
    <fragment src="http://athlete.befit.com"></fragment>
    <fragment src="http://trainng-log.befit.com" primary></fragment>
    <fragment src="http://challanges.befit.com" async></fragment>
</body>
</html>

與普通的 SSI 或 ESI 標籤相比,片段標籤提供了額外的有用屬性:

如上所述,Mosaic 旨在爲微前端提供服務,並提供以下功能優勢:

如果需要更復雜的模板管理,可以簡單地從文件系統或專用服務提供頁面模板。

馬賽克的第二部分是船長。在 Innkeeper 的陪伴下,Skipper 建立了一個先進的 HTTP 路由器,可以在需要隱藏複雜的微服務世界時使用。Skipper 本身提供了基於規則的 HTTP 請求路由,具有過濾和豐富功能。Innkeeper 用作運行時 Skipper 規則管理的 API。

服務器端集成允許從微前端輕鬆編寫應用程序,但它不能解決需要真正豐富的前端應用程序時出現的挑戰。

客戶端集成

最後但並非最不重要的一點是客戶端集成方法。在這裏,微前端的構建是將應用程序集成到用戶 Web 瀏覽器中。應用程序的每個部分都獨立交付給瀏覽器,然後應用程序在呈現時被粘合。

使用這種方法,在運行時構建應用程序不需要額外的基礎設施,而且它似乎是最靈活的。應用程序組件可以共享一些用戶上下文,因此就像在構建時集成的那樣,而不會影響微前端的其他要求。

Iframes

iframes 是一種舊的客戶端集成技術,可用於將一個 HTML 文檔嵌入到另一箇中。在微前端的上下文中,解決方案在於使用 iframe 標記嵌入每個微前端應用程序頁面佈局,其中 src 屬性指向爲應用程序提供服務的 URL。

與這種方法中的 SSI/ESI 類似,每個微前端都可以託管在不同的地址上。與 SSI/ESI 相反,客戶端瀏覽器負責獨立下載每個片段並顯示完整頁面。

<html>
   <body>
     <iframe src="http://athlete.befit.com" style="border: none;"/>
     <iframe src="http://trainng-log.befit.com" style="border: none;"/>
     <iframe src="http://challanges.befit.com" style="border: none;"/>
   </body>
</html>

要成爲一個成熟的微前端,嵌入在 iframe 中的應用程序應該能夠與其父級通信。這可以通過使用 window.postMessage() 方法來實現,但在大多數情況下,將這樣一個低級 API 與一個從微前端的角度簡化集成的庫包裝是一種很好的做法。

除了涵蓋影響渲染內容狀態的微前端之間的數據交換的標準用例之外,還需要啓用父級和微前端之間的通信。後者確保 iframe 的大小適合微前端內容的大小。當 iframe 內容溢出時,必須將有關嵌入內容的實際大小的信息傳播到父應用程序,並且必須由父應用程序調整 iframe 高度。

當微前端平臺本身需要基於 iframe 的集成以確保父應用程序和微前端之間的最高級別隔離時,它的效果最好。在這種情況下,可以使用任何技術或框架創建微前端,包括在客戶端集成中獨一無二的簡單遺留應用程序集成。

微前端的部署也不需要任何特殊的方式來構建或打包源代碼。

iframe 方法確保部署新版本的微前端不會影響其他已經部署的微前端。這種技術自由保證了整個系統不會卡在某個框架中,因爲不需要微前端的兼容性。這樣可以根據每個開發團隊的實際業務優先級來支付技術債務。

這種高度隔離簡化了集成,但同時它會導致一些 UX 限制,在考慮您的集成解決方案時應該考慮這些限制。

當您的主要關注點在於 UX 設計時,iframe 絕對不是最佳選擇。可以提供良好的 UX 設計(在響應式網頁設計的情況下也是如此),但它比其他方法稍微複雜一些。主要限制是由於微前端內容不能超出 iframe 邊界。例如,顯示在多個 iframe 上的彈出窗口無法正確顯示。

需要考慮的另一個因素是下載到瀏覽器的資源開銷。特定微前端所需的每個資源(css、js 等)都必須單獨下載。儘管對於現在客戶端使用的大多數工作站來說這可能不是問題,但請注意,僅將前端框架核心庫的一個實例加載到內存中是不可能的。

Single SPA

Single SPA 是一個 JavaScript 框架,旨在構建由多個單頁應用程序組成的用戶界面,它承諾許多框架的共存。甚至同一框架的不同版本也可以混合在一個頁面中。

使用 Single SPA 時,每個微前端都可以獨立部署。另一個不錯的功能是延遲加載代碼。僅在需要時才加載特定的微前端包,這提高了應用程序的加載速度。

任何 Single SPA 應用程序的架構都包含兩個概念:

將微前端嵌入到 Single SPA 中不需要對前端進行大量調整。新的微前端聲明需要實現單個 SPA 生命週期函數併爲主應用程序公開具有這些實現的文件。

單個 SPA 生命週期函數與 React 組件生命週期函數非常相似——對象 props 具有屬性 domElementGetter,返回應該放置或刪除微前端的 DOM 元素。

如何在代碼中將前端應用程序標記爲單個 SPA 微前端
如果要將前端應用程序標記爲 Single SPA,第一步是準備一個主微前端文件並實現生命週期方法。這可以手動完成,但框架提供了一個方便的助手來完成它。

import React from 'react'
import ReactDOM from 'react-dom'
import singleSpaReact from 'single-spa-react'
import root from './root.component.js'

const reactLifecycles = singleSpaReact({
   React,
   ReactDOM,
   rootComponent: root
})

export function bootstrap(props) {
    return reactLifecycles.bootstrap(props);
}

export function mount(props) {
    return reactLifecycles.mount(props);
}

export function unmount(props) {
    return reactLifecycles.unmount(props);
}

在第二步中,您可以使用 Webpack 將您的應用程序捆綁到一個捆綁文件中,例如運動員. bundle.js,並從客戶端可訪問的任何服務器公開它。

如何在前端應用程序中使用 Single SPA 微前端

應用概念 = 單一 SPA 作爲框架(The application concept = Single SPA as a framework)

第一步是在根應用程序中註冊一個微前端。爲此,您應該使用以下功能:

registerApplication(
    appName: string,
    applicationOrLoadingFn: () => <Function | Promise>,
    activityFn: (location) => boolean,
    customProps?: Object
);

最後一步是 start() 單一 SPA 引擎,然後一切就緒。

import {registerApplication, start} from 'single-spa';

const domElementGetter = () => document.getElementById('micro-font-end-container');
const pathPrefix = (prefix) => {
   return (location) => location.pathname.startsWith(`${prefix}`);
}

registerApplication('athletes', () => import ('athletes.bundle.js'), 
pathPrefix('/athletes'), {domElementGetter});
registerApplication('challenges', () => import ('challenges.bundle.js'), 
pathPrefix('/challenges'), {domElementGetter});
start();
<html>
<body>
   <script src="/dist/root.js"></script>
   <a onclick="singleSpaNavigate('/athletes')">Go to athletes</a>
   <a onclick="singleSpaNavigate('/challenges)">Go to challenges</a> 
   <div id="micro-font-end-container">
       //i.e. here micro frontends can be injected
   </div>
</body>
</html>

包裹概念 = 作爲庫的單一 SPA (The parcel concept = Single SPA as a library)

當使用 Single SPA 作爲框架時,容器應用程序是一個簡單的應用程序容器,這些應用程序會根據根更改進行切換。另一種選擇是包裹概念。在這裏,您在任何框架中創建一個容器應用程序作爲系統的基礎,並且必須將包(或實際上是微前端)直接安裝在特定位置。這樣一頁可以包含多個微前端。這更接近於將用戶界面構建爲解耦特徵的組合,但同時可見和可訪問的概念。

包裹也應該在正確的時間卸載。

在下面的示例中,使用 React 作爲主要框架,因此 componentDidMount 和 componentWillUnmount 可用於掛載和卸載包裹。

class AthletesMicrofrontendComponent extends React.Component {

    componentDidMount() {
        SystemJS.import('athletes.bundle.js')
            .then(athletesApp => {
               const domElement = document.getElementById('athelete-micro-frontend-id');
               mountParcel(athletesApp, {domElement})
            });
    }

    render(){
       return <div id="athelete-micro-frontend-id"></div>
    }
    ...
}

微前端方案比較

下表總結並強調了每種方法的主要優勢和劣勢:

K7Eods

概括

如果你想充分利用你的微服務架構,微前端模式似乎真的是拼圖中缺失的一塊。根據您的需求,確切的解決方案可能會有所不同。例如,對於電子商務網站,您可以選擇任何類型的服務器端集成。但是,當您的目標是高級覆蓋應用程序時,其中一種客戶端技術將是您更好的解決方案。

如果您認爲微前端仍然會帶來太多麻煩,那麼至少選擇構建時模塊化。從長遠來看,它總是會得到回報。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/smzQuXFqUlhamVCFYmVovQ