webpack5 热更新从配置到原理

6,070 阅读4分钟

概述

项目里用到的刷新方案有两种,一般为了区分称为热更新和热重载

  • 热重载 : 顾名思义重新加载,使用 window.location.reload() 刷新界面以达到更新的目的
  • 热更新:只更新修改的文件。不会刷新界面。

Hot Module Replacement(或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需完全刷新。 - 官网

不管热重载和热更新都能很大程度提高我们的开发效率。下面将由浅入深的的梳理下热更新的知识点,而热重载相关知识比较简单,会在热更新的解释中提及。

了解热更新的价值

在我的平时工作中在了解热更新的机制后,在一下几个方面会有用到的机会。

  • 搭建自己的项目,在 webpack 配置中使用
  • 搭建自己的cli,将自己的 webpack 配置封装到cli中,参考 vue-cli
  • 搭建自己的SSR框架时,参考 Nuxt
  • 面试涨薪,走上人生巅峰。

热更新的实现

这边建议在看下面内容的时候先看一遍 官网 相关的知识,下面的内容都是以 官网 的信息为基础进行讲解的。

此文不赘述官网上面的相关知识了。下面主要介绍 通过 Node.js API 来搭建 HMR 的过程的拓展。

官网 是将 Webpack Dev ServerNode.js API 一起使用,来实现 HMR 的搭建的,为了更好的理解 HMR 的原理,这里我们使用 更深一步的 middleware pluginNode.js API 来实现 HMR

首先简单了解下将要用到的几种 middlewareplugin

  • webpack-hot-middleware: 该模块只关注将浏览器客户端连接到 webpack 服务器并接收更新的机制。
  • webpack-dev-middleware: 是一个容器(wrapper),它可以把webpack处理后的文件传递给一个服务器(server)。
  • webpack.HotModuleReplacementPlugin: 用于生成热更新的相关文件。

下面进行实操将这些 middlewareplugin 进行组合,以实现 HMR。首先我们基于官网的例子, 新建出以下所示的目录
具体代码的github地址

├─package.json
├─yarn.lock
├─src
|  ├─index.js - 同官网的例子
|  └print.js
├─server
|   └dev-server.js 
├─bundle
|   └webpack.config.js

package.json

		"express": "^4.17.1",
    "html-webpack-plugin": "^5.3.1",
    "webpack": "^5.39.1",
    "webpack-cli": "^4.7.2",
    "webpack-dev-middleware": "^5.0.0",
    "webpack-hot-middleware": "^2.25.0"

webpack.config.js

const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    mode: "development",
    entry: {
       index: './src/index.js',
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, '../dist'),
        clean: true,
    },
    plugins: [
      new HtmlWebpackPlugin(),
    ]
  };

dev-server.js

const config = require("../bundle/webpack.config"); 
const express = require('express');
const app = express();
const webpack = require("webpack");

// 当前的环境是开发环境
// 修改入口文件,用于热更新通信
config.entry.index = ["webpack-hot-middleware/client", config.entry.index];
// 用与生成热更新的 lastHash.hot-update.json 和 chunkID.lastHash.hot-update.js
config.plugins.push(new webpack.HotModuleReplacementPlugin());
// 准备好热更新的环境之后开始编译
const compiler = webpack(config);
// webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server。
const devMiddleware = require("webpack-dev-middleware")(compiler, {
    publicPath: config.output.publicPath,
    serverSideRender: true 
});

app.use(devMiddleware)

app.use(require("webpack-hot-middleware")(compiler));
compiler.hooks.done.tap("done", stats => {
    const info = stats.toJson();
    if (stats.hasWarnings()) {
        console.warn(info.warnings);
    }

    if (stats.hasErrors()) {
        console.error(info.errors);
        return;
    }
    console.log("打包完成");
});

app.listen(9527, () => {
    console.log("Your app is running", 9527);
});

这个时候我们就实现了类似官网的 HMR 下面我们就基于这个 demo 来浅谈下 HRM 的原理。

热更新的原理

热更新在编译阶段和界面上的体现

当我们编译的时候,可以直观的看到生成了
index.37b9afad78a811b0d250.hot-update.js
index.37b9afad78a811b0d250.hot-update.json
这两个与 HMR 相关的两个文件。
image.png
在浏览器上,发现发送了两个请求
image.png分别用来获取这两个文件。
返回的内容如下

index.37b9afad78a811b0d250.hot-update.json

{"c":["index"],"r":[],"m":[]}

index.37b9afad78a811b0d250.hot-update.js


self["webpackHotUpdateuse"]("index", {
    "./src/print.js":
    ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        __webpack_require__.d(__webpack_exports__, {
            "default": () => (/* binding */ printMe)});
            function printMe() {
                console.log('Updating print.js2..');
            }//# sourceURL=webpack://use/./src/print.js?");
    })},
     function(__webpack_require__) {
     "use strict";
        (() => {
            __webpack_require__.h = () => ("f4e40048658a6c48ff32")
        })();
     }
   );

看到上面的现象之后,大概有许多疑惑,下面带着这些疑惑继续往下看。

热更新的原理讲解

带着上面的问题,我们来探索热更新的意义,首先我们看下热更新的整体流程
无标题-2021-03-29-1729.png

  1. 首先启动 dev-server, 往当前的 options.entry 中插入相关的 SSE 相关的代码。

注入 HTML 5 服务器发送事件 ( SSE, server-sent event ) ,和 心跳检测的相关机制,主要是为了服务器和客户端的通信,当服务器文件发生变化的时候能够方便通知到客户端。

  1. 监听 webpack donecompiler.hooks.done.tap 节点从 stats 中获取到最新的 hash 值,传递给客户端,如果只是做热重载到这步就可以结束了,使用 window.location.reload();
  2. 客户端获取 lastHash.hot-update.json 文件,获取到当前更新了的文件的 chunkId 。
  3. 根据当前更新的 chunkId 去拿 chunkID.lastHash.hot-update.js 文件
  4. 执行 webpackHotUpdate, 从 现有的 cache 中 找到当前的 chunkId 对应的旧的数据信息,进行更新并执行当前最新的 chunk 代码,以便于更新 cache,接着执行对应的 hot.accept 代码来实现 render 操作。

总结

本文没有使用 webpackDevServer,其实 webpackDevServer 就是内置了 Webpack-dev-middlewareExpress 服务器,以及利用 websocket 替代 SSE 来实现webpack-hot-middleware 的逻辑。原理的部分也是帮助大家从整体上认识了 HMR。希望通过这篇文章,可以将热更新活学活用,如果需要了解每一步的具体细节,可以按照这个思路去看看源码。