Node ES Module 为什么需要文件扩展名
解释 Node.js 中 ES Module 必须使用文件扩展名的原因
问题
为什么 Node.js 在使用 ES Module 时必须加上文件扩展名?
解答
这个问题涉及两个方面:如何识别 ES Module,以及为什么需要完整路径。
无法从语法上区分 ES Module
TC39 在设计 ES Module 时,无法从语法上严格区分一段代码是 ES Module 还是传统 script(CommonJS 本质上仍是传统 script)。
虽然有 import、export 语句的代码是 ES Module,但没有这些语句也不代表就不是 ES Module。
Node 社区曾在 TC39 提出提案(tc39/proposal-UnambiguousJavaScriptGrammar)来通过语法区分,可能的方案包括:
方案 1:引入 “use module” 指令
"use module";
// 模块代码
优点是容易理解和实现,没有额外解析成本;缺点是对于已有 export 语句的模块显得多余。
方案 2:通过 export 语句判断
对于不需要 export 的模块,开发者通过 export {} 标记为 ES Module。
// 没有导出内容,但标记为 ES Module
export {};
优点是大多数模块不需要额外标记;缺点是 export 语句不一定在代码头部,解析器需要预扫描。
方案 3:引入新语法标记
优缺点类似方案 1。
这些方案在 TC39 讨论时都未通过,将来也不太可能引入。TypeScript 使用的是方案 2 来确定是否是 ES Module。
需要外部信息标识
既然无法通过代码内容判断,就需要外部信息:
Web 平台:通过 <script type="module"> 标明
<script type="module" src="./app.js"></script>
<link rel="modulepreload" href="./module.js">
new Worker(path, { type: 'module' });
Node.js:通过文件扩展名或 package.json 标明
// 使用 .mjs 扩展名
import "./my-module.mjs";
// 或在 package.json 中设置
{
"type": "module"
}
为什么需要完整路径
Node.js 的 CommonJS 模块有复杂的 fallback 机制。对于 require('./my-module'),会依次查找:
my-module(无扩展名)my-module.jsmy-module/index.js
这种机制基于文件系统访问成本很小的假设,在服务器端可以接受。
但在浏览器端,这样的 fallback 会导致多次网络请求,成本无法接受。因此浏览器端的 import 语句使用标准 URL,通常包含完整的文件扩展名:
// 浏览器端需要完整路径
import "./my-module.js";
Node.js 加入 ES Module 时需要考虑与浏览器的一致性,因此也要求使用完整路径。
浏览器端的额外限制
浏览器端不能直接使用「裸名字」:
// 不能直接使用
import "my-module";
// 需要通过 import maps 定义
<script type="importmap">
{
"imports": {
"my-module": "./node_modules/my-module/index.js"
}
}
</script>
Node.js 虽然可以像 require 那样直接使用 import "my-module",但也加入了类似 import maps 的机制。
Web 平台与 Node.js 的解析差异
对于 import "./file.js":
- Web 平台:总是将
file.js作为 ES Module 解析 - Node.js:根据外部信息解析,
.js后缀默认按 CommonJS 解析,除非 package.json 中设置了"type": "module"
Web 平台在确定脚本资源时(如缓存),不仅考虑 URL,还需要纳入解析目标(parse goal)。
Node.js 为了兼容既有的 CommonJS 资产,决定同时支持 ES Module 和 CommonJS,因此 import "./file.js" 不能总是按 ES Module 解析。Node.js 的模块缓存一直基于 URL 唯一(文件系统没有 MIME type)。
关键点
- TC39 无法从语法上严格区分 ES Module 和传统 script,因此需要外部信息(文件扩展名或配置)来标识
- 浏览器端使用
<script type="module">,Node.js 使用.mjs扩展名或 package.json 的"type": "module"字段 - CommonJS 的 fallback 机制在浏览器端会导致多次网络请求,ES Module 需要完整路径以保持与浏览器的一致性
- Web 平台总是将
.js文件作为 ES Module 解析,而 Node.js 默认按 CommonJS 解析(除非明确配置)
目录