Node.js 原生開發入門完全教程

黎躍春發表於2017-07-04

一、關於

本篇文章致力於教會你如何用Node.js來開發應用,過程中會傳授你所有所需的“高階”JavaScript知識。Node入門看這篇文章就夠了。

二、程式碼狀態

所有程式碼為春哥親測,全部正確通過。

三、閱讀文章的物件

  • 1.有程式設計基礎
  • 2.想轉向Node.js後端的技術愛好者
  • 3.Node.js新手

四、進入正題

1.環境安裝

請直接移步Node.js官網,如下圖所示,直接點選最新版下載並進行安裝。

Node.js安裝完畢後,開啟終端,在終端分別輸入如下命令,檢測是否安裝成功。

Last login: Tue Jun 27 09:19:38 on console
liyuechun:~ yuechunli$ node -v
v8.1.3
liyuechun:~ yuechunli$ npm -v
5.0.3
liyuechun:~ yuechunli$

如果能夠正確顯示node和npm的版本,說明Node.js安裝成功。

2.”Hello World

第一種輸出方式

好了,“廢話”不多說了,馬上開始我們第一個Node.js應用:“Hello World”。

liyuechun:~ yuechunli$ node
> console.log("Hello World!");
Hello World!
undefined
> console.log("從零到壹全棧部落!");
從零到壹全棧部落!
undefined
> process.exit()
liyuechun:~ yuechunli$

在終端裡面直接輸入命令node,接下來輸入一句console.log("Hello World!"); ,回車,即可輸出Hello World

簡單解釋一下為什麼每一次列印後面都出現了一個undefined,原因是因為你輸入js程式碼並按下回車後,node會輸出執行完該程式碼後的返回值,如果沒有返回值,就會顯示undefined,這個跟Chrome的除錯工具相似。

如上程式碼所示,當輸入process.exit()並回車時,即可退出node模式

第二種輸出方式

Last login: Thu Jun 29 18:17:27 on ttys000
liyuechun:~ yuechunli$ ls
Applications        Downloads        Pictures
Creative Cloud Files    Library            Public
Desktop            Movies
Documents        Music
liyuechun:~ yuechunli$ cd Desktop/
liyuechun:Desktop yuechunli$ mkdir nodejs入門
liyuechun:Desktop yuechunli$ pwd
/Users/liyuechun/Desktop
liyuechun:Desktop yuechunli$ cd nodejs入門/
liyuechun:nodejs入門 yuechunli$ pwd
/Users/liyuechun/Desktop/nodejs入門
liyuechun:nodejs入門 yuechunli$ vi helloworld.js
liyuechun:nodejs入門 yuechunli$ cat helloworld.js 
console.log("Hello World!");
liyuechun:nodejs入門 yuechunli$ node helloworld.js 
Hello World!
liyuechun:nodejs入門 yuechunli$

命令解釋:

  • ls:檢視當前路徑下面的檔案和資料夾。
  • pwd:檢視當前所在路徑。
  • cd Desktop:切換到桌面。
  • mkdir nodejs入門:在當前路徑下面建立nodejs入門資料夾。
  • cd nodejs入門:進入nodejs入門資料夾。
  • vi helloworld.js:建立一個helloworld.js檔案,並在檔案裡面輸入console.log("Hello World!"),儲存並退出。
  • cat helloworld.js:檢視helloworld.js檔案內容。
  • node helloworld.js:在當前路徑下面執行helloworld.js檔案。

PS:如果對命令列不熟悉的童鞋,可以用其他編輯器建立一個helloworld.js檔案,在裡面輸入console.log("Hello World!"),將檔案儲存到桌面,然後開啟終端,直接將helloworld.js檔案拖拽到終端,直接在終端中執行node helloworld.js即可在終端輸出Hello World!

好吧,我承認這個應用是有點無趣,那麼下面我們就來點“乾貨”。

下面我們將通過VSCode來進行Node.js的編碼。

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

