国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Node.js原生開發(fā)入門完全教程

alphahans / 1040人閱讀

摘要:原生開發(fā)入門完全教程微信公眾號(hào)開發(fā)企業(yè)級(jí)產(chǎn)品全棧開發(fā)速成周末班首期班號(hào)正式開班,歡迎搶座一關(guān)于本篇文章參考了入門并從零到壹操作了一遍,感謝原作者,同時(shí)也強(qiáng)烈推薦大家移步到原文給予原文作者一個(gè)贊賞支持。

Node.js原生開發(fā)入門完全教程

(Node+Vue+React+微信公眾號(hào)開發(fā))企業(yè)級(jí)產(chǎn)品全棧開發(fā)速成周末班首期班(10.28號(hào)正式開班,歡迎搶座)

一、關(guān)于

本篇文章參考了Node入門https://www.nodebeginner.org/index-zh-cn.html#about并從零到壹操作了一遍,感謝原作者,同時(shí)也強(qiáng)烈推薦大家移步到原文給予原文作者一個(gè)贊賞支持。

二、代碼狀態(tài)

所有代碼為春哥親測(cè),全部正確通過。

三、閱讀文章的對(duì)象

1.有編程基礎(chǔ)
2.想轉(zhuǎn)向Node.js后端的技術(shù)愛好者
3.Node.js新手

四、進(jìn)入正題 1.環(huán)境安裝

請(qǐng)直接移步Node.js官網(wǎng),如下圖所示,直接點(diǎn)擊最新版下載并進(jìn)行安裝。

Node.js安裝完畢后,打開終端,在終端分別輸入如下命令,檢測(cè)是否安裝成功。

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"

第一種輸出方式

好了,“廢話”不多說了,馬上開始我們第一個(gè)Node.js應(yīng)用:“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

簡(jiǎn)單解釋一下為什么每一次打印后面都出現(xiàn)了一個(gè)undefined,原因是因?yàn)槟爿斎雑s代碼并按下回車后,node會(huì)輸出執(zhí)行完該代碼后的返回值,如果沒有返回值,就會(huì)顯示undefined,這個(gè)跟Chrome的調(diào)試工具相似。

如上代碼所示,當(dāng)輸入process.exit()并回車時(shí),即可退出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:查看當(dāng)前路徑下面的文件和文件夾。
pwd:查看當(dāng)前所在路徑。
cd Desktop:切換到桌面。
mkdir nodejs入門:在當(dāng)前路徑下面創(chuàng)建nodejs入門文件夾。
cd nodejs入門:進(jìn)入nodejs入門文件夾。
vi helloworld.js:創(chuàng)建一個(gè)helloworld.js文件,并在文件里面輸入console.log("Hello World!"),保存并退出。
cat helloworld.js:查看helloworld.js文件內(nèi)容。
node helloworld.js:在當(dāng)前路徑下面執(zhí)行helloworld.js文件。

PS:如果對(duì)命令行不熟悉的童鞋,可以用其他編輯器創(chuàng)建一個(gè)helloworld.js文件,在里面輸入console.log("Hello World!"),將文件保存到桌面,然后打開終端,直接將helloworld.js文件拖拽到終端,直接在終端中執(zhí)行node helloworld.js即可在終端輸出Hello World!

好吧,我承認(rèn)這個(gè)應(yīng)用是有點(diǎn)無趣,那么下面我們就來點(diǎn)“干貨”。

下面我們將通過VSCode來進(jìn)行Node.js的編碼。

五、一個(gè)完整的基于Node.js的web應(yīng)用 1.用例

我們來把目標(biāo)設(shè)定得簡(jiǎn)單點(diǎn),不過也要夠?qū)嶋H才行:

用戶可以通過瀏覽器使用我們的應(yīng)用。

當(dāng)用戶請(qǐng)求http://domain/start時(shí),可以看到一個(gè)歡迎頁面,頁面上有一個(gè)文件上傳的表單。

用戶可以選擇一個(gè)圖片并提交表單,隨后文件將被上傳到http://domain/upload,該頁面完成上傳后會(huì)把圖片顯示在頁面上。

差不多了,你現(xiàn)在也可以去Google一下,找點(diǎn)東西亂搞一下來完成功能。但是我們現(xiàn)在先不做這個(gè)。

更進(jìn)一步地說,在完成這一目標(biāo)的過程中,我們不僅僅需要基礎(chǔ)的代碼而不管代碼是否優(yōu)雅。我們還要對(duì)此進(jìn)行抽象,來尋找一種適合構(gòu)建更為復(fù)雜的Node.js應(yīng)用的方式。

2.應(yīng)用不同模塊分析

我們來分解一下這個(gè)應(yīng)用,為了實(shí)現(xiàn)上文的用例,我們需要實(shí)現(xiàn)哪些部分呢?

我們需要提供Web頁面,因此需要一個(gè)HTTP服務(wù)器

對(duì)于不同的請(qǐng)求,根據(jù)請(qǐng)求的URL,我們的服務(wù)器需要給予不同的響應(yīng),因此我們需要一個(gè)路由,用于把請(qǐng)求對(duì)應(yīng)到請(qǐng)求處理程序(request handler)

當(dāng)請(qǐng)求被服務(wù)器接收并通過路由傳遞之后,需要可以對(duì)其進(jìn)行處理,因此我們需要最終的請(qǐng)求處理程序

路由還應(yīng)該能處理POST數(shù)據(jù),并且把數(shù)據(jù)封裝成更友好的格式傳遞給請(qǐng)求處理入程序,因此需要請(qǐng)求數(shù)據(jù)處理功能

我們不僅僅要處理URL對(duì)應(yīng)的請(qǐng)求,還要把內(nèi)容顯示出來,這意味著我們需要一些視圖邏輯供請(qǐng)求處理程序使用,以便將內(nèi)容發(fā)送給用戶的瀏覽器

最后,用戶需要上傳圖片,所以我們需要上傳處理功能來處理這方面的細(xì)節(jié)

現(xiàn)在我們就來開始實(shí)現(xiàn)之路,先從第一個(gè)部分--HTTP服務(wù)器著手。

六、構(gòu)建應(yīng)用的模塊 1.一個(gè)基礎(chǔ)的HTTP服務(wù)器

用VSCode創(chuàng)建一個(gè)server.js的文件,將文件保存到桌面的nodejs入門文件夾里面。

server.js文件里面寫入以下內(nèi)容:

let http = require("http");

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

上面的代碼就是一個(gè)完整的Node.js服務(wù)器,如下圖所示,點(diǎn)擊VSCode左下腳按鈕,打開VSCode終端,在終端中輸入node server.js來進(jìn)行驗(yàn)證。

如上圖所示,一個(gè)基礎(chǔ)的HTTP服務(wù)器搞定。

2.HTTP服務(wù)器原理解析

上面的案例中,第一行請(qǐng)求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。

接下來我們調(diào)用http模塊提供的函數(shù): createServer 。這個(gè)函數(shù)會(huì)返回一個(gè)對(duì)象,這個(gè)對(duì)象有一個(gè)叫做 listen 的方法,這個(gè)方法有一個(gè)數(shù)值參數(shù),指定這個(gè)HTTP服務(wù)器監(jiān)聽的端口號(hào)。

咱們暫時(shí)先不管 http.createServer 的括號(hào)里的那個(gè)函數(shù)定義。

我們本來可以用這樣的代碼來啟動(dòng)服務(wù)器并偵聽8888端口:

var http = require("http");

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

這段代碼只會(huì)啟動(dòng)一個(gè)偵聽8888端口的服務(wù)器,它不做任何別的事情,甚至連請(qǐng)求都不會(huì)應(yīng)答。

3.進(jìn)行函數(shù)傳遞

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

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
> 

請(qǐng)仔細(xì)閱讀這段代碼!在這里,我們把 say 函數(shù)作為execute函數(shù)的第一個(gè)變量進(jìn)行了傳遞。這里傳遞的不是 say 的返回值,而是 say 本身!

這樣一來, say 就變成了execute 中的本地變量 someFunction ,execute可以通過調(diào)用 someFunction() (帶括號(hào)的形式)來使用 say 函數(shù)。

當(dāng)然,因?yàn)?say 有一個(gè)變量, execute 在調(diào)用 someFunction 時(shí)可以傳遞這樣一個(gè)變量。

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

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
> 

我們?cè)?execute 接受第一個(gè)參數(shù)的地方直接定義了我們準(zhǔn)備傳遞給 execute 的函數(shù)。

用這種方式,我們甚至不用給這個(gè)函數(shù)起名字,這也是為什么它被叫做 匿名函數(shù) 。

這是我們和我所認(rèn)為的“進(jìn)階”JavaScript的第一次親密接觸,不過我們還是得循序漸進(jìn)。現(xiàn)在,我們先接受這一點(diǎn):在JavaScript中,一個(gè)函數(shù)可以作為另一個(gè)函數(shù)接收一個(gè)參數(shù)。我們可以先定義一個(gè)函數(shù),然后傳遞,也可以在傳遞參數(shù)的地方直接定義函數(shù)。

4.函數(shù)傳遞是如何讓HTTP服務(wù)器工作的

帶著這些知識(shí),我們?cè)賮砜纯次覀兒?jiǎn)約而不簡(jiǎn)單的HTTP服務(wù)器:

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("請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...");

現(xiàn)在它看上去應(yīng)該清晰了很多:我們向 createServer 函數(shù)傳遞了一個(gè)匿名函數(shù)。

用這樣的代碼也可以達(dá)到同樣的目的:

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

//請(qǐng)求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。
let http = require("http");

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

console.log("請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...");

也許現(xiàn)在我們?cè)搯栠@個(gè)問題了:我們?yōu)槭裁匆眠@種方式呢?

5.基于事件驅(qū)動(dòng)的回調(diào)

事件驅(qū)動(dòng)是Node.js原生的工作方式,這也是它為什么這么快的原因。

當(dāng)我們使用http.createServer方法的時(shí)候,我們當(dāng)然不只是想要一個(gè)偵聽某個(gè)端口的服務(wù)器,我們還想要它在服務(wù)器收到一個(gè)HTTP請(qǐng)求的時(shí)候做點(diǎn)什么。

我們創(chuàng)建了服務(wù)器,并且向創(chuàng)建它的方法傳遞了一個(gè)函數(shù)。無論何時(shí)我們的服務(wù)器收到一個(gè)請(qǐng)求,這個(gè)函數(shù)就會(huì)被調(diào)用。

這個(gè)就是傳說中的回調(diào) 。我們給某個(gè)方法傳遞了一個(gè)函數(shù),這個(gè)方法在有相應(yīng)事件發(fā)生時(shí)調(diào)用這個(gè)函數(shù)來進(jìn)行回調(diào) 。

我們?cè)囋囅旅娴拇a:

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

//請(qǐng)求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。
let http = require("http");

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

console.log("Server has started.");
console.log("請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...");

在上圖中,當(dāng)我們執(zhí)行node server.js命令時(shí),Server has started.正常往下執(zhí)行。

我們看看當(dāng)我們?cè)跒g覽器里面打開http://127.0.0.1:8888時(shí)會(huì)發(fā)生什么。


大家會(huì)發(fā)現(xiàn)在瀏覽器中打開http://127.0.0.1:8888時(shí),在終端會(huì)輸出Request received.,瀏覽器會(huì)輸出添加小精靈微信(ershiyidianjian),加入全棧部落這一句話。

請(qǐng)注意,當(dāng)我們?cè)诜?wù)器訪問網(wǎng)頁時(shí),我們的服務(wù)器可能會(huì)輸出兩次“Request received.”。那是因?yàn)榇蟛糠譃g覽器都會(huì)在你訪問 http://localhost:8888/ 時(shí)嘗試讀取 http://localhost:8888/favicon... )

6.服務(wù)器是如何處理請(qǐng)求的

好的,接下來我們簡(jiǎn)單分析一下我們服務(wù)器代碼中剩下的部分,也就是我們的回調(diào)函數(shù)onRequest()的主體部分。

當(dāng)回調(diào)啟動(dòng),我們的onRequest()函數(shù)被觸發(fā)的時(shí)候,有兩個(gè)參數(shù)被傳入:requestresponse

它們是對(duì)象,你可以使用它們的方法來處理HTTP請(qǐng)求的細(xì)節(jié),并且響應(yīng)請(qǐng)求(比如向發(fā)出請(qǐng)求的瀏覽器發(fā)回一些東西)。

所以我們的代碼就是:當(dāng)收到請(qǐng)求時(shí),使用response.writeHead()函數(shù)發(fā)送一個(gè)HTTP狀態(tài)200和HTTP頭的內(nèi)容類型(content-type),使用response.write()函數(shù)在HTTP相應(yīng)主體中發(fā)送文本添加小精靈微信(ershiyidianjian),加入全棧部落

最后,我們調(diào)用 response.end() 完成響應(yīng)。

目前來說,我們對(duì)請(qǐng)求的細(xì)節(jié)并不在意,所以我們沒有使用 request 對(duì)象。

7.服務(wù)端模塊化

何為模塊

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

在上面的代碼中,Node.js中自帶了一個(gè)叫做“http”的模塊,我們?cè)谖覀兊拇a中請(qǐng)求它并把返回值賦給一個(gè)本地變量。

這把我們的本地變量變成了一個(gè)擁有所有 http 模塊所提供的公共方法的對(duì)象。

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

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

如何自定義模塊

server.js文件的內(nèi)容改成下面的內(nèi)容。

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

//請(qǐng)求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。
let http = require("http");

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

    console.log("Server has started.");
    console.log("請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...");
}

//導(dǎo)出`server`對(duì)象,對(duì)象中包含一個(gè)start函數(shù)
//對(duì)象格式為
/**
 * {
 *    start
 * }
 */

//這個(gè)對(duì)象導(dǎo)入到其他文件中即可使用,可以用任意的名字來接收這個(gè)對(duì)象

exports.start = start;

server.js當(dāng)前的文件路徑下新建一個(gè)index.js文件。內(nèi)容如下:

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

//從`server`模塊中導(dǎo)入server對(duì)象

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

//啟動(dòng)服務(wù)器
server.start();

如下圖所示運(yùn)行index.js文件。



一切運(yùn)行正常,上面的案例中,server.js就是自定義的模塊。

8.如何來進(jìn)行請(qǐng)求的“路由”

我們要為路由提供請(qǐng)求的URL和其他需要的GET及POST參數(shù),隨后路由需要根據(jù)這些數(shù)據(jù)來執(zhí)行相應(yīng)的代碼(這里“代碼”對(duì)應(yīng)整個(gè)應(yīng)用的第三部分:一系列在接收到請(qǐng)求時(shí)真正工作的處理程序)。

因此,我們需要查看HTTP請(qǐng)求,從中提取出請(qǐng)求的URL以及GET/POST參數(shù)。這一功能應(yīng)當(dāng)屬于路由還是服務(wù)器(甚至作為一個(gè)模塊自身的功能)確實(shí)值得探討,但這里暫定其為我們的HTTP服務(wù)器的功能。

我們需要的所有數(shù)據(jù)都會(huì)包含在request對(duì)象中,該對(duì)象作為onRequest()回調(diào)函數(shù)的第一個(gè)參數(shù)傳遞。但是為了解析這些數(shù)據(jù),我們需要額外的Node.JS模塊,它們分別是url和querystring模塊。

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

當(dāng)然我們也可以用querystring模塊來解析POST請(qǐng)求體中的參數(shù),稍后會(huì)有演示。

現(xiàn)在我們來給onRequest()函數(shù)加上一些邏輯,用來找出瀏覽器請(qǐng)求的URL路徑:

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

bogon:如何來進(jìn)行請(qǐng)求的“路由” yuechunli$ node index.js
Server has started.
請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...

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

控制臺(tái)效果如下:

bogon:如何來進(jìn)行請(qǐng)求的“路由” yuechunli$ node index.js
Server has started.
請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...
Request for / received.

接著我在Google瀏覽器里面打開 http://127.0.0.1:8888... ,瀏覽器效果圖如下:

控制臺(tái)效果如下:

為什么在Safari瀏覽器中進(jìn)行請(qǐng)求時(shí),只打印了一個(gè)Request for / received.,而在Google瀏覽器中訪問時(shí),會(huì)多打印一個(gè)Request for /favicon.ico received.,如上圖所示,原因是因?yàn)樵?b>Google瀏覽器中,瀏覽器的原因會(huì)去嘗試請(qǐng)求favicon.ico小圖標(biāo)。

為了演示效果,還有不受Google瀏覽器的favicon.ico請(qǐng)求的干擾,我接著在Safari里面請(qǐng)求http://127.0.0.1:8888/starthttp://127.0.0.1:8888/upload,我們看看控制臺(tái)展示的內(nèi)容是什么。

bogon:如何來進(jìn)行請(qǐng)求的“路由” yuechunli$ node index.js
Server has started.
請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...
Request for /start received.
Request for /upload received.

好了,我們的應(yīng)用現(xiàn)在可以通過請(qǐng)求的URL路徑來區(qū)別不同請(qǐng)求了--這使我們得以使用路由(還未完成)來將請(qǐng)求以URL路徑為基準(zhǔn)映射到處理程序上。

在我們所要構(gòu)建的應(yīng)用中,這意味著來自/start/upload的請(qǐng)求可以使用不同的代碼來處理。稍后我們將看到這些內(nèi)容是如何整合到一起的。

現(xiàn)在我們可以來編寫路由了,建立一個(gè)名為router.js的文件,添加以下內(nèi)容:

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

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

exports.route = route;

如你所見,這段代碼什么也沒干,不過對(duì)于現(xiàn)在來說這是應(yīng)該的。在添加更多的邏輯以前,我們先來看看如何把路由和服務(wù)器整合起來。

首先,我們來擴(kuò)展一下服務(wù)器的start()函數(shù),以便將路由函數(shù)作為參數(shù)傳遞過去:

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

//請(qǐng)求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。
let http = require("http");

let url = require("url");


//用一個(gè)函數(shù)將之前的內(nèi)容包裹起來
let start = (route) => {
        //箭頭函數(shù)
    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();
    }
    //把函數(shù)當(dāng)作參數(shù)傳遞
    http.createServer(onRequest).listen(8888);

    console.log("Server has started.");
    console.log("請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...");
}

exports.start = start;

同時(shí),我們會(huì)相應(yīng)擴(kuò)展index.js,使得路由函數(shù)可以被注入到服務(wù)器中:

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

//從`server`模塊中導(dǎo)入server對(duì)象

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

//啟動(dòng)服務(wù)器
server.start(router.route);

在這里,我們傳遞的函數(shù)依舊什么也沒做。

如果現(xiàn)在啟動(dòng)應(yīng)用(node index.js,始終記得這個(gè)命令行),隨后請(qǐng)求一個(gè)URL,你將會(huì)看到應(yīng)用輸出相應(yīng)的信息,這表明我們的HTTP服務(wù)器已經(jīng)在使用路由模塊了,并會(huì)將請(qǐng)求的路徑傳遞給路由:

bogon:如何來進(jìn)行請(qǐng)求的“路由” v2.0 yuechunli$ node index.js
Server has started.
請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...
Request for / received.
About to route a request for /
9.路由給真正的請(qǐng)求處理程序

現(xiàn)在我們的HTTP服務(wù)器和請(qǐng)求路由模塊已經(jīng)如我們的期望,可以相互交流了,就像一對(duì)親密無間的兄弟。

當(dāng)然這還遠(yuǎn)遠(yuǎn)不夠,路由,顧名思義,是指我們要針對(duì)不同的URL有不同的處理方式。例如處理/start的“業(yè)務(wù)邏輯”就應(yīng)該和處理/upload的不同。

在現(xiàn)在的實(shí)現(xiàn)下,路由過程會(huì)在路由模塊中“結(jié)束”,并且路由模塊并不是真正針對(duì)請(qǐng)求“采取行動(dòng)”的模塊,否則當(dāng)我們的應(yīng)用程序變得更為復(fù)雜時(shí),將無法很好地?cái)U(kuò)展。

我們暫時(shí)把作為路由目標(biāo)的函數(shù)稱為請(qǐng)求處理程序。現(xiàn)在我們不要急著來開發(fā)路由模塊,因?yàn)槿绻?qǐng)求處理程序沒有就緒的話,再怎么完善路由模塊也沒有多大意義。

應(yīng)用程序需要新的部件,因此加入新的模塊 -- 已經(jīng)無需為此感到新奇了。我們來創(chuàng)建一個(gè)叫做requestHandlers的模塊,并對(duì)于每一個(gè)請(qǐng)求處理程序,添加一個(gè)占位用函數(shù),隨后將這些函數(shù)作為模塊的方法導(dǎo)出:

/**
 * 從零到壹全棧部落,添加小精靈微信(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;

現(xiàn)在我們將一系列請(qǐng)求處理程序通過一個(gè)對(duì)象來傳遞,并且需要使用松耦合的方式將這個(gè)對(duì)象注入到route()函數(shù)中。

我們先將這個(gè)對(duì)象引入到主文件index.js中:

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

//從`server`模塊中導(dǎo)入server對(duì)象

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

//對(duì)象構(gòu)造
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;

//啟動(dòng)服務(wù)器
server.start(router.route, handle);

雖然handle并不僅僅是一個(gè)“東西”(一些請(qǐng)求處理程序的集合),我還是建議以一個(gè)動(dòng)詞作為其命名,這樣做可以讓我們?cè)诼酚芍惺褂酶鲿车谋磉_(dá)式,稍后會(huì)有說明。

正如所見,將不同的URL映射到相同的請(qǐng)求處理程序上是很容易的:只要在對(duì)象中添加一個(gè)鍵為"/"的屬性,對(duì)應(yīng)requestHandlers.start即可,這樣我們就可以干凈簡(jiǎn)潔地配置/start"/"的請(qǐng)求都交由start這一處理程序處理。

在完成了對(duì)象的定義后,我們把它作為額外的參數(shù)傳遞給服務(wù)器,為此將server.js修改如下:

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

//請(qǐng)求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。
let http = require("http");

let url = require("url");


//用一個(gè)函數(shù)將之前的內(nèi)容包裹起來
let start = (route,handle) => {
        //箭頭函數(shù)
    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();
    }
    //把函數(shù)當(dāng)作參數(shù)傳遞
    http.createServer(onRequest).listen(8888);

    console.log("Server has started.");
    console.log("請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...");
}

exports.start = start;

這樣我們就在start()函數(shù)里添加了handle參數(shù),并且把handle對(duì)象作為第一個(gè)參數(shù)傳遞給了route()回調(diào)函數(shù)。

然后我們相應(yīng)地在route.js文件中修改route()函數(shù):

有了這些,我們就把服務(wù)器、路由和請(qǐng)求處理程序在一起了。現(xiàn)在我們啟動(dòng)應(yīng)用程序并在瀏覽器中訪問http://127.0.0.1:8888/start,以下日志可以說明系統(tǒng)調(diào)用了正確的請(qǐng)求處理程序:

bogon:路由給真正的請(qǐng)求處理程序 yuechunli$ node index.js
Server has started.
請(qǐng)?jiān)跒g覽器中打開 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/可以看到這個(gè)請(qǐng)求同樣被start請(qǐng)求處理程序處理了:

bogon:路由給真正的請(qǐng)求處理程序 yuechunli$ node index.js
Server has started.
請(qǐng)?jiān)跒g覽器中打開 http://127.0.0.1:8888...
Request for / received.
About to route a request for /
Request handler "start" was called.
10.讓請(qǐng)求處理程序作出響應(yīng)

很好。不過現(xiàn)在要是請(qǐng)求處理程序能夠向?yàn)g覽器返回一些有意義的信息而并非全是添加小精靈微信(ershiyidianjian),加入全棧部落,那就更好了。

這里要記住的是,瀏覽器發(fā)出請(qǐng)求后獲得并顯示的添加小精靈微信(ershiyidianjian),加入全棧部落信息仍是來自于我們server.js文件中的onRequest函數(shù)。

其實(shí)“處理請(qǐng)求”說白了就是“對(duì)請(qǐng)求作出響應(yīng)”,因此,我們需要讓請(qǐng)求處理程序能夠像onRequest函數(shù)那樣可以和瀏覽器進(jìn)行“對(duì)話”。

11.不好的實(shí)現(xiàn)方式

修改requestHandler.js文件內(nèi)容如下:

/**
 * 從零到壹全棧部落,添加小精靈微信(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;

好的。同樣的,請(qǐng)求路由需要將請(qǐng)求處理程序返回給它的信息返回給服務(wù)器。因此,我們需要將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;

正如上述代碼所示,當(dāng)請(qǐng)求無法路由的時(shí)候,我們也返回了一些相關(guān)的錯(cuò)誤信息。

最后,我們需要對(duì)我們的server.js進(jìn)行重構(gòu)以使得它能夠?qū)⒄?qǐng)求處理程序通過請(qǐng)求路由返回的內(nèi)容響應(yīng)給瀏覽器,如下所示:

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

//請(qǐng)求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。
let http = require("http");

let url = require("url");

//用一個(gè)函數(shù)將之前的內(nèi)容包裹起來
let start = (route,handle) => {
        //箭頭函數(shù)
    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();
    }
    //把函數(shù)當(dāng)作參數(shù)傳遞
    http.createServer(onRequest).listen(8888);

    console.log("Server has started.");
}

exports.start = start;

如果我們運(yùn)行重構(gòu)后的應(yīng)用,一切都會(huì)工作的很好:

請(qǐng)求http://localhost:8888/start,瀏覽器會(huì)輸出Hello Start

請(qǐng)求http://localhost:8888/upload會(huì)輸出Hello Upload,

而請(qǐng)求http://localhost:8888/foo 會(huì)輸出404 Not found

好,那么問題在哪里呢?簡(jiǎn)單的說就是: 當(dāng)未來有請(qǐng)求處理程序需要進(jìn)行非阻塞的操作的時(shí)候,我們的應(yīng)用就“掛”了。

沒理解?沒關(guān)系,下面就來詳細(xì)解釋下。

12.阻塞與非阻塞

我們先不解釋這里阻塞非阻塞,我們來修改下start請(qǐng)求處理程序,我們讓它等待10秒以后再返回Hello Start。因?yàn)椋琂avaScript中沒有類似sleep()這樣的操作,所以這里只能夠來點(diǎn)小Hack來模擬實(shí)現(xiàn)。

讓我們將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;

上述代碼中,我先調(diào)用了upload(),會(huì)和此前一樣立即返回。當(dāng)函數(shù)start()被調(diào)用的時(shí)候,Node.js會(huì)先等待10秒,之后才會(huì)返回“Hello Start”。如下圖所示,等待中:

(當(dāng)然了,這里只是模擬休眠10秒,實(shí)際場(chǎng)景中,這樣的阻塞操作有很多,比方說一些長(zhǎng)時(shí)間的計(jì)算操作等。)

接下來就讓我們來看看,我們的改動(dòng)帶來了哪些變化。

如往常一樣,我們先要重啟下服務(wù)器。為了看到效果,我們要進(jìn)行一些相對(duì)復(fù)雜的操作(跟著我一起做): 首先,打開兩個(gè)瀏覽器窗口或者標(biāo)簽頁。在第一個(gè)瀏覽器窗口的地址欄中輸入http://localhost:8888/start, 但是先不要打開它!

在第二個(gè)瀏覽器窗口的地址欄中輸入http://localhost:8888/upload, 同樣的,先不要打開它!

接下來,做如下操作:在第一個(gè)窗口中(“/start”)按下回車,然后快速切換到第二個(gè)窗口中(“/upload”)按下回車。

注意,發(fā)生了什么: /start URL加載花了10秒,這和我們預(yù)期的一樣。但是,/upload URL居然也花了10秒,而它在對(duì)應(yīng)的請(qǐng)求處理程序中并沒有類似于sleep()這樣的操作!

這到底是為什么呢?原因就是start()包含了阻塞操作。形象的說就是“它阻塞了所有其他的處理工作”。

這顯然是個(gè)問題,因?yàn)镹ode一向是這樣來標(biāo)榜自己的:“在node中除了代碼,所有一切都是并行執(zhí)行的”。

這句話的意思是說,Node.js可以在不新增額外線程的情況下,依然可以對(duì)任務(wù)進(jìn)行并行處理 —— Node.js是單線程的。它通過事件輪詢(event loop)來實(shí)現(xiàn)并行操作,對(duì)此,我們應(yīng)該要充分利用這一點(diǎn) —— 盡可能的避免阻塞操作,取而代之,多使用非阻塞操作。

然而,要用非阻塞操作,我們需要使用回調(diào),通過將函數(shù)作為參數(shù)傳遞給其他需要花時(shí)間做處理的函數(shù)(比方說,休眠10秒,或者查詢數(shù)據(jù)庫(kù),又或者是進(jìn)行大量的計(jì)算)。

對(duì)于Node.js來說,它是這樣處理的:“嘿,probablyExpensiveFunction()(譯者注:這里指的就是需要花時(shí)間處理的函數(shù)),你繼續(xù)處理你的事情,我(Node.js線程)先不等你了,我繼續(xù)去處理你后面的代碼,請(qǐng)你提供一個(gè)callbackFunction(),等你處理完之后我會(huì)去調(diào)用該回調(diào)函數(shù)的,謝謝!”

(如果想要了解更多關(guān)于事件輪詢細(xì)節(jié),可以閱讀Mixu的博文——理解node.js的事件輪詢。)

接下來,我們會(huì)介紹一種錯(cuò)誤的使用非阻塞操作的方式。

和上次一樣,我們通過修改我們的應(yīng)用來暴露問題。

這次我們還是拿start請(qǐng)求處理程序來“開刀”。將其修改成如下形式:

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


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

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

  /**
   * exec()做了什么呢?
   * 它從Node.js來執(zhí)行一個(gè)shell命令。
   * 在本例子中,我們用它來獲取當(dāng)前目錄下所有的文件(“l(fā)s -lah”)
   * 然后,當(dāng)`/start` URL請(qǐng)求的時(shí)候?qū)⑽募畔⑤敵龅綖g覽器中。
   * 下面的代碼非常直觀的: 
   * 創(chuàng)建了一個(gè)新的變量content(初始值為“empty”)。
   * 執(zhí)行“l(fā)s -lah”命令,將結(jié)果賦值給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;

和往常一樣,我們啟動(dòng)服務(wù)器,然后訪問“http://localhost:8888/start” 。

載入一個(gè)漂亮的web頁面,其內(nèi)容為“empty”。怎么回事?

如果想要證明這一點(diǎn),可以將“l(fā)s -lah”換成比如“find /”這樣更耗時(shí)的操作來效果。

然而,針對(duì)瀏覽器顯示的結(jié)果來看,我們并不滿意我們的非阻塞操作,對(duì)吧?

好,接下來,我們來修正這個(gè)問題。在這過程中,讓我們先來看看為什么當(dāng)前的這種方式不起作用。

問題就在于,為了進(jìn)行非阻塞工作,exec()使用了回調(diào)函數(shù)。

在我們的例子中,該回調(diào)函數(shù)就是作為第二個(gè)參數(shù)傳遞給exec()的匿名函數(shù):

function (error, stdout, stderr) {
  content = stdout;
}

現(xiàn)在就到了問題根源所在了:我們的代碼是同步執(zhí)行的,這就意味著在調(diào)用exec()之后,Node.js會(huì)立即執(zhí)行 return content ;在這個(gè)時(shí)候,content仍然是“empty”,因?yàn)閭鬟f給exec()的回調(diào)函數(shù)還未執(zhí)行到——因?yàn)閑xec()的操作是異步的。

我們這里“l(fā)s -lah”的操作其實(shí)是非常快的(除非當(dāng)前目錄下有上百萬個(gè)文件)。這也是為什么回調(diào)函數(shù)也會(huì)很快的執(zhí)行到 —— 不過,不管怎么說它還是異步的。

為了讓效果更加明顯,我們想象一個(gè)更耗時(shí)的命令: “find /”,它在我機(jī)器上需要執(zhí)行1分鐘左右的時(shí)間,然而,盡管在請(qǐng)求處理程序中,我把“l(fā)s -lah”換成“find /”,當(dāng)打開/start URL的時(shí)候,依然能夠立即獲得HTTP響應(yīng) —— 很明顯,當(dāng)exec()在后臺(tái)執(zhí)行的時(shí)候,Node.js自身會(huì)繼續(xù)執(zhí)行后面的代碼。并且我們這里假設(shè)傳遞給exec()的回調(diào)函數(shù),只會(huì)在“find /”命令執(zhí)行完成之后才會(huì)被調(diào)用。

那究竟我們要如何才能實(shí)現(xiàn)將當(dāng)前目錄下的文件列表顯示給用戶呢?

好,了解了這種不好的實(shí)現(xiàn)方式之后,我們接下來來介紹如何以正確的方式讓請(qǐng)求處理程序?qū)g覽器請(qǐng)求作出響應(yīng)。

13.以非阻塞操作進(jìn)行請(qǐng)求響應(yīng)

我剛剛提到了這樣一個(gè)短語 —— “正確的方式”。而事實(shí)上通常“正確的方式”一般都不簡(jiǎn)單。

不過,用Node.js就有這樣一種實(shí)現(xiàn)方案: 函數(shù)傳遞。下面就讓我們來具體看看如何實(shí)現(xiàn)。

到目前為止,我們的應(yīng)用已經(jīng)可以通過應(yīng)用各層之間傳遞值的方式(請(qǐng)求處理程序 -> 請(qǐng)求路由 -> 服務(wù)器)將請(qǐng)求處理程序返回的內(nèi)容(請(qǐng)求處理程序最終要顯示給用戶的內(nèi)容)傳遞給HTTP服務(wù)器。

現(xiàn)在我們采用如下這種新的實(shí)現(xiàn)方式:相對(duì)采用將內(nèi)容傳遞給服務(wù)器的方式,我們這次采用將服務(wù)器“傳遞”給內(nèi)容的方式。 從實(shí)踐角度來說,就是將response對(duì)象(從服務(wù)器的回調(diào)函數(shù)onRequest()獲取)通過請(qǐng)求路由傳遞給請(qǐng)求處理程序。 隨后,處理程序就可以采用該對(duì)象上的函數(shù)來對(duì)請(qǐng)求作出響應(yīng)。

原理就是如此,接下來讓我們來一步步實(shí)現(xiàn)這種方案。

先從server.js開始:

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

//請(qǐng)求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。
let http = require("http");

let url = require("url");

//用一個(gè)函數(shù)將之前的內(nèi)容包裹起來
let start = (route,handle) => {
        //箭頭函數(shù)
    let onRequest = (request, response) => {
        
        let pathname = url.parse(request.url).pathname;
        console.log("Request for " + pathname + " received.");
        route(handle, pathname, response);
    }
    //把函數(shù)當(dāng)作參數(shù)傳遞
    http.createServer(onRequest).listen(8888);

    console.log("Server has started.");
}

exports.start = start;

相對(duì)此前從route()函數(shù)獲取返回值的做法,這次我們將response對(duì)象作為第三個(gè)參數(shù)傳遞給route()函數(shù),并且,我們將onRequest()處理程序中所有有關(guān)response的函數(shù)調(diào)都移除,因?yàn)槲覀兿M@部分工作讓route()函數(shù)來完成。

下面就來看看我們的router.js:

同樣的模式:相對(duì)此前從請(qǐng)求處理程序中獲取返回值,這次取而代之的是直接傳遞response對(duì)象。

如果沒有對(duì)應(yīng)的請(qǐng)求處理器處理,我們就直接返回“404”錯(cuò)誤。

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

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


//我們引入了一個(gè)新的Node.js模塊,child_process。之所以用它,是為了實(shí)現(xiàn)一個(gè)既簡(jiǎn)單又實(shí)用的非阻塞操作: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;

我們的處理程序函數(shù)需要接收response參數(shù),為了對(duì)請(qǐng)求作出直接的響應(yīng)。

start處理程序在exec()的匿名回調(diào)函數(shù)中做請(qǐng)求響應(yīng)的操作,而upload處理程序仍然是簡(jiǎn)單的回復(fù)“Hello World”,只是這次是使用response對(duì)象而已。

這時(shí)再次我們啟動(dòng)應(yīng)用(node index.js),一切都會(huì)工作的很好。

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

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

如果想要證明/start處理程序中耗時(shí)的操作不會(huì)阻塞對(duì)/upload請(qǐng)求作出立即響應(yīng)的話,可以將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;

這樣一來,當(dāng)請(qǐng)求http://localhost:8888/start的時(shí)候,會(huì)花10秒鐘的時(shí)間才載入,而當(dāng)請(qǐng)求http://localhost:8888/upload的時(shí)候,會(huì)立即響應(yīng),縱然這個(gè)時(shí)候/start響應(yīng)還在處理中。

14.更有用的場(chǎng)景

到目前為止,我們做的已經(jīng)很好了,但是,我們的應(yīng)用沒有實(shí)際用途。

服務(wù)器,請(qǐng)求路由以及請(qǐng)求處理程序都已經(jīng)完成了,下面讓我們按照此前的用例給網(wǎng)站添加交互:用戶選擇一個(gè)文件,上傳該文件,然后在瀏覽器中看到上傳的文件。 為了保持簡(jiǎn)單,我們假設(shè)用戶只會(huì)上傳圖片,然后我們應(yīng)用將該圖片顯示到瀏覽器中。

好,下面就一步步來實(shí)現(xiàn),鑒于此前已經(jīng)對(duì)JavaScript原理性技術(shù)性的內(nèi)容做過大量介紹了,這次我們加快點(diǎn)速度。

要實(shí)現(xiàn)該功能,分為如下兩步: 首先,讓我們來看看如何處理POST請(qǐng)求(非文件上傳),之后,我們使用Node.js的一個(gè)用于文件上傳的外部模塊。之所以采用這種實(shí)現(xiàn)方式有兩個(gè)理由。

第一,盡管在Node.js中處理基礎(chǔ)的POST請(qǐng)求相對(duì)比較簡(jiǎn)單,但在這過程中還是能學(xué)到很多。
第二,用Node.js來處理文件上傳(multipart POST請(qǐng)求)是比較復(fù)雜的,它不在本文的范疇,但是,如何使用外部模塊卻是在本書涉獵內(nèi)容之內(nèi)。

15.處理POST請(qǐng)求

考慮這樣一個(gè)簡(jiǎn)單的例子:我們顯示一個(gè)文本區(qū)(textarea)供用戶輸入內(nèi)容,然后通過POST請(qǐng)求提交給服務(wù)器。最后,服務(wù)器接受到請(qǐng)求,通過處理程序?qū)⑤斎氲膬?nèi)容展示到瀏覽器中。

/start請(qǐng)求處理程序用于生成帶文本區(qū)的表單,因此,我們將requestHandlers.js修改為如下形式:

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


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

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

  let body = ""+
    ""+
    ""+
    ""+
    ""+
    "
"+ ""+ ""+ "
"+ ""+ ""; 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;

瀏覽器請(qǐng)求http://127.0.0.1:8888/start,效果圖如下:

余下的篇幅,我們來探討一個(gè)更有趣的問題: 當(dāng)用戶提交表單時(shí),觸發(fā)/upload請(qǐng)求處理程序處理POST請(qǐng)求的問題。

現(xiàn)在,我們已經(jīng)是新手中的專家了,很自然會(huì)想到采用異步回調(diào)來實(shí)現(xiàn)非阻塞地處理POST請(qǐng)求的數(shù)據(jù)。

這里采用非阻塞方式處理是明智的,因?yàn)镻OST請(qǐng)求一般都比較“重” —— 用戶可能會(huì)輸入大量的內(nèi)容。用阻塞的方式處理大數(shù)據(jù)量的請(qǐng)求必然會(huì)導(dǎo)致用戶操作的阻塞。

為了使整個(gè)過程非阻塞,Node.js會(huì)將POST數(shù)據(jù)拆分成很多小的數(shù)據(jù)塊,然后通過觸發(fā)特定的事件,將這些小數(shù)據(jù)塊傳遞給回調(diào)函數(shù)。這里的特定的事件有data事件(表示新的小數(shù)據(jù)塊到達(dá)了)以及end事件(表示所有的數(shù)據(jù)都已經(jīng)接收完畢)。

我們需要告訴Node.js當(dāng)這些事件觸發(fā)的時(shí)候,回調(diào)哪些函數(shù)。怎么告訴呢? 我們通過在request對(duì)象上注冊(cè)監(jiān)聽器(listener) 來實(shí)現(xiàn)。這里的request對(duì)象是每次接收到HTTP請(qǐng)求時(shí)候,都會(huì)把該對(duì)象傳遞給onRequest回調(diào)函數(shù)。

如下所示:

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
});

問題來了,這部分邏輯寫在哪里呢? 我們現(xiàn)在只是在服務(wù)器中獲取到了request對(duì)象 —— 我們并沒有像之前response對(duì)象那樣,把 request 對(duì)象傳遞給請(qǐng)求路由和請(qǐng)求處理程序。

在我看來,獲取所有來自請(qǐng)求的數(shù)據(jù),然后將這些數(shù)據(jù)給應(yīng)用層處理,應(yīng)該是HTTP服務(wù)器要做的事情。因此,我建議,我們直接在服務(wù)器中處理POST數(shù)據(jù),然后將最終的數(shù)據(jù)傳遞給請(qǐng)求路由和請(qǐng)求處理器,讓他們來進(jìn)行進(jìn)一步的處理。

因此,實(shí)現(xiàn)思路就是: 將data和end事件的回調(diào)函數(shù)直接放在服務(wù)器中,在data事件回調(diào)中收集所有的POST數(shù)據(jù),當(dāng)接收到所有數(shù)據(jù),觸發(fā)end事件后,其回調(diào)函數(shù)調(diào)用請(qǐng)求路由,并將數(shù)據(jù)傳遞給它,然后,請(qǐng)求路由再將該數(shù)據(jù)傳遞給請(qǐng)求處理程序。

還等什么,馬上來實(shí)現(xiàn)。先從server.js開始:

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

//請(qǐng)求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。
let http = require("http");

let url = require("url");

//用一個(gè)函數(shù)將之前的內(nèi)容包裹起來
let start = (route,handle) => {
        //箭頭函數(shù)
    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);
        });
    }
    //把函數(shù)當(dāng)作參數(shù)傳遞
    http.createServer(onRequest).listen(8888);

    console.log("Server has started.");
}

exports.start = start;

上述代碼做了三件事情: 首先,我們?cè)O(shè)置了接收數(shù)據(jù)的編碼格式為UTF-8,然后注冊(cè)了“data”事件的監(jiān)聽器,用于收集每次接收到的新數(shù)據(jù)塊,并將其賦值給postData 變量,最后,我們將請(qǐng)求路由的調(diào)用移到end事件處理程序中,以確保它只會(huì)當(dāng)所有數(shù)據(jù)接收完畢后才觸發(fā),并且只觸發(fā)一次。我們同時(shí)還把POST數(shù)據(jù)傳遞給請(qǐng)求路由,因?yàn)檫@些數(shù)據(jù),請(qǐng)求處理程序會(huì)用到。

上述代碼在每個(gè)數(shù)據(jù)塊到達(dá)的時(shí)候輸出了日志,這對(duì)于最終生產(chǎn)環(huán)境來說,是很不好的(數(shù)據(jù)量可能會(huì)很大,還記得吧?),但是,在開發(fā)階段是很有用的,有助于讓我們看到發(fā)生了什么。

我建議可以嘗試下,嘗試著去輸入一小段文本,以及大段內(nèi)容,當(dāng)大段內(nèi)容的時(shí)候,就會(huì)發(fā)現(xiàn)data事件會(huì)觸發(fā)多次。

再來點(diǎn)酷的。我們接下來在/upload頁面,展示用戶輸入的內(nèi)容。要實(shí)現(xiàn)該功能,我們需要將postData傳遞給請(qǐng)求處理程序,修改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中,我們將數(shù)據(jù)包含在對(duì)upload請(qǐng)求的響應(yīng)中:

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


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

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

  var body = ""+
    ""+
    ""+
    ""+
    ""+
    "
"+ ""+ ""+ "
"+ ""+ ""; 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;

好了,我們現(xiàn)在可以接收POST數(shù)據(jù)并在請(qǐng)求處理程序中處理該數(shù)據(jù)了。

我們最后要做的是: 當(dāng)前我們是把請(qǐng)求的整個(gè)消息體傳遞給了請(qǐng)求路由和請(qǐng)求處理程序。我們應(yīng)該只把POST數(shù)據(jù)中,我們感興趣的部分傳遞給請(qǐng)求路由和請(qǐng)求處理程序。在我們這個(gè)例子中,我們感興趣的其實(shí)只是text字段。

我們可以使用此前介紹過的querystring模塊來實(shí)現(xiàn):

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


//我們引入了一個(gè)新的Node.js模塊,child_process。之所以用它,是為了實(shí)現(xiàn)一個(gè)既簡(jiǎn)單又實(shí)用的非阻塞操作:exec()。
var exec = require("child_process").exec;
var querystring = require("querystring");

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

  var body = ""+
    ""+
    ""+
    ""+
    ""+
    "
"+ ""+ ""+ "
"+ ""+ ""; 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;

下面我們?yōu)g覽器中訪問http://127.0.0.1:8888/start,如下圖所示:

點(diǎn)擊Submit text按鈕,將跳轉(zhuǎn)到http://127.0.0.1:8888/upload,效果圖如下:

好了,這就是完整的POST請(qǐng)求。

15.處理文件上傳

最后,我們來實(shí)現(xiàn)我們最終的用例:允許用戶上傳圖片,并將該圖片在瀏覽器中顯示出來。

我們通過它能學(xué)到這樣兩件事情:

如何安裝外部Node.js模塊

以及如何將它們應(yīng)用到我們的應(yīng)用中

這里我們要用到的外部模塊是Felix Geisend?rfer開發(fā)的node-formidable模塊。它對(duì)解析上傳的文件數(shù)據(jù)做了很好的抽象。 其實(shí)說白了,處理文件上傳“就是”處理POST數(shù)據(jù) —— 但是,麻煩的是在具體的處理細(xì)節(jié),所以,這里采用現(xiàn)成的方案更合適點(diǎn)。

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

首先在當(dāng)前項(xiàng)目路徑下面通過npm init創(chuàng)建package.json文件:

PS:在終端輸入npm init后,一路回車即可。新增的package.json文件的內(nèi)容如下:

{
  "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"
  }
}

項(xiàng)目整體變化如下圖所示:

現(xiàn)在我們就可以用formidable模塊了——使用外部模塊與內(nèi)部模塊類似,用require語句將其引入即可:

let formidable = require("formidable");

這里該模塊做的就是將通過HTTP POST請(qǐng)求提交的表單,在Node.js中可以被解析。我們要做的就是創(chuàng)建一個(gè)新的IncomingForm,它是對(duì)提交表單的抽象表示,之后,就可以用它解析request對(duì)象,獲取表單中需要的數(shù)據(jù)字段。

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:

");
      res.end(util.inspect({fields: fields, files: files}));
    });
    return;
  }

  // show a file upload form
  res.writeHead(200, {"content-type": "text/html"});
  res.end(
    "
"+ "
"+ "
"+ ""+ "
" ); }).listen(8888);

如果我們將上述代碼,保存到一個(gè)文件中,并通過node來執(zhí)行,就可以進(jìn)行簡(jiǎn)單的表單提交了,包括文件上傳。然后,可以看到通過調(diào)用form.parse傳遞給回調(diào)函數(shù)的files對(duì)象的內(nèi)容,如下所示:

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] } } }

為了實(shí)現(xiàn)我們的功能,我們需要將上述代碼應(yīng)用到我們的應(yīng)用中,另外,我們還要考慮如何將上傳文件的內(nèi)容(保存在./tmp目錄中)顯示到瀏覽器中。

我們先來解決后面那個(gè)問題: 對(duì)于保存在本地硬盤中的文件,如何才能在瀏覽器中看到呢?

顯然,我們需要將該文件讀取到我們的服務(wù)器中,使用一個(gè)叫fs的模塊。

我們來添加/showURL的請(qǐng)求處理程序,該處理程序直接硬編碼將文件./tmp/test.png內(nèi)容展示到瀏覽器中。當(dāng)然了,首先需要將該圖片保存到這個(gè)位置才行。

requestHandlers.js修改為如下形式:

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

var querystring = require("querystring"),
    fs = require("fs");

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

  var body = ""+
    ""+
    ""+
    ""+
    ""+
    "
"+ ""+ ""+ "
"+ ""+ ""; 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 + " "); 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;

我們還需要將這新的請(qǐng)求處理程序,添加到index.js中的路由映射表中:

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

//從`server`模塊中導(dǎo)入server對(duì)象

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

//對(duì)象構(gòu)造
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
handle["/show"] = requestHandlers.show;

//啟動(dòng)服務(wù)器
server.start(router.route, handle);

重啟服務(wù)器之后,通過訪問http://localhost:8888/show看看效果:

原因是當(dāng)前項(xiàng)目路徑下面沒有./tmp/test.png圖片,我們?cè)诋?dāng)前項(xiàng)目路徑下面添加tmp文件夾,在往里面拖拽一張圖片,命名為test.png

再重新啟動(dòng)服務(wù)器,訪問http://localhost:8888/show查看效果如下:

咱繼續(xù),從server.js開始 —— 移除對(duì)postData的處理以及request.setEncoding (這部分node-formidable自身會(huì)處理),轉(zhuǎn)而采用將request對(duì)象傳遞給請(qǐng)求路由的方式:

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

//請(qǐng)求(require)Node.js自帶的 http 模塊,并且把它賦值給 http 變量。
let http = require("http");

let url = require("url");

//用一個(gè)函數(shù)將之前的內(nèi)容包裹起來
let start = (route,handle) => {
        //箭頭函數(shù)
    let onRequest = (request, response) => {
        
        let pathname = url.parse(request.url).pathname;
        console.log("Request for " + pathname + " received.");
        route(handle, pathname, response, request);
    }
    //把函數(shù)當(dāng)作參數(shù)傳遞
    http.createServer(onRequest).listen(8888);

    console.log("Server has started.");
}

exports.           
               
                                           
                       
                 

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/83773.html

相關(guān)文章

  • 前端相關(guān)匯總

    摘要:簡(jiǎn)介前端發(fā)展迅速,開發(fā)者富有的創(chuàng)造力不斷的給前端生態(tài)注入新生命,各種庫(kù)框架工程化構(gòu)建工具層出不窮,眼花繚亂,不盲目追求前沿技術(shù),學(xué)習(xí)框架和庫(kù)在滿足自己開發(fā)需求的基礎(chǔ)上,然后最好可以對(duì)源碼進(jìn)行調(diào)研,了解和深入實(shí)現(xiàn)原理,從中可以獲得更多的收獲隨 showImg(https://segmentfault.com/img/remote/1460000016784101?w=936&h=397)...

    BenCHou 評(píng)論0 收藏0
  • 前端文檔收集

    摘要:系列種優(yōu)化頁面加載速度的方法隨筆分類中個(gè)最重要的技術(shù)點(diǎn)常用整理網(wǎng)頁性能管理詳解離線緩存簡(jiǎn)介系列編寫高性能有趣的原生數(shù)組函數(shù)數(shù)據(jù)訪問性能優(yōu)化方案實(shí)現(xiàn)的大排序算法一怪對(duì)象常用方法函數(shù)收集數(shù)組的操作面向?qū)ο蠛驮屠^承中關(guān)鍵詞的優(yōu)雅解釋淺談系列 H5系列 10種優(yōu)化頁面加載速度的方法 隨筆分類 - HTML5 HTML5中40個(gè)最重要的技術(shù)點(diǎn) 常用meta整理 網(wǎng)頁性能管理詳解 HTML5 ...

    jsbintask 評(píng)論0 收藏0
  • 前端文檔收集

    摘要:系列種優(yōu)化頁面加載速度的方法隨筆分類中個(gè)最重要的技術(shù)點(diǎn)常用整理網(wǎng)頁性能管理詳解離線緩存簡(jiǎn)介系列編寫高性能有趣的原生數(shù)組函數(shù)數(shù)據(jù)訪問性能優(yōu)化方案實(shí)現(xiàn)的大排序算法一怪對(duì)象常用方法函數(shù)收集數(shù)組的操作面向?qū)ο蠛驮屠^承中關(guān)鍵詞的優(yōu)雅解釋淺談系列 H5系列 10種優(yōu)化頁面加載速度的方法 隨筆分類 - HTML5 HTML5中40個(gè)最重要的技術(shù)點(diǎn) 常用meta整理 網(wǎng)頁性能管理詳解 HTML5 ...

    muddyway 評(píng)論0 收藏0
  • 前端相關(guān)大雜燴

    摘要:希望幫助更多的前端愛好者學(xué)習(xí)。前端開發(fā)者指南作者科迪林黎,由前端大師傾情贊助。翻譯最佳實(shí)踐譯者張捷滬江前端開發(fā)工程師當(dāng)你問起有關(guān)與時(shí),老司機(jī)們首先就會(huì)告訴你其實(shí)是個(gè)沒有網(wǎng)絡(luò)請(qǐng)求功能的庫(kù)。 前端基礎(chǔ)面試題(JS部分) 前端基礎(chǔ)面試題(JS部分) 學(xué)習(xí) React.js 比你想象的要簡(jiǎn)單 原文地址:Learning React.js is easier than you think 原文作...

    fuyi501 評(píng)論0 收藏0
  • 后端API從入門到放棄指北

    摘要:菜鳥教程框架中文手冊(cè)入門目標(biāo)使用搭建通過對(duì)數(shù)據(jù)增刪查改沒了純粹占行用的拜 后端API入門學(xué)習(xí)指北 了解一下一下概念. RESTful API標(biāo)準(zhǔn)] 所有的API都遵循[RESTful API標(biāo)準(zhǔn)]. 建議大家都簡(jiǎn)單了解一下HTTP協(xié)議和RESTful API相關(guān)資料. 阮一峰:理解RESTful架構(gòu) 阮一峰:RESTful API 設(shè)計(jì)指南 RESTful API指南 依賴注入 D...

    Jeffrrey 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<