一文讀懂微前端架構

前端開發在程序猿中無疑是一個比較苦逼的存在,作爲一個前端開發,你必須要掌握 Javascript,HTML,CSS 這三大基礎。Javascript 作爲網絡時代最爲重要的開發語言,由於其設計上的限制,一直在演進,經歷了 ES3,ES5,ES6(ECMAScript 2015)... ... 而簡單的 CSS 也無法完成你複雜的需求,你需要 Less/Sass/Sytlus 來增強你的 CSS 的功能。這些還遠遠只是一小部分,你還需要了解:

最可怕的是,這些東西都在飛快地發展和變化中,當你正忙於學習 ES8,ES9,ES10 的新特性的時候,今天我要和大家分享的希望不是壓死前端開發小駱駝的最後一根稻草 -- 微前端。

一、什麼是微前端

而提到微前端就離不開微服務,大家對微服務都比較熟悉了,微服務允許後端體系結構通過鬆散耦合的代碼庫進行擴展,每個代碼庫負責自己的業務邏輯,並公開一個 API,每個 API 均可獨立部署,並且各自由不同的團隊擁有和維護。

前端架構經歷了從單體,到前後端分離,再到微服務,最終發展到現在的微前端的過程如下圖所示:

微前端的思路是把微服務的架構引入到前端,其核心都是要能夠以業務爲單元構建端到端的垂直架構,使得單個的團隊能夠獨立自主的進行相關的開發,同時又具備相當的靈活性,按需求來組成交付應用。

“微前端” 一詞最早於 2016 年底在 ThoughtWorks 技術雷達中提出的。它將微服務的概念擴展到了前端世界。當前的趨勢是構建一個功能強大且功能強大的瀏覽器應用程序(也稱爲單頁應用程序),該應用程序位於微服務架構之上。隨着時間的流逝,通常由一個單獨的團隊開發的前端層會不斷增長,並且變得更加難以維護。

微前端背後的想法是將網站或 Web 應用程序視爲由獨立團隊擁有的功能的組合。每個團隊都有自己關心和專長的不同業務或任務領域。一個團隊是跨職能的,並且從數據庫到用戶界面,端到端地開發其功能。

但是,這個想法並不新鮮。它與 “單體系統” 概念有很多共同點。在過去,類似的方法被稱爲“垂直系統的前端集成”。但是微前端顯然是一個更友好,更輕巧的術語。

在微服務的架構中,後臺的服務已經按照業務進行了分離,而前端仍然是一個單體構建,通過網關來調用不同的後臺服務。這個微服務的思路是相違背的,這也就造成了你的後端團隊是按照業務分割的,但是前端團隊仍然是一個整體。微前端可以有效地改進這一點。

微前端的核心思路其實是遠程應用程序,包含組件 / 模塊 / 包的運行時加載

如上圖,對於用戶而言,訪問的是一個微前端的容器(container),容器加載運行在遠程服務上的應用,把這些遠程應用作爲組件 / 模塊 / 包在本地瀏覽器中加載。

二、爲什麼需要微前端?

它有什麼優勢?

在前面我們看到的微前端之前的架構,所有的前端還是一個單體,前端團隊會依賴所有的服務或者後臺的 API,前端開發會成爲整個系統的瓶頸。使用微前端,就是要讓前端業務從水平分層變爲垂直應用的一部分,進入業務團隊,剝離耦合。

那麼微前端有什麼好處,爲什麼要採用微前端架構呢?

因此,微前端和微服務的本質都是關於去耦合。而只有當應用程序達到一定規模時,這纔開始變得更有意義。

三、如何實現微前端架構

微前端不是一個庫,是一種前端架構的設計思路,要實現微前端,本質上就是在運行時遠程加載應用。

實現微前端,有幾個思路,從構建的角度來看有兩種,編譯時構建微前端和運行時構建微前端:

從前後端責任分層來看,可以從前端或者後端來實現。

通過客戶端框架來實現

