本书状态
你正在阅读的已经是本书的最终版。因此,只有当进行错误更正以及针对新版本Node.js的改动进行对应的修正时,才会进行更新。
本书中的代码案例都在Node.js 0.6.11版本中测试过,可以正确工作。
读者对象
本书最适合与我有相似技术背景的读者: 至少对一门诸如Ruby、Python、PHP或者Java这样面向对象的语言有一定的经验;对JavaScript处于初学阶段,并且完全是一个Node.js的新手。
这里指的适合对其他编程语言有一定经验的开发者,意思是说,本书不会对诸如数据类型、变量、控制结构等等之类非常基础的概念作介绍。要读懂本书,这些基础的概念我都默认你已经会了。
然而,本书还是会对JavaScript中的函数和对象作详细介绍,因为它们与其他同类编程语言中的函数和对象有很大的不同。
本书结构
读完本书之后,你将完成一个完整的web应用,该应用允许用户浏览页面以及上传文件。
当然了,应用本身并没有什么了不起的,相比为了实现该功能书写的代码本身,我们更关注的是如何创建一个框架来对我们应用的不同模块进行干净地剥离。 是不是很玄乎?稍后你就明白了。
本书先从介绍在Node.js环境中进行JavaScript开发和在浏览器环境中进行JavaScript开发的差异开始。
紧接着,会带领大家完成一个最传统的“Hello World”应用,这也是最基础的Node.js应用。
最后,会和大家讨论如何设计一个“真正”完整的应用,剖析要完成该应用需要实现的不同模块,并一步一步介绍如何来实现这些模块。
可以确保的是,在这过程中,大家会学到JavaScript中一些高级的概念、如何使用它们以及为什么使用这些概念就可以实现而其他编程语言中同类的概念就无法实现。
该应用所有的源代码都可以通过 本书Github代码仓库:https://github.com/ManuelKiessling/NodeBeginnerBook/tree/master/code/application.
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”,如下是實作該功能的程式碼:
儲存該文件,並透過Node.js來執行:
正常的話,就會在終端輸出Hello World 。
好吧,我承認這個應用是有點無趣,那麼下面我們就來點「乾貨」。
一個完整的基於Node.js的web應用程式
用例
我們來把目標設定得簡單點,不過也要夠實際才行:
1.使用者可以透過瀏覽器使用我們的應用程式。
2.當使用者要求http://domain/start時,可以看到一個歡迎頁面,頁面上有一個檔案上傳的表單。
3.使用者可以選擇一張圖片並提交表單,隨後檔案會上傳到http://domain/upload,該頁面完成上傳後會把圖片顯示在頁面上。
差不多了,你現在也可以去Google一下,找點東西亂搞一下來完成功能。但是我們現在先不做這個。
更進一步地說,在完成這一目標的過程中,我們不僅僅需要基礎的程式碼而不管程式碼是否優雅。我們也要對此進行抽象,尋找適合建構更為複雜的Node.js應用的方式。
應用不同模組分析
我們來分解這個應用,為了實現上文的用例,我們需要實現哪些部分呢?
1.我們需要提供Web頁面,因此需要一個HTTP伺服器
2.對於不同的請求,根據請求的URL,我們的伺服器需要給予不同的回應,因此我們需要一個路由,用於把請求對應到請求處理程序(request handler)
3.當請求被伺服器接收並透過路由傳遞之後,需要可以對其進行處理,因此我們需要最終的請求處理程序
4.路由也應該能處理POST數據,並且把數據封裝成更友好的格式傳遞給請求處理入程序,因此需要請求數據處理功能
5.我們不僅僅要處理URL對應的請求,還要把內容顯示出來,這意味著我們需要一些視圖邏輯供請求處理程序使用,以便將內容發送給用戶的瀏覽器
6.最後,用戶需要上傳圖片,所以我們需要上傳處理功能來處理這方面的細節
我們先來想想,使用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的文件,並寫入以下程式碼:
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World") ;
response.end();
}).listen(8888);
node server.js
接下來,打開瀏覽器訪問http://localhost:8888/,你會看到一個寫著「Hello World」的網頁。
這很有趣,不是嗎?讓我們先來談談HTTP伺服器的問題,把如何整理專案的事情先放一邊吧,你覺得如何?我保證之後我們會解決那個問題的。
分析HTTP伺服器
那麼接下來,讓我們來分析一下這個HTTP伺服器的組成。
第一行請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
接下來我們呼叫http模組提供的函數: createServer 。這個函數會傳回一個對象,這個物件有一個叫做 listen 的方法,這個方法有一個數值參數,指定這個HTTP伺服器監聽的連接埠號碼。
咱們暫時先不管 http.createServer 的括號裡的那個函數定義。
我們本來可以用這樣的程式碼來啟動伺服器並偵聽8888埠:
server.listen(8888);
這段程式碼只會啟動一個偵聽8888埠的伺服器,它不做任何別的事情,它不做任何別的事情,甚至連請求都不會回應。
最有趣(而且,如果你之前習慣使用一個更保守的語言,例如PHP,它還很奇怪)的部分是 createSever() 的第一個參數,一個函數定義。 實際上,這個函數定義是 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 函式。
function execute(someFunction, value) {
someFunction(value);
someFunction(value);
}some
execute(function(word){ console.log(word) }, "Hello");我們在 execute 接受第一個參數的地方直接定義了我們準備傳遞給 execute 的函數。 用這種方式,我們甚至不用給這個函數取名字,這也是為什麼它被叫做 匿名函數 。
這是我們和我所認為的「進階」JavaScript的第一次親密接觸,不過我們還是得循序漸進。現在,我們先接受這一點:在JavaScript中,一個函數可以作為另一個函數接收一個參數。我們可以先定義一個函數,然後再傳遞,也可以在傳遞參數的地方直接定義函數。
程式碼如下:
var http = require("http");
用這樣的程式碼也可以達到同樣的目的:
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的事件驅動設計能夠真正幫忙的地方了——雖然我們還得學一些新概念才能掌握它。讓我們來看看這些概念是怎麼應用在我們的伺服器程式碼裡的。
我們創建了伺服器,並且向創建它的方法傳遞了一個函數。無論何時我們的伺服器收到一個請求,這個函數就會被呼叫。
我們不知道這件事情什麼時候會發生,但是我們現在有了一個處理請求的地方:它就是我們傳遞過去的那個函數。至於它是被預先定義的函數還是匿名函數,就無關緊要了。
程式碼如下:
var http = require("http ");
function onRequest(request, response) {
console.log("Request received.");response.writeHead(200, {"Content-Type": "text/plain"});
當我們像往常一樣,運行它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 主檔案使用。
也許你已經注意到,我們已經在程式碼中使用了模組了。像這樣:
...
http.createServer(...);
這把我們的本地變數變成了一個擁有所有 http 模組所提供的公共方法的物件。
給這種本地變數取一個和模組名稱一樣的名字是一種慣例,但是你也可以按照自己的喜好來:
...
foo.createServer(...);
等我們把 server.js 變成真正的模組,你就能搞清楚了。
事實上,我們不用做太多的修改。把某段程式碼變成模組意味著我們需要把我們希望提供其功能的部分 導出 到請求這個模組的腳本。
目前,我們的HTTP伺服器需要匯出的功能非常簡單,因為請求伺服器模組的腳本只是需要啟動伺服器而已。
我們把我們的伺服器腳本放到一個叫做 start 的函數裡,然後我們會匯出這個函數。
var http = require("http");
function start() {
function onRequest(request, response) {
console.log("Request received.");
text/plain"});
response.end();
}
console.log("Server has started.");
建立index.js 檔案並寫入以下內容:
var server = require("./server");
程式碼如下:
node index.js非常好,我們現在可以把我們的應用的不同部分放入不同的文件裡,並且通過生成模組的方式把它們連接到一起了。我們仍然只擁有整個應用的最初部分:我們可以接收HTTP請求。但是我們得做點什麼——對於不同的URL請求,伺服器應該有不同的反應。
對於一個非常簡單的應用來說,你可以直接在回呼函數 onRequest() 中做這件事。不過就像我說過的,我們應該要加入一些抽象的元素,讓我們的例子變得更有趣一點。處理不同的HTTP請求在我們的程式碼中是一個不同的部分,叫做「路由選擇」-那麼,我們接下來就創造一個叫做 路由 的模組吧。
如何來進行請求的「路由」
現在我們來為onRequest()函數加上一些邏輯,用來找出瀏覽器請求的URL路徑:
var http = require("http");
var url = require("url");
function start() {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.write("Hello World"); response.write("Hello World"); );
}
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()函數,以便將路由函數作為參數傳遞過去:
function start(route) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
. received.");
route(pathname);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.write("Hello World");
response.write("Hello World");
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
如果現在啟動應用程式(node index.js,始終記得這個命令列),隨後請求一個URL,你將會看到應用輸出相應的信息,這表明我們的HTTP伺服器已經在使用路由模組了,並會將要求的路徑傳遞給路由:
行為驅動執行
請容許我再次脫離主題,在這裡談一談函數式程式設計。
將函數作為參數傳遞並不僅僅出於技術上的考量。對軟體設計來說,這其實是個哲學問題。想想這樣的場景:在index檔案中,我們可以將router物件傳遞進去,伺服器接著可以呼叫這個物件的route函數。
就像這樣,我們傳遞一個東西,然後伺服器利用這個東西來完成一些事。嗨那個叫路由的東西,可以幫我把這個路由一下嗎?
但是伺服器其實不需要這樣的東西。它只需要把事情做完就行,其實為了把事情做完,你根本不需要東西,你需要的是動作。也就是說,你不需要名詞,你需要動詞。
理解了這個概念裡最核心、最基本的思想轉換後,我自然而然地理解了函數程式設計。
我是在讀了Steve Yegge的大作名詞王國中的死刑之後理解函數程式設計。你也去讀這本書吧,真的。這是曾經給我閱讀的快樂的關於軟體的書籍之一。
路由給真正的請求處理程序
回到正題,現在我們的HTTP伺服器和請求路由模組已經如我們的期望,可以相互交流了,就像一對親密無間的兄弟。
當然這還遠遠不夠,路由,顧名思義,是指我們要針對不同的URL有不同的處理方式。例如處理/start的「業務邏輯」就應該和處理/upload的不同。
在現在的實現下,路由過程會在路由模組中“結束”,並且路由模組並不是真正針對請求“採取行動”的模組,否則當我們的應用程式變得更為複雜時,將無法很好地擴展。
我們暫時把作為路由目標的函數稱為請求處理程序。現在我們不要急著開發路由模組,因為如果請求處理程序沒有就緒的話,再怎麼完善路由模組也沒有多大意義。
應用程式需要新的零件,因此加入新的模組 -- 已經無需為此感到新奇了。我們來建立一個叫做requestHandlers的模組,並且對於每一個請求處理程序,添加一個佔位用函數,隨後將這些函數作為模組的方法導出:
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 handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers. upload;
server.start(router.route, handle);
如所見,將不同的URL對應到相同的請求處理程序上是很容易的:只要在物件中加入一個鍵為"/"的屬性,對應requestHandlers.start即可,這樣我們就可以乾淨簡潔地設定/start和/的請求都交由start這一處理程序處理。
在完成了物件的定義後,我們把它作為額外的參數傳遞給伺服器,為此將server.js修改如下:
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.write("Hello World");
response.write("Hello World");
response>;
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
然後我們相應地在route.js檔案中修改route()函數:
exports.route = route;
有了這些,我們就把伺服器、路由和請求處理程序在一起了。現在我們啟動應用程式並在瀏覽器中存取http://localhost:8888/start,以下日誌可以說明系統呼叫了正確的請求處理程序:
讓請求處理程序回應
很好。不過現在如果請求處理程序能夠向瀏覽器傳回一些有意義的資訊而並非全是“Hello World”,那就更好了。這裡要記住的是,瀏覽器發出請求後獲得並顯示的「Hello World」資訊仍是來自於我們server.js檔案中的onRequest函數。
其實“處理請求”說白了就是“對請求作出回應”,因此,我們需要讓請求處理程序能夠像onRequest函數那樣可以和瀏覽器進行“對話”。
不好的實作方式
對於我們這樣擁有PHP或Ruby技術背景的開發者來說,最直截了當的實作方式事實上並不是非常可靠: 看似有效,實則未必如此。這裡我指的「直截了當的實作方式」意思是:讓請求處理程序透過onRequest函數直接回傳(return())他們要展示給使用者的資訊。
我們先就這樣去實現,然後再來看為什麼這不是一種很好的實現方式。
讓我們從讓請求處理程序返回需要在瀏覽器中顯示的資訊開始。我們需要將requestHandler.js修改為以下形式:
}
function upload() {
console.log("Request handler 'upload' was called.");
}
}
[pathname]();
🎜>
exports.route = route;
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
var content = route(handle, pathname)
.end();
}
http.createServer(onRequest).listen(8888);
}
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) {
while (new Date().getTime()
sleep(10000);
(當然了,這裡只是模擬休眠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請求處理程序來「開刀」。將其修改成以下形式:
console.log("Request handler 'start' was called.");
var content = "empty";
content = stdout;
});
}
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
}
exports.start = start;
exports.upload = upload;
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()的匿名函數:
我們這裡「ls -lah」的操作其實是非常快的(除非目前目錄下有上百萬個檔案)。這也是為什麼回呼函數也會很快的執行到 —— 不過,不管怎麼說它還是異步的。
為了讓效果更加明顯,我們想像一個更耗時的命令: “find /”,它在我機器上需要執行1分鐘左右的時間,然而,儘管在請求處理程序中,我把“ls - lah”換成“find /”,當開啟/start URL的時候,依然能夠立即獲得HTTP回應—— 很明顯,當exec()在後台執行的時候,Node.js本身會繼續執行後面的程式碼。而我們這裡假設傳遞給exec()的回呼函數,只會在「find /」指令執行完成之後才會被呼叫。
那究竟我們要如何實現將目前目錄下的檔案清單顯示給使用者呢?
好,在了解了這種不好的實作方式之後,我們接下來來介紹如何以正確的方式讓請求處理程序對瀏覽器請求作出回應。
以非阻塞操作進行請求回應
我剛剛提到了這樣一個短語 —— “正確的方式”。而事實上通常「正確的方式」一般都不簡單。
不過,用Node.js就有這樣一種實作方案: 函數傳遞。下面就讓我們來具體看看如何實現。
到目前為止,我們的應用程式已經可以透過應用各層之間傳遞值的方式(請求處理程序-> 請求路由-> 伺服器)將請求處理程序傳回的內容(請求處理程序最終要顯示給用戶的內容)傳遞給HTTP伺服器。
現在我們採用如下這種新的實作方式:相對採用將內容傳遞給伺服器的方式,我們這次採用將伺服器「傳遞」給內容的方式。 從實作角度來說,就是將response物件(從伺服器的回呼函數onRequest()取得)透過請求路由傳遞給請求處理程序。 隨後,處理程序就可以採用該物件上的函數來回應請求。
原理就是如此,接下來讓我們一步一步實現這個方案。
先從server.js開始:
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
}
console.log("Server has started.");
}
}
同樣的模式:相對先前從請求處理程序中取得回傳值,這次取而代之的是直接傳遞response物件。
如果沒有對應的請求處理器處理,我們就直接回傳「404」錯誤。
程式碼如下:
程式碼如下:
程式碼如下:var exec = require("child_process").exec;
console.log("Request handler 'start' was called.");
});
}function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"} );
這時再次我們啟動應用(node index.js),一切都會運作的很好。
複製程式碼
程式碼如下:
var exec = require("child_process").exec;
function start(response) {
console.log("Request handler 'start' was called.");
{ timeout: 10000, maxBuffer: 20000*1024 },
function (error, stdout, stder) { 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 応答がまだ処理中であっても。
その他の便利なシナリオ
ここまではうまくいきましたが、このアプリケーションは実用的ではありません。
サーバー、リクエスト ルーティング、およびリクエスト ハンドラーが完了しました。前の使用例に従って、Web サイトにインタラクションを追加しましょう。ユーザーはファイルを選択し、ファイルをアップロードし、アップロードされたファイルをブラウザーで確認します。 わかりやすくするために、ユーザーは画像をアップロードするだけで、アプリはその画像をブラウザーに表示すると想定します。
それでは、段階的に実装していきましょう。これまでに JavaScript の原則と技術的な内容をたくさん紹介したので、今回は少しスピードアップしてみましょう。
この機能を実装するには、次の 2 つの手順があります。 まず、POST リクエスト (ファイル以外のアップロード) を処理する方法を見てみましょう。その後、ファイルのアップロードに Node.js の外部モジュールを使用します。この実装には 2 つの理由があります。
まず、Node.js での基本的な POST リクエストの処理は比較的簡単ですが、その過程で多くのことを学ぶことができます。
2 番目に、Node.js を使用してファイルのアップロード (マルチパート POST リクエスト) を処理する方法は比較的複雑であり、本書の範囲外です。ただし、外部モジュールの使用方法は本書の範囲内です。
POST リクエストの処理
簡単な例を考えてみましょう。ユーザーがコンテンツを入力するためのテキストエリアを表示し、それを POST リクエスト経由でサーバーに送信します。最後に、サーバーはリクエストを受信し、ハンドラーを通じて入力コンテンツをブラウザーに表示します。
/start リクエスト ハンドラーはテキスト領域のあるフォームを生成するために使用されるため、requestHandlers.js を次の形式に変更します:
function start(response) {
console.log("リクエスト ハンドラー 'start' が呼び出されました。");
var body = ''
'
response.write(body);
response.end();
}
console.log("リクエスト ハンドラー 'upload' が呼び出されました。");
response.writeHead(200, {"Content-Type": "text/plain"} );
response.write("Hello Upload");
response.end();
}
exports.upload = Upload;
さて、これでアプリケーションは非常に完成し、Webby Awards を受賞することもできます (笑)。 (翻訳者注: Webby Awards は、国際デジタル芸術科学アカデミーが主催する、世界で最も優れた Web サイトを選出する賞です。詳細については、詳細な説明を参照してください) http://localhost にアクセスするとご覧いただけます。 8888/ブラウザで起動 これは簡単な形式です。サーバーを再起動することを忘れないでください。
残りのスペースでは、より興味深い問題について説明します。ユーザーがフォームを送信すると、/upload リクエスト ハンドラーがトリガーされて POST リクエストを処理します。
私たちは初心者の中の専門家であるため、非同期コールバックを使用して POST リクエスト データをノンブロッキングに処理することを考えるのは自然なことです。
ここではノンブロッキングが使用されています