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

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 文件的编译结果的?

执行了 ts-node-dev 命令后,会调用 start 方法,

ts 复制代码
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 的处理函数进行 封装

ts 复制代码
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() 函数。

ts 复制代码
// old 函数
require.extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  // 调用
  module._compile(content, filename);
};

compile() 主要是子进程发送一个通知,让主进程接收到这个通知,然后子进程就自己陷入了 阻塞

ts 复制代码
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 文件。

ts 复制代码
// 主进程监听子进程的消息
child.on('message', function (message: CompileParams) {
  if (
    !message.compiledPath ||
    currentCompilePath === message.compiledPath
  ) {
    return
  }
  currentCompilePath = message.compiledPath
  // 调用主进程的编译器进行偏移
  compiler.compile(message)
})

主进程的 compile 方法,通过覆盖 js 默认的 _compile() 方法来完成 ts 文件编译结果的缓存。

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 文件的过程。

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);
  };
}
相关推荐
前端大卫31 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘1 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare1 小时前
浅浅看一下设计模式
前端
Lee川1 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人2 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼2 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端