Node.js 精讲 – 如何指定模块系统

1. 简介

Node.js 一直以来都是采用的 CommonJS 模块系统,后来 ES6 推出了新型的模块系统,现在熟称为 ES 模块。

Node.js 从2019年的 13.2.0 版开始初步支持 ES 模块。这样就有两个模块系统共存于 Node.js。

本篇文章基于 Node.js 20.10.0 版本讲述如何指定模块系统。

2. 两种模块系统举例

我们先简单温习一下两个模块系统。

举例如下:

ES 模块:

import path from 'node:path';

export function getPath(name) {
	return path.resolve(name);
}

CommonJS:

const path = require('node:path');

module.exports.getPath = function(name) {
	return path.resolve(name);
}

我们这里主要讲在 Node.js 中如何指定模块系统,并不打算细讲这两个模块系统的用法。

3. 如何为执行文件指定模块系统

在 Node.js 中,同一个文件中不可以混用两种模块系统。

决定某个 Node.js 程序文件使用哪种模块系统有如下决定因素,按照优先级由高到低排列:

  1. 文件后缀名
  2. package.json 中的 type 属性
  3. –experimental-default-type 启动参数

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如下:

{
  "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 模块系统。

示例:

node --experimental-default-type=module main.js

该示例表明对main.js文件使用 ES 模块。

大家从名字也可以看出,其实这个启动参数是用来设置默认的模块系统的。

这里有两点要注意:

  1. 命令行中,--experimental-default-type=module 参数设置必须放在 main.js 的前面。
  2. node_modules 文件夹中的文件不会受该启动参数影响,node_modules文件夹中的文件默认是 CommonJS 模块,当然也可以用文件后缀或者package.json为其指定模块系统。
    之所以这么做是为了向后兼容,因为 npm 的包太多了,不可能这么快让所有的包都兼容 ES 模块,
    暂时让 node_modules 下的文件默认按照 CommonJS 模块对待。

4. 如何为字符代码指定模块系统

所谓字符代码就是这样的形式:

node --eval "console.log('fff')"

或者

node --print "console.log('fff')"

或者

echo "console.log('ff')" | node

我们可以用--input-type参数来为它们指定模块系统。

例如:

node --input-type=module --eval "import { sep } from 'node:path'; console.log(sep);"

上面的示例指定了 ES 模块,代码中用ES的importnode:path中导入了sep路径分隔符。

5. REPL目前还不能被指定模块系统

所谓REPL就是在命令行运行node命令,而不指定执行文件和代码,全称是 Read-Eval-Print-Loop,读取-求值-输出-循环。
目前还不能为其指定模块系统,只能使用 CommonJS 模块系统。
但是可以通过import()引入 CommonJS 模块或者 ES 模块。

6. 自动识别

如果你没有通过上述方式指定模块系统,你还可以在命令行使用该参数:--experimental-detect-module,这样 Node.js 会根据代码特征自动识别出该使用哪个模块系统。

举例如下:

main.js文件:


import path from 'node:path';

export function getPath(name) {
	return path.resolve(name);
}

命令行:

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 模块系统中都可以使用。
举例如下:

(async function() {
	const path = await import('node:path');

	console.log(path.resolve('name'));

})();

import()返回Promise
要注意的是import()中的import并不是一个函数,它是一个关键字,你不可以将它赋值给另一个变量,例如:const myImport = import,编译器会报语法错误。

10. 导入模块的规则

既然可以按照文件单独指定模块系统,但是就可能存在某个文件导入具有不同模块系统的其他文件。

导入模块有如下规则:

  1. 被导入的模块文件依然遵从上述的指定模块规则。
  2. 到目前为止,CommonJS 模块文件还不能导入 ES 模块文件。
  3. 但是 ES 模块文件可以导入 CommonJS 模块文件。

举例如下。

ES 模块 main.mjs:

import path from 'node:path';

export default function getPath(name) {
  return path.resolve(name);
}

CommonJS 模块 commonjs.cjs

// 报错,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:

import getPath from './commonjs.cjs';

const path = getPath('fff');
console.log(path);

CommonJS 模块 commonjs.cjs:

const path = require('node:path');

module.exports = function(name) {
  return path.resolve(name);
}

运行node main.mjs,可以顺利运行。

所以当用require('xxx')导入一个文件时,被引入文件必须是 CommonJS 模块,否则会报错。

但是当用import 导入一个文件时,被导入的文件可以是 CommonJS 模块,也可以是 ES 模块。

我们再举个例子说一下import的规则。

假设我们现在的目录结构如下:

node_modules 
	commonjs-package
		index.js
		package.json // 不含有type属性
startup 
	init.js
my-app.js
package.json

假设./package.jsontype属性值为module

由于./package.jsontype属性值为module,所以my-app.js会被当作 ES 模块处理。

my-app.js中有如下import,每个import的解析规则在注释中进行了解释:

// 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. 结束语

完结。

原文链接:https://juejin.cn/post/7328390254997094452 作者:吉灵云

(0)
上一篇 2024年1月28日 上午11:07
下一篇 2024年1月28日 下午4:00

相关推荐

发表回复

登录后才能评论