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.
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-jsdomGenerate a Jest config:
npx jest --initAdd 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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.