MENU

为什么 ts-node-dev 运行这么快

July 26, 2023 • Read: 2547 • 学习记录

ts-node-dev 深度解析

推荐文章:

无意识偏见

为什么要写作(why write)?

导读

通过这篇文件可以了解到文件变动后,为啥服务启动这么快和 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-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);
  };
}
Last Modified: April 24, 2024
Leave a Comment

已有 1 条评论
  1. srm供应商管理系统 srm供应商管理系统     Windows 10 /    Google Chrome

    感谢分享