<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文件。

<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。