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

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 程序文件使用哪种模块系统有如下决定因素,按照优先级由高到低排列:

  1. 文件后缀名
  2. [package.json 中的 type 属性](#package.json 中的 type 属性 "#h3-package.jsonE4B8ADE79A84typeE5B19EE680A7")。
  3. [--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 模块。

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

这里有两点要注意:

  1. 命令行中,--experimental-default-type=module 参数设置必须放在 main.js 的前面。
  2. 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的importnode: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. 导入模块的规则

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

导入模块有如下规则:

  1. 被导入的模块文件依然遵从上述的指定模块规则。
  2. 到目前为止,CommonJS 模块文件还不能导入 ES 模块文件。
  3. 但是 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.jsontype属性值为module

由于./package.jsontype属性值为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. 结束语

完结。

相关推荐
一个处女座的程序猿O(∩_∩)O3 分钟前
完成第一个 Vue3.2 项目后,这是我的技术总结
前端·vue.js
mubeibeinv3 分钟前
项目搭建+图片(添加+图片)
java·服务器·前端
逆旅行天涯10 分钟前
【Threejs】从零开始(六)--GUI调试开发3D效果
前端·javascript·3d
m0_7482552631 分钟前
easyExcel导出大数据量EXCEL文件,前端实现进度条或者遮罩层
前端·excel
web147862107231 小时前
C# .Net Web 路由相关配置
前端·c#·.net
m0_748247801 小时前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
飞的肖1 小时前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
青灯文案11 小时前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
m0_748254881 小时前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
ZJ_.2 小时前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps