微前端架構是一種類似於微服務的架構,它將微服務的理念應用於瀏覽器端,即將 Web 應用由單一的單體應用轉變爲多個小型前端應用聚合爲一的應用。
由此帶來的變化是,這些前端應用可以獨立運行、獨立開發、獨立部署。以及,它們應該可以在共享組件的同時進行並行開發——這些組件可以通過 NPM 或者 Git Tag、Git Submodule 來管理。
注意:這裏的前端應用指的是前後端分離的單應用頁面,在這基礎才談論微前端纔有意義。
結合我最近半年在微前端方面的實踐和研究來看,微前端架構一般可以由以下幾種方式進行:
- 使用 HTTP 服務器的路由來重定向多個應用
- 在不同的框架之上設計通訊、加載機制,諸如 Mooa 和 Single-SPA
- 通過組合多個獨立應用、組件來構建一個單體應用
- 使用純 WebComponent 構建應用
- iFrame。使用 iFrame 及自定義消息傳遞機制
- 結合 WebComponent 和 ShadowDOM 來隔離應用
不同的方式適用於不同的使用場景,當然也可以組合一起使用。那麼,就讓我們來一一瞭解一下,爲以後的架構演進做一些技術鋪墊。
基礎鋪墊:應用分發路由 -> 路由分發應用
在一個單體前端、單體後端應用中,有一個典型的特徵,即路由是由框架來分發的,框架將路由指定到對應的組件或者內部服務中。微服務在這個過程中做的事情是,將調用由函數調用變成了遠程調用,諸如遠程 HTTP 調用。而微前端呢,也是類似的,它是將應用內的組件調用變成了更細粒度的應用間組件調用,即原先我們只是將路由分發到應用的組件執行,現在則需要根據路由來找到對應的應用,再由應用分發到對應的組件上。
後端:函數調用 -> 遠程調用
在大多數的 CRUD 類型的 Web 應用中,也都存在一些極爲相似的模式,即:首頁 -> 列表 -> 詳情:
- 首頁,用於面向用戶展示特定的數據或頁面。這些數據通常是有限個數的,並且是多種模型的。
- 列表,即數據模型的聚合,其典型特點是某一類數據的集合,可以看到儘可能多的數據概要(如 Google 只返回 100 頁),典型見 Google、淘寶、京東的搜索結果頁。
- 詳情,展示一個數據的儘可能多的內容。
如下是一個 Spring 框架,用於返回首頁的示例:
@RequestMapping(value="/")
public ModelAndView homePage(){
return new ModelAndView("/WEB-INF/jsp/index.jsp");
}
對於某個詳情頁面來說,它可能是這樣的:
@RequestMapping(value="/detail/{detailId}")
public ModelAndView detail(HttpServletRequest request, ModelMap model){
....
return new ModelAndView("/WEB-INF/jsp/detail.jsp", "detail", detail);
}
那麼,在微服務的情況下,它則會變成這樣子:
@RequestMapping("/name")
public String name(){
String name = restTemplate.getForObject("http://account/name", String.class);
return Name" + name;
}
前端:組件調用 -> 應用調用
在形式上來說,單體前端框架的路由和單體後端應用,並沒有太大的區別:依據不同的路由,來返回不同頁面的模板。
const appRoutes: Routes = [
{ path: 'index', component: IndexComponent },
{ path: 'detail/:id', component: DetailComponent },
];
而當我們將之微服務化後,則可能變成應用 A 的路由:
const appRoutes: Routes = [
{ path: 'index', component: IndexComponent },
];
外加之應用 B 的路由:
const appRoutes: Routes = [
{ path: 'detail/:id', component: DetailComponent },
];
而問題的關鍵就在於:怎麼將路由分發到這些不同的應用中去。
路由分發式微前端
路由分發式微前端,即通過路由將不同的業務分發到不同的、獨立前端應用上。其通常可以通過 HTTP 服務器的反向代理來實現,又或者是應用框架自帶的路由來解決。
就當前而言,通過路由分發式的微前端架構應該是採用最多、最易採用的 “微前端” 方案。但是這種方式看上去更像是多個前端應用的聚合,即我們只是將這些不同的前端應用拼湊到一起,使他們看起來像是一個完整的整體。但是它們並不是,每次用戶從 A 應用到 B 應用的時候,往往需要刷新一下頁面。
在幾年前的一個項目裏,我們當時正在進行遺留系統重寫。我們制定了一個遷移計劃:
- 首先,使用靜態網站生成動態生成首頁
- 其次,使用 React 計劃棧重構詳情頁
- 最後,替換搜索結果頁
整個系統並不是一次性遷移過去,而是一步步往下進行。因此在完成不同的步驟時,我們就需要上線這個功能,於是就需要使用 Nginx 來進行路由分發。
如下是一個基於路由分發的 Nginx 配置示例:
http {
server {
listen 80;
server_name www.phodal.com;
location /api/ {
proxy_pass http://http://172.31.25.15:8000/api;
}
location /web/admin {
proxy_pass http://172.31.25.29/web/admin;
}
location /web/notifications {
proxy_pass http://172.31.25.27/web/notifications;
}
location / {
proxy_pass /;
}
}
}
在這個示例裏,不同的頁面的請求被分發到不同的服務器上。
隨後,我們在別的項目上也使用了類似的方式,其主要原因是:跨團隊的協作。當團隊達到一定規模的時候,我們不得不面對這個問題。除此,還有 Angluar 跳崖式升級的問題。於是,在這種情況下,用戶前臺使用 Angular 重寫,後臺繼續使用 Angular.js 等保持再有的技術棧。在不同的場景下,都有一些相似的技術決策。
因此在這種情況下,它適用於以下場景:
- 不同技術棧之間差異比較大,難以兼容、遷移、改造
- 項目不想花費大量的時間在這個系統的改造上
- 現有的系統在未來將會被取代
- 系統功能已經很完善,基本不會有新需求
而在滿足上面場景的情況下,如果爲了更好的用戶體驗,還可以採用 iframe 的方式來解決。
使用 iFrame 創建容器
iFrame 作爲一個非常古老的,人人都覺得普通的技術,卻一直很管用。
HTML 內聯框架元素
<iframe>
表示嵌套的正在瀏覽的上下文,能有效地將另一個 HTML 頁面嵌入到當前頁面中。
iframe 可以創建一個全新的獨立的宿主環境,這意味着我們的前端應用之間可以相互獨立運行。採用 iframe 有幾個重要的前提:
- 網站不需要 SEO 支持
- 擁有相應的應用管理機制。
如果我們做的是一個應用平臺,會在我們的系統中集成第三方系統,或者多個不同部門團隊下的系統,顯然這是一個不錯的方案。一些典型的場景,如傳統的 Desktop 應用遷移到 Web 應用:
如果這一類應用過於複雜,那麼它必然是要進行微服務化的拆分。因此,在採用 iframe 的時候,我們需要做這麼兩件事:
- 設計管理應用機制
- 設計應用通訊機制
加載機制。在什麼情況下,我們會去加載、卸載這些應用;在這個過程中,採用怎樣的動畫過渡,讓用戶看起來更加自然。
通訊機制。直接在每個應用中創建 postMessage
事件並監聽,並不是一個友好的事情。其本身對於應用的侵入性太強,因此通過 iframeEl.contentWindow
去獲取 iFrame 元素的 Window 對象是一個更簡化的做法。隨後,就需要定義一套通訊規範:事件名採用什麼格式、什麼時候開始監聽事件等等。
有興趣的讀者,可以看看筆者之前寫的微前端框架:Mooa。
不管怎樣,iframe 對於我們今年的 KPI 怕是帶不來一絲的好處,那麼我們就去造個輪子吧。
自制框架兼容應用
不論是基於 WebComponent 的 Angular,或者是 VirtualDOM 的 React 等,現有的前端框架都離不開基本的 HTML 元素 DOM。
那麼,我們只需要:
- 在頁面合適的地方引入或者創建 DOM
- 用戶操作時,加載對應的應用(觸發應用的啓動),並能卸載應用。
第一個問題,創建 DOM 是一個容易解決的問題。而第二個問題,則一點兒不容易,特別是移除 DOM 和相應應用的監聽。當我們擁有一個不同的技術棧時,我們就需要有針對性設計出一套這樣的邏輯。
儘管 Single-SPA 已經擁有了大部分框架(如 React、Angular、Vue 等框架)的啓動和卸載處理,但是它仍然不是適合於生產用途。當我基於 Single-SPA 爲 Angular 框架設計一個微前端架構的應用時,我最後選擇重寫一個自己的框架,即 Mooa。
雖然,這種方式的上手難度相對比較高,但是後期訂製及可維護性比較方便。在不考慮每次加載應用帶來的用戶體驗問題,其唯一存在的風險可能是:第三方庫不兼容。
但是,不論怎樣,與 iFrame 相比,其在技術上更具有可吹牛逼性,更有看點。同樣的,與 iframe 類似,我們仍然面對着一系列的不大不小的問題:
- 需要設計一套管理應用的機制。
- 對於流量大的 toC 應用來說,會在首次加載的時候,會多出大量的請求
而我們即又要拆分應用,又想 blabla……,我們還能怎麼做?