微前端的優勢和好處

已發表: 2022-03-11

微前端架構是一種設計方法,其中前端應用程序被分解成單獨的、半獨立的“微應用程序”,鬆散地一起工作。 微前端概念模糊地受到微服務的啟發並以微服務命名。

微前端模式的好處包括:

  1. 微前端架構可能更簡單,因此更容易推理和管理。
  2. 獨立開發團隊可以更輕鬆地在前端應用程序上進行協作。
  3. 他們可以通過同時運行“新”應用程序來提供從“舊”應用程序遷移的方法。

儘管微前端最近受到了很多關注,但到目前為止還沒有單一的主導實現,也沒有明確的“最佳”微前端框架。 事實上,根據目標和要求,有多種方法。 請參閱參考書目以了解一些更知名的實現。

在本文中,我們將跳過微前端的大部分理論。 以下是我們不會涉及的內容:

  • 將應用“切片”成微應用
  • 部署問題,包括微前端如何適應 CI/CD 模型
  • 測試
  • 微應用是否應該與後端的微服務一對一對齊
  • 對微前端概念的批評
  • 微前端和普通的舊組件架構之間的區別

相反,我們將提供一個專注於具體實現的微前端教程,重點介紹微前端架構中的重要問題及其可能的解決方案。

我們的實現稱為 Yumcha。 粵語“yum cha”的字面意思是“喝茶”,但日常的意思是“出去吃點心”。 這裡的想法是,應用程序中的各個微應用程序(我們將稱之為組合的頂級應用程序)類似於點心午餐時帶出的各種一口大小的籃子。

如上所述的基於微前端的示例應用程序的概述圖。

我們有時會將 Yumcha 稱為“微前端框架”。 在當今世界,術語“框架”通常用於指代 Angular、React、Vue.js 或其他類似的 Web 應用程序上層結構。 我們根本不是在談論那種意義上的框架。 我們稱 Yumcha 為框架只是為了方便:它實際上更像是一組工具和一些用於構建基於微前端的應用程序的薄層。

微前端教程第一步:組合應用程序的標記

讓我們深入思考如何定義宏應用程序和構成它的微應用程序。 標記一直是網絡的核心。 因此,我們的宏應用程序將通過以下標記來指定:

 <html> <head> <script src="/yumcha.js"></script> </head> <body> <h1>Hello, micro-frontend app.</h1> <!-- HERE ARE THE MICROAPPS! --> <yumcha-portal name="microapp1" src="https://microapp1.example.com"></yumcha-portal> <yumcha-portal name="microapp2" src="https://microapp2.example.com"></yumcha-portal> </body> </html>

使用標記定義我們的宏應用程序使我們能夠充分利用 HTML 和 CSS 的力量來佈局和管理我們的微應用程序。 例如,一個微應用程序可以位於另一個微應用程序的頂部或側面,或位於頁面的角落,或位於手風琴的一個窗格中,或保持隱藏直到發生某些事情,或永久留在後台.

我們將用於微應用的自定義元素命名為<yumcha-portal> ,因為“門戶”是門戶提案中使用的微應用的一個有前途的術語,這是定義用於微前端的標準 HTML 元素的早期嘗試。

實現<yumcha-portal>自定義元素

我們應該如何實現<yumcha-portal> ? 既然它是一個自定義元素,當然是作為一個 Web 組件! 我們可以從眾多強大的競爭者中選擇編寫和編譯微前端 Web 組件; 在這裡,我們將使用聚合物項目的最新版本 LitElement。 LitElement 支持基於 TypeScript 的語法糖,它為我們處理大部分自定義元素樣板。 為了使<yumcha-portal>可用於我們的頁面,我們必須將相關代碼包含為<script> ,就像我們在上面所做的那樣。

但是<yumcha-portal>實際上做了什麼? 第一個近似值是它只創建一個具有指定源的iframe

 render() { return html`<iframe src=${this.src}></iframe>`; }

…其中render是標準的 LitElement 渲染鉤子,使用其html標記的模板字面量。 對於一些瑣碎的用例來說,這個最小的功能可能已經足夠了。

iframe中嵌入微應用

