Subscribe to receive notifications of new posts:

Subscription confirmed. Thank you for subscribing!

透過 Cloudflare Workers 增加微前端的採用

Loading...

Incremental adoption of micro-frontends with Cloudflare Workers

為傳統 Web 應用程式締造微前端優勢

最近,我們撰寫了一篇關於構建 Web 應用程式之新片段架構的文章。該架構快速、經濟高效且可擴展到最大專案,同時支援快速反覆運算週期。此方法使用多重協作 Cloudflare Workers 將微前端轉譯及串流至比傳統用戶端方法互動速度更快的應用程式中,從而獲得更好的使用者體驗及 SEO 分數。

如要啟動一個新專案或有能力從頭開始重寫目前的應用程式,則此方法非常有效。但是,實際上大部分專案都很大,不能從頭開始重新構建,只能逐步採用架構變更。

在這篇文章中,我們提議了一種方法,僅以伺服器端轉譯的片段來取代舊用戶端轉譯應用程式之選定部分。這樣能夠獨立開發能夠更快交互最重要視圖的應用程式,並享受到微前端方法的各種益處,同時避免對舊程式碼基底進行大規模重寫。此方法與框架無關。我們在這篇文章中演示了使用 React、Qwik 及 SolidJS 構建的片段。

大型前端應用程式的痛點

當下開發的許多大型前端應用程式皆不能提供良好的使用者體驗。通常的原因是,使用者下載、解析及執行需要大量 JavaScript 的架構之後,才能與應用程式進行交互。儘管努力透過消極式載入及使用伺服器端轉譯來延遲非關鍵 JavaScript 程式碼,這些大型應用程式仍然需要相當長的時間才能實現互動並響應使用者的輸入。

此外,大型單體應用程式的構建及部署可能較複雜。多個團隊可能協作處理單個程式碼基底,而協調專案測試與部署的工作為開發、部署及反覆運算個別功能帶來了難度。

正如我們在上一篇文章中所概述的,由 Cloudflare Workers 提供技術支援的微前端能夠解決這些問題,但將應用程式單體結構轉換為微前端架構可能既有難度,又代價不菲。使用者或開發者可能需要投入數月甚至數年來執行工程任務,才能感知到任何好處。

我們需要這樣一種方法:專案能夠逐步採用微前端至應用程式中最具影響力的部分,無需一次性重寫整個應用程式。

拯救片段

基於片段之架構的目標是透過將應用程式分解為可在 Cloudflare Workers 中快速轉譯(及快取)的微前端,來顯著降低大型 Web 應用程式的載入及互動等待時間(透過核心 Web 指標予以衡量)。挑戰在於如何將微前端片段整合到傳統的用戶端轉譯應用程式中,同時盡可能降低原始專案成本。

藉由我們提議的技術,我們能夠轉換舊版應用程式 UI 中最具價值的部分,與應用程式的其餘部分隔離開來。

事實證明,在諸多應用程式中,UI 最具價值的部分往往內嵌於提供標頭、頁尾及導覽元素的應用程式「殼層」之中。這些範例包括登入表單、電子商務應用程式中的產品詳細資料面板、電子郵件用戶端中的收件匣等。

我們以登入表單為例。如果我們的應用程式需要幾秒鐘才能顯示登入表單,使用者可能會被登入勸退,我們可能會失去這些用戶。不過,我們可以將登入表單轉換為伺服器端轉譯的片段,能夠即時顯示及互動,而舊版應用程式的其餘部分則會在後台啟動。由於片段能夠搶先實現互動,因而使用者甚至可在舊版應用程式啟動並轉譯頁面其餘部分之前提交其憑據。

顯示登入表單在主應用程式啟動之前可供使用的動畫

較之傳統方法,工程團隊能採用這種方法以幾分之一的時間及工程成本為使用者帶來寶貴的改進。傳統方法要麼會影響使用者體驗的改善,要麼需要對整個應用程式實行冗長而高風險的重寫。這種方法能讓具備單體單頁應用程式的團隊逐步採用微前端架構,將改進目標訂定為應用程式最有價值的部分,因而能夠提前獲得投資回報。

將部分 UI 提取至伺服器端轉譯的片段中時,會有一項有趣的挑戰:在瀏覽器中顯示時,我們希望舊版應用程式及片段看起來像是單一應用程式。這些片段應整齊嵌入舊版應用程式殼層中,透過正確形成 DOM 層次結構來保持應用程式的可存取性,但我們亦希望伺服器端轉譯的片段盡快顯示並實現互動功能,甚至要早於舊版用戶端轉譯應用程式殼層的存在。如何將 UI 片段嵌入至尚不存在的應用程式殼層?我們透過所設計的一種技術解決了這個問題,我們稱之為「片段穿透」。

片段穿透

片段穿透將伺服器端轉譯的微前端片段產生的 HTML/DOM 與舊版用戶端轉譯應用程式產生的 HTML/DOM 結合起來。

微前端片段直接轉譯至 HTML 回應的頂層,經設計可實現即時互動。在後台,舊版應用程式在用戶端轉譯為這些片段的同級。準備就緒后,片段會「穿透」至舊版應用程式中(每個片段的 DOM 會移動至舊版應用程式 DOM 中的適當位置),而不會引致任何視覺副作用或客戶端狀態損失,例如焦點、表單資料或文字選擇。「穿透」之後,片段能夠開始與舊版應用程式通訊,有效成為不可或缺的一部分。

您可以在此看到「登入」片段以及 DOM 頂層的空舊版應用程式「根」元素,然後再實施穿透。

<body>
  <div id="root"></div>
  <piercing-fragment-host fragment-id="login">
    <login q:container...>...</login>
  </piercing-fragment-host>
</body>

您可在此看到,此片段已穿透至轉譯之舊版應用程式中的「登入頁面」div 中。

<body>
  <div id="root">
    <header>...</header>
    <main>
      <div class="login-page">
        <piercing-fragment-outlet fragment-id="login">
          <piercing-fragment-host fragment-id="login">
            <login  q:container...>...</login>
          </piercing-fragment-host>
        </piercing-fragment-outlet>
      </div>
    </main>
    <footer>...</footer>
  </div>
</body>

為了在此過渡期間阻止片段移動並導致可見的佈局偏移,我們套用了 CSS 樣式,可在穿透前後以相同方式定位片段。

應用程式可隨時顯示任意數量的穿透片段,或根本不顯示任何片段。這種技術並不侷限於舊版應用程式的初始負載。此外,還可隨時在應用程式中新增及移除片段。這樣能夠轉譯片段,來回應使用者互動及用戶端路由。

透過片段穿透,您可以開始逐步採用微前端,一次一個片段。您可以決定片段的粒度,以及將應用程式的哪些部分轉換為片段。這些片段不必全部使用相同的 Web 框架,在切換堆疊或多個應用程式的擷取後整合期間大有用處。

「生產力套件」演示

為了演示片段穿透及逐步採用,我們已經開發一個「生產力套件」演示應用程式,可讓使用者管理待辦事項清單、閱讀駭客新聞等。我們實施這個應用程式的殼層作為用戶端渲染的 React 應用程式,這種技術選擇在企業應用程式中較為常見。這是我們的「舊版應用程式」。此應用程式中包含三條路由,皆已更新為使用微前端片段:

  • /login - 具有用戶端驗證的簡單虛擬登入表單,會在使用者未通過身分驗證時顯示 (在 Qwik 中實施)。
  • /todos - 管理一個或多個待辦事項清單,以兩個協作片段的形式實施:
    • 待辦事項清單選取器 - 用於選擇/建立/刪除待辦事項清單的組成部分(在 Qwik 中實施)。
    • 待辦事項清單編輯器 - TodoMVC app 的複製品(在 React 中實施)。
  • /news - HackerNews 演示的複製品(在 SolidJS 中實施)。

此演示展示了舊版應用程式以及每個片段均可使用不同的獨立技術。

視覺化展示穿透至舊版應用程式的片段

應用程式已在 https://productivity-suite.web-experiments.workers.dev/ 中部署。

您需要先登入再試用 - 只需使用您喜歡的任意使用者名稱(無需密碼)。使用者資料儲存於 Cookie 中,因此您可以使用相同的使用者名稱登出及重新登入。登入後,使用應用程式頂部的導覽列可導覽各個頁面。需特別一提的是,您可以瀏覽「待辦事項清單」及「新聞」頁面,查看實際穿透效果。

可隨時嘗試重新載入頁面,查看即時轉譯的片段,而舊版應用程式會在後台緩慢載入。嘗試在舊版應用程式出現之前與片段互動!

