代码改变世界

用requireJS进行模块化的网站开发

2013-12-24 16:51  king0222  阅读(715)  评论(0编辑  收藏  举报

用requireJS进行模块化的网站开发

一.为什么选择requireJS

现目前网上流行两种能进行模块化管理的模块加载器,一种便是遵循AMD(异步模块定义)规范的requireJS,另一种便是遵循CMD(通用模块定义)规范的seaJS.

至于什么是AMD规范什么是CMD规范,简单点说就是一种代码的书写格式或者是API的调用形式。具体看:

两个加载器的模块定义方式略有不同, CMD 推崇依赖就近,AMD 推崇依赖前置。看代码:

 1 // CMD
 2 define(function(require, exports, module) {
 3    var a = require('./a')
 4    a.doSomething()
 5    // 此处略去 100 行
 6    var b = require('./b') // 依赖可以就近书写
 7    b.doSomething()
 8    // ... 
 9 })
10 
11 // AMD 默认推荐的是
12 define(['./a', './b'], function(a, b) {  // 依赖必须一开始就写好
13     a.doSomething()
14     // 此处略去 100 行
15     b.doSomething()
16     ...
17 }) 

在这里选择使用遵循AMD规范的requireJS的一个最重要原因是requireJS有完善的API文档,另外一个就是自己喜好选择了。

注意:本文档不会详细的介绍AMD规范外的细节,需要了解详细的可以看官方文档

二.requireJS 如何使用

在开始页面中引入requireJS,像如下:

1 <!--下面的data-main属性将会配置一个baseUrl(后面解释)的路径为scripts目录下-->  
2 <script data-main="scripts/main.js" src="scripts/require.js"></script>

在requireJS中最好根据baseUrl和path参数来配置模块,而尽量避免使用相对路径的形式,这会给后期的项目优化带来方便。(现在不清楚,后面的解释)

在创建的项目中,目录结构可能像下面这样:

在index.html页面中,引入的脚本为:

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

在app.js中代码如下:

 1 requirejs.config({
 2     //baseUrl路径配置加载默认的模块,像jquery之类的库
 3     baseUrl: 'js/lib',
 4     //如果有另外的一些自定义的模块在另一个文件路径下,可以配置paths,配置值后面无需加上后缀.js,加载器会默认将模块认为是js文件,会自动补全。
 5     paths: {
 6         app: '../app'
 7     }
 8 });
 9 
10 // 在这里开始整个应用的逻辑,应注意线面的第一行中数组的三个模块名对应着callback中的3个参数,顺序不能搞错
11 requirejs(['jquery', 'canvas', 'app/sub'],
12 function   ($,        canvas,   sub) {
13     //jQuery, canvas and the app/sub module are all
14     //loaded and can be used here now.
15 });

三.定义模块

1.如果模块中不需要依赖其他模块的,而仅仅是一个属性名/值对象字面量,可以像如下方式定义该模块:

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

2.如果该模块需要用到一些方法进行逻辑操作,可以在该模块中添加一个funciton来定义,这也是常用的模块定义方法:

 

1 define(function () {
2  //在这里TODO...
3 
4     return {
5         color: "black",
6         size: "unisize"
7     }
8 });

3.第三种也就是我们可能最常用的模块定义方式,需要依赖一些模块,并会依据这些模块来进行一些操作:

 1 //文件存放于my/shirt.js
 2 define(["./cart", "./inventory"], function(cart, inventory) {
 3     //return an object to define the "my/shirt" module.
 4     return {
 5         color: "blue",
 6         size: "large",
 7         addToCart: function() {
 8             inventory.decrement(this);
 9             cart.add(this);
10         }
11     }
12 });

前面提到过,依赖项跟紧跟的函数中的参数一一对应,根据上面代码的依赖关系可以看出目录结构应该是(都位于同一级目录下):

  • my/cart.js
  • my/inventory.js
  • my/shirt.js

4.如果我们需要定义一个没有返回值的模块,而仅仅作为一个函数使用,可以像下面这样返回一个函数:

 1 define(["my/cart", "my/inventory"],
 2     function(cart, inventory) {
 3         //return a function to define "foo/title".
 4         //It gets or sets the window title.
 5         return function(title) {
 6             return title ? (window.title = title) :
 7                    inventory.storeName + ' ' + cart.name;
 8         }
 9     }
10 );

5.模块的定义还可以给它加个模块名,仅仅需要在依赖项数组的前面加上一个字符串而已,像如下:

1 define("foo/title",
2     ["my/cart", "my/inventory"],
3     function(cart, inventory) {
4         //Define foo/title object in here.
5    }
6 );

不过不推荐这样,这对代码移植来说是绝对是个累赘,官方也不推荐写上模块名。

模块的一些其他说明:

1.如果想生成模块的关联urls,可以使用require.toUrl()方法,前提是依赖require:

1 define(["require"], function(require) {
2     var cssUrl = require.toUrl("./style.css");
3 });

2.在项目开发中,常常需要控制台debug,可以想下面这样来调用对应的模块方法:

1 require("module/name").callSomeFunction()

3.循环依赖,如果有两个模块a和b,a需要依赖b,b需要依赖a,以模块b的定义为例:

1 define(["require", "a"],
2     function(require, a) {
3         //"a" in this case will be null if a also asked for b,
4         //a circular dependency.
5         return function(title) {
6             return require("a").doSomething();
7         }
8     }
9 );

注意到第六行,这是实现循环依赖的关键,就是讲require加为依赖项,看懂就行了。

4.JSONP依赖,如果服务器端实现了JSONP提供数据,那么我们在定义模块的时候需要像下面这样去写:

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

第一行中,依赖项是一条url,并且在url后面手动加上"?callback=define",不包括引号,在后面的函数中便能够在参数中拿到JSONP返回的JSON格式的数据了。

4.有时候我们可能需要取消模块的定义,利用require.undef()就可以完成这样的事情。

小结:模块定义无非就是将功能的实现单独拆分出来,类似于在一个函数中return一个对象或者其他的东西,便于其他模块获取模块方法或信息。(模块定义没想象中的那么难,理解其基本原理即可)

四.配置信息

配置信息应该写在顶层html页面中,或者顶层的javascript文件中(非模块定义页)。像下面的index.html页面中配置信息。

 1 <script src="scripts/require.js"></script>
 2 <script>
 3   require.config({
 4     baseUrl: "/another/path",
 5     paths: {
 6         "some": "some/v1.0"
 7     },
 8     waitSeconds: 15
 9   });
10   require( ["some/module", "my/module", "a.js", "b.js"],
11     function(someModule,    myModule) {
12         //This function will be called when all the dependencies
13         //listed above are loaded. Note that this function could
14         //be called before the page is loaded.
15         //This callback is optional.
16     }
17   );
18 </script>

下面我们就来逐个讲解配置的信息:

1.baseUrl:默认加载的模块路径,一般是脚本库路径,像jQuery.

2.paths:这里可以定义一些不同于baseUrl路径的代码目录别名,但路径是相对于baseUrl的,这点注意。

3.waitSeconds:这里设置脚本加载时长,如果超过该时长将放弃加载该脚本。

4.shim:该参数配置那些非模块化的代码文件。例如jQuery,它的代码格式不是一个requireJS所定义的模块模式,因此,我们需要用shim参数来将jQuery配置成一个可用的模块。看下面的代码:

 1 requirejs.config({
 2     shim: {
 3         'backbone': {
 4             deps: ['underscore', 'jquery'],
 5             exports: 'Backbone'
 6         },
 7         'underscore': {
 8             exports: '_'
 9         },
10         'foo': {
11             deps: ['bar'],
12             exports: 'Foo',
13             init: function (bar) {
14                 //类似于初始化,参数要传入依赖项
15                 return this.Foo.noConflict();
16             }
17         }
18     }
19 });
20 
21 //然后在另一个文件名为MyModel.js的文件中创建模块,依赖项为shim中配置的backbone,这样就可以使用Backbone来编写自己的代码了
22 define(['backbone'], function (Backbone) {
23   return Backbone.Model.extend({});
24 });

5.map:该参数在项目中需要用到多版本脚本库的时候会非常有用,比方说一个模块需要依赖低版本的foo脚本库,而另一个模块需要依赖高版本的foo脚本库的时候,这时可以用map参数来正确匹配其依赖项。像下面的代码所示:

 1 requirejs.config({
 2     map: {
 3         'some/newmodule': {
 4             'foo': 'foo1.2'
 5         },
 6         'some/oldmodule': {
 7             'foo': 'foo1.0'
 8         }
 9     }
10 });

上面的代码中,some/newmodule模块需要依赖foo库的高版本,而some/oldmodule需要依赖其低版本, 目录结构应该是这样的:

  • foo1.0.js
  • foo1.2.js
  • some/
    • newmodule.js
    • oldmodule.js

或者可以使用通配符*表示所有的模块,像下面代码所示:

 1 requirejs.config({
 2  map: {
 3         '*': {
 4             'foo': 'foo1.2'
 5         },
 6         'some/oldmodule': {
 7             'foo': 'foo1.0'
 8         }
 9     }
10 });

通配符匹配了所有的模块依赖高版本的foo库,除了后面紧跟着的some/oldmodule模块使用低版本库外。

6.config:该参数用来配置模块信息,如下代码所示:

 1 requirejs.config({
 2     config: {
 3         'bar': {
 4             size: 'large'
 5         },
 6         'baz': {
 7             color: 'blue'
 8         }
 9     }
10 });
11 
12 //这里是commonJS规范的写法,不详述
13 define(function (require, exports, module) {
14     //size值得到'large'
15     var size = module.config().size;
16 });
17 
18 define(['module'], function (module) {
19     //color值得到'blue'
20     var color = module.config().color;
21 });

7.packages:这个单词翻译成中文叫包,有后台语言编程基础的能理解其含义,在这里配置packages就是将整个包(包含一个或多个模块的文件)配置一个别名,默认的包里面最好有一个main.js的模块。像下面这样的目录结构:

  • project-directory/
    • project.html
    • script/
      • cart/
        • main.js
      • store/
        • main.js
        • util.js
      • main.js
      • require.js

首先我们在project.html中包含require.js

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

接着我们可以像这样去加载包:

1 require.config({
2     "packages": ["cart", "store"]
3 });
4 
5 require(["cart", "store", "store/util"],
6 function (cart,   store,   util) {
7     //use the modules as usual.
8 });

这里得稍作解释:require(["cart", "store", "store/util"]这行代码中"cart","store"表示了包名,requireJS会默认从包中寻找main.js去加载,因此"cart"=="cart/main.js",同理"store". 因为模块中还需要store包中的util模块,因此需要用包名/路径到模块的方式的去找到所要依赖的模块。

或者你不想默认去加载main.js这个模块,你把main.js名字改为了store.js,那么你可以像下面这样配置:

1 require.config({
2     packages: [
3         "cart",
4         {
5             name: "store",
6             main: "store"
7         }
8     ]
9 });

8.context:不描述,因为我也不清楚它是什么意思,看英文原文:

context: A name to give to a loading context. This allows require.js to load multiple versions of modules in a page, as long as each top-level require call specifies a unique context string.

9.deps:依赖性,不多复述

10.callback:这个还是有点用的,在所有依赖项加载完成后,可以在callback中做些啥子测试的都Ok.

11.enforceDefine:强制定义,这个东西设置为true的时候会在非define()定义模块的时候抛出一些异常,不用过度理会这个配置(我是这么觉得)。

12.xhtml:不理解,所以不解释,应该用的不多。

13.urlArgs:这个东东就相当有用了,在开发阶段应该将这个配置加上,等到项目发布的时候再将他去除,切记切记。这里有一个配置例子:

urlArgs: "bust=" +  (new Date()).getTime()

scriptType:名字就叫脚本类型,看一下这段英文吧,不懂翻译:

scriptType:Specify the value for the type="" attribute used for script tags inserted into the document by RequireJS. Default is "text/javascript". To use Firefox's JavaScript 1.8 features, use "text/javascript;version=1.8".

五.高级应用

1.如果有加载CDN上的脚本情况,我们可以预留一个本地脚本,以防无法加载CDN上的文件,因此我们可以如下配置:

 1 requirejs.config({
 2     //为了能够查看IE中的异常加上enforceDefine
 3     enforceDefine: true,
 4     paths: {
 5         jquery: [
 6             'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min',
 7             //如果CDN加载失败,就加载本地
 8             'lib/jquery'
 9         ]
10     }
11 });
12 
13 require(['jquery'], function ($) {
14 });

2.有时候为了检查错误,我们可以重写requirejs.onError()方法:

1 requirejs.onError = function (err) {
2  console.log(err.requireType);
3     if (err.requireType === 'timeout') {
4         console.log('modules: ' + err.requireModules);
5     }
6 
7     throw err;
8 };

3.requirejs本身不支持dom加载完成才去执行脚本,但有一个已经完成的模块为我们提供了该功能,当然你需要下载该模块到本地:

1 require(['domReady'], function (domReady) {
2   domReady(function () {
3     //TODO...
4   });
5 });

domReady模块能帮我们在dom加载完成后采取执行脚本,但这里有一种跟简便的方式去写这个domReady方法,在依赖项domReady后面紧跟一个!号即可。

1 require(['domReady!'], function (doc) {
2 });

其他更多内容请看requireJS API官方文档