前言

上篇我们介绍了微前端实现沙箱的几种方式,没看过的可以下看下JS沙箱这篇内容,扫盲一下。接下来我们通过源 码详细分析下qiankun沙箱实现,我们clone下qiankun代码,代码主要在sandbox文件夹下,目录结构为

├── common.ts
├── index.ts             // 入口文件
├── legacy
│   └── sandbox.ts       // 代理沙箱(单实例)
├── patchers             // 该暂时不用关心,主要是给沙箱打补丁增强沙箱能力
│   ├── __tests__
│   ├── css.ts
│   ├── dynamicAppend
│   ├── historyListener.ts
│   ├── index.ts
│   ├── interval.ts
│   └── windowListener.ts
├── proxySandbox.ts       // 代理沙箱(多实例)
└── snapshotSandbox.ts    //快照沙箱

我们主要关注 proxySandbox.ts, snapshotSandbox.ts 文件和 legacy 文件夹。patchers 文件夹的内容主要为了给我们实例的沙箱打补丁,增强沙箱的一些能力先不用关注。

从上面分析我们可看出 qiankun JS沙箱主要有snapshotSandbox快照沙箱,legacySandbox单实例代理沙箱,proxySandbox多实例代理沙箱。

我们从入口文件index.ts可以看到创建沙箱的代码

  let sandbox: SandBox;
  if (window.Proxy) {
    sandbox = useLooseSandbox ? new LegacySandbox(appName) : new ProxySandbox(appName);
  } else {
    sandbox = new SnapshotSandbox(appName);
  }

我们可以看出如果浏览器支持Proxy就用LegacySandbox或ProxySandbox沙箱,比较老的浏览器用SnapshotSandbox沙箱,现在在支持proxy的浏览器qiankun里主要用ProxySandbox。

下面各种沙箱我们具体分析一下

LegacySandbox单实例沙箱

/**
 * 判断该属性也能从对应的对象上被删除
 */
function isPropConfigurable(target: typeof window, prop: PropertyKey) {
  const descriptor = Object.getOwnPropertyDescriptor(target, prop);
  return descriptor ? descriptor.configurable : true;
}
/**
 * 设置window属性
 * @param prop
 * @param value
 * @param toDelete 是否是删除属性
 */
function setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) {
  if (value === undefined && toDelete) {
    delete (window as any)[prop];
  } else if (isPropConfigurable(window, prop) && typeof prop !== 'symbol') {
    Object.defineProperty(window, prop, { writable: true, configurable: true });
    (window as any)[prop] = value;
  }
}
/**
 * 基于 Proxy 实现的沙箱
 * TODO: 为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换
 */
export default class SingularProxySandbox implements SandBox {
  /** 沙箱期间新增的全局变量 */
  private addedPropsMapInSandbox = new Map<PropertyKey, any>();
  /** 沙箱期间更新的全局变量 */
  private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();
  /** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */
  private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();
  name: string; // 名称
  proxy: WindowProxy; // 初始化代理对象
  type: SandBoxType; // 沙箱类型
  sandboxRunning = true; // 沙箱是否在运行
  latestSetProp: PropertyKey | null = null; // 最后设置的props
  /**
   * 激活沙箱的方法
   */
  active() {
    if (!this.sandboxRunning) {
      // 之前记录新增和修改的全局变量更新到当前window上。
      this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
    }
    this.sandboxRunning = true; // 设置沙箱在运行
  }
  /**
   * 失活沙箱的方法
   */
  inactive() {
    // 失活沙箱把记录的初始值还原回去
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
    // 沙箱失活的时候把新增的属性从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; // 获取当前window对象
    const fakeWindow = Object.create(null) as Window; // 创建一个代理对象的window对象
    const proxy = new Proxy(fakeWindow, {
      set: (_: Window, p: PropertyKey, value: any): boolean => {
        if (this.sandboxRunning) { // 判断沙箱是否在启动
          if (!rawWindow.hasOwnProperty(p)) {
            // 当前window上没有该属性,在addedPropsMapInSandbox上记录添加的属性
            addedPropsMapInSandbox.set(p, value);
          } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
            // 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值
            const originalValue = (rawWindow as any)[p];
            modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
          }
          // 记录新增和修改的属性
          currentUpdatedPropsValueMap.set(p, value);
          // 必须重新设置 window 对象保证下次 get 时能拿到已更新的数据
          (rawWindow as any)[p] = value;
          // 更新下最后设置的props
          this.latestSetProp = p;
          return true;
        }
        // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
        return true;
      },
      get(_: Window, p: PropertyKey): any {
        // 判断用window.top, window.parent等也返回代理对象,在ifream环境也会返回代理对象。做到了真正的隔离,
        if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
          return proxy;
        }
        const value = (rawWindow as any)[p];
        return getTargetValue(rawWindow, value); // 返回当前值
      },
      /**
       * 用 in 操作判断属性是否存在的时候去window上判断,而不是在代理对象上判断
       */
      has(_: Window, p: string | number | symbol): boolean {
        return p in rawWindow;
      },
      /**
       * 获取对象属性描述的时候也是从window上去判断,代理对象上可能没有
       */
      getOwnPropertyDescriptor(_: Window, p: PropertyKey): PropertyDescriptor | undefined {
        const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
        if (descriptor && !descriptor.configurable) {
          descriptor.configurable = true;
        }
        return descriptor;
      },
    });
    this.proxy = proxy;
  }
}

上面代码都有注释,整个思路主要还是操作window对象,通过激活沙箱时还原子应用的状态,卸载时还原主应用的状态来实现沙箱隔离的。跟我们上篇文章的简单实现不同点qiankun做了兼容,在健壮性和严谨性都比较好。

接下来,我们重点看下现役的ProxySandbox沙箱

ProxySandbox多实例沙箱

我们先看创建fakeWindow的方法,这里很巧妙,主要是把window上不支持改变和删除的属性,但有get方法的属性创建到fakeWindow上。这里有几个我们平常在业务开发用的不多的几个API,主要是Object.getOwnPropertyDescriptor和Object.defineProperty。具体详细细节,可以参考Object static function

/**
 * 创建一个FakeWindow, 把window上不支持改变和删除的属性创建到我们创建的fake window上
 * @param global 
 * @returns 
 */
function createFakeWindow(global: Window) {
  const propertiesWithGetter = new Map<PropertyKey, boolean>();
  const fakeWindow = {} as FakeWindow;
  Object.getOwnPropertyNames(global)
    // 筛选出不可以改变或者可以删除的属性
    .filter((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      return !descriptor?.configurable;
    })
    // 重新定义这些属性可以可以改变和删除
    .forEach((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      if (descriptor) {
        // 判断有get属性,说明可以获取该属性值
        const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
        if (
          p === 'top' ||
          p === 'parent' ||
          p === 'self' ||
          p === 'window'
        ) {
          descriptor.configurable = true;
          if (!hasGetter) {
            descriptor.writable = true;
          }
        }
        if (hasGetter) propertiesWithGetter.set(p, true);
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
      }
    });
  return {
    fakeWindow,
    propertiesWithGetter, // 记录有get方法的属性
  };
}

前期工作已准备好,接下来我们看沙箱的主要代码

// 全局变量,记录沙箱激活的数量
let activeSandboxCount = 0;
/**
 * 基于 Proxy 实现的沙箱
 */
export default class ProxySandbox implements SandBox {
  /** window 值变更记录 */
  private updatedValueSet = new Set<PropertyKey>();
  name: string; // 名称
  proxy: WindowProxy; // 初始化代理对象
  type: SandBoxType; // 沙箱类型
  sandboxRunning = true; // 沙箱是否在运行
  latestSetProp: PropertyKey | null = null; // 最后设置的props
  active() {
    // 沙箱激活记,记录激活数量
    if (!this.sandboxRunning) activeSandboxCount  ;
    this.sandboxRunning = true;
  }
  inactive() {
    // 失活沙箱,减去激活数量
    if (--activeSandboxCount === 0) {
      // 在白名单的属性要从window上删除
      variableWhiteList.forEach((p) => {
        if (this.proxy.hasOwnProperty(p)) {
          delete window[p];
        }
      });
    }
    this.sandboxRunning = false;
  }
  constructor(name: string) {
    this.name = name;
    this.type = SandBoxType.Proxy;
    const { updatedValueSet } = this;
    const rawWindow = window;
    // 通过createFakeWindow创建一个fakeWindow对象
    const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);
    const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
    const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);
    // 代理 fakeWindow
    const proxy = new Proxy(fakeWindow, {
      set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
        if (this.sandboxRunning) {
          // 判断window上有该属性,并获取到属性的 writable, configurable, enumerable等值。
          if (!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)) {
            const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
            const { writable, configurable, enumerable } = descriptor!;
            if (writable) {
              // 通过defineProperty把值复制到代理对象上,
              Object.defineProperty(target, p, {
                configurable,
                enumerable,
                writable,
                value,
              });
            }
          } else {
            // window上没有属性,支持设置值
            target[p] = value;
          }
          // 存放一些变量的白名单
          if (variableWhiteList.indexOf(p) !== -1) {
            // @ts-ignore
            rawWindow[p] = value;
          }
          // 记录变更记录
          updatedValueSet.add(p);
          this.latestSetProp = p;
          return true;
        }
        // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
        return true;
      },
      get(target: FakeWindow, p: PropertyKey): any {
        if (p === Symbol.unscopables) return unscopables;
        // 判断用window.top, window.parent等也返回代理对象,在ifream环境也会返回代理对象。做到了真正的隔离,
        if (p === 'window' || p === 'self') {
          return proxy;
        }
        if (p === 'globalThis') {
          return proxy;
        }
        if (
          p === 'top' ||
          p === 'parent'
        ) {
          if (rawWindow === rawWindow.parent) {
            return proxy;
          }
          return (rawWindow as any)[p];
        }
        // hasOwnProperty的值表示为rawWindow.hasOwnProperty
        if (p === 'hasOwnProperty') {
          return hasOwnProperty;
        }
        // 如果获取document和eval对象就直接返回,相当月共享一些全局变量
        if (p === 'document' || p === 'eval') {
          setCurrentRunningSandboxProxy(proxy);
          nextTick(() => setCurrentRunningSandboxProxy(null));
          switch (p) {
            case 'document':
              return document;
            case 'eval':
              return eval;
          }
        }
        // 返回当前值
        const value = propertiesWithGetter.has(p)
          ? (rawWindow as any)[p]
          : p in target
          ? (target as any)[p]
          : (rawWindow as any)[p];
        return getTargetValue(rawWindow, value);
      },
      /**
       * 以下这些方法都是在对象的处理上做了很多的兼容,保证沙箱的健壮性和完整性
       */
      has(target: FakeWindow, p: string | number | symbol): boolean {
      },
      getOwnPropertyDescriptor ....
      this.proxy = proxy;
      activeSandboxCount  ;
  }
}

整体我们可以看到先创建fakeWindow对象,然后对这个对象进行代理,ProxySandbox不会操作window上的实例,会使用fakeWindow上的属性,从而实现多实例。

实现代理的过程中还对 as、ownKeys、getOwnPropertyDescriptor、defineProperty、deleteProperty做了重新定义,会保证沙箱的健壮性和完整性。

跟我们上篇文章有点不一样的就是共享对象,qiankun直接写死了,只有doucument和eval是共享的。

最后我们来看下snapshotSandbox沙箱,相对比较简单

SapshotSandbox 快照沙箱

/**
 * 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
 */
