什么是HMR
全名:HotModuleReplacement(热更新),当本地代码发生变化后,通知浏览器进行代码更新,但不会进行状态改变。
怎么实现HMR
这里我们使用webpack-dev-middleware和webpack-hot-middleware
为什么不用webpack-dev-server?
- webpack-dev-server封装的比较完整,难以从配置上看出HMR需要做哪些工作
- webpack-dev-server在本地开发的时候较为快捷,但是在与node结合的时候,不太方便。
- 原理大同小异,webpack-dev-server使用socketIo,而webpack-hot-middleware使用的SSE技术,进行与浏览器沟通
修改webpack配置
module.exports = {
mode: 'development',
context: __dirname,
entry: [
// 需要把这块代码一起打入bundle中
'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',
// 真正的入口
'./client.js'
],
output: {
path: __dirname,
publicPath: '/',
filename: 'bundle.js'
},
devtool: '#source-map',
plugins: [
// 这段不要忘了
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
],
};
node中
var app = express();
(function() {
// Step 1: 创建webpackCompiler
var webpack = require('webpack');
var webpackConfig = require(process.env.WEBPACK_CONFIG ? process.env.WEBPACK_CONFIG : './webpack.config');
var compiler = webpack(webpackConfig);
// Step 2: 接入webpack-dev中间件
app.use(require("webpack-dev-middleware")(compiler, {
logLevel: 'warn', publicPath: webpackConfig.output.publicPath
}));
// Step 3: 接入webpackHot中间件
app.use(require("webpack-hot-middleware")(compiler, {
log: console.log, path: '/__webpack_hmr', heartbeat: 10 * 1000
}));
})();
前端代码中
if (module.hot) {
module.hot.accept();
}
这样就可以实现热更新了,下面我们来详细讲解下webpack-dev-middleware和webpack-hot-middleware是起到了什么作用
原理
第一步
entry: [
// 需要把这块代码一起打入bundle中
'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',
// 真正的入口
'./client.js'
],
- 重点来了:在entry里加入'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',实际上就是找到这段代码合并到bundle包里。
- 这个文件的主要作用就是创建EventSource实例,请求 /__webpack_hmr,监听building、built、sync事件
function init() {
source = new window.EventSource(options.path);
source.onmessage = handleMessage;
}
// Eventsource收到消息,通知所有回调执行
function handleMessage(event) {
lastActivity = new Date();
for (var i = 0; i < listeners.length; i++) {
listeners[i](event);
}
}
return {
addMessageListener: function(fn) {
listeners.push(fn);
},
};
- 回调里检查是否有更新,回调函数会利用HotModuleReplacementPlugin运行时代码进行更新;
check(){
var result = module.hot.check(false, cb);
if (result && result.then) {
result.then(function(updatedModules) {
// 如果updatedModules为null,则整体reload
// 否则执行替换逻辑
var applyResult = module.hot.apply();
});
result.catch(cb);
}
}
- module.hot实际就是HotModuleReplacementPlugin中的hotCheck
- 调用hotDownloadManifest,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(lasthash.hot-update.json),该 Manifest 包含了本次编译hash值 和 更新模块的chunk名,xxxx.hot-update.json
function hotCheck(apply) { hotDownloadManifest(hotRequestTimeout).then(function(update) {}
- 方法通过JSONP请求获取到最新的模块代码,main.xxxx.hotupdate.js
hotDownloadUpdateChunk(chunkId)
- hotAddUpdateChunk动态更新模块代码,
- 调用hotApply方法进行热更新
第二步
把webpackCompiler对象传入expres的中间件webpack-dev-middleware
- 让webpack以watch模式运行
// Start watching
context.watching = context.compiler.watch(watchOptions, (error) => {
if (error) {
context.logger.error(error);
}
});
- 并将文件系统改为内存文件系统,不会把打包后的资源写入磁盘而是在内存中处理;
for (const compiler of compilers) {
// eslint-disable-next-line no-param-reassign
compiler.outputFileSystem = outputFileSystem;
}
context.outputFileSystem = outputFileSystem;
- 将编译的文件返回
async function processRequest() {
const filename = getFilenameFromUrl(context, req.url);
let content = context.outputFileSystem.readFileSync(filename);
// Buffer
content = handleRangeHeaders(context, content, req, res);
// send Buffer
res.send(content);
}
第三步
把webpackCompiler对象传入expres的中间件webpack-hot-middleware
- 监听webpack打包进度
compiler.plugin('done', onDone);
- 打包完成后
publishStats('built', latestStats,eventStream, opts.log);
// EventStream发送消息
eventStream.publish({
name: name,
action: action,
time: stats.time,
hash: stats.hash,
warnings: stats.warnings || [],
errors: stats.errors || [],
modules: buildModuleMap(stats.modules),
});
- 收到的请求如果url是'/webpack_hmr',则把它加入EventStream回调,进行心跳检测
var middleware = function(req, res, next) {
if (closed) return next();
//请求的url地址不是/__webpack_hmr,则走下一个中间件
if (!pathMatch(req.url, opts.path)) return next();
//
eventStream.handler(req, res);
if (latestStats) {
// 给所有的http请求,发送EventStream
publishStats('sync', latestStats, eventStream);
}
};
整体流程和总结
流程图
总结
整体HMR的流程并不复杂,主要的点就是监听webpack打包,server向客户端发送消息通知,客户端拉取最新的module后进行热更。 本篇文章Demo地址传送门