後端有微服務,那前端呢?初探微前端的世界

語言: CN / TW / HK

​前言

最近筆者在工作上一直聽到後端工程師們在談論 Microservices(微服務) 的架構設計,聽到的當下立馬去查詢才知道原來 Microservices 這麼潮,身為前端工程師的我當然也希望前端也可以有這麼新穎的架構,於是這篇文章就要來跟讀者介紹 Micro Frontends(微前端)。

什麼是 Microservices?

在開始進入本篇文章主題之前要先跟讀者們介紹什麼是 Microservices。

Microservices 是一種軟體架構,專注開發在每一個小型功能或者服務上,最後再利用模組化的方式組合出一個大型的應用程式。

如果讀者是前端工程師的話可能會覺得上面的敘述很像是 ES6 module 的架構,開發者只需要專注在每個 module 上的開發,最後再利用 Bundler 打包這些 module 形成一個完整的頁面甚至是應用程式,像下圖這樣。

不過後端跟前端完全不一樣,後端是藉由一個又一個的 request​ 來 real time 的執行相關的程式碼,所以在 Microservices 的架構中,想要讓一個又一個的服務能互相溝通,這時候就是要仰賴各個 API 了。

但這時候會有一個很大的問題,假如這三個 Service 對於前端來講有高度相依性,以上圖為例:一個完整的購物網站必須要先讓使用者登入後才可以進行購買商品以及去購物車結帳,這時候在 Client 端就必須要分別打三次 API 並且互相等待才可以完成這整個流程,甚至假如剛好不小心有一個 Service 壞了需要重新啟動,這時候可能會先產生一個過渡期的 API 避免 Client 端打到有問題的 Service,可是 Client 端也不可能每次都會去記住這個新產生的 API,所以這勢必會造成一個很大問題。

這時候其實可以在這些 Services 上新增一個 API Gateway,對於 Client 端來說我只需要對到一個 Gateway 就好,對於這個 Gateway 來說我一樣是去呼叫各個 Service 並且把資源都處理完後再回傳給前端。

如果有讀者本身是 SRE 熟悉 Docker 或者 K8s 這種用來自動部屬容器化應用程式的平臺,對於上面這張圖應該更熟悉了!像 K8s 就有類似 API Gateway 的 Ingress 而 Docker 則有 routing mesh。

不過光有 API Gateway 其實還沒辦法凸顯 Microservices 的特色,在 Microservices 的架構中其實每個 Service 都可以有自己的 DB,目的就是為了不要讓每個 Service 之間會互相關聯。

但這樣做其實有幾個缺點

很難保證資料的一致性

以上圖為例,假如今天有一個 member 被登出帳號,但這個 member 在被登出帳號之前在** shopping cart** 這個 Service 中有待結帳商品,這時候就會出現 member 資料不一致的問題。

DB 資料遺失

當某一個 Service 壞了需要重啟,這時候 DB 的資料有可能就會遺失導致後續的資料出現錯誤。

為了改善這些缺點這時候就可以將這些 Service 的 DB 設計成可棄性,換句話說就是這些 DB 只是用來作為短期的資料存取而已,背後還有一個共用的大資料庫去更新這些資料,通常都會利用 Redis 這種 cache DB 來進行設計。

為什麼需要 Microservices?

Microservices 的好處就是可以專注開發在每個小服務上,舉例來說以一個購票網站可能會在短時間內湧入大量的流量,這時候 race condition 就顯得相當重要,這時候就可以利用 Go 語言進行開發,亦或者是要開發一個以效能為主的服務,這時候就可以用 Rust 進行開發。

上面也提到 Microservices 必須要仰賴 API 之間的溝通,所以通常在企業等級的產品上都會有拆分模組販售的需求,假如這時候就是利用 Microservices 架構進行開發的話就很好拆分每個服務了。

這些都是使用 Microservices 後所能帶來的好處,所以假如讀者今天需要開發一個非常複查且龐大的平臺,這時候不妨可以利用 Microservices 的架構進行開發喔!

Monolithic Architecture(單體式架構)

講完了 Microservices 後相信讀者應該對於這種架構有了初步的認識,與 Microservices 不一樣的架構就是 Monolithic Architecture(單體式架構)。

一般來說我們正常開發都是使用 Monolithic Architecture,在 Monolithic Architecture 的架構中都會把平臺中所有內容都包裝起來,像下圖這樣:

