【源码】微前端qiankun源码阅读(2):加载子应用与沙箱隔离

前言

上一篇文章了解了qiankun的整体运行。下面继续看:
1.qiankun如何根据entry字段去加载子应用的资源。
2.qiankun提供的沙箱隔离。

正文

(1) loadApp

在上一篇中说到single-spa的app配置需要开发者自己处理加载子应用的逻辑,在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,
    });
  });
}

然后进入到src/loader.ts中查看loadApp都做了什么:

image.png

首先使用importEntry将entry传入,获取到 template, execScripts, assetPublicPath。importEntry这个包的作用就是,给它一个站点链接,它请求到站点的整个html,然后解析出html的各个内容:dom、script、css等,然后根据需要去加载。下面是其全部返回的输出:

image.png

这里也许会有疑问是:为什么像上一篇中直接使用动态加载script技术一样,直接将整个html append到domcument加载出来,不是很省事情吗?
这是因为qiankun要做沙箱隔离,所以先自己解析出资源,处理后再加载到页面。

对于dom和style,qiankun会对其进行一些包装,然后使用getRender下的render方法,将内容append到容器中:

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

最终容器内容如下:

image.png
(2) execScripts 与沙箱隔离

上图可以看到script标签都被注释掉了,下面要使用execScripts去执行JS。在importEntry中查看execScripts:

image.png

其可以传入一个沙箱,让JS都在这个沙箱中执行。回到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,
    );
    // 用沙箱的代理对象作为接下来使用的全局对象
    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一开始默认为window环境,后判断如果sandbox为true,使用createSandboxContainersandboxContainer.instance.proxy来替换。在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隔离机制,分别是 SnapshotSandbox、LegacySandbox和ProxySandbox。

三个沙箱的原理,都比较简单:
SnapshotSandbox:快照沙箱。就是在加载子应用时浅拷贝一份window,名为windowSnapshot。在卸载子应用时,再使用windowSnapshot将window复原。下面是自己的简单实现:

image.png

LegacySandbox
LegacySandbox和快照沙箱差不多,不同的是,其使用Proxy劫持set操作,记录那些被更改的window属性。这样在后续的状态还原时候就不再需要遍历window的所有属性来进行对比,提升了程序运行的性能。但是它最终还是去修改了window上的属性,所以这种机制仍然污染了window的状态。

ProxySandbox:看了LegacySandbox会有疑问,既然都用了代理了,修改代理对象就好了,为什么经过代理后还去修改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初始为一个对象,通过劫持set操作,将value保存到fakeWindow即可,不用去修改window,所以也不用复原操作。

(3) execScripts原理

终于弄明白传入execScripts的沙箱是个啥玩意了,下面回到import-html-entry的execScripts中:

image.png

主要看到getExecutableScriptevalCode,我们有了沙箱后,如何让JS代码在沙箱环境执行呢?

看到getExecutableScript,它将传入的scriptText进行了一层自执行函数包裹,自执行函数接收代理对象,然后函数参数名为window。这样子,scriptText中对window的访问,实际都是访问到代理对象!

image.png

得到最终的code后,调用evalCode

image.png

可以看到,evalCode就是简单的调用eval,执行我们的JS代码,由此实现应用的JS Bundle加载!

总结

这一篇主要讲的是qiankun的loadApps,如何根据entry字段去加载子应用的资源、以及提供的沙箱来执行JS。大概流程就是这样。


image.png

另外有个疑问是,无论是快照沙箱还是代理沙箱,只能监听到window上第一层的key值,对于更深层的对象,如果被修改了那还是会被污染的。

参考

微前端-最容易看懂的微前端知识
微前端01 : 乾坤的Js隔离机制原理剖析(快照沙箱、两种代理沙箱)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容