基本概念
传统的页面开发在代码有所修改后,还需开发者手动刷新浏览器才能看到效果,为了提高开发效率, 一些Web开发框架和工具提供了更便捷的开发方式-只要检测到代码改动就会自动重新构建,然后触发网页刷新. 这种一般称为live reload。 webpack 在 live reload 的基础上又进了一步,可以让代码在网页不刷新的前提下得到最新的改动,甚至可以让我们不需要重新发起请求就能看到更新后的效果。这就是热模块更换(Hot Moudle Replacement,后续简称:HRM)功能。
开启 HRM
const webpack = require('webpack');
devServer: {
contentBase: './dist',
open: true,
port: 3000,
hot: true,
hotOnly: true //配置可选
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html',
}),
new webpack.HotModuleReplacementPlugin() //配置热模块更新
]
上述代码通过 HotModuleReplacementPlugin 启用了 Hot Module Replacement, 则它的接口将被暴露在 module.hot 以及 import.meta.webpackHot 属性下。请注意,只有 import.meta.webpackHot 可以在 strict ESM 中使用。
通常,用户先要检查这个接口是否可访问, 再使用它。你可以这样使用 accept 操作一个更新的模块:
if (module.hot) {
module.hot.accept('./counter.js', function() {
// 对更新过的 counter 模块做些事情...
});
}
// or
if (import.meta.webpackHot) {
import.meta.webpackHot.accept('./counter.js', function () {
// 对更新过的 counter 模块做些事情...
});
}
模块API
module.hot 以及 import.meta.webpackHot下面,暴露了一些API, 借助这些API可以实现对特定模块开启或者关闭HRM,也可以添加热替换之外的逻辑等.
在日常开发中,大部分框架工具已经内置帮我们实现了调用模块API的方法,所以接触不到模块API
日常开发中:
- React Hot Loader: 实时调整 react 组件。
- Vue Loader: 此 loader 支持 vue 组件的 HMR,提供开箱即用体验。
- Elm Hot webpack Loader: 支持 Elm 编程语言的 HMR。
- Angular HMR: 没有必要使用 loader!直接修改 NgModule 主文件就够了,它可以完全控制 HMR API。
- Svelte Loader: 此 loader 开箱即用地支持 Svelte 组件的热更新。
如果需要自己手动去修改调模块API的话,可以了解一下下面几个常用的模块API:
accept
接受(accept)给定 依赖模块(dependencies) 的更新,并触发一个 回调函数 来响应更新,除此之外,你可以附加一个可选的 error 处理程序:
module.hot.accept(
dependencies, // 可以是一个字符串或字符串数组
callback // 用于在模块更新后触发的函数
errorHandler // (err, {moduleId, dependencyId}) => {}
);
// or
import.meta.webpackHot.accept(
dependencies, // 可以是一个字符串或字符串数组
callback, // 用于在模块更新后触发的函数
errorHandler // (err, {moduleId, dependencyId}) => {}
);
decline
拒绝给定依赖模块的更新,使用 'decline' 方法强制更新失败。
module.hot.decline(
dependencies // 可以是一个字符串或字符串数组
);
// or
import.meta.webpackHot.decline(
dependencies // 可以是一个字符串或字符串数组
);
dispose
添加一个处理函数,在当前模块代码被替换时执行。此函数应该用于移除你声明或创建的任何持久资源。如果要将状态传入到更新过的模块,请添加给定 data 参数。更新后,此对象在更新之后可通过 module.hot.data 调用。
module.hot.dispose(data => {
// 清理并将 data 传递到更新后的模块...
});
// or
import.meta.webpackHot.dispose((data) => {
// 清理并将 data 传递到更新后的模块...
});
invalidate
调用此方法将使当前模块无效,而当前模块将在应用 HMR 更新时进行部署并重新创建。这个模块的更新像冒泡一样,拒绝自身更新。
import { x, y } from './dep';
import { processX, processY } from 'anotherDep';
const oldY = y;
processX(x);
export default processY(y);
module.hot.accept('./dep', () => {
if (y !== oldY) {
// 无法处理,冒泡给父级
module.hot.invalidate();
return;
}
// 可以处理
processX(x);
});
removeDisposeHandler
删除由 dispose 或 addDisposeHandler 添加的回调函数。
module.hot.removeDisposeHandler(callback);
// or
import.meta.webpackHot.removeDisposeHandler(callback);
读者想了解更多详细的API webpack官网查看
HRM原理
原理大概是这样: 在本地开发环境下,浏览器是客户端,webpack-dev-server(WDS)相当于我们的服务端。HRM的核心就是客户端从服务端拉取更新后的资源(准确地说,HRM拉取的不是整个资源文件,而是chunk diff),即 chunk需要更新的部分。
具体步骤
第1步: 确定浏览器什么时候去拉取这些更新。
这需要WDS对本地资源文件进行监听,实际上WDS与浏览器之间维护了一个websocket,当本地资源发生变化时,WDS会向浏览器推送更新事件,带上这次构建的hash,让客户端与上次资源进行对比,通过对比hash可以防止冗余更新的出现。
当然websckot并不是开启了HRM才会有,live reload 其实也是依赖这个实现的
- 图1- websocket事件列表*
第2步: 有了恰当的拉取资源的时机,下一步就是知道拉取什么。
这部分信息并没有包含在websocket中,因为刚刚我们只是想知道这次构建的结果是不是和上次一样. 现在客户端已经知道新的构建结果和当前有所差别,那么它会想WDS发起一个请求来获取更改文件的列表, 即哪些模块有了改动。通常这个请求的名字为hash.hot-update.json。
-
图2- 请求chunk地址*
- 图3- WDS向浏览器的返回值*
浏览器借助返回的需要更新chunk的信息向WDS获取具体的新增更新数据
-
图4- URL中包含了需要更新的chunk信息*
-
图5- 返回具体的新增更新数据*
第3步: 获取后的处理
现在客户端已经获取到了chunks的更新,又遇到了一个非常重要的问题,即客户端获取这些增量更新之后要如何处理呢?哪些状态需要保留,哪些需要更新? 这些就不属于webpack的工作了,但是它提供了相关的API(module.hot.accept等),可以供开发者使用这些API针对自身场景进行处理。像react-hot-loader和vue-loader也都是借助这些API来实现的HRM。