资讯专栏INFORMATION COLUMN

Webpack 热更新机制

mikasa / 1037人阅读

摘要:联想到我在微信小程序上的开发体验,真心觉得如果有热更新机制的话,开发效率要高很多。热更新示例下面通过例子来进一步解释热更新机制。

想必作为前端大佬的你,工作中应该用过 webpack,并且对热更新的特性也有了解。如果没有,当然也没关系。

下面我要讲的,是我对 Webpack 热更新机制的一些认识和理解,不足之处,欢迎指正。

首先:

热更新是啥?

热更新,是指 Hot Module Replacement,缩写为 HMR

从名字上解读,就是把“热”的模块进行替换。热,是指这个模块已经在运行中。

不知道你有没有听过或看过这样一段话:“在高速公路上将汽车引擎换成波音747飞机引擎”。

虽然有点牵强,但是放在这里,从某些角度上来说,也还算合适吧。

再扯远一点,说下我目前工作中的遇到的情况,相信很多人也遇到过。

微信小程序的开发工具,没有提供类似 Webpack 热更新的机制,所以在本地开发时,每次修改了代码,预览页面都会刷新,于是之前的路由跳转状态、表单中填入的数据,都没了。

哪怕只是一个文案或属性配置的修改,都会导致刷新,而要重新进入特定页面和状态,有时候很麻烦。对于开发时需要频繁修改代码的情况,这样比较浪费时间。

而如果有类似 Webpack 热更新的机制存在,则是修改了代码,不会导致刷新,而是保留现有的数据状态,只将模块进行更新替换。也就是说,既保留了现有的数据状态,又能看到代码修改后的变化。

很美好,但是想想就觉得是一件肯定不简单的事情。

所以,热更新是啥呢?

引用官方文档,热更新是:

使得应用在运行状态下,不重载刷新就能更新、增加、移除模块的机制
热更新解决的问题

那么热更新要解决的问题,在上面也解释了。用我的话来阐述,就是 在应用程序的开发环境,方便开发人员在不刷新页面的情况下,就能修改代码,并且直观地在页面上看到变化的机制

简单来说,就是为了 提升开发效率

联想到我在微信小程序上的开发体验,真心觉得如果有热更新机制的话,开发效率要高很多。

如果你知道微信小程序已经或计划支持热更新,或者有大佬已经做了类似的工作,欢迎告诉我,感谢!

进一步介绍前,我们来看下 Webpack 热更新如何配置。

热更新配置

如果你之前做的项目是其他人搭建配置了 Webpack 和热更新,那么这里可以了解下热更新是怎么配置的。

我的示例采用 Webpack 4,想直接看代码的话,在这里:

https://github.com/luobotang/...

除了 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,如果你对代码更亲切,那直接去看吧,首页文档里有简单的说明。

示例1:没有热更新的情况

这个例子只是把示例页面的功能简单介绍下,并且让你体会下每次修改代码都要重新刷新页面的痛苦。

页面上只有一个元素,用来展示数值:

入口模块(index.js)引用了两个模块:

timer.js:只提供了一个 start 接口,传入回调函数,然后 timer 会间隔一段时间调用回调函数,并传入一个每次增加的数值

foo.js:没啥功能,就简单暴露一个 message,引入它单纯是区别 timer.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开始计数。

示例2:处理依赖模块的热更新

接下来的例子,展示在 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,页面还是会刷新。

有几点额外说明下:

timer 模块如果修改后不返回 start 接口,那么上述处理机制显然会失效,所以这里的处理是基于模块的接口不变的情况下

timer 模块的 start 调用后显然必须返回一个 stop 函数,否则在 index.js 是没法清除 timer 模块内开启的定时器的,这也很重要

或许你也注意到了,就是对 timer 模块的 start 函数的引用貌似一直没有变过,那为什么在回调函数中的 start 就是新模块了呢?这个其实是有 Webpack 在编译时处理掉的,编译后的代码并非当前的样式,对 start 会进行替换,使得回调中的 start 一定引用到的是新的 timer 模块的 start。感兴趣可以看下 Webpack 文档中对此的相关描述。

此外,除了声明其他模块更新的处理,模块也可以声明自身更新的处理,也是同样的接口,不传参数即可:

module.hot.accept() 告诉 Webpack,当前模块更新不用刷新

module.hot.decline() 告诉 Webpack,当前模块更新时一定要刷新

而且,依赖同一个模块的不同模块,可以有各自不同的声明,这些声明可能是冲突的,比如有的允许依赖模块更新,有的不允许,Webpack 怎么协调这些呢?

Webpack 的实现机制有点类似 DOM 事件的冒泡机制,更新事件先由模块自身处理,如果模块自身没有任何声明,才会向上冒泡,检查使用方是否有对该模块更新的声明,以此类推。如果最终入口模块也没有任何声明,那么就刷新页面了。这也就是为什么在上一个例子中,虽然开启了热更新,但是模块修改后仍旧刷新页面的原因,因为没有任何模块对更新进行处理。

示例3:处理自身模块的热更新

自身模块的更新处理与依赖模块类似,也是要通过 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
https://medium.com/@rajaraodv/webpack-hot-module-replacement-hmr-e756a726a07

这篇英文文章对 Webpack 热更新实现原理方面有深入介绍。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100071.html

相关文章

  • Node.js 更新(一)

    摘要:直到最近在使用微信机器人时,遇到了强烈的需求。增删文件后热更新上面的代码已经不小心实现了增加文件后热更新,因为表示检测的更新,如果增加一个,那么就变成,于是新模块不等于老模块不存在,从而使用注册事件监听器。 背景 刚思考这个话题的时候,首先想到的是 Vue 或 React 的组件热更新(基于 Webpack HMR),后来又想到了 Lua、Erlang 等语言的热更新,不过在实际开发 ...

    LancerComet 评论0 收藏0
  • 9102年:手写一个React脚手架 【优化极致版】

    摘要:马上要出了,完全手写一个优化后的脚手架是不可或缺的技能。每个依赖项随即被处理,最后输出到称之为的文件中,我们将在下一章节详细讨论这个过程。的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 webpack马上要出5了,完全手写一个优化后的脚手架是不可或缺的技能。 本文书写时间 2019年5月9日 , webpack版本 4.30.0最新版本 本人所有代码均手写,亲自试验过可...

    Kylin_Mountain 评论0 收藏0
  • 9102年:手写一个React脚手架 【优化极致版】

    摘要:马上要出了,完全手写一个优化后的脚手架是不可或缺的技能。每个依赖项随即被处理,最后输出到称之为的文件中,我们将在下一章节详细讨论这个过程。的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 webpack马上要出5了,完全手写一个优化后的脚手架是不可或缺的技能。 本文书写时间 2019年5月9日 , webpack版本 4.30.0最新版本 本人所有代码均手写,亲自试验过可...

    whatsns 评论0 收藏0
  • 9102年:手写一个React脚手架 【优化极致版】

    摘要:马上要出了,完全手写一个优化后的脚手架是不可或缺的技能。每个依赖项随即被处理,最后输出到称之为的文件中,我们将在下一章节详细讨论这个过程。的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 webpack马上要出5了,完全手写一个优化后的脚手架是不可或缺的技能。 本文书写时间 2019年5月9日 , webpack版本 4.30.0最新版本 本人所有代码均手写,亲自试验过可...

    bingo 评论0 收藏0
  • webpack4大结局:加入腾讯IM配置策略,实现前端工程化环境极致优化

    摘要:或者的,都会对其进行分析。舒适的开发体验,有助于提高我们的开发效率,优化开发体验也至关重要组件热刷新热刷新自从推出热刷新后,前端开发者在开环境下体验大幅提高。实现热调试后,调试流程大幅缩短,和普通非直出模式调试体验保持一致。 showImg(https://segmentfault.com/img/bVbtOR3?w=1177&h=635); webpack,打包所有的资源 不知道不...

    李增田 评论0 收藏0

发表评论

0条评论

mikasa

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<