JavaScript與Node.js
JavaScript與你
拋開技術(shù),我們先來聊聊你以及你和JavaScript的關(guān)系。本章的主要目的是想讓你看看,對你而言是否有必要繼續(xù)閱讀后續(xù)章節(jié)的內(nèi)容。
如果你和我一樣,那么你很早就開始利用HTML進行“開發(fā)”,正因如此,你接觸到了這個叫JavaScript有趣的東西,而對于JavaScript,你只會基本的操作——為web頁面添加交互。
而你真正想要的是“干貨”,你想要知道如何構(gòu)建復(fù)雜的web站點 —— 于是,你學(xué)習(xí)了一種諸如PHP、Ruby、Java這樣的編程語言,并開始書寫“后端”代碼。
與此同時,你還始終關(guān)注著JavaScript,隨著通過一些對jQuery,Prototype之類技術(shù)的介紹,你慢慢了解到了很多JavaScript中的進階技能,同時也感受到了JavaScript絕非僅僅是window.open()那么簡單。 .
不過,這些畢竟都是前端技術(shù),盡管當(dāng)想要增強頁面的時候,使用jQuery總讓你覺得很爽,但到最后,你頂多是個JavaScript用戶,而非JavaScript開發(fā)者。
然后,出現(xiàn)了Node.js,服務(wù)端的JavaScript,這有多酷啊?
于是,你覺得是時候該重新拾起既熟悉又陌生的JavaScript了。但是別急,寫Node.js應(yīng)用是一件事情;理解為什么它們要以它們書寫的這種方式來書寫則意味著——你要懂JavaScript。這次是玩真的了。
問題來了: 由于JavaScript真正意義上以兩種,甚至可以說是三種形態(tài)存在(從中世紀90年代的作為對DHTML進行增強的小玩具,到像jQuery那樣嚴格 意義上的前端技術(shù),一直到現(xiàn)在的服務(wù)端技術(shù)),因此,很難找到一個“正確”的方式來學(xué)習(xí)JavaScript,使得讓你書寫Node.js應(yīng)用的時候感覺 自己是在真正開發(fā)它而不僅僅是使用它。
因為這就是關(guān)鍵: 你本身已經(jīng)是個有經(jīng)驗的開發(fā)者,你不想通過到處尋找各種解決方案(其中可能還有不正確的)來學(xué)習(xí)新的技術(shù),你要確保自己是通過正確的方式來學(xué)習(xí)這項技術(shù)。
當(dāng)然了,外面不乏很優(yōu)秀的學(xué)習(xí)JavaScript的文章。但是,有的時候光靠那些文章是遠遠不夠的。你需要的是指導(dǎo)。
本書的目標(biāo)就是給你提供指導(dǎo)。更多學(xué)習(xí)資料請點擊此處
簡短申明
業(yè)界有非常優(yōu)秀的JavaScript程序員。而我并非其中一員。
我就是上一節(jié)中描述的那個我。我熟悉如何開發(fā)后端web應(yīng)用,但是對“真正”的JavaScript以及Node.js,我都只是新手。我也只是最近學(xué)習(xí)了一些JavaScript的高級概念,并沒有實踐經(jīng)驗。
因此,本書并不是一本“從入門到精通”的書,更像是一本“從初級入門到高級入門”的書。
如果成功的話,那么本書就是我當(dāng)初開始學(xué)習(xí)Node.js最希望擁有的教程。
服務(wù)端JavaScript
JavaScript最早是運行在瀏覽器中,然而瀏覽器只是提供了一個上下文,它定義了使用JavaScript可以做什么,但并沒有“說”太多關(guān)于 JavaScript語言本身可以做什么。事實上,JavaScript是一門“完整”的語言: 它可以使用在不同的上下文中,其能力與其他同類語言相比有過之而無不及。
Node.js事實上就是另外一種上下文,它允許在后端(脫離瀏覽器環(huán)境)運行JavaScript代碼。
要實現(xiàn)在后臺運行JavaScript代碼,代碼需要先被解釋然后正確的執(zhí)行。Node.js的原理正是如此,它使用了Google的V8虛擬機 (Google的Chrome瀏覽器使用的JavaScript執(zhí)行環(huán)境),來解釋和執(zhí)行JavaScript代碼。
除此之外,伴隨著Node.js的還有許多有用的模塊,它們可以簡化很多重復(fù)的勞作,比如向終端輸出字符串。
因此,Node.js事實上既是一個運行時環(huán)境,同時又是一個庫。
要使用Node.js,首先需要進行安裝。關(guān)于如何安裝Node.js,這里就不贅述了,可以直接參考官方的安裝指南。安裝完成后,繼續(xù)回來閱讀本書下面的內(nèi)容。
“Hello World”
好了,“廢話”不多說了,馬上開始我們第一個Node.js應(yīng)用:“Hello World”。
打開你最喜歡的編輯器,創(chuàng)建一個helloworld.js文件。我們要做就是向STDOUT輸出“Hello World”,如下是實現(xiàn)該功能的代碼:
console.log("Hello World");
保存該文件,并通過Node.js來執(zhí)行:
node helloworld.js
正常的話,就會在終端輸出Hello World。
好吧,我承認這個應(yīng)用是有點無趣,那么下面我們就來點“干貨”。
一個完整的基于Node.js的web應(yīng)用
用例
我們來把目標(biāo)設(shè)定得簡單點,不過也要夠?qū)嶋H才行:
用戶可以通過瀏覽器使用我們的應(yīng)用。
當(dāng)用戶請求http://domain/start時,可以看到一個歡迎頁面,頁面上有一個文件上傳的表單。
用戶可以選擇一個圖片并提交表單,隨后文件將被上傳到http://domain/upload,該頁面完成上傳后會把圖片顯示在頁面上。
差不多了,你現(xiàn)在也可以去Google一下,找點東西亂搞一下來完成功能。但是我們現(xiàn)在先不做這個。
更進一步地說,在完成這一目標(biāo)的過程中,我們不僅僅需要基礎(chǔ)的代碼而不管代碼是否優(yōu)雅。我們還要對此進行抽象,來尋找一種適合構(gòu)建更為復(fù)雜的Node.js應(yīng)用的方式。
應(yīng)用不同模塊分析
我們來分解一下這個應(yīng)用,為了實現(xiàn)上文的用例,我們需要實現(xiàn)哪些部分呢?
我們需要提供Web頁面,因此需要一個HTTP服務(wù)器
對于不同的請求,根據(jù)請求的URL,我們的服務(wù)器需要給予不同的響應(yīng),因此我們需要一個路由,用于把請求對應(yīng)到請求處理程序(request handler)
當(dāng)請求被服務(wù)器接收并通過路由傳遞之后,需要可以對其進行處理,因此我們需要最終的請求處理程序
路由還應(yīng)該能處理POST數(shù)據(jù),并且把數(shù)據(jù)封裝成更友好的格式傳遞給請求處理入程序,因此需要請求數(shù)據(jù)處理功能
我們不僅僅要處理URL對應(yīng)的請求,還要把內(nèi)容顯示出來,這意味著我們需要一些視圖邏輯供請求處理程序使用,以便將內(nèi)容發(fā)送給用戶的瀏覽器
最后,用戶需要上傳圖片,所以我們需要上傳處理功能來處理這方面的細節(jié)
我們先來想想,使用PHP的話我們會怎么構(gòu)建這個結(jié)構(gòu)。一般來說我們會用一個Apache HTTP服務(wù)器并配上mod_php5模塊。
從這個角度看,整個“接收HTTP請求并提供Web頁面”的需求根本不需要PHP來處理。
不過對Node.js來說,概念完全不一樣了。使用Node.js時,我們不僅僅在實現(xiàn)一個應(yīng)用,同時還實現(xiàn)了整個HTTP服務(wù)器。事實上,我們的Web應(yīng)用以及對應(yīng)的Web服務(wù)器基本上是一樣的。
聽起來好像有一大堆活要做,但隨后我們會逐漸意識到,對Node.js來說這并不是什么麻煩的事。
現(xiàn)在我們就來開始實現(xiàn)之路,先從第一個部分--HTTP服務(wù)器著手。
構(gòu)建應(yīng)用的模塊
一個基礎(chǔ)的HTTP服務(wù)器
當(dāng)我準(zhǔn)備開始寫我的第一個“真正的”Node.js應(yīng)用的時候,我不但不知道怎么寫Node.js代碼,也不知道怎么組織這些代碼。
我應(yīng)該把所有東西都放進一個文件里嗎?網(wǎng)上有很多教程都會教你把所有的邏輯都放進一個用Node.js寫的基礎(chǔ)HTTP服務(wù)器里。但是如果我想加入更多的內(nèi)容,同時還想保持代碼的可讀性呢?
實際上,只要把不同功能的代碼放入不同的模塊中,保持代碼分離還是相當(dāng)簡單的。
這種方法允許你擁有一個干凈的主文件(main file),你可以用Node.js執(zhí)行它;同時你可以擁有干凈的模塊,它們可以被主文件和其他的模塊調(diào)用。
那么,現(xiàn)在我們來創(chuàng)建一個用于啟動我們的應(yīng)用的主文件,和一個保存著我們的HTTP服務(wù)器代碼的模塊。
在我的印象里,把主文件叫做index.js或多或少是個標(biāo)準(zhǔn)格式。把服務(wù)器模塊放進叫server.js的文件里則很好理解。
讓我們先從服務(wù)器模塊開始。在你的項目的根目錄下創(chuàng)建一個叫server.js的文件,并寫入以下代碼:
var http = require("http");
http.createServer(function(request, response){
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);
搞定!你剛剛完成了一個可以工作的HTTP服務(wù)器。為了證明這一點,我們來運行并且測試這段代碼。首先,用Node.js執(zhí)行你的腳本:
node server.js
接下來,打開瀏覽器訪問http://localhost:8888/,你會看到一個寫著“Hello World”的網(wǎng)頁。
這很有趣,不是嗎?讓我們先來談?wù)凥TTP服務(wù)器的問題,把如何組織項目的事情先放一邊吧,你覺得如何?我保證之后我們會解決那個問題的。
分析HTTP服務(wù)器
那么接下來,讓我們分析一下這個HTTP服務(wù)器的構(gòu)成。
第一行請求(require)Node.js自帶的http模塊,并且把它賦值給http變量。
接下來我們調(diào)用http模塊提供的函數(shù):createServer。這個函數(shù)會返回一個對象,這個對象有一個叫做listen的方法,這個方法有一個數(shù)值參數(shù),指定這個HTTP服務(wù)器監(jiān)聽的端口號。
咱們暫時先不管http.createServer的括號里的那個函數(shù)定義。
我們本來可以用這樣的代碼來啟動服務(wù)器并偵聽8888端口:
var http = require("http");
var server = http.createServer();
server.listen(8888);
這段代碼只會啟動一個偵聽8888端口的服務(wù)器,它不做任何別的事情,甚至連請求都不會應(yīng)答。
最有趣(而且,如果你之前習(xí)慣使用一個更加保守的語言,比如PHP,它還很奇怪)的部分是createSever()的第一個參數(shù),一個函數(shù)定義。
實際上,這個函數(shù)定義是createServer()的第一個也是唯一一個參數(shù)。因為在JavaScript中,函數(shù)和其他變量一樣都是可以被傳遞的。
進行函數(shù)傳遞
舉例來說,你可以這樣做:
function say(word){
console.log(word);
}
function execute(someFunction, value){
someFunction(value);
}
execute(say,"Hello");
請仔細閱讀這段代碼!在這里,我們把say函數(shù)作為execute函數(shù)的第一個變量進行了傳遞。這里返回的不是say的返回值,而是say本身!
這樣一來,say就變成了execute中的本地變量someFunction,execute可以通過調(diào)用someFunction()(帶括號的形式)來使用say函數(shù)。
當(dāng)然,因為say有一個變量,execute在調(diào)用someFunction時可以傳遞這樣一個變量。
我們可以,就像剛才那樣,用它的名字把一個函數(shù)作為變量傳遞。但是我們不一定要繞這個“先定義,再傳遞”的圈子,我們可以直接在另一個函數(shù)的括號中定義和傳遞這個函數(shù):
function execute(someFunction, value){
someFunction(value);
}
execute(function(word){ console.log(word)},"Hello");
我們在execute接受第一個參數(shù)的地方直接定義了我們準(zhǔn)備傳遞給execute的函數(shù)。
用這種方式,我們甚至不用給這個函數(shù)起名字,這也是為什么它被叫做匿名函數(shù)。
這是我們和我所認為的“進階”JavaScript的第一次親密接觸,不過我們還是得循序漸進?,F(xiàn)在,我們先接受這一點:在JavaScript中,一個 函數(shù)可以作為另一個函數(shù)接收一個參數(shù)。我們可以先定義一個函數(shù),然后傳遞,也可以在傳遞參數(shù)的地方直接定義函數(shù)。
函數(shù)傳遞是如何讓HTTP服務(wù)器工作的
帶著這些知識,我們再來看看我們簡約而不簡單的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);
現(xiàn)在它看上去應(yīng)該清晰了很多:我們向createServer函數(shù)傳遞了一個匿名函數(shù)。
用這樣的代碼也可以達到同樣的目的:
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);
也許現(xiàn)在我們該問這個問題了:我們?yōu)槭裁匆眠@種方式呢?
基于事件驅(qū)動的回調(diào)?實在太多,有興趣的朋友可點擊閱讀全文