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...

相关推荐
friend_ship20 小时前
Vue Cli的配置中configureWebpack和chainWebpack的主要作用及区别是什么?
vue.js·webpack·chainwebpack
friend_ship20 小时前
Vite与Vue Cli的区别与详解
vue.js·webpack·rollup·vite·vue脚手架·vue cli
Man1 天前
webpack分包的几种方式和优缺点
前端·webpack
熊的猫1 天前
webpack 核心模块 — loader & plugins
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
理想不理想v2 天前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
~甲壳虫2 天前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
Beamon__2 天前
element-plus按需引入报错AutoImport is not a function
webpack·element-plus
CodeToGym2 天前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫2 天前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫2 天前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js