微前端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


猜你喜欢
- 作为酷爱编程的老程序员,实在按耐不下这个冲动,Python真的是太火了,不断撩拨我的心。我是对Python存有戒备之心的,想当年我基于Dru
- ''' 爬取豆瓣电影排行榜 设计思路: &nb
- 网站可用性是任何网站的基本要素,而可用的导航更是网站所必需的要素之一。导航决定了用户如何与网站进行交互。如果没有了可用的导航,那么网站内容就
- 安装python中文分词库jieba法1:Anaconda Prompt下输入conda install jieba法2:Terminal下
- MySQL中union和order by是可以一起使用的,但是在使用中需要注意一些小问题,下面通过例子来说明。首先看下面的t1表。1、如果直
- 1、显式等待它指定要查找的节点,然后指定一个最长的等待时间,如果规定时间内加载出来了这个节点,就返回查找的节点;如果规定时间内没有加载出该节
- 一、正则表达式–元字符re 模块使 Python 语言拥有全部的正则表达式功能1. 数量词# 提取大小写字母混合的单词import rea
- 处理pdf文档第一、从文本中提取文本第二、创建PDF两种方法#使用PdfFileWriterimport PyPDF2pdfFiles =
- 如题:我写入关键字到数据库,多的时候用|隔开了,我提取再做相关文章搜索的时候,我怎么提取用|隔开的文字啊,这样我就好用关键字做搜索啊 回复:
- 例子老规矩,先上一个代码:def add(s, x): return s + xdef gen(): for i in range(4):
- 一.环境搭建1.下载安装包访问 Python官网下载地址:https://www.python.org/downloads/下载适合自己系统
- 效果图实现代码vue2 代码如下<!-- 横向柱状图测试结果 --><template> <div
- 看代码 <?php header("Content-type: text/html; charset=utf-8"
- 一、多线程同步由于CPython的python解释器在单线程模式下执行,所以导致python的多线程在很多的时候并不能很好地发挥多核cpu的
- 需求如下: 1.模板页右边包含了一个登陆div,想让没登陆的时候这个div显示,登陆后该div隐藏 2.显示一个欢迎用户的div,主要是想通
- 这篇文章主要介绍了Python pickle模块实现对象序列化,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,
- 当需要存储很多同类型的不通过数据时可能需要使用到嵌套,先用一个例子说明嵌套的使用1、在列表中存储字典#假设年级里有一群国际化的学生,有黄皮肤
- 常量:用于储存一个不会变化也不希望变化的数据的标示符(命名规则与变量相同)定义形式:使用 define() 函数定义使用形式:define(
- 代码如下:<%@LANGUAGE="VBSCRIPT" CODEPAGE="65001&quo
- 如何取回已忘记的密码?forget.asp' 申请<html><head><title>闪亮日子