为什么 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);
  };
}
相关推荐
Myli_ing11 分钟前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
dr李四维28 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
雯0609~1 小时前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ1 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z1 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
前端百草阁1 小时前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4041 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five1 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript