开启模块热替换

要做到实时预览,除了在使用自动刷新中介绍的刷新整个网页外,DevServer 还支持一种叫做模块热替换(Hot Module Replacement)的技术可在不刷新整个网页的情况下做到超灵敏的实时预览。 原理是当一个源码发生变化时,只重新编译发生变化的模块,再用新输出的模块替换掉浏览器中对应的老模块。

模块热替换技术的优势有:

总的来说模块热替换技术很大程度上的提高了开发效率和体验。

模块热替换的原理

模块热替换的原理和自动刷新原理类似,都需要往要开发的网页中注入一个代理客户端用于连接 DevServer 和网页, 不同在于模块热替换独特的模块替换机制。

DevServer 默认不会开启模块热替换模式,要开启该模式,只需在启动时带上参数 --hot,完整命令是 webpack-dev-server --hot

除了通过在启动时带上 --hot 参数,还可以通过接入 Plugin 实现,相关代码如下:

const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');

module.exports = {
  entry:{
    // 为每个入口都注入代理客户端
    main:['webpack-dev-server/client?http://localhost:8080/', 'webpack/hot/dev-server','./src/main.js'],
  },
  plugins: [
    // 该插件的作用就是实现模块热替换,实际上当启动时带上 `--hot` 参数,会注入该插件,生成 .hot-update.json 文件。
    new HotModuleReplacementPlugin(),
  ],
  devServer:{
    // 告诉 DevServer 要开启模块热替换模式
    hot: true,      
  }  
};

在启动 Webpack 时带上参数 --hot 其实就是自动为你完成以上配置。

相比于自动刷新的代理客户端,多出了后三个用于模块热替换的文件,也就是说代理客户端更大了。

可见补丁中包含了 main.css 文件新编译出来 CSS 代码,网页中的样式也立刻变成了源码中描述的那样。

但当你修改 main.js 文件时,会发现模块热替换没有生效,而是整个页面被刷新了,为什么修改 main.js 文件时会这样呢?

Webpack 为了让使用者在使用了模块热替换功能时能灵活地控制老模块被替换时的逻辑,可以在源码中定义一些代码去做相应的处理。

把的 main.js 文件改为如下:

See the Pen main.js by whjin (@whjin) on CodePen.

其中的 module.hot 是当开启模块热替换后注入到全局的 API,用于控制模块热替换的逻辑。

现在修改 AppComponent.js 文件,把 Hello,Webpack 改成 Hello,World,你会发现模块热替换生效了。 但是当你编辑 main.js 时,你会发现整个网页被刷新了。为什么修改这两个文件会有不一样的表现呢?

当子模块发生更新时,更新事件会一层层往上传递,也就是从 AppComponent.js 文件传递到 main.js 文件, 直到有某层的文件接受了当前变化的模块,也就是 main.js 文件中定义的 module.hot.accept(['./AppComponent'], callback), 这时就会调用 callback 函数去执行自定义逻辑。如果事件一直往上抛到最外层都没有文件接受它,就会直接刷新网页。

那为什么没有地方接受过 .css 文件,但是修改所有的 .css 文件都会触发模块热替换呢? 原因在于 style-loader 会注入用于接受 CSS 的代码。

请不要把模块热替换技术用于线上环境,它是专门为提升开发效率生的。

优化模块热替换

其中的 Updated modules: 68 是指 ID 为68的模块被替换了,这对开发者来说很不友好,因为开发者不知道 ID 和模块之间的对应关系,最好是把替换了的模块的名称输出出来。 Webpack 内置的 NamedModulesPlugin 插件可以解决该问题,修改 Webpack 配置文件接入该插件:

const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin');

module.exports = {
  plugins: [
    // 显示出被替换模块的名称
    new NamedModulesPlugin(),
  ],
};

除此之外,模块热替换还面临着和自动刷新一样的性能问题,因为它们都需要监听文件变化和注入客户端。 要优化模块热替换的构建性能,思路和在使用自动刷新中提到的很类似:监听更少的文件,忽略掉 node_modules 目录下的文件。 但是其中提到的关闭默认的 inline 模式手动注入代理客户端的优化方法不能用于在使用模块热替换的情况下, 原因在于模块热替换的运行依赖在每个 Chunk 中都包含代理客户端的代码。