天天看點

微前端的那些事兒為什麼微前端開始在流行——Web 應用的聚合實施微前端的六種方式如何解構單體前端應用——前端應用的微服務式拆分大型 Angular 應用微前端的四種拆分政策前端微服務化:使用微前端架構 Mooa 開發微前端應用前端微服務化:使用特制的 iframe 微服務化 Angular 應用

form:https://microfrontend.cn/

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

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

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

目錄

  • 微前端的那些事兒
  • 實施微前端的六種方式
    • 基礎鋪墊:應用分發路由 -> 路由分發應用
      • 後端:函數調用 -> 遠端調用
      • 前端:元件調用 -> 應用調用
    • 路由分發式微前端
    • 使用 iFrame 建立容器
    • 自制架構相容應用
    • 組合式內建:将應用微件化
    • 純 Web Components 技術建構
    • 結合 Web Components 建構
      • 在 Web Components 中內建現有架構
      • 內建在現有架構中的 Web Components
    • 複合型
  • 為什麼微前端開始在流行——Web 應用的聚合
    • 前端遺留系統遷移
    • 後端解耦,前端聚合
    • 相容遺留系統
  • 如何解構單體前端應用——前端應用的微服務式拆分
    • 前端微服化
      • 獨立開發
      • 獨立部署
      • 我們真的需要技術無關嗎?
      • 不影響使用者體驗
    • 微前端的設計理念
      • 設計理念一:中心化路由
      • 設計理念二:辨別化應用
      • 設計理念三:生命周期
      • 設計理念四:獨立部署與配置自動化
    • 實戰微前端架構設計
      • 獨立部署與配置自動化
      • 應用間路由——事件
  • 大型 Angular 應用微前端的四種拆分政策
    • 前端微服務化:路由懶加載及其變體
    • 微服務化方案:子應用模式
    • 方案對比
      • 标準 LazyLoad
      • LazyLoad 變體 1:建構時內建
      • LazyLoad 變體 2:建構後內建
      • 前端微服務化
    • 總對比
  • 前端微服務化:使用微前端架構 Mooa 開發微前端應用
    • Mooa 概念
    • 微前端主工程建立
    • Mooa 子應用建立
    • 導航到特定的子應用
  • 前端微服務化:使用特制的 iframe 微服務化 Angular 應用
    • iframe 微服務架構設計
    • 微前端架構 Mooa 的特制 iframe 模式
    • 微前端架構 Mooa iframe 通訊機制
      • 釋出主應用事件
      • 監聽子應用事件
    • 示例
  • 資源

為什麼微前端開始在流行——Web 應用的聚合

采用新技術,更多不是因為先進,而是因為它能解決痛點。

過去,我一直有一個疑惑,人們是否真的需要微服務,是否真的需要微前端。畢竟,沒有銀彈。當人們考慮是否采用一種新的架構,除了考慮它帶來好處之外,仍然也考量着存在的大量的風險和技術挑戰。

前端遺留系統遷移

自微前端架構 Mooa 及對應的《微前端的那些事兒》釋出的兩個多月以來,我陸陸續續地接收到一些微前端架構的一些咨詢。過程中,我發現了一件很有趣的事:解決遺留系統,才是人們采用微前端方案最重要的原因。

這些咨詢裡,開發人員所遇到的情況,與我之前遇到的情形并相似,我的場景是:設計一個新的前端架構。他們開始考慮前端微服務化,是因為遺留系統的存在。

過去那些使用 Backbone.js、Angular.js、Vue.js 1 等等架構所編寫的單頁面應用,已經線上上穩定地運作着,也沒有新的功能。對于這樣的應用來說,我們也沒有理由浪費時間和精力重寫舊的應用。這裡的那些使用舊的、不再使用的技術棧編寫的應用,可以稱為遺留系統。而,這些應用又需要結合到新應用中使用。我遇到的較多的情況是:舊的應用使用的是 Angular.js 編寫,而新的應用開始采用 Angular 2+。這對于業務穩定的團隊來說,是極為常見的技術棧。

在即不重寫原有系統的基礎之下,又可以抽出人力來開發新的業務。其不僅僅對于業務人員來說, 是一個相當吸引力的特性;對于技術人員來說,不重寫舊的業務,同時還能做一些技術上的挑戰,也是一件相當有挑戰的事情。

後端解耦,前端聚合

而前端微服務的一個賣點也在這裡,去相容不同類型的前端架構。這讓我又聯想到微服務的好處,及許多項目落地微服務的原因:

在初期,背景微服務的一個很大的賣點在于,可以使用不同的技術棧來開發背景應用。但是,事實上,采用微服務架構的組織和機構,一般都是中大型規模的。相較于中小型,對于架構和語言的選型要求比較嚴格,如在内部限定了架構,限制了語言。是以,在充分使用不同的技術棧來發揮微服務的優勢這一點上,幾乎是很少出現的。在這些大型組織機構裡,采用微服務的原因主要還是在于,使用微服務架構來解耦服務間依賴。

而在前端微服務化上,則是恰恰與之相反的,人們更想要的結果是聚合,尤其是那些 To B(to Bussiness)的應用。

在這兩三年裡,移動應用出現了一種趨勢,使用者不想裝那麼多應用了。而往往一家大的商業公司,會提供一系列的應用。這些應用也從某種程度上,反應了這家公司的組織架構。然而,在使用者的眼裡他們就是一家公司,他們就隻應該有一個産品。相似的,這種趨勢也在桌面 Web 出現。聚合成為了一個技術趨勢,展現在前端的聚合就是微服務化架構。

相容遺留系統

那麼,在這個時候,我們就需要使用新的技術、新的架構,來容納、相容這些舊的應用。而前端微服務化,正好是契合人們想要的這個賣點呗了。

實施微前端的六種方式

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

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

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

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

  1. 使用 HTTP 伺服器的路由來重定向多個應用
  2. 在不同的架構之上設計通訊、加載機制,諸如 Mooa 和 Single-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 應用:

微前端的那些事兒為什麼微前端開始在流行——Web 應用的聚合實施微前端的六種方式如何解構單體前端應用——前端應用的微服務式拆分大型 Angular 應用微前端的四種拆分政策前端微服務化:使用微前端架構 Mooa 開發微前端應用前端微服務化:使用特制的 iframe 微服務化 Angular 應用

如果這一類應用過于複雜,那麼它必然是要進行微服務化的拆分。是以,在采用 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 應用的聚合實施微前端的六種方式如何解構單體前端應用——前端應用的微服務式拆分大型 Angular 應用微前端的四種拆分政策前端微服務化:使用微前端架構 Mooa 開發微前端應用前端微服務化:使用特制的 iframe 微服務化 Angular 應用

這種方式看上去相當的理想,即能滿足多個團隊并行開發,又能建構出适合的傳遞物。

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

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

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

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

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

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

純 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" target="_blank" rel="external nofollow" >
<link rel="import" href="components/d-header.html" target="_blank" rel="external nofollow" >
           

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

微前端的那些事兒為什麼微前端開始在流行——Web 應用的聚合實施微前端的六種方式如何解構單體前端應用——前端應用的微服務式拆分大型 Angular 應用微前端的四種拆分政策前端微服務化:使用微前端架構 Mooa 開發微前端應用前端微服務化:使用特制的 iframe 微服務化 Angular 應用

可以看到這邊方式與我們上面使用 iframe 的方式很相似,元件擁有自己獨立的 

Scripts

 和 

Styles

,以及對應的用于單獨部署元件的域名。然而它并沒有想象中的那麼美好,要直接使用純 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

複合型

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

我就不廢話了~~。

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

重新整理頁面?路由拆分?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 之類對于我的設計是多餘的。

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

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

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

實戰微前端架構設計

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

微前端的那些事兒為什麼微前端開始在流行——Web 應用的聚合實施微前端的六種方式如何解構單體前端應用——前端應用的微服務式拆分大型 Angular 應用微前端的四種拆分政策前端微服務化:使用微前端架構 Mooa 開發微前端應用前端微服務化:使用特制的 iframe 微服務化 Angular 應用

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

  1. 主工程在運作的時候,會去伺服器擷取最新的應用配置。
  2. 主工程在擷取到配置後,将一一建立應用,并為應用綁定生命周期。
  3. 當主工程監測到路由變化的時候,将尋找是否有對應的路由比對到應用。
  4. 當比對對對應應用時,則加載相應的應用。

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

微前端的那些事兒為什麼微前端開始在流行——Web 應用的聚合實施微前端的六種方式如何解構單體前端應用——前端應用的微服務式拆分大型 Angular 應用微前端的四種拆分政策前端微服務化:使用微前端架構 Mooa 開發微前端應用前端微服務化:使用特制的 iframe 微服務化 Angular 應用

整體的流程如下圖所示:

微前端的那些事兒為什麼微前端開始在流行——Web 應用的聚合實施微前端的六種方式如何解構單體前端應用——前端應用的微服務式拆分大型 Angular 應用微前端的四種拆分政策前端微服務化:使用微前端架構 Mooa 開發微前端應用前端微服務化:使用特制的 iframe 微服務化 Angular 應用

獨立部署與配置自動化

我們做的部署政策如下:我們的應用使用的配置檔案叫 

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 動畫也是類似的。

大型 Angular 應用微前端的四種拆分政策

上一個月,我們花了大量的時間不熂設計方案來拆分一個大型的 Angular 應用。從使用 Angular 的 Lazyload 到前端微服務化,進行了一系列的讨論。最後,我們終于有了結果,采用的是 Lazyload 變體:建構時內建代碼 的方式。

過去的幾周裡,作為一個 “專業” 的咨詢師,一直忙于在為客戶設計一個 Angular 拆分的服務化方案。主要是為了達成以下的設計目标:

  • 建構插件化的 Web 開發平台,滿足業務快速變化及分布式多團隊并行開發的需求
  • 建構服務化的中間件,搭建高可用及高複用的前端微服務平台
  • 支援前端的獨立傳遞及部署

簡單地來說,就是要支援應用插件化開發,以及多團隊并行開發。

應用插件化開發,其所要解決的主要問題是:臃腫的大型應用的拆分問題。大型前端應用,在開發的時候要面臨大量的遺留代碼、不同業務的代碼耦合在一起,線上上的時候還要面臨加載速度慢,運作效率低的問題。

最後就落在了兩個方案上:路由懶加載及其變體與前端微服務化

前端微服務化:路由懶加載及其變體

路由懶加載,即通過不同的路由來将應用切成不同的代碼快,當路由被通路的時候,才加載對應元件。在諸如 Angular、Vue 架構裡都可以通過路由 + Webpack 打包的方式來實作。而,不可避免地就會需要一些問題:

難以多團隊并行開發,路由拆分就意味着我們仍然是在一個源碼庫裡工作的。也可以嘗試拆分成不同的項目,再編譯到一起。

每次釋出需要重新編譯,是的,當我們隻是更新一個子子產品的代碼,我們要重新編譯整個應用,再重新釋出這個應用。而不能獨立地去建構它,再釋出它。

統一的 Vendor 版本,統一第三方依賴是一件好事。可問題的關鍵在于:每當我們添加一個新的依賴,我們可能就需要開會讨論一下。

然而,标準 Route Lazyload 最大的問題就是難以多團隊并行開發,這裡之是以說的是 “難以” 是因為,還是有辦法解決這個問題。在日常的開發中,一個小的團隊會一直在一個代碼庫裡開發,而一個大的團隊則應該是在不同的代碼庫裡開發。

于是,我們在标準的路由懶加載之上做了一些嘗試。

對于一個二三十人規模的團隊來說,他們可能在業務上歸屬于不同的部門,技術上也有一些不一緻的規範,如 4 個空格、2 個空格還是使用 Tab 的問題。特别是當它是不同的公司和團隊時,他們可能要放棄測試、代碼靜态檢測、代碼風格統一等等的一系列問題。

微服務化方案:子應用模式

除了路由懶加載,我們還可以采用子應用模式,即每個應用都是互相獨立地。即我們有一個基座工程,當使用者點選相應的路由時,我們去加載這個獨立 的 Angular 應用;如果是同一個應用下的路由,就不需要重複加載了。而且,這些都可以依賴于浏覽器緩存來做。