這種架構不是不好,但假如今天想要拆分或者擴充平臺上的 Service 其實都會比較麻煩一些,而且也會怕牽一髮而動全身,甚至所有的 Service 背後都會連到同一個 DB,這樣極有可能會讓 DB connection 同時連線過高導致 request 一直髮送失敗,所以這也是 Microservices 想要解決的其中一個痛點。

什麼是 Micro Frontends(微前端)?

Micro Frontends 可以想像成前端版的 Microservices,在後端的世界中強調一個又一個 Service 而在前端的世界中則是強調一個又一個的 modules​,如何將網頁中每一個 module 有效的拆分就是 Micro Frontends 要做的工作。

Micro Frontends 的實現方式

接下來要跟讀者介紹的是 Micro Frontends 的實現方式,其實 Micro Frontends 有蠻多種實現方式的:

iframe

透過 iframe​ 的特性讓每個被載入的區塊頁面都可以獨立執行,假如需要有資料上的溝通也可以利用 window.postMessage 來完成,但這樣做會有非常多的缺點,像是有可能載入同樣的程式碼、UI 難以控制、甚至可能會有潛在的安全風險,所以筆者還是建議讀者不要用此方式來進行 Micro Frontends 的實現方式。

Client side 利用 JavaScript 載入 module

這個方法簡單又粗暴,就是利用 JavaScript create 出 script tag​ 後,接著再用 script tag 去載入相關的 module,最後再將其內容塞進去對應的 div​ 內,但缺點就是無法使用 SSR,整體寫法會像下圖這樣。

Web Components

Web Components 可以說是最多人拿來討論的 Micro Frontends 的實現方式了,雖然我們在現今的網頁架構中可以自由地 import 大佬們寫好的元件,但難免都會遇到以下幾個問題:

  • 組相依性的問題:需要安裝只有該元件才會使用的 library,這會造成整個 node_modules 相當龐大。
  • Scope 問題:前端為了樣式上的變化通常都會有藉由許多的 className​ 來進行樣式上的改動,但有可能因為該元件也有撰寫一樣的 className 導致很多時候都需要各種 override,長期下來也會是一種專案維護上的負擔。
  • 版本相容性問題:只要框架進行大改版,基本上就很容易出現元件無法相容新版本的狀況,這時候只能元件作者升級版本之後才能再次使用,相信這個狀況也是許多開發者都會面臨到的困境。

因此 Web Components 的出現就是希望可以解決上述的問題,而 Web Components 一共由以下三種元素組成:

Custom elements

自訂一個語意化標籤來引用 components,並且利用 JavaScript 來建立 custom elements、shadow DOM、HTML templates 三者之間的關聯,這種自定義標籤會像這樣:<my-custom-element></my-custom-element> 。

Shadow DOM

Shadow DOM 簡單來說就是在現有的 DOM tree 中產生出一個完全獨立於其他元素的 sub DOM tree,藉由此方法就可以讓 sub DOM tree 自己獨立運作並且不會干擾到其他 DOM tree 上的元素。

HTML templates

利用 <template>​ 以及 <slot> 元素產生出具有複用性的 HTML 樣板。

感覺上 Web Components 的設計準則就可以當作是 Micro Frontends 的最好的實現方式,透過上述的三種方式就可以產生出一個獨立於原本專案而且又不會影響到頁面的 modules。

可是這種開發方式其實不太符合每一種框架的設計理念(可能只有 Angular 體系最適合XD),而且會讓一個 reusable component 開發起來蠻冗長的,所以 Micro Frontends 還是沒有被很多前端工程師所重視。

Webpack Module Federation

假設我們今天有 A、B 兩個專案,其中 A 專案有元件發現是可以讓 B 專案進行複用的,這時候我們會有以下幾種作法達到這個需求:

  • 將 A 專案中可以被複用的元件手動複制其程式碼至 B 專案。
  • 將 A 薦中可以被複用的元件釋出到 npm 上,並且在 B 專案中安裝。
  • 融合 A 專案與 B 專案並用 monorepo 的架構進行開發。

相信不管是哪個方法都相當複雜而且後續的維護成本也很高,而且隨著專案越來越大,之後的網站效能也是問題,即便用了 code splitting 或者是 dynamic import 能提昇的效能也有限。

不過這個聲音 Webpack 聽到了,在 Webpack 5 誕生後推出了一個全新的技術叫 Module Federation,Module Federation 的誕生就是為了幫助開發者可以 import 外部專案已經 bundle 好的檔案,不用再經由額外的安裝步驟相當方便。

Webpack Module Federation

上面介紹完 Micro Frontends 的實現方式後,接下來就要進入本篇文章的實戰篇了,這邊筆者會以 Webpack Module Federation 進行 Demo。

首先筆者先介紹 Webpack Module Federation 的相關設定,在 Webpack Module Federation 的世界中我們一共會分為 Remote 以及 Host 兩種設定區塊,分別代表的是 modules export 以及 import 的部分,首先先介紹 Remote 的部分,在這個 plugin 中我們一共可以設定幾個內容:

name

設定 export 出去的名字。

filename

設定 export​ 出去的檔名稱,最終的使用方式會在下方的 import​ 區塊做介紹,官方建議使用 remoteEntry.js 作為命名。

exposes

exposes​ 就是負責設定 modules​ 中需要被 export​ 的 components​ 檔案 path alias​,通常這邊的寫法都會是 './componentName': 'componentPath​' ,在 key​ 的部分會用相對路徑的寫法原因是未來在 import​ 的時候就可以用上方設定好的 name 作為檔案抓去的進入點。

舉例來說:我們有一個 Button.jsx​ 的檔案,這時候我們就可以在 exposes​ 的區塊寫上 './Button': './src/Button'​ ,到時候在 import​ 的時候就可以寫上 name/Button 的方式抓去這個檔案了。

shared

shared 則是當 components 有使用到相同的第三方元件時,可以用來設定的相關規範,基本上 shared 一共有三種呈現方式:

  • Array syntax:只要在 Array 內寫上 package name 即可,寫法上會像這樣:shared: ['lodash']
  • Object syntax:可以更詳細的設定 package,會以 package name 作為 Object key,後方的 value 則可以設定此 package 的版本,寫法上會像這樣:shared: { lodash: '^4.17.0' }
  • Object with sharing hints:基於 Object syntax 的架構上,填入 Webpack Module Federation 可支援的相關 Sharing hints。

在 Object with sharing hints 這邊一共可以設定非常多種內容,筆者這裡只單純說明幾個最常用的 sharing hints,想要了解所有的 sharing hints 的讀者可以參考這個連結。

eager:讓 Webpack 可以直接存取 modules,如果設定為 false 則使用 async fetch 的方式去取得分離後的 chunk,若設定為 true 則會打包到 remoteEntry 方便進行直接抓取。

requiredVersion:設定此 package 的版本。

singleton:使用 singleton 就代表此 package 只允許一個版本,並且只加載一次,通常在 react 、 react-dom 這種有 global internal state 的 libarary 加上此設定更是重要。

在 Shared 內筆者建議的寫法是基於目前專案的 package dependencies 上再額外疊加一些自己想要設定的 Object with sharing hints,最終設定完的長像會像下圖這樣:

這裡筆者提醒一下讀者,假如讀者在設定的過程中有遇到這個錯誤:Uncaught Error: Shared module is not available for eager consumption

代表讀者需要額外在新增一個檔案將 component render 到 DOM 的相關程式碼都複制上去,最後再到 index.jsx​ 中去 import 這個檔案。

最終呈現在網頁上的畫面就會像下圖這樣:

再來是 Host 的部分,基本上設定的內容跟 Remote 大同小異,差別在於 Remote 需要設定 exposes 的 components 以及 filename ,而 Host 則是要設定該 Remote 的檔案位置。

在檔案位置中我們會以三種內容組合,寫法上會像這樣:[email protected]/filename​ ,其中 name 就是上方介紹到的 export 出去的名稱,remoteURL 則是 Remote 區塊內 Webpack config 中所設定的 output path 相關設定,而 filename 則是上方介紹到的 export 出去的檔名稱。

最終的寫法就會像下方這樣:

於筆者在 shared 的區塊都沒有加上 eager: true ​,所以這邊在抓取 components 時都是用 async 的方式去抓取,因此筆者建議會使用 <Suspense> 的方式進行 data fetching 這樣會比較安全喔!

最終呈現在網頁上的畫面就會像下圖這樣:

事例地址:https://github.com/w5151381guy/micro-frontend-practice

總結

這次介紹了 Micro Frontends 以及 Microservices 這兩種目前最多人討論的架構,希望可以讓讀者更瞭解一些新時代的架構所帶來的優缺點,文章的篇幅可能有點多,讀者可以邊看邊消化一下這樣才能更好的理解所有的內容。