很好。不過現在要是請求處理程序能夠向瀏覽器返回一些有意義的信息而并非全是“Hello World”,那就更好了。
這里要記住的是,瀏覽器發出請求后獲得并顯示的“Hello World”信息仍是來自于我們_server.js_文件中的_onRequest_函數。
其實“處理請求”說白了就是“對請求作出響應”,因此,我們需要讓請求處理程序能夠像_onRequest_函數那樣可以和瀏覽器進行“對話”。
## 不好的實現方式
對于我們這樣擁有PHP或者Ruby技術背景的開發者來說,最直截了當的實現方式事實上并不是非常靠譜: 看似有效,實則未必如此。
這里我指的“直截了當的實現方式”意思是:讓請求處理程序通過_onRequest_函數直接返回(_return()_)他們要展示給用戶的信息。
我們先就這樣去實現,然后再來看為什么這不是一種很好的實現方式。
讓我們從讓請求處理程序返回需要在瀏覽器中顯示的信息開始。我們需要將_requestHandler.js_修改為如下形式:
~~~
function start() {
console.log("Request handler 'start' was called.");?
return "Hello Start";
}
function upload() {
console.log("Request handler 'upload' was called.");?
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
~~~
好的。同樣的,請求路由需要將請求處理程序返回給它的信息返回給服務器。因此,我們需要將_router.js_修改為如下形式:
~~~
function route(handle, pathname) {
? console.log("About to route a request for " + pathname);?
if (typeof handle[pathname] === 'function') {? ?
return handle[pathname]();? }
else {
? ? console.log("No request handler found for " + pathname);? ?
return "404 Not found";?
}
}
exports.route = route;
~~~
正如上述代碼所示,當請求無法路由的時候,我們也返回了一些相關的錯誤信息。
最后,我們需要對我們的_server.js_進行重構以使得它能夠將請求處理程序通過請求路由返回的內容響應給瀏覽器,如下所示:
~~~
var http = require("http");
var url = require("url");
function start(route, handle) {?
function onRequest(request, response) {? ?
var pathname = url.parse(request.url).pathname;
? ? console.log("Request for " + pathname + " received.");
? ? response.writeHead(200, {"Content-Type": "text/plain"});? ?
var content = route(handle, pathname)
? ? response.write(content);
? ? response.end();?
}
? http.createServer(onRequest).listen(8888);
? console.log("Server has started.");
}
exports.start = start;
~~~
如果我們運行重構后的應用,一切都會工作的很好:請求[http://localhost:8888/start](http://localhost:8888/start),瀏覽器會輸出“Hello Start”,請求[http://localhost:8888/upload](http://localhost:8888/upload)會輸出“Hello Upload”,而請求[http://localhost:8888/foo](http://localhost:8888/foo)?會輸出“404 Not found”。
好,那么問題在哪里呢?簡單的說就是: 當未來有請求處理程序需要進行非阻塞的操作的時候,我們的應用就“掛”了。
沒理解?沒關系,下面就來詳細解釋下。
## 阻塞與非阻塞
正如此前所提到的,當在請求處理程序中包括非阻塞操作時就會出問題。但是,在說這之前,我們先來看看什么是阻塞操作。
我不想去解釋“阻塞”和“非阻塞”的具體含義,我們直接來看,當在請求處理程序中加入阻塞操作時會發生什么。
這里,我們來修改下_start_請求處理程序,我們讓它等待10秒以后再返回“Hello Start”。因為,JavaScript中沒有類似_sleep()_這樣的操作,所以這里只能夠來點小Hack來模擬實現。
讓我們將_requestHandlers.js_修改成如下形式:
~~~
function start() {
? console.log("Request handler 'start' was called.");?
function sleep(milliSeconds) {? ?
var startTime = new Date().getTime();? ?
while (new Date().getTime() startTime + milliSeconds);?
}
sleep(10000);?
return "Hello Start";
}
function upload() {
console.log("Request handler 'upload' was called.");?
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
~~~
上述代碼中,當函數_start()_被調用的時候,Node.js會先等待10秒,之后才會返回“Hello Start”。當調用_upload()_的時候,會和此前一樣立即返回。
(當然了,這里只是模擬休眠10秒,實際場景中,這樣的阻塞操作有很多,比方說一些長時間的計算操作等。)
接下來就讓我們來看看,我們的改動帶來了哪些變化。
如往常一樣,我們先要重啟下服務器。為了看到效果,我們要進行一些相對復雜的操作(跟著我一起做): 首先,打開兩個瀏覽器窗口或者標簽頁。在第一個瀏覽器窗口的地址欄中輸入[http://localhost:8888/start](http://localhost:8888/start), 但是先不要打開它!
在第二個瀏覽器窗口的地址欄中輸入[http://localhost:8888/upload](http://localhost:8888/upload), 同樣的,先不要打開它!
接下來,做如下操作:在第一個窗口中(“/start”)按下回車,然后快速切換到第二個窗口中(“/upload”)按下回車。
注意,發生了什么: /start URL加載花了10秒,這和我們預期的一樣。但是,/upload URL居然_也_花了10秒,而它在對應的請求處理程序中并沒有類似于_sleep()_這樣的操作!
這到底是為什么呢?原因就是_start()_包含了阻塞操作。形象的說就是“它阻塞了所有其他的處理工作”。
這顯然是個問題,因為Node一向是這樣來標榜自己的:_“在node中除了代碼,所有一切都是并行執行的”_。
這句話的意思是說,Node.js可以在不新增額外線程的情況下,依然可以對任務進行并行處理 —— Node.js是單線程的。它通過事件輪詢(event loop)來實現并行操作,對此,我們應該要充分利用這一點 —— 盡可能的避免阻塞操作,取而代之,多使用非阻塞操作。
然而,要用非阻塞操作,我們需要使用回調,通過將函數作為參數傳遞給其他需要花時間做處理的函數(比方說,休眠10秒,或者查詢數據庫,又或者是進行大量的計算)。
對于Node.js來說,它是這樣處理的:_“嘿,probablyExpensiveFunction()(譯者注:這里指的就是需要花時間處理的函數),你繼續處理你的事情,我(Node.js線程)先不等你了,我繼續去處理你后面的代碼,請你提供一個callbackFunction(),等你處理完之后我會去調用該回調函數的,謝謝!”_
(如果想要了解更多關于事件輪詢細節,可以閱讀Mixu的博文——[理解node.js的事件輪詢](http://blog.mixu.net/2011/02/01/understanding-the-node-js-event-loop/)。)
接下來,我們會介紹一種錯誤的使用非阻塞操作的方式。
和上次一樣,我們通過修改我們的應用來暴露問題。
這次我們還是拿_start_請求處理程序來“開刀”。將其修改成如下形式:
~~~
var exec = require("child_process").exec;
function start() {
? console.log("Request handler 'start' was called.");?
var content = "empty";
exec("ls -lah",
function (error, stdout, stderr) {? ?
content = stdout;? });?
return content;
}
function upload() {
console.log("Request handler 'upload' was called.");?
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
~~~
上述代碼中,我們引入了一個新的Node.js模塊,_child_process_。之所以用它,是為了實現一個既簡單又實用的非阻塞操作:_exec()_。
_exec()_做了什么呢?它從Node.js來執行一個shell命令。在上述例子中,我們用它來獲取當前目錄下所有的文件(“ls -lah”),然后,當_/start_URL請求的時候將文件信息輸出到瀏覽器中。
上述代碼是非常直觀的: 創建了一個新的變量_content_(初始值為“empty”),執行“ls -lah”命令,將結果賦值給content,最后將content返回。
和往常一樣,我們啟動服務器,然后訪問“[http://localhost:8888/start](http://localhost:8888/start)” 。
之后會載入一個漂亮的web頁面,其內容為“empty”。怎么回事?
這個時候,你可能大致已經猜到了,_exec()_在非阻塞這塊發揮了神奇的功效。它其實是個很好的東西,有了它,我們可以執行非常耗時的shell操作而無需迫使我們的應用停下來等待該操作。
(如果想要證明這一點,可以將“ls -lah”換成比如“find /”這樣更耗時的操作來效果)。
然而,針對瀏覽器顯示的結果來看,我們并不滿意我們的非阻塞操作,對吧?
好,接下來,我們來修正這個問題。在這過程中,讓我們先來看看為什么當前的這種方式不起作用。
問題就在于,為了進行非阻塞工作,_exec()_使用了回調函數。
在我們的例子中,該回調函數就是作為第二個參數傳遞給_exec()_的匿名函數:
~~~
function (error, stdout, stderr) {?
content = stdout;
}
~~~
現在就到了問題根源所在了:我們的代碼是同步執行的,這就意味著在調用_exec()_之后,Node.js會立即執行?_return content_?;在這個時候,_content_仍然是“empty”,因為傳遞給_exec()_的回調函數還未執行到——因為_exec()_的操作是異步的。
我們這里“ls -lah”的操作其實是非常快的(除非當前目錄下有上百萬個文件)。這也是為什么回調函數也會很快的執行到 —— 不過,不管怎么說它還是異步的。
為了讓效果更加明顯,我們想象一個更耗時的命令: “find /”,它在我機器上需要執行1分鐘左右的時間,然而,盡管在請求處理程序中,我把“ls -lah”換成“find /”,當打開/start URL的時候,依然能夠立即獲得HTTP響應 —— 很明顯,當_exec()_在后臺執行的時候,Node.js自身會繼續執行后面的代碼。并且我們這里假設傳遞給_exec()_的回調函數,只會在“find /”命令執行完成之后才會被調用。
那究竟我們要如何才能實現將當前目錄下的文件列表顯示給用戶呢?
好,了解了這種不好的實現方式之后,我們接下來來介紹如何以正確的方式讓請求處理程序對瀏覽器請求作出響應。
## 以非阻塞操作進行請求響應
我剛剛提到了這樣一個短語 —— “正確的方式”。而事實上通常“正確的方式”一般都不簡單。
不過,用Node.js就有這樣一種實現方案: 函數傳遞。下面就讓我們來具體看看如何實現。
到目前為止,我們的應用已經可以通過應用各層之間傳遞值的方式(請求處理程序 -> 請求路由 -> 服務器)將請求處理程序返回的內容(請求處理程序最終要顯示給用戶的內容)傳遞給HTTP服務器。
現在我們采用如下這種新的實現方式:相對采用將內容傳遞給服務器的方式,我們這次采用將服務器“傳遞”給內容的方式。 從實踐角度來說,就是將_response_對象(從服務器的回調函數_onRequest()_獲取)通過請求路由傳遞給請求處理程序。 隨后,處理程序就可以采用該對象上的函數來對請求作出響應。
原理就是如此,接下來讓我們來一步步實現這種方案。
先從_server.js_開始:
~~~
var http = require("http");
var url = require("url");
function start(route, handle) {?
function onRequest(request, response) {? ?
var pathname = url.parse(request.url).pathname;
? ? console.log("Request for " + pathname + " received.");
? ? route(handle, pathname, response);?
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
~~~
相對此前從_route()_函數獲取返回值的做法,這次我們將response對象作為第三個參數傳遞給_route()_函數,并且,我們將_onRequest()_處理程序中所有有關_response_的函數調都移除,因為我們希望這部分工作讓_route()_函數來完成。
下面就來看看我們的_router.js_:
~~~
function route(handle, pathname, response) {
? console.log("About to route a request for " + pathname);?
if (typeof handle[pathname] === 'function') {
? ? handle[pathname](response);? }
else {
? ? console.log("No request handler found for " + pathname);
? ? response.writeHead(404, {"Content-Type": "text/plain"});
? ? response.write("404 Not found");
? ? response.end();?
}
}
exports.route = route;
~~~
同樣的模式:相對此前從請求處理程序中獲取返回值,這次取而代之的是直接傳遞_response_對象。
如果沒有對應的請求處理器處理,我們就直接返回“404”錯誤。
最后,我們將_requestHandler.js_修改為如下形式:
~~~
var exec = require("child_process").exec;
function start(response) {
? console.log("Request handler 'start' was called.");
? exec("ls -lah", function (error, stdout, stderr) {
? ? response.writeHead(200, {"Content-Type": "text/plain"});
? ? response.write(stdout);
? ? response.end();? });
}
function upload(response) {
? console.log("Request handler 'upload' was called.");
? response.writeHead(200, {"Content-Type": "text/plain"});
? response.write("Hello Upload");
? response.end();
}
exports.start = start;
exports.upload = upload;
~~~
我們的處理程序函數需要接收response參數,為了對請求作出直接的響應。
_start_處理程序在_exec()_的匿名回調函數中做請求響應的操作,而_upload_處理程序仍然是簡單的回復“Hello World”,只是這次是使用_response_對象而已。
這時再次我們啟動應用(_node index.js_),一切都會工作的很好。
如果想要證明_/start_處理程序中耗時的操作不會阻塞對_/upload_請求作出立即響應的話,可以將_requestHandlers.js_修改為如下形式:
~~~
var exec = require("child_process").exec;
function start(response) {
? console.log("Request handler 'start' was called.");
? exec("find /",? ? { timeout: 10000, maxBuffer: 20000*1024 },? ? function (error, stdout, stderr) {
? ? ? response.writeHead(200, {"Content-Type": "text/plain"});
? ? ? response.write(stdout);
? ? ? response.end();? ? });
}
function upload(response) {
? console.log("Request handler 'upload' was called.");
? response.writeHead(200, {"Content-Type": "text/plain"});
? response.write("Hello Upload");
? response.end();
}
exports.start = start;
exports.upload = upload;
~~~
這樣一來,當請求[http://localhost:8888/start](http://localhost:8888/start)的時候,會花10秒鐘的時間才載入,而當請求[http://localhost:8888/upload](http://localhost:8888/upload)的時候,會立即響應,縱然這個時候/start響應還在處理中。