MENU

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

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

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 方法,

copy
  • 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 的处理函数进行 封装

copy
  • 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() 函数。

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

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

copy
  • 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 文件。

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

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

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

copy
  • 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

    感谢分享