webpack 打包完事后,如何通知浏览器呢?如下webpack-dev-server Server.js文件
function Server(compiler, options) { // debugger // Default options if (!options) options = {}; // webpack 配置中的属性,决定通过热更新的方式 this.hot = options.hot || options.hotOnly; compiler.plugin('done', (stats) => { // 这里注册 webpack compiler 对象的事件, 通过websockets 通知客户端浏览器 debugger this._sendStats(this.sockets, stats.toJson(clientStats)); this._stats = stats; }); // Init express server const app = this.app = new express(); // eslint-disable-line app.all('*', (req, res, next) => { // eslint-disable-line if (this.checkHost(req.headers)) { return next(); } res.send('Invalid Host header'); }); // webpackDevMiddleware 监听文件的变换 watch -> build // middleware for serving webpack bundle this.middleware = webpackDevMiddleware(compiler, options); // ... this.listeningApp = http.createServer(app); // ... } // delegate listen call and init sockjs Server.prototype.listen = function (port, hostname, fn) { this.listenHostname = hostname; // eslint-disable-next-line const returnValue = this.listeningApp.listen(port, hostname, (err) => { const sockServer = sockjs.createServer({ // Use provided up-to-date sockjs-client sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js', // Limit useless logs log(severity, line) { if (severity === 'error') { log(line); } } }); sockServer.on('connection', (conn) => { if (!conn) return; if (!this.checkHost(conn.headers)) { this.sockWrite([conn], 'error', 'Invalid Host header'); conn.close(); return; } this.sockets.push(conn); conn.on('close', () => { const connIndex = this.sockets.indexOf(conn); if (connIndex >= 0) { this.sockets.splice(connIndex, 1); } }); // 这里根据webpackConfig 中的配置 devServer.hot= true 通知客户端浏览 更新代码的方式 if (this.hot) this.sockWrite([conn], 'hot'); if (!this._stats) return; this._sendStats([conn], this._stats.toJson(clientStats), true); }); if (fn) { fn.call(this.listeningApp, err); } }); return returnValue; }; Server.prototype.sockWrite = function (sockets, type, data) { sockets.forEach((sock) => { sock.write(JSON.stringify({ type, data })); }); }; // send stats to a socket or multiple sockets Server.prototype._sendStats = function (sockets, stats, force) { if (!force && stats && (!stats.errors || stats.errors.length === 0) && stats.assets && stats.assets.every(asset => !asset.emitted) ) { return this.sockWrite(sockets, 'still-ok'); } 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'); } }; module.exports = Server;
当客户端浏览器收到消息后 type: ok 消息类型发生时,流程如下: webpack打包后的部分代码
//webpack/hot/dev-server.js 也就是webpack 入口添加的文件 module.hot.check(true).then(function(updatedModules) {}).catch(function(updatedModules) {}) // 进入 function hotCheck(apply) { if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status"); hotApplyOnUpdate = apply; hotSetStatus("check"); return hotDownloadManifest().then(function(update) { // update.c标识对应的chunk是否发生了变化 hotAvailableFilesMap = update.c; hotUpdateNewHash = update.h; hotSetStatus("prepare"); var promise = new Promise(function(resolve, reject) { }); // 开始请求 hot-update.json 文件 hotEnsureUpdateChunk(chunkId); return promise; }); } // 请求之前webpack 生成的hot-update.json文件 function hotDownloadManifest() { // eslint-disable-line no-unused-vars return new Promise(function(resolve, reject) { if(typeof XMLHttpRequest === "undefined") return reject(new Error("No browser support")); try { var request = new XMLHttpRequest(); var requestPath = __webpack_require__.p + "" + hotCurrentHash + ".hot-update.json"; request.open("GET", requestPath, true); request.timeout = 10000; request.send(null); } catch(err) { return reject(err); } request.onreadystatechange = function() { if(request.readyState !== 4) return; // ... resolve(update); } }; }); } // 请求之前webpack 生成的hot-update.js 文件 function hotDownloadUpdateChunk(chunkId) { // eslint-disable-line no-unused-vars var head = document.getElementsByTagName("head")[0]; var script = document.createElement("script"); script.type = "text/javascript"; script.charset = "utf-8"; script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js"; head.appendChild(script); } // 请求的js文件执行如下代码 function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars hotAddUpdateChunk(chunkId, moreModules); if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules); } ; // 后续部分逻辑... while(queue.length > 0) { moduleId = queue.pop(); module = installedModules[moduleId]; if(!module) continue; var data = {}; // Call dispose handlers var disposeHandlers = module.hot._disposeHandlers; for(j = 0; j < disposeHandlers.length; j++) { cb = disposeHandlers[j]; cb(data); } hotCurrentModuleData[moduleId] = data; // disable module (this disables requires from this module) module.hot.active = false; // 删除缓存 // remove module from cache delete installedModules[moduleId]; // remove "parents" references from all children 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); } } } // 插入变化的模块 // insert new code for(moduleId in appliedUpdate) { if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) { modules[moduleId] = appliedUpdate[moduleId]; } } // 插入模块后, 重新执行js文件,这个过程浏览器是没有刷新的,可以通过浏览器Network看出 // Load self accepted modules for(i = 0; i < outdatedSelfAcceptedModules.length; i++) { var item = outdatedSelfAcceptedModules[i]; moduleId = item.module; hotCurrentParents = [moduleId]; try { __webpack_require__(moduleId); } catch(err) {} } // __webpack_require__(moduleId); 再次进入 app.js 文件执行 => /* 37 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; __webpack_require__(71); if (true) { module.hot.accept((err) => { if (err) { console.error('Cannot apply HMR update.', err); } }); }
最后再总结一下整个热更新HMR流程吧:
当我们修改文件并保存时,webpack-dev-server 通过 webpack-dev-middleware 能够拿到webpack打包过程的各个生命周期点, webpack打包过程通过HotModuleReplacementPlugin插件生成hot-update.js和hot-update.json文件,前者是变化的模块字符串信息,后者是本次打包之后module模块所对应的chunk信息以及打包后的hash值,决定客户端浏览器是否更新。 然后webpack-dev-server 通过 websockets把消息发送给客户端浏览器,浏览器收到消息后,分别请求这两个文件,后续为删除installedModules全局缓存对象,并重新赋值,再次执行对应的文件,这样就达到了在无刷新浏览器的条件下,更新变化的模块了,webpack更新模块的代码比较复杂,有的细节没有debug到,到此从Server 到 Client流程以及从Client 到 Server流程也就说清楚了
最后
涉及到的相关技术点有的没有提到,比如webpack的打包流程、webpack中检测文件变化的模块、webpack-dev-middleware相关、webpack-dev-server模块还有请求转发等功能没有说到,这个也不在讨论范围内,有兴趣的可以自己clone 代码查看,如果你对webpack打包流程 debug 过 相信再来了解这些东西会好很多 很多...
可能会有同学会说:看这些有什么作用,当然对我来说是当时的好奇心,通过了解大牛的代码实现,能学习到相关优秀的lib库、增强自己对代码的阅读能力。再有就是了解了一些底层再对其使用时,也能游刃有余。
参考:
1、zhuanlan.zhihu.com/p/30669007
3、github.com/webpack/tap… webpack 如何管理生命周期的核心库
4、astexplorer.net 对了解webpack 如何对代码进行ast分析对照很有用