@epochx/micro
@epochx/micro
面向可信内部子应用的 HTML Entry 微前端运行时。它不绑定 React、Vue 或宿主业务,提供入口加载、Shadow DOM、Proxy window、生命周期编排、资源策略、错误分级和统一清理。
能力
- HTML Entry 并发缓存、TTL、超时、重试、大小限制和取消
<base href>、CSSurl()、@import、DOM 资源地址重写- Shadow DOM 样式边界和严格样式加载失败检测
- classic IIFE/UMD Proxy window 沙箱
- native module 兼容模式和全局注册串行保护
bootstrap / mount / update / unmount生命周期串行、超时和状态查询- 并发实例所有权保护,旧实例不会覆盖新实例
- timer、RAF、事件监听、child fetch、adapter cleanup 自动释放
- 同步/异步事件总线错误隔离和监听器泄漏预警
- ESM、CommonJS、TypeScript 类型、浏览器全局 adapter
安装
pnpm add @epochx/micro
宿主应用
import {
createMicroAppInstance,
MicroEventBus,
setupMicroApps,
type MicroAppManifest
} from "@epochx/micro";
const controller = new AbortController();
const bus = new MicroEventBus({
onError(error, context) {
reportError(error, context);
}
});
const manifest: MicroAppManifest = {
name: "orders",
entry: "https://cdn.example.com/micro-apps/orders/",
preload: true,
props: {
auth: { token: "token" },
theme: { mode: "dark" },
bus
},
shared: window.__MICRO_SHARED__,
entryTimeout: 15_000,
scriptTimeout: 15_000,
lifecycleTimeout: 12_000,
styleTimeout: 8_000,
entryRetries: 1,
cacheTtl: 300_000,
maxEntrySize: 5 * 1024 * 1024,
strictStyleLoading: true,
signal: controller.signal,
resourcePolicy: {
allowedOrigins: ["self", "https://static.example.com"],
allowInlineScripts: true,
allowModuleScripts: false
}
};
await setupMicroApps([manifest]);
const host = document.querySelector<HTMLElement>("#micro-host");
if (!host) throw new Error("micro host is missing");
const instance = await createMicroAppInstance(manifest, host, {
onError(appName, error) {
reportError(error, { appName, phase: error.phase });
},
onPerf(appName, metric) {
reportMetric(appName, metric);
}
});
await instance.mount();
await instance.update({ auth: { token: "next-token" }, theme: { mode: "light" }, bus });
console.info(instance.getState());
await instance.unmount();
路由离开时必须调用 unmount()。也可以 abort manifest.signal,运行时会自动排队卸载。
子应用
import { registerMicroSubApp } from "@epochx/micro/sub-app";
let frameworkRoot: { unmount(): void } | undefined;
registerMicroSubApp({
rootSelector: "#root",
render({ root, props, shared, emit, addCleanup }) {
frameworkRoot = createFrameworkRoot(root, props, shared);
const unsubscribe = props.bus?.$on?.("portal:theme-change", updateTheme);
if (typeof unsubscribe === "function") addCleanup(unsubscribe);
emit("micro:ready");
},
update({ props }) {
updatePlatformState(props);
},
destroy() {
frameworkRoot?.unmount();
frameworkRoot = undefined;
}
});
adapter 会注册 window.__CUSTOM_MICRO_APP__ 和 window.__MICRO_APP__。addCleanup 支持同步或异步清理,并按注册逆序执行。
无构建工具的子应用可以使用发布产物 dist/custom-micro-adapter.global.js:
<script src="https://cdn.example.com/custom-micro-adapter.global.js"></script>
<script>
CustomMicroAdapter.register({
render: function (context) {
context.root.textContent = "mounted";
}
});
</script>
该全局文件由 TypeScript adapter 自动构建,不再维护重复实现。
默认安全策略
默认执行以下保护:
- 删除
onclick等 inline event 属性 - 删除
iframe / object / embed - 删除
javascript:等可执行资源 URL - 阻止子应用直接调用
location和history修改宿主导航 - 支持脚本、样式和模板资源 origin allowlist
- 支持外部 classic script 的 SHA-256/384/512 integrity 校验
- 支持注入样式和 module script 的 CSP nonce
兼容旧应用时可显式开启:
resourcePolicy: {
allowInlineEventHandlers: true,
allowEmbeds: true,
allowHostNavigation: true
}
不要在不理解影响时开启这些选项。
重要安全边界
Proxy sandbox 是工程隔离,不是浏览器安全边界。classic script 通过 new Function + with(window) 执行:
- CSP 必须允许
unsafe-eval,否则 classic 模式无法运行。 - JavaScript 仍可能通过原型链、DOM 对象或未代理 Web API 接触真实页面。
- native module 在真实 window 中执行,隔离强度更低。
- 本运行时只适合组织内可信、经过审核的子应用。
- 不可信代码、第三方插件、多租户脚本必须使用跨源 iframe,并配置 sandbox、CSP 和 postMessage 白名单。
失败保护
- entry:超时、重试、响应大小限制、失败缓存清除
- style:默认加载失败或超时即阻止启动,可用
strictStyleLoading: false降级为警告 - script:请求超时、abort、integrity 校验、module 加载超时
- lifecycle:串行队列和独立超时
- mount 失败:触发 abort,随后 unmount 仍会执行子应用释放逻辑
- unmount 失败:清理仍然完成,最终抛出带
phase=lifecycle的MicroRuntimeError - observer:
onPerf/onError/onWarn自身异常不会破坏主流程 - concurrency:同一 Shadow Root 的旧实例会被判定 superseded,不会清空新实例 DOM
性能建议
- 只 preload 高频应用。
- 为 HTML 设置短缓存或
cacheTtl,hash JS/CSS 设置长期缓存。 - 不要通过 props 高频传递大对象;平台状态走 props,业务状态留在子应用 store。
- classic 子应用建议构建为单一 IIFE/UMD 入口,避免多个 script 的全局词法作用域差异。
- 共享依赖只共享兼容版本,避免强行共享不同 React/Vue 大版本。
已知兼容边界
- Shadow DOM 外的弹窗、Portal、字体和全局遮罩需要子应用自行约定挂载点。
- CSS
url()重写覆盖常见语法,不是完整 CSS AST 解析器;复杂 CSS 建议使用独立 stylesheet。 - module script 为浏览器原生执行,不提供强 JS 隔离。
- 生命周期超时只能停止宿主等待,无法强制终止已经运行的同步 JavaScript。
开发与发布检查
pnpm install
pnpm check
check 包括:类型检查、单元测试、ESM/CJS/IIFE 构建、产物 smoke test 和 npm pack。
从原 base 目录迁移
// before
import { registerMicroSubApp } from "../../src/packages/custom-micro-runtime/subAppAdapter";
// after
import { registerMicroSubApp } from "@epochx/micro/sub-app";
宿主改为从 @epochx/micro 导入。生产环境建议发布到内部 npm registry 并锁定明确版本。
Untrusted applications: iframe isolation
Use the browser sandbox when the child is not fully trusted. The default sandbox is allow-scripts only.
import { createIframeMicroAppInstance } from "@epochx/micro";
const app = await createIframeMicroAppInstance(
{
name: "external-reports",
entry: "https://reports.example.com/micro.html",
props: { tenantId: "acme" },
allowedEvents: ["report:ready"],
onEvent(event, payload) {
console.info(event, payload);
},
},
document.querySelector("#micro-host")!,
);
await app.mount();
The child registers lifecycle functions from @epochx/micro/iframe-sub-app. Props and events must be structured-cloneable. For a third-party page with no adapter, set bridge: "none"; it can mount and unmount but cannot update. See SECURITY.md.
Production failure protection
The runtime automatically tears down trusted applications after mount/update failures. Iframe child events are denied by default; configure allowedEvents, and use validateEvent for business payload validation. Set credentialless: true when embedding under COEP/cross-origin isolation. Props and events default to 256 KiB and child events to 100/second. Custom authenticated fetchers do not share HTML cache unless an explicit, tenant-safe cacheKey is provided. The default HTML cache TTL is five minutes and classic scripts default to a 10 MiB limit.
Update semantics
Sub-app adapters no longer call render a second time implicitly when update is missing. Implement update, or explicitly set rerenderOnUpdate: true; the latter performs destroy and cleanup before rendering again. Iframe adapter dispose() stops waiting after five seconds by default and can be configured with disposeTimeout.