CommonJS 模块规范与 ES Module 的差异

在 ES Module 还未全面支持的时候,我们写 ES Module 的语法,importexport需要通过babel来编译,转化成CommonJS规范。现在由于ES Module普及度已经够广了。在复习的过程中也学习到了两者的区别。

Babel 转换 ES Module 为 CommonJS 规范

1
2
3
4
5
6
7
8
// a.js
var a = 2
var b = '123'
var obj = {
test: '12',
}
export default obj
export { a, b }

Babel 会将其转换为

1
2
3
4
5
6
exports.default = {
test: '12'
}
exports.a = 2
exports.b = '123'
export._esModule = true // 有一个标记,说明该模块是由ES Module转换的

可以手动设置关闭 babel 对ES Module的转换,设置modules的值为false

1
2
3
4
5
6
7
"presets": [
[
"env",
{
"modules": false
}
]

Webpack 对ES Module的转换

不仅仅可以通过Babel可以转换ES Module,实际上 Webpack 在处理文件的时候,也会对模块语法进行一定的处理。最终使用webpack内部定义的__webpack_require__函数来引入模块,从入口文件开始引入的模块都存放到一个叫module对象的exports属性里,exports也是一个对象。
格式如下,每引入一个模块都会给exports添加一个属性,最后__webpack_require__函数返回该module.exports对象。

1
2
3
4
5
var module = {
i: moduleId, // 模块ID
l: false,
exports: {}, // 作为结果返回.
}

两者的区别

ES Module

import 是编译阶段执行的,所以可以进行静态分析。
import 语句不能够使用变量,表达式,因为编译阶段这些都还未存在。
export 导出的值其实是一个值的引用,可以理解为存放着具体值的地址。也可以理解为符号链接是一类特殊的文件, 其包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用

对于 ES 模块导出的值,举个例子

1
2
3
4
5
6
7
8
9
10
11
12
// a.js
export var a = 1
export function caculate() {
a++
}

// b.js
import { a, caculate } from 'a.js'

console.log(a) // 1
caculate()
console.log(a) // 2

引入 a.js 的变量 a 和 caculate,通过 caculate 修改 a 的值,b.js 引入的变量a也发生了改变,说明了,import导入的只是一个值的引用。当该值发生变化时,会同步该变化。

CommonJS

require是在运行时加载的,只有在运行的时候才能够得到 CommonJS 模块导出的对象 exports,可以使用变量,表达式,动态加载模块。
module.exports导出的其实是一个值的拷贝,引入模块后修改其导出的值,不会影响模块原本的值。
举个例子,改写上面的例子

1
2
3
4
5
6
7
8
9
10
11
var a = 1
var caculate = () => {
a++
}
module.exports = { a, caculate }
// b.js
var { a, caculate } = require('./a.js')

console.log(a) // 1
caculate()
console.log(a) // 1

但是需要记住的是,一个模块暴露提供的值,不能通过手动赋值的方式改变其的值,会报错Assignment to constant variable,只有其内部模块的方法可以修改值,这也是一种特殊的情况,其他情况下我们都可以将一个模块的暴露的值当成const常量,其引入后不可以被修改。

动态加载

require 在运行时加载模块,所以根据一些条件来进行动态加载,
import在编译阶段会先于其他语句执行,所以是没办法进行运行时加载的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// require
// 正常
const test = require(`${name}.js`)
// 正常
if(count > 10) {
const test = require(`${name}.js`)
}

// import
// 报错
import test from `${name}.js`
// 报错
if(count > 10) {
import test from './test.js'
}

import()

引入了import() 可以进行按需加载,动态加载

import() 返回一个 promise 对象。可以使用 async/await 语法。

1
2
3
4
5
6
7
8
9
10
11
import('./test.js').then(module => console.log)

//条件加载
if(count === 10) {
const test = async () => {
await import('./test.js')
}
}

// 动态名字
import(`${name}.js`).then(...)

当 JS 引擎运行到此语句时就会对test.js进行加载。

Node.js

现在 Node 对 ES Module 的支持还不是很理想,还在测试阶段。所以在Node环境下还是继续使用CommonJS模块规范吧。