如何解構單體前端應用——前端應用的微服務式拆分

Phodal發表於2018-04-09

重新整理頁面?路由拆分?No,動態載入元件。

本文分為以下四部分:

  • 前端微服務化思想介紹
  • 微前端的設計理念
  • 實戰微前端架構設計
  • 基於 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,決定了它不是。

微服務從本質上來說,它應該是去中心化的。但是,它又不能是完全的去中心化。對於一個微服務來說,它需要一個服務註冊中心

服務提供方要註冊通告服務地址,服務的呼叫方要能發現目標服務。

對於一個前端應用來說,這個東西就是路由。

從頁面上來說,只有我們在網頁上新增一個選單連結,使用者才能知道某個頁面是可以使用的。

而從程式碼上來說,那就是我們需要有一個地方來管理我們的應用:**發現存在哪些應用,哪個應用使用哪個路由。

管理好我們的路由,實際上就是管理好我們的應用

設計理念二:標識化應用

在設計一個微前端框架的時候,為每個專案取一個名字的問題糾結了我很久——怎麼去規範化這個東西。直到,我再一次想到了康威定律:

系統設計(產品結構等同組織形式,每個設計系統的組織,其產生的設計等同於組織之間的溝通結構。

換句人話說,就是同一個組織下,不可能有兩個專案的名稱是一樣的。

所以,這個問題很簡單就解決了。

設計理念三:生命週期

Single-SPA 設計了一個基本的生命週期(雖然它沒有統一管理),它包含了五種狀態:

  • load,決定載入哪個應用,並繫結生命週期
  • bootstrap,獲取靜態資源
  • mount,安裝應用,如建立 DOM 節點
  • unload,刪除應用的生命週期
  • unmount,解除安裝應用,如刪除 DOM 節點

於是,我在設計上基本上沿用了這個生命週期。顯然,諸如 load 之類對於我的設計是多餘的。

設計理念四:獨立部署與配置自動化

從某種意義上來說,整個每系統是圍繞著應用配置進行的。如果應用的配置能自動化,那麼整個系統就自動化。

當我們只開發一個新的元件,那麼我們只需要更新我們的元件,並更新配置即可。而這個配置本身也應該是能自動生成的。

實戰微前端架構設計

基於以上的前提,系統的工作流程如下所示:

系統工作流

整體的工程流程如下所示:

  1. 主工程在執行的時候,會去伺服器獲取最新的應用配置。
  2. 主工程在獲取到配置後,將一一建立應用,併為應用繫結生命週期。
  3. 當主工程監測到路由變化的時候,將尋找是否有對應的路由匹配到應用。
  4. 當匹配對對應應用時,則載入相應的應用。

故而,其對應的架構如下圖所示:

Architecture

獨立部署與配置自動化

我們做的部署策略如下:我們的應用使用的配置檔案叫 apps.json,由主工程去獲取這個配置。每次部署的時候,我們只需要將 apps.json 指向最新的配置檔案即可。配置的檔案類如下所示:

  1. 96a7907e5488b6bb.json
  2. 6ff3bfaaa2cd39ea.json
  3. 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('/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

相關文章