1. 程式人生 > 實用技巧 >Micro Frontends extending the microservice idea to frontend development(譯)

Micro Frontends extending the microservice idea to frontend development(譯)

原文地址:Micro Frontends extending the microservice idea to frontend development

由多個擁有獨立釋出能力的團隊構建現代Web應用程式的技術,策略和方法。

什麼是微前端?

"微前端"一詞最早於2016年底出現在ThoughtWorks Technology Radar上。它將微服務的概念擴充套件到了前端世界。當前的趨勢是構建一個特性豐富且功能強大的瀏覽器應用程式(又名單頁應用程式),該應用程式未應用微服務架構。隨著時間的流逝,通常由獨立團隊開發的前端層會不斷增長,並且變得越來越難以維護。這就是我們所謂的巨石應用

微前端背後的想法是將網站或Web應用程式視為由每個獨立團隊擁有功能的合集。每個團隊都有自己關心和專長的不同業務或任務領域。單個團隊是跨職能的,從資料庫到使用者介面,端到端地開發其功能。

但是,這個想法並不新奇。它與自包含系統概念有很多共同點。過去,類似的方法被稱為垂直系統的前端整合。但是微前端顯然是一個更友好,更小巧的術語。

巨石應用

垂直組織

什麼是現代web應用?

在介紹中,我使用了"構建現代web應用程式"一詞。讓我們定義與此術語相關的假設。

將這個東西放到更大的視野中,Aral Balkan寫過一篇部落格文章,介紹了他的Documents-to-Applications Continuum。他提出了滑動比例尺的概念,其中在左側是一個由靜態文件構建且通過連結連線的站點,而在右側是一個純行為驅動的,無內容的應用程式,例如線上照片編輯器。

如果您將專案放置在此範圍的左側,則非常適合在Web伺服器級別進行整合。使用此模型,伺服器從構成使用者請求的頁面的所有元件中收集並連線HTML字串。通過從伺服器重新載入頁面或通過ajax替換頁面的一部分來完成更新。

Gustaf Nilsson Kotte撰寫了有關此主題的綜合文章

當您的使用者介面必須提供實時反饋(即使是在不可靠的連線上)時,僅由伺服器渲染的網站已不再滿足要求。要實現Optimistic UISkeleton Screens,您還需要能夠在裝置本身上更新使用者介面。Google的術語Progressive Web Apps恰如其分地描述了成為網路良好公民(漸進式增強功能)的平衡行為,同時還提供了類似App的功能表現。這種應用程式位於site-app-continuum中間的某個位置。在這裡,僅基於伺服器的解決方案已經不能滿足要求。我們必須將整合轉移到瀏覽器中,這是本文的重點。

微前端背後的核心思想

  • 技術獨立
    每個團隊都應該能夠選擇和升級其技術棧,而不必與其他團隊進行協調。自定義元素是一種隱藏實現細節,同時為其他人提供中立介面的好方法。

  • 團隊程式碼隔離
    即使所有團隊都使用相同的框架,也不要共享執行時間。構建獨立的獨立應用程式。不要依賴共享狀態或全域性變數。

  • 建立團隊字首
    同意尚無法隔離的命名約定。名稱空間CSS,事件,本地儲存和Cookies,以避免衝突並闡明所有權。

  • 通過自定義API支援本機瀏覽器功能
    使用瀏覽器事件進行通訊而不是構建全域性的PubSub系統。如果確實需要構建跨團隊API,請儘量簡單。

  • 建立彈性站點
    即使JavaScript失敗或尚未執行,您的功能也應該很有用。使用通用渲染和漸進增強功能可改善感知效能。

DOM是API

自定義元素,是Web元件規範中的互操作性,是整合到瀏覽器中的一個很好的原始方法。每個團隊都使用自己選擇的網路技術來構建其元件,並將其包裝在一個自定義元素中(例如<order-minicart> </order-minicart\>)。該特定元素(標籤,屬性和事件)的DOM規範充當其他團隊的合約或公共API。優點是他們可以使用元件及其功能,而無需瞭解實現。他們只需要能夠與DOM互動即可。

但是,僅自定義元素並不能滿足我們所有需求。為了解決漸進增強,通用渲染或路由問題,我們需要其他軟體。

該頁面分為兩個主要區域。首先,我們將討論頁面組成-如何根據不同團隊擁有的元件來組裝頁面。之後,我們將展示實現客戶端頁面轉換的示例。

頁面組成

除了使用不同框架本身編寫的客戶端和伺服器端程式碼整合之外,還應該討論很多附帶主題:隔離js的機制,避免css衝突,按需載入資源,在團隊之間共享公共資源,處理資料獲取 並考慮為使用者提供良好的載入狀態。 我們將一步一步地涉及這些主題。