頁面最頂部有一些控制項,可讓您查看片段穿透的實際影響。

  • 使用「舊版 app 啟動程序延遲」滑塊設定舊版應用程式啟動前的模擬延遲。
  • 切換「已啟用穿透」來查看如果 app 不使用片段,使用者會有哪些體驗。
  • 切換「顯示接縫」來查看每個片段在目前頁面上的位置。

運作方式

此應用程式由多個構建區塊組成。

協作 Workers 及舊版應用程式主機概觀

我們演示中的舊版應用程式主機提供定義用戶端 React 應用程式(HTML、JavaScript 及樣式表)的檔案。使用其他技術堆疊構建的應用程式亦可正常運作。片段 Workers 託管微前端片段,如我們之前的片段架構文章中所述。閘道 Worker 處理來自瀏覽器的請求,選擇、擷取及組合來自舊版應用程式及微前端片段的回應式串流。

全部部署這些部分後,能夠協同作業,處理來自瀏覽器的每個請求。我們看看當您轉至「/login」路由時,會出現什麼情況。

檢視登入頁面時的請求流

使用者導覽至應用程式,瀏覽器向閘道 Worker 發出請求以獲取初始 HTML。閘道 Worker 識別正在請求登入頁面的瀏覽器。隨後會發出兩個平行的子請求 - 一個用於擷取舊版應用程式的 index.html;另一個用於請求伺服器端轉譯的登入片段。之後,會將這兩個回應合併到一個回應串流中,其中包含已傳遞至瀏覽器的 HTML。

瀏覽器會顯示 HTML 回應,包含舊版應用程式的空根元素以及伺服器端轉譯的登入片段(可與使用者即時互動)。

瀏覽器隨後會請求舊版應用程式的 JavaScript。此請求由閘道 Worker 進行 Proxy 處理至舊版應用程式主機。同樣,舊版應用程式或片段的任何其他資產均會透過閘道 Worker 路由至舊版應用程式主機或相應的片段 Worker。

下載並執行舊版應用程式的 JavaScript 並在此過程中轉譯應用程式的殼層後,片段穿透就會啟動,將片段移動至舊版應用程式中的適當位置,同時保留其所有 UI 狀態。

雖然我們重點說明登入片段來解釋片段穿透,但也可將同樣的思路套用於 在 /todos/news 路由中實施的其他片段。

穿透程式庫

儘管使用不同的 Web 框架實施,但所有片段均以相同方式使用「穿透程式庫」中的幫助程式整合至舊版應用程式中。此程式庫是我們為演示開發的伺服器端及用戶端公用程式的集合,可處理舊版應用程式與微前端片段之整合。此程式庫的主要功能是 PiercingGateway 類別、片段主機片段出口自訂元素以及 MessageBus 類別。

PiercingGateway

可使用 PiercingGateway 類別來具現化一個閘道 Worker,處理針對我們應用程式之 HTML、JavaScript 及其他資產的所有請求。「PiercingGateway」將請求路由到相應的片段 Workers 或舊版應用程式的主機。此外,還會將這些片段中的 HTML 回應串流與舊版應用程式的回應合併至已返回到瀏覽器的單個 HTML 串流。

可使用穿透程式庫簡單地實施閘道 Worker。建立 PiercingGateway 的新 gateway 執行個體,將 URL 傳遞給舊版應用程式主機,並建立一個函數來確定是否為指定請求啟用穿透。將 gateway 匯出為 Worker 指令碼的預設匯出,以便 Workers 執行階段可以連接其 fetch() 處理程式。

const gateway = new PiercingGateway<Env>({
  // Configure the origin URL for the legacy application.
  getLegacyAppBaseUrl: (env) => env.APP_BASE_URL,
  shouldPiercingBeEnabled: (request) => ...,
});
...

export default gateway;

可透過調用 registerFragment() 方法來註冊片段,以便 gateway 能夠自動將片段之 HTML 及資產的請求路由到其片段 Worker。例如,註冊登入片段如下所示:

gateway.registerFragment({
  fragmentId: "login",
  prePiercingStyles: "...",
  shouldBeIncluded: async (request) => !(await isUserAuthenticated(request)),
});

片段主機及出口

路由請求以及合併閘道 Worker 中的 HTML 回應只完成了實現穿透的一半作業。另一半作業需要在瀏覽器中完成,需要使用我們之前所述的技術將片段穿透至舊版應用程式。

瀏覽器中的片段穿透由一對自訂元素、片段主機(<piercing-fragment-host>)及片段出口(<piercing-fragment-outlet>)促成。

