1. 简介
Node.js 一直以来都是采用的 CommonJS 模块系统,后来 ES6 推出了新型的模块系统,现在熟称为 ES 模块。
Node.js 从2019年的 13.2.0 版开始初步支持 ES 模块。这样就有两个模块系统共存于 Node.js。
本篇文章基于 Node.js 20.10.0 版本讲述如何指定模块系统。
2. 两种模块系统举例
我们先简单温习一下两个模块系统。
举例如下:
ES 模块:
js
import path from 'node:path';
export function getPath(name) {
return path.resolve(name);
}
CommonJS:
js
const path = require('node:path');
module.exports.getPath = function(name) {
return path.resolve(name);
}
我们这里主要讲在 Node.js 中如何指定模块系统,并不打算细讲这两个模块系统的用法。
3. 如何为执行文件指定模块系统
在 Node.js 中,同一个文件中不可以混用两种模块系统。
决定某个 Node.js 程序文件使用哪种模块系统有如下决定因素,按照优先级由高到低排列:
- 文件后缀名。
- [package.json 中的 type 属性](#package.json 中的 type 属性 "#h3-package.jsonE4B8ADE79A84typeE5B19EE680A7")。
- [--experimental-default-type 启动参数](#--experimental-default-type 启动参数 "#h3-E28094experimental-default-type20E590AFE58AA8E58F82E695B0")。
3.1 文件后缀名
优先级最高。
.mjs
后缀,代表 ES 模块,如果使用该后缀名,那无论在哪种情况下,其一定会被解析成 ES 模块。 .cjs
后缀,代表 CommonJS模块,如果使用该后缀名,无论在哪种情况下,其一定会被解析成 CommonJS模块。
3.2 package.json 中的 type 属性
优先级仅次于文件后缀名。
当一个 Node.js 的程序文件运行时,如果文件后缀既不是.mjs
又不是.cjs
,那 Node.js 会先在同目录中寻找package.json
文件,如果没有则依次向上一级目录中寻找package.json
文件,一旦找到,则看该 JSON 文件最顶层有没有type
属性。
package.json
中的type
属性可以指定当前包使用何种模块系统。
type
有两种可能值:"commonjs"
和"module"
。
"commonjs"
代表当前包使用 CommonJS 模块系统。 "module"
代表当前包使用 ES 模块系统。
一个示例package.json
如下:
js
{
"name": "test",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "module"
}
以上package.json
代表当前模块将使用 ES 模块系统。
另外需要注意,对于嵌套包的情况,文件使用哪个模块系统是由和它同层级的package.json
或者父目录的package.json
中,离他最近的那个package.json
决定的,即使该package.json
没有指定type
,也不会继续往上级父层寻找pacakge.json
。
3.3 --experimental-default-type 启动参数
优先级低于文件后缀名和package.json
。
如果文件后缀名既不是.mjs
又不是.cjs
,当前文件所属的package.json
文件也没有指定type
,或者根本没有package.json
文件,这时如果设定了启动参数--experimental-default-type
,那它也可以决定这个文件使用何种模块系统。
注意--experimental-default-type
带有--experimental
前缀,是一个实验性的参数,后续版本有可能转成正式参数,也有可能被废弃。
--experimental-default-type=module
代表使用 ES 模块。 --experimental-default-type=commonjs
代表使用 CommonJS 模块。 默认值是commonjs
,但是后续有可能会改成module
,也就是说现在的 Node.js 默认是 CommonJS 模块系统,但是后续可能改成 ES 模块系统。
示例:
shell
node --experimental-default-type=module main.js
该示例表明对main.js
文件使用 ES 模块。
大家从名字也可以看出,其实这个启动参数是用来设置默认的模块系统的。
这里有两点要注意:
- 命令行中,
--experimental-default-type=module
参数设置必须放在 main.js 的前面。 node_modules
文件夹中的文件不会受该启动参数影响,node_modules
文件夹中的文件默认是 CommonJS 模块,当然也可以用文件后缀或者package.json
为其指定模块系统。 之所以这么做是为了向后兼容,因为 npm 的包太多了,不可能这么快让所有的包都兼容 ES 模块, 暂时让 node_modules 下的文件默认按照 CommonJS 模块对待。
4. 如何为字符代码指定模块系统
所谓字符代码就是这样的形式:
shell
node --eval "console.log('fff')"
或者
shell
node --print "console.log('fff')"
或者
shell
echo "console.log('ff')" | node
我们可以用--input-type
参数来为它们指定模块系统。
例如:
shell
node --input-type=module --eval "import { sep } from 'node:path'; console.log(sep);"
上面的示例指定了 ES 模块,代码中用ES的import
从node:path
中导入了sep
路径分隔符。
5. REPL目前还不能被指定模块系统
所谓REPL就是在命令行运行node
命令,而不指定执行文件和代码,全称是 Read-Eval-Print-Loop,读取-求值-输出-循环。 目前还不能为其指定模块系统,只能使用 CommonJS 模块系统。 但是可以通过import()
引入 CommonJS 模块或者 ES 模块。
6. 自动识别
如果你没有通过上述方式指定模块系统,你还可以在命令行使用该参数:--experimental-detect-module
,这样 Node.js 会根据代码特征自动识别出该使用哪个模块系统。
举例如下:
main.js
文件:
js
import path from 'node:path';
export function getPath(name) {
return path.resolve(name);
}
命令行:
shell
node --experimental-detect-module main.js
这时 Node.js 会自动识别出 main.js
使用了 ES 模块的语法,因此使用 ES 模块。
该参数目前还是实验性质的。
7. 默认是 CommonJS
如果你不做任何明确的指定,那 Node.js 会默认使用 CommonJS 模块系统。 也就是说--experimental-default-type
的默认值是commonjs
。 但是该情况后续很可能改变,其实--experimental-default-type
就是为了过渡成默认为module
即 ES 模块而做准备的。
8. 建议明确指定模块系统
由于 Node.js 有改成默认使用 ES 模块系统的计划,因此 Node.js 技术委员会建议在package.json
中明确指定type
。这样以后即使 Node.js 修改了默认模块系统,影响也不大。
9. import()在两个模块系统都可以使用
import()
这种动态导入的功能在 ES 模块系统和 CommonJS 模块系统中都可以使用。 举例如下:
js
(async function() {
const path = await import('node:path');
console.log(path.resolve('name'));
})();
import()
返回Promise
。 要注意的是import()
中的import
并不是一个函数,它是一个关键字,你不可以将它赋值给另一个变量,例如:const myImport = import
,编译器会报语法错误。
10. 导入模块的规则
既然可以按照文件单独指定模块系统,但是就可能存在某个文件导入具有不同模块系统的其他文件。
导入模块有如下规则:
- 被导入的模块文件依然遵从上述的指定模块规则。
- 到目前为止,CommonJS 模块文件还不能导入 ES 模块文件。
- 但是 ES 模块文件可以导入 CommonJS 模块文件。
举例如下。
ES 模块 main.mjs:
js
import path from 'node:path';
export default function getPath(name) {
return path.resolve(name);
}
CommonJS 模块 commonjs.cjs
js
// 报错,Error [ERR_REQUIRE_ESM]: require() of ES Module xxx not supported.
const getPath = require('./main.mjs');
getPath('fff');
当执行node commonjs.cjs
时,会报错:Error [ERR_REQUIRE_ESM]: require() of ES Module xxx not supported.
。 CommonJS 模块不能导入 ES 模块。
再看下面的例子。 ES 模块 main.mjs:
js
import getPath from './commonjs.cjs';
const path = getPath('fff');
console.log(path);
CommonJS 模块 commonjs.cjs:
js
const path = require('node:path');
module.exports = function(name) {
return path.resolve(name);
}
运行node main.mjs
,可以顺利运行。
所以当用require('xxx')
导入一个文件时,被引入文件必须是 CommonJS 模块,否则会报错。
但是当用import
导入一个文件时,被导入的文件可以是 CommonJS 模块,也可以是 ES 模块。
我们再举个例子说一下import
的规则。
假设我们现在的目录结构如下:
kotlin
node_modules
commonjs-package
index.js
package.json // 不含有type属性
startup
init.js
my-app.js
package.json
假设./package.json
中type
属性值为module
。
由于./package.json
中type
属性值为module
,所以my-app.js
会被当作 ES 模块处理。
my-app.js
中有如下import
,每个import
的解析规则在注释中进行了解释:
js
// init.js 会被当作 ES 模块,因为 ./startup 目录不含有 package.json 文件,
// 所以会遵从上一级的 package.json 中的 type 属性值。
import './startup/init.js';
// commonjs-package 会被当作 CommonJS 模块,因为 ./node_modules/commonjs-package/package.json 不含有 type 属性。
// node_modules 目录中的包默认会被当作 CommonJS 模块。
import 'commonjs-package';
// 会被当作 CommonJS 模块,因为./node_modules/commonjs-package/package.json 不含有 type 属性。
// node_modules 目录中的包默认会被当作 CommonJS 模块。
import './node_modules/commonjs-package/index.js';
11. 结束语
完结。