基本原型

拖拉機商店的不同型號產品頁面將作為以下示例的基礎。

它具有一個變體選擇器,可以在三種不同的拖拉機型號之間切換。 更改產品圖片時,名稱,價格和建議會更新。 還有一個購買按鈕,將選擇的變體新增到購物籃,並在頂部增加一個迷你購物籃,並相應地進行更新。

try in browser & inspect the code

所有HTML都是使用純JavaScript和沒有依賴性的ES6模板字串在客戶端生成的。該程式碼使用簡單的狀態/標記分離,並在每次更改時重新呈現整個HTML客戶端-無需花哨的DOM區別,也沒有通用渲染。也沒有團隊區分-程式碼寫在一個js/css檔案中。

客戶端整合

在此示例中,頁面分為三個團隊擁有的單獨的元件/片段。Team Checkout(藍色)現在負責與購買過程有關的所有事情,即“購買”按鈕和迷你購物籃。Team Inspire(綠色)在此頁面上管理產品推薦。該頁面本身歸團隊產品(紅色)所有。

try in browser & inspect the code

團隊產品決定要包括的功能以及在佈局中的位置。該頁面包含團隊產品本身可以提供的資訊,例如產品名稱,影象和可用的變體。但是它也包括其他團隊的片段(自定義元素)。

如何建立自定義元素?

讓我們以“購買”按鈕為例。Team Product包含按鈕,只需將<blue-buy sku =“t_porsche”> </blue-buy>新增到標記中的所需位置即可。為此,Team Checkout必須在頁面上註冊藍色購買元素。

