npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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");

基座快速接入

基座接入分三步:

  1. 准备应用 manifest。
  2. 实现 getEntryContext(appCode)
  3. 创建并启动 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_DESKappPath/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:应用列表,每项包含 manifestappPathdefaultPath
  • 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 有三层复用语义:

  1. 资源复用:同一个子应用的入口资源可被复用。
  2. 实例复用:同一个运行时名称对应同一个子应用实例。默认运行时名称是 appCode
  3. 应用保活:manifest.runtime.alive = true 时,切走只失活可见 DOM,业务状态保留。

普通跨应用切换不会调用 host.destroy()

强制重挂当前应用:

await host.reload();

彻底销毁指定应用:

await host.destroy("ORDER_APP");