目標
1. 在瀏覽器地址欄輸入“http://demos/start”,進入歡迎頁面,頁面有一個文件上傳表單;
2. 選擇一張圖片並提交表單,文件被上傳到"http://demos/uploads"上傳完成把該圖片顯示在頁面上。
功能模塊分解
1. 需要提供歡迎頁,所以需要一個http服務器;
2. 對於不同請求,根據url,服務器能給與不同響應,需要路由,把請求對應到相應的請求處理程序(request handler)
3. 需要請求處理程序;
4. 路由處理post數據,並把數據封裝成更加友好的格式傳遞給處理程序,需要數據處理功能;
5. 需要視圖邏輯供請求處理程序使用,以便將結果返回給瀏覽器;
6. 需要上傳處理功能處理上傳細節。
如果是php實現,我們需要apache http服務器並配置mod_php5模塊,那么整個“接收請求並回復“並不需要php來做。而使用nodejs,不僅僅是實現應用,同時還實現了http服務器。
一個基礎的http服務器
nodejs支持模塊化,有利於編寫干凈的代碼。除了內置的模塊,我們可以將自己的不同功能的代碼放進不同模塊中,說白了就是寫在不同文件中。
創建js文件取名server.js,寫入代碼如下:
1 var http = require('http'); 2 3 http.createServer(function(req,res){ 4 res.writeHead(200,{'Content-Type':'text/html'}); 5 res.write('<h1>Node.js</h1>'); 6 res.end(""); 7 }).listen(3000,'127.0.0.1');
node server.js
瀏覽器輸入"http://localhost:3000",顯示”Node.js“。
服務器簡單分析
第一行,require請求nodejs內置的http模塊,賦給http變量;接着調用http模塊的createServer方法,返回一個對象,這個對象有一個叫做listen的方法,這個方法的參數指定http服務器監聽的端口號。createServer方法的匿名函數是該方法僅僅需要的一個參數,js中函數和變量一樣可以傳遞。
使用這種方式是因為nodejs是事件驅動的。
當我們使用php時,任何時候每當有請求進入,apache就為這一新請求新建一個進程,並從頭到尾的執行php腳本。htpp.createServer不僅僅是建一個偵聽某端口的服務器,我們還想要他在收到http請求時做點什么。而nodejs是異步的:請求任何時候都可能到達,但是他卻跑在一個單進程中。
當一個請求到達3000端口時怎么控制流程呢?這時候nodejs/javascript的事件驅動機制派上用場了。
我們向創建服務器的方法傳遞了一個函數,任何時候服務器收到請求,這個函數就會被調用,被用作處理請求的的地方。
服務器如何處理請求
當服務器加收請求回調的匿名函數被觸發的時候,有兩參數傳入:request和response。利用這兩個對象的方法處理請求的細節,並返回應答。
使用response.writeHead()發送一個http狀態200和http頭的內容類型等,使用response.write()在響應主體中發送文本,而使用response.end()完成應答。
封裝為模塊:
1 var http = require('http'); 2 3 function serverStart(){ 4 http.createServer(function(req,res){ 5 console.log('http request recieve'); 6 7 res.writeHead(200,{'Content-Type':'text/html'}); 8 res.write('<h1>Node.js</h1>'); 9 res.end(""); 10 }).listen(3000,'127.0.0.1'); 11 12 console.log('http server start on port 3000'); 13 } 14 15 exports.serverStart = serverStart;
第一行http是內置的模塊,這里封裝了我們自己的模塊,通過關鍵字exports露出接口方法,模塊名為該文件名server,之后”server模塊“可以被其他模塊引用。
創建index.js文件,寫入代碼並引用server模塊:
1 var server = require("./server"); 2 3 server.serverStart();
對請求進行路由選擇
為路由提供請求的url和其他GET和POST參數,路由根據這些數據來分發請求到具體的處理程序。因此,我們需要查看並解析http請求,提取出url和get/post參數,暫且將這一解析功能歸於http服務器一部分。請求數據都包含在request對象中,引入內置模塊url和querystring來解析它。
1 var http = require('http'); 2 var url = require('url'); 3 4 function serverStart(){ 5 http.createServer(function(req,res){ 6 var pathname = url.parse(req.url).pathname; 7 console.log('http request for '+pathname+' recieved'); 8 9 res.writeHead(200,{'Content-Type':'text/html'}); 10 res.write('<h1>Node.js</h1>'); 11 res.end(""); 12 }).listen(3000,'127.0.0.1'); 13 14 console.log('http server start on port 3000'); 15 } 16 17 exports.serverStart = serverStart;
通過第6行的解析,不同的請求路徑在第7行輸出不同的pathname。
建立一個名為router.js的文件,編寫路由代碼如下:
1 function route(pathname){ 2 console.log('about to route a request for'+pathname); 3 } 4 5 exports.route = route;
服務器應該知道路由的存在,並加以組合有效地利用。這里可以采用硬編碼方式或依賴注入的方式實現組合,后者更好。
改造server.js如下:
1 var http = require('http'); 2 var url = require('url'); 3 4 function serverStart(route){ 5 http.createServer(function(req,res){ 6 var pathname = url.parse(req.url).pathname; 7 console.log('http request for '+pathname+' recieved'); 8 9 route(pathname); 10 11 res.writeHead(200,{'Content-Type':'text/html'}); 12 res.write('<h1>Node.js</h1>'); 13 res.end(""); 14 }).listen(3000,'127.0.0.1'); 15 16 console.log('http server start on port 3000'); 17 } 18 19 exports.serverStart = serverStart;
改造index.js如下:
1 var server = require("./server"); 2 var router = require("./router"); 3 4 server.serverStart(router.route);
編譯運行,可看到路由模塊被server模塊引入。
行為驅動執行
在index中,我們將router對象傳遞進去,服務器隨后調用這個對象的route函數。但其實服務器自身並不需要這個對象,它只是需要執行某個動作,而這個動作由該對象來完成。具體的細節服務器並不關心,因為這是執行這個動作的那個對象的事。這就是函數式編程。但歸根結底,到了最后一層總有一個對象是真正關心細節並完成細節,輸出結果的。像現實世界的上級把任務給下級,下級給再下級,最終總要有人完成這個任務才好!
路由到處理程序
路由用來轉發請求,將請求分發給實際的處理程序,讓他們解決請求並回復,因此新建請求處理模塊requestHandlers.js,代碼如下:
1 function start(){ 2 console.log("request handler 'start' was called"); 3 } 4 5 function upload(){ 6 console.log("request handler 'upload' was called"); 7 } 8 9 exports.start = start; 10 exports.upload = upload;
有了這個模塊,路由就有路可尋了。
現在需要將處理程序通過一個對象來傳遞並將對象注入到route()函數中,而javascript的對象是”key:value“組合,滿足我們的需要。將存放處理程序的對象引入index,如下:
1 var server = require("./server"); 2 var router = require("./router"); 3 var requestHandlers = require("./requestHandlers"); 4 5 var handler = {}; 6 handler['/'] = requestHandlers.start; 7 handler['/start'] = requestHandlers.start; 8 handler['/upload'] = requestHandlers.upload; 9 10 server.serverStart(router.route,handler);
路徑為”/“或"/start"的請求由start處理,"/upload"由upload處理。
將該對象作為參數傳給服務器,server.js模塊代碼改為:
1 var http = require('http'); 2 var url = require('url'); 3 4 function serverStart(route,handle){ 5 http.createServer(function(req,res){ 6 var pathname = url.parse(req.url).pathname; 7 console.log('http request for ''+pathname+'' recieved'); 8 9 route(handle,pathname); 10 11 res.writeHead(200,{'Content-Type':'text/html'}); 12 res.write('<h1>Node.js</h1>'); 13 res.end(""); 14 }).listen(3000,'127.0.0.1'); 15 16 console.log('http server start on port 3000'); 17 } 18 19 exports.serverStart = serverStart;
handle對象傳遞給了route,此時還得修改router模塊,如下:
1 function route(handle,pathname){ 2 console.log('about to route a request for'+pathname); 3 4 if(typeof handle[pathname] === 'function'){ 5 handle[pathname](); 6 } 7 else{ 8 console.log('no request handler found for'+pathname); 9 } 10 } 11 12 exports.route = route;
首先檢查對應的處理程序是否存在,存在則直接調用相應的函數,否則提示沒有找到對應的函數。這時,服務器,路由,處理程序就聯系在一起了。
讓請求處理程序作出響應
web程序一般是基於http的請求-應答模式,這樣服務器和瀏覽器可以實現通話。
一種不好的實現方式
這種方式讓服務器的函數直接返回展示給用戶的信息。這就需要同時修改服務器、路由、請求處理函數的部分代碼:
requestHandlers.js
1 function start(){ 2 console.log("request handler 'start' was called"); 3 return "hello start!"; 4 } 5 6 function upload(){ 7 console.log("request handler 'upload' was called"); 8 return "hello upload!"; 9 } 10 11 exports.start = start; 12 exports.upload = upload;
requestHandlers.js
1 function route(handle,pathname){ 2 console.log('about to route a request for'+pathname); 3 4 if(typeof handle[pathname] === 'function'){ 5 return handle[pathname](); 6 } 7 else{ 8 console.log('no request handler found for'+pathname); 9 return "404 not found!" 10 } 11 } 12 13 exports.route = route;
server.js
1 var http = require('http'); 2 var url = require('url'); 3 4 function serverStart(route,handle){ 5 http.createServer(function(req,res){ 6 var pathname = url.parse(req.url).pathname; 7 console.log('http request for '+pathname+' recieved'); 8 9 10 res.writeHead(200,{'Content-Type':'text/html'}); 11 var content = route(handle,pathname); 12 res.write(content); 13 res.end(""); 14 }).listen(3000,'127.0.0.1'); 15 16 console.log('http server start on port 3000'); 17 } 18 19 exports.serverStart = serverStart;
這樣的模式運行的也很好,但當有請求處理程序需要執行非阻塞操作,就“掛”了。
阻塞與非阻塞
修改requestHandlers.js
1 function start(){ 2 console.log("request handler 'start' was called"); 3 4 function sleep(milliSeconds){ 5 var startTime = new Date().getTime(); 6 while(new Date().getTime() < startTime + milliSeconds) 7 ; 8 } 9 10 sleep(10000); 11 return "hello start!"; 12 } 13 14 function upload(){ 15 console.log("request handler 'upload' was called"); 16 return "hello upload!"; 17 } 18 19 exports.start = start; 20 exports.upload = upload;
上述的sleep函數會讓方法停頓10秒,然后才會返回結果。實際場景中,需要計算或是查找數據庫等阻塞操作非常多。
現在通過start和upload即可模擬阻塞,打開兩個空白頁面分別輸入“localhost:3000/start”和"localhost:3000/upload",先進行start請求在快速切換到量以頁面進行upload請求,發現start頁面請求恢復用了10秒,而upload也用了10秒。原因是,start中的sleep阻塞了所有其他處理。
而nodejs的特性之一是——單線程,可以在不增加線程情況下,對任務進行並行處理。這里的機制是它使用輪詢(event loop)來實現並行操作,所有盡量避免阻塞操作而是用非阻塞操作。要使用非阻塞,需要使用回調,將函數作為參數傳遞給其他需要費時間處理的函數。
對於nodejs可以形象比喻成“嘿,probablyExpensiveFunction()(需要費時間處理的函數),繼續做你的事情,我(nodejs線程)先不等你了,繼續去處理后面的代碼,請你提供一個回調callBackFunction(),等你處理完了我會去調用該回調函數的。”
一種錯誤的非阻塞方式
requestHandlers.js
1 var exec = require("child_process").exec; 2 3 function start(){ 4 console.log("request handler 'start' was called"); 5 var content = "empty"; 6 7 exec("ls-lah",function(error,stdout,stderr){ 8 content = stdout; 9 }); 10 return content; 11 } 12 13 function upload(){ 14 console.log("request handler 'upload' was called"); 15 return "hello upload!"; 16 } 17 18 exports.start = start; 19 exports.upload = upload;
引入另一個模塊“child_process”,實現非阻塞操作exec()。
exec()的作用是從nodejs執行一個shell命令,例子中使用它獲取當前目錄下所有文件("ls-lah"),然后當start請求時將文件信息輸出到瀏覽器。執行該例子,瀏覽器輸出“empty”。原因在於,nodejs是同步執行,而為了非阻塞,exec()使用了回調函數,回調函數作為參數傳遞給exec(),而exec()是異步操作,即當他還沒來得及執行回調,nodejs的同步性致使nodejs執行了exec()的下一個語句,這時候content並沒有等來exec()的賦值,因此他還是“empty”。這和javascript中的同步異步很相像,每當需要執行ajax,並且需要ajax的結果時,我們將下一步的代碼放在ajax執行成功時的回調里。
非阻塞方式對請求進行響應
我們可以采用值傳遞的方式將請求處理函數返回的內容再回傳給http服務器。上述方式都是將請求處理函數的結果返回給http服務器,而另一種方式是將服務器傳遞給內容,即將response對象傳遞給請求處理程序,程序隨后采用該對象上的方法對對請求進行響應。
server.js
1 var http = require('http'); 2 var url = require('url'); 3 4 function serverStart(route,handle){ 5 http.createServer(function(req,res){ 6 var pathname = url.parse(req.url).pathname; 7 console.log('http request for '+pathname+' recieved'); 8 9 route(handle,pathname,res); 10 11 }).listen(3000,'127.0.0.1'); 12 13 console.log('http server start on port 3000'); 14 } 15 16 exports.serverStart = serverStart;
以上將server.js中有關response的函數調用全部移除,將response對象傳給route。
router.js
1 function route(handle,pathname,response){ 2 console.log('about to route a request for'+pathname); 3 4 if(typeof handle[pathname] === 'function'){ 5 return handle[pathname](response); 6 } 7 else{ 8 console.log('no request handler found for'+pathname); 9 response.writeHead(404,{"Content-Type":"text/plain"}); 10 response.write("404 not found!"); 11 response.end(); 12 } 13 } 14 15 exports.route = route;
requestHandlers.js
1 var exec = require("child_process").exec; 2 3 function start(response){ 4 console.log("request handler 'start' was called"); 5 6 exec("ls-lah",function(error,stdout,stderr){ 7 response.writeHead(200,{"Content-Type":"text/plain"}); 8 response.write(stdout); 9 response.end(); 10 }); 11 12 } 13 14 function upload(response){ 15 console.log("request handler 'upload' was called"); 16 17 response.writeHead(200,{"Content-Type":"text/plain"}); 18 response.write("hello upload"); 19 response.end(); 20 } 21 22 exports.start = start; 23 exports.upload = upload;
處理程序接收response對象,由該對象做出直接響應。在此編譯,打開瀏覽器請求,“localhost:3000/start”不會對"localhost:3000/upload"造成阻塞。
更有用的場景
處理post請求
1 var exec = require("child_process").exec; 2 3 function start(response){ 4 console.log("request handler 'start' was called"); 5 6 var body = '<html>'+'<head>'+'<body>'+'<form action="/upload" method="post">'+'<textarea name="text" rows="20" cols="50"></textarea>'+ 7 '<input type="submit" value="submit"/>'+'</form></body></html>'; 8 9 response.writeHead(200,{"Content-Type":"text/html"}); 10 response.write(body); 11 response.end(); 12 13 } 14 15 function upload(response){ 16 console.log("request handler 'upload' was called"); 17 18 response.writeHead(200,{"Content-Type":"text/plain"}); 19 response.write("hello upload"); 20 response.end(); 21 } 22 23 exports.start = start; 24 exports.upload = upload;
以上是修改后的requestHandlers.js,運行后是個簡單的表單,包括文本域和提交按鈕。post數據有時候會很大,為使過程不阻塞,nodejs會將post數據拆分成很多很小的數據塊,然后通過觸發特定的事件,將這些小數據塊傳給回調函數。特定事件有data(表示新的小數據塊到了)和end(表示所有數據塊都已經接收完畢)。這時候我們需要告訴nodejs事件觸發的時候調用哪些回調函數,通過什么方式告訴呢?在request對象上注冊監聽器(listener)來實現。獲取所有來自請求端的數據並將數據傳給應用層處理,是http服務器的事。所以直接在server里處理post數據,然后將最終的數據傳給路由和請求處理器,讓他們來進一步處理。
server.js
1 function serverStart(route,handle){ 2 http.createServer(function(req,res){ 3 var postData = ""; 4 var pathname = url.parse(req.url).pathname; 5 console.log('http request for '+pathname+' recieved'); 6 7 request.setEncoding("utf8"); 8 9 request.addListener("data",function(postDataChunk){ 10 postData += postDataChunk; 11 console.log('received post data chunk'+postDataChunk+"."); 12 }); 13 14 request.addListener("end",function(postDataChunk){ 15 route(handle,pathname,res,postData); 16 }); 17 18 }).listen(3000,'127.0.0.1'); 19 20 console.log('http server start on port 3000'); 21 } 22 23 exports.serverStart = serverStart;
上述代碼做了3件事,首先設置接收數據格式為utf8,然后綁定data事件的監聽器,用於接收每次傳遞的新數據塊,復制給postData,最后將路由調用移到end中,確保數據接收完成在觸發並且只觸發一次。
requestHandlers.js
1 var querystring = require("querystring"); 2 function start(response,postData){ 3 console.log("request handler 'start' was called"); 4 5 var body = '<html>'+'<head>'+'<body>'+'<form action="/upload" method="post">'+'<textarea name="text" rows="20" cols="50"></textarea>'+ 6 '<input type="submit" value="submit"/>'+'</form></body></html>'; 7 8 response.writeHead(200,{"Content-Type":"text/html"}); 9 response.write(body); 10 response.end(); 11 12 } 13 14 function upload(response,postData){ 15 console.log("request handler 'upload' was called"); 16 17 response.writeHead(200,{"Content-Type":"text/plain"}); 18 response.write("you are sent:"+querystring.parse(postData).text); 19 response.end(); 20 } 21 22 exports.start = start; 23 exports.upload = upload;
通過querystring篩選出我們感興趣的部分,以上就是關於post數據處理的內容。
處理文件上傳
處理文件上傳就是處理post數據,有時候麻煩的是細節處理。所以這里使用現成的方案,即外部模塊node-formidable,用nmp命令安裝:
nmp install formidable
該模塊做的就是通過http post請求提交的表單,在nodejs中可以被解析。我們創建一個IncomingForm,它是對提交的表單的抽象表示,之后就可以用他來解析request對象,獲取需要的字段數據。關於該模塊具體信息及工作原理,其官網有實例。
現在的問題是保存在本地硬盤的內容如何顯示在瀏覽器中。我們需要將其讀取到服務器,使用fs模塊完成。
1 var querystring = require("querystring"), 2 fs = require("fs"); 3 function start(response,postData){ 4 console.log("request handler 'start' was called"); 5 6 var body = '<html>'+'<head>'+'<body>'+'<form action="/upload" method="post">'+'<textarea name="text" rows="20" cols="50"></textarea>'+ 7 '<input type="submit" value="submit"/>'+'</form></body></html>'; 8 9 response.writeHead(200,{"Content-Type":"text/html"}); 10 response.write(body); 11 response.end(); 12 13 } 14 15 function upload(response,postData){ 16 console.log("request handler 'upload' was called"); 17 18 response.writeHead(200,{"Content-Type":"text/plain"}); 19 response.write("you are sent:"+querystring.parse(postData).text); 20 response.end(); 21 } 22 23 function show(response,postData){ 24 console.log("request handler 'show' was called"); 25 fs.readFile("/tmp/test.png","binary",function(error,file){ 26 if(error){ 27 response.writeHead(500,{"Content-Type":"text/plain"}); 28 response.write(error+"\n"); 29 response.end(); 30 }else{ 31 response.writeHead(200,{"Content-Type":"image/png"}); 32 response.write(file,"binary"); 33 response.end(); 34 } 35 }); 36 } 37 38 exports.start = start; 39 exports.upload = upload; 40 exports.show = show;
將其添加到index.js的路由映射表中:
1 var server = require("./server"); 2 var router = require("./router"); 3 var requestHandlers = require("./requestHandlers"); 4 5 var handler = {}; 6 handler['/'] = requestHandlers.start; 7 handler['/start'] = requestHandlers.start; 8 handler['/upload'] = requestHandlers.upload; 9 handler['/show'] = requestHandlers.show; 10 11 server.serverStart(router.route,handler);
編譯,瀏覽器請求即可看見圖片。
最后,
需要在/start表單添加文件上傳元素
將formidable整合到upload中用於將上傳的圖片保存到指定路徑
將上傳的圖片內嵌到/upload url輸出的html中
第一項,移除此前的文本區,添加上傳組件和“multipart/form-data”編碼類型:

1 var querystring = require("querystring"), 2 fs = require("fs"); 3 function start(response,postData){ 4 console.log("request handler 'start' was called"); 5 6 var body = '<html>'+'<head>'+'<body>'+'<form action="/upload" method="post" enctype="multipart/form-data">'+ 7 '<input type="file" name="upload"/><input type="submit" value="upload file">'+'</form></body></html>'; 8 9 response.writeHead(200,{"Content-Type":"text/html"}); 10 response.write(body); 11 response.end(); 12 13 } 14 15 function upload(response,postData){ 16 console.log("request handler 'upload' was called"); 17 18 response.writeHead(200,{"Content-Type":"text/plain"}); 19 response.write("you are sent:"+querystring.parse(postData).text); 20 response.end(); 21 } 22 23 function show(response,postData){ 24 console.log("request handler 'show' was called"); 25 fs.readFile("/tmp/test.png","binary",function(error,file){ 26 if(error){ 27 response.writeHead(500,{"Content-Type":"text/plain"}); 28 response.write(error+"\n"); 29 response.end(); 30 }else{ 31 response.writeHead(200,{"Content-Type":"image/png"}); 32 response.write(file,"binary"); 33 response.end(); 34 } 35 }); 36 } 37 38 exports.start = start; 39 exports.upload = upload; 40 exports.show = show;
這時候我們需要在upload中對請求中的上傳文件進行處理,這樣的話需要將request傳給node-formidable的form-parse函數。由於nodejs不會對數據作緩沖,於是必須將request對象傳入。
server.js:

