Node入門(轉)

原文連接:http://www.nodebeginner.org/index-zh-cn.html

Node入門

做者:  Manuel Kiessling
翻譯:  goddyzhao &  GrayZhang &  MondayChen

 

關於

本書致力於教會你如何用Node.js來開發應用,過程當中會傳授你全部所需的「高級」JavaScript知識。本書毫不是一本「Hello World」的教程。javascript

 

狀態

你正在閱讀的已是本書的最終版。所以,只有當進行錯誤更正以及針對新版本Node.js的改動進行對應的修正時,纔會進行更新。php

本書中的代碼案例都在Node.js 0.6.11版本中測試過,能夠正確工做。html

讀者對象

本書最適合與我有類似技術背景的讀者: 至少對一門諸如Ruby、Python、PHP或者Java這樣面向對象的語言有必定的經驗;對JavaScript處於初學階段,而且徹底是一個Node.js的新手。前端

這裏指的適合對其餘編程語言有必定經驗的開發者,意思是說,本書不會對諸如數據類型、變量、控制結構等等之類很是基礎的概念做介紹。要讀懂本書,這些基礎的概念我都默認你已經會了。java

然而,本書仍是會對JavaScript中的函數和對象做詳細介紹,由於它們與其餘同類編程語言中的函數和對象有很大的不一樣。node

本書結構

讀完本書以後,你將完成一個完整的web應用,該應用容許用戶瀏覽頁面以及上傳文件。git

固然了,應用自己並無什麼了不得的,相比爲了實現該功能書寫的代碼自己,咱們更關注的是如何建立一個框架來對咱們應用的不一樣模塊進行乾淨地剝離。 是否是很玄乎?稍後你就明白了。程序員

本書先從介紹在Node.js環境中進行JavaScript開發和在瀏覽器環境中進行JavaScript開發的差別開始。github

緊接着,會帶領你們完成一個最傳統的「Hello World」應用,這也是最基礎的Node.js應用。web

最後,會和你們討論如何設計一個「真正」完整的應用,剖析要完成該應用須要實現的不一樣模塊,並一步一步介紹如何來實現這些模塊。

能夠確保的是,在這過程當中,你們會學到JavaScript中一些高級的概念、如何使用它們以及爲何使用這些概念就能夠實現而其餘編程語言中同類的概念就沒法實現。

該應用全部的源代碼均可以經過 本書Github代碼倉庫.

目錄

JavaScript與Node.js

JavaScript與你

拋開技術,咱們先來聊聊你以及你和JavaScript的關係。本章的主要目的是想讓你看看,對你而言是否有必要繼續閱讀後續章節的內容。

若是你和我同樣,那麼你很早就開始利用HTML進行「開發」,正因如此,你接觸到了這個叫JavaScript有趣的東西,而對於JavaScript,你只會基本的操做——爲web頁面添加交互。

而你真正想要的是「乾貨」,你想要知道如何構建複雜的web站點 —— 因而,你學習了一種諸如PHP、Ruby、Java這樣的編程語言,並開始書寫「後端」代碼。

與此同時,你還始終關注着JavaScript,隨着經過一些對jQuery,Prototype之類技術的介紹,你慢慢了解到了不少JavaScript中的進階技能,同時也感覺到了JavaScript絕非僅僅是window.open() 那麼簡單。 .

不過,這些畢竟都是前端技術,儘管當想要加強頁面的時候,使用jQuery總讓你以爲很爽,但到最後,你頂可能是個JavaScript用戶,而非JavaScript開發者

而後,出現了Node.js,服務端的JavaScript,這有多酷啊?

因而,你以爲是時候該從新拾起既熟悉又陌生的JavaScript了。可是別急,寫Node.js應用是一件事情;理解爲何它們要以它們書寫的這種方式來書寫則意味着——你要懂JavaScript。此次是玩真的了。