閘道 Worker 將每個片段的 HTML 包裝在片段主機中。在瀏覽器中,片段主機管理片段的生命週期,將片段的 DOM 移動至舊版應用程式中的特定位置時會用到。

<piercing-fragment-host fragment-id="login">
  <login q:container...>...</login>
</piercing-fragment-host>

在舊版應用程式中,開發者透過新增片段出口來標記穿透時片段應出現的位置。演示應用程式的登入路由如下所示:

export function Login() {
  …
  return (
    <div className="login-page" ref={ref}>
      <piercing-fragment-outlet fragment-id="login" />
    </div>
  );
}

將片段出口新增至 DOM 時,會在當前文件中搜尋其關聯片段主機。如果找到片段主機,片段主機及相應內容會移動至出口內。如未找到片段主機,出口會向閘道 Worker 發出請求來擷取片段 HTML,隨後使用 writeitable-dom 程式庫(由 MarkoJS團隊開發的強大小型程式庫)將其直接串流至片段出口。

此回退機制能讓用戶端導覽至包含新片段的路由。如此一來,能夠透過初始(硬)導覽及用戶端(軟)導覽在瀏覽器中轉譯片段。

訊息匯流排

除非我們應用程式中的片段為完全呈現或自包含型,不然,它們還需要與舊版應用程式及其他片段進行通訊。MessageBus 是一個簡單的異步同構且與框架無關的通訊匯流排,舊版應用程式及每個片段均可存取此匯流排。

在我們的演示應用程式中,登入片段需要在驗證使用者身分時通知舊版應用程式。此訊息分派在 Qwik LoginForm 組成部分中實施,如下所示:

const dispatchLoginEvent = $(() => {
  getBus(ref.value).dispatch("login", {
    username: state.username,
    password: state.password,
  });
  state.loading = true;
});

舊版應用程式隨後可以像這樣偵聽這些訊息

useEffect(() => {
  return getBus().listen<LoginMessage>("login", async (user) => {
    setUser(user);
    await addUserDataIfMissing(user.username);
    await saveCurrentUser(user.username);
    getBus().dispatch("authentication", user);
    navigate("/", { replace: true, });
  });
}, []);

之所以選擇這個訊息匯流排實作,蓋因我們需要一個與框架無關的解決方案,並且在伺服器及用戶端上順暢運行。

試一試吧!

藉由片段、片段穿透及 Cloudflare Workers,您能夠提高舊版用戶端轉譯應用程式的效能及開發週期。可逐步採用這些變更,甚至還可在實施包含所選 Web 框架的片段時執行此操作。

可在 https://productivity-suite.web-experiments.workers.dev/ 中查看演示該等功能的「生產力套件」應用程式。

我們展示的所有程式碼均為開源程式碼,已發布至 Github:https://github.com/cloudflare/workers-web-experiments/tree/main/productivity-suite

可隨意複製存放庫。可輕鬆在本地執行,甚至可將您自己的版本(免費)部署至 Cloudflare。我們嘗試盡可能重複使用程式碼。大多數核心邏輯皆位於穿透程式庫內,您可以在自己的專案中嘗試。我們很高興收到回饋、建議或聽聽您想要將其用於哪些應用程式。加入我們的 GitHub 討論或透過我們的discord 管道與我們聯絡。

我們認為,將 Cloudflare Workers 與框架中的最新構想結合起來將推動實現 Web 應用程式使用者及開發者體驗的巨大飛躍。期待看到更多演示、部落格文章及協作,因為我們會繼續衝破 Web 服務的疆域。如果您也想直接參與此歷程,我們亦很高興分享:我們正招賢納士

我們會保護 整個企業網路, 協助客戶打造 有效率的網際網路規模應用程式, 加快任何 網站或網際網路應用程式 阻擋 DDoS 攻擊, 讓 駭客無機可乘, 還能夠協助您 順利展開 Zero Trust 之旅

透過任何裝置造訪 1.1.1.1, 即可開始使用我們的免費應用程式,讓您的網際網路更快速且更安全。

若要深入瞭解我們協助構建更美好網際網路的使命,請從 這裡開始。 您正在尋找新的職業方向,請查看 我們的開放職缺

開發人員週 開發者 Web (TW) Cloudflare Workers (TW) Edge (TW)

Follow on Twitter

Peter Bacon Darwin |@petebd
Igor Minar |@IgorMinar
Cloudflare |Cloudflare

Related Posts