除了路由懶加載,還可以采用的是類似于 Mooa 的應用嵌入方案。如下是基于 Mooa 架構 + Angular 開發而生成的 HTML 示例:

<app-root _nghost-c0="" ng-version="4.2.0">
  ...
  <app-home _nghost-c2="">
    <app-app1 _nghost-c0="" ng-version="5.2.8" style="display: none;"><nav _ngcontent-c0="" class="navbar"></app-app1>
    <iframe frame width="100%" height="100%" src="http://localhost:4200/app/help/homeassets/iframe.html" id="help_206547"></iframe>
  </app-home>
</app-root>
           

Mooa 提供了兩種模式,一種是基于 Single-SPA 的實驗做的,在同一頁面加載、渲染兩個 Angular 應用;一種是基于 iFrame 來提供獨立的應用容器。

解決了以下的問題:

  • 首頁加載速度更快,因為隻需要加載首頁所需要的功能,而不是所有的依賴。
  • 多個團隊并行開發,每個團隊裡可以獨立地在自己的項目裡開發。
  • 獨立地進行子產品化更新,現在我們隻需要去單獨更新我們的應用,而不需要更新整個完整的應用。

但是,它仍然包含有以下的問題:

  • 重複加載依賴項,即我們在 A 應用中使用到的子產品,在 B 應用中也會重新使用到。有一部分可以通過浏覽器的緩存來自動解決。
  • 第一次打開對應的應用需要時間,當然預加載可以解決一部分問題。
  • 在非 iframe 模式下運作,會遇到難以預料的第三方依賴沖突。

于是在總結了一系列的讨論之後,我們形成了一系列的對比方案:

方案對比

在這個過程中,我們做了大量的方案設計與對比,便想寫一篇文章對比一下之前的結果。先看一下圖:

微前端的那些事兒為什麼微前端開始在流行——Web 應用的聚合實施微前端的六種方式如何解構單體前端應用——前端應用的微服務式拆分大型 Angular 應用微前端的四種拆分政策前端微服務化:使用微前端架構 Mooa 開發微前端應用前端微服務化:使用特制的 iframe 微服務化 Angular 應用

表格對比:

x 标準 Lazyload 建構時內建 建構後內建 應用獨立
開發流程 多個團隊在同一個代碼庫裡開發 多個團隊在同不同的代碼庫裡開發 多個團隊在同不同的代碼庫裡開發 多個團隊在同不同的代碼庫裡開發
建構與釋出 建構時隻需要拿這一份代碼去建構、部署 将不同代碼庫的代碼整合到一起,再建構應用 将直接編譯成各個項目子產品,運作時通過懶加載合并 将直接編譯成不同的幾個應用,運作時通過主工程加載
适用場景 單一團隊,依賴庫少、業務單一 多團隊,依賴庫少、業務單一 多團隊,依賴庫少、業務單一 多團隊,依賴庫多、業務複雜
表現方式 開發、建構、運作一體 開發分離,建構時內建,運作一體 開發分離,建構分離,運作一體 開發、建構、運作分離

詳細的介紹如下:

标準 LazyLoad

開發流程:多個團隊在同一個代碼庫裡開發,建構時隻需要拿這一份代碼去部署。

行為:開發、建構、運作一體

适用場景:單一團隊,依賴庫少、業務單一

LazyLoad 變體 1:建構時內建

開發流程:多個團隊在同不同的代碼庫裡開發,在建構時将不同代碼庫的代碼整合到一起,再去建構這個應用。

适用場景:多團隊,依賴庫少、業務單一

變體-建構時內建:開發分離,建構時內建,運作一體

LazyLoad 變體 2:建構後內建

開發流程:多個團隊在同不同的代碼庫裡開發,在建構時将編譯成不同的幾份代碼,運作時會通過懶加載合并到一起。

适用場景:多團隊,依賴庫少、業務單一

變體-建構後內建:開發分離,建構分離,運作一體

前端微服務化

開發流程:多個團隊在同不同的代碼庫裡開發,在建構時将編譯成不同的幾個應用,運作時通過主工程加載。