1 var http = require('http'); 2 var url = require('url'); 3 4 function serverStart(route,handle){ 5 http.createServer(function(req,res){ 6 7 var pathname = url.parse(req.url).pathname; 8 console.log('http request for '+pathname+' recieved'); 9 10 route(route,handle,res,req); 11 12 }).listen(3000,'127.0.0.1'); 13 14 console.log('http server start on port 3000'); 15 } 16 17 exports.serverStart = serverStart;
router.js:

1 function route(handle,pathname,response,request){ 2 console.log('about to route a request for'+pathname); 3 4 if(typeof handle[pathname] === 'function'){ 5 return handle[pathname](response,request); 6 } 7 else{ 8 console.log('no request handler found for'+pathname); 9 response.writeHead(404,{"Content-Type":"text/plain"}); 10 response.write("404 not found!"); 11 response.end(); 12 } 13 } 14 15 exports.route = route;
將上傳和重命名的操作放在一起:

1 var querystring = require("querystring"), 2 fs = require("fs"), 3 formidable = require("formidable"); 4 function start(response){ 5 console.log("request handler 'start' was called"); 6 7 var body = '<html>'+'<head>'+'<body>'+'<form action="/upload" method="post" enctype="multipart/form-data">'+ 8 '<input type="file" name="upload"/><input type="submit" value="upload file">'+'</form></body></html>'; 9 10 response.writeHead(200,{"Content-Type":"text/html"}); 11 response.write(body); 12 response.end(); 13 14 } 15 16 function upload(response,postData){ 17 console.log("request handler 'upload' was called"); 18 19 var form = new formidable.IncomingForm(); 20 form.parse(request,function(error,fields,files){ 21 fs.renameSync(file.upload.path,"/tmp/test.png"); 22 response.writeHead(200,{"Content-Type":"text/html"}); 23 response.write("image:</br>"); 24 response.write("<image src='/show'/>"); 25 response.end(); 26 }); 27 28 } 29 30 function show(response,postData){ 31 console.log("request handler 'show' was called"); 32 fs.readFile("/tmp/test.png","binary",function(error,file){ 33 if(error){ 34 response.writeHead(500,{"Content-Type":"text/plain"}); 35 response.write(error+"\n"); 36 response.end(); 37 }else{ 38 response.writeHead(200,{"Content-Type":"image/png"}); 39 response.write(file,"binary"); 40 response.end(); 41 } 42 }); 43 } 44 45 exports.start = start; 46 exports.upload = upload; 47 exports.show = show;
好,編譯,重啟瀏覽器。選擇本地圖片,上傳並顯示!
結構圖如下:
該實例有關的技術點:
服務器端javascript
函數式編程
阻塞與非阻塞
單線程
回調
事件
nodejs模塊
沒有介紹到的還有數據庫操作、單元測試、開發外部模塊等。
注:本文摘抄自《nodejs入門》