webpack中的HMR(热更新)原理剖析

article/2025/10/7 14:31:42

简介

Hot Module Replacement(以下简称 HMR)是 webpack 发展至今引入的最令人兴奋的特性之一 ,当你对代码进行修改并保存后,webpack 将对代码重新打包,并将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样在不刷新浏览器的前提下就能够对应用进行更新。

基本实现原理大致这样的,构建 bundle 的时候,加入一段 HMR runtime 的 js 和一段和服务沟通的 js 。文件修改会触发 webpack 重新构建,服务器通过向浏览器发送更新消息,浏览器通过 jsonp 拉取更新的模块文件,jsonp 回调触发模块热替换逻辑。

热更新配置

  • 使用webpack-dev-server,设置 hot 属性为 true.写模块时,按照以下写法:

if (module.hot) { //判断是否有热加载module.hot.accept('./hmrTest.js', function() { //热加载的模块路径console.log('Accepting the updated printMe module!'); //热加载的回调,即发生了模块更新时,执行什么 callbackprintMe();})}

缺点:更新逻辑得自己写。比如要使页面显示的内容生效,需要在回调中写入document.append(xxx)

  • react 的热加载,使用 react-hot-loader

import { hot } from'react-hot-loader';const Record = ()=>{...}exportdefault hot(module)(Record);或if (module.hot) {module.hot.accept('./App', function () {var NextApp = require('./App')ReactDOM.render(<NextApp />, rootEl)})}

实现过程

  1. watch 编译过程、devServer 推送更新消息到浏览器

  2. 浏览器接收到服务端消息做出响应

  3. 对模块进行热更新或刷新页面

watch 编译过程、devServer 推送更新消息到浏览器

  1. webpack-dev-server 里引用了 webpack-dev-middleware,相关的 watch 逻辑就是在里面实现的。

//webpack-dev-server/lib/Server.jssetupDevMiddleware() {this.middleware = webpackDevMiddleware(this.compiler,Object.assign({}, this.options, { logLevel: this.log.options.level }));}// webpack-dev-middleware/index.jsif (!options.lazy) {context.watching = compiler.watch(options.watchOptions, (err) => {...});}

以上代码可以看出,webpack-dev-middleware 是通过调用 webpack 的 api 对文件系统 watch 的。watchOptions 如果没有配置的话,会取默认值。值的含义见:https://webpack.js.org/configuration/watch/

  1. 当文件发生变化时,重新编译输出 bundle.js。devServer 下,是没有文件会输出到 output.path 目录下的,这时 webpack 是把文件输出到了内存中。webpack 中使用的操作内存的库是 memory-fs,它是 NodeJS 原生 fs 模块内存版(in-memory)的完整功能实现,会将你请求的url映射到对应的内存区域当中,因此读写都比较快。

// webpack-dev-middleware/lib/fs.jsfileSystem = fs;} elseif (isMemoryFs) {fileSystem = compiler.outputFileSystem;} else {fileSystem = new MemoryFileSystem();compiler.outputFileSystem = fileSystem;}
  1. devServer 通知浏览器端文件发生改变,在启动 devServer 的时候,sockjs 在服务端和浏览器端建立了一个 webSocket 长连接,以便将 webpack 编译和打包的各个阶段状态告知浏览器,最关键的步骤还是 webpack-dev-server 调用 webpack api 监听 compile的 done 事件,当compile 完成后,webpack-dev-server通过 _sendStatus 方法将编译打包后的新模块 hash 值发送到浏览器端。

// webpack-dev-server/lib/Server.jsconst addHooks = (compiler) => {...done.tap('webpack-dev-server', (stats) => {this._sendStats(this.sockets, this.getStats(stats));this._stats = stats;});};..._sendStats(sockets, stats, force) {...this.sockWrite(sockets, 'hash', stats.hash);if (stats.errors.length > 0) {this.sockWrite(sockets, 'errors', stats.errors);} elseif (stats.warnings.length > 0) {this.sockWrite(sockets, 'warnings', stats.warnings);} else {this.sockWrite(sockets, 'ok');}}

浏览器接收到服务端消息做出响应

  1. 这里的主要逻辑位于 webpack-dev-server/client-src 中,webpack-dev-server 修改了webpack 配置中的 entry 属性,在里面添加了 webpack-dev-client 的代码,这样在最后的 bundle.js 文件中就会有接收 websocket 消息的代码了。

//webpack-dev-server/lib/utils/addEntries.jslet hotEntry;if (options.hotOnly) {hotEntry = require.resolve('webpack/hot/only-dev-server');} elseif (options.hot) {hotEntry = require.resolve('webpack/hot/dev-server');}...if (hotEntry && checkInject(options.injectHot, config, true)) {additionalEntries.push(hotEntry);}config.entry = prependEntry(config.entry || './src', additionalEntries);
  1. 以上代码可以看出,如果选择了热加载,输出的 bundle.js 会包含接收 websocket 消息的代码。而且 plugin 也会注入一个 HotModuleReplacementPlugin,构建过程中热加载相关的逻辑都在这个插件中。这个插件主要处理两部分逻辑:

  • 注入 HMR runtime 逻辑

  • 找到修改的模块,生成一个补丁 js 文件和更新描述 json 文件

先看一张图,看看 websocket 中的消息长什么样子:

可以看到,接收的消息只有 type 和 hash 两个内容。在 client 里面的逻辑,他们分别对应不同的处理逻辑:

// webpack-dev-server/client-src/default/index.jshash(hash) {status.currentHash = hash;},...ok() {sendMessage('Ok');if (options.useWarningOverlay || options.useErrorOverlay) {overlay.clear();}if (options.initial) {return (options.initial = false);} // eslint-disable-line no-return-assignreloadApp(options, status);}
  1. 可以看出,当接收到 type 为 hash 消息后会将 hash 值暂存起来,当接收到 type 为 ok 的消息后对应用执行 reload 操作,而 hash 消息是在 ok 消息之前的。再看看 reload 里面的处理逻辑:

// webpack-dev-server/client-src/default/reloadApp.jsif (hot) {...const hotEmitter = require('webpack/hot/emitter');hotEmitter.emit('webpackHotUpdate', currentHash);if (typeof self !== 'undefined' && self.window) {self.postMessage(`webpackHotUpdate${currentHash}`, '*');}}
  1. 可以看出,如果配置了模块热更新,就调用 webpack/hot/emitter 将最新 hash 值发送给 webpack,然后将控制权交给 webpack 客户端代码。如果没有配置模块热更新,就进行 liveReload 的逻辑。webpack/hot/dev-server 中会监听 webpack-dev-server/client-src 发送的 webpackHotUpdate 消息,然后调用 webpack/lib/HotModuleReplacement.runtime 中的 check 方法,检测是否有新的更新:

// webpack/hot/dev-server.jsvar 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();}});// webpack/lib/HotModuleReplacement.runtimefunction hotCheck(apply) {...return hotDownloadManifest(hotRequestTimeout).then(function(update) {...hotEnsureUpdateChunk(chunkId);...return promise;});}
  1. 以上代码可以看出,在 check 过程中,主要调用了两个方法 hotDownloadManifest 和 hotDownloadUpdateChunk。hotDownloadManifest 是通过 Ajax 向服务器请求十分有更新的文件,如果有就返回对应的文件信息,hotDownloadUpdateChunk 是通过Jsonp的方式,请求最新的代码模块。如下图所示:

补充,这两个文件的名称是可以配置的,如果没有配置,则取定义在 WebpackOptionsDefaulter 中的默认配置。

this.set("output.hotUpdateChunkFilename", "[id].[hash].hot-update.js");this.set("output.hotUpdateMainFilename", "[hash].hot-update.json");

对模块进行热更新或刷新页面

综上,我们获得了更新的内容。接下来就可以进行更新了。这部分的逻辑在 webpack/lib/HotModuleReplacement.runtime 中。

  1. 首先,更新过的模块,现在都属于 outdated 的模块,所以先找出过期的模块及其依赖:

//webpack/lib/HotModuleReplacement.runtimefunction getAffectedStuff(updateModuleId) {var outdatedModules = [updateModuleId];var outdatedDependencies = {};...return {type: "accepted",moduleId: updateModuleId,outdatedModules: outdatedModules,outdatedDependencies: outdatedDependencies};}
  1. 根据调用的 Api 信息,对结果进行标注及处理。

switch (result.type) {case"self-declined":...break;case"declined":...break;case"unaccepted":...break;case"accepted":if (options.onAccepted) options.onAccepted(result);doApply = true;break;case"disposed":if (options.onDisposed) options.onDisposed(result);doDispose = true;break;}
  1. 从缓存中删除过期的模块和依赖

// remove module from cachedelete installedModules[moduleId];// when disposing there is no need to call dispose handlerdelete outdatedDependencies[moduleId];// remove "parents" references from all childrenfor (j = 0; j < module.children.length; j++) {...}// remove outdated dependency from module childrenvar dependency;var moduleOutdatedDependencies;for (moduleId in outdatedDependencies) {...}
  1. 将新的模块添加到 modules 中,当下次调用 webpack_require (webpack 重写的 require 方法)方法的时候,就是获取到了新的模块代码了。

// insert new codefor (moduleId in appliedUpdate) {if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {modules[moduleId] = appliedUpdate[moduleId];}}

最后就是错误的兼容了,如果热加载失败,将会刷新浏览器。


http://chatgpt.dhexx.cn/article/YsUeqgc3.shtml

相关文章

HMR(热替换)

HMR 即模块热替换&#xff08;hot module replacement&#xff09;的简称&#xff0c;它可以在应用运行的时候&#xff0c;不需要刷新页面&#xff0c;就可以直接替换、增删模块。webpack 可以通过配置 webpack.HotModuleReplacementPlugin 插件来开启全局的 HMR 能力&#xff…

面试官:说说Webpack的热更新是如何做到的?原理是什么?

一、是什么 HMR全称 Hot Module Replacement&#xff0c;可以理解为模块热替换&#xff0c;指在应用程序运行过程中&#xff0c;替换、添加、删除模块&#xff0c;而无需重新刷新整个应用 例如&#xff0c;我们在应用运行过程中修改了某个模块&#xff0c;通过自动刷新会导致整…

Webpack 热更新HMR 原理全解析

一、什么是 HMR HMR 全称 Hot Module Replacement&#xff0c;中文语境通常翻译为模块热更新&#xff0c;它能够在保持页面状态的情况下动态替换资源模块&#xff0c;提供丝滑顺畅的 Web 页面开发体验。 HMR 最初由 Webpack 设计实现&#xff0c;至今已几乎成为现代工程化工具…

curl.perform() pycurl.error: (23, 'Failed writing body (0 != 59)')

在使用python3.7编码时&#xff0c;引入pycurl模块和StringIO模块后&#xff0c;容易引起上述错误 导入StringIO模块的解决方案&#xff1a; 只有在python2中才能导入StringIO模块&#xff0c;直接 from StringIO import StringIO 即可 但是python3&#xff0c;STringIO和…

关于python的url处理

基本环境&#xff1a; python2.7 1 完整的url语法格式&#xff1a; 协议://用户名密码:子域名.域名.顶级域名:端口号/目录/文件名.文件后缀?参数值#标识 2 urlparse模块对url的处理方法 urlparse模块对url的主要处理方法有&#xff1a;urljoin/urlsplit/urlunsplit/urlpar…

windows10+python3.7使用pip安装pycurl失败

使用pip install pycurl安装pycurl失败&#xff1a; python setup.py egg_info did not run successfully. 可以单独下载pycurl依赖文件然后安装 sArchived: Python Extension Packages for Windows - Christoph Gohlke (uci.edu) 选择Python对应版本的文件进行下载&#xff0…

Pycurl介绍

pycurl — A Python interface to the cURL library Pycurl包是一个libcurl的Python接口.pycurl已经成功的在Python2.2到Python2.5版编译测试过了. Libcurl是一个支持FTP, FTPS, HTTP, HTTPS, GOPHER, TELNET, DICT, FILE 和 LDAP的客户端URL传输库.libcurl也支持HTTPS认证,H…

[windows]python 安装pycurl

问题描述 pip install pycurl 报错 手动安装 下载地址&#xff1a;https://www.lfd.uci.edu/~gohlke/pythonlibs/ 页面搜索&#xff1a; pycurl 下载对应版本的whl文件&#xff0c;我是windows环境 python3.8 所以下载pycurl-7.45.1-cp38-cp38-win32.whl 安装&#xff1a;…

Python实用模块之pycurl

软硬件环境 ubuntu 19.04 64bitanaconda3 with python 3.7.3pycurl 7.43.0.2 简介 CURL是一个基于URL进行数据传输的命令行工具&#xff0c;使用C语言编写&#xff0c;支持http&#xff0c;https&#xff0c;ftp&#xff0c;telnet&#xff0c;file&#xff0c;ldap等常见网络传…

ipcs -a

消息队列、共享内存、信号量

ipcc

IPCCX装完后连接不了LDAP,怎么解决&#xff1f;&#xff1f; 装完了IPCCX, 靠&#xff0c; 直接给我来歌60秒关机&#xff0c; 后面还有LDAP连接问题&#xff0c; 我的IPCCX server 可以ping通ccm server, 为什么LDAP会挂呢&#xff1f;&#xff1f;&#xff1f; 请问我现在要怎…

ipcs报错:kernel not configured for shared memory、semaphore、message queues [解决方法]

前言 今天在复习linux进程间通信的shared memory 共享内存时&#xff0c;在PC端的VMare Workstation虚拟机的Ubuntu上测试我写的shared_memory_CREAT.c 和shared_memory_CONSUME.c 时正常在PC端运行&#xff0c;就想着把程序用交叉编译器编译成arm格式放到linux开发板上运行试…

ipcs

&#xfeff;&#xfeff; linux命令-ipcs 格式&#xff1a;ipcs [-asmq] [-tclup] ipcs [-smq] -i id ipcs -h 功能描述&#xff1a;ipcs命令用来显示系统存在的ipc&#xff08;进程间通信&#xff09;相关信息。 参数&#xff1a;-i 显示指定id的ip…

IPC方案

近期了解了不少网络摄像头相关知识&#xff0c;主要功能组成如下&#xff1a; WIFI&#xff0c;USB接口或者SDIO接口实现 RJ45 本地TF存储 IR CUT&#xff0c;滤光片切换 移动侦测&#xff0c;人体感应 夜视功能&#xff0c;依靠红外灯 云台控制&#xff0c;PWM控制Moto&#…

Linux15 --- 信号量、ipcs

1、IPC机制&#xff1a; 进程间通信&#xff08;管道、信号量、共享内存、消息队列、套接字&#xff09; 2、信号量&#xff1a; 可以类比于红绿灯&#xff0c;对于路口这个共享的通行权&#xff0c;谁得到红绿灯的通行信号&#xff0c;才可以得到路口的通行权&#xff0c;没…

Linux ipcs命令与ipcrm命令的用法详解

转载地址&#xff1a;http://www.jb51.net/article/40805.htm linux/uinx上提供关于一些进程间通信方式的信息&#xff0c;包括共享内存&#xff0c;消息队列&#xff0c;信号 ipcs用法 ipcs -a 是默认的输出信息 打印出当前系统中所有的进程间通信方式的信息 ipcs -m 打印出…

ipcs命令详解——共享内存、消息队列、信号量定位利器

多进程间通信常用的技术手段包括共享内存、消息队列、信号量等等&#xff0c;Linux系统下自带的ipcs命令是一个极好的工具&#xff0c;可以帮助我们查看当前系统下以上三项的使用情况&#xff0c;从而利于定位多进程通信中出现的通信问题。目前也有一些帖子介绍ipcs命令的使用方…

(1)IPC简介

Unix/Linux IPC简介 简述1. 消息传递演变过程2. 同步形式演变 进程、线程与信息共享IPC对象的持续性名字空间fork、exec和exit对IPC对象的影响总结参考资料 简述 IPC是进程间通信(interprocess communication)的简称。用来描述运行在一个操作系统之上的不同进程间各种消息传递…

Linux——信号量(定义、示例、信号量接口、ipcs命令)

目录 1、信号量 2、信号量举例 3、信号量的接口 4、通过控制进程来完成打印机操作 5、ipcs命令 1、信号量 &#xff08;1&#xff09;定义:​​​​​​ ​信号量是一个特殊的变量&#xff0c;一般取正数值。它的值代表允许访问的资源数目&#xff0c;获取资源时&#xff…

什么是IPC?

目录 IPC的简介&#xff1a; IPC的主要功能模块&#xff1a; IPC信号处理过程&#xff1a; IPC硬件构成&#xff1a; IPC的简介&#xff1a; IPC&#xff1a;是IP Camera的简称。它是在前一代模拟摄像机的基础上&#xff0c;集成了编码模块后的摄像机。它和模拟摄像机的区别…