問題來了: 因爲JavaScript真正意義上以兩種,甚至能夠說是三種形態存在(從中世紀90年代的做爲對DHTML進行加強的小玩具,到像jQuery那樣嚴格意義上的前端技術,一直到如今的服務端技術),所以,很難找到一個「正確」的方式來學習JavaScript,使得讓你書寫Node.js應用的時候感受本身是在真正開發它而不只僅是使用它。

由於這就是關鍵: 你自己已是個有經驗的開發者,你不想經過處處尋找各類解決方案(其中可能還有不正確的)來學習新的技術,你要確保本身是經過正確的方式來學習這項技術。

固然了,外面不乏很優秀的學習JavaScript的文章。可是,有的時候光靠那些文章是遠遠不夠的。你須要的是指導。

本書的目標就是給你提供指導。

簡短申明

業界有很是優秀的JavaScript程序員。而我並不是其中一員。

我就是上一節中描述的那個我。我熟悉如何開發後端web應用,可是對「真正」的JavaScript以及Node.js,我都只是新手。我也只是最近學習了一些JavaScript的高級概念,並無實踐經驗。

所以,本書並非一本「從入門到精通」的書,更像是一本「從初級入門到高級入門」的書。

若是成功的話,那麼本書就是我當初開始學習Node.js最但願擁有的教程。

服務端JavaScript

JavaScript最先是運行在瀏覽器中,然而瀏覽器只是提供了一個上下文,它定義了使用JavaScript能夠作什麼,但並無「說」太多關於JavaScript語言自己能夠作什麼。事實上,JavaScript是一門「完整」的語言: 它能夠使用在不一樣的上下文中,其能力與其餘同類語言相比有過之而無不及。

Node.js事實上就是另一種上下文,它容許在後端(脫離瀏覽器環境)運行JavaScript代碼。

要實如今後臺運行JavaScript代碼,代碼須要先被解釋而後正確的執行。Node.js的原理正是如此,它使用了Google的V8虛擬機(Google的Chrome瀏覽器使用的JavaScript執行環境),來解釋和執行JavaScript代碼。

除此以外,伴隨着Node.js的還有許多有用的模塊,它們能夠簡化不少重複的勞做,好比向終端輸出字符串。

所以,Node.js事實上既是一個運行時環境,同時又是一個庫。

要使用Node.js,首先須要進行安裝。關於如何安裝Node.js,這裏就不贅述了,能夠直接參考官方的安裝指南。安裝完成後,繼續回來閱讀本書下面的內容。

「Hello World」

好了,「廢話」很少說了,立刻開始咱們第一個Node.js應用:「Hello World」。

打開你最喜歡的編輯器,建立一個helloworld.js文件。咱們要作就是向STDOUT輸出「Hello World」,以下是實現該功能的代碼:

console.log("Hello World");

保存該文件,並經過Node.js來執行:

node helloworld.js

正常的話,就會在終端輸出Hello World 。

好吧,我認可這個應用是有點無趣,那麼下面咱們就來點「乾貨」。

一個完整的基於Node.js的web應用

用例

咱們來把目標設定得簡單點,不過也要夠實際才行:

  • 用戶能夠經過瀏覽器使用咱們的應用。
  • 當用戶請求http://domain/start時,能夠看到一個歡迎頁面,頁面上有一個文件上傳的表單。
  • 用戶能夠選擇一個圖片並提交表單,隨後文件將被上傳到http://domain/upload,該頁面完成上傳後會把圖片顯示在頁面上。

差很少了,你如今也能夠去Google一下,找點東西亂搞一下來完成功能。可是咱們如今先不作這個。

更進一步地說,在完成這一目標的過程當中,咱們不只僅須要基礎的代碼而無論代碼是否優雅。咱們還要對此進行抽象,來尋找一種適合構建更爲複雜的Node.js應用的方式。

應用不一樣模塊分析

咱們來分解一下這個應用,爲了實現上文的用例,咱們須要實現哪些部分呢?

  • 咱們須要提供Web頁面,所以須要一個HTTP服務器
  • 對於不一樣的請求,根據請求的URL,咱們的服務器須要給予不一樣的響應,所以咱們須要一個路由,用於把請求對應到請求處理程序(request handler)
  • 當請求被服務器接收並經過路由傳遞以後,須要能夠對其進行處理,所以咱們須要最終的請求處理程序
  • 路由還應該能處理POST數據,而且把數據封裝成更友好的格式傳遞給請求處理入程序,所以須要請求數據處理功能
  • 咱們不只僅要處理URL對應的請求,還要把內容顯示出來,這意味着咱們須要一些視圖邏輯供請求處理程序使用,以便將內容發送給用戶的瀏覽器
  • 最後,用戶須要上傳圖片,因此咱們須要上傳處理功能來處理這方面的細節

咱們先來想一想,使用PHP的話咱們會怎麼構建這個結構。通常來講咱們會用一個Apache HTTP服務器並配上mod_php5模塊。
從這個角度看,整個「接收HTTP請求並提供Web頁面」的需求根本不須要PHP來處理。

不過對Node.js來講,概念徹底不同了。使用Node.js時,咱們不只僅在實現一個應用,同時還實現了整個HTTP服務器。事實上,咱們的Web應用以及對應的Web服務器基本上是同樣的。

聽起來好像有一大堆活要作,但隨後咱們會逐漸意識到,對Node.js來講這並非什麼麻煩的事。

如今咱們就來開始實現之路,先從第一個部分--HTTP服務器着手。

構建應用的模塊

一個基礎的HTTP服務器

當我準備開始寫個人第一個「真正的」Node.js應用的時候,我不但不知道怎麼寫Node.js代碼,也不知道怎麼組織這些代碼。 
我應該把全部東西都放進一個文件裏嗎?網上有不少教程都會教你把全部的邏輯都放進一個用Node.js寫的基礎HTTP服務器裏。可是若是我想加入更多的內容,同時還想保持代碼的可讀性呢?

實際上,只要把不一樣功能的代碼放入不一樣的模塊中,保持代碼分離仍是至關簡單的。

這種方法容許你擁有一個乾淨的主文件(main file),你能夠用Node.js執行它;同時你能夠擁有乾淨的模塊,它們能夠被主文件和其餘的模塊調用。

那麼,如今咱們來建立一個用於啓動咱們的應用的主文件,和一個保存着咱們的HTTP服務器代碼的模塊。

在個人印象裏,把主文件叫作index.js或多或少是個標準格式。把服務器模塊放進叫server.js的文件裏則很好理解。

讓咱們先從服務器模塊開始。在你的項目的根目錄下建立一個叫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);

搞定!你剛剛完成了一個能夠工做的HTTP服務器。爲了證實這一點,咱們來運行而且測試這段代碼。首先,用Node.js執行你的腳本:

node server.js

接下來,打開瀏覽器訪問http://localhost:8888/,你會看到一個寫着「Hello World」的網頁。

這頗有趣,不是嗎?讓咱們先來談談HTTP服務器的問題,把如何組織項目的事情先放一邊吧,你以爲如何?我保證以後咱們會解決那個問題的。

分析HTTP服務器

那麼接下來,讓咱們分析一下這個HTTP服務器的構成。

第一行請求(require)Node.js自帶的 http 模塊,而且把它賦值給 http 變量。

接下來咱們調用http模塊提供的函數: createServer 。這個函數會返回一個對象,這個對象有一個叫作 listen 的方法,這個方法有一個數值參數,指定這個HTTP服務器監聽的端口號。

我們暫時先無論 http.createServer 的括號裏的那個函數定義。

咱們原本能夠用這樣的代碼來啓動服務器並偵聽8888端口:

var http = require("http");

var server = http.createServer();
server.listen(8888);

這段代碼只會啓動一個偵聽8888端口的服務器,它不作任何別的事情,甚至連請求都不會應答。

最有趣(並且,若是你以前習慣使用一個更加保守的語言,好比PHP,它還很奇怪)的部分是 createServer() 的第一個參數,一個函數定義。

實際上,這個函數定義是 createServer() 的第一個也是惟一一個參數。由於在JavaScript中,函數和其餘變量同樣都是能夠被傳遞的。

