webpack热更新原理详解

news2024/11/27 4:32:27

文章目录

  • 前言
  • 基础配置
    • 创建项目
    • 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.coloradmin.cn/o/1625672.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LANGUAGE-DRIVEN SEMANTIC SEGMENTATION

环境不易满足&#xff0c;不建议复现

CentOS7利用宝塔面板安装zabbix6.0.x详细安装教程(手把手图文详解版)

最近学习CentOS7安装zabbix6.0.x的版本&#xff0c;尝试了很多教程&#xff0c;很多都比较麻烦且容易出错&#xff0c;最后发现使用宝塔面板最为简单&#xff0c;将具体过程记录下来&#xff0c;一来分享给有需要的人&#xff0c;二来自己也当记录笔记&#xff0c;以免以后遗忘…

Hive中几个非常重要的问题

1、Hive 有哪些方式保存元数据&#xff0c;各有哪些优缺点 (1).DerBy数据库&#xff1a;默认自带 优点&#xff1a;使用简单&#xff0c;不需要额外的配置。 缺点&#xff1a;只有一个客户端&#xff0c;多个客户访问会报错。 (2).使用MySql数据库存储 优点&#xff1a;单独的…

OSPF的LSA详解

一、什么是LSA&#xff1f;LSA作用&#xff1f; 在OSPF协议中&#xff0c;LSA全称链路状态通告&#xff0c;主要由LSA头部信息&#xff08;LSA摘要&#xff09;和链路状态组成。部分LSA只有LSA头部信息&#xff0c;无链路状态信息。使用LSA来传递路由信息和拓扑信息&#xff0c…

linux系统安全

一、账号安全基本措施 1.1 系统账号清理 1.1.1 将用户设置为无法登录-- 改登录shell 登录shell是用户与计算机系统直接交互的接口&#xff0c;使用户能够通过命令行方式进行各种操作和管理。 所以让用户无法登录&#xff0c;其实就是将登录shell改为 /sbin/nologin,可以用us…

移动应用安全

移动应用安全 移动应用安全主要关注Android、iOS、Windows Phone等平台上移动应用软件安全状态。它涉及应用程序在其设计运行的平台上下文中的安全问题、它们使用的框架以及预期的用户集。所有主流的移动平台都提供大量可选的安全控制&#xff0c;旨在帮助软件开发人员构建安全…

面试官问如何实现二级缓存怎么进行回答以及延伸出更多知识点呢?

二级缓存的优势与缺点 优点&#xff1a; 1&#xff09;二级缓存相比只调用一层 Redis 缓存&#xff0c;访问速度更快。对于一些不经常修改的数据而查询十分频繁的可以直接放在本地缓存&#xff08;一级&#xff09;里面。 作为面试者的扩展延伸&#xff1a;我在本地缓存的实…

【MySQL 数据宝典】【索引原理】- 002 示例+逐个字段学明白 Explain

一、Explain 概述 使用 EXPLAIN 关键字可以模拟优化器来执行SQL查询语句&#xff0c;从而知道MySQL是如何处理我们的SQL语句的。分析出查询语句或是表结构的性能瓶颈。 1.1 MySQL 查询过程 通过explain我们可以获得以下信息&#xff1a; 表的读取顺序数据读取操作的操作类型…

华为ensp中链路聚合两种(lacp-static)模式配置方法

作者主页&#xff1a;点击&#xff01; ENSP专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月26日11点54分 链路聚合&#xff08;Link Aggregation&#xff09;&#xff0c;又称为端口聚合&#xff08;Port Trunking&#xff09;&#xff0c;是一种将多条物理…

OU和域用户的创建

OU和域用户的创建 导航 文章目录 OU和域用户的创建导航一、创建ou二、创建用户三、验证 一、创建ou 在服务器管理器里面点击右上角的工具,选择Active Directory 用户和计算机右击我们的域,选择新建,选择组织单位,并填入我们的单位名字 二、创建用户 右击我们刚刚新建的组织…

prompt提示词:AI英语词典优化版Pro,让AI教你学英语,通过AI实现一个网易有道英语词典

目录 一、前言二、效果对比三、优化《AI英语词典》提示词四、其他获奖作品链接 一、前言 不可思议&#xff01;我的AI有道英语字典助手竟然与百度千帆AI应用创意挑战赛K12教育主题赛榜首作品差之毫厘 &#xff0c;真的是高手都是惺惺相惜的&#xff0c;哈哈&#xff0c;自恋一…

pytest参数化数据驱动(数据库/execl/yaml)

常见的数据驱动 数据结构&#xff1a; 列表、字典、json串 文件&#xff1a; txt、csv、excel 数据库&#xff1a; 数据库链接 数据库提取 参数化&#xff1a; pytest.mark.parametrize() pytest.fixture()…

Stable Diffusion WebUI 使用 VAE 增加滤镜效果

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里&#xff0c;订阅后可阅读专栏内所有文章。 大家好&#xff0c;我是水滴~~ 本文主要介绍 VAE 模型&#xff0c;主要内容有&#xff1a;VAE 模型的概念、如果下载 VAE 模型、如何安装 VAE 模型、如…

苍穹外卖学习

并不包含全部视频内容&#xff0c;大部分都按照操作文档来手搓代码&#xff0c;资料&#xff0c;代码都上传git。 〇、实际代码 0.1 Result封装 package com.sky.result;import lombok.Data;import java.io.Serializable;/*** 后端统一返回结果* param <T>*/ Data pub…

【EP2C35F672C8 EDA试验箱下载 38译码器实现】

文章目录 前言一、实验设备1.实验箱2.下载器&#xff1a; 二、编译工程1.编译工程2.添加tcl引脚配置文件2.1将tcl文件拷贝到工程目录下&#xff1a;2.2 在软件中添加tcl文件 3.tcl文件简答讲解 三、下载四、实验结果总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内…

【LeetCode:2095. 删除链表的中间节点 + 链表】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

虚拟现实(VR)的应用场景

虚拟现实&#xff08;VR&#xff09;技术创建和体验三维虚拟世界的计算机仿真技术。用户通过佩戴VR头显等设备&#xff0c;可以完全沉浸在虚拟世界中&#xff0c;并与虚拟世界中的物体进行交互。VR技术具有广泛的应用前景&#xff0c;可以应用于各行各业。以下是一些VR的应用场…

C++ 并发编程 - 入门

目录 写在前面 并发编程&#xff0c;启动&#xff01; 写在前面 计算机的并发指在单个系统里同时执行多个独立的任务。 在过去计算机内只有一个处理器时并发是通过快速的切换进程上下文所实现的&#xff0c;而现在计算机已经步入了多核并发时代&#xff0c;所以多个进程的并…

利用STM32 HAL库实现USART串口通信,并通过printf重定向输出“Hello World“

一、开发环境 硬件&#xff1a;正点原子探索者 V3 STM32F407 开发板 单片机&#xff1a;STM32F407ZGT6 Keil版本&#xff1a;5.32 STM32CubeMX版本&#xff1a;6.9.2 STM32Cube MCU Packges版本&#xff1a;STM32F4 V1.27.1 上一篇使用STM32F407的HAL库只需1行代码实现US…

EDM营销工具效果如何评估?如何优化性能?

EDM营销工具的功能优势有哪些&#xff1f;怎么使用邮件营销工具&#xff1f; 对于许多营销人员来说&#xff0c;如何评估EDM营销工具的效果&#xff0c;却是一个颇具挑战性的问题。AokSend将围绕这一问题展开讨论&#xff0c;帮助读者更好地理解EDM营销工具效果的评估方法和技…