教你怎么使用 webpack3 的 HMR 模块热加载

前注:

文档全文请查看 根目录的文档说明css

若是能够,请给本项目加【Star】和【Fork】持续关注。html

有疑义请点击这里,发【Issues】。vue

点击这里查看DEMOwebpack

七、模块热加载 HMR

7.0、使用说明

安装:git

npm install
复制代码

运行(注意,是 dev):github

npm run dev
复制代码

结论放前面,适合场景:web

  1. 当使用 style-loader 时,修改 css 样式;
  2. 当使用 vue-loader 之类带 HMR 功能的 loader 时,修改对应的模块;
  3. 当仅仅只是须要修改代码后,页面能够自动刷新,以保证当前页面上是最新的代码;

7.一、现象和原理

当谈到 HMR 的时候,首先要明确一个概念,HMR 究竟是什么?npm

若是用过带 HMR 功能的脚手架,例如我分享的这个 Vue的脚手架,大约能给出一个答案:json

  1. 修改代码后,不须要刷新页面,修改后的代码效果,就能马上体如今页面上;
  2. 已有的效果,好比输入框里输入了一些内容,代码更新后,每每内容还在;
  3. 彷佛想不到其余的了;

从现象来看,在必定程度上,这个描述问题不大,但不严谨。数组

咱们须要分析 HMR 究竟是什么?

  1. webpack 是模块化的,每一个 js, css 文件,或者相似的东西,都是一个模块;
  2. webpack 有 模块依赖图(Dependency Graph),也就是说,知道每一个模块之间的依赖关系;
  3. HMR 是模块热加载,指模块被修改后,webpack检测并用新模块更新;
  4. 也就是原来的模块被移除后,用修改后的模块替换;
  5. 表现效果(一般):若是是 js,会从新执行一遍;若是是 css,并用了 style-loader,原有的会被替换;

以上是理论基础,实际流程以下:

  1. 假如 B 模块的代码被更改了,webpack 能够检测到,而且能够知道是哪一个更改了;
  2. 而后根据 依赖图,发现 A 模块依赖于 B 模块,因而向上冒泡到 A 模块中,判断 A 模块里有没有处理热加载的代码;
  3. 若是没有,则继续向上冒泡,直到冒泡到顶层,最后触发页面刷新(若是引用了多个chunk,当一个冒泡到顶层而且没有被处理的话,整个页面都会触发刷新);
  4. 若是中途被捕获,那么将只从新加载冒泡路径上的模块,并触发对应 HMR 处理函数的回调函数;

更具体的内容,请查看官网的说明,附连接以下:

依赖图(Dependency Graph)

模块热替换(Hot Module Replacement)(注:原理)

模块热替换(注:使用方法)

7.二、应用场景

HMR 的应用场景,最合适的是带 HMR 功能的loader。

例如 style-loader,或 vue-loader

缘由很简单,本身在页面里写 HMR 冒泡捕获功能,写起来很麻烦,也很容易致使遗漏。

最重要的是,这些代码并非业务代码,而是 HMR 专用代码,这些代码会在webpack打包时被打包进去(能够查看打包好后的源代码)Z,但这没有意义。

所以在 loader 里进行处理,对模块的加载才是更加有效的。

固然,假如你只是但愿保存修改的代码,会自动触发页面刷新,以保证页面上的代码是最新的,那么也是能够的。

这种状况只须要启用 HMR 功能,不须要写 HMR 的捕获代码,让触发的 update 行为自动冒泡到顶层,触发页面刷新就行了(参照 开发环境中的 6.2)。

具体能够参照下面的示例DEMO。

7.三、使用说明

7.3.一、HMR 的冒泡

先假设引用关系: A -> B -> C

【1】HMR 是向上冒泡的:

  1. C 被更改后,会去找他的父模块 B,查看 B 中有没有关于 C 的 HMR 的处理代码:
  2. 若是没有,那么会继续向上冒泡到 A,查看 A 中有没有关于 B 的 HMR 的处理代码;
  3. 若是 A 没有,由于 A 是入口文件,因此会刷新整个页面;

【2】冒泡过程当中只有父子关系:

C 更改,冒泡到 B(B 无 HMR 处理代码),而后冒泡到 A。

此时,在 A 这里,视为 B 被更改(而不是 C),

所以 A 里面处理 HMR 代码,捕获的模块,应该是 B,而不是 C,

若是 A 的目标是 C,那么该段代码不会响应(虽然冒泡的起点是 C);

【3】HMR 触发,会执行整个冒泡流程中涉及到的模块中的代码:

例如上面的 C 更改,B 捕获到了,从新执行 C;

B 无捕获代码向上冒泡,A捕获到了,从新执行 B 和 C;

假如引用关系是:A -> B -> C 和 D,即 B 里面同时引用 C 和 D 两个模块,而且 B 没有处理 HMR 的代码,A 有:

  1. 冒泡起点是 B:B 从新执行一遍本身的代码,C 和 D 不会执行;
  2. 冒泡起点是 C:B 和 C 从新执行一遍本身的代码, D 不会执行;
  3. 冒泡起点是 D:B 和 D 从新执行一遍本身的代码, C 不会执行;

【4】冒泡行为起点的子模块,其代码不会被从新执行:

先假设引用关系:A -> B -> C -> D,B 没有 处理 HMR 的代码,C 有没有无所谓,A 有。

冒泡起点是 C,所以冒泡到 A。

从上面咱们能够得知,B 和 C 会被从新执行,那么 D 呢?

答案是不会,由于 D 不在冒泡路线上。

总结:

总结以上四点,得出一个结论:

  1. 从修改的模块开始冒泡,直到被捕获为止;
  2. 冒泡路径上的代码(不包含捕获到的模块),都会被从新执行;
  3. 非冒泡路径上的代码,不论是子模块,或者是兄弟模块,都不会被从新执行(除非是整个页面被刷新)Z。

7.3.二、HMR 的隐患

以上特色这就可能带来一些后果(主要是 js 代码):

  1. 假如我代码里,有绑定事件,那么当修改代码并从新执行一遍后,显然会再绑定一次,所以会致使重复绑定的问题(所以要考虑到解绑以前的事件);
  2. 相似的,若是代码里添加了 DOM,那么当从新执行的时候,本来 DOM 节点还在,从新执行的时候又添加了一次;
  3. 若是有某些一次性操做,好比代码里移除了某个 DOM,那么极可能 HMR 不能解决你的问题,也许须要从新刷新后,表现才正常;

7.3.三、HMR 的一个坑

那就是引用时候的名字,和处理的 API,引用的文件名,须要相同;

举例:

// 引入
import foo from './foo.js';

// 处理
module.hot.accept('./foo.js', callback);
复制代码

若是不同,会致使第一次响应正常,后面就可能致使没法正常触发 HMR ,虽然提示模块更新,但不会从新执行模块的代码。

7.四、示例

为了说明 HMR 是怎么使用和生效,这里将给一个最简单的示例,包含 html、css、和 js 代码,来解释其的使用方法。

能够直接 fork 本项目参看源码,如下是分析做用,以及如何生效的。

须要使用的东西:

  1. 使用 webpack-dev-server,参考上一篇六、开发环境
  2. 两个HMR插件:webpack.NamedModulesPluginwebpack.HotModuleReplacementPlugin
  3. 配置一下 package.json,添加一行 scripts :"dev": "webpack-dev-server --open --config webpack.config.js"
  4. style-loader,用于实现 css 的 HMR(使用后默认开启);

依赖图:

app.js        入口文件,在其中配置了 foo.js 和 bar.js 的 HMR 处理函数
├─style.css   样式文件
├─img
│  ├─1.jpg    图片1
│  └─2.jpg    图片2
├─foo.js      模块foo,配置了 HMR 模块热替换的接口
│  └─bar.js   模块bar,是foo的子模块
└─DOM.js      抽象出一个创造 DOM,并插入到 body 标签的函数
复制代码

一、先分析 js 部分

app.js

// 引入资源
import './style.css';
import foo from './foo.js';
import createDOM from './DOM.js'

// 建立一个DOM并插入<body>标签
let el = createDOM({
    id: 'app-box',
    innerHTML: 'app.js<input>'
})
document.body.appendChild(el);

// 本行代码表示app.js已经被执行了一遍
console.log('%c%s', 'color:red;', 'app.js is running...')