進行函數傳遞

舉例來講,你能夠這樣作:

function say(word) {
  console.log(word);
}

function execute(someFunction, value) {
  someFunction(value);
}

execute(say, "Hello");

請仔細閱讀這段代碼!在這裏,咱們把 say 函數做爲execute函數的第一個變量進行了傳遞。這裏傳遞的不是 say 的返回值,而是 say 自己!

這樣一來, say 就變成了execute 中的本地變量 someFunction ,execute能夠經過調用 someFunction() (帶括號的形式)來使用 say 函數。

固然,由於 say 有一個變量, execute 在調用 someFunction 時能夠傳遞這樣一個變量。

咱們能夠,就像剛纔那樣,用它的名字把一個函數做爲變量傳遞。可是咱們不必定要繞這個「先定義,再傳遞」的圈子,咱們能夠直接在另外一個函數的括號中定義和傳遞這個函數:

function execute(someFunction, value) {
  someFunction(value);
}

execute(function(word){ console.log(word) }, "Hello");

咱們在 execute 接受第一個參數的地方直接定義了咱們準備傳遞給 execute 的函數。

用這種方式,咱們甚至不用給這個函數起名字,這也是爲何它被叫作 匿名函數

這是咱們和我所認爲的「進階」JavaScript的第一次親密接觸,不過咱們仍是得按部就班。如今,咱們先接受這一點:在JavaScript中,一個函數能夠做爲另外一個函數接收一個參數。咱們能夠先定義一個函數,而後傳遞,也能夠在傳遞參數的地方直接定義函數。

函數傳遞是如何讓HTTP服務器工做的

帶着這些知識,咱們再來看看咱們簡約而不簡單的HTTP服務器:

var http = require("http");

