背景
模块热替换(HMR - hot module replacement)功能是webpack4 提供在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:
- 保留在完全重新加载页面期间丢失的应用程序状态。
- 只更新变更内容,以节省宝贵的开发时间。
- 在源代码中对 CSS/JS 进行修改,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。
使用热更新
使用 webpack 热更新非常简单,只需要在 webpack 配置文件中添加相关配置即可。具体来说,我们需要添加 webpack-dev-server 和 webpack-hot-middleware 两个插件,然后在 entry 配置中添加一个入口文件,最后将 hot: true 添加到 devServer 配置中即可,如下所示:
javascriptCopy code
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: [
'webpack-hot-middleware/client',
'./src/index.js'
],
output: {
filename: 'bundle.js',
path: __dirname + '/dist'
},
devServer: {
hot: true
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
};
其中,webpack-hot-middleware/client 是 webpack-hot-middleware 的客户端代码,它会与 webpack-dev-server 建立 WebSocket 连接,从而实现代码的热更新。
HMR的工作流程
HMR的工作流程可以分为三个阶段:
检测模块更新
Webpack的HMR runtime会通过一些方法来检测模块的更新,包括监听文件系统的变化、检测依赖关系以及热替换接口等。当一个模块发生变化时,HMR runtime会触发一个更新事件。
更新模块
当模块发生更新事件时,Webpack会重新编译该模块,并生成一个新的模块对象。在生成新的模块对象时,Webpack使用了一种称为“更新分块”的技术,将更新后的代码和之前的代码进行比较,找到差异部分,只更新差异部分的代码。这样做可以提高更新的效率,减少不必要的网络传输。
应用更新
在生成新的模块对象后,HMR runtime会将其注入到应用程序中,并执行相应的回调函数,以完成模块的更新。在注入新的模块时,HMR runtime会使用一些技术来确保更新的正确性和安全性,例如使用“代码版本”来防止旧的代码被错误地执行。
HMR 的工作原理
HMR 的工作原理可以分为以下几个步骤:
- 构建过程中,Webpack 在编译时为每个模块生成唯一的模块 ID。HMR 使用这些 ID 来识别模块。
- 当开发人员保存文件时,Webpack 会重新编译并构建更新过的模块,以及与之相关的模块。
- 在编译过程中,Webpack 会生成一个补丁(patch),其中包含了新旧模块之间的差异。
- 补丁会通过 Websocket 传输到客户端,HMR 运行时(runtime)接收到补丁后,会根据补丁内容来更新模块。
- 更新模块时,HMR 会先检查模块是否接受更新。如果接受,HMR 会用新的代码替换旧的代码,并在必要时重新执行模块。如果不接受,HMR 会刷新整个页面。
如上图所示,右侧Server端使用webpack-dev-server去启动本地服务,内部实现主要使用了webpack、express、websocket。
服务端流程
- 启动webpack-dev-server服务器,创建webpack实例,源代码地址@webpack-dev-server/webpack-dev-server.js#L173
- 添加webpack的done事件回调,用于推送每次编译产生的hash 源代码地址@webpack-dev-server/Server.js#L122
constructor(compiler) {
let sockets = []
let lasthash
compiler.hooks.done.tap('webpack-dev-server', (stats) => {
lasthash = stats.hash
// 每当新一个编译完成后都会向客户端发送消息
sockets.forEach(socket => {
socket.emit('hash', stats.hash) // 先向客户端发送最新的hash值
socket.emit('ok') // 再向客户端发送一个ok
})
})
}
webpack编译后提供提供了一系列钩子函数,以供插件能访问到它的各个生命周期节点,并对其打包内容做修改。compiler.hooks.done则是插件能修改其内容的最后一个节点。
编译完成通过socket向客户端发送消息,推送每次编译产生的hash。另外如果是热更新的话,还会产出二个补丁文件,里面描述了从上一次结果到这一次结果都有哪些chunk和模块发生了变化。
- 添加webpack-dev-middleware中间件,读取文件放到内存中。源代码地址@webpack-dev-server/Server.js#L125
- 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接,源代码地址@webpack-dev-server/Server.js#L745
客户端流程
- webpack-dev-server/client端会监听到此hash消息,源代码地址@webpack-dev-server/index.js#L54
- 客户端收到ok的消息后会执行reloadApp方法进行更新,源代码地址index.js#L101
- 在webpack/hot/dev-server.js会监听webpackHotUpdate事件,对比上上一次编译改变了那些模块源代码地址 dev-server.js#L55
function hotCheck() {
// 是请求 webpack-dev-middleware中间件 获取文件地址
hotDownloadManifest().then(update => {
let chunkIds = Object.keys(update.c)
chunkIds.forEach(chunkId => {
hotDownloadUpdateChunk(chunkId)
})
})
}
- 调用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码,源代码地址JsonpMainTemplate.runtime.js#L14
function hotDownloadUpdateChunk(chunkId) {
let script = document.createElement('script')
script.charset = 'utf-8'
// /main.xxxx.hot-update.js
script.src = '/' + chunkId + "." + hotCurrentHash + ".hot-update.js"
document.head.appendChild(script)
}
- 最后调用hotApply方法进行热更新,将新的模块代码应用到当前模块中。
总结
Webpack HMR通过WebSocket或者基于长轮询的技术实现了无需刷新页面的动态代码更新。HMR runtime会监听文件系统中文件的变化,并在文件发生变化时,向Webpack发送HTTP请求获取最新的代码。Webpack会将最新的代码注入到当前运行的应用程序中,从而实现动态更新的效果。