iframe是每個人都喜歡討厭的 HTML 元素,但實際上它們提供了非常有用的、堅如磐石的沙盒行為。 但是,在使用iframe時仍然需要注意一長串問題,這些問題可能會對我們的應用程序的行為和功能產生影響:

  • 首先, iframe在大小和佈局方面有眾所周知的怪癖。
  • CSS 當然會完全隔離iframe ,無論好壞。
  • 瀏覽器的“後退”按鈕會很好地工作,儘管iframe的當前導航狀態不會反映在頁面的 URL中,所以我們既不能剪切和粘貼 URL 以達到組合應用程序的相同狀態,也不能進行深度鏈接給他們。
  • 根據我們的 CORS 設置,從外部與iframe的通信可能需要通過postMessage協議
  • 必須為iframe邊界的身份驗證做出安排。
  • 一些屏幕閱讀器可能會在iframe邊界處絆倒,或者需要iframe有一個可以向用戶宣布的標題。

不使用iframe可以避免或減輕其中一些問題,我們將在本文後面討論另一種選擇。

從好的方面來說, iframe將擁有自己獨立Content-Security-Policy (CSP)。 此外,如果iframe指向的微應用使用服務工作者或實現服務器端渲染,則一切都會按預期工作。 我們還可以為iframe指定各種沙盒選項以限制其功能,例如能夠導航到頂部框架。

一些瀏覽器已經發布或計劃為iframe發布loading=lazy屬性,該屬性會延遲加載首屏iframe直到用戶滾動到它們附近,但這並不能提供對延遲加載的細粒度控制我們想要。

iframe的真正問題是iframe的內容需要多個網絡請求才能檢索。 頂級index.html被接收,它的腳本被加載,它的 HTML 被解析——但是瀏覽器必鬚髮起另一個對iframe的 HTML 的請求,等待接收它,解析和加載它的腳本,然後渲染iframe的內容。 在許多情況下, iframe的 JavaScript 仍然需要啟動,進行自己的 API 調用,並且只有在這些 API 調用返回並處理數據以供查看後才顯示有意義的數據。

這可能會導致不希望的延遲和渲染偽影,尤其是在涉及多個微應用時。 如果iframe的應用程序實現了 SSR,這將有所幫助,但仍不能避免額外往返的必要性。

因此,我們在設計門戶實施時面臨的主要挑戰之一是如何處理這種往返問題。 我們的目標是單個網絡請求應該關閉整個頁面及其所有微應用,包括每個微應用能夠預填充的任何內容。 這個問題的解決方案在於 Yumcha 服務器。

Yumcha 服務器

此處介紹的微前端解決方案的一個關鍵要素是設置專用服務器來處理微應用組合。 該服務器將請求代理到託管每個微應用的服務器。 當然,設置和管理此服務器需要付出一些努力。 一些微前端方法(例如,single-spa)試圖以易於部署和配置的名義免除對此類特殊服務器設置的需求。

然而,建立這個反向代理的成本被我們獲得的收益所抵消; 事實上,沒有它我們根本無法實現基於微前端的應用程序的一些重要行為。 設置這種反向代理有許多商業和免費的替代方案。

反向代理除了將微應用請求路由到適當的服務器外,還將宏應用請求路由到宏應用服務器。 該服務器以一種特殊的方式為組合應用程序提供 HTML。 在通過代理服務器在諸如http://macroapp.example.com之類的 URL 處從瀏覽器接收到對index.html的請求後,它會檢索index.html ,然後在返回之前對其進行簡單但關鍵的轉換它。

具體來說,HTML 被解析為<yumcha-portal>標記,這可以通過 Node.js 生態系統中可用的勝任 HTML 解析器之一輕鬆完成。 使用<yumcha-portal>src屬性,聯繫運行微應用的服務器並檢索其index.html — 包括服務器端呈現的內容(如果有)。 結果作為<script><template>標記插入到 HTML 響應中,以免被瀏覽器執行。

Yumcha 的服務器架構示意圖。瀏覽器與反向代理通信,後者又與宏應用程序和每個微應用程序通信。 macroapp 步驟轉換並預填充應用程序的主 index.html 文件。

這種設置的優勢首先在於,在第一次請求組合頁面的index.html時,服務器可以從各個微應用服務器中完整地檢索各個頁面——包括 SSR 渲染的內容,如果任何——並向瀏覽器提供一個完整的頁面,包括可用於填充iframe的內容,無需額外的服務器往返(使用未充分利用的srcdoc屬性)。 代理服務器還確保任何有關微應用程序從何處提供服務的細節都不會被窺探。 最後,它簡化了 CORS 問題,因為應用程序請求都來自同一個來源。

回到客戶端, <yumcha-portal>標籤被實例化並找到服務器在響應文檔中放置的內容,並在適當的時間呈現iframe並將內容分配給它的srcdoc屬性。 如果我們不使用iframe (見下文),那麼對應於該<yumcha-portal>標記的內容將插入到自定義元素的影子 DOM 中,如果我們正在使用它,或者直接內聯到文檔中。

至此,我們已經有了一個部分功能的基於微前端的應用程序。

這只是 Yumcha 服務器有趣功能的冰山一角。 例如,我們希望添加一些功能來控制如何處理來自微應用服務器的 HTTP 錯誤響應,或者如何處理響應非常緩慢的微應用——如果一個微應用沒有,我們不想永遠等待頁面提供服務回應! 這些和其他主題我們將留給另一篇文章。

Yumcha 宏應用index.html轉換邏輯可以輕鬆地以無服務器 lambda 函數方式實現,或者作為 Express 或 Koa 等服務器框架的中間件。

基於存根的微應用控制

回到客戶端,我們如何實現微應用還有另一個方面對效率、延遲加載和無卡頓渲染很重要。 我們可以為每個微應用生成iframe標籤,或者使用src屬性(發出另一個網絡請求)或使用srcdoc屬性填充服務器為我們填充的內容。 但在這兩種情況下,該iframe中的代碼將立即啟動,包括加載其所有腳本和鏈接標籤、引導以及任何初始 API 調用和相關數據處理——即使用戶甚至從未訪問過有問題的微應用。

我們對這個問題的解決方案是最初將頁面上的微應用表示為微小的未激活存根,然後可以將其激活。 激活可以由進入視野的微應用區域驅動,使用未充分使用的IntersectionObserver API,或者更常見的是通過從外部發送的預先通知。 當然,我們也可以指定立即激活微應用。

在任何情況下,當且僅當微應用被激活時, iframe才會真正呈現,並且其代碼會被加載和執行。 就我們使用 LitElement 的實現而言,假設激活狀態由activated的實例變量表示,我們將有如下內容:

 render() { if (!this.activated) return html`{this.placeholder}`; else return html` <iframe srcdoc="${this.content}" @load="${this.markLoaded}"></iframe>`; }

微應用間通信

儘管組成宏應用的微應用在定義上是鬆散耦合的,但它們仍然需要能夠相互通信。 例如,導航微應用需要發送通知,提示用戶剛剛選擇的其他微應用應該被激活,而要激活的應用需要接收此類通知。

根據我們極簡主義的心態,我們希望避免引入大量的消息傳遞機制。 相反,本著 Web 組件的精神,我們將使用 DOM 事件。 我們提供了一個簡單的廣播 API,它預先通知所有存根即將發生的事件,等待任何已請求激活的事件類型被激活,然後針對文檔分派事件,任何微應用都可以在該文檔上偵聽它。 鑑於我們所有的iframe都是同源的,我們可以從iframe到達頁面,反之亦然,以找到觸發事件的元素。

路由

在這個時代,我們都開始期望 SPA 中的 URL 欄代表應用程序的視圖狀態,因此我們可以剪切、粘貼、郵寄、文本和鏈接到它,以直接跳轉到應用程序內的頁面。 然而,在微前端應用程序中,應用程序狀態實際上是狀態的組合,每個微應用程序一個狀態。 我們如何表現和控制這一點?

解決方案是將每個微應用的狀態編碼為單個複合 URL,並使用一個小型宏應用路由器,該路由器知道如何將該複合 URL 放在一起並將其分開。 不幸的是,這需要每個微應用中特定於 Yumcha 的邏輯:從宏應用路由器接收消息並更新微應用的狀態,相反,通知宏應用路由器該狀態的變化,以便可以更新復合 URL。 例如,可以想像 Angular 的YumchaLocationStrategy或 React 的<YumchaRouter>元素。

