前言
在微前端实践过程中有一个必然会遇到的问题:全局作用域变量的污染问题,具体来说就是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方案是比较优雅和实用的方案