webpack 模块热更新

  • 模块:项目中使用的每个文件
  • 通过互相引用,这些模块会形成一个图(ModuleGraph)数据结构
  • 在打包过程中,模块被合并成chunk,chunk合并成chunk组
  • 一个chunk组中可能有多个chunk。例如,splitChunksPlugin会将一个chunk组拆分为一个或多个chunk

模块热替换(HRM)

模块热替换(HMR-hot module replacement)功能会在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。

  • HRM Server是服务端,用来将变化的js模块通过websocket的消息通知给浏览器端。
  • HRM Runtime是浏览器端,用于接收HMR Server传递的模块数据,浏览器可以看到.hot-update.json的文件过来。

HotModuleReplacementPlugin是做什么用的

  • webpack构建出来的bundle.js本身是不具备热更新的能力的,HotModuleReplacementPlugin的作用就是将HMR runtime注入到bundle.js,使得bundle.js可以和HMR server建立websocket的通信连接。
  • webpack.HotModuleReplacementPlugin没有必要加,配置了hot:true会自动引入这个plugin

webpack-dev-server和hot-module-replacement-plugin之间的关系

  • webpack-dev-server(WDS)的功能提供bundle server的能力,就是生成的bundle.js文件可以通过localhost://***的方式去访问,另外WDS也提供livereload(浏览器的自动刷新)。
  • hot-module-replacement-plugin的作用是提供HMR的runtime,并且将runtime注入到bundle.js代码里面去。一旦磁盘里面的文件修改,那么HMR server会将有修改的js module信息放送给HMR runtime,然后HMR runtime去局部更新页面的代码。因此这种方式可以不用刷新浏览器。
  • 单独写两个包也是出于功能的解耦来考虑的。简单来说:hot-module-replacement-plugin包给webpack-dev-server提供了热更新的能力。

源码阅读

  • 源码版本

    webpack-dev-server 5.0.4

    webpack 5.91.0

  • 启动 HMR Server

    这个工作主要是在webpack-dev-server中完成

    看webpack-dev-server/lib/Server.js文件,下面的express服务实际上对应的是Bundle Server

js 复制代码
const getExpress = memoize(() => require("express"));

  /**
   * @private
   * @returns {void}
   */
  setupApp() {
    /** @type {import("express").Application | undefined}*/
    this.app = new /** @type {any} */ (getExpress())();
  }

debug调用栈: 启动服务结束之后就通过createSocketServer创建websocket服务

js 复制代码
createWebSocketServer() {
    /** @type {WebSocketServerImplementation | undefined | null} */
    this.webSocketServer = new /** @type {any} */ (this.getServerTransport())(
      this,
    );
    // ...
}
js 复制代码
getServerTransport() {
    let implementation;
    let implementationFound = true;

    switch (
      typeof (
        /** @type {WebSocketServerConfiguration} */
        (this.options.webSocketServer).type
      )
    ) {
      case "string":
        // Could be 'sockjs', in the future 'ws', or a path that should be required
        if (
          /** @type {WebSocketServerConfiguration} */ (
            this.options.webSocketServer
          ).type === "sockjs"
        ) {
          implementation = require("./servers/SockJSServer");
        } else if (
          /** @type {WebSocketServerConfiguration} */ (
            this.options.webSocketServer
          ).type === "ws"
        ) {
          implementation = require("./servers/WebsocketServer");
        } else {
          try {
            // eslint-disable-next-line import/no-dynamic-require
            implementation = require(
              /** @type {WebSocketServerConfiguration} */ (
                this.options.webSocketServer
              ).type,
            );
          } catch (error) {
            implementationFound = false;
          }
        }
        break;
      case "function":
        implementation = /** @type {WebSocketServerConfiguration} */ (
          this.options.webSocketServer
        ).type;
        break;
      default:
        implementationFound = false;
    }

    if (!implementationFound) {
      throw new Error(
        "webSocketServer (webSocketServer.type) must be a string denoting a default implementation (e.g. 'ws', 'sockjs'), a full path to " +
          "a JS file which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer.js) " +
          "via require.resolve(...), or the class itself which extends BaseServer",
      );
    }

    return implementation;
  }

debug调用栈:

在webpack-dev-server/client/index.js中,接收到hash消息后,更新hash;接收ok消息,执行reloadApp

js 复制代码
var onSocketMessage = {
 // ....
  /**
   * @param {string} hash
   */
  hash: function hash(_hash) {
    status.previousHash = status.currentHash;
    status.currentHash = _hash;
  },
  // ....
  ok: function ok() {
    sendMessage("Ok");
    if (options.overlay) {
      overlay.send({
        type: "DISMISS"
      });
    }
    reloadApp(options, status);
  },
  // ...
  }

webpack-dev-server/client/utils/reloadApp.js中的reloadApp,这里利用node.js的EventEmitter,发出webpackHotUpdate消息。这里又将更新的事情给回了webpack(为了更好的维护代码,以及职责划分的更明确)

js 复制代码
import hotEmitter from "webpack/hot/emitter.js";
import { log } from "./log.js";

/** @typedef {import("../index").Options} Options
/** @typedef {import("../index").Status} Status

/**
 * @param {Options} options
 * @param {Status} status
 */
function reloadApp(_ref, status) {
  // ...
 
  if (hot && allowToHot) {
    log.info("App hot update...");
    hotEmitter.emit("webpackHotUpdate", status.currentHash);
    if (typeof self !== "undefined" && self.window) {
      // broadcast update to window
      self.postMessage("webpackHotUpdate".concat(status.currentHash), "*");
    }
  }
  // allow refreshing the page only if liveReload isn't disabled
  else if (liveReload && allowToLiveReload) {
    // ...
  }
}
export default reloadApp;

在webpack的webpack/hot/dev-server.js中,监听webpackUpdate事件,并执行check方法。并在check方法中调用module.hot.check方法进行热更新。

js 复制代码
// 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();
		}
	});
	log("info", "[HMR] Waiting for update signal from WDS...");
js 复制代码
var check = function check() {
		module.hot
			.check(true)
			.then(function (updatedModules) {
				// ....
			})
			.catch(function (err) {
                                // ...
                         });
	};

module.hot.check,通过HotModuleReplacementPlugin已经注入到我们chunk中了(也就是上面说的HMR Runtime),所以后面就是它如何更新bundle.js

见下面这段打包后的代码: // webpack/hot/dev-server.js中的module.hot.check(true)中的module就是通过参数传递过来的

js 复制代码
   /***/
    "./node_modules/.pnpm/webpack@5.91.0_webpack-cli@5.1.4/node_modules/webpack/hot/dev-server.js": /*!****************************************************************************************************!*\
  !*** ./node_modules/.pnpm/webpack@5.91.0_webpack-cli@5.1.4/node_modules/webpack/hot/dev-server.js ***!
  \****************************************************************************************************/
    /***/
    ((module,__unused_webpack_exports,__webpack_require__)=>{
       
        eval(" ... 这部分是webpack/hot/dev-server.js文件源码 //# sourceURL=webpack://ts-animates-webpack-demo/./node_modules/.pnpm/webpack@5.91.0_webpack-cli@5.1.4/node_modules/webpack/hot/dev-server.js?");

        /***/
    }
    ),

debug调用栈:

HMR Runtime中更新bundle.js

开启热更新后,打包生成的代码会比不开启多出很多的东西。

打包后的代码中新增了一个createModuleHotObject

js 复制代码
module.hot = createModuleHotObject(options.id, module);

这个函数就是用来返回一个hot对象,所以调用module.hot.check的时候,实际就是执行hotCheck函数

js 复制代码
function createModuleHotObject(moduleId, me) {
 			var _main = currentChildModule !== moduleId;
 			var hot = {
 				// ...
 		
 				// Module API
 				active: true,
 				accept: function (dep, callback, errorHandler) 
                                // ...
 				},
				decline: function (dep) {
 					// ..
				},
 				dispose: function (callback) {
 					hot._disposeHandlers.push(callback);
				},
 				addDisposeHandler: function (callback) {
 					hot._disposeHandlers.push(callback);
				},
				removeDisposeHandler: function (callback) {
					// ...
                                },
 				invalidate: function () {
 					// ...
 				},
 		
 				// Management API
 				check: hotCheck,
 				apply: hotApply,
 				// ...
 			return hot;
 		}

hotCheck中调用了__webpack_require__.hmrM

js 复制代码
function hotCheck(applyOnUpdate) {
                // ...
 			return setStatus("check")
 				.then(__webpack_require__.hmrM)
 				.then(function (update) {
 					if (!update) {
 					   // ...
 					}
 		
 					return setStatus("prepare").then(function () {
 						var updatedModules = [];
 						currentUpdateApplyHandlers = [];
 		
 						return Promise.all(
 					
 // 会调用__webpack_require__.hmrC.jsonp
 // __webpack_require__.hmrC.jsonp 函数中会执行loadUpdateChunk
 Object.keys(__webpack_require__.hmrC).reduce(function (
 								promises,
 								key
 							) {
 								__webpack_require__.hmrC[key](
 									update.c,
 									update.r,
 									update.m,
 									promises,
 									currentUpdateApplyHandlers,
 									updatedModules
 								);
								return promises;
 							}, [])
						).then(function () {
 							// ...
 						});
 					});
 				});
 		}

__webpack_require__.hmrM 加载.hot-update.json

webpack_reuqire .p指的是本地服务的域名,类似http://0.0.0:300 webpack_require.hmrF去获取.hot-update.json文件的地址

js 复制代码
__webpack_require__.hmrM = () => {
     if (typeof fetch === "undefined") throw new Error("No browser support: need fetch API");
     return fetch(__webpack_require__.p + __webpack_require__.hmrF()).then((response) => {
 	if(response.status === 404) return; // no update available
 	if(!response.ok) throw new Error("Failed to fetch update manifest " + response.statusText);
            return response.json();
 	});
 };
js 复制代码
 	/* webpack/runtime/get update manifest filename */
 	(() => {
 		__webpack_require__.hmrF = () => ("runtime." + __webpack_require__.h() + ".hot-update.json");
 	})();

加载要更新的模块

js 复制代码
function loadUpdateChunk(chunkId, updatedModulesList) {
 	currentUpdatedModulesList = updatedModulesList;
 	return new Promise((resolve, reject) => {
 		waitingUpdateResolves[chunkId] = resolve;
 		// start update chunk loading
 		var url = __webpack_require__.p + __webpack_require__.hu(chunkId);
 		// create error before stack unwound to get useful stacktrace later
 		var error = new Error();
 		var loadingEnded = (event) => {
 			// ...	
 		};
 		__webpack_require__.l(url, loadingEnded);
	});
 }
 		

用上一次的hash请求json和js文件。

__webpack_require__.l类似JSONP的方式进行,因为JSONP获取的代码可以直接执行

js 复制代码
__webpack_require__.l = (url, done, key, chunkId) => {
 	// ...
	if(!script) {
 		needAttach = true;
		script = document.createElement('script'); 		
 		script.charset = 'utf-8';
 		script.timeout = 120;
 		if (__webpack_require__.nc) {
 			script.setAttribute("nonce", __webpack_require__.nc);
                }
               script.setAttribute("data-webpack", dataWebpackPrefix + key);
 		
 		script.src = url;
       }
 	    inProgress[url] = [done];
            var onScriptComplete = (prev, event) => {
 				// ...
 	   }
    var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
    script.onerror = onScriptComplete.bind(null, script.onerror);
    script.onload = onScriptComplete.bind(null, script.onload);
    needAttach && document.head.appendChild(script);
};

请求http://***/runtime.9e7296ebc5ffdc0597f0.hot-update.json的返回内容

js 复制代码
{
    "c": [
        "main",
        "runtime"
    ],
    "r": [],
    "m": []
}

请求 http://***/main.9e7296ebc5ffdc0597f0.hot-update.js 返回的内容。

js 复制代码
self["webpackHotUpdatets_animates_webpack_demo"]("main", {

    /***/
    "./node_modules/.pnpm/vue-loader@17.4.2_vue@3.4.25_webpack@5.91.0/node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[2]!./node_modules/.pnpm/vue-loader@17.4.2_vue@3.4.25_webpack@5.91.0/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[9].use[0]!./src/pages/CssCenter.vue?vue&type=template&id=6b351e44&scoped=true": 
    
 
    /***/
    ((__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{

        eval(" ... 这里是更新后的文件内容...//# sourceURL=webpack://ts-animates-webpack-demo/./src/pages/CssCenter.vue?./node_modules/.pnpm/vue-loader@17.4.2_vue@3.4.25_webpack@5.91.0/node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B2%5D!./node_modules/.pnpm/vue-loader@17.4.2_vue@3.4.25_webpack@5.91.0/node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B9%5D.use%5B0%5D");

        /***/
    }
    )

});

下面是webpackHotUpdatets_animates_webpack_demo函数的定义: 第一个参数是模块名,第二个是对象模块路径和内容,第三个是下次更新的hahs值。

js 复制代码
self["webpackHotUpdatets_animates_webpack_demo"] = (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;
/******/ 			}
/******/ 		};

webpackHotUpdatets_animates_webpack_demo方法主要是设置要更新的模块路径和更新内容,随后执行loadUpdateChunk的resolve方法,表示资源获取完成。

执行完该方法后再次回到hotCheck的setStatus("prepare")方法的第二个then回调

第二个then回调中的代码如下:

js 复制代码
return waitForBlockingPromises(function () {
/******/ 								if (applyOnUpdate) {
/******/ 									return internalApply(applyOnUpdate);
/******/ 								} else {
/******/ 									return setStatus("ready").then(function () {
/******/ 										return updatedModules;
/******/ 									});
/******/ 								}
/******/ 							});

紧接着执行internalApply方法

再执行applyHandler方法

首先主要是执行getAffectedModuleEffects方法

回调internalApply方法,执行result.apply方法。

apply方法用新拉取的模块替换__webpack_require__.m模块里面的旧模块。 并调用currentUpdateRuntime方法设置__webpack_require__.h的下一次更新的hash。再执行module.hot.accept

js 复制代码
results.forEach(function (result) {
/******/ 				if (result.apply) {
/******/ 					var modules = result.apply(reportError);
/******/ 					if (modules) {
/******/ 						for (var i = 0; i < modules.length; i++) {
/******/ 							outdatedModules.push(modules[i]);
/******/ 						}
/******/ 					}
/******/ 				}
/******/ 			});

执行`module.hot.accept`

下面是vue中type=template部分的`module.hot.accept`实现,主要是重新执行render函数来实现的

```js
/* hot reload */
if (true) {
  __exports__.__hmrId = "6b351e44"
  const api = __VUE_HMR_RUNTIME__
  module.hot.accept()
  if (!api.createRecord('6b351e44', __exports__)) {
    api.reload('6b351e44', __exports__)
  }
  
  module.hot.accept(/*! ./CssCenter.vue?vue&type=template&id=6b351e44&scoped=true */ "./src/pages/CssCenter.vue?vue&type=template&id=6b351e44&scoped=true", __WEBPACK_OUTDATED_DEPENDENCIES__ => { /* harmony import */ _CssCenter_vue_vue_type_template_id_6b351e44_scoped_true__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./CssCenter.vue?vue&type=template&id=6b351e44&scoped=true */ "./src/pages/CssCenter.vue?vue&type=template&id=6b351e44&scoped=true");
(() => {
    api.rerender('6b351e44', _CssCenter_vue_vue_type_template_id_6b351e44_scoped_true__WEBPACK_IMPORTED_MODULE_0__.render)
  })(__WEBPACK_OUTDATED_DEPENDENCIES__); })

}

执行完module.hot.accept后,页面就渲染成最新的内容了

css的module.hot.accept是style-loader实现的

源码地址:style-loader/dist/runtime/injectStylesIntoStyleTag.js

最终实现源码地址:style-loader/dist/runtime/styleTagTransform.js

js 复制代码
/* istanbul ignore next  */
function insertStyleElement(options) {
  var element = document.createElement("style");
  options.setAttributes(element, options.attributes);
  options.insert(element, options.options);
  return element;
}
module.exports = insertStyleElement;
js 复制代码
function styleTagTransform(css, styleElement) {
  if (styleElement.styleSheet) {
    styleElement.styleSheet.cssText = css;
  } else {
    while (styleElement.firstChild) {
      styleElement.removeChild(styleElement.firstChild);
    }
    styleElement.appendChild(document.createTextNode(css));
  }
}
module.exports = styleTagTransform;

实现原理也就是创建一个style标签,再将该标签的cssText赋值新的css。

参考文档

www.51cto.com/article/658...

blog.csdn.net/qq_35094120...

相关推荐
西洼工作室13 小时前
Vue CLI为何不显示webpack配置
前端·vue.js·webpack
富贵2号2 天前
从零开始理解 Webpack:构建现代前端工程的基石
webpack
Hashan4 天前
告别混乱开发!多页面前端工程化完整方案(Webpack 配置 + 热更新)
webpack
开心不就得了5 天前
构建工具webpack
前端·webpack·rust
鲸落落丶5 天前
webpack学习
前端·学习·webpack
闲蛋小超人笑嘻嘻6 天前
前端面试十四之webpack和vite有什么区别
前端·webpack·node.js
guslegend6 天前
Webpack5 第五节
webpack
海涛高软7 天前
qt使用opencv的imread读取图像为空
qt·opencv·webpack
行者..................7 天前
手动编译 OpenCV 4.1.0 源码,生成 ARM64 动态库 (.so),然后在 Petalinux 中打包使用。
前端·webpack·node.js
千叶寻-7 天前
package.json详解
前端·vue.js·react.js·webpack·前端框架·node.js·json