These docs cover qiankun 3.0 (RC) — install it with npm i qiankun@rc. For 2.x, see the v2 docs.
Skip to content

Using the sandbox without qiankun ​

@qiankunjs/sandbox can be used directly by a browser host that does not use qiankun or its HTML loader. The package has two layers:

  • StandardSandbox isolates JavaScript global writes and supports classic-script evaluation and ESM imports.
  • createSandbox() adds lifecycle-managed browser integration: optional DOM containment, style isolation, timer and listener cleanup, and isolation plugins.

The lower-level Compartment class is also public for hosts that need a Compartment-shaped global and module-hook facade. It is not the recommended classic-script entry point.

Install ​

bash
pnpm add @qiankunjs/sandbox@rc

The package is browser-only. It depends on DOM APIs, blob URLs, and dynamically injected import maps, so it must not be initialized during Node.js or SSR rendering.

Choose the right entry point ​

ScenarioEntry point
Evaluate classic code or import ESM without DOM containmentStandardSandbox
Contain dynamic DOM, styles, timers, and listenerscreateSandbox() with a container
Use lifecycle cleanup without DOM containmentcreateSandbox() without a container
Build a specialized Compartment hostCompartment

StandardSandbox installs sandbox-local window, self, and globalThis references. A bare Compartment intentionally does not, because those browser globals are not part of the Compartment shape. In development, Compartment.evaluateScript() warns once when window has not been defined as a self-reference. The warning is diagnostic only: execution continues and window.x = 1 can still reach the host.

JavaScript-only usage ​

Evaluate a classic script ​

ts
import { StandardSandbox } from '@qiankunjs/sandbox';

const sandbox = new StandardSandbox('analytics-job', {
  reportId: 'weekly-42',
});

await sandbox.evaluateScript(
  `
    window.analyticsResult = {
      reportId,
      generatedAt: Date.now(),
    };
  `,
  { sourceURL: 'https://analytics.example/job.js' },
);

const view = sandbox.globalThis as Window & {
  analyticsResult?: { reportId: string; generatedAt: number };
};

console.log(view.analyticsResult);
console.log(Reflect.get(window, 'analyticsResult')); // undefined

sandbox.dispose();

The source runs as a native blob script. sourceURL is optional, but supplying the original URL makes stack traces and DevTools source views easier to understand.

Import an ESM graph ​

ts
const sandbox = new StandardSandbox('analytics-module');
const entryUrl = new URL('/modules/analytics-entry.js', document.baseURI).href;
const namespace = await sandbox.import(entryUrl);

console.log(namespace);
sandbox.dispose();

The default module base is document.baseURI, and the default fetch implementation is the native window fetch. StandardSandbox accepts module configuration in its fourth, Compartment-options argument. createSandbox() exposes modules, resolveHook, importHook, and the loadHook alias directly. The sandbox plugins guide includes a precompiled-module example.

Contain a browser widget ​

The following host loads a third-party classic script without depending on qiankun or the loader:

html
<div id="widget-host"></div>
ts
import { createSandbox } from '@qiankunjs/sandbox';

const container = document.querySelector<HTMLElement>('#widget-host')!;
const controller = createSandbox('support-widget', {
  container,
  styleIsolation: true,
});

try {
  // Activate cleanup plugins before the widget installs side effects.
  await controller.mount();

  const response = await fetch('https://widgets.example/support.js');
  if (!response.ok) {
    throw new Error(`Widget request failed: ${response.status}`);
  }

  const source = await response.text();
  await controller.instance.evaluateScript(source, {
    sourceURL: response.url,
  });
} catch (error) {
  // dispose() is safe after a partial mount or evaluation failure.
  await controller.dispose();
  throw error;
}

Application code sees a virtualized document:

  • document.body points to the configured container.
  • document.head points to a <qiankun-head> inside that container.
  • Dynamically created scripts, styles, and stylesheet links are intercepted before insertion.
  • styleIsolation: true scopes styles to [data-name="support-widget"].

The host still owns the application lifecycle. If the widget exports an unmount function, call it before releasing sandbox effects.

ts
const widgetView = controller.instance.globalThis as Window & {
  widgetUnmount?: () => void | Promise<void>;
};
await widgetView.widgetUnmount?.();
await controller.unmount();

// A later activation rebuilds the sandbox integrations.
await controller.mount();

// Terminal owner cleanup; this controller cannot be reused afterward.
await controller.dispose();
container.replaceChildren();

unmount() does not clear the container and does not infer an application's business teardown. dispose() may be called while mounted and is idempotent.

Replace the container between mounts ​

Pass a getter when the host creates a fresh element for each activation:

ts
let currentContainer = document.querySelector<HTMLElement>('#widget-a')!;

const controller = createSandbox('support-widget', {
  container: () => currentContainer,
  styleIsolation: true,
});

await controller.mount();
await controller.unmount();

currentContainer = document.querySelector<HTMLElement>('#widget-b')!;
await controller.mount();

The getter is resolved again for the next mount. Alternatively, mount(container) can override the configured container for one activation.

Use lifecycle cleanup without DOM containment ​

When no container is supplied, createSandbox() omits the dynamic-DOM plugin but retains timer, window-listener, and history cleanup:

ts
const controller = createSandbox('background-integration');

await controller.mount();
await controller.instance.evaluateScript(`
  window.addEventListener('message', () => {});
  window.setInterval(() => {}, 1000);
`);

await controller.unmount();
await controller.dispose();

Mount before evaluating the source. Side effects created before mount() are outside the mount-phase tracking window. Style isolation requires a container; createSandbox('name', { styleIsolation: true }) throws a TypeError.

Container preparation ​

createSandbox() prepares configured containers automatically. The public helper is intended for advanced hosts that assemble the lower-level pieces themselves:

ts
import { prepareSandboxContainer } from '@qiankunjs/sandbox';

const container = document.querySelector<HTMLElement>('#widget-host')!;
const { styleIsolation, cleanup } = prepareSandboxContainer(container, 'support-widget');

console.log(styleIsolation.appName); // support-widget
console.log(styleIsolation.scopeRoot); // [data-name="support-widget"]

// Release the protocol state when the custom host is permanently torn down.
cleanup();

Preparation performs three related operations:

  1. Sets data-name="support-widget" on the container.
  2. Ensures a <qiankun-head> element for virtualized head insertions.
  3. Returns the exact { appName, scopeRoot } pair required by style isolation.

Cleanup only reverts state created by that call. It keeps a pre-existing <qiankun-head>, restores a previous data-name, and does not overwrite a later host change. It is safe to call more than once.

Configuration reference ​

ts
const controller = createSandbox(appName, {
  container,
  provisionContainerHead,
  globals,
  incubatorContext,
  modules,
  resolveHook,
  importHook,
  loadHook,
  plugins,
  styleIsolation,
  fetch,
  nodeTransformer,
  compartmentOptions,
});
OptionPurpose
containerEnables DOM containment; accepts an element or a getter
provisionContainerHeadWhether the sandbox provisions a <qiankun-head> at mount time when the container lacks one, default true; pass false when an external pipeline (such as qiankun's streaming loader) materializes the container structure from the entry HTML
globalsAdds values or property descriptors to the sandbox global
incubatorContextSelects the host window through which global reads fall through
modulesSupplies a table of predefined modules
resolveHookResolves module specifiers
importHook / loadHookLoads custom module descriptors; loadHook is an alias
pluginsAppends custom isolation plugins after the built-in preset
styleIsolationEnables runtime CSS scoping; requires container
fetchOverrides the fetch used by dynamic assets; module sources also use it unless compartmentOptions.moduleHost.fetch is set
nodeTransformerReplaces the default dynamic asset transformation
compartmentOptionsProvides lower-level host options such as moduleHost

Top-level module options take precedence over duplicate values in compartmentOptions. The one exception is moduleHost.fetch: it only loads module sources, so the more specific option takes precedence over the top-level fetch for them, while dynamic assets keep using the top-level fetch. The controller exposes the normalized instance, nodeTransformer, optional styleIsolation, and the mount, unmount, and dispose lifecycle methods.

For custom globals and plugins, see Extending sandbox isolation.

Why the default transformer matters ​

The dynamic-DOM plugin intercepts a script element, but interception alone does not isolate its JavaScript. The default transformer fetches a classic script, wraps it with the current Compartment's classic-script transformer, and gives the browser a blob URL. This is the path that keeps window.someValue = ... inside the sandbox.

The same transformer processes dynamic styles and stylesheet links, applying the internal style-isolation options when enabled. It uses document.baseURI as the standalone asset base.

Advanced hosts can replace it:

ts
const controller = createSandbox('trusted-widget', {
  container,
  nodeTransformer: (node) => node,
});

This identity transformer is intentionally shown as a warning, not a recommended default. It disables classic-script wrapping, and a dynamically appended script may execute against the host window. It also bypasses the default style transformation. Supply a custom transformer only when it implements equivalent controls or every affected asset is trusted.

If an external HTML pipeline needs to transform nodes before insertion, use controller.nodeTransformer rather than the raw callback passed in options. The controller version has already been enriched with the Compartment, fetch implementation, classic-script wrapper, and style-isolation configuration.

Its output also carries an ownership consequence: the controller's transformer marks each node as finished pipeline product, so the sandbox's patched insertion points let it through natively instead of routing it into the dynamic transpilation pipeline a second time. In particular, a <style> or <link> prepared this way is not recorded in the sandbox's dynamic-stylesheet ledger — it survives unmount() and is not re-attached on a later mount(). Its lifecycle belongs to the pipeline that prepared it: remove or re-insert it yourself if your embedding unmounts and remounts. Styles that sandboxed code injects at runtime (through the patched DOM methods, without the mark) keep the full ledger lifecycle — removed at unmount, restored at remount.

Understand the isolation boundary ​

The sandbox prioritizes browser compatibility and shared object identity. Its membrane is deliberately non-transitive.

ts
const sharedSettings = { theme: 'light' };
const sandbox = new StandardSandbox('settings-demo', { sharedSettings });

await sandbox.evaluateScript(`
  window.localFlag = true;
  sharedSettings.theme = 'dark';
`);

After evaluation:

  • window.localFlag in the host is still undefined because the global property write was isolated.
  • sharedSettings.theme is "dark" in both host and sandbox because the same object crossed the boundary.
  • DOM nodes, events, functions, and library instances likewise keep their identity; === and instanceof continue to work.

This model is not suitable for hostile code. It does not harden objects, freeze intrinsics, create another origin, or impose an iframe/Worker callable boundary. Use those browser isolation mechanisms when code must not access host capabilities. The Compartment Alignment RFC explains this design boundary in detail.

Content Security Policy ​

The Classic script evaluator and ESM engine do not rely on eval or new Function, so the framework itself does not require 'unsafe-eval'. Blob URLs, inline import maps, styles, and cross-origin fetches have separate CSP requirements. See Content Security Policy (CSP) for policy examples and the current nonce limitations.

Production checklist ​

  • Use StandardSandbox, not bare Compartment, for classic scripts.
  • Call mount() before application code installs timers or listeners.
  • Keep the default transformer unless a replacement preserves classic-script isolation.
  • Treat application teardown and container clearing as host responsibilities.
  • Call dispose() on permanent removal and on partial-load failure.
  • Test the actual CSP and cross-origin asset policy in the target browser set.
  • Use iframe, Worker, or origin isolation for untrusted code.

Released under the MIT License.