Qiankun Plugin
@evjs/plugin-qiankun lets an evjs single-page application participate in a
qiankun master/slave micro-frontend
topology. It wraps the framework-owned SPA entry, exposes qiankun lifecycles,
and bridges an asynchronous master snapshot into evjs runtime routes.
Use the plugin only when an SPA explicitly runs as a qiankun master or slave. It does not provide an MPA integration.
Install
npm install @evjs/plugin-qiankun qiankun
Master Applications
Configure the master with evPluginQiankunMaster() and a resolver module:
// ev.config.ts
import { defineConfig } from "@evjs/ev";
import { evPluginQiankunMaster } from "@evjs/plugin-qiankun";
export default defineConfig({
routing: { mode: "spa" },
plugins: [
evPluginQiankunMaster({
resolver: "./src/qiankun.master.ts",
}),
],
});
The resolver returns one authoritative, application-level apps/routes
snapshot:
// src/qiankun.master.ts
import { defineQiankunMasterResolver } from "@evjs/plugin-qiankun/runtime";
export default defineQiankunMasterResolver(async () => ({
apps: [
{
name: "catalog",
entry: "//localhost:3001/index.html",
props: {
locale: "en-US",
},
},
{
name: "reports",
entry: "//localhost:3002/index.html",
},
],
routes: [
{
path: "/catalog",
microApp: "catalog",
microAppProps: {
section: "products",
},
},
{
path: "/reports",
microApp: "reports",
mode: "match",
},
{
path: "/legacy-catalog",
redirect: "/catalog",
},
],
history: "browser",
settings: {
sandbox: true,
},
prefetch: ["catalog"],
}));
The master does not declare a fixed qiankun container or activeRule. Before
the framework starts rendering, the plugin resolves the snapshot and installs
an evjs runtime route overlay. Each micro-app route renders a generated React
component that owns its container and calls qiankun loadMicroApp().
The route forms are:
{ path, microApp }uses"prepend"mode by default./catalogtherefore owns/catalogand its descendants, and the matched prefix becomes the mounted slave's base.{ path, microApp, mode: "match" }matches only that route path instead of prepending the path to the slave base.{ path, redirect }creates a runtime redirect. The target may be an absolute application path or anhttp(s)URL.microAppPropsadds route-specific props. Ordinary fields override fields fromapp.props; nestedsettingscan refine qiankun load settings, and nestedlifeCyclesrun after the matching master lifecycle hooks for that route.
Resolver route paths accept ordinary :param and * syntax. The bridge
normalizes them for the evjs runtime router and rejects duplicate, malformed,
or unresolved routes before rendering the master.
The master source tree only needs its own canonical shell Pages:
src/
├── pages/
│ ├── layout.tsx
│ └── page.tsx # /
└── qiankun.master.ts
There is intentionally no src/pages/catalog/page.tsx and no static
#slave-container. The runtime component supplied by the /catalog overlay
owns both concerns:
// src/pages/layout.tsx
import { Link } from "@evjs/ev/navigation";
import type { ReactNode } from "react";
export default function RootLayout({ children }: { children?: ReactNode }) {
return (
<main>
<nav>
<Link to="/">Home</Link>
<a href="/catalog">Catalog</a>
</nav>
{children}
</main>
);
}
Runtime overlay boundary
Resolver routes are runtime state, not authoring input for the canonical CoreGraph:
- they do not create canonical Pages, Routes, or Documents;
- they do not modify
application.routes, the BuildPlan, or deployment route metadata; - they are not included in generated
src/route-types.d.tsPage names,RoutePath, or typed navigation targets; - they are installed through the generated application's runtime update API before its first render.
Keep canonical navigation type usage for canonical Pages. When a URL exists
only in a runtime site snapshot, its availability and validation belong to the
platform/runtime layer; the example above therefore uses an ordinary link for
/catalog.
Slave Applications
A slave exports qiankun lifecycles while remaining independently renderable.
Configure it with evPluginQiankunSlave():
// ev.config.ts
import { defineConfig } from "@evjs/ev";
import { evPluginQiankunSlave } from "@evjs/plugin-qiankun";
export default defineConfig({
routing: { mode: "spa" },
plugins: [
evPluginQiankunSlave({
name: "catalog",
runtime: "./src/qiankun.slave.ts",
}),
],
});
The slave declares only its own root and internal Pages:
src/
├── pages/
│ ├── page.tsx # local /
│ └── details/
│ └── page.tsx # local /details
└── qiankun.slave.ts
It does not duplicate the master's /catalog path in its source tree. With a
default "prepend" master route at /catalog, the master passes /catalog as
the slave base. The slave's local / is then rendered at /catalog, and its
local /details is rendered at /catalog/details.
The slave lifecycle loads the original generated entry, projects the received
base and history through pagesApp.updateRuntime(), and only then calls the
entry's first start(). The router therefore observes the mounted base and
history before the first application render. When run outside qiankun, the same
Pages remain available under their standalone base.
While mounted with browser or hash history, the slave uses a scoped history
adapter. Slave Link and useNavigate() calls still update the shared browser
URL, and native back/forward events update both the host and slave routers, but
the slave does not replace the host's global history.pushState or
history.replaceState methods. The adapter is released on unmount. Memory
history remains isolated and does not write the browser URL.
Application layouts must not add their own popstate listener that compares
window.location with useLocation() and renders a corrective Navigate.
The scoped adapter is the single synchronization point, so native traversal
continues to respect Router blockers and qiankun mount/unmount ownership.
When a mounted master changes the URL programmatically without emitting
popstate, its route component forwards the href change through the qiankun
update lifecycle. The slave refreshes its scoped adapter only when the browser
URL and Router history differ.
The generated route types remain local to the slave source tree: they describe
/ and /details, not the externally assigned /catalog prefix.
The optional runtime module adds lifecycle behavior; it does not replace the framework entry:
// src/qiankun.slave.ts
import { defineQiankunSlaveRuntime } from "@evjs/plugin-qiankun/runtime";
export default defineQiankunSlaveRuntime({
mount(props, ctx) {
console.log(`${ctx.name} preparing to mount`, {
container: props.container,
base: props.base,
history: props.history,
});
},
afterMount(_props, ctx) {
console.log(`${ctx.name} mounted`);
},
afterUpdate(_props, ctx) {
console.log(`${ctx.name} updated`);
},
unmount() {
console.log("slave unmounted");
},
});
In qiankun mode the plugin mounts into props.container. Outside qiankun it
automatically starts the same canonical SPA entry. It does not infer a magic
src/main.tsx or expose a second application entry model. Slave code must use
the supplied container; the plugin never rewrites global document lookup
methods to redirect selectors.
Module References
resolver and runtime accept a string module specifier, a generated module
ref, or an object selecting a named export:
import type { GeneratedModuleRef } from "@evjs/ev/plugin";
type QiankunModuleRef =
| string
| GeneratedModuleRef
| {
module: string | GeneratedModuleRef;
exportName?: string;
};
String references read the default export:
evPluginQiankunMaster({
resolver: "./src/qiankun.master.ts",
});
Object references select named exports:
evPluginQiankunSlave({
runtime: {
module: "/absolute/path/to/generated-slave-runtime.ts",
exportName: "runtime",
},
});
Path-like references are resolved from the project root before bundling.
Package specifiers are resolved from project dependencies. From another
plugin's emitIR() hook, pass the opaque GeneratedModuleRef returned
by ctx.emit.module() directly to emitQiankunMasterIR() or
emitQiankunSlaveIR().
Runtime Shape
The public master shape is:
type QiankunHistoryType = "browser" | "hash" | "memory";
type QiankunRouteMode = "prepend" | "match";
type QiankunLoadSettings = import("qiankun").AppConfiguration;
type QiankunLifeCycles = import("qiankun").LifeCycles<
Record<string, unknown>
>;
interface QiankunApp {
name: string;
entry: string;
props?: Record<string, unknown> & {
settings?: QiankunLoadSettings;
};
}
type QiankunRoute =
| {
path: string;
microApp: string;
mode?: QiankunRouteMode;
microAppProps?: Record<string, unknown> & {
settings?: QiankunLoadSettings;
lifeCycles?: QiankunLifeCycles;
};
}
| {
path: string;
redirect: string;
};
interface QiankunMasterOptions {
apps?: QiankunApp[];
routes?: QiankunRoute[];
base?: string;
history?: QiankunHistoryType;
settings?: QiankunLoadSettings;
lifeCycles?: QiankunLifeCycles;
prefetch?: boolean | "all" | string[];
prefetchThreshold?: number;
}
base defaults to /, history defaults to "browser", and a missing
mode defaults to "prepend". settings is shared by route-mounted
applications. App and route settings layer over it, and route lifecycle hooks
are composed after master lifecycle hooks. The public bridge does not add
request policy or interpret platform-specific fields.
prefetch: "all" prefetches every app after the master starts, while a string
array prefetches the selected app names. prefetch: true waits for the first
mounted app and then prefetches up to prefetchThreshold other apps; the
threshold defaults to 5.
route.microApp strictly matches app.name. Higher-level integrations must
normalize external records into the canonical { name, entry } shape before
returning the snapshot. Unknown fields on the master, app, or route structure
are rejected rather than ignored; extensible integration data belongs in
props or microAppProps.
The master passes the route-derived values through the slave lifecycle props:
interface QiankunLifecycleProps {
container?: Element | string | null;
base?: string;
history?:
| QiankunHistoryType
| { type: "browser" }
| { type: "hash" }
| {
type: "memory";
initialEntries?: string[];
initialIndex?: number;
};
[key: string]: unknown;
}
The optional slave runtime supports:
interface QiankunSlaveRuntime {
bootstrap?(props, ctx): void | Promise<void>;
mount?(props, ctx): void | Promise<void>;
afterMount?(props, ctx): void | Promise<void>;
update?(props, ctx): void | Promise<void>;
afterUpdate?(props, ctx): void | Promise<void>;
unmount?(props, ctx): void | Promise<void>;
}
ctx.loadEntry() loads the original generated entry without starting it.
Calling it during bootstrap() is safe. The first mount() configures runtime
base/history before start(); subsequent remounts reuse the loaded module and
call its render path inside the current container.
mount() and update() run before the framework-owned entry work.
afterMount() runs only after base/history projection and entry start() or
render() complete successfully. afterUpdate() runs after a successful
projection commit and also runs for mounted updates that do not change the
projection. All lifecycle operations remain serialized: a queued update or
unmount waits for the preceding post lifecycle to settle. An afterMount()
failure participates in mount rollback; an afterUpdate() failure rejects that
update without rolling back the already committed projection.
Bundling Qiankun
By default, qiankun is bundled with the application:
evPluginQiankunMaster({
resolver: "./src/qiankun.master.ts",
externalQiankun: false,
});
Set externalQiankun: true only when the runtime environment provides the
module:
evPluginQiankunSlave({
name: "catalog",
externalQiankun: true,
});
Local Development
The plugin does not create a development proxy. Configure dev.proxy when a
master needs to load a slave dev server through the same origin:
// master ev.config.ts
import { defineConfig } from "@evjs/ev";
import { evPluginQiankunMaster } from "@evjs/plugin-qiankun";
export default defineConfig({
routing: { mode: "spa" },
dev: {
port: 3000,
proxy: [
{
context: ["/__qiankun_slave"],
target: "http://localhost:3001",
pathRewrite: {
"^/__qiankun_slave": "",
},
changeOrigin: true,
secure: false,
},
],
},
plugins: [
evPluginQiankunMaster({
resolver: "./src/qiankun.master.ts",
}),
],
});
Point the resolver app entry at the proxied HTML. qiankun 3 expects an HTML
entry URL rather than a { scripts, styles, html } object:
const slaveBase = "/__qiankun_slave";
export default async function resolveQiankunMaster() {
return {
apps: [
{
name: "catalog",
entry: new URL(`${slaveBase}/index.html`, window.location.href).href,
},
],
routes: [{ path: "/catalog", microApp: "catalog" }],
settings: { sandbox: true },
prefetch: true,
};
}
evPluginQiankunSlave() marks the emitted entry script and rewrites generated
root-relative JS/CSS URLs to relative URLs, so the same slave HTML can be
consumed below a proxy prefix. Keep asset proxies in dev.proxy, not in
src/apis; application request Routes should not proxy micro-frontend assets.
Platform Composition
A higher-level integration plugin can reuse emitQiankunMasterIR() or
emitQiankunSlaveIR() from its emitIR() method and the matching
createQiankun*Hooks() helper from setup(). It must normalize external data
before passing a resolver or runtime module to the public bridge and compose
returned hooks with any additional lifecycle behavior it owns.
Applications install either the public master/slave factory or the higher-level integration factory, not both. Platform-specific configuration does not belong in Page config.
Boundaries
@evjs/plugin-qiankun includes:
- master and slave framework-entry wrapping;
- resolver/runtime module loading;
- application-level
apps/routesvalidation; - runtime prepend, match, redirect, and micro-app route components;
- generated micro-app containers and qiankun loading;
- slave base/history projection before first render;
- slave lifecycle exports and standalone rendering;
externalQiankunsupport;- contribution and hook helpers for platform composition.
It does not include:
- external platform data protocols or identity mapping;
- platform-specific runtime, deployment, or development policy;
- automatic local development proxies;
- Page-level qiankun settings;
- canonical CoreGraph Pages, Routes, or Documents derived from resolver data;
- generated
RoutePathentries for runtime resolver routes.