主頁 > 軟體設計 > 微前端框架qiankun之原理與實戰

微前端框架qiankun之原理與實戰

2021-02-23 12:08:19 軟體設計

目錄

  • 一、微前端概述
    • 1. 基本原理
    • 2. 微前端的主要優勢
    • 3. 當前微前端方案的一些缺點
  • 二、qiankun與single-spa實作原理
    • 1. single-spa實作原理
      • (1). 路由問題
      • (2). 應用入口
      • (3). 應用加載
    • 2. qiankun實作原理
      • (1). 應用加載
      • (2). js隔離
      • (3). css隔離
      • (4). 應用通信
  • 三、qiankun實戰
    • 1. 搭建基座應用
    • 2. 搭建子應用
  • 總結

一、微前端概述

1. 基本原理

在正式介紹qiankun之前,我們需要知道,它是基于另一個微前端框架:single-spa 搭建的,qiankun在它的基礎上進行了封裝和增強,使其更加易用,本文我們會先從single-spa入手,一步步介紹qiankun的實作原理,在講解兩者之前,我們先來了解一下何為微前端,

微前端的概念借鑒自后端的微服務,主要是為了解決大型工程在變更、維護、擴展等方面的困難而提出的,目前主流的微前端方案包括以下幾個:

  1. iframe
  2. 基座模式,主要基于路由分發,qiankunsingle-spa就是基于這種模式
  3. 組合式集成,即單獨構建組件,按需加載,類似npm包的形式
  4. EMP,主要基于Webpack5 Module Federation
  5. Web Components

嚴格來講,這些方案都不算是完整的微前端解決方案,它們只是用于解決微前端中運行時容器的相關問題,除了運行時容器,一套完整的微前端方案還需要解決版本管理、質量管控、配置下發、線上監控、灰度發布、安全監測等與工程和平臺相關的問題,而這些問題中的大部分作業目前仍處于探索階段,
在這里插入圖片描述
iframe:是傳統的微前端解決方案,基于iframe標簽實作,技術難度低,隔離性和兼容性很好,但是性能和使用體驗比較差,多用于集成第三方系統;
基座模式:主要基于路由分發,即由一個基座應用來監聽路由,并按照路由規則來加載不同的應用,以實作應用間解耦;
組合式集成:把組件單獨打包和發布,然后在構建或運行時組合;
EMP:基于Webpack5 Module Federation,一種去中心化的微前端實作方案,它不僅能很好地隔離應用,還可以輕松實作應用間的資源共享和通信;
Web Components:是官方提出的組件化方案,它通過對組件進行更高程度的封裝,來實作微前端,但是目前兼容性不夠好,尚未普及,

總的來說,iframe主要用于簡單并且性能要求不高的第三方系統;組合式集成目前主要用于前端組件化,而不是微前端;基座模式EMPWeb Components是目前主流的微前端方案,

本文我們主要對qiankun所基于的基座模式進行介紹,它的主要思路是將一個大型應用拆分成若干個更小、更簡單,可以獨立開發、測驗和部署的子應用,然后由一個基座應用根據路由進行應用切換,

如果以前端組件的概念作類比,我們可以把每個被拆分出的子應用看作是一個應用級組件,每個應用級組件專門實作某個特定的業務功能(如商品管理、訂單管理等),這里實際上談到了微前端拆分的原則:即以業務功能為基本單元,經過拆分后,整個系統的結構也發生了變化:
在這里插入圖片描述

左側是傳統大型單頁應用的前端架構,所有模塊都在一個應用內,由應用本身負責路由管理,是應用分發路由的方式;而右側是基座模式下的系統架構,各個子應用互不相關,單獨運行在不同的服務上,由基座應用根據路由選擇加載哪個應用到頁面內,是路由分發應用的方式,這種方式使得各個模塊的耦合性大大降低,而微前端需要解決的主要問題就是如何拆分和組織這些子應用,

為了讓這些拆分出的子應用在一個單頁面內協同作業,我們需要一個“管理者”應用,這就是我們上面說的基座應用,也叫主應用,基座應用一般是用戶最終訪問的應用,它會根據定義的規則,將不同的應用加載到頁面內供用戶使用,當然,這種架構下的每個子應用也具備單獨訪問的能力,

為了配合基座應用,子應用必須經過一些改造,向外暴露出相應的生命周期鉤子,以便基座應用加載和卸載,實際上,一個典型的基于vue-router的Vue應用與這種架構存在著很大的相似性:
在這里插入圖片描述

在典型的Vue應用中,各個組件當然都必須基于Vue撰寫;但是在微前端架構中,各個子應用可以基于不同的技術框架,這也是它最大的優勢之一,這是因為各個子應用是獨立編譯和部署的,而基座應用是在運行時動態加載的子應用,由于在啟動子應用時已經經歷過編譯階段,所以基座應用加載的都是原生JavaScript代碼,自然與子應用所用的技術框架無關(qiankun甚至能加載jQuery撰寫的頁面),

概念性地講,在微前端架構中,各個子應用將一些特定的業務功能封裝在一個業務黑箱中,只對外暴露少量生命周期方法;基座應用根據路由地址變化,動態地加載對應的業務黑箱,并將其渲染到指定的占位DOM元素上,與Vue應用一樣,微前端也可以一次加載多個業務黑箱,這稱為多實體模式(類似于vue-router的命名視圖),

2. 微前端的主要優勢

  1. 技術兼容性好,各個子應用可以基于不同的技術架構
  2. 代碼庫更小、內聚性更強
  3. 便于獨立編譯、測驗和部署,可靠性更高
  4. 耦合性更低,各個團隊可以獨立開發,互不干擾
  5. 可維護性和擴展性更好,便于區域升級和增量升級

關于技術兼容性,由于在被基座應用加載前, 所有子應用已經編譯成原生代碼輸出,所以基座應用可以加載各類技術堆疊撰寫的應用;由于拆分后應用體積明顯變小,并且每個應用只實作一個業務模塊,因此其內聚性更強;另外子應用本身也是完整的應用,所以它可以獨立編譯、測驗和部署;關于耦合性,由于各個子應用只負責各自的業務模塊,所以耦合性很低,非常便于獨立開發;關于可維護性和擴展性,由于拆分出的應用都是完整的應用,因此專門升級某個功能模塊就成為了可能,并且當需要增加模塊時,只需要創建一個新應用,并修改基座應用的路由規則即可,

不過這種微前端方案仍然存在缺點:

3. 當前微前端方案的一些缺點

  1. 子應用間的資源共享能力較差,使得專案總體積變大
  2. 需要對現有代碼進行改造(指的是未按照微前端形式撰寫的舊工程)

首先,子應用之間保持較高的獨立性,反而使一些公共資源不便于共享,雖然大型第三方庫可以通過externals的方式上傳到cdn,但像一些工具函式,通用業務組件等則不易共享,這就使得專案整體體積反而變大,由于改造成本不高,代碼改造通常算不上很嚴重的問題,但仍存在一定的代價,

介紹完微前端的基本概念,我們就來看一下qiankunsingle-spa的核心實作原理,

二、qiankun與single-spa實作原理

既然qiankun是基于single-spa的,那么我們就來看qiankunsingle-spa在架構中分別扮演了什么角色,

一般來說,微前端需要解決的問題分為兩大類:

  1. 應用的加載與切換
  2. 應用的隔離與通信

應用的加載與切換需要解決的問題包括:路由問題、應用入口、應用加載;應用的隔離與通信需要解決的問題包括:js隔離、css樣式隔離、應用間通信

single-spa很好地解決了路由應用入口兩個問題,但并沒有解決應用加載問題,而是將該問題暴露出來由使用者實作(一般可以用system.js或原生script標簽來實作);qiankun在此基礎上封裝了一個應用加載方案(即import-html-entry),并給出了js隔離css樣式隔離應用間通信三個問題的解決方案,同時提供了預加載功能

借助single-spa提供的能力,我們只能把不同的應用加載到一個頁面內,但是很難保證這些應用不會互相干擾,而qiankun為我們解決了這些后顧之憂,使得它成為一個更加完整的微前端運行時容器,
在這里插入圖片描述
接下來我們借助部分原始碼,分別來看single-spaqiankun是如何一步步實作運行時容器的,

1. single-spa實作原理

我們已經知道,single-spa解決的是應用的加載與切換相關的問題,下面就來看完整的實作程序,

(1). 路由問題

single-spa是通過監聽hashChangepopState這兩個原生事件來檢測路由變化的,它會根據路由的變化來加載對應的應用,相關的代碼可以在single-spasrc/navigation/navigation-events.js 中找到:

...
// 139行
if (isInBrowser) {
  // We will trigger an app change for any routing events.
  window.addEventListener("hashchange", urlReroute);
  window.addEventListener("popstate", urlReroute);
...
// 174行,劫持pushState和replaceState
  window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
  );
  window.history.replaceState = patchedUpdateState(
    window.history.replaceState,
    "replaceState"
  );

我們看到,single-spa在檢測到發生hashChangepopState事件時,會執行urlReroute函式,這里封裝了它對路由問題的解決方案,另外,它還劫持了原生的pushStatereplaceState事件,關于為什么劫持這兩個事件,我們后面會介紹,我們先來看urlReroute函式做了什么:

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

