Webpack实时刷新与模块热替换(HMR)

1. 背景

公司前端项目从webpack1.X 升级到webpack2.X,加之技术需求和业务需求增长过快,开发人员没有充足时间深入学习webpack相关技术栈,导致很多配置失效、冗余或者无法辨别究竟有什么用途。

此外,我们基于docker-machine封装了vue、webpack、nodejs等前端开发环境到docker image,docker-machine本身基于Virtual Box,这种虚拟机+Docker的方式又挖了一把大坑。详情见:Docker搭建前端开发环境

综上,几种情况导致我们在docker中使用webpack-dev-server,没办法实时刷新,但在本机开发时却可以。

启动命令为:

1
webpack-dev-server --devtool inline-source-map --progress --color  --watch

部分webpack配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var webpack = require('webpack');
// ...
// var BrowserSyncPlugin = require('browser-sync-webpack-plugin');
var LiveReloadPlugin = require('webpack-livereload-plugin');

var config = {
entry: //...
output: {
path: path.resolve(__dirname, '../'),
filename: //...
chunkFilename: //...
},
module: {
rules: [{
test: /\.vue$/,
use: 'vue-loader'
}]
//...
},
plugins: [
// new webpack.HotModuleReplacementPlugin(), //热更新插件
(prod || test) ? function() {} : new LiveReloadPlugin({
appendScriptTag: true,
}),
//...
],
//...
}

一副很有历史痕迹的配置文件,留下了几个问题:

  • 问题一:在虚拟机或Docker下为什么不能实时刷新
  • 问题二:看上去功能重复的插件如BrowserSyncPlugin、LiveReloadPlugin、HotModuleReplacementPlugin究竟有什么区别,为什么会被注释

2. 关于查找资料

2.1 百度搜索

很多情况下,国内开发同学习惯在百度搜索相关问题,不可否认,百度搜索的中文问题有时候确实能比较快的找到答案,但在我们这个问题上,通过“webpack”,“自动刷新”,“HMR”等关键词中,找到几篇文章,但都存在一些问题,节选如下:

上面搜索的结果大部分是Webpack相关,除了版本对不上之外,还没法解决我们的问题。当然可以换新的关键词如“webpack 虚拟机 自动刷新”,效果基本和上面一样存在问题。

2.2 Google搜索

百度不行上google,在google通过“webpack hot reload vue virtual machine”等关键词搜到的结果一般来自于stackoverflow、github。一般github的问题会带项目的版本号,至少不会看岔了版本。stackoverflow比较专业的问题也会留下项目版本,操作系统等信息。最后在下面两篇文章中找到了“自动刷新”问题的答案:

Google搜索相对成本比较大,一方面是“技术”成本,另一方面是语言成本。虽然问题能解决,但对于知识体系的梳理并没有特别的帮助。偿还技术债归根到底还是要去看官方文档。

2.3 官方文档

这里列举的官方文档总共有三份,后面章节所有的介绍都基于这三份文档:

注意文档分为六部分,可以在导航栏切换。

5

3. 解决实时刷新

3.1 前期准备

开始解决问题之前,我们先做一下配置最小化,也就是把不清楚的,没有用的配置项去掉,减少干扰。最初的配置文件修改如下:

主要彻底删除了三个插件:browser-sync-webpack-plugin、webpack-livereload-plugin和HotModuleReplacementPlugin相关的所有配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var webpack = require('webpack');
// 省略...

var config = {
entry: //...
output: {
path: path.resolve(__dirname, '../'),
filename: //...
chunkFilename: //...
},
module: {
rules: [{
test: /\.vue$/,
use: 'vue-loader'
}]
//...
},
plugins: [
//...
],
//...
}

3.2 webpack、webpack-dev-server属性操作方式

在官方文档中,webpack和webpack-dev-server的属性一般有两种操作方式,一种是CLI也就是命令行方式,另一种是修改配置的方式

  • CLI方式只需在命令行中用双横杠加属性即可操作属性值

    1
    webpack --progress=true --color=false --watch
  • CLI里面的每一个属性默认值为true,也就是说上面的例子“–watch”等同于“–watch=true”

  • 配置方式需要修改webpack.config文件,下面通过配置方式启用webpack的watch模式

    1
    2
    3
    4
    5
    var config = {
    entry: xx,
    watch:true, //添加watch
    //...
    }
  • CLI和配置方式二选一,不要同时使用,以免造成配置混乱,可读性差

  • CLI和配置方式同时使用时,CLI的优先级高,会盖掉配置文件的配置

3.3 watch与watch-poll

3.3.1 查询文档

https://webpack.js.org/configuration/watch/ 找到webpack的watch属性介绍:

6

  • watch属性用来监听文件变化
  • webpack-dev-server默认开启watch
  • 如果watch模式不起作用(watch对于NFS文件系统和虚拟机没有作用),可以尝试watchOptions.poll模式

同时在https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-中找到了关于webpack-dev-server的watch属性介绍:

7

  • webpack用文件系统通知文件变化。如果使用NFS系统也就是网络文件系统,即网络磁盘、共享盘等,包括虚拟机,这些情况下用watch没有效果,需要打开watchOptions.poll模式

通过上面的信息,我们知道了由于虚拟机不能使用watch通知文件变化,需要用poll这种主动轮询的方式通知。这也许能解决我们的问题。

3.3.2 webpack如何使用watch和watch-poll

  • webpack命令默认没有启动watch,如果需要打开watch,两种方式

    • CLI命令行下添加watch属性

      1
      webpack --watch
    • 或者在配置文件中添加配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      var webpack = require('webpack');
      //...
      var config = {
      entry: xx,
      watch:true, //添加watch
      output: {
      path: path.resolve(__dirname, '../'),
      //...
      },
      //...
      }
  • 如果在虚拟机下用webpack,需要使用watch poll模式,两种方式

    • CLI命令行下添加watch–poll属性

      1
      webpack --watch-poll
    • 或者在配置文件中添加配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      var webpack = require('webpack');
      //...
      var config = {
      entry: xx,
      watchOptions: {
      poll: true
      }, //添加watchOptions节点,设置poll为true
      output: {
      path: path.resolve(__dirname, '../'),
      //...
      },
      //...
      }

3.3.3 webpack-dev-server如何使用watch和watch-poll

  • 因为我们使用的是webpack-dev-server,又因为webpack-dev-server默认开启watch,所以我们没有必要再做任何配置

  • 如果我们在虚拟机下用是webpack-dev-server,那就需要watch poll模式,两种方式

    • CLI命令行下添加watch–poll属性

      1
      webpack-dev-server --watch-poll 
    • 或者在配置文件中添加配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      var webpack = require('webpack');
      //...
      var config = {
      entry: xx,
      devServer:{
      watchOptions: {
      poll: true
      }
      }, //添加devServer节点,添加watchOptions子节点,设置poll为true
      output: {
      path: path.resolve(__dirname, '../'),
      //...
      },
      //...
      }

3.3.4 watch-poll并没有完全解决问题

经过这一番折腾,本以为我们把启动命令从

1
webpack-dev-server --devtool inline-source-map --progress --color  --watch

改成

1
webpack-dev-server --devtool inline-source-map --progress --color  --watch-poll

就能解决【问题一】,然而结果事与愿违。还有其他哪里不对吗?

在Chrome的控制台中,看到下面的信息:

8

8080端口是webpack-dev-server的默认端口,从图中可以得知“[WDS] Disconnected!”,也就是说WebpackDevServer链接中断,紧接着是socket连接被拒。了解webpack-dev-server自动刷新原理的同学应该知道,自动刷新和热更新都是基于websocket来通知触发,这里展开又是长篇大论,所以不再多说,有兴趣自行了解。仅贴一下官方文档的解释,至于为什么贴devServer的inline属性文档说明,还是请看第4章。

9

websocket报错至少告诉我们自动刷新不成功是网络问题,那回顾一下使用虚拟机或者docker machine时的网络架构模式。如下图所示:

10

  • 如果在虚拟机下使用webpack-dev-server,实际上其服务地址应该是192.168.99.100:8080
  • 如果在docker machine中创建容器,并在其中使用webpack-dev-server,实际上其服务地址应该是172.17.0.2:8080,又因为docker容器和docker machine有做端口映射,也可以使用地址192.168.99.100:8080来访问webpack-dev-server服务

