微前端中实现沙箱环境的方案调研

前言

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

posted @ 2022-12-21 23:11  空山与新雨  阅读(448)  评论(0编辑  收藏  举报