ts-node的踩坑之旅

使用ts-node

Typescript 给我们的开发提供了许多便利(类型和名称的自动推断真香,再也不用手打变量名了)。但开发时爽了,运行时却有些麻烦,需要先将 Typescript 代码编译为 Javascript 代码再运行.而很多时候我只是要执行一个一次性的 Nodejs 脚本命令。这时就要使用 ts-node

ts-node 是 Typescript 代码的交互式执行器,如果想要直接执行 Typescript 代码,用 ts-node foo.ts 代替 node foo.ts 即可。

ts 复制代码
const fn = (name : string) => {
    console.log(name);
}

fn("xiaoming")

运行:

ruby 复制代码
$ ts-node foo.ts

xiaoming

但在使用 ts-node 时,常常会遇到一些奇奇怪怪的错误,这些错误主要是由于 Javascript 中的两种模块化规范: Commonjs 与 ESModule 引起的,两者的详细区别可以参考阮一峰老师的《es6标准入门》

踩坑之旅

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"

假如你在编写一个 Typescript 库函数,你希望将其编译为 ESModule ,那么你可以通过在 package.json 中声明 "type": "module" 来告诉使用者你的库函数使用的模块规范是 ESModule 。

但如果你使用 ts-node 来运行这个库时,你很可能会遇到一行错误:

css 复制代码
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"

这是因为通过 tsc --init 生成默认 tsconfig.json 使用的默认模块规范是:"module": "commonjs", 也就是说 Typescript 的默认配置是将代码编译为 commonjs 的模块,而非我们在 package.json 中声明的 module (即 ES module)模块。

ts-node 在运行时会既会读取 package.json 又会读取 tsconfig.json ,而二者的配置相互冲突,于是产生了错误。

那怎么办呢?不用慌, ts-node 的 README.md 文件中已经给出了解决办法:

json 复制代码
{
  "compilerOptions": {
    "module": "ESNext" // or ES2015, ES2020
  },
  "ts-node": {
    // Tell ts-node CLI to install the --loader automatically, explained below
    "esm": true
  }
}

非常好理解,既然 package.jsontsconfig.json 的配置相互冲突,那么让他们一致声明模块为 ESModule 不就好了吗? esm 配置表示在调用 ts-node 时自动添加 ts-node --esm 参数。

因此上面做的事情总结为:

  • 对 Typescript 声明编译目标为 ESModule
  • 对 ts-node 声明运行方法为 ts-node --esm ,将项目看作 ES-Module 模块。

注意

如果你在添加了上述配置后依然报相同的错误,是因为 Nodejs 支持 ES-Module 依然是一个实验性的功能,不稳定, 可见 ECMAScript Modules in Node.js中的一段话:

For the last few years, Node.js has been working to support running ECMAScript modules (ESM). This has been a very difficult feature to support, since the foundation of the Node.js ecosystem is built on a different module system called CommonJS (CJS).

翻译成人话就是:Node.js 底层生态都是用 CommonJS 规范写的,要支持 ESModule 改起来太TM难了。

因此有可能是 Nodejs 的新版本对相关的 API 做了调整, ts-node 还没有来得及进行适配,笔者将 Nodejs 版本从 v20.5.1 降到 v18.17.1 后不再出现错误。

SyntaxError: Cannot use import statement outside a module

既然 ESModule 很容易出问题,那么我们当然得试一试导入导出语句 importexport 了,看看在这种情况下 ts-node 能否正常执行。

下面是一个简单的例子:

json 复制代码
// package.json
{
  ...
  "type": "module",
}
json 复制代码
// tsconfig.json
{
    "compilerOptions": {
        "target": "es2016",
        "module": "esnext",
    }
}
ts 复制代码
// bar.ts
export function bar(){
    console.log('bar');
}
ts 复制代码
// foo.ts
import { bar } from "./bar";

bar()

然后我们执行 ts-node foo.ts

arduino 复制代码
CustomError: Cannot find module 'bar' imported from foo.ts

好的,又双叒叕报错了。因为 ts-node 本质上只是帮我们在内存中做了编译这个步骤,也就是说问题出在 Typescript 编译成 JavaScript 这个环节。 因此我们需要翻阅 Typescript 的文档: 看看 ECMAScript Modules in Node.js 中怎么说,它也举了一个例子,和我们前面的例子非常类似:

ts 复制代码
// ./foo.ts
export function helper() {
    // ...
}
// ./bar.ts
import { helper } from "./foo"; // only works in CJS
helper();

This code works in CommonJS modules, but will fail in ES modules because relative import paths need to use extensions. As a result, it will have to be rewritten to use the extension of the output of foo.ts - so bar.ts will instead have to import from ./foo.js.

上面的代码只能在 CommonJS 模块规范下使用,ESModule 要实现类似的相对路径导入需要使用拓展名,也就是说 ESModule 需要写成下面的样子:

ts 复制代码
// ./bar.ts
import { helper } from "./foo.js"; // works in ESM & CJS
helper();

同理,我们刚才的导入语句需要从:

ts 复制代码
// foo.ts
import { bar } from "./bar";

bar()

修改为:

ts 复制代码
// foo.ts
import { bar } from "./bar.js";

bar()

然后我们尝试执行:

ruby 复制代码
$ ts-node foo.ts

bar

可以正常执行,但很怪,对吧?bar.js 是编译后的产物,我们只有 bar.ts 文件,但写导入语句时却要假装 bar.js 已经被编译好了,已经存在我们的目录下。

为了看看背后发生了什么,我们先不使用 ts-node ,手动编译一下未修改之前的 foo.ts 文件试试:

ts 复制代码
// foo.ts
import { bar } from "./bar";

bar()
tsc
js 复制代码
// foo.js

import { bar } from "./bar";
bar();

没有任何更改,根据 ./bar 找不到 ./bar.js 文件,于是我们必须要加上 .js 后缀让其能够匹配到 ./bar.js 文件,即使这个 ./bar.js 文件在编译后才出现.

那么如果我们编译成 CommonJs 模块呢?

json 复制代码
{
  "compilerOptions": {
	  //...
    "module": "commonjs",
  }
}
tsc
js 复制代码
//foo.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const bar_1 = require("./bar");
(0, bar_1.bar)();

看起来 requireimport 不同,并不需要我们明确指明 js 文件,它可以通过 ./bar 匹配到 ./bar.js 文件。

关于应用场景

虽然 ESModule 已经被浏览器广泛支持,但由于历史问题,Node.js 主要还是使用 CommandJs 的模块规范,虽然 Typescript 的默认编译配置 CommonJs 能满足大部分场景。但有时候我们依然会遇到需要在 Node.js 中使用 ESModule 的情况。

比如你在开发一个全栈项目,你先将 package.json 中的 type 设置为 module ,表示代码会被编译为 ESModule 让浏览器执行。但同时,你又需要编写服务端的 Node.js 代码,或是需要跑一些 Node.js 脚本,因为他们处于同一项目下,一般来说会遵循相同的模块规范。这时可能就需要用 ts-node 跑这些 ESModole 类型的 Node.js 代码。

相关推荐
我要洋人死17 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人29 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人29 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR34 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香36 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q24985969339 分钟前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風6 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#