一、早期在没有工具和规范的情况下,对模块化的发展
阶段一:基于文件的划分模块的方式
实现:将每个功能及其相关状态数据各自单独放到不同的文件中,约定每个文件就是一个独立的模块,使用某个模块就是将这个模块引入到页面中,然后直接调用模块中的成员
缺点:所有模块都直接在全局工作,没有私有空间,所有成员都可以在模块外部被访问或修改,而且模块一段多了过后,容易产生命名冲突,另外无法管理模块与模块之间的依赖关系。
// module-a
var module_name = 'module-a'
function method1 () {
console.log(module_name + '#method1')
}
// module-b
var module_name = 'module-b'
function method1 () {
console.log(module_name + '#method1')
}
// index.html
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
// 命名冲突,这里module-a 和 module-b 的方法相冲突
method1()
// 模块 a、b 内的成员可以被修改
module_name = 'foo'
</script>
阶段二:每个模块只暴露一个全局对象,所有模块成员都挂载到这个对象中
实现:在第一阶段的基础上,通过将每个模块「包裹」为一个全局对象的形式实现,有点类似于为模块内的成员添加了「命名空间」的意思
缺点:通过「命名空间」减小了命名冲突的可能,但还是没有私有空间,所有模块成员仍然可以在模块外部被访问或修改,而且也无法管理模块之间的依赖关系
// module-a
var moduleA = {
module_name : 'module-a',
method1 : function () {
console.log(this.module_name + '#method1')
}
}
// module-b
var moduleB = {
module_name : 'module-b',
method1 : function () {
console.log(this.module_name + '#method1')
}
}
// index.html
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模块成员可以被修改
moduleA.module_name = 'foo'
</script>
阶段三:使用立即执行函数表达式(IIFE:Immediately-Invoked Function Expression)为模块提供私有空间
实现:将每个模块成员都放在一个函数提供的私有作用域中,对于需要暴露给外部的成员,通过挂在到全局对象上的方式实现
优点:有了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问
// module-a
;(function () {
var module_name = 'module-a'
function method1 () {
console.log(module_name + '#method1')
}
window.moduleA = {
method1: method1
}
})()
// module-b
;(function () {
var module_name = 'module-b'
function method1 () {
console.log(module_name + '#method1')
}
window.moduleB = {
method1: method1
}
})()
// index.html
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模块成员不可以被修改
console.log(moduleA.module_name) // => undefined
</script>
阶段四:利用 IIFE 参数作为依赖声明使用
实现:在第三阶段的基础上,利用立即执行函数的参数传递模块依赖项
优点:这使得每一个模块之间的关系变得更加明显
// module-a
;(function ($) {
var module_name = 'module-a'
function method1 () {
$('body').animate({margin: '200px'})
console.log(module_name + '#method1')
}
window.moduleA = {
method1: method1
}
})(jQuery)
// module-b
;(function () {
var module_name = 'module-b'
function method1 () {
console.log(module_name + '#method1')
}
window.moduleB = {
method1: method1
}
})()
// index.html
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
</script>
以上的模块化演变,都是以原始的模块为基础,通过约定的方式去实现模块化的代码组织。
为了统一不同的开发者和不相同项目之间的差异,因此需要一个标准去规范模块化实现的方式。
另外,在模块化当中,针对模块加载的问题,都是通过手动添加,一旦时间久了过后,维护起来非常麻烦。
二、模块化规范的出现
1、CommonJS规范(同步加载模块)
- 允许模块通过
require
方法来同步加载所要依赖的其他模块,然后通过exports
或module.exports
来导出需要暴露的接口。
CommonJS规范主要是用于服务端的模块化规范,可以说NodeJS是它的最佳实践,这是因为它是内置的模块系统。在服务端,模块的加载是运行时同步加载的。而在浏览器端的资源是异步加载的,模块需要提前编译打包处理。
2、AMD规范(异步加载模块)
- AMD规范全称是Asynchronous Module Definition,即异步模块加载机制。它完整描述了模块的定义,依赖关系,引用关系以及加载机制。
AMD对应的就是很有名的RequireJS
RequireJS 是一个 JavaScript模块加载器。它是执行的AMD规范,因此所有的依赖模块都是先执行;即RequireJS是预先把依赖的模块执行。
执行流程:
- require函数检查依赖的模块,根据配置文件,获取js文件的实际路径
- 根据 js 文件实际路径,在dom中插入script节点,并绑定onload事件来获取该模块加载完成的通知。
- 依赖script全部加载完成后,调用回调函数。
- 在AMD规范当中,它约定每个模块都必须要通过define()这个函数去定义。
这个函数默认可以接收两个参数,也可以接收三个参数。- 参数一:这个模块的名字(后期加载这个模块时使用)
- 参数二:一个数组,用来声明这个模块的依赖项。
- 参数三:是一个函数,函数的参数与参数二的依赖项一一对应。每一项分别为
依赖项导出的成员(作用:必需,工厂方法,初始化模块需要执行的函数或对象。如果为函数,它只被执行一次。如果是对象,此对象会作为模块的输出值。)
// 定义一个模块
define('module1', ['jquery','./module2'],function ($, module2) {
// 向外部导出成员,通过 return 方式实现
return {
start: function () {
$('body').animate({margin: '200px'})
module2()
}
}
})
// 载入一个模块
require(['./module1'], function (module1) {
module1.start()
})
缺点: AMD使用复杂,模块 JS 文件请求频繁。
3、CMD规范(异步加载模块)
- CMD的全称是Common Module Definition,即通用模块定义,其提供了模块定义和按需加载执行模块。
- 在CMD中,一个模块就是一个文件,通过
define()
函数定义一个模块。其接收 factory 参数,factory可以是一个函数,也可以是一个对象或者字符串。 - factory为函数时,有三个参数
function(require、exports、module){}
- require是一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口:require(id)
- exports是一个对象,用来向外提供模块接口
- module 是一个对象,用于存储与当前模块相关联的一些属性和方法。
- 实例:
define(function(require、exports、module){
// 通过 require 引入依赖
var $ = require('jquery')
// 通过 exports 或者 module.exports 对外暴露成员
module.exports = function () {
console.log('module 2~')
$('body').append('<p>module2</p>')
}
})
- 淘宝推出的 Sea.js 就是使用了 CMD规范
4、UMD
- UMD 通用模块定义规范(Universal Module Definition),是 CommonJS和AMD 的糅合,
- UMD先判断是否支持 Node.js的模块(exports)是否存在,存在则使用Node.js模块模式;在判断是否支持AMD(define)是否存在,存在则使用AMD方式加载模块
5、 ES Modules
- 它是 ECMAScript 2015 即ES6 中定义的一个最新模块系统。
- ES Modules 模块设计思想:尽量的静态化、使得编译时就能确定模块的依赖关系,以及输入和输出的变量(CommonJS和AMD模块,都只能在运行时确定这些东西)。