1.用例

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

  • 使用者可以通過瀏覽器使用我們的應用。
  • 當使用者請求http://domain/start時,可以看到一個歡迎頁面,頁面上有一個檔案上傳的表單。
  • 使用者可以選擇一個圖片並提交表單,隨後檔案將被上傳到http://domain/upload,該頁面完成上傳後會把圖片顯示在頁面上。

差不多了,你現在也可以去Google一下,找點東西亂搞一下來完成功能。但是我們現在先不做這個。

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

2.應用不同模組分析

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

  • 我們需要提供Web頁面,因此需要一個HTTP伺服器
  • 對於不同的請求,根據請求的URL,我們的伺服器需要給予不同的響應,因此我們需要一個路由,用於把請求對應到請求處理程式(request handler)
  • 當請求被伺服器接收並通過路由傳遞之後,需要可以對其進行處理,因此我們需要最終的請求處理程式
  • 路由還應該能處理POST資料,並且把資料封裝成更友好的格式傳遞給請求處理入程式,因此需要請求資料處理功能
  • 我們不僅僅要處理URL對應的請求,還要把內容顯示出來,這意味著我們需要一些檢視邏輯供請求處理程式使用,以便將內容傳送給使用者的瀏覽器
  • 最後,使用者需要上傳圖片,所以我們需要上傳處理功能來處理這方面的細節

現在我們就來開始實現之路,先從第一個部分–HTTP伺服器著手。

六、構建應用的模組

1.一個基礎的HTTP伺服器

用VSCode建立一個server.js的檔案,將檔案儲存到桌面的nodejs入門資料夾裡面。

server.js檔案裡面寫入以下內容:

let http = require("http");

http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}).listen(8888);

上面的程式碼就是一個完整的Node.js伺服器,如下圖所示,點選VSCode左下腳按鈕,開啟VSCode終端,在終端中輸入node server.js來進行驗證。

如上圖所示,一個基礎的HTTP伺服器搞定。

2.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埠的伺服器,它不做任何別的事情,甚至連請求都不會應答。

3.進行函式傳遞

舉例來說,你可以這樣做:

Last login: Thu Jun 29 20:03:25 on ttys001
liyuechun:~ yuechunli$ node
> function say(word) {
...   console.log(word);
... }
undefined
> 
> function execute(someFunction, value) {
...   someFunction(value);
... }
undefined
> 
> execute(say, "Hello");
Hello
undefined
>

請仔細閱讀這段程式碼!在這裡,我們把 say 函式作為execute函式的第一個變數進行了傳遞。這裡傳遞的不是 say 的返回值,而是 say 本身!

這樣一來, say 就變成了execute 中的本地變數 someFunction ,execute可以通過呼叫 someFunction() (帶括號的形式)來使用 say 函式。

當然,因為 say 有一個變數, execute 在呼叫 someFunction 時可以傳遞這樣一個變數。

我們可以,就像剛才那樣,用它的名字把一個函式作為變數傳遞。但是我們不一定要繞這個“先定義,再傳遞”的圈子,我們可以直接在另一個函式的括號中定義和傳遞這個函式:

Last login: Thu Jun 29 20:04:35 on ttys001
liyuechun:~ yuechunli$ node
> function execute(someFunction, value) {
...   someFunction(value);
... }
undefined
> 
> execute(function(word){ console.log(word) }, "Hello");
Hello
undefined
>

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

用這種方式,我們甚至不用給這個函式起名字,這也是為什麼它被叫做 匿名函式 。

這是我們和我所認為的“進階”JavaScript的第一次親密接觸,不過我們還是得循序漸進。現在,我們先接受這一點:在JavaScript中,一個函式可以作為另一個函式接收一個引數。我們可以先定義一個函式,然後傳遞,也可以在傳遞引數的地方直接定義函式。

4.函式傳遞是如何讓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);

console.log("請在瀏覽器中開啟 http://127.0.0.1:8888...");

現在它看上去應該清晰了很多:我們向 createServer 函式傳遞了一個匿名函式。

