微前端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沙箱的资料请关注我们其它相关文章!