随着前端工程越来越复杂,项目越来越大,现代前端开发时时刻刻都在和各种各样的模块打交道,在程序设计之初,便会根据功能不同将代码拆分成不同的片段,每一片段实现自己特定的使命,最后通过接口将分散的片段组合在一起,,也就是所谓的模块化开发。简单来说一个模块就是一个实现了特定功能的文件,大到项目中引入一个三方包,小到为某个功能单独提供一个JS工具函数文件,这些都可以称为模块。
为什么要采用模块化开发
早期JavaScript是不存在模块的概念的,面对多个JS文件只能通过<script>标签一个一个的插入到HTML中,这就导致如果各JS文件间存在依赖关系还需要确保引入顺序,同时还存在变量命名冲突的风险,而且如果<script>数量众多,每个JS文件都需要向服务器发送请求,那对页面加载性能无疑是灾难。
为了解决非模块开发下的诸多问题,从09年开始,JS社区陆续出现了以RequireJS为代表的AMD规范(Asynchromous Module Definition)、以SeaJS为代表的CMD规范(Common Module Definition)、还有NodeJS对应实现的CommonJS规范。
CommonJS
导入及导出
CommonJS中规定每一个文件就是一个独立的模块,各模块之间互不影响,每个模块都有独立的作用域。使用require进行模块导入,使用module.exports或exports对模块进行导出。
// A.js
// module.exports导出
module.exports = {
test() {
console.log('导出模块方式1')
}
}
// exports导出
exports.test = function() {
console.log('导出模块方式2')
}
// B.js
let a = require('./A.js)
a.test()
两种导出方式是等价的。内在机制大概就是module.exports原本是一个空对象,将需要导出的对象储存在module.exports中,exports内部指向了module.exports。 需要注意的是不能对exports重新赋值,否则会导致exports的指向改变,导致exports导出失败。
ES Module
2015年,ECMAScript6.0正式定义了JS模块标准,使得JS正式具备了模块这一特性。ES Module也是将每个文件作为一个模块,每个模块拥有自身的作用域,使用import进行模块导入,使用export进行导出。
导入及导出
// A.js
// 普通导出
export function bar () {
console.log('ES Module')
}
// 默认导出
let def = 'ES Module'
export default def
// B.js
// 导入
import { bar } from './A.js'
bar()
// 导入默认导出
import a from './A.js'
console.log(a)
// 批量导入
import * as c from './A.js'
CommonJS与ES Module的区别
对于普通的导入导出应该都是现代前端开发手到擒来的事情,而且面试中也没有人会去问如何导入导出模块,所以还是需要关注一下它们的区别
模块处理
在模块处理这一方面,CommonJS中模块依赖之间的关系建立发生在代码运行阶段,而ES Module发生在代码编译阶段。
在CommonJS中,A模块加载B模块时会执行B模块之中的代码,通过require导入的模块路径支持表达式,并且可以通过if进行条件判断是否加载某个模块,所以CommonJS模块在被执行前是没办法明确各依赖之间的关系。
// A.js
module.exports = {
test: '测试'
}
// B.js
let path = 'A.js'
let test
if (true) {
test = require('./' + path)
}
console.log(test)
而在ES Module中,模块在编译阶段就可以分析出模块之间的依赖关系,不支持导入的路径是一个表达式,并且导出语句必须写在模块的顶层区域。
导出值的区别
在ES Module中,导入的值是对原模块导出值的引用,通过下面例子就可以验证,当通过add方法改变原A模块中num的值时,B模块中num的值随着A模块中num的值的改变而改变了。
// A.js
let num = 1
function add () {
num ++
console.log('A模块中num的值: ' + num)
}
export {
num,
add
}
// B.js
import { num, add } from "./A.js";
console.log('导入时num的值:' + num)
add()
console.log('调用add方法后num的值:' + num)
// 导入时num的值:1
// A模块中num的值: 2
// 调用add方法后num的值:2
在CommonJS中,导入的值时原模块导出值的拷贝,B模块中num的值并没有随着A模块中值的改变而改变。
// A.js
let num = 1
function add () {
num ++
console.log('A模块中num的值: ' + num)
}
module.exports = {
num,
add
}
// B.js
let obj = require('./A.js')
console.log('导入时num的值:' + obj.num)
obj.add()
console.log('调用add方法后num的值:' + obj.num)
// 导入时num的值:1
// A模块中num的值: 2
// 调用add方法后num的值:1
总结
ES Module作为官方推出的JS模块标准,也算是明确了JS的模块化发展方向,而社区规范CommonJS规范的也在NodeJS这个顶级实践者中混的风生水起,不过目前NodeJS正在支持ES Module的模块化实现方式,可以通过配置package.json中"type": "module"或将文件后缀改为.mjs实现ES Module的导入导出