一文搞定Webpack HMR

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 处理器用于集成新模块。

我们将关注以下几个主要部分:

  1. Webpack Dev Server (WDS) / Webpack Hot Middleware (WDM) 的文件监听与消息发送
  2. Webpack 编译时 HMR 插件的注入 (主要是 HotModuleReplacementPlugin)
  3. 浏览器端 HMR Runtime 的核心逻辑
  4. 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-loadervue-loader) 来自动处理组件的 HMR。
  • 并非万能: 对于某些类型的代码更改 (例如,修改应用入口文件、全局单例的初始化逻辑等),HMR 可能难以处理,最终还是会回退到完整刷新。

这个详细的解释和概念性的源码分析应该能让你对 Webpack HMR 的流程和原理有一个深入的理解。由于实际源码的复杂性和版本迭代,这里的代码片段是经过简化和抽象的,旨在阐明核心概念。要真正深入源码,建议直接查阅 Webpack、webpack-dev-server 和 webpack-hot-middleware 的 GitHub 仓库。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值