Webpack HMR 允许在运行时更新所有类型的模块,而无需完全刷新。这能极大地改善开发体验,特别是在调整样式或者 UI 逻辑时。
核心流程概述
1.启动与监听:
- 开发者通过
webpack-dev-server
(WDS) 或webpack-hot-middleware
(WDM) 启动一个开发服务器。 - Webpack 开始编译项目,并将 HMR 相关的运行时代码(HMR Runtime)注入到打包后的
bundle.js
中。 - WDS/WDM 会监听项目文件的变化。
2.文件变更:
- 当开发者修改并保存一个文件时,文件系统会通知 WDS/WDM。
3.重新编译:
- WDS/WDM 不会直接重新构建整个项目,而是通知 Webpack 只重新编译发生变更的模块以及与这些模块相关的依赖。
- Webpack 会生成一个或多个
update chunk
(更新块,通常是 JSON 格式的 manifest 和 JavaScript 代码)。这些update chunk
只包含发生变化的代码。
4.通知浏览器 (HMR Runtime) :
- WDS/WDM 通过 WebSocket (通常情况下) 将一个包含新
hash
和update chunk
信息的通知发送给浏览器中运行的 HMR Runtime。
5.浏览器端处理:
- HMR Runtime 接收到通知。
- 它会向服务器发起 HTTP 请求,下载包含具体代码变更的
update chunk
(通常是.hot-update.json
和.hot-update.js
文件)。 .hot-update.json
文件包含了本次更新的hash
以及需要更新的chunk
ID。.hot-update.js
文件包含了实际更新的代码。
6.模块热替换:
- HMR Runtime 尝试应用这些更新。
- 检查更新能力 (Checking) : HMR Runtime 会检查哪些模块可以被“热”更新。一个模块能够被热更新,通常需要该模块或其父模块通过
module.hot.accept()
API 声明了它接受热更新。 - 应用更新 (Applying) :
- 如果一个模块声明了
module.hot.accept()
,并且其依赖也发生了变化,那么 HMR Runtime 会执行accept
回调。开发者可以在这个回调中处理旧模块的清理和新模块的应用,例如重新渲染组件、更新 store 等。 - 如果一个模块自身没有声明
accept
,但其父模块声明了accept
其子模块的更新,那么更新会“冒泡”到父模块,由父模块的accept
回调处理。 - 如果更新冒泡到入口文件或者没有任何模块接受更新,HMR 将会失败。
- 如果一个模块声明了
- 失败处理: 如果 HMR 失败(例如,没有模块接受更新,或者
accept
处理器中发生错误),HMR Runtime 会根据配置决定下一步操作:- 自动刷新 (Live Reload) : 这是最常见的后备方案,浏览器会执行一次完整的页面刷新。
- 不执行任何操作: 开发者会在控制台看到 HMR 失败的错误信息。
7.状态保持: HMR 的一个关键优势是尽可能地保持应用的当前状态。例如,React 组件的状态、Vue 组件的状态、Redux/Vuex store 中的数据等,在成功的 HMR 更新后通常会得到保留,除非 accept
逻辑中明确重置了它们。
核心原理
HMR 的核心在于将发生变化的代码块替换掉旧的代码块,同时尽量不影响其他未变化的代码和应用状态。这依赖于以下几个关键点:
- 模块化: Webpack 将所有资源都视为模块。这种模块化的结构使得替换单个模块成为可能。
- 运行时 (HMR Runtime) : 注入到浏览器端的 HMR Runtime 是整个机制的“大脑”。它负责与开发服务器通信、下载更新、管理模块间的关系、以及执行替换逻辑。
module.hot
API: Webpack 提供了一个module.hot
对象,它包含了一系列 API,允许开发者控制模块如何响应热更新。module.hot.accept(dependencies, callback)
: 声明该模块接受指定依赖的更新。当依赖更新时,执行回调。如果dependencies
未指定或为true
,则表示接受自身模块的更新。module.hot.dispose(callback)
: 注册一个回调函数,在模块被替换之前执行。用于清理旧模块产生的副作用,如移除事件监听器、清除定时器等。module.hot.decline(dependencies)
: 声明该模块不接受指定依赖的更新。如果这些依赖更新,会导致 HMR 失败并可能触发页面刷新。module.hot.status()
: 返回当前 HMR 的状态 (idle, check, prepare, ready, dispose, apply, abort, fail)。
- JSONP (或类似机制) 加载更新:
.hot-update.js
文件通常是以 JSONP 的形式加载的。这意味着它们是可执行的 JavaScript 代码,其中包含了一个全局回调函数,HMR Runtime 会定义这个回调函数来接收和处理更新的代码。 - 模块缓存与替换: Webpack 在浏览器端维护了一个模块缓存 (
installedModules
或类似名称)。当一个模块被热更新时,旧模块的代码会从缓存中移除(或标记为失效),新模块的代码会被执行并放入缓存。dispose
处理器用于清理旧模块,accept
处理器用于集成新模块。
我们将关注以下几个主要部分:
- Webpack Dev Server (WDS) / Webpack Hot Middleware (WDM) 的文件监听与消息发送
- Webpack 编译时 HMR 插件的注入 (主要是
HotModuleReplacementPlugin
) - 浏览器端 HMR Runtime 的核心逻辑
module.hot
API 的实现和使用
1. 文件监听与消息发送 (WDS/WDM 层面 - 概念性)
// 伪代码 - webpack-dev-server/lib/Server.js 或类似逻辑
class DevServer {
constructor(compiler, options) {
this.compiler = compiler;
this.options = options;
this.sockets = []; // 存储 WebSocket 连接
this.setupApp(); // 设置 Express app 和中间件
this.setupWatchFiles(); // 监听文件变化
this.setupSocketConnection(); // 设置 WebSocket 服务
}
setupWatchFiles() {
// 使用 chokidar 或类似库监听文件系统
const watcher = chokidar.watch(this.options.watchFiles, {
ignored: /node_modules/,
persistent: true
});
watcher.on('change', (filePath) => {
console.log(`File ${filePath} has been changed`);
// 通知 Webpack 重新编译 (通常通过 compiler.watch 或 invalidate)
this.compiler.watching.invalidate();
});
}
setupSocketConnection() {
// 使用 ws 或 sockjs 建立 WebSocket 服务器
const wss = new WebSocket.Server({ server: this.listeningApp });
wss.on('connection', (ws) => {
this.sockets.push(ws);
ws.on('close', () => {
this.sockets = this.sockets.filter(s => s !== ws);
});
});
// Webpack 编译完成后,通过 WebSocket 发送消息
this.compiler.hooks.done.tap('DevServer', (stats) => {
if (stats.hasErrors()) {
this.sendMessage(this.sockets, 'errors', stats.toJson().errors);
return;
}
this.sendStats(this.sockets, this.getStats(stats));
});
}
sendStats(sockets, stats, force) {
const { hot, currentHash } = stats; // currentHash 是本次编译的 hash
if (!hot) { // 如果没有启用 HMR,则发送 live-reload 信号
return this.sendMessage(sockets, 'liveReload');
}
// ... 其他逻辑 ...
// 发送 HMR 更新信号
// 'hash' 事件用于更新客户端的编译 hash
this.sendMessage(sockets, 'hash', currentHash);
// 'ok' 事件或 'still-ok' 事件表明编译成功,可以进行热更新检查
// 实际发送的消息结构会更复杂,包含 action: 'built', time, hash, warnings, errors, modules 等
this.sendMessage(sockets, 'ok'); // 简化表示
// 或者更具体的:
// this.sendMessage(sockets, 'building');
// this.sendMessage(sockets, 'built', { hash: currentHash, modules: ... });
}
sendMessage(sockets, type, data) {
sockets.forEach(socket => {
socket.send(JSON.stringify({ type, data }));
});
}
}
// 当 Webpack 编译完成后 (compiler.hooks.done)
// WDS 会获取到新的 stats 对象,其中包含了新的 hash 和变化的模块信息。
// 它会构建一个消息,类似:
// {
// type: "hot", // 或者 "ok", "built"
// data: {
// hash: "new-compilation-hash",
// // WDM 可能会直接发送更新的模块信息
// // WDS 通常只发送 hash,客户端再根据 hash 去拉取 manifest
// }
// }
// 然后通过 WebSocket 发送给客户端。
解释:
chokidar
用于高效地监听文件变化。- 当文件变化时,
compiler.watching.invalidate()
会告诉 Webpack 当前的编译结果已失效,需要重新编译。 - Webpack 编译完成后 (
compiler.hooks.done
),会触发回调。 sendStats
方法会判断是否启用了 HMR。如果启用了,它会通过 WebSocket 向所有连接的客户端发送一个包含新hash
的消息 (通常是hash
事件和ok
或built
事件)。客户端的 HMR Runtime 会监听这些消息。
2. Webpack 编译时 HMR 插件的注入 (HotModuleReplacementPlugin
)
webpack/lib/HotModuleReplacementPlugin.js
是核心。
// 伪代码/简化逻辑 - webpack/lib/HotModuleReplacementPlugin.js
const { sources, Compilation } = require("webpack");
const NormalModule = require("./NormalModule"); // Webpack 中表示模块的类
const RuntimeGlobals = require("./RuntimeGlobals");
const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin");
const {
getFullHash,
getChunkFilenameTemplate
} = require("./javascript/JavascriptModulesPlugin");
const JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin"); // 用于 JSONP 加载
class HotModuleReplacementPlugin {
constructor(options) {
this.options = options || {};
}
apply(compiler) {
const {
hotUpdateChunkFilename, // e.g., "[id].[fullhash].hot-update.js"
hotUpdateMainFilename, // e.g., "[runtime].[fullhash].hot-update.json"
} = compiler.options.output;
compiler.hooks.compilation.tap(
"HotModuleReplacementPlugin",
(compilation) => {
// 确保 HMR 相关的 RuntimeGlobals 被使用
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadManifest)
.tap("HotModuleReplacementPlugin", (chunk, set) => {
set.add(RuntimeGlobals.publicPath);
set.add(RuntimeGlobals.getChunkScriptFilename);
set.add(RuntimeGlobals.loadScript);
set.add(RuntimeGlobals.hasOwnProperty);
compilation.addRuntimeModule(
chunk,
new HotUpdateChunkTemplateRuntimeModule() // 生成加载 manifest 的逻辑
);
return true;
});
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadUpdateHandlers)
.tap("HotModuleReplacementPlugin", (chunk, set) => {
// ... 注入加载 hot-update.js 的逻辑
compilation.addRuntimeModule(
chunk,
new HotModuleReplacementRuntimeModule() // 主要的 HMR 运行时逻辑
);
return true;
});
// 为每个模块添加 module.hot 对象
const provideModuleHot = (module) => {
// module.hot = { ... }
// 这里的实现比较复杂,涉及到模块父子关系、状态管理等
// 简化版:
// module.hot = createHotModule(module.id);
};
compilation.hooks.compilation.tap(
"HotModuleReplacementPlugin",
(compilation) => {
NormalModule.getCompilationHooks(compilation).loader.tap(
"HotModuleReplacementPlugin",
(loaderContext, module) => {
// 在 loaderContext 上挂载 hot API,使得模块内部可以通过 module.hot 访问
loaderContext.hot = module.hot; // 简化,实际更复杂
}
);
}
);
// 记录哪些模块被更改了
let updatedModules = new Set();
compilation.hooks.finishModules.tap(
"HotModuleReplacementPlugin",
(modules) => {
modules.forEach(module => {
if (module.is κάθε()) { // is κάθε() 检查模块是否自接受或被父模块接受
// ...
}
});
}
);
// 生成 .hot-update.json (manifest) 和 .hot-update.js (更新的代码)
compilation.hooks.afterHash.tap("HotModuleReplacementPlugin", () => {
const { records } = compilation;
if (!records) return; // 只有在 watch 模式下才有 records
const { hotUpdateChunkMap, hotUpdateMainContent } = this.generateUpdateManifest(compilation);
// hotUpdateChunkMap: { chunkId: [moduleId1, moduleId2] }
// hotUpdateMainContent: { h: newHash, c: { chunkId: true/false } }
if (Object.keys(hotUpdateChunkMap).length > 0) {
// 为每个发生变化的 chunk 生成 .hot-update.js
for (const chunkId of Object.keys(hotUpdateChunkMap)) {
const chunk = compilation.chunks.find(c => c.id === chunkId);
if (chunk) {
const hotUpdateChunk = this.generateHotUpdateChunk(
compilation,
chunk,
hotUpdateChunkMap[chunkId], // 包含的更新模块 ID
compilation.hash // 当前编译的 fullHash
);
// hotUpdateChunk 是一个 Source 对象
compilation.emitAsset(
compilation.getPath(hotUpdateChunkFilename, {
chunk,
hash: compilation.hash, // 使用 compilation.hash 而不是 chunk.renderedHash
}),
hotUpdateChunk
);
}
}
// 生成 .hot-update.json (manifest)
compilation.emitAsset(
compilation.getPath(hotUpdateMainFilename, {
hash: compilation.hash, // 使用 compilation.hash
runtime: Object.keys(compilation.entrypoints)[0] || "main" // 假设的 runtime 名称
}),
new sources.RawSource(JSON.stringify(hotUpdateMainContent))
);
}
});
}
);
// ... 更多钩子和逻辑
}
generateUpdateManifest(compilation) {
// ... 比较上一次编译和本次编译的模块,找出变化的模块和 chunk
// ... 生成 manifest 内容,结构类似:
// {
// "h": "newFullHash", // 新的完整 hash
// "c": { // 变化的 chunk ID
// "chunkId1": true,
// "chunkId2": true
// },
// "r": [], // removed chunk ids
// "m": [] // removed module ids (optional)
// }
// 同时返回一个 map,记录哪些模块属于哪个 chunk
return { hotUpdateChunkMap: { /* ... */ }, hotUpdateMainContent: { /* ... */ } };
}
generateHotUpdateChunk(compilation, chunk, updatedModules, fullHash) {
// ... 对于 chunk 中的每个 updatedModules
// ... 获取其最新的代码
// ... 将这些代码包装在一个 JSONP 函数中,例如:
// self["webpackHotUpdate<runtimeName>"](chunkId, {
// "moduleId1": function(module, exports, __webpack_require__) {
// /* new module code for moduleId1 */
// },
// "moduleId2": function(module, exports, __webpack_require__) {
// /* new module code for moduleId2 */
// }
// }, fullHash); // fullHash 用于校验
// 返回一个 Source 对象
const hotUpdateContent = {};
for (const moduleId of updatedModules) {
const module = compilation.modules.find(m => m.id === moduleId);
if (module) {
// 获取模块渲染后的代码
// 这是一个非常简化的表示,实际过程复杂得多
// 需要从 module.originalSource() 或类似地方获取,并应用模板
let moduleSource = "/* new code for " + moduleId + " */";
try {
const source = module.source(compilation.dependencyTemplates, compilation.runtimeTemplate);
moduleSource = source.source();
} catch (e) {
console.warn(`Could not get source for module ${moduleId}: ${e}`);
}
hotUpdateContent[moduleId] = `
function(module, exports, __webpack_require__) {
__webpack_require__. આપણા('MODULE', module); // '우리' means 'our' in Gujarati, used as a placeholder for internal webpack functions
${moduleSource}
}
`;
}
}
const chunkIdJson = JSON.stringify(chunk.id);
const modulesJson = JSON.stringify(hotUpdateContent)
.replace(/\u2028/g, "\u2028")
.replace(/\u2029/g, "\u2029");
// 假设的 runtimeName,通常是 output.uniqueName 或 output.library
const runtimeName = compilation.outputOptions.uniqueName || compilation.outputOptions.library || "webpackChunk";
return new sources.RawSource(
`self["webpackHotUpdate${runtimeName}"](${chunkIdJson}, ${modulesJson}${getFullHash ? `, "${fullHash}"` : ""})`
);
}
}
解释:
HotModuleReplacementPlugin
(HMRPlugin) 是 Webpack 内置的插件。- 它在编译 (
compilation
) 过程中注入了 HMR Runtime 的核心逻辑 (HotModuleReplacementRuntimeModule
和HotUpdateChunkTemplateRuntimeModule
)。 RuntimeGlobals.hmrDownloadManifest
和RuntimeGlobals.hmrDownloadUpdateHandlers
是 Webpack 内部用来标记需要 HMR 下载和处理逻辑的全局变量。- 它会修改模块,使其能够访问
module.hot
API。 - 在每次编译后 (
afterHash
),如果检测到模块变化并且处于 watch 模式,它会:- 调用
generateUpdateManifest
生成一个 JSON 文件 (如[runtime].[fullhash].hot-update.json
)。这个文件告诉客户端哪些 chunk 发生了变化以及最新的编译hash
。 - 调用
generateHotUpdateChunk
为每个发生变化的 chunk 生成一个 JavaScript 文件 (如[id].[fullhash].hot-update.js
)。这个 JS 文件包含了一个全局函数调用 (如webpackHotUpdate<runtimeName>
),参数是chunkId
和一个包含{ moduleId: newModuleFunction }
的对象。
- 调用
3. 浏览器端 HMR Runtime 的核心逻辑
这部分代码由 HotModuleReplacementRuntimeModule
和 HotUpdateChunkTemplateRuntimeModule
生成,并注入到打包后的 bundle.js
中。
// 伪代码/简化逻辑 - 浏览器端 HMR Runtime
// (全局作用域或 Webpack 运行时作用域内)
var hotApplyOnUpdate = true; // 控制是否在下载完更新后立即应用
var hotCurrentHash = "initial-compilation-hash"; // 当前客户端认为的最新 hash
var hotRequestTimeout = 10000;
var hotCurrentModuleData = {}; // 存储模块的 hot data
var hotCurrentChildModule; // 用于 accept 冒泡
var hotCurrentParentModule; // 用于 accept 冒泡
// 接收来自 WDS 的 WebSocket 消息
// socket.onmessage = function(event) {
// const message = JSON.parse(event.data);
// if (message.type === "hash") {
// hotSetNewHash(message.data); // 记录服务器最新的 hash
// } else if (message.type === "ok" || message.type === "still-ok" || message.type === "built") {
// hotCheck(true); // 开始检查更新
// } else if (message.type === "errors") {
// // 显示编译错误
// }
// // ... 其他消息类型处理,如 'building', 'warnings'
// };
function hotSetNewHash(newHash) {
hotAvailableFilesMap = {}; // 清空已知的更新文件
hotWaitingFiles = 0;
hotRequestedFilesMap = {};
hotFailedRecjectedModules = null;
hotApplyOnUpdate = true;
hotSetStatus("idle"); // 重置状态
hotCurrentHash = newHash; // 更新为服务器告知的最新 hash
}
// __webpack_require__.h = () => hotCurrentHash; (HMR Runtime 会提供这个函数)
// HMR 主函数,用于检查更新
// 'check' is usually called after receiving 'ok' from WDS
function hotCheck(applyOnUpdate) {
if (hotGetStatus() !== "idle") {
// 如果不是空闲状态 (例如正在检查或应用),则抛出错误或等待
return Promise.resolve();
}
hotRequestedFilesMap = {};
hotWaitingFiles = 0;
hotAvailableFilesMap = {};
hotDeferred = []; // 存储延迟执行的模块
return hotUpdateNewHash.then(function() { // hotUpdateNewHash 内部会调用 hmrDownloadManifest
if (!hotAvailableFilesMap) return; // 没有可用的更新
hotRequestedFilesMap = {};
hotWaitingFiles = 0; // 重置计数器
// 并行下载所有 .hot-update.js 文件
// __webpack_require__.hmrC 是 chunkId 到 .hot-update.js 文件名的映射 (来自 manifest)
// __webpack_require__.hmrF 是 .hot-update.json 文件名 (manifest)
var promises = [];
for (var chunkId in hotAvailableFilesMap) { // hotAvailableFilesMap 由 hmrDownloadManifest 填充
if (Object.prototype.hasOwnProperty.call(hotAvailableFilesMap, chunkId)) {
// __webpack_require__.hmrD 是下载更新块的函数 (hmrDownloadUpdateHandlers)
promises.push(__webpack_require__.hmrD(chunkId));
}
}
return Promise.all(promises).then(function() {
if (applyOnUpdate) {
return hotApply(); // 应用下载好的更新
}
});
});
}
// 这个函数由 .hot-update.json (manifest) 的加载回调触发
// (由 HotUpdateChunkTemplateRuntimeModule 生成的 __webpack_require__.hmrM 函数)
// self["webpackHotUpdate<runtimeName>Manifest"] = function(moreModules, fullHash) { ... }
// 或者更常见的是,manifest 是一个纯 JSON 文件,通过 XHR 加载
// __webpack_require__.hmrM = function() { // hmrDownloadManifest
// return new Promise(function(resolve, reject) {
// if (typeof XMLHttpRequest === "undefined") return reject(new Error("No browser support"));
// try {
// var request = new XMLHttpRequest();
// var requestPath = __webpack_require__.p + __webpack_require__.hmrF(); // publicPath + manifestFileName
// request.open("GET", requestPath, true);
// request.timeout = hotRequestTimeout;
// request.send(null);
// } catch (err) { return reject(err); }
// request.onreadystatechange = function() {
// if (request.readyState !== 4) return;
// if (request.status === 0) { /* aborted */ }
// else if (request.status === 404) { resolve(); /* no update found */ }
// else if (request.status !== 200 && request.status !== 304) { /* error */ }
// else {
// try {
// var update = JSON.parse(request.responseText);
// } catch (e) { reject(e); return; }
// hotAvailableFilesMap = update.c; // c: changed chunk IDs
// hotUpdateNewHash = update.h; // h: new hash
// resolve();
// }
// };
// });
// };
// 这个函数由 .hot-update.js 文件调用
// (由 HotModuleReplacementPlugin 的 generateHotUpdateChunk 生成)
self["webpackHotUpdate<runtimeName>"] = function(chunkId, moreModules, fullHash) {
// moreModules: { moduleId: function(...) }
// fullHash: 可选,用于校验
// 校验 fullHash (如果提供了)
// if (fullHash && fullHash !== hotCurrentHash) { /* 错误处理 */ }
for (var moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
// 1. 获取旧模块
var oldModule = __webpack_require__.c[moduleId]; // __webpack_require__.c 是模块缓存
// 2. 创建 hot module data 对象,用于在 dispose 和 accept 间传递数据
var hotData = {};
if (oldModule && oldModule.hot && oldModule.hot._disposeData) {
hotData.disposeData = oldModule.hot._disposeData;
}
// 3. 执行旧模块的 dispose 处理程序 (如果存在)
if (oldModule && oldModule.hot && oldModule.hot._disposeHandlers) {
oldModule.hot._disposeHandlers.forEach(function(cb) {
cb(hotData); // 将 hotData 传递给 dispose 回调
});
}
// 存储 dispose 后的数据,供新模块的 accept 回调使用
// (实际上,这个数据传递是通过 HMR Runtime 内部机制管理的)
hotCurrentModuleData[moduleId] = hotData;
// 4. 将新模块的代码函数添加到 __webpack_require__.m (模块定义)
// 或者直接替换 __webpack_require__.c[moduleId].exports (如果模块已加载)
// 更准确地说,是更新模块工厂函数
__webpack_require__.m[moduleId] = moreModules[moduleId];
// 5. 标记模块为需要更新 (加入到 hotUpdate 等待队列)
if (!hotUpdate) hotUpdate = {};
hotUpdate[moduleId] = moreModules[moduleId];
}
}
// 如果所有属于这个 chunkId 的 .hot-update.js 都已下载完毕
if (--hotWaitingFiles === 0 && hotChunksLoading === 0) {
// 如果 hotApplyOnUpdate 为 true,则尝试应用更新
if (hotApplyOnUpdate) {
hotApply();
}
}
};
// 应用更新的核心逻辑
function hotApply() {
// ... (状态检查和设置) ...
hotSetStatus("apply");
var options = {}; // HMR apply options
var updatedModules = []; // 记录实际被更新的模块 ID
function getAffectedStuff(updateModuleId) {
// ... 递归查找所有受影响的模块 (父模块)
// ... 检查 module.hot.accept 和 module.hot.decline
// ... 返回一个包含所有需要处理的模块 ID 的列表
var outdatedModules = [updateModuleId];
var outdatedDependencies = {};
var queue = outdatedModules.slice().map(function(id) {
return {
moduleId: id,
parentId: null
};
});
while (queue.length > 0) {
var item = queue.shift();
var moduleId = item.moduleId;
var parentId = item.parentId;
var module = __webpack_require__.c[moduleId]; // 模块缓存
if (!module || module.hot._selfAccepted) { // 如果模块不存在或已自我接受
continue;
}
if (module.hot._selfDeclined) { // 如果模块自我拒绝
return { type: "self-declined", moduleId: moduleId };
}
if (module.hot._declinedDependencies) { // 如果模块拒绝了某个依赖的更新
// ... 检查 item.dependency 是否在 _declinedDependencies 中
}
// 查找父模块
if (!__webpack_require__.parents || !__webpack_require__.parents[moduleId]) continue;
__webpack_require__.parents[moduleId].forEach(function(parentModuleId) {
var parentModule = __webpack_require__.c[parentModuleId];
if (!parentModule) return;
// 检查父模块是否接受此子模块的更新
var acceptedDependencies = parentModule.hot._acceptedDependencies;
if (acceptedDependencies && acceptedDependencies[moduleId]) {
// ... 记录接受关系,添加到 outdatedDependencies
if (!outdatedDependencies[parentModuleId]) outdatedDependencies[parentModuleId] = [];
addAll(outdatedDependencies[parentModuleId], [moduleId]);
// 继续向上查找
} else {
// 如果父模块不接受,则父模块也变为 outdated
if (outdatedModules.indexOf(parentModuleId) < 0) {
queue.push({ moduleId: parentModuleId, parentId: moduleId });
outdatedModules.push(parentModuleId);
}
}
});
}
return {
type: "accepted",
moduleId: updateModuleId, // 触发更新的原始模块
outdatedModules: outdatedModules, // 所有需要重新加载的模块
outdatedDependencies: outdatedDependencies // { parentId: [acceptedChildId] }
};
}
// 遍历所有下载的更新 (hotUpdate 变量中存储的 { moduleId: newCodeFn })
for (var moduleId in hotUpdate) {
if (Object.prototype.hasOwnProperty.call(hotUpdate, moduleId)) {
var newModuleFactory = hotUpdate[moduleId]; // 新的模块工厂函数
var result;
if (newModuleFactory) { // 如果是函数,说明是可执行的模块更新
result = getAffectedStuff(moduleId);
} else { // 否则可能是标记为删除等
result = { type: "disposed", moduleId: moduleId };
}
// 根据 result.type 处理
if (result.type === "self-declined" || result.type === "declined-<y_bin_411>") {
// HMR 失败,需要完整刷新
hotSetStatus("abort");
return Promise.reject(new Error("Aborted because of declined module: " + result.moduleId));
}
if (result.type === "accepted") {
hotApplyHandlers.push({
moduleId: result.moduleId,
outdatedModules: result.outdatedModules,
outdatedDependencies: result.outdatedDependencies,
iterator: function(selectedUpdate) { // selectedUpdate 是 result
var outdatedModules = selectedUpdate.outdatedModules;
var outdatedDependencies = selectedUpdate.outdatedDependencies;
// 1. 调用 dispose 处理程序 (从最深的子模块开始)
for (var i = 0; i < outdatedModules.length; i++) {
var oldModuleId = outdatedModules[i];
var oldModule = __webpack_require__.c[oldModuleId];
if (oldModule && oldModule.hot && oldModule.hot._disposeHandlers) {
oldModule.hot._disposeHandlers.forEach(function(cb) {
cb(hotCurrentModuleData[oldModuleId] || {}); // 传递之前存储的 disposeData
});
oldModule.hot._disposeData = true; // 标记已处理
}
}
// 2. 从缓存中移除旧模块 (或标记为失效)
// 并用新的工厂函数更新 __webpack_require__.m
for (i = 0; i < outdatedModules.length; i++) {
var mid = outdatedModules[i];
delete __webpack_require__.c[mid]; // 从缓存中移除
__webpack_require__.m[mid] = hotUpdate[mid] || newModuleFactory; // 更新模块定义
updatedModules.push(mid);
}
// 3. 执行 accept 处理程序 (从父模块到子模块,或根据依赖关系)
// 重新执行模块代码 (__webpack_require__(moduleId))
for (var currentModuleId in outdatedDependencies) {
if (Object.prototype.hasOwnProperty.call(outdatedDependencies, currentModuleId)) {
var parentModule = __webpack_require__.c[currentModuleId];
var acceptedChildren = outdatedDependencies[currentModuleId];
if (parentModule && parentModule.hot && parentModule.hot._acceptedDependenciesHandlers) {
parentModule.hot._acceptedDependenciesHandlers.forEach(function(handler) {
// handler.callback(acceptedChildren)
// handler.dependencies 是接受的依赖 ID 列表
// 检查 acceptedChildren 是否与 handler.dependencies 匹配
var cb = handler.callback;
var valid = true;
var renewedDependencies = [];
for(var k=0; k < acceptedChildren.length; k++) {
var depId = acceptedChildren[k];
if (handler.dependencies.indexOf(depId) === -1) {
valid = false; // 依赖不匹配
break;
}
renewedDependencies.push(depId);
}
if (valid) {
cb(renewedDependencies); // 执行 accept 回调
}
});
}
}
}
// 4. 对于自接受的模块 (self-accepted)
for (i = 0; i < outdatedModules.length; i++) {
var selfAcceptedModuleId = outdatedModules[i];
var module = __webpack_require__.c[selfAcceptedModuleId]; // 此时应该是新模块了
if (module && module.hot && module.hot._selfAccepted && module.hot._selfAcceptedHandlers) {
module.hot._selfAcceptedHandlers.forEach(function(cb) {
cb(); // 执行自身 accept 回调
});
} else {
// 如果没有 accept handler,但模块被更新了,需要重新执行它
// 这通常发生在入口点或没有显式 accept 的模块
if (__webpack_require__.c[selfAcceptedModuleId]) { // 确保模块已加载
// 重新 require 模块以执行新代码
// __webpack_require__(selfAcceptedModuleId);
// 实际的重新执行逻辑更为复杂,需要确保依赖正确注入
// 通常是标记为需要重新评估,然后在下一轮事件循环中处理
hotDeferred.push(function() {
__webpack_require__(selfAcceptedModuleId);
});
}
}
}
}
});
}
}
}
// 清理 hotUpdate
hotUpdate = undefined;
// 执行所有收集到的 apply 操作
var errors = [];
for(var i = 0; i < hotApplyHandlers.length; i++) {
try {
hotApplyHandlers[i].iterator(hotApplyHandlers[i]);
} catch (err) {
errors.push(err);
// ... 错误处理,可能导致 HMR 失败
}
}
hotApplyHandlers = [];
// 执行延迟的模块(重新评估)
for (i = 0; i < hotDeferred.length; i++) {
hotDeferred[i]();
}
hotDeferred = [];
if (errors.length > 0) {
hotSetStatus("fail");
// 抛出错误,可能触发页面刷新
return Promise.reject(errors[0]);
}
hotSetStatus("idle");
return Promise.resolve(updatedModules); // 返回成功更新的模块 ID 列表
}
function hotSetStatus(newStatus) {
// console.log("[HMR] Status: " + newStatus);
// module.hot.status() 会返回这个状态
__webpack_require__.hmrS = newStatus;
}
function hotGetStatus() {
return __webpack_require__.hmrS;
}
// 初始化 HMR 状态
hotSetStatus("idle");
解释:
- WebSocket 通信: 客户端 HMR Runtime 通过 WebSocket 接收来自 WDS 的
hash
(新编译的 hash) 和ok
(或built
, 表示编译完成) 消息。 hotCheck()
:- 当收到
ok
消息后,调用hotCheck()
。 - 它首先调用
__webpack_require__.hmrM()
(或类似函数) 去服务器下载.hot-update.json
(manifest) 文件。这个 manifest 包含了最新的hash
(h) 和发生变化的chunk
ID (c)。 - 如果 manifest 下载成功并且有更新,它会遍历
hotAvailableFilesMap
(来自 manifest 的c
字段)。 - 对于每个变化的
chunkId
,它调用__webpack_require__.hmrD()
(或类似函数) 去下载对应的.hot-update.js
文件。 - 这些
.hot-update.js
文件内部会调用全局的webpackHotUpdate<runtimeName>()
函数。
- 当收到
webpackHotUpdate<runtimeName>(chunkId, moreModules)
:- 这个函数由
.hot-update.js
文件执行时调用。 moreModules
是一个对象,键是模块 ID,值是新的模块代码 (包裹在函数中)。- 它会遍历
moreModules
:- 获取旧模块实例。
- 执行旧模块的
dispose
处理器 (如果存在),并将disposeData
存储起来。 - 用新的模块工厂函数更新
__webpack_require__.m
(模块定义)。 - 将这些模块加入到
hotUpdate
对象中,等待应用。
- 当一个 chunk 的所有更新都下载完毕后,如果
hotApplyOnUpdate
为true
,则调用hotApply()
。
- 这个函数由
hotApply()
: 这是 HMR 最核心的部分。getAffectedStuff(moduleId)
: 对于每个更新的模块,这个函数会向上遍历模块依赖树,确定哪些模块受到影响,以及它们是如何接受更新的 (自接受、父模块接受)。它会检查module.hot.accept()
和module.hot.decline()
的设置。如果更新无法被接受 (例如,一直冒泡到入口点都没有accept
,或者某个模块decline
了更新),则 HMR 会失败。- Dispose Handlers: 对所有将被替换的模块,执行它们的
_disposeHandlers
。这些处理器用于清理旧模块可能产生的副作用 (如移除事件监听器、清除定时器)。 - Update Modules: 将
__webpack_require__.m
(模块定义) 更新为新的模块代码。从__webpack_require__.c
(模块缓存) 中删除旧的模块实例。 - Accept Handlers:
- 如果一个父模块接受了其子模块的更新 (通过
module.hot.accept(dependencies, callback)
), 则执行父模块注册的callback
,并将更新的子模块 ID 传递给它。开发者通常在这个回调中执行重新渲染、更新数据等操作。 - 如果一个模块是自接受的 (通过
module.hot.accept(callback)
或module.hot.accept()
),则执行其自身的callback
。
- 如果一个父模块接受了其子模块的更新 (通过
- Re-evaluate: 对于没有显式
accept
处理器但被更新的模块 (例如,因为它们是自接受的,或者它们的依赖更新了但它们没有accept
特定依赖),它们的代码需要被重新执行以应用更改。这通常是通过再次调用__webpack_require__(moduleId)
来完成的,或者将其放入一个延迟队列中执行。 - Error Handling: 如果在
accept
或dispose
过程中发生错误,或者更新无法被接受,HMR 会失败,状态变为fail
或abort
,通常会导致浏览器执行完整刷新。 - Status Updates: HMR Runtime 会维护一个状态 (idle, check, prepare, ready, dispose, apply, abort, fail),可以通过
module.hot.status()
查询。
4. module.hot
API 的实现和使用 (概念性)
当 HMRPlugin 处理模块时,它会向模块注入(或使其可以访问)一个 hot
对象。
// 伪代码 - Webpack 模块内部的 module.hot 对象是如何工作的
// 在一个模块被 Webpack 处理时,类似这样的对象会被创建并关联到模块实例
// (通常在 NormalModule.js 中处理)
function createHotModule(moduleId) {
const hot = {
_acceptedDependencies: {}, // { dependencyId: true }
_declinedDependencies: {}, // { dependencyId: true }
_selfAccepted: false,
_selfDeclined: false,
_disposeHandlers: [],
_acceptedDependenciesHandlers: [], // [{ dependencies: [...], callback: fn }]
_selfAcceptedHandlers: [], // [fn]
_disposeData: null, // 用于存储 dispose 回调产生的数据
// module.hot.accept()
accept: function(dependencies, callback) {
if (typeof dependencies === "function") { // accept(callback) -> self accept
callback = dependencies;
dependencies = undefined;
}
if (!callback) { // accept() or accept(true) -> self accept without handler
this._selfAccepted = true;
return;
}
if (!dependencies) { // accept(callback) -> self accept with handler
this._selfAccepted = true;
this._selfAcceptedHandlers.push(callback);
} else { // accept(['./dep'], callback)
const depIds = Array.isArray(dependencies) ? dependencies : [dependencies];
const resolvedDepIds = depIds.map(dep => {
// Webpack 内部需要将 './dep' 这样的相对路径解析为实际的模块 ID
// 这是一个非常复杂的过程,涉及到模块的 context、依赖图等
// 简化:假设 dep 就是模块 ID 或可以被解析为模块 ID
return resolveModuleId(moduleId, dep);
});
this._acceptedDependenciesHandlers.push({
dependencies: resolvedDepIds,
callback: callback
});
resolvedDepIds.forEach(id => this._acceptedDependencies[id] = true);
}
},
// module.hot.decline()
decline: function(dependencies) {
if (typeof dependencies === "undefined") {
this._selfDeclined = true;
} else {
const depIds = Array.isArray(dependencies) ? dependencies : [dependencies];
depIds.forEach(dep => {
const resolvedDepId = resolveModuleId(moduleId, dep);
this._declinedDependencies[resolvedDepId] = true;
});
}
},
// module.hot.dispose()
dispose: function(callback) {
this._disposeHandlers.push(callback);
},
// HMR Runtime 会在模块被替换前调用这个,并把返回值存起来
// hot.data (在下一个版本的模块中)
// 在 HMR Runtime 中,dispose 回调接收一个 data 对象,可以向其添加属性
// e.g., callback(data) { data.myValue = 123; }
// 这个 data 对象 (hotCurrentModuleData[moduleId]) 会在 accept 回调中可用
// (通常是通过 HMR Runtime 内部机制传递,而不是直接作为参数)
// module.hot.data
// 这个属性由 HMR Runtime 填充,包含了上一个版本模块的 dispose 处理器传递的数据
data: hotCurrentModuleData[moduleId] ? hotCurrentModuleData[moduleId].disposeData : null,
status: function() {
return hotGetStatus(); // 调用 HMR Runtime 的状态函数
},
// ... 其他 API 如 addStatusHandler, removeStatusHandler, check, apply
// check 和 apply 通常是给高级用户或工具使用的,直接调用 HMR Runtime 的相应方法
check: hotCheck,
apply: hotApply,
// 用于 HMR Runtime 内部管理
_acceptedDependenciesHandlers: [],
_selfAcceptedHandlers: [],
};
return hot;
}
// 模拟模块解析 (非常简化)
function resolveModuleId(currentModuleId, dependencyPath) {
// 实际的解析非常复杂,涉及到编译器的解析器、文件系统等
// 这里仅作示意
if (dependencyPath.startsWith('./')) {
return `module_id_for(${dependencyPath}_relative_to_${currentModuleId})`;
}
return `module_id_for(${dependencyPath})`;
}
// --- 开发者如何在模块中使用 ---
// // MyComponent.js
// import otherModule from './otherModule';
//
// function render() { /* ... */ }
//
// if (module.hot) {
// // 1. 处理自身更新
// module.hot.accept(err => {
// if (err) {
// console.error('Cannot apply HMR update for MyComponent.js:', err);
// // 可以选择 window.location.reload();
// return;
// }
// console.log('MyComponent.js reloaded via HMR');
// // 通常在这里重新渲染组件或执行必要更新
// render();
// });
//
// // 2. 处理特定依赖的更新
// module.hot.accept('./otherModule.js', () => {
// console.log('otherModule.js was updated. Re-rendering MyComponent or re-importing.');
// // 可能需要重新导入 otherModule,因为它可能已经被更新
// // const newOtherModule = require('./otherModule'); // 注意 require 缓存
// // 或者,如果 otherModule 的导出是动态的,直接重新渲染可能就够了
// render();
// });
//
// // 3. 清理副作用
// module.hot.dispose(data => {
// console.log('MyComponent.js is about to be replaced.');
// // 清理定时器、事件监听器等
// // clearInterval(myInterval);
// // document.removeEventListener('click', handleClick);
// // 可以将需要传递给新模块的数据放到 data 对象上
// data.someState = { preserved: true };
// });
//
// // 4. 获取上一个模块实例传递的数据
// if (module.hot.data && module.hot.data.someState) {
// console.log('Restoring someState from previous module instance:', module.hot.data.someState);
// }
//
// // 5. 拒绝更新 (会导致 HMR 失败并可能刷新页面)
// // module.hot.decline('./criticalDependency.js');
// // module.hot.decline(); // 拒绝自身更新
// }
//
// export default render;
解释:
module.hot
对象是在模块加载时由 Webpack(通过 HMRPlugin 和 HMR Runtime 的配合)提供的。accept(dependencies, callback)
:- 如果没有
dependencies
或dependencies
为true
,则表示模块接受自身的更新。当模块自身代码变化时,callback
(如果提供) 会被执行。 - 如果提供了
dependencies
(一个模块路径或路径数组),则表示该模块接受这些指定依赖的更新。当这些依赖更新时,callback
会被执行。
- 如果没有
dispose(callback)
: 注册一个回调,在模块即将被替换掉之前执行。callback
接收一个data
对象,开发者可以将需要在新模块中恢复的状态或数据附加到这个data
对象上。module.hot.data
: 在新模块实例中,可以通过module.hot.data
访问到旧模块dispose
回调中设置的data
对象。decline(dependencies)
: 声明不接受自身或指定依赖的更新。如果这些模块发生变化,HMR 会失败,通常导致页面刷新。status()
: 返回当前 HMR 的状态。
总结 HMR 的关键点
- 速度: 只重新编译和传输变化的部分,而不是整个应用。
- 状态保持: 成功的 HMR 可以保留应用的大部分状态,避免了因刷新导致的状态丢失。
- 开发者体验: 即时反馈,无需手动刷新,提高了开发效率。
- 需要代码配合: 模块需要通过
module.hot.accept()
来“选择加入”HMR。对于 React、Vue 等框架,通常有专门的 loader 或插件 (如react-hot-loader
,vue-loader
) 来自动处理组件的 HMR。 - 并非万能: 对于某些类型的代码更改 (例如,修改应用入口文件、全局单例的初始化逻辑等),HMR 可能难以处理,最终还是会回退到完整刷新。
这个详细的解释和概念性的源码分析应该能让你对 Webpack HMR 的流程和原理有一个深入的理解。由于实际源码的复杂性和版本迭代,这里的代码片段是经过简化和抽象的,旨在阐明核心概念。要真正深入源码,建议直接查阅 Webpack、webpack-dev-server 和 webpack-hot-middleware 的 GitHub 仓库。