'webpack/hot/dev-server' 文件如下
// => module.hot 被替换成true:在前期ast语法树分析过程中标识代码位置,然后在webpack assets阶段被替换 // => module.hot 被替换成true:在前期ast语法树分析过程中标识代码位置,然后在webpack assets阶段被替换 if(module.hot) { var lastHash; var upToDate = function upToDate() { return lastHash.indexOf(__webpack_hash__) >= 0; }; var check = function check() { module.hot.check(true).then(function(updatedModules) { if(!updatedModules) { console.warn("[HMR] Cannot find update. Need to do a full reload!"); console.warn("[HMR] (Probably because of restarting the webpack-dev-server)"); window.location.reload(); return; } if(!upToDate()) { check(); } require("./log-apply-result")(updatedModules, updatedModules); if(upToDate()) { console.log("[HMR] App is up to date."); } }).catch(function(err) { var status = module.hot.status(); if(["abort", "fail"].indexOf(status) >= 0) { console.warn("[HMR] Cannot apply update. Need to do a full reload!"); console.warn("[HMR] " + err.stack || err.message); // window.location.reload(); } else { console.warn("[HMR] Update failed: " + err.stack || err.message); } }); }; var hotEmitter = require("./emitter"); hotEmitter.on("webpackHotUpdate", function(currentHash) { lastHash = currentHash; if(!upToDate() && module.hot.status() === "idle") { console.log("[HMR] Checking for updates on the server..."); check(); } }); console.log("[HMR] Waiting for update signal from WDS..."); } else { throw new Error("[HMR] Hot Module Replacement is disabled."); }
结论:被insert到客户端浏览器中的这段代码决定了 webpack热更新HMR 的开始,当热更新HMR模式失败时,就直接刷新浏览器了
const { setup } = require('../../util'); 文件如下
module.exports = { setup(config) { const defaults = { plugins: [], devServer: {} }; const result = Object.assign(defaults, config); result.plugins.push(new webpack.HotModuleReplacementPlugin()); result.plugins.push(new HtmlWebpackPlugin({ filename: 'index.html', template: path.join(__dirname, '.assets/layout.html'), title: exampleTitle })); return result; } };
webpack.HotModuleReplacementPlugin 插件的作用就是:在webpack打包生成的代码中添加功能代码,当我们开发时,修改某个文件并保存后,浏览器会拿到修改的模块代码,然后执行并更新依赖, 当然浏览器如何拿到代码以及如何执行更新,下面会讲到,这里先提一下这个插件的作用
webpack entry 入口文件app.js
'use strict'; require('./example'); if (module.hot) { module.hot.accept((err) => { if (err) { console.error('Cannot apply HMR update.', err); } }); }
webpack entry 入口文件example.js
'use strict'; const target = document.querySelector('#target'); target.innerHTML = 'Modify to update this element without reloading the page.';
html Template 模板文件
<!doctype html> <html> <head> <title>WDS ▻ API: Simple Server</title> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="shortcut icon" href="/.assets/favicon.ico"/> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,600|Source+Sans+Pro:400,400i,500,600"/> <link rel="stylesheet" href="/.assets/style.css"/> </head> <body> <main> <header> <h1> <img src="/.assets/icon-square.svg" style="width: 35px; height: 35px;"/> webpack-dev-server </h1> </header> <section> <h2>API: Simple Server</h2> <div id="target"></div> </section> <section> <div id="targetmodule"></div> </section> </main> <script type="text/javascript" src="main.js"></script></body> </html>
以上是涉及到的一些文件...
下面来看具体的效果:运行 node --inspect-brk server.js 文件, 访问http://localhost:8080
上图左侧是 webpack-dev-server 中 websockets server端的代码,借助webpack-dev-middleware注册webapck打包生命周期事件回调函数,将打包过程关键生命点同步到客户端浏览器(右侧) ,从console处可以知道收到了消息类型type:hot、hash、ok。其中hot类型是告诉客户端浏览器更新代码的方式采用 热更新HMR 的方式, 而不是采用热重载live reload 直接刷新浏览器的方式,hash是本次webpack打包后的hash值, ok标识webpack打包生命周期已经完成,可以进行客户端浏览器代码的更新操作了,也就是 webpack热更新HMR的过程。
下面当修改 example.js 文件 也就是浏览器如何更新代码流程 关键时刻到了
//target.innerHTML = 'Modify to update this element without reloading the page.'; target.innerHTML = '热更新HMR的模式';
文件变化后,webpack.HotModuleReplacementPlugin 插件 中关键的 webpack Compilation 对象事件回调函数如下
compilation.plugin("record", function(compilation, records) { // 生成的 records 用于当文件变化后找出变话的模块 debugger if(records.hash === this.hash) return; records.hash = compilation.hash; records.moduleHashs = {}; // 循环每个module, webpack中一个文件就是一个module,且通过hash值判断文件是否有更改 this.modules.forEach(function(module) { var identifier = module.identifier(); var hash = require("crypto").createHash("md5"); module.updateHash(hash); records.moduleHashs[identifier] = hash.digest("hex"); }); records.chunkHashs = {}; // this webpack compilation 对象 this.chunks.forEach(function(chunk) { records.chunkHashs[chunk.id] = chunk.hash; }); records.chunkModuleIds = {}; this.chunks.forEach(function(chunk) { records.chunkModuleIds[chunk.id] = chunk.modules.map(function(m) { return m.id; }); }); }); var initialPass = false; var recompilation = false; compilation.plugin("after-hash", function() { // records 相应的hash 决定模块变化之后的标识 debugger var records = this.records; if(!records) { initialPass = true; return; } if(!records.hash) initialPass = true; var preHash = records.preHash || "x"; var prepreHash = records.prepreHash || "x"; if(preHash === this.hash) { recompilation = true; this.modifyHash(prepreHash); return; } records.prepreHash = records.hash || "x"; records.preHash = this.hash; // complain 对象的hash值 this.modifyHash(records.prepreHash); }); compilation.plugin("additional-chunk-assets", function() { // 这里当modul变化之后,找出变化的module 并并生成json 和对应的module Template模板信息 debugger var records = this.records; if(records.hash === this.hash) return; if(!records.moduleHashs || !records.chunkHashs || !records.chunkModuleIds) return; // 循环遍历module 通过hash值标识module是否变化了 this.modules.forEach(function(module) { var identifier = module.identifier(); var hash = require("crypto").createHash("md5"); module.updateHash(hash); hash = hash.digest("hex"); module.hotUpdate = records.moduleHashs[identifier] !== hash; }); // this.hash webpack Compilation 对象的hash值 var hotUpdateMainContent = { h: this.hash, c: {} }; // records.chunkHashs 包含了 所有chunk的hash值信息 Object.keys(records.chunkHashs).forEach(function(chunkId) { chunkId = isNaN(+chunkId) ? chunkId : +chunkId; // 修改文件导致module 变化 => 找到对应的chunk var currentChunk = this.chunks.find(chunk => chunk.id === chunkId); if(currentChunk) { // 通过chunk 来确定是哪个module变化了 var newModules = currentChunk.modules.filter(function(module) { return module.hotUpdate; }); var allModules = {}; currentChunk.modules.forEach(function(module) { allModules[module.id] = true; }); // 如果项目中有某个模块没有引用了 就会找出改模块 var removedModules = records.chunkModuleIds[chunkId].filter(function(id) { return !allModules[id]; }); // 如果发生了模块module的变化 if(newModules.length > 0 || removedModules.length > 0) { // 根据变化的module 得到 module字符串模板 var source = hotUpdateChunkTemplate.render(chunkId, newModules, removedModules, this.hash, this.moduleTemplate, this.dependencyTemplates); var filename = this.getPath(hotUpdateChunkFilename, { hash: records.hash, chunk: currentChunk }); this.additionalChunkAssets.push(filename); // filename 就是: `${currentChunk}.${records.hash}.hot-update.js}` => 0.9236d98784cee1af7a96.hot-update.js文件 this.assets[filename] = source; // 标识module变化了 hotUpdateMainContent.c[chunkId] = true; currentChunk.files.push(filename); this.applyPlugins("chunk-asset", currentChunk, filename); } } else { hotUpdateMainContent.c[chunkId] = false; } }, this); // 下面是 `${records.hash}.hot-update.json` => 9236d98784cee1af7a96.hot-update.json 文件内容 var source = new RawSource(JSON.stringify(hotUpdateMainContent)); var filename = this.getPath(hotUpdateMainFilename, { hash: records.hash }); this.assets[filename] = source; // 注: 以上添加到this.assets 的内容在 Compiler.emitAssets 阶段 生成文件内容 });
结论: 当文件变化后,webpack 就会编译生成 hot-update.json、以及对应的文件模块hot-update.js信息 用于在Compiler.emitAssets 阶段生成js文件