认识
一、认识
HMR
的全称叫做 Hot Module Replacement
,即模块热替换或者模块热更新。它可以在开发阶段实现局部实时更新、快速重新加载模块和状态保存, 不用刷新整个页面。相比于传统的 Live Reload
所解决的问题:模块局部更新和状态保存。
Webpack
接受更新的策略: 接受自身更新、接受依赖模块的更新和接受多个子模块的更新
二、使用
Webpack
生态下,只需要经过简单的配置,即可启动 HMR
功能,大致分两步:
-
设置
devServer.hot
属性为true
// webpack.config.js
module.exports = {
// ...
devServer: {
// 必须设置 devServer.hot = true,启动 HMR 功能
hot: true
}
}; -
之后,还需要在代码调用
module.hot.accept
接口,声明如何将模块安全地替换为最新代码,如import component from "./component";
let demoComponent = component();
document.body.appendChild(demoComponent);
// HMR interface
if (module.hot) {
// Capture hot update
module.hot.accept("./component", () => {
const nextComponent = component();
// Replace old content with the hot loaded one
document.body.replaceChild(nextComponent, demoComponent);
demoComponent = nextComponent;
});
}
三、工作
Webpack
的 Hot Module Replacement
(HMR
)机制主要分为服务端和客户端两个部分协同工作,实现模块的无刷新热替换,具体流程如下:
一、服务端构建与内存存储: 1. 构造 Webpack
实例与内存文件系统, 首先我们通过 webpack.config.js
创建一个 compiler
对象,并将其 outputFileSystem
属性设置为 memory-fs
的实例,这样 Webpack
打包出来的产物(如 bundle.js
、index.html
、以及后续的热更新文件)都存储在内存中,I/O
操作非常高效。2. 启动 Express
静态服务器, 使用 Express
启动本地服务,通过内存文件系统读取文件,将 index.html
、bundle.js
等返回给客户端。
二、建立服务端与客户端通信通道: WebSocket
长连接, 在页面加载后,客户端与服务器建立一个 WebSocket
长连接。每当 Webpack
重新编译时,服务端会通过 WebSocket
向客户端推送编译后的 hash
和 ok
事件,用以告知客户端当前的构建状态。
三、文件监控与增量构建: 监听文件变更, Webpack
会持续监控源文件变化,一旦发生变更,就会进行增量编译。通过 HotModuleReplacementPlugin
插件,Webpack
会对比旧的 chunk
与新的 chunk
,找出发生变化的模块,生成两个文件: 1. 一个 JSON
格式的 chunkId.hash.hot-update.json
,记录变更模块的依赖关系; 2. 一个 JS
文件 chunkId.hash.hot-update.js
,包含变更模块的代码。
四、通知客户端更新: 触发 Compiler
钩子, 编译完成后, Webpack
的 compiler.hooks.done
钩子被触发,在其回调中,通过 WebSocket
向客户端发送 hash
和 ok
事件,将当前编译的 hash
值传给客户端。
五、客户端检测与加载更新: 1. 比对 hash
值, 客户端收到 hash
事件后,会将当前 hash
与上次记录的 hash
进行比对。如果不一致,就说明有模块发生更新; 2. 请求增量更新清单, 客户端通过 AJAX
请求获取 chunkId.hash.hot-update.json
文件,确认哪些模块发生了变化; 3. 动态加载更新模块, 接下来,客户端使用 JSONP
的方式加载 chunkId.hash.hot-update.js
文件,之所以用 JSONP
是因为返回的代码可以直接执行,无需额外处理。
六、客户端检测与加载更新: 1. 运行时模块替换, Webpack
运行时通过内部维护的 __webpack_modules__
对象,找到需要更新的模块,替换成新加载的模块,并清理缓存; 2. 执行 module.hot.accept
, 每个模块如果注册了 module.hot.accept
回调,运行时就会调用这些回调来执行更新后的逻辑。如果模块没有提供热更新处理函数,运行时会触发兜底策略,一般会选择刷新页面,确保用户拿到最新的代码。
四、问题
4.1 HMR 与 Live Reload 有什么区别?
HMR
: 无需刷新在内存环境中即可替换掉过旧模块, 相对于 Live Reload
, 整体刷新页面方案, HMR
的优点在于可以做到模块局部更新, 可以保存应用的状态, 提高开发效率。
Live Reload
: 代码进行更新后, 在浏览器自动刷新以获取最新前端代码
4.2 Webpack HMR 与 Vite HMR 有什么区别?
在 HMR
方面, Vite
的热更新则只会针对改动的模块进行更新,提高了更新速度。当开发者修改了一个模块的代码, Vite
可以在几毫秒内完成热更新, 将更新后的模块发送到浏览器中, 让开发者能够更快地看到代码修改后的效果。Webpack
的热更新需要整个模块链重新打包和替换,对于大型项目可能会有延迟。在热更新过程中, Webpack
会检测到模块的变化,然后重新编译整个模块链,最后将更新后的模块替换到浏览器中。这个过程相对复杂,可能会导致一定的延迟。
4.3 vue-loader 如何实现 HMR ?
在 HMR
模式下,vue-loader
还会为每一个 Vue
文件注入一段处理模块替换的逻辑
"./src/a.vue":
/*!*******************!*\
!*** ./src/a.vue ***!
\*******************/
/***/
((module, __webpack_exports__, __webpack_require__) => {
// 模块代码
// ...
/* hot reload */
if (true) {
var api = __webpack_require__( /*! ../node_modules/vue-hot-reload-api/dist/index.js */ "../node_modules/vue-hot-reload-api/dist/index.js")
api.install(__webpack_require__( /*! vue */ "../node_modules/vue/dist/vue.runtime.esm.js"))
if (api.compatible) {
module.hot.accept()
if (!api.isRecorded('45c6ab58')) {
api.createRecord('45c6ab58', component.options)
} else {
api.reload('45c6ab58', component.options)
}
module.hot.accept( /*! ./a.vue?vue&type=template&id=45c6ab58& */ "./src/a.vue?vue&type=template&id=45c6ab58&", __WEBPACK_OUTDATED_DEPENDENCIES__ => {
/* harmony import */
_a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./a.vue?vue&type=template&id=45c6ab58& */ "./src/a.vue?vue&type=template&id=45c6ab58&");
(function () {
api.rerender('45c6ab58', {
render: _a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__.render,
staticRenderFns: _a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns
})
})(__WEBPACK_OUTDATED_DEPENDENCIES__);
})
}
}
// ...
/***/
})
这段被注入用于处理模块热替换的代码,主要步骤有:
-
首次执行时,调用
api.createRecord
记录组件配置,api
为vue-hot-reload-api
库暴露的接口 -
执行
module.hot.accept()
语句,监听当前模块变更事件,当模块发生变化时调用api.reload
-
执行
module.hot.accept("xxx.vue?vue&type=template&xxxx", fn)
,监听Vue
文件template
代码的变更事件,当template
模块发生变更时调用api.rerender
可以看到,vue-loader
对 HMR
的支持,基本上围绕 vue-hot-reload-api
展开,当代码文件发生变化触发 module.hot.accept
回调时,会根据情况执行 vue-hot-reload-api
暴露的 reload
与 rerender
函数,两者最终都会触发组件实例的 $forceUpdate
函数强制执行重新渲染。
另外,为什么这里需要调用两次 module.hot.accept
?这是因为 vue-loader
在做转译时,会将 SFC
不同板块拆解成多个 module
,例如 template
对应生成 xxx.vue?vue&type=template
; script
对应生成 xxx.vue?vue&type=script
。因此,vue-loader
必须为这些不同的 module
分别调用 accept
接口,才能处理好不同代码块的变更事件