咱们可能常常听到一些模块化的概念,譬如 AMD
、CommonJS
或 ES Modules
。这些又是什么概念呢?它们为何而存在,做用又是什么呢?本文将对模块化的概念进行逐一分析。node
在了解模块化的概念前,首先先解决一个问题 - 为何须要模块化?webpack
先从实际问题出发,在相似 require.js
、sea.js
、browserify
、webpack
等工具出现以前,咱们可能会遇到以下一些问题:web
js
文件时,可能会出现命名冲突。a.js
中引用了 b.js
,而 b.js
中也引用了 a.js
;二者相互依赖。那咱们如何决定先引用哪一个件呢?咱们再从一个生活中的例子出发,简要了解一下模块化的优势,即为何须要模块化的缘由。npm
假设你有一套修理工具箱,里面包含了属于这套修理工具箱的各类型号的螺丝批、钳子和锤子等等。每次家里水管破了,灯泡坏了什么的,你均可以拿这套修理工具箱进行修理。每次修理可能都会形成一些工具的损耗或损坏,损坏以后咱们就应该去买相同型号的工具进行补充;其余不对头的工具不会回收回这套修理工具箱中,一样这套修理工具箱中的工具也不会随意扔出工具箱中。设计模式
咱们把上述例子转化为模块化来看看。首先,修理工具箱就是 模块
,里面的工具就是 模块
中的各类变量或函数。工具出现损坏等于 模块
内出了什么问题,这时候咱们只须要修复 模块
内的 bug
就行了。其余不对头的工具不会回收回工具箱中,反之工具箱中的工具不会随意被扔出表示 模块
内的变量、函数等不会污染外部的变量、函数等等,反之亦然。这套工具箱能够重复利用也就是 模块
的复用性很强。数组
总结模块化有三大优势:浏览器
接下来将会经过一些常见的例子与概念来解释什么是模块化。注意,闭包在模块化中有着重要的应用,这里假设你对闭包概念已有所了解。安全
IIFE
- 当即执行函数表达式顾名思义,当即执行函数表达式就是一个函数在定义时就会当即执行。服务器
var global = "I'm global"
(function () {
var foo = 'foo'
function bar () {
console.log('bar')
}
console.log(foo)
bar()
console.log(global)
})()
// foo
// bar
// I'm global
var foo = 'global foo'
console.log(foo) // global foo
bar() // Uncaught ReferenceError: bar is not defined
复制代码
能够看到,咱们在当即执行函数表达式的外部访问其变量会抛出错误,而在当即执行函数表达式的内部能够随时访问外部变量。外部与当即执行函数表达式一样有命名为 foo
的变量,但这二者互不影响。其实这种行为就相似于 C++
等语言中类的私有变量、私有方法。闭包
固然咱们还可让当即执行函数表达式放回一些东西,相似类的暴露公共方法、变量的概念。
var module = (function () {
var _privateCnt = 0
var _privateProperty = 'I am private property'
function _privateCnter () {
return _privateCnt += 1
}
function publicCnter () {
return _privateCnter()
}
return {
property: _privateProperty,
publicCnter: publicCnter
}
})()
console.log(module.property) // I am private property
console.log(module.publicCnter()) // 1
console.log(module.publicCnter()) // 2
console.log(module.publicCnter()) // 3
复制代码
这个例子展现了经过当即执行函数表达式将一些变量、方法暴露出去,并防止外部直接修改一些咱们不但愿修改的变量、方法。这样作还有一个好处,就是咱们能够快速地了解到这个当即执行函数为咱们提供了哪些公共属性及方法,而不须要阅读全部逻辑代码。这种方式在设计模式中也称做 模块模式(Module Pattern)
。
CommonJS
CommonJS
主要是为服务端定义的模块规范,它一开始的名字为 ServerJS
。npm
生态系统基本都是基于 CommonJS
规范所创建起来的。
// 在 foo.js 中,咱们导出了变量 foo
module.exports = {
foo: 'foo'
}
// 在 bar.js,咱们经过 require 引入了变量 foo
var module = require('foo')
console.log(module.foo) // foo
复制代码
看起来很简单是吧。可能有人会问了,这个 module
是什么东西呢?其实 module
是 Node 中的一个内置对象。咱们能够在 node
环境下打印看看
咱们能够看到 module
有好几个属性,其中 id
是为了让 node
知道这个模块在哪里,是啥;exports
就是咱们要导出的对象了。
在确保 foo.js
和 bar.js
在同一目录下,咱们再将例子稍加修改:
// foo,js
module.exports = {
foo: 'foo'
}
console.log('module: ', module)
// bar.js
var module = require('./foo')
console.log(module.foo)
复制代码
运行 node bar.js
能够获得如下信息:
经过 CommonJS
规范定义的模块一样有一开始说到的模块的三大优势,其实咱们只须要把这些模块文件看出一个个当即执行函数,也就会很好理解了。
在 CommonJS
里模块都是同步加载的,在浏览器中若是同步去加载模块的话会形成阻塞,致使页面性能降低;而在服务端中,由于文件都存在于同一个硬盘上,因此即便是同步加载都不会有什么影响。
再补充一个小细节,你可能时不时能看到 var exports = module.exports
这样的代码。或许你会问为何要怎么作,难道有什么技巧吗?其实这只是简单的引用而已。即变量 exports
一样指向了 module.exports
的内存地址,也就是二者指向的对象是彻底同样的。咱们想在 module.exports
里添加导出的东西时,只须要在 exports
里加就好了。就是这么简单,只不过被一些说法搞得高深莫测了而已。
var exports = module.exports
exports.foo = 'foo''
复制代码
AMD - Asynchronous Module Definition
刚刚咱们说到 CommonJS
主要是用于服务端的规范,而客户端是没法使用它的,而且 CommonJS
是同步加载模块的。因此咱们又有了叫作 AMD
规范的东西,也就是异步模块定义规范。顾名思义,咱们能够利用这个规范来作到模块与模块的依赖能够经过异步的方式来加载;这也是浏览器(客户端)所但愿的。
AMD
中的核心就是 define
这个方法。
define(
module_id,
[dependencies],
definition
)
复制代码
其中 define
中的 module_id
与 dependencies
为可选参数。
首先 module_id
它是一个字符串,指的是定义的模块的名字,这个名字必须是惟一的。第二个参数 dependencies
是模块所依赖的模块组成的数组,并做为参数传入给第三个参数 definition
工厂方法中。第三个参数 definition
就是为模块初始化要执行的函数或对象。若是为函数,它应该只被执行一次。若是是对象,此对象应该为模块的输出值。
// dep1
define('dep1', [], function () {
return {
doSomething: function () {
console.log('do something')
}
}
})
define('dep2', [], function () {
return {
doOtherThing: function () {
console.log('do other thing')
}
}
})
define('module', ['dep1', 'dep2'], function (dep1, dep2) {
dep1.doSomething()
dep2.doOtherThing()
})
复制代码
虽然 AMD
规范提供了异步加载模块的方案,可是给个人感受就是逻辑不如 CommonJS
直观。所以在 ES6
中也就有了原生的模块化: ESM - ES Modules
。
ES Modules
CommonJS
在服务端中应用普遍,但因为它是同步加载模块的,它在客户端不太合适;而 AMD
支持浏览器异步加载模块,但在服务端却显得没有必要,所以 ES Modules
出现了。咱们先来看看 ES Modules
是如何工做的。
ES Modules
与 CommonJS
很类似,较新的浏览器均已支持 ES Modules
,Node.js
正在慢慢支持相关规范。
ES Modules
的核心为 export
与 import
,分别对应导出模块与导入模块。
导出模块:
// CommonJS
module.exports = foo () {
console.log('here is foo')
}
// ES Modules
export default function bar () {
console.log('here is bar')
}
复制代码
导入模块:
// CommonJS
var foo = require('./foo')
foo() // here is foo
// ES Modules
import bar from './bar'
bar() // here is bar
复制代码
这二者这么类似,它们在实际的表现上有什么不一样呢?
我分别在 Firefox
、Edge
和 Chrome
上测试(Chrome
因为自身的安全策略没法直接经过本地文件进行测试,因此利用插件 Web Server for Chrome
起了个本地服务器。
测试代码以下图(注意咱们在使用 ES Modules
时要给 script
标签加上 type="module"
)
测试的结果显示为:
开始执行 bar.js
开始执行 foo.js
here is foo
here is bar
复制代码
若是咱们使用 CommonJS
又会有什么结果呢?
开始执行 foo.js
here is foo
开始执行 bar.js
here is bar
复制代码
很显然 CommonJS
是同步加载模块的,因此代码的执行也是顺序的。而 ES Modules
是异步加载模块的,且 ES Modules
是编译时加载模块,在运行时(执行代码时)再根据相关的引用去被加载的模块中取值。再详细一点来讲的话,整个过程分以下三个步骤:
exports
和 imports
的空间。这一步称为连接所以在编译期间,编译器先找到了 foo.js
的依赖 bar.js
,先编译 bar.js
而后才是 foo.js
。因此你才会先看到 开始执行 bar.js
。
CommonJS
工做原理为同步加载模块,在 Node.js
中有着普遍的使用,对客户端不友好。AMD
工做原理为异步加载模块。ES Modules
为 ES6
推出的规范,客户端的支持比较好,Node.js
将会慢慢全面支持。它与 AMD
同样,也是异步加载模块。