文档全文请查看 根目录的文档说明。css
若是能够,请给本项目加【Star】和【Fork】持续关注。html
有疑义请点击这里,发【Issues】。vue
点击这里查看DEMOwebpack
安装:git
npm install
复制代码
运行(注意,是 dev):github
npm run dev
复制代码
结论放前面,适合场景:web
style-loader
时,修改 css 样式;vue-loader
之类带 HMR 功能的 loader
时,修改对应的模块;当谈到 HMR 的时候,首先要明确一个概念,HMR 究竟是什么?npm
若是用过带 HMR 功能的脚手架,例如我分享的这个 Vue的脚手架,大约能给出一个答案:json
从现象来看,在必定程度上,这个描述问题不大,但不严谨。数组
咱们须要分析 HMR 究竟是什么?
style-loader
,原有的会被替换;以上是理论基础,实际流程以下:
更具体的内容,请查看官网的说明,附连接以下:
模块热替换(Hot Module Replacement)(注:原理)
HMR 的应用场景,最合适的是带 HMR 功能的loader。
例如 style-loader
,或 vue-loader
。
缘由很简单,本身在页面里写 HMR 冒泡捕获功能,写起来很麻烦,也很容易致使遗漏。
最重要的是,这些代码并非业务代码,而是 HMR 专用代码,这些代码会在webpack打包时被打包进去(能够查看打包好后的源代码)Z,但这没有意义。
所以在 loader 里进行处理,对模块的加载才是更加有效的。
固然,假如你只是但愿保存修改的代码,会自动触发页面刷新,以保证页面上的代码是最新的,那么也是能够的。
这种状况只须要启用 HMR 功能,不须要写 HMR 的捕获代码,让触发的 update 行为自动冒泡到顶层,触发页面刷新就行了(参照 开发环境中的 6.2)。
具体能够参照下面的示例DEMO。
先假设引用关系: A -> B -> C
【1】HMR 是向上冒泡的:
【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 有:
【4】冒泡行为起点的子模块,其代码不会被从新执行:
先假设引用关系:A -> B -> C -> D,B 没有 处理 HMR 的代码,C 有没有无所谓,A 有。
冒泡起点是 C,所以冒泡到 A。
从上面咱们能够得知,B 和 C 会被从新执行,那么 D 呢?
答案是不会,由于 D 不在冒泡路线上。
总结:
总结以上四点,得出一个结论:
以上特色这就可能带来一些后果(主要是 js 代码):
那就是引用时候的名字,和处理的 API,引用的文件名,须要相同;
举例:
// 引入
import foo from './foo.js';
// 处理
module.hot.accept('./foo.js', callback);
复制代码
若是不同,会致使第一次响应正常,后面就可能致使没法正常触发 HMR ,虽然提示模块更新,但不会从新执行模块的代码。
为了说明 HMR 是怎么使用和生效,这里将给一个最简单的示例,包含 html、css、和 js 代码,来解释其的使用方法。
能够直接 fork 本项目参看源码,如下是分析做用,以及如何生效的。
须要使用的东西:
webpack.NamedModulesPlugin
,webpack.HotModuleReplacementPlugin
;package.json
,添加一行 scripts :"dev": "webpack-dev-server --open --config webpack.config.js"
;依赖图:
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
复制代码
简单总结一下以上代码:
console.log()
代码,当出如今浏览器的控制台里的时候,表示该模块代码被从新执行了一遍;console.log()
代码,表示该子模块已经从新加载完毕;console.log()
代码,其次会看到其父模块处理 HMR 的回调函数中的 console.log()
代码;修改 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 场景。
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
文件不按要求写,而是本身乱写,那么显然就不能正常运行。