适用場景:多團隊,依賴庫多、業務複雜

前端微服務化:開發、建構、運作分離

總對比

總體的對比如下表所示:

x 标準 Lazyload 建構時內建 建構後內建 應用獨立
依賴管理 統一管理 統一管理 統一管理 各應用獨立管理
部署方式 統一部署 統一部署 可單獨部署。更新依賴時,需要全量部署 可完全獨立部署
首屏加載 依賴在同一個檔案,加載速度慢 依賴在同一個檔案,加載速度慢 依賴在同一個檔案,加載速度慢 依賴各自管理,首頁加載快
首次加載應用、子產品 隻加載子產品,速度快 隻加載子產品,速度快 隻加載子產品,速度快 單獨加載,加載略慢
前期建構成本 設計建構流程 設計建構流程 設計通訊機制與加載方式
維護成本 一個代碼庫不好管理 多個代碼庫不好統一 後期需要維護元件依賴 後期維護成本低
打包優化 可進行搖樹優化、AoT 編譯、删除無用代碼 可進行搖樹優化、AoT 編譯、删除無用代碼 應用依賴的元件無法确定,不能删除無用代碼 可進行搖樹優化、AoT 編譯、删除無用代碼

前端微服務化:使用微前端架構 Mooa 開發微前端應用

Mooa 是一個為 Angular 服務的微前端架構,它是一個基于 single-spa,針對 IE 10 及 IFRAME 優化的微前端解決方案。

Mooa 概念

Mooa 架構與 Single-SPA 不一樣的是,Mooa 采用的是 Master-Slave 架構,即主-從式設計。

對于 Web 頁面來說,它可以同時存在兩個到多個的 Angular 應用:其中的一個 Angular 應用作為主工程存在,剩下的則是子應用子產品。

  • 主工程,負責加載其它應用,及使用者權限管理等核心控制功能。
  • 子應用,負責不同子產品的具體業務代碼。

在這種模式下,則由主工程來控制整個系統的行為,子應用則做出一些對應的響應。

微前端主工程建立

要建立微前端架構 Mooa 的主工程,并不需要多少修改,隻需要使用 

angular-cli

 來生成相應的應用:

ng new hello-world
           

然後添加 

mooa

 依賴

yarn add mooa
           

接着建立一個簡單的配置檔案 

apps.json

,放在 

assets

 目錄下:

[{
    "name": "help",
    "selector": "app-help",
    "baseScriptUrl": "/assets/help",
    "styles": [
      "styles.bundle.css"
    ],
    "prefix": "help",
    "scripts": [
      "inline.bundle.js",
      "polyfills.bundle.js",
      "main.bundle.js"
    ]
  }
]]
           

接着,在我們的 

app.component.ts

 中編寫相應的建立應用邏輯:

mooa = new Mooa({
  mode: 'iframe',
  debug: false,
  parentElement: 'app-home',
  urlPrefix: 'app',
  switchMode: 'coexist',
  preload: true,
  includeZone: true
});

constructor(private renderer: Renderer2, http: HttpClient, private router: Router) {
  http.get<IAppOption[]>('/assets/apps.json')
    .subscribe(
      data => {
        this.createApps(data);
      },
      err => console.log(err)
    );
}

private createApps(data: IAppOption[]) {
  data.map((config) => {
    this.mooa.registerApplication(config.name, config, mooaRouter.hashPrefix(config.prefix));
  });

  const that = this;
  this.router.events.subscribe((event) => {
    if (event instanceof NavigationEnd) {
      that.mooa.reRouter(event);
    }
  });

  return mooa.start();
}
           

再為應用建立一個對應的路由即可:

{
  path: 'app/:appName/:route',
  component: HomeComponent
}
           

接着,我們就可以建立 Mooa 子應用。

Mooa 子應用建立

Mooa 官方提供了一個子應用的子產品,直接使用該子產品即可:

git clone https://github.com/phodal/mooa-boilerplate
           

然後執行:

npm install
           

在安裝完依賴後,會進行項目的初始化設定,如更改包名等操作。在這裡,将我們的應用取名為 help。

