首页/文章/webpack

一起学习 Webpack 模块热替换

2024-09-09
22488 分钟
...

何为 HMR?

模块热替换(HMR - Hot Module Replacement)功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面

当我们对代码进行修改并保存后,webpack 将对代码重新打包,并将改动的模块发送到浏览器,浏览器通过新的模块替换老的模块,从而实现局部更新且不需要刷新页面

为何需要 HMR?

在 HMR 出现之后,程序的加载都是页面级别的,即使是单个文件的发生改变,都需要刷新整个页面才能够获得最新的代码,且在此之前的数据都会丢失。

当我们遇到如下情况的时候

  • 分步表单,意味着一次更改我们需要填写很多的数据
  • 弹窗信息,意味着必须重新执行弹窗交互

再细小的操作,更新样式文件、备注信息等等操作都需要刷新页面重新加载执行,极大的影响了开发效率。引入 HMR 能够将这些细小的更改通过模块热替换的方式更新到页面上,从而提升开发的效率。

如何使用 HMR?

在 webpack 的配置中,针对于 devServer 配置hot:true


module.exports = {
  
  devServer: {
    
    hot: true
  }
};

在代码里面需要配置module.hot.accept接口,声明如何将模块安全地替换为最新代码

if (module.hot) {
  module.hot.accept(["./hello.js"], () => {
    render();
  });
}


webpack 编译构建流程


- hello.js
- index.js
- package.json
- webpack.config.js

const config = {
  entry: "./index.js", 
  output: {
    filename: "bundle.js", 
    path: path.resolve("dist"), 
  },
  plugins: [new HtmlWebpackPlugin()],
  mode: "development", 
  stats: {
    modules: false, 
    hash: true, 
  },
};

做好相关的配置之后,我们启动项目后,能够通过控制台发现生成了一个 hash 值,且通过浏览器打开网站之后,能够发现 websocket 中也传递了{type: "hash", data: "d76e2c3053202b29bf20"}对应的 hash 值

我们更新文件,触发新的编译,控制台中也会更新对应的数据

能够发现生成了新的hash值,且生成了[hash].hot-update.json/[hash].hot-update.js新的文件,文件上的hash值是上一次生成的hash值。

根据新生成文件名可以发现,上次输出的hash值会作为本次编译新生成的文件标识。依次类推,本次输出的hash值会被作为下次热更新的标识。

通过浏览器可以看到一次更新之后,会请求对应的[hash].hot-update.json/[hash].hot-update.js文件

  • c: 描述哪些 chunk 包含在此次更新中
  • r: 指示是否需要重新加载 Webpack runtime 代码
  • m: 列出本次更新中被修改的模块及其对应的新代码

热更新实现的原理

webpack-dev-server 启动本地服务

上述的 webpack 配置代码,我们通过webpack-dev-server启动代码


{
    "scripts": {
        "dev": "webpack-dev-server",
        "build": "webpack"
    },
}

所有的命令行可以在对应项目的package.jsonbin命令中找到对应的入口文件

{
    "name": "webpack-dev-server",
    "bin": "bin/webpack-dev-server.js",
}

执行pnpm dev 之后大致的流程(简易版本)

setupApp() {
    
    this.app = new (memoize(() => require("express")))();
}

createServer() {
    this.server = require((type)).createServer(options, this.app);
}

createWebSocketServer() {
    
    this.webSocketServer = new (require("./servers/WebsocketServer"))(this);
}

在整个启动本地服务时,涉及到的仓库很多,重点都在new Server()之后的操作

  • new Server之前会先启动webpack,生成compiler实例。compiler上有很多方法,比如可以启动webpack所有编译工作,以及监听本地文件的变化
  • 使用express启动本地服务,使得浏览器可以访问本地的静态资源
  • 本地server启动成功之后再去创建websocket服务,建立本地服务和浏览器的双向通信

修改 entry 配置

在我们启动本地服务之前,代码中修改了entry入口,自动注入了websocket客户端代码和热更新替换的代码

在进入start阶段的时候会调用initialize方法

client/index.jswebsocket客户端的代码,因为websocket是双向通信,上一步通过createServer是创建的本地服务端的websocket代码,还需要客户端代码,因此需要把客户端websocket代码塞到代码中

hot/dev-server.js主要用于检查更新逻辑

监听 webpack 编译结束

当修改完entry入口之后,会执行setupHooks方法,注册监听事件,监听webpack编译完成

setupHooks() {
  
  this.compiler.hooks.done.tap(
    "webpack-dev-server",
    (stats) => {
      if (this.webSocketServer) {
        this.sendStats(this.webSocketServer.clients, this.getStats(stats));
      }
      this.stats = stats;
    },
  );
}