http.createServer(function(request, response) {
  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原生的工做方式。它是事件驅動的,這也是它爲何這麼快的緣由。

你也許會想花點時間讀一下Felix Geisendörfer的大做Understanding node.js,它介紹了一些背景知識。

這一切都歸結於「Node.js是事件驅動的」這一事實。好吧,其實我也不是特別確切的瞭解這句話的意思。不過我會試着解釋,爲何它對咱們用Node.js寫網絡應用(Web based application)是有意義的。

當咱們使用 http.createServer 方法的時候,咱們固然不僅是想要一個偵聽某個端口的服務器,咱們還想要它在服務器收到一個HTTP請求的時候作點什麼。

問題是,這是異步的:請求任什麼時候候均可能到達,可是咱們的服務器卻跑在一個單進程中。

寫PHP應用的時候,咱們一點也不爲此擔憂:任什麼時候候當有請求進入的時候,網頁服務器(一般是Apache)就爲這一請求新建一個進程,而且開始從頭至尾執行相應的PHP腳本。

那麼在咱們的Node.js程序中,當一個新的請求到達8888端口的時候,咱們怎麼控制流程呢?

嗯,這就是Node.js/JavaScript的事件驅動設計可以真正幫上忙的地方了——雖然咱們還得學一些新概念才能掌握它。讓咱們來看看這些概念是怎麼應用在咱們的服務器代碼裏的。

咱們建立了服務器,而且向建立它的方法傳遞了一個函數。不管什麼時候咱們的服務器收到一個請求,這個函數就會被調用。

咱們不知道這件事情何時會發生,可是咱們如今有了一個處理請求的地方:它就是咱們傳遞過去的那個函數。至於它是被預先定義的函數仍是匿名函數,就可有可無了。

這個就是傳說中的 回調 。咱們給某個方法傳遞了一個函數,這個方法在有相應事件發生時調用這個函數來進行 回調 。

至少對我來講,須要一些功夫才能弄懂它。你若是仍是不太肯定的話就再去讀讀Felix的博客文章。

讓咱們再來琢磨琢磨這個新概念。咱們怎麼證實,在建立完服務器以後,即便沒有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 輸出了一段文本。在HTTP服務器開始工做以後,也輸出一段文本。

當咱們與往常同樣,運行它node server.js時,它會立刻在命令行上輸出「Server has started.」。當咱們向服務器發出請求(在瀏覽器訪問http://localhost:8888/),「Request received.」這條消息就會在命令行中出現。

這就是事件驅動的異步服務器端JavaScript和它的回調啦!

(請注意,當咱們在服務器訪問網頁時,咱們的服務器可能會輸出兩次「Request received.」。那是由於大部分瀏覽器都會在你訪問 http://localhost:8888/ 時嘗試讀取 http://localhost:8888/favicon.ico )

服務器是如何處理請求的

好的,接下來咱們簡單分析一下咱們服務器代碼中剩下的部分,也就是咱們的回調函數 onRequest() 的主體部分。

當回調啓動,咱們的 onRequest() 函數被觸發的時候,有兩個參數被傳入:request 和 response 。

它們是對象,你能夠使用它們的方法來處理HTTP請求的細節,而且響應請求(好比向發出請求的瀏覽器發回一些東西)。

因此咱們的代碼就是:當收到請求時,使用 response.writeHead() 函數發送一個HTTP狀態200和HTTP頭的內容類型(content-type),使用 response.write() 函數在HTTP相應主體中發送文本「Hello World"。

最後,咱們調用 response.end() 完成響應。

目前來講,咱們對請求的細節並不在乎,因此咱們沒有使用 request 對象。

服務端的模塊放在哪裏

OK,就像我保證過的那樣,咱們如今能夠回到咱們如何組織應用這個問題上了。咱們如今在 server.js 文件中有一個很是基礎的HTTP服務器代碼,並且我提到一般咱們會有一個叫 index.js 的文件去調用應用的其餘模塊(好比 server.js 中的HTTP服務器模塊)來引導和啓動應用。

咱們如今就來談談怎麼把server.js變成一個真正的Node.js模塊,使它能夠被咱們(還沒動工)的 index.js 主文件使用。

也許你已經注意到,咱們已經在代碼中使用了模塊了。像這樣:

var http = require("http");

...

http.createServer(...);

Node.js中自帶了一個叫作「http」的模塊,咱們在咱們的代碼中請求它並把返回值賦給一個本地變量。

這把咱們的本地變量變成了一個擁有全部 http 模塊所提供的公共方法的對象。

給這種本地變量起一個和模塊名稱同樣的名字是一種慣例,可是你也能夠按照本身的喜愛來:

var foo = require("http");

...

foo.createServer(...);

很好,怎麼使用Node.js內部模塊已經很清楚了。咱們怎麼建立本身的模塊,又怎麼使用它呢?

等咱們把 server.js 變成一個真正的模塊,你就能搞明白了。

事實上,咱們不用作太多的修改。把某段代碼變成模塊意味着咱們須要把咱們但願提供其功能的部分 導出 到請求這個模塊的腳本。

目前,咱們的HTTP服務器須要導出的功能很是簡單,由於請求服務器模塊的腳本僅僅是須要啓動服務器而已。

咱們把咱們的服務器腳本放到一個叫作 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請求,服務器應該有不一樣的反應。

對於一個很是簡單的應用來講,你能夠直接在回調函數 onRequest() 中作這件事情。不過就像我說過的,咱們應該加入一些抽象的元素,讓咱們的例子變得更有趣一點兒。

處理不一樣的HTTP請求在咱們的代碼中是一個不一樣的部分,叫作「路由選擇」——那麼,咱們接下來就創造一個叫作 路由 的模塊吧。

如何來進行請求的「路由」

咱們要爲路由提供請求的URL和其餘須要的GET及POST參數,隨後路由須要根據這些數據來執行相應的代碼(這裏「代碼」對應整個應用的第三部分:一系列在接收到請求時真正工做的處理程序)。

所以,咱們須要查看HTTP請求,從中提取出請求的URL以及GET/POST參數。這一功能應當屬於路由仍是服務器(甚至做爲一個模塊自身的功能)確實值得探討,但這裏暫定其爲咱們的HTTP服務器的功能。

咱們須要的全部數據都會包含在request對象中,該對象做爲onRequest()回調函數的第一個參數傳遞。可是爲了解析這些數據,咱們須要額外的Node.JS模塊,它們分別是urlquerystring模塊。

                               url.parse(string).query
                                           |
           url.parse(string).pathname      |
                       |                   |
                       |                   |
                     ------ -------------------
http://localhost:8888/start?foo=bar&hello=world
                                ---       -----
                                 |          |
                                 |          |
              querystring(string)["foo"]    |
                                            |
                         querystring(string)["hello"]

固然咱們也能夠用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;

如你所見,這段代碼什麼也沒幹,不過對於如今來講這是應該的。在添加更多的邏輯之前,咱們先來看看如何把路由和服務器整合起來。

咱們的服務器應當知道路由的存在並加以有效利用。咱們固然能夠經過硬編碼的方式將這一依賴項綁定到服務器上,可是其它語言的編程經驗告訴咱們這會是一件很是痛苦的事,所以咱們將使用依賴注入的方式較鬆散地添加路由模塊(你能夠讀讀Martin Fowlers關於依賴注入的大做來做爲背景知識)。

首先,咱們來擴展一下服務器的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,使得路由函數能夠被注入到服務器中:

var server = require("./server");
var router = require("./router");

server.start(router.route);

在這裏,咱們傳遞的函數依舊什麼也沒作。

若是如今啓動應用(node index.js,始終記得這個命令行),隨後請求一個URL,你將會看到應用輸出相應的信息,這代表咱們的HTTP服務器已經在使用路由模塊了,並會將請求的路徑傳遞給路由:

bash$ node index.js
Request for /foo received.
About to route a request for /foo

(以上輸出已經去掉了比較煩人的/favicon.ico請求相關的部分)。

行爲驅動執行

請容許我再次脫離主題,在這裏談一談函數式編程。

將函數做爲參數傳遞並不只僅出於技術上的考量。對軟件設計來講,這實際上是個哲學問題。想一想這樣的場景:在index文件中,咱們能夠將router對象傳遞進去,服務器隨後能夠調用這個對象的route函數。

就像這樣,咱們傳遞一個東西,而後服務器利用這個東西來完成一些事。嗨那個叫路由的東西,能幫我把這個路由一下嗎?

可是服務器其實不須要這樣的東西。它只須要把事情作完就行,其實爲了把事情作完,你根本不須要東西,你須要的是動做。也就是說,你不須要名詞,你須要動詞

理解了這個概念裏最核心、最基本的思想轉換後,我天然而然地理解了函數編程。

我是在讀了Steve Yegge的大做名詞王國中的死刑以後理解函數編程。你也去讀一讀這本書吧,真的。這是曾給予我閱讀的快樂的關於軟件的書籍之一。

路由給真正的請求處理程序

回到正題,如今咱們的HTTP服務器和請求路由模塊已經如咱們的指望,能夠相互交流了,就像一對親密無間的兄弟。

固然這還遠遠不夠,路由,顧名思義,是指咱們要針對不一樣的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模塊硬編碼到路由裏來使用,仍是再添加一點依賴注入?雖然和其餘模式同樣,依賴注入不該該僅僅爲使用而使用,但在如今這個狀況下,使用依賴注入可讓路由和請求處理程序之間的耦合更加鬆散,也所以能讓路由的重用性更高。

這意味着咱們得將請求處理程序從服務器傳遞到路由中,但感受上這麼作更離譜了,咱們得一路把這堆請求處理程序從咱們的主文件傳遞到服務器中,再將之從服務器傳遞到路由。

那麼咱們要怎麼傳遞這些請求處理程序呢?別看如今咱們只有2個處理程序,在一個真實的應用中,請求處理程序的數量會不斷增長,咱們固然不想每次有一個新的URL或請求處理程序時,都要爲了在路由裏完成請求處處理程序的映射而反覆折騰。除此以外,在路由裏有一大堆if request == x then call handler y也使得系統醜陋不堪。

仔細想一想,有一大堆東西,每一個都要映射到一個字符串(就是請求的URL)上?彷佛關聯數組(associative array)能完美勝任。

不過結果有點使人失望,JavaScript沒提供關聯數組 -- 也能夠說它提供了?事實上,在JavaScript中,真正能提供此類功能的是它的對象。

在這方面,http://msdn.microsoft.com/en-us/magazine/cc163419.aspx有一個不錯的介紹,我在此摘錄一段:

在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映射到相同的請求處理程序上是很容易的:只要在對象中添加一個鍵爲"/"的屬性,對應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]();的表達式,這個感受就像在前方中提到的那樣:「嗨,請幫我處理了這個路徑」。

有了這些,咱們就把服務器、路由和請求處理程序在一塊兒了。如今咱們啓動應用程序並在瀏覽器中訪問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.
About to route a request for /
Request handler 'start' was called.

讓請求處理程序做出響應

很好。不過如今要是請求處理程序可以向瀏覽器返回一些有意義的信息而並不是全是「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,瀏覽器會輸出「Hello Start」,請求http://localhost:8888/upload會輸出「Hello Upload」,而請求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/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的事件輪詢。)

接下來,咱們會介紹一種錯誤的使用非阻塞操做的方式。

和上次同樣,咱們經過修改咱們的應用來暴露問題。

此次咱們仍是拿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返回。

和往常同樣,咱們啓動服務器,而後訪問「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的時候,會花10秒鐘的時間才載入,而當請求http://localhost:8888/upload的時候,會當即響應,縱然這個時候/start響應還在處理中。

更有用的場景

到目前爲止,咱們作的已經很好了,可是,咱們的應用沒有實際用途。

服務器,請求路由以及請求處理程序都已經完成了,下面讓咱們按照此前的用例給網站添加交互:用戶選擇一個文件,上傳該文件,而後在瀏覽器中看到上傳的文件。 爲了保持簡單,咱們假設用戶只會上傳圖片,而後咱們應用將該圖片顯示到瀏覽器中。

好,下面就一步步來實現,鑑於此前已經對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就能夠看到簡單的表單了,要記得重啓服務器哦!

你可能會說:這種直接將視覺元素放在請求處理程序中的方式太醜陋了。說的沒錯,可是,我並不想在本書中介紹諸如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數據,而後將最終的數據傳遞給請求路由和請求處理器,讓他們來進行進一步的處理。

所以,實現思路就是: 將dataend事件的回調函數直接放在服務器中,在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的模塊。

咱們來添加/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>';

    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,就能夠看到保存在/tmp/test.png的圖片了。

好,最後咱們要的就是:

 

  • /start表單中添加一個文件上傳元素
  • 將node-formidable整合到咱們的upload請求處理程序中,用於將上傳的圖片保存到/tmp/test.png
  • 將上傳的圖片內嵌到/uploadURL輸出的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;

好了,重啓服務器,咱們應用全部的功能就能夠用了。選擇一張本地圖片,將其上傳到服務器,而後瀏覽器就會顯示該圖片。

總結與展望

恭喜,咱們的任務已經完成了!咱們開發完了一個Node.js的web應用,應用雖小,但卻「五臟俱全」。 期間,咱們介紹了不少技術點:服務端JavaScript、函數式編程、阻塞與非阻塞、回調、事件、內部和外部模塊等等。

固然了,還有許多本書沒有介紹到的: 如何操做數據庫、如何進行單元測試、如何開發Node.js的外部模塊以及一些簡單的諸如如何獲取GET請求之類的方法。

但本書畢竟只是一本給初學者的教程 —— 不可能覆蓋到全部的內容。

幸運的是,Node.js社區很是活躍(做個不恰當的比喻就是猶如一羣有多動症小孩子在一塊兒,能不活躍嗎?), 這意味着,有許多關於Node.js的資源,有什麼問題均可以向社區尋求解答。 其中Node.js社區的wiki以及 NodeCloud就是最好的資源。

相關文章
相關標籤/搜索