不管是哪种情况,都与我们目前遇到的不符。目前,我们webpack-dev-server服务地址是localhost:8080,也就是192.168.1.2:8080,事实上webpack-dev-server服务根本不在PC上。仔细想一下,webpack-dev-server默认ip为localhost,默认端口为8080,如果我们没有做配置,按默认来,那么确实是现在这个结果。【问题一】没解决,又生一问题,看看目前的问题库:

  • 问题一:在虚拟机或Docker下为什么不能实时刷新
    • 子问题a:在虚拟机或Docker下webpack-dev-server服务地址设置问题【新增】
  • 问题二:看上去功能重复的插件如BrowserSyncPlugin、LiveReloadPlugin、HotModuleReplacementPlugin究竟有什么区别,为什么会被注释

那么接下来,想办法把webpack-dev-server服务地址问题搞定。

3.4 webpack-dev-server的服务地址相关属性

3.4.1 host

官方文档中,host属性说明如下:

11

通过指定host,可以设置webpack-dev-server服务地址,文档中特别说明“如果需要外部访问服务地址,要将host指定为0.0.0.0”。暂且先不管这个“0.0.0.0”,既然在3.3.4小节中,我们期望的服务地址是“192.168.99.100”,那我们尝试用host指定一下这个IP。

1
webpack-dev-server --devtool inline-source-map --progress --color --host 192.168.99.100  --watch-poll

执行一下,但却得到报错:

12

报错提示为没法监听到“192.168.99.100”的8080端口。

这里也好理解,我们的webpack-dev-server运行在docker容器中,也就是“172.17.0.2”这个IP,docker和docker machine之间是端口映射关系,只有当docker容器的8080端口有服务,docker machine的8080端口才有用。那我们直接在docker容器中把服务放到docker machine上是不行的。

那尝试改成docker容器自己的IP“172.17.0.2”看看结果:

1
webpack-dev-server --devtool inline-source-map --progress --color --host 172.17.0.2  --watch-poll

编译正常,但还存在两个问题:

  • 我们一般不关注docker容器的IP,只关注docker或者docker machine的IP,他们之间有端口映射关系,所以把host改到docker容器上不是什么好方法

  • 虽然编译没报错,但Chrome的控制台报另一个错误了,这回换成“172.17.0.2”这个IP访问不到。事实如此,我们的本机PC和docker 容器之间并不互通,一直以来都是以docker machine作为桥梁。

    13

得,直接把host定位到docker machine不行,把host定位到docker容器本身也不行。那怎么破?这时候再回到官方文档的介绍,试试指定host到“0.0.0.0”,在网络中0.0.0.0意思为整个网络:

1
webpack-dev-server --devtool inline-source-map --progress --color --host 0.0.0.0  --watch-poll

编译也正常,回到Chrome看下控制台,WTF!还有错:

14

这回不是找不到服务地址了,却提示无效的host头部。问题库再来一员。

  • 问题一:在虚拟机或Docker下为什么不能实时刷新
    • 子问题a:在虚拟机或Docker下webpack-dev-server服务地址设置问题【新增】
    • 子问题b: host指定到0.0.0.0提示Invalid Host header【新增】
  • 问题二:看上去功能重复的插件如BrowserSyncPlugin、LiveReloadPlugin、HotModuleReplacementPlugin究竟有什么区别,为什么会被注释

3.4.2 disableHostCheck

【子问题b】相对来说比较好解决,google一下,信息都指向到disableHostCheck这个属性上:

15

说出于安全考虑,检查host看是否存在攻击行为,那我们现在是开发使用,给他禁了吧:

1
webpack-dev-server --devtool inline-source-map --progress --color --host 0.0.0.0 --disable-host-check --watch-poll

这下终于没问题也不报错了,尝试一下改动代码,实时刷新终于OK了!

16

至此,【问题一】彻底解决!但跟host服务地址相关的几个属性有必要再介绍一下。

  • 问题一:在虚拟机或Docker下为什么不能实时刷新【已解决】
    • 子问题a:在虚拟机或Docker下webpack-dev-server服务地址设置问题【已解决】
    • 子问题b: host指定到0.0.0.0提示Invalid Host header【已解决】
  • 问题二:看上去功能重复的插件如BrowserSyncPlugin、LiveReloadPlugin、HotModuleReplacementPlugin究竟有什么区别,为什么会被注释

3.4.3 port

17

host属性用来指定webpack-dev-server服务的IP地址,那么port属性就是指定服务的端口了。由于默认端口是8080,比较容易被占用,所以可以人为去指定。

需要提醒一下,如果是使用docker,默认的8080端口或者指定的其他端口都需要做好映射!

3.4.4 public

18

解释说如果使用inline模式,并代理使用webpack-dev-server,可能会需要public属性。

在我这个例子中,webpack-dev-server的服务地址是0.0.0.0:8080,我们并没有用nginx之类的软件代理这个地址,所以在本例中,我们不需要使用public属性。

但如果0.0.0.0:8080被nginx代理为wds.XX.com这个域名,有可能会需要设置这个属性。

我在解决这一系列问题途中,搜索到stackoverflow和github上某些文章,说只要设置public属性就行了,这不是一个完全正确的答案:

  • host是说webpack-dev-server服务的源在哪里,必须存在目标的位置
  • public是说webpack-dev-server服务发布在哪里,这是指路

3.5 webpack、webpack-dev-server在虚拟机或docker下开启自动刷新方法复盘总结

上面介绍的比较杂,我们重新梳理一下解决【问题一】的路线:

  • 在虚拟机或Docker下为什么不能实时刷新

    这是因为watch属性不能在网络文件系统也就是虚拟机下监听文件变化,必须使用watch-poll主动轮询查询文件是否变更

  • 使用watch-poll后,找不到webpack-dev-server服务地址localhost:8080

    这是因为wds默认地址为localhost:8080,但实际上服务地址应该在虚拟机或docker machine上,指定host为0.0.0.0即可。意思为监听整个网络

  • wds服务地址指定为0.0.0.0:8080后,提示Invalid Host header

    这是因为wds出于安全考虑,检查host是否存在危险,毕竟0.0.0.0监听整个网络,而我们在开发机以及开发环境中并不会有什么风险,可以用过disableHostCheck禁用host检查

问题的答案有两种形式:

  • CLI

    1
    webpack-dev-server --host 0.0.0.0 --disable-host-check --watch-poll
  • 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var webpack = require('webpack');
    //...
    var config = {
    entry: xx,
    devServer:{
    host: "0.0.0.0", //指定wds服务ip为0.0.0.0用于监听整个网络
    // port: 9081, //自定义wds服务端口
    // public: "http://wds.myapp.com", //指定wds服务的发布位置
    disableHostCheck:true, //禁用host检查以解决Invalid Host header错误
    watchOptions: {
    poll: true //开启watch主动轮询模式
    }
    },
    output: {
    path: path.resolve(__dirname, '../'),
    //...
    },
    //...
    }

    同样的属性,配置文件和CLI不要混合使用,上面的配置文件搭配的CLI非常简单:

    1
    webpack-dev-server

最后,用docker的话记得映射wds的端口!

比如配合上面的例子,docker的run命令如下:

1
docker run -p 80:80 -p 8080:8080 centos

到这里还没完!一开始我们说了一些文章关于webpack的介绍混淆了1.x和2.x版本,还多次提到了inline模式。虽然已经解决了自动刷新的问题,但关于自动刷新还知之甚少。那下面来详细了解一下webpack-dev-server的自动刷新模式。

4. webpack-dev-server的两种刷新模式

webpack-dev-server实现自动刷新有两种方式,一种是inline模式,另一种是iframe模式。关于这两种模式的介绍和区别 详情介绍webpack-dev-server,iframe与inline的区别 这篇文章介绍的比较详细。

简单来说:

  • inline是将inline.js打包到bundle.js,inline.js包含了socket通讯代码,可以与wds进行通讯,用来响应wds反馈的文件变化通知,然后刷新整个url
  • iframe是在网页中嵌入一个iframe,文件发生变化时,wds通知到live.bundle.js,live.bundle.js本身也包含socket通讯,收到反馈后reload这个ifame以完成页面刷新

4.1 如何启用inline和iframe模式

文章一开始,我们列举了几篇文章,说都存在问题,包括本章开头介绍inline和iframe的文章也存在这个问题,他们分别是这样介绍的:

每一篇都说iframe无需配置,是默认的,而inline需要用CLI或配置打开。还有其他一些文章,还专门强调使用inline模式需要手动添加entry。这是因为这些文章用的文档都是webpack1.X版本,那我们看看官方是怎么说的:

  • webpack 1.X

    22

  • webpack 2.X

    23

    • inline默认开启
    • iframe模式需要配置
    • 建议使用inline模式配合模块热更新(inline和iframe都支持模块热更新)

综上所述:

  • webpack1.X中默认开启的是iframe模式,而webpack2.X中默认开启inline模式,同时webpack2.X也无需配置entry
  • 所以有必要提醒一下,如果你正在使用webpack2.X,需要启用inline模式,那么在CLI中添加inline是多此一举
  • 另外iframe模式下,访问地址加了目录webpack-dev-server,变成 http://ip:port/webpack-dev-server/index.html
  • inline模式会刷新整个url
  • iframe模式只会刷新iframe里面的页面,url并不会刷新

不管怎么说,理论上这两种模式都能完成自动刷新,下面看看实际运行结果。

4.2 iframe

注意下面的介绍都是基于虚拟机的架构,所以保留了watch-poll、host、disable-host-check等属性设置。

执行命令:

设置属性inline=false以启动iframe模式

1
webpack-dev-server --watch-poll --host 0.0.0.0 --port=9081 --inline=false --disable-host-check

24

iframe模式下,url地址确实增加了webpack-dev-server目录,同时整个页面最顶层有一个状态栏,整个页面被包裹在一个iframe里,注意观察下面的动图,看看url和顶层状态栏,以及页面如何刷新:

25

4.3 inline

inline就是webpack2.X默认模式,所以我们在第3章介绍的案例都是以inline模式运行,不再过多介绍,执行命令,看一下动图:

26

到这还是没结束。因为问题库里面,【问题一】解决了,但【问题二】还没解决:

  • 问题一:在虚拟机或Docker下为什么不能实时刷新【已解决】
    • 子问题a:在虚拟机或Docker下webpack-dev-server服务地址设置问题【已解决】
    • 子问题b: host指定到0.0.0.0提示Invalid Host header【已解决】
  • 问题二:看上去功能重复的插件如BrowserSyncPlugin、LiveReloadPlugin、HotModuleReplacementPlugin究竟有什么区别,为什么会被注释

那么继续往下看!

5. 模块热替换HMR

5.1 什么是HMR

27

上面是官方对于HMR的解释。HMR相对于自动更新来说:

  • 不需要刷新整个页面,因为这可能会影响到现有的表单、缓存等,同时响应速度比较慢
  • 整个应用在运行时,可以替换其中的模块,而不用重启应用

第4章也提到,inline和iframe模式都可以配合HMR使用,那我们如何开启HMR呢?

5.2 使用HMR

28

官方文档对于webpack2.X版本启用hot说明如下:

  • 如果通过CLI也就是命令行方式开启hot,会自动添加HotModuleReplacementPlugin

    1
    webpack-dev-server --host 0.0.0.0 --disable-host-check --watch-poll --hot
  • 如果使用配置方式开启hot,需要手动添加HotModuleReplacementPlugin

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var webpack = require('webpack');
    //...
    var config = {
    entry: xx,
    devServer:{
    host: "0.0.0.0",
    disableHostCheck:true,
    hot:true, //开启hot
    watchOptions: {
    poll: true
    }
    },
    output: {
    path: path.resolve(__dirname, '../'),
    //...
    },
    plugins: [
    new webpack.HotModuleReplacementPlugin(), //热更新插件
    //...
    ],
    //...
    }

上面的例子中,默认使用inline模式配合hot开启HMR,如果要使用iframe模式,记得指定inline=false

现在我们知道【问题二】中HotModuleReplacementPlugin的用途了,只有在通过配置文件方式开启hot模式时,需要手动添加HotModuleReplacementPlugin

  • 问题一:在虚拟机或Docker下为什么不能实时刷新【已解决】
    • 子问题a:在虚拟机或Docker下webpack-dev-server服务地址设置问题【已解决】
    • 子问题b: host指定到0.0.0.0提示Invalid Host header【已解决】
  • 问题二:看上去功能重复的插件如BrowserSyncPlugin、LiveReloadPlugin、HotModuleReplacementPlugin究竟有什么区别,为什么会被注释
    • 子问题c: HotModuleReplacementPlugin是做什么用的【已解决】
    • 子问题d: BrowserSyncPlugin是做什么用的【新增】
    • **子问题e: *LiveReloadPlugin是做什么用的【新增】*

正确开启HMR后,浏览器控制台可以看到这个信息:

29

看一下HMR响应改变的动图,注意观察和inline自动刷新的区别,主要是url没有刷新,以及控制台里面的信息:

30

5.3 其他相关介绍

5.3.1 vue使用HMR

在vue中,使用vue-loader就可以完美兼容HMR模式,看看vue官方怎么介绍的:

https://vue-loader.vuejs.org/zh-cn/features/hot-reload.html

31

5.3.2 webstorm可能引起hot失效的问题

有同学反应,使用webstorm开发时,HMR失效了。这锅不是webstorm一个人背,官方是这样解释的:

32

有些编辑器默认使用“safe write”模式保存文件,比如webstorm,这种模式将文件变化先放到缓存里,并不直接改写文件,所以导致监听文件变更失败。遇到这种情况,可以取消“safe write”模式。

6. 其他替代技术

整篇文章还剩下两个问题:

  • 问题一:在虚拟机或Docker下为什么不能实时刷新【已解决】
    • 子问题a:在虚拟机或Docker下webpack-dev-server服务地址设置问题【已解决】
    • 子问题b: host指定到0.0.0.0提示Invalid Host header【已解决】
  • 问题二:看上去功能重复的插件如BrowserSyncPlugin、LiveReloadPlugin、HotModuleReplacementPlugin究竟有什么区别,为什么会被注释
    • 子问题c: HotModuleReplacementPlugin是做什么用的【已解决】
    • 子问题d: BrowserSyncPlugin是做什么用的【新增】
    • **子问题e: *LiveReloadPlugin是做什么用的【新增】*

一一攻破。

6.1 BrowserSyncPlugin

https://www.npmjs.com/package/browser-sync-webpack-plugin

主要解决浏览器同步问题,比如做兼容性调试时,一般同时打开IE、Chrome或开启手机浏览器模拟器等,该插件可以同步所有浏览器内容。与webpack共同使用时,一般用来代理webpack-dev-server服务已完成更多功能。详细不再过多介绍,按需使用。

6.2 LiveReloadPlugin

https://www.npmjs.com/package/webpack-livereload-plugin

基本功能与webpack-dev-server一致,但如果网站的assets由其他服务器提供,比如某些图片存在阿里云OSS,而你又想使用webpack的自动更新功能。那么就可以使用这个插件。

  • 问题一:在虚拟机或Docker下为什么不能实时刷新【已解决】
    • 子问题a:在虚拟机或Docker下webpack-dev-server服务地址设置问题【已解决】
    • 子问题b: host指定到0.0.0.0提示Invalid Host header【已解决】
  • 问题二:看上去功能重复的插件如BrowserSyncPlugin、LiveReloadPlugin、HotModuleReplacementPlugin究竟有什么区别,为什么会被注释
    • 子问题c: HotModuleReplacementPlugin是做什么用的【已解决】
    • 子问题d: BrowserSyncPlugin是做什么用的【已解决】
    • **子问题e: *LiveReloadPlugin是做什么用的【已解决】*

至此,所有的坑全部填完……

Webpack实时刷新与模块热替换(HMR)

https://wurang.net/webpack_hmr/

作者

Wu Rang

发布于

2017-10-17

更新于

2021-04-07

许可协议


评论