微前端的總結(jié)分享(演講稿版)

開(kāi)篇

時(shí)值當(dāng)下,作為一名合格的前端開(kāi)發(fā)人員,我相信你一定會(huì)有一個(gè)很明顯的感覺(jué):Web 業(yè)務(wù)日益復(fù)雜化和多元化,前端的職責(zé)越來(lái)越重要,戰(zhàn)場(chǎng)越來(lái)越多樣,應(yīng)用也越來(lái)越復(fù)雜,前端開(kāi)發(fā)已經(jīng)由WebPage 模式為主轉(zhuǎn)變?yōu)橐?WebApp 模式為主了——我們已經(jīng)迎來(lái)了一個(gè)“大前端”的時(shí)代。

隨之也會(huì)帶來(lái)許多工程化的問(wèn)題。例如:在一個(gè)相對(duì)長(zhǎng)的時(shí)間跨度下,隨著時(shí)間的推移,越來(lái)越復(fù)雜的前端項(xiàng)目,會(huì)變的越來(lái)越龐大。如何保證項(xiàng)目的可維護(hù)性,開(kāi)發(fā)質(zhì)量,開(kāi)發(fā)體驗(yàn)......

介紹微前端

微前端是在2016年底的 ThoughtWorks Technology Radar 被提出 ,將微服務(wù)這個(gè)被廣泛應(yīng)用于服務(wù)端的技術(shù)范式擴(kuò)展到前端領(lǐng)域。

Micro Frontends 網(wǎng)站對(duì)微前端的定義

簡(jiǎn)單通俗的來(lái)講就是:微前端可以使多個(gè)團(tuán)隊(duì)之間使用不同技術(shù)棧,然后在客戶端(瀏覽器)運(yùn)行時(shí)動(dòng)態(tài)組成一個(gè)完整的 SPA 應(yīng)用。(當(dāng)然也有在構(gòu)建時(shí)組合的方案,但并不符合微前端的核心思想,也不是主流微前端實(shí)現(xiàn)方式。)

總之,微前端是將微服務(wù)概念做了一個(gè)很好的延伸和實(shí)現(xiàn)。都是希望將某個(gè)單一的應(yīng)用,轉(zhuǎn)化為多個(gè)可以獨(dú)立運(yùn)行、獨(dú)立開(kāi)發(fā)、獨(dú)立部署、獨(dú)立維護(hù)的服務(wù)或者應(yīng)用,從而滿足業(yè)務(wù)快速變化以及多團(tuán)隊(duì)可并行開(kāi)發(fā)的需求。

微前端的應(yīng)用場(chǎng)景

  1. 大型單頁(yè)應(yīng)用。這類應(yīng)用的特點(diǎn)是系統(tǒng)體量較大,而且隨著業(yè)務(wù)上的功能升級(jí),項(xiàng)目體積還會(huì)不斷增大。

阿里云就是這個(gè)場(chǎng)景下微前端很好的實(shí)踐成果,采用微前端架構(gòu)方式可無(wú)限擴(kuò)展,其復(fù)雜度不會(huì)很明顯的增長(zhǎng)。(微前端最先提出來(lái)的主要原因,就是如何將巨石應(yīng)用解構(gòu))

  1. 系統(tǒng)重復(fù)模塊的復(fù)用。在多個(gè)獨(dú)立系統(tǒng)內(nèi)部可能會(huì)開(kāi)發(fā)一些重復(fù)度很高的功能,比如用戶管理,權(quán)限管理這些重復(fù)的功能。

微前端可以減少這些重復(fù)的開(kāi)發(fā)成本。(理想情況下,可以將產(chǎn)品原子化,然后根據(jù)業(yè)務(wù)場(chǎng)景的需求,實(shí)現(xiàn)不同應(yīng)用之間頁(yè)面級(jí)別的自由組合,且每個(gè)功能模塊都能單獨(dú)迭代)

  1. 遺留系統(tǒng)的兼容和擴(kuò)展。例:一個(gè)已經(jīng)存在了3,5年的項(xiàng)目,依賴版本落后,里面還存在著一些祖?zhèn)鞔a,因?yàn)榉N種原因項(xiàng)目不能及時(shí)升級(jí)。

