微前端qiankun沙箱實(shí)現(xiàn)源碼解讀
前言
上篇我們介紹了微前端實(shí)現(xiàn)沙箱的幾種方式,沒看過的可以下看下JS沙箱這篇內(nèi)容,掃盲一下。接下來我們通過源 碼詳細(xì)分析下qiankun沙箱實(shí)現(xiàn),我們clone下qiankun代碼,代碼主要在sandbox文件夾下,目錄結(jié)構(gòu)為
├── common.ts ├── index.ts // 入口文件 ├── legacy │ └── sandbox.ts // 代理沙箱(單實(shí)例) ├── patchers // 該暫時不用關(guān)心,主要是給沙箱打補(bǔ)丁增強(qiáng)沙箱能力 │ ├── __tests__ │ ├── css.ts │ ├── dynamicAppend │ ├── historyListener.ts │ ├── index.ts │ ├── interval.ts │ └── windowListener.ts ├── proxySandbox.ts // 代理沙箱(多實(shí)例) └── snapshotSandbox.ts //快照沙箱
我們主要關(guān)注 proxySandbox.ts, snapshotSandbox.ts 文件和 legacy 文件夾。patchers 文件夾的內(nèi)容主要為了給我們實(shí)例的沙箱打補(bǔ)丁,增強(qiáng)沙箱的一些能力先不用關(guān)注。
從上面分析我們可看出 qiankun JS沙箱主要有snapshotSandbox快照沙箱,legacySandbox單實(shí)例代理沙箱,proxySandbox多實(shí)例代理沙箱。
我們從入口文件index.ts可以看到創(chuàng)建沙箱的代碼
let sandbox: SandBox;
if (window.Proxy) {
sandbox = useLooseSandbox ? new LegacySandbox(appName) : new ProxySandbox(appName);
} else {
sandbox = new SnapshotSandbox(appName);
}
我們可以看出如果瀏覽器支持Proxy就用LegacySandbox或ProxySandbox沙箱,比較老的瀏覽器用SnapshotSandbox沙箱,現(xiàn)在在支持proxy的瀏覽器qiankun里主要用ProxySandbox。
下面各種沙箱我們具體分析一下
LegacySandbox單實(shí)例沙箱
/**
* 判斷該屬性也能從對應(yīng)的對象上被刪除
*/
function isPropConfigurable(target: typeof window, prop: PropertyKey) {
const descriptor = Object.getOwnPropertyDescriptor(target, prop);
return descriptor ? descriptor.configurable : true;
}
/**
* 設(shè)置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 實(shí)現(xiàn)的沙箱
* TODO: 為了兼容性 singular 模式下依舊使用該沙箱,等新沙箱穩(wěn)定之后再切換
*/
export default class SingularProxySandbox implements SandBox {
/** 沙箱期間新增的全局變量 */
private addedPropsMapInSandbox = new Map<PropertyKey, any>();
/** 沙箱期間更新的全局變量 */
private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();
/** 持續(xù)記錄更新的(新增和修改的)全局變量的 map,用于在任意時刻做 snapshot */
private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();
name: string; // 名稱
proxy: WindowProxy; // 初始化代理對象
type: SandBoxType; // 沙箱類型
sandboxRunning = true; // 沙箱是否在運(yùn)行
latestSetProp: PropertyKey | null = null; // 最后設(shè)置的props
/**
* 激活沙箱的方法
*/
active() {
if (!this.sandboxRunning) {
// 之前記錄新增和修改的全局變量更新到當(dāng)前window上。
this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
}
this.sandboxRunning = true; // 設(shè)置沙箱在運(yùn)行
}
/**
* 失活沙箱的方法
*/
inactive() {
// 失活沙箱把記錄的初始值還原回去
this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
// 沙箱失活的時候把新增的屬性從window上給刪除
this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));
this.sandboxRunning = false; // 設(shè)置沙箱不在運(yùn)行
}
constructor(name: string) {
this.name = name;
this.type = SandBoxType.LegacyProxy;
const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this;
const rawWindow = window; // 獲取當(dāng)前window對象
const fakeWindow = Object.create(null) as Window; // 創(chuàng)建一個代理對象的window對象
const proxy = new Proxy(fakeWindow, {
set: (_: Window, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) { // 判斷沙箱是否在啟動
if (!rawWindow.hasOwnProperty(p)) {
// 當(dāng)前window上沒有該屬性,在addedPropsMapInSandbox上記錄添加的屬性
addedPropsMapInSandbox.set(p, value);
} else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
// 如果當(dāng)前 window 對象存在該屬性,且 record map 中未記錄過,則記錄該屬性初始值
const originalValue = (rawWindow as any)[p];
modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
}
// 記錄新增和修改的屬性
currentUpdatedPropsValueMap.set(p, value);
// 必須重新設(shè)置 window 對象保證下次 get 時能拿到已更新的數(shù)據(jù)
(rawWindow as any)[p] = value;
// 更新下最后設(shè)置的props
this.latestSetProp = p;
return true;
}
// 在 strict-mode 下,Proxy 的 handler.set 返回 false 會拋出 TypeError,在沙箱卸載的情況下應(yīng)該忽略錯誤
return true;
},
get(_: Window, p: PropertyKey): any {
// 判斷用window.top, window.parent等也返回代理對象,在ifream環(huán)境也會返回代理對象。做到了真正的隔離,
if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
return proxy;
}
const value = (rawWindow as any)[p];
return getTargetValue(rawWindow, value); // 返回當(dāng)前值
},
/**
* 用 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對象,通過激活沙箱時還原子應(yīng)用的狀態(tài),卸載時還原主應(yīng)用的狀態(tài)來實(shí)現(xiàn)沙箱隔離的。跟我們上篇文章的簡單實(shí)現(xiàn)不同點(diǎn)qiankun做了兼容,在健壯性和嚴(yán)謹(jǐn)性都比較好。
接下來,我們重點(diǎn)看下現(xiàn)役的ProxySandbox沙箱
ProxySandbox多實(shí)例沙箱
我們先看創(chuàng)建fakeWindow的方法,這里很巧妙,主要是把window上不支持改變和刪除的屬性,但有g(shù)et方法的屬性創(chuàng)建到fakeWindow上。這里有幾個我們平常在業(yè)務(wù)開發(fā)用的不多的幾個API,主要是Object.getOwnPropertyDescriptor和Object.defineProperty。具體詳細(xì)細(xì)節(jié),可以參考Object static function
/**
* 創(chuàng)建一個FakeWindow, 把window上不支持改變和刪除的屬性創(chuàng)建到我們創(chuàng)建的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) {
// 判斷有g(shù)et屬性,說明可以獲取該屬性值
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, // 記錄有g(shù)et方法的屬性
};
}
前期工作已準(zhǔn)備好,接下來我們看沙箱的主要代碼
// 全局變量,記錄沙箱激活的數(shù)量
let activeSandboxCount = 0;
/**
* 基于 Proxy 實(shí)現(xiàn)的沙箱
*/
export default class ProxySandbox implements SandBox {
/** window 值變更記錄 */
private updatedValueSet = new Set<PropertyKey>();
name: string; // 名稱
proxy: WindowProxy; // 初始化代理對象
type: SandBoxType; // 沙箱類型
sandboxRunning = true; // 沙箱是否在運(yùn)行
latestSetProp: PropertyKey | null = null; // 最后設(shè)置的props
active() {
// 沙箱激活記,記錄激活數(shù)量
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}
inactive() {
// 失活沙箱,減去激活數(shù)量
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創(chuàng)建一個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把值復(fù)制到代理對象上,
Object.defineProperty(target, p, {
configurable,
enumerable,
writable,
value,
});
}
} else {
// window上沒有屬性,支持設(shè)置值
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,在沙箱卸載的情況下應(yīng)該忽略錯誤
return true;
},
get(target: FakeWindow, p: PropertyKey): any {
if (p === Symbol.unscopables) return unscopables;
// 判斷用window.top, window.parent等也返回代理對象,在ifream環(huán)境也會返回代理對象。做到了真正的隔離,
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對象就直接返回,相當(dāng)月共享一些全局變量
if (p === 'document' || p === 'eval') {
setCurrentRunningSandboxProxy(proxy);
nextTick(() => setCurrentRunningSandboxProxy(null));
switch (p) {
case 'document':
return document;
case 'eval':
return eval;
}
}
// 返回當(dāng)前值
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++;
}
}
整體我們可以看到先創(chuàng)建fakeWindow對象,然后對這個對象進(jìn)行代理,ProxySandbox不會操作window上的實(shí)例,會使用fakeWindow上的屬性,從而實(shí)現(xiàn)多實(shí)例。
實(shí)現(xiàn)代理的過程中還對 as、ownKeys、getOwnPropertyDescriptor、defineProperty、deleteProperty做了重新定義,會保證沙箱的健壯性和完整性。
跟我們上篇文章有點(diǎn)不一樣的就是共享對象,qiankun直接寫死了,只有doucument和eval是共享的。
最后我們來看下snapshotSandbox沙箱,相對比較簡單
SapshotSandbox 快照沙箱
/**
* 基于 diff 方式實(shí)現(xiàn)的沙箱,用于不支持 Proxy 的低版本瀏覽器
*/
export default class SnapshotSandbox implements SandBox {
name: string; // 名稱
proxy: WindowProxy; // 初始化代理對象
type: SandBoxType; // 沙箱類型
sandboxRunning = true; // 沙箱是否在運(yùn)行
private windowSnapshot!: Window; // 當(dāng)前快照
private modifyPropsMap: Record<any, any> = {}; // 記錄修改的屬性
constructor(name: string) {
this.name = name;
this.proxy = window;
this.type = SandBoxType.Snapshot;
}
active() {
// 記錄當(dāng)前快照
this.windowSnapshot = {} as Window;
iter(window, (prop) => {
this.windowSnapshot[prop] = window[prop];
});
// 恢復(fù)之前的變更
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]) {
// 記錄變更,恢復(fù)環(huán)境
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
});
this.sandboxRunning = false;
}
}
快照沙箱比較簡單,激活的時候?qū)ψ兏膶傩宰鲂┯涗洠Щ畹臅r候移除這些記錄,還有運(yùn)行期間所有的屬性都報(bào)存在window上,所有只能是單實(shí)例。
結(jié)束語
參考
以上就是JS沙箱,qiankun實(shí)現(xiàn)的比較完善,各種情況基本都考慮到了。下篇我們說一下css常見的隔離方案,更多關(guān)于微前端qiankun沙箱的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript變量類型以及變量之間的轉(zhuǎn)換你了解嗎
這篇文章主要為大家詳細(xì)介紹了JavaScript變量類型以及變量之間的轉(zhuǎn)換,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-02-02
JavaScript預(yù)解析及相關(guān)技巧分析
這篇文章主要介紹了JavaScript預(yù)解析及相關(guān)技巧,結(jié)合實(shí)例形式分析了JavaScript與解析的原理,步驟與相關(guān)技巧,需要的朋友可以參考下2016-04-04
bootstrap select2插件用ajax來獲取和顯示數(shù)據(jù)的實(shí)例
今天小編就為大家分享一篇bootstrap select2插件用ajax來獲取和顯示數(shù)據(jù)的實(shí)例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08
javascript二維數(shù)組轉(zhuǎn)置實(shí)例
這篇文章主要介紹了javascript二維數(shù)組轉(zhuǎn)置方法,實(shí)例分析了數(shù)組行列交換的轉(zhuǎn)置技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-01-01
javascript中l(wèi)ayim之查找好友查找群組
這篇文章主要介紹了javascript中l(wèi)ayim之查找好友查找群組,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02
node在兩個div之間移動,用ztree實(shí)現(xiàn)
本文介紹了“node在兩個div之間移動,用ztree實(shí)現(xiàn)”的方法,需要的朋友可以參考一下2013-03-03

