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

完结。

相关推荐
我要洋人死1 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9152 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼3 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風7 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#