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.jsclass 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.jsasync 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.jscreateWebSocketServer() {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.jsasync initialize() {compilers.forEach((compiler) => {this.addAdditionalEntries(compiler);if (this.options.hot) {// Apply the HMR pluginconst 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.jsfor (const additionalEntry of additionalEntries) {new webpack.EntryPlugin(compiler.context, additionalEntry, {name: undefined,}).apply(compiler);}}
}

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

// node_modules/webpack-dev-server/lib/Server.jsasync initialize() {this.setupHooks();this.setupApp();this.setupDevMiddleware();this.createServer();
}

注册监听编译完成事件

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

// node_modules/webpack-dev-server/lib/Server.jssetupHooks() {this.compiler.hooks.done.tap("webpack-dev-server",(stats) => {...},);
}

启动服务

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

// node_modules/webpack-dev-server/lib/Server.jsconst 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 listthis.sockets.push(socket);});
}

监听文件代码变化

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

// node_modules/webpack-dev-server/lib/Server.jssetupDevMiddleware() {const webpackDevMiddleware = require("webpack-dev-middleware");// middleware for serving webpack bundlethis.middleware = webpackDevMiddleware(this.compiler,this.options.devMiddleware,);
}

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

// node_modules/webpack-dev-middleware/dist/index.jsfunction 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.jswatch(watchOptions, handler) {this.watching = new Watching(this, watchOptions, handler);return this.watching;
}

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

// node_modules/webpack/lib/Watching.jswatch(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.jssetupHooks() {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) {// 更新当前的hashthis.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.jsimport 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.jsimport 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.jsif (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.jsfunction __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.jsfunction 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, updatedModulesListreturn 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请求的urlpromises.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.jsself["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.jsfunction internalApply(options) {options = options || {};applyInvalidatedModules();// 这里的currentUpdateApplyHandlers存储的是上面jsonp请求js文件所创建的callbackvar 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" phasevar disposePromise = setStatus("dispose");results.forEach(function(result) {if (result.dispose)result.dispose();});// Now in "apply" phasevar 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 loadif (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.mzph.cn/web/3695.shtml

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

相关文章

GPU深度学习环境搭建:Win10+CUDA 11.7+Pytorch1.13.1+Anaconda3+python3.10.9

1. 查看显卡驱动及对应cuda版本关系 1.1 显卡驱动和cuda版本信息查看方法 在命令行中输入【nvidia-smi】可以当前显卡驱动版本和cuda版本。 根据显示,显卡驱动版本为:Driver Version: 516.59,CUDA 的版本为:CUDA Version 11.7。 此处我们可以根据下面的表1 显卡驱动和c…

大模型咨询培训老师叶梓:利用知识图谱和Llama-Index增强大模型应用

大模型&#xff08;LLMs&#xff09;在自然语言处理领域取得了显著成就&#xff0c;但它们有时会产生不准确或不一致的信息&#xff0c;这种现象被称为“幻觉”。为了提高LLMs的准确性和可靠性&#xff0c;可以借助外部知识源&#xff0c;如知识图谱。那么我们如何通过Llama-In…

将阿里云中数据传输到其他超算服务器

目录 方法一&#xff1a;在阿里云中连接超算&#xff0c;然后使用rsync&#xff08;速度慢&#xff09; 方法2&#xff1a;rclone(速度很快&#xff0c;100G只花了大约20min) 方法一&#xff1a;在阿里云中连接超算&#xff0c;然后使用rsync/scp&#xff08;速度慢&#xff0…

网贷大数据黑名单要多久才能变正常?

网贷大数据黑名单是指个人在网贷平台申请贷款时&#xff0c;因为信用记录较差而被列入黑名单&#xff0c;无法获得贷款或者贷款额度受到限制的情况。网贷大数据黑名单的具体时间因个人信用状况、所属平台政策以及银行审核标准不同而异&#xff0c;一般来说&#xff0c;需要一定…

mac: docker安装及其Command not found: docker

已经安装了docker desktop&#xff0c;没安装的 点击安装 傻瓜式安装即可 接着打开终端&#xff1a;好一个 Comand not found:docker 看我不把你整顿&#xff0c;解决如下&#xff1a; 如果你在 macOS 上安装了 Docker Desktop&#xff0c;但是终端无法识别 docker 命令&…

微信搜一搜优化:今天你“搜一搜”了吗?

微信“搜一搜”功能的排名规则和机制是微信生态系统中非常重要的一部分&#xff0c;它决定了小程序、公众号、文章、直播等内容在搜索结果中的展示顺序。小柚给大家整理了一份对其排名规则和机制的详细解析&#xff1a; 首先&#xff0c;关键词匹配度是影响搜索结果排名的重要…

HashMap底层实现条分缕析

目录 题外话 正题 哈希表 哈希碰撞 HashMap底层实现 小结 题外话 又水了两天,怪我,在宿舍确实没什么状态,是时候调整调整了 正题 今天直接讲解HashMap底层实现 哈希表 哈希表又称散列表 是数组和单向链表的结合体 如下图 而哈希表存放元素机制是靠哈希函数解析关键…

Jira搭建过程