然後,我們就可以完成子應用的建構。

接着,執行:

yarn build

 就可以建構出我們的應用。

将 

dist

 目錄一下的檔案拷貝到主工程的 src/assets/help 目錄下,再啟動主工程即可。

導航到特定的子應用

在 Mooa 中,有一個路由接口 

mooaPlatform.navigateTo

,具體使用情況如下:

mooaPlatform.navigateTo({
  appName: 'help',
  router: 'home'
});
           

它将觸發一個 

MOOA_EVENT.ROUTING_NAVIGATE

 事件。而在我們調用 

mooa.start()

 方法時,則會開發監聽對應的事件:

window.addEventListener(MOOA_EVENT.ROUTING_NAVIGATE, function(event: CustomEvent) {
  if (event.detail) {
    navigateAppByName(event.detail)
  }
})
           

它将負責将應用導向新的應用。

嗯,就是這麼簡單。DEMO 視訊如下:

Demo 位址見:http://mooa.phodal.com/

GitHub 示例:https://github.com/phodal/mooa

前端微服務化:使用特制的 iframe 微服務化 Angular 應用

Angular 基于 Component 的思想,可以讓其在一個頁面上同時運作多個 Angular 應用;可以在一個 DOM 節點下,存在多個 Angular 應用,即類似于下面的形式:

<app-home _nghost-c3="" ng-version="5.2.8">
  <app-help _nghost-c0="" ng-version="5.2.2" style="display:block;"><div _ngcontent-c0=""></div></app-help>
  <app-app1 _nghost-c0="" ng-version="5.2.3" style="display:none;"><nav _ngcontent-c0="" class="navbar"></div></app-app1>
  <app-app2 _nghost-c0="" ng-version="5.2.2" style="display:none;"><nav _ngcontent-c0="" class="navbar"></div></app-app2>
</app-home>
           

可這一樣一來,難免需要做以下的一些額外的工作:

  • 建立子應用項目模闆,以統一 Angular 版本
  • 建構時,删除子應用的依賴
  • 修改第三方子產品

而在這其中最麻煩的就是第三方子產品沖突問題。思來想去,在三月中旬,我在 Mooa 中添加了一個 iframe 模式。

iframe 微服務架構設計

在這裡,總的設計思想和之前的《如何解構單體前端應用——前端應用的微服務式拆分》中介紹是一緻的:

微前端的那些事兒為什麼微前端開始在流行——Web 應用的聚合實施微前端的六種方式如何解構單體前端應用——前端應用的微服務式拆分大型 Angular 應用微前端的四種拆分政策前端微服務化:使用微前端架構 Mooa 開發微前端應用前端微服務化:使用特制的 iframe 微服務化 Angular 應用

主要過程如下:

  • 主工程在運作的時候,會去伺服器擷取最新的應用配置。
  • 主工程在擷取到配置後,将一一建立應用,并為應用綁定生命周期。
  • 當主工程監測到路由變化的時候,将尋找是否有對應的路由比對到應用。
  • 當比對對對應應用時,則建立或顯示相應應用的 iframe,并隐藏其它子應用的 iframe。

其加載形式與之前的 Component 模式并沒有太大的差別:

微前端的那些事兒為什麼微前端開始在流行——Web 應用的聚合實施微前端的六種方式如何解構單體前端應用——前端應用的微服務式拆分大型 Angular 應用微前端的四種拆分政策前端微服務化:使用微前端架構 Mooa 開發微前端應用前端微服務化:使用特制的 iframe 微服務化 Angular 應用

而為了控制不同的 iframe 需要做到這麼幾件事:

  1. 為不同的子應用配置設定 ID
  2. 在子應用中進行 hook,以通知主應用:子應用已加載
  3. 在子應用中建立對應的事件監聽,來響應主應用的 URL 變化事件
  4. 在主應用中監聽子程式的路由跳轉等需求

因為大部分的代碼可以與之前的 Mooa 複用,于是我便在 Mooa 中實作了相應的功能。

微前端架構 Mooa 的特制 iframe 模式

