用 HMR 提高开发效率
HMR 全称是 Hot Module Replacement,即模块热替换。在这个概念出来之前,我们使用过 Hot Reloading,当代码变更时通知浏览器刷新页面,以避免频繁手动刷新浏览器页面。
HMR 可以理解为增强版的 Hot Reloading,但不用整个页面刷新,而是局部替换掉部分模块代码并且使其生效,可以看到代码变更后的效果。
所以,HMR 既避免了频繁手动刷新页面,也减少了页面刷新时的等待,可以极大地提高前端页面开发效率。
配置使用 HMR
HMR 是 webpack 提供的非常有用的一个功能,跟我们之前提到的一样,安装好 webpack-dev-server, 添加一些简单的配置,即在 webpack 的配置文件中添加启用 HMR 需要的两个插件:
const webpack = require('webpack')
module.exports = {
// ...
devServer: {
hot: true // dev server 的配置要启动 hot,或者在命令行中带参数开启
},
plugins: [
// ...
new webpack.NamedModulesPlugin(), // 用于启动 HMR 时可以显示模块的相对路径
new webpack.HotModuleReplacementPlugin(), // Hot Module Replacement 的插件
],
}
笔者觉得 HMR 应该是 development mode 默认启动的功能,这个希望 webpack 后续能有优化。
HMR 运行原理
HMR 的实现和运行相对复杂,需要多个部分协同配合,这里稍微介绍一下 HRM 的运行原理。
首先我们要知道一个概念:webpack 内部运行时,会维护一份用于管理构建代码时各个模块之间交互的表数据,webpack 官方称之为 Manifest,其中包括入口代码文件和构建出来的 bundle 文件的对应关系。
可以使用 WebpackManifestPlugin 插件来输出这样的一份数据。
了解这个概念后,我们来看一下 HMR 的大致运行流程图。
当你使用前面的配置启动了支持 HMR 的 webpack-dev-server,然后在浏览器打开页面时,你也可以从控制台看到大概的 HMR 执行流程:
开启了 hot 功能的 webpack 会往我们应用的主要代码中添加 WS 相关的代码,用于和服务器保持连接,等待更新动作。
当你配置了 HMR 的插件时,会往应用代码中添加 HMR 运行时的代码,主要用于定义代码模块应用更新时的 API,后面会详细介绍。
有兴趣可以查看源码:HotModuleReplacement.runtime.js。
有了这两个部分就可以支持整个 HMR 的功能了。我们先忽略流程图的右上角部分,左下角的流程相对容易理解:当有更新时,webpack-dev-server 发送更新信号给 HMR 运行时,然后 HMR 再请求所需要的更新数据,请求的更新数据没有问题的话就应用更新。
如果 HMR 只是简单替换了代码模块的内容,如替换掉所谓的 installedModules 中需要更新的部分,那么这样并没有办法把更新后的结果实时地在浏览器上显示出来,所以才会需要流程图的右上角部分。
如果无法理解 installedModules,可以参考第 13 小节中的「bundler 的基础流程」这一部分的内容
前面提到的 HMR 运行时代码会提供定义代码模块应用更新时执行的 API,这些 API 可以让我们在模块中定义接收到 HMR 更新应用信号时,需要额外做什么工作。
例如, style-loader 就需要实现 HMR 接口,当收到更新时,使用新的样式替换掉旧的样式,大概是这样:
if (module.hot) {
module.hot.accept('/some/path', function() {
// ... 用新样式替换旧样式
})
}
详情可以参考 style-loader 中的代码实现:HMR interface implemention in style-loader。
HMR 应用更新时是使用 webpackHotUpdate 来处理的:
webpackHotUpdate(id, {
'modulePath':
function() {
// 模块更新后的代码
}
})
执行 webpackHotUpdate 时如发现模块代码实现了 HMR 接口,就会执行相应的回调或者方法,从而达到应用更新时,模块可以自行管理自己所需要额外做的工作。不过,并不是所有的模块都需要做相关的处理,当遇见没有实现 HMR 接口的模块时,就会往上层冒泡,如本节开头部分的流程图所示。
这里还有一个问题是,webpack 如何保证 HMR 接口中的引用是最新的模块代码?我们看一个简单的例子:
import './index.css'
import hello from './bar'
hello()
if (module.hot) {
module.hot.accept('./bar', () => {
// console.log('Accepting the updated bar module!')
hello()
})
}
从代码上看,hello 都是同一个,这样的话并没有办法引用最新的模块代码,但是我们看一下上述代码在 webpack 构建后的结果:
if (true) {
module.hot.accept("./src/bar.js", function(__WEBPACK_OUTDATED_DEPENDENCIES__) {
/* harmony import */
__WEBPACK_IMPORTED_MODULE_1__bar__ = __webpack_require__("./src/bar.js");
(() => {
// console.log('Accepting the updated bar module!')
Object(__WEBPACK_IMPORTED_MODULE_1__bar__["default"])()
})(__WEBPACK_OUTDATED_DEPENDENCIES__);
})
}
其他代码比较杂,我们集中看 module.hot 的处理部分。这里可以发现,我们的 hello 已经重新使用 __webpack_require__
来引用了,所以可以确保它是最新的模块代码。
基本上 HMR 的执行原理就是这样,更具体的实现部分就不展开讲解了。
在日常开发中,我们需要更多的工具来帮助我们实现 HMR 的接口,避免编写过多 HMR 需要的代码。
例如,React 在组件代码更新时可能需要触发重新 render 来实现实时的组件展示效果,官方提供了一些现有的工具,需要的可以参考一下:hot module replacement tools。
module.hot 常见的 API
前面 HMR 实现部分已经讲解了实现 HMR 接口的重要性,下面来看看常见的 module.hot API 有哪些,以及如何使用。
之前已经简单介绍过,module.hot.accept 方法指定在应用特定代码模块更新时执行相应的 callback,第一个参数可以是字符串或者数组,如:
if (module.hot) {
module.hot.accept(['./bar.js', './index.css'], () => {
// ... 这样当 bar.js 或者 index.css 更新时都会执行该函数
})
}
module.hot.decline 对于指定的代码模块,拒绝进行模块代码的更新,进入更新失败状态,如 module.hot.decline(‘./bar.js’)。这个方法比较少用到。
module.hot.dispose 用于添加一个处理函数,在当前模块代码被替换时运行该函数,例如:
if (module.hot) {
module.hot.dispose((data) => {
// data 用于传递数据,如果有需要传递的数据可以挂在 data 对象上,然后在模块代码更新后可以通过 module.hot.data 来获取
})
}
module.hot.accept 通常用于指定当前依赖的某个模块更新时需要做的处理,如果是当前模块更新时需要处理的动作,使用 module.hot.dispose 会更加容易方便。
module.hot.removeDisposeHandler 用于移除 dispose 方法添加的 callback。
关于 module.hot 的更多 API 详情可以参考官方文档:Hot Module Replacement APIs。
小结
Hot Module Replacement 是 webpack
在 webpack 中配置使用 HMR
HMR 的运行原理
模块中的 HMR 接口 API
参考资料
https://www.kancloud.cn/sllyli/webpack/1242352