表示宏應用狀態的複合 URL。它的查詢字符串解碼為兩個單獨的(雙重編碼)查詢字符串,然後將其傳遞給其 id 被指定為其鍵的微應用。

iframe案例

如上所述,在iframe中託管微應用確實有一些缺點。 有兩種選擇:將它們直接內聯到頁面的 HTML 中,或者將它們放置在 shadow DOM 中。 這兩種選擇都在一定程度上反映了iframe的優缺點,但有時方式不同。

例如,必須以某種方式合併各個微應用 CSP 策略。 屏幕閱讀器等輔助技術應該比iframe更好地工作,假設它們支持 shadow DOM(目前還不是全部支持)。 使用“範圍”的服務工作人員概念來安排註冊微應用的服務工作人員應該很簡單,儘管應用程序必須確保其服務工作人員是在應用程序的名稱下註冊的,而不是"/" 。 與iframe相關的佈局問題都不適用於內聯或影子 DOM 方法。

但是,使用 Angular 和 React 等框架構建的應用程序可能會不滿意內聯或影子 DOM。 對於那些,我們可能會想要使用iframe

就 CSS 而言,內聯和影子 DOM 方法有所不同。 CSS 將被乾淨地封裝在 shadow DOM 中。 如果出於某種原因我們確實想與 shadow DOM 共享外部 CSS,我們將不得不使用可構造的樣式表或類似的東西。 使用內聯微應用,所有 CSS 將在整個頁面中共享。


最後,在<yumcha-portal>中實現內聯和影子 DOM 微應用的邏輯很簡單。 我們檢索給定微應用的內容,從服務器邏輯將其作為 HTML <template>元素插入到頁面中,克隆它,然後將其附加到 LitElement 調用的renderRoot中,這通常是元素的影子 DOM,但可以對於內聯(非影子 DOM)情況,也可以設置為元素本身( this )。

可是等等! 微應用服務器提供的內容是一個完整的 HTML 頁面。 我們不能將帶有htmlheadbody標籤的微應用的 HTML 頁面插入到宏應用的中間,可以嗎?

我們通過利用template標籤的一個怪癖來解決這個問題,其中包裝了從微應用服務器檢索到的微應用內容。 事實證明,當現代瀏覽器遇到template標籤時,雖然它們不會“執行”它,但會解析它,並在這樣做時刪除無效內容,例如<html><head><body>標籤,同時保留其內在內容。 因此<head>中的<script><link>標記以及<body>的內容都被保留了。 這正是我們想要將微應用內容插入頁面的目的。

微前端架構:細節中的魔鬼

如果(a)它們被證明是一種更好的架構方法,並且(b)我們可以弄清楚如何以滿足當今網絡無數實際需求的方式實現它們,那麼微前端將在 webapp 生態系統中紮根。

關於第一個問題,沒有人聲稱微前端是所有用例的正確架構。 特別是,單個團隊的綠地開發幾乎沒有理由採用微前端。 我將把什麼類型的應用程序在什麼類型的上下文中最能從微前端模式中受益的問題留給其他評論員。

在實施和可行性方面,我們已經看到有很多細節需要關注,其中包括一些在本文中甚至沒有提到的細節——特別是身份驗證和安全、代碼複製和 SEO。 儘管如此,我希望本文為微前端提供了一種基本的實現方法,經過進一步的改進,可以滿足現實世界的需求。

參考書目

  • 微前端 — 角度風格 — 第 1 部分
  • 微前端 — 角度風格 — 第 2 部分
  • 使用微前端開發 AngularJS 應用程序
  • 微前端
  • UI 微服務——逆轉反模式(微前端)
  • UI 微服務——一種反模式?
  • 使用微前端的頁面構建採用類似於 Yumcha 的反向代理和 SSI 方法,我強烈推薦。
  • 微前端資源
  • 講台
  • 我不了解微前端。 這是對微前端架構類型和用例的一個很好的概述。
  • 使用 Vue.js、AWS Lambda 和 Hypernova 的無服務器微前端
  • 微前端:一個偉大的、全面的概述。