webpack热更新
Hot Module Replacement
,简称HMR
,无需完全刷新整个页面的同时,更新模块。HMR
的好处,在日常开发工作中体会颇深:节省宝贵的开发时间、提升开发体验。
在这里简单介绍一个 HMR 的原理
HMR 初始化
webpack-dev-server
Webpack-dev-server 的执行过程,其实也是一个 HMR 开启的过程
修改 webpackOptions,添加两个入口文件,一个是 websocket 客户端代码,一个是热更新客户端代码(主要是用于检查更新逻辑)。
启动
webpack
,生成compiler
实例。compiler
上有很多方法,比如可以启动webpack
所有编译工作,以及监听本地文件的变化。使用
express
框架启动本地server
,让浏览器可以请求本地的静态资源。- 启动 server 的时候,监听 compiler 的 done 事件,当监听到一次
webpack
编译结束,就会调用_sendStats
方法通过websocket
给浏览器发送通知,ok
和hash
事件,这样浏览器就可以拿到最新的hash
值了,做检查更新逻辑 - 生成 webpack-dev-middleware 中间件实例,保存在 this.middleware(主要是本地文件的编译和输出以及监听)
- 启动 server 的时候,监听 compiler 的 done 事件,当监听到一次
本地
server
启动之后,再去启动websocket
服务,通过websocket
,可以建立本地服务和浏览器的双向通信。这样就可以实现当本地文件发生变化,立马告知浏览器可以热更新代码啦!
webpack-dev-middleware
文件相关的操作都抽离到webpack-dev-middleware
库了,主要是本地文件的编译和输出以及监听
主要流程都在下述代码中
1 | share.setOptions(context.options); |
- 初始化配置,利用
memory-fs
库将文件打包到内存中(访问文件系统中的文件更快,而且也减少了代码写入文件的开销) - 注册一系列事件。
- 开启对本地文件的监听,当文件发生变化,重新编译,编译完成之后继续监听
一次完整的 HMR 流程
当文件发生变化,就触发重新编译。当监听到一次
webpack
编译结束,_sendStats
方法就通过websoket
给浏览器发送通知hash
事件,更新最新一次打包后的hash
值ok
事件,进行热更新检查
1
2
3
4
5
6this.sockWrite(sockets, 'hash', stats.hash);
if (stats.errors.length > 0) {
this.sockWrite(sockets, 'errors', stats.errors);
} else if (stats.warnings.length > 0) {
this.sockWrite(sockets, 'warnings', stats.warnings);
} else { this.sockWrite(sockets, 'ok'); }客户端接受到 ws 消息后,
hash
事件更新当前hash
值,ok 事件触发hotEmitter
1 | // webpack-dev-server/client/index.js |
web-dev-server 插入的客户端的另一个入口文件
webpack/hot/dev-server.js
,监听hotEmitter
事件,进行热更新检查check
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// webpack/hot/dev-server.js
var check = function check() {
// 热更新核心代码
module.hot.check(true)
.then(function(updatedModules) {
// 容错,直接刷新页面
if (!updatedModules) {
window.location.reload();
return;
}
// 热更新结束,打印信息
if (upToDate()) {
log("info", "[HMR] App is up to date.");
}
})
.catch(function(err) {
window.location.reload();
});
};
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
lastHash = currentHash;
check();
});check
代码module.hot.check
在HotModuleReplacementPlugin.runtime.js
中hotCheck
主要做了三件事利用上一次保存的
hash
值,调用hotDownloadManifest
发送xxx/hash.hot-update.json
的ajax
请求;请求结果获取热更新模块,以及下次热更新的
Hash
标识,并进入热更新准备阶段。调用
hotDownloadUpdateChunk
发送xxx/hash.hot-update.js
请求,通过JSONP
方式。返回结果后,要立即执行
webpackHotUpdate
这个方法。1
2
3window["webpackHotUpdate"] = function (chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
} ;hotAddUpdateChunk
方法会把更新的模块moreModules
赋值给全局全量hotUpdate
。hotUpdateDownloaded
方法会调用hotApply
进行代码的替换。1
2
3
4
5
6
7
8
9
10function hotAddUpdateChunk(chunkId, moreModules) {
// 更新的模块moreModules赋值给全局全量hotUpdate
for (var moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
hotUpdate[moduleId] = moreModules[moduleId];
}
}
// 调用hotApply进行模块的替换
hotUpdateDownloaded();
}hotApply 热更新模块替换
- 删除过期的模块,就是需要替换的模块
通过
hotUpdate
可以找到旧模块1
2
3
4
5
6
7
8
9
10
11
12
13
14var queue = outdatedModules.slice();
while (queue.length > 0) {
moduleId = queue.pop();
// 从缓存中删除过期的模块
module = installedModules[moduleId];
// 删除过期的依赖
delete outdatedDependencies[moduleId];
// 存储了被删掉的模块id,便于更新代码
outdatedSelfAcceptedModules.push({
module: moduleId
});
}
复制代码- 将新的模块添加到 modules 中
1
2
3
4
5
6
7appliedUpdate[moduleId] = hotUpdate[moduleId];
for (moduleId in appliedUpdate) {
if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
复制代码- 通过webpack_require执行相关模块的代码
1
2
3
4
5
6
7
8
9
10for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
var item = outdatedSelfAcceptedModules[i];
moduleId = item.module;
try {
// 执行最新的代码
__webpack_require__(moduleId);
} catch (err) {
// ...容错处理
}
}