为什么 2022 年 ESM 又被发布了一次 ?

简介: 为什么 2022 年 ESM 又被发布了一次 ?


这是我最近的一个疑问,明明我们已经用 ESM 写了N年代码了,为啥 2022 年 Node18 和 TS4.7 又宣称自己开始支持 ESM 了?

一个DEMO项目



// src/a.ts
export const a = 123;
// src/index.ts
import { a } from './a'
import fetch from 'node-fetch'
console.log(a, fetch)
// tsconfig.json
{
 "compilerOptions": {
  "moduleResolution": "node",
  "module": "commonjs",
  "outDir": "dist"
 }
}
// package.json
{
  "name": "node-test",
  "version": "1.0.0",
  "main": "dist/index.js"
}

这是目前 NodeWeb 后端项目的基本配置:

  1. 使用 TS 做类型检查
  2. 使用 TS 作为模块解析工具解析模块,这里一般是 Node,即使用 CommonJS 的模块解析方式
  3. 使用 TS 作为转移工具输出成特定格式的代码,这里一般是 CommonJS 格式


疑惑的源头是 node-fetch



node-fetch@3开始,代码库只提供 ESM 版本,因此如果我们使用最新版,在运行我们的 Node 程序,就会报错:$ tsc && node dist/index.js

/Users/futengda/Desktop/playground/node-test/dist/index.js:4
var node_fetch_1 = require("node-fetch");
                   ^
Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/futengda/Desktop/playground/node-test/node_modules/.pnpm/node-fetch@3.2.4/node_modules/node-fetch/src/index.js from /Users/futengda/Desktop/playground/node-test/dist/index.js not supported.
Instead change the require of /Users/futengda/Desktop/playground/node-test/node_modules/.pnpm/node-fetch@3.2.4/node_modules/node-fetch/src/index.js in /Users/futengda/Desktop/playground/node-test/dist/index.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (/Users/futengda/Desktop/playground/node-test/dist/index.js:4:20) {
  code: 'ERR_REQUIRE_ESM'
}

简化一下是:

require() of ES Module node-fetch@3.2.4 from dist/index.js not supported. Instead change the require to a dynamic import().

这里有两个信息,一个是:require 不能引入 ESM,另一个是:如果一定要引入 ESM 请使用 import 函数。这里我们只讨论前者。

参考:https://nodejs.org/api/esm.html#require

这里引出第一个问题:为什么我写的是 ESM,但是却不允许引入 ESM


我们得到的其实不是 ESM



可以看到 tsconfig.json 中,我们默认使用的是 CommonJS 格式,毕竟 Node 一直用的都是 CommonJS。而问题也正是出自这里,因为我们在 CommonJS 的 dist/index.js 中 require 引入了 ESM 的 node-fetch。

但是,很容易想到,把 tsconfig.json 中的 module 改为 ESNext 不就行了吗?不行,又会有新的错误:

$ tsc && node dist/index.js

(node:77294) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/Users/futengda/Desktop/playground/node-test/dist/index.js:1
import { a } from './a';
^^^^^^
SyntaxError: Cannot use import statement outside a module
    at Object.compileFunction (node:vm:352:18)
    at wrapSafe (node:internal/modules/cjs/loader:1033:15)
    at Module._compile (node:internal/modules/cjs/loader:1069:27)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
    at node:internal/main/run_main_module:17:47

可以看到,提示说无法使用import声明,这里引出第二个问题:为什么即便转译成ESNext格式的ESM,依然不允许使用 import 声明语句


Node 不知道这是 ESM



上方错误栈可以看到,Node 依然使用 node:internal/modules/cjs/loader 来处理我们的 ESM 文件。提示也很清晰,我们需要主动告知 Node 如何处理该文件,因为 Node 自身无法知道它是什么格式的。告知 Node 这是一个 ESM 的方案有两个,要么把它改成 .mjs,要么package.json加一个 type: module,我们选后者。

$ tsc && node dist/index.js

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/futengda/Desktop/playground/node-test/dist/a' imported from /Users/futengda/Desktop/playground/node-test/dist/index.js
    at new NodeError (node:internal/errors:372:5)
    at finalizeResolution (node:internal/modules/esm/resolve:437:11)
    at moduleResolve (node:internal/modules/esm/resolve:1009:10)
    at defaultResolve (node:internal/modules/esm/resolve:1218:11)
    at ESMLoader.resolve (node:internal/modules/esm/loader:580:30)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:294:18)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:80:40)
    at link (node:internal/modules/esm/module_job:78:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}

这里引出第三个问题:为什么即便Node把它当成ESM处理了,依然 Cannot find module dist/a imported from dist/index.js


Node 不认识这种格式的 ESM



”这种格式的 ESM“,这段话让人很费解,难道 ESM 格式还有多种格式?算是吧,虽然ESM标准只有一个,但是ESM的实现各有不同,这里我个人理解成两大类:

  1. ESM 运行时实现
  • Node 的 ESM 运行环境
  • Browser的 ESM 运行环境
  1. ESM 编译时实现
  • Rollup/Webpack/ESBuild 的 ESM 的 打包构建 能力
  • TypeScript/Babel/SWC 的 ESM 的 转译成其他格式 能力
  • Vite 的 ESM 的 bare import转url import 能力

对于 ESM 实现的不同主要体现在如何处理 ESM 格式代码中的模块解析这方面,也就是如何处理 from 后面的 module specifier。

对于运行时实现的模块解析,就是完全遵从 ESM 标准,只不过 Node (现在)相比 Browser 更为强大,除了相对、绝对路径的 module specifier,还可以借助 package.json 的 exports 字段支持 bare specifier。

参考:https://nodejs.org/api/esm.html#import-specifiers 当然浏览器也可能通过 imports-map 来支持 bare specifier,可以期待一下。

而对于编译时实现,虽然我们写的代码是使用 ESM 格式,但是模块解析时却依然走的是 CommonJS 那老一套。因此,当我们在 tsconfig.json 中指定 module 为 esnext 时,其实是想让 TS 生成一个以 CommonJS 方式解析模块的 ESM 格式的代码。

为什么依然走的是老一套?因为历史问题,”自古以来“前端工程化都是基于 Node 的 CommonJS 规范。不过在 Node18 ESM稳定之后,相信历史进程将会推进。

所以,当你告诉 Node,让它使用标准的 ESM 运行时去运行一个“本来打算以 CommonJS 方式解析模块的ESM文件”时,就会出问题,因为这不是Node ESM运行时所支持的 ESM格式。

那么,怎么产出一个 Node ESM运行时支持的 ESM 格式呢?这就回到了最开始的问题,那就是 TS4.7 新推出的 ESM 能力。可以通过在 tsconfig.json 中指定 module 为 nodenext 来实现,现在我们再试一次:

$ tsc && node dist/index.js

error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './a.js'?
1 import { a } from './a'
                    ~~~~~
Found 1 error in src/index.ts:1

又报错了?不过这问题不大,因为现在 TS 可以检测出这段代码不符合 Node ESM 规范,这正是我们想要的。只需要把 ./a 改为 ./a.js 即可,不要觉得在 ts 里写 .js 奇怪,这就是标准写法。


结尾:为什么 ESM 又被发布了一次?



一直有的是:以 CommonJS 作为模块解析方式的 ESM 格式 新发布的是:以 ESM 标准作为模块解析方式的 ESM 格式。

一个很重要的事情是,要明白自己所写的 ESM 到底是运行在什么样的环境里。

相关文章
|
SQL 存储 关系型数据库
一文搞懂SQL优化——如何高效添加数据
**SQL优化关键点:** 1. **批量插入**提高效率,一次性建议不超过500条。 2. **手动事务**减少开销,多条插入语句用一个事务。 3. **主键顺序插入**避免页分裂,提升性能。 4. **使用`LOAD DATA INFILE`**大批量导入快速。 5. **避免主键乱序**,减少不必要的磁盘操作。 6. **选择合适主键类型**,避免UUID或长主键导致的性能问题。 7. **避免主键修改**,保持索引稳定。 这些技巧能优化数据库操作,提升系统性能。
1077 4
一文搞懂SQL优化——如何高效添加数据
|
2月前
|
机器学习/深度学习 人工智能 自然语言处理
B站开源IndexTTS2,用极致表现力颠覆听觉体验
在语音合成技术不断演进的背景下,早期版本的IndexTTS虽然在多场景应用中展现出良好的表现,但在情感表达的细腻度与时长控制的精准性方面仍存在提升空间。为了解决这些问题,并进一步推动零样本语音合成在实际场景中的落地能力,B站语音团队对模型架构与训练策略进行了深度优化,推出了全新一代语音合成模型——IndexTTS2 。
1923 62
|
Android开发
mac下配置adb环境变量
在终端中输入adb命令时,会提示 command not found ,这是是因为mac电脑下没有配置Android环境变量或者环境变量配置错误。
|
安全 网络协议 算法
HTTPS网络通信协议揭秘:WEB网站安全的关键技术
HTTPS网络通信协议揭秘:WEB网站安全的关键技术
887 4
HTTPS网络通信协议揭秘:WEB网站安全的关键技术
|
1月前
|
人工智能 JSON 监控
三步构建AI评估体系:从解决“幻觉”到实现高效监控
AI时代,评估成关键技能。通过错误分析、归类量化与自动化监控,系统化改进AI应用,应对幻觉等问题。Anthropic与OpenAI均强调:评估是产品迭代的核心,数据驱动优于直觉,让AI真正服务于目标。
|
机器学习/深度学习 存储 TensorFlow
【Python机器学习】卷积神经网络卷积层、池化层、Flatten层、批标准化层的讲解(图文解释)
【Python机器学习】卷积神经网络卷积层、池化层、Flatten层、批标准化层的讲解(图文解释)
713 0
|
存储 缓存 数据库
缓存技术有哪些应用场景呢
【10月更文挑战第19天】缓存技术有哪些应用场景呢
|
Kubernetes 架构师 Java
史上最全对照表:大厂P6/P7/P8 职业技能 薪资水平 成长路线
40岁老架构师尼恩,专注于帮助读者提升技术能力和职业发展。其读者群中,多位成员成功获得知名互联网企业的面试机会。尼恩不仅提供系统化的面试准备指导,还特别针对谈薪酬环节给予专业建议,助力求职者在与HR谈判时更加自信。此外,尼恩还分享了阿里巴巴的职级体系,作为行业内广泛认可的标准,帮助读者更好地理解各职级的要求和发展路径。通过尼恩的技术圣经系列PDF,如《尼恩Java面试宝典》等,读者可以进一步提升自身技术实力,应对职场挑战。关注“技术自由圈”公众号,获取更多资源。
|
存储 SQL BI
深入解析实时数仓Doris:介绍、架构剖析、应用场景与数据划分细节
深入解析实时数仓Doris:介绍、架构剖析、应用场景与数据划分细节