Webpack5学习 --- HMR

2,280 阅读6分钟

LiveReloading

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

在每次修改代码以后,我们都需要手动进行编译打包后才可以看到实际打包后的内容

这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成 编译 和 展示

实现方式有以下三种:

  1. webpack watch mode
  2. webpack-dev-server
  3. webpack-dev-middleware

webpack watch mode

webpack给我们提供了watch模式,在该模式下,webpack依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译,我们就我们不需要手动去运行 npm run build指令了

开启watch两种方式

  1. 在导出的配置中,添加 watch: true

webpack.config.js

odule.exports = {
  mode: 'development',
  watch: true,
  entry: './src/index.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  }
}
  1. 启动webpack的命令中,添加 --watch的标识

package.json

"scripts": {
  "build": "webpack",
  "watch": "webpack --watch"
}

webpack-dev-server

使用watch进行代码监听存在着一些问题:

  1. watch只能对代码进行实时编译,并不能开启本地服务,来运行我们的代码,

    让我们查看对应的执行结果,无法做到 live reloading (实时重新加载)

  2. 编译成功后,会形成新的打包文件夹,也就是会频繁操作file stystem,

    而实际上我们只要在项目全部编写完毕的时候再生产打包文件夹即可

  3. 使用watch方式进行代码编译的时候,会刷新整个页面,没有办法保留页面的一些状态

  4. watch方式编译文件,会对整个源代码进行编译,无论是否发生改变

我们可以使用webpack-dev-server(可以简称为WDR)来进行优化

安装

# 安装
npm i webpack-dev-server -D

执行

package.json

"scripts": {
  "serve": "webpack serve"
}

默认webpack-dev-server是运行在8080端口,如果不需要修改webpack-dev-server的相关配置,我们不需要在webpack.config.js中进行任何的配置,直接执行webpack serve即可, webpack会自动帮助我们调用webpack-dev-server

使用webpack-dev-server进行代码打包的过程中,是不会生成对应的打包文件夹的,因为webpack-dev-server内部使用了一个叫做memfs的库,可以将我们打包后的文件夹直接加载到内存中,而不需要频繁操作file system,从而提升编译效率

webpack-dev-middleware

默认情况下webpack-dev-server内部是使用express来搭建一个本地服务器,为我们开启本地服务。

但是有的时候,我们可能需要自定义对应的配置,此时可以使用webpack-dev-middleware

webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server

webpack-dev-server 在内部使用了webpack-dev-middleware

然而它也可以作为一个单独的 package 来使用,以便根据需求进行 更多自定义设置;

# 安装, 这里以express为例, 当然也可以使用别的工具 如koa
npm install --save-dev express webpack-dev-middleware

在项目根目录下,新建server.js,

const express = require('express')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpack = require('webpack')

// 创建服务器对象
const app = express()

// 使用webpack函数加载配置文件,生成一个webpack编译器
const complier = webpack(require('./webpack.config'))

// webpack-dev-middleware会将complire转换为一个中间件
const middleware = webpackDevMiddleware(complier)

// 使用中间件
app.use(middleware)

// 开启服务,此时就可以对该服务进行任何需要的定制
app.listen(3000, () => console.log('serve is running'))
# 执行
node server.js

HMR

HMR的全称是Hot Module Replacement,翻译为模块热替换

模块热替换是指在 应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面

HMR通过如下几种方式,来提高开发的速度:

  1. 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失
  2. 只更新需要变化的内容,节省编译的时间
  3. 修改了css、js源代码,会立即在浏览器更新,相当于直接在浏览器的devtools中直接修改样式

默认情况下,webpack-dev-server已经支持HMR,我们只需要开启即可;

在不开启HMR的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是live reloading

webpack.config.js

module.exports = {
  devServer: {
    hot: true
  }
}

此时我们修改了某一个模块的代码时,依然是刷新的整个页面

这是因为我们需要去指定哪些模块发生更新时,进行HM

index.js --- 主入口文件

if (module.hot) {
  // 参数1: 开启HMR的模块路径
  // 参数2; 在更新完对应模块后,需要执行的回调函数
  module.hot.accept('./math.js', () => console.log('math 模块发送了更新'))
  module.hot.accept('./foo.js', () => console.log('foo 模块发送了更新'))
}
if (module.hot) {
  // accept方法同样支持传递一个数组
  module.hot.accept(['./math.js', './foo.js'])
}

在框架中配置HMR

在开发其他项目时, 经常手动去写入 module.hot.accpet相关的API是十分麻烦的,然而事实上社区已经针对这些有很成熟的解决方案了:

比如vue开发中,我们使用vue-loader,此loader支持vue组件的HMR,提供开箱即用的体验

比如react开发中,有react-refresh,实时调整react组件 (ps: react-refresh只能在开发环境使用,如果在生产环境使用会报错)

React

在之前,React是借助于React Hot Loader来实现的HMR,目前已经改成使用react-refresh来实现了

# @pmmmwh/react-refresh-webpack-plugin 是有@pmmmwh前缀的, 名称什么的可以去github上搜索复制使用即可
# @pmmmwh/react-refresh-webpack-plugin 依赖于react-refresh这个库进行HMR
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh

配置

webpack.config.js

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

module.exports = {
  devServer: {
    hot: true
  }, 

  plugins: [
    new ReactRefreshWebpackPlugin()
  ]
}

babel.config.js

module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-react'
  ],

  plugins: [
    'react-refresh/babel'
  ]
}

Vue

# 安装
npm install vue-loader -D

webpack.config.js

Vue的加载我们需要使用vue-loader,而vue-loader加载的组件默认会帮助我们进行HMR的处理

// 最新版本的vue-loader已经不需要通过require('vue-loader/lib/plugin')的方式引入了
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  devServer: {
    hot: true
  },

  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      }
    ]
  },

  plugins: [
    new VueLoaderPlugin()  
  ]
}

HMR原理

webpack-dev-server会创建两个服务:提供静态资源的服务(express)和Socket服务(net.Socket)

express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)

HMR Socket Server,是一个socket的长连接:

  1. 长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端,而http请求必须要服务器主动发生请求)

  2. 当服务器监听到对应的模块发生变化时,

    会生成两个文件.json(manifest文件 主要记录了更新的位置信息等配置信息)和.js文件(update chunk 记录了实际更新的具体内容)

  3. 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器)

  4. 浏览器拿到两个新的文件后,通过HMR runtime机制(webpack在打包的时候提供),加载这两个文件,并且针对修改的模块进行更新

IAgiAk.png