class BlueBuy extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<button type="button">buy for 66,00 €</button>`;
  }

  disconnectedCallback() { ... }
}

window.customElements.define('blue-buy',BlueBuy);

現在,每次瀏覽器遇到一個新的blue-buy標籤時,都會呼叫connectedCallback。這是對自定義元素的根DOM節點的引用。可以使用標準DOM元素(例如innerHTML或getAttribute())的所有屬性和方法。

命名元素時,規範定義的唯一要求是名稱必須包含短劃線(-),以保持與即將到來的新HTML標籤的相容性。在接下來的示例中,使用了命名約定[team_color]-[feature]。團隊名稱空間可防止名稱衝突,因此只需檢視DOM,就可以清楚地瞭解功能的所有權。

父子通訊 / DOM修改

當用戶在變數選擇器中選擇另一臺拖拉機時,必須相應地更新購買按鈕。要實現此團隊產品,只需從DOM中刪除現有元素,然後插入一個新元素即可。

container.innerHTML;
// => <blue-buy sku="t_porsche">...</blue-buy>
container.innerHTML = '<blue-buy sku="t_fendt"></blue-buy>';

同步呼叫舊元素的connectedCallback,以使該元素有機會清理諸如事件偵聽器之類的東西。之後,將呼叫新建立的t_fendt元素的connectedCallback。

另一個性能更高的選項是僅更新現有元素上的sku屬性。

document.querySelector('blue-buy').setAttribute('sku', 't_fendt');

如果Team Product使用具有DOM差異功能的模板引擎(如React),則將由演算法自動完成。

為了支援這一點,Custom元素可以實現attributeChangedCallback並指定觀察到的屬性列表,應為其觸發此回撥

const prices = {
  t_porsche: '66,00 €',
  t_fendt: '54,00 €',
  t_eicher: '58,00 €',
};

class BlueBuy extends HTMLElement {
  static get observedAttributes() {
    return ['sku'];
  }
  connectedCallback() {
    this.render();
  }
  render() {
    const sku = this.getAttribute('sku');
    const price = prices[sku];
    this.innerHTML = `<button type="button">buy for ${price}</button>`;
  }
  attributeChangedCallback(attr, oldValue, newValue) {
    this.render();
  }
  disconnectedCallback() {...}
}

window.customElements.define('blue-buy', BlueBuy);

為了避免重複,引入了render()方法,該方法在connectedCallback和attributeChangedCallback中被呼叫。這種方法收集所需的資料,而innerHTML就是新的標記。當決定在Custom Element中使用更復雜的模板引擎或框架時,這裡就是初始化程式碼的地方。

瀏覽器支援

上面的示例使用了當前支援Chrome,Safari和Opera自定義元素V1規範。但是,使用document-register-element可以使用輕量級且經歷過戰鬥考驗的polyfill來使其在所有瀏覽器中都能正常工作。在幕後,它使用廣泛支援的Mutation Observer API,因此在後臺不會出現駭人的DOM樹。

框架相容性

由於自定義元素是一種網路標準,因此所有主流的JavaScript框架(如Angular,React,Preact,Vue或Hyperapp)都支援它們。 但是當您進入細節時,某些框架中仍然存在一些實現問題。在無處不在的自定義元素Rob Dodson彙集了一個相容性測試套件,著重強調了尚未解決的問題。

父子或兄弟元件通訊 / DOM事件

但是,屬性傳遞不足以進行所有互動。在我們的示例中,當用戶單擊“購買”按鈕時,迷你購物籃應重新整理。

這兩個片段均由Team Checkout擁有(藍色),因此它們可以構建某種內部JavaScript API,該API可使迷你購物籃知道何時按下按鈕。但這將要求元件例項相互通訊,並且衝突隔離。

較乾淨的方法是使用PubSub機制,在該機制中,一個元件可以釋出訊息,而其他元件可以訂閱特定主題。幸運的是,瀏覽器內建了此功能。這正是單擊,選擇或滑鼠懸停之類的瀏覽器事件的工作方式。除了本機事件,還可以使用新的CustomEvent(...)建立更高級別的事件。事件始終與建立/排程事件的DOM節點相關。大多數本機事件還具有冒泡功能。這樣就可以偵聽DOM特定子樹上的所有事件。如果要偵聽頁面上的所有事件,請將事件偵聽器附加到window元素。在示例中,blue:basket:changed-event的建立如下所示:

class BlueBuy extends HTMLElement {
  [...]
  connectedCallback() {
    [...]
    this.render();
    this.firstChild.addEventListener('click', this.addToCart);
  }
  addToCart() {
    // maybe talk to an api
    this.dispatchEvent(new CustomEvent('blue:basket:changed', {
      bubbles: true,
    }));
  }
  render() {
    this.innerHTML = `<button type="button">buy</button>`;
  }
  disconnectedCallback() {
    this.firstChild.removeEventListener('click', this.addToCart);
  }
}

現在,迷你購物籃可以在視窗上訂閱此事件,並在重新整理資料時得到通知。

class BlueBasket extends HTMLElement {
  connectedCallback() {
    [...]
    window.addEventListener('blue:basket:changed', this.refresh);
  }
  refresh() {
    // fetch new data and render it
  }
  disconnectedCallback() {
    window.removeEventListener('blue:basket:changed', this.refresh);
  }
}

通過這種方法,迷你籃片段向其範圍(視窗)之外的DOM元素添加了一個偵聽器。對於大量的應用程式這都沒問題,但是如果您對此不滿意,則還可以實現一種方法,其中頁面本身(團隊產品)偵聽事件並通過在DOM元素上呼叫refresh()通知迷你購物籃。

// page.js
const $ = document.getElementsByTagName;

$('blue-buy')[0].addEventListener('blue:basket:changed', function() {
  $('blue-basket')[0].refresh();
});

強制性地呼叫DOM方法並不常見,但是可以在video element api中找到。 如果可以,應首選使用宣告式方法(更改屬性)。

伺服器端渲染/通用渲染

自定義元素非常適合在瀏覽器內部整合元件。但是,當構建一個可在Web上訪問的站點時,初始負載效能很可能會變得很重要,並且在下載並執行所有js框架之前,使用者將看到白屏。另外,最好考慮一下如果JavaScript失敗或被阻塞,網站會發生什麼情況。Jeremy Keith在其電子書/播客Resilient Web Design中解釋了其重要性。因此,在伺服器上呈現核心內容的能力是關鍵。不幸的是,Web元件規範根本沒有涉及伺服器渲染。沒有JavaScript,沒有自定義元素:(

自定義元素+伺服器端包含=❤️

為了使伺服器渲染正常工作,重構了前面的示例。每個團隊都有自己的express伺服器,還可以通過url訪問Custom Element的render()方法。

$ curl http://127.0.0.1:3000/blue-buy?sku=t_porsche
<button type="button">buy for 66,00 €</button>

“自定義元素”標記名稱用作路徑名-屬性成為查詢引數。現在,有一種方法可以通過伺服器渲染每個元件的內容。結合<blue-buy >-Custom Elements,可以實現與通用Web元件非常接近的功能:

<blue-buy sku="t_porsche">
  <!--#include virtual="/blue-buy?sku=t_porsche" -->
</blue-buy>

include註釋是伺服器端包含的一部分,該功能在大多數Web伺服器中都可用。是的,將當前日期嵌入到我們網站上,這是過去經常使用的的一種技術。還有一些替代技術,例如ESInodesicompoxuretailor,但是對於我們的專案,SSI已被證明是一種簡單且難以置信的穩定解決方案。

在Web伺服器將完整頁面傳送到瀏覽器之前,#include註釋將替換為/ blue-buy?sku=t_porsche的響應。nginx中的配置如下所示:

upstream team_blue {
  server team_blue:3001;
}
upstream team_green {
  server team_green:3002;
}
upstream team_red {
  server team_red:3003;
}

server {
  listen 3000;
  ssi on;

  location /blue {
    proxy_pass  http://team_blue;
  }
  location /green {
    proxy_pass  http://team_green;
  }
  location /red {
    proxy_pass  http://team_red;
  }
  location / {
    proxy_pass  http://team_red;
  }
}

指令ssi:on; 啟用SSI功能,併為每個團隊新增上游和位置塊,以確保將所有以/blue開頭的url路由到正確的應用程式(team_blue:3001)。另外,/路由對映到紅色隊,該紅色隊控制著主頁/產品頁面。

此動畫在禁用了JavaScript的瀏覽器中顯示了拖拉機商店。

inspect the code

現在,變體選擇按鈕是實際的連結,每次單擊都會導致頁面重新載入。右側的終端說明了如何將頁面請求路由到紅色團隊,該團隊控制產品頁面,然後用藍色和綠色團隊的片段補充標記。

重新開啟JavaScript時,僅第一個請求的伺服器日誌訊息可見。像第一個示例一樣,所有後續的拖拉機更改都由客戶端處理。在後面的示例中,產品資料將從JavaScript中提取,並根據需要通過REST API載入。

您可以在本地計算機上使用此示例程式碼。 僅需要安裝Docker Compose

git clone https://github.com/neuland/micro-frontends.git
cd micro-frontends/2-composition-universal
docker-compose up --build

然後,Docker在埠3000上啟動Nginx,併為每個團隊構建node.js映像。在瀏覽器中開啟http://127.0.0.1:3000/時,應該會看到一個紅色的拖拉機。docker-compose的組合日誌可輕鬆檢視網路中正在發生的事情。遺憾的是,無法控制輸出顏色,因此您必須忍受以下事實:藍色團隊可能會以綠色突出顯示:)

src檔案對映到各個容器中,並且在更改程式碼後,節點應用程式將重新啟動。更改nginx.conf要求重新啟動docker-compose才能生效。因此,隨時隨意擺弄並提供反饋。

資料獲取和載入狀態

SSI/ESI方法的缺點是,最慢的片段確定整個頁面的響應時間。因此,最好是可以快取片段的響應。對於製作成本高且難以快取的片段,通常最好將其從初始渲染中排除。 可以將它們非同步載入到瀏覽器中。在我們的示例中,綠色個性化片段顯示了個性化推薦,這是一個備選方案。

一種可能的解決方案是,紅色團隊僅跳過SSI Include。

之前

<green-recos sku="t_porsche">
  <!--#include virtual="/green-recos?sku=t_porsche" -->
</green-recos>

之後

<green-recos sku="t_porsche"></green-recos>

重要說明:自定義元素無法自動關閉,因此編寫<green-recos sku="t_porsche" />無法正常工作。

Reflow

渲染僅在瀏覽器中進行。但是,從動畫中可以看出,此更改現在導致頁面的大量重排。推薦區域最初是空白的。團隊果嶺JavaScript已載入並執行。進行了用於獲取個性化推薦的API呼叫。呈現推薦標記,並請求關聯的影象。現在,該片段需要更多空間並推動頁面佈局。

有不同的選擇來避免像這樣令人討厭的重排。建議控制頁面的紅色小組可以固定容器的高度。在響應式網站上,確定高度通常很棘手,因為不同的螢幕尺寸可能會有所不同。但是更重要的問題是,這種團隊間協議在紅色和綠色團隊之間建立了緊密的聯絡。如果綠色團隊希望在reco元素中新增其他子標題,則必須與紅色團隊在新高度上進行協調。兩個團隊都必須同時推出他們的更改,以避免佈局混亂。

更好的方法是使用一種稱為骨架屏。紅色小組將綠色recos SSI包括在標記中。此外,團隊綠色更改了其片段的伺服器端渲染方法,以便生成內容的示意圖。骨架標記可以重用實際內容的部分佈局樣式。這樣,它可以保留所需的空間,並且實際內容的填充不會導致跳轉。

骨架螢幕對於客戶端渲染也非常有用。當您的自定義元素由於使用者操作而插入DOM時,它可以立即渲染框架,直到從伺服器需要的資料到達為止。

即使在諸如變數選擇之類的屬性更改上,您也可以決定切換到框架檢視,直到新資料到達。這樣,使用者可以得到片段中正在發生某些事情的指示。但是,當端點快速響應時,新舊資料之間的短暫骨架閃爍也可能很煩人。保留舊資料或使用智慧超時可以有所幫助。因此,請明智地使用此技術,並嘗試獲取使用者反饋。

在頁面之間導航

即將繼續……(我保證)

關注Github Repo以獲得通知

其他資源