<h2 id="12.1">Node.js 概述</h2>
## 簡介
Node是JavaScript語言的服務器運行環境。
所謂“運行環境”有兩層意思:首先,JavaScript語言通過Node在服務器運行,在這個意義上,Node有點像JavaScript虛擬機;其次,Node提供大量工具庫,使得JavaScript語言與操作系統互動(比如讀寫文件、新建子進程),在這個意義上,Node又是JavaScript的工具庫。
Node內部采用Google公司的V8引擎,作為JavaScript語言解釋器;通過自行開發的libuv庫,調用操作系統資源。
### 安裝與更新
訪問官方網站[nodejs.org](http://nodejs.org)或者[github.com/nodesource/distributions](https://github.com/nodesource/distributions),查看Node的最新版本和安裝方法。
官方網站提供編譯好的二進制包,可以把它們解壓到`/usr/local`目錄下面。
```bash
$ tar -xf node-someversion.tgz
```
然后,建立符號鏈接,把它們加到$PATH變量里面的路徑。
```bash
$ ln -s /usr/local/node/bin/node /usr/local/bin/node
$ ln -s /usr/local/node/bin/npm /usr/local/bin/npm
```
下面是Ubuntu和Debian下面安裝Deb軟件包的安裝方法。
```bash
$ curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
$ sudo apt-get install -y nodejs
$ apt-get install nodejs
```
安裝完成以后,運行下面的命令,查看是否能正常運行。
```bash
$ node --version
# 或者
$ node -v
```
更新node.js版本,可以通過node.js的`n`模塊完成。
```bash
$ sudo npm install n -g
$ sudo n stable
```
上面代碼通過`n`模塊,將node.js更新為最新發布的穩定版。
`n`模塊也可以指定安裝特定版本的node。
```bash
$ sudo n 0.10.21
```
### 版本管理工具nvm
如果想在同一臺機器,同時安裝多個版本的node.js,就需要用到版本管理工具nvm。
```bash
$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh
```
安裝以后,nvm的執行腳本,每次使用前都要激活,建議將其加入~/.bashrc文件(假定使用Bash)。激活后,就可以安裝指定版本的Node。
```bash
# 安裝最新版本
$ nvm install node
# 安裝指定版本
$ nvm install 0.12.1
# 使用已安裝的最新版本
$ nvm use node
# 使用指定版本的node
$ nvm use 0.12
```
nvm也允許進入指定版本的REPL環境。
```bash
$ nvm run 0.12
```
如果在項目根目錄下新建一個.nvmrc文件,將版本號寫入其中,就只輸入`nvm use`命令即可,不再需要附加版本號。
下面是其他經常用到的命令。
```bash
# 查看本地安裝的所有版本
$ nvm ls
# 查看服務器上所有可供安裝的版本。
$ nvm ls-remote
# 退出已經激活的nvm,使用deactivate命令。
$ nvm deactivate
```
### 基本用法
安裝完成后,運行node.js程序,就是使用node命令讀取JavaScript腳本。
當前目錄的`demo.js`腳本文件,可以這樣執行。
```bash
$ node demo
# 或者
$ node demo.js
```
使用`-e`參數,可以執行代碼字符串。
```bash
$ node -e 'console.log("Hello World")'
Hello World
```
### REPL環境
在命令行鍵入node命令,后面沒有文件名,就進入一個Node.js的REPL環境(Read–eval–print loop,"讀取-求值-輸出"循環),可以直接運行各種JavaScript命令。
```bash
$ node
> 1+1
2
>
```
如果使用參數 --use_strict,則REPL將在嚴格模式下運行。
```bash
$ node --use_strict
```
REPL是Node.js與用戶互動的shell,各種基本的shell功能都可以在里面使用,比如使用上下方向鍵遍歷曾經使用過的命令。
特殊變量下劃線(_)表示上一個命令的返回結果。
```bash
> 1 + 1
2
> _ + 1
3
```
在REPL中,如果運行一個表達式,會直接在命令行返回結果。如果運行一條語句,就不會有任何輸出,因為語句沒有返回值。
```bash
> x = 1
1
> var x = 1
```
上面代碼的第二條命令,沒有顯示任何結果。因為這是一條語句,不是表達式,所以沒有返回值。
### 異步操作
Node采用V8引擎處理JavaScript腳本,最大特點就是單線程運行,一次只能運行一個任務。這導致Node大量采用異步操作(asynchronous opertion),即任務不是馬上執行,而是插在任務隊列的尾部,等到前面的任務運行完后再執行。
由于這種特性,某一個任務的后續操作,往往采用回調函數(callback)的形式進行定義。
```javascript
var isTrue = function(value, callback) {
if (value === true) {
callback(null, "Value was true.");
}
else {
callback(new Error("Value is not true!"));
}
}
```
上面代碼就把進一步的處理,交給回調函數callback。
Node約定,如果某個函數需要回調函數作為參數,則回調函數是最后一個參數。另外,回調函數本身的第一個參數,約定為上一步傳入的錯誤對象。
```javascript
var callback = function (error, value) {
if (error) {
return console.log(error);
}
console.log(value);
}
```
上面代碼中,callback的第一個參數是Error對象,第二個參數才是真正的數據參數。這是因為回調函數主要用于異步操作,當回調函數運行時,前期的操作早結束了,錯誤的執行棧早就不存在了,傳統的錯誤捕捉機制try...catch對于異步操作行不通,所以只能把錯誤交給回調函數處理。
```javascript
try {
db.User.get(userId, function(err, user) {
if(err) {
throw err
}
// ...
})
} catch(e) {
console.log(‘Oh no!’);
}
```
上面代碼中,db.User.get方法是一個異步操作,等到拋出錯誤時,可能它所在的try...catch代碼塊早就運行結束了,這會導致錯誤無法被捕捉。所以,Node統一規定,一旦異步操作發生錯誤,就把錯誤對象傳遞到回調函數。
如果沒有發生錯誤,回調函數的第一個參數就傳入null。這種寫法有一個很大的好處,就是說只要判斷回調函數的第一個參數,就知道有沒有出錯,如果不是null,就肯定出錯了。另外,這樣還可以層層傳遞錯誤。
```javascript
if(err) {
// 除了放過No Permission錯誤意外,其他錯誤傳給下一個回調函數
if(!err.noPermission) {
return next(err);
}
}
```
### 全局對象和全局變量
Node提供以下幾個全局對象,它們是所有模塊都可以調用的。
- **global**:表示Node所在的全局環境,類似于瀏覽器的window對象。需要注意的是,如果在瀏覽器中聲明一個全局變量,實際上是聲明了一個全局對象的屬性,比如`var x = 1`等同于設置`window.x = 1`,但是Node不是這樣,至少在模塊中不是這樣(REPL環境的行為與瀏覽器一致)。在模塊文件中,聲明`var x = 1`,該變量不是`global`對象的屬性,`global.x`等于undefined。這是因為模塊的全局變量都是該模塊私有的,其他模塊無法取到。
- **process**:該對象表示Node所處的當前進程,允許開發者與該進程互動。
- **console**:指向Node內置的console模塊,提供命令行環境中的標準輸入、標準輸出功能。
Node還提供一些全局函數。
- **setTimeout()**:用于在指定毫秒之后,運行回調函數。實際的調用間隔,還取決于系統因素。間隔的毫秒數在1毫秒到2,147,483,647毫秒(約24.8天)之間。如果超過這個范圍,會被自動改為1毫秒。該方法返回一個整數,代表這個新建定時器的編號。
- **clearTimeout()**:用于終止一個setTimeout方法新建的定時器。
- **setInterval()**:用于每隔一定毫秒調用回調函數。由于系統因素,可能無法保證每次調用之間正好間隔指定的毫秒數,但只會多于這個間隔,而不會少于它。指定的毫秒數必須是1到2,147,483,647(大約24.8天)之間的整數,如果超過這個范圍,會被自動改為1毫秒。該方法返回一個整數,代表這個新建定時器的編號。
- **clearInterval()**:終止一個用setInterval方法新建的定時器。
- **require()**:用于加載模塊。
- **Buffer()**:用于操作二進制數據。
Node提供兩個全局變量,都以兩個下劃線開頭。
- `__filename`:指向當前運行的腳本文件名。
- `__dirname`:指向當前運行的腳本所在的目錄。
除此之外,還有一些對象實際上是模塊內部的局部變量,指向的對象根據模塊不同而不同,但是所有模塊都適用,可以看作是偽全局變量,主要為module, module.exports, exports等。
## 模塊化結構
### 概述
Node.js采用模塊化結構,按照[CommonJS規范](http://wiki.commonjs.org/wiki/CommonJS)定義和使用模塊。模塊與文件是一一對應關系,即加載一個模塊,實際上就是加載對應的一個模塊文件。
require命令用于指定加載模塊,加載時可以省略腳本文件的后綴名。
```javascript
var circle = require('./circle.js');
// 或者
var circle = require('./circle');
```
require方法的參數是模塊文件的名字。它分成兩種情況,第一種情況是參數中含有文件路徑(比如上例),這時路徑是相對于當前腳本所在的目錄,第二種情況是參數中不含有文件路徑,這時Node到模塊的安裝目錄,去尋找已安裝的模塊(比如下例)。
```javascript
var bar = require('bar');
```
有時候,一個模塊本身就是一個目錄,目錄中包含多個文件。這時候,Node在package.json文件中,尋找main屬性所指明的模塊入口文件。
```javascript
{
"name" : "bar",
"main" : "./lib/bar.js"
}
```
上面代碼中,模塊的啟動文件為lib子目錄下的bar.js。當使用`require('bar')`命令加載該模塊時,實際上加載的是`./node_modules/bar/lib/bar.js`文件。下面寫法會起到同樣效果。
```javascript
var bar = require('bar/lib/bar.js')
```
如果模塊目錄中沒有package.json文件,node.js會嘗試在模塊目錄中尋找index.js或index.node文件進行加載。
模塊一旦被加載以后,就會被系統緩存。如果第二次還加載該模塊,則會返回緩存中的版本,這意味著模塊實際上只會執行一次。如果希望模塊執行多次,則可以讓模塊返回一個函數,然后多次調用該函數。
### 核心模塊
如果只是在服務器運行JavaScript代碼,用處并不大,因為服務器腳本語言已經有很多種了。Node.js的用處在于,它本身還提供了一系列功能模塊,與操作系統互動。這些核心的功能模塊,不用安裝就可以使用,下面是它們的清單。
- **http**:提供HTTP服務器功能。
- **url**:解析URL。
- **fs**:與文件系統交互。
- **querystring**:解析URL的查詢字符串。
- **child_process**:新建子進程。
- **util**:提供一系列實用小工具。
- **path**:處理文件路徑。
- **crypto**:提供加密和解密功能,基本上是對OpenSSL的包裝。
上面這些核心模塊,源碼都在Node的lib子目錄中。為了提高運行速度,它們安裝時都會被編譯成二進制文件。
核心模塊總是最優先加載的。如果你自己寫了一個HTTP模塊,`require('http')`加載的還是核心模塊。
### 自定義模塊
Node模塊采用CommonJS規范。只要符合這個規范,就可以自定義模塊。
下面是一個最簡單的模塊,假定新建一個foo.js文件,寫入以下內容。
```javascript
// foo.js
module.exports = function(x) {
console.log(x);
};
```
上面代碼就是一個模塊,它通過module.exports變量,對外輸出一個方法。
這個模塊的使用方法如下。
```javascript
// index.js
var m = require('./foo');
m("這是自定義模塊");
```
上面代碼通過require命令加載模塊文件foo.js(后綴名省略),將模塊的對外接口輸出到變量m,然后調用m。這時,在命令行下運行index.js,屏幕上就會輸出“這是自定義模塊”。
```bash
$ node index
這是自定義模塊
```
module變量是整個模塊文件的頂層變量,它的exports屬性就是模塊向外輸出的接口。如果直接輸出一個函數(就像上面的foo.js),那么調用模塊就是調用一個函數。但是,模塊也可以輸出一個對象。下面對foo.js進行改寫。
```javascript
// foo.js
var out = new Object();
function p(string) {
console.log(string);
}
out.print = p;
module.exports = out;
```
上面的代碼表示模塊輸出out對象,該對象有一個print屬性,指向一個函數。下面是這個模塊的使用方法。
```javascript
// index.js
var m = require('./foo');
m.print("這是自定義模塊");
```
上面代碼表示,由于具體的方法定義在模塊的print屬性上,所以必須顯式調用print屬性。
## 異常處理
Node是單線程運行環境,一旦拋出的異常沒有被捕獲,就會引起整個進程的崩潰。所以,Node的異常處理對于保證系統的穩定運行非常重要。
一般來說,Node有三種方法,傳播一個錯誤。
- 使用throw語句拋出一個錯誤對象,即拋出異常。
- 將錯誤對象傳遞給回調函數,由回調函數負責發出錯誤。
- 通過EventEmitter接口,發出一個error事件。
### try...catch結構
最常用的捕獲異常的方式,就是使用try...catch結構。但是,這個結構無法捕獲異步運行的代碼拋出的異常。
```javascript
try {
process.nextTick(function () {
throw new Error("error");
});
} catch (err) {
//can not catch it
console.log(err);
}
try {
setTimeout(function(){
throw new Error("error");
},1)
} catch (err) {
//can not catch it
console.log(err);
}
```
上面代碼分別用process.nextTick和setTimeout方法,在下一輪事件循環拋出兩個異常,代表異步操作拋出的錯誤。它們都無法被catch代碼塊捕獲,因此catch代碼塊所在的那部分已經運行結束了。
一種解決方法是將錯誤捕獲代碼,也放到異步執行。
```javascript
function async(cb, err) {
setTimeout(function() {
try {
if (true)
throw new Error("woops!");
else
cb("done");
} catch(e) {
err(e);
}
}, 2000)
}
async(function(res) {
console.log("received:", res);
}, function(err) {
console.log("Error: async threw an exception:", err);
});
// Error: async threw an exception: Error: woops!
```
上面代碼中,async函數異步拋出的錯誤,可以同樣部署在異步的catch代碼塊捕獲。
這兩種處理方法都不太理想。一般來說,Node只在很少場合才用try/catch語句,比如使用`JSON.parse`解析JSON文本。
### 回調函數
Node采用的方法,是將錯誤對象作為第一個參數,傳入回調函數。這樣就避免了捕獲代碼與發生錯誤的代碼不在同一個時間段的問題。
```javascript
fs.readFile('/foo.txt', function(err, data) {
if (err !== null) throw err;
console.log(data);
});
```
上面代碼表示,讀取文件`foo.txt`是一個異步操作,它的回調函數有兩個參數,第一個是錯誤對象,第二個是讀取到的文件數據。如果第一個參數不是null,就意味著發生錯誤,后面代碼也就不再執行了。
下面是一個完整的例子。
```javascript
function async2(continuation) {
setTimeout(function() {
try {
var res = 42;
if (true)
throw new Error("woops!");
else
continuation(null, res); // pass 'null' for error
} catch(e) {
continuation(e, null);
}
}, 2000);
}
async2(function(err, res) {
if (err)
console.log("Error: (cps) failed:", err);
else
console.log("(cps) received:", res);
});
// Error: (cps) failed: woops!
```
上面代碼中,async2函數的回調函數的第一個參數就是一個錯誤對象,這是為了處理異步操作拋出的錯誤。
### EventEmitter接口的error事件
發生錯誤的時候,也可以用EventEmitter接口拋出error事件。
```javascript
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();
emitter.emit('error', new Error('something bad happened'));
```
使用上面的代碼必須小心,因為如果沒有對error事件部署監聽函數,會導致整個應用程序崩潰。所以,一般總是必須同時部署下面的代碼。
```javascript
emitter.on('error', function(err) {
console.error('出錯:' + err.message);
});
```
### uncaughtException事件
當一個異常未被捕獲,就會觸發uncaughtException事件,可以對這個事件注冊回調函數,從而捕獲異常。
```javascript
process.on('uncaughtException', function(err) {
console.error('Error caught in uncaughtException event:', err);
});
try {
setTimeout(function(){
throw new Error("error");
},1)
} catch (err) {
//can not catch it
console.log(err);
}
```
只要給uncaughtException配置了回調,Node進程不會異常退出,但異常發生的上下文已經丟失,無法給出異常發生的詳細信息。而且,異常可能導致Node不能正常進行內存回收,出現內存泄露。所以,當uncaughtException觸發后,最好記錄錯誤日志,然后結束Node進程。
```javascript
process.on('uncaughtException', function(err) {
logger(err);
process.exit(1);
});
```
### unhandledRejection事件
iojs有一個unhandledRejection事件,用來監聽沒有捕獲的Promise對象的rejected狀態。
```javascript
var promise = new Promise(function(resolve, reject) {
reject(new Error("Broken."));
});
promise.then(function(result) {
console.log(result);
})
```
上面代碼中,promise的狀態變為rejected,并且拋出一個錯誤。但是,不會有任何反應,因為沒有設置任何處理函數。
只要監聽unhandledRejection事件,就能解決這個問題。
```javascript
process.on('unhandledRejection', function (err, p) {
console.error(err.stack);
})
```
需要注意的是,unhandledRejection事件的監聽函數有兩個參數,第一個是錯誤對象,第二個是產生錯誤的promise對象。這可以提供很多有用的信息。
```javascript
var http = require('http');
http.createServer(function (req, res) {
var promise = new Promise(function(resolve, reject) {
reject(new Error("Broken."))
})
p.info = {url: req.url}
}).listen(8080)
process.on('unhandledRejection', function (err, p) {
if (p.info && p.info.url) {
console.log('Error in URL', p.info.url)
}
console.error(err.stack)
})
```
上面代碼會在出錯時,輸出用戶請求的網址。
```javascript
Error in URL /testurl
Error: Broken.
at /Users/mikeal/tmp/test.js:9:14
at Server.<anonymous> (/Users/mikeal/tmp/test.js:4:17)
at emitTwo (events.js:87:13)
at Server.emit (events.js:169:7)
at HTTPParser.parserOnIncoming [as onIncoming] (_http_server.js:471:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:88:23)
at Socket.socketOnData (_http_server.js:322:22)
at emitOne (events.js:77:13)
at Socket.emit (events.js:166:7)
at readableAddChunk (_stream_readable.js:145:16)
```
## 命令行腳本
node腳本可以作為命令行腳本使用。
```bash
$ node foo.js
```
上面代碼執行了foo.js腳本文件。
foo.js文件的第一行,如果加入了解釋器的位置,就可以將其作為命令行工具直接調用。
```bash
#!/usr/bin/env node
```
調用前,需更改文件的執行權限。
```bash
$ chmod u+x foo.js
$ ./foo.js arg1 arg2 ...
```
作為命令行腳本時,`console.log`用于輸出內容到標準輸出,`process.stdin`用于讀取標準輸入,`child_process.exec()`用于執行一個shell命令。
<h2 id="12.2">module</h2>
## 概述
Node程序由許多個模塊組成,每個模塊就是一個文件。Node模塊采用了CommonJS規范。
根據CommonJS規范,一個單獨的文件就是一個模塊。每一個模塊都是一個單獨的作用域,也就是說,在一個文件定義的變量(還包括函數和類),都是私有的,對其他文件是不可見的。
```javascript
// example.js
var x = 5;
var addX = function(value) {
return value + x;
};
```
上面代碼中,變量`x`和函數`addX`,是當前文件`example.js`私有的,其他文件不可見。
如果想在多個文件分享變量,必須定義為`global`對象的屬性。
```javascript
global.warning = true;
```
上面代碼的`warning`變量,可以被所有文件讀取。當然,這樣寫法是不推薦的。
CommonJS規定,每個文件的對外接口是`module.exports`對象。這個對象的所有屬性和方法,都可以被其他文件導入。
```javascript
var x = 5;
var addX = function(value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
```
上面代碼通過`module.exports`對象,定義對外接口,輸出變量`x`和函數`addX`。`module.exports`對象是可以被其他文件導入的,它其實就是文件內部與外部通信的橋梁。
`require`方法用于在其他文件加載這個接口,具體用法參見《Require命令》的部分。
```javascript
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
```
CommonJS模塊的特點如下。
- 所有代碼都運行在模塊作用域,不會污染全局作用域。
- 模塊可以多次加載,但是只會在第一次加載時運行一次,然后運行結果就被緩存了,以后再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
- 模塊加載的順序,按照其在代碼中出現的順序。
## module對象
Node內部提供一個`Module`構建函數。所有模塊都是`Module`的實例。
```javascript
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
// ...
```
每個模塊內部,都有一個`module`對象,代表當前模塊。它有以下屬性。
- `module.id` 模塊的識別符,通常是帶有絕對路徑的模塊文件名。
- `module.filename` 模塊的文件名,帶有絕對路徑。
- `module.loaded` 返回一個布爾值,表示模塊是否已經完成加載。
- `module.parent` 返回一個對象,表示調用該模塊的模塊。
- `module.children` 返回一個數組,表示該模塊要用到的其他模塊。
- `module.exports` 表示模塊對外輸出的值。
下面是一個示例文件,最后一行輸出module變量。
```javascript
// example.js
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);
```
執行這個文件,命令行會輸出如下信息。
```javascript
{ id: '.',
exports: { '$': [Function] },
parent: null,
filename: '/path/to/example.js',
loaded: false,
children:
[ { id: '/path/to/node_modules/jquery/dist/jquery.js',
exports: [Function],
parent: [Circular],
filename: '/path/to/node_modules/jquery/dist/jquery.js',
loaded: true,
children: [],
paths: [Object] } ],
paths:
[ '/home/user/deleted/node_modules',
'/home/user/node_modules',
'/home/node_modules',
'/node_modules' ]
}
```
如果在命令行下調用某個模塊,比如`node something.js`,那么`module.parent`就是`undefined`。如果是在腳本之中調用,比如`require('./something.js')`,那么`module.parent`就是調用它的模塊。利用這一點,可以判斷當前模塊是否為入口腳本。
```javascript
if (!module.parent) {
// ran with `node something.js`
app.listen(8088, function() {
console.log('app listening on port 8088');
})
} else {
// used with `require('/.something.js')`
module.exports = app;
}
```
### module.exports屬性
`module.exports`屬性表示當前模塊對外輸出的接口,其他文件加載該模塊,實際上就是讀取`module.exports`變量。
```javascript
var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
setTimeout(function() {
module.exports.emit('ready');
}, 1000);
```
上面模塊會在加載后1秒后,發出ready事件。其他文件監聽該事件,可以寫成下面這樣。
```javascript
var a = require('./a');
a.on('ready', function() {
console.log('module a is ready');
});
```
### exports變量
為了方便,Node為每個模塊提供一個exports變量,指向module.exports。這等同在每個模塊頭部,有一行這樣的命令。
```javascript
var exports = module.exports;
```
造成的結果是,在對外輸出模塊接口時,可以向exports對象添加方法。
```javascript
exports.area = function (r) {
return Math.PI * r * r;
};
exports.circumference = function (r) {
return 2 * Math.PI * r;
};
```
注意,不能直接將exports變量指向一個值,因為這樣等于切斷了`exports`與`module.exports`的聯系。
```javascript
exports = function(x) {console.log(x)};
```
上面這樣的寫法是無效的,因為`exports`不再指向`module.exports`了。
下面的寫法也是無效的。
```javascript
exports.hello = function() {
return 'hello';
};
module.exports = 'Hello world';
```
上面代碼中,`hello`函數是無法對外輸出的,因為`module.exports`被重新賦值了。
這意味著,如果一個模塊的對外接口,就是一個單一的值,不能使用`exports`輸出,只能使用`module.exports`輸出。
```javascript
module.exports = function (x){ console.log(x);};
```
如果你覺得,`exports`與`module.exports`之間的區別很難分清,一個簡單的處理方法,就是放棄使用`exports`,只使用`module.exports`。
## AMD規范與CommonJS規范的兼容性
CommonJS規范加載模塊是同步的,也就是說,只有加載完成,才能執行后面的操作。AMD規范則是非同步加載模塊,允許指定回調函數。由于Node.js主要用于服務器編程,模塊文件一般都已經存在于本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以CommonJS規范比較適用。但是,如果是瀏覽器環境,要從服務器端加載模塊,這時就必須采用非同步模式,因此瀏覽器端一般采用AMD規范。
AMD規范使用define方法定義模塊,下面就是一個例子:
```javascript
define(['package/lib'], function(lib){
function foo(){
lib.log('hello world!');
}
return {
foo: foo
};
});
```
AMD規范允許輸出的模塊兼容CommonJS規范,這時`define`方法需要寫成下面這樣:
```javascript
define(function (require, exports, module){
var someModule = require("someModule");
var anotherModule = require("anotherModule");
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
exports.asplode = function (){
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};
});
```
## require命令
### 基本用法
Node使用CommonJS模塊規范,內置的`require`命令用于加載模塊文件。
`require`命令的基本功能是,讀入并執行一個JavaScript文件,然后返回該模塊的exports對象。如果沒有發現指定模塊,會報錯。
```javascript
// example.js
var invisible = function () {
console.log("invisible");
}
exports.message = "hi";
exports.say = function () {
console.log(message);
}
```
運行下面的命令,可以輸出exports對象。
```javascript
var example = require('./example.js');
example
// {
// message: "hi",
// say: [Function]
// }
```
如果模塊輸出的是一個函數,那就不能定義在exports對象上面,而要定義在`module.exports`變量上面。
```javascript
module.exports = function () {
console.log("hello world")
}
require('./example2.js')()
```
上面代碼中,require命令調用自身,等于是執行`module.exports`,因此會輸出 hello world。
### 加載規則
`require`命令用于加載文件,后綴名默認為`.js`。
```javascript
var foo = require('foo');
// 等同于
var foo = require('foo.js');
```
根據參數的不同格式,`require`命令去不同路徑尋找模塊文件。
(1)如果參數字符串以“/”開頭,則表示加載的是一個位于絕對路徑的模塊文件。比如,`require('/home/marco/foo.js')`將加載`/home/marco/foo.js`。
(2)如果參數字符串以“./”開頭,則表示加載的是一個位于相對路徑(跟當前執行腳本的位置相比)的模塊文件。比如,`require('./circle')`將加載當前腳本同一目錄的`circle.js`。
(3)如果參數字符串不以“./“或”/“開頭,則表示加載的是一個默認提供的核心模塊(位于Node的系統安裝目錄中),或者一個位于各級node_modules目錄的已安裝模塊(全局安裝或局部安裝)。
舉例來說,腳本`/home/user/projects/foo.js`執行了`require('bar.js')`命令,Node會依次搜索以下文件。
- /usr/local/lib/node/bar.js
- /home/user/projects/node_modules/bar.js
- /home/user/node_modules/bar.js
- /home/node_modules/bar.js
- /node_modules/bar.js
這樣設計的目的是,使得不同的模塊可以將所依賴的模塊本地化。
(4)如果參數字符串不以“./“或”/“開頭,而且是一個路徑,比如`require('example-module/path/to/file')`,則將先找到`example-module`的位置,然后再以它為參數,找到后續路徑。
(5)如果指定的模塊文件沒有發現,Node會嘗試為文件名添加`.js`、`.json`、`.node`后,再去搜索。`.js`件會以文本格式的JavaScript腳本文件解析,`.json`文件會以JSON格式的文本文件解析,`.node`文件會以編譯后的二進制文件解析。
(6)如果想得到`require`命令加載的確切文件名,使用`require.resolve()`方法。
### 目錄的加載規則
通常,我們會把相關的文件會放在一個目錄里面,便于組織。這時,最好為該目錄設置一個入口文件,讓`require`方法可以通過這個入口文件,加載整個目錄。
在目錄中放置一個`package.json`文件,并且將入口文件寫入`main`字段。下面是一個例子。
```javascript
// package.json
{ "name" : "some-library",
"main" : "./lib/some-library.js" }
```
`require`發現參數字符串指向一個目錄以后,會自動查看該目錄的`package.json`文件,然后加載`main`字段指定的入口文件。如果`package.json`文件沒有`main`字段,或者根本就沒有`package.json`文件,則會加載該目錄下的`index.js`文件或`index.node`文件。
### 模塊的緩存
第一次加載某個模塊時,Node會緩存該模塊。以后再加載該模塊,就直接從緩存取出該模塊的`module.exports`屬性。
```javascript
require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"
```
上面代碼中,連續三次使用`require`命令,加載同一個模塊。第二次加載的時候,為輸出的對象添加了一個`message`屬性。但是第三次加載的時候,這個message屬性依然存在,這就證明`require`命令并沒有重新加載模塊文件,而是輸出了緩存。
如果想要多次執行某個模塊,可以讓該模塊輸出一個函數,然后每次`require`這個模塊的時候,重新執行一下輸出的函數。
所有緩存的模塊保存在`require.cache`之中,如果想刪除模塊的緩存,可以像下面這樣寫。
```javascript
// 刪除指定模塊的緩存
delete require.cache[moduleName];
// 刪除所有模塊的緩存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
```
注意,緩存是根據絕對路徑識別模塊的,如果同樣的模塊名,但是保存在不同的路徑,`require`命令還是會重新加載該模塊。
### 環境變量NODE_PATH
Node執行一個腳本時,會先查看環境變量`NODE_PATH`。它是一組以冒號分隔的絕對路徑。在其他位置找不到指定模塊時,Node會去這些路徑查找。
可以將NODE_PATH添加到`.bashrc`。
```javascript
export NODE_PATH="/usr/local/lib/node"
```
所以,如果遇到復雜的相對路徑,比如下面這樣。
```javascript
var myModule = require('../../../../lib/myModule');
```
有兩種解決方法,一是將該文件加入`node_modules`目錄,二是修改`NODE_PATH`環境變量,`package.json`文件可以采用下面的寫法。
```javascript
{
"name": "node_path",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "NODE_PATH=lib node index.js"
},
"author": "",
"license": "ISC"
}
```
`NODE_PATH`是歷史遺留下來的一個路徑解決方案,通常不應該使用,而應該使用`node_modules`目錄機制。
### 模塊的循環加載
如果發生模塊的循環加載,即A加載B,B又加載A,則B將加載A的不完整版本。
```javascript
// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
```
上面代碼是三個JavaScript文件。其中,a.js加載了b.js,而b.js又加載a.js。這時,Node返回a.js的不完整版本,所以執行結果如下。
```bash
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
```
修改main.js,再次加載a.js和b.js。
```javascript
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
```
執行上面代碼,結果如下。
```bash
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
main.js a2
main.js b2
```
上面代碼中,第二次加載a.js和b.js時,會直接從緩存讀取exports屬性,所以a.js和b.js內部的console.log語句都不會執行了。
### require.main
`require`方法有一個`main`屬性,可以用來判斷模塊是直接執行,還是被調用執行。
直接執行的時候(`node module.js`),`require.main`屬性指向模塊本身。
```javascript
require.main === module
// true
```
調用執行的時候(通過`require`加載該腳本執行),上面的表達式返回false。
## 模塊的加載機制
CommonJS模塊的加載機制是,輸入的是被輸出的值的拷貝。也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個例子。
下面是一個模塊文件`lib.js`。
```javascript
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
```
上面代碼輸出內部變量`counter`和改寫這個變量的內部方法`incCounter`。
然后,加載上面的模塊。
```javascript
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3
```
上面代碼說明,`counter`輸出以后,`lib.js`模塊內部的變化就影響不到`counter`了。
### require的內部處理流程
`require`命令是CommonJS規范之中,用來加載其他模塊的命令。它其實不是一個全局命令,而是指向當前模塊的`module.require`命令,而后者又調用Node的內部命令`Module._load`。
```javascript
Module._load = function(request, parent, isMain) {
// 1. 檢查 Module._cache,是否緩存之中有指定模塊
// 2. 如果緩存之中沒有,就創建一個新的Module實例
// 3. 將它保存到緩存
// 4. 使用 module.load() 加載指定的模塊文件,
// 讀取文件內容之后,使用 module.compile() 執行文件代碼
// 5. 如果加載/解析過程報錯,就從緩存刪除該模塊
// 6. 返回該模塊的 module.exports
};
```
上面的第4步,采用`module.compile()`執行指定模塊的腳本,邏輯如下。
```javascript
Module.prototype._compile = function(content, filename) {
// 1. 生成一個require函數,指向module.require
// 2. 加載其他輔助方法到require
// 3. 將文件內容放到一個函數之中,該函數可調用 require
// 4. 執行該函數
};
```
上面的第1步和第2步,`require`函數及其輔助方法主要如下。
- `require()`: 加載外部模塊
- `require.resolve()`:將模塊名解析到一個絕對路徑
- `require.main`:指向主模塊
- `require.cache`:指向所有緩存的模塊
- `require.extensions`:根據文件的后綴名,調用不同的執行函數
一旦`require`函數準備完畢,整個所要加載的腳本內容,就被放到一個新的函數之中,這樣可以避免污染全局環境。該函數的參數包括`require`、`module`、`exports`,以及其他一些參數。
```javascript
(function (exports, require, module, __filename, __dirname) {
// YOUR CODE INJECTED HERE!
});
```
`Module._compile`方法是同步執行的,所以`Module._load`要等它執行完成,才會向用戶返回`module.exports`的值。
<h2 id="12.3">package.json文件</h2>
## 概述
每個項目的根目錄下面,一般都有一個`package.json`文件,定義了這個項目所需要的各種模塊,以及項目的配置信息(比如名稱、版本、許可證等元數據)。`npm install`命令根據這個配置文件,自動下載所需的模塊,也就是配置項目所需的運行和開發環境。
下面是一個最簡單的package.json文件,只定義兩項元數據:項目名稱和項目版本。
```javascript
{
"name" : "xxx",
"version" : "0.0.0",
}
```
上面代碼說明,`package.json`文件內部就是一個JSON對象,該對象的每一個成員就是當前項目的一項設置。比如`name`就是項目名稱,`version`是版本(遵守“大版本.次要版本.小版本”的格式)。
下面是一個更完整的package.json文件。
```javascript
{
"name": "Hello World",
"version": "0.0.1",
"author": "張三",
"description": "第一個node.js程序",
"keywords":["node.js","javascript"],
"repository": {
"type": "git",
"url": "https://path/to/url"
},
"license":"MIT",
"engines": {"node": "0.10.x"},
"bugs":{"url":"http://path/to/bug","email":"bug@example.com"},
"contributors":[{"name":"李四","email":"lisi@example.com"}],
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "latest",
"mongoose": "~3.8.3",
"handlebars-runtime": "~1.0.12",
"express3-handlebars": "~0.5.0",
"MD5": "~1.2.0"
},
"devDependencies": {
"bower": "~1.2.8",
"grunt": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-jshint": "~0.7.2",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-clean": "~0.5.0",
"browserify": "2.36.1",
"grunt-browserify": "~1.3.0",
}
}
```
下面詳細解釋package.json文件的各個字段。
## scripts字段
`scripts`指定了運行腳本命令的npm命令行縮寫,比如start指定了運行`npm run start`時,所要執行的命令。
下面的設置指定了`npm run preinstall`、`npm run postinstall`、`npm run start`、`npm run test`時,所要執行的命令。
```javascript
"scripts": {
"preinstall": "echo here it comes!",
"postinstall": "echo there it goes!",
"start": "node index.js",
"test": "tap test/*.js"
}
```
## dependencies字段,devDependencies字段
`dependencies`字段指定了項目運行所依賴的模塊,`devDependencies`指定項目開發所需要的模塊。
它們都指向一個對象。該對象的各個成員,分別由模塊名和對應的版本要求組成,表示依賴的模塊及其版本范圍。
```javascript
{
"devDependencies": {
"browserify": "~13.0.0",
"karma-browserify": "~5.0.1"
}
}
```
對應的版本可以加上各種限定,主要有以下幾種:
- **指定版本**:比如`1.2.2`,遵循“大版本.次要版本.小版本”的格式規定,安裝時只安裝指定版本。
- **波浪號(tilde)+指定版本**:比如`~1.2.2`,表示安裝1.2.x的最新版本(不低于1.2.2),但是不安裝1.3.x,也就是說安裝時不改變大版本號和次要版本號。
- **插入號(caret)+指定版本**:比如ˆ1.2.2,表示安裝1.x.x的最新版本(不低于1.2.2),但是不安裝2.x.x,也就是說安裝時不改變大版本號。需要注意的是,如果大版本號為0,則插入號的行為與波浪號相同,這是因為此時處于開發階段,即使是次要版本號變動,也可能帶來程序的不兼容。
- **latest**:安裝最新版本。
package.json文件可以手工編寫,也可以使用`npm init`命令自動生成。
```bash
$ npm init
```
這個命令采用互動方式,要求用戶回答一些問題,然后在當前目錄生成一個基本的package.json文件。所有問題之中,只有項目名稱(name)和項目版本(version)是必填的,其他都是選填的。
有了package.json文件,直接使用npm install命令,就會在當前目錄中安裝所需要的模塊。
```bash
$ npm install
```
如果一個模塊不在`package.json`文件之中,可以單獨安裝這個模塊,并使用相應的參數,將其寫入`package.json`文件之中。
```bash
$ npm install express --save
$ npm install express --save-dev
```
上面代碼表示單獨安裝express模塊,`--save`參數表示將該模塊寫入`dependencies`屬性,`--save-dev`表示將該模塊寫入`devDependencies`屬性。
## peerDependencies
有時,你的項目和所依賴的模塊,都會同時依賴另一個模塊,但是所依賴的版本不一樣。比如,你的項目依賴A模塊和B模塊的1.0版,而A模塊本身又依賴B模塊的2.0版。
大多數情況下,這不構成問題,B模塊的兩個版本可以并存,同時運行。但是,有一種情況,會出現問題,就是這種依賴關系將暴露給用戶。
最典型的場景就是插件,比如A模塊是B模塊的插件。用戶安裝的B模塊是1.0版本,但是A插件只能和2.0版本的B模塊一起使用。這時,用戶要是將1.0版本的B的實例傳給A,就會出現問題。因此,需要一種機制,在模板安裝的時候提醒用戶,如果A和B一起安裝,那么B必須是2.0模塊。
`peerDependencies`字段,就是用來供插件指定其所需要的主工具的版本。
```javascript
{
"name": "chai-as-promised",
"peerDependencies": {
"chai": "1.x"
}
}
```
上面代碼指定,安裝`chai-as-promised`模塊時,主程序`chai`必須一起安裝,而且`chai`的版本必須是`1.x`。如果你的項目指定的依賴是`chai`的2.0版本,就會報錯。
注意,從npm 3.0版開始,`peerDependencies`不再會默認安裝了。
## bin字段
bin項用來指定各個內部命令對應的可執行文件的位置。
```javascript
"bin": {
"someTool": "./bin/someTool.js"
}
```
上面代碼指定,someTool 命令對應的可執行文件為 bin 子目錄下的 someTool.js。Npm會尋找這個文件,在`node_modules/.bin/`目錄下建立符號鏈接。在上面的例子中,someTool.js會建立符號鏈接`npm_modules/.bin/someTool`。由于`node_modules/.bin/`目錄會在運行時加入系統的PATH變量,因此在運行npm時,就可以不帶路徑,直接通過命令來調用這些腳本。
因此,像下面這樣的寫法可以采用簡寫。
```javascript
scripts: {
start: './node_modules/someTool/someTool.js build'
}
// 簡寫為
scripts: {
start: 'someTool build'
}
```
所有`node_modules/.bin/`目錄下的命令,都可以用`npm run [命令]`的格式運行。在命令行下,鍵入`npm run`,然后按tab鍵,就會顯示所有可以使用的命令。
## main字段
`main`字段指定了加載該模塊時的入門文件,默認是模塊根目錄下面的`index.js`。
## config字段
config字段用于向環境變量輸出值。
下面是一個package.json文件。
```javascript
{
"name" : "foo",
"config" : { "port" : "8080" },
"scripts" : { "start" : "node server.js" }
}
```
然后,在`server.js`腳本就可以引用config字段的值。
```javascript
http.createServer(...).listen(process.env.npm_package_config_port)
```
用戶可以改變這個值。
```bash
$ npm config set foo:port 80
```
## 其他
### browser字段
browser指定該模板供瀏覽器使用的版本。Browserify這樣的瀏覽器打包工具,通過它就知道該打包那個文件。
```javascript
"browser": {
"tipso": "./node_modules/tipso/src/tipso.js"
},
```
### engines字段
engines指明了該項目所需要的node.js版本。
### man字段
man用來指定當前模塊的man文檔的位置。
```javascript
"man" :[ "./doc/calc.1" ]
```
### preferGlobal字段
preferGlobal的值是布爾值,表示當用戶不將該模塊安裝為全局模塊時(即不用--global參數),要不要顯示警告,表示該模塊的本意就是安裝為全局模塊。
### style字段
style指定供瀏覽器使用時,樣式文件所在的位置。樣式文件打包工具parcelify,通過它知道樣式文件的打包位置。
```javascript
"style": [
"./node_modules/tipso/src/tipso.css"
]
```
<h2 id="12.4">npm模塊管理器</h2>
## 簡介
npm有兩層含義。一層含義是Node.js的開放式模塊登記和管理系統,網址為[http://npmjs.org](http://npmjs.org)。另一層含義是Node.js默認的模塊管理器,是一個命令行下的軟件,用來安裝和管理node模塊。
npm不需要單獨安裝。在安裝node的時候,會連帶一起安裝npm。但是,node附帶的npm可能不是最新版本,最好用下面的命令,更新到最新版本。
```bash
$ npm install npm@latest -g
```
上面的命令之所以最后一個參數是npm,是因為npm本身也是Node.js的一個模塊。
Node安裝完成后,可以用下面的命令,查看一下npm的幫助文件。
```bash
# npm命令列表
$ npm help
# 各個命令的簡單用法
$ npm -l
```
下面的命令分別查看npm的版本和配置。
```bash
$ npm -v
$ npm config list -l
```
## npm init
`npm init`用來初始化生成一個新的`package.json`文件。它會向用戶提問一系列問題,如果你覺得不用修改默認配置,一路回車就可以了。
如果使用了`-f`(代表force)、`-y`(代表yes),則跳過提問階段,直接生成一個新的`package.json`文件。
```bash
$ npm init -y
```
## npm set
`npm set`用來設置環境變量。
```bash
$ npm set init-author-name 'Your name'
$ npm set init-author-email 'Your email'
$ npm set init-author-url 'http://yourdomain.com'
$ npm set init-license 'MIT'
```
上面命令等于為`npm init`設置了默認值,以后執行`npm init`的時候,`package.json`的作者姓名、郵件、主頁、許可證字段就會自動寫入預設的值。這些信息會存放在用戶主目錄的` ~/.npmrc`文件,使得用戶不用每個項目都輸入。如果某個項目有不同的設置,可以針對該項目運行`npm config`。
```bash
$ npm set save-exact true
```
上面命令設置加入模塊時,`package.json`將記錄模塊的確切版本,而不是一個可選的版本范圍。
## npm info
`npm info`命令可以查看每個模塊的具體信息。比如,查看underscore模塊的信息。
```bash
$ npm info underscore
{ name: 'underscore',
description: 'JavaScript\'s functional programming helper library.',
'dist-tags': { latest: '1.5.2', stable: '1.5.2' },
repository:
{ type: 'git',
url: 'git://github.com/jashkenas/underscore.git' },
homepage: 'http://underscorejs.org',
main: 'underscore.js',
version: '1.5.2',
devDependencies: { phantomjs: '1.9.0-1' },
licenses:
{ type: 'MIT',
url: 'https://raw.github.com/jashkenas/underscore/master/LICENSE' },
files:
[ 'underscore.js',
'underscore-min.js',
'LICENSE' ],
readmeFilename: 'README.md'}
```
上面命令返回一個JavaScript對象,包含了underscore模塊的詳細信息。這個對象的每個成員,都可以直接從info命令查詢。
```bash
$ npm info underscore description
JavaScript's functional programming helper library.
$ npm info underscore homepage
http://underscorejs.org
$ npm info underscore version
1.5.2
```
## npm search
`npm search`命令用于搜索npm倉庫,它后面可以跟字符串,也可以跟正則表達式。
```bash
$ npm search <搜索詞>
```
下面是一個例子。
```bash
$ npm search node-gyp
// NAME DESCRIPTION
// autogypi Autogypi handles dependencies for node-gyp projects.
// grunt-node-gyp Run node-gyp commands from Grunt.
// gyp-io Temporary solution to let node-gyp run `rebuild` under…
// ...
```
## npm list
`npm list`命令以樹型結構列出當前項目安裝的所有模塊,以及它們依賴的模塊。
```bash
$ npm list
```
加上global參數,會列出全局安裝的模塊。
```bash
$ npm list -global
```
`npm list`命令也可以列出單個模塊。
```bash
$ npm list underscore
```
## npm install
### 基本用法
Node模塊采用`npm install`命令安裝。
每個模塊可以“全局安裝”,也可以“本地安裝”。“全局安裝”指的是將一個模塊安裝到系統目錄中,各個項目都可以調用。一般來說,全局安裝只適用于工具模塊,比如npm和grunt。“本地安裝”指的是將一個模塊下載到當前項目的`node_modules`子目錄,然后只有在項目目錄之中,才能調用這個模塊。
```bash
# 本地安裝
$ npm install <package name>
# 全局安裝
$ sudo npm install -global <package name>
$ sudo npm install -g <package name>
```
`npm install`也支持直接輸入Github代碼庫地址。
```bash
$ npm install git://github.com/package/path.git
$ npm install git://github.com/package/path.git#0.1.0
```
安裝之前,`npm install`會先檢查,`node_modules`目錄之中是否已經存在指定模塊。如果存在,就不再重新安裝了,即使遠程倉庫已經有了一個新版本,也是如此。
如果你希望,一個模塊不管是否安裝過,npm 都要強制重新安裝,可以使用`-f`或`--force`參數。
```bash
$ npm install <packageName> --force
```
如果你希望,所有模塊都要強制重新安裝,那就刪除`node_modules`目錄,重新執行`npm install`。
```bash
$ rm -rf node_modules
$ npm install
```
### 安裝不同版本
install命令總是安裝模塊的最新版本,如果要安裝模塊的特定版本,可以在模塊名后面加上@和版本號。
```bash
$ npm install sax@latest
$ npm install sax@0.1.1
$ npm install sax@">=0.1.0 <0.2.0"
```
如果使用`--save-exact`參數,會在package.json文件指定安裝模塊的確切版本。
```bash
$ npm install readable-stream --save --save-exact
```
install命令可以使用不同參數,指定所安裝的模塊屬于哪一種性質的依賴關系,即出現在packages.json文件的哪一項中。
- --save:模塊名將被添加到dependencies,可以簡化為參數`-S`。
- --save-dev: 模塊名將被添加到devDependencies,可以簡化為參數`-D`。
```bash
$ npm install sax --save
$ npm install node-tap --save-dev
# 或者
$ npm install sax -S
$ npm install node-tap -D
```
如果要安裝beta版本的模塊,需要使用下面的命令。
```bash
# 安裝最新的beta版
$ npm install <module-name>@beta (latest beta)
# 安裝指定的beta版
$ npm install <module-name>@1.3.1-beta.3
```
`npm install`默認會安裝dependencies字段和devDependencies字段中的所有模塊,如果使用production參數,可以只安裝dependencies字段的模塊。
```bash
$ npm install --production
# 或者
$ NODE_ENV=production npm install
```
一旦安裝了某個模塊,就可以在代碼中用require命令調用這個模塊。
```javascript
var backbone = require('backbone')
console.log(backbone.VERSION)
```
## 避免系統權限
默認情況下,Npm全局模塊都安裝在系統目錄(比如`/usr/local/lib/`),普通用戶沒有寫入權限,需要用到`sudo`命令。這不是很方便,我們可以在沒有root權限的情況下,安裝全局模塊。
首先,在主目錄下新建配置文件`.npmrc`,然后在該文件中將`prefix`變量定義到主目錄下面。
```bash
prefix = /home/yourUsername/npm
```
然后在主目錄下新建`npm`子目錄。
```bash
$ mkdir ~/npm
```
此后,全局安裝的模塊都會安裝在這個子目錄中,npm也會到`~/npm/bin`目錄去尋找命令。
最后,將這個路徑在`.bash_profile`文件(或`.bashrc`文件)中加入PATH變量。
```bash
export PATH=~/npm/bin:$PATH
```
## npm update,npm uninstall
`npm update`命令可以更新本地安裝的模塊。
```bash
# 升級當前項目的指定模塊
$ npm update [package name]
# 升級全局安裝的模塊
$ npm update -global [package name]
```
它會先到遠程倉庫查詢最新版本,然后查詢本地版本。如果本地版本不存在,或者遠程版本較新,就會安裝。
使用`-S`或`--save`參數,可以在安裝的時候更新`package.json`里面模塊的版本號。
```javascript
// 更新之前的package.json
dependencies: {
dep1: "^1.1.1"
}
// 更新之后的package.json
dependencies: {
dep1: "^1.2.2"
}
```
注意,從npm v2.6.1 開始,`npm update`只更新頂層模塊,而不更新依賴的依賴,以前版本是遞歸更新的。如果想取到老版本的效果,要使用下面的命令。
```bash
$ npm --depth 9999 update
```
`npm uninstall`命令,卸載已安裝的模塊。
```bash
$ npm uninstall [package name]
# 卸載全局模塊
$ npm uninstall [package name] -global
```
## npm run
npm不僅可以用于模塊管理,還可以用于執行腳本。`package.json`文件有一個`scripts`字段,可以用于指定腳本命令,供npm直接調用。
```javascript
{
"name": "myproject",
"devDependencies": {
"jshint": "latest",
"browserify": "latest",
"mocha": "latest"
},
"scripts": {
"lint": "jshint **.js",
"test": "mocha test/"
}
}
```
上面代碼中,`scripts`字段指定了兩項命令`lint`和`test`。命令行輸入`npm run-script lint`或者`npm run lint`,就會執行`jshint **.js`,輸入`npm run-script test`或者`npm run test`,就會執行`mocha test/`。`npm run`是`npm run-script`的縮寫,一般都使用前者,但是后者可以更好地反應這個命令的本質。
`npm run`命令會自動在環境變量`$PATH`添加`node_modules/.bin`目錄,所以`scripts`字段里面調用命令時不用加上路徑,這就避免了全局安裝NPM模塊。
npm內置了兩個命令簡寫,`npm test`等同于執行`npm run test`,`npm start`等同于執行`npm run start`。
`npm run`會創建一個Shell,執行指定的命令,并臨時將`node_modules/.bin`加入PATH變量,這意味著本地模塊可以直接運行。
舉例來說,你執行ESLint的安裝命令。
```bash
$ npm i eslint --save-dev
```
運行上面的命令以后,會產生兩個結果。首先,ESLint被安裝到當前目錄的`node_modules`子目錄;其次,`node_modules/.bin`目錄會生成一個符號鏈接`node_modules/.bin/eslint`,指向ESLint模塊的可執行腳本。
然后,你就可以在`package.json`的`script`屬性里面,不帶路徑的引用`eslint`這個腳本。
```javascript
{
"name": "Test Project",
"devDependencies": {
"eslint": "^1.10.3"
},
"scripts": {
"lint": "eslint ."
}
}
```
等到運行`npm run lint`的時候,它會自動執行`./node_modules/.bin/eslint .`。
如果直接運行`npm run`不給出任何參數,就會列出`scripts`屬性下所有命令。
```bash
$ npm run
Available scripts in the user-service package:
lint
jshint **.js
test
mocha test/
```
下面是另一個`package.json`文件的例子。
```javascript
"scripts": {
"watch": "watchify client/main.js -o public/app.js -v",
"build": "browserify client/main.js -o public/app.js",
"start": "npm run watch & nodemon server.js",
"test": "node test/all.js"
},
```
上面代碼在`scripts`項,定義了四個別名,每個別名都有對應的腳本命令。
```bash
$ npm run watch
$ npm run build
$ npm run start
$ npm run test
```
其中,`start`和`test`屬于特殊命令,可以省略`run`。
```bash
$ npm start
$ npm test
```
如果希望一個操作的輸出,是另一個操作的輸入,可以借用Linux系統的管道命令,將兩個操作連在一起。
```javascript
"build-js": "browserify browser/main.js | uglifyjs -mc > static/bundle.js"
```
但是,更方便的寫法是引用其他`npm run`命令。
```javascript
"build": "npm run build-js && npm run build-css"
```
上面的寫法是先運行`npm run build-js`,然后再運行`npm run build-css`,兩個命令中間用`&&`連接。如果希望兩個命令同時平行執行,它們中間可以用`&`連接。
下面是一個流操作的例子。
```javascript
"devDependencies": {
"autoprefixer": "latest",
"cssmin": "latest"
},
"scripts": {
"build:css": "autoprefixer -b 'last 2 versions' < assets/styles/main.css | cssmin > dist/main.css"
}
```
寫在`scripts`屬性中的命令,也可以在`node_modules/.bin`目錄中直接寫成bash腳本。下面是一個bash腳本。
```javascript
#!/bin/bash
cd site/main
browserify browser/main.js | uglifyjs -mc > static/bundle.js
```
假定上面的腳本文件名為build.sh,并且權限為可執行,就可以在scripts屬性中引用該文件。
```javascript
"build-js": "bin/build.sh"
```
### 參數
`npm run`命令還可以添加參數。
```javascript
"scripts": {
"test": "mocha test/"
}
```
上面代碼指定`npm test`,實際運行`mocha test/`。如果要通過`npm test`命令,將參數傳到mocha,則參數之前要加上兩個連詞線。
```bash
$ npm run test -- anothertest.js
# 等同于
$ mocha test/ anothertest.js
```
上面命令表示,mocha要運行所有`test`子目錄的測試腳本,以及另外一個測試腳本`anothertest.js`。
`npm run`本身有一個參數`-s`,表示關閉npm本身的輸出,只輸出腳本產生的結果。
```bash
// 輸出npm命令頭
$ npm run test
// 不輸出npm命令頭
$ npm run -s test
```
### scripts腳本命令最佳實踐
`scripts`字段的腳本命令,有一些最佳實踐,可以方便開發。首先,安裝`npm-run-all`模塊。
```bash
$ npm install npm-run-all --save-dev
```
這個模塊用于運行多個`scripts`腳本命令。
```bash
# 繼發執行
$ npm-run-all build:html build:js
# 等同于
$ npm run build:html && npm run build:js
# 并行執行
$ npm-run-all --parallel watch:html watch:js
# 等同于
$ npm run watch:html & npm run watch:js
# 混合執行
$ npm-run-all clean lint --parallel watch:html watch:js
# 等同于
$ npm-run-all clean lint
$ npm-run-all --parallel watch:html watch:js
# 通配符
$ npm-run-all --parallel watch:*
```
(1)start腳本命令
`start`腳本命令,用于啟動應用程序。
```javascript
"start": "npm-run-all --parallel dev serve"
```
上面命令并行執行`dev`腳本命令和`serve`腳本命令,等同于下面的形式。
```bash
$ npm run dev & npm run serve
```
如果start腳本沒有配置,`npm start`命令默認執行下面的腳本,前提是模塊的根目錄存在一個server.js文件。
```bash
$ node server.js
```
(2)dev腳本命令
`dev`腳本命令,規定開發階段所要做的處理,比如構建網頁資源。
```javascript
"dev": "npm-run-all dev:*"
```
上面命令用于繼發執行所有`dev`的子命令。
```javascript
"predev:sass": "node-sass --source-map src/css/hoodie.css.map --output-style nested src/sass/base.scss src/css/hoodie.css"
```
上面命令將sass文件編譯為css文件,并生成source map文件。
```javascript
"dev:sass": "node-sass --source-map src/css/hoodie.css.map --watch --output-style nested src/sass/base.scss src/css/hoodie.css"
```
上面命令會監視sass文件的變動,只要有變動,就自動將其編譯為css文件。
```javascript
"dev:autoprefix": "postcss --use autoprefixer --autoprefixer.browsers \"> 5%\" --output src/css/hoodie.css src/css/hoodie.css"
```
上面命令為css文件加上瀏覽器前綴,限制條件是只考慮市場份額大于5%的瀏覽器。
(3)serve腳本命令
`serve`腳本命令用于啟動服務。
```javascript
"serve": "live-server dist/ --port=9090"
```
上面命令啟動服務,用的是[live-server](http://npmjs.com/package/live-server)模塊,將服務啟動在9090端口,展示`dist`子目錄。
`live-server`模塊有三個功能。
- 啟動一個HTTP服務器,展示指定目錄的`index.html`文件,通過該文件加載各種網絡資源,這是`file://`協議做不到的。
- 添加自動刷新功能。只要指定目錄之中,文件有任何變化,它就會刷新頁面。
- `npm run serve`命令執行以后,自動打開瀏覽器。、
以前,上面三個功能需要三個模塊來完成:`http-server`、`live-reload`和`opener`,現在只要`live-server`一個模塊就夠了。
(4)test腳本命令
`test`腳本命令用于執行測試。
```javascript
"test": "npm-run-all test:*",
"test:lint": "sass-lint --verbose --config .sass-lint.yml src/sass/*"
```
上面命令規定,執行測試時,運行`lint`腳本,檢查腳本之中的語法錯誤。
(5)prod腳本命令
`prod`腳本命令,規定進入生產環境時需要做的處理。
```javascript
"prod": "npm-run-all prod:*",
"prod:sass": "node-sass --output-style compressed src/sass/base.scss src/css/prod/hoodie.min.css",
"prod:autoprefix": "postcss --use autoprefixer --autoprefixer.browsers "> 5%" --output src/css/prod/hoodie.min.css src/css/prod/hoodie.min.css"
```
上面命令將sass文件轉為css文件,并加上瀏覽器前綴。
(6)help腳本命令
`help`腳本命令用于展示幫助信息。
```javascript
"help": "markdown-chalk --input DEVELOPMENT.md"
```
上面命令之中,`markdown-chalk`模塊用于將指定的markdown文件,轉為彩色文本顯示在終端之中。
(7)docs腳本命令
`docs`腳本命令用于生成文檔。
```javascript
"docs": "kss-node --source src/sass --homepage ../../styleguide.md"
```
上面命令使用`kss-node`模塊,提供源碼的注釋生成markdown格式的文檔。
### pre- 和 post- 腳本
`npm run`為每條命令提供了`pre-`和`post-`兩個鉤子(hook)。以`npm run lint`為例,執行這條命令之前,npm會先查看有沒有定義prelint和postlint兩個鉤子,如果有的話,就會先執行`npm run prelint`,然后執行`npm run lint`,最后執行`npm run postlint`。
```javascript
{
"name": "myproject",
"devDependencies": {
"eslint": "latest"
"karma": "latest"
},
"scripts": {
"lint": "eslint --cache --ext .js --ext .jsx src",
"test": "karma start --log-leve=error karma.config.js --single-run=true",
"pretest": "npm run lint",
"posttest": "echo 'Finished running tests'"
}
}
```
上面代碼是一個`package.json`文件的例子。如果執行`npm test`,會按下面的順序執行相應的命令。
1. `pretest`
1. `test`
1. `posttest`
如果執行過程出錯,就不會執行排在后面的腳本,即如果prelint腳本執行出錯,就不會接著執行lint和postlint腳本。
下面是一個例子。
```javascript
{
"test": "karma start",
"test:lint": "eslint . --ext .js --ext .jsx",
"pretest": "npm run test:lint"
}
```
上面代碼中,在運行`npm run test`之前,會自動檢查代碼,即運行`npm run test:lint`命令。
下面是一些常見的`pre-`和`post-`腳本。
- `prepublish`:發布一個模塊前執行。
- `postpublish`:發布一個模塊后執行。
- `preinstall`:用戶執行`npm install`命令時,先執行該腳本。
- `postinstall`:用戶執行`npm install`命令時,安裝結束后執行該腳本,通常用于將下載的源碼編譯成用戶需要的格式,比如有些模塊需要在用戶機器上跟本地的C++模塊一起編譯。
- `preuninstall`:卸載一個模塊前執行。
- `postuninstall`:卸載一個模塊后執行。
- `preversion`:更改模塊版本前執行。
- `postversion`:更改模塊版本后執行。
- `pretest`:運行`npm test`命令前執行。
- `posttest`:運行`npm test`命令后執行。
- `prestop`:運行`npm stop`命令前執行。
- `poststop`:運行`npm stop`命令后執行。
- `prestart`:運行`npm start`命令前執行。
- `poststart`:運行`npm start`命令后執行。
- `prerestart`:運行`npm restart`命令前執行。
- `postrestart`:運行`npm restart`命令后執行。
對于最后一個`npm restart`命令,如果沒有設置`restart`腳本,`prerestart`和`postrestart`會依次執行stop和start腳本。
另外,不能在`pre`腳本之前再加`pre`,即`prepretest`腳本不起作用。
注意,即使Npm可以自動運行`pre`和`post`腳本,也可以手動執行它們。
```bash
$ npm run prepublish
```
下面是`post install`的例子。
```javascript
{
"postinstall": "node lib/post_install.js"
}
```
上面的這個命令,主要用于處理從Git倉庫拉下來的源碼。比如,有些源碼是用TypeScript寫的,可能需要轉換一下。
下面是`publish`鉤子的一個例子。
```javascript
{
"dist:modules": "babel ./src --out-dir ./dist-modules",
"gh-pages": "webpack",
"gh-pages:deploy": "gh-pages -d gh-pages",
"prepublish": "npm run dist:modules",
"postpublish": "npm run gh-pages && npm run gh-pages:deploy"
}
```
上面命令在運行`npm run publish`時,會先執行Babel編譯,然后調用Webpack構建,最后發到Github Pages上面。
以上都是npm相關操作的鉤子,如果安裝某些模塊,還能支持Git相關的鉤子。下面以[husky](https://github.com/typicode/husky)模塊為例。
```bash
$ npm install husky --save-dev
```
安裝以后,就能在`package.json`添加`precommit`、`prepush`等鉤子。
```javascript
{
"scripts": {
"lint": "eslint yourJsFiles.js",
"precommit": "npm run test && npm run lint",
"prepush": "npm run test && npm run lint",
"...": "..."
}
}
```
類似作用的模塊還有`pre-commit`、`precommit-hook`等。
### 內部變量
scripts字段可以使用一些內部變量,主要是package.json的各種字段。
比如,package.json的內容是`{"name":"foo", "version":"1.2.5"}`,那么變量`npm_package_name`的值是foo,變量`npm_package_version`的值是1.2.5。
```javascript
{
"scripts":{
"bundle": "mkdir -p build/$npm_package_version/"
}
}
```
運行`npm run bundle`以后,將會生成`build/1.2.5/`子目錄。
`config`字段也可以用于設置內部字段。
```javascript
"name": "fooproject",
"config": {
"reporter": "xunit"
},
"scripts": {
"test": "mocha test/ --reporter $npm_package_config_reporter"
}
```
上面代碼中,變量`npm_package_config_reporter`對應的就是reporter。
### 通配符
npm的通配符的規則如下。
- `*` 匹配0個或多個字符
- `?` 匹配1個字符
- `[...]` 匹配某個范圍的字符。如果該范圍的第一個字符是`!`或`^`,則匹配不在該范圍的字符。
- `!(pattern|pattern|pattern)` 匹配任何不符合給定的模式
- `?(pattern|pattern|pattern)` 匹配0個或1個給定的模式
- `+(pattern|pattern|pattern)` 匹配1個或多個給定的模式
- `*(a|b|c)` 匹配0個或多個給定的模式
- `@(pattern|pat*|pat?erN)` 只匹配給定模式之一
- `**` 如果出現在路徑部分,表示0個或多個子目錄。
## npm link
開發Npm模塊的時候,有時我們會希望,邊開發邊試用。但是,常規情況下,使用一個模塊,需要將其安裝到`node_modules`目錄之中,這對于開發中的模塊,顯然非常不方便。`npm link`就能起到這個作用,建立一個符號鏈接,在全局的`node_modules`目錄之中,生成一個符號鏈接,指向模塊的本地目錄。
為了理解`npm link`,請設想這樣一個場景。你開發了一個模塊`myModule`,目錄為`src/myModule`,你自己的項目`myProject`要用到這個模塊,項目目錄為`src/myProject`。每一次,你更新`myModul`e,就要用`npm publish`命令發布,然后切換到項目目錄,使用`npm update`更新模塊。這樣顯然很不方便,如果我們可以從項目目錄建立一個符號鏈接,直接連到模塊目錄,就省去了中間步驟,項目可以直接使用最新版的模塊。
首先,在模塊目錄(`src/myModule`)下運行`npm link`命令。
```bash
src/myModule$ npm link
```
上面的命令會在Npm的全局模塊目錄內,生成一個符號鏈接文件,該文件的名字就是`package.json`文件中指定的文件名。
```bash
/path/to/global/node_modules/myModule -> src/myModule
```
這個時候,已經可以全局調用`myModule`模塊了。但是,如果我們要讓這個模塊安裝在項目內,還要進行下面的步驟。
切換到項目目錄,再次運行`npm link`命令,并指定模塊名。
```bash
src/myProject$ npm link myModule
```
上面命令等同于生成了本地模塊的符號鏈接。
```bash
src/myProject/node_modules/myModule -> /path/to/global/node_modules/myModule
```
然后,就可以在你的項目中,加載該模塊了。
```javascript
var myModule = require('myModule');
```
這樣一來,`myModule`的任何變化,都可以直接反映在`myProject`項目之中。但是,這樣也出現了風險,任何在`myProject`目錄中對`myModule`的修改,都會反映到模塊的源碼中。
如果你的項目不再需要該模塊,可以在項目目錄內使用`npm unlink`命令,刪除符號鏈接。
```bash
src/myProject$ npm unlink myModule
```
## npm bin
`npm bin`命令顯示相對于當前目錄的,Node模塊的可執行腳本所在的目錄(即`.bin`目錄)。
```bash
# 項目根目錄下執行
$ npm bin
./node_modules/.bin
```
## npm adduser
`npm adduser`用于在npmjs.com注冊一個用戶。
```bash
$ npm adduser
Username: YOUR_USER_NAME
Password: YOUR_PASSWORD
Email: YOUR_EMAIL@domain.com
```
## npm publish
`npm publish`用于將當前模塊發布到`npmjs.com`。執行之前,需要向`npmjs.com`申請用戶名。
```bash
$ npm adduser
```
如果已經注冊過,就使用下面的命令登錄。
```bash
$ npm login
```
登錄以后,就可以使用`npm publish`命令發布。
```bash
$ npm publish
```
如果當前模塊是一個beta版,比如`1.3.1-beta.3`,那么發布的時候需要使用`tag`參數。
```bash
$ npm publish --tag beta
```
如果發布私有模塊,模塊初始化的時候,需要加上`scope`參數。只有npm的付費用戶才能發布私有模塊。
```bash
$ npm init --scope=<yourscope>
```
如果你的模塊是用ES6寫的,那么發布的時候,最好轉成ES5。首先,需要安裝Babel。
```javascript
$ npm install --save-dev babel-cli@6 babel-preset-es2015@6
```
然后,在`package.json`里面寫入`build`腳本。
```javascript
"scripts": {
"build": "babel source --presets babel-preset-es2015 --out-dir distribution",
"prepublish": "npm run build"
}
```
運行上面的腳本,會將`source`目錄里面的ES6源碼文件,轉為`distribution`目錄里面的ES5源碼文件。然后,在項目根目錄下面創建兩個文件`.npmignore`和`.gitignore`,分別寫入以下內容。
```javascrip
// .npmignore
source
// .gitignore
node_modules
distribution
```
## npm deprecate
如果想廢棄某個版本的模塊,可以使用`npm deprecate`命令。
```bash
$ npm deprecate my-thing@"< 0.2.3" "critical bug fixed in v0.2.3"
```
運行上面的命令以后,小于`0.2.3`版本的模塊的`package.json`都會寫入一行警告,用戶安裝這些版本時,這行警告就會在命令行顯示。
## npm owner
模塊的維護者可以發布新版本。`npm owner`命令用于管理模塊的維護者。
```bash
# 列出指定模塊的維護者
$ npm owner ls <package name>
# 新增維護者
$ npm owner add <user> <package name>
# 刪除維護者
$ npm owner rm <user> <package name>
```
<h2 id="12.5">fs 模塊</h2>
fs是filesystem的縮寫,該模塊提供本地文件的讀寫能力,基本上是POSIX文件操作命令的簡單包裝。但是,這個模塊幾乎對所有操作提供異步和同步兩種操作方式,供開發者選擇。
## readFileSync()
readFileSync方法用于同步讀取文件,返回一個字符串。
```javascript
var text = fs.readFileSync(fileName, "utf8");
// 將文件按行拆成數組
text.split(/\r?\n/).forEach(function (line) {
// ...
});
```
該方法的第一個參數是文件路徑,第二個參數是文本文件編碼,默認為utf8。
不同系統的行結尾字符不同,可以用下面的方法判斷。
```javascript
// 方法一,查詢現有的行結尾字符
var EOL = fileContents.indexOf("\r\n") >= 0 ? "\r\n" : "\n";
// 方法二,根據當前系統處理
var EOL = (process.platform === 'win32' ? '\r\n' : '\n')
```
## writeFileSync()
writeFileSync方法用于同步寫入文件。
```
fs.writeFileSync(fileName, str, 'utf8');
```
它的第一個參數是文件路徑,第二個參數是寫入文件的字符串,第三個參數是文件編碼,默認為utf8。
## exists(path, callback)
exists方法用來判斷給定路徑是否存在,然后不管結果如何,都會調用回調函數。
```javascript
fs.exists('/path/to/file', function (exists) {
util.debug(exists ? "it's there" : "no file!");
});
```
上面代碼表明,回調函數的參數是一個表示文件是否存在的布爾值。
需要注意的是,不要在open方法之前調用exists方法,open方法本身就能檢查文件是否存在。
下面的例子是如果給定目錄存在,就刪除它。
```javascript
if(fs.exists(outputFolder)) {
console.log("Removing "+outputFolder);
fs.rmdir(outputFolder);
}
```
## mkdir(),writeFile(),readfile()
mkdir方法用于新建目錄。
```javascript
var fs = require('fs');
fs.mkdir('./helloDir',0777, function (err) {
if (err) throw err;
});
```
mkdir接受三個參數,第一個是目錄名,第二個是權限值,第三個是回調函數。
writeFile方法用于寫入文件。
```javascript
var fs = require('fs');
fs.writeFile('./helloDir/message.txt', 'Hello Node', function (err) {
if (err) throw err;
console.log('文件寫入成功');
});
```
readfile方法用于讀取文件內容。
```javascript
var fs = require('fs');
fs.readFile('./helloDir/message.txt','UTF-8' ,function (err, data) {
if (err) throw err;
console.log(data);
});
```
上面代碼使用readFile方法讀取文件。readFile方法的第一個參數是文件名,第二個參數是文件編碼,第三個參數是回調函數。可用的文件編碼包括“ascii”、“utf8”和“base64”。如果沒有指定文件編碼,返回的是原始的緩存二進制數據,這時需要調用buffer對象的toString方法,將其轉為字符串。
```javascript
var fs = require('fs');
fs.readFile('example_log.txt', function (err, logData) {
if (err) throw err;
var text = logData.toString();
});
```
readFile方法是異步操作,所以必須小心,不要同時發起多個readFile請求。
```js
for(var i = 1; i <= 1000; i++) {
fs.readFile('./'+i+'.txt', function() {
// do something with the file
});
}
```
上面代碼會同時發起1000個readFile異步請求,很快就會耗盡系統資源。
## mkdirSync(),writeFileSync(),readFileSync()
這三個方法是建立目錄、寫入文件、讀取文件的同步版本。
```javascript
fs.mkdirSync('./helloDirSync',0777);
fs.writeFileSync('./helloDirSync/message.txt', 'Hello Node');
var data = fs.readFileSync('./helloDirSync/message.txt','UTF-8');
console.log('file created with contents:');
console.log(data);
```
對于流量較大的服務器,最好還是采用異步操作,因為同步操作時,只有前一個操作結束,才會開始后一個操作,如果某個操作特別耗時(常常發生在讀寫數據時),會導致整個程序停頓。
## readdir()
readdir方法用于讀取目錄,返回一個所包含的文件和子目錄的數組。
```javascript
fs.readdir(process.cwd(), function (err, files) {
if (err) {
console.log(err);
return;
}
var count = files.length;
var results = {};
files.forEach(function (filename) {
fs.readFile(filename, function (data) {
results[filename] = data;
count--;
if (count <= 0) {
// 對所有文件進行處理
}
});
});
});
```
## stat()
stat方法的參數是一個文件或目錄,它產生一個對象,該對象包含了該文件或目錄的具體信息。我們往往通過該方法,判斷正在處理的到底是一個文件,還是一個目錄。
```javascript
var fs = require('fs');
fs.readdir('/etc/', function (err, files) {
if (err) throw err;
files.forEach( function (file) {
fs.stat('/etc/' + file, function (err, stats) {
if (err) throw err;
if (stats.isFile()) {
console.log("%s is file", file);
}
else if (stats.isDirectory ()) {
console.log("%s is a directory", file);
}
console.log('stats: %s',JSON.stringify(stats));
});
});
});
```
## watchfile(),unwatchfile()
watchfile方法監聽一個文件,如果該文件發生變化,就會自動觸發回調函數。
```javascript
var fs = require('fs');
fs.watchFile('./testFile.txt', function (curr, prev) {
console.log('the current mtime is: ' + curr.mtime);
console.log('the previous mtime was: ' + prev.mtime);
});
fs.writeFile('./testFile.txt', "changed", function (err) {
if (err) throw err;
console.log("file write complete");
});
```
unwatchfile方法用于解除對文件的監聽。
## createReadStream()
createReadStream方法往往用于打開大型的文本文件,創建一個讀取操作的數據流。所謂大型文本文件,指的是文本文件的體積很大,讀取操作的緩存裝不下,只能分成幾次發送,每次發送會觸發一個data事件,發送結束會觸發end事件。
```javascript
var fs = require('fs');
function readLines(input, func) {
var remaining = '';
input.on('data', function(data) {
remaining += data;
var index = remaining.indexOf('\n');
var last = 0;
while (index > -1) {
var line = remaining.substring(last, index);
last = index + 1;
func(line);
index = remaining.indexOf('\n', last);
}
remaining = remaining.substring(last);
});
input.on('end', function() {
if (remaining.length > 0) {
func(remaining);
}
});
}
function func(data) {
console.log('Line: ' + data);
}
var input = fs.createReadStream('lines.txt');
readLines(input, func);
```
## createWriteStream()
createWriteStream方法創建一個寫入數據流對象,該對象的write方法用于寫入數據,end方法用于結束寫入操作。
```javascript
var out = fs.createWriteStream(fileName, { encoding: "utf8" });
out.write(str);
out.end();
```
<h2 id="12.6">Path模塊</h2>
## path.join()
`path.join`方法用于連接路徑。該方法的主要用途在于,會正確使用當前系統的路徑分隔符,Unix系統是”/“,Windows系統是”\“。
```javascript
var path = require('path');
path.join(mydir, "foo");
```
上面代碼在Unix系統下,會返回路徑`mydir/foo`。
## path.resolve()
`path.resolve`方法用于將相對路徑轉為絕對路徑。
它可以接受多個參數,依次表示所要進入的路徑,直到將最后一個參數轉為絕對路徑。如果根據參數無法得到絕對路徑,就以當前所在路徑作為基準。除了根目錄,該方法的返回值都不帶尾部的斜杠。
```javascript
// 格式
path.resolve([from ...], to)
// 實例
path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
```
上面代碼的實例,執行效果類似下面的命令。
```bash
$ cd foo/bar
$ cd /tmp/file/
$ cd ..
$ cd a/../subfile
$ pwd
```
更多例子。
```javascript
path.resolve('/foo/bar', './baz')
// '/foo/bar/baz'
path.resolve('/foo/bar', '/tmp/file/')
// '/tmp/file'
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')
// 如果當前目錄是/home/myself/node,返回
// /home/myself/node/wwwroot/static_files/gif/image.gif
```
該方法忽略非字符串的參數。
## accessSync()
`accessSync`方法用于同步讀取一個路徑。
下面的代碼可以用于判斷一個目錄是否存在。
```javascript
function exists(pth, mode) {
try {
fs.accessSync(pth, mode);
return true;
} catch (e) {
return false;
}
}
```
## path.relative
`path.relative`方法接受兩個參數,這兩個參數都應該是絕對路徑。該方法返回第二個路徑想對于地一個路徑的系那個相對路徑。
```javascript
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb')
// '../../impl/bbb'
```
上面代碼中,如果當前目錄是`/data/orandea/test/aaa`,進入`path.relative`返回的相對路徑,就會到達`/data/orandea/impl/bbb`。
如果`path.relative`方法的兩個參數相同,則返回一個空字符串。
<h2 id="12.7">process對象</h2>
`process`對象是Node的一個全局對象,提供當前Node進程的信息。它可以在腳本的任意位置使用,不必通過`require`命令加載。該對象部署了`EventEmitter`接口。
## 進程信息
通過`process`對象,可以獲知當前進程的很多信息。
### 退出碼
進程退出時,會返回一個整數值,表示退出時的狀態。這個整數值就叫做退出碼。下面是常見的Node進程退出碼。
- 0,正常退出
- 1,發生未捕獲錯誤
- 5,V8執行錯誤
- 8,不正確的參數
- 128 + 信號值,如果Node接受到退出信號(比如SIGKILL或SIGHUP),它的退出碼就是128加上信號值。由于128的二進制形式是10000000, 所以退出碼的后七位就是信號值。
## 屬性
process對象提供一系列屬性,用于返回系統信息。
- **process.argv**:返回當前進程的命令行參數數組。
- **process.env**:返回一個對象,成員為當前Shell的環境變量,比如`process.env.HOME`。
- **process.installPrefix**:node的安裝路徑的前綴,比如`/usr/local`,則node的執行文件目錄為`/usr/local/bin/node`。
- **process.pid**:當前進程的進程號。
- **process.platform**:當前系統平臺,比如Linux。
- **process.title**:默認值為“node”,可以自定義該值。
- **process.version**:Node的版本,比如v0.10.18。
下面是主要屬性的介紹。
### stdout,stdin,stderr
以下屬性指向系統IO。
**(1)stdout**
stdout屬性指向標準輸出(文件描述符1)。它的write方法等同于console.log,可用在標準輸出向用戶顯示內容。
```javascript
console.log = function(d) {
process.stdout.write(d + '\n');
};
```
下面代碼表示將一個文件導向標準輸出。
```javascript
var fs = require('fs');
fs.createReadStream('wow.txt')
.pipe(process.stdout);
```
上面代碼中,由于process.stdout和process.stdin與其他進程的通信,都是流(stream)形式,所以必須通過pipe管道命令中介。
```javascript
var fs = require('fs');
var zlib = require('zlib');
fs.createReadStream('wow.txt')
.pipe(zlib.createGzip())
.pipe(process.stdout);
```
上面代碼通過pipe方法,先將文件數據壓縮,然后再導向標準輸出。
**(2)stdin**
stdin代表標準輸入(文件描述符0)。
```javascript
process.stdin.pipe(process.stdout)
```
上面代碼表示將標準輸入導向標準輸出。
由于stdin和stdout都部署了stream接口,所以可以使用stream接口的方法。
```javascript
process.stdin.setEncoding('utf8');
process.stdin.on('readable', function() {
var chunk = process.stdin.read();
if (chunk !== null) {
process.stdout.write('data: ' + chunk);
}
});
process.stdin.on('end', function() {
process.stdout.write('end');
});
```
**(3)stderr**
stderr屬性指向標準錯誤(文件描述符2)。
### argv,execPath,execArgv
argv屬性返回一個數組,由命令行執行腳本時的各個參數組成。它的第一個成員總是node,第二個成員是腳本文件名,其余成員是腳本文件的參數。
請看下面的例子,新建一個腳本文件argv.js。
```javascript
// argv.js
console.log("argv: ",process.argv);
```
在命令行下調用這個腳本,會得到以下結果。
```javascript
$ node argv.js a b c
[ 'node', '/path/to/argv.js', 'a', 'b', 'c' ]
```
上面代碼表示,argv返回數組的成員依次是命令行的各個部分,真正的參數實際上是從`process.argv[2]`開始。要得到真正的參數部分,可以把argv.js改寫成下面這樣。
```javascript
// argv.js
var myArgs = process.argv.slice(2);
console.log(myArgs);
```
execPath屬性返回執行當前腳本的Node二進制文件的絕對路徑。
```javascript
> process.execPath
'/usr/local/bin/node'
>
```
execArgv屬性返回一個數組,成員是命令行下執行腳本時,在Node可執行文件與腳本文件之間的命令行參數。
```bash
# script.js的代碼為
# console.log(process.execArgv);
$ node --harmony script.js --version
```
### process.env
`process.env`屬性返回一個對象,包含了當前Shell的所有環境變量。比如,`process.env.HOME`返回用戶的主目錄。
通常的做法是,新建一個環境變量`NODE_ENV`,用它確定當前所處的開發階段,生產階段設為`production`,開發階段設為`develop`或`staging`,然后在腳本中讀取`process.env.NODE_ENV`即可。
運行腳本時,改變環境變量,可以采用下面的寫法。
```bash
$ export NODE_ENV=production && node app.js
# 或者
$ NODE_ENV=production node app.js
```
## 方法
process對象提供以下方法:
- **process.chdir()**:切換工作目錄到指定目錄。
- **process.cwd()**:返回運行當前腳本的工作目錄的路徑。
- **process.exit()**:退出當前進程。
- **process.getgid()**:返回當前進程的組ID(數值)。
- **process.getuid()**:返回當前進程的用戶ID(數值)。
- **process.nextTick()**:指定回調函數在當前執行棧的尾部、下一次Event Loop之前執行。
- **process.on()**:監聽事件。
- **process.setgid()**:指定當前進程的組,可以使用數字ID,也可以使用字符串ID。
- **process.setuid()**:指定當前進程的用戶,可以使用數字ID,也可以使用字符串ID。
### process.cwd(),process.chdir()
`cwd`方法返回進程的當前目錄(絕對路徑),`chdir`方法用來切換目錄。
```bash
> process.cwd()
'/home/aaa'
> process.chdir('/home/bbb')
> process.cwd()
'/home/bbb'
```
注意,`process.cwd()`與`__dirname`的區別。前者進程發起時的位置,后者是腳本的位置,兩者可能是不一致的。比如,`node ./code/program.js`,對于`process.cwd()`來說,返回的是當前目錄(`.`);對于`__dirname`來說,返回是腳本所在目錄,即`./code/program.js`。
## process.nextTick()
`process.nextTick`將任務放到當前一輪事件循環(Event Loop)的尾部。
```bash
process.nextTick(function () {
console.log('下一次Event Loop即將開始!');
});
```
上面代碼可以用`setTimeout(f,0)`改寫,效果接近,但是原理不同。
```bash
setTimeout(function () {
console.log('已經到了下一輪Event Loop!');
}, 0)
```
`setTimeout(f,0)`是將任務放到下一輪事件循環的頭部,因此`nextTick`會比它先執行。另外,`nextTick`的效率更高,因為不用檢查是否到了指定時間。
根據Node的事件循環的實現,基本上,進入下一輪事件循環后的執行順序如下。
1. `setTimeout(f,0)`
1. 各種到期的回調函數
1. `process.nextTick`
### process.exit()
`process.exit`方法用來退出當前進程,它可以接受一個數值參數。如果參數大于0,表示執行失敗;如果等于0表示執行成功。
```bash
if (err) {
process.exit(1);
} else {
process.exit(0);
}
```
`process.exit()`執行時,會觸發`exit`事件。
### process.on()
`process.on`方法用來監聽各種事件,并指定回調函數。
```javascript
process.on('uncaughtException', function(err){
console.log('got an error: %s', err.message);
process.exit(1);
});
setTimeout(function(){
throw new Error('fail');
}, 100);
```
上面代碼是`process`監聽Node的一個全局性事件`uncaughtException`,只要有錯誤沒有捕獲,就會觸發這個事件。
process支持的事件有以下一些。
- data事件:數據輸出輸入時觸發
- SIGINT事件:接收到系統信號時觸發
```javascript
process.on('SIGINT', function () {
console.log('Got a SIGINT. Goodbye cruel world');
process.exit(0);
});
```
使用時,向該進程發出系統信號,就會導致進程退出。
```bash
$ kill -s SIGINT [process_id]
```
SIGTERM信號表示內核要求當前進程停止,進程可以自行停止,也可以忽略這個信號。
```javascript
var http = require('http');
var server = http.createServer(function (req, res) {
});
process.on('SIGTERM', function () {
server.close(function () {
process.exit(0);
});
});
```
上面代碼表示,進程接到SIGTERM信號之后,關閉服務器,然后退出進程。需要注意的是,這時進程不會馬上退出,而是要回應完最后一個請求,處理完所有回調函數,然后再退出。
### process.kill()
process.kill方法用來對指定ID的線程發送信號,默認為SIGINT信號。
```javascript
process.on('SIGTERM', function(){
console.log('terminating');
process.exit(1);
});
setTimeout(function(){
console.log('sending SIGTERM to process %d', process.pid);
process.kill(process.pid, 'SIGTERM');
}, 500);
setTimeout(function(){
console.log('never called');
}, 1000);
```
上面代碼中,500毫秒后向當前進程發送SIGTERM信號(終結進程),因此1000毫秒后的指定事件不會被觸發。
## 事件
### exit事件
當前進程退出時,會觸發`exit`事件,可以對該事件指定回調函數。
```javascript
process.on('exit', function () {
fs.writeFileSync('/tmp/myfile', '需要保存到硬盤的信息');
});
```
下面是一個例子,進程退出時,顯示一段日志。
```javascript
process.on("exit", code =>
console.log("exiting with code: " + code))
```
注意,此時回調函數只能執行同步操作,不能包含異步操作,因為執行完回調函數,進程就會退出,無法監聽到回調函數的操作結果。
```javascript
process.on('exit', function(code) {
// 不會執行
setTimeout(function() {
console.log('This will not run');
}, 0);
});
```
上面代碼在`exit`事件的回調函數里面,指定了一個下一輪事件循環,所要執行的操作。這是無效的,不會得到執行。
### beforeExit事件
beforeExit事件在Node清空了Event Loop以后,再沒有任何待處理的任務時觸發。正常情況下,如果沒有任何待處理的任務,Node進程會自動退出,設置beforeExit事件的監聽函數以后,就可以提供一個機會,再部署一些任務,使得Node進程不退出。
beforeExit事件與exit事件的主要區別是,beforeExit的監聽函數可以部署異步任務,而exit不行。
此外,如果是顯式終止程序(比如調用process.exit()),或者因為發生未捕獲的錯誤,而導致進程退出,這些場合不會觸發beforeExit事件。因此,不能使用該事件替代exit事件。
### uncaughtException事件
當前進程拋出一個沒有被捕捉的錯誤時,會觸發`uncaughtException`事件。
```javascript
process.on('uncaughtException', function (err) {
console.error('An uncaught error occurred!');
console.error(err.stack);
throw new Error('未捕獲錯誤');
});
```
部署`uncaughtException`事件的監聽函數,是免于Node進程終止的最后措施,否則Node就要執行`process.exit()`。出于除錯的目的,并不建議發生錯誤后,還保持進程運行。
拋出錯誤之前部署的異步操作,還是會繼續執行。只有完成以后,Node進程才會退出。
```javascript
process.on('uncaughtException', function(err) {
console.log('Caught exception: ' + err);
});
setTimeout(function() {
console.log('本行依然執行');
}, 500);
// 下面的表達式拋出錯誤
nonexistentFunc();
```
上面代碼中,拋出錯誤之后,此前setTimeout指定的回調函數亦然會執行。
### 信號事件
操作系統內核向Node進程發出信號,會觸發信號事件。實際開發中,主要對SIGTERM和SIGINT信號部署監聽函數,這兩個信號在非Windows平臺會導致進程退出,但是只要部署了監聽函數,Node進程收到信號后就不會退出。
```javascript
// 讀取標準輸入,這主要是為了不讓當前進程退出
process.stdin.resume();
process.on('SIGINT', function() {
console.log('SIGINT信號,按Control-D退出');
});
```
上面代碼部署了SIGINT信號的監聽函數,當用戶按下Ctrl-C后,會顯示提示文字。
<h2 id="12.8">Buffer對象</h2>
## 概述
Buffer對象是Node.js用來處理二進制數據的一個接口。JavaScript比較擅長處理Unicode數據,對于處理二進制格式的數據(比如TCP數據流),就不太擅長。Buffer對象就是為了解決這個問題而提供的。該對象也是一個構造函數,它的實例代表了V8引擎分配的一段內存,基本上是一個數組,成員都為整數值。
Buffer是Node原生提供的全局對象,可以直接使用,不需要`require('buffer')`。
Buffer對象與字符串的互相轉換,需要指定編碼格式。目前,Buffer對象支持以下編碼格式。
- ascii
- utf8
- utf16le:UTF-16的小頭編碼,支持大于U+10000的四字節字符。
- ucs2:utf16le的別名。
- base64
- hex:將每個字節轉為兩個十六進制字符。
V8引擎將Buffer對象占用的內存,解釋為一個整數數組,而不是二進制數組。所以,`new Uint32Array(new Buffer([1, 2, 3, 4]))`,生成的`Uint32Array`數組是一個4個成員的`Uint32Array`數組,而不是只有單個成員(`[0x1020304]`或者`[0x4030201]`)。
注意,這時二進制數組所對應的內存是從Buffer對象拷貝的,而不是共享的。二進制數組的`buffer`屬性,保留指向原Buffer對象的指針。
二進制數組的操作,與Buffer對象的操作基本上是兼容的,只有輕微的差異。比如,二進制數組的`slice`方法返回原內存的拷貝,而Buffer對象的`slice`方法創造原內存的一個視圖(view)。
## Buffer構造函數
Buffer作為構造函數,可以用`new`命令生成一個實例,它可以接受多種形式的參數。
```javascript
// 參數是整數,指定分配多少個字節內存
var hello = new Buffer(5);
// 參數是數組,數組成員必須是整數值
var hello = new Buffer([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
hello.toString() // 'Hello'
// 參數是字符串(默認為utf8編碼)
var hello = new Buffer('Hello');
// 參數是字符串(不省略編碼)
var hello = new Buffer('Hello', 'utf8');
// 參數是另一個Buffer實例,等同于拷貝后者
var hello1 = new Buffer('Hello');
var hello2 = new Buffer(hello1);
```
下面是讀取用戶命令行輸入的例子。
```javascript
var fs = require('fs');
var buffer = new Buffer(1024);
var readSize = fs.readSync(fs.openSync('/dev/tty', 'r'), buffer, 0, bufferSize);
var chunk = buffer.toString('utf8', 0, readSize);
console.log('INPUT: ' + chunk);
```
運行上面的程序結果如下。
```bash
# 輸入任意內容,然后按回車鍵
foo
INPUT: foo
```
## 類的方法
### Buffer.isEncoding()
Buffer.isEncoding方法返回一個布爾值,表示Buffer實例是否為指定編碼。
```javascript
Buffer.isEncoding('utf8')
// true
```
### Buffer.isBuffer()
Buffer.isBuffer方法接受一個對象作為參數,返回一個布爾值,表示該對象是否為Buffer實例。
```javascript
Buffer.isBuffer(Date) // false
```
### Buffer.byteLength()
Buffer.byteLength方法返回字符串實際占據的字節長度,默認編碼方式為utf8。
```javascript
Buffer.byteLength('Hello', 'utf8') // 5
```
### Buffer.concat()
Buffer.concat方法將一組Buffer對象合并為一個Buffer對象。
```javascript
var i1 = new Buffer('Hello');
var i2 = new Buffer(' ');
var i3 = new Buffer('World');
Buffer.concat([i1, i2, i3]).toString()
// 'Hello World'
```
需要注意的是,如果Buffer.concat的參數數組只有一個成員,就直接返回該成員。如果有多個成員,就返回一個多個成員合并的新Buffer對象。
Buffer.concat方法還可以接受第二個參數,指定合并后Buffer對象的總長度。
```javascript
var i1 = new Buffer('Hello');
var i2 = new Buffer(' ');
var i3 = new Buffer('World');
Buffer.concat([i1, i2, i3], 10).toString()
// 'Hello Worl'
```
省略第二個參數時,Node內部會計算出這個值,然后再據此進行合并運算。因此,顯式提供這個參數,能提供運行速度。
## 實例屬性
### length
length屬性返回Buffer對象所占據的內存長度。注意,這個值與Buffer對象的內容無關。
```javascript
buf = new Buffer(1234);
buf.length // 1234
buf.write("some string", 0, "ascii");
buf.length // 1234
```
上面代碼中,不管寫入什么內容,length屬性總是返回Buffer對象的空間長度。如果想知道一個字符串所占據的字節長度,可以將其傳入Buffer.byteLength方法。
length屬性是可寫的,但是這會導致未定義的行為,不建議使用。如果想修改Buffer對象的長度,建議使用slice方法返回一個新的Buffer對象。
## 實例方法
### write()
write方法可以向指定的Buffer對象寫入數據。它的第一個參數是所寫入的內容,第二個參數(可省略)是所寫入的起始位置(從0開始),第三個參數(可省略)是編碼方式,默認為utf8。
```javascript
var buf = new Buffer(5);
buf.write('He');
buf.write('l', 2);
buf.write('lo', 3);
console.log(buf.toString());
// "Hello"
```
### slice()
slice方法返回一個按照指定位置、從原對象切割出來的Buffer實例。它的兩個參數分別為切割的起始位置和終止位置。
```javascript
var buf = new Buffer('just some data');
var chunk = buf.slice(5, 9);
chunk.toString()
// "some"
```
### toString()
toString方法將Buffer對象,按照指定編碼(默認為utf8)轉為字符串。
```javascript
var hello = new Buffer('Hello');
hello // <Buffer 48 65 6c 6c 6f>
hello.toString() // "Hello"
```
`toString`方法可以只返回指定位置內存的內容,它的第二個參數表示起始位置,第三個參數表示終止位置,兩者都是從0開始計算。
```javascript
var buf = new Buffer('just some data');
console.log(buf.toString('ascii', 5, 9));
// "some"
```
### toJSON()
toJSON方法將Buffer實例轉為JSON對象。如果JSON.stringify方法調用Buffer實例,默認會先調用toJSON方法。
```javascript
var buf = new Buffer('test');
var json = JSON.stringify(buf);
json // '[116,101,115,116]'
var copy = new Buffer(JSON.parse(json));
copy // <Buffer 74 65 73 74>
```
<h2 id="12.9">Events模塊</h2>
## 概述
### 基本用法
`Events`模塊是Node對“發布/訂閱”模式(publish/subscribe)的實現。一個對象通過這個模塊,向另一個對象傳遞消息。
該模塊通過`EventEmitter`屬性,提供了一個構造函數。該構造函數的實例具有on方法,可以用來監聽指定事件,并觸發回調函數。任意對象都可以發布指定事件,被`EventEmitter`實例的`on`方法監聽到。
```javascript
var EventEmitter = require('events').EventEmitter;
var ee = new EventEmitter();
ee.on('someEvent', function () {
console.log('event has occured');
});
function f() {
console.log('start');
ee.emit('someEvent');
console.log('end');
}
f()
// start
// event has occured
// end
```
上面代碼在加載`events`模塊后,通過`EventEmitter`屬性建立了一個`EventEmitter`對象實例,這個實例就是消息中心。然后,通過`on`方法為`someEvent`事件指定回調函數。最后,通過`emit`方法觸發`someEvent`事件。
上面代碼也表明,`EventEmitter`對象的事件觸發和監聽是同步的,即只有事件的回調函數執行以后,函數`f`才會繼續執行。
### on方法
默認情況下,Node.js允許同一個事件最多可以指定10個回調函數。
```javascript
ee.on("someEvent", function () { console.log("event 1"); });
ee.on("someEvent", function () { console.log("event 2"); });
ee.on("someEvent", function () { console.log("event 3"); });
```
超過10個回調函數,會發出一個警告。這個門檻值可以通過setMaxListeners方法改變。
```javascript
ee.setMaxListeners(20);
```
### emit方法
EventEmitter實例的emit方法,用來觸發事件。它的第一個參數是事件名稱,其余參數都會依次傳入回調函數。
```javascript
var EventEmitter = require('events').EventEmitter;
var myEmitter = new EventEmitter;
var connection = function(id){
console.log('client id: ' + id);
};
myEmitter.on('connection', connection);
myEmitter.emit('connection', 6);
```
## EventEmitter接口的部署
Events模塊的作用,還在于其他模塊可以部署EventEmitter接口,從而也能夠訂閱和發布消息。
```javascript
var EventEmitter = require('events').EventEmitter;
function Dog(name) {
this.name = name;
}
Dog.prototype.__proto__ = EventEmitter.prototype;
// 另一種寫法
// Dog.prototype = Object.create(EventEmitter.prototype);
var simon = new Dog('simon');
simon.on('bark', function(){
console.log(this.name + ' barked');
});
setInterval(function(){
simon.emit('bark');
}, 500);
```
上面代碼新建了一個構造函數Dog,然后讓其繼承EventEmitter,因此Dog就擁有了EventEmitter的接口。最后,為Dog的實例指定bark事件的監聽函數,再使用EventEmitter的emit方法,觸發bark事件。
Node內置模塊util的inherits方法,提供了另一種繼承EventEmitter的寫法。
```javascript
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var Radio = function(station) {
var self = this;
setTimeout(function() {
self.emit('open', station);
}, 0);
setTimeout(function() {
self.emit('close', station);
}, 5000);
this.on('newListener', function(listener) {
console.log('Event Listener: ' + listener);
});
};
util.inherits(Radio, EventEmitter);
module.exports = Radio;
```
上面代碼中,Radio是一個構造函數,它的實例繼承了EventEmitter接口。下面是使用這個模塊的例子。
```javascript
var Radio = require('./radio.js');
var station = {
freq: '80.16',
name: 'Rock N Roll Radio',
};
var radio = new Radio(station);
radio.on('open', function(station) {
console.log('"%s" FM %s 打開', station.name, station.freq);
console.log('? ??');
});
radio.on('close', function(station) {
console.log('"%s" FM %s 關閉', station.name, station.freq);
});
```
## 事件類型
Events模塊默認支持兩個事件。
- newListener事件:添加新的回調函數時觸發。
- removeListener事件:移除回調時觸發。
```javascript
ee.on("newListener", function (evtName){
console.log("New Listener: " + evtName);
});
ee.on("removeListener", function (evtName){
console.log("Removed Listener: " + evtName);
});
function foo (){}
ee.on("save-user", foo);
ee.removeListener("save-user", foo);
// New Listener: removeListener
// New Listener: save-user
// Removed Listener: save-user
```
上面代碼會觸發兩次newListener事件,以及一次removeListener事件。
## EventEmitter實例的方法
### once方法
該方法類似于on方法,但是回調函數只觸發一次。
```javascript
var EventEmitter = require('events').EventEmitter;
var myEmitter = new EventEmitter;
myEmitter.once('message', function(msg){
console.log('message: ' + msg);
});
myEmitter.emit('message', 'this is the first message');
myEmitter.emit('message', 'this is the second message');
myEmitter.emit('message', 'welcome to nodejs');
```
上面代碼觸發了三次message事件,但是回調函數只會在第一次調用時運行。
下面代碼指定,一旦服務器連通,只調用一次的回調函數。
```javascript
server.once('connection', function (stream) {
console.log('Ah, we have our first user!');
});
```
該方法返回一個EventEmitter對象,因此可以鏈式加載監聽函數。
### removeListener方法
該方法用于移除回調函數。它接受兩個參數,第一個是事件名稱,第二個是回調函數名稱。這就是說,不能用于移除匿名函數。
```javascript
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter;
emitter.on('message', console.log);
setInterval(function(){
emitter.emit('message', 'foo bar');
}, 300);
setTimeout(function(){
emitter.removeListener('message', console.log);
}, 1000);
```
上面代碼每300毫秒觸發一次message事件,直到1000毫秒后取消監聽。
另一個例子是使用removeListener方法模擬once方法。
```javascript
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter;
function onlyOnce () {
console.log("You'll never see this again");
emitter.removeListener("firstConnection", onlyOnce);
}
emitter.on("firstConnection", onlyOnce);
```
**(3)removeAllListeners方法**
該方法用于移除某個事件的所有回調函數。
```javascript
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter;
// some code here
emitter.removeAllListeners("firstConnection");
```
如果不帶參數,則表示移除所有事件的所有回調函數。
```javascript
emitter.removeAllListeners();
```
**(4)listener方法**
該方法接受一個事件名稱作為參數,返回該事件所有回調函數組成的數組。
```javascript
var EventEmitter = require('events').EventEmitter;
var ee = new EventEmitter;
function onlyOnce () {
console.log(ee.listeners("firstConnection"));
ee.removeListener("firstConnection", onlyOnce);
console.log(ee.listeners("firstConnection"));
}
ee.on("firstConnection", onlyOnce)
ee.emit("firstConnection");
ee.emit("firstConnection");
// [ [Function: onlyOnce] ]
// []
```
上面代碼顯示兩次回調函數組成的數組,第一次只有一個回調函數onlyOnce,第二次是一個空數組,因為removeListener方法取消了回調函數。
<h2 id="12.10">stream接口</h2>
## Stream是什么?
”流“(stream)這個概念,最簡單的理解,就是在數據還沒有接收完成時,就開始處理它。
```javascript
var fs = require('fs');
fs.createReadStream('./data/customers.csv').pipe(process.stdout);
```
上面代碼中,`fs.createReadStream`方法以”流“的方式讀取文件,這可以在文件還沒有讀取完的情況下,就輸出到標準輸出。這顯然對大文件的讀取非常有利。
Unix操作系統從很早以前,就有Stream(流)這個概念,它是不同進程之間傳遞數據的一種方式。管道命令Pipe就起到在不同命令之間,連接Stream的作用。
Stream把較大的數據,拆成很小的部分。只要命令部署了Stream接口,就可以把一個流的輸出接到另一個流的輸入。Node引入了這個概念,通過Stream為異步讀寫數據提供的統一接口。無論是硬盤數據、網絡數據,還是內存數據,都可以采用這個接口讀寫。
讀寫數據有兩種方式。一種方式是同步處理,即先將數據全部讀入內存,然后處理。它的優點是符合直覺,流程非常自然,缺點是如果遇到大文件,要花很長時間,可能要過很久才能進入數據處理的步驟。另一種方式就是Stream方式,它是系統讀取外部數據實際上的方式,即每次只讀入數據的一小塊,像“流水”一樣。所以,Stream方式就是每當系統讀入了一小塊數據,就會觸發一個事件,發出“新數據塊”的信號,只要監聽這個事件,就能掌握進展,做出相應處理,這樣就提高了程序的性能。
Stream接口最大特點就是通過事件通信,具有readable、writable、drain、data、end、close等事件,既可以讀取數據,也可以寫入數據。讀寫數據時,每讀入(或寫入)一段數據,就會觸發一次data事件,全部讀取(或寫入)完畢,觸發end事件。如果發生錯誤,則觸發error事件。
一個對象只要部署了Stream接口,就可以從讀取數據,或者寫入數據。Node內部很多涉及IO處理的對象,都部署了Stream接口,比如HTTP連接、文件讀寫、標準輸入輸出等。
## 基本用法
Node的I/O操作都是異步的,所以與磁盤和網絡的交互,都要通過回調函數。一個典型的寫文件操作,可能像下面這樣。
```javascript
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
fs.readFile(__dirname + '/data.txt', function (err, data) {
res.end(data);
});
});
server.listen(8000);
```
上面的代碼有一個問題,那就是它必須將整個data.txt文件讀入內存,然后再輸入。如果data.txt非常大,就會占用大量的內容。一旦有多個并發請求,操作就會變得非常緩慢,用戶不得不等很久,才能得到結果。
由于參數req和res都部署了Stream接口,可以使用`fs.createReadStream()`替代`fs.readFile()`,就能解決這個問題。
```javascript
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
var stream = fs.createReadStream(__dirname + '/data.txt');
stream.pipe(res);
});
server.listen(8000);
```
Stream接口的最大特點,就是數據會發出node和data事件,內置的pipe方法會處理這兩個事件。
數據流通過pipe方法,可以方便地導向其他具有Stream接口的對象。
```javascript
var fs = require('fs');
var zlib = require('zlib');
fs.createReadStream('wow.txt')
.pipe(zlib.createGzip())
.pipe(process.stdout);
```
上面代碼先打開文本文件wow.txt,然后壓縮,再導向標準輸出。
```javascript
fs.createReadStream('wow.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('wow.gz'));
```
上面代碼壓縮文件wow.txt以后,又將其寫回壓縮文件。
下面代碼新建一個Stream實例,然后指定寫入事件和終止事件的回調函數,再將其接到標準輸入之上。
```javascript
var stream = require('stream');
var Stream = stream.Stream;
var ws = new Stream;
ws.writable = true;
ws.write = function(data) {
console.log("input=" + data);
}
ws.end = function(data) {
console.log("bye");
}
process.stdin.pipe(ws);
```
調用上面的腳本,會產生以下結果。
```bash
$ node pipe_out.js
hello
input=hello
^d
bye
```
上面代碼調用腳本下,鍵入hello,會輸出`input=hello`。然后按下ctrl-d,會輸出bye。使用管道命令,可以看得更清楚。
```bash
$ echo hello | node pipe_out.js
input=hello
bye
```
Stream接口分成三類。
- 可讀數據流接口,用于讀取數據。
- 可寫數據流接口,用于寫入數據。
- 雙向數據流接口,用于讀取和寫入數據,比如Node的tcp sockets、zlib、crypto都部署了這個接口。
## 可讀數據流
“可讀數據流”用來產生數據。它表示數據的來源,只要一個對象提供“可讀數據流”,就表示你可以從其中讀取數據。
```javascript
var Readable = require('stream').Readable;
var rs = new Readable;
rs.push('beep ');
rs.push('boop\n');
rs.push(null);
rs.pipe(process.stdout);
```
上面代碼產生了一個可寫數據流,最后將其寫入標注輸出。可讀數據流的push方法,用來將數據輸入緩存。
`rs.push(null)`中的null,用來告訴rs,數據輸入完畢。
“可讀數據流”有兩種狀態:流動態和暫停態。處于流動態時,數據會盡快地從數據源導向用戶的程序;處于暫停態時,必須顯式調用`stream.read()`等指令,“可讀數據流”才會釋放數據。剛剛新建的時候,“可讀數據流”處于暫停態。
三種方法可以讓暫停態轉為流動態。
- 添加data事件的監聽函數
- 調用resume方法
- 調用pipe方法將數據送往一個可寫數據流
如果轉為流動態時,沒有data事件的監聽函數,也沒有pipe方法的目的地,那么數據將遺失。
以下兩種方法可以讓流動態轉為暫停態。
- 不存在pipe方法的目的地時,調用pause方法
- 存在pipe方法的目的地時,移除所有data事件的監聽函數,并且調用unpipe方法,移除所有pipe方法的目的地
注意,只移除data事件的監聽函數,并不會自動引發數據流進入“暫停態”。另外,存在pipe方法的目的地時,調用pause方法,并不能保證數據流總是處于暫停態,一旦那些目的地發出數據請求,數據流有可能會繼續提供數據。
每當系統有新的數據,該接口可以監聽到data事件,從而回調函數。
```javascript
var fs = require('fs');
var readableStream = fs.createReadStream('file.txt');
var data = '';
readableStream.setEncoding('utf8');
readableStream.on('data', function(chunk) {
data+=chunk;
});
readableStream.on('end', function() {
console.log(data);
});
```
上面代碼中,fs模塊的createReadStream方法,是部署了Stream接口的文件讀取方法。該方法對指定的文件,返回一個對象。該對象只要監聽data事件,回調函數就能讀到數據。
除了data事件,監聽readable事件,也可以讀到數據。
```javascript
var fs = require('fs');
var readableStream = fs.createReadStream('file.txt');
var data = '';
var chunk;
readableStream.setEncoding('utf8');
readableStream.on('readable', function() {
while ((chunk=readableStream.read()) !== null) {
data += chunk;
}
});
readableStream.on('end', function() {
console.log(data)
});
```
readable事件表示系統緩沖之中有可讀的數據,使用read方法去讀出數據。如果沒有數據可讀,read方法會返回null。
“可讀數據流”除了read方法,還有以下方法。
- Readable.pause() :暫停數據流。已經存在的數據,也不再觸發data事件,數據將保留在緩存之中,此時的數據流稱為靜態數據流。如果對靜態數據流再次調用pause方法,數據流將重新開始流動,但是緩存中現有的數據,不會再觸發data事件。
- Readable.resume():恢復暫停的數據流。
- readable.unpipe():從管道中移除目的地數據流。如果該方法使用時帶有參數,會阻止“可讀數據流”進入某個特定的目的地數據流。如果使用時不帶有參數,則會移除所有的目的地數據流。
### read()
read方法從系統緩存讀取并返回數據。如果讀不到數據,則返回null。
該方法可以接受一個整數作為參數,表示所要讀取數據的數量,然后會返回該數量的數據。如果讀不到足夠數量的數據,返回null。如果不提供這個參數,默認返回系統緩存之中的所有數據。
只在“暫停態”時,該方法才有必要手動調用。“流動態”時,該方法是自動調用的,直到系統緩存之中的數據被讀光。
```javascript
var readable = getReadableStreamSomehow();
readable.on('readable', function() {
var chunk;
while (null !== (chunk = readable.read())) {
console.log('got %d bytes of data', chunk.length);
}
});
```
如果該方法返回一個數據塊,那么它就觸發了data事件。
### _read()
可讀數據流的_read方法,可以將數據放入可讀數據流。
```javascript
var Readable = require('stream').Readable;
var rs = Readable();
var c = 97;
rs._read = function () {
rs.push(String.fromCharCode(c++));
if (c > 'z'.charCodeAt(0)) rs.push(null);
};
rs.pipe(process.stdout);
```
運行結果如下。
```bash
$ node read1.js
abcdefghijklmnopqrstuvwxyz
```
### setEncoding()
調用該方法,會使得數據流返回指定編碼的字符串,而不是緩存之中的二進制對象。比如,調用`setEncoding('utf8')`,數據流會返回UTF-8字符串,調用`setEncoding('hex')`,數據流會返回16進制的字符串。
該方法會正確處理多字節的字符,而緩存的方法`buf.toString(encoding)`不會。所以如果想要從數據流讀取字符串,應該總是使用該方法。
```javascript
var readable = getReadableStreamSomehow();
readable.setEncoding('utf8');
readable.on('data', function(chunk) {
assert.equal(typeof chunk, 'string');
console.log('got %d characters of string data', chunk.length);
});
```
### resume()
resume方法會使得“可讀數據流”繼續釋放data事件,即轉為流動態。
```javascript
var readable = getReadableStreamSomehow();
readable.resume();
readable.on('end', function(chunk) {
console.log('數據流到達尾部,未讀取任務數據');
});
```
上面代碼中,調用resume方法使得數據流進入流動態,只定義end事件的監聽函數,不定義data事件的監聽函數,表示不從數據流讀取任何數據,只監聽數據流到達尾部。
### pause()
pause方法使得流動態的數據流,停止釋放data事件,轉而進入暫停態。任何此時已經可以讀到的數據,都將停留在系統緩存。
```javascript
var readable = getReadableStreamSomehow();
readable.on('data', function(chunk) {
console.log('讀取%d字節的數據', chunk.length);
readable.pause();
console.log('接下來的1秒內不讀取數據');
setTimeout(function() {
console.log('數據恢復讀取');
readable.resume();
}, 1000);
});
```
### isPaused()
該方法返回一個布爾值,表示“可讀數據流”被客戶端手動暫停(即調用了pause方法),目前還沒有調用resume方法。
```javascript
var readable = new stream.Readable
readable.isPaused() // === false
readable.pause()
readable.isPaused() // === true
readable.resume()
readable.isPaused() // === false
```
### pipe()
pipe方法是自動傳送數據的機制,就像管道一樣。它從“可讀數據流”讀出所有數據,將其寫出指定的目的地。整個過程是自動的。
```javascript
src.pipe(dst)
```
pipe方法必須在可讀數據流上調用,它的參數必須是可寫數據流。
```javascript
var fs = require('fs');
var readableStream = fs.createReadStream('file1.txt');
var writableStream = fs.createWriteStream('file2.txt');
readableStream.pipe(writableStream);
```
上面代碼使用pipe方法,將file1的內容寫入file2。整個過程由pipe方法管理,不用手動干預,所以可以將傳送數據寫得很簡潔。
pipe方法返回目的地的數據流,因此可以使用鏈式寫法,將多個數據流操作連在一起。
```javascript
a.pipe(b).pipe(c).pipe(d)
// 等同于
a.pipe(b);
b.pipe(c);
c.pipe(d);
```
下面是一個例子。
```javascript
var fs = require('fs');
var zlib = require('zlib');
fs.createReadStream('input.txt.gz')
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('output.txt'));
```
上面代碼采用鏈式寫法,先讀取文件,然后進行壓縮,最后輸出。
下面的寫法模擬了Unix系統的cat命令,將標準輸出寫入標準輸入。
```javascript
process.stdin.pipe(process.stdout);
```
當來源地的數據流讀取完成,默認會調用目的地的end方法,就不再能夠寫入。對pipe方法傳入第二個參數`{ end: false }`,可以讓目的地的數據流保持打開。
```javascript
reader.pipe(writer, { end: false });
reader.on('end', function() {
writer.end('Goodbye\n');
});
```
上面代碼中,目的地數據流默認不會調用end方法,只能手動調用,因此“Goodbye”會被寫入。
### unpipe()
該方法移除pipe方法指定的數據流目的地。如果沒有參數,則移除所有的pipe方法目的地。如果有參數,則移除該參數指定的目的地。如果沒有匹配參數的目的地,則不會產生任何效果。
```javascript
var readable = getReadableStreamSomehow();
var writable = fs.createWriteStream('file.txt');
readable.pipe(writable);
setTimeout(function() {
console.log('停止寫入file.txt');
readable.unpipe(writable);
console.log('手動關閉file.txt的寫入數據流');
writable.end();
}, 1000);
```
上面代碼寫入file.txt的時間,只有1秒鐘,然后就停止寫入。
### 事件
(1)readable
readable事件在數據流能夠向外提供數據時觸發。
```javascript
var readable = getReadableStreamSomehow();
readable.on('readable', function() {
// there is some data to read now
});
```
下面是一個例子。
```javascript
process.stdin.on('readable', function () {
var buf = process.stdin.read();
console.dir(buf);
});
```
上面代碼將標準輸入的數據讀出。
read方法接受一個整數作為參數,表示以多少個字節為單位進行讀取。
```javascript
process.stdin.on('readable', function () {
var buf = process.stdin.read(3);
console.dir(buf);
});
```
上面代碼將以3個字節為單位進行輸出內容。
(2)data
對于那些沒有顯式暫停的數據流,添加data事件監聽函數,會將數據流切換到流動態,盡快向外提供數據。
```javascript
var readable = getReadableStreamSomehow();
readable.on('data', function(chunk) {
console.log('got %d bytes of data', chunk.length);
});
```
(3)end
無法再讀取到數據時,會觸發end事件。也就是說,只有當前數據被完全讀取完,才會觸發end事件,比如不停地調用read方法。
```javascript
var readable = getReadableStreamSomehow();
readable.on('data', function(chunk) {
console.log('got %d bytes of data', chunk.length);
});
readable.on('end', function() {
console.log('there will be no more data.');
});
```
(4)close
數據源關閉時,close事件被觸發。并不是所有的數據流都支持這個事件。
(5)error
當讀取數據發生錯誤時,error事件被觸發。
## 可寫數據流
“可寫數據流”允許你將數據寫入某個目的地。它是數據寫入的一種抽象,不同的數據目的地部署了這個接口以后,就可以用統一的方法寫入。
以下是部署了可寫數據流的一些場合。
- 客戶端的http requests
- 服務器的http responses
- fs write streams
- zlib streams
- crypto streams
- tcp sockets
- child process stdin
- process.stdout, process.stderr
下面是fs模塊的可寫數據流的例子。
```javascript
var fs = require('fs');
var readableStream = fs.createReadStream('file1.txt');
var writableStream = fs.createWriteStream('file2.txt');
readableStream.setEncoding('utf8');
readableStream.on('data', function(chunk) {
writableStream.write(chunk);
});
```
上面代碼中,fs模塊的createWriteStream方法針對特定文件,創建了一個“可寫數據流”,本質上就是對寫入操作部署了Stream接口。然后,“可寫數據流”的write方法,可以將數據寫入文件。
### write()
write方法用于向“可寫數據流”寫入數據。它接受兩個參數,一個是寫入的內容,可以是字符串,也可以是一個stream對象(比如可讀數據流),另一個是寫入完成后的回調函數。
它返回一個布爾值,表示本次數據是否處理完成。如果返回true,就表示可以寫入新的數據了。如果等待寫入的數據被緩存了,就返回false。不過,在返回false的情況下,也可以繼續傳入新的數據等待寫入。只是這時,新的數據不會真的寫入,只會緩存在內存中。為了避免內存消耗,比較好的做法還是等待該方法返回true,然后再寫入。
```javascript
var fs = require('fs');
var ws = fs.createWriteStream('message.txt');
ws.write('beep ');
setTimeout(function () {
ws.end('boop\n');
}, 1000);
```
上面代碼調用end方法,數據就不再寫入了。
### cork(),uncork()
cork方法可以強制等待寫入的數據進入緩存。當調用uncork方法或end方法時,緩存的數據就會吐出。
### setDefaultEncoding()
setDefaultEncoding方法用于將寫入的數據編碼成新的格式。它返回一個布爾值,表示編碼是否成功,如果返回false就表示編碼失敗。
### end()
end方法用于終止“可寫數據流”。該方法可以接受三個參數,全部都是可選參數。第一個參數是最后所要寫入的數據,可以是字符串,也可以是stream對象;第二個參數是寫入編碼;第三個參數是一個回調函數,finish事件觸發時,會調用這個回調函數。
```javascript
var file = fs.createWriteStream('example.txt');
file.write('hello, ');
file.end('world!');
```
上面代碼會在數據寫入結束時,在尾部寫入“world!”。
調用end方法之后,再寫入數據會報錯。
```javascript
var file = fs.createWriteStream('example.txt');
file.end('world!');
file.write('hello, '); // 報錯
```
### 事件
(1)drain事件
`writable.write(chunk)`返回false以后,當緩存數據全部寫入完成,可以繼續寫入時,會觸發drain事件。
```javascript
function writeOneMillionTimes(writer, data, encoding, callback) {
var i = 1000000;
write();
function write() {
var ok = true;
do {
i -= 1;
if (i === 0) {
writer.write(data, encoding, callback);
} else {
ok = writer.write(data, encoding);
}
} while (i > 0 && ok);
if (i > 0) {
writer.once('drain', write);
}
}
}
```
上面代碼是一個寫入100萬次的例子,通過drain事件得到可以繼續寫入的通知。
(2)finish事件
調用end方法時,所有緩存的數據釋放,觸發finish事件。該事件的回調函數沒有參數。
```javascript
var writer = getWritableStreamSomehow();
for (var i = 0; i < 100; i ++) {
writer.write('hello, #' + i + '!\n');
}
writer.end('this is the end\n');
writer.on('finish', function() {
console.error('all writes are now complete.');
});
```
(3)pipe事件
“可寫數據流”調用pipe方法,將數據流導向寫入目的地時,觸發該事件。
該事件的回調函數,接受發出該事件的“可讀數據流”對象作為參數。
```javascript
var writer = getWritableStreamSomehow();
var reader = getReadableStreamSomehow();
writer.on('pipe', function(src) {
console.error('something is piping into the writer');
assert.equal(src, reader);
});
reader.pipe(writer);
```
(4)unpipe事件
“可讀數據流”調用unpipe方法,將可寫數據流移出寫入目的地時,觸發該事件。
該事件的回調函數,接受發出該事件的“可讀數據流”對象作為參數。
```javascript
var writer = getWritableStreamSomehow();
var reader = getReadableStreamSomehow();
writer.on('unpipe', function(src) {
console.error('something has stopped piping into the writer');
assert.equal(src, reader);
});
reader.pipe(writer);
reader.unpipe(writer);
```
(5)error事件
如果寫入數據或pipe數據時發生錯誤,就會觸發該事件。
該事件的回調函數,接受一個Error對象作為參數。
## HTTP請求
HTTP對象使用Stream接口,實現網絡數據的讀寫。
```javascript
var http = require('http');
var server = http.createServer(function (req, res) {
// req is an http.IncomingMessage, which is a Readable Stream
// res is an http.ServerResponse, which is a Writable Stream
var body = '';
// we want to get the data as utf8 strings
// If you don't set an encoding, then you'll get Buffer objects
req.setEncoding('utf8');
// Readable streams emit 'data' events once a listener is added
req.on('data', function (chunk) {
body += chunk;
});
// the end event tells you that you have entire body
req.on('end', function () {
try {
var data = JSON.parse(body);
} catch (er) {
// uh oh! bad json!
res.statusCode = 400;
return res.end('error: ' + er.message);
}
// write back something interesting to the user:
res.write(typeof data);
res.end();
});
});
server.listen(1337);
// $ curl localhost:1337 -d '{}'
// object
// $ curl localhost:1337 -d '"foo"'
// string
// $ curl localhost:1337 -d 'not json'
// error: Unexpected token o
```
data事件表示讀取或寫入了一塊數據。
```javascript
req.on('data', function(buf){
// Do something with the Buffer
});
```
使用req.setEncoding方法,可以設定字符串編碼。
```javascript
req.setEncoding('utf8');
req.on('data', function(str){
// Do something with the String
});
```
end事件,表示讀取或寫入數據完畢。
```javascript
var http = require('http');
http.createServer(function(req, res){
res.writeHead(200);
req.on('data', function(data){
res.write(data);
});
req.on('end', function(){
res.end();
});
}).listen(3000);
```
上面代碼相當于建立了“回聲”服務,將HTTP請求的數據體,用HTTP回應原樣發送回去。
system模塊提供了pump方法,有點像Linux系統的管道功能,可以將一個數據流,原封不動得轉給另一個數據流。所以,上面的例子也可以用pump方法實現。
```javascript
var http = require('http'),
sys = require('sys');
http.createServer(function(req, res){
res.writeHead(200);
sys.pump(req, res);
}).listen(3000);
```
## fs模塊
fs模塊的createReadStream方法用于新建讀取數據流,createWriteStream方法用于新建寫入數據流。使用這兩個方法,可以做出一個用于文件復制的腳本copy.js。
```javascript
// copy.js
var fs = require('fs');
console.log(process.argv[2], '->', process.argv[3]);
var readStream = fs.createReadStream(process.argv[2]);
var writeStream = fs.createWriteStream(process.argv[3]);
readStream.on('data', function (chunk) {
writeStream.write(chunk);
});
readStream.on('end', function () {
writeStream.end();
});
readStream.on('error', function (err) {
console.log("ERROR", err);
});
writeStream.on('error', function (err) {
console.log("ERROR", err);
});d all your errors, you wouldn't need to use domains.
```
上面代碼非常容易理解,使用的時候直接提供源文件路徑和目標文件路徑,就可以了。
```bash
node cp.js src.txt dest.txt
```
Streams對象都具有pipe方法,起到管道作用,將一個數據流輸入另一個數據流。所以,上面代碼可以重寫成下面這樣:
```javascript
var fs = require('fs');
console.log(process.argv[2], '->', process.argv[3]);
var readStream = fs.createReadStream(process.argv[2]);
var writeStream = fs.createWriteStream(process.argv[3]);
readStream.on('open', function () {
readStream.pipe(writeStream);
});
readStream.on('end', function () {
writeStream.end();
});
```
## 錯誤處理
下面是壓縮后發送文件的代碼。
```javascript
http.createServer(function (req, res) {
// set the content headers
fs.createReadStream('filename.txt')
.pipe(zlib.createGzip())
.pipe(res)
})
```
上面的代碼沒有部署錯誤處理機制,一旦發生錯誤,就無法處理。所以,需要加上error事件的監聽函數。
```javascript
http.createServer(function (req, res) {
// set the content headers
fs.createReadStream('filename.txt')
.on('error', onerror)
.pipe(zlib.createGzip())
.on('error', onerror)
.pipe(res)
function onerror(err) {
console.error(err.stack)
}
})
```
上面的代碼還是存在問題,如果客戶端中斷下載,寫入的數據流就會收不到close事件,一直處于等待狀態,從而造成內存泄漏。因此,需要使用[on-finished模塊](https://github.com/jshttp/on-finished)用來處理這種情況。
```javascript
http.createServer(function (req, res) {
var stream = fs.createReadStream('filename.txt')
// set the content headers
stream
.on('error', onerror)
.pipe(zlib.createGzip())
.on('error', onerror)
.pipe(res)
onFinished(res, function () {
// make sure the stream is always destroyed
stream.destroy()
})
})
```
<h2 id="12.11">Child Process模塊</h2>
child_process模塊用于新建子進程。子進程的運行結果儲存在系統緩存之中(最大200KB),等到子進程運行結束以后,主進程再用回調函數讀取子進程的運行結果。
## exec()
`exec`方法用于執行bash命令,它的參數是一個命令字符串。
```javascript
var exec = require('child_process').exec;
var ls = exec('ls -l', function (error, stdout, stderr) {
if (error) {
console.log(error.stack);
console.log('Error code: ' + error.code);
}
console.log('Child Process STDOUT: ' + stdout);
});
```
上面代碼的`exec`方法用于新建一個子進程,然后緩存它的運行結果,運行結束后調用回調函數。
`exec`方法最多可以接受兩個參數,第一個參數是所要執行的shell命令,第二個參數是回調函數,該函數接受三個參數,分別是發生的錯誤、標準輸出的顯示結果、標準錯誤的顯示結果。
由于標準輸出和標準錯誤都是流對象(stream),可以監聽data事件,因此上面的代碼也可以寫成下面這樣。
```javascript
var exec = require('child_process').exec;
var child = exec('ls -l');
child.stdout.on('data', function(data) {
console.log('stdout: ' + data);
});
child.stderr.on('data', function(data) {
console.log('stdout: ' + data);
});
child.on('close', function(code) {
console.log('closing code: ' + code);
});
```
上面的代碼還表明,子進程本身有`close`事件,可以設置回調函數。
上面的代碼還有一個好處。監聽data事件以后,可以實時輸出結果,否則只有等到子進程結束,才會輸出結果。所以,如果子進程運行時間較長,或者是持續運行,第二種寫法更好。
下面是另一個例子,假定有一個child.js文件。
```javascript
// child.js
var exec = require('child_process').exec;
exec('node -v', function(error, stdout, stderr) {
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
if (error !== null) {
console.log('exec error: ' + error);
}
});
```
運行后,該文件的輸出結果如下。
```bash
$ node child.js
stdout: v0.11.14
stderr:
```
exec方法會直接調用bash(`/bin/sh`程序)來解釋命令,所以如果有用戶輸入的參數,exec方法是不安全的。
```javascript
var path = ";user input";
child_process.exec('ls -l ' + path, function (err, data) {
console.log(data);
});
```
上面代碼表示,在bash環境下,`ls -l; user input`會直接運行。如果用戶輸入惡意代碼,將會帶來安全風險。因此,在有用戶輸入的情況下,最好不使用`exec`方法,而是使用`execFile`方法。
## execSync()
`execSync`是`exec`的同步執行版本。
它可以接受兩個參數,第一個參數是所要執行的命令,第二個參數用來配置執行環境。
```javascript
var execSync = require("child_process").execSync;
var SEPARATOR = process.platform === 'win32' ? ';' : ':';
var env = Object.assign({}, process.env);
env.PATH = path.resolve('./node_modules/.bin') + SEPARATOR + env.PATH;
function myExecSync(cmd) {
var output = execSync(cmd, {
cwd: process.cwd(),
env: env
});
console.log(output);
}
myExecSync('eslint .');
```
上面代碼中,`execSync`方法的第二個參數是一個對象。該對象的`cwd`屬性指定腳本的當前目錄,`env`屬性指定環境變量。上面代碼將`./node_modules/.bin`目錄,存入`$PATH`變量。這樣就可以不加路徑,引用項目內部的模塊命令了,比如`eslint`命令實際執行的是`./node_modules/.bin/eslint`。
## execFile()
execFile方法直接執行特定的程序,參數作為數組傳入,不會被bash解釋,因此具有較高的安全性。
```javascript
var child_process = require('child_process');
var path = ".";
child_process.execFile('/bin/ls', ['-l', path], function (err, result) {
console.log(result)
});
```
上面代碼中,假定`path`來自用戶輸入,如果其中包含了分號或反引號,ls程序不理解它們的含義,因此也就得不到運行結果,安全性就得到了提高。
## spawn()
spawn方法創建一個子進程來執行特定命令,用法與execFile方法類似,但是沒有回調函數,只能通過監聽事件,來獲取運行結果。它屬于異步執行,適用于子進程長時間運行的情況。
```javascript
var child_process = require('child_process');
var path = '.';
var ls = child_process.spawn('/bin/ls', ['-l', path]);
ls.stdout.on('data', function (data) {
console.log('stdout: ' + data);
});
ls.stderr.on('data', function (data) {
console.log('stderr: ' + data);
});
ls.on('close', function (code) {
console.log('child process exited with code ' + code);
});
```
spawn方法接受兩個參數,第一個是可執行文件,第二個是參數數組。
spawn對象返回一個對象,代表子進程。該對象部署了EventEmitter接口,它的`data`事件可以監聽,從而得到子進程的輸出結果。
spawn方法與exec方法非常類似,只是使用格式略有區別。
```javascript
child_process.exec(command, [options], callback)
child_process.spawn(command, [args], [options])
```
## fork()
fork方法直接創建一個子進程,執行Node腳本,`fork('./child.js')` 相當于 `spawn('node', ['./child.js'])` 。與spawn方法不同的是,fork會在父進程與子進程之間,建立一個通信管道,用于進程之間的通信。
```javascript
var n = child_process.fork('./child.js');
n.on('message', function(m) {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });
```
上面代碼中,fork方法返回一個代表進程間通信管道的對象,對該對象可以監聽message事件,用來獲取子進程返回的信息,也可以向子進程發送信息。
child.js腳本的內容如下。
```javascript
process.on('message', function(m) {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
```
上面代碼中,子進程監聽message事件,并向父進程發送信息。
## send()
使用 child_process.fork() 生成新進程之后,就可以用 child.send(message, [sendHandle]) 向新進程發送消息。新進程中通過監聽message事件,來獲取消息。
下面的例子是主進程的代碼。
```javascript
var cp = require('child_process');
var n = cp.fork(__dirname + '/sub.js');
n.on('message', function(m) {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });
```
下面是子進程sub.js代碼。
```javascript
process.on('message', function(m) {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
```
<h2 id="12.12">Http模塊</h2>
## 基本用法
### 處理GET請求
`http`模塊主要用于搭建HTTP服務。使用Node搭建HTTP服務器非常簡單。
```javascript
var http = require('http');
http.createServer(function (request, response){
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello World\n');
}).listen(8080, '127.0.0.1');
console.log('Server running on port 8080.');
```
上面代碼第一行`var http = require("http")`,表示加載`http`模塊。然后,調用`http`模塊的`createServer`方法,創造一個服務器實例。
`ceateServer`方法接受一個函數作為參數,該函數的`request`參數是一個對象,表示客戶端的HTTP請求;`response`參數也是一個對象,表示服務器端的HTTP回應。`response.writeHead`方法用來寫入HTTP回應的頭信息;`response.end`方法用來寫入HTTP回應的具體內容,以及回應完成后關閉本次對話。最后的`listen(8080)`表示啟動服務器實例,監聽本機的8080端口。
將上面這幾行代碼保存成文件`app.js`,然后執行該腳本,服務器就開始運行了。
```bash
$ node app.js
```
這時命令行窗口將顯示一行提示“Server running at port 8080.”。打開瀏覽器,訪問http://localhost:8080,網頁顯示“Hello world!”。
上面的例子是收到請求后生成網頁,也可以事前寫好網頁,存在文件中,然后利用`fs`模塊讀取網頁文件,將其返回。
```javascript
var http = require('http');
var fs = require('fs');
http.createServer(function (request, response){
fs.readFile('data.txt', function readData(err, data) {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end(data);
});
// 或者
fs.createReadStream(`${__dirname}/index.html`).pipe(response);
}).listen(8080, '127.0.0.1');
console.log('Server running on port 8080.');
```
下面的修改則是根據不同網址的請求,顯示不同的內容,已經相當于做出一個網站的雛形了。
```javascript
var http = require("http");
http.createServer(function(req, res) {
// 主頁
if (req.url == "/") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("Welcome to the homepage!");
}
// About頁面
else if (req.url == "/about") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("Welcome to the about page!");
}
// 404錯誤
else {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 error! File not found.");
}
}).listen(8080, "localhost");
```
回調函數的req(request)對象,擁有以下屬性。
- url:發出請求的網址。
- method:HTTP請求的方法。
- headers:HTTP請求的所有HTTP頭信息。
### 處理POST請求
當客戶端采用POST方法發送數據時,服務器端可以對data和end兩個事件,設立監聽函數。
```javascript
var http = require('http');
http.createServer(function (req, res) {
var content = "";
req.on('data', function (chunk) {
content += chunk;
});
req.on('end', function () {
res.writeHead(200, {"Content-Type": "text/plain"});
res.write("You've sent: " + content);
res.end();
});
}).listen(8080);
```
data事件會在數據接收過程中,每收到一段數據就觸發一次,接收到的數據被傳入回調函數。end事件則是在所有數據接收完成后觸發。
對上面代碼稍加修改,就可以做出文件上傳的功能。
```javascript
"use strict";
var http = require('http');
var fs = require('fs');
var destinationFile, fileSize, uploadedBytes;
http.createServer(function (request, response) {
response.writeHead(200);
destinationFile = fs.createWriteStream("destination.md");
request.pipe(destinationFile);
fileSize = request.headers['content-length'];
uploadedBytes = 0;
request.on('data', function (d) {
uploadedBytes += d.length;
var p = (uploadedBytes / fileSize) * 100;
response.write("Uploading " + parseInt(p, 0) + " %\n");
});
request.on('end', function () {
response.end("File Upload Complete");
});
}).listen(3030, function () {
console.log("server started");
});
```
## 發出請求
### get()
get方法用于發出get請求。
```javascript
function getTestPersonaLoginCredentials(callback) {
return http.get({
host: 'personatestuser.org',
path: '/email'
}, function(response) {
var body = '';
response.on('data', function(d) {
body += d;
});
response.on('end', function() {
var parsed = JSON.parse(body);
callback({
email: parsed.email,
password: parsed.pass
});
});
});
},
```
### request()
request方法用于發出HTTP請求,它的使用格式如下。
```javascript
http.request(options[, callback])
```
request方法的options參數,可以是一個對象,也可以是一個字符串。如果是字符串,就表示這是一個URL,Node內部就會自動調用`url.parse()`,處理這個參數。
options對象可以設置如下屬性。
- host:HTTP請求所發往的域名或者IP地址,默認是localhost。
- hostname:該屬性會被`url.parse()`解析,優先級高于host。
- port:遠程服務器的端口,默認是80。
- localAddress:本地網絡接口。
- socketPath:Unix網絡套接字,格式為host:port或者socketPath。
- method:指定HTTP請求的方法,格式為字符串,默認為GET。
- path:指定HTTP請求的路徑,默認為根路徑(/)。可以在這個屬性里面,指定查詢字符串,比如`/index.html?page=12`。如果這個屬性里面包含非法字符(比如空格),就會拋出一個錯誤。
- headers:一個對象,包含了HTTP請求的頭信息。
- auth:一個代表HTTP基本認證的字符串`user:password`。
- agent:控制緩存行為,如果HTTP請求使用了agent,則HTTP請求默認為`Connection: keep-alive`,它的可能值如下:
- undefined(默認):對當前host和port,使用全局Agent。
- Agent:一個對象,會傳入agent屬性。
- false:不緩存連接,默認HTTP請求為`Connection: close`。
- keepAlive:一個布爾值,表示是否保留socket供未來其他請求使用,默認等于false。
- keepAliveMsecs:一個整數,當使用KeepAlive的時候,設置多久發送一個TCP KeepAlive包,使得連接不要被關閉。默認等于1000,只有keepAlive設為true的時候,該設置才有意義。
request方法的callback參數是可選的,在response事件發生時觸發,而且只觸發一次。
`http.request()`返回一個`http.ClientRequest`類的實例。它是一個可寫數據流,如果你想通過POST方法發送一個文件,可以將文件寫入這個ClientRequest對象。
下面是發送POST請求的一個例子。
```javascript
var postData = querystring.stringify({
'msg' : 'Hello World!'
});
var options = {
hostname: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
}
};
var req = http.request(options, function(res) {
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('BODY: ' + chunk);
});
});
req.on('error', function(e) {
console.log('problem with request: ' + e.message);
});
// write data to request body
req.write(postData);
req.end();
```
注意,上面代碼中,`req.end()`必須被調用,即使沒有在請求體內寫入任何數據,也必須調用。因為這表示已經完成HTTP請求。
發送過程的任何錯誤(DNS錯誤、TCP錯誤、HTTP解析錯誤),都會在request對象上觸發error事件。
## 搭建HTTPs服務器
搭建HTTPs服務器需要有SSL證書。對于向公眾提供服務的網站,SSL證書需要向證書頒發機構購買;對于自用的網站,可以自制。
自制SSL證書需要OpenSSL,具體命令如下。
```bash
$ openssl genrsa -out key.pem
$ openssl req -new -key key.pem -out csr.pem
$ openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
$ rm csr.pem
```
上面的命令生成兩個文件:ert.pem(證書文件)和 key.pem(私鑰文件)。有了這兩個文件,就可以運行HTTPs服務器了。
Node內置Https支持。
```javascript
var server = https.createServer({
key: privateKey,
cert: certificate,
ca: certificateAuthorityCertificate
}, app);
```
Node.js提供一個https模塊,專門用于處理加密訪問。
```javascript
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
var a = https.createServer(options, function (req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
```
上面代碼顯示,HTTPs服務器與HTTP服務器的最大區別,就是createServer方法多了一個options參數。運行以后,就可以測試是否能夠正常訪問。
```bash
curl -k https://localhost:8000
```
## 模塊屬性
(1)HTTP請求的屬性
- headers:HTTP請求的頭信息。
- url:請求的路徑。
## 模塊方法
(1)http模塊的方法
- createServer(callback):創造服務器實例。
(2)服務器實例的方法
- listen(port):啟動服務器監聽指定端口。
(3)HTTP回應的方法
- setHeader(key, value):指定HTTP頭信息。
- write(str):指定HTTP回應的內容。
- end():發送HTTP回應。
<h2 id="12.13">assert 模塊</h2>
assert模塊是Node的內置模塊,主要用于斷言。如果表達式不符合預期,就拋出一個錯誤。該模塊提供11個方法,但只有少數幾個是常用的。
## assert()
assert方法接受兩個參數,當第一個參數對應的布爾值為true時,不會有任何提示,返回undefined。當第一個參數對應的布爾值為false時,會拋出一個錯誤,該錯誤的提示信息就是第二個參數設定的字符串。
```javascript
// 格式
assert(value, message)
// 例子
var assert = require('assert');
function add (a, b) {
return a + b;
}
var expected = add(1,2);
assert( expected === 3, '預期1加2等于3');
```
上面代碼不會有任何輸出,因為assert方法的第一個參數是true。
```javascript
assert( expected === 4, '預期1加2等于3')
// AssertionError: 預期1加2等于3
```
上面代碼會拋出一個錯誤,因為assert方法的第一個參數是false。
## assert.ok()
ok是assert方法的另一個名字,與assert方法完全一樣。
## assert.equal()
equal方法接受三個參數,第一個參數是實際值,第二個是預期值,第三個是錯誤的提示信息。
```javascript
// 格式
assert.equal(actual, expected, [message])
assert.equal(true, value, message);
// 等同于
assert(value, message);
// 例子
var assert = require('assert');
function add (a, b) {
return a + b;
}
var expected = add(1,2);
// 以下三句效果相同
assert(expected == 3, '預期1+2等于3');
assert.ok(expected == 3, '預期1+2等于3');
assert.equal(expected, 3, '預期1+2等于3');
```
equal方法內部使用的是相等運算符(==),而不是嚴格運算符(===),進行比較運算。
## assert.notEqual()
notEqual方法的用法與equal方法類似,但只有在實際值等于預期值時,才會拋出錯誤。
```javascript
// 格式
assert.notEqual(actual, expected, [message])
// 用法
var assert = require('assert');
function add (a, b) {
return a + b;
}
var expected = add(1,2);
// 以下三種寫法效果相同
assert(expected != 4, '預期不等于4');
assert.ok(expected != 4, '預期不等于4');
assert.notEqual(expected, 4, '預期不等于4');
```
notEqual方法內部使用不相等運算符(!=),而不是嚴格不相等運算符(!==),進行比較運算。
## assert.deepEqual()
deepEqual方法用來比較兩個對象。只要它們的屬性一一對應,且值都相等,就認為兩個對象相等,否則拋出一個錯誤。
```javascript
// 格式
assert.deepEqual(actual, expected, [message])
// 例子
var assert = require('assert');
var list1 = [1, 2, 3, 4, 5];
var list2 = [1, 2, 3, 4, 5];
assert.deepEqual(list1, list2, '預期兩個數組應該有相同的屬性');
var person1 = { "name":"john", "age":"21" };
var person2 = { "name":"john", "age":"21" };
assert.deepEqual(person1, person2, '預期兩個對象應該有相同的屬性');
```
## assert.notDeepEqual()
notDeepEqual方法與deepEqual方法正好相反,用來斷言兩個對象是否不相等。
```javascript
// 格式
assert.notDeepEqual(actual, expected, [message])
// 例子
var assert = require('assert');
var list1 = [1, 2, ,3, 4, 5];
var list2 = [1, 2, 3, 4, 5];
assert.notDeepEqual(list1, list2, '預期兩個對象不相等');
var person1 = { "name":"john", "age":"21" };
var person2 = { "name":"jane", "age":"19" };
// deepEqual checks the elements in the objects are identical
assert.notDeepEqual(person1, person2, '預期兩個對象不相等');
```
## assert.strictEqual()
strictEqual方法使用嚴格相等運算符(===),比較兩個表達式。
```javascript
// 格式
assert.strictEqual(actual, expected, [message])
// 例子
var assert = require('assert');
assert.strictEqual(1, '1', '預期嚴格相等');
// AssertionError: 預期嚴格相等
```
## assert.notStrictEqual()
assert.notStrictEqual方法使用嚴格不相等運算符(!==),比較兩個表達式。
```javascript
// 格式
assert.notStrictEqual(actual, expected, [message])
// 例子
var assert = require('assert');
assert.notStrictEqual(1, true, '預期嚴格不相等');
```
## assert.throws()
throws方法預期某個代碼塊會拋出一個錯誤,且拋出的錯誤符合指定的條件。
```javascript
// 格式
assert.throws(block, [error], [message])
// 例一,拋出的錯誤符合某個構造函數
assert.throws(
function() {
throw new Error("Wrong value");
},
Error,
'不符合預期的錯誤類型'
);
// 例二、拋出錯誤的提示信息符合正則表達式
assert.throws(
function() {
throw new Error("Wrong value");
},
/value/,
'不符合預期的錯誤類型'
);
// 例三、拋出的錯誤符合自定義函數的校驗
assert.throws(
function() {
throw new Error("Wrong value");
},
function(err) {
if ( (err instanceof Error) && /value/.test(err) ) {
return true;
}
},
'不符合預期的錯誤類型'
);
```
## assert.doesNotThrow()
doesNotThrow方法與throws方法正好相反,預期某個代碼塊不拋出錯誤。
```javascript
// 格式
assert.doesNotThrow(block, [message])
// 用法
assert.doesNotThrow(
function() {
console.log("Nothing to see here");
},
'預期不拋出錯誤'
);
```
## assert.ifError()
ifError方法斷言某個表達式是否false,如果該表達式對應的布爾值等于true,就拋出一個錯誤。它對于驗證回調函數的第一個參數十分有用,如果該參數為true,就表示有錯誤。
```javascript
// 格式
assert.ifError(value)
// 用法
function sayHello(name, callback) {
var error = false;
var str = "Hello "+name;
callback(error, str);
}
// use the function
sayHello('World', function(err, value){
assert.ifError(err);
// ...
})
```
## assert.fail()
fail方法用于拋出一個錯誤。
```javascript
// 格式
assert.fail(actual, expected, message, operator)
// 例子
var assert = require('assert');
assert.fail(21, 42, 'Test Failed', '###')
// AssertionError: Test Failed
assert.fail(21, 21, 'Test Failed', '###')
// AssertionError: Test Failed
assert.fail(21, 42, undefined, '###')
// AssertionError: 21 ### 42
```
該方法共有四個參數,但是不管參數是什么值,它總是拋出一個錯誤。如果message參數對應的布爾值不為false,拋出的錯誤信息就是message,否則錯誤信息就是“實際值 + 分隔符 + 預期值”。
<h2 id="12.14">Cluster模塊</h2>
## 概述
### 基本用法
Node.js默認單進程運行,對于32位系統最高可以使用512MB內存,對于64位最高可以使用1GB內存。對于多核CPU的計算機來說,這樣做效率很低,因為只有一個核在運行,其他核都在閑置。cluster模塊就是為了解決這個問題而提出的。
cluster模塊允許設立一個主進程和若干個worker進程,由主進程監控和協調worker進程的運行。worker之間采用進程間通信交換消息,cluster模塊內置一個負載均衡器,采用Round-robin算法協調各個worker進程之間的負載。運行時,所有新建立的鏈接都由主進程完成,然后主進程再把TCP連接分配給指定的worker進程。
```javascript
var cluster = require('cluster');
var os = require('os');
if (cluster.isMaster){
for (var i = 0, n = os.cpus().length; i < n; i += 1){
cluster.fork();
}
} else {
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
}
```
上面代碼先判斷當前進程是否為主進程(cluster.isMaster),如果是的,就按照CPU的核數,新建若干個worker進程;如果不是,說明當前進程是worker進程,則在該進程啟動一個服務器程序。
上面這段代碼有一個缺點,就是一旦work進程掛了,主進程無法知道。為了解決這個問題,可以在主進程部署online事件和exit事件的監聽函數。
```javascript
var cluster = require('cluster');
if(cluster.isMaster) {
var numWorkers = require('os').cpus().length;
console.log('Master cluster setting up ' + numWorkers + ' workers...');
for(var i = 0; i < numWorkers; i++) {
cluster.fork();
}
cluster.on('online', function(worker) {
console.log('Worker ' + worker.process.pid + ' is online');
});
cluster.on('exit', function(worker, code, signal) {
console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
console.log('Starting a new worker');
cluster.fork();
});
}
```
上面代碼中,主進程一旦監聽到worker進程的exit事件,就會重啟一個worker進程。worker進程一旦啟動成功,可以正常運行了,就會發出online事件。
### worker對象
worker對象是`cluster.fork()`的返回值,代表一個worker進程。
它的屬性和方法如下。
(1)worker.id
worker.id返回當前worker的獨一無二的進程編號。這個編號也是cluster.workers中指向當前進程的索引值。
(2)worker.process
所有的worker進程都是用child_process.fork()生成的。child_process.fork()返回的對象,就被保存在worker.process之中。通過這個屬性,可以獲取worker所在的進程對象。
(3)worker.send()
該方法用于在主進程中,向子進程發送信息。
```javascript
if (cluster.isMaster) {
var worker = cluster.fork();
worker.send('hi there');
} else if (cluster.isWorker) {
process.on('message', function(msg) {
process.send(msg);
});
}
```
上面代碼的作用是,worker進程對主進程發出的每個消息,都做回聲。
在worker進程中,要向主進程發送消息,使用`process.send(message)`;要監聽主進程發出的消息,使用下面的代碼。
```javascript
process.on('message', function(message) {
console.log(message);
});
```
發出的消息可以字符串,也可以是JSON對象。下面是一個發送JSON對象的例子。
```javascript
worker.send({
type: 'task 1',
from: 'master',
data: {
// the data that you want to transfer
}
});
```
### cluster.workers對象
該對象只有主進程才有,包含了所有worker進程。每個成員的鍵值就是一個worker進程對象,鍵名就是該worker進程的worker.id屬性。
```javascript
function eachWorker(callback) {
for (var id in cluster.workers) {
callback(cluster.workers[id]);
}
}
eachWorker(function(worker) {
worker.send('big announcement to all workers');
});
```
上面代碼用來遍歷所有worker進程。
當前socket的data事件,也可以用id屬性識別worker進程。
```javascript
socket.on('data', function(id) {
var worker = cluster.workers[id];
});
```
## cluster模塊的屬性與方法
### isMaster,isWorker
isMaster屬性返回一個布爾值,表示當前進程是否為主進程。這個屬性由process.env.NODE_UNIQUE_ID決定,如果process.env.NODE_UNIQUE_ID為未定義,就表示該進程是主進程。
isWorker屬性返回一個布爾值,表示當前進程是否為work進程。它與isMaster屬性的值正好相反。
### fork()
fork方法用于新建一個worker進程,上下文都復制主進程。只有主進程才能調用這個方法。
該方法返回一個worker對象。
### kill()
kill方法用于終止worker進程。它可以接受一個參數,表示系統信號。
如果當前是主進程,就會終止與worker.process的聯絡,然后將系統信號法發向worker進程。如果當前是worker進程,就會終止與主進程的通信,然后退出,返回0。
在以前的版本中,該方法也叫做 worker.destroy() 。
### listening事件
worker進程調用listen方面以后,“listening”就傳向該進程的服務器,然后傳向主進程。
該事件的回調函數接受兩個參數,一個是當前worker對象,另一個是地址對象,包含網址、端口、地址類型(IPv4、IPv6、Unix socket、UDP)等信息。這對于那些服務多個網址的Node應用程序非常有用。
```javascript
cluster.on('listening', function(worker, address) {
console.log("A worker is now connected to " + address.address + ":" + address.port);
});
```
## 不中斷地重啟Node服務
### 思路
重啟服務需要關閉后再啟動,利用cluster模塊,可以做到先啟動一個worker進程,再把原有的所有work進程關閉。這樣就能實現不中斷地重啟Node服務。
首先,主進程向worker進程發出重啟信號。
```javascript
workers[wid].send({type: 'shutdown', from: 'master'});
```
worker進程監聽message事件,一旦發現內容是shutdown,就退出。
```javascript
process.on('message', function(message) {
if(message.type === 'shutdown') {
process.exit(0);
}
});
```
下面是一個關閉所有worker進程的函數。
```javascript
function restartWorkers() {
var wid, workerIds = [];
for(wid in cluster.workers) {
workerIds.push(wid);
}
workerIds.forEach(function(wid) {
cluster.workers[wid].send({
text: 'shutdown',
from: 'master'
});
setTimeout(function() {
if(cluster.workers[wid]) {
cluster.workers[wid].kill('SIGKILL');
}
}, 5000);
});
};
```
### 實例
下面是一個完整的實例,先是主進程的代碼master.js。
```javascript
var cluster = require('cluster');
console.log('started master with ' + process.pid);
// 新建一個worker進程
cluster.fork();
process.on('SIGHUP', function () {
console.log('Reloading...');
var new_worker = cluster.fork();
new_worker.once('listening', function () {
// 關閉所有其他worker進程
for(var id in cluster.workers) {
if (id === new_worker.id.toString()) continue;
cluster.workers[id].kill('SIGTERM');
}
});
});
```
上面代碼中,主進程監聽SIGHUP事件,如果發生該事件就關閉其他所有worker進程。之所以是SIGHUP事件,是因為nginx服務器監聽到這個信號,會創造一個新的worker進程,重新加載配置文件。另外,關閉worker進程時,主進程發送SIGTERM信號,這是因為Node允許多個worker進程監聽同一個端口。
下面是worker進程的代碼server.js。
```javascript
var cluster = require('cluster');
if (cluster.isMaster) {
require('./master');
return;
}
var express = require('express');
var http = require('http');
var app = express();
app.get('/', function (req, res) {
res.send('ha fsdgfds gfds gfd!');
});
http.createServer(app).listen(8080, function () {
console.log('http://localhost:8080');
});
```
使用時代碼如下。
```bash
$ node server.js
started master with 10538
http://localhost:8080
```
然后,向主進程連續發出兩次SIGHUP信號。
```bash
$ kill -SIGHUP 10538
$ kill -SIGHUP 10538
```
主進程會連續兩次新建一個worker進程,然后關閉所有其他worker進程,顯示如下。
```bash
Reloading...
http://localhost:8080
Reloading...
http://localhost:8080
```
最后,向主進程發出SIGTERM信號,關閉主進程。
```bash
$ kill 10538
```
## PM2模塊
PM2模塊是cluster模塊的一個包裝層。它的作用是盡量將cluster模塊抽象掉,讓用戶像使用單進程一樣,部署多進程Node應用。
```javascript
// app.js
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world");
}).listen(8080);
```
上面代碼是標準的Node架設Web服務器的方式,然后用PM2從命令行啟動這段代碼。
```javascript
$ pm2 start app.js -i 4
```
上面代碼的i參數告訴PM2,這段代碼應該在cluster_mode啟動,且新建worker進程的數量是4個。如果i參數的值是0,那么當前機器有幾個CPU內核,PM2就會啟動幾個worker進程。
如果一個worker進程由于某種原因掛掉了,會立刻重啟該worker進程。
```bash
# 重啟所有worker進程
$ pm2 reload all
```
每個worker進程都有一個id,可以用下面的命令查看單個worker進程的詳情。
```bash
$ pm2 show <worker id>
```
正確情況下,PM2采用fork模式新建worker進程,即主進程fork自身,產生一個worker進程。`pm2 reload`命令則會用spawn方式啟動,即一個接一個啟動worker進程,一個新的worker啟動成功,再殺死一個舊的worker進程。采用這種方式,重新部署新版本時,服務器就不會中斷服務。
```bash
$ pm2 reload <腳本文件名>
```
關閉worker進程的時候,可以部署下面的代碼,讓worker進程監聽shutdown消息。一旦收到這個消息,進行完畢收尾清理工作再關閉。
```javascript
process.on('message', function(msg) {
if (msg === 'shutdown') {
close_all_connections();
delete_logs();
server.close();
process.exit(0);
}
});
```
<h2 id="12.15">OS模塊</h2>
os模塊提供與操作系統相關的方法。
## API
### os.tmpdir()
`os.tmpdir`方法返回操作系統默認的臨時文件目錄。
## Socket通信
下面例子列出當前系列的所有IP地址。
```javascript
var os = require('os');
var interfaces = os.networkInterfaces();
for (item in interfaces) {
console.log('Network interface name: ' + item);
for (att in interfaces[item]) {
var address = interfaces[item][att];
console.log('Family: ' + address.family);
console.log('IP Address: ' + address.address);
console.log('Is Internal: ' + address.internal);
console.log('');
}
console.log('==================================');
}
```
<h2 id="12.16">Net模塊和DNS模塊</h2>
net模塊用于底層的網絡通信。
## 服務器端Socket接口
來看一個簡單的Telnet服務的[例子](https://gist.github.com/atdt/4037228)。
```javascript
var net = require('net');
var port = 1081;
var logo = fs.readFileSync('logo.txt');
var ps1 = '\n\n>>> ';
net.createServer( function ( socket ) {
socket.write( logo );
socket.write( ps1 );
socket.on( 'data', recv.bind( null, socket ) );
} ).listen( port );
```
上面代碼,在1081端口架設了一個服務。可以用telnet訪問這個服務。
```bash
$ telnet localhost 1081
```
一旦telnet連入以后,就會顯示提示符`>>>`,輸入命令以后,就會調用回調函數`recv`。
```javascript
function recv( socket, data ) {
if ( data === 'quit' ) {
socket.end( 'Bye!\n' );
return;
}
request( { uri: baseUrl + data }, function ( error, response, body ) {
if ( body && body.length ) {
$ = cheerio.load( body );
socket.write( $( '#mw-content-text p' ).first().text() + '\n' );
} else {
socket.write( 'Error: ' + response.statusCode );
}
socket.write( ps1 );
} );
}
```
上面代碼中,如果輸入的命令是`quit`,然后就退出telnet。如果是其他命令,就發起遠程請求讀取數據,并顯示在屏幕上。
下面代碼是另一個例子,用到了更多的接口。
```javascript
var serverPort = 9099;
var net = require('net');
var server = net.createServer(function(client) {
console.log('client connected');
console.log('client IP Address: ' + client.remoteAddress);
console.log('is IPv6: ' + net.isIPv6(client.remoteAddress));
console.log('total server connections: ' + server.connections);
// Waiting for data from the client.
client.on('data', function(data) {
console.log('received data: ' + data.toString());
// Write data to the client socket.
client.write('hello from server');
});
// Closed socket event from the client.
client.on('end', function() {
console.log('client disconnected');
});
});
server.on('error',function(err){
console.log(err);
server.close();
});
server.listen(serverPort, function() {
console.log('server started on port ' + serverPort);
});
```
上面代碼中,createServer方法建立了一個服務端,一旦收到客戶端發送的數據,就發出回應,同時還監聽客戶端是否中斷通信。最后,listen方法打開服務端。
## 客戶端Socket接口
客戶端Socket接口用來向服務器發送數據。
```javascript
var serverPort = 9099;
var server = 'localhost';
var net = require('net');
console.log('connecting to server...');
var client = net.connect({server:server,port:serverPort},function(){
console.log('client connected');
// send data
console.log('send data to server');
client.write('greeting from client socket');
});
client.on('data', function(data) {
console.log('received data: ' + data.toString());
client.end();
});
client.on('error',function(err){
console.log(err);
});
client.on('end', function() {
console.log('client disconnected');
});
```
上面代碼連接服務器之后,就向服務器發送數據,然后監聽服務器返回的數據。
## DNS模塊
DNS模塊用于解析域名。resolve4方法用于IPv4環境,resolve6方法用于IPv6環境,lookup方法在以上兩種環境都可以使用,返回IP地址(address)和當前環境(IPv4或IPv6)。
```javascript
var dns = require('dns');
dns.resolve4('www.pecollege.net', function (err, addresses) {
if (err)
console.log(err);
console.log('addresses: ' + JSON.stringify(addresses));
});
dns.lookup('www.pecollege.net', function (err, address, family) {
if (err)
console.log(err);
console.log('addresses: ' + JSON.stringify(address));
console.log('family: ' + JSON.stringify(family));
});
```
<h2 id="12.17">Express框架</h2>
## 概述
Express是目前最流行的基于Node.js的Web開發框架,可以快速地搭建一個完整功能的網站。
Express上手非常簡單,首先新建一個項目目錄,假定叫做hello-world。
```bash
$ mkdir hello-world
```
進入該目錄,新建一個package.json文件,內容如下。
```javascript
{
"name": "hello-world",
"description": "hello world test app",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "4.x"
}
}
```
上面代碼定義了項目的名稱、描述、版本等,并且指定需要4.0版本以上的Express。
然后,就可以安裝了。
```bash
$ npm install
```
執行上面的命令以后,在項目根目錄下,新建一個啟動文件,假定叫做index.js。
```javascript
var express = require('express');
var app = express();
app.use(express.static(__dirname + '/public'));
app.listen(8080);
```
然后,運行上面的啟動腳本。
```bash
$ node index
```
現在就可以訪問`http://localhost:8080`,它會在瀏覽器中打開當前目錄的public子目錄(嚴格來說,是打開public目錄的index.html文件)。如果public目錄之中有一個圖片文件`my_image.png`,那么可以用`http://localhost:8080/my_image.png`訪問該文件。
你也可以在index.js之中,生成動態網頁。
```javascript
// index.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello world!');
});
app.listen(3000);
```
然后,在命令行下運行啟動腳本,就可以在瀏覽器中訪問項目網站了。
```bash
$ node index
```
上面代碼會在本機的3000端口啟動一個網站,網頁顯示Hello World。
啟動腳本index.js的`app.get`方法,用于指定不同的訪問路徑所對應的回調函數,這叫做“路由”(routing)。上面代碼只指定了根目錄的回調函數,因此只有一個路由記錄。實際應用中,可能有多個路由記錄。
```javascript
// index.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello world!');
});
app.get('/customer', function(req, res){
res.send('customer page');
});
app.get('/admin', function(req, res){
res.send('admin page');
});
app.listen(3000);
```
這時,最好就把路由放到一個單獨的文件中,比如新建一個routes子目錄。
```javascript
// routes/index.js
module.exports = function (app) {
app.get('/', function (req, res) {
res.send('Hello world');
});
app.get('/customer', function(req, res){
res.send('customer page');
});
app.get('/admin', function(req, res){
res.send('admin page');
});
};
```
然后,原來的index.js就變成下面這樣。
```javascript
// index.js
var express = require('express');
var app = express();
var routes = require('./routes')(app);
app.listen(3000);
```
## 運行原理
### 底層:http模塊
Express框架建立在node.js內置的http模塊上。http模塊生成服務器的原始代碼如下。
```javascript
var http = require("http");
var app = http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.end("Hello world!");
});
app.listen(3000, "localhost");
```
上面代碼的關鍵是http模塊的createServer方法,表示生成一個HTTP服務器實例。該方法接受一個回調函數,該回調函數的參數,分別為代表HTTP請求和HTTP回應的request對象和response對象。
Express框架的核心是對http模塊的再包裝。上面的代碼用Express改寫如下。
```javascript
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello world!');
});
app.listen(3000);
```
比較兩段代碼,可以看到它們非常接近。原來是用`http.createServer`方法新建一個app實例,現在則是用Express的構造方法,生成一個Epress實例。兩者的回調函數都是相同的。Express框架等于在http模塊之上,加了一個中間層。
### 什么是中間件
簡單說,中間件(middleware)就是處理HTTP請求的函數。它最大的特點就是,一個中間件處理完,再傳遞給下一個中間件。App實例在運行過程中,會調用一系列的中間件。
每個中間件可以從App實例,接收三個參數,依次為request對象(代表HTTP請求)、response對象(代表HTTP回應),next回調函數(代表下一個中間件)。每個中間件都可以對HTTP請求(request對象)進行加工,并且決定是否調用next方法,將request對象再傳給下一個中間件。
一個不進行任何操作、只傳遞request對象的中間件,就是下面這樣。
```javascript
function uselessMiddleware(req, res, next) {
next();
}
```
上面代碼的next就是下一個中間件。如果它帶有參數,則代表拋出一個錯誤,參數為錯誤文本。
```javascript
function uselessMiddleware(req, res, next) {
next('出錯了!');
}
```
拋出錯誤以后,后面的中間件將不再執行,直到發現一個錯誤處理函數為止。
### use方法
use是express注冊中間件的方法,它返回一個函數。下面是一個連續調用兩個中間件的例子。
```javascript
var express = require("express");
var http = require("http");
var app = express();
app.use(function(request, response, next) {
console.log("In comes a " + request.method + " to " + request.url);
next();
});
app.use(function(request, response) {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Hello world!\n");
});
http.createServer(app).listen(1337);
```
上面代碼使用`app.use`方法,注冊了兩個中間件。收到HTTP請求后,先調用第一個中間件,在控制臺輸出一行信息,然后通過`next`方法,將執行權傳給第二個中間件,輸出HTTP回應。由于第二個中間件沒有調用`next`方法,所以request對象就不再向后傳遞了。
`use`方法內部可以對訪問路徑進行判斷,據此就能實現簡單的路由,根據不同的請求網址,返回不同的網頁內容。
```javascript
var express = require("express");
var http = require("http");
var app = express();
app.use(function(request, response, next) {
if (request.url == "/") {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Welcome to the homepage!\n");
} else {
next();
}
});
app.use(function(request, response, next) {
if (request.url == "/about") {
response.writeHead(200, { "Content-Type": "text/plain" });
} else {
next();
}
});
app.use(function(request, response) {
response.writeHead(404, { "Content-Type": "text/plain" });
response.end("404 error!\n");
});
http.createServer(app).listen(1337);
```
上面代碼通過`request.url`屬性,判斷請求的網址,從而返回不同的內容。注意,`app.use`方法一共登記了三個中間件,只要請求路徑匹配,就不會將執行權交給下一個中間件。因此,最后一個中間件會返回404錯誤,即前面的中間件都沒匹配請求路徑,找不到所要請求的資源。
除了在回調函數內部判斷請求的網址,use方法也允許將請求網址寫在第一個參數。這代表,只有請求路徑匹配這個參數,后面的中間件才會生效。無疑,這樣寫更加清晰和方便。
```javascript
app.use('/path', someMiddleware);
```
上面代碼表示,只對根目錄的請求,調用某個中間件。
因此,上面的代碼可以寫成下面的樣子。
```javascript
var express = require("express");
var http = require("http");
var app = express();
app.use("/home", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Welcome to the homepage!\n");
});
app.use("/about", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Welcome to the about page!\n");
});
app.use(function(request, response) {
response.writeHead(404, { "Content-Type": "text/plain" });
response.end("404 error!\n");
});
http.createServer(app).listen(1337);
```
## Express的方法
### all方法和HTTP動詞方法
針對不同的請求,Express提供了use方法的一些別名。比如,上面代碼也可以用別名的形式來寫。
```javascript
var express = require("express");
var http = require("http");
var app = express();
app.all("*", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/plain" });
next();
});
app.get("/", function(request, response) {
response.end("Welcome to the homepage!");
});
app.get("/about", function(request, response) {
response.end("Welcome to the about page!");
});
app.get("*", function(request, response) {
response.end("404!");
});
http.createServer(app).listen(1337);
```
上面代碼的all方法表示,所有請求都必須通過該中間件,參數中的“*”表示對所有路徑有效。get方法則是只有GET動詞的HTTP請求通過該中間件,它的第一個參數是請求的路徑。由于get方法的回調函數沒有調用next方法,所以只要有一個中間件被調用了,后面的中間件就不會再被調用了。
除了get方法以外,Express還提供post、put、delete方法,即HTTP動詞都是Express的方法。
這些方法的第一個參數,都是請求的路徑。除了絕對匹配以外,Express允許模式匹配。
```javascript
app.get("/hello/:who", function(req, res) {
res.end("Hello, " + req.params.who + ".");
});
```
上面代碼將匹配“/hello/alice”網址,網址中的alice將被捕獲,作為req.params.who屬性的值。需要注意的是,捕獲后需要對網址進行檢查,過濾不安全字符,上面的寫法只是為了演示,生產中不應這樣直接使用用戶提供的值。
如果在模式參數后面加上問號,表示該參數可選。
```javascript
app.get('/hello/:who?',function(req,res) {
if(req.params.id) {
res.end("Hello, " + req.params.who + ".");
}
else {
res.send("Hello, Guest.");
}
});
```
下面是一些更復雜的模式匹配的例子。
```javascript
app.get('/forum/:fid/thread/:tid', middleware)
// 匹配/commits/71dbb9c
// 或/commits/71dbb9c..4c084f9這樣的git格式的網址
app.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function(req, res){
var from = req.params[0];
var to = req.params[1] || 'HEAD';
res.send('commit range ' + from + '..' + to);
});
```
### set方法
set方法用于指定變量的值。
```javascript
app.set("views", __dirname + "/views");
app.set("view engine", "jade");
```
上面代碼使用set方法,為系統變量“views”和“view engine”指定值。
### response對象
**(1)response.redirect方法**
response.redirect方法允許網址的重定向。
```javascript
response.redirect("/hello/anime");
response.redirect("http://www.example.com");
response.redirect(301, "http://www.example.com");
```
**(2)response.sendFile方法**
response.sendFile方法用于發送文件。
```javascript
response.sendFile("/path/to/anime.mp4");
```
**(3)response.render方法**
response.render方法用于渲染網頁模板。
```javascript
app.get("/", function(request, response) {
response.render("index", { message: "Hello World" });
});
```
上面代碼使用render方法,將message變量傳入index模板,渲染成HTML網頁。
### requst對象
**(1)request.ip**
request.ip屬性用于獲得HTTP請求的IP地址。
**(2)request.files**
request.files用于獲取上傳的文件。
### 搭建HTTPs服務器
使用Express搭建HTTPs加密服務器,也很簡單。
```javascript
var fs = require('fs');
var options = {
key: fs.readFileSync('E:/ssl/myserver.key'),
cert: fs.readFileSync('E:/ssl/myserver.crt'),
passphrase: '1234'
};
var https = require('https');
var express = require('express');
var app = express();
app.get('/', function(req, res){
res.send('Hello World Expressjs');
});
var server = https.createServer(options, app);
server.listen(8084);
console.log('Server is running on port 8084');
```
## 項目開發實例
### 編寫啟動腳本
上一節使用express命令自動建立項目,也可以不使用這個命令,手動新建所有文件。
先建立一個項目目錄(假定這個目錄叫做demo)。進入該目錄,新建一個package.json文件,寫入項目的配置信息。
```javascript
{
"name": "demo",
"description": "My First Express App",
"version": "0.0.1",
"dependencies": {
"express": "3.x"
}
}
```
在項目目錄中,新建文件app.js。項目的代碼就放在這個文件里面。
```javascript
var express = require('express');
var app = express();
```
上面代碼首先加載express模塊,賦給變量express。然后,生成express實例,賦給變量app。
接著,設定express實例的參數。
```javascript
// 設定port變量,意為訪問端口
app.set('port', process.env.PORT || 3000);
// 設定views變量,意為視圖存放的目錄
app.set('views', path.join(__dirname, 'views'));
// 設定view engine變量,意為網頁模板引擎
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
// 設定靜態文件目錄,比如本地文件
// 目錄為demo/public/images,訪問
// 網址則顯示為http://localhost:3000/images
app.use(express.static(path.join(__dirname, 'public')));
```
上面代碼中的set方法用于設定內部變量,use方法用于調用express的中間件。
最后,調用實例方法listen,讓其監聽事先設定的端口(3000)。
```javascript
app.listen(app.get('port'));
```
這時,運行下面的命令,就可以在瀏覽器訪問http://127.0.0.1:3000。
```bash
node app.js
```
網頁提示“Cannot GET /”,表示沒有為網站的根路徑指定可以顯示的內容。所以,下一步就是配置路由。
### 配置路由
所謂“路由”,就是指為不同的訪問路徑,指定不同的處理方法。
**(1)指定根路徑**
在app.js之中,先指定根路徑的處理方法。
```javascript
app.get('/', function(req, res) {
res.send('Hello World');
});
```
上面代碼的get方法,表示處理客戶端發出的GET請求。相應的,還有app.post、app.put、app.del(delete是JavaScript保留字,所以改叫del)方法。
get方法的第一個參數是訪問路徑,正斜杠(/)就代表根路徑;第二個參數是回調函數,它的req參數表示客戶端發來的HTTP請求,res參數代表發向客戶端的HTTP回應,這兩個參數都是對象。在回調函數內部,使用HTTP回應的send方法,表示向瀏覽器發送一個字符串。然后,運行下面的命令。
```bash
node app.js
```
此時,在瀏覽器中訪問http://127.0.0.1:3000,網頁就會顯示“Hello World”。
如果需要指定HTTP頭信息,回調函數就必須換一種寫法,要使用setHeader方法與end方法。
```javascript
app.get('/', function(req, res){
var body = 'Hello World';
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', body.length);
res.end(body);
});
```
**(2)指定特定路徑**
上面是處理根目錄的情況,下面再舉一個例子。假定用戶訪問/api路徑,希望返回一個JSON字符串。這時,get可以這樣寫。
```javascript
app.get('/api', function(request, response) {
response.send({name:"張三",age:40});
});
```
上面代碼表示,除了發送字符串,send方法還可以直接發送對象。重新啟動node以后,再訪問路徑/api,瀏覽器就會顯示一個JSON對象。
```javascript
{
"name": "張三",
"age": 40
}
```
我們也可以把app.get的回調函數,封裝成模塊。先在routes目錄下面建立一個api.js文件。
```javascript
// routes/api.js
exports.index = function (req, res){
res.json(200, {name:"張三",age:40});
}
```
然后,在app.js中加載這個模塊。
```javascript
// app.js
var api = require('./routes/api');
app.get('/api', api.index);
```
現在訪問時,就會顯示與上一次同樣的結果。
如果只向瀏覽器發送簡單的文本信息,上面的方法已經夠用;但是如果要向瀏覽器發送復雜的內容,還是應該使用網頁模板。
### 靜態網頁模板
在項目目錄之中,建立一個子目錄views,用于存放網頁模板。
假定這個項目有三個路徑:根路徑(/)、自我介紹(/about)和文章(/article)。那么,app.js可以這樣寫:
```javascript
var express = require('express');
var app = express();
app.get('/', function(req, res) {
res.sendfile('./views/index.html');
});
app.get('/about', function(req, res) {
res.sendfile('./views/about.html');
});
app.get('/article', function(req, res) {
res.sendfile('./views/article.html');
});
app.listen(3000);
```
上面代碼表示,三個路徑分別對應views目錄中的三個模板:index.html、about.html和article.html。另外,向服務器發送信息的方法,從send變成了sendfile,后者專門用于發送文件。
假定index.html的內容如下:
```html
<html>
<head>
<title>首頁</title>
</head>
<body>
<h1>Express Demo</h1>
<footer>
<p>
<a href="/">首頁</a> - <a href="/about">自我介紹</a> - <a href="/article">文章</a>
</p>
</footer>
</body>
</html>
```
上面代碼是一個靜態網頁。如果想要展示動態內容,就必須使用動態網頁模板。
## 動態網頁模板
網站真正的魅力在于動態網頁,下面我們來看看,如何制作一個動態網頁的網站。
### 安裝模板引擎
Express支持多種模板引擎,這里采用Handlebars模板引擎的服務器端版本[hbs](https://github.com/donpark/hbs)模板引擎。
先安裝hbs。
```html
npm install hbs --save-dev
```
上面代碼將hbs模塊,安裝在項目目錄的子目錄node_modules之中。save-dev參數表示,將依賴關系寫入package.json文件。安裝以后的package.json文件變成下面這樣:
```javascript
// package.json文件
{
"name": "demo",
"description": "My First Express App",
"version": "0.0.1",
"dependencies": {
"express": "3.x"
},
"devDependencies": {
"hbs": "~2.3.1"
}
}
```
安裝模板引擎之后,就要改寫app.js。
```javascript
// app.js文件
var express = require('express');
var app = express();
// 加載hbs模塊
var hbs = require('hbs');
// 指定模板文件的后綴名為html
app.set('view engine', 'html');
// 運行hbs模塊
app.engine('html', hbs.__express);
app.get('/', function (req, res){
res.render('index');
});
app.get('/about', function(req, res) {
res.render('about');
});
app.get('/article', function(req, res) {
res.render('article');
});
```
上面代碼改用render方法,對網頁模板進行渲染。render方法的參數就是模板的文件名,默認放在子目錄views之中,后綴名已經在前面指定為html,這里可以省略。所以,res.render('index') 就是指,把子目錄views下面的index.html文件,交給模板引擎hbs渲染。
### 新建數據腳本
渲染是指將數據代入模板的過程。實際運用中,數據都是保存在數據庫之中的,這里為了簡化問題,假定數據保存在一個腳本文件中。
在項目目錄中,新建一個文件blog.js,用于存放數據。blog.js的寫法符合CommonJS規范,使得它可以被require語句加載。
```javascript
// blog.js文件
var entries = [
{"id":1, "title":"第一篇", "body":"正文", "published":"6/2/2013"},
{"id":2, "title":"第二篇", "body":"正文", "published":"6/3/2013"},
{"id":3, "title":"第三篇", "body":"正文", "published":"6/4/2013"},
{"id":4, "title":"第四篇", "body":"正文", "published":"6/5/2013"},
{"id":5, "title":"第五篇", "body":"正文", "published":"6/10/2013"},
{"id":6, "title":"第六篇", "body":"正文", "published":"6/12/2013"}
];
exports.getBlogEntries = function (){
return entries;
}
exports.getBlogEntry = function (id){
for(var i=0; i < entries.length; i++){
if(entries[i].id == id) return entries[i];
}
}
```
### 新建網頁模板
接著,新建模板文件index.html。
```html
<!-- views/index.html文件 -->
<h1>文章列表</h1>
{{"{{"}}#each entries}}
<p>
<a href="/article/{{"{{"}}id}}">{{"{{"}}title}}</a><br/>
Published: {{"{{"}}published}}
</p>
{{"{{"}}/each}}
```
模板文件about.html。
```html
<!-- views/about.html文件 -->
<h1>自我介紹</h1>
<p>正文</p>
```
模板文件article.html。
```html
<!-- views/article.html文件 -->
<h1>{{"{{"}}blog.title}}</h1>
Published: {{"{{"}}blog.published}}
<p/>
{{"{{"}}blog.body}}
```
可以看到,上面三個模板文件都只有網頁主體。因為網頁布局是共享的,所以布局的部分可以單獨新建一個文件layout.html。
```html
<!-- views/layout.html文件 -->
<html>
<head>
<title>{{"{{"}}title}}</title>
</head>
<body>
{{"{{{"}}body}}}
<footer>
<p>
<a href="/">首頁</a> - <a href="/about">自我介紹</a>
</p>
</footer>
</body>
</html>
```
### 渲染模板
最后,改寫app.js文件。
```javascript
// app.js文件
var express = require('express');
var app = express();
var hbs = require('hbs');
// 加載數據模塊
var blogEngine = require('./blog');
app.set('view engine', 'html');
app.engine('html', hbs.__express);
app.use(express.bodyParser());
app.get('/', function(req, res) {
res.render('index',{title:"最近文章", entries:blogEngine.getBlogEntries()});
});
app.get('/about', function(req, res) {
res.render('about', {title:"自我介紹"});
});
app.get('/article/:id', function(req, res) {
var entry = blogEngine.getBlogEntry(req.params.id);
res.render('article',{title:entry.title, blog:entry});
});
app.listen(3000);
```
上面代碼中的render方法,現在加入了第二個參數,表示模板變量綁定的數據。
現在重啟node服務器,然后訪問http://127.0.0.1:3000。
```bash
node app.js
```
可以看得,模板已經使用加載的數據渲染成功了。
### 指定靜態文件目錄
模板文件默認存放在views子目錄。這時,如果要在網頁中加載靜態文件(比如樣式表、圖片等),就需要另外指定一個存放靜態文件的目錄。
```javascript
app.use(express.static('public'));
```
上面代碼在文件app.js之中,指定靜態文件存放的目錄是public。于是,當瀏覽器發出非HTML文件請求時,服務器端就到public目錄尋找這個文件。比如,瀏覽器發出如下的樣式表請求:
```javascript
<link href="/bootstrap/css/bootstrap.css" rel="stylesheet">
```
服務器端就到public/bootstrap/css/目錄中尋找bootstrap.css文件。
## Express.Router用法
從Express 4.0開始,路由器功能成了一個單獨的組件`Express.Router`。它好像小型的express應用程序一樣,有自己的use、get、param和route方法。
### 基本用法
首先,`Express.Router`是一個構造函數,調用后返回一個路由器實例。然后,使用該實例的HTTP動詞方法,為不同的訪問路徑,指定回調函數;最后,掛載到某個路徑。
```javascript
var router = express.Router();
router.get('/', function(req, res) {
res.send('首頁');
});
router.get('/about', function(req, res) {
res.send('關于');
});
app.use('/', router);
```
上面代碼先定義了兩個訪問路徑,然后將它們掛載到根目錄。如果最后一行改為app.use('/app', router),則相當于為`/app`和`/app/about`這兩個路徑,指定了回調函數。
這種路由器可以自由掛載的做法,為程序帶來了更大的靈活性,既可以定義多個路由器實例,也可以為將同一個路由器實例掛載到多個路徑。
### router.route方法
router實例對象的route方法,可以接受訪問路徑作為參數。
```javascript
var router = express.Router();
router.route('/api')
.post(function(req, res) {
// ...
})
.get(function(req, res) {
Bear.find(function(err, bears) {
if (err) res.send(err);
res.json(bears);
});
});
app.use('/', router);
```
### router中間件
use方法為router對象指定中間件,即在數據正式發給用戶之前,對數據進行處理。下面就是一個中間件的例子。
```javascript
router.use(function(req, res, next) {
console.log(req.method, req.url);
next();
});
```
上面代碼中,回調函數的next參數,表示接受其他中間件的調用。函數體中的next(),表示將數據傳遞給下一個中間件。
注意,中間件的放置順序很重要,等同于執行順序。而且,中間件必須放在HTTP動詞方法之前,否則不會執行。
### 對路徑參數的處理
router對象的param方法用于路徑參數的處理,可以
```javascript
router.param('name', function(req, res, next, name) {
// 對name進行驗證或其他處理……
console.log(name);
req.name = name;
next();
});
router.get('/hello/:name', function(req, res) {
res.send('hello ' + req.name + '!');
});
```
上面代碼中,get方法為訪問路徑指定了name參數,param方法則是對name參數進行處理。注意,param方法必須放在HTTP動詞方法之前。
### app.route
假定app是Express的實例對象,Express 4.0為該對象提供了一個route屬性。app.route實際上是express.Router()的縮寫形式,除了直接掛載到根路徑。因此,對同一個路徑指定get和post方法的回調函數,可以寫成鏈式形式。
```javascript
app.route('/login')
.get(function(req, res) {
res.send('this is the login form');
})
.post(function(req, res) {
console.log('processing');
res.send('processing the login form!');
});
```
上面代碼的這種寫法,顯然非常簡潔清晰。
## 上傳文件
首先,在網頁插入上傳文件的表單。
```html
<form action="/pictures/upload" method="POST" enctype="multipart/form-data">
Select an image to upload:
<input type="file" name="image">
<input type="submit" value="Upload Image">
</form>
```
然后,服務器腳本建立指向`/upload`目錄的路由。這時可以安裝multer模塊,它提供了上傳文件的許多功能。
```javascript
var express = require('express');
var router = express.Router();
var multer = require('multer');
var uploading = multer({
dest: __dirname + '../public/uploads/',
// 設定限制,每次最多上傳1個文件,文件大小不超過1MB
limits: {fileSize: 1000000, files:1},
})
router.post('/upload', uploading, function(req, res) {
})
module.exports = router
```
上面代碼是上傳文件到本地目錄。下面是上傳到Amazon S3的例子。
首先,在S3上面新增CORS配置文件。
```xml
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
```
上面的配置允許任意電腦向你的bucket發送HTTP請求。
然后,安裝aws-sdk。
```bash
$ npm install aws-sdk --save
```
下面是服務器腳本。
```javascript
var express = require('express');
var router = express.Router();
var aws = require('aws-sdk');
router.get('/', function(req, res) {
res.render('index')
})
var AWS_ACCESS_KEY = 'your_AWS_access_key'
var AWS_SECRET_KEY = 'your_AWS_secret_key'
var S3_BUCKET = 'images_upload'
router.get('/sign', function(req, res) {
aws.config.update({accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY});
var s3 = new aws.S3()
var options = {
Bucket: S3_BUCKET,
Key: req.query.file_name,
Expires: 60,
ContentType: req.query.file_type,
ACL: 'public-read'
}
s3.getSignedUrl('putObject', options, function(err, data){
if(err) return res.send('Error with S3')
res.json({
signed_request: data,
url: 'https://s3.amazonaws.com/' + S3_BUCKET + '/' + req.query.file_name
})
})
})
module.exports = router
```
上面代碼中,用戶訪問`/sign`路徑,正確登錄后,會收到一個JSON對象,里面是S3返回的數據和一個暫時用來接收上傳文件的URL,有效期只有60秒。
瀏覽器代碼如下。
```javascript
// HTML代碼為
// <br>Please select an image
// <input type="file" id="image">
// <br>
// <img id="preview">
document.getElementById("image").onchange = function() {
var file = document.getElementById("image").files[0]
if (!file) return
sign_request(file, function(response) {
upload(file, response.signed_request, response.url, function() {
document.getElementById("preview").src = response.url
})
})
}
function sign_request(file, done) {
var xhr = new XMLHttpRequest()
xhr.open("GET", "/sign?file_name=" + file.name + "&file_type=" + file.type)
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
var response = JSON.parse(xhr.responseText)
done(response)
}
}
xhr.send()
}
function upload(file, signed_request, url, done) {
var xhr = new XMLHttpRequest()
xhr.open("PUT", signed_request)
xhr.setRequestHeader('x-amz-acl', 'public-read')
xhr.onload = function() {
if (xhr.status === 200) {
done()
}
}
xhr.send(file)
}
```
上面代碼首先監聽file控件的change事件,一旦有變化,就先向服務器要求一個臨時的上傳URL,然后向該URL上傳文件。
<h2 id="12.18">Koa框架</h2>
Koa是一個類似于Express的Web開發框架,創始人也是同一個人。它的主要特點是,使用了ES6的Generator函數,進行了架構的重新設計。也就是說,Koa的原理和內部結構很像Express,但是語法和內部結構進行了升級。
官方[faq](https://github.com/koajs/koa/blob/master/docs/faq.md#why-isnt-koa-just-express-40)有這樣一個問題:”為什么koa不是Express 4.0?“,回答是這樣的:”Koa與Express有很大差異,整個設計都是不同的,所以如果將Express 3.0按照這種寫法升級到4.0,就意味著重寫整個程序。所以,我們覺得創造一個新的庫,是更合適的做法。“
## Koa應用
一個Koa應用就是一個對象,包含了一個middleware數組,這個數組由一組Generator函數組成。這些函數負責對HTTP請求進行各種加工,比如生成緩存、指定代理、請求重定向等等。
```javascript
var koa = require('koa');
var app = koa();
app.use(function *(){
this.body = 'Hello World';
});
app.listen(3000);
```
上面代碼中,變量app就是一個Koa應用。它監聽3000端口,返回一個內容為Hello World的網頁。
app.use方法用于向middleware數組添加Generator函數。
listen方法指定監聽端口,并啟動當前應用。它實際上等同于下面的代碼。
```javascript
var http = require('http');
var koa = require('koa');
var app = koa();
http.createServer(app.callback()).listen(3000);
```
## 中間件
Koa的中間件很像Express的中間件,也是對HTTP請求進行處理的函數,但是必須是一個Generator函數。而且,Koa的中間件是一個級聯式(Cascading)的結構,也就是說,屬于是層層調用,第一個中間件調用第二個中間件,第二個調用第三個,以此類推。上游的中間件必須等到下游的中間件返回結果,才會繼續執行,這點很像遞歸。
中間件通過當前應用的use方法注冊。
```javascript
app.use(function* (next){
var start = new Date; // (1)
yield next; // (2)
var ms = new Date - start; // (3)
console.log('%s %s - %s', this.method, this.url, ms); // (4)
});
```
上面代碼中,`app.use`方法的參數就是中間件,它是一個Generator函數,最大的特征就是function命令與參數之間,必須有一個星號。Generator函數的參數next,表示下一個中間件。
Generator函數內部使用yield命令,將程序的執行權轉交給下一個中間件,即`yield next`,要等到下一個中間件返回結果,才會繼續往下執行。上面代碼中,Generator函數體內部,第一行賦值語句首先執行,開始計時,第二行yield語句將執行權交給下一個中間件,當前中間件就暫停執行。等到后面的中間件全部執行完成,執行權就回到原來暫停的地方,繼續往下執行,這時才會執行第三行,計算這個過程一共花了多少時間,第四行將這個時間打印出來。
下面是一個兩個中間件級聯的例子。
```javascript
app.use(function *() {
this.body = "header\n";
yield saveResults.call(this);
this.body += "footer\n";
});
function *saveResults() {
this.body += "Results Saved!\n";
}
```
上面代碼中,第一個中間件調用第二個中間件saveResults,它們都向`this.body`寫入內容。最后,`this.body`的輸出如下。
```javascript
header
Results Saved!
footer
```
只要有一個中間件缺少`yield next`語句,后面的中間件都不會執行,這一點要引起注意。
```javascript
app.use(function *(next){
console.log('>> one');
yield next;
console.log('<< one');
});
app.use(function *(next){
console.log('>> two');
this.body = 'two';
console.log('<< two');
});
app.use(function *(next){
console.log('>> three');
yield next;
console.log('<< three');
});
```
上面代碼中,因為第二個中間件少了`yield next`語句,第三個中間件并不會執行。
如果想跳過一個中間件,可以直接在該中間件的第一行語句寫上`return yield next`。
```javascript
app.use(function* (next) {
if (skip) return yield next;
})
```
由于Koa要求中間件唯一的參數就是next,導致如果要傳入其他參數,必須另外寫一個返回Generator函數的函數。
```javascript
function logger(format) {
return function *(next){
var str = format
.replace(':method', this.method)
.replace(':url', this.url);
console.log(str);
yield next;
}
}
app.use(logger(':method :url'));
```
上面代碼中,真正的中間件是logger函數的返回值,而logger函數是可以接受參數的。
### 多個中間件的合并
由于中間件的參數統一為next(意為下一個中間件),因此可以使用`.call(this, next)`,將多個中間件進行合并。
```javascript
function *random(next) {
if ('/random' == this.path) {
this.body = Math.floor(Math.random()*10);
} else {
yield next;
}
};
function *backwards(next) {
if ('/backwards' == this.path) {
this.body = 'sdrawkcab';
} else {
yield next;
}
}
function *pi(next) {
if ('/pi' == this.path) {
this.body = String(Math.PI);
} else {
yield next;
}
}
function *all(next) {
yield random.call(this, backwards.call(this, pi.call(this, next)));
}
app.use(all);
```
上面代碼中,中間件all內部,就是依次調用random、backwards、pi,后一個中間件就是前一個中間件的參數。
Koa內部使用koa-compose模塊,進行同樣的操作,下面是它的源碼。
```javascript
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
yield *next;
}
}
function *noop(){}
```
上面代碼中,middleware是中間件數組。前一個中間件的參數是后一個中間件,依次類推。如果最后一個中間件沒有next參數,則傳入一個空函數。
## 路由
可以通過`this.path`屬性,判斷用戶請求的路徑,從而起到路由作用。
```javascript
app.use(function* (next) {
if (this.path === '/') {
this.body = 'we are at home!';
} else {
yield next;
}
})
// 等同于
app.use(function* (next) {
if (this.path !== '/') return yield next;
this.body = 'we are at home!';
})
```
下面是多路徑的例子。
```javascript
let koa = require('koa')
let app = koa()
// normal route
app.use(function* (next) {
if (this.path !== '/') {
return yield next
}
this.body = 'hello world'
});
// /404 route
app.use(function* (next) {
if (this.path !== '/404') {
return yield next;
}
this.body = 'page not found'
});
// /500 route
app.use(function* (next) {
if (this.path !== '/500') {
return yield next;
}
this.body = 'internal server error'
});
app.listen(8080)
```
上面代碼中,每一個中間件負責一個路徑,如果路徑不符合,就傳遞給下一個中間件。
復雜的路由需要安裝koa-router插件。
```javascript
var app = require('koa')();
var Router = require('koa-router');
var myRouter = new Router();
myRouter.get('/', function *(next) {
this.response.body = 'Hello World!';
});
app.use(myRouter.routes());
app.listen(3000);
```
上面代碼對根路徑設置路由。
Koa-router實例提供一系列動詞方法,即一種HTTP動詞對應一種方法。典型的動詞方法有以下五種。
- router.get()
- router.post()
- router.put()
- router.del()
- router.patch()
這些動詞方法可以接受兩個參數,第一個是路徑模式,第二個是對應的控制器方法(中間件),定義用戶請求該路徑時服務器行為。
```javascript
router.get('/', function *(next) {
this.body = 'Hello World!';
});
```
上面代碼中,`router.get`方法的第一個參數是根路徑,第二個參數是對應的函數方法。
注意,路徑匹配的時候,不會把查詢字符串考慮在內。比如,`/index?param=xyz`匹配路徑`/index`。
有些路徑模式比較復雜,Koa-router允許為路徑模式起別名。起名時,別名要添加為動詞方法的第一個參數,這時動詞方法變成接受三個參數。
```javascript
router.get('user', '/users/:id', function *(next) {
// ...
});
```
上面代碼中,路徑模式`\users\:id`的名字就是`user`。路徑的名稱,可以用來引用對應的具體路徑,比如url方法可以根據路徑名稱,結合給定的參數,生成具體的路徑。
```javascript
router.url('user', 3);
// => "/users/3"
router.url('user', { id: 3 });
// => "/users/3"
```
上面代碼中,user就是路徑模式的名稱,對應具體路徑`/users/:id`。url方法的第二個參數3,表示給定id的值是3,因此最后生成的路徑是`/users/3`。
Koa-router允許為路徑統一添加前綴。
```javascript
var router = new Router({
prefix: '/users'
});
router.get('/', ...); // 等同于"/users"
router.get('/:id', ...); // 等同于"/users/:id"
```
路徑的參數通過`this.params`屬性獲取,該屬性返回一個對象,所有路徑參數都是該對象的成員。
```javascript
// 訪問 /programming/how-to-node
router.get('/:category/:title', function *(next) {
console.log(this.params);
// => { category: 'programming', title: 'how-to-node' }
});
```
param方法可以針對命名參數,設置驗證條件。
```javascript
router
.get('/users/:user', function *(next) {
this.body = this.user;
})
.param('user', function *(id, next) {
var users = [ '0號用戶', '1號用戶', '2號用戶'];
this.user = users[id];
if (!this.user) return this.status = 404;
yield next;
})
```
上面代碼中,如果`/users/:user`的參數user對應的不是有效用戶(比如訪問`/users/3`),param方法注冊的中間件會查到,就會返回404錯誤。
redirect方法會將某個路徑的請求,重定向到另一個路徑,并返回301狀態碼。
```javascript
router.redirect('/login', 'sign-in');
// 等同于
router.all('/login', function *() {
this.redirect('/sign-in');
this.status = 301;
});
```
redirect方法的第一個參數是請求來源,第二個參數是目的地,兩者都可以用路徑模式的別名代替。
## context對象
中間件當中的this表示上下文對象context,代表一次HTTP請求和回應,即一次訪問/回應的所有信息,都可以從上下文對象獲得。context對象封裝了request和response對象,并且提供了一些輔助方法。每次HTTP請求,就會創建一個新的context對象。
```javascript
app.use(function *(){
this; // is the Context
this.request; // is a koa Request
this.response; // is a koa Response
});
```
context對象的很多方法,其實是定義在ctx.request對象或ctx.response對象上面,比如,ctx.type和ctx.length對應于ctx.response.type和ctx.response.length,ctx.path和ctx.method對應于ctx.request.path和ctx.request.method。
context對象的全局屬性。
- request:指向Request對象
- response:指向Response對象
- req:指向Node的request對象
- res:指向Node的response對象
- app:指向App對象
- state:用于在中間件傳遞信息。
```javascript
this.state.user = yield User.find(id);
```
上面代碼中,user屬性存放在`this.state`對象上面,可以被另一個中間件讀取。
context對象的全局方法。
- throw():拋出錯誤,直接決定了HTTP回應的狀態碼。
- assert():如果一個表達式為false,則拋出一個錯誤。
```javascript
this.throw(403);
this.throw('name required', 400);
this.throw('something exploded');
this.throw(400, 'name required');
// 等同于
var err = new Error('name required');
err.status = 400;
throw err;
```
assert方法的例子。
```javascript
// 格式
ctx.assert(value, [msg], [status], [properties])
// 例子
this.assert(this.user, 401, 'User not found. Please login!');
```
以下模塊解析POST請求的數據。
- co-body
- https://github.com/koajs/body-parser
- https://github.com/koajs/body-parsers
```javascript
var parse = require('co-body');
// in Koa handler
var body = yield parse(this);
```
## 錯誤處理機制
Koa提供內置的錯誤處理機制,任何中間件拋出的錯誤都會被捕捉到,引發向客戶端返回一個500錯誤,而不會導致進程停止,因此也就不需要forever這樣的模塊重啟進程。
```javascript
app.use(function *() {
throw new Error();
});
```
上面代碼中,中間件內部拋出一個錯誤,并不會導致Koa應用掛掉。Koa內置的錯誤處理機制,會捕捉到這個錯誤。
當然,也可以額外部署自己的錯誤處理機制。
```javascript
app.use(function *() {
try {
yield saveResults();
} catch (err) {
this.throw(400, '數據無效');
}
});
```
上面代碼自行部署了try...catch代碼塊,一旦產生錯誤,就用`this.throw`方法拋出。該方法可以將指定的狀態碼和錯誤信息,返回給客戶端。
對于未捕獲錯誤,可以設置error事件的監聽函數。
```javascript
app.on('error', function(err){
log.error('server error', err);
});
```
error事件的監聽函數還可以接受上下文對象,作為第二個參數。
```javascript
app.on('error', function(err, ctx){
log.error('server error', err, ctx);
});
```
如果一個錯誤沒有被捕獲,koa會向客戶端返回一個500錯誤“Internal Server Error”。
this.throw方法用于向客戶端拋出一個錯誤。
```javascript
this.throw(403);
this.throw('name required', 400);
this.throw(400, 'name required');
this.throw('something exploded');
this.throw('name required', 400)
// 等同于
var err = new Error('name required');
err.status = 400;
throw err;
```
`this.throw`方法的兩個參數,一個是錯誤碼,另一個是報錯信息。如果省略狀態碼,默認是500錯誤。
`this.assert`方法用于在中間件之中斷言,用法類似于Node的assert模塊。
```javascript
this.assert(this.user, 401, 'User not found. Please login!');
```
上面代碼中,如果this.user屬性不存在,會拋出一個401錯誤。
由于中間件是層級式調用,所以可以把`try { yield next }`當成第一個中間件。
```javascript
app.use(function *(next) {
try {
yield next;
} catch (err) {
this.status = err.status || 500;
this.body = err.message;
this.app.emit('error', err, this);
}
});
app.use(function *(next) {
throw new Error('some error');
})
```
## cookie
cookie的讀取和設置。
```javascript
this.cookies.get('view');
this.cookies.set('view', n);
```
get和set方法都可以接受第三個參數,表示配置參數。其中的signed參數,用于指定cookie是否加密。如果指定加密的話,必須用`app.keys`指定加密短語。
```javascript
app.keys = ['secret1', 'secret2'];
this.cookies.set('name', '張三', { signed: true });
```
this.cookie的配置對象的屬性如下。
- signed:cookie是否加密。
- expires:cookie何時過期
- path:cookie的路徑,默認是“/”。
- domain:cookie的域名。
- secure:cookie是否只有https請求下才發送。
- httpOnly:是否只有服務器可以取到cookie,默認為true。
## session
```javascript
var session = require('koa-session');
var koa = require('koa');
var app = koa();
app.keys = ['some secret hurr'];
app.use(session(app));
app.use(function *(){
var n = this.session.views || 0;
this.session.views = ++n;
this.body = n + ' views';
})
app.listen(3000);
console.log('listening on port 3000');
```
## Request對象
Request對象表示HTTP請求。
(1)this.request.header
返回一個對象,包含所有HTTP請求的頭信息。它也可以寫成`this.request.headers`。
(2)this.request.method
返回HTTP請求的方法,該屬性可讀寫。
(3)this.request.length
返回HTTP請求的Content-Length屬性,取不到值,則返回undefined。
(4)this.request.path
返回HTTP請求的路徑,該屬性可讀寫。
(5)this.request.href
返回HTTP請求的完整路徑,包括協議、端口和url。
```javascript
this.request.href
// http://example.com/foo/bar?q=1
```
(6)this.request.querystring
返回HTTP請求的查詢字符串,不含問號。該屬性可讀寫。
(7)this.request.search
返回HTTP請求的查詢字符串,含問號。該屬性可讀寫。
(8)this.request.host
返回HTTP請求的主機(含端口號)。
(9)this.request.hostname
返回HTTP的主機名(不含端口號)。
(10)this.request.type
返回HTTP請求的Content-Type屬性。
```javascript
var ct = this.request.type;
// "image/png"
```
(11)this.request.charset
返回HTTP請求的字符集。
```javascript
this.request.charset
// "utf-8"
```
(12)this.request.query
返回一個對象,包含了HTTP請求的查詢字符串。如果沒有查詢字符串,則返回一個空對象。該屬性可讀寫。
比如,查詢字符串`color=blue&size=small`,會得到以下的對象。
```javascript
{
color: 'blue',
size: 'small'
}
```
(13)this.request.fresh
返回一個布爾值,表示緩存是否代表了最新內容。通常與If-None-Match、ETag、If-Modified-Since、Last-Modified等緩存頭,配合使用。
```javascript
this.response.set('ETag', '123');
// 檢查客戶端請求的內容是否有變化
if (this.request.fresh) {
this.response.status = 304;
return;
}
// 否則就表示客戶端的內容陳舊了,
// 需要取出新內容
this.response.body = yield db.find('something');
```
(14)this.request.stale
返回`this.request.fresh`的相反值。
(15)this.request.protocol
返回HTTP請求的協議,https或者http。
(16)this.request.secure
返回一個布爾值,表示當前協議是否為https。
(17)this.request.ip
返回發出HTTP請求的IP地址。
(18)this.request.subdomains
返回一個數組,表示HTTP請求的子域名。該屬性必須與app.subdomainOffset屬性搭配使用。app.subdomainOffset屬性默認為2,則域名“tobi.ferrets.example.com”返回["ferrets", "tobi"],如果app.subdomainOffset設為3,則返回["tobi"]。
(19)this.request.is(types...)
返回指定的類型字符串,表示HTTP請求的Content-Type屬性是否為指定類型。
```javascript
// Content-Type為 text/html; charset=utf-8
this.request.is('html'); // 'html'
this.request.is('text/html'); // 'text/html'
this.request.is('text/*', 'text/html'); // 'text/html'
// Content-Type為 application/json
this.request.is('json', 'urlencoded'); // 'json'
this.request.is('application/json'); // 'application/json'
this.request.is('html', 'application/*'); // 'application/json'
```
如果不滿足條件,返回false;如果HTTP請求不含數據,則返回undefined。
```javascript
this.is('html'); // false
```
它可以用于過濾HTTP請求,比如只允許請求下載圖片。
```javascript
if (this.is('image/*')) {
// process
} else {
this.throw(415, 'images only!');
}
```
(20)this.request.accepts(types)
檢查HTTP請求的Accept屬性是否可接受,如果可接受,則返回指定的媒體類型,否則返回false。
```javascript
// Accept: text/html
this.request.accepts('html');
// "html"
// Accept: text/*, application/json
this.request.accepts('html');
// "html"
this.request.accepts('text/html');
// "text/html"
this.request.accepts('json', 'text');
// => "json"
this.request.accepts('application/json');
// => "application/json"
// Accept: text/*, application/json
this.request.accepts('image/png');
this.request.accepts('png');
// false
// Accept: text/*;q=.5, application/json
this.request.accepts(['html', 'json']);
this.request.accepts('html', 'json');
// "json"
// No Accept header
this.request.accepts('html', 'json');
// "html"
this.request.accepts('json', 'html');
// => "json"
```
如果accepts方法沒有參數,則返回所有支持的類型(text/html,application/xhtml+xml,image/webp,application/xml,*/*)。
如果accepts方法的參數有多個參數,則返回最佳匹配。如果都不匹配則返回false,并向客戶端拋出一個406”Not Acceptable“錯誤。
如果HTTP請求沒有Accept字段,那么accepts方法返回它的第一個參數。
accepts方法可以根據不同Accept字段,向客戶端返回不同的字段。
```javascript
switch (this.request.accepts('json', 'html', 'text')) {
case 'json': break;
case 'html': break;
case 'text': break;
default: this.throw(406, 'json, html, or text only');
}
```
(21)this.request.acceptsEncodings(encodings)
該方法根據HTTP請求的Accept-Encoding字段,返回最佳匹配,如果沒有合適的匹配,則返回false。
```javascript
// Accept-Encoding: gzip
this.request.acceptsEncodings('gzip', 'deflate', 'identity');
// "gzip"
this.request.acceptsEncodings(['gzip', 'deflate', 'identity']);
// "gzip"
```
注意,acceptEncodings方法的參數必須包括identity(意為不編碼)。
如果HTTP請求沒有Accept-Encoding字段,acceptEncodings方法返回所有可以提供的編碼方法。
```javascript
// Accept-Encoding: gzip, deflate
this.request.acceptsEncodings();
// ["gzip", "deflate", "identity"]
```
如果都不匹配,acceptsEncodings方法返回false,并向客戶端拋出一個406“Not Acceptable”錯誤。
(22)this.request.acceptsCharsets(charsets)
該方法根據HTTP請求的Accept-Charset字段,返回最佳匹配,如果沒有合適的匹配,則返回false。
```javascript
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
this.request.acceptsCharsets('utf-8', 'utf-7');
// => "utf-8"
this.request.acceptsCharsets(['utf-7', 'utf-8']);
// => "utf-8"
```
如果acceptsCharsets方法沒有參數,則返回所有可接受的匹配。
```javascript
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
this.request.acceptsCharsets();
// ["utf-8", "utf-7", "iso-8859-1"]
```
如果都不匹配,acceptsCharsets方法返回false,并向客戶端拋出一個406“Not Acceptable”錯誤。
(23)this.request.acceptsLanguages(langs)
該方法根據HTTP請求的Accept-Language字段,返回最佳匹配,如果沒有合適的匹配,則返回false。
```javascript
// Accept-Language: en;q=0.8, es, pt
this.request.acceptsLanguages('es', 'en');
// "es"
this.request.acceptsLanguages(['en', 'es']);
// "es"
```
如果acceptsCharsets方法沒有參數,則返回所有可接受的匹配。
```javascript
// Accept-Language: en;q=0.8, es, pt
this.request.acceptsLanguages();
// ["es", "pt", "en"]
```
如果都不匹配,acceptsLanguages方法返回false,并向客戶端拋出一個406“Not Acceptable”錯誤。
(24)this.request.socket
返回HTTP請求的socket。
(25)this.request.get(field)
返回HTTP請求指定的字段。
## Response對象
Response對象表示HTTP回應。
(1)this.response.header
返回HTTP回應的頭信息。
(2)this.response.socket
返回HTTP回應的socket。
(3)this.response.status
返回HTTP回應的狀態碼。默認情況下,該屬性沒有值。該屬性可讀寫,設置時等于一個整數。
(4)this.response.message
返回HTTP回應的狀態信息。該屬性與`this.response.message`是配對的。該屬性可讀寫。
(5)this.response.length
返回HTTP回應的Content-Length字段。該屬性可讀寫,如果沒有設置它的值,koa會自動從this.request.body推斷。
(6)this.response.body
返回HTTP回應的信息體。該屬性可讀寫,它的值可能有以下幾種類型。
- 字符串:Content-Type字段默認為text/html或text/plain,字符集默認為utf-8,Content-Length字段同時設定。
- 二進制Buffer:Content-Type字段默認為application/octet-stream,Content-Length字段同時設定。
- Stream:Content-Type字段默認為application/octet-stream。
- JSON對象:Content-Type字段默認為application/json。
- null(表示沒有信息體)
如果`this.response.status`沒設置,Koa會自動將其設為200或204。
(7)this.response.get(field)
返回HTTP回應的指定字段。
```javascript
var etag = this.get('ETag');
```
注意,get方法的參數是區分大小寫的。
(8)this.response.set()
設置HTTP回應的指定字段。
```javascript
this.set('Cache-Control', 'no-cache');
```
set方法也可以接受一個對象作為參數,同時為多個字段指定值。
```javascript
this.set({
'Etag': '1234',
'Last-Modified': date
});
```
(9)this.response.remove(field)
移除HTTP回應的指定字段。
(10)this.response.type
返回HTTP回應的Content-Type字段,不包括“charset”參數的部分。
```javascript
var ct = this.reponse.type;
// "image/png"
```
該屬性是可寫的。
```javascript
this.reponse.type = 'text/plain; charset=utf-8';
this.reponse.type = 'image/png';
this.reponse.type = '.png';
this.reponse.type = 'png';
```
設置type屬性的時候,如果沒有提供charset參數,Koa會判斷是否自動設置。如果`this.response.type`設為html,charset默認設為utf-8;但如果`this.response.type`設為text/html,就不會提供charset的默認值。
(10)this.response.is(types...)
該方法類似于`this.request.is()`,用于檢查HTTP回應的類型是否為支持的類型。
它可以在中間件中起到處理不同格式內容的作用。
```javascript
var minify = require('html-minifier');
app.use(function *minifyHTML(next){
yield next;
if (!this.response.is('html')) return;
var body = this.response.body;
if (!body || body.pipe) return;
if (Buffer.isBuffer(body)) body = body.toString();
this.response.body = minify(body);
});
```
上面代碼是一個中間件,如果輸出的內容類型為HTML,就會進行最小化處理。
(11)this.response.redirect(url, [alt])
該方法執行302跳轉到指定網址。
```javascript
this.redirect('back');
this.redirect('back', '/index.html');
this.redirect('/login');
this.redirect('http://google.com');
```
如果redirect方法的第一個參數是back,將重定向到HTTP請求的Referrer字段指定的網址,如果沒有該字段,則重定向到第二個參數或“/”網址。
如果想修改302狀態碼,或者修改body文字,可以采用下面的寫法。
```javascript
this.status = 301;
this.redirect('/cart');
this.body = 'Redirecting to shopping cart';
```
(12)this.response.attachment([filename])
該方法將HTTP回應的Content-Disposition字段,設為“attachment”,提示瀏覽器下載指定文件。
(13)this.response.headerSent
該方法返回一個布爾值,檢查是否HTTP回應已經發出。
(14)this.response.lastModified
該屬性以Date對象的形式,返回HTTP回應的Last-Modified字段(如果該字段存在)。該屬性可寫。
```javascript
this.response.lastModified = new Date();
```
(15)this.response.etag
該屬性設置HTTP回應的ETag字段。
```javascript
this.response.etag = crypto.createHash('md5').update(this.body).digest('hex');
```
注意,不能用該屬性讀取ETag字段。
(16)this.response.vary(field)
該方法將參數添加到HTTP回應的Vary字段。
## CSRF攻擊
CSRF攻擊是指用戶的session被劫持,用來冒充用戶的攻擊。
koa-csrf插件用來防止CSRF攻擊。原理是在session之中寫入一個秘密的token,用戶每次使用POST方法提交數據的時候,必須含有這個token,否則就會拋出錯誤。
```javascript
var koa = require('koa');
var session = require('koa-session');
var csrf = require('koa-csrf');
var route = require('koa-route');
var app = module.exports = koa();
app.keys = ['session key', 'csrf example'];
app.use(session(app));
app.use(csrf());
app.use(route.get('/token', token));
app.use(route.post('/post', post));
function* token () {
this.body = this.csrf;
}
function* post() {
this.body = {ok: true};
}
app.listen(3000);
```
POST請求含有token,可以是以下幾種方式之一,koa-csrf插件就能獲得token。
- 表單的_csrf字段
- 查詢字符串的_csrf字段
- HTTP請求頭信息的x-csrf-token字段
- HTTP請求頭信息的x-xsrf-token字段
## 數據壓縮
koa-compress模塊可以實現數據壓縮。
```javascript
app.use(require('koa-compress')())
app.use(function* () {
this.type = 'text/plain'
this.body = fs.createReadStream('filename.txt')
})
```
## 源碼解讀
每一個網站就是一個app,它由`lib/application`定義。
```javascript
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
var app = Application.prototype;
exports = module.exports = Application;
```
`app.use()`用于注冊中間件,即將Generator函數放入中間件數組。
```javascript
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are allowed
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
```
`app.listen()`就是`http.createServer(app.callback()).listen(...)`的縮寫。
```javascript
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
app.callback = function(){
var mw = [respond].concat(this.middleware);
var fn = this.experimental
? compose_es7(mw)
: co.wrap(compose(mw));
var self = this;
if (!this.listeners('error').length) this.on('error', this.onerror);
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).catch(ctx.onerror);
}
};
```
上面代碼中,`app.callback()`會返回一個函數,用來處理HTTP請求。它的第一行`mw = [respond].concat(this.middleware)`,表示將respond函數(這也是一個Generator函數)放入`this.middleware`,現在mw就變成了`[respond, S1, S2, S3]`。
`compose(mw)`將中間件數組轉為一個層層調用的Generator函數。
```javascript
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
yield *next;
}
}
function *noop(){}
```
上面代碼中,下一個generator函數總是上一個Generator函數的參數,從而保證了層層調用。
`var fn = co.wrap(gen)`則是將Generator函數包裝成一個自動執行的函數,并且返回一個Promise。
```javascript
//co package
co.wrap = function (fn) {
return function () {
return co.call(this, fn.apply(this, arguments));
};
};
```
由于`co.wrap(compose(mw))`執行后,返回的是一個Promise,所以可以對其使用catch方法指定捕捉錯誤的回調函數`fn.call(ctx).catch(ctx.onerror)`。
將所有的上下文變量都放進context對象。
```javascript
app.createContext = function(req, res){
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, this.keys);
context.accept = request.accept = accepts(req);
context.state = {};
return context;
};
```
真正處理HTTP請求的是下面這個Generator函數。
```javascript
function *respond(next) {
yield *next;
// allow bypassing koa
if (false === this.respond) return;
var res = this.res;
if (res.headersSent || !this.writable) return;
var body = this.body;
var code = this.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
this.body = null;
return res.end();
}
if ('HEAD' == this.method) {
if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));
return res.end();
}
// status body
if (null == body) {
this.type = 'text';
body = this.message || String(code);
this.length = Buffer.byteLength(body);
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
this.length = Buffer.byteLength(body);
res.end(body);
}
```