聊聊 Webpack 热更新以及原理

作者:微信小助手

发布时间:2021-04-17T11:57:43

什么是热更新

模块热替换(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: {
    HotModuleReplacementPluginnew webpack.HotModuleReplacementPlugin()
}

再设置一下 devServer

devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    hottrue// 重点关注
    historyApiFallbacktrue,
    compresstrue
}
  • 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", {
      valuetrue,
    });

    exports.default = void 0;

    var _default = {
      datafunction data({
        return {};
      },
      computed: {},
      methods: {
        clickMefunction 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 ServerBundle Server 可以让浏览器访问到我们打包出来的文件

下面流程图中的 1、2、A、B阶段

文件热更新阶段

文件经过 Webpack-complier 编译好后传输给 HMR ServerHMR Server 知道哪个资源(模块)发生了改变,并通知 HMR Runtime 有哪些变化(也就是上面我们看到的两个请求),HMR Runtime 就会更新我们的代码,这样我们浏览器就会更新并且不需要刷新

下面流程图的 1、2、3、4、5 阶段

参考 19 | webpack中的热更新及原理分析

深入——源码阅读

我们还看回上图,其中启动阶段图中的 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 接受到变更之后,如何生效的

以下的源码解析分别对应的版本是:

  • webpack—— 5.24.3
  • webpack-dev-server—— 4.0.0-beta.0
  • webpack-dev-middleware—— 4.1.0

启动 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) {