#### 一個基礎的HTTP服務器
當我準備開始寫我的第一個“真正的”Node.js應用的時候,我不但不知道怎么寫Node.js代碼,也不知道怎么組織這些代碼。
我應該把所有東西都放進一個文件里嗎?網上有很多教程都會教你把所有的邏輯都放進一個用Node.js寫的基礎HTTP服務器里。但是如果我想加入更多的內容,同時還想保持代碼的可讀性呢?
實際上,只要把不同功能的代碼放入不同的模塊中,保持代碼分離還是相當簡單的。
這種方法允許你擁有一個干凈的主文件(main file),你可以用Node.js執行它;同時你可以擁有干凈的模塊,它們可以被主文件和其他的模塊調用。
那么,現在我們來創建一個用于啟動我們的應用的主文件,和一個保存著我們的HTTP服務器代碼的模塊。
在我的印象里,把主文件叫做index.js或多或少是個標準格式。把服務器模塊放進叫server.js的文件里則很好理解。
讓我們先從服務器模塊開始。在你的項目的根目錄下創建一個叫`server.js`的文件,并寫入以下代碼:
~~~
var http = require("http");
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);
~~~
搞定!你剛剛完成了一個可以工作的HTTP服務器。為了證明這一點,我們來運行并且測試這段代碼。首先,用Node.js執行你的腳本:
~~~
node server.js
~~~
接下來,打開瀏覽器訪問`http://localhost:8888/`,你會看到一個寫著`“Hello World”`的網頁。
這很有趣,不是嗎?讓我們先來談談HTTP服務器的問題,把如何組織項目的事情先放一邊吧,你覺得如何?我保證之后我們會解決那個問題的。
#### 分析HTTP服務器
那么接下來,讓我們分析一下這個HTTP服務器的構成。
第一行請求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。
接下來我們調用http模塊提供的函數: createServer 。這個函數會返回一個對象,這個對象有一個叫做 listen 的方法,這個方法有一個數值參數,指定這個HTTP服務器監聽的端口號。
咱們暫時先不管 http.createServer 的括號里的那個函數定義。
我們本來可以用這樣的代碼來啟動服務器并偵聽8888端口:
~~~
var http = require("http");
var server = http.createServer();
server.listen(8888);
~~~
這段代碼只會啟動一個偵聽8888端口的服務器,它不做任何別的事情,甚至連請求都不會應答。
最有趣(而且,如果你之前習慣使用一個更加保守的語言,比如PHP,它還很奇怪)的部分是 `createServer()` 的第一個參數,**一個函數定義**。
實際上,這個函數定義是 createServer() 的第一個也是唯一一個參數。因為在JavaScript中,函數和其他變量一樣都是可以被傳遞的。
#### 進行函數傳遞
舉例來說,你可以這樣做:
~~~
function say(word) {
console.log(word);
}
function execute(someFunction, value) {
someFunction(value);
}
execute(say, "Hello");
~~~
請仔細閱讀這段代碼!在這里,我們把 say 函數作為execute函數的第一個變量進行了傳遞。這里傳遞的不是 say 的返回值,而是 say 本身!
這樣一來, say 就變成了execute 中的本地變量 someFunction ,execute可以通過調用 someFunction() (帶括號的形式)來使用 say 函數。
當然,因為 say 有一個變量, execute 在調用 someFunction 時可以傳遞這樣一個變量。
我們可以,就像剛才那樣,用它的名字把一個函數作為變量傳遞。但是我們不一定要繞這個“先定義,再傳遞”的圈子,我們可以直接在另一個函數的括號中定義和傳遞這個函數:
~~~
function execute(someFunction, value) {
someFunction(value);
}
execute(function(word){ console.log(word) }, "Hello");
~~~
我們在 execute 接受第一個參數的地方直接定義了我們準備傳遞給 execute 的函數。
用這種方式,我們甚至不用給這個函數起名字,這也是為什么它被叫做 匿名函數 。
這是我們和我所認為的“進階”JavaScript的第一次親密接觸,不過我們還是得循序漸進。現在,我們先接受這一點:在JavaScript中,一個函數可以作為另一個函數接收一個參數。我們可以先定義一個函數,然后傳遞,也可以在傳遞參數的地方直接定義函數。
#### 函數傳遞是如何讓HTTP服務器工作的
帶著這些知識,我們再來看看我們簡約而不簡單的HTTP服務器:
~~~
var http = require("http");
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);
~~~
現在它看上去應該清晰了很多:我們向 createServer 函數傳遞了一個匿名函數。
用這樣的代碼也可以達到同樣的目的:
~~~
var http = require("http");
function onRequest(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
~~~
也許現在我們該問這個問題了:我們為什么要用這種方式呢?
#### 基于事件驅動的回調
這個問題可不好回答(至少對我來說),不過這是Node.js原生的工作方式。它是事件驅動的,這也是它為什么這么快的原因。
你也許會想花點時間讀一下`Felix Geisend?rfer`的大作`Understanding node.js`,它介紹了一些背景知識。
這一切都歸結于“Node.js是事件驅動的”這一事實。好吧,其實我也不是特別確切的了解這句話的意思。不過我會試著解釋,為什么它對我們用Node.js寫網絡應用(Web based application)是有意義的。
當我們使用 `http.createServer` 方法的時候,我們當然不只是想要一個偵聽某個端口的服務器,我們還想要它在服務器收到一個HTTP請求的時候做點什么。
問題是,這是異步的:請求任何時候都可能到達,但是我們的服務器卻跑在一個單進程中。
寫PHP應用的時候,我們一點也不為此擔心:任何時候當有請求進入的時候,網頁服務器(通常是Apache)就為這一請求新建一個進程,并且開始從頭到尾執行相應的PHP腳本。
那么在我們的`Node.js`程序中,當一個新的請求到達`8888`端口的時候,我們怎么控制流程呢?
嗯,這就是`Node.js/JavaScript`的事件驅動設計能夠真正幫上忙的地方了——雖然我們還得學一些新概念才能掌握它。讓我們來看看這些概念是怎么應用在我們的服務器代碼里的。
我們創建了服務器,并且向創建它的方法傳遞了一個函數。無論何時我們的服務器收到一個請求,這個函數就會被調用。
我們不知道這件事情什么時候會發生,但是我們現在有了一個處理請求的地方:它就是我們傳遞過去的那個函數。至于它是被預先定義的函數還是匿名函數,就無關緊要了。
這個就是傳說中的 回調 。我們給某個方法傳遞了一個函數,這個方法在有相應事件發生時調用這個函數來進行 回調 。
至少對我來說,需要一些功夫才能弄懂它。你如果還是不太確定的話就再去讀讀Felix的博客文章。
讓我們再來琢磨琢磨這個新概念。我們怎么證明,在創建完服務器之后,即使沒有HTTP請求進來、我們的回調函數也沒有被調用的情況下,我們的代碼還繼續有效呢?我們試試這個:
~~~
var http = require("http");
function onRequest(request, response) {
console.log("Request received.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
~~~
注意:在 onRequest (我們的回調函數)觸發的地方,我用 console.log 輸出了一段文本。在HTTP服務器開始工作之后,也輸出一段文本。
當我們與往常一樣,運行它node server.js時,它會馬上在命令行上輸出“Server has started.”。當我們向服務器發出請求(在瀏覽器訪問`http://localhost:8888/` ),“Request received.”這條消息就會在命令行中出現。
這就是事件驅動的異步服務器端JavaScript和它的回調啦!
(請注意,當我們在服務器訪問網頁時,我們的服務器可能會輸出兩次“Request received.”。那是因為大部分瀏覽器都會在你訪問 http://localhost:8888/ 時嘗試讀取 http://localhost:8888/favicon.ico )
#### 服務器是如何處理請求的
好的,接下來我們簡單分析一下我們服務器代碼中剩下的部分,也就是我們的回調函數 onRequest() 的主體部分。
當回調啟動,我們的 onRequest() 函數被觸發的時候,有兩個參數被傳入: request 和 response 。
它們是對象,你可以使用它們的方法來處理HTTP請求的細節,并且響應請求(比如向發出請求的瀏覽器發回一些東西)。
所以我們的代碼就是:當收到請求時,使用` response.writeHead() `函數發送一個HTTP狀態200和HTTP頭的內容類型(content-type),使用` response.write() `函數在HTTP相應主體中發送文本“Hello World"。
最后,我們調用 response.end() 完成響應。
目前來說,我們對請求的細節并不在意,所以我們沒有使用 request 對象。
#### 服務端的模塊放在哪里
OK,就像我保證過的那樣,我們現在可以回到我們如何組織應用這個問題上了。我們現在在 server.js 文件中有一個非常基礎的HTTP服務器代碼,而且我提到通常我們會有一個叫 index.js 的文件去調用應用的其他模塊(比如 server.js 中的HTTP服務器模塊)來引導和啟動應用。
我們現在就來談談怎么把server.js變成一個真正的Node.js模塊,使它可以被我們(還沒動工)的 index.js 主文件使用。
也許你已經注意到,我們已經在代碼中使用了模塊了。像這樣:
~~~
var http = require("http");
...
http.createServer(...);
~~~
Node.js中自帶了一個叫做“http”的模塊,我們在我們的代碼中請求它并把返回值賦給一個本地變量。
這把我們的本地變量變成了一個擁有所有 http 模塊所提供的公共方法的對象。
給這種本地變量起一個和模塊名稱一樣的名字是一種慣例,但是你也可以按照自己的喜好來:
~~~
var foo = require("http");
...
foo.createServer(...);
~~~
很好,怎么使用Node.js內部模塊已經很清楚了。我們怎么創建自己的模塊,又怎么使用它呢?
等我們把 server.js 變成一個真正的模塊,你就能搞明白了。
事實上,我們不用做太多的修改。把某段代碼變成模塊意味著我們需要把我們希望提供其功能的部分 導出 到請求這個模塊的腳本。
目前,我們的HTTP服務器需要導出的功能非常簡單,因為請求服務器模塊的腳本僅僅是需要啟動服務器而已。
我們把我們的服務器腳本放到一個叫做 start 的函數里,然后我們會導出這個函數。
~~~
var http = require("http");
function start() {
function onRequest(request, response) {
console.log("Request received.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
~~~
這樣,我們現在就可以創建我們的主文件 index.js 并在其中啟動我們的HTTP了,雖然服務器的代碼還在 server.js 中。
創建 index.js 文件并寫入以下內容:
~~~
var server = require("./server");
server.start();
~~~
正如你所看到的,我們可以像使用任何其他的內置模塊一樣使用server模塊:請求這個文件并把它指向一個變量,其中已導出的函數就可以被我們使用了。
好了。我們現在就可以從我們的主要腳本啟動我們的的應用了,而它還是老樣子:
~~~
node index.js
~~~
非常好,我們現在可以把我們的應用的不同部分放入不同的文件里,并且通過生成模塊的方式把它們連接到一起了。