這個函式只是呼叫了reroute函式,而reroute函式就是single-spa解決路由問題的核心邏輯,下面我們來分析一下它的實作,由于該函式較長,我們截取其中體現核心思路的代碼進行分析:
src/navigation/reroute.js

export function reroute(pendingPromises = [], eventArguments) {
  ...
  // getAppChanges會根據路由改變應用的狀態,狀態包含4類
  // 待清除、待卸載、待加載、待掛載
  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();
  ...
  // 如果應用已啟動,則呼叫performAppChanges加載和掛載應用
  // 否則,只加載未加載的應用
  if (isStarted()) {
    appChangeUnderway = true;
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
    return performAppChanges();
  } else {
    appsThatChanged = appsToLoad;
    return loadApps();
  }
  ...
  function performAppChanges() {
    return Promise.resolve().then(() => {
      // 1. 派發應用更新前的自定義事件
      // 2. 執行應用暴露出的生命周期函式
      // appsToUnload -> unload生命周期鉤子
      // appsToLoad -> 執行加載方法
      // appsToUnmount -> 卸載應用,并執行對應生命周期鉤子
      // appsToMount -> 嘗試引導和掛載應用
    })
  }
  ...
}

這里就是single-spa解決路由問題的主要邏輯,主要是以下幾步:

  1. 根據傳入的引數activeWhen判斷哪個應用需要加載,哪個應用需要卸載或清除,并將其push到對應的陣列
  2. 如果應用已經啟動,則進行應用加載或切換,針對應用的不同狀態,直接執行應用自身暴露出的生命周期鉤子函式即可,
  3. 如果應用未啟動,則只去下載appsToLoad中的應用,

總的來看,當路由發生變化時,hashChangepopState會觸發,這時single-spa會監聽到,并觸發urlReroute;接著它會呼叫reroute,該函式正確設定各個應用的狀態后,直接通過呼叫應用所暴露出的生命周期鉤子函式即可,當某個應用被推送到appsToMount后,它的mount函式會被呼叫,該應用就會被掛載;而推送到appsToUnmount中的應用則會呼叫其unmount鉤子進行卸載,
在這里插入圖片描述
上面我們還提到,single-spa除了監聽hashChangepopState兩個事件外,還劫持了原生的pushStatereplaceState兩個方法,這是為什么呢?

這是因為像scroll-restorer這樣的第三方組件可能會在頁面滾動時,通過呼叫pushStatereplaceState,將滾動位置記錄在state中,而頁面的url實際上沒有變化,這種情況下,single-spa理論上不應該去重新加載應用,但是由于這種行為會觸發頁面的hashChange事件,所以根據上面的邏輯,single-spa會發生意外多載,

為了解決這個問題,single-spa允許開發者手動設定是否只對url值的變化監聽,而不是只要發生hashChangepopState就去重新加載應用,我們可以像下面一樣在啟動single-spa時添加urlRerouteOnly引數:

singleSpa.start({
  urlRerouteOnly: true,
});

這樣除非url發生了變化,否則pushStatepopState不會導致應用多載,

(2). 應用入口

single-spa采用的是協議入口,即只要實作了single-spa的入口協議規范,它就是可加載的應用,single-spa的規范要求應用入口必須暴露出以下三個生命周期鉤子函式,且必須回傳Promise,以保證single-spa可以注冊回呼函式:

  1. bootstrap
  2. mount
  3. unmount
    在這里插入圖片描述

bootstrap用于應用引導,基座應用會在子應用掛載前呼叫它,舉個應用場景,假如某個子應用要掛載到基座應用內idapp的節點上:

new Vue({
  el: '#app',
  ...
})

但是基座應用中當前沒有idapp的節點,我們就可以在子應用的bootstrap鉤子內手動創建這樣一個節點并插入到基座應用,子應用就可以正常掛載了,所以它的作用就是做一些掛載前的準備作業,

mount用于應用掛載,就是一般應用中用于渲染的邏輯,即上述的new Vue陳述句,我們通常會把它封裝到一個函式里,在mount鉤子函式中呼叫,

unmount用于應用卸載,我們可以在這里呼叫實體的destroy方法手動卸載應用,或清除某些記憶體占用等,

除了以上三個必須實作的鉤子外,single-spa還支持非必須的loadunloadupdate等,分別用于加載、卸載和更新應用,

那么只使用single-spa如何進行子應用加載呢?

(3). 應用加載

實際上single-spa并沒有提供自己的解決方案,而是將它開放出來,由開發者提供,

我們看一下基于system.js如何啟動single-spa

<script type="systemjs-importmap">
  {
    "imports": {
      "app1": "http://localhost:8080/app1.js",
      "app2": "http://localhost:8081/app2.js",
      "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"
    }
  }
</script>
... // system.js的相關依賴檔案
<script>
(function(){
  // 加載single-spa
  System.import('single-spa').then((res)=>{
    var singleSpa = res;
    // 注冊子應用
    singleSpa.registerApplication('app1',
      () => System.import('app1'),
      location => location.hash.startsWith(`#/app1`);
    );
    singleSpa.registerApplication('app2',
      () => System.import('app2'),
      location => location.hash.startsWith(`#/app2`);
    );
    // 啟動single-spa
    singleSpa.start();
  })
})()
</script>

我們在呼叫singleSpa.registerApplication注冊應用時提供的第二個引數就是加載這個子應用的方法,如果需要加載多個js,可以使用多個System.import連續匯入,single-spa會呼叫這個函式,下載子應用代碼并分別呼叫其bootstrapmount方法進行引導和掛載,

從這里我們也可以看到single-spa的弊端,首先我們必須手動實作應用加載邏輯,挨個羅列子應用需要加載的資源,這在大型專案里是十分困難的(特別是使用了檔案名hash時);另外它只能以js檔案為入口,無法直接以html為入口,這使得嵌入子應用變得很困難,也正因此,single-spa不能直接加載jQuery應用,

single-spastart方法也很簡單:

export function start(opts) {
  started = true;
  if (opts && opts.urlRerouteOnly) {
    setUrlRerouteOnly(opts.urlRerouteOnly);
  }
  if (isInBrowser) {
    reroute();
  }
}

先是設定started狀態,然后設定我們上面說到的urlRerouteOnly屬性,接著呼叫reroute,開始首次加載子應用,加載完第一個應用后,single-spa就時刻等待著hashChangepopState事件的觸發,并執行應用的切換,

以上就是single-spa的核心原理,從上面的介紹中不難看出,single-spa只是負責把應用加載到一個頁面中,至于應用能否協同作業,是很難保證的,而qiankun所要解決的,就是協同作業的問題,

2. qiankun實作原理

(1). 應用加載

上面我們說到了,single-spa提供的應用加載方案是開放式的,針對上面我們談到的幾個弊端,qiankun進行了一次封裝,給出了一個更完整的應用加載方案,qiankun的作者將其封裝成了npm插件import-html-entry

該方案的主要思路是允許以html檔案為應用入口,然后通過一個html決議器從檔案中提取js和css依賴,并通過fetch下載依賴,于是在qiankun中你可以這樣配置入口:

const MicroApps = [{
  name: 'app1',
  entry: 'http://localhost:8080',
  container: '#app',
  activeRule: '/app1'
}]

qiankun會通過import-html-entry請求http://localhost:8080,得到對應的html檔案,決議內部的所有scriptstyle標簽,依次下載和執行它們,這使得應用加載變得更易用,我們看一下這具體是怎么實作的,

import-html-entry暴露出的核心介面是importHTML,用于加載html檔案,它支持兩個引數:

  1. url,要加載的檔案地址,一般是服務中html的地址
  2. opts,配置引數

url不必多說,opts如果是一個函式,則會替換默認的fetch作為下載檔案的方法,此時其回傳值應當是Promise;如果是一個物件,那么它最多支持四個屬性:fetchgetPublicPathgetDomaingetTemplate,用于替換默認的方法,這里暫不詳述,

我們截取該函式的主要邏輯:

export default function importHTML(url, opts = {}) {
  ...
  // 如果已經加載過,則從快取回傳,否則fetch回來并保存到快取中
  return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
		.then(response => readResAsString(response, autoDecodeResponse))
		.then(html => {
		  // 對html字串進行初步處理
		  const { template, scripts, entry, styles } = 
		    processTpl(getTemplate(html), assetPublicPath);
		  // 先將外部樣式處理成行內樣式
		  // 然后回傳幾個核心的腳本及樣式處理方法
		  return getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({
				template: embedHTML,
				assetPublicPath,
				getExternalScripts: () => getExternalScripts(scripts, fetch),
				getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
				execScripts: (proxy, strictGlobal, execScriptsHooks = {}) => {
					if (!scripts.length) {
						return Promise.resolve();
					}
					return execScripts(entry, scripts, proxy, {
						fetch,
						strictGlobal,
						beforeExec: execScriptsHooks.beforeExec,
						afterExec: execScriptsHooks.afterExec,
					});
				},
			}));
		});
}

省略的部分主要是一些引數預處理,我們從return陳述句開始看,具體程序如下:

  1. 檢查是否有快取,如果有,直接從快取中回傳
  2. 如果沒有,則通過fetch下載,并字串化
  3. 呼叫processTpl進行一次模板決議,主要任務是掃描出外聯腳本和外聯樣式,保存在scriptsstyles
  4. 呼叫getEmbedHTML,將外聯樣式下載下來,并替換到模板內,使其變成內部樣式
  5. 回傳一個物件,該物件包含處理后的模板,以及getExternalScriptsgetExternalStyleSheetsexecScripts等幾個核心方法
    在這里插入圖片描述

processTpl主要基于正則運算式對模板字串進行決議,這里不進行詳述,我們來看getExternalScriptsgetExternalStyleSheetsexecScripts這三個方法:
getExternalStyleSheets

export function getExternalStyleSheets(styles, fetch = defaultFetch) {
  return Promise.all(styles.map(styleLink => {
	if (isInlineCode(styleLink)) {
	  // if it is inline style
	  return getInlineCode(styleLink);
	} else {
	  // external styles
	  return styleCache[styleLink] ||
	  (styleCache[styleLink] = fetch(styleLink).then(response => response.text()));
	}
  ));
}

遍歷styles陣列,如果是行內樣式,則直接回傳;否則判斷快取中是否存在,如果沒有,則通過fetch去下載,并進行快取,

getExternalScripts與上述程序類似,

execScripts是實作js隔離的核心方法,我們放在下一部分js隔離里講解,

通過呼叫importHTML方法,qiankun可以直接加載html檔案,同時將外聯樣式處理成內部樣式表,并且決議出JavaScript依賴,更重要的是,它獲得了一個可以在隔離環境下執行應用腳本的方法execScripts

(2). js隔離

上面我們說到,qiankun通過import-html-entry,可以對html入口進行決議,并獲得一個可以執行腳本的方法execScriptsqiankun引入該介面后,首先為該應用生成一個window的代理物件,然后將代理物件作為引數傳入介面,以保證應用內的js不會對全域window造成影響,由于IE11不支持proxy,所以qiankun通過快照策略來隔離js,缺點是無法支持多實體場景,

我們先來看基于proxy的js隔離是如何實作的,首先看import-html-entry暴露出的介面,照例我們只截取核心代碼:
execScripts

export function execScripts(entry, scripts, proxy = window, opts = {}) {
  ... // 初始化引數
  return getExternalScripts(scripts, fetch, error)
	.then(scriptsText => {
	  // 在proxy物件下執行腳本的方法
	  const geval = (scriptSrc, inlineScript) => {
	    const rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
	    const code = getExecutableScript(scriptSrc, rawCode, proxy, strictGlobal);
        (0, eval)(code);
        afterExec(inlineScript, scriptSrc);
	  };
	  // 執行單個腳本的方法
      function exec (scriptSrc, inlineScript, resolve) { ... }
      // 排期函式,負責逐個執行腳本
      function schedule(i, resolvePromise) { ... }
      // 啟動排期函式,執行腳本
      return new Promise(resolve => schedule(0, success || resolve));
    });
});

這個函式的關鍵是定義了三個函式:gevalexecschedule,其中實作js隔離的是geval函式內呼叫的getExecutableScript函式,我們看到,在調這個函式時,我們把外部傳入的proxy作為引數傳入了進去,而它回傳的是一串新的腳本字串,這段新的字串內的window已經被proxy替代,具體實作邏輯如下:

