<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【微前端】qiankun 到底是個(gè)什么鬼

          共 16465字,需瀏覽 33分鐘

           ·

          2021-08-15 13:43

          點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)

          回復(fù)1,加入高級(jí)Node交流群

          前言

          在上一篇文章【微前端】single-spa 到底是個(gè)什么鬼[1] 聊到了 single-spa 這個(gè)框架僅僅實(shí)現(xiàn)了子應(yīng)用的生命周期的調(diào)度以及 url 變化的監(jiān)聽。微前端的一個(gè)特點(diǎn)都沒有實(shí)現(xiàn),嚴(yán)格來(lái)說算不上微前端框架。

          今天就來(lái)聊一個(gè)真正的微前端框架:qiankun[2]。同樣地,本文不會(huì)教大家怎么實(shí)現(xiàn)一個(gè) Demo,因?yàn)楣俜降?Github 已經(jīng)有一個(gè)很好的 Demo[3] 了,如果你覺得官網(wǎng)的 Demo 太復(fù)雜了,也可以看我自己實(shí)現(xiàn)的小 Demo[4]

          qiankun 到底做了什么

          首先,qiankun 并不是單一個(gè)框架,它在 single-spa 基礎(chǔ)上添加更多的功能。以下是 qiankun 提供的特性:

          ?實(shí)現(xiàn)了子應(yīng)用的加載,在原有 single-spa 的 JS Entry 基礎(chǔ)上再提供了 HTML Entry?樣式和 JS 隔離?更多的生命周期:beforeMount, afterMount, beforeUnmount, afterUnmount?子應(yīng)用預(yù)加載?全局狀態(tài)管理?全局錯(cuò)誤處理

          接下來(lái)不會(huì)一個(gè)特性一個(gè)特性地講,因?yàn)檫@樣會(huì)很無(wú)聊,講完你也只能知道這是個(gè)啥,不能深入了解是怎么來(lái)的。所以我更愿意聊一下這些特性是怎么來(lái)的,它們是怎么被想到的。

          多入口

          先復(fù)習(xí)一下 single-spa 是怎么注冊(cè)子應(yīng)用的:

          singleSpa.registerApplication(
          'appName',
          () => System.import('appName'),
          location => location.pathname.startsWith('appName'),
          );

          可以看到 single-spa 采用 JS Entry 的方式接入微應(yīng)用,也即:輸出一個(gè) JS,然后 bootstrap, mount, unmount 函數(shù)。

          但是事件并沒有這么簡(jiǎn)單:我們項(xiàng)目一般都會(huì)將靜態(tài)資源放到 CDN 上來(lái)加速。為了不受緩存的影響,我們還會(huì)將 JS 文件命名成 contenthash 的亂碼文件名: jlkasjfdlkj.jalkjdsflk.js。這樣一來(lái),每次子應(yīng)用一發(fā)布,入口 JS 文件名肯定又要改了,導(dǎo)致主應(yīng)用引入的 JS url 又得改了。麻煩!

          打包成單個(gè) JS 文件的另一個(gè)問題就是打包的優(yōu)化都沒了:按需加載、首屏資源加載優(yōu)化、css 獨(dú)立打包等優(yōu)化措施全 ???。

          很多時(shí)候,子應(yīng)用一般都已經(jīng)是線上的應(yīng)用了,比如 https://abcd.com。微前端融合多個(gè)子應(yīng)用本質(zhì)上不就是融合多個(gè) HTML 嘛?那為什么不給你子應(yīng)用的 HTML,主應(yīng)用就自動(dòng)接入收工了呢?操作起來(lái)應(yīng)該和在 <iframe/> 和插入 src 是一樣的才對(duì)味。

          這種通過提供 HTML 入口來(lái)接入子應(yīng)用的方式就叫 HTML Entry。 qiankun 的一大亮點(diǎn)就是提供了 HTML Entry,在調(diào)用 qiankun 的注冊(cè)子應(yīng)用函數(shù)時(shí)可以這么寫:

          registerMicroApps([
          {
          name: 'react app', // 子應(yīng)用名
          entry: '//localhost:7100', // 子應(yīng)用 html 或網(wǎng)址
          container: '#yourContainer', // 掛載容器選擇器
          activeRule: '/yourActiveRule', // 激活路由
          },
          ]);

          start(); // Go

          用起來(lái)毫不費(fèi)力,只需要在 JS 入口加上 single-spa 的生命周期鉤子,再發(fā)布就可以直接接入了。

          import-html-entry

          然而,HTML Entry 并不是給個(gè) HTML 的 url 就可以直接接入整個(gè)子應(yīng)用這么簡(jiǎn)單了。子應(yīng)用的 HTML 文件就是一堆亂七八糟的標(biāo)簽文本。<link><style><script> 得處理吧?要寫正則表達(dá)式吧?頭要禿了吧?

          所以 qiankun 的作者自己也寫了一個(gè)專門處理 HTML Entry 這種需求的 NPM 包:import-html-entry[5]。用法如下:

          import importHTML from 'import-html-entry';

          importHTML('./subApp/index.html')
          .then(res => {
          console.log(res.template); // 拿到 HTML 模板

          res.execScripts().then(exports => { // 執(zhí)行 JS 腳本
          const mobx = exports; // 獲取 JS 的輸出內(nèi)容
          // 下面就是拿到 JS 入口的內(nèi)容,并用來(lái)做一些事
          const { observable } = mobx;
          observable({
          name: 'kuitos'
          })
          })
          });

          當(dāng)然,qiankun 已經(jīng)將 import-html-entry 與子應(yīng)用加載函數(shù)完美地結(jié)合起來(lái),大家只需要知道這個(gè)庫(kù)是用來(lái)獲取 HTML 模板內(nèi)容,Style 樣式和 JS 腳本內(nèi)容就可以了。

          有了上面的了解后,相信大家對(duì)于如何加載子應(yīng)用就有思路了,偽代碼如下:

          // 解析 HTML,獲取 html,js,css 文本
          const {htmlText, jsText, cssText} = importHTMLEntry('https://xxxx.com')

          // 創(chuàng)建容器
          const $= document.querySelector(container)
          $container.innerHTML = htmlText

          // 創(chuàng)建 style 和 js 標(biāo)簽
          const $style = createElement('style', cssText)
          const $script = createElement('script', jsText)

          $container.appendChild([$style, $script])

          在第三步,我們不禁有個(gè)疑問:當(dāng)前這個(gè)應(yīng)用完美地插入了 style 和 script 標(biāo)簽,那下一個(gè)應(yīng)用 mount 時(shí)就會(huì)被前面的 style 和 script 污染了呀。

          為了解決這兩個(gè)問題,不得不做好應(yīng)用之間的樣式和 JS 的隔離。

          樣式隔離

          qiankun 實(shí)現(xiàn) single-spa 推薦的兩種樣式隔離方案:ShadowDOM 和 Scoped CSS。

          先來(lái)說說 ShadowDOM,qiankun 的源碼實(shí)現(xiàn)也很簡(jiǎn)單,只是添加一個(gè) Shadow DOM 節(jié)點(diǎn),偽代碼如下:

            if (strictStyleIsolation) {
          if (!supportShadowDOM) {
          // 報(bào)錯(cuò)
          // ...
          } else {
          // 清除原有的內(nèi)容
          const { innerHTML } = appElement;
          appElement.innerHTML = '';

          let shadow: ShadowRoot;

          if (appElement.attachShadow) {
          // 添加 shadow DOM 節(jié)點(diǎn)
          shadow = appElement.attachShadow({ mode: 'open' });
          } else {
          // deprecated 的操作
          // ...
          }
          // 在 shadow DOM 節(jié)點(diǎn)添加內(nèi)容
          shadow.innerHTML = innerHTML;
          }
          }

          通過 Shadow DOM 的天然的隔離特性來(lái)實(shí)現(xiàn)子應(yīng)用間的樣式隔離。

          另一個(gè)方案就是 Scoped CSS 了,說白了就是通過修改 CSS 選擇器來(lái)實(shí)現(xiàn)子應(yīng)用間的樣式隔離。 比如,你有這樣的 CSS 代碼:

          .container {
          background: red;
          }

          div {
          color: red;
          }

          qiankun 會(huì)掃描給定的 CSS 文本,通過正則匹配在選擇器前加上子應(yīng)用的名字,如果遇到元素選擇器,就加一個(gè)爸爸類名給它,比如:

          .subApp.container {
          background: red;
          }

          .subApp div {
          color: red;
          }

          JS 隔離

          第一步要隔離的是對(duì)全局對(duì)象 window 上的變量進(jìn)行隔離。不能 A 子應(yīng)用 window.setTimeout = undefined 之后, B 子應(yīng)用用 setTimeout 的時(shí)候就涼了。

          所以 JS 隔離深一層本質(zhì)就是記錄當(dāng)前 window 對(duì)象以前的值,在 A 子應(yīng)用進(jìn)來(lái)時(shí)一頓亂搞之后,要將所有值都恢復(fù)過來(lái)(恢復(fù)現(xiàn)場(chǎng))。這就是 SnapshotSandbox 的做法,偽代碼如下:

          class SnapshotSandbox {
          ...

          active() {
          // 記錄當(dāng)前快照
          this.windowSnapshot = {} as Window;
          getKeys(window).forEach(key => {
          this.windowSnapshot[key] = window[key];
          })

          // 恢復(fù)之前的變更
          getKeys(this.modifyPropsMap).forEach((key) => {
          window[key] = this.modifyPropsMap[key];
          });

          this.sandboxRunning = true;
          }

          inactive() {
          this.modifyPropsMap = {};

          // 記錄變更,恢復(fù)環(huán)境
          getKeys(window).forEach((key) => {
          if (window[key] !== this.windowSnapshot[key]) {
          this.modifyPropsMap[key] = window[key];
          window[key] = this.windowSnapshot[key];
          }
          });

          this.sandboxRunning = false;
          }
          }

          除了 SnapShotSandbox,qiankun 還提供了一種使用 ES 6 Proxy 實(shí)現(xiàn)的沙箱:

          class SingularProxySandbox {
          /** 沙箱期間新增的全局變量 */
          private addedPropsMapInSandbox = new Map<PropertyKey, any>();

          /** 沙箱期間更新的全局變量 */
          private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();

          /** 持續(xù)記錄更新的(新增和修改的)全局變量的 map,用于在任意時(shí)刻做 snapshot */
          private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();

          active() {
          if (!this.sandboxRunning) {
          // 恢復(fù)子應(yīng)用修改過的值
          this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
          }

          this.sandboxRunning = true;
          }

          inactive() {
          // 恢復(fù)加載子應(yīng)用前的 window
          this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
          // 刪掉子應(yīng)用期間新加的 window
          this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));

          this.sandboxRunning = false;
          }

          constructor(name: string) {
          this.name = name;
          this.type = SandBoxType.LegacyProxy;
          const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this;

          const rawWindow = window;
          const fakeWindow = Object.create(null) as Window;

          const proxy = new Proxy(fakeWindow, {
          set: (_: Window, key: PropertyKey, value: any): boolean => {
          if (this.sandboxRunning) {
          if (!rawWindow[key]) {
          addedPropsMapInSandbox.set(key, value); // 將沙箱期間新加的值記錄下來(lái)
          } else if (!modifiedPropsOriginalValueMapInSandbox.has(key)) {
          modifiedPropsOriginalValueMapInSandbox.set(key, rawWindow[key]); // 記錄沙箱前的值
          }

          currentUpdatedPropsValueMap.set(key, value); // 記錄沙箱后的值

          // 必須重新設(shè)置 window 對(duì)象保證下次 get 時(shí)能拿到已更新的數(shù)據(jù)
          (rawWindow as any)[key] = value;
          }
          },

          get(_: Window, key: PropertyKey): any {
          return rawWindow[key]
          },
          }
          }
          }

          兩者差不太多,那怎么不直接用 Proxy 高級(jí)方案呢,因?yàn)樵谝恍┑桶姹镜臑g覽器下是沒有 Proxy 對(duì)象的,所以 SnapshotSandbox 其實(shí)是 SingularProxySandbox 的降級(jí)方案。

          然而,問題還是沒有解決完。上面這種情況僅適用于一個(gè)頁(yè)面只有一個(gè)子應(yīng)用的情況,這種情況也被稱為單例(singular mode)。 如果一個(gè)頁(yè)面有多個(gè)子應(yīng)用那一個(gè) SingluarProxySandbox 明顯不夠的。為了解決這個(gè)問題,qiankun 提供了 ProxySandbox,偽代碼如下:

          class ProxySandbox {
          ...

          active() { // +1 廢話
          if (!this.sandboxRunning) activeSandboxCount++;
          this.sandboxRunning = true;
          }

          inactive() { // -1 廢話
          if (--activeSandboxCount === 0) {
          variableWhiteList.forEach((p) => {
          if (this.proxy.hasOwnProperty(p)) {
          delete window[p]; // 刪除白名單里子應(yīng)用添加的值
          }
          });
          }

          this.sandboxRunning = false;
          }

          constructor(name: string) {
          ...
          const rawWindow = window; // 原 window 對(duì)象
          const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow); // 將真 window 上的 key-value 復(fù)制到假 window 對(duì)象上

          const proxy = new Proxy(fakeWindow, { // 代理復(fù)制出來(lái)的 window
          set: (target: FakeWindow, key: PropertyKey, value: any): boolean => {
          if (this.sandboxRunning) {
          target[key] = value // 修改 fakeWindow 上的值

          if (variableWhiteList.indexOf(key) !== -1) {
          rawWindow[key] = value; // 白名單的話,修改真 window 上的值
          }

          updatedValueSet.add(p); // 記錄修改的值
          }
          },

          get(target: FakeWindow, key: PropertyKey): any {
          return target[key] || rawWindow[key] // 在 fakeWindow 上找,找不到從直 window 上找
          },
          }
          }
          }

          從上面可以看到,在 active 和 inactive 里并沒有太多在恢復(fù)現(xiàn)場(chǎng)操作,因?yàn)橹灰討?yīng)用 unmount,把 fakeWindow 一扔掉就完事了。

          等等,說了這么多上面還只是討論 window 對(duì)象的隔離呀,格局是不是小了點(diǎn)?是小了。

          沙箱

          現(xiàn)在我們?cè)賮?lái)審視一下沙箱這個(gè)玩意,其實(shí)無(wú)論沙箱也好 JS 隔離也好,最終要實(shí)現(xiàn)的是給子應(yīng)用一個(gè)獨(dú)立的環(huán)境,這也意味著我們有成百上千的東西要做補(bǔ)丁來(lái)打造終極的類 <iframe> 硬隔離。

          然而,qiankun 也不是萬(wàn)能的,它只對(duì)某些重要的函數(shù)和監(jiān)聽器進(jìn)行打補(bǔ)丁。

          其中最重要的補(bǔ)丁就是 insertBeforeappendChild 和 removeChild 的補(bǔ)丁了。

          當(dāng)我們加載子應(yīng)用的時(shí)候,免不了遇到動(dòng)態(tài)添加/移除 CSS 和 JS 腳本的情況。這時(shí) <head> 或 <body> 都有可能調(diào)用 insertBeforeappendChildremoveChild 這三個(gè)函數(shù)來(lái)插入或者刪除 <style><link> 或者 <script> 元素。

          所以,這三個(gè)函數(shù)在被 <head> 或 <body> 調(diào)用時(shí),就要用上補(bǔ)丁,主要目的是別插入到主應(yīng)用的 <head> 和 <body> 上,要插在子應(yīng)用里。打補(bǔ)丁偽代碼如下:

          // patch(element)
          switch (element.tagName) {
          case LINK_TAG_NAME: // <link> 標(biāo)簽
          case STYLE_TAG_NAME: { // <style> 標(biāo)簽
          if (scopedCSS) { // 使用 Scoped CSS
          if (element.href;) { // 處理如 <link rel="icon" href="favicon.ico"> 的玩意
          stylesheetElement = convertLinkAsStyle( // 獲取 <link> 里的 CSS 文本,并使用 css.process 添加前綴
          element,
          (styleElement) => css.process(mountDOM, styleElement, appName), // 添加前綴回調(diào)
          fetch,
          );
          dynamicLinkAttachedInlineStyleMap.set(element, stylesheetElement); // 緩存,下次加載沙箱時(shí)直接吐出來(lái)
          } else { // 處理如 <style>.container { background: red }</style> 的玩意
          css.process(mountDOM, stylesheetElement, appName);
          }
          }

          return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode); // 插入到掛載容器上
          }

          case SCRIPT_TAG_NAME: {
          const { src, text } = element as HTMLScriptElement;

          if (element.src) { // 處理外鏈 JS
          execScripts(null, [src], proxy, { // 獲取并執(zhí)行 JS
          fetch,
          strictGlobal,
          });

          return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode); // 插入到掛載容器上
          }

          // 處理內(nèi)聯(lián) JS
          execScripts(null, [`<script>${text}</script>`], proxy, { strictGlobal });
          return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode);
          }

          default:
          break;
          }

          當(dāng)在創(chuàng)建沙箱時(shí)打完補(bǔ)丁后,在處理樣式和 JS 腳本時(shí)就可以針對(duì)當(dāng)前子應(yīng)用來(lái)應(yīng)用樣式和 JS 了。上面我們還注意到 CSS 樣式文本是被保存的,所以當(dāng)子應(yīng)用 remount 的時(shí)候,這些樣式也可以作為緩存直接一波補(bǔ)上,不需要再做處理了。

          剩下的補(bǔ)丁都是給 historyListenerssetIntervaladdEventListenersremoveEventListeners 做的補(bǔ)丁,無(wú)非就是 mount 時(shí)記錄 listeners 以及一些添加的值,在 unmount 的時(shí)候再一次性執(zhí)行掉或者刪除掉,不再贅述。

          更多的生命周期

          如果當(dāng)前項(xiàng)目遷移成子應(yīng)用,在入口的 JS 就不得不配合 qiankun 來(lái)做一些改動(dòng),而這些改動(dòng)有可能影響子應(yīng)用的獨(dú)立運(yùn)行。比如,接入了微前端后,可能就不得不在本地先起一個(gè)主應(yīng)用,再起一個(gè)子應(yīng)用,然后才能做開發(fā)和調(diào)試,那這也太蛋疼了。

          為了解決子應(yīng)用也能獨(dú)立運(yùn)行的問題,qiankun 注入了一些變量,來(lái)告訴子應(yīng)用說:喂,你現(xiàn)在是兒子,要用子應(yīng)用的渲染方式。而當(dāng)子應(yīng)用獲取不到這些注入的變量時(shí),它就知道:哦,我現(xiàn)在要獨(dú)立運(yùn)行了,用回原來(lái)的渲染方式就可以了,比如:

          if (window. __POWERED_BY_QIANKUN__) {
          console.log('微前端場(chǎng)景')
          renderAsSubApp()
          } else {
          console.log('單體場(chǎng)景')
          previousRenderApp()
          }

          怎么注入就是個(gè)問題了,不能簡(jiǎn)單的 window.__POWERED_BY_QIANKUN__ = true 就完事了,因?yàn)樽討?yīng)用會(huì)在編譯時(shí)就要這個(gè)變量了。所以,qiankun 在 single-spa 提供的生命周期 load, mount, unmount 做了變量的注入,偽代碼如下:

          // getAddOn
          export default function getAddOn(global: Window): FrameworkLifeCycles<any> {
          return {
          async beforeLoad() {
          // eslint-disable-next-line no-param-reassign
          global.__POWERED_BY_QIANKUN__ = true;
          },

          async beforeMount() {
          // eslint-disable-next-line no-param-reassign
          global.__POWERED_BY_QIANKUN__ = true;
          },

          async beforeUnmount() {
          // eslint-disable-next-line no-param-reassign
          delete global.__POWERED_BY_QIANKUN__;
          },
          };
          }

          // loadApp
          const addOnLifeCycles = getAddOn(window)

          return {
          load: [addOnLifeCycles.beforeLoad, subApp.load],
          mount: [addOnLifeCycles.mount, subApp.mount],
          unmount: [addOnLifeCycles.unmount, subApp.unmount]
          }

          總結(jié)一下,新增的生命周期有:

          ?beforeLoad?beforeMount?afterMount?beforeUnmount?afterUnmount

          loadApp

          好了,上面就是加載一個(gè)子應(yīng)用的所有步驟了,這里先做個(gè)小總結(jié):

          ?import-html-entry 解析 html,獲取 JavaScript, CSS, HTML?創(chuàng)建容器 container,同時(shí)加上 css 樣式隔離:在 container 上添加 Shadow DOM 或者對(duì) CSS 文本 添加前綴實(shí)現(xiàn) Scoped CSS?創(chuàng)建沙箱,監(jiān)聽 window 的變化,并對(duì)一些函數(shù)打上補(bǔ)丁?提供更多的生命周期,在 beforeXXX 里注入一些 qiankun 提供的變量?返回帶有 bootstrap, mount, unmount 屬性的對(duì)象

          預(yù)加載

          從上面可以看到加載一個(gè)子應(yīng)用的時(shí)候需要很多的步驟,我們不禁想到:如果在 mount 第一個(gè)子應(yīng)用空閑時(shí)候,可以預(yù)先加載別的子應(yīng)用,那之后切換子應(yīng)用就可以更快了,也即子應(yīng)用預(yù)加載。

          在空閑的時(shí)候干一些事,可以使用瀏覽器提供的 requestIdleCallback。OK,那我們?cè)賮?lái)定義一下“預(yù)加載”是什么,其實(shí)就是把 CSS 和 JS 下載下來(lái)就完事了,所以 qiankun 的源碼也是很簡(jiǎn)單的:

          requestIdleCallback(async () => {
          const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
          requestIdleCallback(getExternalStyleSheets);
          requestIdleCallback(getExternalScripts);
          });

          現(xiàn)在,我們?cè)賮?lái)腦洞大開一下:難道一下子就要所有子應(yīng)用都要預(yù)加載么?不見得吧?有可能一些子應(yīng)用要預(yù)加載,一些不需要。

          所以 qiankun 提供了三種預(yù)加載策略:

          ?全部子應(yīng)用都立馬預(yù)加載?全部子應(yīng)用都在第一個(gè)子應(yīng)用加載后才預(yù)加載?在 criticalAppNames 數(shù)組里的子應(yīng)用要立馬預(yù)加載,在 minorAppsName 數(shù)組里的子應(yīng)用在第一個(gè)子應(yīng)用加載后才預(yù)加載

          源碼實(shí)現(xiàn)如下:

          export function doPrefetchStrategy(
          apps: AppMetadata[],
          prefetchStrategy: PrefetchStrategy,
          importEntryOpts?: ImportEntryOpts,
          ) {
          const appsName2Apps = (names: string[]): AppMetadata[] => apps.filter((app) => names.includes(app.name));

          if (Array.isArray(prefetchStrategy)) {
          // 全部都在第一個(gè)子應(yīng)用加載后才預(yù)加載
          prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts);
          } else if (isFunction(prefetchStrategy)) {
          (async () => {
          // 一半一半
          const { criticalAppNames = [], minorAppsName = [] } = await prefetchStrategy(apps);
          prefetchImmediately(appsName2Apps(criticalAppNames), importEntryOpts);
          prefetchAfterFirstMounted(appsName2Apps(minorAppsName), importEntryOpts);
          })();
          } else {
          switch (prefetchStrategy) {
          case true: // 全部都在第一個(gè)子應(yīng)用加載后才預(yù)加載
          prefetchAfterFirstMounted(apps, importEntryOpts);
          break;

          case 'all': // 全部子應(yīng)用都立馬預(yù)加載
          prefetchImmediately(apps, importEntryOpts);
          break;

          default:
          break;
          }
          }
          }

          全局狀態(tài)管理

          全局狀態(tài)很有可能出現(xiàn)在微前端的場(chǎng)景中,比如主應(yīng)用提供可以一些初始化好的 SDK。剛開始先傳個(gè)未初始好的 SDK,等主應(yīng)用把 SDK 初始化好了,再通過回調(diào)通知子應(yīng)用:醒醒,SDK 準(zhǔn)備好了。

          這種思路和 Redux, Event Bus 一模一樣。 狀態(tài)都存在 window 的 gloablState 全局對(duì)象里,再添加一個(gè) onGlobalStateChange 回調(diào)就完事了,實(shí)現(xiàn)偽代碼如下:

          let gloablState = {}
          let deps = {}

          // 觸發(fā)全局監(jiān)聽
          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));
          }
          });
          }

          // 添加全局狀態(tài)變化的監(jiān)聽器
          function onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
          deps[id] = callback;
          if (fireImmediately) {
          const cloneState = cloneDeep(globalState);
          callback(cloneState, cloneState);
          }
          }

          // 更新 globalState
          function setGlobalState(state: Record<string, any> = {}) {
          const prevState = globalState
          globalState = {...cloneDeep(globalState), ...state}
          emitGlobal(globalState, prevState);
          }

          // 注銷該應(yīng)用下的依賴
          function offGlobalStateChange() {
          delete deps[id];
          }

          onGlobalStateChange 添加監(jiān)聽器,當(dāng)調(diào)用 setGlobalState 更新值,值改了,調(diào)用 emitGlobal,執(zhí)行所有對(duì)應(yīng)的監(jiān)聽器。調(diào)用 offGlobalStateChange 刪掉監(jiān)聽器。Easy ~

          全局錯(cuò)誤處理

          主要監(jiān)聽了 error 和 unhandledrejection 兩個(gè)錯(cuò)誤事件:

          export function addGlobalUncaughtErrorHandler(errorHandler: OnErrorEventHandlerNonNull): void {
          window.addEventListener('error', errorHandler);
          window.addEventListener('unhandledrejection', errorHandler);
          }

          export function removeGlobalUncaughtErrorHandler(errorHandler: (...args: any[]) => any) {
          window.removeEventListener('error', errorHandler);
          window.removeEventListener('unhandledrejection', errorHandler);
          }

          使用的時(shí)候添加監(jiān)聽器,不要的時(shí)候移除監(jiān)聽器,不廢話。

          總結(jié)

          再次總結(jié)一下 qiankun 做了什么事情:

          ?實(shí)現(xiàn) loadApp 函數(shù),是最關(guān)鍵、重要的一步

          ?實(shí)現(xiàn) CSS 樣式隔離,主要有 Shadow DOM 和 Scoped CSS 兩種方案?實(shí)現(xiàn)沙箱,JS 隔離,主要對(duì) window 對(duì)象、各種 listeners 和方法進(jìn)行隔離?提供很多生命周期,并在一些 beforeXXX 的鉤子里注入 qiankun 提供的變量

          ?提供預(yù)加載,提前下載 HTML、CSS、JS,并有三種策略

          ?全部立馬預(yù)加載?全部在第一個(gè)加載后預(yù)加載?一些立馬預(yù)加載,一些在第一個(gè)加載后預(yù)加載

          ?提供全局狀態(tài)管理,類似 Redux,Event Bus?提供全局錯(cuò)誤處理,主要監(jiān)聽 error 和 unhandledrejection 兩個(gè)事件



          最后

          雖然阿里說:“可能是你見過最完善的微前端解決方案??”。但是從上面對(duì)源碼的解讀也可以看出來(lái),qiankun 也有一些事情沒有做的。比如沒有對(duì) localStorage 進(jìn)行隔離,如果多個(gè)子應(yīng)用都用到 localStorage 就有可能沖突了,除此之外,還有 cookie, indexedDB 的共享等。再比如如果單個(gè)頁(yè)面下多個(gè)子應(yīng)用都依賴了前端路由怎么辦呢?當(dāng)然這里的質(zhì)疑也僅是我個(gè)人的猜想。

          另一件事想說的是:微前端的難點(diǎn)并不是 single-spa 的生命周期、路由挾持。而是如何加載好一個(gè)子應(yīng)用。從上面可以看到,有很多 hacky 的編碼,比如在選擇器前面加前綴,將子應(yīng)用的 <link><script> 加載到子應(yīng)用上,監(jiān)聽 window 的變化,恢復(fù)現(xiàn)場(chǎng)等等,都是臺(tái)上一句話,臺(tái)下想禿頭的操作。如果不是真見過,估計(jì)想破頭都想不出來(lái)。

          也正是這些 hacky 代碼,在搭建微前端的時(shí)候會(huì)遇到非常多的問題,而且微前端的目的是要將多個(gè)??山聚合起來(lái),所以微前端的解決方案是注定沒有銀彈的,且行且珍惜吧。

          References

          [1] 【微前端】single-spa 到底是個(gè)什么鬼: https://www.jianshu.com/p/23f37053c1d9
          [2] qiankun: https://qiankun.umijs.org/
          [3] Demo: https://github.com/umijs/qiankun/tree/master/examples
          [4] Demo: https://github.com/Haixiang6123/qiankun-bigass-app
          [5] import-html-entry: https://github.com/kuitos/import-html-entry

          如果覺得這篇文章還不錯(cuò)
          點(diǎn)擊下面卡片關(guān)注我
          來(lái)個(gè)【分享、點(diǎn)贊、在看】三連支持一下吧

             “分享、點(diǎn)贊在看” 支持一波 

          瀏覽 84
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产高清视频在线 | 色播在线永久免费视频 | 成人性爱视频免费网站 | 手机av网 | 五月婷婷综合91 |