为什么 ts-node-dev 运行这么快
ts-node-dev 深度解析
推荐文章:
导读
通过这篇文件可以了解到文件变动后,为啥服务启动这么快和 ts-node-dev 的一些原理,以及理解到在 node 里面自定义扩展其他文件内容的方式。
也可以先阅读下笔者这篇文章:简单好用的 Typescript 项目重启工具:ts-node-dev 的浅析
一、为什么 ts-node-dev 运行这么快?
当监听的文件发生变动后,为什么服务启动的很快?
主要有两点:
1. 每次只编译发生改动的文件
ts-node-dev 会缓存每个 ts
文件的编译结果(也就是对应的 js 文件),每次只是重新编译发生改动的 .ts
源文件。
2. 每次重新启动都共享 typescript 编译器
ts-node-dev 启动时,会随着主进程启动而启动一个子进程,主线程中存在 Typescript 编译器,子进程运行 index.ts
文件,文件发生变动后,每次只是重新启动一个子进程来运行,减少了 Typescript 编译器
实例化需要的时间。
二、源码分析
对上面两点进行源码分析
ts-node-dev 是怎么缓存 ts 文件的编译结果的?
执行了 ts-node-dev 命令后,会调用 start
方法,
function start() {
// ......
// script 就是 index.ts
let cmd = nodeArgs.concat(wrapper, script, scriptArgs)
const childHookPath = compiler.getChildHookPath()
// 挂载一个 hook.js,子线程在执行 index.ts 文件之前执行这个 hook。
cmd = (opts.priorNodeArgs || []).concat(['-r', childHookPath]).concat(cmd)
log.debug('Starting child process %s', cmd.join(' '))
child = fork(cmd[0], cmd.slice(1), {
cwd: process.cwd(),
env: process.env,
})
// ......
}
然后子进程启动时,首先执行这个 hook
,其主要是注册了编译 ts
文件的处理函数,这个函数主要是对 node 里面自带的 js 的处理函数进行 封装。
registerExtensions(['.ts', '.tsx']);
function registerExtensions(extensions: string[]) {
extensions.forEach(function (ext) {
// 这里 old 就是 node 中 js 的处理函数
const old = require.extensions[ext] || require.extensions['.js']
// 当子进程 require 一个 ts 文件时,会调用这个函数
require.extensions[ext] = function (m: any, fileName) {
const _compile = m._compile
// 对旧的 _compile 方法进行包装
m._compile = function (code: string, fileName: string) {
// 这里 compile() 函数很重要
return _compile.call(this, compile(code, fileName), fileName)
}
// 调用
return old(m, fileName)
}
})
// ......
}
当 requeire 一个 ts 文件时,会首先调用上面的 ts 的处理函数,其实际上是执行下面这个函数,调用上面被包装了的 _compile()
函数。
// old 函数
require.extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
// 调用
module._compile(content, filename);
};
compile()
主要是子进程发送一个通知,让主进程接收到这个通知,然后子进程就自己陷入了 阻塞。
const compile = (code: string, fileName: string) => {
const compiledPath = getCompiledPath(code, fileName, compiledDir)
if (process.send) {
try {
// 子进程发送一个消息通知
process.send({
compile: fileName,
compiledPath: compiledPath,
})
} catch (e) {
}
} else {
sendFsCompileRequest(fileName, compiledPath)
}
// 子进程等待 ts 文件编译完成
waitForFile(compiledPath + '.done')
const compiled = fs.readFileSync(compiledPath, 'utf-8')
// 返回编译完成的 js 文件内容
return compiled
}
下面的操作都是在主进程进行
主进程里面监听子线程发送的消息内容,调用在主进程中注册的 Typescript 文件编译器来编译 ts 文件。
// 主进程监听子进程的消息
child.on('message', function (message: CompileParams) {
if (
!message.compiledPath ||
currentCompilePath === message.compiledPath
) {
return
}
currentCompilePath = message.compiledPath
// 调用主进程的编译器进行偏移
compiler.compile(message)
})
主进程的 compile 方法,通过覆盖 js 默认的 _compile()
方法来完成 ts 文件编译结果的缓存。
compile: function (params: CompileParams) {
const fileName = params.compile
const code = fs.readFileSync(fileName, 'utf-8')
// compiledPath = 文件名+文件内容 计算的 hash 值
const compiledPath = params.compiledPath
// Prevent occasional duplicate compilation requests
if (compiledPathsHash[compiledPath]) {
return
}
compiledPathsHash[compiledPath] = true
// 实现编译结果的缓存函数
function writeCompiled(code: string, fileName?: string) {
// code 就是编译后的 js 文件内容,通过写入文件来缓存
fs.writeFile(compiledPath, code, (err) => {
// 通过文件名,来通知子进程,编译完成。
fs.writeFile(compiledPath + '.done', '', (err) => {
err && log.error(err)
})
})
}
// 存在,就说明该文件不需要编译,直接返回。这里就利用到了缓存
if (fs.existsSync(compiledPath)) {
return
}
// 这里覆盖 js 的 _compile() 函数,实现编译结果的缓存
const m: any = {
_compile: writeCompiled,
}
const _compile = () => {
const ext = path.extname(fileName)
const extHandler = require.extensions[ext]!
// 主进程中注册的 ts 文件的编译处理函数
extHandler(m, fileName)
}
try {
// 调用编译函数
_compile()
} catch (e) {
// ......
}
},
ts-node
中的 ts 文件编译函数,这个发生实际的编译 ts 文件的过程。
function registerExtension(
ext: string,
service: Service,
originalHandler: (m: NodeModule, filename: string) => any
) {
// old 是 node 中的 js 文件的 handler
const old = require.extensions[ext] || originalHandler;
require.extensions[ext] = function (m: any, filename) {
const _compile = m._compile;
m._compile = function (code: string, fileName: string) {
// 实际的 ts 文件的编译过程
const result = service.compile(code, fileName);
// 这里的 _compile 就是将编译结果写入文件的 writeCompiled() 函数
return _compile.call(this, result, fileName);
};
return old(m, filename);
};
}