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 Modules 解析 - resolve 机制
require 文件的绝对路径,
require('./utils') 会解析到 PATH/TO/project/utils.js;而 require('project-utils') 会解析到 PATH/TO/project/node_modules/project-utils/src/index.js,以及内置模块等。需要注意的是在浏览器中,require 需要带上完整的后缀名(浏览器并不能查找服务器的文件),但一般 bundler 会帮你处理好。require.cache 这个全局变量中,查找此文件是否已经已缓存,并在存在时直接使用缓存的文件内容(即这个文件的导出信息等)。js、json 文件都是通过 fs.readFileSync 读取文件内容。js 文件,将文件内容字符串外层包裹一个函数,执行这个函数。对于 JSON 文件,将内容包裹挂载到 module.exports 下。在上述过程中进行操作拦截,就可以实现很多有用的功能。比如:
.ts 文件去注册自定义的处理函数,将其编译为可以直接执行的 js 代码(ts-node/register).js 代码进行预处理(babel-register),在代码执行时进行覆盖率统计(istanbul)require.cache 进行缓存清除来实现 Node.js 服务的热更新(decache),但这里涉及到 require cache 的缓存策略