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");
  }
};

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. 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()
	}
});

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");
  },
	

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;
});

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;
	}

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;
								});
							}
						});
					});
				});
			});
	}

可以看到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;
				}
			};
		}
	};

以下面代码作为例子:

// 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. 我们更改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": []
}

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.......';
    }
    

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

    // 20221205152831
    // http://localhost:8080/index.a75a200ec8e959f6ed40.hot-update.json
    
    {
      "c": [
        "index"
      ],
      "r": [
        
      ],
      "m": [
        "./src/add.js"
      ]
    }
    

另外,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")
/******/ })();
/******/ 
/******/ }
);

可以看到其内部调用了函数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;
/******/ 			}
/******/ 		};

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热更新原理
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Neil-

你们的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值