webpack热更新原理详解

文章目录

  • 前言
  • 基础配置
    • 创建项目
    • HMR配置
  • HMR交互概览
  • HMR流程概述
  • HMR实现细节
    • 初始化
    • 注册监听编译完成事件
    • 启动服务
    • 监听文件代码变化
    • 服务端发送消息
    • 客户端收到消息
    • 热更新文件请求
    • 热更新代码替换
  • 问题思考

前言

刷新分为两种:一种是页面刷新,不保留页面状态,就是简单粗暴,直接window.location.reload();另一种只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。

Webpack热更新( Hot Module Replacement,简称 HMR,后续均以 HMR 替代),无需完全刷新整个页面的同时,更新代码变动的模块,是 Webpack 内置的最有用的功能之一。

HMR 的好处,在日常开发工作中体会颇深:节省宝贵的开发时间、提升开发体验。引用官网的描述来概述一下:

HMR 功能会在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面期间丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 在源代码中对 CSS / JS 进行修改,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

基础配置

创建项目

mkdir webpack-test && cd webpack-test // 创建文件夹并进入
npm init -y // 快速创建一个项目配置
npm i webpack webpack-dev-server webpack-cli -D // 下载开发环境依赖项
mkdir src && mkdir dist // 创建资源目录和输出目录
type nul>webpack.dev.js // 因为是在开发环境需要热更新,所以直接创建dev配置文件

当前npm包版本如下:

"devDependencies": {
  "webpack": "^5.90.3",
  "webpack-cli": "^5.1.4",
  "webpack-dev-server": "^5.0.2"
}

编写配置文件 webpack.dev.js

'use strict';

const path = require('path');

module.exports = {
    entry: './src/index.js', // 入口文件
    output: {
			path: path.resolve(__dirname, 'dist'), // 输出到哪个文件夹
			filename: 'output.js' // 输出的文件名
    },
    mode: 'development', // 开发模式
    devServer: {
      static: path.resolve(__dirname, "dist")
    }
};

新建文件 src/index.js

document.write('hello world~')

package.json添加一条命令。

  "scripts": {
    "dev": "webpack-dev-server --config webpack.dev.js"
  },

npm run dev 运行

我们看到文件已经打包完成了,但是在dist目录里并没有看到文件,这是因为WDS(webpack-dev-server)是把编译好的文件放在缓存中,没有放在磁盘上,但是我们是可以访问到的,

output.js 对应你在webpack配置文件中的输出文件,配置的是什么就访问什么

http://localhost:8080/output.js

显然我们想看效果而不是打包后的代码,所以我们在dist目录里创建一个index.html文件引入即可,

<script src="./output.js"></script>

重新访问 http://localhost:8080,内容出来了,我们接下来修改index.js文件,来看下是否可以自动刷新。

'use strict' 

document.write('hello world changed')

这确实是热更新,但是这种是每一次修改会重新刷新整个页面,大家可以打开控制台查看。WDS 提供了实时重加载的功能,但是不能局部刷新。必须配合后两步的配置才能实现局部刷新。

HMR配置

我们需要的是更新修改的模块,但是不要刷新页面。

修改 webpack.dev.js

...
module.exports = {
    ...
    devServer: {
        ...
        hot: true // 多了这一行

    },
    ...
};

重新执行 npm run dev

我们修改一下文件,形成引用关系

index.js

import { test } from './child' 

console.log('index.js文件')
test()

child.js

export function test() {
  console.log('child.js文件')
}

但是,当我们修改并保存js文件之后,页面依旧自动刷新了,这里并没有触发热模块。

所以,HMR并不像 Webpack 的其他特性一样可以开箱即用,需要有一些额外的操作。我们需要去指定哪些模块发生更新时进行HMR,如下处理:

在入口页index.js面再添加一段

...
if (module.hot) {
    module.hot.accept();
}
...

会看到修改index.js或者child.js文件,都会进行模块热更新。

也可以去指定哪些模块发生更新时进行HMR,如下代码:

if (module.hot) {
  module.hot.accept('./child', () => {
    console.log('child.js文件进行了更新')
  });
}

修改后会看到,当修改index.js文件时,会直接reload,但修改child.js文件时,会进行模块热更新。

那为什么平时修改代码的时候不用监听 module.hot.accept 也能实现热更新?那是因为我们使用的 loader 已经在幕后帮我们实现了。

HMR交互概览

我们通过观察编译及前后端的流程交互,来对热更新过程有个初步了解。

项目启动之后,会进行首次构建打包,控制台中会输出整个构建过程。

在浏览器websocket通讯中可以看到服务端告知编译后的hash值

在代码修改后,可以在控制台中观察到新生成文件,注意到新生成的文件hash值是上一次编译后告知浏览器的hash值。

  • main.86ed99e1dcba0ac82fdf.hot-update.js
  • main.86ed99e1dcba0ac82fdf.hot-update.json

这时候再去看浏览器websocket通讯,后端又告知了最新编译后的hash值。

之后前端向后端依次进行 json,js 文件的请求,文件拼接的hash值是上一次后端通知的值。

而最新告知的hash值留待下次进行文件请求进行hash拼接。

点开查看 main.hash.hot-update.json 请求,返回的结果中,c(main) 表示当前要热更新的文件名是 main。m(remove)表示移除的文件(包含路径)。

查看 main.hash.hot-update.js,返回的内容是使用 webpackHotUpdate+当前的项目名(webpack_test) 标识的 main 内容。

如果没有任何改动,对 child.js 文件直接保存,控制台输出编译打包信息,并没有生成新的文件和hash值。

控制台输出如下

assets by status 261 KiB [cached] 1 asset
cached modules 173 KiB (javascript) 27.4 KiB (runtime) [cached] 39 modules
./src/child.js 58 bytes [built]
webpack 5.90.3 compiled successfully in 126 ms

websocket通讯如下:

HMR流程概述

接下来我们开始从源码角度,简述 HMR 实现热更新的过程。

上图是 webpack 配合 webpack-dev-server 进行应用开发的模块热更新流程图。

  • 上图底部红色框内是服务端,而上面的橙色框内是浏览器端。
  • 绿色填充的方框是 webpack 代码控制的区域。深蓝色填充的方框是 webpack-dev-server 代码控制的区域,洋红色填充的方框是文件系统,文件修改后的变化就发生在这,而青色填充的方框是应用本身。

上图显示了我们修改代码到模块热更新完成的一个周期,通过深绿色的阿拉伯数字符号已经将 HMR 的整个过程标识了出来。

  1. 在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包。

  2. webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 webpack-dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack 将代码打包到内存中。

  3. webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了 static 属性时,webpack-dev-server 会监听这些文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 window.location.reload()。注意,这儿是浏览器刷新,和 HMR 是两个概念。

  4. webpack-dev-server 代码的工作,主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端 webpack-dev-server/client 和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。

  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更新模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 webpack-dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。

  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 webpack_require.hmrM 向 server 端发送 fetch 请求,服务端返回一个 json,该 json 包含模块变更的信息的 json 文件,模块名与 hash进行组合获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。

  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModuleReplacement.runtime 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。最后一步,当 HMR 失败后,回退到 window.location.reload() 操作,也就是进行浏览器刷新来获取最新打包代码。

HMR实现细节

下面我们通过分析源代码,来对热更新过程进行更深一层了解,此次分析具体实现时仅关注核心代码实现。

初始化

热更新开始,new Server() 后会直接调用 server.start()。

// node_modules/webpack-dev-server/lib/Server.js

class Server {
	async start() {
		await this.normalizeOptions();
		await this.initialize();

		if (this.options.webSocketServer) {
			this.createWebSocketServer();
		}
	}
}

可以看到在 start 方法中,即开始进行webSocket服务的初始化。

normalizeOptions 方法构造 webSocket 请求地址。最终得到的结果为:‘protocol=ws:&hostname=0.0.0.0&port=9000&pathname=/ws’

// node_modules/webpack-dev-server/lib/Server.js

async normalizeOptions() {
	const { options } = this;
	options.client.webSocketURL = {
		protocol: parsedURL.protocol,
		hostname: parsedURL.hostname,
		port: parsedURL.port.length > 0 ? Number(parsedURL.port) : "",
		pathname: parsedURL.pathname,
		username: parsedURL.username,
		password: parsedURL.password,
	};


	const defaultWebSocketServerOptions = { path: "/ws" };

	if (typeof options.webSocketServer === "undefined") {
		options.webSocketServer = {
			type: defaultWebSocketServerType,
			options: defaultWebSocketServerOptions,
		};
	}
}

执行 createWebSocketServer 方法,创建websocket服务

// node_modules/webpack-dev-server/lib/Server.js

createWebSocketServer() {
	this.webSocketServer = new (this.getServerTransport())(this); // this.webSocketServer = new WebsocketServer(this);

	if (this.options.hot === true || this.options.hot === "only") {
		this.sendMessage([client], "hot");
	}
	if (this.options.liveReload) {
		this.sendMessage([client], "liveReload");
	}
	this.sendStats([client], this.getStats(this.stats), true);
}

getServerTransport() {
	let implementation;
	if (this.options.webSocketServer.type === "ws") {
		implementation = require("./servers/WebsocketServer");
	}
	return implementation;
}

start 函数中,还有 initialize 方法没有看,这个函数中首先执行 addAdditionalEntries 方法,进行客户端的初始化。添加 node_modules/webpack-dev-server/client/index.js 和 node_modules/webpack/hot/dev-server.js 到入口文件中。

// node_modules/webpack-dev-server/lib/Server.js

async initialize() {
	compilers.forEach((compiler) => {
		this.addAdditionalEntries(compiler);

		if (this.options.hot) {
			// Apply the HMR plugin
			const plugin = new webpack.HotModuleReplacementPlugin();
			plugin.apply(compiler);
		}
	});
}

