Node.js ESM 解析算法(Resolution Algorithm)的职责只有两个:
- 将
import中的 specifier 解析为最终 URL - 判断该 URL 对应的模块格式
例如:
Node.js 最终需要得到:
react -> file:///project/node_modules/react/index.js
./utils.js -> file:///project/src/utils.js
#config -> file:///project/src/config/index.js
以及:
module
commonjs
json
wasm
等模块格式。
整个 ESM 规范本质上是在描述:
specifier
↓
resolved URL
↓
module format
↓
load
↓
execute
一、ESM_RESOLVE 总体流程
Node.js ESM 的入口算法:
ESM_RESOLVE(specifier, parentURL)
可以简化为:
整个算法的核心目标:
specifier
↓
唯一 URL
而不是像 CommonJS 那样不断猜测。
二、specifier 分类
Node.js 将 specifier 分为四类。
1. URL Specifier
例如:
或者:
如果本身是合法 URL:
new URL(specifier)
成功。
那么直接返回。
2. Relative Specifier
例如:
Node.js 根据当前模块位置解析。
假设:
file:///app/src/main.js
执行:
得到:
file:///app/src/utils.js
本质上就是:
3. Package Imports
例如:
或者:
以:
#
开头。
会进入:
PACKAGE_IMPORTS_RESOLVE()
读取当前包:
例如:
最终得到:
./src/db/index.js
4. Bare Specifier
例如:
既不是:
URL
路径
#import
就属于裸包名。
进入:
PACKAGE_RESOLVE()
开始查找 node_modules。
三、PACKAGE_RESOLVE
这是 Node.js 查找 npm 包的核心逻辑。
例如:
当前文件:
/app/src/pages/home/index.js
Node.js 会不断向上查找:
/app/src/pages/home/node_modules/react
/app/src/pages/node_modules/react
/app/src/node_modules/react
/app/node_modules/react
/node_modules/react
直到找到。
等价于:
while(currentDirectory){
查找 node_modules/packageName
找不到继续向上
}
四、读取 package.json
找到包以后:
node_modules/react/package.json
Node.js 开始读取配置。
优先级如下:
exports
↓
main
↓
直接路径
五、exports 机制
现代 Node.js 包解析几乎完全依赖 exports。
例如:
允许:
对应:
.
得到:
./dist/index.js
允许:
对应:
./jsx-runtime
得到:
./dist/jsx-runtime.js
但是:
如果 exports 中不存在:
则报错:
Package Path Not Exported
六、exports 的本质
exports 可以理解为:
包对外暴露的公开 API
例如:
允许:
禁止:
即使文件真实存在。
七、Conditional Exports
exports 不一定是字符串。
可以是对象。
例如:
Node.js 会根据条件选择。
ESM:
匹配:
得到:
./dist/index.mjs
CommonJS:
匹配:
得到:
./dist/index.cjs
八、PACKAGESELFRESOLVE
假设:
当前代码就在:
my-lib
内部。
那么:
不会去外部 node_modules 查找。
Node.js 会发现:
当前包名就是 my-lib
直接使用当前 package.json 的 exports。
这就是:
PACKAGE_SELF_RESOLVE
九、imports 机制
imports 和 exports 很像。
区别:
exports 给别人用
imports 给自己用
例如:
之后:
实际解析:
./src/db/index.js
再例如:
得到:
./src/utils/string.js
十、Pattern Match
imports 和 exports 支持通配符。
例如:
执行:
匹配:
*
得到:
user
最终:
./src/features/user.js
十一、PACKAGETARGETRESOLVE
这是 exports/imports 真正执行映射的地方。
支持四种类型。
String
直接解析。
Object
根据 conditions 选择。
Array
前一个失败。
继续尝试下一个。
null
明确禁止导出。
十二、ESMFILEFORMAT
URL 定位完成后。
Node.js 需要判断:
这个文件应该按什么格式加载?
.mjs
module
.cjs
commonjs
.json
json
.wasm
wasm
.node
addon
原生扩展模块。
十三、type 字段的作用
对于:
.js
文件。
Node.js 会查找最近的 package.json。
例如:
那么:
被解释为:
ESM
如果:
则:
被解释为:
CommonJS
因此:
.mjs
永远 ESM。
.cjs
永远 CommonJS。
.js
取决于 type。
十四、为什么 ESM 不支持目录导入
CommonJS:
Node.js 会尝试:
foo.js
foo.json
foo.node
foo/index.js
foo/index.json
ESM:
不会猜。
如果:
foo
是目录。
直接报错:
Unsupported Directory Import
正确写法:
或者:
十五、LOOKUPPACKAGESCOPE
Node.js 如何找到最近的 package.json?
算法:
当前目录
↓
父目录
↓
继续向上
↓
直到根目录
例如:
/app/src/pages/home/index.js
查找:
/app/src/pages/home/package.json
/app/src/pages/package.json
/app/src/package.json
/app/package.json
找到第一个就停止。
十六、自定义 ESM Resolver
Node.js 默认解析:
如果想支持:
怎么办?
答案:
Loader Hook
例如:
运行:
node --loader ./loader.mjs app.js
之后:
就会自动映射:
src/utils.js
十七、完整解析链路
Node.js ESM 解析可以总结为:
总结
Node.js ESM 的核心思想不是“寻找文件”。
而是:
specifier
↓
确定 URL
↓
确定模块格式
↓
加载执行
整个 exports、imports、type、conditional exports、loader 机制,本质上都是围绕这一目标构建的。
ESM 解析模型相比 CommonJS 更严格、更静态、更接近浏览器,也更适合现代工具链(Vite、Webpack、Rspack、Rollup、Turbopack)进行分析和优化。