// 两个子模块建立DOM并插入<body>标签
foo()

// 这里是控制 HMR 的函数
// 注:
// 这里引用的 foo.js 模块,那么处理 foo.js HMR 效果的代码必须写在这里;
// 特别提示:这段代码不能抽象封装到另一个js文件中(即便那个js文件也被 app.js import进来)
// 推测是根据webpack的依赖图,向上找父模块,而后在父模块的代码中,找有没有处理 HMR 的代码
if (module.hot) {
    module.hot.accept('./foo.js', function (url) {
        // 回调函数只有url一个参数,类型是数组
        // 执行时机是 foo.js 中的代码执行完毕后执行
        console.log('%c%s', 'color:#FF00FF;', `[${url}] is update`)
    })
}
复制代码

foo.js

// 引入资源
import createDOM from './DOM'
import bar from "./bar.js";
// bar 中建立的DOM逻辑,在 foo 中执行
bar()

// 执行本段代码的时候,表示 foo.js 被从新执行了
console.log('%c%s', 'color:green;', 'foo.js is running...')

function Foo() {
    let el = createDOM({
        id: 'foo-box',
        classList: 'foo',
        innerHTML: 'foo.js<input>'
    })

    document.body.appendChild(el);
}

// 导出给 app.js 执行
export default Foo

// 这里写 bar.js 的 HMR 逻辑
if (module.hot) {
    module.hot.accept('./bar.js', function (args) {
        console.log('%c%s', 'color:#FF00FF', `[${args}] is update`)
    })
}
复制代码

bar.js

// 引入资源
import createDOM from './DOM'

// 执行本段代码的时候,表示 bar.js 被从新执行了
console.log('%c%s', 'color:blue;', 'bar.js is running...')

function Bar() {
    let el = createDOM({
        id: 'bar-box',
        classList: 'bar',
        innerHTML: 'bar.js<input>'
    })

    document.body.appendChild(el);
}

// 导出给 foo.js 执行
export default Bar
复制代码

简单总结一下以上代码:

  1. app.js 做为入口文件,他引入了本身的子模块 foo.js,以及 css 资源文件,而且处理本身子模块的 HMR 行为;
  2. foo.js 做为 app.js 的子模块,他引入了本身的子模块 bar.js ,而且处理本身子模块的 HMR行为;
  3. bar.js 没作什么特殊的;
  4. 三个模块里,都有一行 console.log() 代码,当出如今浏览器的控制台里的时候,表示该模块代码被从新执行了一遍;
  5. 父模块处理子模块的 HMR 时,回调函数里有一行 console.log() 代码,表示该子模块已经从新加载完毕;
  6. 所以,理论上,咱们修改 foo.js 或者 bar.js 文件后,首先会看到该模块的 console.log() 代码,其次会看到其父模块处理 HMR 的回调函数中的 console.log() 代码;

首次刷新页面后,控制台先输出三条 log,和几行 HMR代码,略略略。

修改 foo.js

当咱们修改 foo.js 的 log 代码:console.log('%c%s', 'color:green;', 'foo.js is running...I change it')

控制台输出:

foo.js is running...I change it
[./foo.js] is update
[HMR] Updated modules:
[HMR]  - ./foo.js
[HMR] App is up to date.
复制代码

正如咱们所料,foo.js 代码被从新执行了一遍,而后触发了 app.js 里面 module.hot.accept() 的回调函数(注意,有前后顺序)。

而且,页面上多了一个 DOM 节点(来自 bar.js的,由于在 foo.js 里面执行了 bar()),这正是咱们前面所提出来的,HMR 机制的天生缺陷之一。

另外请注意,因此 bar.js 是 foo.js 的子模块,但因为 bar.js 并无被修改,因此 bar.js 里面的代码没有从新执行一遍(除了他暴露给 foo.js 的接口)。

修改 bar.js

当咱们修改 bar.js 的 log 代码:console.log('%c%s', 'color:blue;', 'bar.js is running...and bar has been changed')

控制台输出:

bar.js is running...and bar has been changed
[./bar.js] is update
[HMR] Updated modules:
[HMR]  - ./bar.js
[HMR] App is up to date.
复制代码

bar.js 是 foo.js 的子模块,并且 foo.js 里面有关于处理 bar.js 的模块 HMR 功能的代码。

所以 bar.js 被修改后,冒泡到本身的父模块时就被捕获到,并无继续向上冒泡。

让 bar.js 的修改冒泡到 app.js

假如让 bar.js 的修改冒泡到 app.js 会发生什么事情呢?先修改代码:

app.js 尝试让 app.js 同时捕获 foo.js 和 bar.js 的修改

// from
module.hot.accept('./foo.js', function (url) {

// to
module.hot.accept(['./foo.js', './bar.js'], function (url) {
复制代码

foo.js 注释掉对 bar.js 的 HMR 功能的处理代码

// from 
if (module.hot) {
    module.hot.accept('./bar.js', function (args) {
        console.log('%c%s', 'color:#FF00FF', `[${args}] is update`)
    })
}

// to 
// if (module.hot) {
//     module.hot.accept('./bar.js', function (args) {
//         console.log('%c%s', 'color:#FF00FF', `[${args}] is update`)
//     })Z
// }
复制代码

恢复以前 foo.js 和 bar.js 的 console.log() 的修改

// foo.js
// from
console.log('%c%s', 'color:green;', 'foo.js is running...I change it')

// to
console.log('%c%s', 'color:green;', 'foo.js is running...')


// bar.js
// from
console.log('%c%s', 'color:blue;', 'bar.js is running...and bar has been changed')

// to
console.log('%c%s', 'color:blue;', 'bar.js is running...')
复制代码

修改完毕,此时刷新一下页面,重置状态。而后咱们给 bar.js 添加一行代码 console.log('bar.js is be modified')

控制台输出:

bar.js is running...
bar.js is be modified
foo.js is running...
[./foo.js] is update
[HMR] Updated modules:
[HMR]  - ./bar.js
[HMR]  - ./foo.js
[HMR] App is up to date.
复制代码

这说明,webpack 成功捕捉到了 bar.js 的修改,而且更新了 bar.js 和 foo.js 。

而且,虽然在 app.js 里去尝试捕获 bar.js ,然而,由于 bar.js 并非 app.js 的子模块(而是子模块的子模块),所以是捕获不到的。

复数监视

module.hot.accept这个函数中,参数一能够接受一个数组,表示监视的模块能够是复数。

因此不须要写多个函数来监视多个模块,若是他们之间逻辑是复用的话,那么一个模块就好了。

总结:

js 文件被修改,会致使冒泡过程当中,涉及到的 js 文件,都被从新执行一遍。


二、再分析 css 部分

在使用 style-loader 后,咱们不须要配置任何东西,就能够实现 HMR 效果。

style.css

#app-box {
    color: red;
}
复制代码

默认打开页面,会发现页面上 app.js 那一行的字体颜色是红色。

修改这个css样式为:

#app-box {
    color: red;
    font-size: 24px;
}
复制代码

在保存这个css文件后,会发现页面在没有刷新的状况下,样式已经改变了。

因为咱们开发通常都会采用 style-loader,并且 css 因为是替代效果,也不是可执行代码,所以天生适用于 HMR 场景。

7.五、总结

css 文件没有什么好说的,只要使用 style-loader 便可。

由于 HMR 的特性(会从新执行 js 文件),因此若是没有 loader 辅助的话,写在 HMR 下可用的 js 代码是很麻烦的。

想象一下,你的js代码里有一个建立并插入 DOM 的操做,而后在你每次修改这个模块里的代码时,都会建立一个新的 DOM,并插入。

例如本 DEMO 里,修改 foo.js 文件,会致使从新执行 foo 模块时,执行 bar.js 暴露出来的接口 bar,

因而页面被重复插入一个 DOM,这显然不符合咱们的预期。

固然了,也有解决办法,刷新页面便可恢复正常。

相似的还有 绑定事件(致使重复绑定),发起异步请求(致使屡次发起异步请求)等。

那么有没有解决办法呢?

答案是使用相关的 loader,而且写符合相关格式的代码。

例如 vue-loader 能够处理 .vue 结尾的文件。在你修改 .vue 文件的时候,就能够自动处理。假如你 .vue 文件不按要求写,而是本身乱写,那么显然就不能正常运行。

相关文章
相关标签/搜索