sendStats(clients, stats, force) {
  this.currentHash = stats.hash;
  this.sendMessage(clients, "hash", stats.hash);

  if ((stats.errors).length > 0 ||(stats.warnings).length > 0) {
    const hasErrors = (stats.errors).length > 0;

    if ((stats.warnings).length > 0) {
      let params;
      if (hasErrors) {
        params = { preventReloading: true };
      }
      this.sendMessage(clients, "warnings", stats.warnings, params);
    }
    if ((stats.errors).length > 0) {
      this.sendMessage(clients, "errors", stats.errors);
    }
  } else {
    this.sendMessage(clients, "ok");
  }
}

每当webpack编译完成就会出发donehook,从而调用sendStats方法通过websocket给浏览器发送消息,hash/ok事件,浏览器能够拿到最新的hash值,检查更新逻辑

监听文件变化

每次文件发生变化之后,都需要触发文件编译,那么久还需要监听文件发生改变。该操作主要是通过webpack-dev-middleware库实现的。

start函数中,会执行setupDevMiddleware方法,该方法主要是执行webpack-dev-middleware库的。

webpack-dev-middleware: 该库主要做文件相关的操作,本地文件输出以及监听 webpack-dev-server: 该库主要只负责启动服务和前置准备工作

webpack-dev-middleware中主要实现

compiler.watch(watchOptions, errorHandler)

compiler.outputFileSystem = memfs.createFsFromVolume(new memfs.Volume())

调用了compiler.watch方法开启对文件的编译,文件变化的时候重新编译文件。

更改outputFileSystem,使用memory-fs将所有的output存储在内存中,减少对文件系统的操作

浏览器接收热更新的通知

在上面讲到每一次webpack编译结束之后,都会通过donehook 调用sendStats方法通过websocket传递相关的数据。

客户端中会被注入webpack-dev-server/client/index.js代码,主要用于接收相关数据

var onSocketMessage = {
  hash: function hash(_hash) {
    status.previousHash = status.currentHash;
    status.currentHash = _hash;
  },
  ok: function ok() {
    sendMessage("Ok");
    if (options.overlay) {
      overlay.send({
        type: "DISMISS"
      });
    }
    reloadApp(options, status);
  }
};


socket(socketURL, onSocketMessage, options.reconnect);


reloadApp(){
	if (hot && allowToHot) {
    log.info("App hot update...");
    hotEmitter.emit("webpackHotUpdate", status.currentHash);
    if (typeof self !== "undefined" && self.window) {
      self.postMessage("webpackHotUpdate".concat(status.currentHash), "*");
    }
  }
}

注入的客户端代码职责

  • socket方法建立了websocket和服务端的连接,并注册了一系列的监听事件
    • hash事件,更新最新一次打包后的hash
    • ok事件,进行热更新检查
  • ok事件中执行reloadApp方法,通过eventEmitter发出webpackHotUpdate事件,通知webpack该干活了

那么,webpack肯定是需要监听webpackHotUpdate事件的,没错,就在之前放入webpack/hot/dev-server.js代码中

var check = function check() {
  module.hot
    .check(true)
    .then(function (updatedModules) {
      if (!updatedModules) {
        window.location.reload();
        return;
      }
      if (upToDate()) {
        log("info", "[HMR] App is up to date.");
      }
    })
    .catch(function (err) {});
};

var 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();
  }
});

能够看到hot/dev-server.js监听了webpackHotUpdate事件,并且会去执行module.hot.check方法

HotModuleReplacementPlugin

我们能够浏览器的Sourcesbundle.js中找到上述代码,创建对应hot对象,里面就能够对应的check方法。注入的代码可以在HotModuleReplacement.runtime.js找到

当我们配置hot属性的时候webpack-dev-server会自动转成HotModuleReplacementPlugin

if (this.options.hot) {
  const HMRPluginExists = compiler.options.plugins.find(
    (p) => p && p.constructor === webpack.HotModuleReplacementPlugin,
  );

  if (HMRPluginExists) {
    this.logger.warn(
      `"hot: true" automatically applies HMR plugin, you don't have to add it manually to your webpack configuration.`,
    );
  } else {
    
    const plugin = new webpack.HotModuleReplacementPlugin();

    plugin.apply(compiler);
  }
}

HotModuleReplacementPlugin会悄悄的加一些代码到产物中

module.hot.check

上述知道了 module.hot.check 的来源,现在看看该check函数具体做了什么事情

  • 调用$hmrDownloadManifest$获取当前的hash.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;
    
            if (!response.ok)
                throw new Error("Failed to fetch update manifest " + response.statusText);
    
            return response.json();
        }
        );
    }
    __webpack_require__.hmrF = () => ("main." + __webpack_require__.h() + ".hot-update.json");
    
  • 再调用$hmrDownloadUpdateHandlers$["jsonp"]请求js文件

    
    
    __webpack_require__.hu = (chunkId) => {
      return "" + chunkId + "." + __webpack_require__.h() + ".hot-update.js";
    }
    
    
    

apply

