HMR 全称 Hot Module Replacement,中文语境通常翻译为模块热更新,全解它能够在保持页面状态的原原理情况下动态替换资源模块,提供丝滑顺畅的理系列 Web 页面开发体验。 HMR 最初由 Webpack 设计实现,全解至今已几乎成为现代工程化工具必备特性之一。原原理 在 HMR 之前,理系列应用的全解加载、更新是原原理一种页面级别的原子操作,即使只是理系列单个代码文件发生变更都需要刷新整个页面才能最新代码映射到浏览器上,这会丢失之前在页面执行过的全解所有交互与状态,例如: 对于复杂表单场景,原原理这意味着你可能需要重新填充非常多字段信息 弹框消失,理系列你必须重新执行交互动作才会重新弹出 再小的全解改动,例如更新字体大小,改变备注信息都会需要整个页面重新加载执行,影响开发体验。引入 HMR 后,虽然无法覆盖所有场景,但大多数小改动都可以实时热更新到页面上,从而确保连续、顺畅的开发调试体验,对开发效率有较大增益效果。源码库 Webpack 生态下,只需要经过简单的配置即可启动 HMR 功能,大致上分两步: 配置 devServer.hot 属性为 true,如: 模块代码的替换逻辑可能非常复杂,幸运的是我们通常不太需要对此过多关注,因为业界许多 Webpack Loader 已经提供了针对不同资源的 HMR 功能,例如: 因此,站在使用的角度,只需要针对不同资源配置对应支持 HMR 的 Loader 即可,很容易上手。 Webpack HMR 特性的原理并不复杂,核心流程: 接下来我会展开 HMR 的核心源码,详细讲解 Webpack 5 中 Hot Module Replacement 原理的关键部分,内容略微晦涩,不感兴趣的同学可以直接跳到下一章。 执行 npx webpack serve 命令后,WDS 调用 HotModuleReplacementPlugin 插件向应用的主 Chunk 注入一系列 HMR Runtime,包括: 关于 Webpack Runtime,可参考 Webpack 原理系列六:彻底理解 Webpack 运行时。 经过 HotModuleReplacementPlugin 处理后,构建产物中即包含了所有运行 HMR 所需的客户端运行时与接口。这些 HMR 运行时会在浏览器执行一套基于 WebSocket 消息的时序框架,如图: 除注入客户端代码外,HotModuleReplacementPlugin 插件还会借助 Webpack 的 watch 能力,在代码文件发生变化后执行增量构建,生成: 实际效果: 客户端接受到 hash 消息后,首先发出 manifest 请求获取本轮热更新涉及的 chunk,如: 注意,在 Webpack 4 及之前,热更新文件以模块为单位,即所有发生变化的模块都会生成对应的热更新文件; Webpack 5 之后热更新文件以 chunk 为单位,如上例中,main chunk 下任意文件的变化都只会生成 main.[hash].hot-update.js 更新文件。 manifest 请求完成后,客户端 HMR 运行时开始下载发生变化的 chunk 文件,将最新模块代码加载到本地。 经过上述步骤,浏览器加载完最新模块代码后,HMR 运行时会继续触发 module.hot.accept 回调,将最新代码替换到运行环境中。 module.hot.accept 是 HMR 运行时暴露给用户代码的重要接口之一,它在 Webpack HMR 体系中开了一个口子,让用户能够自定义模块热替换的逻辑。module.hot.accept 接口签名如下: 它接受两个参数: 例如,对于如下代码: 示例中,module.hot.accept 函数监听 ./bar.js 模块的变更事件,一旦代码发生变动就触发回调,将 ./bar.js 导出的值应用到页面上,从而实现热更新效果。 module.hot.accept 的作用并不复杂,但使用过程中还是有一些值得注意的点,下面细讲。 module.hot.accept 函数只接受具体路径的 path 参数,也就是说我们无法通过 glob 或类似风格的方式批量注册热更新回调。 一旦某个模块没有注册对应的 module.hot.accept 函数后,HMR 运行时会执行兜底策略,通常是刷新页面,确保页面上运行的始终是最新的代码。 在 Webpack HMR 框架中,module.hot.accept 函数只能捕获当前模块对应子孙模块的更新事件,例如对于下面的模块依赖树: 示例中,更新事件会沿着模块依赖树自底向上逐级传递,从 foo 到 index ,从 bar-1 到 bar 再到 index,但不支持反向或跨子树传递,也就是说: 这一特性与 DOM 事件规范中的冒泡过程极为相似,使用时如果摸不准模块的依赖关系,建议直接在应用的入口文件中编写热更新函数。 除上述调用方式外,module.hot.accept 函数还支持无参数调用风格,作用是捕获当前文件的变更事件,并从模块第一行开始重新运行该模块的代码,例如: 示例模块发生变动之后,会从头开始重复执行 console.log 语句。 回顾整个 HMR 过程,所有的状态流转均由 WebSocket 消息驱动,这部分逻辑由 HMR 运行时控制,开发者几乎无感。 唯一需要开发者关心的是为每一个需要处理热更新的文件注册 module.hot.accept 回调,所幸这部分需求已经被许多成熟的 Loader 处理,作为示例,下一节我们挖掘 vue-loader 源码,学习如何灵活使用 module.hot.accept 函数处理文件更新。 vue-loader 是一个用于处理 Vue Single File Component 的 Webpack 加载器,它能够将如下格式的内容转译为可在浏览器运行的等价代码: 除常规的代码转译外,在 HMR 模式下,vue-loader 还会为每一个 Vue 文件注入一段处理模块替换的逻辑,如:一、原原理什么是理系列 HMR
1.1 HMR 之前
1.2 使用 HMR
二、实现原理
2.1 注入 HMR 客户端运行时
2.2 增量构建
2.3 加载更新
2.4module.hot.accept回调
2.4.1 失败兜底
2.4.2 更新事件冒泡
2.4.3 无参数调用
2.5 小结
三、 vue-loader 如何实现 HMR