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

目录
  • 前言
  • LegacySandbox单实例沙箱
  • ProxySandbox多实例沙箱
  • SapshotSandbox 快照沙箱
  • 结束语

前言

上篇我们介绍了微前端实现沙箱的几种方式,没看过的可以下看下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上,所有只能是单实例。

结束语

参考

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

(0)

相关推荐

  • ant-design-pro使用qiankun微服务配置动态主题色的问题

    使用微服务后,遇到一个问题,主应用和子应用的主题色不一致.希望可以通过主应用的颜色动态变换子应用的主题色ant-design-pro可以通过config.ts配置全局主题色所有最优的方法是可以通过改变主题色,来动态配置.官方最新版本的https://github.com/ant-design/ant-design-pro/releases/tag/v5.2.0已经支持了,通过<SettingDrawer>设置之前的版本不支持直接修改主题色.但是ant-design支持全局修改组件颜色跟着官方的

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

    目录 主应用配置 子应用配置 微服务现在挺火的,优点也很明显如果有多个应用都有相同页面时,就可以使用微服务,可以避免重复写代码 在网上搜了下,很多例子都是基于官方文档的例子,官方文档:https://umijs.org/zh-CN/plugins/plugin-qiankun.比较简单,实际使用场景会有特殊情况我根据自己项目的情况总结了一下使用方法 我们两个项目都是ant-design-pro的,我这里的例子也用的ant-design-pro项目生成的, githup地址:https://git

  • 微前端qiankun改造日渐庞大的项目教程

    项目背景 很多小伙伴在工作中都碰到过和我一样的场景,手上的某个项目越来越大,眼看着每次build时间越来越长,吐了.在杭州某独角兽我碰到了这样的一个项目,他叫运营后台,听名字就知道,他的主要用户是运营人员.问题就是随着公司业务的越来越多,这个运营后台承担的已经不是某一块业务了,而是所有业务的运营操作的中后台都在这上面.你可以这样理解,这个系统的每个一级菜单都是一块独立的业务,相互之间没有任何瓜葛:按常规的理解,这应该是单独的每一个project比较合理,但是正因为他的用户又都是公司的同一群人,他

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

    目录 前言 LegacySandbox单实例沙箱 ProxySandbox多实例沙箱 SapshotSandbox 快照沙箱 结束语 前言 上篇我们介绍了微前端实现沙箱的几种方式,没看过的可以下看下JS沙箱这篇内容,扫盲一下.接下来我们通过源 码详细分析下qiankun沙箱实现,我们clone下qiankun代码,代码主要在sandbox文件夹下,目录结构为 ├── common.ts ├── index.ts // 入口文件 ├── legacy │ └── sandbox.ts // 代理沙

  • React前端开发createElement源码解读

    目录 React 与 Babel 元素标签转译 组件转译 子元素转译 createElement 源码 函数入参 第一段代码 __self 和 __source 第二段代码 props 对象 第三段代码 children 第四段代码 defaultProps 第五段代码 owner ReactElement 源码 REACT_ELEMENT_TYPE 回顾 React 与 Babel 元素标签转译 用过 React 的同学都知道,当我们这样写时: <div id="foo">

  • 基于微前端qiankun的多页签缓存方案实践

    目录 一.多页签是什么? 1.1 单页面应用实现多页签 1.2 使用qiankun进行微前端改造后,多页签缓存有什么不同 二.方案选择 2.1 方案一:多个子应用同时存在 2.2 方案二:同一时间仅加载一个子应用,同时保存其他应用的状态 2.3 最终选择 三.具体实现 3.1 从组件级别的缓存到应用级别的缓存 3.2 移花接木——将vnode重新挂载到一个新实例上 3.3 解决应用级缓存方案的问题 3.3.1 vue-router相关问题 3.3.2 父子组件通信 3.3.3 缓存管理,防止内存

  • 详解webpack-dev-middleware 源码解读

    前言 Webpack 的使用目前已经是前端开发工程师必备技能之一.若是想在本地环境启动一个开发服务,大家只需在 Webpack 的配置中,增加 devServer的配置来启动.devServer 配置的本质是 webpack-dev-server 这个包提供的功能,而 webpack-dev-middleware 则是这个包的底层依赖. 截至本文发表前,webpack-dev-middleware 的最新版本为 webpack-dev-middleware@3.7.2,本文的源码来自于此版本.本

  • 浅谈SpringMVC请求映射handler源码解读

    请求映射源码 首先看一张请求完整流转图(这里感谢博客园上这位大神的图,博客地址我忘记了): 前台发送给后台的访问请求是如何找到对应的控制器映射并执行后续的后台操作呢,其核心为DispatcherServlet.java与HandlerMapper.在spring boot初始化的时候,将会加载所有的请求与对应的处理器映射为HandlerMapper组件.我们可以在springMVC的自动配置类中找到对应的Bean. @Bean @Primary @Override public RequestM

  • SpringBoot自定义加载yml实现方式,附源码解读

    目录 自定义加载yml,附源码解读 解决方法 源码解读 如何引入多个yml方法 方案一:无前缀,使用@Value注解 方案二:有前缀,无需@Value注解 自定义加载yml,附源码解读 昨天在对公司的微服务配置文件标准化的过程中,发现将原来的properties文件转为yml文件之后,微服务module中标记有@Configuration的配置类都不能正常工作了,究其原因,是由于@PropertySource属性默认只用于标记并告诉spring boot加载properties类型的文件 spr

  • Evil.js项目源码解读

    目录 引言 源码解析 立即执行函数 为什么要用立即执行函数? includes方法 map方法 filter方法 setTimeout Promise.then JSON.stringify Date.getTime localStorage.getItem 用途 引言 2022年8月18日,一个名叫Evil.js的项目突然走红,README介绍如下: 什么?黑心996公司要让你提桶跑路了? 想在离开前给你们的项目留点小 礼物 ? 偷偷地把本项目引入你们的项目吧,你们的项目会有但不仅限于如下的神

  • go 熔断原理分析与源码解读

    目录 正文 熔断原理 熔断器实现 hystrixBreaker和googlebreaker对比 源码解读 结束语 正文 熔断机制(Circuit Breaker)指的是在股票市场的交易时间中,当价格的波动幅度达到某一个限定的目标(熔断点)时,对其暂停交易一段时间的机制.此机制如同保险丝在电流过大时候熔断,故而得名.熔断机制推出的目的是为了防范系统性风险,给市场更多的冷静时间,避免恐慌情绪蔓延导致整个市场波动,从而防止大规模股价下跌现象的发生. 同样的,在高并发的分布式系统设计中,也应该有熔断的机

  • 通过示例源码解读React首次渲染流程

    目录 说明 题目 首次渲染流程 render beginWork completeUnitOfWork commit 准备阶段 before mutation 阶段 mutation 阶段 切换 Fiber Tree layout 阶段 题目解析 总结 说明 本文结论均基于 React 16.13.1 得出,若有出入请参考对应版本源码.参考了 React 技术揭秘. 题目 在开始进行源码分析前,我们先来看几个题目: 题目一: 渲染下面的组件,打印顺序是什么? import React from

  • [转]prototype 源码解读 超强推荐第1/3页

    复制代码 代码如下: Prototype is a JavaScript framework that aims to ease development of dynamic web applications. Featuring a unique, easy-to-use toolkit for class-driven development and the nicest Ajax library around, Prototype is quickly becoming the codeb

随机推荐