AMD 之 RequireJS

References

从概念入手:http://javascript.ruanyifeng.com/tool/requirejs.html
从例子入手:
RequireJS 入门一:http://www.cnblogs.com/snandy/archive/2012/05/22/2513652.html
RequireJS 入门二:http://www.cnblogs.com/snandy/archive/2012/05/23/2513712.html
RequireJS 入门三:http://www.cnblogs.com/snandy/archive/2012/05/24/2514700.html
RequireJS 进阶一:http://www.cnblogs.com/snandy/archive/2012/06/06/2536969.html
RequireJS 进阶二:http://www.cnblogs.com/snandy/archive/2012/06/07/2537477.html
RequireJS 进阶三:http://www.cnblogs.com/snandy/archive/2012/06/08/2538001.html

例子

学习新东西,最好的办法就是从简单的例子入手,

入门例子一: 引用 jQuery 模块

达到的目的,将 jQuery 作为一个 AMD 模块,被 main.js 引用。

测试环境

1
$ mkdir sample-1 | cd sample-1

创建 package.json

1
$ npm init -y

安装 Grunt,Grunt Connect

1
$ npm install grunt grunt-contrib-connect --save-dev

创建 Gruntfile.js 并配置 Grunt Connect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'use strict';  
module.exports = function (grunt) {
// Project configuration.
grunt.initConfig({
connect: {
server: {
options: {
port: 8000,
hostname: '*',
keepalive: true,
base: ['.']
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-connect');
}

测试文件

我们将创建如下测试文件

1
2
3
4
5
.
├── index.html
├── jquery-1.7.2.js
├── main.js
└── require.js

注: require.js 和 jquery-1.7.2.js 是通过官网直接下载的,通过 sample1.zip 源码直接下载。

index.html

1
2
3
4
5
6
7
8
9
10
11
<!doctype html>
<html>
<head>
<title>requirejs入门(一)</title>
<meta charset="utf-8">
<script data-main="main" src="require.js"></script>
</head>
<body>

</body>
</html>

使用requirejs很简单,只需要在 head 中通过 script 标签引入它即可,其它文件模块都不再使用 script 标签引入。(我的备注:因为 requirejs 遵循 AMD 异步加载的方式,也就是说,当需要某个模块的时候,才会开始异步加载,所以,无需在页面中通过script 标签显示加载。)

细心的同学会发现 script 标签上了多了一个自定义属性:data-main="main",等号右边的 main 指的main.js。当然可以使用任意的名称。这个 main 指主模块或入口模块,好比 c 或 java 的主函数 main。

main.js

1
2
3
4
5
6
7
8
9
require.config({
paths: {
jquery: 'jquery-1.7.2'
}
});

require(['jquery'], function($) {
alert($().jquery);
});

注:如果文件名“jquery-1.7.2.js”改为“jquery.js”就不必配置 paths 参数了。参看 require config 查看更多参数配置;

main.js 的主要作用是加载 jQuery 模块,然后在回调函数中通过$使用它,并输出 jQuery 版本号。

jQuery 是如何让自己被当做一个模块被其它模块来使用的呢?

  1. 首先 jQuery1.7 以后,开始支持 AMD 规范了,既是,通过 RequireJs Define 函数将自己定义成一个了 AMD 模块;jQuery 将自己定为 AMD 模块的源码片段如下,

    1
    2
    3
    4
    // 一直将代码拖至最底部
    if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
    define( "jquery", [], function () { return jQuery; } );
    }

    可见,jQuery 通过 RequireJs Define 方法 将自己定义为了一个 AMD 模块,并返回可被第三方模块调用的jQuery对象。

  2. main.js 为什么使用的$来引用 jQuery 模块?而不是由 #1 返回的jQuery对象?
    是因为 _$_ 与 jQUery 等价,看 jQuery-1.7.2 的源码,拖至最后

    1
    2
    // Expose jQuery to the global object
    window.jQuery = window.$ = jQuery;

    可见,jQuery 对 $jQuery 做了等价处理,调用$也就等价于调用了jQuery对象

执行结果

1
$ grunt connect

打开 http://localhost:8000 我们看到输出了期望的结果,jquery 的版本号

可以看到require.js正确的加载了 main.js 的所有相关的依赖包。

入门例子二: 引用自定义模块(独立)

自定义一个 AMD 模块,selector

测试文件

文件结构

1
2
3
4
5
6
7
8
.
├── index.html
├── js
│   ├── main.js
│   ├── selector.js
├── node_modules
├── package.json
└── require.js

这次,自定义 AMD 模块 Selector.js,然后通过 main.js 加载

selector.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* css选择器,根据2\8原则,这里只实现最常用的三种
* 注:当结果集只有一个元素时将直接返回该元素
* ....
*/
define(function() {

function query(selector,context) {
var s = selector,
doc = document,
regId = /^#[\w\-]+/,
regCls = /^([\w\-]+)?\.([\w\-]+)/,
regTag = /^([\w\*]+)$/,
regNodeAttr = /^([\w\-]+)?\[([\w]+)(=(\w+))?\]/;

var context =
context == undefined ?
document :
typeof context == 'string' ?
doc.getElementById(context.substr(1,context.length)) :
context;

if(regId.test(s)) {
return doc.getElementById(s.substr(1,s.length));
}
// 略...
}

return query;
});

selector.js 的作用就是遍历 DOM,根据相关规则找到 CSS 名称所对应的 DOM 节点;这里我们通过 RequireJs define方法创建selector模块,

  1. define方法的参数为一个匿名函数,表示自身不引用任何其它第三方模块,该匿名函数执行后返回 query 对象,是一个可供其它模块调用的对象( 注意,query 是一个函数的入口地址。)
  2. 模块的名称就是文件名,既selector;其它模块需要引用它的时候,需要指定该名称。

main.js

1
2
3
4
5
6
7
8
require.config({
baseUrl: 'js'
});

require(['selector'], function(query) {
var els = query('.wrapper');
console.log(els)
});

require.config
baseUrl 参数的值为 _js_,表示模块文件被引用的根路径;这里的写法表示是相对路径,相对 require.js 被引用的页面的地址路径,这里是 index.html

require([‘selector’], function(query)
引用selector模块,并调用selectorquery 方法查找 .warpper CSS 节点。

main.js -> selector.js 代码分析
之前看代码,看到这里,有些疑惑了,var els = query('.wrapper'); 调用 selector 模块中的 query 方法,调用的时候只传入了一个参数值 ‘.wrapper’,但是 selector 模块中的 query 方法需要两个参数,selectorcontext, debug selector进去以后,发现 selector 的参数值为 ‘.wrapper’,而 context 的值为 undefined,而后,代码继续处理,将 context 参数值设置为了 document

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!doctype html>
<html>
<head>
<title>requirejs入门(二)</title>
<meta charset="utf-8">
<style type="text/css">
.wrapper {
width: 200px;
height: 200px;
background: gray;
}
</style>
</head>
<body>
<div class="wrapper"></div>
<script data-main="js/main" src="require.js"></script>
</body>
</html>

注意,因为要遍历 DOM,所以,把 script 脚本放到了末尾。

执行结果

1
$ grunt connect

访问 http://localhost:8000

正确加载了相关的 js,

并且在浏览器的控制台中输出了 .wrapper 节点。

完整测试用例下载

入门例子三: 引用自定义模块(依赖)

这篇来写一个具有依赖的事件模块 event。event 提供三个方法 bind、unbind、trigger 来管理 DOM 元素事件。

event 依赖于 cache 模块,cache 模块类似于 jQuery 的 $.data 方法。提供了 set、get、remove 等方法用来管理存放在 DOM 元素上的数据。

示例实现功能:为页面上所有的段落P元素添加一个点击事件,响应函数会弹出 P 元素的 innerHTML。

测试文件

目录结构

1
2
3
4
5
6
7
8
.
├── index.html
├── js
│   ├── cache.js
│   ├── event.js
│   ├── main.js
│   └── selector.js
└── require.js

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!doctype html>
<html>
<head>
<title>requirejs入门(三)</title>
<meta charset="utf-8">
<style type="text/css">
p {
width: 200px;
background: gray;
}
</style>
</head>
<body>
<p>p1</p><p>p2</p><p>p3</p><p>p4</p><p>p5</p>
<script data-main="js/main" src="require.js"></script>
</body>
</html>

cache.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
define(function() {
var idSeed = 0,
cache = {},
id = '_ guid _';

// @private
function guid(el) {
return el[id] || (el[id] = ++idSeed);
}

return {
set: function(el, key, val) {

if (!el) {
throw new Error('setting failed, invalid element');
}

var id = guid(el),
c = cache[id] || (cache[id] = {});
if (key) c[key] = val;

return c;
},
get: function(el, key, create) {
if (!el) {
throw new Error('getting failed, invalid element');
}

var id = guid(el),
elCache = cache[id] || (create && (cache[id] = {}));

if (!elCache) return null;

return key !== undefined ? elCache[key] || null : elCache;
},
// 略去...
};
});

这里返回的供其它模块调用的是由多个键值对(值是 function )构成的 _JS_ 对象,该对象提供了一系列的 setgethas等方法。

event.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
define(['cache'], function(cache) {
var doc = window.document,
w3c = !!doc.addEventListener,
expando = 'snandy' + (''+Math.random()).replace(/\D/g, ''),
triggered,
addListener = w3c ?
function(el, type, fn) { el.addEventListener(type, fn, false); } :
function(el, type, fn) { el.attachEvent('on' + type, fn); },
removeListener = w3c ?
function(el, type, fn) { el.removeEventListener(type, fn, false); } :
function(el, type, fn) { el.detachEvent('on' + type, fn); };

// 略去...

return {
bind : bind,
unbind : unbind,
trigger : trigger
};
});

define 有两个参数,第一个参数表示该模块依赖于 cache 模块,第二个参数是一个方法 function,该方法的参数接收的就是 cache 模块返回的 _js_ 对象。这样,当 _requirejs_ 加载模块的时候,通过 _event_ 会自动将 _cache_ 模块也下载下来

main.js

1
2
3
4
5
6
7
8
9
10
11
12
require.config({
baseUrl: 'js'
});

require(['selector', 'event'], function($, E) {
var els = $('p');
for (var i=0; i<els.length; i++) {
E.bind(els[i], 'click', function() {
alert(this.innerHTML);
});
}
});

baseUrl 中加载 selectorevent 模块,function($, E) 分表表示所引用的 selectorevent 模块的返回值对象。

回调函数中使用选择器$(别名)和事件管理对象E(别名)给页面上的所有P元素添加点击事件。
注意:require的第一个参数数组内的模块名必须和回调函数的形参一一对应。

完整测试用例下载

执行结果

可见,当 event 模块被加载之前,cache模块提前加载了。

当点击 _P_ 元素以后,弹出了 _P_ 元素的 innerHTML

进阶例子一: 体验压缩 r.js

r.js

RequireJS 提供了一个打包压缩工具 r.js 来对模块进行合并压缩。r.js 非常强大,不但可以压缩jscss,甚至可以对整个项目进行打包。

r.js的压缩工具使用UglifyJSClosure Compiler。默认使用UglifyJS( jQuery 也是使用它压缩)。此外r.js需要node.js环境,当然它也可以运行在Java环境中如Rhino

测试文件

目录结构

1
2
3
4
5
6
7
8
9
10
.
├── built.js
├── index.html
├── js
│   ├── cache.js
│   ├── event.js
│   ├── main.js
│   └── selector.js
├── r.js
└── require.js

http://requirejs.org/docs/download.html#rjs 下载 r.js,并放置到项目根路径中。
完全使用 入门例子三-引用自定义模块-依赖 的测试文件,这里仅仅是体验一下通过r.js是如何进行压缩的,唯一需要修改的是 index.html

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!doctype html>
<html>
<head>
<title>requirejs进阶(一)</title>
<meta charset="utf-8"/>
<style type="text/css">
p {
background: #999;
width: 200px;
}
</style>
</head>
<body>
<p>p1</p><p>p2</p><p>p3</p><p>p4</p><p>p5</p>
<script data-main="built" src="require.js"></script>
</body>
</html>

注意,data-main 改为了 “built”,上一篇的是“main”。因为我们将使用 r.js 把 js 目录下的 cache.js,event.js,selector.js,main.js 合并压缩成了一个文件 buit.js

完整测试用例下载

压缩测试

完整压缩
1
2
3
4
5
6
7
8
9
10
11
$ node r.js -o baseUrl=js name=main out=built.js

Tracing dependencies for: main
Uglifying file: /Users/mac/workspace/javascript/requirejs/r4/built.js

/Users/mac/workspace/javascript/requirejs/r4/built.js
\----------------
/Users/mac/workspace/javascript/requirejs/r4/js/selector.js
/Users/mac/workspace/javascript/requirejs/r4/js/cache.js
/Users/mac/workspace/javascript/requirejs/r4/js/event.js
/Users/mac/workspace/javascript/requirejs/r4/js/main.js

从日志记录中可以看到 cache.js,event.js,selector.js,main.js 压缩合并到了一个文件 built.js 中了且所有的代码被压缩成一行,且方法名参数名都被转义了。

执行过程中,r.js会自动找出main.js所依赖的模块,然后对它们进行压缩。

合并排除

excludeShallow

在合并的时候将某个文件排除在外,

1
$ node r.js -o baseUrl=js name=main out=built.js excludeShallow=selector

可以看到,selector.js 不再被合并,

1
2
3
4
5
6
7
8
Tracing dependencies for: main
Uglifying file: /Users/mac/workspace/javascript/requirejs/r4/built.js

/Users/mac/workspace/javascript/requirejs/r4/built.js
\----------------
/Users/mac/workspace/javascript/requirejs/r4/js/cache.js
/Users/mac/workspace/javascript/requirejs/r4/js/event.js
/Users/mac/workspace/javascript/requirejs/r4/js/main.js

selector.js 会被单独通过网络进行加载

备注:这里selector.js不仅不会被合并同样不会被压缩,完整被保留下来。

压缩优化

optimize

参数值:[none/uglify/closure];none: 表示不压缩,uglify/closure: 使用 uglify 或者 closure 进行压缩;
Default Value: uglify

我们来试验不做内容压缩,

1
$ node r.js -o baseUrl=js name=main out=built.js optimize=none

再次打开 built.js,内容上并没有做压缩了..

压缩释疑

压缩过程中,之前我有个疑问,就是,模块是以 文件名 来引用的,比如 selector.js,默认 main.js 是按照文件名 selector 来进行模块引用的(参考入门例子二: 引用自定义模块(独立)),那么问题来了,现在压缩了,就一个文件了 built.jsselector 模块的文件名( selector.js )没了,那么现在其它模块如何引用 selector 模块?r.js 在压缩的时候,非常的聪明,它会判断,并设置 _ID_,在定义的时候,标识模块名,所以,压缩后的 selector.js 是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
define('selector',[],function() {

function query(selector,context) {
var s = selector,
doc = document,
regId = /^#[\w\-]+/,
regCls = /^([\w\-]+)?\.([\w\-]+)/,
regTag = /^([\w\*]+)$/,
regNodeAttr = /^([\w\-]+)?\[([\w]+)(=(\w+))?\]/;
.....
.....
}
})

自动的将文件名 selector 作为 define 函数的 _id_。

进阶例子二: 压缩之 paths 和 css

完整测试用例下载

paths

域外资源

入门例子一: 引用 jQuery 模块中,我们使用过paths,那个时候,被引用的 jQuery 模块是本地文件,我们称之为在同一个域中,对同一个域中的 _js_ 文件,我们可以进行压缩与合并;但是,如果我们引用的脚本不在同一个域中呢?比如,我们引用的是某个 CDN 上的 jQuery 模块,比如 http://code.jquery.com/jquery-1.7.2.min.js ,那么这个时候,被引用的 jQuery 模块就不是本地文件,自然也就 _不能_ 对其进行 合并压缩,强行压缩会导致错误,必须排除。

压缩测试

还是延用进阶例子一: 体验压缩 r.js中的例子,只是这次,jquery 模块是通过远程加载的,

修改 main.js

  1. 添加 paths 属性,表示 jqueryCDN 上获取
  2. 让其使用 jquery 模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require.config({
baseUrl: 'js',
paths:{
'jquery': 'http://code.jquery.com/jquery-1.7.2.min.js'
}
});

require(['jquery', 'selector', 'event'], function($, E) {
var els = $('p');
for (var i=0; i<els.length; i++) {
E.bind(els[i], 'click', function() {
alert(this.innerHTML);
});
}
});

执行压缩

不做排除

1
2
3
4
5
6
7
8
9
10
11
12
$ node r.js -o baseUrl=js name=main out=built.js

Tracing dependencies for: main
Error: ENOENT: no such file or directory, open '/Users/mac/workspace/javascript/requirejs/r4/js/jquery.js'
In module tree:
main

Error: ENOENT: no such file or directory, open '/Users/mac/workspace/javascript/requirejs/r4/js/jquery.js'
In module tree:
main

at Error (native)

报错,试图压缩的时候,却不能从本地加载 jquery 模块;(备注,说明,即便是加上了 paths 属性,RequireJS 依然是从本地优先加载。)

排除域外资源

那么也就是说,必须把 paths.jquery 在压缩过程中排除

  1. 使用paths.jquery=empth:(注意,后面有个:号)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $ node r.js -o baseUrl=js name=main out=built.js paths.jquery=empty:

    Tracing dependencies for: main
    Cannot optimize network URL, skipping: empty:.js
    Uglifying file: /Users/mac/workspace/javascript/requirejs/r4/built.js

    /Users/mac/workspace/javascript/requirejs/r4/built.js
    ----------------
    /Users/mac/workspace/javascript/requirejs/r4/js/selector.js
    /Users/mac/workspace/javascript/requirejs/r4/js/cache.js
    /Users/mac/workspace/javascript/requirejs/r4/js/event.js
    /Users/mac/workspace/javascript/requirejs/r4/js/main.js

    执行成功,成功将 jquery 模块从合并压缩过程中移除。

    可以看到,jquery 模块会从 CDN 上单独的获取;

  2. 使用excludeShallow
    进阶例子一: 体验压缩 r.js 中通过 excludeShallow 也进行过排除,这里我们试试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $ node r.js -o baseUrl=js name=main out=built.js excludeShallow=jquery

    Tracing dependencies for: main
    Error: ENOENT: no such file or directory, open '/Users/mac/workspace/javascript/requirejs/r4/js/jquery.js'
    In module tree:
    main

    Error: ENOENT: no such file or directory, open '/Users/mac/workspace/javascript/requirejs/r4/js/jquery.js'
    In module tree:
    main

    at Error (native)

    仍然报错,看来 RequireJS 首先会强行对所引用的资源进行本地资源检查,也就是说但凡是通过 require 方法引用的资源,必须保存在本地,否则报错(当然可以通过 paths.jquery=empty: 的方式排除);

总结,excludeShallow看来是对本地已经存在的资源在合并压缩阶段进行排除,paths.jquery=empth:是对本地不存在的网络资源在合并压缩阶段进行排除。

css 压缩

文件结构

目录结构

1
2
3
4
5
6
7
8
9
10
├── built.js
├── css
│   ├── built.css
│   ├── form.css
│   ├── grid.css
│   ├── main.css
│   └── nav.css
├── index.html
├── r.js
└── require.js

main.css

1
2
3
@import url("nav.css");
@import url("grid.css");
@import url("form.css");

main.css 是合并主文件,可以视为配置文件;需要合并压缩的文件通过 @import 命令引入

执行压缩

根据 main.css 中的配置,将 main.cssnav.cssgrid.css 以及 form.css 合并成一个文件 built.css

1
2
3
4
5
6
7
8
$ node r.js -o cssIn=css/main.css out=css/built.css

/Users/mac/workspace/javascript/requirejs/r5/css/built.css
\----------------
/Users/mac/workspace/javascript/requirejs/r5/css/nav.css
/Users/mac/workspace/javascript/requirejs/r5/css/grid.css
/Users/mac/workspace/javascript/requirejs/r5/css/form.css
/Users/mac/workspace/javascript/requirejs/r5/css/main.css

合并后的 built.css

1
2
3
4
5
6
7
8
9
10
11
.nav {
margin: 10px;
background: #999;
}
.grid {
border: 1px solid #888;
}
.form {
padding: 5px;
color: blue;
}

只是合并,并没有压缩

进阶例子三: 通过配置文件 build.js 进行压缩

通过 build.js 可配置的方式根据不同的页面的需求,生成不同的打包压缩的输出文件。

文件结构

完整测试用例下载

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
├── build.js
├── css
├── img
├── js
│   ├── cache.js
│   ├── event.js
│   ├── page1.js
│   ├── page2.js
│   └── selector.js
├── page1.html
├── page2.html
├── r.js
└── require.js

page1.js
page1.html 通过 requirejs 引用 page1.jspage1.js 依赖于 eventselector 模块

1
2
3
define(['event', 'selector'], function(E, S) {
// todo with E and $
});

page1.html

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html>
<head>
<title>requirejs进阶(三)</title>
<meta charset="utf-8"/>
<script data-main="js/page1" src="require.js"></script>
</head>
<body>
</body>
</html>

page2.js
page2.html 通过 requirejs 引用 page2.jspage2.js 依赖于 eventselectorjquery 模块,

1
2
3
4
5
6
7
8
9
10
require.config({
baseUrl: 'js',
paths: {
'jquery': 'http://code.jquery.com/jquery-1.7.2.min.js'
}
});

require(['jquery', 'event', 'selector'], function($, E, S) {
alert($);
});

注意,jquery 模块不是引用的本地文件,合并压缩的时候需要将其排除,参考排除域外资源

page2.html

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html>
<head>
<title>requirejs进阶(三)</title>
<meta charset="utf-8"/>
<script data-main="js/page2" src="require.js"></script>
</head>
<body>
</body>
</html>

build.js
合并压缩的配置文件,build.js configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
({
appDir: "./",
baseUrl: "js",
dir: "dist",
paths: {
jquery: 'empty:'
},
modules: [
{
name: "page1"
},
{
name: "page2"
}
]
})

  • appDir
    应用程序的路径,表示与 build.js 在同级目录中
  • baseUrl
    模块引用的路径,表示模块文件在 appDir + baseUrl 中,既 ./js
  • dir
    压缩合并后输出路径,./dist
  • paths
    jquery: ‘empty’, 参考排除域外资源
  • modules
    name 表示合并模块的入口文件( require 脚本文件 ),通过文件名分别匹配并找到 page1.jspage2.js(因为没有设置 _id_),为什么通过文件名去匹配相关 require 模块,参考压缩释疑;然后,将 page1.jspage2.js 所依赖的模块进行合并压缩,并且直接使用 page1.jspage2.js 作为合并压缩后的文件。

执行压缩

执行压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ node r.js -o build.js

Tracing dependencies for: page1

Tracing dependencies for: page2
Cannot optimize network URL, skipping: empty:.js
Uglifying file: /Users/mac/workspace/javascript/requirejs/r6/dist/build.js
Uglifying file: /Users/mac/workspace/javascript/requirejs/r6/dist/js/cache.js
Uglifying file: /Users/mac/workspace/javascript/requirejs/r6/dist/js/event.js
Uglifying file: /Users/mac/workspace/javascript/requirejs/r6/dist/js/page1.js
Uglifying file: /Users/mac/workspace/javascript/requirejs/r6/dist/js/page2.js
Uglifying file: /Users/mac/workspace/javascript/requirejs/r6/dist/js/selector.js
Uglifying file: /Users/mac/workspace/javascript/requirejs/r6/dist/r.js
Uglifying file: /Users/mac/workspace/javascript/requirejs/r6/dist/require.js

可见,分别 tracing require 模块 page1page2,然后对其所引用的模块进行合并压缩,并输出为两个合并压缩后的文件 page1.jspage2.js

dist/ 目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dist
├── build.js
├── build.txt
├── css
├── img
├── js
│   ├── cache.js
│   ├── event.js
│   ├── page1.js
│   ├── page2.js
│   └── selector.js
├── page1.html
├── page2.html
├── r.js
└── require.js

可见,首先拷贝了整个 appDir 中的所有文件,并将合并后的文件分别放入 page1.jspage2.js 中,供不同的页面 page1.html 以及 page2.html 使用。

page1.js

1
2
3
$ cat dist/js/page1.js 
define("cache",[],function(){function d(b){return b[c]||(b[c]=++a)}var a=0,b={},c="_ guid _";return{set:function(a,c,e){if(!a)throw new Error("setting failed, invalid element");var f=d(a),g=b[f]||(b[f]={});return c&&(g[c]=e),g }
...... } } ) ......

page2.js

1
2
cat dist/js/page2.js 
define("cache",[],function(){function d(b){return b[c]||(b[c]=++a)}var a=0,b={},c="_ guid _";return{set:function(a,c,e){if(!a)throw new Error("setting failed, invalid element");var f=d(a),g=b[f]||(b[f]={});return c&&(g[c]=e),g},get:function(a,c,e){if(!a) ...... } } } )

结果验证

分别访问 page1.htmlpage2.html

page1.html

可见,除 require.js 以外,只会加载 page1.js

page2.html

可见,除 require.js, 只会加载 page2.js 以及从 CDN 中加载的 jquery 模块。

我的思考

虽然刚全面接触 javascript 时间不长,但是可以明显的感到 javascript 的模块化进程

前端模块化(既浏览器端模块化)

  1. 通过页面使用 script 标签的方式
    这种方式往往需要通过人工的方式,必须保证 javascript 文件引入顺序的正确性,因为一个独立的 javascript 文件就是一个模块;在这个领域有所成就的,比较突出的,就是使用 bowergrunt-wiredep来构建这种依赖关系,bower 通过 bower.json 来描述模块间的依赖关系,grunt-wiredep 通过 bower.json 中所描述的依赖关系按照正确的顺序来正确的加载模块。而这种做法的弊端也非常的明显,模块和模块之间的依赖关系是需要通过独立的第三方配置来进行描述的,而不是由模块自身来决定的,比如 Java 的 import;所以,这个方式过于原始,javascript 本身并没有实现模块化,还是零散的,没有达到一门语言所应具备的模块化标准。

  2. 通过 AMD 规范,既 RequireJS
    由于 #1 的不足,所以产生了 AMD 标准,也才有了 AMD 的一个实现 RequireJS; RequireJS 真正实现了 javascript 构建前端代码的模块化方案,就像实现了 javaimport 功能;这样呢,模块化就是由 javascript 模块自己来决定了,而不像 #1 那样,交给一个独立的第三方配置来管理( bower.json );从此 javascript 就前端应用而言,有了自己的模块化标准方式,也就正式成为一门模块化的语言了。

服务器端模块化

Ok,自从 Node 的出现,利用谷歌的 Chrome ES 引擎javascript 开始涉足服务器端的编程了,那么既然要作为一个标准的服务器端的开发语言,代码模块化是必须的,于是,有了服务器端 CommonJS 模块化标准,通过require('jquery')的方式引入模块;与 RequireJS 不同的是 CommonJS 加载模块是同步加载的(因为所有的文件都在服务器端本地,同步加载是最高效的),RequireJS 因为是通过浏览器进行加载,所以它的加载方式势必是异步的。

Ok,到目前为止,Javascript 前端和后端分别有了自己的模块化的方式,RequireJSCommonJS 模块化方式,嗯,的确,Javascript 够折腾的,连一个模块化的方式也搞得这么麻烦,更要命的是,因为前端和后端的模块化方式不同,于是呢,前后端的构建方式也就不同了,如果你使用 javascript 来构建你的前后端,那么必然会对应两套构建方式,一套基于 RequireJS 的,一套基于 CommonJS 的,会很麻烦… 于是,在这个场景下,webpack 诞生了,它的作用就是统一 javascript 的前后端构建方式和流程,使得你用一套方案来解决构建的问题,这才是 webpack 的真正精华和意义所在。(遗憾的是,到目前为止,本人在网上没有看到任何对此分析准确的文章,原创精华,转载需注明出处!)

_插曲_

browserify

虽然前后端有了不同的模块化方案(一个同步、一个异步),但是并不意味着,后端不能开发前端的代码,不仅可以开发,而且可以通过后端 CommonJS 的模块化方式来写前端的代码,这就出现问题了,通过 CommonJS 模块化标准写的前端代码,虽然在后端可以执行,但是真放到浏览器端就会出问题了,不能通过 CommonJS 模块化的方式同步加载模块,报错;于是,才有了 browserify,它的作用,就是将使用 CommonJS 模块化标准的代码翻译成前端浏览器可以识别和适配的代码,其实方式也非常的简单和裸感,就是直接将所有依赖包直接合并到一个文件中让浏览器一次性的加载… (呵呵,等价于绕开前端浏览器的异步加载模块流程( AMD ),呵呵,写到这里,感觉真的好玩,javascript 不仅使用一个合并文件来提升性能,没想到这里,也通过一个文件的方式使得前后端不同的模块化代码可以彼此适配;注明:版权所有,转载需注明出处!)

API

从这章开始,梳理 RequireJS 的底层逻辑和接口;从官网的 API 文档入手;
RequireJS API 中文
RequireJS API En
官网上只是给出了程序的片段来说明概念,很不全面,也不具备直接的操作性,为了更好的理解,我将官网上的概念转换为具体的项目,便于直接操作,使用 Webstorm 来构建项目,通过 bower 来下载和管理第三方依赖,项目结构一览

源码下载 my.zip

define

意义

通过define定义好模块 _A_ 以后,模块 _B_ 在引用 _A_ 的时候,那么 _A_ 的作用域就仅限于 _B_ 中,而不会像以前那样,_A_ 的作用范围会暴露给全局,进而当代码量不断扩大以后,导致项目失控;-> 进而让 javascript 成为真正意义上的 模块化语言

简单键值对

相关脚本
shirt.js

1
2
3
4
define({
color: "black",
size: "unisize"
});

直接使用键值对,这样,在 page1.js 获得 shirt 模块以后,得到的,就是一个可以直接调用的键值对。

page1.js

1
2
3
4
5
6
require.config({});

require(['shirt'], function(shirt) {

alert(shirt.color+";"+shirt.size);
});

注意,page1.jsshirt.js 在同级目录中,所以 require.config 不用配置 baseUrlpaths 等。

page1.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>define with key-value</title>
</head>
<body>
<script data-main="app/page1/page1.js" src="bower_components/requirejs/require.js"></script>
</body>
</html>

执行结果
直接使用 webstore 运行,打开 page1.html

补充
返回的键值对,对值没有特殊的限制,其中的值也可以是一个 function,可以参考 存在依赖的函数定义cart.js 的定义方式。

函数定义

相关脚本
shirt.js

1
2
3
4
5
6
define(function(){
return {
color: "black",
size: "unisize"
}
});

通过函数返回一个键值对,这个做法与简单键值对是等价的,唯一的区别是,前者是直接将define的回调函数的返回值设置为使用键值对,而这里使用的是 function 的返回值,是等价的。

1
2
3
4
5
6
require.config({});

require(['shirt'], function(shirt) {

alert(shirt.color+";"+shirt.size);
});

page2.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>define with key-value</title>
</head>
<body>
<script data-main="app/page1/page2.js" src="bower_components/requirejs/require.js"></script>
</body>
</html>

自定义模块名

1
2
3
4
5
6
7
8
9
10
11
12
define('selector',[],function() {
function query(selector,context) {
var s = selector,
doc = document,
regId = /^#[\w\-]+/,
regCls = /^([\w\-]+)?\.([\w\-]+)/,
regTag = /^([\w\*]+)$/,
regNodeAttr = /^([\w\-]+)?\[([\w]+)(=(\w+))?\]/;
.....
.....
}
})

这里,第一个参数 ‘selector’ 指定了模块的名称,那么另外模块加载它的时候,需要根据指定的模块名既 ‘selector’ 来引用;这个方式通常用在 requirejs 将多个模块文件合并成一个文件以后的引用方式,具体参考进阶例子一: 体验压缩 r.js部分;不过,在开发的时候,requirejs 是不推荐的,推荐是一个文件一个独立的模块。

存在依赖的函数定义

shirt.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//my/title.js now has some dependencies, a cart and inventory
//module in the same directory as title.js
define(["./cart", "./inventory"], function(cart, inventory) {
//return an object to define the "my/shirt" module.
return {
color: "blue",
size: "large",
addToCart: function() {
inventory.decrement(this);
cart.add(this);
alert("add to cart success, the attribute of this shirt is "+ this.color +" color and "+ this.size +" size");
}
}
}
);

shirt 模块依赖于 cartinventory 模块;其回调方法 function(cart, inventory) 中的 cartinventory 分别是 cartinventory 模块的返回值。

cart.js

1
2
3
4
5
6
7
8
define(function(){

return {
add : function(shirt){
console.log("add shirt: "+ shirt.color + "; "+ shirt.size );
}
}
});

通过 function 返回键值对,只是这次呢,值也是一个 function

inventory.js

1
2
3
4
5
6
7
8
define(function(){

return {
decrement : function(shirt){
console.log("decrement shirt: "+ shirt.color + "; "+ shirt.size);
}
}
});

page3.js

1
2
3
4
5
6
require.config({});

require(['shirt'], function(shirt) {

shirt.addToCart();
});

page3.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>define by dependencies</title>
</head>
<body>
<script data-main="app/page3/page3.js" src="bower_components/requirejs/require.js"></script>
</body>
</html>

执行结果
shirt 模块的输出

控制台也打印出了 cartinventory 模块的输出

存在依赖的函数定义2 - 使用 require()

_背景_
假设当你有这么一堆的模块需要引用的时候,这么使用非常的笨拙;

1
2
3
4
5
6
define(
['dep1', 'dep2', 'dep3', 'dep4', 'dep5', 'dep6', 'dep7', 'dep8'],
function(dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8){
...
}
);

于是呢,我们可以在 define 函数中使用 require 函数,像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
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'),
dep8 = require('dep8');
...
}
});

重构 shirt 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
define(function(require) {
//return an object to define the "my/shirt" module.
return {
color: "blue",
size: "large",
addToCart: function() {
require("./inventory").decrement(this);
require("./cart").add(this);
alert("add to cart success, the attribute of this shirt is "+ this.color +" color and "+ this.size +" size");
}
}
}
);

这样,我们可以使用 require 方法,在需要的时候,再引入模块,具有更大的灵活性。

定义一个函数模块

定义一个返回值是函数的模块,

title.js

1
2
3
4
5
6
7
8
9
10
11
//my/title.js now has some dependencies, a cart and inventory
//module in the same directory as title.js
define(["./cart", "./inventory"], function(cart, inventory) {
//return a function to define "foo/title".
//It gets or sets the window title.
return function(title) {
return title ? (window.title = title) :
inventory.storeName + ' ' + cart.name;
}
}
);

这里,返回的是一个函数,是一个方法的入口地址;好玩,那么在其他模块引用它的时候,可以将它当做一个方法来使用。

cart.js

1
2
3
4
5
define(function(){
return {
name : "yellow cart"
}
});

inventory.js

1
2
3
4
5
define(function(){
return {
storeName : "yellow store"
}
});

page5.js

1
2
3
4
5
require.config({});

require(['title'], function(title) {
alert(title());
});

可以看到,这里将 title 模块当做一个方法在调用。

require

require 作为模块 - 作用

开始学习 requirejs 的时候一直有这么一个疑问,就是,define模块不也可以调用require吗?那么为什么还要单独定义这么一个require模块,而不是直接将require定义为一个内部方法即可?后面渐渐的明白了,类似于 Java 一样,程序要运行起来必须要有一个main方法,表示整个程序的入口,require模块其实就是担当了这么一个角色,作为main方法,是整个 javascript 模块化程序 的入口。所以,一个前端页面,只对应一个require模块,但可以对应多个define模块。

http://www.tuicool.com/articles/ENbI7j3
https://github.com/tnajdek/angular-requirejs-seed

require 作为方法 - 动态加载模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
define(function ( require ) {

var isReady = false, foobar;

require(['foo', 'bar'], function (foo, bar) {
isReady = true;
foobar = foo() + bar();
});

return {
isReady: isReady,
foobar: foobar
};

});

可以通过设置这样一个标志位,来检查所需要的模块 foobar 是否已经加载完成。

require 作为方法 - JSONP 服务依赖

JSONP是在javascript中服务调用的一种方式。它仅需简单地通过一个script标签发起HTTP GET请求,是实现跨域服务调用一种公认手段。
为了在RequireJS中使用JSON服务,须要将callback参数的值指定为”define”。这意味着你可将获取到的JSONP URL的值看成是一个模块定义。
下面是一个调用JSONP API端点的示例。该示例中,JSONP的callback参数为”callback”,因此”callback=define”告诉API将JSON响应包裹到一个”define()”中:

1
2
3
4
5
6
7
require(["http://example.com/api/data.json?callback=define"],
function (data) {
//The data object will be the API response for the
//JSONP data call.
console.log(data);
}
);

TODO 有待验证

错误处理

局部捕获

可以在 require 方法中传入第三个参数( function(err) ),来捕获错误。

1
2
3
4
5
6
7
8
9
require(
[ "backbone" ],
function ( Backbone ) {
return Backbone.View.extend({ /* ... */ });
},
function (err) {
// ...
}
);

全局捕获

通常的错误都是404(未找到)错误,网络超时或加载的脚本含有错误。RequireJS 有些工具来处理它们:require 特定的错误回调(errback),一个“paths”数组配置,以及一个全局的requirejs.onError事件。
传入 errback 及 requirejs.onError 中的 error object 通常包含两个定制的属性:
requireType: 含有类别信息的字串值,如“timeout”,“nodefine”, “scripterror”
requireModules: 超时的模块名/URL数组。

1
2
3
requirejs.onError = function (err) {
// ...
};

当局部捕获失败的时候,会在这里被全局捕获。

配置 Config

require 方法本身也是一个对象,它带有一个 config 方法,用来配置 require.js 运行参数。config 方法接受一个对象作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require.config({
baseUrl: 'js',
paths: {
jquery: [
'//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
'lib/jquery'
]
},
shim: {
angular : { exports : 'angular'}
}
});

require(['jquery', 'angular'], function($, angular) {
...
});

baseUrl

设置加载模块的根路径,注意,是从 require.js 所在的当前路径开始设置的。

paths

1
2
3
4
5
6
paths: {
jquery: [
'//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
'lib/jquery'
]
},

会首先从 //cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js 下载 jquery 模块,若不成功,则从本地加载,从路径 baseUrl + paths.jquery 既是加载 js/lib/jquery.js(注意,如果是从本地加载,后缀名 .js 需要省略)。

shim

有些库不是 AMD 兼容的,这时就需要指定 shim 属性的值。shim 可以理解成“垫片”,用来帮助 require.js 加载非 AMD 规范的库。

官网例子
详细配置参考,http://www.requirejs.org/docs/api.html#config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
requirejs.config({
shim: {
'jquery.colorize': {
deps: ['jquery'],
exports: 'jQuery.fn.colorize'
},
'jquery.scroll': {
deps: ['jquery'],
exports: 'jQuery.fn.scroll'
},
'backbone.layoutmanager': {
deps: ['backbone']
exports: 'Backbone.LayoutManager'
}
}
});

deps:
导入当前模块,必须先导入其依赖模块。

exports:
暴露非 AMD 模块中的某个对象,可以看到,我们可以只暴露 jquery 中的某个对象,比如 jQuery.fn.colorize 或者 jQuery.fn.scroll

两点注意事项,

  1. shim 配置仅设置了代码的依赖关系,想要实际加载 shim 指定的或涉及的模块,仍然需要一个常规的 require/define 调用。设置 shim 本身不会触发代码的加载。我的理解,这里说的很清楚,定了了依赖关系,需要使用该模块的时候,依然需要使用 require 的方式;不过,还有一点这里没说,那就是,将其暴露成 AMD 模块。
  2. Only use other “shim” modules as dependencies for shimmed scripts, or AMD libraries that have no dependencies and call define() after they also create a global (like jQuery or lodash). Otherwise, if you use an AMD module as a dependency for a shim config module, after a build, that AMD module may not be evaluated until after the shimmed code in the build executes, and an error will occur. The ultimate fix is to upgrade all the shimmed code to have optional AMD define() calls.
    我的解读:不要将有依赖的 AMD 模块配置为 shim 模块使用,否则会出错。

这里使用 RequireJS 加载 AngularJS 作为例子,来看看 shim 如何使用

config.js

1
2
3
4
5
6
7
8
9
10
11
requirejs.config({
paths: {
angular: '../../bower_components/angular/angular.min'
},
shim: {
angular : { exports : 'angular'}
}
});

// 直接引用 app 模块,并不设置任何的回调函数
requirejs(['app']);

angular 通过 shim 配置为模块,AngularJS 有一个全局对象,既 angular ( 和 jQueryjquery 全局对象如出一辙 ),通过 shim 我们达到了两个目的,

  1. AngularJSangluar 对象 通过 shim exports 暴露成 requirejs 兼容的模块。
  2. 限定了 angular 对象的作用范围,它的作用范围被限于调用它的模块中了。

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
define(['angular'], function(angular) {

angular.module('myApp', [])
.controller('MyController', ['$scope', function ($scope) {
$scope.name = 'Change the name';
}]);

// 将 document.body 作为 Angular 应用 (既是 Angular App);
angular.element(document.body).ready(function() {
angular.bootstrap(document.body, ['myApp']);
});

// 原来 RequireJS 甚至可以不返回任何东西(void)
});

正如上述 #2 所描述的那样,对象 angular 的作用范围仅在 app 模块中有效了。

page6.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Shim with Angular</title>
<script data-main="app/page6/config.js" src="bower_components/requirejs/require.js"></script>
</head>
<body>
<div ng-controller="MyController">
<input type="text" ng-model="name" />
<span>{{name}}</span>
</div>
</body>
</html>

启动 page6.html,可以看到 angular 生效了

一个更完美的例子angularjs 开发环境篇之 angular-requirejs-seed

合并压缩 r.js

当 javascript 模块化以后,势必产生很多的小文件,比如 jquery,大概有数十个小文件吧;这么多的小文件对于其他服务器端的程序来说,没问题,但是对于浏览器而言,一次性加载这么多的小文件势必造成浏览器的加载时间过长;于是,requirejs 诞生了 r.js,用来将小文件进行合并和压缩,具体参考进阶例子一: 体验压缩 r.js部分。