require.js 最佳实践

require.js是一个js库,相关的基础知识,前面转载了两篇博文:Javascript模块化编程(require.js), Javascript模块化工具require.js教程RequireJS 参考文章javascript

1. require.js的主要做用是js的工程化,规范化:css

1)它是一个js脚本的加载器,它遵循AMD(Asynchronous Module Definition)规范,实现js脚本的异步加载,不阻塞页面的渲染和其后的脚本的执行。html

    并提供了在加载完成以后的执行相应回调函数的功能;java

2)它要求js脚本的模块化,也就是文件化;require.js的做用之一就是加载js模块,也就是js文件。因此咱们的js的书写应该模块化,也就是文件化。node

3)它能够管理js模块/文件之间的依赖; js模块化,文件化以后,它们之间的依赖能够经过require.js优雅的解决;jquery

4)require.js中提供的优化器 r.js 能够来优化页面中的js脚本和css文件,达到提升页面响应速度,减小页面所须要的http/https请求次数。在极端优化的状况下,经过r.js优化以后的页面只须要一次js脚本请求和一次CSS文件请求。这就极大的减小了页面所须要的http/https请求的次数,提升了页面的加载速度。r.js的优化分为两种方式:一是压缩js和css文件,也就是去掉空格,空行,将长变量名换成短变量名之类的;二是合并多个js文件为一个js文件,合并多个css文件为一个nginx

5) 经过使用require.js以后,咱们只须要在页面引入一行<script>标签,相似于:<script src="js/require.js" data-main="js/login.js"></script>,甚至也能够只引入一行<style>标签,十分优雅。注意引入一行<script>标签并不等价于只须要一次js的http/https的请求。web

2. require.js模块的写法:apache

require.js要求咱们的js模块,也就是js文件按照必定的格式书写:也就是最好经过define()函数来写js模块,好比:math.js编程

define(function(){
	var add = function(x,y){
		return x+y;
	};
	return{
		add:add
	};
});

math.js经过define()函数,定义了一个符合require.js要求的js模块,它的返回值是一个对象,有一个属性add,它是一个函数。经过下的方式就能够来调用该js模块中定义的函数:

require.config({
	baseUrl:"/ems/js/",
	paths:{
		"math":"math"
	}
});

require(["math"], function(math){
	alert(math.add(100,20));
});

require.config的主要做用是配置 模块ID/模块名称 和 它对应的js文件所在的位置。上面的那个配置就是将 /ems/js/math.js(ems是项目名称) 文件配置成一个ID为math的模块,而后经过 require(["math"], function(math)(){}); 就能够异步来加载 /ems/js/math.js 文件,加载完成以后,执行回调函数。回调函数中调用了math模块中的add方法。

在看一个例子:

/**
 * This jQuery plugin displays pagination links inside the selected elements.
 *
 * @author Gabriel Birke (birke *at* d-scribe *dot* de)
 * @version 1.2
 * @param {int} maxentries Number of entries to paginate
 * @param {Object} opts Several options (see README for documentation)
 * @return {Object} jQuery Object
 */
define(['jquery'], function($){
	jQuery.fn.pagination = function(maxentries, opts){
		opts = jQuery.extend({
			items_per_page:10,
			num_display_entries:10,
			current_page:0,
			num_edge_entries:0,
			link_to:"#",
			prev_text:"Prev",
			next_text:"Next",
			ellipse_text:"...",
			prev_show_always:true,
			next_show_always:true,
			callback:function(){return false;}
		},opts||{});
		
		return this.each(function() {			
			function numPages() {
				return Math.ceil(maxentries/opts.items_per_page);
			}				
			function getInterval()  {
				var ne_half = Math.ceil(opts.num_display_entries/2);
				var np = numPages();
				var upper_limit = np-opts.num_display_entries;
				var start = current_page>ne_half?Math.max(Math.min(current_page-ne_half, upper_limit), 0):0;
				var end = current_page>ne_half?Math.min(current_page+ne_half, np):Math.min(opts.num_display_entries, np);
				return [start,end];
			}			
			function pageSelected(page_id, evt){
				current_page = page_id;
				drawLinks();
				var continuePropagation = opts.callback(page_id, panel);
				if (!continuePropagation) {
					if (evt.stopPropagation) {
						evt.stopPropagation();
					}
					else {
						evt.cancelBubble = true;
					}
				}
				return continuePropagation;
			}			
			function drawLinks() {
				panel.empty();
				var interval = getInterval();
				var np = numPages();				
				var getClickHandler = function(page_id) {
					return function(evt){ return pageSelected(page_id,evt); };
				}				
				var appendItem = function(page_id, appendopts){
					page_id = page_id<0?0:(page_id<np?page_id:np-1);
					appendopts = jQuery.extend({text:page_id+1, classes:""}, appendopts||{});
					if(page_id == current_page){
						var lnk = jQuery("<span class='current'>"+(appendopts.text)+"</span>");
					}else{
						var lnk = jQuery("<a>"+(appendopts.text)+"</a>")
							.bind("click", getClickHandler(page_id))
							.attr('href', opts.link_to.replace(/__id__/,page_id));		
					}
					if(appendopts.classes){lnk.addClass(appendopts.classes);}
					panel.append(lnk);
				}				
				if(opts.prev_text && (current_page > 0 || opts.prev_show_always)){
					appendItem(current_page-1,{text:opts.prev_text, classes:"prev"});
				}				
				if (interval[0] > 0 && opts.num_edge_entries > 0)
				{
					var end = Math.min(opts.num_edge_entries, interval[0]);
					for(var i=0; i<end; i++) {
						appendItem(i);
					}
					if(opts.num_edge_entries < interval[0] && opts.ellipse_text)
					{
						jQuery("<span>"+opts.ellipse_text+"</span>").appendTo(panel);
					}
				}				
				for(var i=interval[0]; i<interval[1]; i++) {
					appendItem(i);
				}				
				if (interval[1] < np && opts.num_edge_entries > 0)
				{
					if(np-opts.num_edge_entries > interval[1]&& opts.ellipse_text)
					{
						jQuery("<span>"+opts.ellipse_text+"</span>").appendTo(panel);
					}
					var begin = Math.max(np-opts.num_edge_entries, interval[1]);
					for(var i=begin; i<np; i++) {
						appendItem(i);
					}
					
				}				
				if(opts.next_text && (current_page < np-1 || opts.next_show_always)){
					appendItem(current_page+1,{text:opts.next_text, classes:"next"});
				}
			}			
			var current_page = opts.current_page;			
			maxentries = (!maxentries || maxentries < 0)?1:maxentries;
			opts.items_per_page = (!opts.items_per_page || opts.items_per_page < 0)?1:opts.items_per_page;			
			var panel = jQuery(this);
			this.selectPage = function(page_id){ pageSelected(page_id);}
			this.prevPage = function(){ 
				if (current_page > 0) {
					pageSelected(current_page - 1);
					return true;
				}
				else {
					return false;
				}
			}
			this.nextPage = function(){ 
				if(current_page < numPages()-1) {
					pageSelected(current_page+1);
					return true;
				}
				else {
					return false;
				}
			};			
			drawLinks();	       
	        opts.callback(current_page, this);
		});
	};	
	return jQuery.fn.pagination;
});

上面的define()函数定义了一个jquery的分页插件(文件名:jquery.pagination.js),它符合require.js模块的规范。define(['jquery'], function($)... 表示该模块依赖于 jquery 模块,并向回调函数传入jquery的全局对象 $, 那么这里的 ['jquery'] 又来自哪里呢?它其实来自于:

require.config({
        baseUrl:"/ems/js/",
	paths: {
		"jquery": "jquery.min"
	}
});

该配置将 /ems/js/jquery.min.js 配置成require.js的模块,模块ID为"jquery",因此咱们才能使用 define(['jquery'], function($) 来引用"jquery"模块。

define(["xxx","yyy"], function(xxx,yyy){}); define函数定义符合require.js规范的模块,数组参数指定该模块依赖的全部模块,那么这些被依赖的模块异步加载完成以后,而后执行回调函数,回调函数的返回值就是该模块的定义。返回值通常是一个对象,或者一个函数。而后该模块又能够被其它模块所依赖和使用。好比上面的: jquery.pagination.js,它定义了一个jquery的分页插件,那么经过下面的配置,我就可使用它:

require.config({
	baseUrl:"/ems/js/",
	paths: {
		"jquery": "jquery.min",
		"pagination": "jquery.pagination"
	}	
});

require(["pagination"], function(pagination){
     $.patination(20);// ....

});

再看一个例子(文件名:dateUtil.js):

define(function(){
	var dateFormat = function(fmt, date){
		if(!(date instanceof Date))
			return;
		var o = {
		        "M+": date.getMonth() + 1, // 月份
		        "d+": date.getDate(), //日
		        "H+": date.getHours(), //24小时制
		        "h+" : date.getHours()%12 == 0 ? 12 : date.getHours()%12, //12小时制  
		        "m+": date.getMinutes(), //分
		        "s+": date.getSeconds(), //秒
		        "q+": Math.floor((date.getMonth() + 3) / 3), //季度
		        "S": date.getMilliseconds()  //毫秒
		    };
		    if (/(y+)/.test(fmt))
		        fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
		    for (var k in o)
		    if (new RegExp("(" + k + ")").test(fmt))
		        fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k])
		                            : (("00" + o[k]).substr(("" + o[k]).length)));
		    return fmt;
	};
	
	return {
		format:dateFormat
	};
});

经过下面的配置,就可使用dataUtil.js中的format()函数来格式化日期对象:

require.config({
	baseUrl:"/ems/js/",
	paths: {
		"dateUtil": "dateUtil"		
	}	
});

require(["dateUtil"], function(dateUtil){
   alert(dateUtil.format("yyyy-MM-dd", new Date());
});

咱们在页面中引入上面的文件(文件名:main.js),就能够看到执行效果:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
      <span>body1111</span>
<script src="js/require.js" data-main="js/main.js"></script>
</body>
</html>

执行结果:

3. require.config函数

require.config如上面所说,主要是定义 模块ID 和 它所对应的js文件的位置。参数是一个json格式的对象,baseUrl属性指定paths中的路径的相对路径。paths是一个key/value的键值对形式,key表示模块ID,value表示相对于baseUrl的相对路径,须要省略文件后缀 .js 。还有一个shim的经常使用属性,用来配置不符合require.js规范的js模块(没有使用define()来书写),使之也能被咱们的require()函数来使用。可是shim配置的模块,没法经过cnd使用。其使用方法参见前面的转载文章。

4. require()函数

require.config({
	paths: {
		"jquery": "jquery.min",
		"math":"math",
		"dateUtil":"dateUtil"
	}
});

require(['jquery', 'math', "dateUtil" ], function ($, math, dateUtil){
	alert($("span").text());
	alert(math.add(1,30));
	alert(dateUtil.format("yyyy-MM-dd", new Date()));
});

require()函数,第一个参数,引入依赖的require模块, 在全部依赖异步加载完成以后,将这些模块的返回的对象或者返回的函数传入回调函数,那么在回调函数中就可使用这些被依赖的模块的功能了。好比上面使用 math.add(1,30)。

5. r.js 优化(合并压缩js和CSS文件)

按照require方式进行模块化以后,必然会产生不少的js文件,它们经过环环相扣的方式,按照依赖关系异步加载,那么必然会致使js文件所须要的http/https请求极大的增长,这时,就应该使用 r.js 来优化了。它能够将一个页面好比 login.jsp, 须要的全部的js文件合并成一个js文件也就是说只须要一次http/https请求就好了。因此就解决了js模块化以后http/https请求增多的问题,而且还减小到了只须要一次请求。同时r.js还能够压缩js文件。而且正对css文件也能够一样的方式减少合并压缩。优化通常在开发完成以后,发布以前进行。

1)js文件合并压缩:

好比开发时,某页面以下:

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/bootstrap.min.css" />
<link rel="stylesheet" href="css/matrix-login.css" />
<link rel="stylesheet" href="css/bootstrap-responsive.min.css" />
</head>
<body>
<!--Header-part-->
<div id="header">
  <h1><a href="javascript:;">Admin</a></h1>
