浅析Webpack 热模块更换(Hot Moudle Replacement)的原理

378 阅读5分钟

基本概念

传统的页面开发在代码有所修改后,还需开发者手动刷新浏览器才能看到效果,为了提高开发效率, 一些Web开发框架和工具提供了更便捷的开发方式-只要检测到代码改动就会自动重新构建,然后触发网页刷新. 这种一般称为live reload。 webpack 在 live reload 的基础上又进了一步,可以让代码在网页不刷新的前提下得到最新的改动,甚至可以让我们不需要重新发起请求就能看到更新后的效果。这就是热模块更换(Hot Moudle Replacement,后续简称:HRM)功能。

开启 HRM

const webpack = require('webpack');
devServer: {
    contentBase: './dist',
    open: true,
    port: 3000,
    hot: true,
    hotOnly: true //配置可选
},
plugins: [
    new HtmlWebpackPlugin({
         template: 'src/index.html',
    }),
    new webpack.HotModuleReplacementPlugin() //配置热模块更新
]

上述代码通过 HotModuleReplacementPlugin 启用了 Hot Module Replacement, 则它的接口将被暴露在 module.hot 以及 import.meta.webpackHot 属性下。请注意,只有 import.meta.webpackHot 可以在 strict ESM 中使用。

通常,用户先要检查这个接口是否可访问, 再使用它。你可以这样使用 accept 操作一个更新的模块:

if (module.hot) {
  module.hot.accept('./counter.js', function() {
    // 对更新过的 counter 模块做些事情...
  });
}

// or
if (import.meta.webpackHot) {
  import.meta.webpackHot.accept('./counter.js', function () {
     // 对更新过的 counter 模块做些事情...
  });
}

模块API

module.hot 以及 import.meta.webpackHot下面,暴露了一些API, 借助这些API可以实现对特定模块开启或者关闭HRM,也可以添加热替换之外的逻辑等.

在日常开发中,大部分框架工具已经内置帮我们实现了调用模块API的方法,所以接触不到模块API

日常开发中:

  • React Hot Loader: 实时调整 react 组件。
  • Vue Loader: 此 loader 支持 vue 组件的 HMR,提供开箱即用体验。
  • Elm Hot webpack Loader: 支持 Elm 编程语言的 HMR。
  • Angular HMR: 没有必要使用 loader!直接修改 NgModule 主文件就够了,它可以完全控制 HMR API。
  • Svelte Loader: 此 loader 开箱即用地支持 Svelte 组件的热更新。

如果需要自己手动去修改调模块API的话,可以了解一下下面几个常用的模块API:

accept

接受(accept)给定 依赖模块(dependencies) 的更新,并触发一个 回调函数 来响应更新,除此之外,你可以附加一个可选的 error 处理程序:

module.hot.accept(
  dependencies, // 可以是一个字符串或字符串数组
  callback // 用于在模块更新后触发的函数
  errorHandler // (err, {moduleId, dependencyId}) => {}
);

// or
import.meta.webpackHot.accept(
  dependencies, // 可以是一个字符串或字符串数组
  callback, // 用于在模块更新后触发的函数
  errorHandler // (err, {moduleId, dependencyId}) => {}
);

decline

拒绝给定依赖模块的更新,使用 'decline' 方法强制更新失败。

module.hot.decline(
  dependencies // 可以是一个字符串或字符串数组
);

// or
import.meta.webpackHot.decline(
  dependencies // 可以是一个字符串或字符串数组
);

dispose

添加一个处理函数,在当前模块代码被替换时执行。此函数应该用于移除你声明或创建的任何持久资源。如果要将状态传入到更新过的模块,请添加给定 data 参数。更新后,此对象在更新之后可通过 module.hot.data 调用。

module.hot.dispose(data => {
  // 清理并将 data 传递到更新后的模块...
});

// or
import.meta.webpackHot.dispose((data) => {
  // 清理并将 data 传递到更新后的模块...
});

invalidate

调用此方法将使当前模块无效,而当前模块将在应用 HMR 更新时进行部署并重新创建。这个模块的更新像冒泡一样,拒绝自身更新。

import { x, y } from './dep';
import { processX, processY } from 'anotherDep';

const oldY = y;

processX(x);
export default processY(y);

module.hot.accept('./dep', () => {
  if (y !== oldY) {
    // 无法处理,冒泡给父级
    module.hot.invalidate();
    return;
  }
  // 可以处理
  processX(x);
});

removeDisposeHandler

删除由 dispose 或 addDisposeHandler 添加的回调函数。

module.hot.removeDisposeHandler(callback);
// or
import.meta.webpackHot.removeDisposeHandler(callback);

读者想了解更多详细的API webpack官网查看

HRM原理

原理大概是这样: 在本地开发环境下,浏览器是客户端,webpack-dev-server(WDS)相当于我们的服务端。HRM的核心就是客户端从服务端拉取更新后的资源(准确地说,HRM拉取的不是整个资源文件,而是chunk diff),即 chunk需要更新的部分。

具体步骤

第1步: 确定浏览器什么时候去拉取这些更新。

这需要WDS对本地资源文件进行监听,实际上WDS与浏览器之间维护了一个websocket,当本地资源发生变化时,WDS会向浏览器推送更新事件,带上这次构建的hash,让客户端与上次资源进行对比,通过对比hash可以防止冗余更新的出现。

当然websckot并不是开启了HRM才会有,live reload 其实也是依赖这个实现的

WX20220810-180040.png
  • 图1- websocket事件列表*

第2步: 有了恰当的拉取资源的时机,下一步就是知道拉取什么。

这部分信息并没有包含在websocket中,因为刚刚我们只是想知道这次构建的结果是不是和上次一样. 现在客户端已经知道新的构建结果和当前有所差别,那么它会想WDS发起一个请求来获取更改文件的列表, 即哪些模块有了改动。通常这个请求的名字为hash.hot-update.json。

WX20220810-181218.png
  • 图2- 请求chunk地址*

WX20220810-181251.png
  • 图3- WDS向浏览器的返回值*

浏览器借助返回的需要更新chunk的信息向WDS获取具体的新增更新数据

WX20220810-181330.png
  • 图4- URL中包含了需要更新的chunk信息*

WX20220810-181509.png
  • 图5- 返回具体的新增更新数据*

第3步: 获取后的处理

现在客户端已经获取到了chunks的更新,又遇到了一个非常重要的问题,即客户端获取这些增量更新之后要如何处理呢?哪些状态需要保留,哪些需要更新? 这些就不属于webpack的工作了,但是它提供了相关的API(module.hot.accept等),可以供开发者使用这些API针对自身场景进行处理。像react-hot-loader和vue-loader也都是借助这些API来实现的HRM。