EP14 - 微前端架構和Module Federation經驗

最近都在準備面試尤其是系統設計環節,因為面試的這個 position 比較偏全端所以平常什麼 scaling, load balancing, caching 等等都要知道一些,雖然本來就知道但得自己找題目練習一遍讓自己習慣在這樣的環境把這些思考邏輯弄好讓對方知道你在表達什麼和為什麼需要這些東西,如果這次有上那可能之後會有一集講如何準備 principal software engineer 的面試,如果沒上,那之後就會有一集講我如何fail 我的軟體工程師面試。

今天聊聊微前端,尤其是用 webpack module federation 為基礎架構的微前端,如果有聽過前面集數的人應該有聽過我小聊過微前端不過大部分是在鋪陳當時的話題比如 Nextjs 之類的,如果你有用過微服務 micro service 的話你會發現說每個 service 只 focus 在一個 function 比如這個管理 product data 這個管理 payment,然後每一個微服務可以單獨的 deploy、monitor、scaling等等,現在的 cloud provider 都把這些做好了,那這些微服務可以是 ECS instance 或是 Lambda function,反正重點就是可能因為你想要好測試、縮短部署的時間、減少整體的loading,所以選擇用微服務。

微前端大概就是類似的概念,對使用者來說這個前端 app 可能是一體的但對工程師來說每一頁或是極端一點每一個 component 可能是來自於不同的組,像我們公司是有各個不同的 micro app,每一個 app 都是獨立的,有自己的 repo、CICD pipeline、release schedule、等等而不用像是傳統的都在同一個 application 這樣。

從 high level 這樣看當使用者登入後會先下載一個 micro app 我們稱作為 shell app,這個 shell app會根據使用者的權限再去下載這個使用者可以看到且使用的其他 micro app,這些當然也是配合 API 來看我們可以下載什麼 applications,剩下的就是讓 module federation 去抓這些 JavaScript 然後跑起來。

剛剛一直提到的 module federation 是個 webpack 的 plugin,這個設定其實很簡單,最主要的就是:

  1. 我們透過 webpack transpile 之後那個 final bundle 要叫什麼名字,這樣之後別的 app 想要用你的 code 時候才知道從哪裡抓
  2. 你想要從自己的 codebase 分享什麼東西出去,module federation 就是可以隨時的分享 code 所以這邊可以自定義你想要 expose 什麼 module 出去
  3. 定義你從哪一個 bundle 把東西拿進來用,當知道那個 app 叫什麼名字和module名稱,你可以直接在你的 app 裡面做 import 的動作,webpack 自然知道從哪裡抓這個 module,不過如果你有用 TypeScript 的話那個 types 要自己設定
  4. 定義使用的 library version,假設一個頁面有 4 個 React components從 4 個不同的地方抓,我們不會希望要下載 React 四遍,這邊設定好了 version module federation 就會自動知道說如果目前有這個 version 的 library 已經下載好了我們就不會再下載一次

At the end of the day, 這些 app 從使用者角度看都是同一個 app 但從 engineering 角度看每一頁可能都是不用的 micro app,也因為都有自己的 repo 自己的測試 pipeline deployment,每一個實質上都是獨立運作的。

這樣跟 npm package 有什麼不同,真的要說就是主動權吧,假設這些 micro app 都是npm package那只要我們的 shell app 沒有 npm install 最新的檔案那它永遠是舊的,因為 module federation 關係大家實際上是可以在自己想要的時間做部署這樣也會自動更新這些 micro app 也就不需要等其他 app 下載最新的 npm package 然後測試能不能跑能不能 build 這樣。

順帶提一下我們後端是 GraphQL server 而每一個組都可以有自己的 sub graph 最後 publish 到這個大的 Apollo gateway,每個組可能會為了一個micro app開一個sub graph這樣對應到那個app需要的queries 和 mutations,但如果gateway上面已經有你需要用的query就不用重新做一個了,這邊聽不懂沒關係總之就是微前端會叫一個可能只屬於自己的 API 然後後面會跟專門的微服務溝通。

我們這樣的系統已經兩年左右了,這邊列出幾個我覺得蠻大的缺點,不過這邊聲明有些缺點可能只是因為我們架構想錯了而導致的,或許別人的經驗會不一樣:

  1. 這非常有可能是我們架構問題,先前提到我們可以定義我們想要某些 library 的版本是多少,這樣 module federation會知道說除非版本不同要不然不會重新下載同一個 library,舉例來說假設 webpack 已經下載了 React 18,我們這些 micro app也需要 React 18,webpack看到已經下載了就不會再下載 React 一次了,但也因為這樣我們不能做 tree shaking 因為無法預測其他 app 不需要什麼功能比如 MUI 所有 components 其實都被打包了,就算我們的 app 只用了 10% 的東西我們還是會下載所有 MUI components
  2. 升級版本很麻煩而且需要大家同事做這件事情,拿 React 為例,從 17 到 18,每一個 app 都需要在他們自己的 app 做更新和測試,而且像 React 這樣的package又不能不同版本都一起下載執行,因為這樣導致我們慢了一年才更新完成,我們 MUI 也是花了一年時間從 4 升級到 5。
  3. 不好 debug,像是 npm package 我其實可以進 node_modules 看或是在那邊加 console.log,雖然code不好讀但至少我可以這麼做弄一些比較簡單的 debugging,module federation debug 我得去那個 repo 找那個 code因為沒有 node_modules,我只是告訴 webpack 這個 module 在這個 URL 而沒有真的下載下來,從這邊延伸就是 development 其實也有點麻煩,類似 npm package 需要先npm link 才能看到這樣。還有一些神秘的 bug,主要原因應該是定義的 version 沒有對上可能就會下載一份新的然後就會出現神奇的錯誤或是想之前發生過 MUI style 被覆蓋的 bug。
  4. 有人覺得微服務很複雜,微前端其實也很複雜,像AWS還有CloudFormation 可以用自動把東西都定義好,微前端沒有這類的東西,沒有人開發一個 CLI 自動生成 project 和必要的 S3 bucket 做部署之類的,只有一個前人寫好的文檔讓我們 follow 而已
  5. 前面雖然說每個project是獨立的可以自由的選擇何時部署但假設你有import一個別人的component而你的feature需要這個component的更新之類的,你們就需要溝通好何時deploy之類的,不是什麼大事畢竟我們有時候也會等後端先部署再部署前端東西

後來搜集意見和開會後決定之後這樣的架構會慢慢淘汰掉,取而代之就是透過 nginx 自定義不同的 route 然後分配這些 route 到不同的 application,這樣使用者進了特定的 path 就會到某個 app 這樣,就不會有之前的依賴性了,而我之前講的 Nextjs application 也是因為這樣新的架構而允許我們用不同的框架而不是綁死只能用 React做CSR。

其實這邊有些東西或許monorepo是可以解決的但感覺這跟人和文化有關,除非這些app都是自己的組的,要不然要大家從原本 multi-repo 移到 monorepo 這個挺困難的而且帶來的好處應該比這個 migration 的意願還要小很多

總之我大概沒事不會去碰 module federation 了

如果你喜歡這內容的話幫我在 Twitter 和 Threads 上面分享給其他正在前端這條路上努力的朋友們,也別忘了訂閱我們電子報收到第一手消息喔 🚀