export default class SnapshotSandbox implements SandBox {
  name: string; // 名称
  proxy: WindowProxy; // 初始化代理对象
  type: SandBoxType; // 沙箱类型
  sandboxRunning = true; // 沙箱是否在运行
  private windowSnapshot!: Window; // 当前快照
  private modifyPropsMap: Record<any, any> = {}; // 记录修改的属性
  constructor(name: string) {
    this.name = name;
    this.proxy = window;
    this.type = SandBoxType.Snapshot;
  }
  active() {
    // 记录当前快照
    this.windowSnapshot = {} as Window;
    iter(window, (prop) => {
      this.windowSnapshot[prop] = window[prop];
    });
    // 恢复之前的变更
    Object.keys(this.modifyPropsMap).forEach((p: any) => {
      window[p] = this.modifyPropsMap[p];
    });
    this.sandboxRunning = true;
  }
  inactive() {
    this.modifyPropsMap = {};
    iter(window, (prop) => {
      if (window[prop] !== this.windowSnapshot[prop]) {
        // 记录变更,恢复环境
        this.modifyPropsMap[prop] = window[prop];
        window[prop] = this.windowSnapshot[prop];
      }
    });
    this.sandboxRunning = false;
  }
}

快照沙箱比较简单,激活的时候对变更的属性做些记录,失活的时候移除这些记录,还有运行期间所有的属性都报存在window上,所有只能是单实例。

结束语

参考

  • Object static function
  • qiankun

以上就是JS沙箱,qiankun实现的比较完善,各种情况基本都考虑到了。下篇我们说一下css常见的隔离方案,更多关于微前端qiankun沙箱的资料请关注Devmax其它相关文章!

微前端qiankun沙箱实现源码解读的更多相关文章

  1. 如何分离iOS APNS通知的沙箱和生产设备令牌

    我不小心并在同一个db表中混合沙箱和生产设备令牌.它导致一些安装生产应用程序的设备无法接收推送通知.如何从db表中分离沙箱令牌和生产令牌?非常感谢您的帮助!!

  2. xcode – Mac App Store拒绝 – 未启用应用沙箱

    我已将我的应用程序提交到MacAppStore,并且验证正常.但是,我继续使用以下内容获取无效二进制消息;AppsandBoxnotenabled–Thefollowingexecutablesmustincludethe“com.apple.security.app-sandBox”entitlementwithaBooleanvalueoftrueintheentitlementsproper

  3. 如何在Xcode 4中编码和沙箱助手应用?

    这就是问题:我有一个包含HelperApp的MainApp.Helper应用程序用于登录项目,因此我需要区分MainApp和HelperApp软件包ID.由于BuildPhasecopy,我将HelperApp复制到MainApp中.如果我代码和沙箱HelperApp上传阶段停止…

  4. ios – instagram沙箱用户邀请已发送但用户在哪里接受?

    我正在使用Instagram并且能够将邀请发送给我的第二个用户.但是,当第二个用户登录此链接时:他们得到了{“code”:403,“error_type”:“OAuthForbiddenException”,“error_message”:“您不是此客户端的沙箱用户”}我还以该用户身份登录到官方Instagram应用程序,但我仍无法接受邀请.沙箱用户如何接受邀请才能使用我的应用?解决方法弄清楚了.

  5. swift2 – Playground Xcode:无法获得沙箱扩展

    我在IOS9xcode项目中的.playground文件中出现此错误消息:我所做的是:>开始了Xcode7/Swift2项目>使用“podinstallAlamofire”>使用以下代码创建.playground文件:进口Alamofire如何在.playground文件中测试此库而不会出错?根据业主的说法,从版本3开始,游乐场支持似乎已被删除:Weremovedtheplaygroundinth

  6. Vue qiankun微前端实现详解

    这篇文章主要为大家介绍了Vue qiankun微前端实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  7. 微前端架构ModuleFederationPlugin源码解析

    这篇文章主要为大家介绍了微前端架构ModuleFederationPlugin源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  8. 浅谈前端JS沙箱实现的几种方式

    在微前端领域当中,沙箱是很重要的一件事情。本文就浅谈前端JS沙箱实现的几种方式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. 详解ant-design-pro使用qiankun微服务

    这篇文章主要介绍了ant-design-pro使用qiankun微服务详解,其实微服务需要有主应用和子应用, 一个子应用可以配置多个相关联的主应用,配置方法都是一样的,对ant-design-pro微服务配置相关知识,感兴趣的朋友一起看看吧

  10. 微前端qiankun沙箱实现源码解读

    这篇文章主要为大家介绍了微前端qiankun沙箱实现的源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部