微前端提供了一種增量升級(jí)的能力,在不重寫(xiě)原有系統(tǒng)的基礎(chǔ)之上,實(shí)施漸進(jìn)式重構(gòu)。對(duì)于新功能的開(kāi)發(fā)可以使用新的技術(shù),避免了繼續(xù)使用過(guò)時(shí)的技術(shù)。極大的降低長(zhǎng)期項(xiàng)目迭代維護(hù)的難度。

微前端的核心思想

  1. 技術(shù)無(wú)關(guān):微應(yīng)用之間可以選擇不同的技術(shù)棧。

  2. 環(huán)境獨(dú)立:為了達(dá)到高度解耦的目的,每個(gè)微應(yīng)用不應(yīng)當(dāng)共享運(yùn)行時(shí)環(huán)境,即使所有微應(yīng)用都使用了相同的框架,它們之間也應(yīng)該盡量避免依賴共享狀態(tài)或全局變量。(也就是應(yīng)用之間的 css 和 js 隔離)

  3. 原生優(yōu)先:優(yōu)先使用瀏覽器原生事件進(jìn)行通信。(如果確實(shí)必須跨應(yīng)用進(jìn)行通信,盡量讓通信內(nèi)容和方式變得簡(jiǎn)單,這樣能有效地減少微應(yīng)用之間的公共依賴)

  4. 獨(dú)立開(kāi)發(fā)、部署:微應(yīng)用可獨(dú)立開(kāi)發(fā),部署完成后主框架自動(dòng)完成同步更新

為什么不用 iframe

說(shuō)到微前端的方案,iframe 可以說(shuō)是最簡(jiǎn)單的微前端基石方案了,提供了瀏覽器原生的硬隔離方案,不論是 css 還是 js 隔離,然后正好也滿足了獨(dú)立運(yùn)行,開(kāi)發(fā),部署,維護(hù)?!珵槭裁此械默F(xiàn)代化微前端方案都不用iframe呢?

因?yàn)閕frame最大的特性同時(shí)也是它最大的問(wèn)題所在,它的隔離性無(wú)法被突破,導(dǎo)致應(yīng)用間上下文無(wú)法被共享,隨之帶來(lái)的開(kāi)發(fā)體驗(yàn)、產(chǎn)品體驗(yàn)的問(wèn)題。

  1. url 不同步。因?yàn)椴皇菃雾?yè)面應(yīng)用,瀏覽器刷新會(huì)導(dǎo)致 iframe url 狀態(tài)丟失、后退前進(jìn)按鈕無(wú)法使用。

  2. UI 不同步,DOM 結(jié)構(gòu)不共享。iframe 內(nèi)的彈窗無(wú)法應(yīng)用到整個(gè)大應(yīng)用中,只能在對(duì)應(yīng)的窗口內(nèi)展示。(iframe還不會(huì)自動(dòng)調(diào)節(jié)寬高)

  3. 全局上下文完全隔離,內(nèi)存變量不共享。就需要設(shè)計(jì) iframe 應(yīng)用之間的通信、數(shù)據(jù)同步等需求。(iframe 可以通過(guò) postMessage通信)

  4. 。每次子應(yīng)用進(jìn)入都是一次瀏覽器上下文重建、資源重新加載的過(guò)程,占用大量資源的同時(shí)也在極大地消耗資源。

第1個(gè)問(wèn)題可以解決,第4個(gè)問(wèn)題不是不能忽略。第2和3很難解決。
(騰訊公開(kāi)了一個(gè)基于 iframe 的微前端方案,但是要解決的問(wèn)題很多,目前還沒(méi)開(kāi)源)

