代码改变世界

RequireJs构建网站

2013-12-24 15:49  king0222  阅读(1015)  评论(0编辑  收藏  举报

requireJS构建网站

一.概要

requireJS添加了对jquery的amd支持,并整合了一个require-jquery.js脚本库,因此当我们需要require+jquery来构建项目的时候仅需要引入require-jquery.js就搞定了。

模块化管理有什么作用:

 1.易管理维护。
 2.按需加载,作为网站开发,能够减少http请求就尽量减少http请求,以求能让网页迅速打开并能浏览,多余的脚本实在没必要载入。
 3.避免全局污染,模块化的开发使没一个模块中声明的变量都局限在当前模块中,除非你自己定义了全局变量a=xxx;(记得加个var)
 4.requireJS的加载机制并不会产生阻塞渲染的现象,因此在渲染速度上比传统script标签引入的形式要好。

二.如何构建

接下来看看我们需要怎么做,为了便于理解,我们来构建一个多页面的场景,
项目目录结构如下:

有必要说明一下目录结构,这个目录结构跟客户端的结构基本上是一致的,目的就是为了多项目能放在一起统一管理,并方便继承一些公有的类或者对象。

我们看一下首页代码:

 1 html/fbs/index.html
 2 <!DOCTYPE html>
 3 <html lang="en">
 4 <head>
 5 <meta charset="utf-8"/>
 6 <title>index</title>
 7 <link rel="stylesheet" type="text/css" href="../../css/fbs/app.css">
 8 <!--[if lt IE 9]>
 9 <script src="../../js/lib/html5shiv.js"></script>
10 <![endif]-->
11 <script data-main="main1" src="../../js/lib/require-jquery.js"></script>
12 <script type="text/javascript">
13 //为什么将配置信息写在这里?注意到配置参数中有uriArgs,它能使我们的任何加载进来的js页面都不是缓存页面,方便开发过程。若是将配置信息写到main1.js中,我们加载main1.js的时候会得到它的缓存。
14 require.config({
15   baseUrl:'../../js/fbs',
16   urlArgs:'date='+ (new Date()).getTime()
17 });
18 </script>
19 </head>
20 <body>
21 
22 </body>
23 </html>

 

 

看到代码中加了一个html5shiv.js的脚本,这个东东是为了解决ie9以下的版本对html5新标签的支持问题。

再看到它的下一行,就是我们所需要引入的脚本库require-jquery.js。里面的data-main属性指定了程序的入口,现在它的入口便是main1这个模块,但这里没有说明它所在的具体路径。(别着急,下面讲述)。

再看到下面的行内脚本块中,我们队require进行一个配置,这个配置信息指定了模块的基本加载路径在什么位置,并且配置了开发中不进行缓存。

1. baseUrl:指定模块基本加载路径,就上面提到的data-main属性值为main1,在配置完成后,require会在基本路径中去查找main1.js这个文件,也就是'../../js/fbs/main1.js'这个路径。还要另一种方式就是将baseUrl隐形的配置在data-main属性中,即data-main='../../js/fbs/main1';那么require就会将'../../js/fbs/'当成baseUrl.**因此在如果您在data-main中填写了完整路径,就不需要在配置信息中再写一个路径,若路径不统一,将出错。**

2. urlArgs:给加载的模块名称后面加上一个字符串,以防止浏览器加载缓存,这对开发过程极为方便。

若需要了解更多配置信息,请参看[用requireJS进行模块化的网站开发](require.html)

接下来我们要做的是什么,无论什么项目,我们都希望有一个全局的对象,以保存一些全局信息,也方便操作其子集的一些功能。比如我们可能有AppMain.js或者App.js这样一个文件,这个文件中可能创建了一个全局对象window.AppMain=AppMain || {};然后我们利用这个对象包含我们整个项目中一些小的视图。当我们需要从一个视图操作另一个视图的时候就可以通过AppMain去拿到视图并操作它了。

在网站中,每一个页面都是一个对象,切换到不同页面的时候,原来页面中的js变量或者是对象等在新页面中不会产生作用,也不会存在(除非你在这个页面也创建了它们)。

因此,在这里我们有两个页面,我们就应该(只是应该而已)有两个这样的全局的对象。在上面的图片可以看到有main1.js和main2.js分别对应于index.html和contact.html页面创建的,但这两个js文件并不能作为全局对象来操作模块s,只能作为程序的入口,至于为什么,请备好瓜子往下看。

三.模块编写

在index.html中我们引入了require-jquery.js并赋予data-main属性值为main1,因此main1.js即为程序入口。
看main1.js代码:

1 js/fbs/main1.js:
2 require(['jquery','app'],function($,app){
3   app.init();
4   var persion=new app.App;
5   persion.say('this is index page!');
6 });

 

 

看到第一行代码中引入了jquery,和app,然而我们项目中并不存在jquery.js这个文件,它包含在了require-jquery.js中。requireJS要求我们在需要用到jquery的时候把的引用的加上。app模块就对应了当前目录下的app.js文件。(不清楚目录结构,就返回看看前面的图片)

看到我们引入了模块之后,紧接着是一个回调函数,回调函数的参数一一对应这所引用的模块。

在回调函数代码块里面是对app模块的具体应用。下面我们先看看app模块中的具体代码:

 1 js/fbs/app.js:
 2 define(['jquery','ux/jquery.log','utils'],function($,log,utils){
 3   //一个构造函数,@1
 4   function App(){
 5     this.name='app',
 6     this.version='2013.1.1'
 7   }
 8   //构造函数实例方法,@2
 9   App.prototype.say=function(text){
10     $('body').append('<h1>'+text+'</h1>');
11   }
12   //一个很随便的函数,@3
13   function init(){
14     $('button').each(function(i){
15       $(this).on('click',function(){
16         console.log('hello');
17         $(this).log('you have click me');
18       });
19     });
20   }
21   //这里是当页面加载完成后就会执行的脚本。@4
22   $(function(){
23     $('body').append('render something!');
24   });
25   //用return返回对外输出的接口,@5
26   return {
27     init:init,//提供给外部一个初始化函数
28     App:App, //提供给外部一个构造函数
29     name:'ken'
30   }
31 });

 



我们用define来定义模块,数组中是模块的依赖项,回调函数是其实现代码。
看到@1处,我们在这里定义了一个构造函数,并在@2处给这个构造函数添加了一个实例方法。@3处我们随便写了一个函数,以对外提供作为一个页面的初始化函数来使用(这里仅仅是为了举例,别太纠结)。@4处我们写了个jquery常常用到的写法,就是dom加载完成后才执行里面的代码,在这里依然奏效。@5处是我们可能会用的稍多一点的,也就是对外提供接口。

就像我们前面的代码中:

app.init();
var persion=new app.App;
persion.say('this is index page!');

要想能够使用app的init方法,就需要我们在app模块中提供这样的一个对外接口;要想能够通过app来new一个App,我们也可以将app模块中的app构造函数提供出去。因此就有了:

return {
init:init,//提供给外部一个初始化函数
App:App //提供给外部一个构造函数
}

相当简单明了易懂。

好了,我们重新回到app模块的定义中,在讲一下其依赖项中的'ux/jquery.log',这对应了ux目录下的jquery.log.js文件,里面写了一个jquery的插件。代码如下:

js/fbs/ux/jquery.log.js:
$.fn.log=function(options){
  return this.html(options);
}

这就是他的代码,跟我们平时所写的jquery插件没什么不同,它一样能够在这里运行正常。不过,我们现在是模块化开发,我们当然要统一模块的书写方式,给它加个皮囊就够了:

define(['jquery'],function($){
//todo ...
});

加上这个东东,我们的代码就是一个比较正规的AMD模块了,这对于代码优化是有好处的。加上皮囊后代码为:

js/fbs/ux/jquery.log.js:
define(['jquery'],function($){
  $.fn.log=function(options){
    return this.html(options);
  }
});

因为在app模块中已经添加了这个插件的依赖,requireJS会把它加载到页面中,在之后的模块中可以不再需要添加对它的依赖(前提是你知道它已经被加载过),当然如果你在定义模块的时候再加上对它的依赖也是没问题的,因为requireJS能够判断处该模块是否加载过,若加载过则不会再次加载,

我们在前面提到过一个关于全局控制的问题,我们需要一个对象来作为全局控制器,或许从我们的第一感觉来说我们应该将main1.js这个文件作为index.html页面的全局控制器,毕竟脚本是从这里作为一个入口的。但是在requireJS中,模块之间的调用只能在模块中有效,即以define来定义的模块。main1.js仅仅是一个脚本程序的入口而已,它并不是一个模块。在我们用define来定义的模块中,无法对其进行操作。所以我们应该将第一个加载的模块作为全局控制器,这里可以用app.js。

对上面这段话做一个测试,我们在目录结构中创建了js/fbs/foo.js文件。我们将在这个模块中去调用app模块的某些属性。代码如下:

//js/fbs/foo.js
define(['jquery','require','app'],function($,require,app){
require(['app'],function(app){
$('body').append('<h2>app name:'+app.name+'</h2>')
});
});

因为这是一个循环调用,在app.js中我们添加了对foo的依赖,这里我们又需要对app进行依赖。对于循环依赖我们必须在回调函数中再次require('module name')才能保证脚本正确执行,像上面代码第二行所示。另外,在第一行的模块引入中,我们其实是不需要将app依赖项也写进去的,它也能正常工作,还有require的依赖项我们也可以去除,因为index.html页面再前面已经加载完成了require-jquery.js,但是这里我们希望把所有的依赖项都写上去。

//去除后依然正常工作,但不建议这么做
define(['jquery'],function($){
//the same...
});

就像在每一个项目中我们都需要一个工具类一样,这里我们创建了一个utils.js的模块,提供各种方法给各个模块使用,它的实现十分简单,就是返回一个包含各个工具方法的对象。

define(['jquery'],function($){
  return{
    alert1:function(){
      alert('alert1');
    }
  //alert2, alert3, ...
  }
});

当我们需要使用utils里面提供的方法的时候仅需要将utils模块依赖进来,这里对foo模块稍为修改一下:

//js/fbs/foo.js
define(['jquery','require','app','utils'],function($,require,app,utils){
  require(['app'],function(app){
    $('body').append('<h2>app name:'+app.name+'</h2>');
  });
  utils.alert1();//表现很好.
});

四.API提取数据

看到项目目录中有js/api/这个目录,里面有api.js文件,这个文件中存放的是对api们的封装,这样做有一个好处在于调试的时候只需要在api.js中查找相应代码调试即可,或者只需要在ajax方法中调试。代码如下:

 1 /*
 2 usage:
 3   api.getAppList({
 4     callback:function(){}
 5   });
 6 
 7 */
 8 define(['jquery'],function($){
 9 var ajax=function(options){
10 $.ajax({
11   url:options.dataType?(options.url):(options.url+'?callback=?'),
12   dataType:options.dataType || 'jsonp',
13   data:options.data || null,
14   success:function(data,textStatus){
15     console.log(options.url+' is load success');
16     options.callback(data);
17   },
18   error:function(XMLHttpRequest, textStatus, errorThrown){
19     console.log(options.url+' loading error and   textStatus:'+textStatus);
20   }
21 });
22 };
23 return{
24   getAppList:function(options){
25     var url="http://www.xxx.yyy";
26     ajax({url:url,callback:options.callback});
27   },
28   getAppInfo:function(options){
29     ajax({url:options.url,callback:options.callback});
30   }
31   //get...
32 };
33 });

 

 

需要用到api的时候将其依赖到模块中即可,另外这个js文件可供其他项目共享。

五.模块与控制器的统一

我们的jquery代码常常没有一种比较规范的写法,除了插件式的代码能让人看起来代码规范些,回顾到上面所列出的app.js页面中的代码就能看出这是多么令人恶心的代码结构。现在流行了很多的MVC库,可能你也知道Backbone是个什么东西。它将功能组织拆分成模块,控制器和视图三部分。可能在我们这里用上这样的MVC架构并不是一个很好的选择,我们不需要将代码刻意组织的这么复杂。但是,对于像Backbone那样的MVC框架,我们可以从中提取一些对于开发过程中有用的技术,比方说控制器。

先看看下面这样的代码:

 1 var Con=Controller.create({
 2 elements:{
 3 '.login-button':'login',
 4 'input.cancel' :'cancel'
 5 },
 6 events:{
 7 'click .login-button':'doLogin'
 8 },
 9 doLogin:function(){
10 alert('do login');
11 }
12 });
13 var con=new Con({el:$('#login')});

 

在上面的代码中,elements块中是对选择器做了一个缓存操作,其实际效果就像:this.login=$('.login-button');所以我们在下面的代码中可以用this.login来表示$('.login-button')了。

events块中的代码则是添加事件绑定,就像:$('.login-button').bind('click',doLogin);

而因为Controller.create()所创建出来的仅仅是一个构造函数,我们需要用new关键字来生成实例。var con=new Con({el:$('#login')});中的el参数指定了控制中的选择器都会在$('#login')的范围内去查找。当然这个参数可以在创建控制器的时候写进去,如果不指定el参数,则脚本会从页面的body标签内去查找。以下是该控制器允许的几种写法:

 1 //方式一:
 2 var Con=Controller.create({
 3 el:$('#login'),
 4 elements:{
 5 //the same...
 6 },
 7 events:{
 8 //the same...
 9 },
10 doLogin:function(){
11 //the same...
12 }
13 });
14 var con=new Con;
15 
16 //方式二:
17 var Con=new (Controller.create({
18 el:$('#login'),
19 elements:{
20 //the same...
21 },
22 events:{
23 //the same...
24 },
25 doLogin:function(){
26 //the same...
27 }
28 }));

 

如果你喜欢这样的代码书写方式,那么你可以在require模块中添加对控制器的依赖,以便你能够以这样的方式去书写代码,当然如果在代码的入口引入了(像这里的main1.js和main2.js),那么在你的其他模块中就不需要去引入这个控制器模块了。我们将这个控制器的实现放在js/lib/controller.js文件中,即作为一个基本代码库。当我们在main1.js文件中添加了对它的依赖后,我们便可以在app模块中使用这样的方式去编写代码了:

 1 //js/fbs/main1.js
 2 require(['jquery','../lib/controller','app'],function($,controller,App){
 3 var con=App.con;
 4 con.showName();
 5 });
 6 
 7 
 8 //js/fbs/app.js
 9 define(['jquery','ux/jquery.log','foo'],function($,log,foo){
10 var con=new ($.controller({
11 //init是创建实例的时候便会立即执行的代码
12 init:function(){console.log('controller init')},
13 el:$('body > article'),
14 elements:{
15 'h1':"header"
16 },
17 events:{
18 'click h1':"showContent"
19 },
20 showContent:function(){
21 alert('h1 text is:'+this.header.text());
22 },
23 showName:function(){
24 alert('my name of controller');
25 }
26 }));
27 //若有需要,可以对外提供
28 return {
29 con:con
30 }
31 });

 

六.代码压缩

网站不可能以现在这样的方式就发布出去,要知道我们的代码都模块化了,有多少模块我们的页面就需要加载多少个文件,这会产生很多的http请求,即便这是异步加载,但性能肯定不会有多好。因此我们需要一种技术将这些模块合并和压缩,网络上的压缩脚本的技术有很多,什么YUIXXX,google closure combiler,还有一些可视化的压缩工具,或者一些网页形式的在线压缩功能。这里我们采用requireJS官方推荐的r.js来压缩我们的代码。

环境配置

一.用r.js压缩代码,需要nodeJS的环境,因此我们需要安装nodeJS,windows环境下,直接到http://nodejs.org/下载Node.xxx.msi后(xxx是指其版本号,别纠结),根据提示直接安装即可。可以在终端输入node -v来确认是否安装成功,若安装成功则会显示版本号。

二.用r.js压缩当然需要r.js这个文件了,最方便的方式就是直接下载下来即可。[下载r.js]() 或者你也可以用npm install的方式来下载r.js,不过这个方式会将一整个包括require.js的东西下载下来。方法就是打开终端,cd到你的项目目录,然后敲入"npm install requirejs"回车即可。

上面两个步骤就完成了我们所需要的环境,接下来我们需要了解一下压缩代码所需要的配置信息。先看一下tool目录下的app.build.js文件内容:

 1 ({
 2 //项目根目录,这个路径是相对于r.js的所在路径的
 3 appDir:'../',
 4 
 5 //模块目录,这个路径相对于appDir所配置的路径
 6 baseUrl:'js/fbs',
 7 
 8 //优化后的代码存放目录,若没有WEB_FRAME_BUILD这个文件夹则生成一个,这个路径相对于baseUrl所配置的路径
 9 dir:'../../WEB_FRAME_BUILD',
10 
11 //这个是配置代码是否需要压缩的,为none则不压缩
12 //optimize:'none',
13 
14 //路径别名,即我们的模块中所依赖的jquery,因为它已经包含在了require-jquery.js中,所以我们需要这样声明一下
15 paths:{
16 'jquery':'../lib/require-jquery'
17 },
18 
19 //模块,我们的项目中有两个页面,两个主要的程序入口,根据需要,有多少主模块就写多少在里面了
20 modules:[
21 {
22 name:'main1', //对应main1.js
23 exclude:['jquery'] //排除jquery模块的引入压缩,因为已经存在于require-jquery中
24 },
25 {
26 name:'main2', //对应main2.js
27 exclude:['jquery']
28 }
29 ]
30 })

 

上面的代码是为r.js所写的配置信息,那么如何去执行压缩呢。

先看到我们项目结构中,我把r.js和这个配置文件app.build.js同放在tool目录下。

代码压缩

注意:代码压缩之前,我们需要注意一件事情,就是以防配置文件写错,导致的代码压缩过程会覆盖掉原始文件,因此,最好在压缩代码之前,将项目备份一下。

接下来,我们打开终端,cd到我们的项目目录下的tool录下,即 cd xxx/xxxx/WEB_FRAME/tool/。

再接下来,敲入"node r.js -o app.build.js"回车即可搞定,NodeJS便会根据的脚本依赖关系将能够统一成一个文件的模块都合并起来并进行压缩。

 

"node r.js -o app.build.js"中的-o表示的是optimize,即优化。

优化完成后你会发现一个跟你项目目录同级的文件目录:

 

我们在新生成的项目中用浏览器打开里面的Index.html页面,并用开发者工具观察其加载项:

 

让我们跟未压缩之前情况对比一下:

 

两者之前的对比是很明显的,未优化之前我们需要很多的http请求才能加载完所需要的模块,优化完成后,很多模块都被集中到一个文件中了,这里main1.js是压缩完成后的代码,其他模块的代码都被自动包含到里面了。

若需要深入了解r.js的配置信息,可以到[这里]去查看详细的配置。

压缩后的调试

但代码压缩完成后,我们遇到一个问题,就是在压缩的代码发布出去后,如何方便的调试问题,压缩的代码调试起来可不是件轻松的事情,好在requireJS也考虑到了这方面的问题。

我们只需要稍为修改一下配置便能轻松解决调试的问题。因为我们的配置信息是写在html页面中的,这些页面并不会进行压缩,也不会有html缓存的问题,我们所需要做的仅仅是配置一下调试情况下用原始文件,而非调试情况下用压缩文件就好了。这个配置可以在paths属性中设置:

 1 //原来的代码:
 2 <script data-main="main1" src="../../js/lib/require-jquery.js"></script>
 3 <script type="text/javascript">
 4 require.config({
 5 baseUrl:'../../js/fbs',
 6 urlArgs:'date='+ (new Date()).getTime()
 7 });
 8 </script>
 9 
10 //修改为:(添加了paths配置)
11 <script data-main="main1" src="../../js/lib/require-jquery.js"></script>
12 <script type="text/javascript">
13 require.config({
14 baseUrl:'../../js/fbs',
15 //调试情况下,注释掉下面的配置
16 paths:{
17 'main1':'main1-build'
18 },
19 urlArgs:'date='+ (new Date()).getTime()
20 });
21 </script>

 

在代码压缩完成后,所有的文件都还存在,除了main1.js,main2.js这两个主文件内容变成了多个文件的合并后并压缩的代码,因此,我们需要从原始项目中复制这些压缩文件的原始文件到对应的目录中。并且还要修改压缩后的文件的文件名。这里我们将压缩后的main1.js修改为main1-build.js,将main2.js修改为main2-build.js。

因此,在我们的项目中,压缩后的项目文件要比原始目录文件多出一些主文件: