Node.js

[Node] 随遇而安 TypeScript(三):监控文件变更

2020-02-14  本文已影响0人  何幻

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 中,在 reportDiagnosticreportWatchStatus 中打个断点,

然后按 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 代码(编译产物)的回调函数,看起来会比较烧脑一些,
幸运的是代码没有被压缩和混淆,不然真是遇到麻烦了。

参考

github: debug-typescript
TypeScipt v3.7.3

上一篇 下一篇

猜你喜欢

热点阅读