m-fe/react-ts-webpack
在 Web 開發導論/微前端與大前端一文中,筆者簡述了微服務與微前端的設計理念以及微前端的潛在可行方案。微服務與微前端,都是希望將某個單一的單體應用,轉化為多個可以獨立執行、獨立開發、獨立部署、獨立維護的服務或者應用的聚合,從而滿足業務快速變化及分散式多團隊並行開發的需求。如康威定律(Conway’s Law)所言,設計系統的組織,其產生的設計和架構等價於組織間的溝通結構;微服務與微前端不僅僅是技術架構的變化,還包含了組織方式、溝通方式的變化。微服務與微前端原理和軟體工程,物件導向設計中的原理同樣相通,都是遵循單一職責(Single Responsibility)、關注分離(Separation of Concerns)、模組化(Modularity)與分而治之(Divide & Conquer)等基本的原則。
fe-boilerplates 是筆者的前端專案模板集錦,包含了單模組單頁面、單模組多頁面、(偽)多模組單頁面、微前端專案等不同型別的模板,其中微前端專案模組 m-fe/react-ts-webpack 與前者的區別即在於微前端中的各個模組能夠獨立開發,獨立版本釋出,獨立部署,獨立載入。分散式協作勢必會帶來協同以及開發流程上的挑戰,在設計微前端專案架構的時候開發易用性也是非常重要的考量點。在年度總結中我也討論了使用 TS 面向重構程式設計的意義,歡迎參考 Backend-Boilerplates/node 中的 ts-*
專案,使用 TS 進行全棧開發。
當我們考量專案框架、模板或者腳手架的時候,首先想到的點就是希望儘可能對上層遮蔽細節,但是對於長期維護的、多人協作的中大型專案而言,如果專案的主導者直接使用了部分抽象的腳手架,不免會給未來的更新、迭代帶來一定的技術負債;同時,目前也有很多成熟的工程化腳手架,因此筆者選擇以專案模板的形式抽象出微前端中所需要的部分。儘可能地遵循簡約、直觀的原則,減少抽象/Magic Function 等;大型專案可能會抽象出專用的開發工具流,但是對於大部分專案而言,在現有框架/工具鏈的基礎上進行適當封裝會是較優選擇。
# 拉取並且提取出子專案
git clone https://github.com/wxyyxc1992/fe-boilerplate
cp fe-boilerplate/micro-frontend/react-ts-webpack ../
# 新增全域性的依賴更新工具
$ yarn global add npm-check-updates
# 為各個子專案安裝依賴,以及連結各個子專案
$ npm run bootstrap && npm run build
# 執行預編譯操作
$ npm run build
# 以基礎模式執行 Host APP,此時 Host APP 作為獨立應用啟動
$ cd packages/rtw-host-app & npm run dev:sa
# 以標準模式執行子應用
$ cd packages/rtw-mobx-app & npm run dev
# 返回根目錄
$ cd .. & npm start
複製程式碼
值得說明的是,微前端作為概念對於不同人承載了不同的考量,其實現方式、落地路徑也是見仁見智,若有不妥,敬請指教。
Features
- 非 APP 類可單獨釋出,APP 類可單獨執行,與釋出。釋出版本可包含 ES, CJS, UMD 等,dist 目錄下包含 ES/CJS 模組,build 目錄下包含 APP 完整資源以及 UMD 模組。
- 版本控制: 子應用資源不使用 Hash 方式,而是使用語義化版本,
/[cdnHost]/[projectName]/[subAppName]/[x.y.z]/index.{js,css}
- 樣式,LESS 檔案支援 CSS Modules,CSS/SCSS 使用標準 CSS
- 狀態管理,靈活支援 Redux/MobX/Dva 等不同的狀態管理框架,對於 Redux 提供全域性統一的 Store 宣告
Structure | 專案結構
完整的微前端應用,可能會包含以下組成部分:
- Module | 模組: 模組是可單獨編譯、釋出的基礎單元,基礎模式下可直接打包入主應用,標準模式下多專案共用時可單獨打包為 AMD/UMD 格式,通過 SystemJS 引入
- Page | 頁面: 頁面不可單獨編譯,使用 Webpack SplitChunk 或其他機制進行非同步載入
- App | 應用: 應用是對模組的擴充套件,是實際使用者可見的部分
- Widget | 控制元件: 控制元件是特殊的模組,譬如通用的無業務元件等
- Extension | 擴充套件: 擴充套件是特殊的應用,提供了跨模組的通用功能,類似於 Chrome Extension 的定位
基於此,我們可以將某個微前端應用抽象為如下不同的模組組:
基礎模組:
- rtw: 根目錄,public 目錄下包含了部分跨模組整合測試的程式碼
核心模組:
- rtw-core/rtw-sdk/rtw-shared: 暴露給子應用可用的通用基礎類、模型定義、部分無介面獨立模組等。rtw-core 建議不放置介面相關,使用 Jest UT 方式進行功能驗證。
- rtw-bootstrap: 完整專案級別編譯與啟動入口,包含專案的執行時配置、依賴配置\訊息匯流排、註冊中心、核心模組載入機制等。
- rtw-host-app: 提供介面基礎容器,譬如應用標準的 Layout,Menu 等元件;提供 Redux 核心 Store。
子業務應用:
- rtw-mobx-app: MobX 示例應用
- rtw-redux-app: Redux 示例應用
擴充套件模組:
- rtw-widgets: 包含部分業務型控制元件,提供給所有的子應用使用,提取通用業務邏輯、對上遮蔽部分第三方依賴關係,類似於完整的 OSS 檔案上傳控制元件等。
- rtw-extensions: 包含部分業務無關的通用型外掛,類似於 Chrome Extension 的定位。
- rtw-worker: 包含通用的 Web Worker & WASM 計算模組,子應用內也可以通過 Buffer 方式直接引入自定義的 Worker
如果希望在子應用 A 中載入子應用 B 的例項,則應該使用類似於依賴注入的方式,從統一的註冊中心中獲取該例項物件。所有各個模組共享的基礎庫,都必須以 UMD 模式載入到全域性;rtw-host-app 中宣告與使用需要展示哪些模組,rtw-bootstrap 中註冊可提供的 UMD 子模組。
開發模式
筆者一直推崇漸進式的工程架構,因此該模板對於複雜度要求較低的專案而言,可以直接從基礎模式啟動,與其他 TS 專案並無太大區別。
基礎模式
基礎模式類似於(偽)多模組單頁面,僅有唯一的 Host APP 作為編譯與執行的入口,其他包體(譬如 rtw-core)直接打包進主包體中,不使用 SystemJS 進行獨立載入。
rtw-core
rtw-core 及相似的庫承載了公共的結構定義、工具類等,在該包體目錄下執行 npm run build
命令即可以生成 ES/CJS/UMD 等多種型別檔案,以及 types 型別定義;可以直接通過 npm publish 來發布到公共/私有的 NPM 倉庫中。
其他包體通過 NPM 安裝 rtw-core 並使用,如果以標準模式執行,則需要首先載入該庫到全域性作用域,利用 RequireJS/SystemJS 等工具遵循 AMD 規範來注入到其他依賴的庫/應用中。
值得一提的是,對於子應用中,如果存在需要共享元件/型別的情景。對於型別資訊,建議是將子應用同樣編譯打包釋出到 NPM 倉庫中,純元件可以直接引入,對於業務元件建議通過全域性的註冊中心來獲取。
rtw-host-app
在 rtw-host-app 包下,執行 npm run dev:sa
命令,會從 src/index.sa
檔案啟動應用;如上文所述,該模式僅會基於 Webpack Splitted Chunk 進行非同步載入,其開發流程與標準的單模組應用並無區別。
標準模式
rtw-bootstrap & rtw-host-app
rtw-bootstrap 是微前端應用的實際啟動點,其核心功能是執行依賴與子應用的註冊。在啟動時,其會根據傳入的 __HOST_APP__
與 __DEV_APP__
等變數資訊完成應用的順序載入與啟動。在標準模式下,rtw-host-app 的入口是 src/index
檔案,該模式下,index 檔案會對外暴露 render 函式,該函式會由 rtw-bootstrap 注入 importApp 函式來執行子應用載入:
export function render(_importApp: Function) {
importApp = _importApp;
ReactDOM.render(
...
);
}
複製程式碼
換言之,rtw-bootstrap 提供了應用載入的能力,而 rtw-host-app 決定了應該載入哪些應用;在實際案例中,我們應該將使用者許可權控制、選單與子應用資訊獲取等業務操作放置在 rtw-host-app 中。
rtw-redux-app & rtw-mobx-app
這裡以 rtw-mobx-app 為例介紹如何進行子應用開發,如果是專案已經發布上線,那麼我們可以通過 Resource Overrides 等線上資源請求轉發的工具將線上資源請求轉發到本地伺服器。在進行本地開發時,因為子應用本身並不會包含 ReactDOM.render
或者類似的將 Virtual DOM 渲染到介面的函式,因此在執行 npm run dev
之後,本地會開啟生成 UMD 檔案的 Webpack Dev Server。參考子應用的 public/index.html
檔案:
<script src="./bootstrap/static.js" type="text/javascript"></script>
<script src="./bootstrap/runtime.js" type="text/javascript"></script>
<script src="./bootstrap/vendors.js" type="text/javascript"></script>
<script>
// 聯調環境
// window.__HOST_APP__ = {
// id: 'host',
// name: 'HOST APP',
// module: 'http://0.0.0.0:8081/index.js',
// css: 'http://0.0.0.0:8081/index.css'
// };
// 正式開發環境
window.__HOST_APP__ = {
title: 'HOST APP',
module: '/release/rtw-host-app/index.js',
css: '/release/rtw-host-app/index.css'
};
window.__DEV_APP__ = { id: 'dev', name: 'DEV APP', module: '/index.js' };
</script>
<script src="./bootstrap/index.js" type="text/javascript"></script>
複製程式碼
可以看出子應用的啟動需要依賴於 rtw-bootstrap 以及 rtw-host-app,如果專案已經發布上線,那麼建議是直接從 CDN 載入資源;否則可以將資源放置到 public/release
目錄下。如果本地需要同時除錯 Host APP,則直接也將 Host APP 以開發方式執行(npm run dev
),然後直接引入 Webpack Dev Server 生成的資源地址即可。