現(xiàn)代微前端方案的選擇

微前端是一個(gè)技術(shù)應(yīng)用架構(gòu)體系,微前端架構(gòu)解決方案大概分為兩類場(chǎng)景(技術(shù)實(shí)現(xiàn)角度):

  • 單實(shí)例:即同一時(shí)刻,只有一個(gè)子應(yīng)用被展示,子應(yīng)用具備一個(gè)完整的應(yīng)用生命周期。通?;?url 的變化來(lái)做子應(yīng)用的切換?!J降?qiankun(qiankun2.0 時(shí)支持了多應(yīng)用并行)

  • 多實(shí)例:同一時(shí)刻可展示多個(gè)子應(yīng)用,子應(yīng)用更像是一個(gè)業(yè)務(wù)組件而不是應(yīng)用,可以說(shuō)是微應(yīng)用粒度的前端組件化。——去中心模式的 emp

emp

介紹:emp是YY業(yè)務(wù)中臺(tái)Web團(tuán)隊(duì)的微前端解決方案。emp 基于 Webpack 5 的 Module Federation(模塊聯(lián)邦)實(shí)現(xiàn),提供了在當(dāng)前應(yīng)用中遠(yuǎn)程加載其他服務(wù)器上應(yīng)用的能力(每個(gè)應(yīng)用之間都可以彼此分享資源),將多個(gè)獨(dú)立構(gòu)建應(yīng)用組成一個(gè)應(yīng)用程序。

優(yōu)點(diǎn):第三方依賴可共享,減少重復(fù)的代碼加載。

缺點(diǎn):無(wú)法涵蓋所有的框架,而且極度依賴于Webpack5。

因?yàn)閑mp主要解決的是業(yè)務(wù)的拆分,不是跨框架調(diào)用。算是對(duì)跨技術(shù)棧沒(méi)有高要求的微前端方案吧。

qiankun

介紹:qiankun 是基于 single-spa 封裝實(shí)現(xiàn)的(single-spa 做了完善的應(yīng)用加載邏輯,qiankun 的路由系統(tǒng)就是基于此實(shí)現(xiàn)的),與框架無(wú)關(guān)的微前端內(nèi)核。qiankun 算得上真正意義上的微前端,是螞蟻沉淀了自己的微前端方案并開(kāi)源的成果。

優(yōu)點(diǎn):真正做到了與技術(shù)棧無(wú)關(guān)。

缺點(diǎn):Css隔離方案并不完美。

我們最后會(huì)選擇 qiankun 作為我們的微前端解決方案。

我們的項(xiàng)目都基于umi,而 umi 提供了配套的 qiankun 插件 @umijs/plugin-qiankun,方便 umi 應(yīng)用通過(guò)修改配置的方式切換成微前端架構(gòu)系統(tǒng),幾乎零成本的接入(沒(méi)有使用umi的應(yīng)用,直接使用qiankun代碼其實(shí)也不需要改造太多,qiankun 工程侵入性很?。?,并且不再需要去關(guān)注各種過(guò)程中的技術(shù)細(xì)節(jié)。

至此,已經(jīng)介紹完了微前端的一些基本內(nèi)容。

但是想要進(jìn)階更高級(jí)的工程師,必然不能只停留于調(diào)用API,更要了解其底層設(shè)計(jì)思想和實(shí)現(xiàn)原理。
下面就簡(jiǎn)單深入下 qiankun。

qiankun在技術(shù)細(xì)節(jié)上的決策和基本實(shí)現(xiàn)原理

微前端方案涉及到的技術(shù)點(diǎn)
路由系統(tǒng)

基于Single-SPA實(shí)現(xiàn)。把所有應(yīng)用都注冊(cè)在基座上,通過(guò)基座應(yīng)用來(lái)監(jiān)聽(tīng)路由,按照路由規(guī)則來(lái)加載不同的應(yīng)用,來(lái)實(shí)現(xiàn)應(yīng)用間的解耦。

應(yīng)用加載

子應(yīng)用提供什么形式的資源作為渲染入口?

HTML Entry VS JS Entry

Js Entry的問(wèn)題在于:

  1. 需將子應(yīng)用的所有資源(包括 css、圖片等資源)打成一個(gè)Entry Script,Code Splitting 也無(wú)法應(yīng)用,資源加載速度變慢。

  2. 而且每一次子應(yīng)用發(fā)布,主應(yīng)用都需要重新配置打包,因?yàn)閖s/css地址的hash會(huì)變。

  3. 主應(yīng)用為子應(yīng)用預(yù)留的容器 id 還需與子應(yīng)用容器保持一致。

相比之下HTML Entry,子應(yīng)用地址只需配一次,子應(yīng)用的信息可以得到完整的保留。
缺點(diǎn):將子應(yīng)用資源解析的消耗留到了運(yùn)行時(shí)。

qiankun 采用 HTML Entry 方案替代 single-spa 的 JS Entry 加載子應(yīng)用的方案進(jìn)行優(yōu)化。(以達(dá)到像接入一個(gè) iframe 一樣簡(jiǎn)單的目的)
通過(guò) html 作為應(yīng)用入口,然后通過(guò)解析html從中提取 js 和 css 依賴下載,同時(shí)將 HTML Document 作為子節(jié)點(diǎn)塞到主應(yīng)用的容器中。(本質(zhì)上 HTML 充當(dāng)?shù)氖菓?yīng)用靜態(tài)資源表的角色)。

@umijs/plugin-qiankun 插件的應(yīng)用加載配置
應(yīng)用隔離(微前端方案中最關(guān)鍵的問(wèn)題)
Css隔離

CSS Module 簡(jiǎn)單高效,也更加智能化。問(wèn)題在于:雖然可以保證自己的子應(yīng)用做到隔離,但是無(wú)法保證依賴的第三方庫(kù)的全局樣式可以做到應(yīng)用之間的隔離。

下面介紹下qiankun的方案選擇:

  • Dynamic Stylesheet(動(dòng)態(tài)樣式表):
    動(dòng)態(tài)的加載和卸載樣式表。在應(yīng)用切出/卸載后,同時(shí)卸載掉其樣式表。

原理:瀏覽器會(huì)對(duì)所有的樣式表的插入、移除做整個(gè) CSSOM 的重構(gòu),從而保證了在一個(gè)時(shí)間點(diǎn)里,只有一個(gè)應(yīng)用的樣式表是生效的。(上面提到的 HTML Entry 方案則天生具備樣式隔離的特性,因?yàn)閼?yīng)用卸載后會(huì)直接移除去 HTML 結(jié)構(gòu),從而自動(dòng)移除了其樣式表)

問(wèn)題:可以確保子應(yīng)用之間的樣式?jīng)_突,但子應(yīng)用和主應(yīng)用之間的沖突是無(wú)法避免,只有通過(guò)手動(dòng)的方式確保,比如給主應(yīng)用所有樣式添加一個(gè)前綴。(但在螞蟻在實(shí)踐中,大多數(shù)主應(yīng)用可能只提供一個(gè)頭部,側(cè)邊欄的組件)

  • Shadow DOM(qiankun 2.0 版本支持,在開(kāi)啟 strictStylesolution時(shí),將采用 shadow DOM的方式進(jìn)行樣式隔離):
    將微應(yīng)用插入到 qiankun 創(chuàng)建好的 shadow Tree 中,微應(yīng)用的樣式(包括動(dòng)態(tài)插入的樣式)都會(huì)被掛載到這個(gè) shadow Host 節(jié)點(diǎn)下,最終整個(gè)應(yīng)用的所有 DOM 都會(huì)被繪制成一顆shadow tree。

