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替換頁面的一部分來完成更新。
當您的使用者介面必須提供實時反饋(即使是在不可靠的連線上)時,僅由伺服器渲染的網站已不再滿足要求。要實現Optimistic UI或Skeleton 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伺服器中都可用。是的,將當前日期嵌入到我們網站上,這是過去經常使用的的一種技術。還有一些替代技術,例如ESI,nodesi,compoxure和tailor,但是對於我們的專案,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的瀏覽器中顯示了拖拉機商店。
現在,變體選擇按鈕是實際的連結,每次單擊都會導致頁面重新載入。右側的終端說明了如何將頁面請求路由到紅色團隊,該團隊控制產品頁面,然後用藍色和綠色團隊的片段補充標記。
重新開啟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" />無法正常工作。
渲染僅在瀏覽器中進行。但是,從動畫中可以看出,此更改現在導致頁面的大量重排。推薦區域最初是空白的。團隊果嶺JavaScript已載入並執行。進行了用於獲取個性化推薦的API呼叫。呈現推薦標記,並請求關聯的影象。現在,該片段需要更多空間並推動頁面佈局。
有不同的選擇來避免像這樣令人討厭的重排。建議控制頁面的紅色小組可以固定容器的高度。在響應式網站上,確定高度通常很棘手,因為不同的螢幕尺寸可能會有所不同。但是更重要的問題是,這種團隊間協議在紅色和綠色團隊之間建立了緊密的聯絡。如果綠色團隊希望在reco元素中新增其他子標題,則必須與紅色團隊在新高度上進行協調。兩個團隊都必須同時推出他們的更改,以避免佈局混亂。
更好的方法是使用一種稱為骨架屏。紅色小組將綠色recos SSI包括在標記中。此外,團隊綠色更改了其片段的伺服器端渲染方法,以便生成內容的示意圖。骨架標記可以重用實際內容的部分佈局樣式。這樣,它可以保留所需的空間,並且實際內容的填充不會導致跳轉。
骨架螢幕對於客戶端渲染也非常有用。當您的自定義元素由於使用者操作而插入DOM時,它可以立即渲染框架,直到從伺服器需要的資料到達為止。
即使在諸如變數選擇之類的屬性更改上,您也可以決定切換到框架檢視,直到新資料到達。這樣,使用者可以得到片段中正在發生某些事情的指示。但是,當端點快速響應時,新舊資料之間的短暫骨架閃爍也可能很煩人。保留舊資料或使用智慧超時可以有所幫助。因此,請明智地使用此技術,並嘗試獲取使用者反饋。
在頁面之間導航
即將繼續……(我保證)
關注Github Repo以獲得通知
其他資源
- Book: Micro Frontends in Action由我編寫。
- Talk: Micro Frontends - MicroCPH, Copenhagen 2019(幻燈片)Nitty Gritty Details或Frontend,Backend,