神族九帝's blog 神族九帝's blog
首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)

神族九帝,永不言弃

首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)
  • 工作笔记

  • 前端基础建设与架构 30 讲

  • vue2源码学习

  • 剖析vuejs内部运行机制

  • TypeScript 入门实战笔记

  • vue3源码学习

  • 2周刷完100道前端优质面试真题

  • 思维导图

  • npm发包

  • 重学node

  • 前端性能优化方法与实战

  • webpack原理与实战

    • 开篇词:Webpack 现代化前端应用的基石
    • 第01讲:Webpack 究竟解决了什么问题?
    • 第02讲:如何使用 Webpack 实现模块化打包?
    • 第03讲:如何通过 Loader 实现特殊资源加载?
    • 第04讲:如何利用插件机制横向扩展 Webpack 的构建能力?
    • 第05讲:探索 Webpack 运行机制与核心工作原理
    • 第06讲:如何使用 Dev Server 提高你的本地开发效率?
    • 第07讲:如何配置 Webpack SourceMap 的最佳实践?
    • 第08讲:如何让你的模块支持热替换(HMR)机制?
    • 第09讲:Tree Shaking 和 sideEffects
    • 第10讲:Code Splitting(分块打包)
    • 第11讲:如何优化Webpack的构建速度和打包结果
    • 第12讲:如何选择打包工具:Rollup 和 Webpack ?
    • 第13讲:如何使用 Parcel 零配置完成打包任务?
    • 加餐:Vue3 到底带来了哪些变化
  • webGl

  • 前端优化

  • Web3

  • React

  • 更多

  • 笔记
  • webpack原理与实战
wu529778790
2021-11-09

第08讲:如何让你的模块支持热替换(HMR)机制?

模块热替换(HMR)

开启 HMR

使用这个特性最简单的方式就是,在运行 webpack-dev-server 命令时,通过 --hot 参数去开启这个特性。

或者也可以在配置文件中通过添加对应的配置来开启这个功能。那我们这里打开配置文件,这里需要配置两个地方:

  • 首先需要将 devServer 对象中的 hot 属性设置为 true;
  • 然后需要载入一个插件,这个插件是 webpack 内置的一个插件,所以我们先导入 webpack 模块,有了这个模块过后,这里使用的是一个叫作 HotModuleReplacementPlugin 的插件。

具体配置代码如下:

20211109213919

配置完成以后,我们打开命令行终端,运行 webpack-dev-server,启动开发服务器。那接下来你就可以来体验 HMR 了。

我们回到开发工具中,这里我们先来尝试修改一下 CSS 文件。样式文件修改保存过后,确实能够以不刷新的形式更新到页面中。

然后我们再来尝试一下修改 JS 文件。保存过后你会发现,这里的页面依然自动刷新了,好像并没有之前所说 HMR 的体验。

为了再次确认,你可以尝试先在页面中的编辑器里随意添加一些文字,然后修改代码,保存过后你就会看到页面自动刷新,页面中的状态也就丢失了

那这是为什么呢?为什么 CSS 文件热替换没出现问题,而到了 JS 这块就不行了呢?我们又该如何去实现其他类型模块的热替换呢?

HMR 的疑问

通过之前的体验我们发现模块热替换确实提供了非常友好的体验,但是当我们自己去尝试开启 HMR 过后,效果却不尽如人意。

很明显:HMR 并不像 Webpack 的其他特性一样可以开箱即用,需要有一些额外的操作。

具体来说,Webpack 中的 HMR 需要我们手动通过代码去处理,当模块更新过后该,如何把更新后的模块替换到页面中。

Q1:可能你会问,为什么我们开启 HMR 过后,样式文件的修改就可以直接热更新呢?我们好像也没有手动处理样式模块的更新啊?

A1:这是因为样式文件是经过 Loader 处理的,在 style-loader 中就已经自动处理了样式文件的热更新,所以就不需要我们额外手动去处理了。

Q2:那你可能会想,凭什么样式就可以自动处理,而我们的脚本就需要自己手动处理呢?

A2:这个原因也很简单,因为样式模块更新过后,只需要把更新后的 CSS 及时替换到页面中,它就可以覆盖掉之前的样式,从而实现更新。

而我们所编写的 JavaScript 模块是没有任何规律的,你可能导出的是一个对象,也可能导出的是一个字符串,还可能导出的是一个函数,使用时也各不相同。所以 Webpack 面对这些毫无规律的 JS 模块,根本不知道该怎么处理更新后的模块,也就无法直接实现一个可以通用所有情况的模块替换方案。

那这就是为什么样式文件可以直接热更新,而 JS 文件更新后页面还是回退到自动刷新的原因。

Q3:那可能还有一些平时使用 vue-cli 或者 create-react-app 这种框架脚手架工具的人会说,“我的项目就没有手动处理,JavaScript 代码照样可以热替换,也没你说的那么麻烦”。

A3:这是因为你使用的是框架,使用框架开发时,我们项目中的每个文件就有了规律,例如 React 中要求每个模块导出的必须是一个函数或者类,那这样就可以有通用的替换办法,所以这些工具内部都已经帮你实现了通用的替换操作,自然就不需要手动处理了。

当然如果你之前没有接触过这样的工具,那你可以忽略这一条,这也并不影响后面的理解。

综上所述,我们还是需要自己手动通过代码来处理,当 JavaScript 模块更新过后,该如何将更新后的模块替换到页面中。

HMR APIs

HotModuleReplacementPlugin 为我们的 JavaScript 提供了一套用于处理 HMR 的 API,我们需要在我们自己的代码中,使用这套 API 将更新后的模块替换到正在运行的页面中。

接下来我们回到代码中,尝试通过 HMR 的 API 手动处理模块更新后的热替换。

这里我们打开 main.js,具体代码如下:

20211109214212

这是 Webpack 打包的入口文件,正常情况下,在这个文件中会加载一些其他模块。正是因为在 main.js 中使用了这些模块,所以一旦这些模块更新了过后,我们在 main.js 中就必须重新使用更新后的模块。

所以说,我们需要在这个文件中添加一些额外的代码,去处理它所依赖的这些模块更新后的热替换逻辑。

对于开启 HMR 特性的环境中,我们可以访问到全局的 module 对象中的 hot 成员,这个成员是一个对象,这个对象就是 HMR API 的核心对象,它提供了一个 accept 方法,用于注册当某个模块更新后的处理函数。accept 方法第一个参数接收的就是所监视的依赖模块路径,第二个参数就是依赖模块更新后的处理函数。

那我们这里先尝试注册 ./editor 模块更新过后的处理函数,第一个参数就是 editor 模块的路径,第二个参数则需要我们传入一个函数,然后在这个函数中打印一个消息,具体代码如下:

20211109214312

完成过后,我们打开命令行终端再次启动 webpack-dev-server 命令,然后回到浏览器,打开开发人员工具。

此时,如果我们修改了 editor 模块,保存过后,浏览器的控制台中就会自动打印我们上面在代码中添加的消息,而且浏览器也不会自动刷新了。

那也就是说一旦这个模块的更新被我们手动处理了,就不会触发自动刷新;反之,如果没有手动处理,热替换会自动 fallback(回退)到自动刷新。

JS 模块热替换

了解了这个 HMR API 的作用过后,接下来需要考虑的就是:具体如何实现 editor 模块的热替换。

这个模块导出的是一个 createEditor 函数,我们先正常把它打印到控制台,然后在模块更新后的处理函数中再打印一次,具体代码如下:

20211109214345

这个时候如果你再次修改 editor 模块,保存过后,你就会发现当模块更新后,我们这里拿到的 createEditor 函数也就更新为了最新的结果,具体结果如下图所示:

20211109214421

既然模块文件更新后 createEditor 函数可以自动更新,那剩下的就好办了。我们这里使用 createEditor 函数是用来创建一个界面元素的,那模块一旦更新了,这个元素也就需要重新创建,所以我们这里先移除原来的元素,然后再调用更新后的 createEditor 函数,创建一个新的元素追加到页面中,具体代码如下:

20211109214453

但如果只是这样实现的话,一次热替换结束后,第二次就没法再实现热替换了。因为第二次执行这个函数的时候,editor 变量指向的元素已经在上一次执行时被移除了,所以我们这里还应该记录下来每次热替换创建的新元素,以便于下一次热替换时的操作,具体代码如下:

20211109214514

完成以后,我们再来尝试修改 editor 模块,此时就应该是正常的热替换效果了,具体效果如下图:

CgqCHl67uamADY97AA4Ht2ooOXA786

热替换的状态保持

此时,如果我们尝试在界面上输入一些内容(形成页面操作状态),然后回到代码中再次修改 editor 模块。那此时你仍然会发现问题,由于热替换时,把界面上之前的编辑器元素移除了,替换成了一个新的元素,所以页面上之前的状态同样会丢失。

这也就证明我们的热替换操作还需要改进,我们必须在替换时把状态保留下来。

我们回到 main.js 中,要想保留这个状态也很简单,就是在替换前先拿到编辑器中的内容,然后替换后在放回去就行了。那因为我这里使用的是可编辑元素,而不是文本框,所以我们需要通过 innerHTML 拿到之前编辑的内容,然后设置到更新后创建的新元素中,具体代码如下:

20211109214633

这样就可以解决界面状态保存的问题了。

至此,对于 editor 模块的热替换逻辑就算是全部实现了。通过这个过程你应该能够发现,为什么 Webpack 需要我们自己处理 JS 模块的热更新了:因为不同的模块有不同的情况,不同的情况,在这里处理时肯定也是不同的。就好像,我们这里是一个文本编辑器应用,所以需要保留状态,如果不是这种类型那就不需要这样做。所以说 Webpack 没法提供一个通用的 JS 模块替换方案。

图片模块热替换

相比于 JavaScript 模块热替换,图片的热替换逻辑就简单多了,这里我们快速来看一下。

我们同样通过 module.hot.accept 注册这个图片模块的热替换处理函数,在这个函数中,我们只需要重新给图片元素的 src 设置更新后的图片路径就可以了。因为图片修改过后图片的文件名会发生变化,而这里我们就可以直接得到更新后的路径,所以重新设置图片的 src 就能实现图片热替换,具体代码如下:

20211109214716

常见问题

如果你刚开始使用 Webpack 的 HMR 特性,肯定会遇到一些问题,接下来我分享几个最容易发生的问题。

第一个问题,如果处理热替换的代码(处理函数)中有错误,结果也会导致自动刷新。例如我们这里在处理函数中故意加入一个运行时错误,代码如下:

20211109214739

直接测试你会发现 HMR 不会正常工作,而且根本看不到异常,效果如下图:

Ciqc1F67ubeAT4knAA-uyhsCyD0801

这是因为 HMR 过程报错导致 HMR 失败,HMR 失败过后,会自动回退到自动刷新,页面一旦自动刷新,控制台中的错误信息就会被清除,这样的话,如果不是很明显的错误,就很难被发现。

在这种情况下,我们可以使用 hotOnly 的方式来解决,因为现在使用的 hot 方式,如果热替换失败就会自动回退使用自动刷新,而 hotOnly 的情况下并不会使用自动刷新。

我们回到配置文件中,这里我们将 devServer 中的 hot 等于 true 修改为 hotOnly 等于 true,具体代码如下:

20211109214840

配置完成以后,重新启动 webpack-dev-server。此时我们再去修改代码,无论是否处理了这个代码模块的热替换逻辑,浏览器都不会自动刷新了,这样的话,热替换逻辑中的错误信息就可以直接看到了,具体效果如下图:

20211109214859

第二个问题,对于使用了 HMR API 的代码,如果我们在没有开启 HMR 功能的情况下运行 Webpack 打包,此时运行环境中就会报出 Cannot read property 'accept' of undefined 的错误,具体错误信息如下:

20211109214925

原因是 module.hot 是 HMR 插件提供的成员,没有开启这个插件,自然也就没有这个对象。

解决办法也很简单,与我们在业务代码中判断 API 兼容一样,我们先判断是否存在这个对象,然后再去使用就可以了,具体代码如下:

20211109214945

除此之外,可能你还有一个问题:我们在代码中写了很多与业务功能本身无关的代码,会不会对生产环境有影响?

那这个问题的答案很简单,我通过一个简单的操作来帮你解答,我们回到配置文件中,确保已经将热替换特性关闭,并且移除掉了 HotModuleReplacementPlugin 插件,然后打开命令行终端,正常运行一下 Webpack 打包,打包过后,我们找到打包生成的 bundle.js 文件,然后找到里面 main.js 对应的模块,具体结果如下图:

20211109215012

你会发现之前我们编写的处理热替换的代码都被移除掉了,只剩下一个 if (false) 的空判断,这种没有意义的判断,在压缩过后也会自动去掉,所以根本不会对生产环境有任何影响。

写在最后

以上就是我们对 Webpack 模块热替换特性做的一些探索,整体下来可能你会觉得 HMR 比较麻烦,需要写一些额外的代码,甚至觉得不如不用。

我个人的看法是利大于弊,这个道理就像是为什么现在的开发者都愿意写单元测试一样,对于长期开发的项目而言,这点额外的工作不算什么,而且如果你能为自己的代码设计出一些规律,那你也可以实现一个通用替换方案。

那当然,如果你是使用 React 或者 Vue.js 这类的框架开发,那么使用 HMR 功能会更加简单,因为大部分框架都有成熟的 HMR 方案,你只需要使用就可以了。但是如果你是使用纯原生 JavaScript 开发,那 HMR 功能使用起来相对就会麻烦一点。这也正是为什么大部分人都喜欢选择集成式框架的原因。

关于框架的 HMR,因为在大多数情况下是开箱即用的,所以这里不做过多介绍,详细可以参考:

  • React HMR 方案;
  • Vue.js HMR 方案。

编辑 (opens new window)
上次更新: 2025/03/11, 11:29:39
第07讲:如何配置 Webpack SourceMap 的最佳实践?
第09讲:Tree Shaking 和 sideEffects

← 第07讲:如何配置 Webpack SourceMap 的最佳实践? 第09讲:Tree Shaking 和 sideEffects→

最近更新
01
Code Review
10-14
02
ComfyUI
10-11
03
vscode插件开发
08-24
更多文章>
Power by vuepress | Copyright © 2015-2025 神族九帝
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×