到目前為止,我們做的已經很好了,但是,我們的應用沒有實際用途。
服務器,請求路由以及請求處理程序都已經完成了,下面讓我們按照此前的用例給網站添加交互:用戶選擇一個文件,上傳該文件,然后在瀏覽器中看到上傳的文件。 為了保持簡單,我們假設用戶只會上傳圖片,然后我們應用將該圖片顯示到瀏覽器中。
好,下面就一步步來實現,鑒于此前已經對JavaScript原理性技術性的內容做過大量介紹了,這次我們加快點速度。
要實現該功能,分為如下兩步: 首先,讓我們來看看如何處理POST請求(非文件上傳),之后,我們使用Node.js的一個用于文件上傳的外部模塊。之所以采用這種實現方式有兩個理由。
第一,盡管在Node.js中處理基礎的POST請求相對比較簡單,但在這過程中還是能學到很多。?
第二,用Node.js來處理文件上傳(multipart POST請求)是比較復雜的,它_不_在本書的范疇,但,如何使用外部模塊卻是在本書涉獵內容之內。
## 處理POST請求
考慮這樣一個簡單的例子:我們顯示一個文本區(textarea)供用戶輸入內容,然后通過POST請求提交給服務器。最后,服務器接受到請求,通過處理程序將輸入的內容展示到瀏覽器中。
_/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>';
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;
~~~
好了,現在我們的應用已經很完善了,都可以獲得威比獎(Webby Awards)了,哈哈。(譯者注:威比獎是由國際數字藝術與科學學院主辦的評選全球最佳網站的獎項,具體參見詳細說明)通過在瀏覽器中訪問[http://localhost:8888/start](http://localhost:8888/start)就可以看到簡單的表單了,要記得重啟服務器哦!
你可能會說:這種直接將視覺元素放在請求處理程序中的方式太丑陋了。說的沒錯,但是,我并不想在本書中介紹諸如MVC之類的模式,因為這對于你了解JavaScript或者Node.js環境來說沒多大關系。
余下的篇幅,我們來探討一個更有趣的問題: 當用戶提交表單時,觸發_/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
});
~~~
問題來了,這部分邏輯寫在哪里呢? 我們現在只是在服務器中獲取到了_request_對象 —— 我們并沒有像之前_response_對象那樣,把 request 對象傳遞給請求路由和請求處理程序。
在我看來,獲取所有來自請求的數據,然后將這些數據給應用層處理,應該是HTTP服務器要做的事情。因此,我建議,我們直接在服務器中處理POST數據,然后將最終的數據傳遞給請求路由和請求處理器,讓他們來進行進一步的處理。
因此,實現思路就是: 將_data_和_end_事件的回調函數直接放在服務器中,在_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>';
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_模塊來實現:
~~~
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>';
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 Geisend?rfer開發的_node-formidable_模塊。它對解析上傳的文件數據做了很好的抽象。 其實說白了,處理文件上傳_“就是”_處理POST數據 —— 但是,麻煩的是在具體的處理細節,所以,這里采用現成的方案更合適點。
使用該模塊,首先需要安裝該模塊。Node.js有它自己的包管理器,叫_NPM_。它可以讓安裝Node.js的外部模塊變得非常方便。通過如下一條命令就可以完成該模塊的安裝:
~~~
npm install formidable
~~~
如果終端輸出如下內容:
~~~
npm info build Success: formidable@1.0.9
npm ok
~~~
就說明模塊已經安裝成功了。
現在我們就可以用_formidable_模塊了——使用外部模塊與內部模塊類似,用require語句將其引入即可:
~~~
var formidable = require("formidable");
~~~
這里該模塊做的就是將通過HTTP POST請求提交的表單,在Node.js中可以被解析。我們要做的就是創建一個新的_IncomingForm_,它是對提交表單的抽象表示,之后,就可以用它解析request對象,獲取表單中需要的數據字段。
node-formidable官方的例子展示了這兩部分是如何融合在一起工作的:
~~~
var formidable = require('formidable'),
http = require('http'),
util = require('util');
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
// parse a file upload
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(util.inspect({fields: fields, files: files}));
});
return;
}
// show a file upload form
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>'
);
}).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_目錄中)顯示到瀏覽器中。
我們先來解決后面那個問題: 對于保存在本地硬盤中的文件,如何才能在瀏覽器中看到呢?
顯然,我們需要將該文件讀取到我們的服務器中,使用一個叫_fs_的模塊。
我們來添加_/show_URL的請求處理程序,該處理程序直接硬編碼將文件_/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>';
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);
~~~
重啟服務器之后,通過訪問[http://localhost:8888/show](http://localhost:8888/show),就可以看到保存在_/tmp/test.png_的圖片了。
好,最后我們要的就是:
* 在_/start_表單中添加一個文件上傳元素
* 將node-formidable整合到我們的_upload_請求處理程序中,用于將上傳的圖片保存到_/tmp/test.png_
* 將上傳的圖片內嵌到_/upload_URL輸出的HTML中
第一項很簡單。只需要在HTML表單中,添加一個_multipart/form-data_的編碼類型,移除此前的文本區,添加一個文件上傳組件,并將提交按鈕的文案改為“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>';
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_對象從服務器開始一路通過請求路由,再傳遞給請求處理程序。 或許還有更好的方案,但是,不管怎么說,目前這樣做可以滿足我們的需求。
到這里,我們可以將_postData_從服務器以及請求處理程序中移除了 —— 一方面,對于我們處理文件上傳來說已經不需要了,另外一方面,它甚至可能會引發這樣一個問題: 我們已經“消耗”了_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>';
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) {
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;
~~~
好了,重啟服務器,我們應用所有的功能就可以用了。選擇一張本地圖片,將其上傳到服務器,然后瀏覽器就會顯示該圖片。