参考书籍:《Webpack实战:入门、进阶与调优》
起步
项目结构
main.js
function Component(){
var div=document.createElement('div')
div.innerHTML="h1234r"
return div
}
document.body.appendChild(Component())
应用
Webpack在live reload(修改代码自动刷新页面)的基础上又进了一步,可以让代码在网页不刷新的前提下得到最新的改动,甚至不需要重新发起请求就能看到更新后的效果。这就是模块热替换功能(Hot ModuleReplacement,HMR)
HMR对于大型应用尤其适用。试想一个复杂的系统每改动一个地方都要经历资源重构建、网络请求、浏览器渲染等过程,怎么也要几秒甚至几十秒的时间才能完成;况且我们调试的页面可能位于很深的层级,每次还要通过一些人为操作才能验证结果,其效率是非常低下的。而HMR则可以在保留页面当前状态的前提下呈现出最新的改动,可以节省开发者大量的时间成本
HMR是需要手动开启的,并且有一些必要条件。项目必须基于webpack-dev-server或者webpack-dev-middle进行开发的。以webpack-dev-server开启HMR为例:
const webpack=require('webpack')//1. 加载
const HtmlWebpackPlugin=require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output:{
filename:'bundle.js',
},
plugins:[
new HtmlWebpackPlugin({title:'output management'}),
new webpack.HotModuleReplacementPlugin() //2. 配置
],
devtool: 'inline-source-map',
devServer:{
contentBase:'./dist'
}
};
上面配置产生的结果是Webpack会为每个模块绑定一个module.hot对象,这个对象包含了HMR的API。借助这些API我们不仅可以实现对特定模块开启或关闭HMR,也可以添加热替换之外的逻辑。
调用HMR API有两种方式,一种是手动地添加这部分代码;另一种是借助一些现成的工具,比如react-hot-loader、vue-loader等。如果应用的逻辑比较简单,我们可以直接手动添加代码来开启HMR:
main.js
//在文件下加上:
if(module.hot){
module.hot.accept()
}
由于main.js是应用的入口,那么就可以把调用HMR API的代码放在该入口中,这样HMR对于main.js和其依赖的所有模块都会生效
webpack.config.js
//...
module.exports = {
//...
devServer:{
contentBase:'./dist',
hot:true //开启
}
};
当发现有模块发生变动时,HMR会使应用在当前浏览器环境下重新执行一遍main.js(包括其依赖)的内容,但是页面本身不会刷新。举例来说:
function Component(){
var div=document.createElement('div')
//如果我更改了innerHTML的内容,main.js就会重新执行一次
//那么页面上就会再生成一个div
div.innerHTML="hm3r"
return div
}
document.body.appendChild(Component())
if(module.hot){
module.hot.accept()
}

大多数时候,还是建议应用的开发者使用第三方提供的HMR解决方案,因为HMR触发过程中可能会有很多预想不到的问题,导致模块更新后应用的表现和正常加载的表现不一致。为了解决这类问题,Webpack社区中已经有许多相应的工具提供了解决方案。比如react组件的热更新由react-hot-loader来处理,我们直接拿来用就行
原理
在本地开发环境下,浏览器是客户端,webpack-dev-server(WDS)相当于是我们的服务端。
HMR的核心就是客户端从服务端拉取更新后的资源(准确地说,HMR拉取的不是整个资源文件,而是chunk diff,即chunk需要更新的部分。
第1步就是浏览器什么时候去拉取这些更新,这需要WDS对本地源文件进行监听。
实际上WDS与浏览器之间维护了一个websocket,当本地资源发生变化时WDS会向浏览器推送更新事件,并带上这次构建的hash,让客户端与上一次资源进行比对。
通过hash的比对可以防止冗余更新的出现。因为很多时候源文件的更改并不一定代表构建结果的更改
这同时也解释了为什么当我们开启多个本地页面时,代码一改所有页面都会更新。当然webscoket并不是只有开启了HMR才会有,live reload其实也是依赖这个而实现的。
有了恰当的拉取资源的时机,下一步就是要知道拉取什么。
现在客户端已经知道新的构建结果和当前的有了差别,就会向WDS发起一个请求来获取更改文件的列表,即哪些模块有了改动。通常这个请求的名字为[hash].hot-update.json
接口请求地址

返回值

该返回结果告诉客户端,需要更新的chunk为main,版本为(构建hash)a290a57cc678baf02a02。
这样客户端就可以再借助这些信息继续向WDS获取该chunk的增量更新
现在客户端已经获取到了chunk的更新,到这里又遇到了一个非常重要的问题,即客户端获取到这些增量更新之后如何处理?哪些状态需要保留,哪些又需要更新?
这个就不属于Webpack的工作了,但是它提供了相关的API(如module.hot.accept),开发者可以使用这些API针对自身场景进行处理。