用這樣的程式碼也可以達到同樣的目的:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
let http = require("http");

//箭頭函式
let onRequest = (request, response) => {
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("Hello World");
    response.end();
}
//把函式當作引數傳遞
http.createServer(onRequest).listen(8888);

console.log("請在瀏覽器中開啟 http://127.0.0.1:8888...");

也許現在我們該問這個問題了:我們為什麼要用這種方式呢?

5.基於事件驅動的回撥

事件驅動是Node.js原生的工作方式,這也是它為什麼這麼快的原因。

當我們使用http.createServer方法的時候,我們當然不只是想要一個偵聽某個埠的伺服器,我們還想要它在伺服器收到一個HTTP請求的時候做點什麼。

我們建立了伺服器,並且向建立它的方法傳遞了一個函式。無論何時我們的伺服器收到一個請求,這個函式就會被呼叫。

這個就是傳說中的回撥 。我們給某個方法傳遞了一個函式,這個方法在有相應事件發生時呼叫這個函式來進行回撥 。

我們試試下面的程式碼:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
let http = require("http");

//箭頭函式
let onRequest = (request, response) => {
    console.log("Request received.");
    response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
    response.write("新增小精靈微信(ershiyidianjian),加入全棧部落");
    response.end();
}
//把函式當作引數傳遞
http.createServer(onRequest).listen(8888);

console.log("Server has started.");
console.log("請在瀏覽器中開啟 http://127.0.0.1:8888...");

在上圖中,當我們執行node server.js命令時,Server has started.正常往下執行。

我們看看當我們在瀏覽器裡面開啟http://127.0.0.1:8888時會發生什麼。

大家會發現在瀏覽器中開啟http://127.0.0.1:8888時,在終端會輸出Request received.,瀏覽器會輸出新增小精靈微信(ershiyidianjian),加入全棧部落這一句話。

請注意,當我們在伺服器訪問網頁時,我們的伺服器可能會輸出兩次“Request received.”。那是因為大部分瀏覽器都會在你訪問 http://localhost:8888/ 時嘗試讀取 http://localhost:8888/favicon… )

6.伺服器是如何處理請求的

好的,接下來我們簡單分析一下我們伺服器程式碼中剩下的部分,也就是我們的回撥函式onRequest()的主體部分。

當回撥啟動,我們的onRequest()函式被觸發的時候,有兩個引數被傳入:request 和response 。

它們是物件,你可以使用它們的方法來處理HTTP請求的細節,並且響應請求(比如向發出請求的瀏覽器發回一些東西)。

所以我們的程式碼就是:當收到請求時,使用response.writeHead()函式傳送一個HTTP狀態200和HTTP頭的內容型別(content-type),使用response.write()函式在HTTP相應主體中傳送文字新增小精靈微信(ershiyidianjian),加入全棧部落

最後,我們呼叫 response.end() 完成響應。

目前來說,我們對請求的細節並不在意,所以我們沒有使用 request 物件。

7.服務端模組化

何為模組

let http = require("http");
...
http.createServer(...);

在上面的程式碼中,Node.js中自帶了一個叫做“http”的模組,我們在我們的程式碼中請求它並把返回值賦給一個本地變數。

這把我們的本地變數變成了一個擁有所有 http 模組所提供的公共方法的物件。

給這種本地變數起一個和模組名稱一樣的名字是一種慣例,但是你也可以按照自己的喜好來:

var foo = require("http");
...
foo.createServer(...);

如何自定義模組

server.js檔案的內容改成下面的內容。

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
let http = require("http");

//用一個函式將之前的內容包裹起來
let start = () => {
        //箭頭函式
    let onRequest = (request, response) => {
        console.log("Request received.");
        response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
        response.write("新增小精靈微信(ershiyidianjian),加入全棧部落");
        response.end();
    }
    //把函式當作引數傳遞
    http.createServer(onRequest).listen(8888);

    console.log("Server has started.");
    console.log("請在瀏覽器中開啟 http://127.0.0.1:8888...");
}