</div>
<!--close-Header-part--> 

<!--top-Header-menu-->
<div id="user-nav" class="navbar navbar-inverse">
  <ul class="nav">
    <li class=""><span id="top_header">xxxx系统</span></li>
    <li><span id="cur_user">当前登录用户:</span></li>
    <li id="top_logout" style="float:right;">
    	<a href="${ctx}/logout"><i class="icon icon-share-alt"></i></a>
    </li>
  </ul>
</div>
<script src="js/require.min.js" data-main="js/main.js"></script>
</body>
</html>

其main.js文件以下:

require.config({	
	baseUrl:"/emsjs/",
	paths: {		
		"jquery":"jquery.min",
		"dateUtil":"dateUtil"
	}
});

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

那么显然该页面有 3个 CSS文件,2个js文件。那么针对js文件,咱们可使用node.js来合并:

咱们看到将 jquery.js, dateUtil.js, main.js 三个文件合并压缩成了一个文件:login.js, 那么咱们在页面中就只须要引入login.js文件就好了。

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

 改为:

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

2) CSS文件合并压缩:

合并压缩以前,须要先定义一个main.css文件:

@import url(bootstrap.min.css);
@import url(bootstrap-responsive.min.css);
@import url(matrix-login.css);

而后调用命令合并压缩:

四个CSS文件合并成了一个css文件:login.css。咱们看下压缩以后的login.css:

上面三行CSS的link:

<link rel="stylesheet" href="css/bootstrap.min.css" />
<link rel="stylesheet" href="css/matrix-login.css" />
<link rel="stylesheet" href="css/bootstrap-responsive.min.css" />

就能够换成一行:

<link rel="stylesheet" href="css/login.css" />

r.js的优化的详细介绍,能够参考前面转载的 RequireJS 参考文章 中的进阶的三篇文章。也能够参考require.js官网关于r.js的介绍。

6. require.js 最佳实践

前面说了那么多,最后才说到require.js的最佳实践。

1)使用 define() 定义符合require规范的模块;

2)使用require.config() 配置模块ID和它对应的js模块所在文件路径;require.config()是将define()定义的模块和require()依赖的模块链接起来;

3)使用require()指定其所依赖的模块,在回调中实现页面上须要的功能,固然define()函数也须要指定其所依赖的模块;

     require()和define()函数其实十分类似,都指定依赖的模块,都有回调函数;

4)使用r.js合并优化。这里最重要。合并优化涉及到一个取舍问题,好比前面的 jquery.min.js 是否应该被合并进去呢?由于jquery.min.js是一个通用的js库文件,那么其实几乎每个页面都须要改文件,那么其实咱们只是在第一次访问该网站时,须要下载一次jquery.min.js文件,其后使用的都是缓存中的,status都是304;可是若是咱们每一个页面都将 jquery.min.js 合并进该页面的惟一的 js 文件,那么jquery.min.js就会被每一个页面所下载,由于每一个页面都合并了它。我的是以为不该该将jquery.min.js这样的通用库合并进去的,而是应该放入cnd中,这样既不会受到浏览器访问同一个域名时,并发数量的限制,也可使其可以被缓存。可是 304 好像也是须要发送一次http/https请求的?因此如何取舍呢?CSS文件bootstrap.min.css也遇到类似的取舍问题。

我的倾向于不合并jquery.min.js和bootstrap.min.css等相似的基础文件。因此最佳require.js的实践就是,每一个页面只引入一个js文件,该js文件不合并jquery.min.js文件以及相似的js文件,合并其它全部依赖的js文件。每一个页面除了bootstrap.min.css相似的基础文件须要的<link >以外,还引入一个合并其它全部须要的css文件的<link>标签。

7. 关于压缩

关于压缩,上面说到了使用 r.js 进行压缩是指去掉空格,空行,将长变量名换成短的等等;压缩还有另一层压缩:配置tomcat或者nginx/apache等web服务器,也是能够配置对CSS/JS/HTML等进行压缩的,通常浏览器都支持(能解压)服务器端进行的gzip压缩。