原因

在说明什么是热更新之前,我们先说说为什么要使用热更新。

在我们打包之后,甚至使用了webpack-dev-server, 我们会发现,假如,我们在页面中填写了一些数据,然后我们修改了页面的样式,会发现,页面会刷新,填写的数据也会丢失。

所以,热更新的作用就是

  • 保留在完全重新加载页面时丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式

我们可以看下例子:
结构
image.png

  1. {
  2. "name": "webpack-demo",
  3. "version": "1.0.0",
  4. "description": "描述",
  5. "private": true,
  6. "scripts": {
  7. "start": "webpack-dev-server"
  8. },
  9. "author": "chenYongRen",
  10. "license": "ISC",
  11. "devDependencies": {
  12. "autoprefixer": "^10.4.7",
  13. "clean-webpack-plugin": "^4.0.0",
  14. "css-loader": "^6.7.1",
  15. "express": "^4.18.1",
  16. "file-loader": "^6.2.0",
  17. "html-webpack-plugin": "^5.5.0",
  18. "node-sass": "^7.0.1",
  19. "postcss-loader": "^7.0.0",
  20. "sass-loader": "^13.0.0",
  21. "style-loader": "^3.3.1",
  22. "url-loader": "^4.1.1",
  23. "webpack": "^5.73.0",
  24. "webpack-cli": "^4.9.2",
  25. "webpack-dev-middleware": "^5.3.3",
  26. "webpack-dev-server": "^4.9.3"
  27. },
  28. "dependencies": {},
  29. "browserslist": [
  30. "> 1%",
  31. "last 2 versions"
  32. ]
  33. }
  1. const path = require('path')
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const { CleanWebpackPlugin } = require("clean-webpack-plugin");
  4. module.exports = {
  5. mode: 'development',
  6. devtool: 'eval',
  7. entry: {
  8. main: './src/index.js'
  9. },
  10. devServer: {
  11. // 服务器启动在哪个文件夹下
  12. static: {
  13. directory: path.join(__dirname, 'dist'),
  14. },
  15. // 自动打开浏览器
  16. open: true,
  17. // 默认端口号
  18. port: 8080
  19. },
  20. module: {
  21. rules:[
  22. {
  23. test: /\.(jpg|png|gif)$/,
  24. use:{
  25. loader: 'url-loader',
  26. options: {
  27. name:'[name]_[hash].[ext]',
  28. outputPath: 'images/',
  29. limit: 2048 // 2kb
  30. }
  31. }
  32. },
  33. {
  34. test: /\.scss$/,
  35. use:[
  36. 'style-loader',
  37. {
  38. loader:'css-loader',
  39. options: {
  40. importLoaders: 2
  41. }
  42. },
  43. 'sass-loader',
  44. 'postcss-loader'
  45. ]
  46. },
  47. {
  48. test: /\.css$/,
  49. use:[
  50. 'style-loader',
  51. 'css-loader',
  52. 'postcss-loader'
  53. ]
  54. }
  55. ]
  56. },
  57. plugins:[new HtmlWebpackPlugin({
  58. template: 'src/index.html'
  59. }), new CleanWebpackPlugin()],
  60. output: {
  61. // 我所有打包生成的文件之间的引用,前面都加根路径
  62. // 其实一般不加也可以
  63. publicPath:'/',
  64. filename: '[name].js',
  65. path: path.resolve(__dirname, 'dist')
  66. }
  67. }
  1. import './style.css'
  2. var btn = document.createElement('button');
  3. btn.innerHTML = '新增';
  4. document.body.appendChild(btn);
  5. btn.onclick = function(){
  6. var div = document.createElement('div');
  7. div.innerHTML = 'item'
  8. document.body.appendChild(div)
  9. }
  1. div:nth-of-type(odd){
  2. background: yellow;
  3. }

效果
image.png
如果现在把style.css的背景改成blue,他会重新刷新。
image.png

💡但是webpack5不存在这个为题,就算不加任何配置,也能实现热更替的功能。 所以下面方法是针对webpack5以下的版本

在 webpack5以下的版本如果遇到页面的样式修改导致整个页面刷新的问题
就要设置热更新的配置,以及插件
hot: true,
hotOnly: true

并且在webpack5中
hotOnly已经废除。
直接使用hot:’only’

然后重启命令就可以生效了

  1. const path = require('path')
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const { CleanWebpackPlugin } = require("clean-webpack-plugin");
  4. // 引入webpack来使用热更新
  5. const webpack = require('webpack');
  6. module.exports = {
  7. mode: 'development',
  8. devtool: 'eval',
  9. entry: {
  10. main: './src/index.js'
  11. },
  12. devServer: {
  13. // 服务器启动在哪个文件夹下
  14. static: {
  15. directory: path.join(__dirname, 'dist'),
  16. },
  17. // 自动打开浏览器
  18. open: true,
  19. // 默认端口号
  20. port: 8080,
  21. // webpack devServer开启 hot Module Replacement模式
  22. hot: true,
  23. // 即便hmr的功能没有生效,也不让浏览器自动重新刷新
  24. hotOnly: true
  25. },
  26. module: {
  27. rules:[
  28. {
  29. test: /\.(jpg|png|gif)$/,
  30. use:{
  31. loader: 'url-loader',
  32. options: {
  33. name:'[name]_[hash].[ext]',
  34. outputPath: 'images/',
  35. limit: 2048 // 2kb
  36. }
  37. }
  38. },
  39. {
  40. test: /\.scss$/,
  41. use:[
  42. 'style-loader',
  43. {
  44. loader:'css-loader',
  45. options: {
  46. importLoaders: 2
  47. }
  48. },
  49. 'sass-loader',
  50. 'postcss-loader'
  51. ]
  52. },
  53. {
  54. test: /\.css$/,
  55. use:[
  56. 'style-loader',
  57. 'css-loader',
  58. 'postcss-loader'
  59. ]
  60. }
  61. ]
  62. },
  63. plugins:[new HtmlWebpackPlugin({
  64. template: 'src/index.html'
  65. }), new CleanWebpackPlugin(),
  66. // 热更新插件
  67. new webpack.HotModuleReplacementPlugin()
  68. ],
  69. output: {
  70. // 我所有打包生成的文件之间的引用,前面都加根路径
  71. // 其实一般不加也可以
  72. publicPath:'/',
  73. filename: '[name].js',
  74. path: path.resolve(__dirname, 'dist'),
  75. }
  76. }

但要说明一点,就是这里的是针对css样式修改,不会改js渲染出来的内容。

接下来我们再看看下面的例子
image.png
我们先把热更新关掉

  1. const path = require('path')
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const { CleanWebpackPlugin } = require("clean-webpack-plugin");
  4. // 引入webpack来使用热更新
  5. const webpack = require('webpack');
  6. module.exports = {
  7. mode: 'development',
  8. devtool: 'eval',
  9. entry: {
  10. main: './src/index.js'
  11. },
  12. devServer: {
  13. // 服务器启动在哪个文件夹下
  14. static: {
  15. directory: path.join(__dirname, 'dist'),
  16. },
  17. // 自动打开浏览器
  18. open: true,
  19. // 默认端口号
  20. port: 8080,
  21. // webpack devServer开启 hot Module Replacement模式
  22. // hot: 'only'
  23. },
  24. module: {
  25. rules:[
  26. {
  27. test: /\.(jpg|png|gif)$/,
  28. use:{
  29. loader: 'url-loader',
  30. options: {
  31. name:'[name]_[hash].[ext]',
  32. outputPath: 'images/',
  33. limit: 2048 // 2kb
  34. }
  35. }
  36. },
  37. {
  38. test: /\.scss$/,
  39. use:[
  40. 'style-loader',
  41. {
  42. loader:'css-loader',
  43. options: {
  44. importLoaders: 2
  45. }
  46. },
  47. 'sass-loader',
  48. 'postcss-loader'
  49. ]
  50. },
  51. {
  52. test: /\.css$/,
  53. use:[
  54. 'style-loader',
  55. 'css-loader',
  56. 'postcss-loader'
  57. ]
  58. }
  59. ]
  60. },
  61. plugins:[new HtmlWebpackPlugin({
  62. template: 'src/index.html'
  63. }), new CleanWebpackPlugin(),
  64. // 热更新插件
  65. // new webpack.HotModuleReplacementPlugin()
  66. ],
  67. output: {
  68. // 我所有打包生成的文件之间的引用,前面都加根路径
  69. // 其实一般不加也可以
  70. publicPath:'/',
  71. filename: '[name].js',
  72. path: path.resolve(__dirname, 'dist'),
  73. }
  74. }
  1. function counter(){
  2. var div = document.createElement('div')
  3. div.setAttribute('id','counter')
  4. div.innerHTML = 1;
  5. div.onclick = function(){
  6. div.innerHTML = parseInt(div.innerHTML, 10) + 1
  7. }
  8. document.body.appendChild(div)
  9. }
  10. export default counter;
  1. function number(){
  2. var div = document.createElement('div')
  3. div.setAttribute('id','number')
  4. div.innerHTML = 1000;
  5. document.body.appendChild(div)
  6. }
  7. export default number;
  1. // import './style.css'
  2. // var btn = document.createElement('button');
  3. // btn.innerHTML = '新增';
  4. // document.body.appendChild(btn);
  5. // btn.onclick = function(){
  6. // var div = document.createElement('div');
  7. // div.innerHTML = 'item'
  8. // document.body.appendChild(div)
  9. // }
  10. import counter from './counter'
  11. import number from './number'
  12. counter();
  13. number();

点击上面的数字会递增,但是如果这是我们修改number.js的数字
image.png
会直接清掉上面的数据
image.png

然后我们试试把热更新启动

  1. const path = require('path')
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const { CleanWebpackPlugin } = require("clean-webpack-plugin");
  4. // 引入webpack来使用热更新
  5. const webpack = require('webpack');
  6. module.exports = {
  7. mode: 'development',
  8. devtool: 'eval',
  9. entry: {
  10. main: './src/index.js'
  11. },
  12. devServer: {
  13. // 服务器启动在哪个文件夹下
  14. static: {
  15. directory: path.join(__dirname, 'dist'),
  16. },
  17. // 自动打开浏览器
  18. open: true,
  19. // 默认端口号
  20. port: 8080,
  21. // webpack devServer开启 hot Module Replacement模式
  22. hot: 'only'
  23. },
  24. module: {
  25. rules:[
  26. {
  27. test: /\.(jpg|png|gif)$/,
  28. use:{
  29. loader: 'url-loader',
  30. options: {
  31. name:'[name]_[hash].[ext]',
  32. outputPath: 'images/',
  33. limit: 2048 // 2kb
  34. }
  35. }
  36. },
  37. {
  38. test: /\.scss$/,
  39. use:[
  40. 'style-loader',
  41. {
  42. loader:'css-loader',
  43. options: {
  44. importLoaders: 2
  45. }
  46. },
  47. 'sass-loader',
  48. 'postcss-loader'
  49. ]
  50. },
  51. {
  52. test: /\.css$/,
  53. use:[
  54. 'style-loader',
  55. 'css-loader',
  56. 'postcss-loader'
  57. ]
  58. }
  59. ]
  60. },
  61. plugins:[new HtmlWebpackPlugin({
  62. template: 'src/index.html'
  63. }), new CleanWebpackPlugin(),
  64. // 热更新插件
  65. new webpack.HotModuleReplacementPlugin()
  66. ],
  67. output: {
  68. // 我所有打包生成的文件之间的引用,前面都加根路径
  69. // 其实一般不加也可以
  70. publicPath:'/',
  71. filename: '[name].js',
  72. path: path.resolve(__dirname, 'dist'),
  73. }
  74. }

我们可以发现,他虽然没有刷新,但是它也没有改变
image.png

如何解决这个问题。
这就需要module.hot来完成。

// import './style.css'

// var btn = document.createElement('button');
// btn.innerHTML = '新增';
// document.body.appendChild(btn);

// btn.onclick = function(){
//   var div = document.createElement('div');
//   div.innerHTML = 'item'
//   document.body.appendChild(div)
// }

import counter from './counter'
import number from './number'

counter();
number();

// 如果启动了热模块
if(module.hot){
  // 如果number这个文件发生变化,就会执行后面的函数
  module.hot.accept('./number', ()=>{
    number();
  })
}

image.png

image.png
当然这个写法只是给我们了解module.hot.accept的作用。
这样就没问题了

// import './style.css'

// var btn = document.createElement('button');
// btn.innerHTML = '新增';
// document.body.appendChild(btn);

// btn.onclick = function(){
//   var div = document.createElement('div');
//   div.innerHTML = 'item'
//   document.body.appendChild(div)
// }

import counter from './counter'
import number from './number'

counter();
number();

// 如果启动了热模块
if(module.hot){
  // 如果number这个文件发生变化,就会执行后面的函数
  module.hot.accept('./number', ()=>{
    document.body.removeChild(document.getElementById('number'))
    number();
  })
}

而且功能也实现了
image.png

我们这里的时候其实就可以发现,style样式修改我们只需要配置一下热模块就可以了,
但是为什么修改js文件的时候还要使用module.hot.accept这样的代码呢。

原因很简单:
因为module.hot.accept这样的代码,在css-loader里面它已经帮你编写好了,其实就是css-loader底层帮我们实现了这样的代码。

类似vue也是。

但是如果我们的项目之中,遇到比较偏的文件类型,比如数据文件,他们的loader里面并没有这样的内置效果时,就需要我们这样写。

参考

模块热替换
模块热替换配置
hmr底层原理