Webpack 热更新原理

news/2024/4/29 7:56:16/文章来源:https://blog.csdn.net/qq_34574204/article/details/133089655

什么是热更新

模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新

一般的刷新我们分两种:

  • 一种是页面刷新,不保留页面状态,就是简单粗暴,直接 window.location.reload()
  • 另一种是基于 WDS (Webpack-dev-server) 的模块热替换,只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。

可以看到相比于第一种,热更新对于我们的开发体验以及开发效率都具有重大的意义。
HMR 作为一个 Webpack 内置的功能,可以通过 HotModuleReplacementPlugin--hot 开启。

具体我们如何在 webpack 中使用这个功能呢?

热更新的使用以及简单分析

  • 如何使用热更新
    npm install webpack webpack-dev-server --save-dev

    设置 HotModuleReplacementPluginHotModuleReplacementPluginwebpack 是自带的

    	plugins: {HotModuleReplacementPlugin: new webpack.HotModuleReplacementPlugin()
    }
    

    再设置一下 devServer

    	devServer: {contentBase: path.resolve(__dirname, 'dist'),hot: true, // 重点关注historyApiFallback: true,compress: true
    }
    

    hottrue,代表开启热更新

  • 两个重要的文件
    当我们改变我们项目的文件的时候,比如我修改 Vue 的一个 方法:
    更改前

    clickMe() {console.log('我是 Gopal,欢迎关注「前端杂货铺」');
    }
    

    更改后

    clickMe() {console.log('我是 Gopal,欢迎关注「前端杂货铺」,一起学习成长吧');
    }
    

    浏览器会去请求两个文件
    在这里插入图片描述
    接下来我们看看这两个文件:

    • JSON 文件,h 代表本次新生成的 Hash 值为 0c256052432b51ed32c8——本次输出的 Hash 值会被作为下次热更新的标识。c 表示当前要热更新的文件对应的是哪个模块,可以让 webpack 知道它要更新哪个模块

      {"h": "0c256052432b51ed32c8","c": {"201": true}
      }
      
    • js 文件,就是本次修改的代码,重新编译打包后的,大致是下面这个样子(已删减一些并格式化过,这里看不懂没关系的,就记住是返回要更新的模块就好了),webpackHotUpdate 方法就是用来更新模块的,201 对应的是哪个模块(我们称它为模块标识),其他的就是要更新的模块的内容了

      	webpackHotUpdate(201, {"./src/views/moveTransfer/list/index.vue?vue&type=script&lang=js&": function (module,exports,__webpack_require__) {"use strict";var _Object$defineProperty = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/object/define-property */ "./node_modules/@babel/runtime-corejs3/core-js-stable/object/define-property.js");_Object$defineProperty(exports, "__esModule", {value: true,});exports.default = void 0;var _default = {data: function data() {return {};},computed: {},methods: {clickMe: function clickMe() {console.log("我是 Gopal,欢迎关注「前端杂货铺」,一起学习成长吧");},},};exports.default = _default;},
      });
      

      那么问题来了,我修改了文件,浏览器是怎么知道要更新的呢?

  • 了解一下 Websocket

    热更新使用到了 Websocket,这里不会细讲 Websocket,可以看下阮一峰老师的 WebSocket 教程,下面是一个 简单的例子

    // 执行上面语句之后,客户端就会与服务器进行连接。
    var ws = new WebSocket("wss://echo.websocket.org");// 实例对象的 onopen 属性,用于指定连接成功后的回调函数
    ws.onopen = function(evt) { console.log("Connection open ..."); ws.send("Hello WebSockets!");
    };// 实例对象的 onmessage 属性,用于指定收到服务器数据后的回调函数。可以接受二进制数据,blob 对象或者 Arraybuffer 对象
    ws.onmessage = function(evt) {console.log( "Received Message: " + evt.data);ws.close();
    };// 实例对象的 onclose 属性,用于指定连接关闭后的回调函数。
    ws.onclose = function(evt) {console.log("Connection closed.");
    };  
    

    上面通过 new Websocket 创建一个客户端与服务端通信的实例,并通过 onmessage属性,接受指定服务器返回的数据,并进行相应的处理。

    这里大概解释下,为什么是 Websocket ?因为 Websocket 是一种双向协议,它最大的特点就是 服务器可以主动向客户端推送消息,客户端也可以主动向服务器发送信息。这是 HTTP 不具备的,热更新实际上就是服务器端的更新通知到客户端,所以选择了 Websocket
    接下来让我们进一步的讨论关于热更新的原理

  • 热更新原理

    • 热更新的过程

      • Webpack-complierwebpack 的编译器,将 JavaScript 编译成 bundle(就是最终的输出文件)
      • HMR Server:将热更新的文件输出给 HMR Runtime
      • Bunble Server:提供文件在浏览器的访问,也就是我们平时能够正常通过 localhost 访问我们本地网站的原因
      • HMR Runtime:开启了热更新的话,在打包阶段会被注入到浏览器中的 bundle.js,这样 bundle.js 就可以跟服务器建立连接,通常是使用 websocket ,当收到服务器的更新指令的时候,就去更新文件的变化
      • bundle.js:构建输出的文件
    • 启动阶段

      文件经过 Webpack-complier 编译好后传输给 Bundle Server,Bundle Server 可以让浏览器访问到我们打包出来的文件

      下面流程图中的 1、2、A、B阶段
      在这里插入图片描述

    • 文件热更新阶段

      • 文件经过 Webpack-complier 编译好后传输给 HMR Server
      • HMR Server 知道哪个资源(模块)发生了改变,并通知 HMR Runtime 有哪些变化(也就是上面我们看到的两个请求),
      • HMR Runtime 就会更新我们的代码,这样我们浏览器就会更新并且不需要刷新
        上面流程图的 1、2、3、4、5 阶段
  • 深入——源码阅读
    我们还看回上图,其中启动阶段图中的 1、2、A、B阶段就不讲解了,主要看热更新阶段主要讲 3、4 和 5 阶段

    • 在开始接下开的阅读前,我们再回到最初的问题上我本地修改了文件,浏览器是怎么知道要更新的呢?

      • 通过上面的流程图,其实我们可以猜测,本地实际上启动了一个 HMR Server 服务,而且在启动 Bundle Server 的时候已经往我们的 bundle.js 中注入了 HMR Runtime(主要用来启动 Websocket,接受 HMR Server 发来的变更)
        -所以我们聚焦以下几点:
    • Webpack 如何启动了 HMR Server

      • HMR Server 如何跟 HMR Runtime 进行通信的
      • HMR Runtime 接受到变更之后,如何生效的
    • 启动 HMR Server

      • 这个工作主要是在 webpack-dev-server 中完成的

      • lib/Server.js setupApp 方法,下面的 express 服务实际上对应的是 Bundle Server

        setupApp() {// Init express server// eslint-disable-next-line new-cap// 初始化 express 服务// 使用 express 框架启动本地 server,让浏览器可以请求本地的静态资源。this.app = new express();
        }
        

        启动服务结束之后就通过 createSocketServer 创建 websocket 服务

        listen(port, hostname, fn) {this.hostname = hostname;return (findPort(port || this.options.port).then((port) => {this.port = port;return this.server.listen(port, hostname, (err) => {if (this.options.hot || this.options.liveReload) {// 启动 express 服务之后,启动 websocket 服务this.createSocketServer();}});}));
        }
        
        createSocketServer() {this.socketServer = new this.SocketServerImplementation(this);this.socketServer.onConnection((connection, headers) => {});
        }
        
    • HMR ServerHMR Runtime 的通信

      首先要通信的第一个问题在于——通信的时机,什么时候我去通知客户端我的文件更新。通过 webpack 创建的 compiler 实例(监听本地文件的变化、文件改变自动编译、编译输出),可以往 compiler.hooks.done 钩子(代表 webpack 编译完之后触发)注册事件, 当监听到一次 webpack 编译结束,就会调用 sendStats 方法。

      lib/Server.js 中的 setupHooks 方法

      // lib/Server.js
      // 绑定监听事件
      setupHooks() {// ...const addHooks = (compiler) => {// 监听 webpack 的 done 钩子,tapable 提供的监听方法// done 标识编译结束const { compile, invalid, done } = compiler.hooks;compile.tap('webpack-dev-server', invalidPlugin);invalid.tap('webpack-dev-server', invalidPlugin);done.tap('webpack-dev-server', (stats) => {// 当监听到一次webpack编译结束,就会调用 sendStats 方法this.sendStats(this.sockets, this.getStats(stats));this.stats = stats;});};
      }
      

      当监听到一次 webpack 编译结束,就会调用 sendStats 方法,里面会向客户端发送 hashok 事件

      // lib/Server.js
      // send stats to a socket or multiple sockets
      sendStats(sockets, stats, force) {// ok和 hashthis.sockWrite(sockets, 'hash', stats.hash);if (stats.errors.length > 0) {this.sockWrite(sockets, 'errors', stats.errors);} else if (stats.warnings.length > 0) {this.sockWrite(sockets, 'warnings', stats.warnings);} else {this.sockWrite(sockets, 'ok');}
      }
      

      client-src/default/index.js 中,会去更新 hash,并且在 ok 的时候去进行检查更新 reloadApp

      // client-src/default/index.js 
      const onSocketMessage = {// 更新 current Hashhash(hash) {status.currentHash = hash;},'progress-update': function progressUpdate(data) {if (options.useProgress) {log.info(`${data.percent}% - ${data.msg}.`);}sendMessage('Progress', data);},ok() {sendMessage('Ok');if (options.useWarningOverlay || options.useErrorOverlay) {overlay.clear();}if (options.initial) {return (options.initial = false);}// 进行更新检查等操作reloadApp(options, status);}};
      

      接下来我们看看 client-src/default/utils/reloadApp.js 中的 reloadApp。这里又利用 node.jsEventEmitter,发出webpackHotUpdate 消息。这里又将更新的事情给回了 webpack(为了更好的维护代码,以及职责划分的更明确。)

      function reloadApp({ hotReload, hot, liveReload },{ isUnloading, currentHash }
      ) {// ...if (hot) {log.info('App hot update...');//  hotEmitter 其实就是 EventEmitter 的实例const hotEmitter = require('webpack/hot/emitter');// 又利用 node.js 的 EventEmitter,发出 webpackHotUpdate 消息。// websocket 仅仅用于客户端(浏览器)和服务端进行通信。而真正做事情的活还是交回给了 webpack。hotEmitter.emit('webpackHotUpdate', currentHash);if (typeof self !== 'undefined' && self.window) {// broadcast update to windowself.postMessage(`webpackHotUpdate${currentHash}`, '*');}}// ...
      }module.exports = reloadApp;
      

      webpackhot/dev-server.js 中,监听 webpackHotUpdate 事件,并执行 check 方法。并在 check 方法中调用 module.hot.check 方法进行热更新。

      // hot/dev-server.js
      // 监听webpackHotUpdate事件
      hotEmitter.on("webpackHotUpdate", function (currentHash) {lastHash = currentHash;if (!upToDate() && module.hot.status() === "idle") {log("info", "[HMR] Checking for updates on the server...");check();}
      });
      
      var check = function check() {//  moudle.hot.check 开始热更新// 之后的源码都是HotModuleReplacementPlugin塞入到bundle.js中的哦,我就不写文件路径了module.hot.check(true).then(function (updatedModules) {// ...}).catch(function (err) {// ...});
      };
      

      至于 module.hot.check ,实际上通过 HotModuleReplacementPlugin 已经注入到我们 chunk 中了(也就是我们上面所说的 HMR Runtime),所以后面就是它是如何更新 bundle.js 的呢?

    • HMR Runtime 中更新 bundle.js
      如果我们仔细看我们的打包后的文件的话,开启热更新之后生成的代码会比不开启多出很多东西(为了更加直观看到,可以将其输出到本地),这些就是帮助 webpack 在浏览器端去更新 bundle.js 的 HMR Runtime 代码

      来看打包后的代码中新增了一个 createModuleHotObject

      module.hot = createModuleHotObject(options.id, module);
      

      实际上这个函数就是用来返回一个 hot 对象,所以调用 module.hot.check 的时候,实际上就是执行 hotCheck 函数

      function createModuleHotObject(moduleId, me) {var hot = {// Module APIaddDisposeHandler: function (callback) {hot._disposeHandlers.push(callback);},removeDisposeHandler: function (callback) {var idx = hot._disposeHandlers.indexOf(callback);if (idx >= 0) hot._disposeHandlers.splice(idx, 1);},// Management APIcheck: hotCheck,apply: hotApply,status: function (l) {if (!l) return currentStatus;registeredStatusHandlers.push(l);},addStatusHandler: function (l) {registeredStatusHandlers.push(l);},removeStatusHandler: function (l) {var idx = registeredStatusHandlers.indexOf(l);if (idx >= 0) registeredStatusHandlers.splice(idx, 1);},};currentChildModule = undefined;return hot;
      }
      

      其中就有 hotCheck 中调用了 __webpack_require__.hmrM

      function hotCheck(applyOnUpdate) {setStatus("check");return __webpack_require__.hmrM().then(function (update) {}
      }
      

      __webpack_require__.hmrM——加载.hot-update.json

      • 来看 __webpack_require__.hmrM, 其中 __webpack_require__.p 指的是我们本地服务的域名,类似 http://0.0.0.0:9528
      • 另外 __webpack_require__.hmrF 去获取 .hot-update.json 文件的地址,就是我们之前提到的重要文件之一
      __webpack_require__.hmrM = () => {if (typeof fetch === "undefined") throw new Error("No browser support: need fetch API");return fetch(__webpack_require__.p + __webpack_require__.hmrF()).then((response) => {if(response.status === 404) return; // no update availableif(!response.ok) throw new Error("Failed to fetch update manifest " + response.statusText);return response.json();});
      };/* webpack/runtime/get update manifest filename */
      (() => {__webpack_require__.hmrF = () => ("main." + __webpack_require__.h() + ".hot-update.json");
      })();
      
    • 加载要更新的模块

      下面来看如何加载我们要更新的模块的,可以看到打包出来的代码中有 loadUpdateChunk

      function loadUpdateChunk(chunkId) {return new Promise((resolve, reject) => {var url = __webpack_require__.p + __webpack_require__.hu(chunkId);// create error before stack unwound to get useful stacktrace latervar error = new Error();var loadingEnded = (event) => {// ...加载后的处理};__webpack_require__.l(url, loadingEnded);});
      }
      

      再来看 __webpack_require__.l,主要通过类似 JSONP 的方式进行,因为JSONP获取的代码可以直接执行。

          __webpack_require__.l = (url, done, key, chunkId) => {// ...if (!script) {script = document.createElement("script");script.charset = "utf-8";script.timeout = 120;if (__webpack_require__.nc) {script.setAttribute("nonce", __webpack_require__.nc);}script.setAttribute("data-webpack", dataWebpackPrefix + key);script.src = url;}// ...needAttach && document.head.appendChild(script);
      };
      

      还记得我们一开始提到的返回的 JS 中就是一个 webpackHotUpdate 函数么?实际上在我们的 HMR Runtime 中就是全局定义了(下面的名称是 webpackHotUpdatelearn_hot_reload,应该是 webpack 版本不一样导致的,不影响理解)

      // webpackHotUpdate + 项目名
      self["webpackHotUpdatelearn_hot_reload"] = (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;}
      };
      

      所以,客户端接受到服务器端推动的消息后,如果需要热更新,浏览器发起 http 请求去服务器端获取新的模块资源解析并局部刷新页面
      在这里插入图片描述

  • 总结
    本文介绍了 webpack 热更新的简单使用、相关的流程以及原理。小结一下,webpack 如果开启了热更新的时候

    • HMR Runtime 通过 HotModuleReplacementPlugin 已经注入到我们 chunk 中了

    • 除了开启一个 Bundle Server,还开启了 HMR Server,主要用来和 HMR Runtime 中通信

    • 在编译结束的时候,通过 compiler.hooks.done,监听并通知客户端

    • 客户端接收到之后,就会调用 module.hot.check 等,发起 http 请求去服务器端获取新的模块资源解析并局部刷新页面

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

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

相关文章

python装13的一些写法

一些当你离职后,让老板觉拍大腿的代码 1. any(** in ** for ** in **) 判断某个集合元素,是否包含某个/某些元素 代码: if __name__ __main__:# 判断 list1 中是否包含某个/某些元素list1 [1,2,3,4]a any(x in [5,4] for x in list1) 输…

基于微信小程序的音乐播放器设计与实现(源码+lw+部署文档+讲解等)

前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗 👇🏻…

RocketMQ 源码分析——Producer

文章目录 消息发送代码实现消息发送者启动流程检查配置获得MQ客户端实例启动实例定时任务 Producer 消息发送流程选择队列默认选择队列策略故障延迟机制策略*两种策略的选择 技术亮点:ThreadLocal 消息发送代码实现 下面是一个生产者发送消息的demo(同步发送&#…

Unity丨移动相机朝向目标并确定目标在摄像机可视范围内丨摄像机注释模型丨摄像机移动丨不同尺寸模型优化丨

文章目录 问题描述功能展示技术细节小结 问题描述 本文提供的功能是摄像机朝向目标移动,并确定整个目标出现在摄像机视角内,针对不同尺寸的模型优化。 功能展示 提示:这里可以添加技术名词解释 技术细节 直接上代码 using UnityEngine;…

【Linux】【网络】传输层协议:UDP

文章目录 UDP 协议1. 面向数据报2. UDP 协议端格式3. UDP 的封装和解包4. UDP 的缓冲区 UDP 协议 UDP传输的过程类似于寄信。 无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接。不可靠:没有确认机制,没有重传机制&am…

基于微信小程序的超市售货管理平台设计与实现(源码+lw+部署文档+讲解等)

前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗 👇🏻…

面向面试知识--MySQL数据库与索引

面向面试知识–MySQL数据库与索引 优化难点与面试点 什么是MySQL索引? 索引的MySQL官方定义:索引是帮助MySQL快速获取数据的数据结构。 动力节点原文: MysQL官方对于索引的定义:索引是帮助MySQL高效获取数据的数据结构。 MysQL在存储数据之…

【Unity实战】从零手戳一个库存背包系统

文章目录 前言素材开始一、绘制背包UI二、背包开启关闭三、初始化背包网格四、 添加物品五、 拖拽交换功能物品六、 物品拆分七、 物品堆叠八、 拖拽还原九、 引入字典存储数据十、 拾取物品十一、 丢弃物品 最终效果源码完结 前言 库存背包系统是大多数游戏的关键部分&#x…

python项目2to3方案预研

目录 官方工具2to3工具安装参数解释基本使用工具缺陷 future工具安装参数解释基本使用工具缺陷 python-modernize工具安装参数解释基本使用工具缺陷 pyupgrade工具安装参数解释基本使用工具缺陷 对比 官方工具2to3 2to3 是Python官方提供的用于将Python 2代码转换为Python 3代…

解决qml编译时出现错误ninja: build stopped: subcommand failed.

qml编译时出现错误ninja: build stopped: subcommand failed. 如下图: 解决这个编译错误其实很简单,我把Window写错了,写成了window, 如果有类似的报错,可以检查一下qml代码是否有问题。当然在Qt Creator里也没有错误提示&#x…

Canal实现Mysql数据同步至Redis、Elasticsearch

文章目录 1.Canal简介1.1 MySQL主备复制原理1.2 canal工作原理 2.开启MySQL Binlog3.安装Canal3.1 下载Canal3.2 修改配置文件3.3 启动和关闭 4.SpringCloud集成Canal4.1 Canal数据结构![在这里插入图片描述](https://img-blog.csdnimg.cn/c64b40c2231a4ea39a95aac81d771bd1.pn…

重装系统(配置环境)

这里写目录标题 0.重装系统1.python1.1 anaconda1.2 pycharm1.3 深度学习环境配置 2.java2.1.安装JDK2.2.配置JDK环境变量2.3IDEA2.4 Maven 3.大数据3.1 虚拟机3.2 Hadoop平台3.3 存储3.4 采集3.5 计算3.6 查询3.7 可视化 0.重装系统 // An highlighted block var foo bar;1.…

【性能优化下】组织结构同步优化二,全量同步/增量同步,断点续传实现方式

看到这一篇文章的 xdm ,应该对组织结构同步有一些想法了吧,如果没有,可以看前面两篇文章,可以通过如下地址查看一下: 【性能优化上】第三方组织结构同步优化一,你 get 到了吗? 坑爹&#xff0c…

UE5读取json文件

一、下载插件 在工程中启用 二、定义读取外部json文件的函数,参考我之前的文章 ue5读取外部文件_艺菲的博客-CSDN博客 三、读取文件并解析为json对象 这里Load Text就是自己定义的函数,ResourceBundle为一个字符串常量,通常是读取的文件夹…

数据预处理方式合集

删除空行 #del all None value data_all.dropna(axis1, howall, inplaceTrue) 删除空列 #del all None value data_all.dropna(axis0, howall, inplaceTrue) 缺失值处理 观测缺失值 观测数据缺失值有一个比较好用的工具包——missingno,直接传入DataFrame&…

【Html】用CSS定义咖啡 - 咖啡配料展示

显示效果 代码 index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>CodePen - For The Love Of Coffee</title><link rel"stylesheet" href"./style.css">&l…

Jenkins结合Gitlab,实现镜像构建及推送

docker-compose jenkins的docker-compose目录为为/home/jenkins&#xff0c;这个后面写脚本的时候需要对应上 version: 3 services:docker_jenkins:restart: alwaysimage: jenkins/jenkins:ltscontainer_name: docker_jenkinsprivileged: true ports:- 8080:8080- 50000:5000…

MyBatis基础之执行SQL

文章目录 执行 SQL 语句1. 增删改操作insert 元素insert 过程中的主键回填delete 元素 和 update 元素 2. getMapper 方法3. 查操作select 元素select 与 聚合函数 4. 传递多个参数使用 Map 传递多参数使用 JavaBean 传递多参使用注解方式传递多参数 执行 SQL 语句 Mapper 是 …

(2) Java 8 实战第二版——补充 收集数据、并行数据处理能力与性能

第6章 用Collectors类创建和使用收集器将数据流归约为一个值汇总&#xff1a;归约的特殊情况数据分组和分区开发你的自定义收集器 对一个交易列表按货币分组&#xff0c;获得该货币的所有交易额总和&#xff08;返回一个Map<Currency, Integer>&#xff09;。将交易列表…

面向组织分析的内容

声明 本文是学习GB-T 42859-2023 航天产品质量问题三个面向分析方法实施要求. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件规定了航天产品质量问题三个面向分析方法实施的一般要求、程序和分析内容。 本文件适用于承担航天产品研制…