微前端中实现沙箱环境的方案调研
前言
在微前端实践过程中有一个必然会遇到的问题:全局作用域变量的污染问题,具体来说就是window对象挂载数据会被主子应用获取和修改导致数据相互污染问题,这时候如果能在应用之间做个数据隔离,最好能实现一个沙箱环境,对解决问题很有帮助。
iframe方案
说到沙箱隔离,首先想到的是iframe,自带数据隔离能力,从iframe中获取到的window对象是一个全新和纯净的对象,然而在如果要作为沙箱执行业务代码的话是不行的,但是完全可以作为一个执行脚本环境,既安全,又简单:
const parent = window;
const frame = document.createElement('iframe');
const data = [1, 2, 3, 4, 5, 6];
// 当前页面给 iframe 发送消息
frame.onload = function (e) {
frame.contentWindow.postMessage(data);
};
document.body.appendChild(frame);
// iframe 接收到消息后处理
const code = `return dataInIframe.filter((item) => item % 2 === 0)`;
frame.contentWindow.addEventListener('message', function (e) {
const func = new frame.contentWindow.Function('dataInIframe', code);
parent.postMessage(func(e.data));
});
// 父页面接收 iframe 发送过来的消息
parent.addEventListener(
'message',
function (e) {
console.log('message from iframe:', e.data);
},
false,
);
快照方案
在微前端框架qiankun中提供了快照方案,其原理就是在应用加载之时保存最初的window对象,卸载应用之时通过diff操作记录改过的属性即制作快照,当再次激活应用的时候恢复之前的快照。该方案的缺点是会污染window导致,多个应用无法同时处于激活状态,优点是兼容性好。
// 保存差异的方式
function createSandbox(){
let originWindow = {}
let diffMap = {};
return {
toActive(){
originWindow = {};
// 保存初始window对象
Object.keys(window).forEach(prop=>{
originWindow[prop] = window[prop];
})
// 将上次退出的时候保存的差异还原回去,也就是恢复快照
Object.keys(diffMap).forEach(prop=>{
window[prop] = diffMap[prop];
})
},
toInActive(){
Object.keys(window).forEach(prop=>{
if(window[prop] !== originWindow[prop]){
// 保存差异
diffMap[prop] = window[prop]
// 还原现场
window[prop] = originWindow[prop];
}
})
}
}
}
window.originData = '最初的window上的数据';
console.log(window.originData, window.a1, window.b1); // 最初的window上的数据 undefined undefined
const sandbox1 = createSandbox(); // 创建应用的时候,同时创建沙箱
sandbox1.toActive(); // 沙箱激活
window.a1 = 'aaaaa'; // 应用修改window上的属性
console.log(window.originData, window.a1, window.b1); // 最初的window上的数据 aaaaa undefined
sandbox1.toInActive(); // 切换应用前沙箱1退出
const sandbox2 = createSandbox(); // 创建应用的时候,同时创建沙箱
sandbox2.toActive(); // 沙箱激活
console.log(window.originData, window.a1, window.b1); // 最初的window上的数据 undefined undefined
window.b1 = 'bbbbb'; // 应用修改window上的属性
console.log(window.originData, window.a1, window.b1); // 最初的window上的数据 undefined bbbbb 和上面的数据做个对比
sandbox2.toInActive(); // 从应用2切换至1
sandbox1.toActive(); // 从应用2切换至1
console.log(window.originData, window.a1, window.b1); // 最初的window上的数据 aaaaa undefined 和上面的数据做个对比
sandbox1.toInActive(); // 从应用1切换至2
sandbox2.toActive(); // 从应用1切换至2
console.log(window.originData, window.a1, window.b1); // 最初的window上的数据 undefined bbbbb 和上面的数据做个对比
代理方案
使用ES6中的proxy语法对自定义的全局对象代理,这样当在沙箱内部对window对象修改的时候,实际上修改的是自定义的全局对象,而不会影响到真正的window对象。其优点是不会污染window,支持多个应用同时激活。 缺点是部分浏览器不支持proxy,
function createProxySandBox(){
const rawWindow = window;
const fakeWindow = {};
const proxy = new Proxy(fakeWindow, {
get:(target, p)=>{
if(target.hasOwnProperty(p)){
return target[p];
}
return rawWindow[p];
},
set(target, p, value){
if(!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)){
rawWindow[p] = value
} else {
target[p] = value;
}
}
})
return proxy;
}
const sandbox1 = createProxySandBox();
((window) => {
window.a = 'a';
})(sandbox1);
const sandbox2 = createProxySandBox();
((window) => {
console.log(window.a)
window.a = 'fff';
})(sandbox2);
console.log(window.a)
总结
proxy方案是比较优雅和实用的方案