因為最近有東西需要用到node.js,所以我就在linux虛擬機上安裝了node.js,對於javascript,也是第一次接觸。
剛入門,就是一個實用的案例,畢竟這些東西都是實踐出真知。這個案例就是一個web應用,允許用戶上傳圖片並在當前網頁顯示出來。我們來看看,這個案例是如何從一個簡簡單單的代碼變成具有我們上面功能的應用。
我們先看看如何在網頁顯示我們輸出的消息,這條消息自然就是我們每個程序員所寫下的第一句話:“Hello Word”。是的,這句話已經成為我們無論學習什么語言或是任何操作系統,都要寫的第一句話,因為這句話說明我們已經邁出了新世界的第一步!
先上代碼:
var http = require("http"); http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World"); response.end();}).listen(8888);
這段簡短的代碼就已經告訴我們如何在網頁中顯示消息。首先,是第一句,注意require()函數,這個函數是表示我們要申請的模塊,這里申請的是http模塊並把它的返回值賦值給我們聲明的變量,這樣我們的變量就會得到http模塊所提供的所有公共方法(將該變量命名為我們所要申請的模塊是一個好習慣,當然你完全可以自定義)。什么是模塊?我們可以將模塊理解為庫或包,事實上,它也差不多正是這樣,因為這些模塊里面,封裝着我們想要使用的函數,接下來我們還會學到,怎樣創建自己的模塊。好了,現在進入正題。也許有些人會問,什么是http模塊?好吧,如果真的有人因為這個問題而無法前進,那么我就在這里講一下,但是,我真的不是什么高手,javascript我只是稍微懂點基礎罷了,大家只要將它簡單認為是這樣的東西,當我們要在網頁顯示消息的時候,一定要發出一個請求,告訴服務器,我要在一個網頁前顯示消息,而服務器就會響應我們的請求顯示出來,所以,大家在接下來的代碼中可以看到,http模塊中的createServer()的參數是一個響應處理的函數(在javascript中,函數是可以作為參數的,因為函數本身是對象),這個函數需要兩個參數,request(請求)和response(響應)。對於createServer()這個函數,它的參數是一個requestListener,就是自動被添加到request事件上的函數,然后返回一個網頁服務對象,就是顯示我們消息的網頁(它的作用就是建立一個可以響應請求的服務器)。我們要想輸出“Hello Word"這個消息,最主要的是在響應這方面下工夫。我們先要設置一下我們的消息的顯示格式,比如在writeHead()函數中設置狀態碼和內容類型,這里是txt,所以就是“text/plain",然后就是用write()設定我們要輸出的消息,最后就是以end()來結束我們這次的響應動作。很多人一定對這個函數有個疑問,就是那兩個參數到底是什么?好吧,這里我就稍微講一下,第一個是狀態碼,表示網頁服務器http響應狀態的三位數字,像是我們上面的200,就表示我們的請求已成功,請求所希望的響應頭或數據體將隨此響應返回,如果是404,就表示請求失敗,請求所希望得到的資源未被服務器發現,所以404這個狀態碼被廣泛應用於當服務器不想揭示到底為何請求被拒絕或者沒有其他適合的響應可用的情況(現在你知道為什么當我們瀏覽的網頁出現問題時會顯示404了吧)。詳細的情況請看這條鏈接:http://baike.baidu.com/view/1790469.htm。第二個是表示MIME類型,所謂的MIME類型,就是設定某種擴展名的文件用一種應用程序來打開的方式類型,當該擴展名文件被訪問的時候,瀏覽器會自動使用指定應用程序來打開,多用於指定一些客戶端自定義的文件名,以及一些媒體文件打開方式,像是圖片,txt等等,這里的text/plain就表示普通文檔txt,如果是png圖片,就是image/png,更多的情況看下面這條鏈接:http://baike.baidu.com/view/160611.htm
相信大家注意到了,最后的listen()這個監聽器。只要有做過界面的人,像是搞過android控件的人,一定對監聽器非常熟悉。這里就是監聽我們的端口號為8888的網頁響應動作,只要我們運行上面的代碼,然后在我們的瀏覽器中輸入http://localhost:8888 /就會在當前的網頁中看到"Hello Word"。
值得注意的是,我們這里有一個匿名函數,這個函數就是我們處理響應和請求的實際處理函數,我們可以將它們提取出來,像是下面這樣:
var http = require("http"); function onRequest(request, response){ response.writeHead(200, {"Content-Type":"text/plain"}); response.write("Hellod Word"); response.end(); } http.createServer(onRequest).listen(8888);
結果都是一樣的,至於使用哪種方式,就看個人需要,不過匿名函數的使用方式比較普遍。
為什么要對我們的處理函數進行監聽呢?那是因為node.js是基於事件驅動的。好吧,這句話實在是太泛泛而談,就像我們的大學課堂老師,總是用似是而非的話來忽悠我們(因為他們自己也是似是而非)。node.js的工作原理是這樣的:除了我們的代碼,它的所有東西都是並行的!幾乎所有的東西都是同時運行的!為什么是除了我們的代碼呢?你可以這樣想象,我們的代碼就好比是整個應用的國王,它一直都在昏昏欲睡,然后,它有一大幫手下,當一個手下向他詢問任務的時候,它會醒過來,指示任務然后繼續昏昏欲睡,在上一個手下在執行任務的時候,會有其他手下繼續詢問任務並且執行。問題,為什么我們的代碼要"昏昏欲睡"呢?只要稍微明白並發的同學就會明白,如果不這樣做,就會出現同時多個手下詢問任務的情況。這種情況不好嗎?是的,很不好,因為這個國王並不是一個多么能干的國王(請別介意,事實上我們所寫的代碼真的絕大部分都是不"精明"的),所以,出現這種情況,它可能會出現錯誤,所以,一對一是最好的,而且這樣的效率更高。然后,每個手下在完成任務后就會向我們的國王報告情況,這也是一對一的。所以,大家明白了沒?(事實上,我也不是很明白,但是接觸過並發,所以多多少少都能想象出來),所以,這里就有個監聽器,這個監聽器就是當我們的任務完成時向我們的國王進行報告。
關於這個例子還沒完,因為這里有個重要的知識點必須講清楚,就是回調。想象這種情況,我們的服務器是跑在一個單線程里的(是的,node.js是單線程的,因為它已經將除了代碼以外的操作都設置為並行的),但是我們的服務器是要處理http請求的,這個請求可是任何時候都會過來,我們的服務器能夠恰好的響應嗎?於是,回調就變得非常重要,就是我們createServer()里的函數參數。無論何時,什么樣的請求過來,只要它作為參數傳進來,那么,我們的服務器都能根據這個參數自動調用合適的函數,恰當的處理這個請求,這就是回調(不熟悉回調的話,你只要這么想,回調的英文就是callback,我們的函數已經寫好了,就像一個選秀選手在后台等着我們的主持人叫他出來表演,當主持人拿到它的號碼(參數),就會叫他,這就是回調)。所以,我們的服務器是異步的。
就算我們的回調函數沒有被調動,其他代碼還是可以被執行的。就像這樣:
function onRequest(request, response){ console.log("server has started"); .... }
這里只是在回調函數里添加一句:console.log("server has started")(如果不清楚這一句的話,就將它當成我們javascript中的alert()),本來應該是不會執行的,如果是我們平時的串行代碼(就算是我們以前寫的並發也是不行的,除非是特殊的方法),但是,神奇的海螺小姐又出現了!它能夠被顯示出來!我們要分清楚一個問題,就是我們的請求真正想要觸發的,是我添加的語句的下面,前面並不是回調函數真正想要執行的部分(我用了“真正”這個詞,因為這里必須跟大家說明一下,我們這里處理的只是response的部分,我並沒有對request進行任何細節上的處理,所以,理論上,當任何請求過來的時候它都能被執行,至於是否能夠被響應,則是看請求的類型),所以,當有請求過來的時候,就算它不能觸發我們的回調函數,我們的代碼只要能夠執行,還是會執行的,因為這時任何請求都能使它運行。
好了,我們繼續往下講,前面的廢話實在是太多了(請原諒我,因為我也是一只初學雞,所以有些問題很大家一樣,也是一頭霧水,只能將自己知道的所有東西都倒出來了)。
我們上面講的,只是實現一個非常基礎的http服務器,它還並不是一個真正意義上的node.js模塊,因為它不能被其他模塊調用。那么,我們接下來怎樣讓它變成真正的模塊呢?就是抓住我上面的關鍵點,能夠被其他模塊調用。通常,像是上面的創建服務器的代碼,我們一般命名為server.js,而且我們都會有一個index.js作為驅使其他模塊的國王。接下來我們就是要讓這個手下能夠聽話了:
var http = require("http"); function start(){ function onRequest(request, response){ ... } http.createServer(onRequest).listen(8888); } exports.start = start;
我們這段代碼唯一的不同點就是我們在結尾多了這一句:exports.start = start。這段代碼就是將我們的start()函數導出去,然后讓我們的index.js能夠啟動這個服務器,因為一般我們對服務器的操作就僅是啟動它而已。
我們在index.js中只需這么寫:
var server = require("./server"); server.start();
同樣的道理,我們請求server模塊並將其賦給一個變量,然后調用這個模塊中的start()函數。這里與我們上面請求內置模塊是一樣的操作,但是,自定義的模塊的請求是需要在前面加上"./"。
通過上面的例子,我們已經可以初步掌握如何創建模塊以及如何在另一個模塊中調用其他模塊的方法,接下來,還是繼續探討其他模塊的創建,就是有關於處理不同的url請求。當然,我們完全可以將代碼放在我們已經建好的server模塊中,但是我們一般都會新建一個模塊,因為處理url請求並不是服務器要做的,它是要交給路由器的(路由器這個詞大家一定非常熟悉吧,路由這個名字真的是非常形象,我對於譯者的崇拜之情油然而生)。我們還是馬上建立一個路由器模塊router.js吧:
function route(pathname){ console.log("About to route:" + pathname); } exports.route = route;
然后我們的服務器還是要進行修改:
var http = require("http"); url = require("url"); function start(route){ function onRequest(requset, response){ var pathname = url.parse(request.url).pathname; console.log(pathname + "has received"); route(pathname);
} ... } exports.start = start;
接着就是index.js:
var server = require("./server"); router = require("./router"); server.start(router.route);
相信大家一定還是會注意到,就是導出來的函數在除了index.js之外的其他模塊中,根本就不需要申請router模塊就能直接使用,像是作為參數傳遞。我曾經將index.js的server.start()直接改為start(),結果它顯示的是start()沒有被定義這個錯誤,然后繼續手賤,在server.js中添加一個route的函數並且導出,但是什么情況都沒有發生!route函數並沒顯示任何錯誤!按道理來說,像是我們的java,如果你的兩個類中的方法名是一樣的,你必須指點對象,但是這里似乎並不是這樣的。關於這個問題,我想放在后面再講,因為后面的代碼中會出現新的start函數。
我們這里要先講一下路由器的工作原理。路由器會根據請求的url和其他需要的get,post參數來執行相應的處理程序,於是我們需要從http請求中提取出url和get,post參數。這一部分的功能到底是路由器還是服務器或者作為一個新的功能模塊,我們這里就不討論,因為這也太難為我這個初學者了,這是我的第一個應用,我對web編程並沒有更多的經驗,所以,這里就先放在server模塊里。好了,那么,我們所需要的參數都是來自於請求,就是request,那么,我們怎樣提取出來呢?解析request,我們需要其他模塊,像是我上面用到的url模塊,事實上,我們還與querystring模塊,至於哪個更好,我們先表下不談,就着url模塊來講如何提取,下面就是這兩個模塊的提取原理:
url.parse(string).query
|
url.parse(string).pathname |
| |
| |
------ -------------------
http://localhost:8888/start?http=bar&hello=world
--- -----
| |
| |
querystring(string)["http"] |
|
querystring(string)["hello"]
對於上面的原理,我們只需要知道/start?http=bar&hello=world這一部分。相信大家看圖就能明白這里面的組成。/start是我們在index.js中調用的server中的start函數,根就是我們的路徑名pathname,后面就是我們的顯示消息,“Hello Word",但是這里卻是"hell0=word",為什么會是這樣呢?如果接觸過javascript的同學就不難明白,因為我們的url是被處理過的,所有的字母都已經被轉化為小寫,並且空格對應於=。這部分我就不再講解了,因為我覺得我的javascript知識也實在是太匱乏了,所以大家感興趣的話,還是自己看看相關的書吧。
我們現在已經可以通過不同的url路徑來區別不同的請求了,也把路由器和服務器整合起來,現在我們來看看這里面的一些亮點吧。首先,就是我們的服務器要想知道路由器的存在並且加以利用,就像我前面講過的,我們為什么不可以直接將這個路由器硬編碼進我們服務器代碼中呢?我們可以這么干:
function start(router.route){ ... }
但是,我們都知道,這樣做的壞處就是我們的代碼太"硬"了!編寫這樣的代碼,會使得我們的復用性大大下降,所以,node.js采用的是依賴注入的方式來實現松散的注入路由模塊。依賴注入,這是一個新字眼,相信我,這個字眼所代表的意思我們在java這樣的面相對象編程語言中已經是接觸過了,但是它的實際意義卻是非常龐大的,像我這樣的新手當然是不敢有任何奢望來向大家講解這個話題,但是我會結合我們上面的例子稍微講一下。依賴注入,說白一點,就是我們的對象的創建的時候如果需要得到另一個對象的引用(因為我們的對象的創建可能需要另一個對象中的方法),那么我們當然是會將那個對象的引用作為參數傳進來。這就是所謂的"依賴",傳對象的引用就是所謂的"注入"。這樣的行為我們在java中是習以為常的,但是,這種行為會造成我們的代碼的耦合性較大,因為我們的對象的創建是需要其他代碼的,這樣,當這部分的代碼發生變化的時候,我們的新對象就要發生變化。哈,這樣好像我是在將依賴注入是一種不好的行為,但是,我必須強調,這里的情況是指我們傳進的對象是特定的對象。比如說,像是上面的例子,我們可以在新建一個js文件,這個js文件里也有一個導出的route函數,那么,如果是我們以前的思維,那么,在我們的server.js中傳進的route函數一定要制定是哪一個,但是,即使你真這么做了,也不需要這么做,因為我們的解釋器能夠找到正確的執行函數(我不知道這里面是怎么發生,這個問題我想,我后面會專門寫篇文章來研究一下,這里就先跳過)。所以,我們的代碼就不會發生於特定的對象耦合的情況,這樣以后如果要修改的話,就會相對容易,也不會有副作用。其實,我感覺這點可能與javascript中解釋器尋找函數的機制是一樣的,因為javascript的函數的調用是可以只寫函數名的!另外,稍微說點題外話,其實上面的話題是與函數式編程有點,由於這方面我根本沒有接觸,所以上面講的自然是小生亂彈了。
上面的例子我們的路由模塊並沒有針對不同的url執行不同的行為,只是我們的服務器能夠與路由溝通了而已。所以,我們的腳步仍要繼續。
function start() { console.log("Request handler 'start' was called."); } function upload() { console.log("Request handler 'upload' was called."); } exports.start = start; exports.upload = upload;
這里的requestHandler模塊中的start()和upload()函數也並沒有什么真正的處理,它們只是占位用的函數,就是我們為我們的start和upload這樣的處理占個位置。然后我們的問題又來了,就是我們到底要將這段代碼放在哪里?這里是將處理動作和路由聯系起來,但是我們需要將這段代碼硬編碼進我們的服務器里嗎?然后這樣寫:
if(request == x){
handler y;
}
這樣我們豈不是要隨着我們所要處理的請求的增加而不斷擴充if!事實證明,當你的代碼中有一大串if,那說明你的代碼一定有問題!所以。正確的做法就是我們再重新寫一個新的模塊,這一次就是requestHandlers模塊,然后再將這個對象傳進來。
我們的代碼如下:
首先,是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對象,而且接下來的語句就讓人很頭疼:handle["/"] = requestHandlers.start.怎么回事?為什么會這樣寫啊?其實,javascritp的對象只是鍵/值對的集合,我們可以想象成是一個鍵位字符串類型的字典,並且值可以是函數。所以,我們這里就完全看到了這種特性,我們的對象甚至可以這樣寫:handle["/"],"/"就是一個鍵,然后它的值就是requestHandlers.start。用這樣的方式就能將我們requestHandlers中的處理行為放進一個對象里。
接下來就是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;
router.js也要相應的做出改變:
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對象相應的值是否是處理函數,如果是,我們就調用。
現在我們的服務器,路由和處理程序已經聯系在一起了。
好了,我們現在要做的就是i,我們不想要我們的瀏覽器就只是返回"Hello Word"這么沒有營養的信息,我們想要我們的處理程序能夠返回有用的相關信息,當然我們完全可以這樣做,就是在相關的處理程序的最后return相關信息。是的,這樣做的確沒有錯,但是,會出現問題,就是我們的代碼會掛!是的,這里就是阻塞問題了。
上面的操作是阻塞的,我們會想要這樣子做,就是當程序還在處理一個請求的同時,我們會想要我們的程序繼續處理新的請求。不要感到不可思議,這是web編程,想想我們平時使用瀏覽器的時候,總是喜歡同時打開多個頁面是吧,如果像是上面那樣做,我們的代碼就會出現阻塞,就會出現一個頁面必須等待另一個頁面打開完成。那么,該怎么才能使用所謂的非阻塞操作呢?
我們先從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;
這里的區別就是我們將response作為參數傳給route函數,然后我們繼續看route函數:
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;
接着我們再將requestHandlers.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;
這里我們注意一下這個新的模塊"child_process”,這個模塊有一個exec函數,這是一個非阻塞的函數,它真的是很可怕的東西!為什么這么說呢?我們來舉個例子:
var exec = require("child_process").exec; function start(){ var content = "empty"; exec("ls-lah", function(error, stdout, stderr){ content = stdout; } return content; }
我們這里的exec執行的是一個shell命令(如果你有過linux編程,一定知道這是什么東西),將當前目錄下的文件信息輸出到瀏覽器,但是我們一旦真正運行,就會發現,輸出是empty。為什么呢?因為我們的exec為什么是非阻塞的呢?因為它回調了一個函數stdout(這個函數的作用就是返回輸出的結果),然后賦給content,但是因為我們的exec是非阻塞的,就算我們的exec的回調還沒結束,但是我們已經執行到下面的return content了,而且這時,content依然是"empty"。所以,明白非阻塞是很重要的,但是我們也要注意非阻塞有可能給我們造成的麻煩。
繼續回到我們上面的例子。為什么我們要將response作為參數並且將這部分的工作移到requestHandlers里呢?因為我們想要的是對response的直接響應。之前我們的代碼是這樣的:我們的服務器將response傳給我們的route,我們的route進行處理好,我們的服務器再對resopnse進行響應,現在是這樣:我們的服務器依然是將response傳給route,但是現在我們的route就已經對response進行響應,這里就少了一層,但是我們的代碼的邏輯更加清晰,因為我們處理response的程序和響應response的程序理應放在一塊。
進行到這一步,我們發現,我們的應用根本沒有什么實際意義!我們還是只是顯示一些信息而已!不行,我們必須做些更有用的東西出來!!這次的目標就是允許用戶上傳文件,然后我們將這個文件顯示出來,比如說圖片。好了,我們先來看看如何處理POST請求。
我們的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;
我們這里的修改就是多了body,是的,我們只是顯示一個文本區供用戶輸入,然后通過POST請求提交給服務器,接着服務器通過處理程序將內容顯示到瀏覽器。所以,這里需要生成帶文本區的表單。
然后我們就來處理upload。這部分當然是采用非阻塞,因為POST請求一般都是非常重的,所謂的重,就是指用戶一般都會上傳大量的內容,如果采用阻塞的操作,就會使用戶的執行動作完全卡死。為了使整個過程都是非阻塞的,node.js會將POST數據分成許多小的數據塊,然后通過觸發特定的事件,將這些小的數據塊傳遞給回調函數,這里特定的事件有data事件(表示有新的小數據塊到了),end事件(表示所有的數據塊已經接受完畢)。所以,我們自然就要告訴解釋器,當這些事件觸發時應該回調哪些函數。我們可以在request上注冊監聽器,如:
request.addListener("data", function(chunk){
...
});
request.addListener("end", function(){
...
});
問題來了,就是這部分代碼應該放在哪里?這種問題當我們在不斷擴展自己程序的功能時非常常見,因為我們必須處理好我們每個模塊應該要處理的功能。這里的話,應該是放在服務器里,因為服務器應該是獲取POST請求的數據並進行處理然后交給應用層處理。所以我們的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事件會不斷收集數據,最后將所有的數據塊傳遞給end事件,end事件中調用的是route函數,因為這是在所有的數據塊接收完畢后才執行的動作。這里還有一個地方,就是每次數據到達的時候都會有輸出日志,這對於最終的生產環境來說是不好的,但是對於開發階段來說卻能讓我們更加直觀的追蹤我們收到的數據。
接着我們要進行的是upload功能,於是我們處理數據的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:
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請求中的數據感興趣的只是里面的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: " + postData); querystring.parse((postData)text);
response.end(); } exports.start = start; exports.upload = upload;
以上的例子只是說明我們該處理文件(text數據),但是我們最終的目標是要處理圖片,所以,我們需要用到formidable模塊,但是這些模塊並不是我們的node.js默認的模塊,我們需要使用外部模塊,我們需要下載並安裝,於是我們可以通過這樣的命令:
npm install formidable;
這樣我們的node.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;
這里我們的requestHandlers.js只需做上面一樣的修改就行,添加一個專門處理文件讀取的fs模塊就行。
接下里就是實現我們的最終要求了。我們要做的第一步就是在/start表單中添加一個上傳元素,這個的實現很簡單,就是將我們的表單進行修改就行,添加一個文件上傳組件(如果是對html的表單上的組件不熟悉的話,抱歉,這里實在是沒有多少篇幅可以講這部分的內容,還請自行百度),如:
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傳遞給formidable的form.parse函數,但是我們有的只是response和postData數組,所以我們只能將request一路從服務器通過路由接着傳遞給我們的處理程序。現在的我們已經可以將所有有關於postData的部分從我們的服務器和處理程序中移除了,因為它們對於我們的上傳文件已經沒有什么作用了。顯示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, request);
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
然后是router.js:
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;
formidable會將我們的上傳文件自動保存到/tmp目錄中,我們需要做的就是保證保存成/tmp/test.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;
現在上圖:




好了,到了這里,我們所要實現的目標已經達成了。相信大家一定對於node.js有了一定的初步了解了。
