microapp-hmr
v1.0.9
Published
Module-level HMR for MicroApp/UMD: EventSource + fetch/eval hot-update, component change analysis, MicroApp.reload(). Webpack 4/5.
Maintainers
Readme
microapp-hmr
模块级热更新运行时 · 为微前端架构打造的增量开发利器
代码保存 → 浏览器自动感知 → 只重载受影响的子应用,整页不刷新
📖 简介
microapp-hmr 是为 MicroApp/UMD 场景设计的模块级热更新解决方案。它让微前端架构下的开发体验飞跃提升——修改子应用代码后,只有受影响的子应用自动重载,宿主页面保持不刷新,开发效率成倍提升。
核心特性
- 🚀 真正的模块级 HMR:代码保存 → 自动感知 → 只重载受影响的子应用
- 🔧 零侵入业务代码:通过轻量级 monkey patch 接入,异常时自动降级
- 📊 可观测性:内置控制台面板,实时展示变更文件、影响的子应用和组件
- 🛡️ 安全可靠:构建失败自动跳过 HMR,仅限开发环境使用
- 🔌 多工程支持:一个 devServer + 多个仅构建工程,通过 Broker 统一热更新
设计理念
当前采用整包重载而非细粒度组件级热更,是基于微前端架构的理性选择:
- 子应用为独立 bundle,不共享宿主模块图
- 无常驻运行时做细粒度补丁
- 整包重载侵入最小、行为可预期
- 内置静态分析追踪「文件变更影响的组件」,便于排查
未来可在此基础上探索组件级 HMR,保留组件 state、只替换变更模块。
📦 安装
# 作为开发依赖安装(仅开发态使用)
npm install microapp-hmr -D安全警告
- HMR 运行时通过
fetch+new Function执行远端脚本,仅限可信内网开发环境。请勿在公网或不可信环境开启 HMR。enhance-dev-server.cjs中挂载的/__webpack_hmr与/__microapp_hmr_publish__端点同样只面向本地/内网开发,如需在多工程场景下使用 Broker,请确保这些端点不暴露到生产环境,并在需要时配置MICROAPP_HMR_BROKER_TOKEN做简单鉴权。
🚀 快速开始
前提条件
- webpack 4 或 webpack 5 + HotModuleReplacementPlugin
- webpack-hot-middleware
- 宿主应用提供
window.__HMR_RELOAD_MODULE__(moduleName)接口(由 MicroApp 补丁注入) - 可选:
@babel/parser、@babel/traverse(用于组件提取,未安装时自动降级)
方案一:一键集成(推荐)
在 webpack 配置的开发分支中调用 applyToWebpack。缺必需配置时仅跳过 HMR、打 warning,不抛错,工程照常构建。
// project.js 或 webpack.config.js
if (process.env.NODE_ENV !== 'production') {
const microappHmr = require('microapp-hmr');
microappHmr.applyToWebpack(webpackConfig, webpack, {
// 可选:重载前清除的全局变量
clearGlobalsBeforeReload: ['Konva'],
// 可选:未解析到模块名时的行为
whenNoModuleNames: 'reload-page', // 'reload-page' 或 'none'
});
}什么时候需要提供
isModuleEntry/isPageEntry?
- 默认(不传):按 @hi 约定识别
m/与p/;- 自定义前缀时:请同时提供两者,或改用
moduleEntryPrefixes+pageEntryPrefixes。设计上要求二者成对出现,是为了保证以下链路一致:
- 模块入口:注入 HMR runtime,负责连接 EventSource、触发子应用重载
- 页面入口:注入 MicroApp 补丁,挂载
__HMR_RELOAD_MODULE__函数;该函数只有在页面里执行,宿主/SystemJS 才会先于子应用加载,MicroApp 才能被正确 patch
方案二:手动配置
如需更细粒度的控制,可分别配置各个组件:
// Loader:组件提取
webpackConfig.module
.rule('hmr-component-extractor')
.test(/\.(jsx?|tsx?)$/)
.exclude.add(/node_modules/)
.end()
.use('hmr-loader')
.loader(microappHmr.getLoaderPath())
.end();
// Plugin:广播文件变更
webpackConfig
.plugin('hmr-file-change')
.use(microappHmr.HMRFileChangePlugin, [{
logLevel: 'none',
historyLimit: 10
}]);
// 运行时注入(仅对模块入口 prepend runtime)
const isModuleEntry = (name) => name && String(name).startsWith('m/');
entryStore.forEach((entry, entryName) => {
if (isModuleEntry(entryName)) {
entry.prepend(microappHmr.getRuntimePath());
}
});与 @hi/cli 集成
当与 @hi/cli 一起使用时,建议显式传入与你工程一致的 webpack 实例(支持 webpack4 / webpack5)。在混合依赖环境中直接 require('webpack') 可能拿到错误版本,导致 HMR 失效。下面示例优先拿项目 webpack,不匹配时回退到 @hi/cli-core 路径解析:
// project.js
const { dirname } = require('path');
function getWebpackForProject() {
try {
const wp = require('webpack');
if (wp.version && (/^4\./.test(wp.version) || /^5\./.test(wp.version))) return wp;
} catch {}
const cliCoreDir = dirname(require.resolve('@hi/cli-core/package.json'));
return require(require.resolve('webpack', { paths: [cliCoreDir] }));
}
const webpack = getWebpackForProject();
function buildConfig(webpackConfig) {
if (process.env.NODE_ENV !== 'production') {
const microappHmr = require('microapp-hmr');
microappHmr.applyToWebpack(webpackConfig, webpack, {
// 可省略:默认已按 @hi 的 m/p 识别
isModuleEntry: (name) => name && String(name).startsWith('m/'),
isPageEntry: (name) => name && String(name).startsWith('p/'),
});
}
}开发命令配置
在加载 @hi/cli-core 之前先加载包内 enhance-dev-server(对 @hi/cli 的 build 中间件做 HMR 补丁),再执行 cli。强烈建议单独定义开发专用命令,不要覆盖生产 build:
{
"scripts": {
"dev:hmr": "microapp-hmr-dev-build",
"build:hmr": "microapp-hmr-dev-build",
"build": "hicli"
}
}无需任何参数;enhance-dev-server、prepend 脚本等均在包内。microapp-hmr-dev-build 仅适用于开发态 HMR,不应直接绑定到生产 build,避免线上构建意外引入 HMR 中间件。
⚙️ 配置参考
客户端配置
在 HTML 中通过 window.__MICROAPP_HMR_CONFIG__ 配置(可覆盖编译期注入的值):
<script>
window.__MICROAPP_HMR_CONFIG__ = {
whenNoModuleNames: 'reload-page',
hmrPath: '/__webpack_hmr',
hmrOrigin: 'http://127.0.0.1:9601',
logLevel: 'detailed',
changeAnalysisExpanded: true,
};
</script>applyToWebpack 选项
| 选项 | 说明 | 默认值 |
|------|------|--------|
| isModuleEntry | 识别子应用/模块入口(与 isPageEntry 成对传入) | 默认按 m/ |
| isPageEntry | 识别页面入口(与 isModuleEntry 成对传入) | 默认按 p/ |
| moduleEntryPrefixes | 显式模块 entry 前缀数组(与 pageEntryPrefixes 成对传入) | ["m"] |
| pageEntryPrefixes | 显式页面 entry 前缀数组(与 moduleEntryPrefixes 成对传入) | ["p"] |
| moduleDir | 模块目录名(用于模块名路径解析) | "modules" |
| pageDir | 页面目录名(用于兼容检查) | "pages" |
| warnIfEntryDirsMissing | 目录不存在时告警(不阻塞) | false |
| publicPath | 资源公共路径 | 从 webpack 推导 |
| hmrOrigin | HMR 源地址 | 与 publicPath 同源 |
| hmrPath | EventSource 路径 | "/__webpack_hmr" |
| whenNoModuleNames | 未解析到模块名时的行为 | "reload-page" |
| showHmrPanel | 是否显示 HMR 面板 | true |
| pluginLogLevel | Node 侧日志级别(含 broker 调试) | "none" |
| runtimeLogLevel | 浏览器侧日志级别 | "detailed" |
| historyLimit | 面板历史记录条数 | 10 |
| clearGlobalsBeforeReload | 重载前清除的全局变量 | null |
环境变量配置
通过 .env 进行差异化配置,避免提交到代码仓库:
# 插件日志级别(同时控制 broker 调试日志)
MICROAPP_HMR_PLUGIN_LOG_LEVEL=none
# 运行时日志级别
MICROAPP_HMR_RUNTIME_LOG_LEVEL=detailed
# 是否显示 HMR 面板
MICROAPP_HMR_SHOW_PANEL=true
# 历史记录条数
MICROAPP_HMR_HISTORY_LIMIT=10
# 多工程 Broker:仅配置端口即可,host/协议沿用当前 dev 地址
MICROAPP_HMR_BROKER_PORT=9601
# 可选:完整 Broker 地址(优先级高于 BROKER_PORT)
# MICROAPP_HMR_BROKER_ORIGIN=http://127.0.0.1:9601
# MICROAPP_HMR_BROKER_PATH=/__microapp_hmr_publish__
# MICROAPP_HMR_BROKER_TOKEN=dev-only-secret优先级:显式传入选项 > 环境变量 > 默认值。
🔄 多工程场景
当一个 devServer 服务多个仅构建工程时,通过 Broker 模式实现统一热更新。
架构示意
┌─────────────────┐ ┌─────────────────┐
│ DevServer 工程 │ │ 仅构建工程 A │
│ (Broker) │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ 构建 + 服务 │ │ │ │ 构建 │ │
│ └─────────────┘ │ │ └─────────────┘ │
│ ↑ │ │ ↓ │
└────────┼─────────┘ └────────┼─────────┘
│ 广播事件 │ POST
│ │ 变更事件
┌────┴─────────────────────────┴────┐
│ Broker 服务 │
│ /__webpack_hmr (SSE) │
│ /__microapp_hmr_publish__ (API) │
└────────────────────────────────────┘配置步骤
DevServer 工程:正常集成
applyToWebpack并运行microapp-hmr-dev-build。devServer 会自动挂载/__webpack_hmr与/__microapp_hmr_publish__,无需额外配置 Broker。仅构建工程:同样集成
applyToWebpack,以 watch 方式运行microapp-hmr-dev-build,在.env中配置 同一 Broker 端口:
MICROAPP_HMR_BROKER_PORT=9601浏览器只需连接一台 devServer,即可接收所有工程的变更事件。只要有一个工程开启 devServer,其它 N 个工程配置相同端口并跑 microapp-hmr-dev-build,就都具备 HMR 能力。
多工程下的缓存与重载
仅构建工程的子应用可能由不同端口提供(如 http://127.0.0.1:9602/m/xxx/index.js)。重载时会优先使用 instance.state.module.entry 作为 SystemJS 缓存清理的 URL,确保清理的是实际加载的模块地址,避免「更新后仍显示旧内容」;若框架未暴露该字段,则回退到 moduleEntryUrlTemplate 推导。
🎯 设计决策
为什么是「模块级」而非「组件级」?
当前微前端架构下,整包重载是最优折衷:
| 架构约束 | 说明 |
|---------|------|
| 宿主与子应用分离 | 页面入口是宿主 bundle,子应用为独立 entry,无静态依赖图 |
| 事件接收方 | EventSource 由模块 entry 建立,收到事件的是已加载模块的上下文 |
| 模块名解析 | 默认按 @hi/cli-core 的 m/ 前缀识别;也可用 moduleEntryPrefixes 显式覆盖 |
变更处理策略
有 moduleNames(变更在模块目录下,路径匹配上述前缀) → 调用
__HMR_RELOAD_MODULE__(name),只重载对应子应用,不刷新整页。无 moduleNames(变更在 pages/ 或其它)** → 宿主代码已变,仅重载子应用无效,必须整页刷新。
→ 刷新前将变更文件写入 sessionStorage,刷新后控制台打印触发文件。
未采用的方案
- ❌ 页面变更时只重载当前页上的顶层 MicroApp:无法更新宿主代码
- ❌ 插件推导 page → 所用模块映射:无静态可解析映射,不可靠
- ❌ 依赖页面 entry 自带的 webpack HMR:行为不统一
🔍 常见问题排查
"Several Konva instances detected" 警告
原因:HMR 重载时新旧 Konva 实例共存
解决:配置 clearGlobalsBeforeReload: ["Konva"]
"HMR_RELOAD_MODULE is not a function"
原因:@hi/core 未加载或 monkey-patch 未应用
检查:确保 monkey-patch-microapp.js 已注入到页面入口;控制台应出现 "[HMR] MicroApp 补丁已应用"
EventSource 连接异常
原因:hmrOrigin 与 devServer 实际地址不一致
检查:确保 hmrOrigin、publicPath 与 devServer 端口一致(如 http://127.0.0.1:9601)
构建失败为何不触发重载?
原因:编译有错误时(ESLint/TS 报错),新 bundle 不可用,不应 reload
行为:插件检测到 stats.hasErrors() 时,广播 hmr-build-failed 事件,面板显示红标
如何临时关闭 HMR?
在控制台执行:window.__HMR_STOP__(),会恢复 document.head.appendChild、关闭 EventSource 并清理状态
多工程下「仅构建工程」更新后仍显示旧内容?
原因:SystemJS 缓存 key 与真实模块 URL 不一致(例如用错了端口)
解决:当前实现会优先使用 instance.state.module.entry 清理缓存;若框架未暴露该字段,请确保 moduleEntryUrlTemplate 与实际产出路径一致
TypeScript 报错:Property '__COMPONENT_MAP__' / '__MODULE_ID_MAP__' does not exist on type 'Window'
原因:HMR loader 会在运行时往 window 上挂这些属性,TypeScript 需要加载 microapp-hmr 的 global.d.ts 才能通过类型检查。
推荐做法(二选一):
项目根下已有全局声明文件(如
typings.d.ts、env.d.ts):在其中追加一行即可,无需新建文件。/// <reference types="microapp-hmr/global.d.ts" />没有现成全局声明文件:在
tsconfig.json里用include显式包含包内的类型声明,不影响@types/*的自动发现。{ "compilerOptions": { ... }, "include": ["**/*", "node_modules/microapp-hmr/global.d.ts"] }
不推荐:根目录单独新建 microapp-hmr.d.ts(多一个零散文件);或设置 compilerOptions.types: ["microapp-hmr", ...](会变成白名单模式,需手动维护所有 @types/*)。
📦 构建与发布
# 安装依赖
npm install
# 构建
npm run build # Rollup 编译:生成 dist/ 和 runtime.js
# 测试
npm test输出:
dist/:Rollup 编译的 CJS/ESM 模块dist/runtime.js:运行时文件(IIFE,可直接 prepend 注入)
模块导出
// 主入口
require('microapp-hmr') → {
DEFAULT_OPTIONS,
APPLY_WEBPACK_DEFAULTS,
getLoaderPath,
getRuntimePath,
HMRFileChangePlugin,
applyToWebpack,
// ...
}
// 单项导入
require('microapp-hmr/loader') // Webpack Loader
require('microapp-hmr/plugin') // HMRFileChangePlugin
require('microapp-hmr/runtime') // Runtime 脚本路径📄 License
MIT © 2026
🙏 致谢
感谢所有为微前端架构贡献智慧的开发者们。microapp-hmr 站在巨人的肩膀上,致力于让微前端开发体验更美好。
