Open soda-x opened 7 years ago
在 compilation 对象中我们可以获取到和构建相关所有的依赖,而这些依赖真是需要去监听的内容。
“真是” -> “正是” ?
原因很简单,因为在 webpack(webpackConfig) 的阶段中,webpack 注入很多内部的自有插件,webpack 源码非常让人值得学习的一点就是插件机制应用的如火纯情。
“如火纯情” -> “炉火纯青” ?
@dzyhenry 谢谢指正
你好,想咨询下,我在一台电脑上修改vue文件会自动刷新,换另一台电脑不行,应该如何调试webpack这块的源码呢
本个系列的文章会被分成两篇文章
(一)主要描述下问题的表现,并 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
事件再次被执行。