function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) {
	const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${scriptSrc}\n`;

	// 通過這種方式獲取全域 window,因為 script 也是在全域作用域下運行的,所以我們通過 window.proxy 系結時也必須確保系結到全域 window 上
	// 否則在嵌套場景下, window.proxy 設定的是內層應用的 window,而代碼其實是在全域作用域運行的,會導致閉包里的 window.proxy 取的是最外層的微應用的 proxy
	const globalWindow = (0, eval)('window');
	globalWindow.proxy = proxy;
	// TODO 通過 strictGlobal 方式切換切換 with 閉包,待 with 方式坑趟平后再合并
	return strictGlobal
		? `;(function(window, self, globalThis){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`
		: `;(function(window, self, globalThis){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`;
}

在這里插入圖片描述
核心代碼就是由兩個矩形框起來的部分,它把決議出的scriptText(即腳本字串)用with(window){}包裹起來,然后把window.proxy作為函式的第一個引數傳進來,所以with語法內的window實際上是window.proxy

這樣,當在執行這段代碼時,所有類似var name = '張三'這樣的陳述句添加的全域變數name,實際上是被掛載到了window.proxy上,而不是真正的全域window上,當應用被卸載時,對應的proxy會被清除,因此不會導致js污染,而當你配置webpack的打包型別為lib時,你得到的介面大概如下:

var jquery = (function(){})();

如果你的應用內使用了jquery,那么這個jquery物件就會被掛載到window.proxy上,不過如果你在代碼內直接寫window.name = '張三'來生成全域變數,那么qiankun就無法隔離js污染了,

import-html-entry實作了上述能力后,qiankun要做的就很簡單了,只需要在加載一個應用時為其初始化一個proxy傳遞進來即可:
proxySandbox.ts

export default class ProxySandbox implements SandBox {
  ...
  constructor(name: string) {
    ...
    const proxy = new Proxy(fakeWindow, {
      set () { ... },
      get () { ... }
    }
  }
}

每次加載一個應用,qiankun就初始化這樣一個proxySandbox,傳入上述execScripts函式中,

在IE下,由于proxy不被支持,并且沒有可用的polyfill,所以qiankun退而求其次,采用快照策略實作js隔離,它的大致思路是,在加載應用前,將window上的所有屬性保存起來(即拍攝快照);等應用被卸載時,再恢復window上的所有屬性,這樣也可以防止全域污染,但是當頁面同時存在多個應用實體時,qiankun無法將其隔離開,所以IE下的快照策略無法支持多實體模式,

關于快照模式我們就不詳細介紹了,接下來看一下qiankun如何實作css樣式隔離,

(3). css隔離

目前qiankun主要提供了兩種樣式隔離方案,一種是基于shadowDom的;另一種則是實驗性的,思路類似于Vue中的scoped屬性,給每個子應用的根節點添加一個特殊屬性,用作對所有css選擇器的約束,

開啟樣式隔離的語法如下:

registerMicroApps({
  name: 'app1',
  ...
  sandbox: {
    strictStyleIsolation: true
    // 實驗性方案,scoped方式
    // experimentalStyleIsolation: true
  },
})

當啟用strictStyleIsolation時,qiankun將采用shadowDom的方式進行樣式隔離,即為子應用的根節點創建一個shadow root,最終整個應用的所有DOM將形成一棵shadow tree,我們知道,shadowDom的特點是,它內部所有節點的樣式對樹外面的節點無效,因此自然就實作了樣式隔離,

但是這種方案是存在缺陷的,因為某些UI框架可能會生成一些彈出框直接掛載到document.body下,此時由于脫離了shadow tree,所以它的樣式仍然會對全域造成污染,

此外qiankun也在探索類似于scoped屬性的樣式隔離方案,可以通過experimentalStyleIsolation來開啟,這種方案的策略是為子應用的根節點添加一個特定的隨機屬性,如:

<div
  data-qiankun-asiw732sde
  id="__qiankun_microapp_wrapper__"
  data-name="module-app1"
>

然后為所有樣式前面都加上這樣的約束:

.app-main { 
  字體大小:14 px ; 
}
// ->
div[data-qiankun-asiw732sde] .app-main {  
  字體大小:14 px ; 
}

經過上述替換,這個樣式就只能在當前子應用內生效了,雖然該方案已經提出很久了,但仍然是實驗性的,因為它不支持@ keyframes,@ font-face,@ import,@ page(即不會被重寫),

(4). 應用通信

一般來說,微前端中各個應用之前的通信應該是盡量少的,而這依賴于應用的合理拆分,反過來說,如果你發現兩個應用間存在極其頻繁的通信,那么一般是拆分不合理造成的,這時往往需要將它們合并成一個應用,

當然了,應用間存在少量的通信是難免的,qiankun官方提供了一個簡要的方案,思路是基于一個全域的globalState物件,這個物件由基座應用負責創建,內部包含一組用于通信的變數,以及兩個分別用于修改變數值和監聽變數變化的方法:setGlobalStateonGlobalStateChange

以下代碼用于在基座應用中初始化它:

import { initGlobalState, MicroAppStateActions } from 'qiankun';

const initialState = {};
const actions: MicroAppStateActions = initGlobalState(initialState);

export default actions;

這里的actions物件就是我們說的globalState,即全域狀態,基座應用可以在加載子應用時通過propsactions傳遞到子應用內,而子應用通過以下陳述句即可監聽全域狀態變化:

actions.onGlobalStateChange (globalState, oldGlobalState) {
  ...
}

同樣的,子應用也可以修改全域狀態:

actions.setGlobalState(...);

在這里插入圖片描述

此外,基座應用和其他子應用也可以進行這兩個操作,從而實作對全域狀態的共享,這樣各個應用之間就可以通信了,這種方案與Redux和Vuex都有相似之處,只是由于微前端中的通信問題較為簡單,所以官方只提供了這樣一個精簡方案,關于其實作原理這里不再贅述,感興趣的可以去看一下原始碼,

關于qiankun的核心原理到這里就介紹完了,下面我們看一下如果使用qankun搭建一個微前端專案,

三、qiankun實戰

我們上面說到,qiankun是基于基座模式的,所以它必然有一個基座應用(主應用),來管理各個子應用的加載和卸載,我們以一個基于Vue的管理系統為例,來看如何搭建一個微前端專案,

目前主流的管理系統大都是基于以下頁面結構:
在這里插入圖片描述
左側menu是選單區,點擊不同的選單會在content內容區域加載不同的選單頁,而頂部的header一般是一些用戶資訊,右側的help一般是幫助區域,

一般來說,微前端的子應用不宜拆分得過細,應該以業務領域進行大致拆分,所以我們將menuheaderhelp這三個區域放在基座應用中實作;然后我們將每個一級選單用一個微應用來實作,這樣,當我們打開不同一級選單下的頁面時,就會發生應用切換;而在同一個一級選單下進行選擇時,不會切換應用,

1. 搭建基座應用

有了思路之后,我們就可以開始搭建基座應用了,首先我們需要先搭建好一個常規的Vue應用,并撰寫好以上三個區域對應的組件,此時的根組件結構大概如下:

<template>
  <div class="container">
    <div class="app-header">
      <prime-header></prime-header>
    </div>
    <div class="app-menu">
      <prime-menu></prime-menu>
    </div>
    <div class="app-content">
      <div id="root-view"></div>
    </div>
    <div class="help-taggle">
      <prime-help></prime-menu>
    </div>
  </div>
</template>

我們在內容區域寫了這樣一個標簽<div id="root-view"></div>,它就像vue-router中的占位組件<router-view>一樣,用于后續加載子應用,

除了內容區域外,我們應該已經搭建好了一個完整的Vue應用,現在我們需要對它進行改造,使得它成為一個基座應用,

首先,我們需要安裝qiankun

yarn add qiankun

然后我們創建一個檔案,來定義各個子應用的入口:
micro-app.js

const microApps: Array<any> = [{
  name: 'module-app1',
  entry: 'https://app1.example.com',
  activeRule: '/app1',
  container: '#root-view',
  sandbox: {
    strictStyleIsolation: true // 開啟樣式隔離
  }
}, {
  name: 'module-shop',
  entry: 'https://app2.example.com',
  activeRule: '/app2',
  container: '#root-view',
  sandbox: {
    strictStyleIsolation: true // 開啟樣式隔離
  }
}, ...
];

export default microApps;

需要注意的是,這里所有子應用的name都不能重復,因為qiankun需要基于它進行快取,

entry屬性是定義子應用入口,基于qiankun的應用一般直接寫子應用的入口html地址即可,

activeRule則是定義何時加載該子應用,該引數最侄訓被映射成為single-spa中的activeWhen引數,用于匹配url,它們的格式也是一樣的,可以是字串,或正則運算式,或是一個回傳boolean值的函式,這里子應用1的activeRule定義為/app1,就意味著當url的hash部分是以/app1開頭時,基座應用就需要去加載應用app1了,一般來說我們可能更傾向于使用history模式,此時它直接匹配的就是域名后面的字串,要啟用history模式,只需要進行如下配置:

const router = new VueRouter({
  mode: 'history',
  routes
});

container是定義子應用的加載位置,還記得我們之前留出的id為router-view的標簽嗎,這里我們就是指定它為加載位置,整個子應用會加載為它的子樹,

sandbox我們上面已經提到了,用于設定是否啟用沙箱模式,默認為true,這里我們將其設定為物件,可以同時開啟子應用的樣式隔離,

有了這個陣列,我們接著來看如何使用它,

實際上這只需要兩步,第一是注冊所有的微應用,第二是啟動qiankun,這兩步都有對應的api,我們現在希望在加載左側選單欄的時候去注冊和啟動qiankun,于是我們可以在對應的組件內這樣寫:
menu.vue

<script>
// 引入qiankun注冊子應用和啟動的介面函式
import { registerMicroApps, start } from 'qiankun';
// 引入微應用入口配置
import microApps from './modules/micro-apps';
@Component
export default class PrimeMenu extends Vue {
  async created () {
    registerMicroApps(microApps, {
      // 注冊一些全域生命周期鉤子,如進行日志列印,如果不需要可以不傳
      beforeMount () {},
    });
    // 啟動qiankun,并開啟預加載
    start({ prefetch: true });
  }
}
</script>

到了這里其實基座應用已經搭建好了,以history模式為例,根據qiankunsingle-spa的實作原理,當我們點擊選單的時候,我們可能會通過以下兩種方式修改地址欄的地址:

window.history.pushState({}, '', '/app1/overview');
// 或者這樣
window.location.href = '/app1/overview';

這兩種方式都會觸發hashChange事件,而qiankun借助single-spa所系結的監聽事件,會偵測到地址變化,從而執行single-spareroute函式;隨后qiankun通過import-html-entry提供的能力,去下載app1對應的html檔案,并從中決議出所依賴的腳本和樣式檔案,qiankun會將其封裝在一個沙箱中,執行這些腳本并添加樣式表,從而渲染出子應用,經過這些步驟,應用app1就會像一個原生的組件一樣被渲染到基座應用的占位節點內,

當點擊其他選單時,同樣會觸發hashChange事件,single-spa會重新執行reroute,卸載原子應用,加載新的子應用,

2. 搭建子應用

除了需要處理基座應用,子應用也需要進行一定的改造,

假設我們現在已經啟動了一個完整的概況頁應用,地址為https://app1.example.com,我們現在希望這個頁面可以像原生組件一樣直接渲染到基座應用的content內容區域,那么按照single-spa的應用入口協議,我們必須在子應用內暴露出三個必要的生命周期鉤子函式:
main.js

...
let instance = null;
// 定義渲染函式
function render (props: any = {}) {
  instance = new Vue({
    router: router(props),
    store,
    render: h => h(App)
  }).$mount(`#app`);
}
// bootstrap引導函式
export async function bootstrap () {
  console.log('[vue] vue app bootstraped');
}
// 掛載函式
export async function mount (props: any) {
  console.log('[vue] props from main framework', props);
  // storeTest(props);
  render(props);
}
// 卸載函式
export async function unmount () {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
}
// 在qiankun環境下,修正加載路徑
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// 在非qiankun環境下,直接執行渲染
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

我們看到,不同于之前的寫法,這里使用export向外暴露出了三個生命周期函式,

__webpack_public_path__賦值的陳述句是為了防止子應用資源路徑例外,由qiankun官方推薦設定,

最后面的判斷陳述句,檢查了window.__POWERED_BY_QIANKUN__是否存在,這是為了檢查當前子應用是否正在被qiankun框架所加載;如果不是被qiankun加載的,那么直接執行渲染即可,

現在的子應用已經符合了single-spa入口協議規范,理論上它可以被single-spa所加載,但這還不夠,因為按照正常的打包規則,應用最侄訓被打包成為一個匿名的立即執行函式,single-spa仍然無法從中決議出這些生命周期鉤子,所以接下來我們需要修改webpack配置,讓它以lib的格式打包:
vue.config.js

module.exports = {
  ...
  configureWebpack: {
    output: {
      // 把子應用打包成 umd 庫格式
      library: `micro-app1-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_micro-app1`
    }
  },
  // 以下配置可以修復一些字體檔案加載路徑問題
  chainWebpack: config => {
    config
      .plugin('html')
      .tap(args => {
        args[0].name = name;
        return args
      });

    config.module
      .rule("fonts")
      .test(/.(ttf|otf|eot|woff|woff2)$/)
      .use("url-loader")
      .loader("url-loader")
      .tap(options => {
        options = {
          // limit: 10000,
          name: '/static/fonts/[name].[ext]',
        }
        return options
      })
      .end();
  },
}

umd格式的應用會向外暴露指定的生命周期鉤子函式,便于single-spa決議,jsonpFunction配置是webpack打包之后保存在window中的key,只需保證各個子應用不一致即可,其余配置主要是解決一些加載路徑問題,

完成以上配置后,分別啟動基座應用和子應用,當訪問基座應用并點擊概況選單時,地址欄的url會發生變化,qiankun就會自動去子應用所在的服務下載html檔案,并決議出腳本和樣式進行渲染,于是這個子應用就會展示在圖中的content內容區域了,至此,一個簡單的qiankun應用就搭建完畢了,

總結

qiankunsingle-spa的原理算不上特別復雜,使用起來也很方便,并且qiankun還支持加載jQuery技術堆疊的頁面,對舊系統可以說是非常友好的,

從目前微前端的發展來看,webpack5的Module Federation可能會成為一個焦點功能,用于解決微前端應用間的資源共享問題(EMP已經做到了這一點,qiankun的維護者也表示會集成這一功能),當然了,隨著Web Components功能的不斷增強,也許瀏覽器本身就是微前端最完美的運行時容器,除了運行時容器,本文開篇提到的版本管理、質量管控、配置下發、線上監控、灰度發布、安全監測等,也必將成為微前端領域接下來要重點解決的問題,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/262534.html

標籤:其他

上一篇:去了位元組跳動,才知道年薪 40w 的 90 后 Android 開發者有這么多?

下一篇:初識SpringCloud 整體架構

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more