Webpack 热更新原理

2023-09-20 18:41:10

什么是热更新

模块热替换(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和 hash
        this.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 Hash
        hash(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 window
            self.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 API
          addDisposeHandler: function (callback) {
            hot._disposeHandlers.push(callback);
          },
          removeDisposeHandler: function (callback) {
            var idx = hot._disposeHandlers.indexOf(callback);
            if (idx >= 0) hot._disposeHandlers.splice(idx, 1);
          },
          // Management API
          check: 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 available
          if(!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 later
          var 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 请求去服务器端获取新的模块资源解析并局部刷新页面

更多推荐

代码随想录Day1 数组基础

本文详细说明和思路来源于:代码随想录视频讲解:手把手带你撕出正确的二分法|二分查找法|二分搜索法|LeetCode:704.二分查找_哔哩哔哩_bilibiliLeetcodeT704题目链接704.二分查找-力扣(LeetCode)题目概述1:思路:1.因为数组是升序排列,且数组的元素不重复,所以使用二分查找法2.注

八、逻辑覆盖

不要让你的弱点妨碍你的优势。有两种互补的逻辑测试方法。第一种被称为语义逻辑覆盖,这种覆盖只考虑逻辑表达式的含义而不管表达式的构成。语义逻辑覆盖的优点在于即使谓词被写成一种表面不同但是具有等价语义的形式,依然可以产生相同的测试用例。第二种被称为语法逻辑覆盖,这种覆盖专门根据逻辑表达式的构成来生成测试用例。语法逻辑覆盖的优

bloaty

安装教程参考:https://github.com/google/bloatyBloaty是一个用于分析二进制文件大小的工具,它可以深入分析二进制文件,使用自定义的ELF、DWARF和Mach-O解析器,将二进制文件的每个字节准确地归因于生成它的符号或编译单元。以下是一些使用Bloaty的基本步骤:安装Bloaty:你

【C++】STL详解(六)—— list的模拟实现

​​📝个人主页:@Sherry的成长之路🏠学习社区:Sherry的成长之路(个人社区)📖专栏链接:C++学习🎯长路漫漫浩浩,万事皆有期待上一篇博客:【C++】STL详解(五)——list的介绍及使用文章目录list的三个类及其成员函数接口总览结点类的模拟实现构造函数迭代器类的模拟实现迭代器类存在的意义迭代器类的

[Qt/C/C++]JSON和程序发布

文章摘于爱编程的大丙文章目录1.JSON1.1Json数组1.2Json对象1.3注意事项2.Qt中JSON操作2.1QJsonValue2.2QJsonObject2.3QJsonArray2.4QJsonDocument2.5举例2.5.1写文件2.5.2读文件3.cjson库的使用3.1cJSON结构体3.2cJ

华为OD机考算法题:分积木

目录题目部分解读与分析代码实现题目部分题目分积木难度难题目说明Solo和koko是两兄弟,妈妈给了他们一大堆积木,每块积木上都有自己的重量。现在他们想要将这些积木分成两堆。哥哥Solo负责分配,弟弟koko要求两个人获得的积木总重量“相等”(根据Koko的逻辑),个数可以不同,不然就会哭,但koko只会先将两个数转成二

Linux 系统目录结构 & 终端

系统目录结构Linux或Unix操作系统中,所有文件和目录呈一个以根节点为始的倒置的树状结构。文件系统的最顶层是根目录,用/来表示根目录。在根目录之下的既可以是目录,也可以是文件,而每一个目录中又可以包含子目录文件。如此,就构成了一个庞大的文件系统。在命令窗口下输入命令:ls/,显示根目录下的文件:树状目录结构:目录解

从零开始搭建成绩查询系统

在当前的数字化时代,教育行业借助技术手段不断推动教学效果的提升。作为教师,搭建一个专属的成绩查询系统可以更好地管理学生成绩,并即时向家长反馈。本文将详细介绍如何从零开始搭建一个成绩查询系统,以提升教学管理的便捷性和效率。当然,对于非技术型的教师来说,使用现成工具是个更快速实用的选择。易查分等工具被许多教师用来制作查分网

(VS报错)已在 xxxxx.exe 中执行断点指令(__debugbreak()语句或类似调用)-解决方法&&C++创建对象四种方式

上述报错困扰了我好几天,在网上搜了一天,到最后还是没有解决问题试过通过项目属性->C/C+±>代码生成->启用增强指令集->选择AVX,这种方法也没用但问题出现在创建对象时内存分配问题上方法一:如果是这样创建对象,并且写了析构函数会报错,把析构函数删去之后程序就能正常运行了。方法二:创建对象的方式改为这样,程序也可以正

第九章(2):长短期记忆网络(Long short-term memory, LSTM)与pytorch示例(简单字符级语言模型训练器)

第九章(2):长短期记忆网络(Longshort-termmemory,LSTM)与pytorch示例(简单字符级语言模型训练器)作者:安静到无声个人主页作者简介:人工智能和硬件设计博士生、CSDN与阿里云开发者博客专家,多项比赛获奖者,发表SCI论文多篇。Thanks♪(・ω・)ノ如果觉得文章不错或能帮助到你学习,可

Python计算机二级知识点整理

1.当一个进程在运行过程中释放了系统资源后要调用唤醒进程原语唤醒进程原语是把进程从等待队列里移出到就绪队列并设置进程为就绪状态,当一个进程在运行过程中释放了系统资源后进入就绪状态,调用唤醒进程原语。2.3.4.在希尔排序法中,每经过一次数据交换后能消除多个逆序在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数

热文推荐