想必做为前端大佬的你,工做中应该用过 webpack,而且对热更新的特性也有了解。若是没有,固然也不要紧。javascript
下面我要讲的,是我对 Webpack 热更新机制的一些认识和理解,不足之处,欢迎指正。html
首先:前端
热更新,是指 Hot Module Replacement,缩写为 HMR。vue
从名字上解读,就是把“热”的模块进行替换。热,是指这个模块已经在运行中。java
不知道你有没有听过或看过这样一段话:“在高速公路上将汽车引擎换成波音747飞机引擎”。webpack
虽然有点牵强,可是放在这里,从某些角度上来讲,也还算合适吧。git
再扯远一点,说下我目前工做中的遇到的状况,相信不少人也遇到过。github
微信小程序的开发工具,没有提供相似 Webpack 热更新的机制,因此在本地开发时,每次修改了代码,预览页面都会刷新,因而以前的路由跳转状态、表单中填入的数据,都没了。web
哪怕只是一个文案或属性配置的修改,都会致使刷新,而要从新进入特定页面和状态,有时候很麻烦。对于开发时须要频繁修改代码的状况,这样比较浪费时间。小程序
而若是有相似 Webpack 热更新的机制存在,则是修改了代码,不会致使刷新,而是保留现有的数据状态,只将模块进行更新替换。也就是说,既保留了现有的数据状态,又能看到代码修改后的变化。
很美好,可是想一想就以为是一件确定不简单的事情。
因此,热更新是啥呢?
引用官方文档,热更新是:
使得应用在运行状态下,不重载刷新就能更新、增长、移除模块的机制
那么热更新要解决的问题,在上面也解释了。用个人话来阐述,就是 在应用程序的开发环境,方便开发人员在不刷新页面的状况下,就能修改代码,而且直观地在页面上看到变化的机制。
简单来讲,就是为了 提高开发效率。
联想到我在微信小程序上的开发体验,真心以为若是有热更新机制的话,开发效率要高不少。
若是你知道微信小程序已经或计划支持热更新,或者有大佬已经作了相似的工做,欢迎告诉我,感谢!
进一步介绍前,咱们来看下 Webpack 热更新如何配置。
若是你以前作的项目是其余人搭建配置了 Webpack 和热更新,那么这里能够了解下热更新是怎么配置的。
个人示例采用 Webpack 4,想直接看代码的话,在这里:
除了 Webpack,还须要 webpack-dev-server
(或 webpack-dev-middleware
)。
为 Webpack 开发环境开启热更新,要作两件事:
HotModuleReplacementPlugin
插件webpack-dev-server
的热更新开关HotModuleReplacementPlugin
插件是 Webpack 自带的,在 webpack.config.js
加入就好:
// webpack.config.js
module.exports = {
// ...
plugins: [
webpack.HotModuleReplacementPlugin(),
// ...
]
}
复制代码
若是直接经过 webpack-dev-server 启动 Webpack 的开发环境,那么能够这样打开 webpack-dev-server 的热更新开关:
// webpack.config.js
module.exports = {
// ...
devServer: {
hot: true,
// ...
}
}
复制代码
也很简单。
下面经过例子来进一步解释热更新机制。若是你以前对 Webpack 热更新的体验,是 Vue 经过 vue-loader 提供给你的,也就是说你在本身的代码中从没有写过或者见到过相似:
if (module.hot) {
module.hot.accept(/* ... */)
// ...
}
复制代码
这样的代码,那么下面的例子就恰好适合看一看了。
这些例子就在上面的 webpack-hmr-demo,若是你对代码更亲切,那直接去看吧,首页文档里有简单的说明。
这个例子只是把示例页面的功能简单介绍下,而且让你体会下每次修改代码都要从新刷新页面的痛苦。
页面上只有一个元素,用来展现数值:
<div id="root" class="number"></div>
复制代码
入口模块(index.js)引用了两个模块:
入口模块的功能很简单,调用 timer.start()
,再传入的回调函数中,每次将获得的数值更新到页面上显示:
import { start } from './timer'
import { message } from './foo'
var current = 0
var root = document.getElementById('root')
start(onUpdate, current)
console.log(message)
function onUpdate(i) {
current = i
root.textContent = '#' + i
}
复制代码
将这个项目运行起来,打开的页面中就是在一直刷新展现增长的数值而已,相似这样:
一旦修改任何模块的代码,例如改变 timer 中定时器的间隔时间(如从1秒改为3秒),或者 onUpdate 中展现的内容(如 '#' + i
改为 '*' + i
),页面都会刷新,已经有的状态清除,从新从0开始计数。
接下来的例子,展现在 index.js 如何处理其余模块的更新。
依赖的模块发生更新,要么是接受变动(页面不用刷新,模块替换下就好),要么不接受(必须得刷新)。
Webpack 将热更新相关接口以 module.hot
暴露到模块中,在使用前,最好判断下当前的环境是否支持热更新,也就是上面看到的这样的代码:
if (module.hot) {
// ...
}
复制代码
延续上一个例子,选择接受并处理 timer 的更新,但对于 foo 模块,不接受:
if (module.hot) {
module.hot.accept('timer', () => {
// ...
})
module.hot.decline('./foo')
}
复制代码
因此,在热更新的机制中,实际上是以这种“声明”的方式告知 Webpack,哪些模块的更新是被处理的,哪些模块的更新又不被处理。固然对于要处理的模块的更新,自行在 module.hot.accept() 的第二个参数即回调函数中进行处理,会在声明的模块被替换后执行。
下面来看对 timer 模块更新的处理。
timer 模块的 start 函数调用后返回一个能够终止定时器的 stop 函数,借助它咱们实现对旧的 timer 模块的清理,并基于当前状态从新调用新的 timer 模块的 start 函数:
var stop = start(onUpdate, current) // 先记录下返回的 stop 函数
// ...
if (module.hot) {
module.hot.accept('timer', () => {
stop()
stop = start(onUpdate, current)
})
// ...
}
复制代码
处理逻辑如上所述,先经过以前记录的 stop 中止旧模块的定时器,而后调用新模块的 start 继续计数,而且传入当前数值从而没必要从0开始从新计数。
看起来仍是比较简单的吧。运行起来的效果是,若是修改 timer 中的定时器间隔时间,当即在页面上就能看到效果,并且页面并不会刷新致使从新从0开始计数:
在运行几秒后,修改 timer 模块中定时器的间隔时间为 100ms
修改 foo 中的 message,页面仍是会刷新。
有几点额外说明下:
此外,除了声明其余模块更新的处理,模块也能够声明自身更新的处理,也是一样的接口,不传参数便可:
module.hot.accept()
告诉 Webpack,当前模块更新不用刷新module.hot.decline()
告诉 Webpack,当前模块更新时必定要刷新并且,依赖同一个模块的不一样模块,能够有各自不一样的声明,这些声明多是冲突的,好比有的容许依赖模块更新,有的不容许,Webpack 怎么协调这些呢?
Webpack 的实现机制有点相似 DOM 事件的冒泡机制,更新事件先由模块自身处理,若是模块自身没有任何声明,才会向上冒泡,检查使用方是否有对该模块更新的声明,以此类推。若是最终入口模块也没有任何声明,那么就刷新页面了。这也就是为何在上一个例子中,虽然开启了热更新,可是模块修改后仍旧刷新页面的缘由,由于没有任何模块对更新进行处理。
自身模块的更新处理与依赖模块相似,也是要经过 module.hot 的接口向 Webpack 声明。不过模块自身的更新,可能须要在模块被 Webpack 替换以前就作一些处理,更新后的处理则没必要经过特别接口来作,直接写到新模块代码里面就好。
module.hot.dispose()
用于注册当前模块被替换前的处理函数,而且回调函数接收一个 data 对象,能够向其写入须要保存的数据,这样在新的模块执行时能够经过 module.hot.data
获取到:
var current = 0
if (module.hot && module.hot.data) {
current = module.hot.data.current
}
复制代码
首先,模块执行时,先检查有没有旧模块留下来的数据,若是有,就恢复。
而后在模块被替换前的执行处理,这里就是记录数据、停掉现有的定时器:
if (module.hot)
module.hot.accept()
module.hot.dispose(data => {
data.current = current
stop()
})
}
复制代码
作了这些处理以后,修改 index.js 的 onUpdate,使得渲染到页面的数值改变,也能够在不刷新的状况下体现:
在运行几秒后,修改 onUpdate() 中的
'#' + i
为'*' + i
看过上面的例子,咱们来总结下。
Webpack 的热更新,其实只是提供一套接口和基础的模块替换的实现。做为开发者,须要在代码中经过热更新接口(module.hot.xxx)向 Webpack 声明依赖模块和当前模块是否可以更新,以及更新的先后进行的处理。
若是接受更新,那么须要开发者本身来在模块被替换前清理或保留必要的数据、状态,并在模块被替换后恢复以前的数据、状态。
固然,像咱们在使用 Vue 或 React 进行开发时,vue-loder 等插件已经帮咱们作了这些事情,而且对于 *.vue 文件在更新时要若是进行处理,不少细节也只有 vue-loader 内部比较清楚,咱们就放心使用好了。
可是对于 Webpack 热更新是怎么一回事,若是可以有深刻了解固然更好,我就遇到过同事在 Vue 组件中自行对 DOM 进行处理(为了封装一个直接操做 DOM 的组件),结果因为热更新的存在,致使一些状态的清除有问题的状况。
这种状况,只有开发者本身才能处理,vue-loader 可无法处理这样的特殊状况。至少知道如何使用 Webpack 的热更新接口,这种状况下开发者就能自行处理了。
本文对于 Webpack 热更新机制的介绍还只是在接口使用的层面,或者大致的机制上,没有深刻说明热更新的实现原理和细节。时间、篇幅有限,那就先放一张图出来,或许有时间再细说一下。
上图来源:
Webpack & The Hot Module Replacement medium.com/@rajaraodv/…
这篇英文文章对 Webpack 热更新实现原理方面有深刻介绍。