终于到了最后一步热更新的操作,所有的代码逻辑都在internalApply

  • 删除过期的模块

    var queue = outdatedModules.slice();
    while (queue.length > 0) {
        moduleId = queue.pop();
        
        module = installedModules[moduleId];
        
        delete outdatedDependencies[moduleId];
    }
    
    
  • 将新的模块添加到更新列表,__webpack_require__执行相关模块的代码

    appliedUpdate[moduleId] = newModuleFactory;
     
    for (var updateModuleId in appliedUpdate) {
      if (__webpack_require__.o(appliedUpdate, updateModuleId)) {
          __webpack_require__.m[updateModuleId] = appliedUpdate[updateModuleId];
      }
    }
    
  • 执行hot._acceptedDependenciescallback

总结

上述我们通过八个步骤大致讲解了HMR的实现原理

  • 通过webpack-dev-server创建本地服务,修改entry入口,注入websocket客户端代码和热更新替换代码hot-serverbundle
  • webpack创建的时候会通过HotModuleReplacementPluginbundle中注入热更新代码
  • 通过compiler.watch开启文件监听,每一次编译完成触发compiler.hooks.done;监听compiler.hooks.done每次完成编译之后给客户端发送hash/ok事件
  • webpack/client接收到ok事件之后,通过eventEmitter佛那个送webpackHotUpdate事件
  • webpack/hot-server中会监听webpackHotUpdate事件,从而执行module.hot.check(HotModuleReplacementPlugin注入的代码)完成热更新操作

参考文章

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
webpack2025-03-05
webpack构建流程、基础配置、打包性能优化、热更新、和Vite区别
一、webpack构建流程 四个阶段: 初始化阶段 :读取配置文件、创建complier实例 编译阶段 :解析入口点、模块加载(loader) 打包阶段 :模块合并与优化、代码分割 输出阶段 :生成输出文件 1.配置文件解析: Webpack读取并解析webpack.config.js文件,获取构建所需的配置信息。 2.入口处理: Webpack从入口文件开始,构建模块依赖图。 3.模块加载与转换: 使用加载器处理不同类型的模块,如CS...
学习面试
webpack2025-03-04
深入解析 Webpack 的构建流程
1\. 初始化阶段:准备启动引擎 当你启动 时,它就像一个超级工厂的启动按钮,开始准备好一切——加载配置文件、初始化插件,并准备好开始生产。 实例:准备你的工厂配置 解释: 入口文件(Entry) :Webpack 从这里开始它的构建任务,像一个工厂的生产线入口。 输出路径(Output) :Webpack 会把构建好的产品放在这里。 就是每个产品的唯一标识,确保浏览器不会缓存过时的文件。 模块加载(Loaders) :当 Webpac...
学习面试
webpack2024-12-24
为什么组件库打包用 Rollup 而不是 Webpack
Rolup 是一个打包工具,类似 Webpack。 组件库打包基本都是用 Rollup。 那 Webpack 和 Rollup 有什么区别呢?为什么组件库打包都用 Rollup 呢? 我们来试一下: 我们创建两个模块: src/index.js src/utils.js 很简单的两个模块,我们分别用 rollup 和 webpack 来打包下: 安装 rollup: 创建 rollup.config.js 配置入口模块,打包产物的位置、...
学习
webpack2024-07-10
浅谈 Webpack5 之模块联邦
Webpack 模块联邦是一种先进的代码共享技术,它允许在多个独立构建的 Web 应用程序之间共享代码,而无需将这些代码提前发布到 npm 仓库或其他中央存储。 这项技术特别适用于微前端架构,因为它能让各个前端团队独立开发、部署自己的应用,同时还能无缝共享组件、库或功能模块。 通俗点说就是在微前端架构中每个应用都是独立的,独立开发,独立部署,在开发过程中避免不了有些模块是重复使用的,如何把这些重复的模块利用起来,一般情况下会通过 npm...
学习
webpack2024-04-07
Webpack 和 Rollup:您应该知道的一些事情
本文只做 Webpack 和 Rollup 的对比总结,不参入 esbuild、Vite 或其他工具的讨论 Webpack 和 Rollup 都是现在前端领域比较流行的打包工具,它们的相似之处在于可以帮助我们 把多个 JavaScript 文件打包成一个可以在浏览器中加载并在生产中使用的文件。     打包成单个文件的好处是可以减少网络请求、提高网站性能等目的。 这篇文章主要想通过对比的方式讲讲这两种打包工具的差异...
学习面试
AI2026-09-01
Deep Agents 01:何为 Agent Harness,以及如何开始
1、本篇任务:完成一份多步骤、带证据的技术调研 普通客服 Agent 的问题短、工具少、输出即时。技术调研或编码任务会持续很久,产生计划、搜索结果、文件和中间结论。Deep Agents 在 LangChain/LangGraph 之上预装规划、虚拟文件系统、上下文压缩和子 Agent,适合这类开放任务。 本课让 Agent 比较两种向量数据库,并交付一份可验证报告。 2、什么时候需要 Deep Agent 满足以下两项以上再考虑:任务...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录