addAdditionalEntries(compiler) {
	let additionalEntries = [];
	if (this.options.webSocketServer) {
		additionalEntries.push(
			`${require.resolve("../client/index.js")}?${webSocketURLStr}`
		);
	}

	if (this.options.hot === "only") {
		additionalEntries.push(require.resolve("webpack/hot/only-dev-server"));
	} else if (this.options.hot) {
		additionalEntries.push(require.resolve("webpack/hot/dev-server"));
	}

	if (typeof webpack.EntryPlugin !== "undefined") {
		// node_modules/webpack-dev-server/client/index.js?protocol=ws%3A&hostname=0.0.0.0&port=9000&pathname=%2Fws&logging=info&overlay=true&reconnect=10&hot=true&live-reload=true
		// node_modules/webpack/hot/dev-server.js
		for (const additionalEntry of additionalEntries) {
			new webpack.EntryPlugin(compiler.context, additionalEntry, {
					name: undefined,
			}).apply(compiler);
		}
	}
}

initialize 中还有如下函数,接下来我们对重点进行介绍。

// node_modules/webpack-dev-server/lib/Server.js

async initialize() {
	this.setupHooks();
	this.setupApp();
	this.setupDevMiddleware();
	this.createServer();
}

注册监听编译完成事件

首先执行的是 setupHooks 方法来注册监听事件的,监听每次 webpack 编译完成,该方式利用的是 webpack 的 done 钩子。

// node_modules/webpack-dev-server/lib/Server.js

setupHooks() {
	this.compiler.hooks.done.tap(
		"webpack-dev-server",
		(stats) => {
			...
		},
	);
}

启动服务

接着执行 setupApp 方法,启动node静态资源服务,可以让浏览器可以请求本地的静态资源。

// node_modules/webpack-dev-server/lib/Server.js

const getExpress = memoize(() => require("express"));
setupApp() {
	this.app = new getExpress();
}

在 initialize 方法的最后,执行了 createServer 方法

createServer() {
		this.server = require("http").createServer(
				options,
				this.app
		);

		this.server.on("connection", (socket) => {
				// Add socket to list
				this.sockets.push(socket);
		});
}

监听文件代码变化

每次修改代码,就会触发编译。说明我们还需要监听本地代码的变化,这里主要是通过 setupDevMiddleware 方法实现的。

// node_modules/webpack-dev-server/lib/Server.js

setupDevMiddleware() {
	const webpackDevMiddleware = require("webpack-dev-middleware");

	// middleware for serving webpack bundle
	this.middleware = webpackDevMiddleware(
		this.compiler,
		this.options.devMiddleware,
	);
}

webpack-dev-middleware 内置于 webpack-dev-server,主要是用于监测代码文件变化,处理文件编译等流程。那我们来看下 webpack-dev-middleware 源码里做了什么事。

// node_modules/webpack-dev-middleware/dist/index.js

function wdm() {
	const context = { compiler };

	// 若writeToDisk配置项为true,则打包到磁盘
	if (options.writeToDisk) {
		setupWriteToDisk(context);
	}

	// 打包到内存(通过memfs)
	setupOutputFileSystem(context); 

	// 开始监听
	context.compiler.watch(watchOptions, errorHandler);
}

当 writeToDisk 进行了配置,则进行编译,并将编译后的文件输出到磁盘。

执行 setupOutputFileSystem 方法,这个方法主要目的就是将编译后的文件打包到内存。这就是为什么在开发的过程中,你会发现 dist 目录没有打包后的代码,因为都在内存中。原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于memfs。

为什么代码的改动保存会自动编译,重新打包?这一系列的重新检测编译就归功于 compiler.watch 这个方法了,该方法开启对本地文件的监听,当文件发生变化,重新编译,编译完成之后继续监听。监听本地文件的变化主要是通过文件的生成时间是否有变化。

每个打包的文件作为一个简单的 javascript 对象保存在了内存中,当浏览器请求该文件时,上一步开启的静态资源服务直接去内存中找保存的 javascript 对象返回给浏览器端。

我们可以继续深入了解,compiler 中 watch 的具体实现

// node_modules/webpack/lib/Compiler.js

watch(watchOptions, handler) {
   this.watching = new Watching(this, watchOptions, handler);
   return this.watching;
}

第一次会主动触发this._go()进行编译

// node_modules/webpack/lib/Watching.js

watch(files, dirs, missing) {
 this.watcher = this.compiler.watchFileSystem.watch(...args, () => {
     this._invalidate(
         fileTimeInfoEntries,
         contextTimeInfoEntries,
         changedFiles,
         removedFiles
     );
     this._onChange();
 });
}

_invalidate() {
 this._go(...args);
}

// Watching.js的constructor()->_invalidate()->_go()
_go(fileTimeInfoEntries, contextTimeInfoEntries, changedFiles, removedFiles) {
   const run = () => {
       this.compiler.compile(onCompiled);
   };
   run();
}

执行编译

// node_modules/webpack/lib/Compiler.js
compile(callback) {
   this.hooks.make.callAsync(compilation, err => {});
}

每次编译结束时注册监听

// node_modules/webpack/lib/Watching.js

_done(err, compilation) {
 this.watch(
     compilation.fileDependencies,
     compilation.contextDependencies,
     compilation.missingDependencies
 );
}

服务端发送消息

在热更新开始时,代码中执行了注册监听事件的逻辑。监听的完整实现如下。

// node_modules/webpack-dev-server/lib/Server.js

setupHooks() {
	this.compiler.hooks.done.tap(
		"webpack-dev-server",
		(stats) => {
			if (this.webSocketServer) {
				this.sendStats(this.webSocketServer.clients, this.getStats(stats));
			}
			this.stats = stats;
		},
	);
}

当监听到webpack编译结束,就会调用 sendStats 方法。

// node_modules/webpack-dev-server/lib/Server.js

// Send stats to a socket or multiple sockets
sendStats(clients, stats, force) {
	// 更新当前的hash
	this.currentHash = stats.hash;
	// 发送给客户端当前的hash值
	this.sendMessage(clients, "hash", stats.hash);
	// 发送给客户端ok的指令
	this.sendMessage(clients, "ok");
}

通过 websocket 给浏览器发送通知,ok 和 hash 事件,这样浏览器就可以拿到最新的 hash 值了,做检查更新逻辑。

客户端收到消息

那客户端和服务端如何通讯的呢?打开浏览器开发者调试工具,可以看到在 webpack 打包好的 output.js 中包含了以下代码。

__webpack_require__("./node_modules/webpack-dev-server/client/index.js?protocol=ws%3A&hostname=0.0.0.0&port=8080&pathname=%2Fws&logging=info&overlay=true&reconnect=10&hot=true&live-reload=true");

在上文介绍过初始化过程会中注入 webpack-dev-server/client/index.js 和 webpack/hot/dev-server.js 到入口文件中。

  • webpack-dev-server/client/index.js

    首先这个文件用于 websocket 的。我们在 webpack-dev-server 初始化的过程中,启动的是本地服务端的 websocket。那客户端也就是我们的浏览器,浏览器还没有和服务端通信的代码呢?因此我们需要把websocket客户端通信代码偷偷塞到我们的代码中。

  • webpack/hot/dev-server.js

    这个文件主要是用于检查更新逻辑的。

下面重点讲的就是 sendStats 方法中的 ok 和 hash 事件都做了什么。

// node_modules/webpack-dev-server/client/index.js

import reloadApp from "./utils/reloadApp.js";

var onSocketMessage = {
  hash: function hash(_hash) {
    status.previousHash = status.currentHash;
    status.currentHash = _hash;
  },
  ok: function ok() {
    sendMessage("Ok");
    reloadApp(options, status);
  },
};

var socketURL = createSocketURL(parsedResourceQuery);
socket(socketURL, onSocketMessage, options.reconnect);

webpack-dev-server/client/index.js 当接收到 hash 消息后会将 hash 值暂存到 currentHash 变量,当接收到 ok 的消息后执行 reloadApp 方法。且 hash 消息是在 ok 消息之前。

热更新检查事件是调用reloadApp方法。

// node_modules/webpack-dev-server/client/utils/reloadApp.js

import hotEmitter from "webpack/hot/emitter.js";
function reloadApp(_ref, status) {
 function applyReload(rootWindow, intervalId) {
     rootWindow.location.reload();
 }

 var search = self.location.search.toLowerCase();
 var allowToHot = search.indexOf("webpack-dev-server-hot=false") === -1;
 var allowToLiveReload = search.indexOf("webpack-dev-server-live-reload=false") === -1;

 if (hot && allowToHot) {
     hotEmitter.emit("webpackHotUpdate", status.currentHash);
 }
 else if (liveReload && allowToLiveReload) {
     // 根据条件判断执行applyReload()方法
 }
}

如果配置了模块热更新,则执行 hotEmitter.emit(“webpackHotUpdate”, status.currentHash) 将最新 hash 值发送给 webpack,然后将控制权交给 webpack 客户端代码。如果没有配置模块热更新,就直接调用 location.reload 方法刷新页面。

比较奇怪的是,这个方法利用 node.js 的 EventEmitter,发出webpackHotUpdate 消息。

// node_modules/webpack/hot/emitter.js
var EventEmitter = require("events");
module.exports = new EventEmitter();

这是为什么?为什么不直接进行检查更新呢?

个人理解就是为了更好的维护代码,以及职责划分的更明确。websocket 仅仅用于客户端和服务端进行通信。而真正做事情的活还是交回给了webpack。即 webpack/hot/dev-server.js 监听 webpack-dev-server/client/index.js 发送的 webpackHotUpdate 消息。

webpack/hot/dev-server.js 监听到 webpackHotUpdate 的消息后,获取到最新的hash值,然后进行检查更新了,调用 module.hot.check 方法。

module.hot.check(true) 触发,然后判断是否需要重启。

// node_modules/webpack/hot/dev-server.js

if (module.hot) {
	var lastHash;
	var check = function check() {
		module.hot
			.check(true)
			.then(function (updatedModules) {
				if (!updatedModules) {
					// 容错,直接刷新页面
					if (typeof window !== "undefined") {
						window.location.reload();
					}
					return;
				}
			})
	};
	var hotEmitter = require("./emitter");
	hotEmitter.on("webpackHotUpdate", function (currentHash) {
		lastHash = currentHash;
	});

	check();
}

问题又来了,module.hot.check 又是哪里冒出来了的!可以通过阅读下面的说明得到答案。

在编译形成最终代码时,会注入 HotModuleReplacement.runtime.js 代码,拦截require,进行 createRequire 和 createModuleHotObject。

  • createRequire

    构建当前 request 的 parent 和 children,本质是在 require 的基础上保存各个模块之间的依赖关系,为后面的热更新做准备,因为一个文件的更新必定涉及到另外依赖模块的相关更新。

  • createModuleHotObject

    构建当前 module 的 hotAPI,后面的热更新都需要通过 hotCheck 和 hotApply 进行操作。

// node_modules/webpack/lib/hmr/HotModuleReplacement.runtime.js

function __webpack_require__(moduleId) {
   var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: __webpack_require__ };
   __webpack_require__.i.forEach(function (handler) { handler(execOptions); });

   return module.exports;
}


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


function createRequire(require, moduleId) {
   var me = installedModules[moduleId];
   var fn = function (request) {
       if (me.hot.active) {
           if (installedModules[request]) {
               var parents = installedModules[request].parents;
               if (parents.indexOf(moduleId) === -1) {
                   parents.push(moduleId);
               }
           } else {
               currentParents = [moduleId];
               currentChildModule = request;
           }
           if (me.children.indexOf(request) === -1) {
               me.children.push(request);
           }
       } else {
           currentParents = [];
       }
       return require(request);
   };
   return fn;
}

function createModuleHotObject(moduleId, me) {
 var hot = {
			active: true,
			accept: function (dep, callback, errorHandler) {
     },
     check: hotCheck,
     apply: hotApply,
     data: currentModuleData[moduleId]
 };
 currentChildModule = undefined;
 return hot;
}

module.hot.check 最终会触发 hotCheck() 方法。

热更新文件请求

进入 HotCheck 方法,利用上一次保存的 hash 值,调用 __webpack_require__.hmrM 发送获取 app.hash.hot-update.json 的 fetch 请求,得到 update = {c:[“main”], m:[], r:[]} 的更新内容。

// node_modules/webpack/lib/hmr/HotModuleReplacement.runtime.js

function hotCheck(applyOnUpdate) {
   return setStatus("check")
       .then(__webpack_require__.hmrM) // 为fetch("http://localhost:8080/main.fc1c69066ce336693703.hot-update.json")
       .then(function (update) {
          // update = {c:["main"], m:[], r:[]} 更新内容

           return setStatus("prepare").then(function () {
               var updatedModules = [];
               currentUpdateApplyHandlers = [];

               return Promise.all(
                   Object.keys(__webpack_require__.hmrC).reduce(function (
                       promises,
                       key
                   ) {
                       // key=jsonp
                       // __webpack_require__.hmrC[key](
                       //     update.c,
                       //     update.r,
                       //     update.m,
                       //     promises,
                       //     currentUpdateApplyHandlers,
                       //     updatedModules
                       // ); ===> 转化为jsonp,便于理解
                       __webpack_require__.hmrC.jsonp(update.c, update.r, update.m, promises, currentUpdateApplyHandlers, updatedModules);
                       // chunkIds, removedChunks, removedModules, promises, applyHandlers, updatedModulesList
                       return promises;
                   },
                       [])
               ).then(function () {
                   return waitForBlockingPromises(function () { // 等待所有的promise更新完成
                       if (applyOnUpdate) {
                           // hotCheck(true)
                           return internalApply(applyOnUpdate);
                       } else {
                           return setStatus("ready").then(function () {
                               return updatedModules;
                           });
                       }
                   });
               });
           });
       });
}

__webpack_require__.hmrM = () => {
 if (typeof fetch === "undefined") throw new Error("No browser support: need fetch API");

 // 保留的是client客户端的域名:
 __webpack_require__.p = "http://localhost:8080/"

 // 保留的是上一次的hash值:
 __webpack_require__.h = () => ("fc1c69066ce336693703")

  __webpack_require__.hmrF = () => ("main." + __webpack_require__.h() + ".hot-update.json"); 
 // fetch("http://localhost:8080/main.fc1c69066ce336693703.hot-update.json")
 return fetch(__webpack_require__.p + __webpack_require__.hmrF()).then((response) => {
     return response.json();
 });
};

用上一步获取到的 app.hash.hot-update.json 请求结果来进一步来获取热更新js模块,触发 __webpack_require__.hmrC.jsonp() 通过 JSONP 方式请求 app.hash.hot-update.js,并进入热更新准备阶段。

// node_modules/webpack/lib/hmr/HotModuleReplacement.runtime.js

// $hmrDownloadUpdateHandlers$[key] => runtime转化为:
__webpack_require__.hmrC.jsonp = function (chunkIds, ...) {
 applyHandlers.push(applyHandler);

 chunkIds.forEach(function (chunkId) {
		// 拼接jsonp请求的url
		promises.push($loadUpdateChunk$(chunkId, updatedModulesList));
 });
};

// 拼接jsonp请求的url
var waitingUpdateResolves = {};
function loadUpdateChunk(chunkId, updatedModulesList) {
 return new Promise((resolve, reject) => {

   waitingUpdateResolves[chunkId] = resolve;
   
     __webpack_require__.hu = "" + chunkId + "." + __webpack_require__.h() + ".hot-update.js";
     var url = __webpack_require__.p + __webpack_require__.hu(chunkId);
     __webpack_require__.l(url, loadingEnded);
 });
}

// document.body.appendChild(new Script()),正式发起get请求(jsonp请求)
var inProgress = {};
__webpack_require__.l = (url, done, key, chunkId) => {
 inProgress[url] = [done];
 var onScriptComplete = (prev, event) => {
     var doneFns = inProgress[url];
     delete inProgress[url];
     script.parentNode && script.parentNode.removeChild(script);
     doneFns && doneFns.forEach((fn) => (fn(event)));
 };
 script.onload = onScriptComplete.bind(null, script.onload);
 needAttach && document.head.appendChild(script);
};

创建 http://localhost:8080/main.f1bcf354bbddd26daa90.hot-update.js 的 promise 请求,并且加入到 promise 数组中。

这里要解释下为什么使用 JSONP 获取最新代码?主要是因为JSONP获取的代码可以直接执行进行更新。为什么要直接执行?我们来回忆下app.hash.hot-update.js的代码格式是怎么样的。

可以发现,新编译后的代码是在一个webpackHotUpdate函数体内部的。也就是要立即执行 webpackHotUpdate 这个方法。

output.js 在 window 对象上定义了 webpackHotUpdate+当前的项目名(webpack_test) 方法;在这里定义了如何解析前面 app.hash.hot-update.js 请求返回的js内容。 webpackHotUpdate+当前的项目名(webpack_test)(chunkId, moreModules, runtime),直接遍历 moreModules,并且执行更新。

// app.hash.hot-update.js

self["webpackHotUpdate"] = (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;
	}
}

在js文件立即执行对应的 module 代码的缓存并且触发对应 promise 的 resolve 请求,从而顺利回调 internalApply() 方法

热更新代码替换

最终会调用 module.hot.apply 内部方法 internalApply 进行代码替换。

// node_modules/webpack/lib/hmr/HotModuleReplacement.runtime.js

function internalApply(options) {
		options = options || {};
		applyInvalidatedModules();
		// 这里的currentUpdateApplyHandlers存储的是上面jsonp请求js文件所创建的callback
		var results = currentUpdateApplyHandlers.map(function(handler) {
				return handler(options);
		});
		currentUpdateApplyHandlers = undefined;
		var errors =
		.map(function(r) {
				return r.error;
	
		.filter(Boolean);
		if (errors.length > 0) {
				return setStatus("abort").then(function() {
						throw errors[0];
				});
		}
		// Now in "dispose" phase
		var disposePromise = setStatus("dispose");
		results.forEach(function(result) {
				if (result.dispose)
						result.dispose();
		});
		// Now in "apply" phase
		var applyPromise = setStatus("apply");
		var error;
		var reportError = function(err) {
				if (!error)
						error = err;
		};
		var outdatedModules = [];
		results.forEach(function(result) {
				if (result.apply) {
						// 这里的result的是上面jsonp请求js文件所创建的callback所返回Object的apply方法
						var modules = result.apply(reportError);
						if (modules) {
								for (var i = 0; i < modules.length; i++) {
										outdatedModules.push(modules[i]);
								}
						}
				}
		});
		return Promise.all([disposePromise, applyPromise]).then(function() {
				// handle errors in accept handlers and self accepted module load
				if (error) {
						return setStatus("fail").then(function() {
								throw error;
						});
				}
				if (queuedInvalidatedModules) {
						return internalApply(options).then(function(list) {
								outdatedModules.forEach(function(moduleId) {
										if (list.indexOf(moduleId) < 0)
												list.push(moduleId);
								});
								return list;
						});
				}
				return setStatus("idle").then(function() {
						return outdatedModules;
				});
		});
}

问题思考

  1. HMR是怎样实现自动编译的?

    webpack通过watch可以监听文件的变化进行文件编译。

  2. 开发的过程中,并没有在 dist 目录中找到 webpack 打包好的文件,它们去哪呢?

    webpack 编译后,webpack-dev-middleware 通过 memfs 将文件打包到了内存中,不生成文件的原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销。

  3. 编译后新产生的两个文件又是干嘛的?

    main.hash.hot-update.json

    告知哪些chunk发生了改变,以及移除哪些chunk

    main.hash.hot-update.js

    告知浏览器,main 代码块中的./src/xxx.js模块变更的内容

    首先是通过fetch的方式,利用上一次保存的hash值请求hot-update.json文件。这个描述文件的作用就是提供了修改的文件所在的chunkId。

    然后通过JSONP的方式,利用hot-update.json返回的chunkId及上一次保存的hash 拼接文件名进而获取文件内容。

  4. 模块内容的变更浏览器又是如何感知的?

    webpack-dev-middleware利用sockjs和webpack-dev-server/client建立webSocket长连接。将webpack的编译编译打包的各个阶段告诉浏览器端。主要告诉新模块hash的变化,但是webpack-dev-server/client是无法获取更新的代码的,通过webpack/hot/server获取更新的模块,然后HMR对比更新模块和模块的依赖。

  5. webpack-dev-server 依赖 webpack-dev-middleware 库,那么 webpack-dev-middleware 在 HMR 过程中扮演什么角色?

    webpack-dev-middleware扮演是中间件的角色,一头可以调用webpack暴露的API检测代码的变化,一头可以通过sockjs和webpack-dev-server/client建立webSocket长连接,将webapck打包编译的各个阶段发送给浏览器端。

  6. 怎么实现局部更新的?

    当hot-update.js文件加载好后,就会执行window.webpackHotUpdate,进而调用了hotApply。hotApply根据模块ID找到旧模块然后将它删除,然后执行父模块中注册的accept回调,从而实现模块内容的局部更新。

  7. 使用 HMR 的过程中,通过 Chrome 开发者工具我们知道浏览器是通过 websocket 和 webpack-dev-server 进行通信的,但是 websocket 的 message 中并没有发现新模块代码。打包后的新模块又是通过什么方式发送到浏览器端的呢?为什么新的模块不通过 websocket 随消息一起发送到浏览器端呢?

    功能块的解耦,各个模块各司其职,webpack-dev-server/client 只负责消息的传递而不负责新模块的获取,而这些工作应该有 HMR runtime 来完成,HMR runtime 才应该是获取新代码的地方。

  8. 当模块的热替换过程中,如果替换模块失败,有什么回退机制吗?

    模块热更新的错误处理,如果在热更新过程中出现错误,热更新将回退到刷新浏览器

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/576285.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

品鉴中的文化传承:如何理解红酒在历史与文化中的地位

红酒不仅是产品&#xff0c;更是一种文化和历史的传承。在品鉴雷盛红酒的过程中&#xff0c;了解红酒背后的历史和文化&#xff0c;能够更好地理解其风格和特点&#xff0c;提升品鉴体验。 红酒的历史可以追溯到公元前6000年左右的古埃及时期。自那时起&#xff0c;红酒就成为了…

新手Pytorch入门笔记-transforms.Compose()

我使用的图片是上图&#xff0c;直接下载即可 transforms.Compose 是PyTorch中的一个实用工具&#xff0c;用于创建一个包含多个数据变换操作的变换对象。这些变换操作通常用于数据预处理&#xff0c;例如图像数据的缩放、裁剪、旋转等。使用transforms.Compose 可以将多个数据…

Linux系统编程---线程同步

一、同步概念 同步即协同步调&#xff0c;按预定的先后次序运行。 协同步调&#xff0c;对公共区域数据【按序】访问&#xff0c;防止数据混乱&#xff0c;产生与时间有关的错误。 数据混乱的原因&#xff1a; 资源共享(独享资源则不会)调度随机(意味着数据访问会出现竞争)线…

监控员工上网用什么软件比较好 八款电脑监控神器送给你

监控员工上网用什么软件比较好 八款电脑监控神器送给你 监控员工上网行为的软件有多种&#xff0c;每款软件都有其独特的功能和优势。现在让我们一起来探寻最佳员工上网监控神器&#xff01; 想知道哪款电脑监控软件最炫酷、最实用吗&#xff1f;来看看这里&#xff0c;为你揭…

36.WEB渗透测试-信息收集-企业信息收集(3)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;35.WEB渗透测试-信息收集-企业信息收集&#xff08;2&#xff09; 重要信息收集&#xf…

Python 中的递归排列

在 Python 中使用递归计算排列,适合绝对初学者 介绍 有些人发现很难理解递归算法。 这个技巧向绝对初学者展示了如何使用递归查找排列。Python 背景 这个技巧的想法来自一个问答问题:可怜的 OP 花了三天时间“翻头”,试图弄清楚一小段代码如何能够生成输入列表项的所有排列。…

ROS_第一个程序_Hello_world

ROS的第一个项目&#xff1a;输出Hello World 我们将学习如何创建一个简单的ROS&#xff08;Robot Operating System&#xff09;项目&#xff0c;该项目将在终端中输出"Hello World"。我们将使用Python语言进行编程。 环境准备 首先&#xff0c;确保你的计算机已…

【目标检测】基于深度学习的布匹表面缺陷检测(yolov5算法,4类,附代码和数据集)

写在前面: 首先感谢兄弟们的关注和订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。(专栏订阅用户订阅专栏后免费提供数据集和源码一份,超级VIP用户不在服务范围之内) 路虽远,行则将至;事虽难,做…

硬件24、嘉立创EDA丝印的优化和调整

1、调整全部丝印的属性 先选中一个丝印&#xff0c;然后右键点击它&#xff0c;选择查找&#xff0c;然后选择查找全部 选择查找全部这个时候可以设置所有丝印在元件的位置了&#xff0c;布局-》属性位置&#xff0c;位号&#xff0c;属性位置设置为上边&#xff0c;这时丝印就…

全志ARM-网络链接

命令扫描周围的WIFI热点 nmcli dev wifi 命令接入网络 nmcli dev wifi connect &#xff08;WiFi名&#xff0c;不要有空格&#xff09;password (WiFi密码) 查看IP地址 ip addr show wlan0或ifconfig 出现successfully就连接成功了

计应2班01

public class Demo {public void sum(double num1 , double num2){System.out.println(num1 num2);} }import org.junit.Test;public class Test1 { // 定义方法 // test sum // testSum // public void // TestTestpublic void testSum(){Demo de…

如何通过文件下发平台,让数据发挥其真正的价值?

银行网点文件下发平台是专门设计用于银行系统内部或与外部机构之间安全、高效地传输和分发文件的系统。目前使用较多的方式是FTP、邮件、物理媒介等&#xff0c;但都存在一定问题&#xff1a; 1、物理媒介&#xff1a;如U盘、光盘等&#xff0c;通过快递服务发送给分支机构&…

面向对象设计与分析(42)工厂方法模式

文章目录 定义示例实际应用 定义 工厂方法模式&#xff0c;定义一个用于创建对象的接口&#xff08;工厂方法&#xff09;&#xff0c;返回对象基类&#xff0c;让子类去实现该接口&#xff0c;从而返回具体的子类对象。 结构 工厂方法模式包含以下主要角色&#xff1a; 抽象…

观成科技:蔓灵花组织加密通信研究分析总结

1.概述 蔓灵花&#xff0c;又名"Bitter"&#xff0c;常对南亚周边及孟加拉湾海域的相关国家发起网络攻击&#xff0c;主要针对巴基斯坦和中国两国。其攻击目标主要包括政府部门、核工业、能源、国防、军工、船舶工业、航空工业以及海运等行业&#xff0c;其主要意图…

【学习笔记】Python 使用 matplotlib 画图

文章目录 安装中文显示折线图、点线图柱状图、堆积柱状图坐标轴断点参考资料 本文将介绍如何使用 Python 的 matplotlib 库画图&#xff0c;记录一些常用的画图 demo 代码 安装 # 建议先切换到虚拟环境中 pip install matplotlib中文显示 新版的 matplotlib 已经支持字体回退…

Django框架之python后端框架介绍

一、网络框架及MVC、MTV模型 1、网络框架 网络框架&#xff08;Web framework&#xff09;是一种软件框架&#xff0c;用于帮助开发人员构建Web应用程序和Web服务。它提供了一系列预先编写好的代码和工具&#xff0c;以简化开发过程并提高开发效率。网络框架通常包括以下功能…

go语言并发实战——日志收集系统(十) 重构tailfile模块实现同时监控多个日志文件

前言 在上一篇文章中&#xff0c;我们实现了通过etcd来同时指定多个不同的有关分区与日志文件的路径&#xff0c;但是锁着一次读取配置的增多&#xff0c;不可避免的出现了一个问题&#xff1a;我们如何来监控多个日志文件&#xff0c;这样原来的tailFile模块相对于当下场景就…

【JavaScript】内置对象 ④ ( Math 内置对象常用方法 | 取绝对值 | 向下取整 | 向上取整 | 四舍五入取整 | 取随机数 )

文章目录 一、Math 内置对象常用方法1、计算绝对值 - Math.abs2、取整计算 - Math.floor 向下取整 / Math.ceil 向上取整 / Math.round 四舍五入3、随机数 - Math.random4、代码示例 - 猜随机数 一、Math 内置对象常用方法 1、计算绝对值 - Math.abs 向 Math.abs() 方法中 传入…

简单的jmeter脚本自动化

1、创建线程组&#xff0c;定义自定义变量&#xff0c;保存请求默认值 2、用csv编写测试用例 备注&#xff1a;如果单元格内本身就有引号&#xff0c;则格式会有点小问题&#xff0c;不能直接修改为csv 用txt打开后 有引号的需要在最外层多包一层引号&#xff0c;每个引号前…

LM1875L-TB5-T 音频功率放大器 PDF中文资料_参数_引脚图

LM1875L-TB5-T 规格信息&#xff1a; 商品类型音频功率放大器 音频功率放大器的类型- 输出类型1-Channel (Mono) 作业电压16V ~ 60V 输出功率25W x 1 4Ω 额外特性过流保护,热保护 UTC LM1875是一款单片功率放大器&#xff0c;可为消费类音频应 用提供极低失真和高品质的…
最新文章