簡介
CoffeeScript 是構建在 JavaScript 基礎之上的一種全新程式語言,提供了能夠吸引 Python 或 Ruby 愛好者的整潔的語法。此外還提供了受 Haskell 和 Lisp 等語言啟發得出的許多函數語言程式設計特性。
在本 系列文章 的 第 1 部分 中,我們瞭解了使用 CoffeeScript 的優勢。此外還設定了開發環境,執行了指令碼。在 第 2 部分 中,我們在嘗試解決數學問題的過程中嘗試了許多 CoffeeScript 特性,探索了 CoffeeScript 程式語言。在 第 3 部分 中,為一個 Web 應用程式編寫了客戶端程式碼。
在最後的這篇文章中,您將編寫伺服器端元件,並完成應用程式 — 所有一切都是使用 CoffeeScript 完成的。
下載 本文中使用的原始碼。
呼叫所有 Web 服務
第 3 部分 中的 Web 應用程式使用一個關鍵字執行了 Google 和 Twitter 搜尋。對於應用程式的客戶端,您模擬了來自伺服器的結果。為了實際實現此類功能,您需要應用程式的伺服器端呼叫 Google 和 Twitter 提供的 Web 服務。兩家公司均提供了非常簡單的搜尋服務。您只需對搜尋服務發出 HTTP GET 請求即可。清單 1 展示了發出 HTTP GET 請求的一般函式。
清單 1. 獲取 Web 資源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
http = require "http" fetchPage = (host, port, path, callback) -> options = host: host port: port path: path req = http.get options, (res) -> contents = "" res.on 'data', (chunk) -> contents += "#{chunk}" res.on 'end', () -> callback(contents) req.on "error", (e) -> console.log "Erorr: {e.message}" |
require
語句是指令碼的第一條語句,在本系列的 第 1 部分 中已經對此進行了簡單的介紹。這是一種 Node.js 模組匯入語法,或者至少應該說是這種語法的 CoffeeScript 版本。“原生” 版本應該是 var http = require("http");
。在這篇文章中,您將使用多個 Node.js 核心模組。(這些模組的工作原理不在本文討論範圍之內。)如果您安裝了 Node.js,那麼就應該能使用本文中使用的所有模組(請參見 第 1 部分)。對於 清單 1 中的示例,您使用的是 http
模組,它為發出和接收 HTTP 請求提供了一些非常有用的類和函式。
清單 1 隨後定義了一個 fetchPage
函式,可以接受以下四個引數:
1. 資源的 host
名稱。
2. 資源的 port
。
3. 資源的 path
。
4. 一個 callback
函式。
Node.js 中任何型別的 I/O 函式在本質上都是非同步的,因此在完成時需要通過一個 callback
函式進行呼叫。fetchPage
函式接受一個 callback
函式作為第四個引數。隨後使用前三個引數,通過 http
模組的 get
函式發出一條 HTTP GET 請求。
fetchPage
函式也獲取一個 callback
函式,將有一個 ClientResponse
例項傳遞給後一個函式。ClientResponse
是 http
模組中定義的一個物件,它實現了 ReadableStream
介面(Node.js 中的核心介面)。這是一個非同步介面,接受兩個事件:data
和 end
。其惟一的函式用於為這些事件註冊回撥。在從您發出 HTTP GET 請求的資源接收到資料時,將發生資料事件。
資源將一次性返回所有資料,但更常見的做法是分塊傳送資料。接收到各塊時,資料事件將被觸發,回撥將被呼叫。您建立了一個名為 contents
的變數;每次接收到另一個塊時,都會將其附加到 contents
。接收了所有資料之後,即觸發 end
事件。現在,您獲得了全部資料,因此可以將 contents
傳遞給傳入 fetchPage
函式的 callback
函式。定義了這個多用途函式之後,下面我們將為 Google 和 Twitter 搜尋 API 建立一些專用函式,如 清單 2 所示。
清單 2. Google 與 Twitter 搜尋函式
1 2 3 4 5 6 7 8 9 |
googleSearch = (keyword, callback) -> host = "ajax.googleapis.com" path = "/ajax/services/search/web?v=1.0&q=#{encodeURI(keyword)}" fetchPage host, 80, path, callback twitterSearch = (keyword, callback) -> host = "search.twitter.com" path = "/search.json?q=#{encodeURI(keyword)}" fetchPage host, 80, path, callback |
清單 2 中定義了兩個函式:
1. googleSearch
,用於獲取一個 keyword
和一個 callback
函式。它將固定主機,並使用 CoffeeScript 的字串插值建立路徑,隨後使用 fetchPage
。
2. twitterSearch
,該函式與 googleSearch
極為相似,但使用了不同的主機和路徑值。
對於兩個路徑值,您都要使用字串插值和 JavaScript 提供的便捷的 encodeURI
函式來處理任何空格或其他特殊字元。現在您已經擁有了這些搜尋函式,下面即可為合併搜尋場景建立特殊函式。
合併非同步函式
您可以通過多種方法在 Google 和 Twitter 上執行合併搜尋。您可以呼叫 googleSearch
,隨後在 callback
中呼叫 twitterSearch
,或者相反。然而,Node.js 的非同步/回撥架構使您能夠更優雅、更高效地完成任務。清單 3 展示了合併搜尋。
清單 3. 同時搜尋 Google 和 Twitter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
combinedSearch = (keyword, callback) -> data = google : "" twitter : "" googleSearch keyword, (contents) -> contents = JSON.parse contents data.google = contents.responseData.results if data.twitter != "" callback(data) twitterSearch keyword, (contents) -> contents = JSON.parse contents data.twitter = contents.results if data.google != "" callback(data) |
combinedSearch
函式有一項現在已經廣為人知的特徵:接受一個關鍵字和一個回撥。隨後它為合併搜尋結果建立一個資料結構,名為data
。data
物件擁有一個 google
欄位和一個 twitter
欄位,兩者均初始化為空字串。下一步是呼叫 googleSearch
函式。在回撥中,您將使用標準 JSON.parse
函式解析來自 Google 的結果。Google 返回的 JSON 文字將解析為 JavaScript 物件。這種它來設定data.google
欄位的值。呼叫 googleSearch
之後,再呼叫 twitterSearch
。其 callback
函式與 googleSearch
的回撥函式極為相似。
有必要理解,在兩個回撥中,您都要檢查是否有來自另一個回撥的資料。您無法確知哪個回撥先完成。因此需要檢視是否有來自 Google 和 Twitter 的資料。確認之後,即可呼叫之前傳入 combinedSearch
函式的 callback
函式。您現在得到了一個同時搜尋 Google 和 Twitter 並提供合併結果的函式。下一個任務就是將這樣的結果公開到您在本系列的 第 3 部分
CoffeeScript Web 伺服器
至此,您已經得到了:
1. 一個能夠傳送關鍵字、顯示搜尋結果的網頁。
2.一個能夠接受關鍵字並生成 Google 和 Twitter 搜尋結果的函式。
怎樣將這一切關聯起來?您可以將該伺服器稱為 Web 伺服器、應用伺服器,甚至是中介軟體。無論怎樣稱呼,在 CoffeeScript 為它編寫程式碼都非常容易。
Web 伺服器需要滿足兩個目的。顯然,它需要接受合併搜尋的請求。此外還需要提供您在 第 3 部分 中建立的靜態資源。您要建立的是一個 Web 應用程式,因此必須密切注意同源策略。搜尋呼叫必須發往生成網頁的相同位置。我們首先來處理靜態資源。清單 4 展示了一個處理靜態資源的函式。
清單 4. 處理靜態資源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
path = require "path" fs = require "fs" serveStatic = (uri, response) -> fileName = path.join process.cwd(), uri path.exists fileName, (exists) -> if not exists response.writeHead 404, 'Content-Type': 'text/plain' response.end "404 Not Found #{uri}!\n" return fs.readFile fileName, "binary", (err,file) -> if err response.writeHead 500, 'Content-Type': 'text/plain' response.end "Error #{uri}: #{err} \n" return response.writeHead 200 response.write file, "binary" response.end() |
serveStatic
函式處理 Web 應用程式中對靜態資源的請求。請注意,您還需要使用兩個 Node.js 模組:
1. path
是一個處理檔案路徑的實用工具庫。
2. 檔案系統或 fs
提供了 Node.js 中的所有檔案 I/O,大體上就是基於標準 POSIX 函式的一個包裝器。
serveStatic
函式接受兩個引數:
1. uri
實際上是 Web 瀏覽器所請求的靜態檔案的相對路徑。
2. ServerResponse
物件,是 http
模組中定義的另外一種型別。它的功能之一就是使您能夠寫入 HTTP GET 向資源請求的資料。
在 serveStatic
中,使用 process.cwd
將檔案的相對路徑轉為絕對路徑。process
物件是一個全域性物件,指示正在執行 Node.js 的系統程式。它的 cwd
方法提供了當前工作目錄。使用 path
模組,整合當前工作目錄和您需要的檔案的相對目錄,結果將得到一個絕對路徑。有了絕對路徑,您就可以再次使用 path
模組,檢查檔案是否存在。檢查一個檔案是否存在時,需要涉及到 I/O,因此這是一個非同步函式。為其傳遞 fileName
和回撥函式。回撥函式將提供一個布林值,使您瞭解檔案是否存在。如果不存在,那麼您就就需要寫出一條 HTTP 404 “檔案未找到”訊息。
如果檔案確實存在,那麼就需要使用 fs
模組和它的 readFile
方法(非同步方法)來讀取檔案的內容。該方法將獲取 fileName
、一個型別和一個回撥函式。回撥函式獲取兩個引數:
1. 一個表明在從檔案系統中讀取資源時遇到的任何問題的錯誤引數。如果存在問題,系統會向客戶端返回一個 HTTP 500 錯誤訊息。
2. 如果沒有問題,則會顯示 HTTP 200 OK 訊息,並將檔案的內容回發給客戶端。
此函式能對靜態檔案進行相對較為簡單的處理。下一部分將討論您希望動態響應一個搜尋請求的更為困難的場景。
動態響應與伺服器
示例 Web 伺服器主要處理對靜態資源的請求和動態搜尋請求。我們的戰略是使用特定 URL 來處理搜尋請求,隨後將其他請求分載到serveStatic
函式。為搜尋請求使用 /doSearch
的相對 URL。清單 5 展示了 Web 伺服器程式碼。
清單 5. CoffeeScript Web 伺服器
1 2 3 4 5 6 7 8 9 |
url = require "url" server = http.createServer (request, response) -> uri = url.parse(request.url) if uri.pathname is "/doSearch" doSearch uri, response else serveStatic uri.pathname, response server.listen 8080 console.log "Server running at http://127.0.0.1:8080" |
這個指令碼同樣從載入一個 Node.js 模組開始。url
模組是解析 URL 時的一個有用的庫。下一步是使用 清單 1 中載入的 http
模組建立 Web 伺服器。使用該模組的 createServer
方法,該方法將獲取一個回撥函式,每次對 Web 伺服器發出一條請求時,都會呼叫這個回撥函式。該回撥函式接受兩個引數:一個 ServerRequest
例項和一個 ServerResponse
例項。兩種型別都是在 http
模組中定義的。在回撥函式中,使用 url
模組的 parse
方法,解析對伺服器發出的請求的 URL。這將為您提供一個 URL 物件,您可以使用它的pathname
屬性獲取相對路徑。如果 pathname
是 /doSearch
,則應呼叫 doSearch
函式(詳見下文討論)。否則就應該呼叫 清單 5 中的serveStatic
函式。清單 6 展示了 doSearch
的工作方式。
清單 6. 處理搜尋請求
1 2 3 4 5 6 7 8 9 10 |
doSearch = (uri, response) -> query = uri.query.split "&" params = {} query.forEach (nv) -> nvp = nv.split "=" params[nvp[0]] = nvp[1] keyword = params["q"] combinedSearch keyword, (results) -> response.writeHead 200, 'Content-Type': 'text/plain' response.end JSON.stringify results |
doSearch
函式將解析 URL 的查詢字串,可以在 uri
物件的查詢屬性中找到這個字串。根據 “&” 字元拆分字串。隨後根據等號字元拆分各子字串,獲得查詢字串中的名稱值對。將各名稱值對儲存在 params
物件中。獲取 "q"
引數,以便獲得您希望搜尋的關鍵字。將此傳遞給 清單 3 中的 combinedSearch
函式。您必須為其傳遞一個回撥函式。示例回撥函式直接寫出一條 HTTP 200 OK 訊息,並使用標準函式 JSON.stringify
將結果轉為字串。
這就是伺服器所需的一切。在下一節中,我們將介紹如何將這樣的伺服器程式碼與本系列 第 3 部分 中的客戶端程式碼掛接起來。
呼叫搜尋伺服器
在 第 3 部分 中,您編寫了一個使用模擬資料提供搜尋結果的 MockSearch
類。現在,您將定義一個新類,呼叫搜尋伺服器來執行真正的搜尋。清單 7 顯示了新的搜尋類。
清單 7. 實際搜尋類
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class CombinedSearch search: (keyword, callback) -> xhr = new XMLHttpRequest xhr.open "GET", "/doSearch?q=#{encodeURI(keyword)}", true xhr.onreadystatechange = -> if xhr.readyState is 4 if xhr.status is 200 response = JSON.parse xhr.responseText results = google: response.google.map (result) -> new GoogleSearchResult result twitter: response.twitter.map (result) -> new TwitterSearchResult result callback results xhr.send null |
CombinedSearch
類擁有單獨一個方法,即 search 方法,它與 MockSearch
的 search 方法具有相同的特徵。也就是接受一個關鍵字和一個回撥函式。在函式內:
1. 使用 XMLHttpRequest
(所有 Web 開發人員的 “老朋友”),通過傳遞到函式中的 /doSearch
路徑和關鍵字向伺服器發出 HTTP 請求。
2. 獲得響應之後,使用 JSON.parse
來解析它。
3. 建立一個包含 google
和 twitter
欄位的結果物件。使用 第 3 部分 中的 GoogleSearchResult
和 TwitterSearchResult
類來建立這些欄位。
4. 將結果傳遞迴 callback
函式。
現在,您需要的只是在 Web 頁面的 doSearch
方法中使用這些類,而不是在 MockSearch
中使用。清單 8 展示瞭如何使用CombinedSearch
類。
清單 8. 使用 CombinedSearch 類
1 2 3 4 5 6 7 8 9 10 |
@doSearch = -> $ = (id) -> document.getElementById(id) kw = $("searchQuery").value appender = (id, data) -> data.forEach (x) -> $(id).innerHTML += "<p>#{x.toHtml()}</p>" ms = new CombinedSearch ms.search kw, (results) -> appender("gr", results.google) appender("tr", results.twitter) |
將 清單 8 與 第 3 部分 中的 doSearch
對比,您不會發現很多的差異。惟一不同的就是第七行。這裡例項化的不再是 MockSearch
例項,而是一個 CombinedSearch
例項。其他所有部分都是完全相同的。您從網頁獲取關鍵字,呼叫搜尋,隨後通過呼叫各SearchResult
物件的 toHtml
方法來附加結果。圖 1 展示了包含來自伺服器的 “實時” 搜尋結果的 Web 應用程式。
圖 1. 執行示例 Web 應用程式
為了實現客戶端程式碼的更改,您需要使用 coffee -c search.coffee
進行重新編譯。如需執行應用程式,請使用 coffee search-server.coffee
。隨後即可開啟瀏覽器,轉到 http://127.0.0.1:8080,並嘗試執行各種查詢。
在這篇文章中,您完成了 Web 應用程式,構建了伺服器端元件來補充 第 3 部分 中的客戶端程式碼。現在,在本 系列 結束時,您獲得了一個完全在 CoffeeScript 中編寫的完整應用程式。您使用了 Node.js 中的許多特性,這使您能夠將 CoffeeScript 用作伺服器端技術。
人們對 Node.js 的普遍異議就是其非阻塞式風格會導致多層回撥函式。這可能導致您難以理清頭緒,而 JavaScript 繁冗的語法進一步加大了複雜度。CoffeeScript 並未改變使用所有這些回撥的需求,但其優雅的語法確實使您能夠更加輕鬆地編寫和理解此類程式碼