微前端的那些事兒

微前端是一種相似於微服務的架構,它將微服務的理念應用於瀏覽器端,即將 Web 應用由單一的單體應用轉變爲多個小型前端應用聚合爲一的應用。各個前端應用還能夠獨立運行獨立開發獨立部署javascript

同時,它們也能夠在共享組件的同時進行並行開發——這些組件能夠經過 NPM 或者 Git Tag、Git Submodule 來管理。css

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

 

爲何微前端開始在流行——Web 應用的聚合

採用新技術,更多不是由於先進,而是由於它能解決痛點。前端

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

前端遺留系統遷移

自微前端框架 Mooa 及對應的《微前端的那些事兒》發佈的兩個多月以來,我陸陸續續地接收到一些微前端架構的一些諮詢。過程當中,我發現了一件頗有趣的事:解決遺留系統,纔是人們採用微前端方案最重要的緣由react

這些諮詢裏,開發人員所遇到的狀況,與我以前遇到的情形並類似,個人場景是:設計一個新的前端架構。他們開始考慮前端微服務化,是由於遺留系統的存在。webpack

過去那些使用 Backbone.js、Angular.js、Vue.js 1 等等框架所編寫的單頁面應用,已經在線上穩定地運行着,也沒有新的功能。對於這樣的應用來講,咱們也沒有理由浪費時間和精力重寫舊的應用。這裏的那些使用舊的、再也不使用的技術棧編寫的應用,能夠稱爲遺留系統。而,這些應用又須要結合到新應用中使用。我遇到的較多的狀況是:舊的應用使用的是 Angular.js 編寫,而新的應用開始採用 Angular 2+。這對於業務穩定的團隊來講,是極爲常見的技術棧。git

在即不重寫原有系統的基礎之下,又能夠抽出人力來開發新的業務。其不只僅對於業務人員來講, 是一個至關吸引力的特性;對於技術人員來講,不重寫舊的業務,同時還能作一些技術上的挑戰,也是一件至關有挑戰的事情。github

後端解耦,前端聚合

而前端微服務的一個賣點也在這裏,去兼容不一樣類型的前端框架。這讓我又聯想到微服務的好處,及許多項目落地微服務的緣由:web

在初期,後臺微服務的一個很大的賣點在於,可使用不一樣的技術棧來開發後臺應用。可是,事實上,採用微服務架構的組織和機構,通常都是中大型規模的。相較於中小型,對於框架和語言的選型要求比較嚴格,如在內部限定了框架,限制了語言。所以,在充分使用不一樣的技術棧來發揮微服務的優點這一點上,幾乎是不多出現的。在這些大型組織機構裏,採用微服務的緣由主要仍是在於,使用微服務架構來解耦服務間依賴

而在前端微服務化上,則是偏偏與之相反的,人們更想要的結果是聚合,尤爲是那些 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 應用:

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 的方式很類似,組件擁有本身獨立的 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

複合型

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

我就不廢話了~~。

微前端架構選型指南

在上一節《實施前端微服務化的六七種方式》中,介紹了在實施微前端的過程當中,咱們採用的一些不一樣方案的架構方案。在這篇文章中,我將總結如何依據不一樣的狀況來選擇合適的方案。

快速選型指南圖

我仍是直接先給結論:

微前端選型指南

關鍵點的相關解釋以下:

框架限制。在後臺微服務系統裏,人們使用其它語言的庫來開發新的服務,如用於人工智能的 Python。可是在前端,幾乎不存在這種可能性。因此當咱們的前端框架只有一個時,咱們在採用微前端的技術時,可選範圍就更大了。而遺憾的是,多數組織須要兼容遺留系統。

IE 問題。不管是在幾年前,仍是在今年,咱們實施微前端最早考慮的就是對於 IE 的支持。在我遇到的項目上,基本上都須要支持 IE,所以在技術選型上就受限必定的限制。而在咱們那些不須要支持 IE 的項目上,他們就可使用 WebComponents 技術來構建微前端應用。

依賴獨立。即各個微前端應用的依賴是要統一管理,仍是要在各個應該中本身管理。統一管理能夠解決重複加載依賴的問題,獨立管理會帶來額外的流量開銷和等待時間。

