文章

nodejs 中的模块

TODO

模块加载器

CommonJS module loaderECMAScript module loader
synchronousasynchronous
responsible for handlingrequire()responsible for handling import, import()
monkey patchablenot monkey patchable
supports folders as modulesdirectory indexes must be fully specified
can ignore .js, .json, .node extmandatory ext (only .js, .cjs, .mjs)
support JSONneed assert {type: 'json'}
cannot be used to load esmcan 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 中的 mainexports 都可以用于指定 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 进行授权