背景
传统开发页面,每次更新的时候需要我们手动刷新浏览器才会更新。自从构建工具横空出世。我们可以通过热更新的方式来进行更新。通称HMR(hot module replacement),也就是模块热替换,当你每次需要更新代码的时候,不在需要手动刷新即可实现效果预览。
前置知识
探究热更新是怎么实现之前,我们需要知道几样东西,本文比较硬核,如果不想看可以直接关闭,并且对于一些东西都给出了链接,链接全部为英文文档,如果有中文文档会特殊说明。对了本文是基于webpack5的。
- webpack.HotModuleReplacementPlugin
- sockjs (处理socket请求)
- memory-fs (在内存中去做文件读写IO相关操作)
- webpack-dev-middleware (启动相关服务等)
- Compiler (Compiler是
webpack编译后返回的对象) - Compilation (Compilation是
webpack每次编译后所生成的,其中Compiler是贯穿webpack整个生命周期,而Compilation只负责编译之后模块的更改等相关操作) - tapable (
webpack核心事件流,Compiler和Compliation都基于这个实现,类似于node的eventEmitter扩展内容,如果不写插件可以选择不看或者不研究底层实现也可以选择放弃,然后关闭即可) webpack一些知识和一些底层知识,否则你估计会看不懂,并且属于懵逼状态
memory-fs
内存文件系统,webpack默认生成的文件会丢到内存中,并不会直接写入硬盘
webpack.HotModuleReplacementPlugin
这个是HMR实现核心,由webpack内部所提供的一个插件,该插件会提供一些方法,用于检查变更,并告诉你每次变更所生成的文件hash
sockjs
用于发送socket请求,通知浏览器进行更改
webpack-dev-middleware
webpack-dev-middleware会接收一个webpack所返回的一个compiler对象,然后调用compiler.watch监听文件更改,
我们知道webpack热更新的时候,并不会写入硬盘中,这是因为默认的情况下是写入内存的,因为内存的读取速度比硬盘会快很多,我们可以通过devServer.writeToDisk来修改
if (options.writeToDisk) {
// 写入硬盘
} else {
// 写入内存,并设置内存中的路径为当前路径
const memoryFs = new MemoryFs()
}
这样我们就可以知道文件是否更改,并且把文件存在了内存中,现在在回到webpack-dev-server
HMR是怎么实现的
这里用webpack-dev-server进行举例,所有热更新原理基本类似。手写我们实现一个简单的webpack配置文件。
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
devServer: {
host: 'localhost',
contentBase: path.resolve(__dirname, 'src')
port: 8080,
hot: true,
open: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: './src/pages/index.html',
filename: 'index.html'
})
]
}
简单实现
这是一个简单的流程图,不涉及任何复杂逻辑,这图中我们需要关注几个问题
- 为什么需要添加
webpack.HotModuleReplacementPlugin webpack-dev-server怎么知道文件发生了变化- 如何实现更新
现在我们有了上面的问题,来逐个去看下
- 为什么需要
webpack.HotModuleReplacementPlugin
这个是webpack官方所提供的插件,该插件提供了一些方法,当我们启动的时候webpack会给每个文件注入moudle一个对象,这个对象里面有个module.hot对象,具体可以参考中文文档,并且webpack每次重新构建的时候会生成一个hash,当我们把devServer.writeToDisk设置为true的时候,每次更改还会生成这2个文件。
这个是webpack所注入的module对象
hot-update.js是每次需要更新的文件hot-update.json下次更新需要生成的hash
[id].[fullhash].hot-update.js
[runtime].[fullhash].hot-update.json
- webpack-dev-server怎么知道文件发生了变化
这个问题比较简单,webpack-dev-server启动的时候,初始化了express,并且把expres传递给了webpack-dev-middleware,webpack-dev-middleware也是webpack团队出品,devServer几乎所有api都是通过这个库去实现的。
webpack构建的时候会返回一个compiler对象,该对象会提供一个watch方法用来监听文件更改,而webpack-dev-middleware就是通过这种方法监听文件更改,webpack.HotModuleReplacementPlugin也是一样的操作。并且每次重新构建的时候webpack-dev-server都会监听compiler的compile、invalid、done方法,会通知socket服务用于是否展示overlay(遮罩层)。
compile在compilation生成之前,compilation是webpack每次重新编译所返回的一个对象invalid编译失败done编译成功
- 如何实现更新 webpack-dev-server在启动的时候还会给entry添加2个文件
webpack-dev-server/clinet/index.js
webpack/hot/devServer.js // 如果设置了hotonly会注入webpack/hot/only-dev-server
现在我们知道了每次构建所产生的hash,和产生的文件,但这时候还没办法热更新。我们可以记录每次构建所生成的hash,用于判断和当前的hash是否匹配,从而实现热更新,并且通知浏览器进行更新。
webpack-dev-server/client/index.js负责每次更新的状态,当状态为ok和warinings的时候会执行一个reloadApp方法,该方法会emit一个webpackHotUpdate事件,并把变更的文件hash传递出去,然后webpack/hot/devServer.js负责监听这个事件,当监听到该事件的时候,保存该文件hash,并且判断hash是否已更新并且module.hot.status为idle的时候,执行一个check方法,check会拿到一个需要更新的模块,如果有需要更新,则调用location.reload()方法,进行更新。
// 具体状态判断判断代码在webpack-dev-server/client/index.js, 代码就不贴了。
reloadApp
// 路径为:webpack-dev-server/client/utils/reloadApp.js
var _require = require('./log'),
log = _require.log;
function reloadApp(_ref, _ref2) {
var hotReload = _ref.hotReload,
hot = _ref.hot,
liveReload = _ref.liveReload;
var isUnloading = _ref2.isUnloading,
currentHash = _ref2.currentHash;
if (isUnloading || !hotReload) {
return;
}
if (hot) {
log.info('[WDS] App hot update...');
var hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
if (typeof self !== 'undefined' && self.window) {
// broadcast update to window
self.postMessage("webpackHotUpdate".concat(currentHash), '*');
}
} // allow refreshing the page only if liveReload isn't disabled
else if (liveReload) {
var rootWindow = self; // use parent window for reload (in case we're in an iframe with no valid src)
var intervalId = self.setInterval(function () {
if (rootWindow.location.protocol !== 'about:') {
// reload immediately if protocol is valid
applyReload(rootWindow, intervalId);
} else {
rootWindow = rootWindow.parent;
if (rootWindow.parent === rootWindow) {
// if parent equals current window we've reached the root which would continue forever, so trigger a reload anyways
applyReload(rootWindow, intervalId);
}
}
});
}
function applyReload(rootWindow, intervalId) {
clearInterval(intervalId);
log.info('[WDS] App updated. Reloading...');
rootWindow.location.reload();
}
}
module.exports = reloadApp;
这里emit其实是用的node的eventEmitter模块,下面是具体更新操作。
// 位于webppack/hot/dev-sever.js,下面是
if (module.hot) {
var lastHash;
var upToDate = function upToDate() {
return lastHash.indexOf(__webpack_hash__) >= 0;
};
var log = require("./log");
var check = function check() {
module.hot
.check(true)
.then(function(updatedModules) {
if (!updatedModules) {
log("warning", "[HMR] Cannot find update. Need to do a full reload!");
log(
"warning",
"[HMR] (Probably because of restarting the webpack-dev-server)"
);
window.location.reload();
return;
}
if (!upToDate()) {
check();
}
require("./log-apply-result")(updatedModules, updatedModules);
if (upToDate()) {
log("info", "[HMR] App is up to date.");
}
})
.catch(function(err) {
var status = module.hot.status();
if (["abort", "fail"].indexOf(status) >= 0) {
log(
"warning",
"[HMR] Cannot apply update. Need to do a full reload!"
);
log("warning", "[HMR] " + log.formatError(err));
window.location.reload();
} else {
log("warning", "[HMR] Update failed: " + log.formatError(err));
}
});
};
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
lastHash = currentHash;
if (!upToDate() && module.hot.status() === "idle") {
log("info", "[HMR] Checking for updates on the server...");
check();
}
});
log("info", "[HMR] Waiting for update signal from WDS...");
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
}
现在我们已经大概了解了热更新的过程,估计看到这里,你已经有了大概的理解,下面丢个完整流程图进行理解。
上面是
webpack5的更新过程,webpack4的时候是通过创建script标签,并插入dom来实现的。
由于掘金不支持md流程图,所以就只能截图了。所以看上去会有点糊。