Cloudflare Workers 和微前端:為彼此而生
?????♀? 編者按:本文作者是螞蟻集團(tuán)前端工程師有知,介紹了 Cloudflare 提出的一種「新的」微前端方案以及其極致的首屏優(yōu)化背后的實(shí)現(xiàn)原理。

https://blog.cloudflare.com/better-micro-frontends/
PS:關(guān)注過 Angular 的同學(xué)應(yīng)該對(duì) Igor Minar 這個(gè)名字不陌生,他是 AngularJS/Angular 的 co-founder 之一,并長期擔(dān)任 Angular Team 的 Tech Leader 負(fù)責(zé) Angular 團(tuán)隊(duì)的整體運(yùn)轉(zhuǎn),2021 年 11 月離開了工作 12 年的 Google 去了 Cloudflare。
本文主要為大家介紹下 Cloudflare 提出的一種「新的」微前端方案以及其極致的首屏優(yōu)化背后的實(shí)現(xiàn)原理,有興趣的同學(xué)也可以直接去看 Cloudflare 的原文:
?Cloudflare Workers 和微前端:為彼此而生 https://blog.cloudflare.com/zh-cn/better-micro-frontends-zh-cn/
?通過 Cloudflare Workers 增加采用微前端 https://blog.cloudflare.com/zh-cn/fragment-piercing-zh-cn/
客戶端微前端方案的問題
文章中首先指出了當(dāng)前常見的客戶端微前端方案的問題:
Module Federation:共享庫本身不能與微應(yīng)用一起做構(gòu)建時(shí) tree-shaking。
Module Federation:共享庫本身會(huì)帶來隱式的耦合,版本升級(jí)會(huì)變得非常麻煩。這也是 qiankun 不推薦用 externals 的方式來復(fù)用依賴的原因。
瀑布式請(qǐng)求:必須等頂層的主應(yīng)用啟動(dòng)之后,才能開始啟動(dòng)微應(yīng)用,微應(yīng)用才能發(fā)起請(qǐng)求,導(dǎo)致運(yùn)行時(shí)間進(jìn)一步被延時(shí)。這個(gè)是微前端方案首屏慢的最根本原因。
hydration delay:即便我們加上了服務(wù)端渲染(SSR),用戶的首屏雖然快了,但仍然需要等待客戶端框架完成 hydration 之后才能開始交互。
Cloudflare 的片段架構(gòu)(Fragments Architecture)
為解決這些問題,Cloudflare 提出了他們針對(duì)微前端的片段架構(gòu):整個(gè)渲染流里有幾個(gè)關(guān)鍵要素:
應(yīng)用程序由一顆片段樹組成
瀏覽器向根片段發(fā)起請(qǐng)求,根片段與子片段通信,生成最終響應(yīng)
每個(gè)片段運(yùn)行在獨(dú)立的 worker 中
整個(gè)響應(yīng)過程都是 并行 + 流式的
優(yōu)勢
片段架構(gòu)的主要優(yōu)勢有這些:
安全性:前置的關(guān)鍵接口請(qǐng)求都是在服務(wù)端 worker 中進(jìn)行,不用擔(dān)心前端敏感代碼泄露的問題
基于 Worker 的服務(wù)端渲染:SSR + CDN 加持,Lighthouse 評(píng)分極高
盡早交互(Eager interactivity):不用等框架走完 hydration 流程,響應(yīng)回來瀏覽器渲染開始就能開始交互(實(shí)際需要框架配合,比如 demo 里的 Qwik)
實(shí)現(xiàn)原理
先看下 Demo 的最終效果:


這里面有幾點(diǎn)特性值得關(guān)注:
首屏?xí)紫确祷仨撁娴?critical 部分內(nèi)容,且渲染出來是可以立即交互的。比如 Login 頁面的中間表單部分,Todos 頁面的列表部分。
所有的渲染都是流式的,包括客戶端路由切換引發(fā)的重新渲染
整體架構(gòu)如下圖,本文將會(huì)通過問題來解構(gòu),看看 Cloudflare 是如何基于 Worker 來實(shí)現(xiàn)更優(yōu)異性能表現(xiàn)的微前端的。Demo 源碼:https://github.com/cloudflare/workers-web-experiments/tree/main/productivity-suite
1. 如何實(shí)現(xiàn)流式聚合響應(yīng)
Gateway Worker
所有流量的入口是一個(gè) gateway worker,gateway worker 的作用是用來分發(fā)流量,包括穩(wěn)定流量、靜態(tài)資源流量等。本質(zhì)就是一個(gè)實(shí)現(xiàn)了 fetch 的 Cloudflare Worker。
重點(diǎn)是 handleHtmlRequest 這里,這里會(huì)將當(dāng)前訪問 url 需要的 Fragments 流聚合成一個(gè)流:
只需要確保 legacy index.html 是第一個(gè)響應(yīng),其他 Fragments 流的順序無所謂。最后出來的響應(yīng):

每個(gè) Fragment 由?piercing-fragment-host
?標(biāo)簽包裹,每個(gè)標(biāo)簽內(nèi)都是一個(gè)完整的 HTML 字符串(含 head、body 部分)。
有似曾相識(shí)的感覺嗎?
2. 如何確保先渲染的 Fragment 流能渲染到正確的位置
這個(gè)主要作用是將首屏最先需要交互的片段渲染出來,并渲染到正確的位置,比如一個(gè) Login 頁面的中間表單部分。而不是按照完整的頁面結(jié)構(gòu)渲染。
總共分兩步:
原始渲染的 Fragment 本身就是包含 UI 且可響應(yīng)的(借助 Qwik),本身由
piercing-fragment-host
?這個(gè)統(tǒng)一的 web component 負(fù)責(zé)組件激活。每一個(gè) Fragment 在注冊(cè)之初便有基礎(chǔ)樣式,這些基礎(chǔ)樣式用于確保首屏的 Fragment 在客戶端 hydration 之后不會(huì)出現(xiàn)視覺閃爍或者失焦。Legacy App 會(huì)后置渲染出
piercing-fragment-outlet
組件,當(dāng)組件激活后,會(huì)將對(duì)應(yīng)的piercing-fragment-host
移動(dòng)到 outlet 對(duì)應(yīng)的 DOM 樹里。
片段穿孔
片段穿孔指的便是上述的兩步流程,作用就是如何將新的 Fragment 動(dòng)態(tài)的整合到 Legacy 應(yīng)用中。其中有兩個(gè)重要的 web component 組件,piercing-fragment-host
和piercing-fragment-outlet
。
piercing-fragment-host
Fragment 整個(gè) HTML 的片段宿主,如首屏響應(yīng)可能長這樣:
Legacy 應(yīng)用的 root 節(jié)點(diǎn)還未渲染,但是 todos Fragment 的內(nèi)容已經(jīng)通過 ssr 響應(yīng)正常渲染出來了。這里有一個(gè)細(xì)節(jié)處理,跟 qiankun 的方案一毛一樣,誰能看出來是用來處理什么場景的?
piercing-fragment-outlet
Legacy App 渲染時(shí)會(huì)提供的 web component,當(dāng)它開始激活時(shí),會(huì)去 dom 樹中尋找對(duì)應(yīng) Fragment ID 的 host 節(jié)點(diǎn),并將其移動(dòng)到正確的位置。
比如激活前:
激活后:
為了避免 DOM 樹移動(dòng)過程中產(chǎn)生的視覺閃爍或者失焦,每個(gè) Fragment 在注冊(cè)之前必須提供一些基礎(chǔ)樣式,比如 todos Fragment 的基礎(chǔ)樣式是整個(gè) DOM 渲染在屏幕的中心區(qū)域:
通過片段穿孔,可以漸進(jìn)式的使用微前端,一次一個(gè) Fragment,且每個(gè) Fragment 之間技術(shù)??梢允仟?dú)立的。
3. Fragment 之間如何通信
假如 Fragment 之間需要知道彼此的存在并通信,比如 TodoMVC 的場景里,服務(wù)端在響應(yīng)之前,TodoList Fragment 需要展示當(dāng)前用戶選擇的分組,Todos Fragment 需要展示當(dāng)前用戶選擇的分組下對(duì)應(yīng)的具體的 ToDo 項(xiàng),這中間是如何實(shí)現(xiàn)通信的。
Cloudflare 這里的解決方案是實(shí)現(xiàn)了一個(gè)同構(gòu)的、框架無關(guān)的消息總線 MessageBus。
比如 Todos Fragment 在 Worker 里通過這種方式獲取當(dāng)前用戶登錄用戶名及已選的分組信息:
在瀏覽器 React 環(huán)境里通過類似方式監(jiān)聽分組 Fragment 的選中事件:
4. 其他問題
關(guān)于片段穿孔的預(yù)留位置
片段在首次渲染時(shí)需要提供預(yù)置樣式,用于在穿孔發(fā)生之前也能渲染在用戶屏幕合適的位置。Demo 中用的是絕對(duì)定位的方案,這其實(shí)會(huì)受限于用戶屏幕的尺寸和分辨率,且都是手動(dòng)的,非常容易出錯(cuò)。這個(gè)問題的解決方案會(huì)比較麻煩,可能涉及到針對(duì) Legacy App 的代碼侵入,比如在 Legacy App 里提前針對(duì) Fragment 預(yù)先寫好 placeholder 元素及其樣式。
Fragment 副作用的 reapply
片段在加載、卸載的過程中,有一些全局文檔腳本的副作用可能需要重新執(zhí)行(比如切換到 TodoList 時(shí)發(fā)起請(qǐng)求)。Demo 里的解決方案是通過?addDefaultFnExportToBundle
插件,將 entry 的副作用生成一個(gè) default 導(dǎo)出,并在每次片段加載的時(shí)候重新執(zhí)行 default function。
publicPath 的問題
每個(gè)片段都運(yùn)行在同一個(gè)域名上下文中,導(dǎo)致原始片段中的相對(duì)路徑的靜態(tài)資源請(qǐng)求都會(huì)打向當(dāng)前域名。Demo 里的解法是在構(gòu)建時(shí)指定固定的 publicPath 來區(qū)分,比如 /_fragments/todos,而 gateway worker 則通過前綴來分發(fā)靜態(tài)資源流量。
qiankun 沒這個(gè)問題原因是,我們選擇的解法是運(yùn)行時(shí)動(dòng)態(tài)設(shè)置 publicPath,而不會(huì)是構(gòu)建時(shí)決策。
可借鑒的方向
服務(wù)端組合流式響應(yīng)
可組合、非阻塞式的流式響應(yīng)非常有吸引力,結(jié)合低代碼搭建、配置直出等場景應(yīng)該會(huì)很有效果。
客戶端流式渲染 writable-dom
writable-dom 是一個(gè)用于在客戶端將流響應(yīng)寫入到指定的 document dom 節(jié)點(diǎn)中,這還不是最強(qiáng)的,最強(qiáng)的是它能保證渲染過程中資源的阻塞邏輯跟原生的瀏覽器解析邏輯一致,比如樣式表加載完成之后,再去渲染后續(xù)的 DOM 樹,從而避免異步樣式表帶來的內(nèi)容閃爍的問題。
不過前提是目標(biāo)資源本身也支持流式響應(yīng)才有意義。
未解決的問題
文章開篇提到的,客戶端微前端方案的依賴 tree-shaking 及共享問題,并沒有解決只是繞過了(片段夠小,不需要共享庫代碼)。
雖說支持多 web 框架,但是并沒有客戶端的沙箱方案,一樣可能出現(xiàn)樣式?jīng)_突、同一技術(shù)棧多版本沖突等問題。
最后
過去幾個(gè)月最讓我興奮的便是接連看到 AWS、Cloudflare 這些云廠商開始入局微前端,并提供了基于他們的云基礎(chǔ)設(shè)施的微前端解決方案。這至少說明微前端現(xiàn)在不僅只是一個(gè)廣泛被熱議的話題,其背后的復(fù)雜度更是足以撐起一個(gè)商業(yè)化解決方案的,「微前端不過是又一個(gè) buzz word」這一論斷其實(shí)可以不攻自破了。