iframe 可以建立一個全新的獨立的宿主環境,這意味着我們的 Angular 應用之間可以互相獨立運作,我們唯一要做的是:建立一個通訊機制。

它可以不修改子應用代碼的情況下,可以直接使用。與此同時,它在一般的 iframe 模式進行了優化。使用普通的 iframe 模式,意味着:我們需要加載大量的重複元件,即使經過 Tree-Shaking 優化,它也将帶來大量的重複内容。如果子應用過多,那麼它在初始化應用的時候,體驗可能就沒有那麼友好。但是與此相比,在初始化應用的時候,加載所有的依賴在主程式上,也不是一種很友好的體驗。

于是,我就在想能不能建立一個更友好地 IFrame 模式,在裡面對應用及依賴進行處理。如下,就是最後生成的頁面的 iframe 代碼:

<app-home _nghost-c2="" ng-version="5.2.8">
  <iframe frames">"" width="100%" height="100%" src="http://localhost:4200/assets/iframe.html" id="help_206547" style="display:block;"></iframe>
  <iframe frames">"" width="100%" height="100%" src="http://localhost:4200/assets/iframe.html" id="app_235458 style="display:none;"></iframe>
</app-home>
           

對,兩個 iframe 的 src 是一樣的,但是它表現出來的确實是兩個不同的 iframe 應用。那個 iframe.html 裡面其實是沒有内容的:

<!doctype html>
<html s">"en">
<head>
  <meta charset="utf-8">
  <title>App1</title>
  <base href="/">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>

</body>
</html>
           

(PS:詳細的代碼可以見 https://github.com/phodal/mooa)

隻是為了建立 iframe 的需要而存在的,對于一個 Angular 應用來說,是不是一個 iframe 的差別并不大。但是,對于我們而言,差別就大了。我們可以使用自己的方式來控制這個 IFrame,以及我們所要加載的内容。如:

  • 共同 Style Guide 中的 CSS 樣式。如,在使用 iframe 內建時,移除不需要的
  • 去除不需要重複加載的 JavaScript。如,打包時不需要的 zone.min.js、polyfill.js 等等

注意

:對于一些共用 UI 元件而言,仍然需要重複加載。這也就是 iframe 模式下的問題。

微前端架構 Mooa iframe 通訊機制

為了在主工程與子工程通訊,我們需要做到這麼一些事件政策:

釋出主應用事件

由于,我們使用 Mooa 來控制 iframe 加載。這就意味着我們可以通過 

document.getElementById

 來擷取到 iframe,随後通過 

iframeEl.contentWindow

 來釋出事件,如下:

let iframeEl: any = document.getElementById(iframeId)
if (iframeEl && iframeEl.contentWindow) {
  iframeEl.contentWindow.mooa.option = window.mooa.option
  iframeEl.contentWindow.dispatchEvent(
    new CustomEvent(MOOA_EVENT.ROUTING_CHANGE, { detail: eventArgs })
  )
}
           

這樣,子應用就不需要修改代碼,就可以直接接收對應的事件響應。

監聽子應用事件

由于,我們也希望能直接在主工程中處理子程式的事件,并且不修改原有的代碼。是以,我們也使用同樣的方式來在子應用中監聽主應用的事件:

iframeEl.contentWindow.addEventListener(MOOA_EVENT.ROUTING_NAVIGATE, function(event: CustomEvent) {
  if (event.detail) {
    navigateAppByName(event.detail)
  }
})
           

示例

同樣的我們仍以 Mooa 架構作為示例,我們隻需要在建立 mooa 執行個體時,配置使用 iframe 模式即可:

this.mooa = new Mooa({
  mode: 'iframe',
  debug: false,
  parentElement: 'app-home',
  urlPrefix: 'app',
  switchMode: 'coexist',
  preload: true,
  includeZone: true
});

...

that.mooa.registerApplicationByLink('help', '/assets/help', mooaRouter.matchRoute('help'));
that.mooa.registerApplicationByLink('app1', '/assets/app1', mooaRouter.matchRoute('app1'));
this.mooa.start();

...
this.router.events.subscribe((event: any) => {
  if (event instanceof NavigationEnd) {
    that.mooa.reRouter(event);
  }
});