Qiankun原理詳解JS沙箱是如何做隔離
前言
相信大家也知道 qiankun 有 SnapshotSandbox, LegacySandbox 和 ProxySandbox 這些沙箱,而它們又可以分為單例和多例兩種模式,網上也有很多文章對其進行介紹。
但這些文章的關注點都是沙箱的環(huán)境恢復做的事,那 JS 的隔離到底是怎么做到的呢?
換個問法,當我寫 window.a = 1 的時候,a 是怎么被掛載到這些 XXXSandbox 上的呢?又或者我直接云修改 window.a = 123 時,JS 沙箱到底是怎么隔離這個 a 的呢?
總不能這樣吧:
window = window.sandbox window.a = 1 // window.sandbox.a = 1
這篇文章就來簡單聊聊 qiankun 沙箱那些事。
復習一下沙箱
這里我們還是稍微復習一下 qiankun 的三大沙箱吧。
SanpshotSandbox
第一種是快照沙箱。
它的原理是:把主應用的 window 對象做淺拷貝,將 window 的鍵值對存成一個 Hash Map。之后無論微應用對 window 做任何改動,當要在恢復環(huán)境時,把這個 Hash Map 又應用到 window 上就可以了。 大概如下圖所示。

稍微做下小結:
- 微應用 mount 時
- 先把上一次記錄的變更 modifyPropsMap 應用到微應用的全局 window,沒有則跳過
- 淺復制主應用的 window key-value 快照,用于下次恢復全局環(huán)境
- 微應用 unmount 時
- 將當前微應用 window 的 key-value 和 快照 的 key-value 進行 Diff,Diff 出來的結果用于下次恢復微應用環(huán)境的依據
- 將上次快照的 key-value 拷貝到主應用的 window 上,以此恢復環(huán)境
LegacySandbox
上面的 SnapshotSandbox 有一個問題:每次微應用 unmount 時都要對每個屬性值做一次 Diff,類似這樣:
for (const prop in window) {
if (window[prop] !== this.windowSnapshot[prop]) {
// 記錄微應用的變更
this.modifyPropsMap[prop] = window[prop];
// 恢復主應用的環(huán)境
window[prop] = this.windowSnapshot[prop];
}
}
如果有 1000 個屬性就要對比 1000 次,不是那么優(yōu)雅。
LegacySandbox 的想法則是 通過監(jiān)聽對 window 的修改來直接記錄 Diff 內容,因為只要對 window 屬性進行設置,那么就會有兩種情況:
- 如果是新增屬性,那么存到 addedMap 里
- 如果是更新屬性,那么把原來的鍵值存到 prevMap,把新的鍵值存到 newMap
(當然這里的變量名做了簡化)
通過 addedMap, prevMap 和 newMap 這三個變量就能反推出微應用以及原來環(huán)境的變化,qiankun 也能以此作為恢復環(huán)境的依據。

當然這里的監(jiān)聽用到了 ES6 的新語法 Proxy,不過這里先不展開討論,在之后的系列文章上會會自己手動實現(xiàn)一個簡單的沙箱。
ProxySandbox
前面兩種沙箱都是 單例模式 下使用的沙箱。也即一個頁面中只能同時展示一個微應用,而且無論是 set 還是 get 依然是直接操作 window 對象。
在這樣單例模式下,當微應用修改全局變量時依然會在原來的 window 上做修改,因此如果在同一個路由頁面下展示多個微應用時,依然會有環(huán)境變量污染的問題。
為了避免真實的 window 被污染,qiankun 實現(xiàn)了 ProxySandbox。它的想法是:
- 把當前 window 的一些原生屬性(如document, location等)拷貝出來,單獨放在一個對象上,這個對象也稱為 fakeWindow
- 之后對每個微應用分配一個 fakeWindow
- 當微應用修改全局變量時:
- 如果是原生屬性,則修改全局的 window
- 如果是原生屬性,則修改 fakeWindow 里的內容
- 微應用獲取全局變量時:
- 如果是原生屬性,則從 window 里拿
- 如果不是原生屬性,則優(yōu)先從 fakeWindow 里獲取
這樣一來連恢復環(huán)境都不需要了,因為每個微應用都有自己一個環(huán)境,當在 active 時就給這個微應用分配一個 fakeWindow,當 inactive 時就把這個 fakeWindow 存起來,以便之后再利用。

