前端應用的微服務式拆分
本文分為以下四部分:
-
前端微服務化思想介紹
-
微前端的設計理念
-
實戰微前端架構設計
-
基於 Mooa 進行前端微服務化
前端微服化
對於前端微服化來說,有這麼一些方案:
-
Web Component 顯然可以一個很優秀的基礎架構。然而,我們並不可能去大量地複寫已有的應用。
-
iFrame。你是說真的嗎?
-
另外一個微前端框架 Single-SPA,顯然是一個更好的方式。然而,它並非 Production Ready。
-
通過路由來切分應用,而這個跳轉會影響使用者體驗。
-
等等。
因此,當我們考慮前端微服務化的時候,我們希望:
-
獨立部署
-
獨立開發
-
技術無關
-
不影響使用者體驗
獨立開發
在過去的幾星期裡,我花費了大量的時間在學習 Single-SPA 的程式碼。但是,我發現它在開發和部署上真的太麻煩了,完全達不到獨立部署地標準。按 Single-SPA 的設計,我需要在入口檔案中聲名我的應用,然後才能去構建:
-
declareChildApplication('inferno', () => import('src/inferno/inferno.app.js'), pathPrefix('/inferno'));
同時,在我的應用裡,我還需要去指定我的生命週期。這就意味著,當我開發了一個新的應用時,必須更新兩份程式碼:主工程和應用。這時我們還極可能在同一個原始碼裡工作。
當出現多個團隊的時候,在同一份原始碼裡工作,顯然變得相當的不可靠——比如說,對方團隊使用的是 Tab,而我們使用的是 2 個空格,隔壁的老王用的是 4 個空格。
獨立部署
一個單體的前端應用最大的問題是,構建出來的 js、css 檔案相當的巨大。而微前端則意味著,這個檔案被獨立地拆分成多個檔案,它們便可以獨立去部署應用。
我們真的需要技術無關嗎?
等等,我們是否真的需要技術無關?如果我們不需要技術無關的話,微前端問題就很容易解決了。
事實上,對於大部分的公司和團隊來說,技術無關只是一個無關痛癢的話術。當一家公司的幾個創始人使用了 Java,那麼極有可能在未來的選型上繼續使用 Java。除非,一些額外的服務來使用 Python 來實現人工智慧。因此,在大部分的情況下,仍然是技術棧唯一。
對於前端專案來說,更是如此:一個部門裡基本上只會選用一個框架。
於是,我們選擇了 Angular。
不影響使用者體驗
使用路由跳轉來進行前端微服務化,是一種很簡單、高效的切分方式。然而,路由跳轉地過程中,會有一個白屏的過程。在這個過程中,跳轉前的應用和將要跳轉的應用,都失去了對頁面的控制權。如果這個應用出了問題,那麼使用者就會一臉懵逼。
理想的情況下,它應該可以被控制。
微前端的設計理念
設計理念一:中心化路由
網際網路本質是去中心化的嗎?不,DNS 決定了它不是。TAB,決定了它不是。
微服務從本質上來說,它應該是去中心化的。但是,它又不能是完全的去中心化。對於一個微服務來說,它需要一個服務註冊中心:
服務提供方要註冊通告服務地址,服務的呼叫方要能發現目標服務。
對於一個前端應用來說,這個東西就是路由。
Menu
從頁面上來說,只有我們在網頁上新增一個菜單鏈接,使用者才能知道某個頁面是可以使用的。
404
而從程式碼上來說,那就是我們需要有一個地方來管理我們的應用:**發現存在哪些應用,哪個應用使用哪個路由。
管理好我們的路由,實際上就是管理好我們的應用。
設計理念二:標識化應用
在設計一個微前端框架的時候,為每個專案取一個名字的問題糾結了我很久——怎麼去規範化這個東西。直到,我再一次想到了康威定律:
系統設計(產品結構等同組織形式,每個設計系統的組織,其產生的設計等同於組織之間的溝通結構。
康威定律
換句人話說,就是同一個組織下,不可能有兩個專案的名稱是一樣的。
所以,這個問題很簡單就解決了。
設計理念三:生命週期
Single-SPA 設計了一個基本的生命週期(雖然它沒有統一管理),它包含了五種狀態:
-
load,決定載入哪個應用,並繫結生命週期
-
bootstrap,獲取靜態資源
-
mount,安裝應用,如建立 DOM 節點
-
unload,刪除應用的生命週期
-
unmount,解除安裝應用,如刪除 DOM 節點
於是,我在設計上基本上沿用了這個生命週期。顯然,諸如 load 之類對於我的設計是多餘的。
設計理念四:獨立部署與配置自動化
從某種意義上來說,整個每系統是圍繞著應用配置進行的。如果應用的配置能自動化,那麼整個系統就自動化。
當我們只開發一個新的元件,那麼我們只需要更新我們的元件,並更新配置即可。而這個配置本身也應該是能自動生成的。
實戰微前端架構設計
基於以上的前提,系統的工作流程如下所示:
系統工作流
整體的工程流程如下所示:
-
主工程在執行的時候,會去伺服器獲取最新的應用配置。
-
主工程在獲取到配置後,將一一建立應用,併為應用繫結生命週期。
-
當主工程監測到路由變化的時候,將尋找是否有對應的路由匹配到應用。
-
當匹配對對應應用時,則載入相應的應用。
故而,其對應的架構如下圖所示:
Architecture
獨立部署與配置自動化
我們做的部署策略如下:我們的應用使用的配置檔案叫 apps.json
,由主工程去獲取這個配置。每次部署的時候,我們只需要將 apps.json
指向最新的配置檔案即可。配置的檔案類如下所示:
-
96a7907e5488b6bb.json
-
6ff3bfaaa2cd39ea.json
-
dcd074685c97ab9b.json
一個應用的配置如下所示:
-
{
-
"name": "help",
-
"selector": "help-root",
-
"baseScriptUrl": "/assets/help",
-
"styles": [
-
"styles.bundle.css"
-
],
-
"prefix": "help",
-
"scripts": [
-
"inline.bundle.js",
-
"polyfills.bundle.js",
-
"main.bundle.js"
-
]
-
}
這裡的 selector
對應於應用所需要的 DOM 節點,prefix 則是用於 URL 路由上。這些都是自動從 index.html
檔案和 package.json
中獲取生成的。
應用間路由——事件
由於現在的應用變成了兩部分:主工程和應用部分。就會出現一個問題:只有一個工程能捕獲路由變化。當由主工程去改變應用的二級路由時,就無法有效地傳達到子應用。在這時,只能通過事件的方式去通知子應用,子應用也需要監測是否是當前應用的路由。
-
if (event.detail.app.name === appName) {
-
let urlPrefix = 'app'
-
if (urlPrefix) {
-
urlPrefix = `/${window.mooa.option.urlPrefix}/`
-
}
-
router.navigate([event.detail.url.replace(urlPrefix + appName, '')])
-
}
相似的,當我們需要從應用 A 跳轉到應用 B 時,我們也需要這樣的一個機制:
-
window.addEventListener('mooa.routing.navigate', function(event: CustomEvent) {
-
const opts = event.detail
-
if (opts) {
-
navigateAppByName(opts)
-
}
-
})
剩下的諸如 Loading 動畫也是類似的。
使用 Mooa 進行
So,我們就有了前端微服務框架 Mooa。它基於 single-spa && single-spa-angular-cli,並且符合以上的設計思想。
GayHub 地址:https://github.com/phodal/mooa
對於主工程而言,只需要以下的幾行程式碼就可以完成上面的功能:
-
http.get<any[]>('/assets/apps.json')
-
.subscribe(data => {
-
data.map((config) => {
-
that.mooa.registerApplication(config.name, config, mooaRouter.matchRoute(config.prefix));
-
});
-
this.mooa.start();
-
});
-
this.router.events.subscribe((event: any) => {
-
if (event instanceof NavigationEnd) {
-
that.mooa.reRouter(event);
-
}
-
});
並新增一個對應的子應用路由:
-
{
-
path: 'app/:appName/:route',
-
component: HomeComponent
-
}
則如上所述的四個步驟。
對於子工程而言,則只需要一個對應的 Hook 操作:
-
mooaPlatform.mount('help').then((opts) => {
-
platformBrowserDynamic().bootstrapModule(AppModule).then((module) => {
-
opts['attachUnmount'](module);
-
});
-
});
並設定好對應的 base_href:
-
providers: [
-
{provide: APP_BASE_HREF, useValue: mooaPlatform.appBase()},
-
]
更多
嗯,就是這麼簡單。DEMO 視訊如下:
Demo 地址見:http://mooa.phodal.com/
GitHub 示例:https://github.com/phodal/mooa