實施微前端的六種方式

Phodal發表於2018-07-11

微前端架構是一種類似於微服務的架構,它將微服務的理念應用於瀏覽器端,即將 Web 應用由單一的單體應用轉變為多個小型前端應用聚合為一的應用

由此帶來的變化是,這些前端應用可以獨立執行獨立開發獨立部署。以及,它們應該可以在共享元件的同時進行並行開發——這些元件可以通過 NPM 或者 Git Tag、Git Submodule 來管理。

注意:這裡的前端應用指的是前後端分離的單應用頁面,在這基礎才談論微前端才有意義。

結合我最近半年在微前端方面的實踐和研究來看,微前端架構一般可以由以下幾種方式進行:

  1. 使用 HTTP 伺服器的路由來重定向多個應用
  2. 在不同的框架之上設計通訊、載入機制,諸如 MooaSingle-SPA
  3. 通過組合多個獨立應用、元件來構建一個單體應用
  4. iFrame。使用 iFrame 及自定義訊息傳遞機制
  5. 使用純 Web Components 構建應用
  6. 結合 Web Components 構建

不同的方式適用於不同的使用場景,當然也可以組合一起使用。那麼,就讓我們來一一瞭解一下,為以後的架構演進做一些技術鋪墊。

基礎鋪墊:應用分發路由 -> 路由分發應用

在一個單體前端、單體後端應用中,有一個典型的特徵,即路由是由框架來分發的,框架將路由指定到對應的元件或者內部服務中。微服務在這個過程中做的事情是,將呼叫由函式呼叫變成了遠端呼叫,諸如遠端 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 應用的時候,往往需要重新整理一下頁面。

在幾年前的一個專案裡,我們當時正在進行遺留系統重寫。我們制定了一個遷移計劃:

  1. 首先,使用靜態網站生成動態生成首頁
  2. 其次,使用 React 計劃棧重構詳情頁
  3. 最後,替換搜尋結果頁

整個系統並不是一次性遷移過去,而是一步步往下進行。因此在完成不同的步驟時,我們就需要上線這個功能,於是就需要使用 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 應用:

Angular Tabs 示例

如果這一類應用過於複雜,那麼它必然是要進行微服務化的拆分。因此,在採用 iframe 的時候,我們需要做這麼兩件事:

  • 設計管理應用機制
  • 設計應用通訊機制

載入機制。在什麼情況下,我們會去載入、解除安裝這些應用;在這個過程中,採用怎樣的動畫過渡,讓使用者看起來更加自然。

通訊機制。直接在每個應用中建立 postMessage 事件並監聽,並不是一個友好的事情。其本身對於應用的侵入性太強,因此通過 iframeEl.contentWindow 去獲取 iFrame 元素的 Window 物件是一個更簡化的做法。隨後,就需要定義一套通訊規範:事件名採用什麼格式、什麼時候開始監聽事件等等。

有興趣的讀者,可以看看筆者之前寫的微前端框架:Mooa

不管怎樣,iframe 對於我們今年的 KPI 怕是帶不來一絲的好處,那麼我們就去造個輪子吧。

自制框架相容應用

不論是基於 Web Components 的 Angular,或者是 VirtualDOM 的 React 等,現有的前端框架都離不開基本的 HTML 元素 DOM。

那麼,我們只需要:

  1. 在頁面合適的地方引入或者建立 DOM
  2. 使用者操作時,載入對應的應用(觸發應用的啟動),並能解除安裝應用。

第一個問題,建立 DOM 是一個容易解決的問題。而第二個問題,則一點兒不容易,特別是移除 DOM 和相應應用的監聽。當我們擁有一個不同的技術棧時,我們就需要有針對性設計出一套這樣的邏輯。

儘管 Single-SPA 已經擁有了大部分框架(如 React、Angular、Vue 等框架)的啟動和解除安裝處理,但是它仍然不是適合於生產用途。當我基於 Single-SPA 為 Angular 框架設計一個微前端架構的應用時,我最後選擇重寫一個自己的框架,即 Mooa

雖然,這種方式的上手難度相對比較高,但是後期訂製及可維護性比較方便。在不考慮每次載入應用帶來的使用者體驗問題,其唯一存在的風險可能是:第三方庫不相容

但是,不論怎樣,與 iFrame 相比,其在技術上更具有可吹牛逼性,更有看點。同樣的,與 iframe 類似,我們仍然面對著一系列的不大不小的問題:

  • 需要設計一套管理應用的機制。
  • 對於流量大的 toC 應用來說,會在首次載入的時候,會多出大量的請求

而我們即又要拆分應用,又想 blabla……,我們還能怎麼做?

組合式整合:將應用微件化

組合式整合,即通過軟體工程的方式在構建前、構建時、構建後等步驟中,對應用進行一步的拆分,並重新組合。

從這種定義上來看,它可能算不上並不是一種微前端——它可以滿足了微前端的三個要素,即:獨立執行獨立開發獨立部署。但是,配合上前端框架的元件 Lazyload 功能——即在需要的時候,才載入對應的業務元件或應用,它看上去就是一個微前端應用。

與此同時,由於所有的依賴、Pollyfill 已經儘可能地在首次載入了,CSS 樣式也不需要重複載入。

常見的方式有:

  • 獨立構建元件和應用,生成 chunk 檔案,構建後再歸類生成的 chunk 檔案。(這種方式更類似於微服務,但是成本更高)
  • 開發時獨立開發元件或應用,整合時合併元件和應用,最後生成單體的應用。
  • 在執行時,載入應用的 Runtime,隨後載入對應的應用程式碼和模板。

