webpack热更新原理解析
创始人
2024-03-20 01:04:01
0

热更新原理

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 等额外的事件。

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 APIactive: 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 statesbreak;}},// Management APIcheck: 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 calldata: 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热更新原理

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【Ctfer训练计划】——(三... 作者名:Demo不是emo  主页面链接:主页传送门 创作初心ÿ...