溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

微前端框架qiankun隔離方法怎么使用

發(fā)布時(shí)間:2023-02-10 09:18:25 來源:億速云 閱讀:141 作者:iii 欄目:開發(fā)技術(shù)

本文小編為大家詳細(xì)介紹“微前端框架qiankun隔離方法怎么使用”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“微前端框架qiankun隔離方法怎么使用”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識(shí)吧。

    沙箱隔離

    在基于single-spa開發(fā)的微前端應(yīng)用中,子應(yīng)用開發(fā)者需要特別注意的是:

    要謹(jǐn)慎修改和使用全局變量上的屬性(如window、document等),以免造成依賴該屬性的自身應(yīng)用或其它子應(yīng)用運(yùn)行時(shí)出現(xiàn)錯(cuò)誤;

    要謹(jǐn)慎控制CSS規(guī)則的生效范圍,避免覆蓋污染其它子應(yīng)用的樣式;

    但這樣的低級(jí)人為保證機(jī)制是無法在大規(guī)模的團(tuán)隊(duì)開發(fā)過程中對(duì)應(yīng)用的獨(dú)立性起到完善保護(hù)的,而qiankun框架給我們提供的最便利和有用的功能就是其基于配置的自動(dòng)化沙箱隔離機(jī)制了。有了框架層面的子應(yīng)用隔離支持,用戶無論是在編寫JS代碼還是修改CSS樣式時(shí)都不必再擔(dān)心代碼對(duì)于全局環(huán)境的污染問題了。沙箱機(jī)制一方面提升了微應(yīng)用框架運(yùn)行的穩(wěn)定性和獨(dú)立性,另一方面也降低了微前端開發(fā)者的心智負(fù)擔(dān),讓其只需專注于自己的子應(yīng)用代碼開發(fā)之中。

    JS隔離

    在JS隔離方面,qiankun為開發(fā)者提供了三種不同模式的沙箱機(jī)制,分別適用于不同的場(chǎng)景之中。

    1. Snapshot沙箱

    該沙箱主要用于不支持Proxy對(duì)象的低版本瀏覽器之中,不能由用戶手動(dòng)指定該模式,qiankun會(huì)自動(dòng)檢測(cè)瀏覽器的支持情況并降級(jí)到Snapshot沙箱實(shí)現(xiàn)。由于這種實(shí)現(xiàn)方式在子應(yīng)用運(yùn)行過程中實(shí)際上修改了全局變量,因此不能用于多例模式之中(同時(shí)存在多個(gè)已掛載的子應(yīng)用)。

    該沙箱實(shí)現(xiàn)方式非常簡(jiǎn)潔,下面我們給出其簡(jiǎn)化后的實(shí)現(xiàn)

    // 基于 diff 方式實(shí)現(xiàn)的沙箱,用于不支持 Proxy 的低版本瀏覽器
    export default class SnapshotSandbox implements SandBox {
      private windowSnapshot!: Window;
      private modifyPropsMap: Record<any, any> = {};
      constructor() {}
      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];
        });
      }
      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];
          }
        });
      }
    }

    沙箱內(nèi)部存在兩個(gè)對(duì)象變量windowSnapshotmodifyPropsMap ,分別用來存儲(chǔ)子應(yīng)用掛載前原始window對(duì)象上的全部屬性以及子應(yīng)卸載時(shí)被其修改過的window對(duì)象上的相關(guān)屬性。

    Snapshot沙箱會(huì)在子應(yīng)用mount前將modifyPropsMap中存儲(chǔ)的屬性重新賦值給window以恢復(fù)該子應(yīng)用之前執(zhí)行時(shí)的全局變量上下文,并在子應(yīng)用unmount后將windowSnapshot中存儲(chǔ)的屬性重新賦值給window以恢復(fù)該子應(yīng)用運(yùn)行前的全局變量上下文,從而使得兩個(gè)不同子應(yīng)用的window相互獨(dú)立,達(dá)到JS隔離的目的。

    2. Legacy沙箱

    當(dāng)用戶手動(dòng)配置sandbox.loose: true時(shí)該沙箱被啟用。Legacy沙箱同樣會(huì)對(duì)window造成污染,但是其性能比要比snapshot沙箱好,因?yàn)樵撋诚洳挥帽闅vwindow對(duì)象。同樣legacy沙箱也只適用于單例模式之中。

    下面一起來看一下其簡(jiǎn)化后的大致實(shí)現(xiàn)方式

    /**
     * 基于 Proxy 實(shí)現(xiàn)的沙箱
     * TODO: 為了兼容性 singular 模式下依舊使用該沙箱,等新沙箱穩(wěn)定之后再切換
     */
    export default class LegacySandbox implements SandBox {
      /** 沙箱代理的全局變量 */
      proxy: WindowProxy;
      /** 沙箱期間新增的全局變量 */
      private addedPropsMapInSandbox = new Map<PropertyKey, any>();
      /** 沙箱期間更新的全局變量 */
      private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();
      /** 持續(xù)記錄更新的(新增和修改的)全局變量的 map,用于在任意時(shí)刻做 snapshot */
      private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();
      constructor() {
        const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this;
        const rawWindow = window;
        const fakeWindow = Object.create(null) as Window;
        const setTrap = (p: PropertyKey, value: any, originalValue: any) => {
          if (!rawWindow.hasOwnProperty(p)) {
            // 當(dāng)前 window 對(duì)象不存在該屬性,將其記錄在新增變量之中
            addedPropsMapInSandbox.set(p, value);
          } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
            // 如果當(dāng)前 window 對(duì)象存在該屬性,且 record map 中未記錄過,則記錄該屬性初始值
            modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
          }
          // 無論何種修改都記錄在currentUpdatedPropsValueMap中
          currentUpdatedPropsValueMap.set(p, value);
          // 必須重新設(shè)置 window 對(duì)象保證下次 get 時(shí)能拿到已更新的數(shù)據(jù)
          (rawWindow as any)[p] = value;
        };
        const proxy = new Proxy(fakeWindow, {
          set: (_: Window, p: PropertyKey, value: any): boolean => {
            const originalValue = (rawWindow as any)[p];
            return setTrap(p, value, originalValue, true);
          },
          get(_: Window, p: PropertyKey): any {
            // avoid who using window.window or window.self to escape the sandbox environment to touch the really window or use window.top to check if an iframe context
            if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
              return proxy;
            }
            const value = (rawWindow as any)[p];
            return value;
          },
        });
        this.proxy = proxy
      }
      active() {
        // 激活時(shí)將子應(yīng)用之前的所有改變重新賦予window,恢復(fù)其運(yùn)行時(shí)上下文
        this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v));
      }
      inactive() {
        // 卸載時(shí)將window上修改的值復(fù)原,新添加的值刪除
        this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v));
        this.addedPropsMapInSandbox.forEach((_, p) => this.setWindowProp(p, undefined, true));
      }
      private setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) {
        if (value === undefined && toDelete) {
          delete (this.globalContext as any)[prop];
        } else {
          (this.globalContext as any)[prop] = value;
        }
      }
    }

    Legacy沙箱為一個(gè)空對(duì)象fakewindow使用proxy代理攔截了其全部的set/get等操作,并在loader中用其替換了window。當(dāng)用戶試圖修改window屬性時(shí),fakewindow上代理的set操作生效捕獲了相關(guān)修改,其分別將新增的屬性和修改前的值存入addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox這兩個(gè)Map之中,此外還將所有修改記錄在了currentUpdatedPropsValueMap之中,并改變了window對(duì)象。

    這樣當(dāng)子應(yīng)用掛載前,legacy沙箱會(huì)將currentUpdatedPropsValueMap之中記錄的子應(yīng)用相關(guān)修改重新賦予window,恢復(fù)其運(yùn)行時(shí)上下文。當(dāng)子應(yīng)用卸載后,legacy沙箱會(huì)遍歷addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox這兩個(gè)Map并將window上的相關(guān)值恢復(fù)到子應(yīng)用運(yùn)行之前的狀態(tài)。最終達(dá)到了子應(yīng)用間JS隔離的目的。

    3. Proxy沙箱

    Proxy沙箱是qiankun框架中默認(rèn)使用的沙箱模式(也可以通過配置sandbox.loose: false來開啟),只有該模式真正做到了對(duì)window的無污染隔離(子應(yīng)用完全不能修改全局變量),因此可以被應(yīng)用在單/多例模式之中。

    Proxy沙箱的原理也非常簡(jiǎn)單,它將window上的所有屬性遍歷拷貝生成一個(gè)新的fakeWindow對(duì)象,緊接著使用proxy代理這個(gè)fakeWindow,用戶對(duì)window操作全部被攔截下來,只作用于在這個(gè)fakeWindow之上。

    // 便利window拷貝創(chuàng)建初始代理對(duì)象
    function createFakeWindow(globalContext: Window) {
      const fakeWindow = {} as FakeWindow;
      Object.getOwnPropertyNames(globalContext)
        .forEach((p) => {
          const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
          rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
        });
      return { fakeWindow };
    }
    /**
     * 基于 Proxy 實(shí)現(xiàn)的沙箱
     */
    export default class ProxySandbox implements SandBox {
      // 標(biāo)志該沙箱是否被啟用
      sandboxRunning = true;
      constructor() {
        const { fakeWindow } = createFakeWindow(window);
        const proxy = new Proxy(fakeWindow, {
          set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
            if(this.sandboxRunning){
              // 修改代理對(duì)象的值
              target[p] = value;
              return true; 
            }
          }
          get: (target: FakeWindow, p: PropertyKey): any => {
            // avoid who using window.window or window.self to escape the sandbox environment to touch the really window
            if (p === 'window' || p === 'self' || p === 'globalThis') {
              return proxy;
            }
            // 獲取代理對(duì)象的值
          	const value = target[p];
            return value;
          },
        })
      }
      active() {
        if (!this.sandboxRunning) activeSandboxCount++;
        this.sandboxRunning = true;
      }
      inactive() {
        this.sandboxRunning = false;
      }
    }

    CSS隔離

    對(duì)于CSS隔離的方式,在默認(rèn)情況下由于切換子應(yīng)用時(shí),其相關(guān)的CSS內(nèi)外連屬性會(huì)被卸載掉,所以可以確保單實(shí)例場(chǎng)景子應(yīng)用之間的樣式隔離,但是這種方式無法確保主應(yīng)用跟子應(yīng)用、或者多實(shí)例場(chǎng)景的子應(yīng)用樣式隔離。不過,qiankun也提供了兩種可配置生效的內(nèi)置方式供使用者選擇。

    1. ShadowDOM

    當(dāng)用戶配置sandbox.strictStyleIsolation: true時(shí),ShadowDOM樣式沙箱會(huì)被開啟。在這種模式下 qiankun 會(huì)為每個(gè)微應(yīng)用的容器包裹上一個(gè) shadow dom 節(jié)點(diǎn),從而確保微應(yīng)用的樣式不會(huì)對(duì)全局造成影響。

    // 在子應(yīng)用的DOM樹最外層進(jìn)行一次包裹
    function createElement(
      appContent: string,
      strictStyleIsolation: boolean,
      scopedCSS: boolean,
      appInstanceId: string,
    ): HTMLElement {
      // 包裹節(jié)點(diǎn)
      const containerElement = document.createElement('div');
      containerElement.innerHTML = appContent;
      // 子應(yīng)用最外層節(jié)點(diǎn)
      const appElement = containerElement.firstChild as HTMLElement;
      // 當(dāng)開啟了ShadowDOM沙箱時(shí)
      if (strictStyleIsolation) {
        const { innerHTML } = appElement;
        appElement.innerHTML = '';
        let shadow: ShadowRoot;
    		// 判斷瀏覽器兼容的創(chuàng)建ShadowDOM的方式,并使用該方式創(chuàng)建ShadowDOM根節(jié)點(diǎn)
        if (appElement.attachShadow) {
          shadow = appElement.attachShadow({ mode: 'open' });
        } else {
          // createShadowRoot was proposed in initial spec, which has then been deprecated
          shadow = (appElement as any).createShadowRoot();
        }
        // 將子應(yīng)用內(nèi)容掛在ShadowDOM根節(jié)點(diǎn)下
        shadow.innerHTML = innerHTML;
      }
    	// 。。。。。。
      return appElement;
    }

    這種方式雖然看起來清晰簡(jiǎn)單,還巧妙利用了瀏覽器對(duì)于ShadowDOM的CSS隔離特性,但是由于ShadowDOM的隔離比較嚴(yán)格,所以這并不是一種無腦使用的方案。例如:如果子應(yīng)用內(nèi)存在一個(gè)彈出時(shí)會(huì)掛在document根元素的彈窗,那么該彈窗的樣式是否會(huì)受到ShadowDOM的影響而失效?所以開啟該沙箱的用戶需要明白自己在做什么,且可能需要對(duì)子應(yīng)用內(nèi)部代碼做出一定的調(diào)整。

    2. Scoped CSS

    因?yàn)镾hadowDOM存在著上述的一些問題,qiankun貼心的為用戶提供了另一種更加無腦簡(jiǎn)便的樣式隔離方式,那就是Scoped CSS。通過配置sandbox.experimentalStyleIsolation: true,Scoped樣式沙箱會(huì)被開啟。

    在這種模式下,qiankun會(huì)遍歷子應(yīng)用中所有的CSS選擇器,通過對(duì)選擇器前綴添加一個(gè)固定的帶有該子應(yīng)用標(biāo)識(shí)的屬性選擇器的方式來限制其生效范圍,從而避免子應(yīng)用間、主應(yīng)用與子應(yīng)用的樣式相互污染。

    export const QiankunCSSRewriteAttr = 'data-qiankun';
    // 在子應(yīng)用的DOM樹最外層進(jìn)行一次包裹
    function createElement(
      appContent: string,
      strictStyleIsolation: boolean,
      scopedCSS: boolean,
      appInstanceId: string,
    ): HTMLElement {
      // 包裹節(jié)點(diǎn)
      const containerElement = document.createElement('div');
      containerElement.innerHTML = appContent;
      // 子應(yīng)用最外層節(jié)點(diǎn)
      const appElement = containerElement.firstChild as HTMLElement;
      // 。。。。。。
      // 當(dāng)開啟了Scoped CSS沙箱時(shí)
      if (scopedCSS) {
        // 為外層節(jié)點(diǎn)添加qiankun自定義屬性,其值設(shè)定為子應(yīng)用id標(biāo)識(shí)
        const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr);
        if (!attr) {
          appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId);
        }
    		// 獲取子應(yīng)用中全部樣式并進(jìn)行處理
        const styleNodes = appElement.querySelectorAll('style') || [];
        forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
          css.process(appElement!, stylesheetElement, appInstanceId);
        });
      }
      return appElement;
    }

    qiankun首先對(duì)子應(yīng)用最外層的包裹節(jié)點(diǎn)(一般為div節(jié)點(diǎn))添加一個(gè)屬性名為data-qiankun,值為appInstanceId的屬性。接著遍歷處理子應(yīng)用中的所有樣式。

    export const process = (
      appWrapper: HTMLElement,
      stylesheetElement: HTMLStyleElement | HTMLLinkElement,
      appName: string,
    ): void => {
      // lazy singleton pattern
      if (!processor) {
        processor = new ScopedCSS();
      }
    	// ?。?!注意,對(duì)于link標(biāo)簽引入的外聯(lián)樣式不支持。qiankun在初期解析使用的import-html-entry在解析html模版時(shí)會(huì)將所有外聯(lián)樣式拉取并轉(zhuǎn)換為style標(biāo)簽包裹的內(nèi)聯(lián)樣式,所以這里不再處理link的外聯(lián)樣式。
      if (stylesheetElement.tagName === 'LINK') {
        console.warn('Feature: sandbox.experimentalStyleIsolation is not support for link element yet.');
      }
      const mountDOM = appWrapper;
      if (!mountDOM) {
        return;
      }
    	// 獲取包裹元素標(biāo)簽
      const tag = (mountDOM.tagName || '').toLowerCase();
      if (tag && stylesheetElement.tagName === 'STYLE') {
        // 生成屬性選擇器前綴,準(zhǔn)備將其添加在選擇器前(如div[data-qiankun=app1])
        const prefix = `${tag}[${QiankunCSSRewriteAttr}="${appName}"]`;
        processor.process(stylesheetElement, prefix);
      }
    };
    // 。。。。。。
    process(styleNode: HTMLStyleElement, prefix: string = '') {
      if (styleNode.textContent !== '') {
        // 獲取相關(guān)css規(guī)則rules
        const textNode = document.createTextNode(styleNode.textContent || '');
        this.swapNode.appendChild(textNode);
        const sheet = this.swapNode.sheet as any; // type is missing
        const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
        // 重寫這些CSS規(guī)則,將前綴添加進(jìn)去
        const css = this.rewrite(rules, prefix);
        // 用重寫后的CSS規(guī)則覆蓋之前的規(guī)則
        styleNode.textContent = css;
        // 標(biāo)志符,代表該節(jié)點(diǎn)已經(jīng)處理過
        (styleNode as any)[ScopedCSS.ModifiedTag] = true;
        return;
      }
    	// 監(jiān)聽節(jié)點(diǎn)變化
      const mutator = new MutationObserver((mutations) => {
        for (let i = 0; i < mutations.length; i += 1) {
          const mutation = mutations[i];
          // 忽略已經(jīng)處理過的節(jié)點(diǎn)
          if (ScopedCSS.ModifiedTag in styleNode) {
            return;
          }
          // 如果新增了未處理過的子節(jié)點(diǎn)(代表了用戶新注入了一些屬性),那么會(huì)再次重寫所有的CSS規(guī)則以確保新增的CSS不會(huì)污染子應(yīng)用外部
          if (mutation.type === 'childList') {
            const sheet = styleNode.sheet as any;
            const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
            const css = this.rewrite(rules, prefix);
            styleNode.textContent = css;
            (styleNode as any)[ScopedCSS.ModifiedTag] = true;
          }
        }
    	});
      // 注冊(cè)監(jiān)聽
      mutator.observe(styleNode, { childList: true });
    }
    // 具體CSS規(guī)則重寫方式
    private rewrite(rules: CSSRule[], prefix: string = '') {
      // 。。。。。。
      // 這里省略其實(shí)現(xiàn)方式,整體實(shí)現(xiàn)思路簡(jiǎn)單但步驟很繁瑣,主要就是對(duì)字符串的正則判斷和替換修改。
      // 1. 對(duì)于根選擇器(html/body/:root等),直接將其替換為prefix
      // 2. 對(duì)于其它選擇器,將prefix放在最前面( selector1 selector2, selector3 =》 prefix selector1 selector2,prefix selector3)
    }

    可以看到,qiankun通過為子應(yīng)用的外層包裹元素注入屬性并將子應(yīng)用全部樣式的作用范圍都限制在該包裹元素下(通過添加指定的屬性選擇器作為前綴)實(shí)現(xiàn)了scoped樣式沙箱隔離。需要注意的是,如果用戶在運(yùn)行時(shí)對(duì)內(nèi)聯(lián)樣式進(jìn)行修改,qiankun是可以偵測(cè)到并幫助用戶限制其作用范圍,但如果用戶在運(yùn)行時(shí)引入了新的外聯(lián)樣式或者自行創(chuàng)建了新的內(nèi)聯(lián)標(biāo)簽,那么qiankun并不會(huì)做出反應(yīng),相關(guān)的CSS規(guī)則還是可能會(huì)污染全局樣式。

    通信方式

    對(duì)于微前端來說,除了應(yīng)用間的隔離外,應(yīng)用間的通信也是非常重要的部分。這里,single-spa提供了從主應(yīng)用向子應(yīng)用傳遞customProps的方式實(shí)現(xiàn)了最基礎(chǔ)的參數(shù)傳遞。但是真實(shí)的開發(fā)場(chǎng)景需要的信息傳遞是非常復(fù)雜的,靜態(tài)的預(yù)設(shè)參數(shù)傳遞只能起到很小的作用,我們還需要一種更加強(qiáng)大的通信機(jī)制來幫助我們開發(fā)應(yīng)用。

    這里,qiankun在框架內(nèi)部預(yù)先設(shè)計(jì)實(shí)現(xiàn)了完善的發(fā)布訂閱模式,降低了開發(fā)者的上手門檻。我們首先來看一下qiankun中的通信是如何進(jìn)行的。

    // ------------------主應(yīng)用------------------
    import { initGlobalState, MicroAppStateActions } from 'qiankun';
    // 初始化 state
    const actions: MicroAppStateActions = initGlobalState(state);
    // 在當(dāng)前應(yīng)用監(jiān)聽全局狀態(tài),有變更觸發(fā) callback
    actions.onGlobalStateChange((state, prev) => {
      // state: 變更后的狀態(tài); prev 變更前的狀態(tài)
      console.log(state, prev);
    });
    // 按一級(jí)屬性設(shè)置全局狀態(tài),微應(yīng)用中只能修改已存在的一級(jí)屬性
    actions.setGlobalState(state);
    // 移除當(dāng)前應(yīng)用的狀態(tài)監(jiān)聽,微應(yīng)用 umount 時(shí)會(huì)默認(rèn)調(diào)用
    actions.offGlobalStateChange();
    // ------------------子應(yīng)用------------------
    // 從生命周期 mount 中獲取通信方法,使用方式和 master 一致
    export function mount(props) {
      props.onGlobalStateChange((state, prev) => {
        // state: 變更后的狀態(tài); prev 變更前的狀態(tài)
        console.log(state, prev);
      });
      props.setGlobalState(state);
    }

    接下來,讓我們一起來看一下它是如何實(shí)現(xiàn)的。

    import { cloneDeep } from 'lodash';
    import type { OnGlobalStateChangeCallback, MicroAppStateActions } from './interfaces';
    // 全局狀態(tài)
    let globalState: Record<string, any> = {};
    // 緩存相關(guān)的訂閱者
    const deps: Record<string, OnGlobalStateChangeCallback> = {};
    // 觸發(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));
        }
      });
    }
    // 初始化
    export function initGlobalState(state: Record<string, any> = {}) {
      if (state === globalState) {
        console.warn('[qiankun] state has not changed!');
      } else {
        const prevGlobalState = cloneDeep(globalState);
        globalState = cloneDeep(state);
        emitGlobal(globalState, prevGlobalState);
      }
      // 返回相關(guān)方法,形成閉包存儲(chǔ)相關(guān)狀態(tài)
      return getMicroAppStateActions(`global-${+new Date()}`, true);
    }
    export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {
      return {
        /**
         * onGlobalStateChange 全局依賴監(jiān)聽
         *
         * 收集 setState 時(shí)所需要觸發(fā)的依賴
         *
         * 限制條件:每個(gè)子應(yīng)用只有一個(gè)激活狀態(tài)的全局監(jiān)聽,新監(jiān)聽覆蓋舊監(jiān)聽,若只是監(jiān)聽部分屬性,請(qǐng)使用 onGlobalStateChange
         *
         * 這么設(shè)計(jì)是為了減少全局監(jiān)聽濫用導(dǎo)致的內(nèi)存爆炸
         *
         * 依賴數(shù)據(jù)結(jié)構(gòu)為:
         * {
         *   {id}: callback
         * }
         *
         * @param callback
         * @param fireImmediately 是否立即執(zhí)行callback
         */
        onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
          if (!(callback instanceof Function)) {
            console.error('[qiankun] callback must be function!');
            return;
          }
          if (deps[id]) {
            console.warn(`[qiankun] '${id}' global listener already exists before this, new listener will overwrite it.`);
          }
          / 注冊(cè)訂閱
          deps[id] = callback;
          if (fireImmediately) {
            const cloneState = cloneDeep(globalState);
            callback(cloneState, cloneState);
          }
        },
        /**
         * setGlobalState 更新 store 數(shù)據(jù)
         *
         * 1. 對(duì)輸入 state 的第一層屬性做校驗(yàn),只有初始化時(shí)聲明過的第一層(bucket)屬性才會(huì)被更改
         * 2. 修改 store 并觸發(fā)全局監(jiān)聽
         *
         * @param state
         */
        setGlobalState(state: Record<string, any> = {}) {
          if (state === globalState) {
            console.warn('[qiankun] state has not changed!');
            return false;
          }
          const changeKeys: string[] = [];
          const prevGlobalState = cloneDeep(globalState);
          globalState = cloneDeep(
            Object.keys(state).reduce((_globalState, changeKey) => {
              if (isMaster || _globalState.hasOwnProperty(changeKey)) {
                changeKeys.push(changeKey);
                return Object.assign(_globalState, { [changeKey]: state[changeKey] });
              }
              console.warn(`[qiankun] '${changeKey}' not declared when init state!`);
              return _globalState;
            }, globalState),
          );
          if (changeKeys.length === 0) {
            console.warn('[qiankun] state has not changed!');
            return false;
          }
          // 觸發(fā)全局監(jiān)聽
          emitGlobal(globalState, prevGlobalState);
          return true;
        },
        // 注銷該應(yīng)用下的依賴
        offGlobalStateChange() {
          delete deps[id];
          return true;
        },
      };
    }

    可以看到在initGlobalState函數(shù)的執(zhí)行中完成了一個(gè)發(fā)布訂閱模式的創(chuàng)建工作,并返回了相關(guān)的訂閱/發(fā)布/注銷方法。接著qiankun將這些返回的方法通過生命周期函數(shù)mount傳遞給子應(yīng)用,這樣子應(yīng)用就能夠拿到并使用全局狀態(tài)了,從而應(yīng)用間的通信就得以實(shí)現(xiàn)了。此外offGlobalStateChange會(huì)在子應(yīng)用unmount時(shí)自動(dòng)調(diào)用以解除該子應(yīng)用的訂閱,避免內(nèi)存泄露。

    讀到這里,這篇“微前端框架qiankun隔離方法怎么使用”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識(shí)點(diǎn)還需要大家自己動(dòng)手實(shí)踐使用過才能領(lǐng)會(huì),如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI