RequireJS入门与进阶

RequireJSJames Burke创建,他也是AMD规范的创始人。

 

RequireJS会让你以不同于往常的方式去写JavaScript。你将不再使用script标签在HTML中引入JS文件,以及不用通过script标签顺序去管理依赖关系。

 

当然也不会有阻塞(blocking)的情况发生。好,以一个简单示例开始。

 

新建一个目录,结构如下

 

 

目录r1下有index.htmljquery-1.7.2.jsmain.jsrequire.jsrequire.jsjquery-1.7.2.js去各自官网下载即可。

 

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标签引入它(实际上除了require.js,其它文件模块都不再使用script标签引入)。

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

});

 

main.js中就两个函数调用require.configrequire

 

require.config用来配置一些参数,它将影响到requirejs库的一些行为。

require.config的参数是一个JS对象,常用的配置baseUrlpaths等。

 

这里配置了paths参数,使用模块名“jquery”,其实际文件路径jquery-1.7.2.js(后缀.js可以省略)。

 

我们知道jQuery1.7后开始支持AMD规范,即如果jQuery作为一个AMD模块运行时,它的模块名是“jquery”。注意“jquery”是固定的,不能写“jQuery”或其它。

 

注:如果文件名“jquery-1.7.2.js”改为“jquery.js”就不必配置paths参数了。

 

jQuery中的支持AMD代码如下

?

1

2

3

if(typeofdefine ==="function"&& define.amd && define.amd.jQuery ) {

    define("jquery", [],function() {returnjQuery; } );

}

 

我们知道jQuery最终向外暴露的是全局的jQuery和 $。如下

?

1

2

// Expose jQuery to the global object

window.jQuery = window.$ = jQuery;

 

如果将jQuery应用在模块化开发时,其实可以不使用全局的,即可以不暴露出来。需要用到jQuery时使用require函数即可,

 

这里require函数的第一个参数是数组,数组中存放的是模块名(字符串类型),数组中的模块与回调函数的参数一一对应。这里的例子则只有一个模块“jquery”。

 

把目录r1放到apache或其它web服务器上,访问index.html

 

网络请求如下

 

 

我们看到除了require.jsmain.jsjquery-1.7.2.js也请求下来了。而它们正是通过requirejs请求的。

 

页面上会弹出jQuery的版本

 

 

这是一个很简单的示例,使用requirejs动态加载jquery。使用到了以下知识点

1data-main属性

2require.config方法

3require函数

上一篇是把整个jQuery库作为一个模块。这篇来写一个自己的模块:选择器。

为演示方便这里仅实现常用的三种选择器idclassNameattributeRequireJS使用define来定义模块。

 

新建目录结构如下

 

这次新建了一个子目录js,把main.jsselctor.js放入其中,require.js仍然和index.html在同一级目录。

 

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>

        <divclass="wrapper"></div>

        <script data-main="js/main"src="require.js"></script>

    </body>

</html>

  

这次把script标签放到了div的后面,因为要用选择器去获取页面dom元素,而这要等到dom ready后。

因为把main.js放到js目录中,这里data-main的值须改为“js/main”。

 

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

define(function() {

  

    functionquery(selector,context) {

        vars = selector,

            doc = document,

            regId = /^#[\w\-]+/,

            regCls = /^([\w\-]+)?\.([\w\-]+)/,

            regTag = /^([\w\*]+)$/,

            regNodeAttr = /^([\w\-]+)?\[([\w]+)(=(\w+))?\]/;

          

        varcontext = 

                context == undefined ?

                document :

                typeofcontext =='string'?

                doc.getElementById(context.substr(1,context.length)) :

                context;

                  

        if(regId.test(s)) {

            returndoc.getElementById(s.substr(1,s.length));

        }

        // ...

    }

      

    returnquery;

});

 

define的参数为一个匿名函数,该匿名函数执行后返回queryquery为函数类型。query就是选择器的实现函数。

 

main.js 如下

?

1

2

3

4

5

6

7

8

require.config({

    baseUrl:'js'

});

  

require(['selector'],function(query) {

    varels = query('.wrapper');

    console.log(els)

});

  

require.config方法执行配置了baseUrl为“js”,baseUrl指的模块文件的根目录,可以是绝对路径或相对路径。这里用的是相对路径。相对路径指引入require.js的页面为参考点,这里是index.html

 

把目录r2放到apache或其它web服务器上,访问index.html

 

网络请求如下

 

 

main.jsselector.js都请求下来了。

selector.js下载后使用query获取页面class为“.wrapper”的元素,控制台输出了该元素。如下

 

 

总结:

1、使用baseUrl来配置模块根目录,baseUrl可以是绝对路径也可以是相对路径。

2、使用define定义一个函数类型模块,RequireJS的模块可以是JS对象,函数或其它任何类型(CommonJS/SeaJS则只能是JS对象)。

 

这篇来写一个具有依赖的事件模块eventevent提供三个方法bindunbindtrigger来管理DOM元素事件。

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

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

 

创建的目录如下

 

为了获取元素,用到了上一篇写的selector.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

define(function() {

    varidSeed = 0,

        cache = {},

        id ='_ guid _';

  

    // @private

    functionguid(el) {

        returnel[id] || (el[id] = ++idSeed);

    }

  

    return{

        set:function(el, key, val) {

  

            if(!el) {

                thrownewError('setting failed, invalid element');

            }

  

            varid = guid(el),

                c = cache[id] || (cache[id] = {});

            if(key) c[key] = val;

  

            returnc;

        },

  

        // 略去...

    };

});

cache模块的写法没啥特殊的,与selector不同的是返回的是一个JS对象。

 

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) {

    vardoc = 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

    };

});

  

event依赖于cache,定义时第一个参数数组中放入“cache”即可。第二个参数是为函数类型,它的参数就是cache模块对象。
这样定义后,当require事件模块时,requirejs会自动将event依赖的cache.js也下载下来。

 

main.js 如下

?

1

2

3

4

5

6

7

8

9

10

11

12

require.config({

    baseUrl:'js'

});

  

require(['selector','event'],function($, E) {

    varels = $('p');

    for(vari=0; i<els.length; i++) {

        E.bind(els[i],'click',function() {

            alert(this.innerHTML);

        });

    }

});

  

依然先配置了下模块的根目录js,然后使用require获取selectorevent模块。
回调函数中使用选择器$(别名)和事件管理对象E(别名)给页面上的所有P元素添加点击事件。
注意:require的第一个参数数组内的模块名必须和回调函数的形参一一对应。

 

把目录r3放到apache或其它web服务器上,访问index.html。网络请求如下

 

 

我们看到当selector.jsevent.js下载后,event.js依赖的cache.js也被自动下载了。这时点击页面上各个P元素,会弹出对应的innerHTML。如下

 

 

总结:
当一个模块依赖(a)于另一个模块(b)时,定义该模块时的第一个参数为数组,数组中的模块名(字符串类型)就是它所依赖的模块。
当有多个依赖模时,须注意回调函数的形参顺序得和数组元素一一对应。此时requirejs会自动识别依赖,且把它们都下载下来后再进行回调。

 

 

就在前天晚上RequireJS发布了一个大版本,直接从version1.0.8升级到了2.0。随后的几小时James Burke又迅速的将版本调整为2.0.1,当然其配套的打包压缩工具r.js也同时升级到了2.0.1。此次变化较大,代码也进行了重构,层次更清晰可读。功能上主要变化如下:

 

1,延迟模块的执行。

这是一个很大变化,以前模块加载后factory立马执行。性能上肯定有一些损耗。2.0修改实现,再没人诟病AMD的模块是立即执行的。现在也可以等到require的时候才执行。

 

2config增加了shimmapmoduleenforceDefine

shim参数解决了使用非AMD方式定义的模块(如jQuery插件)及其载入顺序。使用shim参数来取代1.0版本的order插件。其实在1.0版本中就曾经有人开发过usewrap插件来解决此类问题。考虑到很多开发者有此类需求(比如某些JS模块是较早时候其他人开发的,非AMD方式)此次2.0版本直接将其内置其中。

下面是一个使用jQuery插件形式配置的参数。我们知道jQuery插件本质上是将命名空间挂在全局的jQueryjQuery.fn上而非使用define定义的模块。而jQuery插件都依赖于jQuery,即在require插件时得保证jQuery先下载下来。可以如下配置

?

1

2

3

4

5

6

require.config({

    shim: {

        'jquery-slide': ['jquery']

    }

});

require(['jquery-slide']);

这时会保证先下载jquery.js,然后再下载jquery-slide.js

 

map参数用来解决同一个模块的不同版本问题,这一灵感来自于DojopackageMap。有这样的场景:开发初期使用了的jquery-1.6.4,后期升级到了1.7.2。但担心有些依赖jquery-1.6.4的代码升级到1.7.2后有问题。因此保守的让这部分代码继续使用1.6.4版本。这时map参数将派上用场。

假如AB模块中使用了jquery-1.6.4.jsCD模块中使用了jquery-1.7.2.js。如下

?

1

2

3

4

5

6

7

8

9

10

11

12

requirejs.config({

    map: {

        'A': {

            'jquery':'jquery-1.6.4'

        },

        'B': {

            'jquery':'jquery-1.7.2'

        }

    }

});

require(['A']);// download jquery-1.6.4.js

require(['B']);// download jquery-1.7.2.js

这时require(['A'])将会下载jquery-1.6.4.jsrequire(['B'])会下载jquery-1.7.2.js。模块“A”如果写成“*”则表示除了B模块使用jquery-1.7.2之外其它模块都使用jquery-1.6.4map参数解决了模块的各个版本问题,很好的向下兼容了老代码。

 

config参数用来给指定的模块传递一些有用的数据。如下

?

1

2

3

4

5

6

7

require.config({

    config: {

        'A': {

            info: {name:'jack'}

        }

    }

});

使用A的模块中可以通过A.config().info获取到该数据信息。如

?

1

2

3

4

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

    varinfo = a.config().info;

    console.log(info);

});

 

enforceDefine用来强制模块使用define定义,默认为false。如underscore不再支持AMD后,其代码移除了define。此时如果仍然使用requirejs来载入它,它就是普通的js文件了。此时如果enforceDefine设为true,虽然underscore.js能下载但requirejs会报错。如

?

1

2

3

4

5

6

require.config({

    enforceDefine:true

});

require(['underscore'],function(_){

    console.log(_)

})

 错误信息

 

 

4require函数增加了第三个参数errbacks

很明显该函数指模块文件没有载入成功时的回调。这个也是应一些开发者得要求而增加,其中还包括另一个著名AMD的实现curl的作者John Hann

?

1

2

3

4

5

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

    console.log('success');

},function(err){

    console.log(err)

});

 err会给出一些出错提示信息。

 

5,更强大的paths参数。

requirejs 1.x版本中已经有paths参数,用来映射模块别名。requirejs2.0更加强大,可以配置为一个数组,顺序映射。当前面的路径没有成功载入时可接着使用后面的路径。如下

?

1

2

3

4

5

6

7

8

9

10

11

12

requirejs.config({

    enforceDefine:true,

    paths: {

        jquery: [

            'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min',

            'lib/jquery'

        ]

    }

});

  

require(['jquery'],function($) {

});

google cdn上的jquery.min.js没有获取时(假如google宕机),可以使用本地的lib/jquery.js

 

6,在模块载入失败回调中可以使用undef函数移除模块的注册。

这个灵感来自dojo AMD loaderRequireJS取名undef。如下

?

1

2

3

4

5

6

7

8

require(['jquery'],function($) {

    //Do something with $ here

},function(err) {

    varfailedId = err.requireModules && err.requireModules[0];

    if(failedId ==='jquery') {

        requirejs.undef(failedId);

    } 

});

 

7,删除了jQuery domready相关代码。

这次没人再诟病RequireJSjQuery耦合的太紧密。

 

8,删除了prioritypackagePathscatchError.define

这几个参数已经有相应的替代品。

 

最后需要注意的是,虽然功能增加了不少。但代码量却减少了近60行。主要是去掉了jQuery ready相关代码。另外newContext函数依然有1000多行。

 

 

为了应对日益复杂,大规模的JavaScript开发。我们化整为零,化繁为简。将复杂的逻辑划分一个个小单元,各个击破。这时一个项目可能会有几十个甚至上百个JS文件,每个文件为一个模块单元。如果上线时都是这些小文件,那将对性能造成一定影响。

 

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

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

 