微前端方案的對比:簡要對比

若是你對上述的幾個方面,仍然不是很熟悉的話,請閱讀《實施前端微服務化的六七種方式》。

方式 開發成本 維護成本 可行性 同一框架要求 實現難度 潛在風險
路由分發 這個方案太普通了
iFrame 這個方案太普通了
應用微服務化 ★★★★ 針對每一個框架作定製及 Hook
微件化 ★★★★★ 針對構建系統,如 webpack 進行 hack
微應用化 ★★★ 統一不一樣應用的構建規範
純 Web Components ★★ 新技術,瀏覽器的兼容問題
結合 Web Components ★★ 新技術,瀏覽器的兼容問題

一樣的,一些複雜概念的解釋以下:

應用微服務化,即每一個前端應用一個獨立的服務化前端應用,並配套一套統一的應用管理和啓動機制,諸如微前端框架 Single-SPA 或者 mooa 。

微件化,即經過對構建系統的 hack,使不一樣的前端應用可使用同一套依賴。它在應用微服務化的基本上,改進了重複加載依賴文件的問題。

微應用化,又能夠稱之爲組合式集成,即經過軟件工程的方式,在開發環境對單體應用進行拆分,在構建環境將應用組合在一塊兒構建成一個應用。詳細的細節,能夠期待後面的文章《一個單體前端應用的拆解與微服務化》

微前端方案的對比:複雜方式

以前看到一篇微服務相關的 文章,介紹了不一樣微服務的區別,其採用了一種比較有意思的對比方式特別詳細,這裏就使用一樣的方式來展現:

架構目標 描述
a. 獨立開發 獨立開發,而不受影響
b. 獨立部署 能做爲一個服務來單獨部署
c. 支持不一樣框架 能夠同時使用不一樣的框架,如 Angular、Vue、React
d. 搖樹優化 能消除未使用的代碼
e. 環境隔離 應用間的上下文不受干擾
f. 多個應用同時運行 不一樣應用能夠同時運行
g. 共用依賴 不一樣應用是否共用底層依賴庫
h. 依賴衝突 依賴的不一樣版本是否致使衝突
i. 集成編譯 應用最後被編譯成一個總體,而不是分開構建

那麼,對於下表而言,表中的 a~j 分別表示上面的幾種不一樣的架構考慮因素。

(PS:考慮到 Web Components 幾個單詞的長度,暫時將它簡稱爲 WC~~)

方式 a b c d e f g h i
路由分發 O O O O O O      
iFrame O O O O O O      
應用微服務化 O O O     O      
微件化 O O     - - O -  
微應用化 O O   O - - O - O
純 WC O O   O O O - - O
結合 WC O O O O O O     O

圖中的 O 表示支持,空白表示不支持,- 表示不受影響。

再結合以前的選型指南:

微前端選型指南

(PS:本圖採用 Keynote 繪製)

你是否找到你想到的架構了?

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

刷新頁面?路由拆分?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

總體的流程以下圖所示:

Workflow

獨立部署與配置自動化

咱們作的部署策略以下:咱們的應用使用的配置文件叫 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 frameborder="" 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 模式下運行,會遇到難以預料的第三方依賴衝突。

因而在總結了一系列的討論以後,咱們造成了一系列的對比方案:

方案對比

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

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 微服務架構設計

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

Mooa 架構

主要過程以下:

  • 主工程在運行的時候,會去服務器獲取最新的應用配置。
  • 主工程在獲取到配置後,將一一建立應用,併爲應用綁定生命週期。
  • 當主工程監測到路由變化的時候,將尋找是否有對應的路由匹配到應用。
  • 當匹配對對應應用時,則建立或顯示相應應用的 iframe,並隱藏其它子應用的 iframe。

其加載形式與以前的 Component 模式並無太大的區別:

Mooa Component 加載

而爲了控制不一樣的 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 frameborder="" width="100%" height="100%" src="http://localhost:4200/assets/iframe.html" id="help_206547" style="display:block;"></iframe> <iframe frameborder="" 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 lang="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); } }); 

子程序則直接使用:https://github.com/phodal/mooa-boilerplate 就能夠了。

資源

相關資料:

 

轉自https://microfrontend.cn/

相關文章
相關標籤/搜索