文件夾
javascript與node.js
javascript與你
由於javascript真正意義上有兩種,甚至可以說是三種形態(從最先的做爲DHTML進行加強的小工具,到像jQuery那樣嚴格意義上的前端技術。再到現在的服務端技術),所以,比較難找到一個」正確「的方式來學習JavaScript,使得讓你書寫Node.js應用的時候感受本身是在真正開發它而不不過使用它。
簡短聲明
server端javascript
javascript最先是執行在瀏覽器中。然而瀏覽器僅僅是提供了一個上下文,它定義了使用javascript可以作什麼。但並無」說「太多關於JavaScript語言自己可以作什麼。其實,javascript是一門」完整「的語言:它可以使用在不一樣的上下文中。其能力與其餘同類語言相比有過之而無不及。
Node.js其實就是第二種上下文。它贊成在後端(脫離瀏覽器環境)執行JavaScript。
要實現在後臺運行javascript代碼。代碼需要先被解釋而後正確的運行。
Node.js的原理正是如此,它使用了Google的V8虛擬機(Google的Chrome瀏覽器使用的JavaScript運行環境)。來解釋和運行JavaScript代碼。javascript
除此以外,伴隨着Node.js的還有不少實用的模塊,它們可以簡化很是多反覆的勞做,比方向終端輸出字段串。
所以,Node.js其實既是一個執行時環境。同一時候又是一個庫。
要使用Node.js,首先需要進行安裝。
"hello world"
建立一個helloworld.js文件。
寫入console.log("Hello World");
保存該文件,經過Node.js來運行
node helloworld.js
終端輸出Hello World
一個完整的基於Node.js的web應用
用例
咱們來把目標設定簡單點,只是也要夠實際才行:
當用戶請求http://domain/start時,可以看到一個歡迎頁面,頁面上有一個文件上傳的表單。
用戶可以選擇一個圖片並提交表單,隨後文件將被上傳到http://domain/upload。該頁面完畢上傳後會把圖片顯示在頁面上。
更進一步地說,在完畢這一目標的過程當中。咱們不僅需要基礎的代碼而不管代碼是否優雅。
咱們還要對此進行抽象,來尋找一種適合構建更爲複雜的Node.js應用的方式。php
應用不一樣模塊分析
咱們來分解一下這個應用,爲了實現上文的用例。咱們需要實現哪些部分呢?
咱們需要提供Web頁面,所以需要一個HTTPserver
對於不一樣的請求,依據請求的URL。咱們的server需要給予不一樣的響應,所以咱們需要一個路由,用戶把請求相應到請求處理程序(request handler)
路由還應該能處理POST數據。並且把數據封裝成更友好的格式傳遞給請求處理入程序。所以需要請求數據處理功能
咱們不僅要處理URL相應的請求。還要把內容顯示出來。這意味着咱們需要一些視圖邏輯供請求處理程序使用。以便將內容發送給用戶的瀏覽器
最後。用戶需要上傳圖片,因此咱們需要上傳處理功能來處理這方面的細節
咱們先來想一想,使用PHP的話咱們會怎麼構建這個結構。通常來講咱們會用一個Apache HTTPserver並配上mod_php5模塊。
從這個角度看。整個「接收HTTP請求並提供Web頁面」的需求根本不需要PHP來處理。
只是對Node.js來講,概念全然不同了。使用Node.js時,咱們不僅在實現一個應用。同一時候還實現了整個HTTPserver。其實,咱們的Web應用以及相應的Webserver基本上是同樣的。
現在開始從HTTPserver着手。
構建應用的模塊
一個基礎的HTTPserver
討論把所有東西放進一個文件中嗎?爲了保持代碼的可讀性。把不一樣功能的代碼放入不一樣的模塊中,保持代碼分離。
這樣的方法贊成你擁有一個乾淨的主文件(main file)。你可以用Node.js運行它。同一時候你可以擁有乾淨的模塊,它們可以被主文件和其餘的模塊調用。
那麼。現在咱們來建立一個用於啓動咱們的應用的主文件,和一個保持着咱們HTTPserver代碼的模塊。
把主文件叫作index.js或多或少是個標準格式。
把服務器模塊放進叫server.js的文件中則很是好理解。html
讓咱們先從服務器模塊開始。在你的項目的根文件夾下建立一個叫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);
搞定!你剛剛完畢了一個可以工做的HTTPserver。爲了證實這一點,咱們來執行並且測試這段代碼。
首先。用Node.js執行你的腳本:前端
node server.js
接下來,打開瀏覽器訪問http://localhost:8888/,你會看到一個寫着"Hello World"的網頁。
這很是有趣。不是嗎?讓咱們先來談談HTTPserver的問題,把怎樣組織項目的事情先放一邊吧。你認爲怎樣?我保證以後咱們會解決那個問題的。
分析HTTPserver
那麼接下來,讓咱們分析一下這個HTTPserver的構成。
第一行請求(require)Node.js自帶的http模塊。並且把它賦值給http變量。
接下來咱們調用http模塊提供的函數:createServer。這個函數會返回一個對象,這個對象有一個叫作listen的方法,這種方法有一個數值參數,指定這個HTTPserver監聽的port號。
我們臨時先不管http.createServer的括號中的那個函數定義。
咱們原本可以用這種代碼來啓動server並偵聽8888port:
var http = require("http");
var server = http.createServer();
server.listen(8888);
這段代碼僅僅會啓動一個偵聽8888port的server。它不作不論什麼別的事情。甚至連請求都不會應答。
最有趣(而且,假設你以前習慣使用一個更加保守的語言。比方PHP,它還很是奇怪)的部分是createServer()的第一個參數,一個函數定義。
實際上,這個函數定義是createServer()的第一個也是惟一一個參數。
因爲在JavaScript中,函數和其它變量同樣都是可以被傳遞的。java
進行函數傳遞
舉例來講,你可以這樣作:
function say(word){
console.log(word);
}
function execute(someFunction, value){
someFunction(value);
}
execute(say, "Hello");
請細緻閱讀這段代碼!在這裏。咱們把say函數做爲execute函數的第一個變量進行了傳遞。
這裏返回的不是say的返回值。而是say自己!node
這樣一來,say就變成了execute中的本地變量someFunction,execute可以經過調用someFunction()(帶括號的形式)來使用say函數。
固然,因爲say有一個變量,execute在調用someFunction時可以傳遞這樣一個變量。
咱們可以,就像剛纔那樣,用它的名字把一個函數做爲變量傳遞。
但是咱們不必定要繞這個「先定義,再傳遞」的圈子,咱們可以直接在還有一個函數的括號裏定義和傳遞這個函數:git
function execute(someFunction, value){
someFunction(value);
}
execute(function(word){
console.log(word);
}, "Hello");
咱們在ececute接受第一個參數的地方直接定義了咱們準備傳遞給execute的函數。
用這樣的方式,咱們甚至不用給這個函數起名字,這也是爲何它被叫作匿名函數。
這是咱們和我所以爲的「進階」JavaScript的第一次親熱接觸,只是咱們仍是得按部就班。現在。咱們先接受這一點:在JavaScript中,一個函數可以做爲還有一個函數接收一個參數。
咱們可以先定義一個函數。而後傳遞,也可以在傳遞參數的地方直接定義函數。github
函數傳遞是怎樣讓HTTPserver工做的
帶着這些知識。咱們再來看看咱們簡約而不簡單的HTTPserver:
var http = require("http");
http.createServer(function(){
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原生的工做方式。它是事件驅動的,這也是它爲何這麼快的緣由。
這一切都歸結於"Node.js是事件驅動的"這一事實。好吧,事實上我也不是特別確切的瞭解這句話的意思。只是我會試着解釋,爲何它對咱們用Node.js寫網絡應用(Web based application)是有意義的。
當咱們使用http.createServer方法的時候,咱們固然不只僅是想要一個偵聽某個port的server,咱們還想要它在server收到一個HTTP請求的時候作點什麼。
問題是,這是異步的:請求不論何時均可能到達,但是咱們的server卻跑在一個單進程中。
寫PHP應用的時候,咱們一點也不爲此操心:不論何時當有請求進入的時候,網頁server(通常是Apache)就爲這一請求新建一個進程,並且開始從頭至尾運行對應的PHP腳本。
那麼在咱們的Node.js程序中。當一個新的請求到達8888port的時候,咱們怎麼控制流程呢?
嗯,這就是Node.js/JavaScript的事件驅動設計能夠真正幫上忙的地方了--儘管咱們還得學一些新概念才幹掌握它。讓咱們來看看這些概念是怎麼應用在咱們的server代碼裏的。
咱們建立了server,並且向建立它的方法傳遞了一個函數。
無論什麼時候咱們的server收到一個請求,這個函數就會被調用。web
咱們不知道這件事情何時會發生,但是咱們現在有了一個處理請求的地方:它就是咱們傳遞過去的那個函數。至於它是被預先定義的函數仍是匿名函數,就可有可無了。
這個就是傳說中的回調。咱們給某個方法傳遞了一個函數。這種方法在有對應事件發生時調用這個函數來進行回調。
讓咱們再來琢磨琢磨這個新概念。咱們怎麼證實。在建立完server以後。即便沒有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輸出了一段文本。在HTTPserver開始工做以後,也輸出一段文本。
當咱們與往常同樣。執行它node server.js時,它會當即在命令行上輸出"Server has started."。
當咱們向服務器發出請求(在瀏覽器訪問http://localhost:8888/),"Request received."這條消息就會在命令行中出現。shell
這就是事件驅動的異步server端JavaScript和它的回調啦!
(請注意,當咱們在server訪問網頁時。咱們的server可能會輸出兩次"Request received."。那是因爲大部分server都會在你訪問http://localhost:8888/時嘗試讀取http://localhost:8888/favicon.ico)
server是怎樣處理請求的
好的,接下來咱們簡單分析一下咱們server代碼中剩下的部分,也就是咱們的回調函數onRequest()的主體部分。
當回調啓動。咱們的onRequest()函數被觸發的時候,有兩個參數被傳入:
它們是對象。你可以使用它們的方法來處理HTTP請求的細節,並且響應請求(比方向發出請求的瀏覽器發回一些東西)。
因此咱們的代碼就是:當收到請求時,使用response.writeHead()函數發送一個HTTP狀態200和HTTP頭的內容類型(content-type),使用response.write()函數在HTTP對應主體中發送文本「Hello World」。
最後,咱們調用response.end()完畢響應。
眼下來講,咱們對請求的細節並不在乎。因此咱們沒有使用request對象。
服務端的模塊放在哪裏
咱們現在在server.js文件裏有一個很基礎的HTTP服務器代碼,而且我提到一般咱們會有一個叫index.js的文件去調用應用的其它模塊(比方server.js中的HTTP服務器模塊)來引導和啓動應用。
現在看看怎麼把server.js變成一個真正的Node.js模塊,使它可以被咱們(還沒動工)的index.js主文件使用。
var http = require("http");
...
http.createServer(...);
Node.js中自帶了一個叫作"http"的模塊,咱們在咱們的代碼中請求它並把返回值賦給一個本地變量。
這把咱們的本地變量變成了一個擁有所有http模塊所提供的公共方法的對象。
給這樣的本地變量起一個和模塊名稱同樣的名字是一種慣例,但是你也可以依照本身的喜愛來:
其實,咱們不用作太多的改動。把某段代碼變成模塊意味着咱們需要把咱們但願提供其功能的部分導出到請求這個模塊的腳本。
眼下,咱們的HTTPserver需要導出的功能很easy,因爲請求server模塊的腳本不過需要啓動server而已。
咱們把咱們的server腳本放到一個叫作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
咱們現在可以把咱們的應用的不一樣部分放入不一樣的文件中,並且經過生成模塊的方式把它們鏈接在一塊兒了。
咱們仍然僅僅擁有整個應用的最初部分:咱們可以接收HTTP請求。
但是咱們得作點什麼--對於不一樣的URL請求,server應該有不一樣的反應。
對於一個很easy的應用來講,你可以直接在回調函數onRequest()中作這件事情。只是就像我說過的,咱們應該增長一些抽象的元素,讓咱們的樣例變得更有趣一點兒。
處理不一樣的HTTP請求在咱們的代碼中是一個不一樣的部分,叫作「路由選擇」--那麼,咱們接下來就創造一個叫作路由的模塊。
怎樣來進行請求的"路由"
咱們要爲路由提供請求的URL和其餘需要的GET及POST參數。隨後路由需要依據這些數據來運行相應的代碼(這裏「代碼」相應整個應用的第三部分:一系列在接收到請求時真正工做的處理程序)。
咱們需要的所有數據都會包括在request對象中,該對象做爲onRequest()回調函數的第一個參數傳遞。但是爲了解析這些數據。咱們需要額外的Node.js模塊。它們各自是url和querystring模塊。
固然咱們也可以用querystring模塊來解析POST請求體中的參數,稍後會有演示。
現在咱們來給onRequest()函數加上一些邏輯。用來找出瀏覽器請求的URL路徑:
var http = require("http");
var url = require("url");
function start(){
function onRequest(request, response){
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " 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;
咱們的應用現在可以經過請求的URL路徑來差異不一樣請求了-這使咱們得以使用路由(還未完畢)來將請求以URL路徑爲基準映射處處理程序上。
在咱們所要構建的應用中,這意味着來自/start和/upload的請求可以使用不一樣的代碼來處理。稍後咱們將看到這些內容是怎樣整合到一塊兒的。
現在咱們可以來編寫路由,創建一個名爲router.js的文件,加入下面內容:
function route(pathname){
console.log("About to route a request for " + pathname);
}
exports.route = route;
這段代碼貌似什麼也沒幹,只是對於現在來講這是應該的。
在加入不少其它的邏輯曾經,咱們先來看看怎樣把路由和server整合起來。
咱們的server應當知道路由的存在並加以有效利用。
固然可以經過硬編碼的方式將這一依賴綁定到server上,但是其餘語言的編程經驗告訴咱們這會是一件很痛苦的事,所以咱們將使用依賴注入的方式較鬆散地加入路由模塊。
首先,咱們來擴展一下server的start()函數,以便將路由函數做爲參數傳遞過去:
var http = require("http");
var url = require("url");
function start(route){
function onRequest(request, response){
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(pathname);
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,使得路由函數可以被注入到server中:
var server = require("./server");
var router = require("./router");
server.start(router.route);
在這裏,咱們傳遞的函數依然什麼也沒作。
假設現在啓動應用(node index.js。始終記得這個命令行)。隨後請求一個URL,你將會看到應用輸出對應的信息。這代表咱們的HTTPserver已經在使用路由模塊了。並會將請求的路徑傳遞給路由:
node index.js
Request for /foo received.
About to route a request for /foo
以上輸出已經去掉了favicon.icon請求相關部分。
行爲驅動運行
在index文件裏,咱們可以將router對象傳遞進去。server隨後可以調用這個對象的route函數。
就像這樣,咱們傳遞一個東西,而後server利用這個東西來完畢一些事。
但是server事實上不需要這種東西。
它僅僅需要把事情作完便可,事實上爲了把事情作完,你根本不需要東西,你需要的是動做。
路由給真正的請求處理程序
路由,是指咱們要針對不一樣的URL有不一樣的處理方式。
好比處理/start的「業務邏輯」就應該和處理/upload的不一樣。
在現在的實現下,路由過程會在路由模塊中「結束」,並且路由模塊並不是真正針對請求「採取行動」的模塊,不然當咱們的應用程序變得更爲複雜時。將沒法很是好地擴展。
咱們臨時把做爲路由目標的函數稱爲請求處理程序。
現在咱們不要急着來開發路由模塊,因爲假設請求處理沒有就緒的話,再怎麼無缺路由模塊也沒有多大意義。
應用程序需要新的部件,咱們來建立一個叫作requestHandlers的模塊。並對於每一個請求處理程序。加入一個佔位用函數。隨後將這些函數做爲模塊的方法導出:
function start(){
console.log("Request handler 'start' was called.");
}
function upload(){
console.log("Request handler 'upload' was called.");
}
exports.start = start;
exports.upload = upload;
這樣咱們就可以把請求處理程序和路由模塊鏈接起來。讓路由「有路可尋」。
在這裏咱們得作個決定:是將requestHandlers模塊硬編碼到路由裏來使用。仍是再加入一點依賴注入?儘管和其餘模式同樣,依賴注入不該該只爲使用而使用,但在現在這個狀況下,使用依賴注入可以讓路由和請求處理程序之間的耦合更加鬆散,也所以能讓路由的重用性更高。
這意味着咱們得將請求處理程序從server傳遞到路由中,但感受上這麼作更離譜了。咱們得一路把這堆請求處理程序從咱們的主文件傳遞到server中。再將之從server傳遞到路由。
那麼咱們要怎麼傳遞這些請求處理程序呢?別看現在咱們僅僅有2個處理程序,在一個真實的應用中。請求處理程序的數量會不斷添加。咱們固然不想每次有一個新的URL或請求處理程序時,要爲了在路由裏完畢請求處處理程序的映射而重複折騰。除此以外。在路由裏有一大堆 if request == x then call handle y也使得系統醜陋不堪。
細緻想一想,有一大堆東西,每個都要映射到一個字符串(就是請求的URL)上?彷佛關聯數組(associative array)能完美勝任。
只是結果有點使人失望,JavaScript沒提供關聯數組--也可以說它提供了?其實,在JavaScript總,真正能提供此類功能的是它的對象。
在這方面。msdn提供了一段摘要:
在C++或C#中,當咱們談到對象,指的是類或者結構體的演示樣例。
對象依據他們實例化的模板(就是所謂的類)。會擁有不一樣的屬性的和方法。但在JavaScript裏對象不是這個概念。
在JavaScript中,對象就是一個鍵值對的集合,你可以把JavaScript的對象想象成一個鍵爲字符串類型的字典。
但假設JavaScript的對象不過鍵值對的集合,它又怎麼會擁有方法呢?
現在咱們已經肯定將一系列請求處理程序經過一個對象來傳遞,並且需要鬆耦合的方式將這個對象注入到route()函數中。
咱們先將這個對象引入到主文件index.js中:
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {};
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
server.start(router.route, handle);
儘管handle並不不過一個」東西「(一些請求處理程序的集合),我仍是建議以一個動詞做爲其命名。這樣作可以讓咱們在路由中使用更流暢的表達式,稍後會有說明。
正如所見。將不一樣的URL映射到一樣的請求處理程序上是很是easy的:僅僅要在對象中加入一個鍵爲"/"的屬性。相應requestHandlers.start就能夠。這樣咱們就可以乾淨簡潔地配置/start和/的請求都交由start這一處理程序處理。
在完畢了對象的定義後。咱們把它做爲額外的參數傳遞給服務器,爲此將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.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;
這樣咱們就在start()函數里加入了handle參數,並且把handle對象做爲第一個參數傳遞給了route()回調函數。
而後咱們對應地在route.js文件裏改動route()函數:
function route(handle, pathname) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname]();
} else {
console.log("No request handler found for " + pathname);
}
}
exports.route = route;
經過以上代碼。咱們首先檢查給定的路徑相應的請求處理程序是否存在,假設存在的話直接調用相應的函數。
咱們可以用從關聯數組中獲取元素同樣的方式從傳遞的對象中獲取請求處理函數,所以就有了簡潔流暢的形如handle[pathname]();的表達式。
有了這些,咱們就把server、路由和請求處理程序在一塊兒了。
現在咱們啓動應用程序並在瀏覽器中訪問http://localhost:8888/start,下面日誌可以說明系統調用了正確的請求處理程序:
Server has started.
Request for /start received.
About to route a request for /start
Request handler 'start' was called.
並且在瀏覽器中打開http://localhost:8888可以看到這個請求相同被start請求處理程序處理了:
Request for / received.
Aboute to route a request for /
Request handler 'start' was called.
讓請求處理程序做出響應
事實上處理請求就是對請求作出響應。所以,咱們需要讓請求處理程序能夠像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;
好的。相同的,請求路由需要將請求處理程序返回給它的信息返回給server。所以,咱們需要將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;
假設咱們執行重構後的應用。一切都會工做很是好
當有請求處理程序需要進行非堵塞的操做時候,咱們的應用就"掛"了。
堵塞與非堵塞
正如此前所提到的,當在請求處理程序中包含非堵塞操做時就會出問題。
但是,在說這以前。咱們先看看什麼是堵塞操做。
這裏,咱們來改動下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秒,實際場景中,這種堵塞操做有很是多。例如說一些長時間的計算操做等。)
如往常同樣,咱們先要從新啓動下server。爲了看到效果,咱們要進行一些相對複雜的操做:首先,打開兩個瀏覽器窗體或者標籤頁。在第一個瀏覽器窗體的地址欄中輸入http://localhost:8888/start,但是先不要打開它!
在第二個瀏覽器窗體的地址欄中輸入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(),等你處理完以後我會去調用該回調函數的。
接下來。咱們會介紹一種錯誤的使用非堵塞操做的方式。
和上次同樣,咱們經過改動咱們的應用來暴露問題。
此次咱們仍是拿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"),而後,當/startURL請求的時候將文件信息輸出到瀏覽器中。
上述代碼是很直觀的:建立了一個新的變量content(初始值爲"empty"),運行"ls -lah"命令,將結果賦值給content,最後將content返回。
和往常同樣,咱們啓動server,而後訪問"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就有這樣一種實現方案:函數傳遞。如下就讓咱們來詳細看看怎樣實現。
到眼下爲止,咱們的應用已經可以經過應用各層之間傳遞值的方式(請求處理程序->請求路由->server)將請求處理程序返回的內容(請求處理程序終於要顯示給用戶的內容)傳遞給HTTPserver。
現在咱們採用例如如下這樣的新的實現方式:相對採用將內容傳遞給server的方式,咱們此次採用將server"傳遞"給內容的方式。
從實踐角度來講,就是將response對象(從server的回調函數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/plian"});
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的時候,會花10秒鐘的時間才加載,而當請求http://localhost:8888/upload的矢耦,會立刻響應,縱然這個時候/start響應還在處理中。
更實用的場景
到眼下位置,咱們作的已經很是好了,但是,咱們的應用沒有實際用途。
server,請求路由以及請求處理程序都已經完畢了,如下讓咱們依照此前的用例給站點加入交互:用戶選擇一個文件,上傳該文件,而後在瀏覽器中看到上傳的文件。
爲了保持簡單。咱們若是用戶僅僅會上傳圖片。而後咱們應用將該圖片顯示到瀏覽器中。
好。如下就一步步來實現。鑑於此前已經對javascript原理性技術性的內容作過大量介紹了。此次咱們加快點速度。
要實現該功能,分爲例如如下兩步:首先。讓咱們來看看怎樣處理POST請求(非文件上傳),以後。咱們使用Node.js的一個用於文件上傳的外部模塊。之因此採用這樣的實現方式有兩個理由。
第一,雖然在Node.js中處理基礎的POST請求相對照較簡單,但在這過程當中仍是能學到很是多。
第二,用Node.js來處理文件上傳(multipart POST請求)是比較複雜的,它不在本書的範疇。但。怎樣使用外部模塊倒是在本書涉及內容以內。
處理POST請求
考慮這樣一個簡單的樣例:咱們顯示一個文本區(textarea)供用戶輸入內容,而後經過POST請求提交給server。最後,server接受到請求。經過處理程序將輸入的內容展現到瀏覽器中
/start請求處理程序用於生成帶文本區的表單。所以,咱們將requestHandlers.js改動爲例如如下形式:
function start(response) {
console.log("Request handler 'start' was called.");
var body = [
'<html>',
'<head>',
'<meta http-equiv=\'Content-Type\' content=\'text/html;\' charset=\'utf-8\' />',
'</head>',
'<body>',
'<form action=\'/upload\' method=\'post\'>',
'<textarea name=\'text\' rows=\'20\' cols=\'60\'></textarea>',
'<input type=\'submit\' value=\'Submit text\' />',
'</form>',
'</body>',
'</html>'
].join("");
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
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就可以看到簡單的表單,需要從新啓動server。
接下探討一個問題:當用戶提交表單時,觸發/upload請求處理程序處理POST請求的問題。
現在,採用異步回調來實現非堵塞地處理POST請求的數據。
這裏採用非堵塞方式處理是明智的,因爲POST請求通常都比較重--用戶可能會輸入大量的內容。
用堵塞的方式處理大數據量的請求一定會致使用戶操做的堵塞。
爲了使整個過程非堵塞,Node.js會將POST數據拆分紅很是多小的數據塊。而後經過觸發特定的事件。將這些小數據塊傳遞給回調函數。這裏的特定的事件有data事件(表示新的小數據塊到到達了)以及end事件(表示所有的數據都已經接收完成)。
咱們需要告訴Node.js當這些事件觸發的時候,回調哪些函數。咱們經過在request對象上註冊監聽器(listener)來實現。這裏的request對象是每次接收到HTTP請求時候。都會把該對象傳遞給onRequest回調函數。
例如如下所看到的:
request.addListener("data", function(chunk) {
//called when a new chunk of data was received
});
request.addListener("end", function() {
//called when all chunks of data have been received
});
問題來了。這部分邏輯寫在哪裏?咱們現在僅僅是在server中獲取到了request對象--咱們並無像以前response對象那樣。把request對象傳遞給請求路由和請求處理程序。
在我看來,獲取所有來自請求的數據,而後將這些數據給應用層處理,應該是HTTPserver要作的事情。
所以,我建議,咱們直接在server中處理POST數據,而後將終於的數據傳遞給請求路由和請求處理器,讓他們來進行進一步的處理。
所以,實現思路就是,將data和end事件的回調函數直接放在server中,在data事件回調中收集所有的POST數據,當接收到所有數據,觸發end事件後。其回調函數調用請求路由。並將數據傳遞給它。而後。請求路由再將該數據傳遞給請求處理程序。
先從server.js開始:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var postData = "";
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
request.setEncoding("utf8");
request.addListener("data", function(postDataChunk) {
postData += postDataChunk;
console.log("Received POST data chunk '" + postDataChunk + "'.");
});
request.addListener("end", function() {
route(handle, pathname, response, postData);
});
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
exports.start = start;
}
上述代碼作了三件事情:首先。咱們設置了接收數據的編碼格式爲utf-8。而後註冊了"data"事件的監聽器,用於收集每次接收到的新數據塊,並將其賦值給postData變量,最後,咱們將請求路由的調用移動end事件處理程序中,以確保它僅僅會當所有數據接收完成後才觸發。並且僅僅觸發一次。咱們同一時候還把POST數據傳遞給請求路由,因爲這些數據,請求處理程序會用到。
上述代碼在每個數據塊到達的時候輸出了日誌。這對於終於環境來講,是很是很差的(數據量可能會很是大)。但是。在開發階段是很是實用的。有助於讓咱們看到發生了什麼。
我建議可以嘗試下,嘗試着去輸入一小段文本,以及大段內容,當大段內容的時候,就會發現data事件會觸發屢次。
接下來在/upload頁面,展現用戶輸入的內容。要實現該功能,咱們需要將postData傳遞給請求處理程序,改動router.js爲例如如下形式:
function route(handle, pathname, response, postData) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response, postData);
} 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;
而後。在requestHandlers.js中,咱們將數據包括在對upload請求的響應中:
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = [
'<html>',
'<head>',
'<meta http-equiv=\'Content-Type\' content=\'text/html;\' charset=\'utf-8\' />',
'</head>',
'<body>',
'<form action=\'/upload\' method=\'post\'>',
'<textarea name=\'text\' rows=\'20\' cols=\'60\'></textarea>',
'<input type=\'submit\' value=\'Submit text\' />',
'</form>',
'</body>',
'</html>'
].join("");
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent: " + postData);
response.end();
}
exports.start = start;
exports.upload = upload;
咱們現在可以接收POST數據並在請求處理程序中處理該數據。
最後,當前咱們是把請求的整個消息體傳遞給了請求路由和請求處理程序。咱們應該僅僅把POST數據中。咱們感興趣的部分傳遞給請求路由和請求處理程序。在咱們這個樣例中,咱們感興趣的事實上僅僅是text字段。
咱們可以使用此前介紹過querystring模塊來實現requestHandlers.js:
var querystring = require("querystring");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = [
'<html>',
'<head>',
'<meta http-equiv=\'Content-Type\' content=\'text/html;\' charset=\'utf-8\' />',
'</head>',
'<body>',
'<form action=\'/upload\' method=\'post\'>',
'<textarea name=\'text\' rows=\'20\' cols=\'60\'></textarea>',
'<input type=\'submit\' value=\'Submit text\' />',
'</form>',
'</body>',
'</html>'
].join("");
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: " + querystring.parse(postData).text);
response.end();
}
exports.start = start;
exports.upload = upload;
以上就是關於處理POST數據的全部內容。
處理文件上傳
最後,咱們來實現終於的用例:贊成用戶上傳圖片,並將該圖片在瀏覽器中顯示出來。
回到90年代,這個用例全然可以知足用於IPO的商業模型。如今。經過它可以接到兩個內容:怎樣安裝外部Node.js模塊。以及怎樣將它們應用到咱們應用中。
這裏咱們要用到的外部模塊是Felix Geisendorfer開發的node-formidable模塊。它對解析上傳的文件數據作了很是好的抽象。
處理文件上傳"就是"處理POST數據--但是,麻煩的是在詳細的處理細節,因此,這裏採用現成的方案更合適點。
使用該模塊。首先需要安裝該模塊。
Node.js有它本身的包管理器。叫NPM。它可以讓安裝Node.js的外部模塊變得很方便。經過例如如下一條命令就可以完畢該模塊的安裝:
npm install formidable
假設安裝出現錯誤。運行
再又一次npm install formidable
假設終端輸出例如如下內容:
npm ok
現在咱們就可以用formidable模塊了--使用外部模塊與內部模塊相似,用require語句將其引入就能夠:
var formidable = require("formidable");
這裏該模塊作的就是將經過HTTP POST請求提交的表單,在Node.js中可以被解析。
咱們要作的就是建立一個新的IncomingForm,它是對提交表單的抽象表示。以後。就可以用它解析request對象,獲取表單中需要的數據字段。
node-formidable官方的樣例展現了這兩部分是怎樣融合在一塊兒工做的:
server.js
var formidable = require('formidable'),
http = require('http'),
sys = require('sys');
http.createServer(function(req, res){
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
res.writeHead(200, {'content-type': 'text/plain'});
res.write('received upload:\n\n');
res.end(sys.inspect({fields:fields, files: files}));
});
return;
}
res.writeHead(200, {'content-type': 'text/html'});
res.end(
[
'<form action="/upload" enctype="multipart/form-data" method="post">',
'<input type="text" name="title" /><br />',
'<input type="file" name="upload" multiple="multiple" /><br />',
'<input type="submit" value="upload" />',
'</form>'
].join('')
);
}).listen(8888);
假設咱們將上述代碼,保存到一個文件裏,並經過node來運行,就可以進行簡單的表單提交了,包含文件上傳。而後。可以看到經過調用form.parse傳遞給回調函數的files對象的內容。例如如下所看到的:
received upload:
{
fields: {title: 'Hello World'},
files: {
upload: {
size: 1558,
path: '/tmp/1c747974a27a6292743669e91f29350b',
name: 'us-flag.png',
type: 'image/png',
lastModifiedDate: Tue, 21 Jun 2011 07:02:41 GMT,
_writeStream: [Object],
length: [Getter],
filename: [Getter],
mime: [Getter]
}
}
}
爲了實現咱們的功能。咱們需要將上述代碼應用到咱們的應用中,另外。咱們還要考慮怎樣將上傳文件的內容(保存在/tmp文件夾中)顯示到瀏覽器中。
咱們先來解決後面那個問題:對於保存在本地硬盤的文件,怎樣才幹在瀏覽器中看到呢?
顯然。咱們需要將該文件讀取到咱們的server中。使用一個叫fs的模塊。
咱們來加入/showURL的請求處理程序,該處理程序直接硬編碼將文件/tmp/test.png內容展現到瀏覽器中。固然了。首先需要將該圖片保存到這個位置才行。
將requestHandlers.js改動爲例如如下形式:
var querystring = require("querystring"),
fs = require("fs");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = [
'<html>',
'<head>',
'<meta http-equiv=\'Content-Type\' content=\'text/html;\' charset=\'utf-8\' />',
'</head>',
'<body>',
'<form action=\'/upload\' method=\'post\'>',
'<textarea name=\'text\' rows=\'20\' cols=\'60\'></textarea>',
'<input type=\'submit\' value=\'Submit text\' />',
'</form>',
'</body>',
'</html>'
].join("");
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: " + querystring.parse(postData).text);
response.end();
}
function show(response, postData) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if (error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "\n");
response.end();
} else {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
}
});
}
exports.start = start;
exports.upload = upload;
exports.show = show;
咱們還需要將這新的請求處理程序,加入到index.js中的路由映射表中:
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {};
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
handle["/show"] = requestHandlers.show;
server.start(router.route, handle);
從新啓動server以後。經過訪問http://localhost:8888/show,就可以看到保存在/tmp/test.png的圖片了。
最後咱們需要作的是:
在/start表單中加入一個文件上傳元素
將node-formidable整合到咱們的upload請求處理程序中。用於將上傳的圖片保存到/tmp/test.png
將上傳的圖片內嵌到/uploadURL輸出的HTML中
第一項很是easy。僅僅需要在HTML表單中,加入一個multipart/form-data的編碼類型。移除此前的文本區。加入一個文件上傳組件,並將提交button的文案改成"Upload file"就能夠。
例如如下requestHandler.js所看到的:
var querystring = require("querystring"),
fs = require("fs");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = [
'<html>',
'<head>',
'<meta http-equiv=\'Content-Type\' content=\'text/html;\' charset=\'utf-8\' />',
'</head>',
'<body>',
'<form action=\'/upload\' enctype="multipart/form-data" method=\'post\'>',
'<input type="file" name="upload" />',
'<input type=\'submit\' value=\'Upload file\' />',
'</form>',
'</body>',
'</html>'
].join("");
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: " + querystring.parse(postData).text);
response.end();
}
function show(response, postData) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if (error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "\n");
response.end();
} else {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
}
});
}
exports.start = start;
exports.upload = upload;
exports.show = show;
很是好。下一步相對照較複雜。這裏有這樣一個問題:咱們需要在upload處理程序中對上傳的文件進行處理,這種花。咱們就需要將request對象傳遞給node-formidable的form.parse函數。
但是,咱們有的僅僅是response對象和postData數組。
看樣子,咱們僅僅能不得不將request對象從server開始一路經過請求路由,再傳遞給請求處理程序。也許還有更好的方案,但是。不管怎麼說,眼下這樣作可以知足咱們的需求。
到這裏,咱們可以將postData從server以及請求處理程序中移除了一方面。對於咱們處理文件上傳來講已經不需要了,另一方面,它甚至可能會引起這樣一個問題:咱們已經「消耗」了request對象中的數據。這意味着,對於form.parse來講,當它想要獲取數據的時候就什麼也獲取不到了。(因爲Node.js不會對數據作緩存)
咱們從server.js開始:移除對postData的處理以及request.setEncoding(這部分node-formidable自身會處理),轉而採用將request對象傳遞給請求路由的方式:
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, request);
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
接下來是router.js咱們再也不需要傳遞postData了,此次要傳遞request對象:
function route(handle, pathname, response, request) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response, request);
} else {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/html"});
response.write("404 Not found");
response.end();
}
}
exports.route = route;
現在,request對象就可以在咱們的upload請求處理程序中使用了。
node-formidable會處理將上傳的文件保存到本地/tmp文件夾中。而咱們需要作的是確保該文件保存成/tmp/test.png。沒錯,咱們保持簡單,並若是僅僅贊成上傳PNG圖片。
這裏採用fs.renameSync(path1, path2)來實現。要注意的是,正如其名,該方法是同步運行的,也就是說。假設該重命名的操做很是耗時的話會堵塞。這塊咱們先不考慮。
接下來。咱們把處理文件上傳以及重命名的操做放到一塊兒,例如如下requestHandlers.js所看到的:
var querystring = require("querystring"),
fs = require("fs"),
formidable = require("formidable");
function start(response) {
console.log("Request handler 'start' was called.");
var body = [
'<html>',
'<head>',
'<meta http-equiv=\'Content-Type\' content=\'text/html;\' charset=\'utf-8\' />',
'</head>',
'<body>',
'<form action=\'/upload\' enctype="multipart/form-data" method=\'post\'>',
'<input type="file" name="upload" multiple="multiple" />',
'<input type=\'submit\' value=\'Upload file\' />',
'</form>',
'</body>',
'</html>'
].join("");
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, request) {
console.log("Request handler 'upload' was called.");
var form = new formidable.IncomingForm();
console.log("about to parse");
form.parse(request, function(error, fields, files) {
console.log("parsing done");
fs.renameSync(files.upload.path, "/tmp/test.png");
response.writeHead(200, {"Content-Type": "text/html"});
response.write("received image:<br />");
response.write("<img src='/show' />");
response.end();
});
}
function show(response, postData) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if (error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "\n");
response.end();
} else {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
}
});
}
exports.start = start;
exports.upload = upload;
exports.show = show;
好了,從新啓動server,咱們應用所有的功能就可以用了。選擇一張本地圖片,將其上傳到server,而後瀏覽器就會顯示該圖片。
總結與展望
咱們開發完了一個Node.js的web應用,應用雖小。但卻"五臟俱全"。
期間,咱們介紹了很是多技術點:服務端JavaScript、函數式編程、堵塞與非堵塞、回調、事件、內部和外部模塊等待。
還有本書沒有介紹到的,怎樣操做數據庫、怎樣進行單元測試、怎樣開發Node.js的外部模塊以及一些簡單的諸如怎樣獲取GET請求之類的方法。
有什麼問題可以向社區尋求解答。
參考地址:https://github.com/joyent/node/wiki