隔離原理
看完上面,你大概也知道了這些沙箱是怎么恢復環(huán)境的 但是,回到我們的問題:qiankun 是怎么把 a 和這些沙箱聯(lián)系起來呢?也即寫下 window.a = 1 是怎么做到對 a 變量隔離的呢?
這個邏輯的實現(xiàn)并不在 qiankun 的源碼里,而是在它所依賴的 import-html-entry 中,這里做一下簡化:
const executableScript = `
;(function(window, self, globalThis){
;${scriptText}${sourceUrl}
}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);
`
eval.call(window, executableScript)
把上面字符串代碼展開來看看:
function fn(window, self, globalThis) {
// 你的 JavaScript code
}
const bindedFn = fn.bind(window.proxy);
bindedFn(window.proxy, window.proxy, window.proxy);
可以發(fā)現(xiàn)這里的代碼做了三件事:
- 把要執(zhí)行 JS 代碼放在一個立即執(zhí)行函數(shù)中,且函數(shù)入參有 window, self, globalThis
- 給這個函數(shù) 綁定上下文 window.proxy
- 執(zhí)行這個函數(shù),并 把上面提到的沙箱對象 window.proxy 作為入參分別傳入
因此,當我們在 JS 文件里有 window.a = 1 時,實際上會變成:
function fn(window, self, globalThis) {
window.a = 1;
}
const bindedFn = fn.bind(window.proxy);
bindedFn(window.proxy, window.proxy, window.proxy);
那么此時,window.a 的 window 就不是全局 window 而是 fn 的入參 window 了。又因為我們把 window.proxy 作為入參傳入,所以 window.a 實際上為 window.proxy.a = 1。這也正好解釋了 qiankun 的 JS 隔離邏輯。
XXX is undefined
不知道看完上面的實現(xiàn),你有沒有發(fā)現(xiàn)問題。
假如現(xiàn)在代碼里有隱式聲明或調用全局對象的代碼:
add = (a, b) => {
return a + b
}
add(1, 2)
當這樣調用 add 時,上下文 this 則為剛剛綁定的 window.proxy。由于隱式聲明 add 不會自動掛載到 window.proxy 上,所以當執(zhí)行 add,eval 就會報 add is undefined。詳見 這個 Issue。
不要覺得這種情況不會發(fā)生,實際上,這還是挺常見的:
- 老舊的第三方 SDK JS 文件
- Webpack 插件引入的 JS
- 公司網關層自動注入的 JS
- 等等...
我之前就遇到過這種情況:比如下面 Webpack 會注入腳手架定義好的 CDN 資源重試邏輯:
<script>
var __JS_RETRY__ = {};
function __rpReport(data) {
console.log('__rpReport');
}
function __rpJsReport(loadType, msidType, url) {
console.log('__rpJsReport');
}
function __retryPlugin(event) {
console.log('retryPlugin')
}
// 改成下面就可以了
// window.__JS_RETRY__ = {};
//
// window.__rpReport = (data) => {
// console.log('__rpReport');
// }
//
// window.__rpJsReport = (loadType, msidType, url) => {
// console.log('__rpJsReport');
// }
//
// window.__retryPlugin = (event) => {
// console.log('retryPlugin')
// }
</script>
這個問題的解決的方法也很簡單:
- 把代碼 a = 1 改成 window.a
- 添加全局聲明 window a
這樣一來,你就得每次打包代碼以及發(fā)布時執(zhí)行一個腳本來做這些文本替換,非常麻煩。而京東的新微應用框架 MicroApp 則提供了一套插件系統(tǒng):

它可以讓開發(fā)者在執(zhí)行 JS 前去做代碼文本的替換:
import microApp from '@micro-zoe/micro-app'
microApp.start({
plugins: {
// ...
modules: {
'appName1': [{
loader(code, url, options) {
if (url === 'xxx.js') {
// 替換有問題的代碼
code = code.replace('var abc =', 'window.abc =')
}
return code
}
}],
}
}
})
如果要對接別的團隊的微應用時,而且正好他們有 a = 1 這樣的代碼,那么在加載微應用的時候直接修復全局變量的問題,不需要通知他們修改,也不失為一種策略吧。
總結
總結一下,qiankun 一共有 3 種沙箱:
- SnapshotSandbox:記錄 window 對象,每次 unmount 都要和微應用的環(huán)境進行 Diff
- LegacySandbox:在微應用修改 window.xxx 時直接記錄 Diff,將其用于環(huán)境恢復
- ProxySandbox:為每個微應用分配一個 fakeWindow,當微應用操作 window 時,其實是在 fakeWindow 上操作
要和這些沙箱結合起來使用,qiankun 會把要執(zhí)行的 JS 包裹在立即執(zhí)行函數(shù)中,通過綁定上下文和傳參的方式來改變 this 和 window 的值,讓它們指向 window.proxy 沙箱對象,最后再用 eval 來執(zhí)行這個函數(shù)。
以上就是Qiankun原理詳解JS沙箱是如何做隔離的詳細內容,更多關于Qiankun原理JS沙箱隔離的資料請關注腳本之家其它相關文章!
相關文章
JQ中$(window).load和$(document).ready區(qū)別與執(zhí)行順序
JQ中的$(document).ready()大家應該用的非常多,基本每個JS腳本中都有這個函數(shù)的出現(xiàn)有時甚至會出現(xiàn)多個,那么另一個加載函數(shù)$(window).load相對出現(xiàn)的次數(shù)就很少了,下面為大家介紹一下兩者的區(qū)別與他們的執(zhí)行順序2017-03-03
JS跨域(Access-Control-Allow-Origin)前后端解決方案詳解
這篇文章主要介紹了瀏覽器跨域(Access-Control-Allow-Origin)解決方案詳解包括了前端跨域,后端跨域,js原生實現(xiàn)jsonp,jQuery實現(xiàn)jsonp,vue.js實現(xiàn)jsonp,需要的朋友可以參考下2022-01-01
自定義range?sliders滑塊實現(xiàn)元素拖動方法
這篇文章主要為大家介紹了自定義range?sliders滑塊實現(xiàn)元素拖動方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08