这篇以一个简单的示例来感受下r.js,创建的目录如下

 

入门之三目录结构一样,写了三个模块cacheeventselector。这三个模块的代码就不在贴了。main.js也未做修改,实现的功能仍然是为页面上的所有段落P元素添加个点击事件,点击后弹出PinnerHTML。唯一的区别是目录中多了个r.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 {

                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.jsjs目录下的cache.jsevent.jsselector.jsmain.js合并压缩后写到r4目录中。

 

好,让我们开始合并压缩吧。

1,打开windows命令窗口,cdr4目录中

 

 

2,输入命令

node r.js -o baseUrl=js name=main out=built.js

 

 

命令行信息可以看到已经将各个js文件合并成功了。这时回到r4目录会发现多了一个built.js文件。

 

好了,合并压缩过程完成了。

 

把目录r4放到apache或其它web服务器上,访问index.html。网络请求如下

 

 

可以看到除了require.js,就只有built.js了。大大减少了JS文件的http请求。这时点击页面上各个P元素,会弹出对应的innerHTML

 

这说明功能完损无缺。

 

下面对命令行做个简单解释。
node r.js -o baseUrl=js name=main out=built.js

-o         表示优化,该参数是固定的,必选。
baseUrl  指存放模块的根目录,这里是r4/js,因为cdr4中了,只需设置为js。可选,如果没有设置将从r4中查找main.js
name     模块的入口文件,这里设置成“main”,那么r.js会从baseUrl+main去查找。这里实际是r4/js/main.jsr.js会分析main.js,找出其所依赖的所有其它模块,然后合并压缩。
out        指合并压缩后输出的文件路径,这里直接是built.js,那么将输出到根目录r4下。

 

好了,再介绍两个参数

1excludeShallow 合并时将排除该文件

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

 

 

可以看到输出信息中不再包括selector.js。这时运行index.html页面,会发现selector.js被单独请求下来了。

 

 

2optimize (none/uglify/closure)  指定是否压缩,默认为uglify
不传该参数时r.js默认以UglifyJS压缩。设置为none则不会压缩,仅合并,这在开发阶段是很用用的。
node r.js -o baseUrl=js name=main out=built.js optimize=none

 

这时生成的built.js是没有压缩的。

 

总结:
这篇演示了采用模块开发后上线前的一个小示例:把所有模块文件合并为一个文件。

先下载r.js放到开发目录中,然后使用命令行来合并压缩。其中演示了命令行参数-obaseUrlnameoutexcludeShallowoptimize的使用。-onameout是必选的,其它为可选。

 

 

 

这一篇来认识下打包工具的paths参数,在入门一中就介绍了require.config方法的paths参数。用来配置jquery模块的文件名(jQuery作为AMD模块时id为“jquery”,但文件名可通过paths配置可以不必是“jquery.js”,而是带有版本的如“jquery-1.7.2.js”)。

 

在入门一中,jquery-1.7.2.jsmain.js都在一个域中,即把jquery-1.7.2.js下载到本地了。但有时可能一些JS资源不在同一个域。比如直接使用Google CDN上的jquery 1.7.2版本。而这时应该如何使用打包工具r.js呢?

 

r.js自然不会去载入非本地资源,即没有办法去把外域的js文件请求下来再合并,压缩。当使用paths参数后,使用r.js合并压缩时要忽略paths映射的文件-不合并它。让其作为一个独立模块请求。

 

创建目录及文件如下

 

和上一篇一样,但main.js代码不同,注意目录中没有jQuery库。

 

main.js

?

1

2

3

4

5

6

7

8

9

10

require.config({

    baseUrl:'js',

    paths: {

        'jquery':'https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min'

    }

});

  

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

    alert($);

});

配置了paths参数,即jquery模块使用Google CDN的文件。

 

如果按照上一篇的命令来执行合并压缩,

node r.js -o baseUrl=js name=main out=built.js

 

发现命令行报错了,提示“D:\work\req\r5\js\jquery.js”不存在。刚刚新建的目录中的确没有jquery.js,因为我们使用的是Google CDN上的jquery

 

此时压缩参数paths就排上用处了,修改如下

node r.js -o baseUrl=js name=main out=built.js paths.jquery=empty:

 

注意红色圈住的参数(empty后有个冒号哦),表示paths.jquery不参与合并,压缩。这时生成的built.js也就不包含它了。

 

把目录r5放到apache或其它web服务器上,访问index.html

 

网络请求如下

 

built.js包含了main.jsevent.jscache.jsselector.jsjquery则是独立的一个请求,来自ajax.googleapis.com

 

再看看如何使用r.js来合并压缩css文件。在r5下新建一个css文件夹,里面有四个css文件:main.cssnav.cssform.cssgrid.css

 

main.css是合并的主文件,或称配置文件。要合并的文件使用@import引入。如下

 

main.css

?

1

2

3

@importurl("nav.css");

@importurl("grid.css");

@importurl("form.css");

 

另外三个是普通的css文件,里面定义的各种样式。这里不贴代码了。这里将使用命令行将这四个文件合并后生成到r5/css/built.css

node r.js -o cssIn=css/main.css out=css/built.css

 

这时回到r5/css目录会发现多了一个built.css文件,该文件是另外四个css文件的合并项。

 

还可以使用optimizeCss参数设置来配置是否压缩及压缩选项。optimizeCss的取值有standard/none/standard.keepLines/standard.keepComments/standard.keepComments.keepLines

none  不压缩,仅合并

standard  标准压缩 去换行、空格、注释

standard.keepLines  除标准压缩外,保留换行

standard.keepComments  除标准压缩外,保留注释

standard.keepComments.keepLines  除标准压缩外,保留换行和注释

 

示例:

node r.js -o cssIn=css/main.css out=css/built.css optimizeCss=standard

压缩后built.css整个为一行了。

 

总结:

1,对于path配置的非本地的模块文件,使用r.js合并压缩时需要配置paths.xx=empty:

2cssInoptimizeCss参数的使用来合并压缩css文件。

 

 

 

 

进阶的前面两篇讲述了r.js如何通过命令行把所有的模块压缩为一个js文件或把所有的css压缩为一个css文件。其中包括一些压缩配置参数的使用。

 

但以上两种方式有几个问题

1、通过命令手动配置压缩选项显得很呆板
2、都仅合并为一个文件

对于最后只生成一个文件的库来说,这种方式并无不妥。比如jQuery,它的工程中小文件有20多个,打包后只有一个jquery-1.x.x.js。对于多数实际应用项目来说,可能打包后需要生成多个js文件。有些是页面打开时就要用到的,有些是用户点击或输入时按需加载的。

 

r.js有另外一种方式来合并压缩,即通过一个配置文件(如build.js)。配置文件内部采用前端工程师非常熟悉JSON格式。这样当项目开发目录固定后,配置文件也相应固定。通过配置文件就很好的隔离了开发环境及上线环境。

 

这次我们创建的目录中包含所有前端资源,jscss,图片。

 

其中有两个页面page1.htmlpage2.html。这两个页面分别使用page1.jspage2.js

 

page1.js依赖于eventselectorpage2.js依赖于eventselectorjQueryjQuery是非本地的,没有合并前我们直接访问这两个页面,那么单个的js文件会依次下载。

 

 

 

现在使用r.js来合并压缩,使每个页面除下载require.js外只下载各自合并的大文件page1.jspage2.js


build.js如下

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

({

    appDir:"./",

    baseUrl:"js",

    dir:"../r6-built",

    paths: {

        jquery:'empty:'

    },

    modules: [

        {

            name:"page1"

        },

        {

            name:"page2"

        }

    ]

})

 

进入命令行输入如下命令
node r.js -o build.js

 

会发现在和r6同级的目录生成了r6-built目录

 

该目录包含于r6一样的层级结构,这时访问该目录中的page1.htmlpage2.html

 

 

这时的page1.jspage2.js就是其它模块文件的合并。另外在r6-built中其它的模块文件也被压缩了。

 

build.js中可以配置很多其它参数,可以在这个示例文件中找到更多配置选项。这里不一一列举。

 

总结:

通过配置文件方式可以实现更加强大,灵活的合并工作。可以生成多个合并文件,包括不同页面的jscss

 

posted @ 2014-08-01 12:03  稣惜陌  阅读(899)  评论(0编辑  收藏  举报