之前有一篇博客非常详细的介绍了sea.js的加载流程,以及源代码实现,链接地址:http://www.cnblogs.com/chaojidan/p/4123980.html
这篇博客我主要讲下sea.js的介绍和使用。
首先,先介绍下sea.js的CMD规范,以及跟其他规范的区别。
CommonJS 原来叫 ServerJS,推出 Modules/1.0 规范后,在 Node.js 等环境下取得了很不错的实践。
09年下半年这帮充满干劲的小伙子们想把 ServerJS 的成功经验进一步推广到浏览器端,于是将社区改名叫 CommonJS,同时激烈争论 Modules 的下一版规范。分歧和冲突由此诞生,逐步形成了三大流派:
- Modules/1.x 流派。这个观点觉得 1.x 规范已经够用,只要移植到浏览器端就好。主流代表是服务端的开发人员。
- Modules/Async 流派。这个观点觉得浏览器有自身的特征,不应该直接用 Modules/1.x 规范。这个观点下的典型代表是 AMD 规范及其实现 RequireJS。
- Modules/2.0 流派。这个观点觉得浏览器有自身的特征,不应该直接用 Modules/1.x 规范,但应该尽可能与 Modules/1.x 规范保持一致。这个观点下的典型代表是 BravoJS 和 FlyScript 的作者。BravoJS 作者对 CommonJS 的社区的贡献很大,这份 Modules/2.0-draft 规范花了很多心思。FlyScript 的作者提出了 Modules/Wrappings 规范,这规范是 CMD 规范的前身。可惜的是 BravoJS 太学院派,FlyScript 后来做了自我阉割,将整个网站(flyscript.org)下线了。
第二流派:AMD 与 RequireJS
AMD 风格下,通过参数传入依赖模块,破坏了就近声明 (需要时,才声明)原则。比如:
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.foo()
}
})
第三流派:Modules/2.0 CMD模块
CMD 里,默认推荐的是
define(function(require, exports, module) { //a,b模块只下载好了,并且只执行了模块中的define方法,而define方法中的function要等到require时,才会执行
var a = require('a'); //延迟执行了a,b模块
var b = require('b');
// do sth
})
区别:
1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
2. CMD 推崇依赖就近,AMD 推崇依赖前置。看代码:
// CMD
define(function(require, exports, module) {
var a = require('./a');
a.doSomething()
//此处略去 100 行
var b = require('./b')
// 依赖可以就近书写
b.doSomething();
})
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) {
// 依赖必须一开始就写好
a.doSomething();
// 此处略去 100 行
b.doSomething();
})
虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。
3. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。
比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
CMD 可以使得构建时的复杂度降低。
目前 Sea.js 拥有 plugin-combo 插件,模块的合并可以放在线上动态做。有些情况下(比较容易出现),动态 combo 的地址会很长:
https://a.alipaybojects.com/??path/to/a.js,path/to/b.js..................path/to/long-url.js
当 url 地址很长时,超过 2083(好像是这个值),在 IE 以及一些服务器配置下,过长的 url 会出问题。这时经典的解决办法是将 url 拆分成多段:
https://a.alipaybojects.com/??path/to/a.js,path/to/b.js..................path/to/u.js
https://a.alipaybojects.com/??path/to/f.js,path/to/g.js..................path/to/long-url.js
拆分后,在 CMD 规范下,上面两个 url 可以并发同时请求,谁先返回都没问题。但在 AMD 下,上面的需求,就挂了,很难实现。
你会说 RequireJS 鼓励的是项目上线前,通过构建工具先构建好,不需要线上 combo,也就不会遇到上面的问题。
Sea.js 放得更宽泛,提前合并好,还是线上时才动态 combo,对 CMD 模块来说都可行。很多时候,combo 真心省事,也更自然。前端开发并非处处要像 Java 一样引入严格的构建过程。
CMD 的懒执行策略,也更有利于页面性能。
RequireJS 2.0 后,不少理念也在悄悄地发生着变化,现在好像也支持懒执行了。
然后,介绍下sea.js的方法和使用。
type="text/javascript" src="js/seajs/2.0.0/sea-debug.js?t=123" data-config="sea-js-config.js?t=123"
上面的data-config是指sea.js的配置文件的路径。还有一个属性是data-main,它是项目的起始模块,如果定义了会先执行此模块。data-main是可选项。
首先我们来看看sea-js-config.js
seajs.config({
// 配置插件
plugins: ['shim'],
// 配置别名
alias: {
// 配置 jquery 的 shim 配置,这样我们就可以通过 require('jquery') 来获取 jQuery
'jquery': {
src: 'libs/jquery/1.9.1/jquery.js', //注意,这里是从sea.js所在目录的上两节目录开始查找文件
exports: 'jQuery'
}
}
});
plugins选项配置插件,这里使用了shim插件。由于jquery不是一个标准的CMD模块,所以直接加载jquery是错误的。这里使用了shim插件,会自动把jquery转换成一个标准的CMD模块。不用人工改动jquery源码。alias是配置别名,方便加载的。
看个例子:
项目主模块app.js
define(function(require, exports, module) {
//加载jquery, 并把它$设为全局变量
window.$ = window.jQuery = $ = require('jquery');
//定义一个全局的模块加载函数.module为模块名,options为参数
exports.script_load = function(module, options) {
//使用require.async异步加载模块。模块需要提供一个固定的对外调用接口,这里定义为run。
require.async('modules/' + module, function(module) {
if (typeof(module.run) === 'function') {
module.run(options);
}
});
}
window.script_load = exports.script_load
});
上面我们加载了jquery, 并且定义了一个模块加载函数。现在我们在html页面加入如下代码:
<script type="text/javascript">
seajs.use('modules/app', function(app) {
app.script_load('index');
});
</script>
use方法执行时,会先加载app模块,加载并执行完后,就进入function中,这时就会调用app.script_load方法,此方法就会去加载index模块,加载完成后,执行index中的代码,index中会返回run方法。index执行完毕后,会调用require.async的回调方法:
if (typeof(module.run) === 'function') {
module.run(options);
}
因此index模块中返回了run方法,因此就执行index中的run方法。
index.js
define(function(require, exports, module) {
exports.run = function() {
$('#alert').click(function() {
alert('弹出了一个框!');
});
}
});
SeaJS中使用“define”函数定义一个模块,define可以接收三个参数,
define可以接收的参数分别是模块ID,依赖模块数组及工厂函数。
我阅读源代码后发现define对于不同参数个数的解析规则如下:
如果只有一个参数,则赋值给factory。
如果有两个参数,第二个赋值给factory;第一个如果是array则赋值给deps,否则赋值给id。
如果有三个参数,则分别赋值给id,deps和factory。
id是一个模块的标识字符串,define只有一个参数时,id会被默认赋值为此js文件的绝对路径。
如example.com下的a.js文件中使用define定义模块,则这个模块的ID会赋值为 http://example.com/a.js ,没有特别的必要建议不要传入id。deps一般也不需要传入,需要用到的模块用require加载即可。
工厂函数function是模块的主体和重点。在只传递一个参数给define时(推荐写法),这个参数就是工厂函数,此时工厂函数的三个参数分别是:
• require——模块加载函数,用于记载依赖模块。
• exports——接口点,将数据或方法定义在其上则将其暴露给外部调用。
• module——模块的元数据。
module是一个对象,存储了模块的元信息,具体如下:
• module.id——模块的ID。
• module.dependencies——一个数组,存储了此模块依赖的所有模块的ID列表。
• module.exports——与exports指向同一个对象。
三种编写模块的模式:
第一种定义模块的模式是基于exports的模式:
define(function(require, exports, module) {
var a = require('a'); //引入a模块
var b = require('b'); //引入b模块
var data1 = 1; //私有数据
var func1 = function() { //私有方法
return a.run(data1);
}
exports.data2 = 2; //公共数据
exports.func2 = function() { //公共方法
return 'hello';
}
});
上面是一种比较“正宗”的模块定义模式。除了将公共数据和方法附加在exports上,也可以直接返回一个对象表示模块,如下面的代码与上面的代码功能相同:(第二种)
define(function(require, exports, module) {
var a = require('a'); //引入a模块
var b = require('b'); //引入b模块
var data1 = 1; //私有数据
var func1 = function() { //私有方法
return a.run(data1);
}
return {
data2: 2,
func2: function() {
return 'hello';
}
};
});
如果模块定义没有其它代码,只返回一个对象,还可以有如下简化写法。第三种方法对于定义纯JSON数据的模块非常合适。
define({
data: 1,
func: function() {
return 'hello';
}
});
绝对地址——给出js文件的绝对路径。如
require("http://example/js/a");
就代表载入 http://example/js/a.js 。
基址地址——如果载入字符串标识既不是绝对路径也不是以”./”开头的相对地址,则相对SeaJS全局配置中的“base”来寻址。
注意上面在载入模块时都不用传递后缀名“.js”,SeaJS会自动添加“.js”。但是下面三种情况下不会添加:
载入css时,如
require("./module1-style.css");
路径中含有”?”时,如
require(<a href="http://example/js/a.json?cb=func">http://example/js/a.json?cb=func</a>);
路径以”#”结尾时,如
require("http://example/js/a.json#");
根据应用场景的不同,SeaJS提供了三个载入模块的API,分别是seajs.use,require和require.async。
seajs.use主要用于载入入口模块。入口模块相当于C程序的main函数,同时也是整个模块依赖树的根。seajs.use用法如下:
//单一模式
seajs.use('./a');
//回调模式
seajs.use('./a', function(a) {
a.run();
});
//多模块模式
seajs.use(['./a', './b'], function(a, b) {
a.run();
b.run();
});
一般seajs.use只用在页面载入入口模块,SeaJS会顺着入口模块解析所有依赖模块并将它们加载。如果入口模块只有一个,也可以通过给引入sea.js的script标签加入”data-main”属性来省略seajs.use,例如,
<!DOCTYPE HTML>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>TinyApp</title>
</head>
<body>
<p class="content"></p>
<script src="./sea.js" data-main="./init"></script>
</body>
</html>
传给require的路径标识必须是字符串字面量,不能是表达式,如下面使用require的方法是错误的:
require('module' + '1');
require('Module'.toLowerCase());
这都会造成SeaJS无法进行正确的正则匹配以下载相应的js文件。
上文说过SeaJS会在html页面打开时通过静态分析,一次性下载所有需要的js文件,如果想要某个js文件在用到时才下载,可以使用require.async。
require.async('/path/to/module/file', function(m) {
//code of callback...
});
这样只有在用到这个模块时,对应的js文件才会被下载,也就实现了JavaScript代码的按需加载。
SeaJS提供了一个seajs.config方法可以设置全局配置,接收一个表示全局配置的配置对象。
seajs.config({
base: 'path/to/jslib/',
alias: {
'app': 'path/to/app/'
},
charset: 'utf-8',
timeout: 20000,
debug: false
});
其中base表示基址寻址时的基址路径。例如base设置为 http://example.com/js/3-party/ ,则
var $ = require('jquery');
会载入 http://example.com/js/3-party/jquery.js 。
alias可以对较长的常用路径设置缩写。
charset表示下载js时script标签的charset属性。
timeout表示下载文件的最大时长,以毫秒为单位。
debug表示是否工作在调试模式下。
要将现有JS库如jQuery与SeaJS一起使用,只需根据SeaJS的的模块定义规则对现有库进行一个封装。例如,下面是对jQuery的封装方法:
define(function() {
//{{{jQuery原有代码开始
//}}}jQuery原有代码结束
return $.noConflict();
});
特别注意:下面这种写法是错误的!
define(function(require, exports) {
// 错误用法!!!
exports = {
foo: 'bar',
doSomething: function() {}
};
});
正确的写法是用 return 或者给 module.exports 赋值:
define(function(require, exports, module) {
// 正确写法
module.exports = {
foo: 'bar',
doSomething: function() {}
};
});
提示:exports 仅仅是 module.exports 的一个引用。在 factory 内部给 exports 重新赋值时,并不会改变 module.exports 的值。因此给 exports 赋值是无效的,不能用来更改模块接口。
传给 factory 构造方法的 exports 参数是 module.exports 对象的一个引用。
只通过 exports 参数来提供接口,有时无法满足开发者的所有需求。 比如当模块的接口是某个类的实例时,需要通过module.exports 来实现:
define(function(require, exports, module) {
// exports 是 module.exports 的一个引用
console.log(module.exports === exports); // true
// 重新给 module.exports 赋值
module.exports = new SomeClass(); //当模块的接口是某个类的实例时
// exports 不再等于 module.exports
console.log(module.exports === exports); // false
});
注意:对 module.exports 的赋值需要同步执行,不能放在回调函数里。下面这样是不行的:
define(function(require, exports, module) {
// 错误用法
setTimeout(function() {
module.exports = { a: "hello" };
}, 0);
});
seajs.config({
alias: {
'jquery': 'jquery/1.7.2/jquery-debug.js'
}
});
seajs.use(['./a','jquery'],function(a,$){
var num = a.a;
$('#J_A').text(num);
})
use方法将会从我们的config配置信息中查看 ,是否有预先需要被加载的模块。如果有,就先加载,没有就加载a和jquery模块。