Node.js 使用 CJS 规范(使用 require 来进行模块导入),除了 .js 文件之外,Node.js 还支持以拓展的形式来提供自定义拓展名的模块加载机制。

这也是 tsx、ts-node、require-ts 这些工具库的工作原理。

核心逻辑都是通过 require.extension,注册了 .ts 文件的处理逻辑。

以 tsx 为例:

function transformer(module: Module, filePath: string) {
  // ...

  let code = fs.readFileSync(filePath, 'utf8')

  // ...

  module._compile(code, filePath)
}

;['.js', '.ts', '.tsx', '.jsx'].forEach((extension) => {
  extensions[extension] = transformer;
})

而在 require-ts 中,使用了 pirates 这个库来简化注册逻辑:

const compiler = new Compiler(/* ... */);

const EXTS = ['.ts', '.tsx']
addHook(
  (code, filename) => {
    return compiler.compile(filename, code)
  },
  { exts: EXTS, matcher: () => true }
)

Node.js 中的 require 执行逻辑

详情请看:Node.js Modules 解析 - resolve 机制

  1. Resolution。基于入参拼接出 require 文件的绝对路径,
    1. 当路径中不包含后缀名时,会按照 Node.js 的模块解析策略来进行处理,如 require('./utils') 会解析到 PATH/TO/project/utils.js;而 require('project-utils') 会解析到 PATH/TO/project/node_modules/project-utils/src/index.js,以及内置模块等。需要注意的是在浏览器中,require 需要带上完整的后缀名(浏览器并不能查找服务器的文件),但一般 bundler 会帮你处理好。
  2. 基于绝对路径,去 require.cache 这个全局变量中,查找此文件是否已经已缓存,并在存在时直接使用缓存的文件内容(即这个文件的导出信息等)。
  3. Loading。基于绝对路径实例化一个 Module 类实例,基于路径后缀名调用内置的处理函数。比如 jsjson 文件都是通过 fs.readFileSync 读取文件内容。
  4. Wrapping。对于 js 文件,将文件内容字符串外层包裹一个函数,执行这个函数。对于 JSON 文件,将内容包裹挂载到 module.exports 下。
  5. Evaluating。执行这个文件内容。
  6. Caching。对于未缓存的文件,将其执行结果缓存起来。

在上述过程中进行操作拦截,就可以实现很多有用的功能。比如: