前端微应用-乾坤(qiankun)原理分析-沙箱隔离(js)

上篇中咱们分析了css沙箱,看起来并没有很重要,但是相比起来js、window沙箱的重要性就大了很多。qiankun默认是开启 的,说一下真实遇到的案例:

我之前搞一个JSP项目就发现在那个工程里面JSON.stringify({name: '张三'}),得到'"{\\"name\\":\\"张三\\"}"',看着好像并没有什么问题,但是我JSON.parse()解析时发现得到的还是字符串 ,经过审查才发现里面的JSON方法被重写 了!!!

其实这只是我遇到的一个案例,有些插件为了满足功能把好多方法都会重写,比如Vue2为了监听重写了Array的一系列方法、single-spa 为了监听路由重写了pushState、replaceState等等(这种是不影响原API使用 的)。就怕有些像我上面那个案例样子直接影响了原API 的执行结果。

可以看出 js、window很有必要,从源码中看到qiankun共实现了3种沙箱:

  • 针对不支持proxy api使用的是快照的形式实现的。SnapshotSandbox
  • 针对支持proxy api使用的是proxy的形式实现的。ProxySandbox

咱们在上一篇中有说qiankun的主应用和子应用是公用了一个HTML,那么这里你可以想下?他俩是不是公用一个window,当然是肯定的啦!

公用一个window,那么切换子应用的时候要把上一个应用放在window的方法给清除掉,不然可能就会给下个应用照成影响了。

就例如: 我们想在一个A应用 需要监听console的日志,那么咱们重写下console的API,做数据上报 。但是我切换到了B应用 没有这个需求!

那么咱们需要再A应用离开 的时候,记录下他更改了哪些东西,因为咱们再回到A应用 时还要有这些方法。并且把他对console.log给还原回来

这也就是SnapshotSandbox快照沙箱 的实现。

SnapshotSandbox 沙箱

上面咱们其实已经把快照沙箱说的很清楚了,那么这里咱们看看怎么才能把window里面的方法记录下来呢?并且还原的呢?

js 复制代码
import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces';

function iter(obj: typeof window, callbackFn: (prop: any) => void) { // 循环windows
  // eslint-disable-next-line guard-for-in, no-restricted-syntax
  for (const prop in obj) {
    // patch for clearInterval for compatible reason, see #1490
    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]; // 5. 再次载入微应用时,恢复API
    });

    this.sandboxRunning = true;

    // 1、active执行完就会接着执行 子应用
    // 2. 子应用要对window.console.log做重写
  }

  inactive() { // 离开微应用时
    this.modifyPropsMap = {};

    iter(window, (prop) => { // 3. 离开时开始跟快照做 对比 记录哪里不一样,并且把原来更改的给回退回来
      if (window[prop] !== this.windowSnapshot[prop]) {
        // 记录变更,恢复环境
        this.modifyPropsMap[prop] = window[prop]; // 记录更改 为第5步做准备
        window[prop] = this.windowSnapshot[prop]; // 4. 把变动更改回来 
      }
    });

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

    this.sandboxRunning = false;
  }
}

在上面代码中有注释,咱们这里再重复一遍,避免大家看不懂。

    1. 执行active时,也就时再微应用首次加载时,把window的快照windowSnapshot浅拷贝记录下来。
    1. 经过执行js,假如这个时候对window.console进行了重写。
    1. 要离开子应用时执行inactive,这个时候遍历当前的window对象跟快照windowSnapshot做对比。
    1. 找到不一样的地方,把原来改过的给恢复回来。并且记录都做了哪些更改modifyPropsMap.
    1. 再次进入子应用时,把modifyPropsMap里面的内容给恢复到window上。

我在给大家在看个例子如下:

经测试快照只是做了一层对比(本身就是浅拷贝),这么更改并不会被记录下来。会影响其他子应用的,如有有类似的需求需要自己记录下原本的变更。

ProxySandbox 沙箱

ProxySandbox是针对支持proxy的浏览器实现的,那么它又是如何实现的呢?

其实如果步考虑各种兼容 来看会比较好理解一点,我们先看下简易版本的ProxySandbox的实现:

其实是执行scripts时,把代理的proxy作为参数穿了进来,可以看下如下几个打印:

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

那么如何兼容consoleAPI呢?使用with,先看一个使用with的示例

