前端JS沙箱实现方案:从iframe到Proxy多实例解析

作者:木棉时间:2026-10-24 13:01:10浏览:438

本文深入解析前端微前端架构中JS沙箱的几种核心实现方式。首先介绍利用iframe天然隔离特性的方案,其次探讨不支持Proxy时的Diff快照机制,重点分析基于ES6 Proxy的单实例与多实例沙箱实现原理及代码示例,对比各方案在全局变量隔离、路由处理及多应用共存方面的优劣,为生产环境选型提供参考。

前言

在微前端架构中,JS沙箱机制至关重要。例如,single-spa框架未内置JS沙箱,构建大型微前端应用时极易引发变量冲突,给系统稳定性带来巨大风险。微前端场景下,document、location等全局对象需在各子应用间共享。由于子应用常由不同团队开发,难以强制约束全局变量的使用。此外,部分页面可能同时运行多个子应用,这就要求沙箱机制具备加载、卸载及状态恢复的能力,并支持多沙箱共存。

iframe实现沙箱

前端HTML中的iframe标签提供了一种有效的隔离手段。通过iframe对象,我们可以利用contentWindow获取原生浏览器对象,该对象天然具备所有属性,且与主应用环境完全隔离。具体实现代码如下:


let iframe  = document.createElement('iframe',{src:'about:blank'});
document.body.appendChild(iframe);
const sandboxGlobal = iframe.contentWindow;

注意:仅同域iframe可获取对应的contentWindow。将iframe的src设置为about:blank可确保同域,且避免资源加载,具体参考iframe src相关文档。

前言中提到,微前端除了需要隔离的window环境外,还需共享部分全局对象。此时可借助代理(Proxy)技术实现共享。具体代码实现如下:


class SandboxWindow {
    /**
     * 构造函数
     * @param {*} context 需要共享的对象
     * @param {*} frameWindow iframe的window
     */
    constructor(context, frameWindow) {
        
        return new Proxy(frameWindow, {
            get(target, name) {
                if (name in context) { // 优先使用共享对象
                    return context[name];
                }
                return target[name];
            },
            set(target, name, value) {
                if (name in context) { // 修改共享对象的值
                    return context[name] = value;
                }
                target[name] = value;
            }
        })
    }
}

// 需要全局共享的变量
const context = { document:window.document, history: window.history }

// 创建沙箱
const newSandboxWindow = new SandboxWindow(context, sandboxGlobal); 

// 判断沙箱上的对象和全局对象是否相等
console.log('equal',newSandboxWindow.document === window.document)

newSandboxWindow.abc = '1'; //在沙箱上添加属性
console.log(window.abc);   // 在全局上查看属性
console.log(newSandboxWindow.abc) //在沙箱上查看属性

运行上述代码,观察执行结果。

前言

通过上述iframe沙箱方案,可实现以下特性:

  • 全局变量隔离,例如setTimeout、location及不同版本的React可相互隔离。
  • 路由隔离,应用既可实现独立路由,也可共享全局路由。
  • 支持多实例,允许同时运行多个独立的微应用。

diff方式实现沙箱

在不支持Proxy的浏览器环境中,可通过Diff方式实现沙箱。应用运行时保存window对象的快照,将当前window的所有属性复制至快照对象。子应用卸载时,对window对象修改进行Diff计算,将差异属性存入modifyMap。再次挂载时,将这些修改的属性重新应用至window。具体代码如下:


class DiffSandbox {
  constructor(name) {
    this.name = name;
    this.modifyMap = {}; // 存放修改的属性
    this.windowSnapshot = {};
  }
  active() {
    // 缓存active状态的沙箱
    this.windowSnapshot = {};
    for (const item in window) {
      this.windowSnapshot[item] = window[item];
    }

    Object.keys(this.modifyMap).forEach(p => {
      window[p] = this.modifyMap[p];
    })

  }

  inactive() {
    for (const item in window) {
      if (this.windowSnapshot[item] !== window[item]) {
        // 记录变更
        this.modifyMap[item] = window[item];
        // 还原window
        window[item] = this.windowSnapshot[item];
      }
    }
  }
}

const diffSandbox = new DiffSandbox('diff沙箱');
diffSandbox.active();  // 激活沙箱
window.a = '1'
console.log('开启沙箱:',window.a);
diffSandbox.inactive(); //失活沙箱
console.log('失活沙箱:', window.a);
diffSandbox.active();   // 重新激活
console.log('再次激活', window.a);

运行代码,查看实际输出结果。

前言

该方案不支持多实例,因为运行期间所有属性均直接保存在window对象上,无法实现真正的隔离。

基于代理(Proxy)实现单实例沙箱

在ES6环境中,可利用Proxy对象劫持window对象的修改。基本逻辑是记录window对象的变更,在卸载时删除这些记录,在应用再次激活时恢复记录,从而模拟沙箱环境。具体代码如下:


// 修改window属性的公共方法
const updateWindowProp = (prop, value, isDel) => {
    if (value === undefined || isDel) {
        delete window[prop];
    } else {
        window[prop] = value;
    }
}

class ProxySandbox {

