[TOC]
AMD规范
模块的重要作用:有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。
目前,通行的Javascript模块规范共有两种:CommonJS和AMD。
nodejs的模块系统,就是参照CommonJS规范实现的,由于nodejs是服务器环境,有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。
nodejs的所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。
1. AMD说明
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
define和require这两个定义模块、调用模块的方法,合称为AMD模式。它的模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。
AMD模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
2. RequireJS库实现了AMD标准
RequireJS是一个工具库,主要用于客户端的模块管理。它可以让客户端的代码分成一个个模块,实现异步或动态加载,从而提高代码的性能和可维护性。
2.1 require.js的加载
首先,需要去require官网下载require.js文件,比如2.3.6版本,下载好后,放到一个文件夹中,然后在文件夹中新建一个html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<title>require测试</title>
</head>
<script data-main="main" src="require.js"></script>
</html>
在html文件中的script元素中,src属性加载了require文件,data-main属性指定网页程序的主模块,即项目文件夹中的main.js文件。main.js文件内容:
console.log('主函数文件1');
require([], function(params) {
console.log('主函数文件2');
})
console.log('主函数文件3');
在浏览器中打开html文件,打开终端就能看见输出内容了。
3. 模块定义:define方法
define方法用于定义模块,RequireJS要求每个模块放在一个单独的文件里。
define(id?, dependencies?, factory);
define方法有三个参数:
id:第一个参数,id,是个字符串。它指的是定义中模块的名字,这个参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。
dependencies:第二个参数,是个定义中模块所依赖模块的数组。这个参数是可选的。依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。
factory:第三个参数,为模块初始化要执行的函数或对象。这个参数必填。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。
模块按照是否依赖其他模块,可以分为两种情况:1、不依赖其他模块的独立模块。2、依赖其他模块的模块。
3.1 独立模块的定义
如果被定义的模块是一个独立模块,不需要依赖任何其他模块,可以直接用define方法生成。
define定义的模块可以返回任何值,不限于对象。
define({
method1: function() {},
method2: function() {},
}); // 定义拥有method1、method2两个方法的模块
另一种等价的写法是,把对象写成一个函数,该函数的返回值就是输出的模块。
define(function () {
// 这种写法自由度更高一点,可以在函数体内写一些模块初始化代码
return {
method1: function() {},
method2: function() {},
};
});
3.2 非独立模块的定义
如果被定义的模块需要依赖其他模块,则define方法必须采用下面的格式:
define(['module1', 'module2'], function(m1, m2) {
return {
method: function() {
m1.methodA();
m2.methodB();
}
};
});
define方法的第一个参数是一个数组,它的成员是当前模块所依赖的模块。比如上面的代码中定义的新模块依赖于module1模块和module2模块,只有先加载这两个模块,新模块才能正常运行。一般情况下,module1模块和module2模块指的是,当前目录下的module1.js文件和module2.js文件,等同于写成[’./module1’, ‘./module2’]。
define方法的第二个参数是一个函数,当前面数组的所有成员加载成功后,它将被调用。它的参数与数组的成员一一对应,比如function(m1, m2)就表示,这个函数的第一个参数m1对应module1模块,第二个参数m2对应module2模块。这个函数必须返回一个对象,供其他模块调用。
上面代码表示新模块返回一个对象,该对象的method方法就是外部调用的接口,menthod方法内部调用了m1模块的methodA方法和m2模块的methodB方法。
需要注意的是,回调函数必须返回一个对象,这个对象就是你定义的模块。
如果依赖的模块很多,参数与模块一一对应的写法非常麻烦。为了避免像上面代码那样繁琐的写法,RequireJS提供一种更简单的写法。
define(
function (require) {
var dep1 = require('dep1'),
dep2 = require('dep2'),
dep3 = require('dep3'),
dep4 = require('dep4'),
dep5 = require('dep5'),
dep6 = require('dep6'),
dep7 = require('dep7'),
...
}
});
一个例子,通过判断浏览器是否为IE,而选择加载zepto或jQuery。
define(('__proto__' in {} ? ['zepto'] : ['jquery']), function($) {
return $;
});
上面代码定义了一个中间模块,该模块先判断浏览器是否支持__proto__属性(除了IE,其他浏览器都支持),如果返回true,就加载zepto库,否则加载jQuery库。
4. 调用模块:require方法
require方法用于调用模块。它的参数与define方法类似。
第一个参数,是一个表示依赖关系的数组。
第二个参数,是一个回调函数,当依赖模块都加载完成后,会当成参数传给回调函数,然后执行这个回调函数。
第三个参数,处理错误的回调函数,接受一个error对象作为参数。
require对象还允许指定一个全局性的Error事件的监听函数。所有没有被上面的方法捕获的错误,都会被触发这个监听函数。
// 所有没有被require第三个参数回调方法捕获的错误,都会被触发这个监听函数。
requirejs.onError = function (err) {
};
require(["backbone"], function ( Backbone ) {
return Backbone.View.extend({ /* ... */ });
},
function (err) {
// ...
}
);
// 第一个参数灵活调用:判断浏览器是否支持原生JSON,
// 如果支持,则传入undefined,否则传入util目录下的json2模块
require( [ window.JSON ? undefined : 'util/json2' ], function ( JSON ) {
JSON = JSON || window.JSON;
console.log( JSON.parse( '{ "JSON" : "HERE" }' ) );
});
require方法也可以用在define方法内部。
define(function (require) {
var otherModule = require('otherModule');
});
// 动态加载:内部加载了foo和bar两个模块,在没有加载完成前,isReady属性值为false,
// 加载完成后就变成了true。因此,可以根据isReady属性的值,决定下一步的动作。
define(function ( require ) {
var isReady = false, foobar;
require(['foo', 'bar'], function (foo, bar) {
isReady = true;
foobar = foo() + bar();
});
return {
isReady: isReady,
foobar: foobar
};
});
其他一些例子:
// 返回一个promise对象,可以在该对象的then方法,指定下一步的动作。
define(['lib/Deferred'], function( Deferred ){
var defer = new Deferred();
require(['lib/templates/?index.html','lib/data/?stats'],
function( template, data ){
defer.resolve({ template: template, data:data });
}
);
return defer.promise();
});
// JSONP模式可以直接在require中调用,方法是指定JSONP的callback参数为define。
require( [
"http://someapi.com/foo?callback=define"
], function (data) {
console.log(data);
});
5. 配置require.js:config方法
require方法本身也是一个对象,它带有一个config方法,用来配置require.js运行参数。config方法接受一个对象作为参数。对象有以下主要成员:
1、paths:
paths参数指定各个模块的位置。这个位置可以是同一个服务器上的相对位置,也可以是外部网址。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置,上面的示例就表示如果CDN加载失败,则加载服务器上的备用脚本。需要注意的是,指定本地文件路径时,可以省略文件最后的js后缀名。
2、baseUrl:
baseUrl参数指定本地模块位置的基准目录,即本地模块的路径是相对于哪个目录的。该属性通常由require.js加载时的data-main属性指定。
3、shim:
有些库不是AMD兼容的,这时就需要指定shim属性的值。shim可以理解成“垫片”,用来帮助require.js加载非AMD规范的库。
require.config({
baseUrl: 'src',
paths: {
moment: '/moment-2.8.3/moment',
jquery: [
'//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
'lib/jquery'
],
"backbone": "vendor/backbone", // 非AMD规范的库
"underscore": "vendor/underscore" // 非AMD规范的库
},
shim: {
"backbone": {
deps: [ "underscore" ], // backbone依赖于underscore
exports: "Backbone" // 导出的名字,别的模块使用这个名字引用这个模块
},
"underscore": {
exports: "_"
}
}
});
5.1 关于paths属性中名字问题
如果一个模块在定义的时候,已经指定名字了:
define('jquery', [], function() { ... });
当在require的config中的paths使用别的名字引用:
myJquery: 'lib/jquery'
当引用文件后,在jquery.js文件中声明的名字jquery与配置的名字myJquery不同,便不会把jquery模块赋值给myJquery,导致myJquery的值是undefined。
所以我们在使用一个第三方的时候,一定要注意它是否声明了一个确定的模块名。
如果自己定义的模块,没有指定模块名字:
define([], function() {
});
我们可以在 requirejs.config
里,使用任意一个模块名来引用它。这样的话,就让我们的命名非常自由。
5.2 config的使用
第一种在html页面中使用script形式使用:
<!DOCTYPE html>
<html lang="en">
<script src="require.js"></script>
<script>
require.config({
baseUrl: 'src',
paths: {
jquery: './dep/jquery/dist/jquery.min',
}
});
</script>
<script>
require(['main'], function () {
// 执行
});
</script>
</html>
第二种是在入口文件main.js里面定义:
require.config({
baseUrl: 'src',
paths: {
jquery: './dep/jquery/dist/jquery.min',
}
});
require([], function(params) {
console.log('主函数文件');
})
6. require插件
RequireJS允许使用插件,加载各种格式的数据。完整的插件清单可以查看官方网站。
下面是插入文本数据所使用的text插件的例子。
define([
'backbone',
'text!templates.html'
], function( Backbone, template ){
// ...
});
上面代码加载的第一个模块是backbone,第二个模块则是一个文本,用’text!’表示。该文本作为字符串,存放在回调函数的template变量中。