[Node] 随遇而安 TypeScript(三):监控文件变更
1. 背景
我们知道 TypeScript 的命令行工具是可以 watch 的,进程启动后,终端是这样的,
$ tsc index.ts --watch
[00:00:00 AM] Starting compilation in watch mode...
index.ts
文件变更后,如果没有错误,终端会追加这样一行,
[00:01:00 AM] Found 0 errors. Watching for file changes.
如果编译过程中有错误,终端也会追加错误信息,
index.ts:1:1 - error TS2304: Cannot find name 'x'.
1 x
~
[00:02:00 AM] Found 1 error. Watching for file changes.
这是 TypeScript tsc
watch 的全过程。
除此之外,TypeScript 还支持 API 接口的方式对文件进行 watch。
const ts = require('typescript');
// 要监控的 ts 文件
const watchFilePath = ...;
// 诊断信息、watch 状态,会根据这两个回调函数反馈出来
const reportDiagnostic = (...args) => ...;
const reportWatchStatus = (...args) => ...;
const rootFiles = [watchFilePath];
const options = {};
const host = ts.createWatchCompilerHost(
rootFiles,
options,
ts.sys,
ts.createEmitAndSemanticDiagnosticsBuilderProgram,
reportDiagnostic,
reportWatchStatus,
);
const watchProgram = ts.createWatchProgram(host);
完整的代码可参考这里 github: debug-typescript。
以上代码,会监控指定 ts 文件的变更,然后从接口而不是命令行中,取到编译相关的信息。
这一切是怎么完成的呢?
要追踪整条链路,我们还得从 Node.js 标准库 fs.watchFile 开始。
2. fs.watchFile
fs.watchFile 是 Node.js 标准库中的方法,
具体用法如下,
const fs = require('fs');
fs.watchFile(
fileName, // 要监控的文件路径
{ // (可选)
persistent: true, // 程序执行完后,当前进程是否挂住,默认为 true(挂住)
interval: 250, // 每个多久检测一次,默认值 5000 ms
},
fileChanged, // 文件变更时的回调
);
值得注意的是,persistent
不论为 true
还是 false
,
fs.watchFile
执行完后,都会执行下一行语句,关键在于整个程序执行完后是否挂住。
了解了 fs.watchFile
的用法之后,TypeScript 文件监控的实现,也就容易跟踪了。
3. 调试
(1)source map 问题
根据 github: debug-typescript 的说明,
我们选择 debug watch
可对 watch 文件的主程序进行调试。
VSCode 中直接按 F5
,断点会停在第一行,
我们来看这里,
const typescript = path.join(root, './lib/typescript.js');
此处引用了 TypeScript 源码仓库中 lib/
文件夹的 typescript.js
文件,
线上地址在这里,github: TypeScipt/lib/typescript.js
并没有像以前的文章中提到的那样,引用了本地 TypeScript 的编译结果 built/local/typescript.js
。
这是有原因的,
因为本地的编译结果 built/local/typescript.js
相关的 source map 有问题。
built/local/typescript.js.map
内容如下,
{
"version": 3,
"file": "typescript.js",
"sources": [
"typescriptServices.out.js"
],
"sourceRoot": "",
"mappings": ";;;;;;;;;;;;;;;",
"preExistingComment": "typescriptServices.js.map"
}
其中 mappings
只包含了一些分号,因此 VSCode 调试器无法根据编译产物定位到源码中。
VSCode 调试器要么会卡住,要么会提前终止掉。
手动将 built/local/typescript.js.map
文件删掉就可以调试了,
与直接引用 lib/typescript.js
的效果是一样的。
关于 TypeScript 本地编译,可参考 github: debug-typescript,
$ node node_modules/.bin/gulp LKG
(2)调用栈
好了我们言归正传,我们可以在第 7
行打个断点,
然后单步调试进去,跑到被 require
的文件中,
这正是
lib/typescript.js
文件。
下一步由于我们已经知道了 Node.js watch 文件变更的方法,
所以就可以简化调试过程了。
直接在 lib/typescript.js
中搜索 fs.watchFile
。
找到了 6
个结果,只有一个结果不在注释中,
位于 fsWatchFileWorker
lib/typescript.js#L5334 函数中,
function fsWatchFileWorker(fileName, callback, pollingInterval) {
_fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged);
...
return {
close: function () { return _fs.unwatchFile(fileName, fileChanged); }
};
function fileChanged(curr, prev) {
...
callback(fileName, eventKind);
}
}
我们看到它执行了 watchFile
操作,
_fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged);
每 250 ms
检测一下文件变更,如有变更就回调 fileChanged
。
在这一行打个断点,看看能否得到调用栈信息。
watch fileName
的值,是待编译源文件的绝对地址,
调用栈最底部,正是 debug-watch/index.js
中调用 ts.createWatchProgram
的位置,
(3)监听文件变更
我们接着在 fileChanged
lib/typescript.js#L5342 回调函数中打个断点,
然后按 F5
期望程序先跑完,再去修改源文件,看看能否触发 fileChanged
。
结果,出乎意料的是,程序又来到了 watchFile
的断点处,fileName
改成了其他的值,
/Users/.../Microsoft/TypeScript/node_modules/_@types_browserify@12.0.36@@types/browserify/index.d.ts
这些文件是 TypeScript 预加载的文件,我们在之前的文章中,也曾遇到过。
可见 TypeScript 对这些预加载文件,也会进行监控。
把 watchFile
断点去掉,然后按 F5
执行,发现会直接进入 fileChanged
中,
其中,fileName
的值来自闭包中,
/Users/.../Microsoft/node_modules/@types"
这里暂时略过不去追究,按 F5
继续执行,我们发现调试器挂住了,进程并没有结束,
VSCode 调试器底下的状态栏仍然是橙色的。
现在我们去修改一下待编译的源文件 debug/index.ts
并保存。
程序果然来到了
fileChanged
lib/typescript.js#L5342 的断点中。
fileName
的值正是刚才修改 debug/index.ts
文件的绝对地址,
/Users/.../Microsoft/TypeScript/debug/index.ts
(4)回调数据
回到主程序 debug-watch/index.js
中,在 reportDiagnostic
和 reportWatchStatus
中打个断点,
然后按 F5
继续执行,就可以看到监控到文件变更后,TypeScript 的调用链路了。
args
中包含了这些内容,
TypeScript 检测到了文件变更,正在进行增量编译(incremental compilation)
File change detected. Starting incremental compilation...
这就又给我们带来了疑问,TypeScript 到底是怎样进行增量编译的呢?
这些探索留给后续的文章吧。
4. 后记
增量编译看起来是一种很神秘的技术,这也是写文本的原因之一,
从 watch 角度来跟踪增量编译,算是一个不错的切入点。
本文主要介绍了 Node.js fs.watchFile
库函数,以及 TypeScript 监控文件变更的具体细节。
其中遇到了 lib/typescript.js
的 source map 文件,导致我们只能调试 js 代码了。
js 代码(编译产物)的回调函数,看起来会比较烧脑一些,
幸运的是代码没有被压缩和混淆,不然真是遇到麻烦了。