为什么 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);
  };
}
相关推荐
腾讯TNTWeb前端团队7 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰10 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪10 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪10 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy11 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom12 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom12 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom12 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom12 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom12 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试