js 复制代码
const person = {
    name: "Alice",
    age: 25,
    greet: function() {
        console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
    }
};

with (person) {
    console.log(name); // 输出 Alice
    console.log(age);  // 输出 25
    greet();           // 输出 Hello, my name is Alice and I am 25 years old.
}

with 语句用于扩展一个语句的作用域链。它允许你在一个代码块中使用一个对象的属性和方法,而不需要重复地引用该对象。不过,由于其可能导致代码难以理解和维护,with 语句在严格模式下是被禁止使用的。

在回到咱们的执行scripts中使用with是怎么做的呢?如下:

js 复制代码
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,并且走代理 的问题了。

如上那种代理其实也会污染原本的windows的(被浅拷贝了),可以看个代理的示例:

那么乾坤中的ProxySandbox是如何实现的呢?可以一起看下createFakeWindowAPI

ts 复制代码
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;
    }) //1. 只剩下这些全局属性: ['Infinity', 'NaN', 'undefined', 'window', 'document', 'location', 'top', 'chrome']
    .forEach((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(globalContext, p); // 2. 获取到全局属性的描述符
      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)); // 3. 创建一个不可配置的属性,并冻结它,注入到fakeWindow
      }
    });

  return {
    fakeWindow,
    propertiesWithGetter,
  };
}

可以看到代理成功并且隔离环境了:

这里看到其实咱们没有更改原本windows全局的API,只是新增了一些属性。如果我在乾坤一个子应用环境 下更改了window原本全局的属性,那么在子应用中,这个属性也会被更改。

下图是一个乾坤应用环境的示例:可以看到我在app-vue-hash应用下更改了console.log,然后在app-vue-history应用下也被更改了。

其实可以在源码proxyget上看到,如果fakeWindow没有该API,还是会从全局的window上获取的。

关于为什么没有深拷贝window上的APi 做绝对隔离,这里的话应该是出于性能方面考虑吧!

总结

到这里其实已经完成所有qiankun的分享,各个方面都详细说了它的优缺点。

因为他是容器、子应用的模式,可以把全局的方法放在容器的windows上,比如

  • 可以放封装好的axios,然后达到统一后端的API交互,就不用每个模块都引入axios的相关代码了。
  • 可以放一个发布订阅者的API,达到跨应用交互api调用的效果。
  • 如果框架都一致,可以考虑把组件都放在容器的windows上,然后子应用就可以直接用了。

Q:import-html-entry 中的子应用信息缓存,会照成内纯泄漏吗?

js 复制代码
const styleCache = {};
const scriptCache = {};
const embedHTMLCache = {};

A:正常应该是不会的,因为他不是持续增长的,他是随着你加载过的子应用数量增长的。除非在特殊场景,比如你的子应用成百上千 ,并且还都在一个tab页面都执行过了。无特殊情况下可以放心。

Q: 如果我们研发团队代码都很标准 ,那么是不是可以不用开启沙箱了?

A: 可以的,其实正常项目也很少在全局window上放东西。只要不在window放全局的方法以及注意body、html、:root那么久可以不开启。

相关推荐
iOS阿玮15 分钟前
待业的两个月,让我觉得独立开发者才是职场的归宿。
前端·app
八了个戒23 分钟前
「数据可视化 D3系列」入门第六章:比例尺的使用
前端·javascript·信息可视化·数据可视化·canvas
少糖研究所31 分钟前
ACPA算法详解
前端
Mores42 分钟前
开源 | ImageMinify:轻量级智能图片压缩工具,为你的项目瘦身加速
前端
执梦起航44 分钟前
webpack理解与使用
前端·webpack·node.js
ai大师44 分钟前
Cursor怎么使用,3分钟上手Cursor:比ChatGPT更懂需求,用聊天的方式写代码,GPT4、Claude 3.5等先进LLM辅助编程
前端
Json_1 小时前
使用vue2技术写了一个纯前端的静态网站商城-鲜花销售商城
前端·vue.js·html
1024熙1 小时前
【Qt】——理解信号与槽,学会使用connect
前端·数据库·c++·qt5
少糖研究所1 小时前
ColorThief库是如何实现图片取色的?
前端
冴羽1 小时前
SvelteKit 最新中文文档教程(22)—— 最佳实践之无障碍与 SEO
前端·javascript·svelte