npm.io
0.0.2 • Published 3d ago

@epochx/micro

Licence
MIT
Version
0.0.2
Deps
0
Size
241 kB
Vulns
0
Weekly
0

@epochx/micro

面向可信内部子应用的 HTML Entry 微前端运行时。它不绑定 React、Vue 或宿主业务,提供入口加载、Shadow DOM、Proxy window、生命周期编排、资源策略、错误分级和统一清理。

能力

  • HTML Entry 并发缓存、TTL、超时、重试、大小限制和取消
  • <base href>、CSS url()@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
  • 阻止子应用直接调用 locationhistory 修改宿主导航
  • 支持脚本、样式和模板资源 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=lifecycleMicroRuntimeError
  • 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.

Keywords