• webpack热更新原理解析


    热更新原理

    1. webpack-dev-server启动本地服务

    这里首先会启动webpack并生成compiler实例(compiler实例通过各种事件钩子可以实现监听编译无效、编译结束等功能);

    然后会通过express启动一个本地服务,用于服务浏览器对打包资源的请求;
    同时,server启动后会启动一个websocket服务,用于服务端与浏览器之间的全双工通信(比如本地资源更新并打包结束后通知客户端请求新的资源);

    webpack-dev-server/client/index.js目录下onSocketMessage函数如下:

    
    var onSocketMessage = {
      hot: function hot() {
        if (parsedResourceQuery.hot === "false") {
          return;
        }
        options.hot = true;
      },
      liveReload: function liveReload() {
        if (parsedResourceQuery["live-reload"] === "false") {
          return;
        }
        options.liveReload = true;
      },
      /**
       * @param {string} hash
       */
      hash: function hash(_hash) {
        status.previousHash = status.currentHash;
        status.currentHash = _hash;
      },
      logging: setAllLogLevel,
      /**
       * @param {boolean} value
       */
      overlay: function overlay(value) {
        if (typeof document === "undefined") {
          return;
        }
        options.overlay = value;
      },
      /**
       * @param {number} value
       */
      reconnect: function reconnect(value) {
        if (parsedResourceQuery.reconnect === "false") {
          return;
        }
        options.reconnect = value;
      },
      "still-ok": function stillOk() {
        log.info("Nothing changed.");
        if (options.overlay) {
          hide();
        }
        sendMessage("StillOk");
      },
      ok: function ok() {
        sendMessage("Ok");
        if (options.overlay) {
          hide();
        }
        reloadApp(options, status);
      },
      close: function close() {
        log.info("Disconnected!");
        if (options.overlay) {
          hide();
        }
        sendMessage("Close");
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    2. 修改webpack.config.js的entry配置

    启动本地服务后,会在入口动态新增两个文件入口并一同打包到bundle文件中,如下:

    // 修改后的entry入口
    { entry:
        { index: 
            [
                // socket客户端代码,onSocketMessge,处理ok/hash等消息
                'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080',
                // 监听'webpackHotUpdate热更新检查
                'xxx/node_modules/webpack/hot/dev-server.js',
                // 开发配置的入口
                './src/index.js'
        	],
        },
    }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里需要说明下两个文件的作用:

    1. webpack/hot/dev-server.js:该函数主要用于处理检测更新,将其注入到客户端代码中,然后当接收到服务端发送的webpackHotUpdate消息后调用module.hot.check()方法检测更新;有更新时通过module.hot.apply()方法应用更新
    2. webpack-dev-server/client/index.js:动态注入socket客户端代码,通过onSocketMessage函数处理socket服务端的消息;用于更新hash及热模块检测和替换;
    // webpack/hot/dev-server.js核心代码
    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(); // module.hot.check()
    	}
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3. webpack监听文件变化

    监听文件变化主要通过setupDevMiddleware方法,底层主要是通过webpack-dev-middleware,调用了compiler.watch方法监听文件的变化;

    当文件变化时,通过memory-fs库将打包后的文件写入内存,而不是\dist目录;

    其实就是因为webpack-dev-server只负责启动服务和前置准备工作所有文件相关的操作都抽离到webpack-dev-middleware库了,主要是本地文件的编译和输出以及监听;

    Compiler 支持可以监控文件系统的 监听(watching) 机制,并且在文件修改时重新编译。 当处于监听模式(watch mode)时, compiler 会触发诸如 watchRun, watchCloseinvalid 等额外的事件。

    要注意的是,make 事件在 webpack 启动和每当 监听文件变化 时都会触发。

    4. 监听webpack编译结束

    通过webpack-dev-server/lib/Server.js中的setupHooks()方法监听webpack编译完成;主要是通过done钩子监听到当次compilation编译完成时,触发done回调并调用sendStats发送socket消息okhash事件

    5. 浏览器接收到热更新的通知

    上面讲到,当次compilation结束后会通过websocket发送消息通知到客户端,客户端检测是否需要热更新;客户端根据消息类型(ok/hash/hot/ovelay/invalid等)做对应的处理

    客户端接收websocket的代码在启动webpack服务后会动态加入到entry入口中并打包到bundle.js中,因此可以正常接收socket服务端消息

    在这里插入图片描述

    以下是部分socketMessge的处理函数,这里hash可以看到用于更新previousHashcurrentHashok事件主要用于进行热更新检查,主要通过reloadApp实现,其内部则是通过node的EventEmitter发送了webpackHotUpdate事件触发热更新检查;而真正的热更新检查是由HotModuleReplacementPluginmodule.hot.check()实现的;

      /**
       * @param {string} hash
       */
      hash: function hash(_hash) {
        status.previousHash = status.currentHash;
        status.currentHash = _hash;
      },
      ok: function ok() {
        sendMessage("Ok");
    
        if (options.overlay) {
          hide();
        }
    
        reloadApp(options, status);
      },
      /**
       * @param {boolean} value
       */
      overlay: function overlay(value) {
        if (typeof document === "undefined") {
          return;
        }
    
        options.overlay = value;
      },
      invalid: function invalid() {
        log.info("App updated. Recompiling..."); // Fixes #1042. overlay doesn't clear if errors are fixed but warnings remain.
    
        if (options.overlay) {
          hide();
        }
    
        sendMessage("Invalid");
      },
    	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    module.hot.checkmodule.hot.apply方法与HotModuleReplacementPlugin相关,接下来我们看看其作用

    6. HotModuleReplacementPlugin

    如下所示,我们可以看到module.hot的定义由createModuleHotObject决定,内部的hot对象中定义了check: hotChekapply: hotApply等;具体实现需要借助setStatus函数及对应status

    由于这些代码需要在HMR中使用,也是运行时代码,所以同样会被开始就注入到入口文件中

    // *\node_modules\webpack\lib\hmr\HotModuleReplacement.runtime.js
    $interceptModuleExecution$.push(function (options) {
    	var module = options.module;
    	var require = createRequire(options.require, options.id);
    	module.hot = createModuleHotObject(options.id, module);
    	module.parents = currentParents;
    	module.children = [];
    	currentParents = [];
    	options.require = require;
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    createModuleHotObject的实现如下:

    	function createModuleHotObject(moduleId, me) {
    		var _main = currentChildModule !== moduleId;
    		var hot = {
    			// private stuff
    			_acceptedDependencies: {},
    			_acceptedErrorHandlers: {},
    			_declinedDependencies: {},
    			_selfAccepted: false,
    			_selfDeclined: false,
    			_selfInvalidated: false,
    			_disposeHandlers: [],
    			_main: _main,
    			_requireSelf: function () {
    				currentParents = me.parents.slice();
    				currentChildModule = _main ? undefined : moduleId;
    				__webpack_require__(moduleId);
    			},
    
    			// Module API
    			active: true,
    			accept: function (dep, callback, errorHandler) {
    				if (dep === undefined) hot._selfAccepted = true;
    				else if (typeof dep === "function") hot._selfAccepted = dep;
    				else if (typeof dep === "object" && dep !== null) {
    					for (var i = 0; i < dep.length; i++) {
    						hot._acceptedDependencies[dep[i]] = callback || function () {};
    						hot._acceptedErrorHandlers[dep[i]] = errorHandler;
    					}
    				} else {
    					hot._acceptedDependencies[dep] = callback || function () {};
    					hot._acceptedErrorHandlers[dep] = errorHandler;
    				}
    			},
    			decline: function (dep) {
    				if (dep === undefined) hot._selfDeclined = true;
    				else if (typeof dep === "object" && dep !== null)
    					for (var i = 0; i < dep.length; i++)
    						hot._declinedDependencies[dep[i]] = true;
    				else hot._declinedDependencies[dep] = true;
    			},
    			dispose: function (callback) {
    				hot._disposeHandlers.push(callback);
    			},
    			addDisposeHandler: function (callback) {
    				hot._disposeHandlers.push(callback);
    			},
    			removeDisposeHandler: function (callback) {
    				var idx = hot._disposeHandlers.indexOf(callback);
    				if (idx >= 0) hot._disposeHandlers.splice(idx, 1);
    			},
    			invalidate: function () {
    				this._selfInvalidated = true;
    				switch (currentStatus) {
    					case "idle":
    						currentUpdateApplyHandlers = [];
    						Object.keys($hmrInvalidateModuleHandlers$).forEach(function (key) {
    							$hmrInvalidateModuleHandlers$[key](
    								moduleId,
    								currentUpdateApplyHandlers
    							);
    						});
    						setStatus("ready");
    						break;
    					case "ready":
    						Object.keys($hmrInvalidateModuleHandlers$).forEach(function (key) {
    							$hmrInvalidateModuleHandlers$[key](
    								moduleId,
    								currentUpdateApplyHandlers
    							);
    						});
    						break;
    					case "prepare":
    					case "check":
    					case "dispose":
    					case "apply":
    						(queuedInvalidatedModules = queuedInvalidatedModules || []).push(
    							moduleId
    						);
    						break;
    					default:
    						// ignore requests in error states
    						break;
    				}
    			},
    
    			// Management API
    			check: hotCheck,
    			apply: hotApply,
    			status: function (l) {
    				if (!l) return currentStatus;
    				registeredStatusHandlers.push(l);
    			},
    			addStatusHandler: function (l) {
    				registeredStatusHandlers.push(l);
    			},
    			removeStatusHandler: function (l) {
    				var idx = registeredStatusHandlers.indexOf(l);
    				if (idx >= 0) registeredStatusHandlers.splice(idx, 1);
    			},
    
    			//inherit from previous dispose call
    			data: currentModuleData[moduleId]
    		};
    		currentChildModule = undefined;
    		return hot;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106

    7. module.hot.check 开始热更新

    hotCheck的实现如下:

    	function hotCheck(applyOnUpdate) {
    		if (currentStatus !== "idle") {
    			throw new Error("check() is only allowed in idle status");
    		}
    		return setStatus("check")
    			.then($hmrDownloadManifest$)
    			.then(function (update) {
    				if (!update) {
    					return setStatus(applyInvalidatedModules() ? "ready" : "idle").then(
    						function () {
    							return null;
    						}
    					);
    				}
    
    				return setStatus("prepare").then(function () {
    					var updatedModules = [];
    					currentUpdateApplyHandlers = [];
    
    					return Promise.all(
    						Object.keys($hmrDownloadUpdateHandlers$).reduce(function (
    							promises,
    							key
    						) {
    							$hmrDownloadUpdateHandlers$[key](
    								update.c,
    								update.r,
    								update.m,
    								promises,
    								currentUpdateApplyHandlers,
    								updatedModules
    							);
    							return promises;
    						},
    						[])
    					).then(function () {
    						return waitForBlockingPromises(function () {
    							if (applyOnUpdate) {
    								return internalApply(applyOnUpdate);
    							} else {
    								return setStatus("ready").then(function () {
    									return updatedModules;
    								});
    							}
    						});
    					});
    				});
    			});
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    可以看到check状态成功后会进入prepare状态,成功后会返回一个promise对象;

    $hmrDownloadUpdateHandlers$.$key$ = function (
    		chunkIds,
    		removedChunks,
    		removedModules,
    		promises,
    		applyHandlers,
    		updatedModulesList
    	) {
    		applyHandlers.push(applyHandler);
    		currentUpdateChunks = {};
    		currentUpdateRemovedChunks = removedChunks;
    		currentUpdate = removedModules.reduce(function (obj, key) {
    			obj[key] = false;
    			return obj;
    		}, {});
    		currentUpdateRuntime = [];
    		chunkIds.forEach(function (chunkId) {
    			if (
    				$hasOwnProperty$($installedChunks$, chunkId) &&
    				$installedChunks$[chunkId] !== undefined
    			) {
    				promises.push($loadUpdateChunk$(chunkId, updatedModulesList));
    				currentUpdateChunks[chunkId] = true;
    			} else {
    				currentUpdateChunks[chunkId] = false;
    			}
    		});
    		if ($ensureChunkHandlers$) {
    			$ensureChunkHandlers$.$key$Hmr = function (chunkId, promises) {
    				if (
    					currentUpdateChunks &&
    					$hasOwnProperty$(currentUpdateChunks, chunkId) &&
    					!currentUpdateChunks[chunkId]
    				) {
    					promises.push($loadUpdateChunk$(chunkId));
    					currentUpdateChunks[chunkId] = true;
    				}
    			};
    		}
    	};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    以下面代码作为例子:

    // src/index.js
    import { addByBit, add } from './add';
    
    export default function () {
        console.log('rm console loader test==', addByBit(1,2));
        return 'hello index file.......';
    }
    
    
    // src/foo.js
    export default () => {
        console.log('hello webpack demos!')
        return 'hello webpack'
    }
    
    
    // src/add.js
    // add function
    export function add(a, b) {
        console.log('a + b===', a + b);
        return a + b;
    }
    
    // add by bit-operation
    export function addByBit(a, b) {
        if (b === 0) return a;
        let c = a ^ b,
            d = (a & b) << 1;
    
        return addByBit(c, d);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    1. 我们更改src/index.js,将return 'hello index file.......';更改为return 'hello index file.';

    触发更新时首先会发送ajax请求http://localhost:8081/index.3b102885936c5d7de6d5.hot-update.json3b102885936c5d7de6d5为oldhash,对应的返回为:

    // 20221205151344
    // http://localhost:8080/index.455e1dda0f8e3cbf9ba0.hot-update.json
    
    {
      "c": [
        "index"
      ],
      "r": [],
      "m": []
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    c: chunkIds,
    r: removedChunks,
    m: removedModules

    1. 我们更改src/index.js,移除add.js文件引用

      export default function () {
          console.log('rm console loader test==');
          return 'hello index file.......';
      }
      
      • 1
      • 2
      • 3
      • 4

      本次更新后得到的[id]-[hash].hot-update.json为:

      // 20221205152831
      // http://localhost:8080/index.a75a200ec8e959f6ed40.hot-update.json
      
      {
        "c": [
          "index"
        ],
        "r": [
          
        ],
        "m": [
          "./src/add.js"
        ]
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

    另外,webpack还会通过JSONP方式请求http://localhost:8080/index.455e1dda0f8e3cbf9ba0.hot-update.js3b102885936c5d7de6d5为oldhash,对应的返回为待更新模块的更新后的chunk代码;

    self["webpackHotUpdatedemo"]("index",{
    
    /***/ "./src/index.js":
    /*!**********************!*\
      !*** ./src/index.js ***!
      \**********************/
    /***/ ((__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) => {
    
    eval("/* harmony import */ var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./add */ \"./src/add.js\");\n\r\n\r\n/* harmony default export */ function __WEBPACK_DEFAULT_EXPORT__() {\r\n    console.log('rm console loader test==', (0,_add__WEBPACK_IMPORTED_MODULE_0__.addByBit)(1,2));\r\n    return 'hello index file.';\r\n}\n\n//# sourceURL=webpack://demo/./src/index.js?");
    
    /***/ })
    
    },
    /******/ function(__webpack_require__) { // webpackRuntimeModules
    /******/ /* webpack/runtime/getFullHash */
    /******/ (() => {
    /******/ 	__webpack_require__.h = () => ("8be9bd00cb51bd4d68f1")
    /******/ })();
    /******/ 
    /******/ }
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    可以看到其内部调用了函数self["webpackHotUpdatedemo"],其定义如下,入参分别为chunkIdmoreModulesruntimeruntime用于更新最新的文件hash,

    webpack的输出产物除了业务代码外,还有包括支持webpack模块化、异步模块加载、热更新等特性的支撑性代码,这些代码称为runtime

    self["webpackHotUpdatedemo"] = (chunkId, moreModules, runtime) => {
    /******/ 			for(var moduleId in moreModules) {
    /******/ 				if(__webpack_require__.o(moreModules, moduleId)) {
    /******/ 					currentUpdate[moduleId] = moreModules[moduleId];
    /******/ 					if(currentUpdatedModulesList) currentUpdatedModulesList.push(moduleId);
    /******/ 				}
    /******/ 			}
    /******/ 			if(runtime) currentUpdateRuntime.push(runtime);
    /******/ 			if(waitingUpdateResolves[chunkId]) {
    /******/ 				waitingUpdateResolves[chunkId]();
    /******/ 				waitingUpdateResolves[chunkId] = undefined;
    /******/ 			}
    /******/ 		};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    8. module.hot.apply

    通过webpack/lib/hmr/JavascriptHotModuleReplacement.runtime.js中的相关逻辑做热更新;

    1. 首先需要删除过期的模块
      具体通过webpack/lib/hmr/JavascriptHotModuleReplacement.runtime.js中的dispose方法实现

    2. 将新的模块添加到modules中,并更新模块代码

      具体通过webpack/lib/hmr/JavascriptHotModuleReplacement.runtime.js中的apply方法实现。

      • insert new code
      • run new runtime modules
      • call accept handlers
      • Load self accepted modules

    参考文献

    1. 轻松理解webpack热更新原理
  • 相关阅读:
    复杂微纳结构制造需求旺盛 微纳3D打印市场发展前景广阔
    《安富莱嵌入式周报》第323期:NASA开源二代星球探索小车, Matlab2023b,蓝牙照明标准NLC, Xilinx发布电机套件,Clang V17发布
    mongodb入门(三)
    【NLP的python库(03/4) 】: 全面概述
    html visibilitychange 事件
    【风险管理】MT4外汇交易新手指南:掌握资金管理的重要性
    DirectX12初始化三——DirectX图形基础结构,功能支持检测,资源驻留
    zabbix监控添加监控项及其监控Mysql、nginx
    ZXing - barcode scanning library for Java, Android
    打印机选择问题咨询 -11111
  • 原文地址:https://blog.csdn.net/luofeng457/article/details/128159754