日常开发中我们使用最多的两种引入模块的方式 import 和 require,分别对应着两种模块标准,CommonJs 和 ES6 Module,js社区中还有一些其他的模块标准入 AMD、CMD等使用很少,CommonJs 和 ES6 Module 实际开发中使用得比较多有必要深入了解下
CommonJs
简称cjs、后面为了方便都会用cjs代替、是由js社区提出的一系列标准,Node.js中采用了其标准,这也是cjs活跃的一个重要原因
导出
在cjs中,通过module.exports
的方式暴露自身
module.exports = {
name: 'CommonJs',
add(a, b) {
return a + b
}
}
在上面代码中我们暴露了一个对象,包含name
和add
两个属性,还支持一种简化写法,如下写法与上面写法作用相同
exports.name = 'CommonJs'
export.add = (a, b) => a + b
- 不要直接给
exports
和module
赋值,那样可能达不到你想要的效果
exports = {
name: 'CommonJs'
}
module = {
exports: {
name: 'CommonJs'
}
}
以上两种写法都无法导出name
属性
- 不要混用
module.exports = {
add(a, b) {
return a + b
}
}
exports.name = 'CommonJs'
以上代码只会到处add
属性,原因:exports
你可以把它看成最开始声明在顶部的指向module.exports
的变量,module.exports
改变为其他对象后exports
还是原来的对象
- 导出位置并不一定是文件底部,我们应该遵循将
exports
和module.exports
放在文件底部的最佳实践
导入
在cjs中通过require
语法进行导入
// common.js
module.exports = {
name: 'CommonJs',
add(a, b) {
return a + b
}
}
// index.js
const { name } = require('./common.js')
const add = require('./common.js').add
👆代码中我们两次使用了require
引入了 common.js
文件,有人可能会问会不会导致文件被多次引用造成性能问题?答案是不会,这里我们可以看作系统为我们做了个缓存,如果没加载则加载并执行该文件,若加载过则直接获取上一次导出的内容
ES6 Module
简称esm、为了方便以下用简称替代,与cjs不同,esm是js官方发布的模块标准,属于语法的一部分
导出
esm使用export关键字进行导出模块,分为两种:默认导出、命名导出
命名导出
这个类似于cjs中的exports.xx
export const name = 'esm'
export function add(a, b) {
return a + b
}
还有一种写法就是先声明
const name = 'esm'
function add(a, b) {
return a + b
}
export {
name,
add,
// 重命名
name as newName
}
以上两种写法导出结果一样,不过第二种多了个newName,命名导出可以导出多个
默认导出
与命名导出不同,默认导出只能有一个,使用export default
导出模块
// 导出对象
export default {
name: 'esm',
add(a, b) {
return a + b
}
}
// 导出字符串
export default 'esm'
// 导出函数
export default function(a, b) {
return a + b
}
同时使用
与cjs的exports和module.exports不同的是,这两种导出是可以同时使用的
export const name = 'esm'
export default function(a, b) {
return a + b
}
这样就是既有默认导出也有命名导出的写法
导入
esm通过import
进行导入
// main.js
export const name = 'esm'
export function add(a, b) {
return a + b
}
export default 'modules'
// index.js
// 导入命名导出的模块
import { name, add } from './main.js'
// 重命名
import { name as newName } from './main.js'
// 导入默认导出模块
import modules from './main.js'
// 同时使用
import modules, {name, add} from './main.js'
还有一种整体导入的方式
import * as all from './main.js'
这时候我们的all对象上会包含name
和add
的同时还会包含default
属性,他的值是默认导出的值,这种写法相信使用react
的同学会熟悉
import React, { useState } from 'react'
复合写法
命名导出的模块有一个小技巧,就是当我们有个文件需要集合多个文件模块并导出时,我们可以这么写
// name.js
export const name = 'esm'
// add.js
export function add(a, b) {
return a + b
}
// main.js
export * from './name.js'
export * from './add.js'
这样main.js就导出了name
和add
,默认导出的暂不支持这种写法
cjs(CommonJs) 和 esm(ES6 Module) 的区别
上面我们已经两者在语法上、环境上的区别,除了这些其实他们还有一些却别
模块依赖关系引入时间
可以理解为引入的模块内的代码执行的时间, esm发生在编译阶段,模块的引入必须生命在文件头部,且路径名不能动态制定,
cjs发生在执行阶段,且模块路径可以通过字符串拼接等运算动态指定
// module.js
console.log('done')
// cjs 导出
exports.name = 'cjs'
// esm 导出
export const name ='esm'
// index.js
// esm 写法
import {name} from './module'
document.querySelector('#btn').addEventListener('click', () => {
console.log(name, '>>>>')
})
// cjs写法
document.querySelector('#btn').addEventListener('click', () => {
const name = require('./module.js')
console.log(name, '>>>>')
})
如上,当我们使用ems写法时候,打印的done
会在没做任何操作的时候就输出,而cjs写法只有在点击按钮的时候才会输出
值复制与映射
在cjs中我们导入的值是一个导出值的副本,而在esm中我们获取到的是一个映射
// module.js
let name = 'cjs'
module.exports = {
name,
changeName(value) {
name = value;
return name
}
}
// index.js
let { name, changeName } = require('./module')
console.log(name) // 'cjs'
changeName('changed')
console.log(name) // 'cjs'
name = 'changed'
console.log(name) // 'changed'
而当我们用ems的时候情况就会有所变化
// module.js
let name = 'cjs'
function changeName(value) {
name = value
}
export {
name,
changeName
}
// index.js
import { name, changeName } from './module'
console.log(name) // 'esm'
changeName('changed')
console.log(name) // 'changed'
name = 'esm'
console.log(name) // 'esm'
当我们使用默认导入模块的时候,导入的模块是只读的
// module.js
export default 'default'
// index.js
import name from './module.js'
name = 'changed' // Error
循环依赖
所谓循环依赖就是A依赖了B,B依赖了A,比如:
// a.js
import { name } from 'b.js'
// b.js
import { age } from 'a.js
通常情况下我们在项目中应该避免这种情况的发生,但是当项目复杂度上升,开发的人变多,就很容易不经意间出现这种关系,因此esm和cjs提出了不同的处理方案
cjs处理循环依赖
设想我们有如下代码,a.js和b.js互相依赖,index.js中引入了a.js
// a.js
const name = require('./b.js')
console.log('b.name', name)
module.exports = 'a.js'
//b.js
const name = require('./a.js')
console.log('a.name', name)
module.exports = 'b.js'
//index.js
require('./a.js')
在cjs中会输出如下结果
a.name {}
b.name b.js
我们来看看cjs中的实际执行流程,
index.js
中引入a.js
,首先会进入a.js
a.js
开头引入了b.js
,,进入b.jsb.js
开头引入了a.js
,这里产生了循环依赖,但是因为a.js
已经在index.js
中引入过,则直接取他的结果,但是这时候由于a.js
还未执行完成,获取到的结果是默认的module.exports
即空对象,所以此处的name会是{}
b.js
执行完成导出了b.js
,执行权交回a.js
a.js
此时获取到的是b.js
导出的结果b.js
a.js
执行完成交回index.js
esm 处理
我们用esm
的重写上面的例子
// a.js
import name from './b.js'
console.log('b.name', name)
export default 'a.js'
//b.js
import name from './a.js'
console.log('a.name', name)
export default 'b.js'
//index.js
import './a.js'
以上代码无论是在webpack中还是在浏览器原生模块中都会报错
// 原生
Uncaught ReferenceError: Cannot access 'name' before initialization
//webpack
Uncaught ReferenceError: Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization
这个报错就是临时性死区的一个报错,因为当你在b.js
导入的a.js
中的name
,而此时a.js
还未定义,类似如下代码
console.log(name)
const name = 'a.js'
所以我们如果是使用的esm
的方式,基本上可以避免循环依赖的问题😏,那要是已经出现了循环依赖,如何快速定位呢,webpack
也有一个插件circular-dependency-plugin
总结
本文主要讲解了esm
和cjs
的用法以及主要区别,对比了循环依赖的解决方案,部分结果和我找到的一些网上的结果不一致,结果均已通过代码论证,有不同欢迎讨论