【源碼】微前端qiankun源碼閱讀(2):加載子應(yīng)用與沙箱隔離

前言

上一篇文章了解了qiankun的整體運(yùn)行。下面繼續(xù)看:
1.qiankun如何根據(jù)entry字段去加載子應(yīng)用的資源。
2.qiankun提供的沙箱隔離。

正文

(1) loadApp

在上一篇中說(shuō)到single-spa的app配置需要開(kāi)發(fā)者自己處理加載子應(yīng)用的邏輯,在qiankun的registerMicroApps中,封裝了loadApp方法。

export function registerMicroApps<T extends ObjectType>(
  apps: Array<RegistrableApp<T>>,
  lifeCycles?: FrameworkLifeCycles<T>,
) {
  // Each app only needs to be registered once
  const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name));

  microApps = [...microApps, ...unregisteredApps];

  unregisteredApps.forEach((app) => {
    const { name, activeRule, loader = noop, props, ...appConfig } = app;

    registerApplication({
      name,
      app: async () => {
        loader(true);
        await frameworkStartedDefer.promise;

        const { mount, ...otherMicroAppConfigs } = (
          await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
        )();

        return {
          mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
          ...otherMicroAppConfigs,
        };
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}

然后進(jìn)入到src/loader.ts中查看loadApp都做了什么:

image.png

首先使用importEntry將entry傳入,獲取到 template, execScripts, assetPublicPath。importEntry這個(gè)包的作用就是,給它一個(gè)站點(diǎn)鏈接,它請(qǐng)求到站點(diǎn)的整個(gè)html,然后解析出html的各個(gè)內(nèi)容:dom、script、css等,然后根據(jù)需要去加載。下面是其全部返回的輸出:

image.png

這里也許會(huì)有疑問(wèn)是:為什么像上一篇中直接使用動(dòng)態(tài)加載script技術(shù)一樣,直接將整個(gè)html append到domcument加載出來(lái),不是很省事情嗎?
這是因?yàn)閝iankun要做沙箱隔離,所以先自己解析出資源,處理后再加載到頁(yè)面。

對(duì)于dom和style,qiankun會(huì)對(duì)其進(jìn)行一些包裝,然后使用getRender下的render方法,將內(nèi)容append到容器中:

      if (element) {
        rawAppendChild.call(containerElement, element);
      }

最終容器內(nèi)容如下:

image.png
(2) execScripts 與沙箱隔離

上圖可以看到script標(biāo)簽都被注釋掉了,下面要使用execScripts去執(zhí)行JS。在importEntry中查看execScripts:

image.png

其可以傳入一個(gè)沙箱,讓JS都在這個(gè)沙箱中執(zhí)行。回到qiankun的loadApp方法中:

export async function loadApp<T extends ObjectType>(
  app: LoadableApp<T>,
  configuration: FrameworkConfiguration = {},
  lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {
  const { entry, name: appName } = app;
  const {
    singular = false,
    sandbox = true,
    excludeAssetFilter,
    globalContext = window,
    ...importEntryOpts
  } = configuration;
  ... ...
  const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
  let global = globalContext;

  const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose;
  let sandboxContainer;
  if (sandbox) {
    sandboxContainer = createSandboxContainer(
      appInstanceId,
      // FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
      initialAppWrapperGetter,
      scopedCSS,
      useLooseSandbox,
      excludeAssetFilter,
      global,
    );
    // 用沙箱的代理對(duì)象作為接下來(lái)使用的全局對(duì)象
    global = sandboxContainer.instance.proxy as typeof window;
    mountSandbox = sandboxContainer.mount;
    unmountSandbox = sandboxContainer.unmount;
  }

  ... ...

  // get the lifecycle hooks from module exports
  const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox);

從上面可以看到,global一開(kāi)始默認(rèn)為window環(huán)境,后判斷如果sandbox為true,使用createSandboxContainersandboxContainer.instance.proxy來(lái)替換。在src/sandbox/index.ts下查看createSandboxContainer

import LegacySandbox from './legacy/sandbox';
import ProxySandbox from './proxySandbox';
import SnapshotSandbox from './snapshotSandbox';

export function createSandboxContainer(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  scopedCSS: boolean,
  useLooseSandbox?: boolean,
  excludeAssetFilter?: (url: string) => boolean,
  globalContext?: typeof window,
) {
  let sandbox: SandBox;
  if (window.Proxy) {
    sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) : new ProxySandbox(appName, globalContext);
  } else {
    sandbox = new SnapshotSandbox(appName);
  }
  ... ...
}

可以看到qiankun有三種JS隔離機(jī)制,分別是 SnapshotSandbox、LegacySandbox和ProxySandbox。

三個(gè)沙箱的原理,都比較簡(jiǎn)單:
SnapshotSandbox:快照沙箱。就是在加載子應(yīng)用時(shí)淺拷貝一份window,名為windowSnapshot。在卸載子應(yīng)用時(shí),再使用windowSnapshot將window復(fù)原。下面是自己的簡(jiǎn)單實(shí)現(xiàn):

image.png

LegacySandbox
LegacySandbox和快照沙箱差不多,不同的是,其使用Proxy劫持set操作,記錄那些被更改的window屬性。這樣在后續(xù)的狀態(tài)還原時(shí)候就不再需要遍歷window的所有屬性來(lái)進(jìn)行對(duì)比,提升了程序運(yùn)行的性能。但是它最終還是去修改了window上的屬性,所以這種機(jī)制仍然污染了window的狀態(tài)。

ProxySandbox:看了LegacySandbox會(huì)有疑問(wèn),既然都用了代理了,修改代理對(duì)象就好了,為什么經(jīng)過(guò)代理后還去修改window啊:

    const proxy = new Proxy(fakeWindow, {
      set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
        if (this.sandboxRunning) {
          this.registerRunningApp(name, proxy);
          // We must kept its description while the property existed in globalContext before
          if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) {
            const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
            const { writable, configurable, enumerable } = descriptor!;
            if (writable) {
              Object.defineProperty(target, p, {
                configurable,
                enumerable,
                writable,
                value,
              });
            }
          } else {
            // @ts-ignore
            target[p] = value;
          }
          ... ...
        }
      },
    });

上面代碼中fakeWindow初始為一個(gè)對(duì)象,通過(guò)劫持set操作,將value保存到fakeWindow即可,不用去修改window,所以也不用復(fù)原操作。

(3) execScripts原理

終于弄明白傳入execScripts的沙箱是個(gè)啥玩意了,下面回到import-html-entry的execScripts中:

image.png

主要看到getExecutableScriptevalCode,我們有了沙箱后,如何讓JS代碼在沙箱環(huán)境執(zhí)行呢?

看到getExecutableScript,它將傳入的scriptText進(jìn)行了一層自執(zhí)行函數(shù)包裹,自執(zhí)行函數(shù)接收代理對(duì)象,然后函數(shù)參數(shù)名為window。這樣子,scriptText中對(duì)window的訪(fǎng)問(wèn),實(shí)際都是訪(fǎng)問(wèn)到代理對(duì)象!

image.png

得到最終的code后,調(diào)用evalCode

image.png

可以看到,evalCode就是簡(jiǎn)單的調(diào)用eval,執(zhí)行我們的JS代碼,由此實(shí)現(xiàn)應(yīng)用的JS Bundle加載!

總結(jié)

這一篇主要講的是qiankun的loadApps,如何根據(jù)entry字段去加載子應(yīng)用的資源、以及提供的沙箱來(lái)執(zhí)行JS。大概流程就是這樣。


image.png

另外有個(gè)疑問(wèn)是,無(wú)論是快照沙箱還是代理沙箱,只能監(jiān)聽(tīng)到window上第一層的key值,對(duì)于更深層的對(duì)象,如果被修改了那還是會(huì)被污染的。

參考

微前端-最容易看懂的微前端知識(shí)
微前端01 : 乾坤的Js隔離機(jī)制原理剖析(快照沙箱、兩種代理沙箱)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,283評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,953評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,714評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,186評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,410評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,776評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,210評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,654評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容