前言
前不久在参与一个开源库的开发的时候,看到了该库是使用 esno 来执行就脚本,而之前在 Node 环境下开发的时候,如果我们想执行 TS 文件的话,我都是使用 ts-node 这个库,相信不少同学应该也是用过这个库的。
那为什么会选择了 esno 呢?于是我今天就去了解了一下 esno ,看看 esno 到底做了什么?
基于 esbuild
esno 是基于 esbuild 的 TS/ESNext node 运行时。
该库会针对不同的模块化标准,在 esno README 中写道:
- esno - Node in CJS mode - by esbuild-register
- esmo - Node in ESM mode - by esbuild-node-loader
和 ts-node 最根本上的区别就是使用 esbuild 作编译器了,使用上也是比较简单的,和 ts-node 没差别,不管怎么说 esbuild 的速度确实是快。
esno 工作原理
esno 是通过 esbuild-register, esbuild-register 又是通过 pirates 添加拦截 require 钩子,替换 module._compile 方法来进行完成编译功能,对,就是这么简单。
而这里要说一个知识, 就是 node require 文件的时候到底发生了什么,为什么可以做到直接编译 ts?
require hook
当我们 require 一个 js 文件的时候 会按顺序执行以下方法:
- Module.load
- Module._extensions['.js']
- Module._compile
而直接需要更改 ts 我们可以通过 ._extensions['.ts'] 更改这个方法即可,所以我们也可以通过这个原理 进行简单的写一个 esbuild 编译 ts 文件。
实现一个 esno
我们先准备一下要运行的 ts 代码, 一个 sayHello.js:
import { hello } from "./hello";
hello("esno");
再准备一个 hello.ts:
export const hello = (str: string) => {
console.log("hello " + str);
};
而我们要用的写法是:
node esno.js ./sayHello.ts
首先第一步我们需要重写 Module._extensions['.ts'] 方法,在里面读取文件内容,然后调用 esbuild.transformSync 来把 ts 转成 js,之后调用 Module._compile 来处理编译后的 js。 然后我们用 process.argv 来取具体需要编译的文件路径,argv[2]; 简单写一下,以下:
const path = require("path");
const esbuild = require("esbuild");
const fs = require("fs");
const filePath = process.argv[2];
require.extensions[".ts"] = function (module, filename) {
const filePath = path.resolve(__dirname, filename);
const content = fs.readFileSync(filePath, "utf-8");
const { code } = esbuild.transformSync(content, {
loader: "ts",
target: "es2017",
format: "cjs",
});
module._compile(code, filename);
};
require(filePath);
测试一下:
非常 nice,当然距离真正使用还有很多细节需要处理,这只是简单的介绍一下原理。
总结
esno 作为使用 esbuild 为编译器,还是比较新颖,但也是必然出现的结果,目前真正使用我认为还是 ts-node 比较稳健一些。
而无论是 ts-node 还是 esno 都是通过修改 require hook 完成,所以才会有 pirates 等库的出现,方便其他的三方库进行添加 require 的钩子,处理好细节,这次简单的通过这个案例(直接执行 ts 文件),我们清楚了在 node 中是如何直接运行 ts 文件的。