微前端通常由客戶端工具來支持實現(聽上去好有道理),有許多支持客戶端開發微前端的實現工具,包括:Piral,Open Components,qiankun,Luigi,Frint.js 等。其中 qiankun 是螞蟻金服開發的。

在客戶端還可以通過輔助庫的方式來實現,輔助庫可以爲共享依賴項,路由事件或不同的微前端及其生命週期來提供一些基礎架構。

下面的一個示例是通過諸如導入映射或打包特定塊等機制處理共享依賴關係。

相關的工具有 Webpack5 Module Federation,Siteless,Single SPA,Postal.js 等

微前端並非只能在客戶端來實現,類似於服務端渲染,同樣可以通過服務端來實現。

服務端微前端的支持工具有:Mosaic,PuzzleJs,Podium,Micromono 等。

四、運行時微前端的具體實現方式

iframes 是可以在 html 中嵌入另一個 HTML。下面就是用 iframe 實現微前端的一個例子:

<!DOCTYPE html>
<html>
<body>
<iframe src="http://localhost:3006" width="600" height="900">
  <p>Your browser does not support iframes.</p>
</iframe>
<iframe src="http://localhost:3007" width="600" height="900">
  <p>Your browser does not support iframes.</p>
</iframe>
</body>
</html>

如果不考慮體驗問題,iframe 幾乎是最完美的微前端解決方案了。iframe 提供了瀏覽器原生的隔離方案,不論是樣式隔離、js 隔離這類問題統統都能被完美解決。但它的最大問題也在於他的隔離性無法被突破,導致應用間上下文無法被共享,隨之帶來的開發體驗、產品體驗的問題。這裏的主要問題包括:

所以雖然使用 iframe 可以實現遠程加載的效果,但是因爲這些限制,很少會有應用會使用。

Nginx 路由

worker_processes 4;
events { worker_connections 1024; }
http {
    server {
        listen 80;
        root  /usr/share/nginx/html;
        include /etc/nginx/mime.types;
        location /app1 {
            try_files $uri app1/index.html;
        }
        location /app2 {
            try_files $uri app2/index.html;
        }
        location /app3 {
            try_files $uri app3/index.html;
        }
    }
}

無論你採用哪一種的微前端架構,Nginx 方向代理或者其它的 API 網關的解決方案都能夠提供方便靈活的後端路由功能。但是通過這種方式,需要定義一個通用可擴展的路由規則,否則當引入新的應用的時候,還需要修改 Nginx 的路由配置,那就很不方便了。

Webpack 5 Module Federation

Webpack5 的 Module Federation 是一個令人興奮的革新,它能夠很方便的支持微前端的構建。模塊聯合允許 JavaScript 應用程序從另一個應用程序動態加載代碼,並在此過程中能共享依賴關係。如果使用 Module Federation 的應用程序不具有聯合代碼所需的依賴關係,則 Webpack 將從該聯合構建源中下載缺少的依賴關係。

在 Module Federation 的上下文中,啓動代碼是一種將運行時代碼附加到遠程容器啓動序列的實施策略。這真的很有用,因爲通過 Hook 無法訪問 ModuleFederation 及其運行時,無法對其進行擴展或添加一行代碼,這些代碼可以像動態設置遠程容器的公共路徑那樣進行操作。這在普通的 webpack 應用程序中是微不足道的,但是在一個無法訪問的自定義運行時容器中卻很難做到,該容器爲模塊聯合遠程編排提供了動力。簡單來說,Module Federation 注入一段運行時的代碼來負責加載和編排遠程的應用代碼,並能夠管理和加載遠程應用的依賴。

下面是一個對應的例子:

module.exports = {
    mode: 'development',
    devServer: {
        port: 8080,
    },
    plugins: [
        new ModuleFederationPlugin({
            name: 'container',
            remotes: {
                microFrontEnd1: 'microFrontEnd1@http://localhost:8081/remoteEntry.js',
                microFrontEnd2: 'microFrontEnd2@http://localhost:8082/remoteEntry.js',            },
        })
    ]
};

