為了幫助開發人員建置更好的 Web 應用程式,我們研究並設計了一種片段架構,以使用 Cloudflare Workers 建置微前端。該架構速度快如閃電,開發和營運具有成本效益,並且可以擴展以滿足最大企業團隊的需求,而不會影響發布速度或使用者體驗。
在本文中,我們將分享此架構的技術概觀和概念驗證。
為什麼選擇微前端?
現代前端 Web 開發的挑戰之一是應用程式變得越來越大、越來越複雜。對於支援電子商務、銀行、保險、旅遊和其他產業的企業 Web 應用程式尤其如此,在這些產業中,由一個統一的使用者介面提供對大量功能的存取。在此類專案中,許多團隊通常協作建置單個 Web 應用程式。這些整體式 Web 應用程式通常使用 React、Angular 或 Vue 等 JavaScript 技術構建,涵蓋數千甚至數百萬行程式碼。
當一個單一的 JavaScript 架構與這種規模的應用程式一起使用時,就會帶來緩慢而脆弱的使用者體驗,且 Lighthouse 評分很低。此外,協作開發團隊常常難以維護和發展他們所開發的應用程式部分,因為他們的命運與所有其他團隊的命運息息相關,因此一個團隊的錯誤和技術債務往往會影響到所有人。
借鑒微服務的概念,前端社群已經開始提倡微前端,使團隊能夠獨立於其他團隊開發和部署他們的功能。每個微前端都是一個獨立的小型應用程式,可以獨立開發和發布,負責轉譯頁面的一個「片段」。然後應用程式將這些片段組合在一起,這樣從使用者的角度來看它就像一個單一的應用程式。
由多個微前端組成的應用程式
片段可以表示不同層次的應用程式功能,如「帳戶管理」或「結帳」,或同一層次的功能,如「標題」或「導覽列」。
用戶端微前端
微前端的一種常見方法是依靠用戶端程式碼來延遲載入並將片段拼接在一起(例如透過模組聯合)。用戶端微前端應用程式存在許多問題。
通用程式碼必須複製或作為共用庫發布。共用庫本身就存在問題。由於無法在建置時對未使用的庫程式碼進行 tree-shake 處理,這會導致將不必要的程式碼下載到瀏覽器,並且在需要更新共用庫時團隊之間的協調可能會變得複雜而不便。
此外,必須先啟動頂級容器應用程式,才能夠請求微前端,且微前端也需要先啟動,才能夠變得可互動。如果它們是巢狀結構,那麼您最終可能會收到大量要求獲取微前端的請求,從而導致進一步的執行階段延遲。
這些問題可能會導致使用者遭遇緩慢的應用程式啟動體驗。
伺服器端轉譯可以與用戶端微前端一起使用,以提高瀏覽器顯示應用程式的速度,但實作此功能會顯著增加開發、部署和操作的複雜性。此外,大多數伺服器端轉譯方法在使用者能夠與應用程式完全交互之前仍然存在水合作用延遲。
解決這些挑戰是探索替代解決方案的主要動機,該解決方案依賴於 Cloudflare Workers 提供的分散式低延遲屬性。
Cloudflare Workers 上的微前端
Cloudflare Workers 是一個計算平台,可提供高度可擴展、低延遲的 JavaScript 執行環境,可在全球超過 275 個地點使用。在我們的探索中,我們使用 Cloudflare Workers 從我們全球網路的任何地方託管和轉譯微前端。
片段架構
在此架構中,應用程式由一棵「片段」樹組成,每個「片段」都部署到 Cloudflare Workers,這些 Workers 協作在伺服器端轉譯整體回應。瀏覽器向「根片段」發出請求,「根片段」將與「子片段」通訊以產生最終回應。由於 Cloudflare Worker 可以在幾乎沒有開支的情況下相互通訊,因此應用程式可以透過子片段快速在伺服器端轉譯,所有子片段都並行工作以轉譯自己的 HTML,將它們的結果流式傳輸到父片段,父片段將它們組合成最終回應並流式傳送到瀏覽器。
片段架構的高層級概觀
造訪「Cloud Gallery」
我們建置了一個「Cloud Gallery」應用程式範例,以展示它在實踐中的工作原理。它部署到 Cloudflare Workers 的 https://cloud-gallery.web-experiments.workers.dev/
示範應用程式是使用我們的片段架構建置的簡單篩選雲端影像庫。嘗試在預先輸入中選擇一個標記,以篩選庫中列出的影像。然後變更雲端影像流的延遲,以查看在頁面完成載入之前預先輸入篩選如何進行交互。
多個 Cloudflare Workers
該應用程式由六個協作但可獨立部署的 Cloudflare Worker 組成,每個 Cloudflare Worker 轉譯自己的螢幕片段,並提供自己的用戶端邏輯以及 CSS 樣式表和影像等資產。
Cloud Gallery 應用程式的架構概觀
「主要」片段充當應用程式的根。「標題」片段有一個滑桿,用於設定顯示庫影像的人為延遲。「內文」片段包含「篩選器」片段和「圖庫」片段。最後,「頁尾」片段僅顯示一些靜態內容。
GitHub 上提供了示範應用程式的完整原始程式碼。
優點和功能
這種構架由多個協作伺服器端轉譯片段組成,並部署到 Cloudflare Workers,其具有一些有趣的功能。
封裝
片段是完全封裝的,因此它們可以控制自己擁有的內容以及提供給其他片段的內容。
片段可以獨立開發和部署
更新其中一個片段只需重新部署該片段即可。對主要應用程式的下一個請求將使用新片段。此外,片段可以託管自己的資產(用戶端 JavaScript、影像等),這些資產透過其父片段流式傳輸到瀏覽器。
僅伺服器程式碼不會傳送到瀏覽器
除了降低將不必要的程式碼下載到瀏覽器的成本外,僅在伺服器端轉譯片段所需的安全性敏感程式碼永遠不會暴露給其他片段,也不會下載到瀏覽器。此外,功能可以安全地隱藏在片段中的功能標誌後面,從而在安全推出新行為方面具有更大的靈活性。
可組合性
片段是完全可組合的——任何片段都可以包含其他片段。產生的樹狀結構讓您在構建和部署應用程式時擁有更大的靈活性。這有助於大型專案擴展其開發和部署。此外,對片段組合方式的精細控制,允許單獨快速對伺服器端轉譯來說成本高昂的片段。
出色的 Lighthouse 評分
流式傳輸伺服器轉譯的 HTML 可帶來出色的使用者體驗和 Lighthouse 評分,這實際上意味著更滿意的使用者和更高的商務轉化機會。
Cloud Gallery 應用程式的 Lighthouse 評分
每個片段都可以將請求平行傳輸到其子片段,並將產生的 HTML 流透過管道傳輸到其自己的單個流式伺服器端轉譯回應中。這不僅可以減少轉譯整個頁面的時間,而且將每個片段流式傳輸到瀏覽器可以減少每個片段的第一個位元組接收時間。
迅速的互動功能
片段架構的一項強大功能是,即使應用程式的其餘部分(包括其他片段)仍在流式傳輸到瀏覽器,片段也可以變得可互動。
在我們的示範中,即使「圖庫」片段的影像 HTML 仍在載入中,「篩選器」片段在轉譯后也能立即具有互動性。
為了更容易看到這一點,我們在「標題」的頂部新增了一個滑桿,它可以模擬網路或資料庫延遲,這會減慢呈現「圖庫」影像的 HTML 流。即使「圖庫」片段仍在載入,「篩選器」片段中的提前輸入內容也已經完全可互動。
試想一下,對於網際網路連線不可靠的 Web 應用程式使用者來說,這種迅速的互動功能可以避免所有的挫敗感。
背後原理
如前所述,此架構依賴於將此應用程式部署為盡可能多的協作 Cloudflare Worker。讓我們看看這在實踐中是如何運作的。
我們嘗試了各種技術,雖然這種方法可以與許多前端庫和框架一起使用,但我們發現 Qwik 框架特別適合,因為它以 HTML 優先且具有低 JavaScript 開支,這避免了任何水合作用問題。
實作片段
每個片段都是伺服器端轉譯的 Qwik 應用程式,並部署到其自己的 Cloudflare Worker。這意味著您甚至可以直接瀏覽到這些片段。例如,「header」片段被部署到 https://cloud-gallery-header.web-experiments.workers.dev/。
自託管「標題」片段的螢幕擷取畫面
標題片段使用 Qwik 定義為 Header
元件。該元件透過 fetch()
處理常式在 Cloudflare Worker 中轉譯:
cloud-gallery/header/src/entry.ssr.tsx
export default {
fetch(request: Request, env: Record<string, unknown>): Promise<Response> {
return renderResponse(request, env, <Header />, manifest, "header");
},
};
[renderResponse](https://github.com/cloudflare/workers-web-experiments/blob/main/cloud-gallery/helpers/src/renderResponse.ts)()
函數是我們編寫的一個協助程式,它在伺服器端轉譯片段並將其流式傳輸到我們從 fetch()
處理常式傳回的 Response
主體中。
標題片段從其 Cloudflare Worker 提供自己的 JavaScript 和影像資產。我們將 Wrangler 設定為將這些資產上傳到 Cloudflare,並從我們的網路提供這些內容。
實作片段組合
包含子片段的片段具有其他職責:
在轉譯自己的 HTML 時請求並插入子片段。
將子片段資產的請求代理到適當的片段。
插入子片段
子片段在其父片段中的位置可以由我們開發的 FragmentPlaceholder
協助程式元件指定。例如,「主體」片段具有「篩選器」和「圖庫」片段。
<div class="content">
<FragmentPlaceholder name="filter" />
<FragmentPlaceholder name="gallery" />
</div>
cloud-gallery/body/src/root.tsx
FragmentPlaceholder
元件負責對片段發出請求,並將片段流管道傳輸到輸出流中。
代理資產請求
如前所述,片段可以託管自己的資產,尤其是用戶端 JavaScript 檔案。當資產請求到達父片段時,它需要知道哪個子片段應該接收請求。
export default {
async fetch(
request: Request,
env: Record<string, unknown>
): Promise<Response> {
// Proxy requests for assets hosted by a fragment.
const asset = await tryGetFragmentAsset(env, request);
if (asset !== null) {
return asset;
}
// Otherwise server-side render the template injecting child fragments.
return renderResponse(request, env, <Root />, manifest, "div");
},
};
在我們的示範中,我們使用這樣的約定,即此類資產路徑將以 /_fragment/<fragment-name>
為首碼。例如,標題標誌影像圖片路徑為 /_fragment/header/cf-logo.png
。我們開發了一個 [tryGetFragmentAsset](https://github.com/cloudflare/workers-web-experiments/blob/main/cloud-gallery/helpers/src/fragmentHelpers.tsx#L28)()
協助程式,可以將其新增到父片段的 fetch()
處理常式中來處理這個問題:
cloud-gallery/body/src/entry.ssr.tsx
片段資產路徑
如果片段託管其自己的資產,那麼我們需要確保它轉譯的任何 HTML 在引用這些資產時都使用上面提到的特殊 _fragment/<fragment-name>
路徑首碼。我們已經在我們開發的協助程式中為此實作了一項策略。
export const Header = component$(() => {
useStylesScoped$(HeaderCSS);
useFragmentRoot();
return (...);
});
FragmentPlaceholder
元件向片段請求新增一個 base
searchParam 來告訴它這個首碼應該是什麼。[renderResponse](https://github.com/cloudflare/workers-web-experiments/blob/main/cloud-gallery/helpers/src/renderResponse.ts)()
協助程式擷取此首碼並將其提供給 Qwik 伺服器端轉譯器。這可確保對用戶端 JavaScript 的任何請求都具有正確的首碼。片段可以套用我們開發的名為 [useFragmentRoot](https://github.com/cloudflare/workers-web-experiments/blob/main/cloud-gallery/helpers/src/base.ts#L22)()
的勾點。這允許元件從 FragmentContext
上下文中收集首碼。
export const Image = component$((props: Record<string, string | number>) => {
const { base } = useContext(FragmentContext);
return <img {...props} src={base + props.src} />;
});
例如,由於「標題」片段將 Cloudflare 和 GitHub 標誌作為資產託管,因此它必須呼叫 useFragmentRoot()
勾點:
cloud-gallery/header/src/root.tsx
然後可以在需要套用首碼的元件中存取 FragmentContext
值。例如 Image 元件:
cloud-gallery/helpers/src/image/image.tsx
服務繫結片段
Cloudflare Workers 提供了一種稱為服務繫結的機制,可以在 Cloudflare Workers 之間有效地發出請求,從而避免網路請求。在示範中,我們使用這種機制從父片段向它們的子片段發出請求,幾乎沒有效能開支,同時仍然允許片段獨立部署。
與當前解決方案的比較
此片段架構具有三個屬性,將其與其他當前解決方案區分開來。
與單體或用戶端微前端不同,片段作為獨立的伺服器端轉譯應用程式開發和部署,這些應用程式在伺服器端組合在一起。這顯著提高了轉譯速度,並降低了瀏覽器中的互動延遲。
與具有 Node.js 或雲端功能的伺服器端轉譯微前端不同,Cloudflare Workers 是一個具有無區域部署模型的全球分散式計算平台。它具有令人難以置信的低延遲,並且片段之間的通訊開支幾乎為零。
與基於模組聯合的解決方案不同,片段的用戶端 JavaScript 非常特定於它所支援的片段。這意味著它足夠小,我們不需要共用庫程式碼,消除了更新共用庫時的版本偏離問題和協調問題。
未來的可能性
此示範只是一個概念驗證,因此仍有一些領域需要研究。以下是我們未來想要探索的一些功能。
快取
每個微前端片段都可以根據其內容的靜態程度獨立於其他片段進行快取。請求完整頁面時,片段只需要對已變更的微前端執行伺服器端轉譯。
快取部分片段輸出的應用程式
使用單個片段快取,您可以更快地將 HTML 回應傳回到瀏覽器,並避免在重新轉譯不必要內容時產生計算成本。
片段路由和用戶端導覽
我們的示範應用程式使用微前端片段來組成單個頁面。不過,我們也可以使用這種方法來實作頁面路由。當伺服器端轉譯時,主要片段可以根據造訪的 URL 插入適當的「頁面」片段。當在用戶端的應用程式內導覽時,主要片段將保持不變,而顯示的「頁面」片段將變更。
將每個路由委派給不同片段的應用程式
此方法將伺服器端和用戶端路由的優點與片段的強大功能相結合。
使用其他前端框架
儘管 Cloud Gallery 應用程式使用 Qwik 來實作所有片段,但也可以使用其他框架。如果真的有必要,甚至可以混合和匹配框架。
為了獲得良好的結果,選擇的框架應當能夠進行伺服器端轉譯,並且應該具有較小的用戶端 JavaScript 佔用空間。HTML 流式傳輸功能雖然不是必需的,但可以顯著提高大型應用程式的效能。
使用不同前端框架的應用程式
逐步移轉策略
採用新的架構、計算平台和部署模型需要同時考慮很多,對於現有的大型應用程式來說,風險和成本都非常高。為了使這種基於片段的架構可用於舊版專案,逐步採用策略是關鍵。
開發人員可以透過將舊版應用程式中的單個使用者介面移轉到片段來進行測試,只需對舊版應用程式進行最少的變更即可整合。隨著時間的推移,可以移動應用程式的更多部分,一次一個片段。
慣例重於設定
正如您在 Cloud Gallery 示範應用程式中看到的那樣,建立基於片段的微前端需要相當多的設定。大量此類設定都是非常機械的,可以透過慣例和更好的工具抽象出來。遵循 Ruby on Rails 中以生產力為中心的優先順序和基於檔案系統的路由中繼框架,我們可以省去很多這樣的設定。
自己試試吧!
還有很多東西需要挖掘!近年來,Web 應用程式已經取得了長足的進步,其增長勢頭不容小覷。微前端的傳統實作在幫助開發人員擴展大型應用程式的開發和部署方面取得了喜憂參半的成功。然而,Cloudflare Workers 解鎖了新的可能性,可以幫助我們解決許多現有挑戰,並幫助我們建置更好的 Web 應用程式。
感謝 Cloudflare Workers 慷慨提供的免費方案,您可以查看圖庫示範程式碼並自行部署。
如果您對這些內容感興趣,並且想與我們合作改善 Cloudflare Workers 的開發人員體驗,我們很高興地告訴您,我們正在招聘!