Frontend Development 16 min read

Implementing a Simplified Qiankun JavaScript Sandbox: Snapshot, Singular Proxy, and Multiple Proxy Sandboxes

This article walks through building a lightweight Qiankun JS sandbox by first explaining sandbox principles, then creating a snapshot sandbox, a singular proxy sandbox that records changes via ES6 Proxy, and finally a multiple‑proxy sandbox that isolates each micro‑frontend with its own fake window, complete with test cases and setup instructions.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing a Simplified Qiankun JavaScript Sandbox: Snapshot, Singular Proxy, and Multiple Proxy Sandboxes

This article is originally published on the Rare Earth Juejin technical community and may not be reproduced without permission.

Preface

Hello everyone, I am Haiguai. Today we continue the discussion of Qiankun's JS sandbox. The original source code can be found in the src/sandbox directory.

The real code in Qiankun is hard to read because it contains a lot of fallback logic and edge‑case handling. In this tutorial we will implement a simplified version of the Qiankun JS sandbox, leaving out most exceptional cases. Advanced readers can clone the Qiankun source to explore further.

All code for this article is placed in the mini-js-sandbox repository; extract what you need.

Sandbox Principle

First, understand what a sandbox is used for. In everyday life we encounter sandboxes such as military sand tables, Minecraft sandbox, GTA simulated cities, etc. The main purpose of a sandbox is environment isolation: after micro‑application A finishes, the environment must be restored before loading micro‑application B to avoid pollution.

In JavaScript, a sandbox isolates global variables. The problematic code example is:

window.jQuery = {}

window.jQuery.ajax();

To execute this safely we store the code as a string codeStr and wrap it in a function that receives a proxy window:

// code to be placed in the sandbox
const codeStr = `
  window.jQuery = {}

  window.jQuery.ajax();
`;

// final code that will be evaluated
const finalCode = `
function fn(window) {
  ${codeStr}
}

// pass the sandbox proxy as argument
fn(window.proxy)
`;

// execute the snippet
eval(finalCode);

Now window inside codeStr refers to window.proxy instead of the real global object. The next sections show how to implement window.proxy .

Environment Setup

We adopt a TDD approach. Create a project and install Jest with JSDOM support:

npm i -D jest @types/jest jest-environment-jsdom

Generate a Jest config:

npx jest --init

Add a simple function to src/SnapshotSandbox.js and write a test in src/SnapshotSandbox.test.js :

const add = (a, b) => {
  return a + b;
}

Run the test with npm run test and verify it passes.

Snapshot Sandbox

The snapshot sandbox records the entire window state before activation and restores it on deactivation. The core implementation:

class SnapshotSandbox {
  windowSnapshot = {}
  modifiedMap = {}
  proxy = window;

  constructor() {}

  active() {
    // record current window key‑values
    Object.entries(window).forEach(([key, value]) => {
      this.windowSnapshot[key] = value;
    });
    // restore previously modified keys
    Object.keys(this.modifiedMap).forEach(key => {
      window[key] = this.modifiedMap[key];
    });
  }

  inactive() {
    this.modifiedMap = {};
    Object.keys(window).forEach(key => {
      if (window[key] !== this.windowSnapshot[key]) {
        this.modifiedMap[key] = window[key];
        window[key] = this.windowSnapshot[key];
      }
    });
  }
}

module.exports = SnapshotSandbox;

Tests verify that activation restores previous values and that deactivation records changes.

Singular Proxy Sandbox

To avoid full diff on every deactivation, we use an ES6 Proxy to watch each set operation. Three maps are maintained:

addedMap – records newly added global variables.

originMap – records the original values of modified variables.

updatedMap – records the latest values after modification.

class SingularProxySandbox {
  addedMap = new Map();
  originMap = new Map();
  updatedMap = new Map();

  setWindowKeyValues(key, value, shouldDelete) {
    if (value === undefined || shouldDelete) {
      delete window[key];
    } else {
      window[key] = value;
    }
  }

  constructor() {
    const fakeWindow = Object.create(null);
    const { addedMap, originMap, updatedMap } = this;
    this.proxy = new Proxy(fakeWindow, {
      set(_, key, value) {
        const originValue = window[key];
        if (!window.hasOwnProperty(key)) {
          addedMap.set(key, value);
        } else if (!originMap.has(key)) {
          originMap.set(key, originValue);
        }
        updatedMap.set(key, value);
        window[key] = value;
        return true;
      },
      get(_, key) {
        return window[key];
      }
    });
  }

  active() {
    this.updatedMap.forEach((value, key) => this.setWindowKeyValues(key, value));
  }

  inactive() {
    this.addedMap.forEach((_, key) => this.setWindowKeyValues(key, undefined, true));
    this.originMap.forEach((value, key) => this.setWindowKeyValues(key, value));
  }
}

module.exports = SingularProxySandbox;

Corresponding Jest tests check that the maps record the correct values and that activation/inactivation restores the environment.

Multiple Proxy Sandbox

The previous sandboxes still focus on restoring a single global environment. For N micro‑applications we allocate N independent sandbox environments. Each sandbox gets its own fakeWindow that copies only native, non‑configurable properties from the real window . The implementation creates a proxy that forwards reads to the real window for native properties and to the fake window for sandbox‑specific ones.

let activeSandboxCount = 0;

class MultipleProxySandbox {
  proxy = {};

  constructor(props) {
    const { fakeWindow, keysWithGetters } = this.createFakeWindow();
    this.proxy = new Proxy(fakeWindow, {
      set(target, key, value) {
        // ignore non‑native properties that already exist on real window
        if (!target[key] && window[key]) {
          return true;
        }
        target[key] = value;
        return true;
      },
      get(target, key) {
        const actualTarget = keysWithGetters[key] ? window : (key in target ? target : window);
        return actualTarget[key];
      }
    });
  }

  createFakeWindow() {
    const fakeWindow = {};
    const keysWithGetters = {};
    Object.getOwnPropertyNames(window)
      .filter(key => {
        const descriptor = Object.getOwnPropertyDescriptor(window, key);
        return !descriptor?.configurable; // treat non‑configurable as native
      })
      .forEach(key => {
        fakeWindow[key] = window[key];
        keysWithGetters[key] = true;
      });
    return { fakeWindow, keysWithGetters };
  }

  active() { activeSandboxCount += 1; }
  inactive() { activeSandboxCount -= 1; }
}

module.exports = MultipleProxySandbox;

Tests verify that non‑native properties are not overwritten and that each sandbox has its own isolated environment.

Summary

The most important function of a sandbox is to isolate multiple JS environments so that different micro‑applications do not interfere with each other. For single‑application scenarios we can use SnapshotSandbox (full diff on deactivation) or SingularProxySandbox (record changes via Proxy). The most efficient approach for many micro‑applications is the MultipleProxySandbox , which assigns each app a separate fakeWindow and avoids any global pollution. Older browsers that lack Proxy support may still need the snapshot approach.

JavaScriptproxytestingqiankunsandboxJestmicro frontends
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.