《webpack实现热更新(HMR)原理分析》
# 简介
模块热替换(HMR - Hot Module Replacement),是指在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。开启 HMR 能大大提高开发效率。
# 启用HMR
- 配置 devServer(webpack-dev-server) 使得支持 HMR
- 使用内置插件 HotModuleReplacementPlugin(如果没有设置,则会被自动添加)
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
hot: true, // 启动
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
// new webpack.HotModuleReplacementPlugin() 设置 hot: true,该插件会被自动添加
]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 看得到的热更新
热更新大部分的处理我们是看不到的,但是还是可以从请求中窥得一二。
将任意 vue 项目跑起来,并且开启热更新 devServer{ hot: true}
。
# 1. 首次请求页面/刷新
可以看到除了资源以外,服务端和浏览器还建立了 websocket
长链接,并且发送了一个 hash
值。这是服务器与客户端通信的关键!
# 2. 第一次修改
服务端通过 websocket
向浏览器发送 hash
值。浏览器拿到 hash
值做对比,发现有不一致则向浏览器请求新资源文件。
12678d3d9e7227e8b774.hot-update.json
:已改动模块的 json 文件,用于下次对比。app.12678d3d9e7227e8b774.hot-update.js
:已改动模块的 js 文件,用于更新视图。
# 3. 多次修改
从下面截图可以看到,每次服务端发送的消息的 hash 将作为下次 hot-update.json 和 hot-update.js 文件的 hash。
将看到的和没看到的地方补充一下,就能得到下面一个详细的更新流程图
# 热更新流程图示
虽然看起来步骤有点多,实际上关键点只有几个:
- 服务端和客户端通过
websocket
建立长连接,实现通讯 - 每个文件都有一个
hash
值标记,并且通过websocket
传给客户端,便于比较 - 服务端监听文件变化,每次变化重新编译后生成新
hash
值,并发送给客户端 - 客户端监听
hash
值变化,如果一致则使用缓存,不一致则使用新hash
值向服务器请求新的模块文件替换,实现局部刷新
# 从代码看变化
从浏览器请求中可以看到变化,从打包后代码里也能找到热更新的痕迹。
为了演示,在App.vue
中引入两个非常简单的组件
test1.vue
<template>
<div>
<h2>test1111</h2>
</div>
</template>
2
3
4
5
test2.vue
<template>
<div>
<h2>test22222</h2>
</div>
</template>
2
3
4
5
# 打包后的代码
项目打包后输出 app.js
, 这里包含了所有的信息。
通过 websocket
我们可以拿到一个 hash 值 1553ffbe6e5f353c4019
, 使用这个值在 app.js 中搜索。可以看到,这个被赋值给了一个变量,并且被函数使用。
app.js
// ...
// 记住这个webpackHotUpdate
var parentHotUpdateCallback = (typeof self !== 'undefined' ? self : this)["webpackHotUpdate"];
(typeof self !== 'undefined' ? self : this)["webpackHotUpdate"] = // eslint-disable-next-line no-unused-vars
function webpackHotUpdateCallback(chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
if (parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules);
} ;
// 通过hash值下载新的模块js
function hotDownloadUpdateChunk(chunkId) {
var script = document.createElement("script");
script.charset = "utf-8";
script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
if (null) script.crossOrigin = null;
document.head.appendChild(script);
}
// 通过hash值下载新的json
function hotDownloadManifest(requestTimeout) {
requestTimeout = requestTimeout || 10000;
return new Promise(function(resolve, reject) {
if (typeof XMLHttpRequest === "undefined") {
return reject(new Error("No browser support"));
}
try {
var request = new XMLHttpRequest();
var requestPath = __webpack_require__.p + "" + hotCurrentHash + ".hot-update.json";
request.open("GET", requestPath, true);
request.timeout = requestTimeout;
request.send(null);
} catch (err) {
return reject(err);
}
request.onreadystatechange = function() {
// ...
};
});
}
// ...
var hotCurrentHash = "1553ffbe6e5f353c4019";
// test2.vue 组件代码(篇幅过长,通过关键字查找即可)
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 记住这个id
eval("/很多代码/!./src/components/test2.vue?vue&type=template&id=2e742a00&\n");
}),
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 修改组件
对 test2.vue 进行修改,触发重新编译
<template>
<div>
<h2>test22222添加</h2>
</div>
</template>
2
3
4
5
浏览器重新请求新的模块文件
# hot-update.js
// webpackHotUpdate 是不是很眼熟
webpackHotUpdate("app", {
// 这个id是不是很眼熟
/***/ "很多代码./src/components/test2.vue?vue&type=template&id=2e742a00&":
// 这个函数是不是很眼熟
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/很多代码/!./src/components/test2.vue?vue&type=template&id=2e742a00&\n");
}
)
})
2
3
4
5
6
7
8
9
10
11
hot-update.js 返回一个 webpackHotUpdate
函数,当接收到这个函数的时候,HotModuleReplacement.runtime
会根据 chunkId 和 组件id 标识去查找,在内存中进行更新对应模块。
# 自定义热更新机制(module.hot)
以上的内容开启热更新后默认的更新机制,我们是可以通过 webpack 提供的接口 module.hot 自定义热更新机制。
为了演示,我们对文件进行调整下:
新增 src/hello.js
console.log("hello -- 11")
App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<test1></test1>
<test2></test2>
<h2>
这里是Hello-- <span id="hello"></span>
</h2>
</div>
</template>
2
3
4
5
6
7
8
9
10
main.js
import Vue from 'vue'
import App from './App.vue'
require('./hello')
Vue.config.productionTip = false
// 重要
if(module.hot) {
module.hot.accept('./hello.js', function() {
console.log('hello 改变了')
document.getElementById('hello').innerHTML = 'hello 改变了'
})
}
new Vue({
render: h => h(App),
}).$mount('#app')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
项目启动,可以看到控制台输出了 hello -- 11
,此时hello.js 文件并没有改变
现在调整下 src/hello.js,再看控制台
console.log("hello -- 22")
浏览器并没有刷新,并且控制台输出了 hello -- 22
,视图也按预期进行了更新
# 实现原理总结
- 服务端和客户端使用 websocket 实现长连接
- webpack 监听源文件的变化,即当开发者保存文件时触发 webpack 的重新编译。
- 每次编译都会生成 hash 值、已改动模块的 json 文件、已改动模块代码的 js 文件
- 编译完成后通过 socket 向客户端推送当前编译的 hash 戳
- 客户端的 websocket 监听到有文件改动推送过来的 hash 戳,会和上一次对比
- 一致则走缓存
- 不一致则通过 ajax 和 jsonp 向服务端获取最新资源
- 使用内存文件系统去替换有修改的内容实现局部刷新
# 最后
想看更详细的介绍和总结可以看以下文章:
参考文章: