xpe
v1.0.0
Published
下一代微前端应用运行底座
Readme
XPE
XPE 是下一代微前端应用运行底座。
它让基座、业务子应用和本地调试环境使用同一套接入模型,统一处理应用注册、上下文下发、权限读取、路由同步、跨应用导航和应用保活。
安装
pnpm add xpe包入口
// 基座应用:注册子应用、启动容器、处理顶层 URL。
import { createHost } from "xpe/host";
// 子应用:声明生命周期、读取上下文、同步路由、跨应用导航。
import { createAppClient, defineAppLifecycle } from "xpe/child";
// 类型定义:manifest、context、导航目标等。
import type { AppContext, AppManifest } from "xpe";
// 子应用本地独立调试。
import { createDevShell } from "xpe/dev-shell";核心概念
XPE 把页面分成两层路由:
用户可见 URL:/a/#/orders/1001
子应用资源: https://order.example.com/#/orders/1001/a是基座分配给子应用的入口路径。#/orders/1001是子应用自己的内部路由。- Vue Router、React Router 等框架仍然负责子应用内部跳转。
- XPE 负责把子应用内部路由同步到顶层 URL,并把浏览器前进/后退同步回子应用。
导航分三类:
// 1. 子应用内部跳转:继续用框架自己的 router。
router.push("/orders/1001");
// 2. 子应用跳另一个子应用:用 XPE app navigation。
client.navigate({ appCode: "SERVICE_DESK", path: "/tickets/TK-1002" });
// 3. 子应用跳基座页面:用 XPE host navigation。
client.navigateHost("/profile");基座快速接入
基座接入分三步:
- 准备应用 manifest。
- 实现
getEntryContext(appCode)。 - 创建并启动
createHost()。
import { createHost } from "xpe/host";
import type { AppContext, AppManifest } from "xpe";
// manifest 只描述“这个应用怎么被基座找到和装载”。
// 菜单、权限、当前用户、租户、组织等进入态数据放到 getEntryContext()。
const orderManifest: AppManifest = {
appCode: "ORDER_APP",
appName: "订单中心",
// 用户可见 URL 前缀,例如 /a/#/orders/1001。
basePath: "/a",
runtime: {
// 子应用真实资源地址。XPE 会在运行时把 hash/path 改成目标内部路由。
url: "https://order.example.com/#/overview",
// true 表示普通切换时保留子应用实例和业务状态。
alive: true,
},
};
const serviceDeskManifest: AppManifest = {
appCode: "SERVICE_DESK",
appName: "客服工单",
basePath: "/b",
runtime: {
url: "https://service-desk.example.com/#/tickets",
alive: true,
},
};
async function getEntryContext(appCode: string): Promise<AppContext> {
const manifest = appCode === "ORDER_APP" ? orderManifest : serviceDeskManifest;
const defaultPath = appCode === "ORDER_APP" ? "/overview" : "/tickets";
const permissions = appCode === "ORDER_APP" ? ["order.view", "order.edit"] : ["ticket.view"];
return {
appCode: manifest.appCode,
appName: manifest.appName,
basePath: manifest.basePath,
entryPath: `${manifest.basePath}/#/`,
defaultPath,
// 不可进入时返回 no_permission / not_opened / expired,XPE 会触发 onUnauthorized。
accessStatus: "enterable",
currentUser: {
memberId: "member-1",
userId: "user-1",
realName: "XPE User",
phone: "13800000000",
},
currentTenant: {
tenantId: "tenant-1",
tenantCode: "XBHS",
tenantName: "小贝喝水",
},
currentOrganization: null,
// 子应用运行时可通过 client.getContext() 和 client.hasPermission() 使用。
menus: [],
permissions,
};
}
const container = document.querySelector("#xpe-app");
if (!(container instanceof HTMLElement)) {
throw new Error("XPE container missing");
}
const host = createHost({
container,
// 进入非法 URL 时默认打开哪个应用。
defaultAppCode: "ORDER_APP",
apps: [
{ manifest: orderManifest, appPath: "/a", defaultPath: "/overview" },
{ manifest: serviceDeskManifest, appPath: "/b", defaultPath: "/tickets" },
],
getEntryContext,
onUnauthorized(context) {
// 可在这里跳无权限页、弹窗、埋点。
console.warn("应用不可进入", context.appCode, context.reason);
},
onHostNavigate(target) {
// 子应用调用 client.navigateHost() 时进入这里。
// 交给基座自己的 router 处理。
const path = typeof target === "string" ? target : target.path;
void hostRouter.push(path);
},
onError(error) {
console.error(error);
},
});
// 默认会自动监听浏览器前进/后退,并在非法 URL 时替换到默认应用。
void host.start();基座菜单跳转:
// 跳订单应用内部页面,顶层 URL 写成 /a/#/orders/1001。
await host.navigate("ORDER_APP", "/orders/1001");
// 跳客服工单应用,顶层 URL 写成 /b/#/tickets/TK-1002/audit?tab=events。
await host.navigate("SERVICE_DESK", {
path: "/tickets/TK-1002/audit",
query: { tab: "events" },
});常见跳转关系:
// 基座菜单、顶部搜索、快捷入口跳子应用页面。
await host.navigate("ORDER_APP", "/orders/1001");
// A 子应用跳 B 子应用页面。
client.navigate({
appCode: "SERVICE_DESK",
path: "/tickets/TK-1002/audit",
query: { from: "orders", orderId: 1001 },
});
// 子应用跳基座自己的页面。
client.navigateHost({
path: "/host/settings",
query: { tab: "workspace" },
});子应用快速接入
子应用入口使用 defineAppLifecycle() 声明挂载和卸载逻辑。
import { defineAppLifecycle } from "xpe/child";
defineAppLifecycle({
async mount(container, context, client) {
// container:XPE 分配给本次生命周期的真实 DOM 容器。
// context:基座下发的用户、租户、权限、菜单等进入上下文。
// client:子应用 SDK,提供上下文、事件、路由同步、跨应用导航等能力。
return () => {
// 子应用卸载清理逻辑。
};
},
});默认挂载容器是 #app。根节点不是 #app 时,可以通过 defaultContainer 指定:
defineAppLifecycle({
defaultContainer: "#micro-app-root",
async mount(container, context, client) {
// ...
},
});Vue 3 + TypeScript 接入
Vue Router 内部跳转继续用 router.push()。XPE 只通过 client.syncRouter(router) 做同步桥接。
import { createApp } from "vue";
import { createMemoryHistory, createRouter } from "vue-router";
import { defineAppLifecycle } from "xpe/child";
import App from "./App.vue";
const router = createRouter({
// 子应用推荐 memory history,由 XPE 负责同步顶层 URL。
history: createMemoryHistory(),
routes: [
{ path: "/", redirect: "/overview" },
{ path: "/overview", component: () => import("./views/Overview.vue") },
{ path: "/orders/:id", component: () => import("./views/OrderDetail.vue") },
],
});
defineAppLifecycle({
async mount(container, context, client) {
const app = createApp(App);
// 推荐通过 provide 注入,业务组件中自行封装 useXpe()。
app.provide("xpeContext", context);
app.provide("xpeClient", client);
app.use(router);
app.mount(container);
// 同步子应用 router 和基座 URL。
const stopRouteSync = await client.syncRouter(router, {
initialPath: context.defaultPath,
});
return () => {
stopRouteSync();
app.unmount();
container.innerHTML = "";
};
},
});业务页面内部跳转仍然这样写:
router.push("/orders/1001");React 接入
React Router 的内部跳转继续用自己的 navigate()。基础接入示例:
import { createMemoryRouter, RouterProvider } from "react-router-dom";
import { createRoot, type Root } from "react-dom/client";
import { defineAppLifecycle } from "xpe/child";
import App from "./App";
let root: Root | null = null;
defineAppLifecycle({
async mount(container, context, client) {
const router = createMemoryRouter(
[
{ path: "/", element: <App /> },
{ path: "/orders/:id", element: <App /> },
],
{
initialEntries: [context.defaultPath],
}
);
root = createRoot(container);
root.render(<RouterProvider router={router} />);
// 路由同步可以封装在应用入口,业务页面继续使用 React Router。
return () => {
root?.unmount();
root = null;
container.innerHTML = "";
};
},
});React 项目可在应用入口封装路由同步逻辑,业务页面不需要感知 XPE。
Vue 2 接入
Vue 2 同样只需要在 XPE 生命周期里创建和销毁实例:
import Vue from "vue";
import VueRouter from "vue-router";
import { defineAppLifecycle } from "xpe/child";
import App from "./App.vue";
Vue.use(VueRouter);
defineAppLifecycle({
async mount(container, context, client) {
const router = new VueRouter({
mode: "abstract",
routes: [
{ path: "/", redirect: context.defaultPath },
{ path: "/orders/:id", component: () => import("./views/OrderDetail.vue") },
],
});
const instance = new Vue({
router,
render: (h) => h(App),
});
instance.$mount(container);
return () => {
instance.$destroy();
container.innerHTML = "";
};
},
});Vue 2 项目可在应用入口封装路由同步逻辑,业务页面继续使用 Vue Router。
Angular 接入
Angular 应用在 XPE 生命周期里 bootstrap 和 destroy:
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { defineAppLifecycle } from "xpe/child";
import { AppModule } from "./app/app.module";
defineAppLifecycle({
async mount() {
const moduleRef = await platformBrowserDynamic().bootstrapModule(AppModule);
return () => {
moduleRef.destroy();
};
},
});Angular 项目可在应用入口封装路由同步逻辑,业务页面继续使用 Angular Router。
子应用导航
子应用内部路由
业务内部页面跳转继续用框架 router:
router.push("/orders/1001");
// React Router: navigate("/orders/1001")这类跳转保持框架原有写法即可。
跨子应用跳转
从当前子应用跳到另一个子应用:
client.navigate({
appCode: "SERVICE_DESK",
path: "/tickets/TK-1002/audit",
query: {
tab: "events",
from: "orders",
orderId: 1001,
},
});如果 SERVICE_DESK 的 appPath 是 /b,顶层 URL 会写成:
/b/#/tickets/TK-1002/audit?tab=events&from=orders&orderId=1001跳转基座页面
如果你的系统把个人中心、账号安全、通知中心、全局设置等页面放在基座,可以让子应用发起 host navigation:
client.navigateHost("/profile");
client.navigateHost({
path: "/account/security",
query: { tab: "password" },
});基座侧通过 onHostNavigate() 交给自己的 router:
const host = createHost({
container,
apps,
getEntryContext,
onHostNavigate(target) {
const path = typeof target === "string" ? target : target.path;
void hostRouter.push(path);
},
});这类页面通常由基座自己的 router 处理;只有它们本身也按独立子应用交付时,才需要注册成 XPE 应用。
通信
XPE 把导航和业务消息分开处理:
- 页面跳转使用
host.navigate()、client.navigate()、client.navigateHost()。 - 业务消息使用
emit/on/off。 - 用户、租户、权限、菜单等进入态数据通过
getEntryContext()下发,子应用通过client.getContext()读取。
基座和子应用通信
基座向指定子应用发送消息:
host.emit("ORDER_APP", "order:refresh", {
orderId: "1001",
});子应用监听消息:
defineAppLifecycle({
async mount(container, context, client) {
const unsubscribe = client.on<{ orderId: string }>("order:refresh", (payload) => {
void refreshOrder(payload.orderId);
});
return () => {
unsubscribe();
};
},
});子应用向基座发送消息:
client.emit("order:selected", {
orderId: "1001",
});基座监听指定子应用的消息:
const unsubscribe = host.on<{ orderId: string }>("ORDER_APP", "order:selected", (payload) => {
console.log(payload.orderId);
});子应用和子应用通信
子应用之间的页面级跳转使用 client.navigate():
client.navigate({
appCode: "SERVICE_DESK",
path: "/tickets/TK-1002",
query: { from: "orders", orderId: "1001" },
});子应用之间的业务消息由基座转发,避免子应用直接依赖彼此实例:
host.on<{ orderId: string }>("ORDER_APP", "order:selected", (payload) => {
host.emit("SERVICE_DESK", "order:selected", payload);
});本地子应用调试
子应用脱离基座单独启动时,可以用 createDevShell() 注入本地上下文:
import { createDevShell } from "xpe/dev-shell";
const devShell = createDevShell({
context: {
appCode: "ORDER_APP",
appName: "订单中心",
basePath: "/a",
entryPath: "/a/#/",
defaultPath: "/overview",
accessStatus: "enterable",
currentUser: {
memberId: "member-1",
userId: "user-1",
realName: "Local User",
phone: "13800000000",
},
currentTenant: {
tenantId: "tenant-1",
tenantCode: "LOCAL",
tenantName: "本地租户",
},
currentOrganization: null,
menus: [],
permissions: ["order.view"],
},
});
devShell.install();API 参考
createHost(options)
基座高层入口。
常用参数:
container:子应用运行时挂载容器。apps:应用列表,每项包含manifest、appPath、defaultPath。getEntryContext(appCode):返回当前用户进入某应用时的上下文。defaultAppCode:URL 不匹配任何应用时默认进入的应用。onHostNavigate(target):子应用跳基座页面时触发。onUnauthorized(context):应用不可进入时触发。onError(error):运行时错误回调。
返回值:
start():启动当前 URL 对应的应用,默认监听浏览器前进/后退。navigate(appCode, pathOrTarget):基座菜单跳子应用页面。emit(appCode, eventName, payload):基座向指定子应用发送消息。on(appCode, eventName, handler):基座监听指定子应用的消息。off(appCode, eventName, handler):取消基座消息监听。reload():强制重挂当前应用。preload(manifest):预加载应用资源。destroy(appCode):彻底销毁指定子应用实例。dispose():取消浏览器监听。
defineAppLifecycle(options)
子应用通用生命周期入口。
常用参数:
mount(container, context, client):挂载子应用,返回 cleanup 函数。defaultContainer:高级选项,默认#app。appCode/basename/routeMode:高级选项,通常由 XPE 自动推导。
createAppClient(options)
子应用 SDK。优先使用 defineAppLifecycle() 注入的 client;业务深层组件可以通过框架的 provide/inject、context、hook 再向下传递。
脱离生命周期单独获取能力时,可以使用 createAppClient()。
常用方法:
getContext():读取进入上下文。hasPermission(code):判断权限。navigate(target):跨子应用跳转。navigateHost(target):跳基座页面。syncRouter(router, options):同步子应用 router 和基座 URL。emit/on/off:子应用与基座事件通信。
缓存和重新挂载
XPE 有三层复用语义:
- 资源复用:同一个子应用的入口资源可被复用。
- 实例复用:同一个运行时名称对应同一个子应用实例。默认运行时名称是
appCode。 - 应用保活:
manifest.runtime.alive = true时,切走只失活可见 DOM,业务状态保留。
普通跨应用切换不会调用 host.destroy()。
强制重挂当前应用:
await host.reload();彻底销毁指定应用:
await host.destroy("ORDER_APP");