深入讲解qiankun的JS沙箱隔离机制原理与实践

前端 潘老师 3天前 11 ℃ (0) 扫码查看

乾坤(qiankun)作为一款热门的微前端框架,其中,沙箱隔离机制是保障微应用独立运行的关键,尤其是JS沙箱,在防止微应用之间的代码冲突方面发挥着重要作用。接下来,我们就深入探究一下qiankun中JS沙箱的原理与实现。

一、为什么需要JS沙箱

在实际开发中,我们经常会遇到一些棘手的问题。比如,我之前参与一个JSP项目时,就发现JSON.stringify({name: '张三'})的结果是'"{\\"name\\":\\"张三\\"}"',而当使用JSON.parse()解析时,得到的竟然还是字符串。经过仔细审查,才发现原来是JSON方法被重写了。

这种情况并非个例,有些插件为了实现特定功能,会重写很多方法。像Vue2为了实现数据监听,重写了Array的一系列方法;single - spa为了监听路由,重写了pushStatereplaceState等方法(不过这些重写一般不会影响原API的正常使用)。但像我遇到的JSON方法被重写的情况,就直接影响了原API的执行结果。

qiankun中默认开启了js、window沙箱,这是非常有必要的。它主要通过三种沙箱来实现隔离:SnapshotSandboxProxySandbox以及下文会提到的LegacySandbox(原文未详细提及,简单介绍下)。接下来,我们分别深入了解一下这几种沙箱的实现原理。

二、SnapshotSandbox沙箱

(一)SnapshotSandbox沙箱的原理

SnapshotSandbox是针对不支持proxy api的低版本浏览器设计的,它采用快照的形式来实现沙箱隔离。简单来说,就是在微应用加载时,记录下当前window对象的状态(也就是拍个“快照”),当微应用离开时,对比当前window对象和快照的差异,把更改的部分恢复,并记录这些变更;再次进入微应用时,恢复之前记录的变更。

(二)SnapshotSandbox沙箱的代码实现

下面我们结合代码来深入理解一下:

import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces';

// 定义一个函数,用于循环遍历window对象的属性,并对每个属性执行回调函数
function iter(obj: typeof window, callbackFn: (prop: any) => void) { 
  // 使用for...in循环遍历对象的属性
  // eslint-disable-next-line guard-for-in, no-restricted-syntax
  for (const prop in obj) { 
    // 处理兼容性问题,对于clearInterval属性特殊处理
    if (obj.hasOwnProperty(prop) || prop === 'clearInterval') { 
      callbackFn(prop);
    }
  }
}

/**
 * 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
 */
export default class SnapshotSandbox implements SandBox {
  proxy: WindowProxy;
  name: string;
  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() { 
    // 记录当前window的快照
    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]; 
      }
    });

    if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] ${this.name} origin window restore...`, Object.keys(this.modifyPropsMap));
    }

    this.sandboxRunning = false;
  }
}

在这段代码中:

  • active方法在微应用首次加载时被调用,它先创建一个空的windowSnapshot对象,然后通过iter函数遍历window对象,将每个属性的值复制到windowSnapshot中,完成当前window状态的记录。接着,恢复之前离开微应用时记录的变更。
  • inactive方法在离开微应用时执行,它先清空modifyPropsMap,然后再次遍历window对象,对比当前属性值和windowSnapshot中的值。如果有差异,就将当前值记录到modifyPropsMap中,并把window对象的属性值恢复为快照中的值。

需要注意的是,快照沙箱只是进行了一层浅拷贝对比。例如,对console对象内部方法的更改可能不会被记录下来,这种情况可能会影响其他子应用。如果有更复杂的需求,开发者可能需要自行记录原本的变更。

三、ProxySandbox沙箱

(一)ProxySandbox沙箱的原理

ProxySandbox是针对支持proxy的浏览器实现的沙箱机制。它的核心原理是利用JavaScript的Proxy对象,创建一个代理对象来拦截对window对象的访问,从而实现对微应用环境的隔离。

(二)ProxySandbox沙箱的简易实现示例

为了更好地理解,我们先来看一个简易版本的实现示例:

const proxy = {}
(function(window) {
  // code 部分
    console.log(window,window.console, console) // {}, undefined, console {debug: ƒ, error: ƒ, info: ƒ, log: ƒ, warn: ƒ, …}
}(proxy))

在这个示例中,我们创建了一个空对象proxy,并将其作为参数传递给一个立即执行函数。在函数内部,打印windowwindow.consoleconsole。可以看到,window是一个空对象,window.consoleundefined,而console是正常的console对象。这表明,通过这种方式,我们可以在一定程度上模拟一个独立的window环境。

(三)解决兼容性问题与实现真正的隔离

然而,上述示例还存在一些问题,比如如何兼容console的API呢?这就用到了with语句。with语句可以扩展一个语句的作用域链,允许在代码块中直接使用对象的属性和方法,而无需重复引用对象。但需要注意的是,with语句在严格模式下是被禁止使用的,因为它可能会使代码难以理解和维护。

在实际的ProxySandbox实现中,我们可以这样使用with语句:

const proxy = new Proxy(window, {})
(function(window) {
  with (proxy) {
     // code 部分
    console.log(window,window.console, console) // {}, undefined, console {debug: ƒ, error: ƒ, info: ƒ, log: ƒ, warn: ƒ, …}
  }
}(proxy))

这样,就解决了使用window全局API并且走代理的问题。但这种简单的代理方式也会污染原本的window对象(被浅拷贝了)。例如:

a = {name: 1, age: 2} {name:1, age:2} 
b = new Proxy(a, {}) < Proxy (0bject) {name: 1, age: 2} 
b.six =3 <3 
a <{name:1, age: 2, six: 3} 

可以看到,对代理对象b添加属性,会影响到原对象a

(四)qiankun中ProxySandbox的具体实现

那么,乾坤中的ProxySandbox是如何实现的呢?我们来看一下createFakeWindowAPI的代码:

const rawObjectDefineProperty = Object.defineProperty;

function createFakeWindow(globalContext: Window) {
  const propertiesWithGetter = new Map<PropertyKey, boolean>();
  const fakeWindow = {} as FakeWindow;

  Object.getOwnPropertyNames(globalContext)
   .filter((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
      return!descriptor?.configurable;
    }) 
   .forEach((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(globalContext, p); 
      if (descriptor) {
        const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
        if (
          p === 'top' ||
          p === 'parent' ||
          p ==='self' ||
          p === 'window' ||
          (process.env.NODE_ENV === 'test' && (p ==='mockTop' || p ==='mockSafariTop'))
        ) {
          descriptor.configurable = true;
          if (!hasGetter) {
            descriptor.writable = true;
          }
        }
        if (hasGetter) propertiesWithGetter.set(p, true);
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor)); 
      }
    });

  return {
    fakeWindow,
    propertiesWithGetter,
  };
}

在这段代码中:

  • 首先,通过Object.getOwnPropertyNames获取globalContext(也就是window对象)的所有属性名,并过滤掉那些可配置的属性。
  • 然后,对剩下的属性获取其描述符,并根据属性名进行一些特殊处理,比如将topparentselfwindow等属性设置为可配置的。
  • 最后,使用Object.defineProperty将这些属性定义到fakeWindow对象中,并冻结它们,防止意外修改。

通过这种方式,创建了一个相对独立的fakeWindow对象,实现了一定程度的环境隔离。在实际应用中,如果在乾坤的一个子应用环境下更改了window原本全局的属性,在子应用中这个属性也会被更改。这是因为在proxyget操作中,如果fakeWindow没有该API,还是会从全局的window上获取。至于为什么没有对window上的API进行深拷贝做绝对隔离,大概率是出于性能方面的考虑。

四、关于qiankun沙箱的常见问题解答

(一)import-html-entry中的子应用信息缓存会造成内存泄漏吗?

const styleCache = {};
const scriptCache = {};
const embedHTMLCache = {};

正常情况下,这种缓存不会造成内存泄漏。因为它是随着加载过的子应用数量增长的,并非持续无限制地增长。除非在特殊场景下,比如子应用数量成百上千,并且都在同一个tab页面执行过,才可能出现问题。在一般情况下,开发者可以放心使用。

(二)如果研发团队代码都很标准,是否可以不开启沙箱?

如果团队代码编写非常规范,且不在window对象上放置全局方法,同时注意bodyhtml:root等相关元素的使用,那么是可以不开启沙箱的。实际上,在正常项目中,也很少会在全局window上放置东西,所以在满足上述条件时,不开启沙箱也是可行的。

通过对qiankun中JS沙箱的深入分析,我们了解了其原理、实现方式以及在实际应用中的一些注意事项。希望这些内容能帮助大家在使用qiankun进行微前端开发时,更好地理解和运用沙箱隔离机制。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/front/17830.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】