微前端

前言

在軟件開發中,逐漸出現了類,模塊化,組件化,設計模式等來解耦和拆分咱們的代碼,使得代碼更易讀,易維護。而微前端架構其實也是一種新的思想來幫助咱們更好的拆分一些如今方式沒法解決的問題而已。html

什麼是微前端

  • 微服務的架構思想在前端的映射和落地
  • 針對複雜且大型的web前端的總體架構和組織結構問題,將單體的前端拆分紅更小,更簡單的模塊,使其能夠獨立開發,測試和部署,最後將其整合到一塊兒。

設計理念

相似與操做系統,將系統的實現與系統的基本操做規則區分開來。將核心功能模塊化,劃分紅幾個獨立的進程,各自運行,全部的服務進程都運行在不一樣的地址空間,讓服務各自獨立。
設計理念.png前端

落地到瀏覽器

落地到瀏覽器.png

如上圖所示:微前端落地到瀏覽器,瀏覽器將承載一個html頁面,在頁面中安裝,啓動相應的服務。web

微前端的核心價值

  • 不受技術棧的約束,可使用任何技術棧來研發獨立的模塊
  • 自動同步更新,獨立開發,部署,測試
  • 將大型web應用拆分,使其更易維護,測試,同時使模塊與模塊以前更加獨立,解決部分緊耦合的問題

微前端帶來的問題

  • 部署
  • 服務拆分標準
  • 拆的太多會過於分散,拆的太少會過於密集

拆分方式

我的感受除去微前端的一些技術實現,它主要難點就在於如何拆分應用,在拆分的同時還需結合團隊規模等問題:npm

  • 按功能維度拆分
  • 按業務邏輯拆分
  • 按前端路由拆分

應用場景

並非全部應用都適合使用微前端架構,在一個簡單的單體應用中使用,反而拔苗助長。
微前端主要應用在大型的互聯網應用,該應用可能具有幾個特色:系統龐大到不少人去開發,頁面數量達到某個量級,在這種狀況下可能會致使系統難以維護,代碼量逐漸增大,同時也使得協做方面難以管控,包括測試,迴歸,而且在工程化方面編譯顯的耗時。總結有如下幾點狀況bootstrap

  • 業務愈來愈多
  • 組件愈來愈多
  • 文件愈來愈多
  • 打包編譯速度愈來愈慢
  • 開發啓動速度愈來愈慢
  • 定位文件愈來愈慢

微前端落地的幾種實現方式

  • npm:子系統以NPM包的形式發佈,打包構建的時候集成到主系統一塊兒打包發佈。
  • iframe:子工程之間徹底獨立,以iframe的方式集成到主系統,這樣也能使用不能的技術棧去實現。
  • 使用現有框架:single-spa,Mooa,qiankun

微前端基本原理

基本原理.png

和微服務同樣,微前端的獨立部署是關鍵。減小服務間的耦合性,不管前端代碼部署在哪裏,每一個微前端都有本身持續交付pipeline,進行構建,測試,部署到生產環境中。最後將多個子系統集成到主系統中。設計模式

微前端架構

微前端架構.png

基座工程:數組

  • 路由控制層: 根據url變化來調其不一樣的子應用
  • 應用註冊: 註冊每一個子應用的信息
  • 生命週期管理: 獲得每一個應用的生命週期,如安裝,卸載等管理
  • 應用加載器:加載對應的子應用
  • 服務發現:獲得每一個子應用的服務,入口文件等信息

子應用:promise

  • 子應用之間相互隔離並獨立運行
  • Manifest:記錄的該應用入口文件,地址等信息
  • 生命週期:暴露生命週期函數工基座工程管理

single-spa的生命週期管理

生命週期.png

  • not_loaded: 還未加載,默認狀態
  • load_source_code: 加載模塊中
  • not_bootstrapped: 加載完成,可是還未啓動
  • bootstrapping: 正在執行的bootstrap生命週期
  • not_mounted: 未裝載
  • mounting:正在裝載
  • mounted: 已裝載
  • updating: 更新
  • unloading: 清楚加載
  • unmounting: 卸載

路由控制層

路由控制層主要監控路由的變動,經過路由變動來控制子系統是否須要加載。子系統路由發生變化首先會有主系統攔截路由變動時間,決定是否加載子系統,若是路由不須要切換子系統,則將該事件交還給子系統處理。
屏幕快照 2020-05-10 下午9.14.38.png瀏覽器

應用註冊

應用註冊.png

應用註冊其實就是相似於平時的帳號註冊,須要填寫一些基本信息,而在微前端中所指的應用註冊主要是指app名稱,以及對應子系統配置文件的url。而在single-spa中registerApplication主要包含三個參數,appName,app,activeWhen緩存

  • appName:app名稱
  • activeWhen:返回true則加載應用
  • app:加載子應用

    在主應用中會註冊多個子應用,而這些子應用的信息及狀態會進行保存和管理。

模塊加載器

模塊加載器.png

註冊對應的子系統,當路由規則匹配到某個子系統的時候會先去加載該子系統的manifest文件來獲取該子系統的信息,經過該文件去加載對應的子系統。

single-spa實現思路

實現思路.png

single-spa執行隊列有兩個入口,一是經過監聽路由的變化,二是register函數。
每次觸發會先判斷是否已啓動,若是未啓動則執行loadApps去加載須要加載的app。若是已啓動則調用performAppChanges函數去mount app。
在執行期間,若是有新的app進來也就是隊列發生了變動,會將新的app緩存待執行完當前的再循環執行下一次,這個操做由finishUpAndReturn這個函數內部作判斷來完成。

single-spa主要有一下幾個模塊:

  • applcation : app的註冊,生命週期過濾,任務超時等處理函數
  • lifecycles:應用的生命週期管理
  • navigation:監聽全局路由變化,執行隊列的核心函數。
  • parcels:掛在parcel的核心函數,返回parcel的各個生命週期鉤子

下面來大體看一下幾個核心函數大體的實現

registerApplication

該函數接收4個參數:

  • appNameOrConfig:app名稱或一個包含這4個參數的對象,若是是對象的話則下面三個參數就不用傳了
  • appOrLoadAppFn:子系統的bundle代碼以及生命週期函數
  • activeWhen:什麼時候激活子應用去mount,返回一個boolean
  • customProps:自定義傳遞給子系統的屬性
export function registerApplication(
  appNameOrConfig,
  appOrLoadApp,
  activeWhen,
  customProps
) {
  const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );

  apps.push(
    assign(
      {
        loadErrorTime: null,
        status: NOT_LOADED,
      },
      registration
    )
  );
  
  reroute();
}
  • sanitizeArguments主要爲了格式化參數,以支持另一種對象的方式傳遞
  • 而後將該註冊的應用添加到一個數組中,並給該應用一個初始化的狀態爲NOT_LOADED
  • 最後執行reroute函數

路由監聽

function urlReroute() {
  reroute([], arguments);
}

window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
  • 經過監聽hashChange和popstate事件來執行reroute函數
const originalAddEventListener = window.addEventListener;

window.addEventListener = function(eventName, fn) {
    if (typeof fn === "function") {
      if (
        routingEventsListeningTo.indexOf(eventName) >= 0 &&
        !find(capturedEventListeners[eventName], listener => listener === fn)
      ) {
        capturedEventListeners[eventName].push(fn);
        return;
      }
    }

    return originalAddEventListener.apply(this, arguments);
  };
  • 改寫addEventListener監聽函數,使每次路由變化事件先由single-spa接管,而後再執行原監聽函數交還給系統路由。
  • 這裏會先將監聽函數保存在capturedEventListeners[eventName]數組中,在調用完reroute以後再去執行capturedEventListeners這個隊列裏面的事件函數,這樣可以保證single-spa每次先執行

reroute

reroute.png

let appChangeUnderway = false,
peopleWaitingOnAppChange = [];

function reroute(pendingPromises = [], eventArguments) {
  .......
}

函數接收兩個參數:

  • pendingPromises:執行reroute期間,再次調用reroute函數所產生的app
  • eventArguments:路由監聽事件的event參數

下面都是reroute函數裏面的代碼:

if (appChangeUnderway) {
    return new Promise((resolve, reject) => {
      peopleWaitingOnAppChange.push({
        resolve,
        reject,
        eventArguments
      });
    });
  }
  • 若是reroute在執行期間再次被調用,則會先將數據緩存到peopleWaitingOnAppChange當中,當reroute當前次調用結束後遞歸執行,以保證執行順序
let wasNoOp = true;

  if (isStarted()) {
    appChangeUnderway = true;
    return performAppChanges();
  } else {
    return loadApps();
  }
  • wasNoOp: 等於true的時候表示app沒有發生變動,也就是沒有發生狀態的變化。
  • isStarted: 判斷是否已經啓動
  • 若是啓動了則執行performAppChanges,並將appChangeUnderway=true
  • 不然執行loadApps
function loadApps() {
    return Promise.resolve().then(() => {
      const loadPromises = getAppsToLoad().map(toLoadPromise);

      if (loadPromises.length > 0) {
        wasNoOp = false;
      }

      return (
        Promise.all(loadPromises)
          .then(callAllEventListeners)
          .then(() => [])
          .catch(err => {
            callAllEventListeners();
            throw err;
          })
      );
    });
  }
  • getAppsToLoad:根據一些條件來篩選出須要加載的app
  • toLoadPromise:將每一個app都封裝到一個promise中,返回一個數組
  • 經過promise.All執行每一個promise,主要判斷每一個app是否有bootstrap,mount,unmount生命週期,有則將app狀態修改成NOT_BOOTSTRAPPED,沒有則更改成SKIP_BECAUSE_BROKEN
  • 執行callAllEventListeners函數,來調用攔截下來的原生事件。
function performAppChanges() {
    return Promise.resolve().then(() => {
      const unloadPromises = getAppsToUnload().map(toUnloadPromise);

      const unmountUnloadPromises = getAppsToUnmount()
        .map(toUnmountPromise)
        .map(unmountPromise => unmountPromise.then(toUnloadPromise));

      const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
      if (allUnmountPromises.length > 0) {
        wasNoOp = false;
      }

      const unmountAllPromise = Promise.all(allUnmountPromises);

      const appsToLoad = getAppsToLoad();

      /* We load and bootstrap apps while other apps are unmounting, but we
       * wait to mount the app until all apps are finishing unmounting
       */
      const loadThenMountPromises = appsToLoad.map(app => {
        return toLoadPromise(app)
          .then(toBootstrapPromise)
          .then(app => {
            return unmountAllPromise.then(() => toMountPromise(app));
          });
      });
      if (loadThenMountPromises.length > 0) {
        wasNoOp = false;
      }

      /* These are the apps that are already bootstrapped and just need
       * to be mounted. They each wait for all unmounting apps to finish up
       * before they mount.
       */
      const mountPromises = getAppsToMount()
        .filter(appToMount => appsToLoad.indexOf(appToMount) < 0)
        .map(appToMount => {
          return toBootstrapPromise(appToMount)
            .then(() => unmountAllPromise)
            .then(() => toMountPromise(appToMount));
        });
      if (mountPromises.length > 0) {
        wasNoOp = false;
      }
      return unmountAllPromise
        .catch(err => {
          callAllEventListeners();
          throw err;
        })
        .then(() => {
          /* Now that the apps that needed to be unmounted are unmounted, their DOM navigation
           * events (like hashchange or popstate) should have been cleaned up. So it's safe
           * to let the remaining captured event listeners to handle about the DOM event.
           */
          callAllEventListeners();

          return Promise.all(loadThenMountPromises.concat(mountPromises))
            .catch(err => {
              pendingPromises.forEach(promise => promise.reject(err));
              throw err;
            })
            .then(finishUpAndReturn);
        });
    });
  }
  • unloadPromises: 先拿到須要unload的app,而後封裝到一個執行unload的promise中
  • unmountUnloadPromise: 拿到須要unmount的app,封裝到一個promise中,再將unmountPromise的結果封裝到uploadPromise中
  • allUnmountPromises: 將unmount和unload合併
  • unmountAllPromise: 執行allUnmountPromises,這裏不等待執行完成直接執行下面代碼
  • getAppsToLoad:加載須要load的app
  • loadThenMountPromises: 將須要load的app封裝到loadPromise中,執行該promise,完成後封裝到BootstrapPromise中,再接着執行BootStrapPromise,最後等上面unmountAllPromise的執行完畢後將app封裝到toMountPromise中去
  • mountPromises:拿到全部須要mount的app,返回的是一個promise
  • 最後在unmountAllPromise執行完後調用callAllEventListeners,而後去掛在app,mount完後執行finishUpAndReturn來看隊列中是否還有等待的任務,遞歸執行reroute

總結

能不用則不用!!!!!

相關文章
相關標籤/搜索