Webpack 热更新以及原理

佛系码 等级 442 0 0

什么是热更新

模块热替换(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,欢迎关注「前端杂货铺」,一起学习成长吧');  
}  

浏览器会去请求两个文件

Webpack 热更新以及原理

接下来我们看看这两个文件:

  • 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 ServerBundle Server 可以让浏览器访问到我们打包出来的文件

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

文件热更新阶段

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

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

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

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) {  
            // 启动 express 服务之后,启动 websocket 服务  
            this.createSocketServer();  
          }  
        });  
      })  
  );  
}  
createSocketServer() {  
  this.socketServer = new this.SocketServerImplementation(this);  

  this.socketServer.onConnection((connection, headers) => {  

  });  
}  

HMR Server 和 HMR 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.jsHMR 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 版本不一样导致的,不影响理解)至于生成的代码是如何生效的,请移步我的另外一篇文章——【Webpack 进阶】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 热更新的简单使用、相关的流程以及原理。小结一下,webpack 如果开启了热更新的时候

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

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

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

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

Webpack 热更新以及原理

收藏
评论区

相关推荐

用 webpack 4.0 撸单页/多页脚手架 (jquery, react, vue, typescript)
1.导语         首先来简单介绍一下webpack:现代 JavaScr
Webpack 5.0 初体验
<p style"textalign: center;"<img class"rich_pages" dataratio"0.66640625" datas"300,640" src"https://usergoldcdn.xitu.io/2020/4/12/1716ebf44174dac8?w1080&h720&fjpeg&s
Webpack 打包资源篇
<section datatools"新媒体排版" dataid"13439" datastyletype"9" style"visibility: visible;"<p style"maxwidth: 100%; visibility: visible;"<br mpafromtpl"t" style"visibility
开发 vue3 + ts + vite / webpack 的管理系统
cooladmin 地址 演示 账户:admin 密码:123456 介绍 由来cooladmin自 2018 年初以来,一直本着快速通用开发为理念,同时看重每个页面的设计,无论在代码上还是 UI 设计上都受到广大开发者的喜爱。cool是酷的意思,admin一般的认知是
安装 | Vue3中文文档
安装Vue.js 设计的初衷就包括可以被渐进式地采用。这意味着它可以根据需求以多种方式集成到一个项目中。将 Vue.js 添加到项目中有三种主要方式。1. 在页面上以 的形式导入。2. 使用 安装它。3. 使用官方的 来构建一个项目,它为现代前端工作流程提供了功能齐备的构建设置 (例如,热重载、保存时的提示等等)。每个版本的详细发行说明可在
Vue进阶(幺柒柒):Vue应用Sass、Scss、Less和Stylus
Vue项目中使用预处理器,可以有效减少css代码量, 推荐使用Sass、Scss、Less预处理器。可以在创建项目的时候选择预处理器 (Sass/Less/Stylus)。如果当时没有选择,内置的 webpack 仍然会被预配置为可以完成所有的处理,也可以手动安装相应的 webpack loader: Sassnpm
Webpack 热更新以及原理
什么是热更新模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新一般的刷新我们分两种: 一种是页面刷新,不保留页面状态,就是简单粗暴,直接 window.location.reload()。 另一种是基于 WDS (Webpackde
7 个实战技巧帮你提升前端技术水平
项目架构 1.封装项目的基础库优秀的基础库可以保证项目的最低质量下限和更好的可扩展性。通常我们说的基础库包括组件库、基础 css 库、基础工具库。 2.层级管理管理你的请求,建议把你项目的 api 层独立出来为一个层级管理,这样有利于在复杂接口下,更好的管理,降低复杂度。 性能优化 1.缓存缓存可以减少请求,加快速度。比如从商品列表跳往详情页,可以用 coo
React.js中JSX的原理与关键实现
在开始开发之前,我们需要创建一个空项目文件夹。安装1. 初始化 npm init y 2.安装webpack相关依赖npm install webpack webpackcli D 3. 安装babelloader相关依赖 npm install babelloader @babel/core @babel/presetenv D 4.
Vue3.0系列——「vue3.0学习手册」第一期
Vue3.0一、项目搭建vite是尤大大开发的一款意图取代webpack的工具。其实现原理是利用ES6的import发送请求加载文件的特性。拦截这些请求,做一些编译,省去webpack冗长的打包时间。并将其与Rollup捆绑在一起用于生产。在开发过程中没有捆绑。源代码中的ES Import语法直接提供给浏览器,浏览器通过本机<script module支持对
Webpack学习整理集锦【从最基础的demo入手,自己实现一个脚手架 】
前言本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。开源网址https://github.com/maomi
webpack 基本配置
概念本 质上,webpack是一个现代 JavaScript 应用程序的 静态模块打包器(module bundler) 。当 webpack处理应用程序时,它会递归地构建一个 依 赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle 。 安装确保安装了nodejs 项目文件环境cd
Next.js 11 的 一些新特性
作者: ✏️在6 月 15 日举行的上,来自世界各地的开发者,共同见证了 Vercel 团队最新版本的发布。Next.js 11 中包含的更新和新功能对已经流行的库进行了重大改进。在本文中,我们将深入研究其中一些对用户和开发人员体验都产生影响的更新。让我们开始吧!一致性就其核心而言,一致性是一组原则或编码指南,它将一些决策责任从开发人员手中夺走。通过设置默认
vscode+vue简单安装教程
1、安装vscode、node.js。2、打开vscode终端,全局安装vuecli:npm install g vuecli用于构建项目。3、继续安装webpack(打包工具):npm install g webpack。4、安装完成创建一个文件夹用于存放项目,比如myvue,cd到该文件夹,使用项目创建命令:vue init webpack myvue。
Vue 从安装到创建项目
1.安装Node可以直接在官网中下载安装默认自动安装Node和NPM(Node Package Manager) 完成后检查安装版本:node v npm v2.安装webpack webpack全局安装npm install webpack g3.安装vue脚手架 全局安装脚手架3npm install @vue/cli g 备注

热门文章

Redis实现分布式锁Vue3现状报告及未来规划日常必备的JS工具函数大全

最新文章

日常必备的JS工具函数大全Vue3现状报告及未来规划Redis实现分布式锁