//匯出`server`物件,物件中包含一個start函式
//物件格式為
/**
 * {
 *    start
 * }
 */

//這個物件匯入到其他檔案中即可使用,可以用任意的名字來接收這個物件

exports.start = start;

server.js當前的檔案路徑下新建一個index.js檔案。內容如下:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//從`server`模組中匯入server物件

let server = require('./server');

//啟動伺服器
server.start();

如下圖所示執行index.js檔案。

一切執行正常,上面的案例中,server.js就是自定義的模組。

8.如何來進行請求的“路由”

我們要為路由提供請求的URL和其他需要的GET及POST引數,隨後路由需要根據這些資料來執行相應的程式碼(這裡“程式碼”對應整個應用的第三部分:一系列在接收到請求時真正工作的處理程式)。

因此,我們需要檢視HTTP請求,從中提取出請求的URL以及GET/POST引數。這一功能應當屬於路由還是伺服器(甚至作為一個模組自身的功能)確實值得探討,但這裡暫定其為我們的HTTP伺服器的功能。

我們需要的所有資料都會包含在request物件中,該物件作為onRequest()回撥函式的第一個引數傳遞。但是為了解析這些資料,我們需要額外的Node.JS模組,它們分別是url和querystring模組。

                               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路徑:

接下來我在終端執行node index.js命令,如下所示:

bogon:如何來進行請求的“路由” yuechunli$ node index.js
Server has started.
請在瀏覽器中開啟 http://127.0.0.1:8888...

我先在Safari瀏覽器中開啟http://127.0.0.1:8888,瀏覽器展示效果如下:

控制檯效果如下:

bogon:如何來進行請求的“路由” yuechunli$ node index.js
Server has started.
請在瀏覽器中開啟 http://127.0.0.1:8888...
Request for / received.

接著我在Google瀏覽器裡面開啟 http://127.0.0.1:8888… ,瀏覽器效果圖如下:

控制檯效果如下:

為什麼在Safari瀏覽器中進行請求時,只列印了一個Request for / received.,而在Google瀏覽器中訪問時,會多列印一個Request for /favicon.ico received.,如上圖所示,原因是因為在Google瀏覽器中,瀏覽器的原因會去嘗試請求favicon.ico小圖示。

為了演示效果,還有不受Google瀏覽器的favicon.ico請求的干擾,我接著在Safari裡面請求http://127.0.0.1:8888/starthttp://127.0.0.1:8888/upload,我們看看控制檯展示的內容是什麼。

bogon:如何來進行請求的“路由” yuechunli$ node index.js
Server has started.
請在瀏覽器中開啟 http://127.0.0.1:8888...
Request for /start received.
Request for /upload received.

好了,我們的應用現在可以通過請求的URL路徑來區別不同請求了–這使我們得以使用路由(還未完成)來將請求以URL路徑為基準對映到處理程式上。

在我們所要構建的應用中,這意味著來自/start/upload的請求可以使用不同的程式碼來處理。稍後我們將看到這些內容是如何整合到一起的。

現在我們可以來編寫路由了,建立一個名為router.js的檔案,新增以下內容:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

function route(pathname) {
  console.log("About to route a request for " + pathname);
}

exports.route = route;

如你所見,這段程式碼什麼也沒幹,不過對於現在來說這是應該的。在新增更多的邏輯以前,我們先來看看如何把路由和伺服器整合起來。

首先,我們來擴充套件一下伺服器的start()函式,以便將路由函式作為引數傳遞過去:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
let http = require("http");

let url = require("url");

//用一個函式將之前的內容包裹起來
let start = (route) => {
        //箭頭函式
    let onRequest = (request, response) => {

        let pathname = url.parse(request.url).pathname;
        console.log("Request for " + pathname + " received.");
        route(pathname);

        response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
        response.write("新增小精靈微信(ershiyidianjian),加入全棧部落");
        response.end();
    }
    //把函式當作引數傳遞
    http.createServer(onRequest).listen(8888);

    console.log("Server has started.");
    console.log("請在瀏覽器中開啟 http://127.0.0.1:8888...");
}

