nodejs 中的模块
TODO
模块加载器
CommonJS module loader | ECMAScript module loader |
---|---|
synchronous | asynchronous |
responsible for handlingrequire() | responsible for handling import , import() |
monkey patchable | not monkey patchable |
supports folders as modules | directory indexes must be fully specified |
can ignore .js , .json , .node ext | mandatory ext (only .js , .cjs , .mjs ) |
support JSON | need assert {type: 'json'} |
cannot be used to load esm | can be used to load cjs |
虽然在 cjs 中使用 import() 可以获取到 esm 模块的内容,但这并不是 CommonJS 模块加载器所提供的功能。
1
2
3
4
(async () => {
const aModule = await import("./a.mjs") // 注意只能使用 await,而不能使用 .then()
console.log(aModule) // [Module: null prototype] { a: '123' }
})()
monkey patchable (猴子补丁)
模块入口
package.json 中的 main
和 exports
都可以用于指定 cjs 和 esm 模块的入口。两者同时存在时,exports
的优先级高于 main
(如果支持 exports 的话)
main
功能有限,只能指定一个入口exports
是在 12.7.0 版本时添加的,要求值必须以./
开头。它可以指定多个入口。并且它定义了导出域,如果尝试导入一个不在exports
中定义的路径,那么将找不到对应内容
exports 限定了导出
以前,用户可以这样导入内容:
1
2
3
4
// node_modules/my-module/package.json
{
"name": "my-module"
}
1
2
// node_modules/my-module/secret.js
module.exports = 'secret'
1
2
3
// index.js
const secret = require('my-module/secret')
console.log(secret) // 'secret'
现在,你可以通过 exports 来限定导出
1
2
3
4
5
// node_modules/my-module/package.json
{
"name": "my-module",
"exports": "./bundle/index.js"
}
1
2
3
// index.js
const secret = require('my-module/secret')
// 报错:Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './secret' is not defined by "exports"
exports 指定 subpath exports
1
2
3
4
5
6
7
8
9
10
{
"name": "my-module",
"exports": {
".": "./bundle/index.js",
"./foo": "./bundle/foo.js",
"./bar.js": "./bundle/bar.js",
// 注意,这里的属性名同样要求以 ./ 开头
// "foo": "./bundle/foo.js", // ❌
}
}
使用 subpath exports 导入时:
1
2
3
4
5
6
7
8
9
// esm
import myModule from 'my-module'
import foo from 'my-module/foo' // 不能写成 'my-module/foo.js'
import bar from 'my-module/bar.js' // 不能写成 'my-module/bar'
// cjs
const myModule = require('my-module')
const foo = require('my-module/foo') // 不能写成 'my-module/foo.js'
const bar = require('my-module/bar.js') // 不能写成 'my-module/bar'
通过上面案例,可以看出,如果你开发一个包,并且有 subpath exports 时,那么你至少要提供两种 subpath exports,一种带后缀名,一种不带后缀名!
此外,如果你只有一个模块入口,那么你可以使用语法糖:
1
2
3
4
{
"exports": "./bundle/index.js"
// 等同 "exports": { ".": "./bundle/index.js" }
}
如果你需要导出非常多的模块入口,那么可以这样:
1
2
3
4
5
6
7
8
9
10
{
"name": "my-module",
"exports": {
".": "./index.js",
"./features/*": "./bundle/*.js",
"./features/*.js": "./bundle/*.js",
// 批量导出时,如果想要因此其中的子文件夹,可以显示为其附上 null 值
"./features/private-internal/*": null
}
}
exports 指定 conditional exports
常用的条件有:
types
browser
import
require
default
手写案例:
1
2
3
4
5
6
7
8
9
10
{
"name": "my-module",
"exports": {
"require": "./bundle/index.cjs",
"import": "./bundle/index.mjs",
// 对于任意环境,default 都会成功匹配到,所以 default 一定要放在最后面
// 习惯上,可以忽略 import,而是使用 default 代替。所以 index.js 中使用的是 esm。
"default": "./bundle/index.js"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// node_modules/my-module/bundle/index.mjs
export default 'esm'
// node_modules/my-module/bundle/index.cjs
module.exports = 'cjs'
// test.mjs
import myModule from 'my-module'
console.log(myModule) // 'esm'
// test.cjs
const myModule = require('my-module')
console.log(myModule) // 'cjs'
如果想要结合 subpath,可以这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "my-module",
"exports": {
".": {
"require": "./bundle/index.cjs"
"import": "./bundle/index.mjs",
},
"./foo": {
"require": "./bundle/foo.cjs"
"import": "./bundle/foo.mjs",
},
"./foo.js": {
"require": "./bundle/foo.cjs"
"import": "./bundle/foo.mjs",
}
}
}
案例:axios 中的 exports
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"exports": {
".": {
// 如果提供了 types,要写在最前面
"types": {
"require": "./index.d.cts",
"default": "./index.d.ts"
},
"browser": {
"require": "./dist/browser/axios.cjs",
"default": "./index.js"
},
"default": {
"require": "./dist/node/axios.cjs",
"default": "./index.js"
}
},
}
}
subpath imports
子路径,也就是包名以 #
开头,从而实现模块路径的简写。
比如,现在 package.json 中定义:
1
2
3
4
5
6
7
8
9
10
// package.json
{
"imports": {
"#internal/*.js": "./src/internal/*.js",
"#async_hooks": {
"node": "async_hooks",
"default": "./async-hooks-stub.js"
}
}
}
然后可以这样直接使用:
1
2
3
4
5
6
import internalZ from '#internal/z.js'
// 加载 ./src/internal/z.js 模块
import {AsyncResource} from '#async_hooks'
// 加载 ./async-hooks-stub.js 模块
参考
本文由作者按照 CC BY 4.0 进行授权