当ECMAScript module(简称es module) 的模块化语法import/export产生的时候,commonjs的require/module.exports已经在nodejs的环境中被广泛使用,特别是es module中的default语法,让es module和commonjs之间的转换变得复杂。本文结合自己在es module和commonjs两种模块化互相转换的实践,聊聊会遇到的问题以及如何解决。
- es module中export default带来的问题
- __esModule
- __esModule存在的问题
一、es module中export default带来的问题
es module模块间的互相引用不会有任何的问题,随着nodejs环境和浏览器环境都陆续开始支持es module, 一些主流的npm包的最新版本,都会提供es module的源文件,一般通过指定package.json中的module或者exports字段来提供esm形式的源文件。但是,如果在es module中引用一个commonjs中的模块,就可能会出现引用问题。
出问题的本质就是esm中export default的语法,在commonjs中是无法对应的。
举例来说:
//commonjs模块 a.js
module.exports.name = 'Jony'
module.exports.hobby = 'Play'
//main.js引用a.js
const person = require("./a.js") //这样引没用问题
//main.js引用a.js,用esm的形式引用commonjs
import person from './a.js'
person = ???
上述的例子中,如果我们需要以es module的形式来引用a.js。因为a.js是commonjs形式的,我们在a.js中找不到与export default相对应的属性。
二、__esModule
在问题一中的两个例子,本质都说明了esm 中export default的语法,在commonjs中很难找到对应。babel首先在commonjs模块的文件中引入__esModule标识,如果__esModule为true,那么将commonjs转化为es module,export default导出的是module.exports.default。如果__esModule为false,那么commonjs转化为es module后,export default到处的是整个module.exports。
我们同样拿上述章节中的例子,来说明:
//commonjs模块 a.js
module.exports.name = 'Jony'
module.exports.hobby = 'Play'
这里没用__esModule属性,默认__esModule为false
//main.js引用a.js
const person_cjs = require("./a.js") //这样引没有问题
person_cjs = {name,hobby}
因为__esModule为false或者不存在,那么export default导出的是整个module.exports对象。
//main.js引用a.js,用esm的形式引用commonjs
import person_esm from './a.js'
person_esm = {name,hobby}
再来看一个例子,如果包含commonjs中包含了module.exports.default属性,以及__esModule位true。
//commonjs 模块a.js
module.exports.default = "aa"
module.exports.name = "Jony"
module.exports.hobby = 'Play'
module.exports.__esModule = true
这里__esModule为true,且存在module.exports.default属性,那么以es module的方式引用上述文件得到的结果为:
//main.js引用a.js,用esm的形式引用commonjs
import person_esm from './a.js'
person_esm = "aa"
三、__esModule存在的问题
__esModule首先由Babel提出,其他的构建工具或者系统在commonjs和es module的转换中都遵循了这个规则。但是在使用在转换和引用的过程中需要注意几个细节。
(1)如果commonjs的模块中存在__esModule为false,到处的是整个Module.exports对象,如果设置了__esModule=false,这个对象中可能会多一个属性__esModule.
因此如果__esModule为false,可以不设置。否则到处的对象会多一个__esModule属性。
//commonjs a.js
module.exports.a = 2
module.exports.b = 3
module.exports.__esModule = false
//main.js
import x from './a.js'
x = {a:2,b:3,__esModule:false}
(2)如果commonjs的模块中存在__esModule为true,但是不存在module.exports.default属性。
这种情况下,不同的构建工具或者系统可能有不同的表现,以esbuild的最新版本为例子,对于这种情况esbuild构建后认为default = undefined.
//commonjs a.js
module.exports.a = 2
module.exports.b = 3
module.exports.__esModule = true
//main.js
import x from './a.js'
x = undefined
此外,也有一些工具的处理,如果__esModule为true,但是不存在module.exports.default不存在,就把这种情况当成__esModule为false来处理。
(3)如果在.mjs文件中引用
//commonjs a.js
module.exports.default = "aa"
module.exports.a = 2
module.exports.b = 3
module.exports.__esModule = true
//main.mjs
import x from './a.js'
x = {
default:'aa',
a:2,
b:2,
__esModule:true
}
以.mjs后缀结尾的文件,是nodejs中支持了es module的形式,此时如果在.mjs后缀结尾的文件中引用commonjs,一般不会做特殊处理。需要__esModule的场景本身就是为了兼容nodejs环境中,无法直接运行es module。
因此这里__esModule不会生效,所有属性都被当成普通属性。