- 一、什么是HMR
- 二、图解HMR工作流程
- 1、第一步,在webpack的watch模式下,文件系统中的某一个文件发生修改,webpack监听到文件变化,根据配置文件对模块重新打包,并将打包后的代码通过简单的javascript对象保存在内存中。
- 2、第二步,是webpack-dev-server和webpack之间的接口交互。在这一步中主要是webpack-dev-server的中间件webpack-dev-middleware和webpack之间的交互,webpack-dev-middleware调用webpack暴露的API对代码进行监控,并告诉webpack,将代码打包到内存中。
- 3、第三步,webpack-dev-server对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase为true时,Server会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器对应的应用进行live reload。这个是浏览器刷新,不是HMR。
- 4、第四步,是webpack-dev-server代码工作,该步骤主要是通过socket在浏览器和服务端之间建立一个websocket长连接,将webpack编译打包的各个阶段的状态信息告知浏览器,同时也包含第三步中server监听静态文件的变化信息。浏览器根据这些socket信息进行不同的操作。服务端传递的最主要信息还是新模块的hash值,后面的步骤都是根据这一hash值来进行模块热更新替换的。
- 5、webpack-dev-server/client端并不能够请求更新的代码,也不会执行热更新模块操作,而把这些工作又交给webpack,webpack/hot/dev/server的工作就是就是根据webpack-dev-server/client传给它的信息以及dev-server的配置就定了是浏览器刷新还是热模块更新。如果仅仅是刷新浏览器,就不会又后续过程。
- 6、HotModuleReplacement.runtime是客户端HMR的中枢,它接收上一步传递给他的新模块的hash值,它通过JsonpMainTemplate.runtime向server端发送ajax请求,服务端返回一个json,该json包含了所有要更新代模块的hash值,获取到更新列表后,该模块在通过jsonp请求,获取到最新的模块代码,这个就是下图中7、8、9三个步骤。
- 7、第10步是决定HMR是否成功的关键步骤,在该步骤中,HotModulePlugin将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后进行依赖检查,更新模块的同时更新与该模块依赖的模块。
- 8、最后一步,当HMR失败后,回退到live Reload操作,也就是浏览器刷新来获取重新打包内容。
- ">以下就是HMR流程图:

