认识
一、认识
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;
});
}
三、工作
-
使用
Express
启动本地服务, 构造静态服务器: 通过webpack.config.js
创建webpack
实例, 将compiler.outputFileSystem
修改为memory-fs
的实例: 创建compiler
对象, 并修改compiler.outputFileSystem = memory-fs 实例
, 将Webpack
的产物打包到内存中, 因此读写都比较快。memory-fs
是NodeJS
原生fs
模块内存版(in-memory
)的完整功能实现; 使用Express
启动本地服务, 处理响应来自客户端的资源请求: 从内存中读取index.html
、bundle.js
、chunkId.hash.hot-update.json
、chunkId.hash.hot-update.js
文件内容返回给客户端。 -
浏览器加载页面后, 服务端和客户端建立
WebSocket
长连接: 启动一个WebSocket
服务器,Webpack
重新编译时, 向客户端推送hash
和ok
两个事件 -
Webpack
监听文件的变化, 每当文件发生变更时,Webpack
将会监控到此时文件变更事件,并找到其对应的module
, 增量构建发生变更的模块: 每次编译都会生成hash
值。通过HotModuleReplacementPlugin
插件, 对比编译后的chunk
和module
,将更新后的module
和runtime
形成新的HotUpdateChunk
, 生成记录着已改动模块的chunkId.hash.hot-update.json
文件和记录着已改动模块代码的chunkId.hash.hot-update.js
文件 -
编译完成后, 触发
compiler.hooks.done
钩子, 在回调函数中通过Websocket
发送hash
事件和ok
事件, 把变更模块通知到客户端 -
客户端通过
hash
事件接受本次编译之后的hash
值, 如果前后hash
值不一致, 则ajax
请求manifest
资源文件chunkId.hash.hot-update.json
确认增量变更module
和chunk
, 然后再通过JSONP
请求chunkId.hash.hot-update.js
获取变更module
和chunk
的代码: 之所以使用JSONP
获取变更的代码而不用ajax
或者websocket
是因为JSONP
获取的代码可以直接执行。 -
获取变更代码之后, 使用新的模块对旧模块进行热替换,并删除其缓存: 在
webpack
的运行时中, 通过__webpack__modules__
来维护所有的模块, 通过chunk
的方式加载最新的modules
,找到__webpack__modules__
中对应的模块逐一替换,并删除其上下缓存。 -
Webpack
运行时触发变更模块的module.hot.accept
回调, 执行代码变更逻辑: 一旦某个模块没有注册对应的module.hot.accept
函数后,HMR
运行时会执行兜底策略, 通常是刷新页面, 确保页面上运行的始终是最新的代码。
四、问题
4.1 HMR 与 Live Reload 有什么区别?
HMR
: 无需刷新在内存环境中即可替换掉过旧模块, 相对于 Live Reload
, 整体刷新页面方案, HMR
的优点在于可以做到模块局部更新, 可以保存应用的状态, 提高开发效率。
Live Reload
: 代码进行更新后, 在浏览器自动刷新以获取最新前端代码
4.2 Webpack HMR 与 Vite HMR 有什么区别?
与webpack
的热更新对比起来,两者都是建立socket
联系,但是两者不同的是,前者是通过bundle.js
的hash
来请求变更的模块,进行热替换。后者是根据自身维护HmrModule
,通过文件类型以及服务端对文件的监听给客户端发送不同的message
,让浏览器做出对应的行为操作。
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
接口,才能处理好不同代码块的变更事件