[architecture] Micro-Frontends
The Basics of Microfrontends
名詞定義
在 micro-frontend 中會把一個 monolithic 的 App 拆分成多個可以各自獨立運作的單元,這裡我們把每個獨立的單元稱作模組(module)或遠端(remote),而多個模組整合成的頁面,則稱作容器(container)或 host。
透過這個 host 來管理不同 App 間的顯示與隱藏。
實際在開發時,會希望:
- 不同模組開發時,只需啟動自己要開發的模組即可
- 當有需要整合不同模組的地方(或者是 production 的環境),才用 container/host 的方式啟動
為什麼要 Micro-frontends?
透過 Microfrontends 的概念和實作,跨組間的工程師可以獨立作業不會互相干擾;而同一個 module 內也會更容易理解和調整,某種程度來說達到低耦合高內聚的效果。
每個 micro-frontends 不會被不同的 React 版本、前端框架所干擾,可以有各自的 Hot Module Replacement,比起整個 App 的 refresh,開發體驗會好上許多。
- 可獨立部署
- 錯誤發生的風險隔離在較小區塊
- 規模較小,較容易理解、重構或置換
- 未和其他系統共享資料狀態,行為更好預測
Integration 的幾種方式
這裡的 integration 指的是如何將不同的 modules 整合在一個頁面中。
Build-Time Integration(Compile-Time Integration / Build-time Sharing)
Container 被瀏覽器載入**「前」**,就已經載入不同 modules 的程式碼。它的流程會類似:
- 負責開發模組的團隊將其所開發的模組部署成 npm package
- 負責 container 的團隊將模組以
nom install
下載並安裝在 container 中 - 將 container 進行打包,打包時就會包含各模組的原始碼
這種作法的好處是容易安裝和理解;壞處則是只要任何 modules 有更新,container(host) 就需要重新打包,這會導致 container 和 modules 間會有比較緊密的相依。
Run-Time Integration(Client-Side Integration / Runtime Sharing):Micro-frontends 是這種
Container 被瀏覽器載入**「後」**,才開始載入不同 module 的程式碼。它的流程會類似:
- 工程團隊部署一個模組到特定的網址(URL)
- 當使用者瀏覽到特定頁面時,container 會載入
- container 載入後,會 fetch 和執行其所需的模組
這種作法的好處是模組的 deploy 和 container 間是獨立的,即使模組更新了,container 也不用重新執行 bundle,每個模組間可以各自部署、各自更新,container 可以即時呈現最新的元件,此外,container 可以動態決定去載入不同版本的模組來使用(例如,A/B testing);缺點則是通常所需的工具和設置會比較複雜。
Server Integration
透過 server 來決定要不要傳送特定的 App 給 client。
The Basic of Module Federation
TL;DR
- remote 的 module 會用
exposes
將函式或元件 export 出來 - host/container 則會用
remotes
來指定要讀取哪一個 remote 的模組
host、remote 使用 Webpack
透過 Webpack 提供的 Module Federation Plugin,可以載入遠端的程式。舉例來說,將 Products 這個 module 載入 Container 中使用的話,Webpack 的設定會類似這樣。
在 Product (remote) 的 Webpack 設定檔:
// products/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
devServer: {
port: 8081,
},
plugins: [
// 將 Products expose 出去
new ModuleFederationPlugin({
// 在 host(container) 的 webpack config 中,會在 @ 前加上這個 key
name: 'products',
// manifest file,以讓 host(container) 知道要如何處理這個 remote,一般不太需要改這個名稱
filename: 'remoteEntry.js',
// path aliases
exposes: {
'./ProductsIndex': './src/index',
},
}),
],
};
在 Container (host) 的 webpack 設定檔:
// container/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
devServer: {
port: 8080,
},
plugins: [
new ModuleFederationPlugin({
name: 'container',
// 如果在 node_modules 中找不到就會進來 remotes 找
remotes: {
// key: 對應到在 host(container) 中要 import 時的名稱
// value: @ 前面表示在 Products 的 webpack 設定檔中所對應到的 name
productsApp: 'products@http://localhost:8081/remoteEntry.js',
},
}),
],
};
如此就能在 Container 中去載 入 Products:
// container/src/bootstrap.js
// 對應到 webpack 中 remotes 設定的 key
import 'productsApp/ProductsIndex';
console.log('Container');