Vue 核心成员 antfu 写的 esno 有什么用?

2,993 阅读2分钟

前言

前不久在参与一个开源库的开发的时候,看到了该库是使用 esno 来执行就脚本,而之前在 Node 环境下开发的时候,如果我们想执行 TS 文件的话,我都是使用 ts-node 这个库,相信不少同学应该也是用过这个库的。

那为什么会选择了 esno 呢?于是我今天就去了解了一下 esno ,看看 esno 到底做了什么?

基于 esbuild

esno 是基于 esbuild 的 TS/ESNext node 运行时。

该库会针对不同的模块化标准,在 esno README 中写道:

和 ts-node 最根本上的区别就是使用 esbuild 作编译器了,使用上也是比较简单的,和 ts-node 没差别,不管怎么说 esbuild 的速度确实是快。image.png

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);

测试一下:

image.png

非常 nice,当然距离真正使用还有很多细节需要处理,这只是简单的介绍一下原理。

总结

esno 作为使用 esbuild 为编译器,还是比较新颖,但也是必然出现的结果,目前真正使用我认为还是 ts-node 比较稳健一些。

而无论是 ts-node 还是 esno 都是通过修改 require hook 完成,所以才会有 pirates 等库的出现,方便其他的三方库进行添加 require 的钩子,处理好细节,这次简单的通过这个案例(直接执行 ts 文件),我们清楚了在 node 中是如何直接运行 ts 文件的。