
1、自动刷新
webpack.config配置参数devServer中存在inline参数,参数可以设置devServer的两种模式inline和iframe,默认使用inline
devServer: {
contentBase: "./dist",
// inline: false,
hot: true,
},
官方是这样描述inline模式的
Toggle between the dev-server's two different modes. By default the application will be served with inline mode enabled. This means that a script will be inserted in your bundle to take care of live reloading, and build messages will appear in the browser console.
大致意思是说在inline模式下,在bundle.js中会插入代码来处理自动刷新,由于没看源码,根据开关inline模式以后,webpack-dev-server启动时候的控制台输出,和官方文档说明,所以个人猜测,大致实现:
bundle.js插入socket的片段,在浏览器加载后建立socket连接- 利用
webpack -watch来监听文件变化,发送socket信息告知浏览器,浏览器接受信息调用reload来实现刷新页面刷新
通过分析浏览器请求(inline模式会在加载bundle.js后发起一个websocket请求)和bundle.js代码确认了这一点(onSocketMsg-> reloadApp-> applyReload),也就是说使用webpack-dev-server启动就可以开启自动刷新功能了
2. HMR
HMR(Hot Module Replacement),添加、修改模块(修改JS/CSS)后浏览器内容通过非刷新的方式自动更新,提高开发效率,要注意HMR只应在开发环境使用
HMR在配置成功以后,修改CSS/JS不会进行页面刷新(注意保存文件变更的时候,浏览器Tab页是否是自动刷新)
2.1 HMR配置
HMR配置有四个关键点:
- 使用
webpack-dev-server作为服务器启动 webpack.config的devServer中配置hot: truewebpack.config的plugins增加HotModuleReplacementPlugin- 使用
module.hot.accept增加HMR代码
webpack.config
module.exports = {
mode: 'development',
devtool: 'inline-source-map',
entry: {
main: __dirname + '/app/main.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
devServer: {
contentBase: "./dist",//本地服务器所加载的页面所在的目录
// inline: true, //实时刷新
hot: true,
},
plugins: [
new HtmlWebpackPlugin({
template: __dirname + '/app/index.tmpl.html'
}),
new webpack.HotModuleReplacementPlugin()
],
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
};
main.js
import './extra.js'
import './style.css'
if (module.hot) {
module.hot.accept('./extra.js', function() {
console.log('Accepting the updated printMe module!');
})
// 关闭指定子模块的HMR
// module.hot.decline('./extra.js')
}
extra.js
if(module.hot){
// 监听当前模块HMR异常
// module.hot.accept(() => {
// console.log('error handler')
// })
// 关闭当前模块HMR
// module.hot.decline()
// 存储模块数据
// module.hot.dispose(disposeHander)
// if (module.hot.data) {
// console.log(module.hot.data.a)
// }
// 移除模块数据
// module.hot.removeDisposeHandler(disposeHander)
}
配置成功以后修改extra.js或者style.css保存,都会触发HMR,控制台会输出[HMR]开头的日志
PS:
-
css文件我们会配置
style-loader,style-loader中已经增加了module.hot.accept的支持,所以即使不配置module.hot.accept,对于css也可以HMR,但是如果JS没有调用module.hot.accept,HMR执行找不到对应的内容,则会直接刷新页面,可以设置参数hotOnly: true来防止自动刷新 -
webpack plugins未添加
HotModuleReplacementPlugin使用hot: true的时候会报错Uncaught Error: [HMR] Hot Module Replacement is disabled. -
官网配置分为Node版本和Web版本,由于自己也了解不深,所以不探讨Node版本如何使用
webpack的HMR
2.2 使用HMR API
要想使用HMR,需要利用到HMR的API,在添加了HotModuleReplacementPlugin模块后,就可以使用module.hot的API
Module API
用于模块处理的API
2.2.1 accept
module.hot.accept(dependencies,callback):添加需要HMR的模块,以及触发变更后的回调,dependencies支持字符串或者字符串数组
module.hot.accept(errorHandler):被添加的模块内部使用可以捕获HMR异常时候抛出的异常
2.2.2 decline
module.hot.decline(dependencies):和accept操作相反,标记一个不需要HMR管理的模块
module.hot.decline():JS模块中使用,将导致该模块无法被HMR
2.2.3 dispose
module.hot.addDisposeHandler/moduel.hot.dispose(data =>{}):即使使用HMR,每次更新后数据是不能进行保留的,如果想将数据传递给更新后的模块,使用dispose对全局对象data赋值,之后可以通过module.hot.data来获取
module.hot.removeDisposeHandler(callback):移除通过dispose和addDisposeHandler添加的回调,移除后module.hot.data将变为空对象{}(调用dispose之前module.hot.data是undefined)
Management API
用于HMR状态管理的API
2.2.4 status
module.hot.status(): HMR过程是存在变化状态的,通过该函数来获取到HMR的当前状态
2.3 vue-cli3配置HMR
使用vue-cli3构建,通过vue-cli-service serve启动的项目,其本身会添加HMR,在vue.config.js的devServer中配置hot: false可以关闭。
但是项目中发现一个情况,当配置了https: true以后,必须指定host: localhost,HMR功能才可以开启,否则HMR功能将失效(其实是整个自动更新功能都失效了,不会发起socket请求),而在webpack中并没有出现该情况。
据此揣测,vue-cli-service在使用https: true的时候,socket链接必须指定使用hostname才可以进行构建,根据查找bundle.js代码,确认该逻辑(可以在bundle.js查找socket关键字,可以看到hostname相关检测逻辑)。
3、HMR实现
3.1 工作原理
本小节内容都不是人话,可跳过~
一、应用程序(Applicationn):
- 应用程序在获取到
socket服务器发送的更新信息 - 检查HMR的运行状态
- HMR运行环境异步下载更新内容并通知应用程序
- 应用程序告诉HMR运行环境去应用更新
- HMR运行环境应用变更信息
二、编译部分(Compiler):
修改文件以后,编译器触发更新操作,更新两部分内容:manifest和chunks
manifest包含了新的变化的所有模块的代码块,编译器确保module IDs和chunk IDs在本次构建中
三、模块(Module)
只影响HMR中包含了的模块(module.hot.accept()),比如:style-loader,其中实现了HMR接口,所以在样式更新的时候,新样式替换老样式
如果模块没有HMR的处理,则更新操作持续冒泡,也就是模块树中只要单个模块发生了更新,整个都会重新加载
四、运行过程(Runtime)
运行过程会持续使用check和apply来进行module.hot.staus的更新
check: 请求更新清单,请求成功后,比较更新的内容和当前的内容,将所有更新的内容进行存储直到所有内容更下载完成,切换到ready状态准备
apply: 将updated的模块标记为invalid,一直冒泡标记到entry point,所有无效模块处理完成后,更新所有的accept 的handlers都会被触发,然后状态切换为idle状态
3.2 原理解析
查看bundle.js中的代码,将3.1中的内容翻译一下:
- 在
webpack-dev-server配置了hot:true的基础上,bundle.js中的reloadApp()会进行HMR的逻辑(另一个逻辑是之前面的自动刷新),这个时候会开始判断HMR的status,并发起一个AJAX请求chunk.hot-update.json,该请求会返回一个JSON对象
{
c: {
main: true
},
h: nextChunk
}
- 请求成功以后,继续更新HMR的
status,会比较返回的nextChunk和当前的chunk是否相同,如果相同结束HMR,如果不同将nextChunk存储并继续进行HMR - 根据
chunk拼接<script>的src,并将<script>动态插入html的<head>中(比如我的配置就是:main.chunk.hot-update.js),操作后chunk数组序号+1 - 由于
<head>插入了新的脚本<script src="main.chunk.hot-update.js"></script>,实现动态更新JS而不刷新页面
补充:对于添加了module.hot.dispose的部分,创建了全局对象data={}来保存数据,从而实现重新加载JS以后,仍然可以访问该数值的内容
3. 总结
HMR的核心点有两个:
- 如何在服务端文件变化之后通知客户端?使用socket
- 如何判断文件是否发生变化?客户端和服务端都保存
chunk,比较是否发生变化
webpack的内容其实真的有点多,仅仅HMR包含的内容就很多,自己的总结也不够完整,只能说是刚好入门。不过整个过程一边自己猜想,一遍跟源码,然后验证自己猜想,真的是很有意思的过程。
4. 参考
webpack官方文档:
CSDN上一篇特别棒的资料: