# 配置 HMR 热更新
# 什么是 HMR
模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一, 它允许在运行时更新各种模块,而无需进行完全刷新。
一般如果我们使用了 webpack-dev-server,当我们修改了项目中的文件的时候,一般会重新刷新一下页面,这会导致我们刚刚在页面中操作的东西都被还原。
# 举两个🌰
# less 中:
首先我们在修改 index.js 文件,下面的 js 代码的意思就是页面上插入一个按钮,点这个按钮的时候,生成一个 <div>item</div> 元素;
import './index.less';
var btn = document.createElement('button');
btn.innerHTML = '新增';
document.body.appendChild(btn);
btn.onclick = function() {
var div = document.createElement('div');
div.innerHTML = 'item';
document.body.appendChild(div);
}
接着修改 index.less 文件:用于给偶数的 item 加一个背景色。
div:nth-of-type(odd) {
background: red;
}
然后我们运行 npm start,点击 item 可以在页面中看到:

我们在修改一下 index.less 文件,
div:nth-of-type(odd) {
background: yellow;
}
保存后我们会发现,页码被刷新了,重置了之前的红色条纹。当再点击的时候,才会出现 黄色条纹:


# js 中
我们修改一下 index.js,并在 src 下新建 number.js 和 counter.js,当作我们项目的两个模块。
index.js如下:
// import './index.less';
// var btn = document.createElement('button');
// btn.innerHTML = '新增';
// document.body.appendChild(btn);
// btn.onclick = function() {
// var div = document.createElement('div');
// div.innerHTML = 'item';
// document.body.appendChild(div);
// }
import counter from './counter';
import number from './number';
counter();
number();
if(module.hot) {
module.hot.accept('./number', () => {
document.body.removeChild(document.getElementById('number'));
number();
})
}
number.js:新建一个div,并给这个div赋值1000
function number() {
var div = document.createElement('div');
div.setAttribute('id', 'number');
div.innerHTML = 3000;
document.body.appendChild(div);
}
export default number;
counter.js:新建一个div,并给这个div赋值1,并给这个div添加一个点击事件,每当点击的时候,自动加一。
function counter() {
var div = document.createElement('div');
div.setAttribute('id', 'counter');
div.innerHTML = 1;
div.onclick = function() {
div.innerHTML = parseInt(div.innerHTML, 10) + 1
}
document.body.appendChild(div);
}
export default counter;
我们重新运行 npm start,我们可以看到如下图:

接着我们点击 counter.js 导出的数字,让其变为 16,接着我们将 number.js 中的 1000 改为 3000,

修改之后:

我们会发现上面我们辛苦点的数字又被还原到了 1。
要解决上面两个问题,我们就需要使用 HMR 了。
# 配置
我们修改 webpack.congig.js 配置文件:
const webpack = require('webpack');
...
module.exports = {
...
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true,
hotOnly: true
},
...
plugins: [
...
new webpack.HotModuleReplacementPlugin()
],
...
}
修改完后,我们重启一下服务:npm start
# 再看 less:
我们先点几下新增,如下图所示:

接着我们将 less 中 yellow 改为 #4caf50,保存后回到页面:

之前我们新增的 item 还在,而且颜色变成了我们修改后的样子。
less 热更新成功
# 再看 js:
我们先点几下 counter.js 里面的数字,如下图所示:

接着我们将 number.js 中的数字 1000 改为 6000,保存后回到页面:

之前我们新增的 16 还在,但是数字没有变成 6000:
这是因为我们还需要再 index.js 代码中加上一行代码:
if(module.hot) {
module.hot.accept('./number', () => {
document.body.removeChild(document.getElementById('number'));
number();
})
}
上面的的代码意思就是 如果我们开启了热更新,并且我们发现 number.js 有变动的话,我们就重新的把原来的 number.js 创建的 <div> 删除,并重新运行一下 number.js
重新起一下服务,在按照上面的步骤操作一下,我们发现新增的 16 还在,数字也改成了 6000:

那么为什么我们在打包 less 的时候就不需要写着一行代码呢,其实是因为 css-loader 默认已经帮我们做了这一件事情了,其中我们经常使用 React、vue 框架他们的底层已经帮我们做好了这些事情,所以我们在代码上面基本上没有看到过类似上面的代码。
至此, js 热更新成功。
# 实现原理
来看一张图,如下:

先来讲几个概念:
File System
代表我们的文件系统,里面有我们的所有代码文件
Webpack Compile
Webpack 的编译器,将 JS 编译成 Bundle
HMR Server
将热更新的文件输出给 HMR Rumtime
Bundle server
提供文件在浏览器器的访问
HMR Rumtime
客户端 HMR 的中枢,用来更新文件的变化,与 HMR server 通过 websocket 保持长链接,由此传输热更新的文件
bundle.js
代表构建出来的文件
# 大致流程
分为两个流程,一个是文件系统的文件通过 webpack 的编译器进行编译,接着被放到 Bundle Server 服务器上,也就是 1 -> 2 -> A -> B 的流程;
第二个流程是,当文件系统发生改变的时候,Webpack 会重新编译,将更新后的代码发送给了 HMR Server,接着便通知给了 HMR Runtime,一般来说热更新的文件或者说是 module 是以 json 的形式传输给 浏览器的 HMR Runtime 的,最终 HMR Runtime 就会更新我们前端的代码。
要注意的几个点:
webpack-dev-server是将打包的代码放到内存之中,不是在output指定的目录之下,这样能使webpack速度更快。webpack-dev-server底层是基于webpack-dev-middleware这个库的,他能调用webpack相关的Api对代码变化进行监控,并且告诉webpack,将代码打包到内存中。Websocket不会将更新好的代码直接发给服务器端,而是发一个更新模块的哈希值,真正处理这个hash的还是webpack。

- 浏览器端
HMR.runtime会根据最新的hash值,向服务器端拿到所有要更新的模块的hash值,接着再通过一个jsonp请求来获取这些hash对应的最新模块代码。


- 浏览器端拿到最新的更新代码后,如我们在配置文件中配置的一样,是根据
HotModuleReplacementPlugin对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。 - 当模块的热替换过程中,如果替换模块失败,就会会推倒
live reload操作,也就是进行浏览器刷新来获取最新打包代码。
更加详细的解读大家可以参考 Webpack HMR 原理解析,写的巨详细。大家有兴趣也可以看看源码,不必太深入,有一个大致了解即可。
之后有时间笔者也会专门针对这个原理写一篇文章。
# 相关链接
- HMR 使用
- HMR 相关API
- Webpack HMR 原理解析
- Webpack 热更新实现原理分析
- 使用服务器发送事件
- webpack-dev-server 仓库
- webpack 仓库
- webpack 热更新相关代码
# 示例代码
示例代码可以看这里: