本篇文檔的整理源自 https://www.jianshu.com/p/bf187fed8609,對於我個人而言,讓我意識到我在學習node的時候關於一些代碼的思考根本沒有,學習完之後以爲自己會了,其實還是什麼都不明白。
本書中的代碼案例都在Node.js 0.6.11版本中測試過,可以正確工作。
讀完本書之後,你將完成一個完整的web應用,該應用允許用戶瀏覽頁面以及上傳文件。
本書先從介紹在Node.js環境中進行JavaScript開發和在瀏覽器環境中進行JavaScript開發的差異開始。
緊接着,會帶領大家完成一個最傳統的“Hello World”應用,這也是最基礎的Node.js應用。
最後,會和大家討論如何設計一個“真正”完整的應用,剖析要完成該應用需要實現的不同模塊,並一步一步介紹如何來實現這些模塊。
可以確保的是,在這過程中,大家會學到JavaScript中一些高級的概念、如何使用它們以及爲什麼使用這些概念就可以實現而其他編程語言中同類的概念就無法實現。
該應用所有的源代碼都可以通過 Github代碼倉庫查看,你也可以選擇先將代碼clone之後再繼續進行下面的閱讀。
一、本文檔結構
目錄
一、本文檔結構
二、JavaScript與Node.js
- JavaScript與你
- 簡短申明
- 服務器端JavaScript
- “Hello World”
三、一個完整的基於Node.js的web應用
- 用例
- 應用不同模塊分析
四、構建應用的模塊
- 一個基礎的HTTP服務器
- 分析HTTP服務器
- 進行函數傳遞
- 函數傳遞是如何讓HTTP服務器工作的
- 基於事件驅動的回調
- 服務器是如何處理請求的
- 服務端的模塊放在哪裏
- 如何來進行請求的“路由”
- 行爲驅動執行
- 路由給真正的請求處理程序
- 讓請求處理程序作出響應
①.不好的實現方式
②.阻塞與非阻塞
③.以非阻塞操作進行請求響應
五、更有用的場景
- 處理POST請求
- 處理文件上傳
六、總結與展望
二、JavaScript與Node.js(4節)
1.JavaScript與你
Node.js,服務端的JavaScript。----重新認識JavaScript。
2.簡短申明
本書並不是一本“從入門到精通”的書,更像是一本“從初級入門到高級入門”的書。
如果成功的話,那麼本書就是我們開始學習Node.js最希望擁有的教程。
3.服務端JavaScript
要實現在後臺運行JavaScript代碼,代碼需要先被解釋然後正確的執行。Node.js的原理正是如此,它使用了Google的V8虛擬機 (Google的Chrome瀏覽器使用的JavaScript執行環境),來解釋和執行JavaScript代碼。
Node.js事實上既是一個運行時環境,同時又是一個庫
4."Hello World"
打開你最喜歡的編輯器,創建一個helloworld.js文件。輸出“Hello World”,如下是實現該功能的代碼:
console.log("Hello World");
保存該文件,並通過Node.js來執行:
node helloworld.js
正常的話,就會在終端輸出Hello World 。
第二部分結束。
三、一個完整的基於Node.js的web應用
1.用例
我們來把目標設定得簡單點,不過也要夠實際才行:
- 用戶可以通過瀏覽器使用我們的應用。
- 當用戶請求http://domain/start時,可以看到一個歡迎頁面,頁面上有一個文件上傳的表單。
- 用戶可以選擇一個圖片並提交表單,隨後文件將被上傳到http://domain/upload,該頁面完成上傳後會把圖片顯示在頁面上。
差不多了,你現在也可以去Google一下,找點東西亂搞一下來完成功能。但是我們現在先不做這個。
更進一步地說,在完成這一目標的過程中,我們不僅僅需要基礎的代碼而不管代碼是否優雅。我們還要對此進行抽象,來尋找一種適合構建更爲複雜的Node.js應用的方式。
2.應用不同模塊分析(很重要很受益的一部分)
我們來分解一下這個應用,爲了實現上文的用例,我們需要實現哪些部分呢?
- 我們需要提供Web頁面,因此需要一個HTTP服務器
- 對於不同的請求,根據請求的URL,我們的服務器需要給予不同的響應,因此我們需要一個路由,用於把請求對應到請求處理程序(request handler)
- 當請求被服務器接收並通過路由傳遞之後,需要可以對其進行處理,因此我們需要最終的請求處理程序
- 路由還應該能處理POST數據,並且把數據封裝成更友好的格式傳遞給請求處理入程序,因此需要請求數據處理功能
- 我們不僅僅要處理URL對應的請求,還要把內容顯示出來,這意味着我們需要一些視圖邏輯供請求處理程序使用,以便將內容發送給用戶的瀏覽器
- 最後,用戶需要上傳圖片,所以我們需要上傳處理功能來處理這方面的細節
現在我們就來開始實現之路,先從第一個部分--HTTP服務器着手。
四、構建應用的模塊
1.一個基礎的HTTP服務器
現在我們來創建一個用於啓動我們的應用的主文件,和一個保存着我們的HTTP服務器代碼的模塊。
爲便於理解,把主文件叫做index.js或多或少是個標準格式。把服務器模塊放進叫server.js的文件裏則很好理解。
讓我們先從服務器模塊開始。如果你clone了項目會發現我們將要寫的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);
這個時候,在終端中輸入
node server
,瀏覽器訪問http://localhost:8888,你會看到一個寫着“Hello World”的網頁。到這裏相信大家都非常非常的熟悉,但是關於這幾行代碼的原理與分析你是否考慮過,或者自己明白了?到這裏,我們就先來談談HTTP服務器的問題,而把如何組織項目的事情先放一邊,我相信對於理解不深的人來說,會讓你對這幾行代碼有一個更清楚的理解。
2.分析HTTP服務器
第一行請求(
require
)Node.js自帶的 http 模塊,並且把它賦值給 http 變量。
接下來我們調用http模塊提供的函數: createServer 。這個函數會返回一個對象,這個對象有一個叫做 listen 的方法,這個方法有一個數值參數,指定這個HTTP服務器監聽的端口號。
咱們暫時先不管 http.createServer 的括號裏的那個函數定義。
我們本來可以用這樣的代碼來啓動服務器並偵聽8888端口:
var server = http.createServer();
server.listen(8888);
至此你肯定名了http是Node.js自帶的模塊,也知道了createServer是http模塊提供的函數,並且知道了這個函數返回的對象有一個listen偵聽端口的方法。接下來我們要開始對createServer函數中的內容進行一個剖析。
3.進行函數傳遞
我們先來看幾行代碼,並仔細閱讀它:
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中,一個 函數可以作爲另一個函數接收一個參數。我們可以先定義一個函數,然後傳遞,也可以在傳遞參數的地方直接定義函數。
本章總共11節,本章第三節結束。
4.函數傳遞是如何讓HTTP服務器工作的
帶着剛纔我們剛學習的一點小東西(如果有丁點疑惑,返回去繼續瞅一瞅,確保完全明白每個字節的意思),我們來看看我們簡約而不簡單的HTPP服務器:
var http = require("http");
http.createServer(function(request, response){
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);</pre>
是的,這個時候你已經知道,我們向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);
這個時候你有沒有疑問?我們爲什麼要用這樣的方式呢?對於剛剛開始學習看到這裏的我來說,我肯定不會去想這個問題的,開玩笑,我甚至都沒去想http這個東西是node自帶的模塊。
那麼作者提出的這個爲什麼,到底是想要說什麼?我們繼續進行下一節--【基於事件驅動的回調】,什麼是【基於事件驅動的回調】?這是什麼東西?
這個【基於事件驅動的回調】是個什麼東西?重要的文字寫三遍。
本章總共十一節,下面我們開始第五節的內容。
5.基於事件驅動的回調
於是我帶着【基於事件驅動的回調】這個名詞來到本節的時候,作者首先說了一句“這個問題不好回答(至少對於我來說)”
然後關於這個事件驅動回調的背景知識,作者給了一篇文章的鏈接,感興趣的可以去瞅瞅:http://debuggable.com/posts/understanding-node-js:4bd98440-45e4-4a9a-8ef7-0f7ecbdd56cb
這一切都歸結於“Node.js是事件驅動的”這一事實。好吧,其實我也不是特別確切的瞭解這句話的意思。不過我會試着解釋,爲什麼它對我們用Node.js寫網絡應用(Web based application)是有意義的。
- 當我們使用 http.createServer 方法的時候,我們當然不只是想要一個偵聽某個端口的服務器,我們還想要它在服務器收到一個HTTP請求的時候做點什麼。
問題是,這是異步的:請求任何時候都可能到達,但是我們的服務器卻跑在一個單進程中.
在我們的Node.js程序中,當一個新的請求到達8888端口的時候,我們怎麼控制流程呢?
- 我們創建了服務器,並且向創建它的方法傳遞了一個函數。無論何時我們的服務器收到一個請求,這個函數就會被調用。
- 我們不知道這件事情什麼時候會發生,但是我們現在有了一個處理請求的地方:它就是我們傳遞過去的那個函數。至於它是被預先定義的函數還是匿名函數,就無關緊要了
- 這個就是傳說中的 回調 。我們給某個方法傳遞了一個函數,這個方法在有相應事件發生時調用這個函數來進行 回調 。
讓我們再來琢磨琢磨這個新概念。我們怎麼證明,在創建完服務器之後,即使沒有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.");
代碼與解析分析:
當我們與往常一樣,運行它node server.js
時,它會馬上在命令行上輸出“Server has started.”。當我們向服務器發出請求(在瀏覽器訪問http://localhost:8888/ ),“Request received.”這條消息就會在命令行中出現。
這就是事件驅動的異步服務器端JavaScript和它的回調啦!
(請注意,當我們在服務器訪問網頁時,我們的服務器可能會輸出兩次“Request received.”。那是因爲大部分服務器都會在你訪問 http://localhost:8888 /時嘗試讀取 http://localhost:8888/favicon.ico )
本節總結:說了一大堆,通過代碼我們可以簡單理解,服務器啓動執行後端console
方法,客戶端向服務器發出請求,再執行另一個console
方法,這就是事件驅動的異步服務端JavaScript和它的回調。關於這個問題可自行多多研究。
本章總共11節,本章第五節結束。
6.服務器是如何處理請求的
好的,接下來我們簡單分析一下我們服務器代碼中剩下的部分,也就是我們的回調函數 onRequest() 的主體部分。
當回調啓動,我們的 onRequest() 函數被觸發的時候,有兩個參數被傳入: request 和 response 。
它們是對象,你可以使用它們的方法來處理HTTP請求的細節,並且響應請求(比如向發出請求的瀏覽器發回一些東西)。
所以我們的代碼就是:當收到請求時,使用 response.writeHead() 函數發送一個HTTP狀態200和HTTP頭的內容類型(content-type),使用 response.write() 函數在HTTP相應主體中發送文本“Hello World"。
最後,我們調用 response.end() 完成響應。
目前來說,我們對請求的細節並不在意,所以我們沒有使用 request 對象。
7.服務端的模塊放在哪裏
到這裏我們開始說
如何組織應用
這個問題上。
把服務器腳本放到一個叫做start的函數裏,然後導出這個函數,在主文件中使用:
#server.js
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
var server = require("./server");
server.start();
我們仍然只擁有整個應用的最初部分:我們可以接收HTTP請求。但是我們得做點什麼——對於不同的URL請求,服務器應該有不同的反應。對於一個非常簡單的應用來說,你可以直接在回調函數 onRequest() 中做這件事情,但是,當然應用不會這麼簡單了。
處理不同的HTTP請求在我們的代碼中是一個不同的部分,叫做“路由選擇”——那麼,我們接下來就創造一個叫做 路由 的模塊吧。
本章總共11節,本章第⑦節結束。
8.如何來進行請求的“路由”
我們要爲路由提供請求的URL和其他需要的GET及POST參數,隨後路由需要根據這些數據來執行相應的代碼.
因此,我們需要查看HTTP請求,從中提取出請求的URL以及GET/POST參數。
我們需要的所有數據都會包含在request對象中,該對象作爲onRequest()回調函數的第一個參數傳遞。但是爲了解析這些數據,我們需要額外的Node.JS模塊,它們分別是
ur
l和querystring
模塊。
現在我們來給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可以看到會輸出不同的
console
,我們可以通過請求的URL不同來區別不同的請求了。
現在我們可以來編寫路由了,建立一個名爲
router.js
的文件,添加以下內容:
function route(pathname){
console.log("About to route a request for "+ pathname);
}
exports.route = route;
這個時候,
index.js
、server.js
、rounter.js
全都有了,如何互相關聯就不贅述了。直接進入下一節。
9.行爲驅動執行
談一談函數編程:
將函數作爲參數傳遞並不僅僅出於技術上的考量。對軟件設計來說,這其實是個哲學問題。想想這樣的場景:在index文件中,我們可以將router對象傳遞進去,服務器隨後可以調用這個對象的route函數。
就像這樣,我們傳遞一個東西,然後服務器利用這個東西來完成一些事。嗨那個叫路由的東西,能幫我把這個路由一下嗎?
但是服務器其實不需要這樣的東西。它只需要把事情做完就行,其實爲了把事情做完,你根本不需要東西,你需要的是動作。也就是說,你不需要名詞,你需要動詞。
理解了這個概念裏最核心、最基本的思想轉換後,我自然而然地理解了函數編程。
據說這篇文章會讓你對函數編程有一個更好的理解:https://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html