看到很多小伙伴对jira有兴趣,我们今天就来分享一下jira的搭建吧 首先要明白jira是什么? 看来搭建jira也是我们测试人员需要具备的技能之一了.下面是详细的大家步骤: 1.系统环境准备 Centos 7.5 Mysql 5.6 Java1.8 2.软件安装包 atlassian-jira-software-7.13.0-x64.bin …

Linux_环境变量

目录 1、查询所有环境变量 2、常见的环境变量 2.1 PATH 2.2 HOME 2.3 PWD 3、增加新的环境变量 4、删除环境变量 5、main函数的三个形参 5.1 argv字符串数组 5.2 env字符串数组 6、系统调用接口 6.1 getenv 6.2 putenv 7、全局变量environ 结语 前言&…

SpringBoot + kotlin 协程小记

前言&#xff1a; Kotlin 协程是基于 Coroutine 实现的&#xff0c;其设计目的是简化异步编程。协程提供了一种方式&#xff0c;可以在一个线程上写起来像是在多个线程中执行。 协程的基本概念&#xff1a; 协程是轻量级的&#xff0c;不会创建新的线程。 协程会挂起当前的协…

中颖51芯片学习9. PWM(12bit脉冲宽度调制)

中颖51芯片学习9. PWM&#xff08;12bit脉冲宽度调制&#xff09; 一、资源简介二、PWM工作流程三、寄存器介绍1. PWMx控制寄存器PWMxCON2. PWM0周期寄存器PWM0PH/L3. PWM1周期寄存器PWM1PH/L4. PWM0占空比控制寄存器PWM0DH/L5. PWM1占空比控制寄存器 PWM1DH/L6. 占空比寄存器与…

跨语言指令调优深度探索

目录 I. 介绍II. 方法与数据III. 结果与讨论1. 跨语言迁移能力2. 问题的识别3. 提高跨语言表现的可能方向 IV. 结论V. 参考文献 I. 介绍 在大型语言模型的领域&#xff0c;英文数据由于其广泛的可用性和普遍性&#xff0c;经常被用作训练模型的主要语料。尽管这些模型可能在英…

ESLlint重大更新后,使用旧版ESLint搭配Prettier的配置方式

概要 就在前几天&#xff0c;ESLint迎来了一次重大更新&#xff0c;9.0.0版本&#xff0c;根据官方文档介绍&#xff0c;使用新版的先决条件是Node.js版本必须是18.18.0、20.9.0&#xff0c;或者是>21.1.0的版本&#xff0c;新版ESLint将不再直接支持以下旧版配置(非扁平化…

二、OSPF协议基础

基于SPF算法&#xff08;Dijkstra算法&#xff09;的链路状态路由协议OSPF&#xff08;Open Shortest Path First&#xff0c;开放式最短路径优先&#xff09; 目录 1.RIP在大型网络中部署所面临的问题 2.Router ID 3.OSPF的报文 4.OSPF邻居建立过程 5.OSPF报文的确认机制…

SAP的生成式AI

这是一篇openSAP中关于SAP生成式AI课程的笔记,原地址https://open.sap.com/courses/genai1/ 文章目录 Unit 1: Approaches to artificial intelligence概念三种范式监督学习非监督学习强化学习Unit 2: Introduction to generative AI生成式AI基础模型关系基础模型有哪些能力呢…

怎么通过isinstance(Obj,Class)验证?【isinstance】

最近有这样一个项目&#xff0c;这个项目可以用一个成熟的项目的构造树&#xff0c;读取树&#xff0c;再检索的过程&#xff0c;现在有新的需求&#xff0c;另一个逻辑构造同样节点结构的树&#xff0c;pickle序列化保存&#xff0c;再使用原来项目的读取、检索函数&#xff0…

一年期免费SSL证书申请方法

免费SSL证书的申请已经成为当今互联网安全实践中的重要环节&#xff0c;它不仅有助于保护网站数据传输的隐私性和完整性&#xff0c;还能提升用户信任度&#xff0c;因为现代浏览器会明确标识出未使用HTTPS&#xff08;即未部署SSL证书&#xff09;的网站为“不安全”。以下是一…

互联网安全面临的全新挑战

前言 当前移动互联网安全形势严峻&#xff0c;移动智能终端漏洞居高不下、修复缓慢&#xff0c;移动互联网恶意程序持续增长&#xff0c;同时影响个人和企业安全。与此同时&#xff0c;根据政策形势移动互联网安全监管重心从事前向事中事后转移&#xff0c;需加强网络安全态势感…

玩转必应bing国内广告投放,正确的攻略方式!

搜索引擎广告作为精准触达潜在客户的重要渠道&#xff0c;一直是众多企业营销策略中的关键一环&#xff0c;在国内市场&#xff0c;虽然百度占据主导地位&#xff0c;但必应Bing凭借其独特的用户群体、高质量的搜索体验以及与微软生态的紧密集成&#xff0c;为广告主提供了不可…

相关运算及实现

本文介绍相关运算及实现。 相关运算在相关检测及数字锁相放大中经常用到&#xff0c;其与卷积运算又有一定的联系&#xff0c;本文简要介绍其基本运算及与卷积运算的联系&#xff0c;并给出实现。 1.定义 这里以长度为N的离散时间序列x(n),y(n)为例&#xff0c;相关运算定义如…