为什么 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);
  };
}
相关推荐
我要洋人死24 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人35 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人36 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR41 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香43 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風6 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#