    active() {
        // 根据记录还原沙箱
        this.currentUpdatedPropsValueMap.forEach((v, p) => updateWindowProp(p, v));
    }
    inactive() {
        // 1 将沙箱期间修改的属性还原为原先的属性
        this.modifiedPropsMap.forEach((v, p) => updateWindowProp(p, v));
        // 2 将沙箱期间新增的全局变量消除
        this.addedPropsMap.forEach((_, p) => updateWindowProp(p, undefined, true));
    }

    constructor(name) {
        this.name = name;
        this.proxy = null;
        // 存放新增的全局变量
        this.addedPropsMap  = new Map(); 
        // 存放沙箱期间更新的全局变量
        this.modifiedPropsMap = new Map();
        // 存在新增和修改的全局变量,在沙箱激活的时候使用
        this.currentUpdatedPropsValueMap = new Map();

        const { addedPropsMap, currentUpdatedPropsValueMap, modifiedPropsMap } = this;
        const fakeWindow = Object.create(null);
        const proxy = new Proxy(fakeWindow, {
            set(target, prop, value) {
                if (!window.hasOwnProperty(prop)) {
                    // 如果window上没有的属性,记录到新增属性里
                    // debugger;
                    addedPropsMap.set(prop, value);
                } else if (!modifiedPropsMap.has(prop)) {
                    // 如果当前window对象有该属性,且未更新过,则记录该属性在window上的初始值
                    const originalValue = window[prop];
                    modifiedPropsMap.set(prop, originalValue);
                }
                // 记录修改属性以及修改后的值
                currentUpdatedPropsValueMap.set(prop, value);
                // 设置值到全局window上
                updateWindowProp(prop, value);
                return true;
            },
            get(target, prop) {
                return window[prop];
            },
        });
        this.proxy = proxy;
    }
}


const newSandBox = new ProxySandbox('代理沙箱');
const proxyWindow = newSandBox.proxy;
proxyWindow.a = '1'
console.log('开启沙箱:', proxyWindow.a, window.a);
newSandBox.inactive(); //失活沙箱
console.log('失活沙箱:', proxyWindow.a, window.a);
newSandBox.active(); //失活沙箱
console.log('重新激活沙箱:', proxyWindow.a, window.a);

运行代码,观察执行结果。

前言

此方案同一时刻仅能激活一个沙箱。若同时激活多个,全局对象上的变量会被多个沙箱更新,导致全局变量冲突。

基于代理(Proxy)实现多实例沙箱

在单实例场景中,fakeWindow为空对象,不具备存储变量的功能,微应用创建的变量最终均挂载在window上,这限制了同一时刻只能有一个激活的微应用。


class MultipleProxySandbox {

    active() {
        this.sandboxRunning = true;
    }
    inactive() {
        this.sandboxRunning = false;
    }

    /**
     * 构造函数
     * @param {*} name 沙箱名称 
     * @param {*} context 共享的上下文
     * @returns 
     */
    constructor(name, context = {}) {
        this.name = name;
        this.proxy = null;
        const fakeWindow = Object.create({});
        const proxy = new Proxy(fakeWindow, {
            set: (target, name, value) => {
                if (this.sandboxRunning) {
                    if (Object.keys(context).includes(name)) {
                        context[name] = value;
                    }
                    target[name] = value;
                }
            },
            get: (target, name) => {
                // 优先使用共享对象
                if (Object.keys(context).includes(name)) {
                    return context[name];
                }
                return target[name];
            }
        })
        this.proxy = proxy;
    }
}

const context = { document: window.document };

const newSandBox1 = new MultipleProxySandbox('代理沙箱1', context);
newSandBox1.active();
const proxyWindow1 = newSandBox1.proxy;

const newSandBox2 = new MultipleProxySandbox('代理沙箱2', context);
newSandBox2.active();
const proxyWindow2 = newSandBox2.proxy;
console.log('共享对象是否相等', window.document === proxyWindow1.document, window.document ===  proxyWindow2.document);

proxyWindow1.a = '1'; // 设置代理1的值
proxyWindow2.a = '2'; // 设置代理2的值
window.a = '3';  // 设置window的值
console.log('打印输出的值', proxyWindow1.a, proxyWindow2.a, window.a);


newSandBox1.inactive(); newSandBox2.inactive(); // 两个沙箱都失活

proxyWindow1.a = '4'; // 设置代理1的值
proxyWindow2.a = '4'; // 设置代理2的值
window.a = '4';  // 设置window的值
console.log('失活后打印输出的值', proxyWindow1.a, proxyWindow2.a, window.a);

newSandBox1.active(); newSandBox2.active(); // 再次激活

proxyWindow1.a = '4'; // 设置代理1的值
proxyWindow2.a = '4'; // 设置代理2的值
window.a = '4';  // 设置window的值
console.log('失活后打印输出的值', proxyWindow1.a, proxyWindow2.a, window.a);

运行代码,观察结果如下:

前言

此方案支持同一时刻激活多个沙箱,从而实现了多实例沙箱的功能。

结束语

以上为微前端中常用的沙箱实现方式。若要在生产环境中使用,需进行大量判断与约束。下篇文章将深入源码,解析微前端框架qiankun的沙箱实现机制。上述示例代码已上传至github,仓库名为js-sandbox,欢迎查阅。

参考

iframe src ES6 Proxy

到此这篇关于前端JS沙箱实现几种方式的文章介绍完毕,更多相关JS 沙箱内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

作者最新文章