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

作者:杰出D 时间:2024-05-02 16:10:25 

前言

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

来源:https://juejin.cn/post/6981756262304186405

标签:微前端,qiankun,沙箱
0
投稿

猜你喜欢

  • 游戏开发进阶Unity网格(Mesh\\动态合批\\骨骼动画\\蒙皮)

    2022-03-18 11:20:26
  • 对SQL Server聚集索引的指示综合描述

    2010-08-31 14:25:00
  • 轻松实现TensorFlow微信跳一跳的AI

    2021-11-24 10:35:40
  • 让复杂导航设计变得简单

    2008-01-07 11:50:00
  • 使用BootStrap和Metroui设计的metro风格微网站或手机app界面

    2024-05-02 17:32:09
  • SpringBoot首页设置解析(推荐)

    2021-11-03 05:43:00
  • taobao cdn的缓存?

    2009-09-19 17:21:00
  • python爬取网页转换为PDF文件

    2023-02-11 08:48:24
  • 利用Python实现自动生成数据日报

    2022-08-26 11:12:48
  • 在EditPlus中配置Perl开发编译环境

    2023-12-04 08:34:08
  • python频繁写入文件时提速的方法

    2023-11-11 01:48:40
  • K-means聚类算法介绍与利用python实现的代码示例

    2023-07-29 11:08:55
  • python moviepy 的用法入门篇

    2022-09-03 11:41:16
  • Python数据分析库pandas基本操作方法

    2022-07-17 23:15:18
  • Python 微信爬虫完整实例【单线程与多线程】

    2023-08-19 23:12:58
  • W3C优质网页小贴士(三)

    2008-04-09 13:32:00
  • FrontPage XP设计教程3——网页的布局

    2008-10-11 12:20:00
  • Git撤销已经推送(push)至远端仓库的提交(commit)信息操作

    2022-05-31 04:33:28
  • Python随手笔记之标准类型内建函数

    2022-10-23 11:00:45
  • Python 如何实现文件自动去重

    2021-07-16 13:50:54
  • asp之家 网络编程 m.aspxhome.com