exports.start = start;

同時,我們會相應擴充套件index.js,使得路由函式可以被注入到伺服器中:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//從`server`模組中匯入server物件

let server = require('./server');
let router = require("./router");

//啟動伺服器
server.start(router.route);

在這裡,我們傳遞的函式依舊什麼也沒做。

如果現在啟動應用(node index.js,始終記得這個命令列),隨後請求一個URL,你將會看到應用輸出相應的資訊,這表明我們的HTTP伺服器已經在使用路由模組了,並會將請求的路徑傳遞給路由:

bogon:如何來進行請求的“路由” v2.0 yuechunli$ node index.js
Server has started.
請在瀏覽器中開啟 http://127.0.0.1:8888...
Request for / received.
About to route a request for /

9.路由給真正的請求處理程式

現在我們的HTTP伺服器和請求路由模組已經如我們的期望,可以相互交流了,就像一對親密無間的兄弟。

當然這還遠遠不夠,路由,顧名思義,是指我們要針對不同的URL有不同的處理方式。例如處理/start的“業務邏輯”就應該和處理/upload的不同。

在現在的實現下,路由過程會在路由模組中“結束”,並且路由模組並不是真正針對請求“採取行動”的模組,否則當我們的應用程式變得更為複雜時,將無法很好地擴充套件。

我們暫時把作為路由目標的函式稱為請求處理程式。現在我們不要急著來開發路由模組,因為如果請求處理程式沒有就緒的話,再怎麼完善路由模組也沒有多大意義。

應用程式需要新的部件,因此加入新的模組 — 已經無需為此感到新奇了。我們來建立一個叫做requestHandlers的模組,並對於每一個請求處理程式,新增一個佔位用函式,隨後將這些函式作為模組的方法匯出:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

function start() {
  console.log("Request handler 'start' was called.");
}

function upload() {
  console.log("Request handler 'upload' was called.");
}

exports.start = start;
exports.upload = upload;

現在我們將一系列請求處理程式通過一個物件來傳遞,並且需要使用鬆耦合的方式將這個物件注入到route()函式中。

我們先將這個物件引入到主檔案index.js中:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//從`server`模組中匯入server物件

let server = require('./server');
let router = require("./router");
let 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修改如下:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
let http = require("http");

let url = require("url");

//用一個函式將之前的內容包裹起來
let start = (route,handle) => {
        //箭頭函式
    let onRequest = (request, response) => {

        let pathname = url.parse(request.url).pathname;
        console.log("Request for " + pathname + " received.");
        route(handle,pathname);

        response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
        response.write("新增小精靈微信(ershiyidianjian),加入全棧部落");
        response.end();
    }
    //把函式當作引數傳遞
    http.createServer(onRequest).listen(8888);

    console.log("Server has started.");
    console.log("請在瀏覽器中開啟 http://127.0.0.1:8888...");
}

exports.start = start;

這樣我們就在start()函式裡新增了handle引數,並且把handle物件作為第一個引數傳遞給了route()回撥函式。

然後我們相應地在route.js檔案中修改route()函式:

有了這些,我們就把伺服器、路由和請求處理程式在一起了。現在我們啟動應用程式並在瀏覽器中訪問http://127.0.0.1:8888/start,以下日誌可以說明系統呼叫了正確的請求處理程式:

bogon:路由給真正的請求處理程式 yuechunli$ node index.js
Server has started.
請在瀏覽器中開啟 http://127.0.0.1:8888...
Request for /start received.
About to route a request for /start
Request handler 'start' was called.

並且在瀏覽器中開啟http://127.0.0.1:8888/可以看到這個請求同樣被start請求處理程式處理了:

bogon:路由給真正的請求處理程式 yuechunli$ node index.js
Server has started.
請在瀏覽器中開啟 http://127.0.0.1:8888...
Request for / received.
About to route a request for /
Request handler 'start' was called.

