You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The watching may loop in a unlucky case, but this should not result in a different compilation hash. I. e. the webpack-dev-server doesn't trigger a update if the hash is equal.
本个系列的文章会被分成两篇文章
(一)主要描述下问题的表现,并 dive into webpack watch system
(二)解决问题,从根本上解决 webpack 的 bug
最近做一个内部工具时碰到了一个很有意思的问题
多次 rebuild 的现象
搜了下,发现 webpack 可追溯的 issue 记录为 Files created right before watching starts make watching go into a loop
该问题不论你是在使用
webpack-dev-middleware
或者webpack --watch
又或者webpack-dev-server
都可以复现。webpack 作者 @sokra 对其解释为:
白话理解为:确实有问题,但是呢,最关键的 compilation hash 不会变,所以上层使用时,自己内部处理下这个逻辑。
但实际情况呢, webpack-dev-server 等作者不认这一说!
粗暴的解决方案
至于不想刨根问底,这里也有狗皮膏药的解决方案:
刨根问底
当然狗皮膏药并不是本文的重点,刚好借此一窥,webpack 中整体的 watch 机制。
如果不想看那么多代码片段,也可以看我在梳理代码逻辑时做的笔记,笔记中红色流程为初始化时的调用链路,蓝色部分为文件变更后事件回调链路。
首先我们可以确定一点的是,不管是 webpack 自身的 cli 工具还是 webpack-dev-middleware 和 webpack-dev-server 都是通过
Compiler.prototype.watch
来实现了 watch 的功能,进而来实现调试阶段的高性能需求。为了比较清晰的知道整一个流程,我们从创建一个 Compiler 实例开始说起
webpack Compiler 实例的创建
总所周知我们通过
const compiler = webpack(webpackConfig);
这种方式来创建一个 Compiler 的实例,一般也叫做 webpack 的实例,compiler 实例对象中包含着和打包相关的所有参数,plugins loaders 等等。这种情况下 webpack 并不会默认进行构建编译的过程,如果想要启动编译则需要执行一下compiler.run(callback)
。 另外我们也可以通过webpack(webpackConfig, callback);
默认来启动构建编译流程。对于今天我们想要了解的 watch 过程我们这边只需要知道,当构建参数中含有明确开启 watch 配置项时整个流程的走向是
compiler.watch(watchOptions, callback);
而非compiler.run(callback);
。题外话: 或许你比较好奇 compilation 是什么,它包含着 chunks modules 等信息,构建依赖文件变更时都会重新生成 compilation,而 compiler 只有一个。
compiler.watch 中创建 watch 服务
在这边需要注意的是
startTime
每次编译执行时_go
方法将被调用,调用时会赋值编译启动时间,该时刻在认定文件是否需要再次编译或者是否变更时非常非常重要!首次编译初始化
当如上
this._go()
被执行时,即开始了首次的编译过程敲黑板: 注意此时 startTime 被正式赋值为 首次构建编译开始的时间,同时
compile
的执行标志着首次编译的开始。此次文章并不会涉及 webpack 的事件流,以及编译过程中 loaders 和 plugins 等的流转过程,这边我们只需要知道,执行 compile 后进入了编译流程即可。
由代码可以看出在正常流程下正常编译流程完毕后,调用
_done
方法。在 compilation 对象中我们可以获取到和构建相关所有的依赖,而这些依赖正是需要去监听的内容。
正式开启文件监听
上个过程中我们可以看到最后我们把构建依赖,传递给了 watch 的方法。
这里我们注意到 watch 实际调用的是
compiler.watchFileSystem.watch
。看过源码的可能会很好奇,因为在Compiler
的源码中没有定义过这个原型链上的方法。原因很简单,因为在webpack(webpackConfig)
的阶段中,webpack 注入很多内部的自有插件,webpack 源码非常让人值得学习的一点就是插件机制应用的炉火纯青。具体我们可以看到这 webpack.js,而通过这个线索我们找到了NodeEnvironmentPlugin,开始有所眉目我们看到了熟悉的 watch 字眼 NodeWatchFileSystem,通过它进而我们终于找到了 NodeWatchFileSystem 兴奋之余 watch 服务最终的启动者 watchpack 也浮出水面。题外话: 这边比较有趣的是 NodeEnvironmentPlugin 这个 plugin,在这个 plugin 中默认设置了
NodeOutputFileSystem
NodeJsInputFileSystem
CachedInputFileSystem
,以NodeOutputFileSystem
为例,在 webpack 默认情况下编译完成后文件内容都会通过 io 输出到实际的文件目录中,但是毕竟涉及 io 操作这种性能并不能满足调试的需求,所以在 webpack-dev-middleware 中会将 NodeOutputFileSystem 原本默认的 fs 替换为memory-fs
进而 boost performance。另外CachedInputFileSystem
等也是通过本地构建的缓存文件物理加速。由于这些内容并不是本文重点,所以不再展开,有兴趣的同学可以继续深挖。基于 webpack 的源码不难发现最终 watch 交由的是 Watchpack 实例的 watch 方法。
接下来我们看到
这边对 webpack 不是很熟悉的同学可能会比较困惑为什么 file 和 dir 需要进行区分 watch,默认情况下,通过 webpack resolve 后我们能拿到每个模块精确的路径地址,但是在一些特别的用法下,比如使用
require.context(path)
就会对该 path 所对应的目录加以监听。所以在一般业务场景下只会涉及到
this._fileWatcher
。根据如上代码我们可以获知
watcherManager.watchFile(file, this.watcherOptions, startTime)
返回了 一个watcher
而
_fileWather
根本上是对返回的 watcher 做了一次事件绑定。那我们看看
watcherManager.watchFile(file, this.watcherOptions, startTime)
到底创建了一个怎么样的 watcher。Step1: this.getDirectoryWatcher(directory, options)
如上所知不管是传入的内容是 file 路径还是 directory 路径,都会被转到
getDirectoryWatcher
言下之意就是一个目录下所有的文件都会被对应到一个 directoryWatcher。
在新建一个 DirectoryWatcher 的实例时
可以发现,webpack watch 文件夹变更的能力实际输出者为
chokidar
并且对 directoryPath 对应的 chokidar watcher,绑定
add
,addDir
,change
,unlink
,unlinkDir
,error
等事件。并执行了
this.doInitialScan();
。根据如上代码我们可以获知,在执行首次扫描时,会把当前文件夹下的内容读取出来。对文件则进行
this.setFileTime(itemPath, +stat.mtime, true);
这边不对 setFileTime 做过多阐述,他有两种使用场景。
一种来源于 initialScan 会把所有的文件的最新修改时间全部读取出来,为之后判断文件变更触发更新提供依据。另外一个场景就是触发更新了。
Step2: directoryWatcher.watch((p, startTime))
该代码记录了一个 filepath 创建一个 Watcher 的过程,最后返回了该 wathcer。
所以再反观
我们就可以知道,这边是对每个文件绑定了一个 change 和 remove 事件。
文件发生变更后,最初会被 directoryWatcher 监听到,进而触发对应的 fileWatcher 的 change 事件。
而
_onChange
会被调用进而触发了 Watchpack 实例的 change 事件, 该事件由在
NodeWatchFileSystem
中绑定。那如何触发重编译呢?答案在
aggregated
事件中。触发
invalidate
事件,因为_go
事件再次被执行。The text was updated successfully, but these errors were encountered: