微前端qiankun沙箱實現(xiàn)源碼解讀
前言
上篇我們介紹了微前端實現(xiàn)沙箱的幾種方式,沒看過的可以下看下JS沙箱這篇內(nèi)容,掃盲一下。接下來我們通過源 碼詳細分析下qiankun沙箱實現(xiàn),我們clone下qiankun代碼,代碼主要在sandbox文件夾下,目錄結(jié)構(gòu)為
├── common.ts ├── index.ts // 入口文件 ├── legacy │ └── sandbox.ts // 代理沙箱(單實例) ├── patchers // 該暫時不用關(guān)心,主要是給沙箱打補丁增強沙箱能力 │ ├── __tests__ │ ├── css.ts │ ├── dynamicAppend │ ├── historyListener.ts │ ├── index.ts │ ├── interval.ts │ └── windowListener.ts ├── proxySandbox.ts // 代理沙箱(多實例) └── snapshotSandbox.ts //快照沙箱
我們主要關(guān)注 proxySandbox.ts, snapshotSandbox.ts 文件和 legacy 文件夾。patchers 文件夾的內(nèi)容主要為了給我們實例的沙箱打補丁,增強沙箱的一些能力先不用關(guān)注。
從上面分析我們可看出 qiankun JS沙箱主要有snapshotSandbox快照沙箱,legacySandbox單實例代理沙箱,proxySandbox多實例代理沙箱。
我們從入口文件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單實例沙箱
/** * 判斷該屬性也能從對應(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 實現(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; // 沙箱是否在運行 latestSetProp: PropertyKey | null = null; // 最后設(shè)置的props /** * 激活沙箱的方法 */ active() { if (!this.sandboxRunning) { // 之前記錄新增和修改的全局變量更新到當前window上。 this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v)); } this.sandboxRunning = true; // 設(shè)置沙箱在運行 } /** * 失活沙箱的方法 */ inactive() { // 失活沙箱把記錄的初始值還原回去 this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v)); // 沙箱失活的時候把新增的屬性從window上給刪除 this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true)); this.sandboxRunning = false; // 設(shè)置沙箱不在運行 } 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; // 創(chuàng)建一個代理對象的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); // 必須重新設(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); // 返回當前值 }, /** * 用 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)來實現(xiàn)沙箱隔離的。跟我們上篇文章的簡單實現(xiàn)不同點qiankun做了兼容,在健壯性和嚴謹性都比較好。
接下來,我們重點看下現(xiàn)役的ProxySandbox沙箱
ProxySandbox多實例沙箱
我們先看創(chuàng)建fakeWindow的方法,這里很巧妙,主要是把window上不支持改變和刪除的屬性,但有g(shù)et方法的屬性創(chuàng)建到fakeWindow上。這里有幾個我們平常在業(yè)務(wù)開發(fā)用的不多的幾個API,主要是Object.getOwnPropertyDescriptor和Object.defineProperty。具體詳細細節(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方法的屬性 }; }
前期工作已準備好,接下來我們看沙箱的主要代碼
// 全局變量,記錄沙箱激活的數(shù)量 let activeSandboxCount = 0; /** * 基于 Proxy 實現(xiàn)的沙箱 */ export default class ProxySandbox implements SandBox { /** window 值變更記錄 */ private updatedValueSet = new Set<PropertyKey>(); name: string; // 名稱 proxy: WindowProxy; // 初始化代理對象 type: SandBoxType; // 沙箱類型 sandboxRunning = true; // 沙箱是否在運行 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對象就直接返回,相當月共享一些全局變量 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++; } }
整體我們可以看到先創(chuàng)建fakeWindow對象,然后對這個對象進行代理,ProxySandbox不會操作window上的實例,會使用fakeWindow上的屬性,從而實現(xiàn)多實例。
實現(xiàn)代理的過程中還對 as、ownKeys、getOwnPropertyDescriptor、defineProperty、deleteProperty做了重新定義,會保證沙箱的健壯性和完整性。
跟我們上篇文章有點不一樣的就是共享對象,qiankun直接寫死了,只有doucument和eval是共享的。
最后我們來看下snapshotSandbox沙箱,相對比較簡單
SapshotSandbox 快照沙箱
/** * 基于 diff 方式實現(xiàn)的沙箱,用于不支持 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]; }); // 恢復(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ū)ψ兏膶傩宰鲂┯涗?,失活的時候移除這些記錄,還有運行期間所有的屬性都報存在window上,所有只能是單實例。
結(jié)束語
參考
以上就是JS沙箱,qiankun實現(xiàn)的比較完善,各種情況基本都考慮到了。下篇我們說一下css常見的隔離方案,更多關(guān)于微前端qiankun沙箱的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript變量類型以及變量之間的轉(zhuǎn)換你了解嗎
這篇文章主要為大家詳細介紹了JavaScript變量類型以及變量之間的轉(zhuǎn)換,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-02-02JavaScript預(yù)解析及相關(guān)技巧分析
這篇文章主要介紹了JavaScript預(yù)解析及相關(guān)技巧,結(jié)合實例形式分析了JavaScript與解析的原理,步驟與相關(guān)技巧,需要的朋友可以參考下2016-04-04bootstrap select2插件用ajax來獲取和顯示數(shù)據(jù)的實例
今天小編就為大家分享一篇bootstrap select2插件用ajax來獲取和顯示數(shù)據(jù)的實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08javascript二維數(shù)組轉(zhuǎn)置實例
這篇文章主要介紹了javascript二維數(shù)組轉(zhuǎn)置方法,實例分析了數(shù)組行列交換的轉(zhuǎn)置技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-01-01javascript中l(wèi)ayim之查找好友查找群組
這篇文章主要介紹了javascript中l(wèi)ayim之查找好友查找群組,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02