早期的JavaScript发展初期只是为了少量的页面交互逻辑,且功能(逻辑)简单,代码量少,甚至于早期的Web是没有前端这个说法的,后端顺便一写JS。
随着时间发展,进入Web2.0时代,CPU等硬件性能的提升也使得浏览器性能得到了提升,很多页面交互逻辑迁移到了客户端(浏览器),加上新技术不断涌现(Ajax),JQuery等前端库层出不穷,代码量日益膨胀。
这时候JS作为动态语言的定位就显得捉襟见肘,没有类的概念,没有模块,简单的代码组织不足以驾驭如此大规模的代码。
模块
从最简单的开始:
function step1() {
};
function step2() {
};
step1();
step2();
这是上古时期的JS书写方式,亦或者初学JavaScript的新手书写JS的方法,当然也属于面向过程式的。
当逻辑交互增多,一个JS文件显然不够了,需要引入多个JS文件,并且这种书写的JS问题也很明显。
- 函数都是在
global
下定义,别人可以随意的修改操控这些全局函数,污染了全局变量。 - 如果有人要是在另一个文件的人也定义了一个
step
函数会如何?会发生命名冲突。
为了解决如上问题,对象的写法应运而生,可以把所有的模块成员封装在一个对象中。
var Moudle = {
value1: 1,
value2: 2,
method1: function() {
/*do something*/
},
method2: function() {
/*do somethinlg*/
}
}
调用时,只要保证模块名唯一即可。
当然,这并没有从根本上解决这个问题,外部依然可以随意修改内部成员。
Moudle.value1 = 10;
这样会产生安全问题。
后来又有人使用立即执行的函数表达式,也叫IIFE模式。
var Moudle = (function() {
var a = 1;
var b = 2;
function method1() {
console.log(a);
};
function method2() {
console.log(b);
};
return {
method1: method1,
method2: method2
}
})();
Moudle.method1(); // 1
Moudle.method2(); // 2
这样在模块外部无法修改我们没有暴露出来的变量、函数。
上述做法就是我们模块化的基础。
AMD/CMD/CommonJS是什么?
CommonJS / Node.js
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。
为什么JS可以在服务器端运行就标志着模块化编程到来呢?因为,在浏览器下,没有模块也不是很致命的问题,毕竟网页程序的复杂度有限。但对于服务端就不一样了,如果没有模块与操作系统底层或者其他应用程序互动,根本无法编程。
Node.js是CommonJS规范的实现
node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。
- 根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块获取,除非定义为global对象的属性。
- 模块输出:模块只有一个出口,
module.exports
对象,我们需要把模块希望输出的内容放入对象。 - 加载模块:加载模块使用require()方法,该方法获取一个文件并执行,返回文件内部的module.exports对象。
模块定义a.js
var myModule = {
a: 10,
sayA: function() {
return this.a;
}
}
module.exports = myModule;
加载模块
var myModule = require("./a.js");
console.log(myModule.sayA()); // 10
CommonJS定义的模块分为:{模块引用(require)} {模块定义(exports)} {模块标识(module)}
require()
用来引入外部模块;exports
对象用于导出当前模块的方法或变量,唯一的导出口;module
对象就代表模块本身。
AMD / require.js
背景
在CommonJS规范的Node.js诞生后,服务端的模块化概念已经形成,很自然的,大家就想要客户端(浏览器)模块。而且最好两者都兼容,一个模块不用改,在两端都可以运行。
但是有一个缺陷,使得CommonJS规范不适用于浏览器环境。
var myModule = require("./a.js");
console.log(myModule.sayA()); // 10
第二行的sayA()方法,在第一行的a模块之后运行,必须得等到a.js加载完成后才能使用方法,如果无法加载或者加载没有完成那就会造成阻塞,直到a.js加载完成后续的代码才会执行。即:它们是同步的。
同步加载对于服务器端不是难事,因为资源都存放在服务器端,即用即取,完全可以同步加载,等待的时间就是服务器的硬盘读取时间,这个时间肯定比浏览器快的多。
受到网速限制,如果等待很长时间都未能加载,页面就会“假死”,这对用户来说是相当不友好的。
因此,基于这样的特殊背景,浏览器端的模块,不能采用同步加载,只能采用异步加载,这就是AMD规范的诞生。
require.js是AMD规范的实现
AMD介绍
AMD(Asynchronous Module Definition),中文名异步模块定义,是一个在浏览器端模块化开发的规范。
由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎的RequireJS,实际上AMD是requireJS在推广过程中对模块定义的规范化的产出。
requireJS解决了什么问题呢?
- 实现JS文件的异步加载,避免网页失去相应。
- 管理模块之间的依赖性,便于代码的编写和维护。
<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script src="4.js"></script>
<script src="5.js"></script>
<script src="6.js"></script>
这样的代码,很多人都应该写过,繁多复杂,并且,还要体现依赖性,如照这样写,说明1.js必定被2,3,4,5,6所依赖。如果依赖关系在复杂一点,可读性,维护将变得很差。
还有一个缺点,就是script标签加载时的阻塞问题。
使用require.js,首先需要引入它。
requireJS
<script src="./lib/require.js" defer async="true"></script>
当然,加载这个文件也可能使网页失去响应,要么放在body下方;要么写上async属性表明这个文件需要异步加载,避免网页失去响应,IE不支持这个属性,所以写上defer。
加载require.js之后,下一步就是加载自己的代码了,假定我们的代码文件存放在app下,那么写成这样就好了。
<script src="./lib/require.js" defer async="true" data-main="./app/a.js"></script>
data-main属性的作用是,指定网页程序的主模块,如上代码,就是app目录下的a.js。这个文件会第一个被require.js加载。由于require.js默认的文件后缀名的js,所以可以把main.js简写成main。
主模块写法
a.js,把它称为“主模块”,意思是整个网页的入口代码,类似C语言里的main()函数,所有代码都从这儿开始运行。
怎么写a.js?
如果你的a.js不依赖任何模块,那么直接写就行了。
alert("加载成功!");
但很显然存在模块,并且主模块a.js依赖于其他模块,比如你的tab模块、轮播图模块等等。
这时就要用到AMD规范定义的require()函数。
注意,这个不是CommonJS里的require函数了。
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
// some code here
});
require()函数接受两个参数。
- 第一个参数:表示所依赖的模块,例子中就是
['moduleA','moduleB','moduleC']
,即主模块依赖这三个模块; - 第二个参数:是一个回调函数,只有前面的所依赖的模块加载完成,回调函数才会被调用,加载的模块会以参数的形式传入函数,从而在回调函数中使用这些模块。
require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
注意: 默认情况下,require.js会假设这三个模块与主模块目录相同,关于目录指定,可以查看官方文档
假设现在主模块依赖moduleA、moduleB、moduleC这三个模块,那么主模块可以这样写:
require(["moduleA","moduleB","moduleC"],function(moduleA,moduleB,moduleC) {
var sum = moduleA.str + moduleA.str + moduleC.str;
console.log(sum); // " module A module A module C"
});
在CommonJS中,模块的出口完全靠module.exports
,在requireJS中,我们又如何定义模块呢?
在之前的例子中,A,B,C三个模块存放在app目录下,require.js假定这三个模块与主模块(a.js)在同一个目录中,然后就可以自动加载他们了。
RequireJS以一个相对于baseUrl的地址来加载所有的代码。 页面顶层
<script>
标签含有一个特殊的属性data-main
,require.js
使用它来启动脚本加载过程,而baseUrl
一般设置到与该属性相一致的目录。
因为我们显式指定了data-main属性,那么baseUrl就会与data-main属性所在目录一制,自动的从目录内加载文件。
当然,我们也可以对模块的加载行为进行自定义。require.config()就写在主模块(a.js)的头部。参数就是一个对象,这个对象的paths属性指定在各个模块的加载路径。
假设现在有两个依赖模块在lib目录下,那么我们可以在主模块头部直接指定每个模块的路径。
require.config({
paths: {
"moduleA": "../lib/moduleA",
"moduleB": "../lib/moduleB",
"moduleC": "moduleC",
}
})
require(["moduleA","moduleB","moduleC"],function(moduleA,moduleB,moduleC) {
var sum = moduleA.str + moduleA.str + moduleC.str;
console.log(sum); // " module A module A module C"
});
注意,不要写成../lib/moduleA.js
。
如果写成了,解析目录就会变成你的HTML页面所在的直接目录下。
当然,你也可以指定主机名,也就是直接指定它的绝对URL。
require.config({
paths: {
"jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
}
});
require.js要求,每个模块是一个单独的js文件。这样的话,如果加载多个模块,就会发出多次HTTP请求,会影响网页的加载速度。因此,require.js提供了一个优化工具,当模块部署完毕以后,可以用这个工具将多个模块合并在一个文件中,减少HTTP请求数。
模块如何定义
require.js加载的模块,采用AMD规范,也就是说,模块必须按照AMD的规范来写。
模块必须采用特定的define()函数来定义,如果一个模块不依赖其他模块,那么直接定义了define()函数之中。
比如我们新建一个math模块 —— math.js。
define(function() {
var add = function(x,y) {
return x+y;
};
return {
add: add
};
});
加载方法:
require(["math"],function(math) {
console.log(math.add(1,1)); // 2
});
如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。
// math.js
define(["myLib"],function(myLib) {
function foo() {
console.log(myLib.num);
}
return {
foo: foo
}
});
主模块
a.js
依赖math
模块,math
模块依赖myLib
模块
AMD中文网
CMD / Sea.js
CMD(Common Module Definition通过模块定义),CMD规范是国内发展出来的,比如AMD有个requireJS,CMD有个SeaJS,SeaJS要解决的问题
Sea.js推崇一个模块一个文件,遵循统一的写法。
- 一个文件一个模块,所以经常就用文件名作为模块id
- CMD推崇依赖就近,所以一般不在define的参数中写依赖,在factory中写。
CMD是懒加载,虽然会一开始就并行加载js文件,但是不会执行,而是在需要的时候才执行
define
define(id?, deps?, factory);
参数factory
也有三个参数
function(reuqire, exports, module);
- require:一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口。
- exports:一个对象,用来向外提供模块接口。
- module:上面存储了与当前模块相关联的一些属性和方法。
// 定义模块 myModule.js
define(function(require, exports, module) {
var $ = require('jquery.js')
$('div').addClass('active');
});
// 加载模块
seajs.use(['myModule.js'], function(my){
});
总结:
规范实现
CommonJS / Node.js
AMD / Require.js
CMD / Sea.js
针对端
AMD / CMD主要针对浏览器端
CommonJS主要针对服务端
加载区别
// CMD
define(function (require, exports, module) {
var a = require("./a");
a.doSomething();
var b = require("./b");
b.doSomething();
})
// AMD
define(["./a", "./b"], function (a, b) {
a.doSomething();
b.doSomething();
})
主要区别:执行时机 处理不同,注意不是加载的时机或者方式不同。
CMD推崇依赖就近,AMD推崇依赖前置。
CMD是延迟执行,AMD是提前执行。
加载模块时,都是异步加载。
AMD因为依赖前置,当所有模块加载好后,就会立即执行,进入require回调函数,执行主逻辑。因为异步原因,模块加载和执行不一定一致,如a模块和b模块,b模块先加载完成,那么会先执行b模块,但是,主逻辑一定是在所有依赖模块加载后才执行的。
CMD因为依赖就近,模块加载好后并不执行,只是下载而已,当所有依赖模块加载完成后进入主逻辑,遇到require语句时才会执行对应模块,这样模块执行顺序和书写顺序一致。
js模块化编程之彻底弄懂CommonJS和AMD/CMD!
AMD/CMD/CommonJs到底是什么?它们有什么区别?
前端模块化开发的价值
MODULE?
JavaScript AMD 与 CMD 规范