学习 JavaScript 语言,你会发现它有两种格式的模块。
一种是 ES6 模块,简称 ESM;另一种是 Node.js 专用的 CommonJS 模块,简称 CJS。这两种模块不兼容。
很多人使用 Node.js,只会用require()
加载模块,遇到 ES6 模块就不知道该怎么办。本文就来谈谈,ES6 模块在 Node.js 里面怎么使用。
一、两种模块的差异
ES6 模块和 CommonJS 模块有很大的差异。
语法上面,CommonJS 模块使用require()
加载和module.exports
输出,ES6 模块使用import
和export
。
用法上面,require()
是同步加载,后面的代码必须等待这个命令执行完,才会执行。import
命令则是异步加载,或者更准确地说,ES6 模块有一个独立的静态解析阶段,依赖关系的分析是在那个阶段完成的,最底层的模块第一个执行。
二、Node.js 的区分
Node.js 要求 ES6 模块采用.mjs
后缀文件名。也就是说,只要脚本文件里面使用import
或者export
命令,那么就必须采用.mjs
后缀名。Node.js 遇到.mjs
文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"
。
如果不希望将后缀名改成.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。
{ "type": "module" }
一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。
# 解释成 ES6 模块 $ node my-app.js
如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs
。如果没有type
字段,或者type
字段为commonjs
,则.js
脚本会被解释成 CommonJS 模块。
总结为一句话:.mjs
文件总是以 ES6 模块加载,.cjs
文件总是以 CommonJS 模块加载,.js
文件的加载取决于package.json
里面type
字段的设置。
注意,ES6 模块与 CommonJS 模块尽量不要混用。require
命令不能加载.mjs
文件,会报错,只有import
命令才可以加载.mjs
文件。反过来,.mjs
文件里面也不能使用require
命令,必须使用import
。
三、CommonJS 模块加载 ES6 模块
CommonJS 的require()
命令不能加载 ES6 模块,会报错,只能使用import()
这个方法加载。
(async () => { await import('./my-app.mjs'); })();
上面代码可以在 CommonJS 模块中运行。
require()
不支持 ES6 模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层await
命令,导致无法被同步加载。
四、ES6 模块加载 CommonJS 模块
ES6 模块的import
命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。
// 正确 import packageMain from 'commonjs-package'; // 报错 import { method } from 'commonjs-package';
这是因为 ES6 模块需要支持静态代码分析,而 CommonJS 模块的输出接口是module.exports
,是一个对象,无法被静态分析,所以只能整体加载。
加载单一的输出项,可以写成下面这样。
import packageMain from 'commonjs-package'; const { method } = packageMain;
五、同时支持两种格式的模块
一个模块同时要支持 CommonJS 和 ES6 两种格式,也很容易。
如果原始模块是 ES6 格式,那么需要给出一个整体输出接口,比如export default obj
,使得 CommonJS 可以用import()
进行加载。
如果原始模块是 CommonJS 格式,那么可以加一个包装层。
import cjsModule from '../index.js'; export const foo = cjsModule.foo;
上面代码先整体输入 CommonJS 模块,然后再根据需要输出具名接口。
你可以把这个文件的后缀名改为.mjs
,或者将它放在一个子目录,再在这个子目录里面放一个单独的package.json
文件,指明{ type: "module" }
。
另一种做法是在package.json
文件的exports
字段,指明两种格式模块各自的加载入口。
"exports":{ "require": "./index.js", "import": "./esm/wrapper.js" }
上面代码指定require()
和import
,加载该模块会自动切换到不一样的入口文件。
(完)
canbingzt 说:
```
const { method } = packageMain;:w
```
这里是不是错了
2020年8月20日 19:51 | # | 引用
Le 说:
发现这里很少有python等其它技术文章,JS的比例太重
2020年8月21日 08:34 | # | 引用
callstar 说:
指明{ module: "type" }
----------------------
写反了吧
2020年8月21日 08:37 | # | 引用
AMD Yes 说:
阮老师本来就是js博主啊,你让一个js博主去讲其它的东西,你不扯淡嘛.阮老师不是写敷衍文章的人
2020年8月21日 08:49 | # | 引用
阮一峰 说:
@callstar,@canbingzt:
谢谢指出,已经改正了。
@Le:
这边以学习笔记居多,以后我学习 Python 时,就会有相关的文章。
2020年8月21日 08:53 | # | 引用
zz 说:
写的很好,浅显易懂:
总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。
注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import。
2020年8月21日 09:38 | # | 引用
davix 说:
为啥node不统一为ES6啊
2020年8月21日 12:00 | # | 引用
tron 说:
就像我中华,你不能让所有民族的同胞都说普通话
2020年8月21日 15:52 | # | 引用
hello 说:
先有的CommonJS后有的ES6哇
2020年8月21日 17:41 | # | 引用
180see 说:
这就像是在说元朝拥有世界上最大的版图。
2020年8月21日 21:10 | # | 引用
Heaan 说:
我记得能不能使用ESM还和node的版本有关的
2020年8月21日 23:48 | # | 引用
火火阿小 说:
又学到了新知识
2020年8月22日 23:44 | # | 引用
好好❤️自己 说:
“ ES6 模块内部可以使用顶层await命令,导致无法被同步加载。”
这里说的是 es6 模块只能被异步加载吗?
“CommonJS 模块的输出接口是module.exports,是一个对象,无法被静态分析,所以只能整体加载。”
对象为什么就不能被静态分析了呢?
2020年8月25日 22:52 | # | 引用
刘玮 说:
废话,人家本来就是前端大神,着重点肯定是前端了
2020年8月28日 10:51 | # | 引用
钊 说:
请问用import引入 commonjs 模块有限制吗?比如不能有require.
2020年9月11日 10:49 | # | 引用
大名 说:
这篇文章解了我的疑惑。
2020年9月11日 14:36 | # | 引用
Aaronlam 说:
我个人通过收集整理,整理出以下几种在Node旧版或者新版中使用ESM的方式,不知是否有误,请老师及各位帮忙过目一下。
1.使用babel,npm i @babel/core @babel/register @babel/preset-env -D,并增加新的入口文件,代码内容如下:
注:该方法会影响应用启动速度,因为在启动时babel需要编译
require(‘@babel/register’) ({
Presets: [‘@babel/preset-env’]
})
module.exports = require(‘./index.js’)
2.旧版Node,使用Node的试验特性,在启动时使用命令node --experimental-modules,并配合如下的其中一种模式与之配合开启:
注:使用该试验特性后,所有的import都要写完整文件的名称,不能省略文件拓展名
1.ES6的模块文件拓展名改为mjs
2.修改package.json的type属性为如下值(每个目录都支持以放置package.json文件并通过type字段告诉node该以哪种方式处理模块):
注:该模式虽然方便,但是可能会影响老项目的兼容性
Module
Commonjs
3.正式支持esm的Node版本,可以通过以下几种方式开启:
1.ES6的模块文件拓展名改为mjs
2.Package.json的type属性修改为module(每个目录都支持以放置package.json文件的形式,单独开启该目录的处理)
3.在启动时使用命令node --input-type=module
2020年10月16日 03:10 | # | 引用
just do it 说:
经我测试,这句话,是有问题的
ES6 模块的import命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。
我测试时,是可以加载单一的输出项
//test.js 位于node_modules/test/index.js
const add = function(x,y){
return x+y;
}
const minus = function(x,y){
return x-y;
}
module.exports = {
add,
minus
}
main.js
import { add, minus } from 'test';
console.log(add(100, 30));
console.log(minus(100, 20));
2020年11月13日 15:11 | # | 引用
chenguzhen87 说:
ES6 模块内部可以使用顶层await命令,这个说法是错误的吧,只有在deno中模块可直接使用await,而不需要包裹在一个asnyc函数里
2020年12月 2日 19:58 | # | 引用
cp1001 说:
只是说法不准确,顶层await是es2021的新特性,目前只有chrome浏览器高于89版本才支持(2021年后),及node15+默认支持无需携带试验参数。在低版本使用还是需要借助babel帮忙的。
2021年3月30日 11:23 | # | 引用
elsa 说:
@just do it:
这里用的是ES6的对象解构特性,实际上CommonJS模块还是整体加载的
2021年6月24日 11:56 | # | 引用
思高提 说:
@just do it:
是的,确实如此,作者应该是说反了。
2022年5月11日 15:41 | # | 引用
l2en 说:
```js
如果原始模块是 ES6 格式,那么需要给出一个整体输出接口,比如export default obj,使得 CommonJS 可以用import()进行加载
```
没理解这里介绍,为什么说CommonJS 可以用import()进行加载
2022年6月 9日 16:30 | # | 引用