10.讓請求處理程式作出響應

很好。不過現在要是請求處理程式能夠向瀏覽器返回一些有意義的資訊而並非全是新增小精靈微信(ershiyidianjian),加入全棧部落,那就更好了。

這裡要記住的是,瀏覽器發出請求後獲得並顯示的新增小精靈微信(ershiyidianjian),加入全棧部落資訊仍是來自於我們server.js檔案中的onRequest函式。

其實“處理請求”說白了就是“對請求作出響應”,因此,我們需要讓請求處理程式能夠像onRequest函式那樣可以和瀏覽器進行“對話”。

11.不好的實現方式

修改requestHandler.js檔案內容如下:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

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修改為如下形式:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

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進行重構以使得它能夠將請求處理程式通過請求路由返回的內容響應給瀏覽器,如下所示:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
let http = require("http");

let url = require("url");

//用一個函式將之前的內容包裹起來
let start = (route,handle) => {
        //箭頭函式
    let onRequest = (request, response) => {

        let pathname = url.parse(request.url).pathname;
        console.log("Request for " + pathname + " received.");
        route(handle,pathname);

        response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
        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

好,那麼問題在哪裡呢?簡單的說就是: 當未來有請求處理程式需要進行非阻塞的操作的時候,我們的應用就“掛”了。

沒理解?沒關係,下面就來詳細解釋下。

12.阻塞與非阻塞

我們先不解釋這裡阻塞非阻塞,我們來修改下start請求處理程式,我們讓它等待10秒以後再返回Hello Start。因為,JavaScript中沒有類似sleep()這樣的操作,所以這裡只能夠來點小Hack來模擬實現。

讓我們將requestHandlers.js修改成如下形式:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

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;

上述程式碼中,我先呼叫了upload(),會和此前一樣立即返回。當函式start()被呼叫的時候,Node.js會先等待10秒,之後才會返回“Hello Start”。如下圖所示,等待中:

(當然了,這裡只是模擬休眠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請求處理程式來“開刀”。將其修改成如下形式:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//我們引入了一個新的Node.js模組,child_process。之所以用它,是為了實現一個既簡單又實用的非阻塞操作:exec()。
var exec = require("child_process").exec;

function start() {
  console.log("Request handler 'start' was called.");

  /**
   * exec()做了什麼呢?
   * 它從Node.js來執行一個shell命令。
   * 在本例子中,我們用它來獲取當前目錄下所有的檔案(“ls -lah”)
   * 然後,當`/start` URL請求的時候將檔案資訊輸出到瀏覽器中。
   * 下面的程式碼非常直觀的: 
   * 建立了一個新的變數content(初始值為“empty”)。
   * 執行“ls -lah”命令,將結果賦值給content,最後將content返回。
   */
  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;

和往常一樣,我們啟動伺服器,然後訪問“http://localhost:8888/start” 。

載入一個漂亮的web頁面,其內容為“empty”。怎麼回事?

如果想要證明這一點,可以將“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 /”命令執行完成之後才會被呼叫。

那究竟我們要如何才能實現將當前目錄下的檔案列表顯示給使用者呢?

好,瞭解了這種不好的實現方式之後,我們接下來來介紹如何以正確的方式讓請求處理程式對瀏覽器請求作出響應。

13.以非阻塞操作進行請求響應

我剛剛提到了這樣一個短語 —— “正確的方式”。而事實上通常“正確的方式”一般都不簡單。

不過,用Node.js就有這樣一種實現方案: 函式傳遞。下面就讓我們來具體看看如何實現。

到目前為止,我們的應用已經可以通過應用各層之間傳遞值的方式(請求處理程式 -> 請求路由 -> 伺服器)將請求處理程式返回的內容(請求處理程式最終要顯示給使用者的內容)傳遞給HTTP伺服器。

現在我們採用如下這種新的實現方式:相對採用將內容傳遞給伺服器的方式,我們這次採用將伺服器“傳遞”給內容的方式。 從實踐角度來說,就是將response物件(從伺服器的回撥函式onRequest()獲取)通過請求路由傳遞給請求處理程式。 隨後,處理程式就可以採用該物件上的函式來對請求作出響應。

原理就是如此,接下來讓我們來一步步實現這種方案。

先從server.js開始:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
let http = require("http");

let url = require("url");

//用一個函式將之前的內容包裹起來
let start = (route,handle) => {
        //箭頭函式
    let onRequest = (request, response) => {

        let 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:

同樣的模式:相對此前從請求處理程式中獲取返回值,這次取而代之的是直接傳遞response物件。

如果沒有對應的請求處理器處理,我們就直接返回“404”錯誤。

最後,我們將requestHandler.js修改為如下形式:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//我們引入了一個新的Node.js模組,child_process。之所以用它,是為了實現一個既簡單又實用的非阻塞操作:exec()。
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),一切都會工作的很好。

在瀏覽器中開啟 http:127.0.0.0:8888/start 效果圖如下所示:

在瀏覽器中開啟 http:127.0.0.0:8888/upload 效果圖如下所示:

如果想要證明/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響應還在處理中。

14.更有用的場景

到目前為止,我們做的已經很好了,但是,我們的應用沒有實際用途。

伺服器,請求路由以及請求處理程式都已經完成了,下面讓我們按照此前的用例給網站新增互動:使用者選擇一個檔案,上傳該檔案,然後在瀏覽器中看到上傳的檔案。 為了保持簡單,我們假設使用者只會上傳圖片,然後我們應用將該圖片顯示到瀏覽器中。

好,下面就一步步來實現,鑑於此前已經對JavaScript原理性技術性的內容做過大量介紹了,這次我們加快點速度。

要實現該功能,分為如下兩步: 首先,讓我們來看看如何處理POST請求(非檔案上傳),之後,我們使用Node.js的一個用於檔案上傳的外部模組。之所以採用這種實現方式有兩個理由。

第一,儘管在Node.js中處理基礎的POST請求相對比較簡單,但在這過程中還是能學到很多。 第二,用Node.js來處理檔案上傳(multipart POST請求)是比較複雜的,它不在本文的範疇,但是,如何使用外部模組卻是在本書涉獵內容之內。

15.處理POST請求

考慮這樣一個簡單的例子:我們顯示一個文字區(textarea)供使用者輸入內容,然後通過POST請求提交給伺服器。最後,伺服器接受到請求,通過處理程式將輸入的內容展示到瀏覽器中。

/start請求處理程式用於生成帶文字區的表單,因此,我們將requestHandlers.js修改為如下形式:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//我們引入了一個新的Node.js模組,child_process。之所以用它,是為了實現一個既簡單又實用的非阻塞操作:exec()。
var exec = require("child_process").exec;

function start(response) {
  console.log("Request handler 'start' was called.");

  let 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="5" cols="60"></textarea>'+
    '<input type="submit" value="Submit text" />'+
    '</form>'+
    '</body>'+
    '</html>';

    response.writeHead(200, {"Content-Type": "text/html;charset=utf-8"});
    response.write(body);
    response.end();
}

function upload(response) {
  console.log("Request handler 'upload' was called.");
  response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
  response.write("Hello Upload");
  response.end();
}

exports.start = start;
exports.upload = upload;

瀏覽器請求http://127.0.0.1:8888/start,效果圖如下:

餘下的篇幅,我們來探討一個更有趣的問題: 當使用者提交表單時,觸發/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開始:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
let http = require("http");

let url = require("url");

//用一個函式將之前的內容包裹起來
let start = (route,handle) => {
        //箭頭函式
    let onRequest = (request, response) => {

        let postData = "";
        let 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為如下形式:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

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請求的響應中:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//我們引入了一個新的Node.js模組,child_process。之所以用它,是為了實現一個既簡單又實用的非阻塞操作:exec()。
var exec = require("child_process").exec;

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模組來實現:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//我們引入了一個新的Node.js模組,child_process。之所以用它,是為了實現一個既簡單又實用的非阻塞操作:exec()。
var exec = require("child_process").exec;
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;

下面我們瀏覽器中訪問http://127.0.0.1:8888/start,如下圖所示:

點選Submit text按鈕,將跳轉到http://127.0.0.1:8888/upload,效果圖如下:

好了,這就是完整的POST請求。

15.處理檔案上傳

最後,我們來實現我們最終的用例:允許使用者上傳圖片,並將該圖片在瀏覽器中顯示出來。

我們通過它能學到這樣兩件事情:

  • 如何安裝外部Node.js模組
  • 以及如何將它們應用到我們的應用中

這裡我們要用到的外部模組是Felix Geisendörfer開發的node-formidable模組。它對解析上傳的檔案資料做了很好的抽象。 其實說白了,處理檔案上傳“就是”處理POST資料 —— 但是,麻煩的是在具體的處理細節,所以,這裡採用現成的方案更合適點。

使用該模組,首先需要安裝該模組。Node.js有它自己的包管理器,叫NPM。它可以讓安裝Node.js的外部模組變得非常方便。

首先在當前專案路徑下面通過npm init建立package.json檔案:

PS:在終端輸入npm init後,一路回車即可。新增的package.json檔案的內容如下:

{
  "author" : "liyuechun",
  "description" : "",
  "license" : "ISC",
  "main" : "index.js",
  "name" : "fileupload",
  "scripts" : {
    "test" : "echo \"Error: no test specified\" && exit 1"
  },
  "version" : "1.0.0"
}

接下來,在終端輸入如下命令安裝formidable外部模組。
如下所示:

liyuechun:fileupload yuechunli$ ls
index.js                requestHandlers.js      server.js
package.json            router.js
liyuechun:fileupload yuechunli$ npm install formidable
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN fileupload@1.0.0 No description
npm WARN fileupload@1.0.0 No repository field.

+ formidable@1.1.1
added 1 package in 1.117s
liyuechun:fileupload yuechunli$

package.json檔案變化如下:

{
  "author": "liyuechun",
  "description": "",
  "license": "ISC",
  "main": "index.js",
  "name": "fileupload",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "version": "1.0.0",
  "dependencies": {
    "formidable": "^1.1.1"
  }
}

專案整體變化如下圖所示:

現在我們就可以用formidable模組了——使用外部模組與內部模組類似,用require語句將其引入即可:

let formidable = require("formidable");

這裡該模組做的就是將通過HTTP POST請求提交的表單,在Node.js中可以被解析。我們要做的就是建立一個新的IncomingForm,它是對提交表單的抽象表示,之後,就可以用它解析request物件,獲取表單中需要的資料欄位。

node-formidable官方的例子展示了這兩部分是如何融合在一起工作的:

let 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
    let 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修改為如下形式:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

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中的路由對映表中:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//從`server`模組中匯入server物件

let server = require('./server');
let router = require("./router");
let 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圖片,我們在當前專案路徑下面新增tmp資料夾,在往裡面拖拽一張圖片,命名為test.png

再重新啟動伺服器,訪問http://localhost:8888/show檢視效果如下:

我們繼續,從server.js開始 —— 移除對postData的處理以及request.setEncoding (這部分node-formidable自身會處理),轉而採用將request物件傳遞給請求路由的方式:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

//請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
let http = require("http");

let url = require("url");

//用一個函式將之前的內容包裹起來
let start = (route,handle) => {
        //箭頭函式
    let onRequest = (request, response) => {

        let 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物件:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

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所示:

/**
 * 從零到壹全棧部落,新增小精靈微信(ershiyidianjian)
 */

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;

重啟伺服器,瀏覽器訪問http://127.0.0.1:8888,效果圖如下:

選擇一張圖片上傳,檢視效果:

16.原始碼下載

所有原始碼:https://github.com/fullstacktribe/001nodejs

相關文章