上面的代碼是微前端的容器端的配置,容器負責加載其它遠程應用的代碼。這個例子裏,它加載了兩個遠程應用。

module.exports = {
    mode: 'development',
    devServer: {
        port: 8081,
    },
    plugins: [
        new ModuleFederationPlugin({
            name: 'microFrontEnd1',
            filename: 'remoteEntry.js',
            exposes: {
                './MicroFrontEnd1Index': './src/index',
            },
        }),
    ]
};

每一個微前端的 Webpack 配置如上。

利用 ModuleFederationPlugin,remote 可以用來加載遠端的應用,而 Expose 可以把自己的組件暴露爲遠端組件。

在 container 中,只需要調用以下的代碼來加載遠端組件。

import 'microFrontEnd1/MicroFrontEnd1Index';
import 'microFrontEnd2/MicroFrontEnd2Index';

Module Federation 的加載過程如上圖所示:

  1. localhost 加載 index.html

  2. main.js 是 Module Federation 的核心的編排代碼,負責加載遠程組件。

  3. remoteEntry.js 是 Module Federation 暴露的遠程組件的代碼

  4. src_ 是打包後的代碼,其中 bootstrap_js 是容器側的代碼,index_js 是微前端側的代碼。

Module Federation 實現了類似動態鏈接庫的能力,可以在運行時加載遠程代碼,遠程代碼本質上是一個加載在 window 上的全局變量,Module Federation 可以幫助解決依賴的問題。Javascrip 作爲上古語言,沒有提供依賴管理,導致留給各路大神各種發揮的空間。

Module Federation 的缺點就是依賴 Webpack 5,包直接掛載爲全局變量。

EMP 微前端是基於 Module Federation 的微前端解決方案。

Single SPA

單頁面應用是當今爲 Web 應用的主流,區別於傳統的多頁面應用,整個 SPA 只有一個頁面,其內容都是通過 Javascript 的功能來加載。

SPA 是一個 Web 應用程序,僅包含一個 HTML 頁面。提供動態更新,它允許在不刷新頁面的情況下與頁面進行交互。利用單頁應用程序,可以顯着降低服務器負載並提高加載速度,從而獲得更好的用戶體驗,因爲 SPA 僅在先前加載整個頁面時才按需導入數據。

除了開發複雜,對於 SEO 不友好,但頁面應用的最大技術缺陷是 URL 不適合共享,因爲 SPA 只有一個地址。

single-spa 是一個框架,用於將前端應用程序中的多個 JavaScript 微前端組合在一起。

使用 single-spa 構建前端可以帶來很多好處,例如:

single-spa 應用程序包含以下內容:

Single-SPA 註冊的應用程序擁有普通 SPA 所具有的所有功能,只是它沒有 HTML 頁面。SPA 包含許多已註冊的應用程序,每個應用程序都有其自己的框架。已註冊的應用程序具有其自己的客戶端路由和它們自己的框架 / 庫。它們呈現自己的 HTML,並且在安裝時有完全的自由去做他們想做的任何事情。掛載的概念是指已註冊的應用程序是否正在將內容放在 DOM 上。決定是否掛載已註冊應用程序的是其活動功能。每當未掛載已註冊的應用程序時,它都應保持完全休眠狀態直到掛載。

Single SPA 的樣例代碼如下:

1. 微前端代碼:

import React from "react";
import ReactDOM from "react-dom";
import singleSpaReact from "single-spa-react";
import Root from "./root.component";
const lifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: Root,
  errorBoundary(err, info, props) {
    // Customize the root error boundary for your microfrontend here.
    return null;
  },
});
export const { bootstrap, mount, unmount } = lifecycles;

Single SPA 的微前端是純的 JS 組件,不包含 HTML,需要通過容器來加載。

2. 容器的 Root Config

在容器側,需要通過 Import Map 或者 Webpack 來定義遠程組件,並註冊遠程應用。

{
  "imports": {
    "@naughty/root-config": "//localhost:9000/naughty-root-config.js",
    "@naughty/app": "//localhost:8500/naughty-app.js",
    "react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js",
    "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js"
  }
}

容器側的 HTML 文件使用 import map 來定義遠程依賴,其中 root-config 是編排代碼,負責遠程應用的註冊和加載。同時需要定義所有共享的依賴,這裏例子中是 react 和 react-dom

import { registerApplication, start } from "single-spa";
registerApplication({
  name: "@single-spa/welcome",
  app: () =>
    System.import(
      "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
    ),
  activeWhen: ["/welcome"],
});
registerApplication(
  '@naughty/app',
  () => System.import('@naughty/app'),
  location => location.pathname.startsWith('/app')
);
start({
  urlRerouteOnly: true,
});

在 root-config 中,我們註冊了兩個遠程應用,使用不同的 url 來加載。/welcome 會加載 welcome 應用,而 / app 會加載我們的 app 應用。

Single SPA 的核心是利用不同的 URL 路由來加載遠程組件,它可以和 Webpack(打包時構建依賴)或者 Import Map(運行時使用瀏覽器導入依賴)一起工作。注意,不要在你的微前端中混用兩種依賴機制。

Single SPA 還提供一個 layout 引擎,可以幫助你快速的構建微前端。

相比 Module Federation,Single SPA 的代碼和生命週期的管理更清楚,提供清晰的接口,缺點是共享的依賴需要手工通過 import map 來管理。

要做一個好的微前端因爲受限於瀏覽器和 JS 的一些特性,並不容易。除了我們今天分享的內容,還面臨着諸多的挑戰:如何解決 css/js 的衝突,使得組件和應用完全隔離;如何解決不同應用間的通信;如何處理路由;如何保證 UI 風格的統一等等。

五、微前端的問題和缺點

講了這麼多的優點和實現,那麼微前端是不是解決前端開發問題的銀彈呢?當然不是。所有的架構都是取捨和權衡,這個世界上並不存在銀彈,微前端架構和微服務一樣也存在他的弊端,單體架構未必就差。

1. 微前端的構建通常比較複雜,從工具,打包,到部署,微前端都是更爲複雜的存在,天下沒有免費的午餐,對於小型項目,它的成本太高。

2. 每個團隊可以使用不同的框架,這個聽上去很美,但是實際操作起來,除了要支持歷史遺留的應用,它的意義不大。同時也爲帶來體驗上的問題。可以遠程加載不同的框架代碼是一回事,把它們都用好是另一回事。

3. 性能上來看,如果優化得不好微前端的性能可能會存在問題,至少微前端框架是額外的一層加載。如果不同的微前端使用了不同的框架,那麼每一個框架都需要額外的加載。

微前端架構還在發展之中,本文提到的 iframe/nginx/module federation/single-spa 只是諸多解決方案中的一小部分,前端的發展變化和生態系統實在是豐富,其他的方案諸如 umd / 乾坤,Piral,open comonent 等等。當使用你也可以選擇標準的 Web Component 或者 ES Modules 來構建微前端,但是這些標準的瀏覽器支持不是特別好,這個是前端開發永遠的痛。(詛咒 IE)

大家對於微前端有什麼想法或者問題,歡迎一起討論。

關於作者:陶剛,Splunk 資深軟件工程師,架構師,畢業於北京郵電大學,現在在溫哥華負責 Splunk 機器學習雲平臺的開發,曾經就職於 SAP,EMC,Lucent 等企業,擁有豐富的企業應用軟件開發經驗,熟悉軟件開發的各種技術,平臺和開發過程,在商務智能,機器學習,數據可視化,數據採集,網絡管理等領域都有涉及。

關於 EAWorld:使能數字轉型,共創數智未來!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uujwipKW-gTef7Wd6MdsTQ