We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
之前分享过qiankun在组内的落地情况,简单分析了JS沙箱的加载流程和隔离原理。本文基于之前的分享,对qiankun的JS沙箱隔离原理进一步进行解析。
qiankun提供了三种JS沙箱:
后面两种统称为代理沙箱,因为都是基于Proxy实现的;不同场景条件下使用不同的沙箱。先回顾下JS沙箱的加载流程,简单看下qiankun是如何初始化沙箱的。
当前的版本,默认情况下,不管单例还是多例,用的都是ProxySandbox,若浏览器环境不支持Proxy,则使用SnapshotSandbox,如果想要使用LegacySandbox,需要手动配置sandbox: { loose: true }。
// https://github.com/umijs/qiankun/blob/master/src/sandbox/index.ts export function createSandboxContainer( appName: string, elementGetter: () => HTMLElement | ShadowRoot, scopedCSS: boolean, useLooseSandbox?: boolean, excludeAssetFilter?: (url: string) => boolean, globalContext?: typeof window, ) { let sandbox: SandBox; // 当前环境是否支持Proxy if (window.Proxy) { // 是否配置loose模式 sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) : new ProxySandbox(appName, globalContext); } else { // 不支持Proxy sandbox = new SnapshotSandbox(appName); } return { instance: sandbox, async mount() { sandbox.active(); }, async unmount() { sandbox.inactive(); }, }; }
SnapshopSandbox是基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器。该沙箱主要有两个中间变量:
windowSnapshot
modifyPropsMap
兼容IE。
单例沙箱;遍历window进行diff,性能差;会污染到window。
// https://github.com/umijs/qiankun/blob/master/src/sandbox/snapshotSandbox.ts 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() { this.windowSnapshot = {} as Window; // 遍历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 = {}; // 遍历window,从快照中获取初始值,恢复环境 iter(window, (prop) => { if (window[prop] !== this.windowSnapshot[prop]) { // 记录变更 this.modifyPropsMap[prop] = window[prop]; window[prop] = this.windowSnapshot[prop]; } }); this.sandboxRunning = false; } }
LegacySandbox是基于Proxy实现的单例沙箱。该沙箱主要用到了三个变量:
沙箱激活时,遍历currentUpdatedPropsValueMap,若有数据,则还原上一次卸载前的数据到window。
window对象发生修改时,使用代理的set方法进行拦截:
判断window是否有该属性,没有的话则为新增数据,将该属性添加到addedPropsMapInSandbox对象中。
判断modifiedPropsOriginalValueMapInSandbox是否有该属性,没有的话说明该属性暂未被记录过,记录该属性的初始值。
将该属性记录到currentUpdatedPropsValueMap中,方便随时保存快照。
将该属性的变更作用到window,保证下次get时能拿到已更新的数据。
沙箱卸载时,遍历modifiedPropsOriginalValueMapInSandbox,若有修改数据,则还原;遍历addedPropsMapInSandbox,若有新增数据,删除,还原window到初始状态。
性能较snapshotSandbox好,因为不需要遍历进行diff。
单例;不支持IE;会污染window。
// https://github.com/umijs/qiankun/blob/master/src/sandbox/legacy/sandbox.ts export default class LegacySandbox 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; globalContext: typeof window; type: SandBoxType; sandboxRunning = true; // 操作window,用于激活恢复沙箱/卸载还原window private setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) { if (value === undefined && toDelete) { // eslint-disable-next-line no-param-reassign delete (this.globalContext as any)[prop]; } else if (isPropConfigurable(this.globalContext, prop) && typeof prop !== 'symbol') { Object.defineProperty(this.globalContext, prop, { writable: true, configurable: true }); // eslint-disable-next-line no-param-reassign (this.globalContext as any)[prop] = value; } } active() { if (!this.sandboxRunning) { // 恢复window至上次卸载前的状态 this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v)); } this.sandboxRunning = true; } inactive() { // 将window上修改过的属性还原 this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v)); // 将window上新增的属性还原 this.addedPropsMapInSandbox.forEach((_, p) => this.setWindowProp(p, undefined, true)); this.sandboxRunning = false; } constructor(name: string, globalContext = window) { this.name = name; this.globalContext = globalContext; this.type = SandBoxType.LegacyProxy; const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this; const rawWindow = globalContext; const fakeWindow = Object.create(null) as Window; // set拦截,用于保存变更,刷新window const setTrap = (p: PropertyKey, value: any, originalValue: any, sync2Window = true) => { if (this.sandboxRunning) { if (!rawWindow.hasOwnProperty(p)) { // 如果当前 window 对象不存在该属性,则将该属性记录到addedPropsMapInSandbox中 addedPropsMapInSandbox.set(p, value); } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) { // 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值 modifiedPropsOriginalValueMapInSandbox.set(p, originalValue); } // 将数据保存至快照 currentUpdatedPropsValueMap.set(p, value); if (sync2Window) { // 必须重新设置 window 对象保证下次 get 时能拿到已更新的数据 (rawWindow as any)[p] = value; } this.latestSetProp = p; return true; } return true; }; const proxy = new Proxy(fakeWindow, { // proxy拦截,此处拦截了很多方法,具体见源码 set: (_: Window, p: PropertyKey, value: any): boolean => { const originalValue = (rawWindow as any)[p]; return setTrap(p, value, originalValue, true); }, }); this.proxy = proxy; } }
ProxySandbox是基于Proxy实现的沙箱,支持多例。
支持多例;不会污染window。
不支持IE。
// https://github.com/umijs/qiankun/blob/master/src/sandbox/proxySandbox.ts const variableWhiteListInDev = process.env.NODE_ENV === 'development' || window.__QIANKUN_DEVELOPMENT__ ? ['__REACT_ERROR_OVERLAY_GLOBAL_HOOK__'] : []; // who could escape the sandbox const variableWhiteList: PropertyKey[] = ['System', '__cjsWrapper', ...variableWhiteListInDev ]; let activeSandboxCount = 0; /** * 基于 Proxy 实现的沙箱 */ export default class ProxySandbox implements SandBox { /** window 值变更记录 */ private updatedValueSet = new Set<PropertyKey>(); name: string; type: SandBoxType; proxy: WindowProxy; globalContext: typeof window; sandboxRunning = true; latestSetProp: PropertyKey | null = null; private registerRunningApp(name: string, proxy: Window) { if (this.sandboxRunning) { setCurrentRunningApp({ name, window: proxy }); nextTask(() => { setCurrentRunningApp(null); }); } } active() { if (!this.sandboxRunning) activeSandboxCount++; this.sandboxRunning = true; } inactive() { if (--activeSandboxCount === 0) { variableWhiteList.forEach((p) => { if (this.proxy.hasOwnProperty(p)) { // @ts-ignore delete this.globalContext[p]; } }); } this.sandboxRunning = false; } constructor(name: string, globalContext = window) { this.name = name; this.globalContext = globalContext; this.type = SandBoxType.Proxy; const { updatedValueSet } = this; const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext); const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>(); const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key); const proxy = new Proxy(fakeWindow, { set: (target: FakeWindow, p: PropertyKey, value: any): boolean => { // 此处操作的全局对象target是代理的fakeWindow,不是真实的window对象。 if (this.sandboxRunning) { this.registerRunningApp(name, proxy); if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) { const descriptor = Object.getOwnPropertyDescriptor(globalContext, p); const { writable, configurable, enumerable } = descriptor!; if (writable) { Object.defineProperty(target, p, { configurable, enumerable, writable, value, }); } } else { // @ts-ignore target[p] = value; } if (variableWhiteList.indexOf(p) !== -1) { // @ts-ignore globalContext[p] = value; } updatedValueSet.add(p); this.latestSetProp = p; return true; } return true; }, get: (target: FakeWindow, p: PropertyKey): any => { this.registerRunningApp(name, proxy); // 若target(fakeWindow)存在该属性,则返回,不存在则从rawWindow上查找 const value = propertiesWithGetter.has(p) ? (globalContext as any)[p] : p in target ? (target as any)[p] : (globalContext as any)[p]; return getTargetValue(globalContext, value); }, }); this.proxy = proxy; activeSandboxCount++; } }
下面对几种沙箱的源码进行精简,在源代码的基础上进行删减,方便理解。模拟过程中使用了ts-node来跑ts脚本: npx ts-node a.ts。
npx ts-node a.ts
准备一些初始的数据:
// interface export type SandBox = { /** 沙箱导出的代理实体 */ proxy: MockWindow; /** 沙箱是否在运行中 */ sandboxRunning: boolean; /** latest set property */ active: () => void; /** 关闭沙箱 */ inactive: () => void; }; export type MockWindow = any; // node 环境 模拟简单版的window var window: MockWindow = { name: "bigo", a: { b: { c: { d: 123 } } } }
// snapshotSandbox.ts import { SandBox, MockWindow } from "./interface"; function iter(obj: typeof window, callbackFn: (prop: any) => void) { for (const prop in obj) { if (obj.hasOwnProperty(prop) || prop === "clearInterval") { callbackFn(prop); } } } class SnapshotSandbox implements SandBox { sandboxRunning = true; private windowSnapshot!: MockWindow; private modifyPropsMap: Record<any, any> = {}; constructor() { this.proxy = window; this.modifyPropsMap = {}; } proxy: WindowProxy; active() { // 记录当前快照 this.windowSnapshot = {} as MockWindow; 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; } } const snapshotSandbox = new SnapshotSandbox(); console.log(`window初始值: ${JSON.stringify(window)}\n`); ((window: MockWindow) => { // 激活沙箱 snapshotSandbox.active(); window.name = "bigo-active"; window.a.b.c.d = 234; console.log(`active1: ${window.name}, ${window.a.b.c.d}`); // 退出沙箱 snapshotSandbox.inactive(); console.log(`inactive: ${window.name}, ${window.a.b.c.d}`); // 激活沙箱 snapshotSandbox.active(); console.log(`re-active: ${window.name}, ${window.a.b.c.d}\n`); })(snapshotSandbox.proxy); console.warn(`沙箱激活/卸载后的window值: ${JSON.stringify(window)}`);
执行npx ts-node snapshotSandbox.ts结果如下,可以看到window的复杂对象被污染了。
npx ts-node snapshotSandbox.ts
import { SandBox, MockWindow } from "./interface"; class LegacySandbox implements SandBox { /** 沙箱期间新增的全局变量 */ private addedPropsMapInSandbox = new Map<PropertyKey, any>(); /** 沙箱期间更新的全局变量 */ private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>(); /** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */ private currentUpdatedPropsValueMap = new Map<PropertyKey, any>(); proxy: WindowProxy; sandboxRunning = true; constructor() { const rawWindow = window; const fakeWindow = Object.create(null) as MockWindow; const setTrap = (p: PropertyKey, value: any, originalValue: any) => { if (this.sandboxRunning) { if (!rawWindow.hasOwnProperty(p)) { // 新增字段,记录到addedPropsMapInSandbox this.addedPropsMapInSandbox.set(p, value); } else if (!this.modifiedPropsOriginalValueMapInSandbox.has(p)) { // 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值 this.modifiedPropsOriginalValueMapInSandbox.set(p, originalValue); } this.currentUpdatedPropsValueMap.set(p, value); // 必须重新设置 window 对象保证下次 get 时能拿到已更新的数据 (rawWindow as any)[p] = value; return true; } return true; }; const proxy = new Proxy(fakeWindow, { // 拦截方法 set: (_: MockWindow, p: PropertyKey, value: any): boolean => { const originalValue = (rawWindow as any)[p]; return setTrap(p, value, originalValue); }, get(_: MockWindow, p: PropertyKey): any { return rawWindow[p]; }, }); this.proxy = proxy; } active() { if (!this.sandboxRunning) { // 激活,还原上次卸载前的数据 this.currentUpdatedPropsValueMap.forEach((v, p) => window[p] = v); } this.sandboxRunning = true; } inactive() { // 卸载,还原window数据 this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => window[p] = v); // 删除window新增数据 this.addedPropsMapInSandbox.forEach((_, p) => delete window[p]); this.sandboxRunning = false; } } let legacySandbox = new LegacySandbox(); console.log(`window初始值: ${JSON.stringify(window)}\n`); ((window: MockWindow) => { // 激活沙箱 legacySandbox.active(); window.name = "bigo-active"; window.a.b.c.d = 234; console.log(`active: ${window.name}, ${window.a.b.c.d}`); // 退出沙箱 legacySandbox.inactive(); console.log(`inactive: ${window.name}, ${window.a.b.c.d}\n`); // 激活沙箱 legacySandbox.active(); console.log(`re-active: ${window.name}, ${window.a.b.c.d}`); legacySandbox.inactive(); console.log(`re-inactive: ${window.name}, ${window.a.b.c.d}\n`); })(legacySandbox.proxy); console.warn(`沙箱激活/卸载后的window值: ${JSON.stringify(window)}`);
执行npx ts-node legacySnadbox.ts结果如下,可以看到window的复杂对象被污染了。
npx ts-node legacySnadbox.ts
import type { SandBox, MockWindow } from "./interface"; type FakeWindow = MockWindow & Record<PropertyKey, any>; class ProxySandbox implements SandBox { proxy: WindowProxy; sandboxRunning = true; latestSetProp: PropertyKey | null = null; active() { this.sandboxRunning = true; } inactive() { this.sandboxRunning = false; } constructor() { // 深拷贝,简单版 const fakeWindow = JSON.parse(JSON.stringify(window)) const proxy = new Proxy(fakeWindow, { set: (target: FakeWindow, p: PropertyKey, value: any): boolean => { // 设置值时只操作fakeWindow if (this.sandboxRunning) { target[p] = value; } return true; }, get: (target: FakeWindow, p: PropertyKey): any => { // 先从fakeWindow获取,获取不到则从rawWindow获取 return target[p] ? target[p] : window[p]; }, }); this.proxy = proxy; } } let proxysandbox1 = new ProxySandbox(); let proxySandbox2 = new ProxySandbox(); console.log(`window初始值: ${JSON.stringify(window)}\n`); ((window: MockWindow) => { // 激活沙箱 proxysandbox1.active(); window.name = "bigo-active1"; window.a.b.c.d = 234; console.log(`active1: ${window.name}, ${window.a.b.c.d}`); // 退出沙箱 proxysandbox1.inactive(); console.log(`inactive1: ${window.name}, ${window.a.b.c.d}`); // 激活沙箱 proxysandbox1.active(); console.log(`re-active1: ${window.name}, ${window.a.b.c.d}`); proxysandbox1.inactive(); console.log(`re-inactive1: ${window.name}, ${window.a.b.c.d}\n`); })(proxysandbox1.proxy); ((window: MockWindow) => { // 激活沙箱 proxySandbox2.active(); window.name = "bigo-active2"; window.a.b.c.d = 345; console.log(`active2: ${window.name}, ${window.a.b.c.d}`); // 退出沙箱 proxysandbox1.inactive(); console.log(`inactive2: ${window.name}, ${window.a.b.c.d}`); // 激活沙箱 proxysandbox1.active(); console.log(`re-active2: ${window.name}, ${window.a.b.c.d}\n`); proxysandbox1.inactive(); console.log(`re-inactive2: ${window.name}, ${window.a.b.c.d}\n`); })(proxySandbox2.proxy); console.warn(`沙箱激活/卸载后的window值: ${JSON.stringify(window)}`);
执行npx ts-node proxySandbox.ts结果如下,可以看到window的复杂对象没有被污染,可以做到真正意义上的隔离。
npx ts-node proxySandbox.ts
以上就是我对qiankun沙箱原理的一些总结,qiankun源码的可读性比较强,推荐大伙一起去看看,也欢迎一起探讨学习,如有错误,欢迎指正~
The text was updated successfully, but these errors were encountered:
No branches or pull requests
qiankun-JS沙箱原理解析
之前分享过qiankun在组内的落地情况,简单分析了JS沙箱的加载流程和隔离原理。本文基于之前的分享,对qiankun的JS沙箱隔离原理进一步进行解析。
qiankun提供了三种JS沙箱:
后面两种统称为代理沙箱,因为都是基于Proxy实现的;不同场景条件下使用不同的沙箱。先回顾下JS沙箱的加载流程,简单看下qiankun是如何初始化沙箱的。
沙箱加载流程
当前的版本,默认情况下,不管单例还是多例,用的都是ProxySandbox,若浏览器环境不支持Proxy,则使用SnapshotSandbox,如果想要使用LegacySandbox,需要手动配置sandbox: { loose: true }。
流程图
源码
沙箱隔离原理
SnapshopSandbox
SnapshopSandbox是基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器。该沙箱主要有两个中间变量:
windowSnapshot
用于沙箱激活时记录当前window快照。modifyPropsMap
用于沙箱卸载时记录变更,沙箱激活时还原变更。沙箱激活/卸载流程
流程图
优点
兼容IE。
缺点
单例沙箱;遍历window进行diff,性能差;会污染到window。
源码
LegacySandbox
LegacySandbox是基于Proxy实现的单例沙箱。该沙箱主要用到了三个变量:
沙箱激活/卸载流程
沙箱激活时,遍历currentUpdatedPropsValueMap,若有数据,则还原上一次卸载前的数据到window。
window对象发生修改时,使用代理的set方法进行拦截:
判断window是否有该属性,没有的话则为新增数据,将该属性添加到addedPropsMapInSandbox对象中。
判断modifiedPropsOriginalValueMapInSandbox是否有该属性,没有的话说明该属性暂未被记录过,记录该属性的初始值。
将该属性记录到currentUpdatedPropsValueMap中,方便随时保存快照。
将该属性的变更作用到window,保证下次get时能拿到已更新的数据。
沙箱卸载时,遍历modifiedPropsOriginalValueMapInSandbox,若有修改数据,则还原;遍历addedPropsMapInSandbox,若有新增数据,删除,还原window到初始状态。
流程图
优点
性能较snapshotSandbox好,因为不需要遍历进行diff。
缺点
单例;不支持IE;会污染window。
源码
ProxySandbox
ProxySandbox是基于Proxy实现的沙箱,支持多例。
沙箱激活/卸载流程
流程图
优点
支持多例;不会污染window。
缺点
不支持IE。
源码
模拟
下面对几种沙箱的源码进行精简,在源代码的基础上进行删减,方便理解。模拟过程中使用了ts-node来跑ts脚本:
npx ts-node a.ts
。准备一些初始的数据:
SnapshopSandbox
执行
npx ts-node snapshotSandbox.ts
结果如下,可以看到window的复杂对象被污染了。LegacySandbox
执行
npx ts-node legacySnadbox.ts
结果如下,可以看到window的复杂对象被污染了。ProxySandbox
执行
npx ts-node proxySandbox.ts
结果如下,可以看到window的复杂对象没有被污染,可以做到真正意义上的隔离。总结
以上就是我对qiankun沙箱原理的一些总结,qiankun源码的可读性比较强,推荐大伙一起去看看,也欢迎一起探讨学习,如有错误,欢迎指正~
The text was updated successfully, but these errors were encountered: