这两种模块两种模块想必大家都很熟悉了,现在node环境下同时兼容这两种模块,那么这两种模块之间是不是可以混用呢?答案是某种程度上可以,但是又不完全可以。开始之前先了解一个小知识,node环境下使用esm有两种方式,一是可以将文件后缀改为.mjs,二是可以在项目package.json文件中设置type:module。这两种方法都可以开启node的esm支持。
有的小伙伴可能会问,我们日常开发一直都是使用esm模块导入导出的呀,没配置type:module,也没使用.mjs后缀。原因是日常开发中无论使用哪种构建工具,最后都会被dev-server编译为浏览器兼容的模块格式,代码并不会在node环境中运行。但是有的模块是会在node环境中执行的,比如各种工具的config.js文件,因为node默认是cjs,所以使用es模块的import会报这个错误,翻译过来就是上面说的,要加载es模块需要使用.mjs拓展或者设置type:module
回到今天主题,esm和cjs相互导入的行为:
我的node版本是v17.9.1
首先来看esm导入cjs:
// a.js
module.exports.a = 1;
module.exports.b = 2;
// b.js
import a from "./a.js";
import { b } from "./a.js";
console.log(a);
console.log(b);
/*
结果是这样的
====================================
{a: 1, b: 2}
2
====================================
*/
所以可以看到现在的node是支持modules.exports作为默认导出,同时也支持解构exports对象得到具名导出,很符合直觉
那么cjs导入esm呢:
// b.js
export const c = 3;
export default 4;
//a.js
//报错了!
const { c } = require('./b.js')
const d = require('./b.js').default
从这段代码可以发现一个问题,那就是esm中的默认导出,在cjs中没有对应的语法。我们没办法在commonjs中‘默认导入’。当然这也无所谓,因为cjs没办法同步导入esm模块中的东西,所以无论怎么写都是会报错的。根据阮一峰老师的说法,因为cjs同步加载,不支持模块顶层await,所以没办法导入es模块。这个理由感觉上就很合理不是吗,毕竟模块导入会触发模块内代码的执行。如果同步导入含有顶层await的es模块代码就违背了require运行时同步导入的原则了。当然我也看了下node的文档:
我就不翻译了,可以发现确实是这样
不过还有一种办法可以在cjs中导入esm模块:
// a.js 导入b中的东西
async function importModuleB() {
const exportFromB = await import("./esmInput.mjs");
console.log(exportFromB);
}
importModuleB();
es模块是有异步导入语法的,也就是import('path/to/module'),会返回一个promise,
最终导出的是这么一个东西:
可以看到默认导出变成了导出对象的default属性。我们可以注意到模块对象上还有一个Symbol。toStringTag属性,值为‘Module’。了解的小伙伴肯定知道,这个属性决定了Object.prototype.toString.call(xxx)时的行为。也就是我们常说的判断对象类型,比如数组会返回[object Array],对象会返回[object Object]。所以这里的Module也就意味着这个实例的类型为Module。
说到这我们大概也能看出来。es模块还是对兼容原有的cjs生态做了很多工作的。能够让我们优雅的使用原有生态下的内容。但这是建立在我的node版本较新的基础上的。在阮一峰老师的文章中,es模块不支持具名导入cjs。而且我也没找到具体是node哪个版本实现的这个功能,大伙可以试一下。