<h2 id="10.1">Underscore.js</h2>
## 概述
[Underscore.js](http://underscorejs.org/)是一个很精干的库,压缩后只有4KB。它提供了几十种函数式编程的方法,弥补了标准库的不足,大大方便了JavaScript的编程。MVC框架Backbone.js就将这个库作为自己的工具库。除了可以在浏览器环境使用,Underscore.js还可以用于Node.js。
Underscore.js定义了一个下划线(_)对象,函数库的所有方法都属于这个对象。这些方法大致上可以分成:集合(collection)、数组(array)、函数(function)、对象(object)和工具(utility)五大类。
## 集合相关方法
Javascript语言的数据集合,包括两种结构:数组和对象。以下的方法同时适用于这两种结构。
### 集合处理
数组处理指的是对数组元素进行加工。
**(1)map**
map方法对集合的每个成员依次进行某种操作,将返回的值依次存入一个新的数组。
```javascript
_.map([1, 2, 3], function(num){ return num * 3; });
// [3, 6, 9]
_.map({one : 1, two : 2, three : 3}, function(num, key){ return num * 3; });
// [3, 6, 9]
```
**(2)each**
each方法与map类似,依次对数组所有元素进行某种操作,不返回任何值。
```javascript
_.each([1, 2, 3], alert);
_.each({one : 1, two : 2, three : 3}, alert);
```
**(3)reduce**
reduce方法依次对集合的每个成员进行某种操作,然后将操作结果累计在某一个初始值之上,全部操作结束之后,返回累计的值。该方法接受三个参数。第一个参数是被处理的集合,第二个参数是对每个成员进行操作的函数,第三个参数是累计用的变量。
```javascript
_.reduce([1, 2, 3], function(memo, num){ return memo + num; }, 0);
// 6
```
reduce方法的第二个参数是操作函数,它本身又接受两个参数,第一个是累计用的变量,第二个是集合每个成员的值。
**(4)reduceRight**
reduceRight是逆向的reduce,表示从集合的最后一个元素向前进行处理。
```javascript
var list = [[0, 1], [2, 3], [4, 5]];
var flat = _.reduceRight(list, function(a, b) { return a.concat(b); }, []);
// [4, 5, 2, 3, 0, 1]
```
**(5)shuffle**
shuffle方法返回一个打乱次序的集合。
```javascript
_.shuffle([1, 2, 3, 4, 5, 6]);
// [4, 1, 6, 3, 5, 2]
```
**(6)invoke**
invoke方法对集合的每个成员执行指定的操作。
```javascript
_.invoke([[5, 1, 7], [3, 2, 1]], 'sort')
// [[1, 5, 7], [1, 2, 3]]
```
**(7)sortBy**
sortBy方法根据处理函数的返回值,返回一个排序后的集合,以升序排列。
```javascript
_.sortBy([1, 2, 3, 4, 5, 6], function(num){ return Math.sin(num); });
// [5, 4, 6, 3, 1, 2]
```
**(8)indexBy**
indexBy方法返回一个对象,根据指定键名,对集合生成一个索引。
```javascript
var person = [{name: 'John', age: 40}, {name: 'larry', age: 50}, {name: 'curly', age: 60}];
_.indexBy(person, 'age');
// { "50": {name: 'larry', age: 50},
"60": {name: 'curly', age: 60} }
```
### 集合特征
Underscore.js提供了一系列方法,判断数组元素的特征。这些方法都返回一个布尔值,表示是否满足条件。
**(1)every**
every方法判断数组的所有元素是否都满足某个条件。如果都满足则返回true,否则返回false。
```javascript
_.every([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });
// false
```
**(2)some**
some方法则是只要有一个元素满足,就返回true,否则返回false。
```javascript
_.some([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });
// true
_.some([null, 0, 'yes', false])
// true
```
**(3)size**
size方法返回集合的成员数量。
```javascript
_.size({one : 1, two : 2, three : 3});
// 3
```
**(4)sample**
sample方法用于从集合中随机取样。
```javascript
_.sample([1, 2, 3, 4, 5, 6])
// 4
```
### 集合过滤
Underscore.js提供了一系列方法,用于过滤数组,找到符合要求的成员。
**(1)filter**
filter方法依次对集合的每个成员进行某种操作,只返回操作结果为true的成员。
```javascript
_.filter([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });
// [2, 4, 6]
```
**(2)reject**
reject方法只返回操作结果为false的成员。
```javascript
_.reject([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });
// [1, 3, 5]
```
**(3)find**
find方法依次对集合的每个成员进行某种操作,返回第一个操作结果为true的成员。如果所有成员的操作结果都为false,则返回undefined。
```javascript
_.find([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });
// 2
```
**(4)contains**
contains方法表示如果某个值在数组内,则返回true,否则返回false。
```javascript
_.contains([1, 2, 3], 3);
// true
```
**(5)countBy**
countBy方法依次对集合的每个成员进行某种操作,将操作结果相同的成员算作一类,最后返回一个对象,表明每种操作结果对应的成员数量。
```javascript
_.countBy([1, 2, 3, 4, 5], function(num) {
return num % 2 == 0 ? 'even' : 'odd';
});
// {odd: 3, even: 2}
```
**(6)where**
where方法检查集合中的每个值,返回一个数组,其中的每个成员都包含指定的键值对。
```javascript
_.where(listOfPlays, {author: "Shakespeare", year: 1611});
// [{title: "Cymbeline", author: "Shakespeare", year: 1611},
// {title: "The Tempest", author: "Shakespeare", year: 1611}]
```
**(7)max,min**
max方法返回集合中的最大值。如果提供一个处理函数,则该函数的返回值用作排名标准。
```javascript
var person = [{name: 'John', age: 40},
{name: 'larry', age: 50},
{name: 'curly', age: 60}];
_.max(person, function(per){ return per.age; });
// {name: 'curly', age: 60};
```
min方法返回集合中的最小值。如果提供一个处理函数,则该函数的返回值用作排名标准。
```javascript
var numbers = [10, 5, 100, 2, 1000];
_.min(numbers)
// 2
```
## 对象相关方法
**(1)toArray**
toArray方法将对象转为数组,只包含对象成员的值。典型应用是将对类似数组的对象转为真正的数组。
```javascript
_.toArray({a:0,b:1,c:2});
// [0, 1, 2]
```
**(2)pluck**
pluck方法将多个对象的某一个属性的值,提取成一个数组。
```javascript
var person = [{name: 'John', age: 40},
{name: 'larry', age: 50},
{name: 'curly', age: 60}];
_.pluck(person, 'name');
// ["moe", "larry", "curly"]
```
## 与函数相关的方法
### 绑定运行环境和参数
在不同的运行环境下,JavaScript函数内部的变量所在的上下文是不同的。这种特性会给程序带来不确定性,为了解决这个问题,Underscore.js提供了两个方法,用来给函数绑定上下文。
**(1)bind方法**
该方法绑定函数运行时的上下文,返回一个新函数。
```javascript
var o = {
p: 2,
m: function (){console.log(this.p);}
};
o.m()
// 2
_.bind(o.m,{p:1})()
// 1
```
上面代码将o.m方法绑定到一个新的对象上面。
除了前两个参数以外,bind方法还可以接受更多参数,它们表示函数方法运行时所需的参数。
```javascript
var add = function(n1,n2,n3) {
console.log(this.sum + n1 + n2 + n3);
};
_.bind(add, {sum:1}, 1, 1, 1)()
// 4
```
上面代码中bind方法有5个参数,最后那三个是给定add方法的运行参数,所以运行结果为4。
**(2)bindall方法**
该方法可以一次将多个方法,绑定在某个对象上面。
```javascript
var o = {
p1 : '123',
p2 : '456',
m1 : function() { console.log(this.p1); },
m2 : function() { console.log(this.p2); },
};
_.bindAll(o, 'm1', 'm2');
```
上面代码一次性将两个方法(m1和m2)绑定在o对象上面。
**(3)partial方法**
除了绑定上下文,Underscore.js还允许绑定参数。partial方法将函数与某个参数绑定,然后作为一个新函数返回。
```javascript
var add = function(a, b) { return a + b; };
add5 = _.partial(add, 5);
add5(10);
// 15
```
**(4)wrap方法**
该方法将一个函数作为参数,传入另一个函数,最终返回前者的一个新版本。
```javascript
var hello = function(name) { return "hello: " + name; };
hello = _.wrap(hello, function(func) {
return "before, " + func("moe") + ", after";
});
hello();
// 'before, hello: moe, after'
```
上面代码先定义hello函数,然后将hello传入一个匿名定义,返回一个新版本的hello函数。
**(5)compose方法**
该方法接受一系列函数作为参数,由后向前依次运行,上一个函数的运行结果,作为后一个函数的运行参数。也就是说,将f(g(),h())的形式转化为f(g(h()))。
```javascript
var greet = function(name){ return "hi: " + name; };
var exclaim = function(statement){ return statement + "!"; };
var welcome = _.compose(exclaim, greet);
welcome('moe');
// 'hi: moe!'
```
上面代码调用welcome时,先运行greet函数,再运行exclaim函数。并且,greet函数的运行结果是exclaim函数运行时的参数。
### 函数运行控制
Underscore.js允许对函数运行行为进行控制。
**(1)memoize方法**
该方法缓存一个函数针对某个参数的运行结果。
```javascript
var fibonacci = _.memoize(function(n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
});
```
**(2)delay方法**
该方法可以将函数推迟指定的时间再运行。
```javascript
var log = _.bind(console.log, console);
_.delay(log, 1000, 'logged later');
// 'logged later'
```
上面代码推迟1000毫秒,再运行console.log方法,并且指定参数为“logged later”。
**(3)defer方法**
该方法可以将函数推迟到待运行的任务数为0时再运行,类似于setTimeout推迟0秒运行的效果。
```javascript
_.defer(function(){ alert('deferred'); });
```
**(4)throttle方法**
该方法返回一个函数的新版本。连续调用这个新版本的函数时,必须等待一定时间才会触发下一次执行。
```javascript
// 返回updatePosition函数的新版本
var throttled = _.throttle(updatePosition, 100);
// 新版本的函数每过100毫秒才会触发一次
$(window).scroll(throttled);
```
**(5)debounce方法**
该方法返回的新函数有调用的时间限制,每次调用必须与上一次调用间隔一定的时间,否则就无效。它的典型应用是防止用户双击某个按钮,导致两次提交表单。
```javascript
$("button").on("click", _.debounce(submitForm, 1000, true));
```
上面代码表示click事件发生后,调用函数submitForm的新版本。该版本的两次运行时间,必须间隔1000毫秒以上,否则第二次调用无效。最后那个参数true,表示click事件发生后,立刻触发第一次submitForm函数,否则就是等1000毫秒再触发。
**(6)once方法**
该方法返回一个只能运行一次的新函数。该方法主要用于对象的初始化。
```javascript
var initialize = _.once(createApplication);
initialize();
initialize();
// Application只被创造一次
```
**(7)after方法**
该方法返回的新版本函数,只有在被调用一定次数后才会运行,主要用于确认一组操作全部完成后,再做出反应。
```javascript
var renderNotes = _.after(notes.length, render);
_.each(notes, function(note) {
note.asyncSave({success: renderNotes});
});
```
上面代码表示,函数renderNotes是函数render的新版本,只有调用notes.length次以后才会运行。所以,后面就可以放心地等到notes的每个成员都处理完,才会运行一次renderNotes。
## 工具方法
### 链式操作
Underscore.js允许将多个操作写成链式的形式。
```javascript
_.(users)
.filter(function(user) { return user.name === name })
.sortBy(function(user) { return user.karma })
.first()
.value()
```
### template
该方法用于编译HTML模板。它接受三个参数。
```javascript
_.template(templateString, [data], [settings])
```
三个参数的含义如下:
- templateString:模板字符串
- data:输入模板的数据
- settings:设置
**(1)templateString**
模板字符串templateString就是普通的HTML语言,其中的变量使用<%= … %>的形式插入;data对象负责提供变量的值。
```javascript
var txt = "<h2><%= word %></h2>";
_.template(txt, {word : "Hello World"})
// "<h2>Hello World</h2>"
```
如果变量的值包含五个特殊字符(& < > " ' /),就需要用<%- ... %>转义。
```javascript
var txt = "<h2><%- word %></h2>";
_.template(txt, {word : "H & W"})
// <h2>H & W</h2>
```
JavaScript命令可以采用<% … %>的形式插入。下面是判断语句的例子。
```javascript
var txt = "<% var i = 0; if (i<1){ %>"
+ "<%= word %>"
+ "<% } %>";
_.template(txt, {word : "Hello World"})
// Hello World
```
常见的用法还有循环语句。
```javascript
var list = "<% _.each(people, function(name) { %> <li><%= name %></li> <% }); %>";
_.template(list, {people : ['moe', 'curly', 'larry']});
// "<li>moe</li><li>curly</li><li>larry</li>"
```
如果template方法只有第一个参数templateString,省略第二个参数,那么会返回一个函数,以后可以向这个函数输入数据。
```javascript
var t1 = _.template("Hello <%=user%>!");
t1({ user: "<Jane>" })
// 'Hello <Jane>!'
```
** (2)data **
templateString中的所有变量,在内部都是obj对象的属性,而obj对象就是指第二个参数data对象。下面两句语句是等同的。
```javascript
_.template("Hello <%=user%>!", { user: "<Jane>" })
_.template("Hello <%=obj.user%>!", { user: "<Jane>" })
```
如果要改变obj这个对象的名字,需要在第三个参数中设定。
```javascript
_.template("<%if (data.title) { %>Title: <%= title %><% } %>", null,
{ variable: "data" });
```
因为template在变量替换时,内部使用with语句,所以上面这样的做法,运行速度会比较快。
<h2 id="10.2">Modernizr</h2>
## 概述
随着HTML5和CSS3加入越来越多的模块,检查各种浏览器是否支持这些模块,成了一大难题。Modernizr就是用来解决这个问题的一个JavaScript库。
首先,从modernizr.com下载这个库。下载的时候,可以选择所需要的模块。然后,将它插入HTML页面的头部,放在head标签之中。
```html
<!DOCTYPE html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8">
<script src="js/modernizr.js"></script>
</head>
</html>
```
## CSS的新增class
使用Modernizr以后,首先会把html元素的class替换掉。以chrome浏览器为例,新增的class大概是下面的样子。
```html
<html class="js no-touch postmessage history multiplebgs boxshadow opacity cssanimations csscolumns cssgradients csstransforms csstransitions fontface localstorage sessionstorage svg inlinesvg blobbuilder blob bloburls download formdata">
```
IE 7则是这样:
```html
<html class="js no-touch postmessage no-history no-multiplebgs no-boxshadow no-opacity no-cssanimations no-csscolumns no-cssgradients no-csstransforms no-csstransitions fontface localstorage sessionstorage no-svg no-inlinesvg wf-loading no-blobbuilder no-blob no-bloburls no-download no-formdata">
```
然后,就可以针对不同的CSS class,指定不同的样式。
```css
.button {
background: #000;
opacity: 0.75;
}
.no-opacity .button {
background: #444;
}
```
## JavaScript侦测
除了提供新增的CSS class,Modernizr还提供JavaScript方法,用来侦测浏览器是否支持某个功能。
```javascript
Modernizr.cssgradients; //True in Chrome, False in IE7
Modernizr.fontface; //True in Chrome, True in IE7
Modernizr.geolocation; //True in Chrome, False in IE7
if (Modernizr.canvas){
// 支持canvas
} else {
// 不支持canvas
}
if (Modernizr.touch){
// 支持触摸屏
} else {
// 不支持触摸屏
}
```
## 加载器
Modernizr允许根据Javascript侦测的不同结果,加载不同的脚本文件。
```javascript
Modernizr.load({
test : Modernizr.localstorage,
yep : 'localStorage.js',
nope : 'alt-storageSystem.js',
complete : function () { enableStorgeSaveUI();}
});
```
Modernizr.load方法用来加载脚本。它的属性如下:
- test:用来测试浏览器是否支持某个属性。
- yep:如果浏览器支持该属性,加载的脚本。
- nope:如果浏览器不支持该属性,加载的脚本。
- complete:加载完成后,运行的JavaScript代码。
可以指定在支持某个功能的情况,所要加载的JavaScript脚本和CSS样式。
```javascript
Modernizr.load({
test : Modernizr.touch,
yep : ['js/touch.js', 'css/touchStyles.css']
});
```
<h2 id="10.3">Datejs</h2>
## 概述
Datejs是一个用来操作日期的库,官方网站为[datejs.com](http://www.datejs.com/)。
下载后插入网页,就可以使用。
```html
<script type="text/javascript" src="date.js"></script>
```
官方还提供多种语言的版本,可以选择使用。
```html
// 美国版
<script type="text/javascript" src="date-en-US.js"></script>
// 中国版
<script type="text/javascript" src="date-zh-CN.js"></script>
```
## 方法
Datejs在原生的Date对象上面,定义了许多语义化的方法,可以方便地链式使用。
### 日期信息
```javascript
Date.today() // 返回当天日期,时间定在这一天开始的00:00
Date.today().getDayName() // 今天是星期几
Date.today().is().friday() // 今天是否为星期五,返回true或者false
Date.today().is().fri() // 等同于上一行
Date.today().is().november() // 今天是否为11月,返回true或者false
Date.today().is().nov() // 等同于上一行
Date.today().isWeekday() // 今天是否为工作日(周一到周五)
```
### 日期的变更
```javascript
Date.today().next().friday() // 下一个星期五
Date.today().last().monday() // 上一个星期一
new Date().next().march() // 下个三月份的今天
new Date().last().week() // 上星期的今天
Date.today().add(5).days() // 五天后
Date.friday() // 本周的星期五
Date.march() // 今年的三月
Date.january().first().monday() // 今年一月的第一个星期一
Date.dec().final().fri() // 今年12月的最后一个星期五
// 先将日期定在本月15日的下午4点30分,然后向后推90天
Date.today().set({ day: 15, hour: 16, minute: 30 }).add({ days: 90 })
(3).days().fromNow() // 三天后
(6).months().ago() // 6个月前
(12).weeks().fromNow() // 12个星期后
(30).days().after(Date.today()) // 30天后
```
### 日期的解析
```javascript
Date.parse('today')
Date.parse('tomorrow')
Date.parse('July 8')
Date.parse('July 8th, 2007')
Date.parse('July 8th, 2007, 10:30 PM')
Date.parse('07.15.2007')
```
<h2 id="10.4">D3.js</h2>
D3.js是一个用于网页作图、生成互动图形的JavaScript函数库。它提供一个d3对象,所有方法都通过这个对象调用。
## 操作网页元素
D3提供了一系列操作网页元素的方法,很类似jQuery,也是先选中某个元素(select方法),然后对其进行某种操作。
```javascript
var body = d3.select("body");
var div = body.append("div");
div.html("Hello, world!");
```
select方法用于选中一个元素,而selectAll方法用于选中一组元素。
```javascript
var section = d3.selectAll("section");
var div = section.append("div");
div.html("Hello, world!");
```
大部分D3的方法都返回D3对象的实例,这意味着可以采用链式写法。
```javascript
d3.select("body")
.style("color", "black")
.style("background-color", "white");
```
需要注意的是append方法返回一个新对象。
```javascript
d3.selectAll("section")
.attr("class", "special")
.append("div")
.html("Hello, world!");
```
## 生成svg元素
D3作图需要svg元素,可以用JavaScript代码动态生成。
```javascript
var v = d3.select("#graph")
.append("svg");
v.attr("width", 900).attr("height", 400);
```
## 生成图形
### 选中对象集
selectAll方法不仅可以选中现有的网页元素,还可以选中不存在的网页元素。
```javascript
d3.select(".chart")
.selectAll("div");
```
上面代码表示,selectAll方法选中了.chart元素下面所有现有和将来可能出现的div元素。
### 绑定数据
data方法用于对选中的结果集绑定数据。
```javascript
var data = [4, 8, 15, 16, 23, 42, 12];
d3.select(".chart")
.selectAll("div")
.data(data)
.enter().append("div")
.style("width", function(d) { return d * 10 + "px"; })
.text(function(d) { return d; });
```
上面代码中,enter方法和append方法表示由于此时div元素还不存在,必须根据数据的个数将它们创造出来。style方法和text方法的参数是函数,表示函数的运行结果就是设置网页元素的值。
上面代码的运行结果是生成一个条状图,但是没有对条状图的长度进行控制,下面采用scale.linear方法对数据长度进行设置。
```javascript
var data = [4, 8, 15, 16, 23, 42, 12];
var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, 420]);
d3.select(".chart")
.selectAll("div")
.data(data)
.enter().append("div")
.style("width", function(d) { return x(d) + "px"; })
.text(function(d) { return d; });
```
## 操作SVG图形
使用SVG图形生成条形图,首先是选中矢量图格式,然后每个数据值生成一个g元素(group),再在每个g元素内部生成一个rect元素和text元素。
```javascript
var width = 840,
barHeight = 20;
var x = d3.scale.linear()
.domain([0, d3.max(dataArray)])
.range([0, width]);
var chart = d3.select(".bar-chart-svg")
.attr("width", width)
.attr("height", barHeight * dataArray.length);
var bar = chart.selectAll("g")
.data(dataArray)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
bar.append("rect")
.attr("width", x)
.attr("height", barHeight - 1);
bar.append("text")
.attr("x", function(d) { return x(d) - 3; })
.attr("y", barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d; });
```
## 加载XML文件
```javascript
d3.xml('example', 'image/svg+xml', function (error, data) {
if (error) {
console.log('加载SVG文件出错!', error);
}
else {
// 处理SVG文件
}
});
```
<h2 id="10.5">设计模式</h2>
"设计模式"(Design Pattern)是针对编程中经常出现的、具有共性的问题,所提出的解决方法。著名的《设计模式》一书一共提出了23种模式。
## Singleton
Singleton模式指的是一个“类”只能创造一个实例。由于JavaScript语言没有类,单个对象可以直接生成,所以实际上,没有必要部署Singleton模式。但是,还是可以做到的。
```javascript
var someClass = {
_singleton: null,
getSingleton: function() {
if (!this._singleton) {
this._singleton = {
// some code here
}
}
return this._singleton;
}
};
var instance = someClass.getSingleton();
```
生成实例的时候,调用getSingleton方法。该方法首先检查_singleton属性是否有值,如果有值就返回这个属性,如果为空则生成新的实例,并赋值给_singleton属性,然后返回这个实例。这样就保证了生成的实例都是同一个对象。
为了保证实例不被改写,可以关闭它的写入开关。
```javascript
Object.defineProperty(namespace, "singleton",
{ writable: false, configurable: false, value: { ... } });
```
也可以考虑使用Object.preventExtensions()、Object.seal()、Object.freeze()等方法,限制对实例进行写操作。
<h2 id="10.6">排序算法</h2>
排序算法是将一系列的值按照顺序进行排列的方法。
## 冒泡排序
### 简介
冒泡排序(Bubble Sort)是最易懂的排序算法,但是效率较低,生产环境中很少使用。
它的基本思想是:
1. 依次比较相邻的两个数,如果不符合排序规则,则调换两个数的位置。这样一遍比较下来,能够保证最大(或最小)的数排在最后一位。
2. 再对最后一位以外的数组,重复前面的过程,直至全部排序完成。
由于每进行一次这个过程,在该次比较的最后一个位置上,正确的数会自己冒出来,就好像“冒泡”一样,这种算法因此得名。
以对数组[3, 2, 4, 5, 1] 进行从小到大排序为例,步骤如下:
1. 第一位的“3”与第二位的“2”进行比较,3大于2,互换位置,数组变成[2, 3, 4, 5, 1] 。
2. 第二位的“3”与第三位的“4”进行比较,3小于4,数组不变。
3. 第三位的“4”与第四位的“5”进行比较,4小于5,数组不变。
4. 第四位的“5”与第五位的“1”进行比较,5大于1,互换位置,数组变成[2, 3, 4, 1, 5] 。
第一轮排序完成,可以看到最后一位的5,已经是正确的数了。然后,再对剩下的数[2, 3, 4, 1] 重复这个过程,每一轮都会在本轮最后一位上出现正确的数。直至剩下最后一个位置,所有排序结束。
### 算法实现
先定义一个交换函数,作用是交换两个位置的值。
```javascript
function swap(myArray, p1, p2){
var temp = myArray[p1];
myArray[p1] = myArray[p2];
myArray[p2] = temp;
}
```
然后定义主函数。
```javascript
function bubbleSort(myArray){
var len = myArray.length,
i, j, stop;
for (i=0; i < len; i++){
for (j=0, stop=len-1-i; j < stop; j++){
if (myArray[j] > myArray[j+1]){
swap(myArray, j, j+1);
}
}
}
return myArray;
}
```
## 选择排序
### 简介
选择排序(Selection Sort)与冒泡排序类似,也是依次对相邻的数进行两两比较。不同之处在于,它不是每比较一次就调换位置,而是一轮比较完毕,找到最大值(或最小值)之后,将其放在正确的位置,其他数的位置不变。
以对数组[3, 2, 4, 5, 1] 进行从小到大排序为例,步骤如下:
1. 假定第一位的“3”是最小值。
2. 最小值“3”与第二位的“2”进行比较,2小于3,所以新的最小值是第二位的“2”。
3. 最小值“2”与第三位的“4”进行比较,2小于4,最小值不变。
4. 最小值“2”与第四位的“5”进行比较,2小于5,最小值不变。
5. 最小值“2”与第五位的“1”进行比较,1小于2,所以新的最小值是第五位的“1”。
6. 第五位的“1”与第一位的“3”互换位置,数组变为[1, 2, 4, 5, 3]。
这一轮比较结束后,最小值“1”已经排到正确的位置了,然后对剩下的[2, 4, 5, 3]重复上面的过程。每一轮排序都会将该轮的最小值排到正确的位置,直至剩下最后一个位置,所有排序结束。
### 算法实现
先定义一个交换函数。
```javascript
function swap(myArray, p1, p2){
var temp = myArray[p1];
myArray[p1] = myArray[p2];
myArray[p2] = temp;
}
```
然后定义主函数。
```javascript
function selectionSort(myArray){
var len = myArray.length,
min;
for (i=0; i < len; i++){
// 将当前位置设为最小值
min = i;
// 检查数组其余部分是否更小
for (j=i+1; j < len; j++){
if (myArray[j] < myArray[min]){
min = j;
}
}
// 如果当前位置不是最小值,将其换为最小值
if (i != min){
swap(myArray, i, min);
}
}
return myArray;
}
```
## 插入排序
### 简介
插入排序(insertion sort)比前面两种排序方法都更有效率。它将数组分成“已排序”和“未排序”两部分,一开始的时候,“已排序”的部分只有一个元素,然后将它后面一个元素从“未排序”部分插入“已排序”部分,从而“已排序”部分增加一个元素,“未排序”部分减少一个元素。以此类推,完成全部排序。
以对数组[3, 2, 4, 5, 1] 进行从小到大排序为例,步骤如下:
1. 将数组分成[3]和[2, 4, 5, 1]两部分,前者是已排序的,后者是未排序的。
2. 取出未排序部分的第一个元素“2”,与已排序部分最后一个元素“3”比较,因为2小于3,所以2排在3前面,整个数组变成[2, 3]和[4, 5, 1]两部分。
3. 取出未排序部分的第一个元素“4”,与已排序部分最后一个元素“3”比较,因为4大于3,所以4排在3后面,整个数组变成[2, 3, 4]和[5, 1]两部分。
4. 取出未排序部分的第一个元素“5”,与已排序部分最后一个元素“4”比较,因为5大于4,所以5排在4后面,整个数组变成[2, 3, 4, 5]和[1]两部分。
5. 取出未排序部分的第一个元素“1”,与已排序部分最后一个元素“5”比较,因为1小于5,所以再与前一个元素“4”比较;因为1小于4,再与前一个元素“3”比较;因为1小于3,再与前一个元素“2”比较;因为小于1小于2,所以“1”排在2的前面,整个数组变成[1, 2, 3, 4, 5]。
### 算法实现
算法的实现如下:
```javascript
function insertionSort(myArray) {
var len = myArray.length, // 数组的长度
value, // 当前比较的值
i, // 未排序部分的当前位置
j; // 已排序部分的当前位置
for (i=0; i < len; i++) {
// 储存当前位置的值
value = myArray[i];
/*
* 当已排序部分的当前元素大于value,
* 就将当前元素向后移一位,再将前一位与value比较
*/
for (j=i-1; j > -1 && myArray[j] > value; j--) {
myArray[j+1] = myArray[j];
}
myArray[j+1] = value;
}
return myArray;
}
```
## 合并排序
### 简介
前面三种排序算法只有教学价值,因为效率低,很少实际使用。合并排序(Merge sort)则是一种被广泛使用的排序方法。
它的基本思想是,将两个已经排序的数组合并,要比从头开始排序所有元素来得快。因此,可以将数组拆开,分成n个只有一个元素的数组,然后不断地两两合并,直到全部排序完成。
以对数组[3, 2, 4, 5, 1] 进行从小到大排序为例,步骤如下:
1. 将数组分成[3, 2, 4]和[5, 1]两部分。
2. 将[3, 2, 4]分成[3, 2]和[4]两部分。
3. 将[3, 2]分成[3]和[2]两部分,然后合并成[2, 3]。
4. 将[2, 3]和[4]合并成[2, 3, 4]。
5. 将[5, 1]分成[5]和[1]两部分,然后合并成[1, 5]。
6. 将[2, 3, 4]和[1, 5]合并成[1, 2, 3, 4, 5]。
### 算法实现
这里的关键是如何合并两个已经排序的数组。具体实现请看下面的函数。
```javascript
function merge(left, right){
var result = [],
il = 0,
ir = 0;
while (il < left.length && ir < right.length){
if (left[il] < right[ir]){
result.push(left[il++]);
} else {
result.push(right[ir++]);
}
}
return result.concat(left.slice(il)).concat(right.slice(ir));
}
```
上面的merge函数,合并两个已经按升序排好序的数组。首先,比较两个数组的第一个元素,将其中较小的一个放入result数组;然后,将其中较大的一个与另一个数组的第二个元素进行比较,再将其中较小的一个放入result数组的第二个位置。以此类推,直到一个数组的所有元素都进入result数组为止,再将另一个数组剩下的元素接着result数组后面返回(使用concat方法)。
有了merge函数,就可以对任意数组排序了。基本方法是将数组不断地拆成两半,直到每一半只包含零个元素或一个元素为止,然后就用merge函数,将拆成两半的数组不断合并,直到合并成一整个排序完成的数组。
```javascript
function mergeSort(myArray){
if (myArray.length < 2) {
return myArray;
}
var middle = Math.floor(myArray.length / 2),
left = myArray.slice(0, middle),
right = myArray.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
```
上面的代码有一个问题,就是返回的是一个全新的数组,会多占用空间。因此,修改上面的函数,使之在原地排序,不多占用空间。
```javascript
function mergeSort(myArray){
if (myArray.length < 2) {
return myArray;
}
var middle = Math.floor(myArray.length / 2),
left = myArray.slice(0, middle),
right = myArray.slice(middle),
params = merge(mergeSort(left), mergeSort(right));
// 在返回的数组头部,添加两个元素,第一个是0,第二个是返回的数组长度
params.unshift(0, myArray.length);
// splice用来替换数组元素,它接受多个参数,
// 第一个是开始替换的位置,第二个是需要替换的个数,后面就是所有新加入的元素。
// 因为splice不接受数组作为参数,所以采用apply的写法。
// 这一句的意思就是原来的myArray数组替换成排序后的myArray
myArray.splice.apply(myArray, params);
// 返回排序后的数组
return myArray;
}
```
## 快速排序
### 简介
快速排序(quick sort)是公认最快的排序算法之一,有着广泛的应用。
它的基本思想很简单:先确定一个“支点”(pivot),将所有小于“支点”的值都放在该点的左侧,大于“支点”的值都放在该点的右侧,然后对左右两侧不断重复这个过程,直到所有排序完成。
具体做法是:
1. 确定“支点”(pivot)。虽然数组中任意一个值都能作为“支点”,但通常是取数组的中间值。
2. 建立两端的指针。左侧的指针指向数组的第一个元素,右侧的指针指向数组的最后一个元素。
3. 左侧指针的当前值与“支点”进行比较,如果小于“支点”则指针向后移动一位,否则指针停在原地。
4. 右侧指针的当前值与“支点”进行比较,如果大于“支点”则指针向前移动一位,否则指针停在原地。
5. 左侧指针的位置与右侧指针的位置进行比较,如果前者大于等于后者,则本次排序结束;否则,左侧指针的值与右侧指针的值相交换。
6. 对左右两侧重复第2至5步。
以对数组[3, 2, 4, 5, 1] 进行从小到大排序为例,步骤如下:
1. 选择中间值“4”作为“支点”。
2. 第一个元素3小于4,左侧指针向后移动一位;第二个元素2小于4,左侧指针向后移动一位;第三个元素4等于4,左侧指针停在这个位置(数组的第2位)。
3. 倒数第一个元素1小于4,右侧指针停在这个位置(数组的第4位)。
4. 左侧指针的位置(2)小于右侧指针的位置(4),两个位置的值互换,数组变成[3, 2, 1, 5, 4]。
5. 左侧指针向后移动一位,第四个元素5大于4,左侧指针停在这个位置(数组的第3位)。
6. 右侧指针向前移动一位,第四个元素5大于4,右侧指针移动向前移动一位,第三个元素1小于4,右侧指针停在这个位置(数组的第3位)。
7. 左侧指针的位置(3)大于右侧指针的位置(2),本次排序结束。
8. 对 [3, 2, 1]和[5, 4]两部分各自不断重复上述步骤,直到排序完成。
### 算法实现
首先部署一个swap函数,用于互换两个位置的值。
```javascript
function swap(myArray, firstIndex, secondIndex){
var temp = myArray[firstIndex];
myArray[firstIndex] = myArray[secondIndex];
myArray[secondIndex] = temp;
}
```
然后,部署一个partition函数,用于完成一轮排序。
```javascript
function partition(myArray, left, right) {
var pivot = myArray[Math.floor((right + left) / 2)],
i = left,
j = right;
while (i <= j) {
while (myArray[i] < pivot) {
i++;
}
while (myArray[j] > pivot) {
j--;
}
if (i <= j) {
swap(myArray, i, j);
i++;
j--;
}
}
return i;
}
```
接下来,就是递归上面的过程,完成整个排序。
```javascript
function quickSort(myArray, left, right) {
if (myArray.length < 2) return myArray;
left = (typeof left !== "number" ? 0 : left);
right = (typeof right !== "number" ? myArray.length - 1 : right);
var index = partition(myArray, left, right);
if (left < index - 1) {
quickSort(myArray, left, index - 1);
}
if (index < right) {
quickSort(myArray, index, right);
}
return myArray;
}
```
<h2 id="10.7">PhantomJS</h2>
## 概述
有时,我们需要浏览器处理网页,但并不需要浏览,比如生成网页的截图、抓取网页数据等操作。[PhantomJS](http://phantomjs.org/)的功能,就是提供一个浏览器环境的命令行接口,你可以把它看作一个“虚拟浏览器”,除了不能浏览,其他与正常浏览器一样。它的内核是WebKit引擎,不提供图形界面,只能在命令行下使用,我们可以用它完成一些特殊的用途。
PhantomJS是二进制程序,需要[安装](http://phantomjs.org/download.html)后使用。
```bash
$ npm install phantomjs -g
```
使用下面的命令,查看是否安装成功。
```bash
$ phantomjs --version
```
## REPL环境
phantomjs提供了一个完整的REPL环境,允许用户通过命令行与PhantomJS互动。键入phantomjs,就进入了该环境。
```bash
$ phantomjs
```
这时会跳出一个phantom提示符,就可以输入Javascript命令了。
```bash
phantomjs> 1+2
3
phantomjs> function add(a,b) { return a+b; }
undefined
phantomjs> add(1,2)
3
```
按ctrl+c可以退出该环境。
下面,我们把上面的add()函数写成一个文件add.js文件。
```javascript
// add.js
function add(a,b){ return a+b; }
console.log(add(1,2));
phantom.exit();
```
上面的代码中,console.log()的作用是在终端窗口显示,phantom.exit()则表示退出phantomjs环境。一般来说,不管什么样的程序,exit这一行都不能少。
现在,运行该程序。
```bash
$ phantomjs add.js
```
终端窗口就会显示结果为3。
下面是更多的例子。
```javascript
phantomjs> phantom.version
{
"major": 1,
"minor": 5,
"patch": 0
}
phantomjs> console.log("phantom is awesome")
phantom is awesome
phantomjs> window.navigator
{
"cookieEnabled": true,
"language": "en-GB",
"productSub": "20030107",
"product": "Gecko",
// ...
}
```
## webpage模块
webpage模块是PhantomJS的核心模块,用于网页操作。
```javascript
var webPage = require('webpage');
var page = webPage.create();
```
上面代码表示加载PhantomJS的webpage模块,并创建一个实例。
下面是webpage实例的属性和方法介绍。
### open()
open方法用于打开具体的网页。
```javascript
var page = require('webpage').create();
page.open('http://slashdot.org', function (s) {
console.log(s);
phantom.exit();
});
```
上面代码中,open()方法,用于打开具体的网页。它接受两个参数。第一个参数是网页的网址,这里打开的是著名新闻网站[Slashdot](http://slashdot.org),第二个参数是回调函数,网页打开后该函数将会运行,它的参数是一个表示状态的字符串,如果打开成功就是success,否则就是fail。
注意,只要接收到服务器返回的结果,PhantomJS就会报告网页打开成功,而不管服务器是否返回404或500错误。
open方法默认使用GET方法,与服务器通信,但是也可以使用其他方法。
```javascript
var webPage = require('webpage');
var page = webPage.create();
var postBody = 'user=username&password=password';
page.open('http://www.google.com/', 'POST', postBody, function(status) {
console.log('Status: ' + status);
// Do other things here...
});
```
上面代码中,使用POST方法向服务器发送数据。open方法的第二个参数用来指定HTTP方法,第三个参数用来指定该方法所要使用的数据。
open方法还允许提供配置对象,对HTTP请求进行更详细的配置。
```javascript
var webPage = require('webpage');
var page = webPage.create();
var settings = {
operation: "POST",
encoding: "utf8",
headers: {
"Content-Type": "application/json"
},
data: JSON.stringify({
some: "data",
another: ["custom", "data"]
})
};
page.open('http://your.custom.api', settings, function(status) {
console.log('Status: ' + status);
// Do other things here...
});
```
### evaluate()
evaluate方法用于打开网页以后,在页面中执行JavaScript代码。
```javascript
var page = require('webpage').create();
page.open(url, function(status) {
var title = page.evaluate(function() {
return document.title;
});
console.log('Page title is ' + title);
phantom.exit();
});
```
网页内部的console语句,以及evaluate方法内部的console语句,默认不会显示在命令行。这时可以采用onConsoleMessage回调函数,上面的例子可以改写如下。
```javascript
var page = require('webpage').create();
page.onConsoleMessage = function(msg) {
console.log('Page title is ' + msg);
};
page.open(url, function(status) {
page.evaluate(function() {
console.log(document.title);
});
phantom.exit();
});
```
上面代码中,evaluate方法内部有console语句,默认不会输出在命令行。这时,可以用onConsoleMessage方法监听这个事件,进行处理。
### includeJs()
includeJs方法用于页面加载外部脚本,加载结束后就调用指定的回调函数。
```javascript
var page = require('webpage').create();
page.open('http://www.sample.com', function() {
page.includeJs("http://path/to/jquery.min.js", function() {
page.evaluate(function() {
$("button").click();
});
phantom.exit()
});
});
```
上面的例子在页面中注入jQuery脚本,然后点击所有的按钮。需要注意的是,由于是异步加载,所以`phantom.exit()`语句要放在`page.includeJs()`方法的回调函数之中,否则页面会过早退出。
### render()
render方法用于将网页保存成图片,参数就是指定的文件名。该方法根据后缀名,将网页保存成不同的格式,目前支持PNG、GIF、JPEG和PDF。
```javascript
var webPage = require('webpage');
var page = webPage.create();
page.viewportSize = { width: 1920, height: 1080 };
page.open("http://www.google.com", function start(status) {
page.render('google_home.jpeg', {format: 'jpeg', quality: '100'});
phantom.exit();
});
```
该方法还可以接受一个配置对象,format字段用于指定图片格式,quality字段用于指定图片质量,最小为0,最大为100。
### viewportSize,zoomFactor
viewportSize属性指定浏览器视口的大小,即网页加载的初始浏览器窗口大小。
```javascript
var webPage = require('webpage');
var page = webPage.create();
page.viewportSize = {
width: 480,
height: 800
};
```
viewportSize的Height字段必须指定,不可省略。
zoomFactor属性用来指定渲染时(render方法和renderBase64方法)页面的放大系数,默认是1(即100%)。
```javascript
var webPage = require('webpage');
var page = webPage.create();
page.zoomFactor = 0.25;
page.render('capture.png');
```
### onResourceRequested
onResourceRequested属性用来指定一个回调函数,当页面请求一个资源时,会触发这个回调函数。它的第一个参数是HTTP请求的元数据对象,第二个参数是发出的网络请求对象。
HTTP请求包括以下字段。
- id:所请求资源的编号
- method:使用的HTTP方法
- url:所请求的资源 URL
- time:一个包含请求时间的Date对象
- headers:HTTP头信息数组
网络请求对象包含以下方法。
- abort():终止当前的网络请求,这会导致调用onResourceError回调函数。
- changeUrl(newUrl):改变当前网络请求的URL。
- setHeader(key, value):设置HTTP头信息。
```javascript
var webPage = require('webpage');
var page = webPage.create();
page.onResourceRequested = function(requestData, networkRequest) {
console.log('Request (#' + requestData.id + '): ' + JSON.stringify(requestData));
};
```
### onResourceReceived
onResourceReceived属性用于指定一个回调函数,当网页收到所请求的资源时,就会执行该回调函数。它的参数就是服务器发来的HTTP回应的元数据对象,包括以下字段。
- id:所请求的资源编号
- url:所请求的资源的URL
- time:包含HTTP回应时间的Date对象
- headers:HTTP头信息数组
- bodySize:解压缩后的收到的内容大小
- contentType:接到的内容种类
- redirectURL:重定向URL(如果有的话)
- stage:对于多数据块的HTTP回应,头一个数据块为start,最后一个数据块为end。
- status:HTTP状态码,成功时为200。
- statusText:HTTP状态信息,比如OK。
如果HTTP回应非常大,分成多个数据块发送,onResourceReceived会在收到每个数据块时触发回调函数。
```javascript
var webPage = require('webpage');
var page = webPage.create();
page.onResourceReceived = function(response) {
console.log('Response (#' + response.id + ', stage "' + response.stage + '"): ' + JSON.stringify(response));
};
```
## system模块
system模块可以加载操作系统变量,system.args就是参数数组。
```javascript
var page = require('webpage').create(),
system = require('system'),
t, address;
// 如果命令行没有给出网址
if (system.args.length === 1) {
console.log('Usage: page.js <some URL>');
phantom.exit();
}
t = Date.now();
address = system.args[1];
page.open(address, function (status) {
if (status !== 'success') {
console.log('FAIL to load the address');
} else {
t = Date.now() - t;
console.log('Loading time ' + t + ' ms');
}
phantom.exit();
});
```
使用方法如下:
```bash
$ phantomjs page.js http://www.google.com
```
## 应用
Phantomjs可以实现多种应用。
### 过滤资源
处理页面的时候,有时不希望加载某些特定资源。这时,可以对URL进行匹配,一旦符合规则,就中断对资源的连接。
```javascript
page.onResourceRequested = function(requestData, request) {
if ((/http:\/\/.+?\.css$/gi).test(requestData['url'])) {
console.log('Skipping', requestData['url']);
request.abort();
}
};
```
上面代码一旦发现加载的资源是CSS文件,就会使用`request.abort`方法中断连接。
### 截图
最简单的生成网页截图的方法如下。
```javascript
var page = require('webpage').create();
page.open('http://google.com', function () {
page.render('google.png');
phantom.exit();
});
```
page对象代表一个网页实例;open方法表示打开某个网址,它的第一个参数是目标网址,第二个参数是网页载入成功后,运行的回调函数;render方法则是渲染页面,然后以图片格式输出,该方法的参数就是输出的图片文件名。
除了简单截图以外,还可以设置各种截图参数。
```javascript
var page = require('webpage').create();
page.open('http://google.com', function () {
page.zoomFactor = 0.25;
console.log(page.renderBase64());
phantom.exit();
});
```
zoomFactor表示将截图缩小至原图的25%大小;renderBase64方法则是表示将截图(PNG格式)编码成Base64格式的字符串输出。
下面的例子则是使用了更多参数。
```javascript
// page.js
var page = require('webpage').create();
page.settings.userAgent = 'WebKit/534.46 Mobile/9A405 Safari/7534.48.3';
page.settings.viewportSize = { width: 400, height: 600 };
page.open('http://slashdot.org', function (status) {
if (status !== 'success') {
console.log('Unable to load!');
phantom.exit();
} else {
var title = page.evaluate(function () {
var posts = document.getElementsByClassName("article");
posts[0].style.backgroundColor = "#FFF";
return document.title;
});
window.setTimeout(function () {
page.clipRect = { top: 0, left: 0, width: 600, height: 700 };
page.render(title + "1.png");
page.clipRect = { left: 0, top: 600, width: 400, height: 600 };
page.render(title + '2.png');
phantom.exit();
}, 1000);
}
});
```
上面代码中的几个属性和方法解释如下:
- settings.userAgent:指定HTTP请求的userAgent头信息,上面例子是手机浏览器的userAgent。
- settings.viewportSize:指定浏览器窗口的大小,这里是400x600。
- evaluate():用来在网页上运行Javascript代码。在这里,我们抓取第一条新闻,然后修改背景颜色,并返回该条新闻的标题。
- clipRect:用来指定网页截图的大小,这里的截图左上角从网页的(0. 0)坐标开始,宽600像素,高700像素。如果不指定这个值,就表示对整张网页截图。
- render():根据clipRect的范围,在当前目录下生成以第一条新闻的名字命名的截图。
### 抓取图片
使用官方网站提供的[rasterize.js](https://github.com/ariya/phantomjs/blob/master/examples/rasterize.js),可以抓取网络上的图片,将起保存在本地。
```javascript
phantomjs rasterize.js http://ariya.github.com/svg/tiger.svg tiger.png
```
使用[rasterize.js](https://github.com/ariya/phantomjs/blob/master/examples/rasterize.js),还可以将网页保存为pdf文件。
```javascript
phantomjs rasterize.js 'http://en.wikipedia.org/w/index.php?title=Jakarta&printable=yes' jakarta.pdf
```
### 生成网页
phantomjs可以生成网页,使用content方法指定网页的HTML代码。
```javascript
var page = require('webpage').create();
page.viewportSize = { width: 400, height : 400 };
page.content = '<html><body><canvas id="surface"></canvas></body></html>';
phantom.exit();
```
官方网站有一个[例子](https://github.com/ariya/phantomjs/blob/master/examples/colorwheel.js),通过创造svg图片,然后截图保存成png文件。
![](https://lh3.googleusercontent.com/-xSIzxPtJULw/TVzeP4NPMDI/AAAAAAAAB10/k-c8jB6I5Cg/s288/colorwheel.png)
<h2 id="10.8">Bower:客户端库管理工具</h2>
## 概述
随着网页功能变得越来越复杂,同一张网页加载多个JavaScript函数库早已是家常便饭。开发者越来越需要一个工具,对浏览器端的各种库进行管理,比如搜索、自动安装\卸载、检查更新、确保依赖关系等等。Bower就是为了解决这个问题而诞生的针对浏览器端的库管理工具。
Bower基于node.js,所以安装之前,必须先确保已安装node.js。
```bash
$ sudo npm install bower --global
```
运行上面的命令以后,Bower就已经安装在你的系统中了。运行帮助命令,查看Bower是否安装成功。
```bash
$ bower help
```
下面的命令可以更新或卸载Bower。
```bash
# 更新
$ sudo npm update -g bower
# 卸载
$ sudo npm uninstall --global bower
```
## 常用操作
### 项目初始化
在项目根目录下,运行下面的命令,进行初始化。
```bash
$ bower init
```
通过回答几个问题,就会自动生成bower.json文件。这是项目的配置文件,下面是一个例子。
```javascript
{
"name": "app-name",
"version": "0.1.0",
"main": ["path/to/app.html", "path/to/app.css", "path/to/app.js"],
"ignore": [".jshintrc","**/*.txt"],
"dependencies": {
"sass-bootstrap": "~3.0.0",
"modernizr": "~2.6.2",
"jquery": "latests"
},
"devDependencies": {"qunit": ">1.11.0"}
}
```
有了bower.json文件以后,就可以用bower install命令,一下子安装所有库。
```bash
$ bower install
```
bower.json文件存放在库的根目录下,它的作用是(1)保存项目的库信息,供项目安装时使用,(2)向Bower.com提交你的库,该网站会读取bower.json,列入在线索引。
```bash
$ bower register <my-package-name> <git-endpoint>
# 实例:在 bower.com 登记jquery
$ bower register jquery git://github.com/jquery/jquery
```
注意,如果你的库与现有的库重名,就会提交失败。
### 库的安装
bower install命令用于安装某个库,需要指明库的名字。
```bash
$ bower install backbone
```
Bower会使用库的名字,去在线索引中搜索该库的网址。某些情况下,如果一个库很新(或者你不想使用默认网址),可能需要我们手动指定该库的网址。
```bash
$ bower install git://github.com/documentcloud/backbone.git
$ bower install http://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone-min.js
$ bower install ./some/path/relative/to/this/directory/backbone.js
```
上面的命令说明,指定的网址可以是github地址、http网址、本地文件。
默认情况下,会安装该库的最新版本,但是也可以手动指定版本号。
```bash
$ bower install jquery-ui#1.10.1
```
上面的命令指定安装jquery-ui的1.10.1版。
如果某个库依赖另一个库,安装时默认将所依赖的库一起安装。比如,jquery-ui依赖jquery,安装时会连jquery一起安装。
安装后的库默认存放在项目的bower_components子目录,如果要指定其他位置,可在.bowerrc文件的directory属性设置。
### 库的搜索和查看
bower search命令用于使用关键字,从在线索引中搜索相关库。
```bash
bower search jquery
```
上面命令会得到下面这样的结果。
```bash
Search results:
jquery git://github.com/components/jquery.git
jquery-ui git://github.com/components/jqueryui
jquery.cookie git://github.com/carhartl/jquery-cookie.git
jquery-placeholder git://github.com/mathiasbynens/jquery-placeholder.git
jquery-file-upload git://github.com/blueimp/jQuery-File-Upload.git
jasmine-jquery git://github.com/velesin/jasmine-jquery
jquery.ui git://github.com/jquery/jquery-ui.git
...
```
bower info命令用于查看某个库的详细信息。
```bash
bower info jquery-ui
```
查看结果会列出该库的依赖关系(dependencies),以及可以得到的版本(Available versions)。
### 库的更新和卸载
bower update用于更新一个库,将其更新为最新版本。
```bash
$ bower update jquery-ui
```
如果不给出库名,则更新所有库。
bower uninstall命令用于卸载指定的库。
```bash
$ bower uninstall jquery-ui
```
注意,默认情况下会连所依赖的库一起卸载。比如,jquery-ui依赖jquery,卸载时会连jquery一起卸载,除非还有别的库依赖jquery。
### 列出所有库
bower list或bower ls命令,用于列出项目所使用的所有库。
```bash
Bower list
Bower ls
```
## 配置文件.bowerrc
项目根目录下(也可以放在用户的主目录下)的.bowerrc文件是Bower的配置文件,它大概像下面这样。
```javascript
{
"directory" : "components",
"json" : "bower.json",
"endpoint" : "https://Bower.herokuapp.com",
"searchpath" : "",
"shorthand_resolver" : ""
}
```
其中的属性含义如下。
- directory:存放库文件的子目录名。
- json:描述各个库的json文件名。
- endpoint:在线索引的网址,用来搜索各种库。
- searchpath:一个数组,储存备选的在线索引网址。如果某个库在endpoint中找不到,则继续搜索该属性指定的网址,通常用于放置某些不公开的库。
- shorthand_resolver:定义各个库名称简写形式。
<h2 id="10.9">Grunt:任务自动管理工具</h2>
在Javascript的开发过程中,经常会遇到一些重复性的任务,比如合并文件、压缩代码、检查语法错误、将Sass代码转成CSS代码等等。通常,我们需要使用不同的工具,来完成不同的任务,既重复劳动又非常耗时。Grunt就是为了解决这个问题而发明的工具,可以帮助我们自动管理和运行各种任务。
简单说,Grunt是一个自动任务运行器,会按照预先设定的顺序自动运行一系列的任务。这可以简化工作流程,减轻重复性工作带来的负担。
## 安装
Grunt基于Node.js,安装之前要先安装Node.js,然后运行下面的命令。
```bash
sudo npm install grunt-cli -g
```
grunt-cli表示安装的是grunt的命令行界面,参数g表示全局安装。
Grunt使用模块结构,除了安装命令行界面以外,还要根据需要安装相应的模块。这些模块应该采用局部安装,因为不同项目可能需要同一个模块的不同版本。
首先,在项目的根目录下,创建一个文本文件package.json,指定当前项目所需的模块。下面就是一个例子。
```javascript
{
"name": "my-project-name",
"version": "0.1.0",
"author": "Your Name",
"devDependencies": {
"grunt": "0.x.x",
"grunt-contrib-jshint": "*",
"grunt-contrib-concat": "~0.1.1",
"grunt-contrib-uglify": "~0.1.0",
"grunt-contrib-watch": "~0.1.4"
}
}
```
上面这个package.json文件中,除了注明项目的名称和版本以外,还在devDependencies属性中指定了项目依赖的grunt模块和版本:grunt核心模块为最新的0.x.x版,jshint插件为最新版本,concat插件不低于0.1.1版,uglify插件不低于0.1.0版,watch插件不低于0.1.4版。
然后,在项目的根目录下运行下面的命令,这些插件就会被自动安装在node_modules子目录。
```bash
npm install
```
上面这种方法是针对已有package.json的情况。如果想要自动生成package.json文件,可以使用npm init命令,按照屏幕提示回答所需模块的名称和版本即可。
```bash
npm init
```
如果已有的package.json文件不包括Grunt模块,可以在直接安装Grunt模块的时候,加上--save-dev参数,该模块就会自动被加入package.json文件。
```bash
npm install <module> --save-dev
```
比如,对应上面package.json文件指定的模块,需要运行以下npm命令。
```bash
npm install grunt --save-dev
npm install grunt-contrib-jshint --save-dev
npm install grunt-contrib-concat --save-dev
npm install grunt-contrib-uglify --save-dev
npm install grunt-contrib-watch --save-dev
```
## 命令脚本文件Gruntfile.js
模块安装完以后,下一步在项目的根目录下,新建脚本文件Gruntfile.js。它是grunt的配置文件,就好像package.json是npm的配置文件一样。Gruntfile.js就是一般的Node.js模块的写法。
```javascript
module.exports = function(grunt) {
// 配置Grunt各种模块的参数
grunt.initConfig({
jshint: { /* jshint的参数 */ },
concat: { /* concat的参数 */ },
uglify: { /* uglify的参数 */ },
watch: { /* watch的参数 */ }
});
// 从node_modules目录加载模块文件
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
// 每行registerTask定义一个任务
grunt.registerTask('default', ['jshint', 'concat', 'uglify']);
grunt.registerTask('check', ['jshint']);
};
```
上面的代码用到了grunt代码的三个方法:
- **grunt.initConfig**:定义各种模块的参数,每一个成员项对应一个同名模块。
- **grunt.loadNpmTasks**:加载完成任务所需的模块。
- **grunt.registerTask**:定义具体的任务。第一个参数为任务名,第二个参数是一个数组,表示该任务需要依次使用的模块。default任务名表示,如果直接输入grunt命令,后面不跟任何参数,这时所调用的模块(该例为jshint,concat和uglify);该例的check任务则表示使用jshint插件对代码进行语法检查。
上面的代码一共加载了四个模块:jshint(检查语法错误)、concat(合并文件)、uglify(压缩代码)和watch(自动执行)。接下来,有两种使用方法。
(1)命令行执行某个模块,比如
```bash
grunt jshint
```
上面代码表示运行jshint模块。
(2)命令行执行某个任务。比如
```bash
grunt check
```
上面代码表示运行check任务。如果运行成功,就会显示“Done, without errors.”。
如果没有给出任务名,只键入grunt,就表示执行默认的default任务。
## Gruntfile.js实例:grunt-contrib-cssmin模块
下面通过cssmin模块,演示如何编写Gruntfile.js文件。cssmin模块的作用是最小化CSS文件。
首先,在项目的根目录下安装该模块。
```bash
npm install grunt-contrib-cssmin --save-dev
```
然后,新建文件Gruntfile.js。
```javascript
module.exports = function(grunt) {
grunt.initConfig({
cssmin: {
minify: {
expand: true,
cwd: 'css/',
src: ['*.css', '!*.min.css'],
dest: 'css/',
ext: '.min.css'
},
combine: {
files: {
'css/out.min.css': ['css/part1.min.css', 'css/part2.min.css']
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.registerTask('default', ['cssmin:minify','cssmin:combine']);
};
```
下面详细解释上面代码中的三个方法,下面一个个来看。
**(1)grunt.loadNpmTasks**
grunt.loadNpmTasks方法载入模块文件。
```javascript
grunt.loadNpmTasks('grunt-contrib-cssmin');
```
你需要使用几个模块,这里就要写几条grunt.loadNpmTasks语句,将各个模块一一加载。
如果加载模块很多,这部分会非常冗长。而且,还存在一个问题,就是凡是在这里加载的模块,也同时出现在package.json文件中。如果使用npm命令卸载模块以后,模块会自动从package.json文件中消失,但是必须手动从Gruntfile.js文件中清除,这样很不方便,一旦忘记,还会出现运行错误。这里有一个解决办法,就是安装load-grunt-tasks模块,然后在Gruntfile.js文件中,用下面的语句替代所有的grunt.loadNpmTasks语句。
```javascript
require('load-grunt-tasks')(grunt);
```
这条语句的作用是自动分析package.json文件,自动加载所找到的grunt模块。
**(2)grunt.initConfig**
grunt.initConfig方法用于模块配置,它接受一个对象作为参数。该对象的成员与使用的同名模块一一对应。由于我们要配置的是cssmin模块,所以里面有一个cssmin成员(属性)。
cssmin(属性)指向一个对象,该对象又包含多个成员。除了一些系统设定的成员(比如options),其他自定义的成员称为目标(target)。一个模块可以有多个目标(target),上面代码里面,cssmin模块共有两个目标,一个是“minify”,用于压缩css文件;另一个是“combine”,用于将多个css文件合并一个文件。
每个目标的具体设置,需要参考该模板的文档。就cssmin来讲,minify目标的参数具体含义如下:
- **expand**:如果设为true,就表示下面文件名的占位符(即\*号)都要扩展成具体的文件名。
- **cwd**:需要处理的文件(input)所在的目录。
- **src**:表示需要处理的文件。如果采用数组形式,数组的每一项就是一个文件名,可以使用通配符。
- **dest**:表示处理后的文件名或所在目录。
- **ext**:表示处理后的文件后缀名。
除了上面这些参数,还有一些参数也是grunt所有模块通用的。
- **filter**:一个返回布尔值的函数,用于过滤文件名。只有返回值为true的文件,才会被grunt处理。
- **dot**:是否匹配以点号(.)开头的系统文件。
- **makeBase**:如果设置为true,就只匹配文件路径的最后一部分。比如,a?b可以匹配/xyz/123/acb,而不匹配/xyz/acb/123。
关于通配符,含义如下:
- \*:匹配任意数量的字符,不包括/。
- ?:匹配单个字符,不包括/。
- \*\*:匹配任意数量的字符,包括/。
- {}:允许使用逗号分隔的列表,表示“or”(或)关系。
- !:用于模式的开头,表示只返回不匹配的情况。
比如,foo/\*.js匹配foo目录下面的文件名以.js结尾的文件,foo/\*\*/\*.js匹配foo目录和它的所有子目录下面的文件名以.js结尾的文件,!\*.css表示匹配所有后缀名不为“.css”的文件。
使用通配符设置src属性的更多例子:
```javascript
{src: 'foo/th*.js'}grunt-contrib-uglify
{src: 'foo/{a,b}*.js'}
{src: ['foo/a*.js', 'foo/b*.js']}
```
至于combine目标,就只有一个files参数,表示输出文件是css子目录下的out.min.css,输入文件则是css子目录下的part1.min.css和part2.min.css。
files参数的格式可以是一个对象,也可以是一个数组。
```javascript
files: {
'dest/b.js': ['src/bb.js', 'src/bbb.js'],
'dest/b1.js': ['src/bb1.js', 'src/bbb1.js'],
},
// or
files: [
{src: ['src/aa.js', 'src/aaa.js'], dest: 'dest/a.js'},
{src: ['src/aa1.js', 'src/aaa1.js'], dest: 'dest/a1.js'},
],
```
如果minify目标和combine目标的属性设置有重合的部分,可以另行定义一个与minify和combine平行的options属性。
```javascript
grunt.initConfig({
cssmin: {
options: { /* ... */ },
minify: { /* ... */ },
combine: { /* ... */ }
}
});
```
**(3)grunt.registerTask**
grunt.registerTask方法定义如何调用具体的任务。“default”任务表示如果不提供参数,直接输入grunt命令,则先运行“cssmin:minify”,后运行“cssmin:combine”,即先压缩再合并。如果只执行压缩,或者只执行合并,则需要在grunt命令后面指明“模块名:目标名”。
```bash
grunt # 默认情况下,先压缩后合并
grunt cssmin:minify # 只压缩不合并
grunt css:combine # 只合并不压缩
```
如果不指明目标,只是指明模块,就表示将所有目标依次运行一遍。
```bash
grunt cssmin
```
## 常用模块设置
grunt的[模块](http://gruntjs.com/plugins)已经超过了2000个,且还在快速增加。下面是一些常用的模块(按字母排序)。
- **grunt-contrib-clean**:删除文件。
- **grunt-contrib-compass**:使用compass编译sass文件。
- **grunt-contrib-concat**:合并文件。
- **grunt-contrib-copy**:复制文件。
- **grunt-contrib-cssmin**:压缩以及合并CSS文件。
- **grunt-contrib-imagemin**:图像压缩模块。
- **grunt-contrib-jshint**:检查JavaScript语法。
- **grunt-contrib-uglify**:压缩以及合并JavaScript文件。
- **grunt-contrib-watch**:监视文件变动,做出相应动作。
模块的前缀如果是grunt-contrib,就表示该模块由grunt开发团队维护;如果前缀是grunt(比如grunt-pakmanager),就表示由第三方开发者维护。
以下选几个模块,看看它们配置参数的写法,也就是说如何在grunt.initConfig方法中配置各个模块。
### grunt-contrib-jshint
jshint用来检查语法错误,比如分号的使用是否正确、有没有忘记写括号等等。它在grunt.initConfig方法里面的配置代码如下。
```javascript
jshint: {
options: {
eqeqeq: true,
trailing: true
},
files: ['Gruntfile.js', 'lib/**/*.js']
},
```
上面代码先指定jshint的[检查项目](http://www.jshint.com/docs/options/),eqeqeq表示要用严格相等运算符取代相等运算符,trailing表示行尾不得有多余的空格。然后,指定files属性,表示检查目标是Gruntfile.js文件,以及lib目录的所有子目录下面的JavaScript文件。
### grunt-contrib-concat
concat用来合并同类文件,它不仅可以合并JavaScript文件,还可以合并CSS文件。
```javascript
concat: {
js: {
src: ['lib/module1.js', 'lib/module2.js', 'lib/plugin.js'],
dest: 'dist/script.js'
}
css: {
src: ['style/normalize.css', 'style/base.css', 'style/theme.css'],
dest: 'dist/screen.css'
}
},
```
js目标用于合并JavaScript文件,css目标用语合并CSS文件。两者的src属性指定需要合并的文件(input),dest属性指定输出的目标文件(output)。
### grunt-contrib-uglify
uglify模块用来压缩代码,减小文件体积。
```javascript
uglify: {
options: {
banner: bannerContent,
sourceMapRoot: '../',
sourceMap: 'distrib/'+name+'.min.js.map',
sourceMapUrl: name+'.min.js.map'
},
target : {
expand: true,
cwd: 'js/origin',
src : '*.js',
dest : 'js/'
}
},
```
上面代码中的options属性指定压缩后文件的文件头,以及sourceMap设置;target目标指定输入和输出文件。
### grunt-contrib-copy
[copy模块](https://github.com/gruntjs/grunt-contrib-copy)用于复制文件与目录。
```javascript
copy: {
main: {
src: 'src/*',
dest: 'dest/',
},
},
```
上面代码将src子目录(只包含它下面的第一层文件和子目录),拷贝到dest子目录下面(即dest/src目录)。如果要更准确控制拷贝行为,比如只拷贝文件、不拷贝目录、不保持目录结构,可以写成下面这样:
```javascript
copy: {
main: {
expand: true,
cwd: 'src/',
src: '**',
dest: 'dest/',
flatten: true,
filter: 'isFile',
},
},
```
### grunt-contrib-watch
[watch模块](https://github.com/gruntjs/grunt-contrib-watch)用来在后台运行,监听指定事件,然后自动运行指定的任务。
```javascript
watch: {
scripts: {
files: '**/*.js',
tasks: 'jshint',
options: {
livereload: true,
},
},
css: {
files: '**/*.sass',
tasks: ['sass'],
options: {
livereload: true,
},
},
},
```
设置好上面的代码,打开另一个进程,运行grunt watch。此后,任何的js代码变动,文件保存后就会自动运行jshint任务;任何sass文件变动,文件保存后就会自动运行sass任务。
需要注意的是,这两个任务的options参数之中,都设置了livereload,表示任务运行结束后,自动在浏览器中重载(reload)。这需要在浏览器中安装[livereload插件](http://livereload.com/)。安装后,livereload的默认端口为localhost:35729,但是也可以用livereload: 1337的形式重设端口(localhost:1337)。
### 其他模块
下面是另外一些有用的模块。
**(1)grunt-contrib-clean**
该模块用于删除文件或目录。
```javascript
clean: {
build: {
src: ["path/to/dir/one", "path/to/dir/two"]
}
}
```
**(2)grunt-autoprefixer**
该模块用于为CSS语句加上浏览器前缀。
```javascript
autoprefixer: {
build: {
expand: true,
cwd: 'build',
src: [ '**/*.css' ],
dest: 'build'
}
},
```
**(3)grunt-contrib-connect**
该模块用于在本机运行一个Web Server。
```javascript
connect: {
server: {
options: {
port: 4000,
base: 'build',
hostname: '*'
}
}
}
```
connect模块会随着grunt运行结束而结束,为了使它一直处于运行状态,可以把它放在watch模块之前运行。因为watch模块需要手动中止,所以connect模块也就会一直运行。
**(4)grunt-htmlhint**
该模块用于检查HTML语法。
```javascript
htmlhint: {
build: {
options: {
'tag-pair': true,
'tagname-lowercase': true,
'attr-lowercase': true,
'attr-value-double-quotes': true,
'spec-char-escape': true,
'id-unique': true,
'head-script-disabled': true,
},
src: ['index.html']
}
}
```
上面代码用于检查index.html文件:HTML标记是否配对、标记名和属性名是否小写、属性值是否包括在双引号之中、特殊字符是否转义、HTML元素的id属性是否为唯一值、head部分是否没有script标记。
**(5)grunt-contrib-sass模块**
该模块用于将SASS文件转为CSS文件。
```javascript
sass: {
build: {
options: {
style: 'compressed'
},
files: {
'build/css/master.css': 'assets/sass/master.scss'
}
}
}
```
上面代码指定输出文件为build/css/master.css,输入文件为assets/sass/master.scss。
**(6)grunt-markdown**
该模块用于将markdown文档转为HTML文档。
```javascript
markdown: {
all: {
files: [
{
expand: true,
src: '*.md',
dest: 'docs/html/',
ext: '.html'
}
],
options: {
template: 'templates/index.html',
}
}
},
```
上面代码指定将md后缀名的文件,转为docs/html/目录下的html文件。template属性指定转换时采用的模板,模板样式如下。
```html
<!DOCTYPE html>
<html>
<head>
<title>Document</title>
</head>
<body>
<div id="main" class="container">
<%=content%>
</div>
</body>
</html>
```
<h2 id="10.10">RequireJS和AMD规范</h2>
## 概述
RequireJS是一个工具库,主要用于客户端的模块管理。它可以让客户端的代码分成一个个模块,实现异步或动态加载,从而提高代码的性能和可维护性。它的模块管理遵守[AMD规范](https://github.com/amdjs/amdjs-api/wiki/AMD)(Asynchronous Module Definition)。
RequireJS的基本思想是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载。
首先,将require.js嵌入网页,然后就能在网页中进行模块化编程了。
```javascript
<script data-main="scripts/main" src="scripts/require.js"></script>
```
上面代码的data-main属性不可省略,用于指定主代码所在的脚本文件,在上例中为scripts子目录下的main.js文件。用户自定义的代码就放在这个main.js文件中。
### define方法:定义模块
define方法用于定义模块,RequireJS要求每个模块放在一个单独的文件里。
按照是否依赖其他模块,可以分成两种情况讨论。第一种情况是定义独立模块,即所定义的模块不依赖其他模块;第二种情况是定义非独立模块,即所定义的模块依赖于其他模块。
**(1)独立模块**
如果被定义的模块是一个独立模块,不需要依赖任何其他模块,可以直接用define方法生成。
```javascript
define({
method1: function() {},
method2: function() {},
});
```
上面代码生成了一个拥有method1、method2两个方法的模块。
另一种等价的写法是,把对象写成一个函数,该函数的返回值就是输出的模块。
```javascript
define(function () {
return {
method1: function() {},
method2: function() {},
};
});
```
后一种写法的自由度更高一点,可以在函数体内写一些模块初始化代码。
值得指出的是,define定义的模块可以返回任何值,不限于对象。
**(2)非独立模块**
如果被定义的模块需要依赖其他模块,则define方法必须采用下面的格式。
```javascript
define(['module1', 'module2'], function(m1, m2) {
...
});
```
define方法的第一个参数是一个数组,它的成员是当前模块所依赖的模块。比如,['module1', 'module2']表示我们定义的这个新模块依赖于module1模块和module2模块,只有先加载这两个模块,新模块才能正常运行。一般情况下,module1模块和module2模块指的是,当前目录下的module1.js文件和module2.js文件,等同于写成['./module1', './module2']。
define方法的第二个参数是一个函数,当前面数组的所有成员加载成功后,它将被调用。它的参数与数组的成员一一对应,比如function(m1, m2)就表示,这个函数的第一个参数m1对应module1模块,第二个参数m2对应module2模块。这个函数必须返回一个对象,供其他模块调用。
```javascript
define(['module1', 'module2'], function(m1, m2) {
return {
method: function() {
m1.methodA();
m2.methodB();
}
};
});
```
上面代码表示新模块返回一个对象,该对象的method方法就是外部调用的接口,menthod方法内部调用了m1模块的methodA方法和m2模块的methodB方法。
需要注意的是,回调函数必须返回一个对象,这个对象就是你定义的模块。
如果依赖的模块很多,参数与模块一一对应的写法非常麻烦。
```javascript
define(
[ 'dep1', 'dep2', 'dep3', 'dep4', 'dep5', 'dep6', 'dep7', 'dep8'],
function(dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8){
...
}
);
```
为了避免像上面代码那样繁琐的写法,RequireJS提供一种更简单的写法。
```javascript
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');
...
}
});
```
下面是一个define实际运用的例子。
```javascript
define(['math', 'graph'],
function ( math, graph ) {
return {
plot: function(x, y){
return graph.drawPie(math.randomGrid(x,y));
}
}
};
);
```
上面代码定义的模块依赖math和graph两个库,然后返回一个具有plot接口的对象。
另一个实际的例子是,通过判断浏览器是否为IE,而选择加载zepto或jQuery。
```javascript
define(('__proto__' in {} ? ['zepto'] : ['jquery']), function($) {
return $;
});
```
上面代码定义了一个中间模块,该模块先判断浏览器是否支持__proto__属性(除了IE,其他浏览器都支持),如果返回true,就加载zepto库,否则加载jQuery库。
### require方法:调用模块
require方法用于调用模块。它的参数与define方法类似。
```javascript
require(['foo', 'bar'], function ( foo, bar ) {
foo.doSomething();
});
```
上面方法表示加载foo和bar两个模块,当这两个模块都加载成功后,执行一个回调函数。该回调函数就用来完成具体的任务。
require方法的第一个参数,是一个表示依赖关系的数组。这个数组可以写得很灵活,请看下面的例子。
```javascript
require( [ window.JSON ? undefined : 'util/json2' ], function ( JSON ) {
JSON = JSON || window.JSON;
console.log( JSON.parse( '{ "JSON" : "HERE" }' ) );
});
```
上面代码加载JSON模块时,首先判断浏览器是否原生支持JSON对象。如果是的,则将undefined传入回调函数,否则加载util目录下的json2模块。
require方法也可以用在define方法内部。
```javascript
define(function (require) {
var otherModule = require('otherModule');
});
```
下面的例子显示了如何动态加载模块。
```javascript
define(function ( require ) {
var isReady = false, foobar;
require(['foo', 'bar'], function (foo, bar) {
isReady = true;
foobar = foo() + bar();
});
return {
isReady: isReady,
foobar: foobar
};
});
```
上面代码所定义的模块,内部加载了foo和bar两个模块,在没有加载完成前,isReady属性值为false,加载完成后就变成了true。因此,可以根据isReady属性的值,决定下一步的动作。
下面的例子是模块的输出结果是一个promise对象。
```javascript
define(['lib/Deferred'], function( Deferred ){
var defer = new Deferred();
require(['lib/templates/?index.html','lib/data/?stats'],
function( template, data ){
defer.resolve({ template: template, data:data });
}
);
return defer.promise();
});
```
上面代码的define方法返回一个promise对象,可以在该对象的then方法,指定下一步的动作。
如果服务器端采用JSONP模式,则可以直接在require中调用,方法是指定JSONP的callback参数为define。
```javascript
require( [
"http://someapi.com/foo?callback=define"
], function (data) {
console.log(data);
});
```
require方法允许添加第三个参数,即错误处理的回调函数。
```javascript
require(
[ "backbone" ],
function ( Backbone ) {
return Backbone.View.extend({ /* ... */ });
},
function (err) {
// ...
}
);
```
require方法的第三个参数,即处理错误的回调函数,接受一个error对象作为参数。
require对象还允许指定一个全局性的Error事件的监听函数。所有没有被上面的方法捕获的错误,都会被触发这个监听函数。
```javascript
requirejs.onError = function (err) {
// ...
};
```
### AMD模式小结
define和require这两个定义模块、调用模块的方法,合称为AMD模式。它的模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。
AMD模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。
## 配置require.js:config方法
require方法本身也是一个对象,它带有一个config方法,用来配置require.js运行参数。config方法接受一个对象作为参数。
```javascript
require.config({
paths: {
jquery: [
'//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
'lib/jquery'
]
}
});
```
config方法的参数对象有以下主要成员:
**(1)paths**
paths参数指定各个模块的位置。这个位置可以是同一个服务器上的相对位置,也可以是外部网址。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置,上面的示例就表示如果CDN加载失败,则加载服务器上的备用脚本。需要注意的是,指定本地文件路径时,可以省略文件最后的js后缀名。
```javascript
require(["jquery"], function($) {
// ...
});
```
上面代码加载jquery模块,因为jquery的路径已经在paths参数中定义了,所以就会到事先设定的位置下载。
**(2)baseUrl**
baseUrl参数指定本地模块位置的基准目录,即本地模块的路径是相对于哪个目录的。该属性通常由require.js加载时的data-main属性指定。
**(3)shim**
有些库不是AMD兼容的,这时就需要指定shim属性的值。shim可以理解成“垫片”,用来帮助require.js加载非AMD规范的库。
```javascript
require.config({
paths: {
"backbone": "vendor/backbone",
"underscore": "vendor/underscore"
},
shim: {
"backbone": {
deps: [ "underscore" ],
exports: "Backbone"
},
"underscore": {
exports: "_"
}
}
});
```
上面代码中的backbone和underscore就是非AMD规范的库。shim指定它们的依赖关系(backbone依赖于underscore),以及输出符号(backbone为“Backbone”,underscore为“_”)。
## 插件
RequireJS允许使用插件,加载各种格式的数据。完整的插件清单可以查看[官方网站](https://github.com/jrburke/requirejs/wiki/Plugins)。
下面是插入文本数据所使用的text插件的例子。
```javascript
define([
'backbone',
'text!templates.html'
], function( Backbone, template ){
// ...
});
```
上面代码加载的第一个模块是backbone,第二个模块则是一个文本,用'text!'表示。该文本作为字符串,存放在回调函数的template变量中。
## 优化器r.js
RequireJS提供一个基于node.js的命令行工具r.js,用来压缩多个js文件。它的主要作用是将多个模块文件压缩合并成一个脚本文件,以减少网页的HTTP请求数。
第一步是安装r.js(假设已经安装了node.js)。
```bash
npm install -g requirejs
```
然后,使用的时候,直接在命令行键入以下格式的命令。
```bash
node r.js -o <arguments>
```
<argument>表示命令运行时,所需要的一系列参数,比如像下面这样:
```bash
node r.js -o baseUrl=. name=main out=main-built.js
```
除了直接在命令行提供参数设置,也可以将参数写入一个文件,假定文件名为build.js。
```javascript
({
baseUrl: ".",
name: "main",
out: "main-built.js"
})
```
然后,在命令行下用r.js运行这个参数文件,就OK了,不需要其他步骤了。
```bash
node r.js -o build.js
```
下面是一个参数文件的范例,假定位置就在根目录下,文件名为build.js。
```javascript
({
appDir: './',
baseUrl: './js',
dir: './dist',
modules: [
{
name: 'main'
}
],
fileExclusionRegExp: /^(r|build)\.js$/,
optimizeCss: 'standard',
removeCombined: true,
paths: {
jquery: 'lib/jquery',
underscore: 'lib/underscore',
backbone: 'lib/backbone/backbone',
backboneLocalstorage: 'lib/backbone/backbone.localStorage',
text: 'lib/require/text'
},
shim: {
underscore: {
exports: '_'
},
backbone: {
deps: [
'underscore',
'jquery'
],
exports: 'Backbone'
},
backboneLocalstorage: {
deps: ['backbone'],
exports: 'Store'
}
}
})
```
上面代码将多个模块压缩合并成一个main.js。
参数文件的主要成员解释如下:
- **appDir**:项目目录,相对于参数文件的位置。
- **baseUrl**:js文件的位置。
- **dir**:输出目录。
- **modules**:一个包含对象的数组,每个对象就是一个要被优化的模块。
- **fileExclusionRegExp**:凡是匹配这个正则表达式的文件名,都不会被拷贝到输出目录。
- **optimizeCss**: 自动压缩CSS文件,可取的值包括“none”, “standard”, “standard.keepLines”, “standard.keepComments”, “standard.keepComments.keepLines”。
- **removeCombined**:如果为true,合并后的原文件将不保留在输出目录中。
- **paths**:各个模块的相对路径,可以省略js后缀名。
- **shim**:配置依赖性关系。如果某一个模块不是AMD模式定义的,就可以用shim属性指定模块的依赖性关系和输出值。
- **generateSourceMaps**:是否要生成source map文件。
更详细的解释可以参考[官方文档](https://github.com/jrburke/r.js/blob/master/build/example.build.js)。
运行优化命令后,可以前往dist目录查看优化后的文件。
下面是另一个build.js的例子。
```javascript
({
mainConfigFile : "js/main.js",
baseUrl: "js",
removeCombined: true,
findNestedDependencies: true,
dir: "dist",
modules: [
{
name: "main",
exclude: [
"infrastructure"
]
},
{
name: "infrastructure"
}
]
})
```
上面代码将模块文件压缩合并成两个文件,第一个是main.js(指定排除infrastructure.js),第二个则是infrastructure.js。
<h2 id="10.11">Lint 工具</h2>
## 概述
Lint工具用于检查代码的语法是否正确、风格是否符合要求。
JavaScript语言的最早的Lint工具,是Douglas Crockford开发的JSLint。由于该工具所有的语法规则,都是预设的,用户无法改变。所以,很快就有人抱怨,JSLint不是让你写成正确的JavaScript,而是让你像Douglas Crockford一样写JavaScript。
JSHint可以看作是JSLint的后继者,最大特定就是允许用户自定义自己的语法规则,写在项目根目录下面的`.jshintrc`文件。
JSLint和JSHint同时检查你的语法和风格。另一个工具JSCS则是只检查语法风格。
最新的工具ESLint不仅允许你自定义语法规则,还允许用户创造插件,改变默认的JavaScript语法,比如支持ES6和JSX的语法。
## ESLint
### 基本用法
首先,安装ESLint。
```bash
$ npm i -g eslint
```
其次,在项目根目录下面新建一个`.eslintrc`文件,里面定义了你的语法规则。
```javascript
{
"rules": {
"indent": 2,
"no-unused-vars": 2,
"no-alert": 1
},
"env": {
"browser": true
}
}
```
上面的`.eslintrc`文件是JSON格式,里面首先定义,这些规则只适用于浏览器环境。如果要定义,同时适用于浏览器环境和Node环境,可以写成下面这样。
```javascript
{
"env": {
"browser": true,
"node": true
}
}
```
然后,上面的`.eslintrc`文件定义了三条语法规则。每个语法规则后面,表示这个规则的级别。
- 0:关闭该条规则。
- 1:违反这条规则,会抛出一个警告。
- 2:违反这条规则,会抛出一个错误。
接下来,新建一个`index.js`文件。
```javascript
var unusued = 'I have no purpose!';
function greet() {
var message = 'Hello, World!';
alert(message);
}
greet();
```
然后,运行ESLint检查该文件,结果如下。
```bash
$ eslint index.js
index.js
1:5 error unusued is defined but never used no-unused-vars
5:5 warning Unexpected alert no-alert
✖ 2 problems (1 error, 1 warning)
```
上面代码检查出两个问题,一个是定义了变量却没有使用,二是存在alert。
### 预置规则
自己设置所有语法规则,是非常麻烦的。所以,ESLint提供了预设的语法样式,比较常用的Airbnb的语法规则。由于这个规则集涉及ES6,所以还需要安装Babel插件。
```bash
$ npm i -g babel-eslint eslint-config-airbnb
```
安装完成后,在`.eslintrc`文件中注明,使用Airbnb语法规则。
```bash
{
"extends": "eslint-config-airbnb"
}
```
你也可以用自己的规则,覆盖预设的语法规则。
```javascript
{
"extends": "eslint-config-airbnb",
"rules": {
"no-var": 0,
"no-alert": 0
}
}
```
### 语法规则
(1)indent
indent规则设定行首的缩进,默认是四个空格。下面的几种写法,可以改变这个设置。
```javascript
// 缩进为4个空格(默认值)
"indent": 2
// 缩进为2个空格
"indent": [2, 2]
// 缩进为1个tab键
"indent": [2, "tab"]
// 缩进为2个空格,
// 同时,switch...case结构的case也必须缩进,默认是不打开的
"indent": [2, 2, {"SwitchCase": 1}]
```
(2)no-unused-vars
不允许声明了变量,却不使用。
```javascript
"no-unused-vars": [2, {"vars": "local", "args": "after-used"}]
```
上面代码中,vars字段表示只检查局部变量,允许全局变量声明了却不使用;args字段表示函数的参数,只要求使用最后一个参数,前面的参数可以不使用。
(3)no-alert
不得使用alert、confirm和prompt。