原理:Shadow DOM內(nèi)部所有節(jié)點(diǎn)的樣式對(duì)樹(shù)外面的節(jié)點(diǎn)是無(wú)效的,因此微應(yīng)用的樣式只會(huì)作用在 Shadow Tree 內(nèi)部,自然就實(shí)現(xiàn)了樣式隔離。

問(wèn)題:一旦子應(yīng)用中出現(xiàn)運(yùn)行時(shí)越界跑到外面構(gòu)建 DOM 的場(chǎng)景,必定會(huì)導(dǎo)致構(gòu)建出來(lái)的 DOM 無(wú)法應(yīng)用子應(yīng)用樣式的情況。(例:像 antd modal 組件是動(dòng)態(tài)掛載到 document.body)

所以,qiankun 的 css 隔離方案不是特別完美。(還有一個(gè)實(shí)驗(yàn)性的樣式隔離特性 experimentalStyleIsolation ,會(huì)改寫(xiě)子應(yīng)用所添加的樣式為所有樣式規(guī)則增加一個(gè)特殊的選擇器規(guī)則來(lái)限定其影響范圍。但目前 @keyframes, @font-face, @import, @page 等規(guī)則不會(huì)支持)

Js隔離
  • proxySandbox 沙箱:
    解析 script 標(biāo)簽,用 with 語(yǔ)句包裹起來(lái),然后把 Proxy 包裝的 fakeWindow (window 上不可修改的屬性) 作為第一個(gè)參數(shù)傳進(jìn)去。
    (with做的是擴(kuò)展語(yǔ)句的作用域鏈,也就是將 Proxy(fakeWindow) 添加到作用域鏈的頂部)

這里稍微再深入一下,看下簡(jiǎn)化后的代碼,具體是如何實(shí)現(xiàn)的(源碼鏈接):

// zone.js將覆蓋Object.defineProperty
const rawObjectDefineProperty = Object.defineProperty;

function createFakeWindow(globalContext: Window) {
  const propertiesWithGetter = new Map<PropertyKey, boolean>();
  const fakeWindow = {} as FakeWindow;
  Object.getOwnPropertyNames(globalContext)
    .filter((p) => {
      //篩選不可修改的屬性描述
      const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
      return !descriptor?.configurable;
    })
    .forEach((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
      if (descriptor) {
        const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
        //對(duì)窗口對(duì)象的處理,使top/self/window屬性可配置和可寫(xiě),否則當(dāng)get trap(捕獲器)返回時(shí)會(huì)導(dǎo)致TypeError。
        if (p === 'top' || p === 'parent' || p === 'self' || p === 'window') {
          descriptor.configurable = true;
          if (!hasGetter) {
            descriptor.writable = true;
          }
        }
        if (hasGetter) propertiesWithGetter.set(p, true);
        // 凍結(jié)描述符以避免被zone.js修改
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
      }
    });
  return {
    fakeWindow,
    propertiesWithGetter,
  };
}

可以到 fakeWindow 就是 createFakeWindow 返回出來(lái)的,主要是從 window 上篩選出不可修改的屬性,偽造一個(gè) window 。

const useNativeWindowForBindingsProps = new Map<PropertyKey, boolean>([
  ['fetch', true],
  ['mockDomAPIInBlackList', process.env.NODE_ENV === 'test'],
]);
const nativeGlobal = new Function('return this')();
export default class ProxySandbox implements SandBox {
  name: string;
  type: SandBoxType;
  proxy: WindowProxy;
  globalContext: typeof window;
  sandboxRunning = true;
  //......

  /** 啟動(dòng)沙箱 */
  active() {
    if (!this.sandboxRunning) activeSandboxCount++;
    this.sandboxRunning = true;
  }
  /** 關(guān)閉沙箱 */
  inactive() {
    if (--activeSandboxCount === 0) {
      variableWhiteList.forEach((p) => {
        if (this.proxy.hasOwnProperty(p)) {
          delete this.globalContext[p];
        }
      });
    }
    this.sandboxRunning = false;
  }

  constructor(name: string, globalContext = window) {
    this.name = name;
    this.globalContext = globalContext;
    this.type = SandBoxType.Proxy;
    const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext);
    const hasOwnProperty = (key: PropertyKey) =>
      fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key);

    const proxy = new Proxy(fakeWindow, {
      set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
        if (this.sandboxRunning) {
          // 當(dāng)屬性在globalContext中存在時(shí),必須保持它的描述一致
          if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) {
            const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
            const { writable, configurable, enumerable } = descriptor!;
            if (writable) {
              //將修改對(duì)象屬性代理到 fakeWindow
              Object.defineProperty(target, p, {
                configurable,
                enumerable,
                writable,
                value,
              });
            }
          } else {
            target[p] = value;
          }
          return true;
        }
        // 在 strict-mode 下,Proxy 的 handler.set 返回 false 會(huì)拋出 TypeError,在沙箱卸載的情況下應(yīng)該忽略錯(cuò)誤
        return true;
      },
      get: (target: FakeWindow, p: PropertyKey): any => {
        if (p === Symbol.unscopables) return unscopables;
        if (p === 'window' || p === 'self') {
          //防止使用 window.window 或 window.self 去逃離沙箱環(huán)境獲取到真正 window
          return proxy;
        }
        if (p === 'globalThis') {
          // 劫持 globalWindow 訪問(wèn)與 globalThis 關(guān)鍵字
          return proxy;
        }
        if (p === 'top' || p === 'parent') {
          // 如果你的主應(yīng)用程序在 iframe 上下文中, 允許些 props 離開(kāi)沙箱
          if (globalContext === globalContext.parent) {
            //如果一個(gè)窗口沒(méi)有父窗口,則它的 parent 屬性為自身的引用.
            return proxy;
          }
          return (globalContext as any)[p];
        }
        if (p === 'hasOwnProperty') {
          //先查找 fakeWindow,后查找 globalContext 對(duì)象自身屬性中是否具有指定的屬性
          return hasOwnProperty;
        }
        if (p === 'document') {
          //將返回的 子應(yīng)用的 document 更正為主應(yīng)用的 document
          return document;
        }
        if (p === 'eval') {
          return eval;
        }
        const value = propertiesWithGetter.has(p)
          ? (globalContext as any)[p]
          : p in target
          ? (target as any)[p]
          : (globalContext as any)[p];

        // 一些dom api必須綁定到本機(jī)窗口,否則會(huì)導(dǎo)致異常報(bào)錯(cuò):'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation'
        const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;
        //getTargetValue 主要對(duì) window.console、window.atob 這類的檢測(cè)處理,不然微應(yīng)用中調(diào)用時(shí)會(huì)拋出 Illegal invocation 異常
        return getTargetValue(boundTarget, value);
      },
      //......
    });
    this.proxy = proxy;
  }
}

主要看下 Proxy 攔截操作中的 get 和 set 做了什么 :

setter:這里就是把對(duì) window 屬性的修改,全局屬性的操作代理到 fakeWindow 上。

getter:就是對(duì)于屬性值的獲取做了一些限制,防止逃離沙箱環(huán)境獲取到真正的 window。

這樣在執(zhí)行代碼時(shí),所有全局變量就會(huì)被掛載到了 fakeWindow 上,而不是真正的全局 window 上,當(dāng)應(yīng)用被卸載時(shí),對(duì)應(yīng)的 Proxy 會(huì)被清除,所以不會(huì)導(dǎo)致全局污染。

  • SnapshotSandbox(qiankun 2.0 版本支持):在不支持 proxy 特性的瀏覽器(IE11)上,使用快照模式來(lái)保證兼容性。

也簡(jiǎn)單看下代碼的實(shí)現(xiàn):

function iter(obj: typeof window, callbackFn: (prop: any) => void) {
  for (const prop in obj) {
    if (obj.hasOwnProperty(prop) || prop === 'clearInterval') {
      callbackFn(prop);
    }
  }
}
/**
 * 基于 diff 方式實(shí)現(xiàn)的沙箱,用于不支持 Proxy 的低版本瀏覽器
 */
 export default class SnapshotSandbox implements SandBox {
  proxy: WindowProxy;
  name: string;
  type: SandBoxType;
  sandboxRunning = true;
  private windowSnapshot!: Window;
  private modifyPropsMap: Record<any, any> = {};

  constructor(name: string) {
    this.name = name;
    this.proxy = window;
    this.type = SandBoxType.Snapshot;
  }
  /** 啟動(dòng)沙箱 */
  active() {
    // 記錄當(dāng)前快照
    this.windowSnapshot = {} as Window;
    iter(window, (prop) => {
      this.windowSnapshot[prop] = window[prop];
    });
    // 恢復(fù)之前的變更
    Object.keys(this.modifyPropsMap).forEach((p: any) => {
      window[p] = this.modifyPropsMap[p];
    });
    this.sandboxRunning = true;
  }
  /** 關(guān)閉沙箱 */
  inactive() {
    this.modifyPropsMap = {};
    iter(window, (prop) => {
      if (window[prop] !== this.windowSnapshot[prop]) {
        // 記錄變更,恢復(fù)環(huán)境
        this.modifyPropsMap[prop] = window[prop];
        window[prop] = this.windowSnapshot[prop];
      }
    });
    this.sandboxRunning = false;
  }
}

大致思路:在加載應(yīng)用前,把 window 上所有的屬性保存起來(lái)(拍攝快照)。應(yīng)用被卸載時(shí),再恢復(fù) window 上的所有屬性,所以也可以防止全局污染。
但是當(dāng)頁(yè)面同時(shí)存在多個(gè)頁(yè)面實(shí)例時(shí),就無(wú)法把它們隔離開(kāi)來(lái)了。所以快照策略并不支持多實(shí)例模式。

內(nèi)部通信
  • 基于 props 以單向數(shù)據(jù)流的方式傳遞給子應(yīng)用。(主要解決父子應(yīng)用的強(qiáng)耦合時(shí)的通信)
  • initGlobalState(state) 定義全局狀態(tài),并返回通信方法

基座會(huì)創(chuàng)建一個(gè)內(nèi)部包含通信的變量和兩個(gè)用來(lái)修改和監(jiān)聽(tīng)變量值的方法。
下面看下簡(jiǎn)化后的源碼,去除了console.warn和console.error:

//cloneDeep深度拷貝
import { cloneDeep } from 'lodash';
import type { OnGlobalStateChangeCallback, MicroAppStateActions } from './interfaces';

let globalState: Record<string, any> = {};
const deps: Record<string, OnGlobalStateChangeCallback> = {};
// 觸發(fā)全局監(jiān)聽(tīng)
function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {
  Object.keys(deps).forEach((id: string) => {
    if (deps[id] instanceof Function) {
      deps[id](cloneDeep(state), cloneDeep(prevState));
    }
  });
}
export function initGlobalState(state: Record<string, any> = {}) {
  if (state === globalState) { } else {
    const prevGlobalState = cloneDeep(globalState);
    globalState = cloneDeep(state);
    emitGlobal(globalState, prevGlobalState);
  }
  return getMicroAppStateActions(`global-${+new Date()}`, true);
}
export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {
  return {
    onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
      //訂閱
      deps[id] = callback;
      const cloneState = cloneDeep(globalState);
      //是否立即觸發(fā)
      if (fireImmediately) {
        callback(cloneState, cloneState);
      }
    },
    setGlobalState(state: Record<string, any> = {}) {
      if (state === globalState) {
        return false;
      }
      const changeKeys: string[] = [];
      //記錄之前的 globalState
      const prevGlobalState = cloneDeep(globalState);
      //生成新的 globalState
      globalState = cloneDeep(
        Object.keys(state).reduce((_globalState, changeKey) => {
          if (isMaster || _globalState.hasOwnProperty(changeKey)) {
            changeKeys.push(changeKey);
            return Object.assign(_globalState, { [changeKey]: state[changeKey] });
          }
          return _globalState;
        }, globalState),
      );
      if (changeKeys.length === 0) {
        return false;
      }
      //發(fā)布
      emitGlobal(globalState, prevGlobalState);
      return true;
    },
    // 注銷該應(yīng)用下的依賴
    offGlobalStateChange() {
      delete deps[id];
      return true;
    },
  };
}

簡(jiǎn)單來(lái)講就是標(biāo)準(zhǔn)的訂閱-發(fā)布模式,就是通過(guò)訂閱全局變量的修改狀態(tài)來(lái)實(shí)現(xiàn)通信。(具體源碼可以看這里)

資源加載

在微前端方案中存在一個(gè)典型的問(wèn)題:
如果子應(yīng)用比較多,就會(huì)存在之間重復(fù)依賴的場(chǎng)景。解決方案是在主應(yīng)用中主動(dòng)的依賴基礎(chǔ)框架,然后子應(yīng)用保守的將基礎(chǔ)的依賴處理掉,但是,這個(gè)機(jī)制里存在一個(gè)問(wèn)題,如果子應(yīng)用中既有react15又有react16,這時(shí)主應(yīng)用該如何做?

螞蟻的方案是在主應(yīng)用中維護(hù)一個(gè)語(yǔ)義化版本的映射表,在運(yùn)行時(shí)分析當(dāng)前的子應(yīng)用,最后可以決定真實(shí)運(yùn)行時(shí)真正的消費(fèi)到哪一個(gè)基礎(chǔ)框架的版本,可以實(shí)現(xiàn)真正運(yùn)行時(shí)的依賴系統(tǒng),也能解決子應(yīng)用多版本共存時(shí)依賴去從的問(wèn)題,能確保最大程度的依賴復(fù)用。

結(jié)尾

講了這么多,最后簡(jiǎn)單提一下之后可能會(huì)遇到的微前端問(wèn)題。

當(dāng)一個(gè)單體應(yīng)用被拆成若干個(gè),其維護(hù)成本也相應(yīng)增加。
如何管理多個(gè)版本,如何復(fù)用公共組件等,導(dǎo)致管理版本變得復(fù)雜,依賴關(guān)系也極其復(fù)雜。

還有,如果應(yīng)用拆分的粒度過(guò)小,開(kāi)發(fā)體驗(yàn)也會(huì)不太友好。
應(yīng)用如果是不同的人開(kāi)發(fā)的話,如果需求跨多個(gè)業(yè)務(wù),此時(shí)需要與多個(gè)開(kāi)發(fā)應(yīng)用者合作,溝通成本大大增加。

最后

趕在春節(jié)放假前,進(jìn)行微前端的技術(shù)分享,這篇可以看作是演講稿。內(nèi)容少點(diǎn)的可以看 PPT

參考學(xué)習(xí)鏈接:
https://martinfowler.com/articles/micro-frontends.html
https://swearer23.github.io/micro-frontends/

https://developer.aliyun.com/article/742576?spm=a2c6h.14164896.0.0.6e633edbLg3STt
https://zhuanlan.zhihu.com/p/78362028
https://zhuanlan.zhihu.com/p/131022025
https://zhuanlan.zhihu.com/p/355419817
https://www.yuque.com/kuitos/gky7yw/nwgk5a
https://www.yuque.com/zhuanjia/oeisq4/vt6kto

https://zhuanlan.zhihu.com/p/97226980
https://zhuanlan.zhihu.com/p/356225293

https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容