應用間的關係如下圖所示(其忽略圖中的 “前端微服務化”):

組合式整合對比

這種方式看上去相當的理想,即能滿足多個團隊並行開發,又能構建出適合的交付物。

但是,首先它有一個嚴重的限制:必須使用同一個框架。對於多數團隊來說,這並不是問題。採用微服務的團隊裡,也不會因為微服務這一個前端,來使用不同的語言和技術來開發。當然了,如果要使用別的框架,也不是問題,我們只需要結合上一步中的自制框架相容應用就可以滿足我們的需求。

其次,採用這種方式還有一個限制,那就是:規範!****規範!****規範!。在採用這種方案時,我們需要:

  • 統一依賴。統一這些依賴的版本,引入新的依賴時都需要一一加入。
  • 規範應用的元件及路由。避免不同的應用之間,因為這些元件名稱發生衝突。
  • 構建複雜。在有些方案裡,我們需要修改構建系統,有些方案裡則需要複雜的架構指令碼。
  • 共享通用程式碼。這顯然是一個要經常面對的問題。
  • 制定程式碼規範。

因此,這種方式看起來更像是一個軟體工程問題。

現在,我們已經有了四種方案,每個方案都有自己的利弊。顯然,結合起來會是一種更理想的做法。

考慮到現有及常用的技術的侷限性問題,讓我們再次將目光放得長遠一些。

純 Web Components 技術構建

在學習 Web Components 開發微前端架構的過程中,我嘗試去寫了我自己的 Web Components 框架:oan。在新增了一些基本的 Web 前端框架的功能之後,我發現這項技術特別適合於作為微前端的基石

Web Components 是一套不同的技術,允許您建立可重用的定製元素(它們的功能封裝在您的程式碼之外)並且在您的 Web 應用中使用它們。

它主要由四項技術元件:

  • Custom elements,允許開發者建立自定義的元素,諸如 。
  • Shadow DOM,即影子 DOM,通常是將 Shadow DOM 附加到主文件 DOM 中,並可以控制其關聯的功能。而這個 Shadow DOM 則是不能直接用其它主文件 DOM 來控制的。
  • HTML templates,即 <template><slot> 元素,用於編寫不在頁面中顯示的標記模板。
  • HTML Imports,用於引入自定義元件。

每個元件由 link 標籤引入:

<link rel="import" href="components/di-li.html">
<link rel="import" href="components/d-header.html">
複製程式碼

隨後,在各自的 HTML 檔案裡,建立相應的元件元素,編寫相應的元件邏輯。一個典型的 Web Components 應用架構如下圖所示:

Web Components 架構

可以看到這邊方式與我們上面使用 iframe 的方式很相似,元件擁有自己獨立的 ScriptsStyles,以及對應的用於單獨部署元件的域名。然而它並沒有想象中的那麼美好,要直接使用 Web Components 來構建前端應用的難度有:

  • 重寫現有的前端應用。是的,現在我們需要完成使用 Web Components 來完成整個系統的功能。
  • 上下游生態系統不完善。缺乏相應的一些第三方控制元件支援,這也是為什麼 jQuery 相當流行的原因。
  • 系統架構複雜。當應用被拆分為一個又一個的元件時,元件間的通訊就成了一個特別大的麻煩。

Web Components 中的 ShadowDOM 更像是新一代的前端 DOM 容器。而遺憾的是並不是所有的瀏覽器,都可以完全支援 Web Components。

結合 Web Components 構建

Web Components 離現在的我們太遠,可是結合 Web Components 來構建前端應用,則更是一種面向未來演進的架構。或者說在未來的時候,我們可以開始採用這種方式來構建我們的應用。好在,已經有框架在打造這種可能性。

就當前而言,有兩種方式可以結合 Web Components 來構建微前端應用:

  • 使用 Web Components 構建獨立於框架的元件,隨後在對應的框架中引入這些元件
  • 在 Web Components 中引入現有的框架,類似於 iframe 的形式

前者是一種元件式的方式,或者則像是在遷移未來的 “遺留系統” 到未來的架構上。

在 Web Components 中整合現有框架

現有的 Web 框架已經有一些可以支援 Web Components 的形式,諸如 Angular 支援的 createCustomElement,就可以實現一個 Web Components 形式的元件:

platformBrowser()
	.bootstrapModuleFactory(MyPopupModuleNgFactory)
		.then(({injector}) => {
			const MyPopupElement = createCustomElement(MyPopup, {injector});
			customElements.define(‘my-popup’, MyPopupElement);
});
複製程式碼

在未來,將有更多的框架可以使用類似這樣的形式,整合到 Web Components 應用中。

整合在現有框架中的 Web Components

另外一種方式,則是類似於 Stencil 的形式,將元件直接構建成 Web Components 形式的元件,隨後在對應的諸如,如 React 或者 Angular 中直接引用。

如下是一個在 React 中引用 Stencil 生成的 Web Components 的例子:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

import 'test-components/testcomponents';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();
複製程式碼

在這種情況之下,我們就可以構建出獨立於框架的元件。

同樣的 Stencil 仍然也只是支援最近的一些瀏覽器,比如:Chrome、Safari、Firefox、Edge 和 IE11

複合型

複合型,對就是上面的幾個類別中,隨便挑幾種組合到一起。

我就不廢話了~~。

結論

那麼,我們應該用哪種微前端方案呢?答案見下一篇《微前端快速選型指南》

相關資料:

相關文章