ESModule与CommonJS互相引用的问题
前言
最近使用Rollup
打包输出CJS
模块的代码,时不时会提示导入的模块是ESM
规范,需要使用动态导入的方式引入这个模块,这个问题引起了我的注意,所以决定深入研究一下。
- 简单介绍
ESM
和CJS
的原理 ESM
和CJS
的互相引用问题
CJS和ESM的历史演变
JavaScript一开始诞生是在浏览器上作为脚本语言使用的,当时在浏览器上并不存在模块的概念。它就像日本动漫里的男主角,刚转生到异世界时没有任何的武器和装备(不是龙傲天),所以说一开始的JavaScript也是不完善的,需要时间的历练,打怪升级学技能。
直到Node.JS的诞生,让JavaScript能在服务端运行,同时Node.js社区的发展给JavaScript带来了模块化的概念,使用module.exports
导出和require
引入模块,这个规范叫做CommonJS
。它的出现使得服务端的JavaScript应用程序的开发更加规范化和标准化。
由于CommonJS
的引入模块原理是同步加载的方式,要是在浏览器端使用这个规范,则需要等待所有的模块加载完才能执行后面的代码。由于浏览器中的JavaScript引擎是单线程的,如果加载JavaScript模块的过程中,代码执行的时间过长,就会导致其他代码无法执行,从而影响整个页面的渲染和交互,最终影响了用户体验。因此,浏览器需要一种异步加载模块方式。
在2015年的6月,ES6
标准正式推出,在这个标准里带来了JavaScript异步加载模块的ESM
规范。
ESM
,也叫做 ECMAScript modules
或者ES modules
,是来自ES6
规范里的模块化概念,使用import
和export
关键字,来实现js的模块化导入和导出。
ESModule和CommonJS互相引用
完全不同的两个规范,互相导入的话会导致什么情况呢?
在继续之前,我们需要了解Node.js支持运行ESM
规范的最低版本是:v13.2.0
,在之前的版本中,想要在node中使用ESM
,需要添加--experimental-module
参数,在v13.2.0
版本之后可以直接使用。
我将会使用Node.jsv16.18.0
版本进行下面的测试,话不多说,立马进入。
CJS导入ESM模块
- 先新建一个
ESM
模块,文件名为:esm.js
,代码如下:
// esm.js
export const a = 1;
export const b = 2;
export function foo () {
return 3;
}
- 另外新建一个
CJS
模块,文件名为:cjs.js
,代码如下:
// cjs.js
const { a, b, foo } = require('./esm.js')
console.log(a);
console.log(b);
console.log(foo());
- 运行
cjs.js
文件,结果提示以下的错误信息:
export const a = 1;
^^^^^^
SyntaxError: Unexpected token 'export'
通过来说,如果在CJS
上通过相对路径方式引入.js
结尾的ESM
模块,就会遇到上面的问题。根本原因Node.js底层在加载这个esm.js
文件时,把它识别成了CJS
规范,最后使用cjs的loader进行处理时抛出的异常:
那么问题来了,Node.js是如何识别引入的文件是ESM
还是CJS
规范的?其实Node.js有一个判断的机制叫做:Determining module system。用图例简单解析这个机制的判断逻辑:
为了Determining module system
能够把esm.js
正确识别成ESM
模块,我们有2种方案:
第一种:修改文件名后缀的方式。
步骤如下:
- 我们修改一下
esm.js
文件为esm.mjs
,然后调整一下cjs.js
文件的内容如下:
// cjs.js
const { a, b, foo } = require('./esm.mjs');
console.log(a);
console.log(b);
console.log(foo());
- 执行命令
node cjs.js
,又抛出异常了,不过这次的报错稍微不太一样:
node:internal/modules/cjs/loader:1031
throw new ERR_REQUIRE_ESM(filename, true);
^
Error [ERR_REQUIRE_ESM]: require() of ES Module esm.mjs not supported.
Instead change the require of esm.mjs to a dynamic import() which is available in all CommonJS modules.
- 大致的意思是
ESM
模块不支持通过require()
函数引入,需要替换成通过import()
函数动态引入ESM
模块,那我们就改一下cjs.js
文件中代码,通过动态import的方式引入:
async function main() {
const mod = await import('./esm.mjs');
const { a, b, foo } = mod;
console.log(a);
console.log(b);
console.log(foo());
}
main();
- 成功输出:
1
2
3
第二种:在文件同级或者父级目录加一个package.json
文件,并且加上"type"字段,值为"module"。
为了让cjs.js
和esm.js
分别识别成正确的规范。我们改造成下图的结构:
其中lib/package.json
的文件内容如下,这样esm.js
就会被识别成ESM
模块。
// lib/package.json
{
"type": "module"
}
然后cjs.js
文件导入esm.js
我们可以改写成:
// cjs.js
async function main() {
// 这里改成导入./lib/esm.js 注意看后缀是 .js
const mod = await import('./lib/esm.js');
const { a, b, foo } = mod;
console.log(a);
console.log(b);
console.log(foo());
}
main();
最后运行命令:node ./cjs.js
,成功!!!
1
2
3
经过以上的实验,我们了解到Node.js的底层机制已经支持CJS
模块导入ESM
模块,不过需要注意以下的几个点:
- 根据
Determining Module System
机制,需要修改ESM
模块的文件后缀名为.mjs
或者通过package.json
加上"type": "module"
字段。 CJS
只能通过import()
函数的方式导入ESM
模块。
ESM导入CJS模块
在继续之前,我们需要了解Node.js支持运行ESM
规范的最低版本是:v13.2.0
,在此之前,想要在node中使用ESM
,需要添加--experimental-module
参数。在v13.2.0
版本之后可以直接使用。
首先新建一个文件夹,结构如下:
- 编辑根目录下的
package.json
文件,加上type
字段,值为"module",如下所示:
// package.json
{
...,
"type": "module",
...,
}
这样esm.js
就会被当做ESM
模块处理。
- 编辑
lib/package.json
,内容如下:
// lib/package.json
{
// 或者可以加上"type": "commonjs"
}
esm.js
和cjs.js
文件内容分别为:
// ./lib/cjs.js
module.exports = {
a: 1,
b: 2,
foo: function () {
return 3
}
}
// esm.js
import pkg from './lib/cjs.js'
const { a, b, foo} = pkg;
console.log(a);
console.log(b);
console.log(foo());
- 运行
node esm.js
,输出了以下信息:
1
2
3
ESM
成功引入CJS
模块!!!
在Node.js中ESM
模块是支持引入CJS
模块的,但是注意以下几点:
- 为了让Node.js正确识别这是一个
CJS
模块,根据Determining Module System
机制,需要修改ESM
模块的文件后缀名为.cjs
,或者通过同级或者最近父级文件夹下的package.json
加上"type": "commonjs"
字段。 - 转换时,
CJS
模块的module.exports
与ESM
模块的export default
对应。
总结
经过以上的章节,我们了解到CJS
和ESM
模块各自的特点,也知道了Node.js的Determining module system机制时是如何识别代码属于哪种模块,再通过不同的loader去转换代码。
当你需要封装一些类库,引入不同规范依赖的时候也能够游刃有余地处理,也可以配合一些打包工具例如Rollup
的使用。
原本两种模块规范是分别用在不同的端,但是由于ESM
规范的异步加载优势,天生支持Tree Shaking
,以及各种JavaScript打包工具的流行,例如Webpack
,Rollup
等等,越来越多的开发者在封装各类工具库时使用ESM
规范。
后面我将会写一篇文章讲解使用Rollup
工具的简单使用,来解决打包成CJS
规范时处理ESM
。不过也许在不远的将来,Node.js默认使用ESM
规范执行js代码也说不定。
感谢你阅读到这里,如有发现文章中存在错误的地方也请指正。