- 三、详解流程
- 四、总结
- 1、在启动阶段
- 2、更新阶段
- 五、参考:
一、什么是HMR
Hot Module Replacement是指当我们对代码修改并保存后,webpack将会对代码重新打包,并将新的模块发送给浏览器端,浏览器用新的模块代替老得模块,来实现不刷新页面的前提下更新页面。
二、图解HMR工作流程
1、第一步,在webpack的watch模式下,文件系统中的某一个文件发生修改,webpack监听到文件变化,根据配置文件对模块重新打包,并将打包后的代码通过简单的javascript对象保存在内存中。
2、第二步,是webpack-dev-server和webpack之间的接口交互。在这一步中主要是webpack-dev-server的中间件webpack-dev-middleware和webpack之间的交互,webpack-dev-middleware调用webpack暴露的API对代码进行监控,并告诉webpack,将代码打包到内存中。
3、第三步,webpack-dev-server对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase为true时,Server会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器对应的应用进行live reload。这个是浏览器刷新,不是HMR。
4、第四步,是webpack-dev-server代码工作,该步骤主要是通过socket在浏览器和服务端之间建立一个websocket长连接,将webpack编译打包的各个阶段的状态信息告知浏览器,同时也包含第三步中server监听静态文件的变化信息。浏览器根据这些socket信息进行不同的操作。服务端传递的最主要信息还是新模块的hash值,后面的步骤都是根据这一hash值来进行模块热更新替换的。
5、webpack-dev-server/client端并不能够请求更新的代码,也不会执行热更新模块操作,而把这些工作又交给webpack,webpack/hot/dev/server的工作就是就是根据webpack-dev-server/client传给它的信息以及dev-server的配置就定了是浏览器刷新还是热模块更新。如果仅仅是刷新浏览器,就不会又后续过程。
6、HotModuleReplacement.runtime是客户端HMR的中枢,它接收上一步传递给他的新模块的hash值,它通过JsonpMainTemplate.runtime向server端发送ajax请求,服务端返回一个json,该json包含了所有要更新代模块的hash值,获取到更新列表后,该模块在通过jsonp请求,获取到最新的模块代码,这个就是下图中7、8、9三个步骤。
7、第10步是决定HMR是否成功的关键步骤,在该步骤中,HotModulePlugin将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后进行依赖检查,更新模块的同时更新与该模块依赖的模块。
8、最后一步,当HMR失败后,回退到live Reload操作,也就是浏览器刷新来获取重新打包内容。
以下就是HMR流程图:
三、详解流程
1、webpack对文件系统进行watch监听,并打包到内存中
webapck-dev-middle调用webpack中compiler的watch方法对文件系统进行监听,当文件发生改变后webpack进行重新编译打包,然后保存到内存中。
// webpack-dev-middleware/index.js中,调用compiler的watch方法进行文件监听context.watching = compiler.watch(options.watchOptions, (err) => {if (err) {context.log.error(err.stack || err);if (err.details) {context.log.error(err.details);}}});
webpack将bundle.js文件打包到内存中,不生成文件的原因就是在于访问内存的代码比访问文件系统更快,并且能减少代码写入的开销。在webpack-dev-middle中引入了memory-fs的依赖库,webpack-dev-middle将webpack原本的outputFileSystem替换成了MemoryFileSystem的实例,这样就可以将代码写入内存中。
// webpack-dev-middleware/lib/fs.js中,将fs替换成memory-fsconst MemoryFileSystem = require('memory-fs');setFs(context, compiler) {let fileSystem;// store our files in memoryconst isConfiguredFs = context.options.fs;const isMemoryFs =!isConfiguredFs &&!compiler.compilers &&compiler.outputFileSystem instanceof MemoryFileSystem;context.fs = new MemoryFileSystem();}
2、启动devServer
在启动devServer的时候,sockjs在服务端和客户端之间建立了一个websocket长连接,以便将webpack编译和打包的各个阶段告诉浏览器端,最关键的步骤是webpack-dev-server调用webpack api监听compiler的done这个钩子,当compile完成后,webpack-dev-server通过_sendStatus方法将编译打包后的新模块的hash值发送给浏览器端。
// webpack-dev-server/Server.js中,监听compiler的done事件setupHooks() {compiler.hooks.done.tap('webpack-dev-server', (stats) => {this._sendStats(this.sockets, this.statsObj.toJson(stats));this._stats = stats;});}// 给浏览器发送socket事件,将打包后的hash值进行发送_sendStats(sockets, stats, force) {this.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');}}sockWrite(sockets, type, data) {sockets.forEach((socket) => {this.socketServer.send(socket, JSON.stringify({ type, data }));});}
当文件进行了修改时,因为调用了compiler的watch方法,所以就会触发重新编译,HotModuleReplacement这个插件会监听compilation这个钩子,收集变更的文件信息。
3、webpack-dev-server/client接收服务端消息作出响应
webpack-dev-server修改了webpack配置中的entry属性,在里面添加了webpack-dev-client的代码,相当于增加了入口文件的配置,分别是webpack-dev-server/client/index.js和webpack/hot/dev-server两个入口文件,分别是用来作为浏览器端启动的WS客户端,监听WS的事件和得到接收消息后做更新操作。
// webpack-dev-server/lib/utils/addEntris.js中,// 把webpack/hot/dev-server和webpack-dev-server/client/index.js添加到入口中let hotEntry;if (options.hotOnly) {hotEntry = require.resolve('webpack/hot/only-dev-server');} else if (options.hot) {hotEntry = require.resolve('webpack/hot/dev-server');}const clientEntry = `${require.resolve('../../client/')}?${domain}${sockHost}${sockPath}${sockPort}`;const additionalEntries = [clientEntry]additionalEntries.push(hotEntry);config.entry = prependEntry(config.entry || './src', additionalEntries
webpack-dev-server/client客户端接收到type为hash消息后会将hash值暂存起来,当接受到type为ok的消息后对应用执行reload操作。在reload操作中,webpack-dev-server/client会根据hot配置绝对浏览器是刷新还是进行热更新。
// webpack-dev-server/client/index.jsvar onSocketMessage = {hash: function hash(_hash) { // 当接收到hash的消息后,对hash值进行缓存status.currentHash = _hash;},ok: function ok() { // 接收到ok的时候进行刷新还是reload操作sendMessage('Ok');reloadApp(options, status);}}// webpack-dev-server/client/reloadApp.js中function reloadApp() {if (hot) {var hotEmitter = require('webpack/hot/emitter');hotEmitter.emit('webpackHotUpdate', currentHash);} else if (liveReload) {rootWindow.location.reload();}}
在webpack/hot/dev-server中,监听客户端发出的webpackHotUpdate事件。
// webpack/hot/dev-servervar 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();}});
4、webpack接收到最新hash进行验证
首先webpack/hot/dev-server接收到webpack-dev-server/client/index.js中发送的webpackHotUpdate这个消息,会进行check检查,会调用module.hot.check,实质调用webpack/lib/HotModuleReplacement.runtime中check方法,检查是否有更新。
在check过程中会利用webpack/lib/JsonpTemplate.runtime中的两个方法:hotDownloadManifest和hotDownloadUpdateChunk。其中hotDownloadManifest这个方法是根据调用Ajax请求manifest文件,会返回一个[hash].hot-update.json文件,该方法返回变化文件的hash值。hotDownloadUpdateChunk这个方法会根据返回的hash值向服务端发送jsonp请求,获取最新的模块代码,同时也将模块代码返回给HMR runtime,HMR runtime会根据返回的新的代码块进行处理。
// webpack/hot/dev-server中// 当接收到webpackHotUpdate事件后会调用module.hot.check的方法var check = function check() {module.hot.check(true).catch(function(err) { // 监听错误,如果出错就重新加载页面var status = module.hot.status();if(["abort", "fail"].indexOf(status) >= 0) {window.location.reload();}});};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();}});// webpack/hotModuleReplacement.runtime.js中module.hot = {check: function() {return hotDownloadManifest().then(function() { // 发送ajax请求获取更新的hashhotDownloadUpdateChunk(); // 通过jsonp请求获取最新的代码hotApply(); // 替换过期的模块return promise;});}}
5、对模块代码进行热更新
这一步是热更新关键的一步,更新代码都发生在hotModuleReplacement中的hotApply方法中。首先找出outdateModules和outdateDependencies,然后删除缓存中过期的模块和依赖,最后将新的模块和依赖添加到modules中,当再次调用调用的时候,就是获取最新的代码块了。
// webpack/lib/HotModuleReplacement.runtimefunction hotApply() {var idx;var queue = outdatedModules.slice();while (queue.length > 0) {moduleId = queue.pop();module = installedModules[moduleId];// 删除过期的模块delete installedModules[moduleId];// 删除过期的依赖delete outdatedDependencies[moduleId];// 移除所有子节点for (j = 0; j < module.children.length; j++) {var child = installedModules[module.children[j]];if (!child) continue;idx = child.parents.indexOf(moduleId);if (idx >= 0) {child.parents.splice(idx, 1);}}}// 插入新的代码for (moduleId in appliedUpdate) {if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {modules[moduleId] = appliedUpdate[moduleId];}}}
6、最后更新页面
当用新的模块代码替换老的模块后,但是我们的业务代码并不能知道代码已经发生变化,也就是说,当文件修改后,我们需要在 入口文件中调用 HMR 的 accept 方法,添加模块更新后的处理函数,及时将修改方法进行调用。
// index.jsif(module.hot) {module.hot.accept('./hello.js', function() {// 业务代码})}
四、总结
一个完整的HMR流程整个周期分为启动阶段和文件监听更新流程。
1、在启动阶段
- webpack和webpack-dev-server进行交互。在webpack-dev-server创建一个server,通过express的中间件系统注册在webpack-dev-middleware中定义好的中间件。
- webpack-dev-server启动webpack打包的watch模式。在这种模式下webpack会监听文件变化,一旦文件发生改变,则就会重新打包,watch模式下打包的结果也不会写入硬盘上,写入到内存中。
- webpack-dev-server通过webpack-dev-middleware和webpack进行交互,包含将fs重写成memory-fs对象、将静态js文件发送给浏览器等功能。
- 在webpack-dev-server中会接收comipler对象,通过这个对象可以注册钩子函数,来监听打包构建过程中的生命中周期。
- 如果webpack.config.js中devServer中的contentBase为true,则webpack-dev-server会监听文件夹中的文件变化,发生改变则通知浏览器重新刷新页面重新请求文件。
打开浏览器后,webpack-dev-server会利用sockjs在浏览器和服务器中间建立一个Websocket长连接,这个长连接是浏览器和webpack-dev-server的通信桥梁,它之间通信内容主要是传递编译模块的文件信息(hash值),如果webpack监控的文件修改了,webpack/hot/dev-server来实现热更新还是刷新页面。
2、更新阶段
当文件发生改变后,webpack会重新编译文件,这个时候我们在webpack.config.js中添加的HotModuleReplacement插件会生成两次编译之间的差异列表(manifest)文件[hash].hot-update.json,这个manifest JSON文件包含了文件变化的内容[id].[hash].hot-update.js。编译完成后,这时会触发webpack-dev-server中注册的done的钩子会用Websocket推送编译之后的hash值,后续还会发送一个type为ok的消息。
- 当客户端接收到服务端发送的hash和ok请求后,在webpack-dev-server/client客户端中也会发送一个webpackHotUpdate事件,这个事件接收方是webpack/hot/dev-server,接收这个事件后,会进行检查操作,这个就会调用hotModuleReplacement.runtime中注册的hotCheck方法。
- hotCheck这个方法中,会将新生成的文件放入modules中来进行替换上一次打包编译生成的文件,并且会向服务端发起一个ajax请求,来获取这个manifest文件,其实就是[hash].hot-update.json文件内容,里面是发生变化的js文件。
- manifest列表文件内容拿到后,会告诉HMR发生了哪些js文件改变,这个时候HMR runtime会按照变更信息列表发送Jsonp请求,将两次编译的差异文件[id].[hash].hot-update.js获取下来,最后插入到head标签中执行,最终完成全部流程。
五、参考:
Webpack HMR 原理解析
带你看懂 HMR 热更新原理
webpack 工程化实践总结之webpack 核心模块、Compiler 和 Compilation、基本流程和 HMR
