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
方法,
copy
- 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 的处理函数进行 封装。
copy
- 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()
函数。
copy
- // old 函数
- require.extensions['.js'] = function(module, filename) {
- var content = fs.readFileSync(filename, 'utf8');
- // 调用
- module._compile(content, filename);
- };
compile()
主要是子进程发送一个通知,让主进程接收到这个通知,然后子进程就自己陷入了 阻塞。
copy
- 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 文件。
copy
- // 主进程监听子进程的消息
- child.on('message', function (message: CompileParams) {
- if (
- !message.compiledPath ||
- currentCompilePath === message.compiledPath
- ) {
- return
- }
- currentCompilePath = message.compiledPath
- // 调用主进程的编译器进行偏移
- compiler.compile(message)
- })
主进程的 compile 方法,通过覆盖 js 默认的 _compile()
方法来完成 ts 文件编译结果的缓存。
copy
- 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 文件的过程。
copy
- 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);
- };
- }
感谢分享