在當今這個協做和社交應用的世界裏,其關鍵是要有一個能簡單構建和易於部署的後臺。許多組織機構都依賴於一個應用棧(Application Stack),其使用下面三項技術:html
這個棧對於移動應用來講至關流行,由於原生數據格式是JSON,它容易被應用解析,例如經過使用 Cocoa 的 NSJSONSerialization
類或其它相似的解析器。node
在本教程中,你將學會如何搭建了一個 Node.js 環境,驅動 Express;在此平臺之上,你將構建一個經過 REST API 來提供一個 MongoDB 數據庫的服務器,就像這樣:ios
在一個 HTML 表格中呈現的後端數據庫git
本教程的第二部分重點放在 iOS 應用端。你將構建一個很酷的叫作「有趣的地方」的應用,標記有趣的位置,讓其它用戶可以找出他們附近有趣的地方。下面稍微窺探一下你將構建的應用:github
TourMyTown 的主視圖web
本教程假設你已經瞭解了 JavaScript 和 Web 開發的基礎,但對 Node.js、Express 以及 MongoDB 都不熟悉。正則表達式
大多數 Objective-C 開發者都不太熟悉 JavaScript ,但它對於 Web 開發者來講是極其常見的語言。由於這個緣由,Node 做爲一個 Web 框架收穫了大量人氣,但還有更多緣由使其成爲後端服務的絕好選擇:mongodb
內建的服務器功能數據庫
經過它的包管理器作到良好的項目管理express
一個快速的 JavaScript 引擎,也就是 V8
異步事件驅動編程模型
一個異步的關於事件和回調的編程模型很是適合服務器,它要等待許多事情,例如到來的請求以及經過其它服務(例如 MongoDB)的內部進程通訊。
MongoDB 是一個低開銷的數據庫,其全部實體都是自由形式 BSON —— 「二進制 JSON」 —— 文檔。這能讓你同異構數據打交道,並且處理各類各樣的數據格式也變得很容易。由於 BSON 與 JSON 兼容,構建一個 REST API 就很簡單——服務器代碼可以傳遞請求到數據驅動器而不須要不少的中間處理。
Node 和 MongoDB 在本質上都具備可擴展性,可以輕鬆地在跨越分佈式模型中的多個機器,實現同步;這個組合對於不具備均勻分佈負載的應用來講是一個理想選擇。
本教程假設你使用 OS X Mountain Lion 或 Mavericks ,Xcode 及其 command line tools 都已經安裝好了。
第一步是安裝 Homebrew 。就像 CocoaPods 爲 Cocoa 管理各類包 和 Gem 爲 Ruby 管理各類包同樣,Homebrew 管理 OS X 上的 Unix 工具。它構建在 Ruby 和 Git 之上,並且它具備高度的靈活性和可定製性。
若是你已經安裝了 Homebrew ,那就能夠跳過下面的步驟。否則,打開終端執行下列命令來安裝 Homebrew :
ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
注意:
cURL
是使用 URL 請求來發送和接收文件與數據的稱手工具。此處你使用它加載 Homebrew 安裝腳本——在本教程後面,你還會使用它與 Node 服務器交互。
一旦安裝好 Homebrew ,就在終端輸入下面的命令:
brew update
這只是更新 Homebrew ,讓你擁有最新的軟件包列表。
如今,經過 Homebrew 安裝 MongoDB ,使用下面的命令:
brew install mongodb
記下 MongoDB 被安裝的位置,它就在輸出的「Summary」中。稍後你將用它加載 MongoDB 服務。
從 http://nodejs.org/download/ 下載並運行 Node.js 安裝器。
一旦安裝完成,你就立刻測試 Node.js 是否安裝成功。
在終端裏輸入:
node
這能讓你進入 Node.js 的交互式運行環境,在此你能夠執行 JavaScript 表達式。
在提示符後輸入下面的表達式:
console.log("Hello World");
你將獲得以下輸出:
Hello World undefined
console.log
在 Node.js 中至關於 NSLog
。固然,console
的輸出流比 NSLog
的要複雜得多:它有 console.info
、console.assert
、console.error
以及你指望的從更先進的記錄器例如CocoaLumberjack
而來的其它流。
寫在輸出裏的 「undefined」 值是 console.log
的返回值,而 console.log
沒有返回值。 由於 Node.js 老是顯示出全部表達式的輸出,不管其返回值是否有定義。
注意:若是你之前使用過 JavaScript ,你須要知道 Node.js 環境和瀏覽器環境之間有些許不一樣。
全局
對象被叫作global
而不是window
。在 Node.js 交互提示符後鍵入global
並按下回車就會顯示 global 命名空間裏全部的方法和對象;固然,直接使用 Node.js 文檔 來作參考更容易些。 :]global
對象有全部預約義的常數、函數以及數據類型,均可用於全部運行在 Node.js 環境裏的程序。任何用戶創造的變量一樣也都添加到全局上下文對象。基本上global
的輸出將列出全部內存中能夠訪問的事物。
Node.js 的交互式環境對於玩耍和調試 JavaScript 表達式是很棒的,但一般你都會使用腳本文件來作實際的事情。就像 iOS 應用包含有 Main.m
做爲其入口點,Node.js 的默認入口點就是 index.js
。然而,不一樣於 Objective-C ,這裏沒有 main 函數;相反, index.js
將從頭至尾的執行。
按下 Control+C 兩次以退出 Node.js Shell。執行下面的命令,新建一個目錄以保存你的腳本:
mkdir ~/Documents/NodeTutorial
而後執行下面的命令進入新建的目錄並使用你默認的文本編輯器新建一個腳本文件:
cd ~/Documents/NodeTutorial/; edit index.js
在 index.js
中添加以下代碼:
console.log("Hello World.");
保存你的工做,回到終端執行下面的命令看看你的腳本如何運行:
node index.js
再一次,咱們看到了熟悉的 「Hello World」 輸出。你也能夠執行 node .
來運行你的腳本,.
就會默認查找 index.js
。
當然,一個 「Hello World」 腳本成不了一個服務器,但這是測試你的安裝是否成功的快速方式。下一節將向你介紹 Node.js 包的世界,這會成爲你那閃亮的新 Web 服務器的基礎!
Node.js 應用程序都被分紅不一樣的包,這就是 Node.js 世界的「框架」。 Node.js 自帶有幾個基礎且強大的包,但還有超過 50000 個由活躍的開發社區提供的公開包——若是你不能找到你須要的包,你本身也能夠比較容易地創造。
注意:查看 https://npmjs.org/ 可獲得全部可用包的列表
用下列代碼替換 index.js
的內容:
//1 var http = require('http'); //2 http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/html'}); res.end('<html><body><h1>Hello World</h1></body></html>'); }).listen(3000); console.log('Server running on port 3000.');
依次按照編號好的註釋看看:
require
引入(import)模塊(module)到當前文件。本次你引入了 HTTP 庫。
你建立一個 Web 服務,它對簡單的 HTTP 請求的迴應是發送一個 200 應答,並將頁面內容放在應答裏。
Node.js 做爲一個運行時環境的最大的優點之一就是他的 事件驅動模型(event-driven model)
。它圍繞着異步調用的回調函數的概念來設計。在上面的例子裏,你正監聽 3000 端口等着傳入的 HTTP 請求。當你收到一個請求,你的腳本調用 function (req, res) {…}
並返回一個應答給調用者。
保存你的文件,回到終端並執行以下命令:
node index.js
你將在控制檯看到以下輸出:
打開你最喜歡的瀏覽器導航至 http://localhost:3000 ;好好瞧着, Node.js 正在提供給你的是一個 「Hello World」 頁面。
你的腳本還在哪裏,耐心地等待從 3000 端口傳入的 HTTP 請求。要幹掉(kill)你的 Node 實例,只需在終端按下 Ctrl+C
。
注意:Node 包一般由頂層函數或引入的對象寫就。經過使用 require ,這個函數在以後就被分配給一個頂層變量。這樣有助於以一個健全的方式管理範圍(scope)以及暴露(expose)模塊的 API 。稍後在本教程中你會看到如何建立一個自定義模塊,你將爲 MongoDB 添加一個驅動器。
前一小節覆蓋了 Node.js 內建的模塊,那第三方的模塊該怎麼處理呢?例如你以後須要的 Express 模塊,它爲你的服務器平臺提供路由中間件。
外部模塊一樣可使用 require 函數引入到文件裏,但你須要分開下載它們而後才能用於你的 Node 實例。
Node.js 使用 npm
—— Node 包模塊——來下載、安裝以及管理包依賴。若是你熟悉 CocoaPods 或者 Ruby gems ,那麼你對 npm
也會以爲熟悉。你的 Node.js 應用程序使用package.json
,它專門定義配置和 npm
依賴。
Express 是一個流行的 Node.js 模塊,提供路由中間件。爲何你會須要這個獨立的包呢?考慮下面的情形。
若是你只使用 http
模塊自身,你不得不分開解析每一個請求的位置以找出提供什麼內容給請求者——如此這般,事情很快就會變得難以處理。
然而,用 Express 你就能容易地爲每一個請求定義路由和回調。 Express 一樣讓爲基於 HTTP 動詞(例如 POST, PUT, GET, DELETE, HEAD, 等)以提供不一樣的回調變得很容易。
一個 HTTP 請求包含一個方式——或者動詞——的值。默認值是 GET
,它是爲了獲取數據,例如瀏覽器中 Web 頁面。 POST
意味着上傳數據,例如提交 Web 表單。對於 Web API 來講,POST
一般用於添加數據,但它一樣可用於遠程處理調用類型端點(but it can also be used for remote procedure call-type endpoints.)。
PUT
與 POST
的不一樣在於它一般用於替換已有數據。在實踐中, POST
和 PUT
一般以一樣的方式使用:在請求 Body 裏提供實體以放進後端的數據存儲裏。 DELETE
用於從你的後端數據存儲裏移除條目。
POST
、GET
、PUT
以及 DELETE
就是 HTTP 實現的 CRUD 模型 —— Create、Read、Update 以及 Delete。
還有其它一些少有人知的 HTTP 動詞。 HEAD
表現得像一個 GET
但只返回應答頭而沒有 Body 。這有助於最小化數據傳輸,若是應答頭中的信息足夠肯定是否有可用的新數據。其它動詞如 TRACE
和 CONNECT
用於網絡路由。
在終端裏執行下列命令:
edit package.json
這會建立一個新的 package.json
,它將包含你的包配置和依賴。
添加以下代碼到 package.json
中:
{ "name": "mongo-server", "version": "0.0.1", "private": true, "dependencies": { "express": "3.3.4" } }
這個文件定義了一些元數據,例如項目的名字和版本,一些腳本,以及對於你的目的來講最重要的包依賴。下面說明每行的意思:
name
是項目的名字。
version
是項目目前的版本。
private
防止項目被意外地公開,若是你設置其爲 true 。
dependencies
是一個包含你應用使用的模塊的列表。
依賴以 鍵/值 形式接受模塊名和版本。你的依賴列表包含有 3.3.4 版本的 Express; 若是你想指明 Node.js 去使用最新版本的包,你可使用通配符"*"。
保存文件,在終端裏執行下列命令:
npm install
你會看到以下輸出:
install
下載並安裝 package.json
指定的依賴——以及你的依賴自己的依賴!:] ——存進一個叫作 node_modules
的目錄,並讓你的應用程序使用它們。
一旦 npm
完成,你就能夠在你的應用程序中使用 Express 了。
在 index.js
中找到下列行:
var http = require('http');
並添加 Express 的 require
調用,以下所示:
var http = require('http'), express = require('express');
這就引入了 Express 包,並將其存在變量 express
中。
添加以下代碼到 index.js
,就在剛在添加的區域的下面:
var app = express(); app.set('port', process.env.PORT || 3000);
這就建立了一個 Express 應用並設置其默認端口爲 3000 。你能夠經過建立一個環境變量PORT
來覆蓋此默認值。這種類型的自定義在開發工具中很是方便,特別是若是你有多個應用程序監聽這好幾個端口。
添加以下代碼到 index.js
,就在剛剛添加的區域的下面:
app.get('/', function (req, res) { res.send('<html><body><h1>Hello World</h1></body></html>'); });
這就建立了一個路由處理器(route handler)
,它是給定 URL 的請求處理者鏈的花哨名字。Express 匹配請求中指定的路徑並執行適當的回調。
你上面的回調告訴 Express 去匹配根路徑 "/" 並返回一個給定的 HTML 。 send
爲你格式化各類響應頭——例如 content-type
和 status code
—— 如此你就能專一於編寫偉大代碼了。
最後,替換 http.createServer(...)
爲下面的實現:
http.createServer(app).listen(app.get('port'), function(){ console.log('Express server listening on port ' + app.get('port')); });
這比以前的稍微緊湊些。 app
分開實現 function(req,res)
回調,而不是在 createServer
這裏內聯地包含它們。你一樣添加了一個完成處理器回調,一旦端口準備好接收請求它就會被調用。如今你的應用在打印 「listening」 消息到控制檯以前就等着端口準備好。
爲了審查,index.js
整個看起來以下所示:
var http = require('http'), express = require('express'); var app = express(); app.set('port', process.env.PORT || 3000); app.get('/', function (req, res) { res.send('<html><body><h1>Hello World</h1></body></html>'); }); http.createServer(app).listen(app.get('port'), function(){ console.log('Express server listening on port ' + app.get('port')); });
保存你的文件,並在終端執行下列命令:
node index.js
回到瀏覽器,從新載入 http://localhost:3000 去看看你的 Hello World 頁面是否依然加載。
你的頁面看起來與以前沒有區別,但有不止一種方法能夠查看引擎蓋下發生了什麼事。
建立終端的另外一個實例,並執行以下命令:
curl -v http://localhost:3000
你會看到以下輸出:
curl
吐出你的 HTTP 請求的頭和內容,給你顯示服務傳來的東西的原始細節。注意 X-Powered-By : Express
頭;Express 會自動添加這個元數據到應答裏。
用 Express 提供靜態文件很是容易。
添加以下語句到 index.js
頂部的 require
區域:
path = require('path');
再添加下面一行到 app.set
語句以後:
app.use(express.static(path.join(__dirname, 'public')));
這就告訴 Express 去使用 express.static
中間件 ,它爲到來的請求提供靜態文件做爲應答。
path.join(__dirname, 'public')
映射本地子目錄 public
到基路由 /
; 它使用 Node.js path
模塊建立一個平臺無關的子目錄字符串。
index.js
如今看起來以下:
//1 var http = require('http'), express = require('express'), path = require('path'); //2 var app = express(); app.set('port', process.env.PORT || 3000); app.use(express.static(path.join(__dirname, 'public'))); app.get('/', function (req, res) { res.send('<html><body><h1>Hello World</h1></body></html>'); }); http.createServer(app).listen(app.get('port'), function(){ console.log('Express server listening on port ' + app.get('port')); });
使用了靜態處理器,任何在 /public
中的東西均可以由其名字訪問到。
爲了證實這一點,按下 Control+C 幹掉你的 Node 實例,而後執行下面的命令:
mkdir public; edit public/hello.html
添加以下代碼到 hello.html
:
<html></body>Hello World</body></html>
這就建立了一個新的 public
目錄並建立了一個基礎的靜態 HTML 文件。
再次用命令 node index.js
重啓你的 Node 實例。 瀏覽器打開http://localhost:3000/hello.html 你就會看到這個新建立的頁面,以下所示:
靜態頁面是不錯,但 Express 的真正威力是動態路由。 Express 在路由字符串上使用一個正則表達是匹配器,容許你爲路由定義參數。
舉個例子,路由字符串能夠包含下列元素:
靜態元素—— /files
只會匹配 http://localhost:3000/pages
(譯者注:彷佛有點問題,應該只會匹配 http://localhost:3000/files
)
以「:」開頭的參數—— /files/:filename
匹配 /files/foo
和 /files/bar
,但不能匹配/files
以「?」結尾的可選參數——/files/:filename?
匹配 /files/foo
也能匹配 /files
正則表達式—— /^\/people\/(\w+)/
匹配 /people/jill
和 /people/john
要試試看,就添加下列路由到 index.js
中 app.get
語句後:
app.get('/:a?/:b?/:c?', function (req,res) { res.send(req.params.a + ' ' + req.params.b + ' ' + req.params.c); });
這就建立了一個新的路由,它接收三個路徑層級並在應答 Body 中顯示這些路徑組件。任何由:
開始的東西都映射到所提供名字的請求參數上。
重啓你的 Node 實例,再將瀏覽器轉到 http://localhost:3000/hi/every/body 。你將看到以下頁面:
「hi」 是 req.params.a
的值,「every」 是req.params.b
的值,最後 「body」 分配給 req.params.c
。
路由匹配對於構建 REST API 來講頗有用,你能夠用動態路徑指定後端數據存儲的特定元素。
除了 app.get
, Express 還支持 app.post
、app.put
、app.delete
等等。
服務器錯誤可用一到兩種方式處理。你能夠傳遞一個異常給調用棧——這樣作可能幹掉應用——或者你能夠捕捉錯誤並返回一個合適的錯誤碼。
HTTP 1.1 協議在 4xx 和 5xx 範圍內定義了好幾個錯誤碼。 400 段的錯誤用於用戶錯誤,例如請求一個不存在的條目:一個熟悉的錯誤碼是 404 Not Found 錯誤。500 段的錯誤表示服務器錯誤,例如超時或者編程錯誤(好比 null 解除引用(null dereference))。
你將添加一個捕捉全部(catch-all)的路由並在請求內容不能被找到時返回一個 404 頁面。由於路由處理器按照它們設置 app.use
或 app.verb 的順序添加,一個捕捉全部(catch-all)的路由能夠添加在路由鏈的最後。
添加以下代碼到 index.js
,就在最後的 app.get
與調用 http.createServer
之間:
app.use(function (req,res) { //1 res.render('404', {url:req.url}); //2 });
這些代碼會致使 404 頁面的加載,若是此處沒有前一個調用使用 res.send()
.
這裏有一些值得記錄的點:
app.use(callback)
匹配全部請求。當它被放在全部 app.use
和 app.verb 的列表的最後,callback 就會成爲捕捉全部(catch-all)。
res.render(view, params)
調用使用模版引擎( templating engine)
渲染的輸出填充響應 Body 。 一個模版引擎使用磁盤上一個叫作「View」的模版文件並用一組鍵值參數替換其中的變量以生成一個新的文檔。
等等——一個「模版引擎」?這貨搞什麼飛機?
Express 能使用好幾種模版引擎來提供視圖。要讓這個例子工做起來,你將添加一個流行的Jade 模版引擎到你的應用程序中。
Jade 是一種簡單的語言,它避開括號並使用空白符來代替,以肯定 HTML 標籤的順序和內容。它一樣可使用變量、條件判斷、迭代以及分支以便動態地建立 HTML 文檔。
更新 package.json
中的依賴爲:
{ "dependencies": { "express": "3.3.4", "jade": "1.1.5" }
回到終端,幹掉你的 Node 實例,並執行以下命令:
npm update
這將下載並安裝 jade
包。
添加以下代碼到 index.js
,就在第一個 app.set
以後:
app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade');
上面的第一行指定了視圖模版的位置,第二行就設置 jade 做爲視圖渲染引擎。
在終端執行以下命令:
mkdir views; edit views/404.jade
添加下列代碼到 404.jade
中:
doctype html body h1= 'Could not load page: ' + url
Jade 模版中的前兩行建立了一個有 body
元素的新 HTML 文檔。第三行經過縮進創在 body
內建了一個 h1
元素。間距在 Jade 中很重要! :]
h1
元素的文本由 「Could not load page」 和做爲 index.js
中 res.render()
的一部分傳遞的url
參數串聯而成。
做爲一個快速檢查,你的 index.js
如今看起來以下:
var http = require('http'), express = require('express'), path = require('path'); var app = express(); app.set('port', process.env.PORT || 3000); app.set('views', path.join(__dirname, 'views')); //A app.set('view engine', 'jade'); //B app.use(express.static(path.join(__dirname, 'public'))); app.get('/', function (req, res) { res.send('<html><body><h1>Hello World</h1></body></html>'); }); app.use(function (req,res) { res.render('404', {url:req.url}); }); http.createServer(app).listen(app.get('port'), function(){ console.log('Express server listening on port ' + app.get('port')); });
重啓你的 Node 實例,使用瀏覽器加載 URL http://localhost:3000/show/a/404/page 。你將看到以下頁面:
如今你的 index.js
中有足夠的啓動代碼去接收傳入的請求並提供一些基本的響應。而缺失的部分就是數據庫持久化,它能將這些東西變成一個有用的Web應用程序,可以被一個移動應用所利用。
MongoDB 是一個存儲 JSON 對象的數據庫。不像 SQL 數據庫,相似 Mongo 的 NoSQL 數據庫
不支持實體關係。進一步說明,沒有預約義的模式,因此同一集合裏的實體不須要有一樣的字段或符合預約義的模式。
MongoDB 一樣提供了強大的的查詢語言 map-reduce
以及對定位數據的支持。MongoDB 因其擴展、複製和碎片(scale, replicate and shard)能力而廣受歡迎。擴展和高可用特性不在本教程覆蓋範圍。
MongoDB 最大的缺點是缺乏關係支持,並且在內存映射實際的數據庫文件時可能會佔用過多內存。這些問題能夠經過仔細構造的數據獲得緩解;這將在本教程的第二部分進行說明。
由於 MongoDB 文檔 和 JSON 的親密關係,MongoDB 對於 Web 和移動應用都是很棒的選擇。 MongoDB 不存儲原始 JSON;而是叫作 BSON(即 Binary JSON) 格式的文檔,這對於數據存儲和查詢來講更有效率。BSON 同時還支持比 JSON 更多的數據類型,例如日期和C類型(C-type)。
MongoDB 是一個原生應用程序,經過驅動器(drivers)
訪問。有好多種驅動器可用於幾乎任何環境,固然包括 Node.js 。MongoDB 驅動器鏈接 MongoDB 服務器併發出命令去更新或讀取數據。
這就意味着你須要運行一個 MongoDB 實例以在一個打開的端口上監聽。幸運的是,這就是你的下一個步驟!:]
新開一個終端窗口並執行以下命令:
cd /usr/local/opt/mongodb/; mongod
譯者注:這裏可能會發生錯誤 ERROR: dbpath (/data/db) does not exist.
,試試先建立一個自定義路徑,再用 mongd --dbpath '~/somepath'
來啓動服務器。
這就能啓動一個 MongoDB 守護服務器。
如今 MongoDB 已經啓動,運行在默認端口 27017 上。
雖然 MongoDB 驅動器提供了數據庫鏈接,但它依然須要被鏈接到服務器以便轉換傳入的 HTTP 請求爲適當的數據庫命令。
還記得你以前實現的 /:a/:b/:c
路由嗎?若是你可使用這個模式去查找數據庫實體如何?
既然 MongoDB 文檔被組織爲集合,那麼路由就能夠很簡單如: /:collection/:entity
,這能讓你以超級 fashion 的 RESTful 的方式使用一個簡單的地址系統去訪問對象。
幹掉你的 Node 實例並在終端執行下列命令:
edit collectionDriver.js
添加以下代碼到 collectionDriver.js
:
var ObjectID = require('mongodb').ObjectID;
這一行引入了各個須要的包;在本例中,是來自 MongoDB 包的 ObjectID
。
注意:若是你比較熟悉傳統數據庫,你可能明白朮語「主鍵」。MongoDB有相似的概念:默認來講,新實體都會被分配一個惟一的
_id
字段,其類型爲ObjectID
,這是 MongoDB 用來優化查找和插入的。由於 ObjectID 是一個 BSON 類型而不是 JSON 類型,你必須轉換任何傳入的字符串爲 ObjectID ,若是它們用於和一個_id
字段進行比較。
添加以下代碼到 collectionDriver.js
剛纔那行後面:
CollectionDriver = function(db) { this.db = db; };
這個函數定義了 CollectionDriver
構造器方法;它存儲一個 MongoDB 客戶端實例以便以後使用。在 JavaScript 中, this
是當前上下文的引用,就像 Objective-C 中的 self
。
繼續添加以下代碼當剛剛添加的代碼塊下面:
CollectionDriver.prototype.getCollection = function(collectionName, callback) { this.db.collection(collectionName, function(error, the_collection) { if( error ) callback(error); else callback(null, the_collection); }); };
這一段定義了一個幫助方法 getCollection
以便經過名字去獲取一個 Mongo 集合。你經過添加函數到 prototype
定義了類方法。
db.collection(name,callback)
獲取集合對象並返回集合——或一個錯誤——給回調。
繼續添加以下代碼到剛纔添加的代碼塊下面:
CollectionDriver.prototype.findAll = function(collectionName, callback) { this.getCollection(collectionName, function(error, the_collection) { //A if( error ) callback(error); else { the_collection.find().toArray(function(error, results) { //B if( error ) callback(error); else callback(null, results); }); } }); };
A 行的 CollectionDriver.prototype.findAll
獲取集合,若是沒有如不能訪問 MongoDB 服務器這樣的錯誤,它就調用 B 行的 find()
。這將返回全部找到的對象。
find()
返回一個數據遊標(data cursor)
,它可用於遍歷匹配對象。find()
一樣能接受一個選擇器對象來過濾結果。 toArray()
組織全部的結果爲一個數組並將其傳遞給回調。最後回調返回給調用者一個找到的對象的數組或者一個錯誤。
繼續添加以下代碼到剛纔添加的代碼塊下面:
CollectionDriver.prototype.get = function(collectionName, id, callback) { //A this.getCollection(collectionName, function(error, the_collection) { if (error) callback(error); else { var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$"); //B if (!checkForHexRegExp.test(id)) callback({error: "invalid id"}); else the_collection.findOne({'_id':ObjectID(id)}, function(error,doc) { //C if (error) callback(error); else callback(null, doc); }); } }); };
在 A 行, CollectionDriver.prototype.get
使用 _id
從一個集合中獲取單個條目。相似於prototype.findAll
方法,這個調用首先獲取一個集合對象而後在返回的對象上執行一個findOne
。由於這匹配 _id
字段,本例中的一個 find()
或 findOne()
將會使用正確的數據類型來匹配它。
MongoDB 存儲 _id
字段爲 BSON 類型 ObjectID
。在上面的 C 行,ObjectID()
接受一個字符串並將其轉換爲一個 BSON ObjectID 去匹配集合。然而,ObjectID()
很小氣,須要適當的十六進制字符串不然它會返回一個錯誤:所以,B 行會先用正則做檢查。
這不能保證有一個與 _id
匹配的對象,但它保證 ObjectID
可以傳遞字符串。選擇器{'_id':ObjectID(id)}
使用提供的 id
匹配 _id
字段。
注意:從一個不存在的集合或實體中讀取不是一個錯誤—— MongoDB 驅動器只會返回一個空的容器。
繼續添加以下代碼到剛纔添加的代碼塊下面:
exports.CollectionDriver = CollectionDriver;
這一行定義或暴露實體用於其餘應用程序,它們以一個需求模塊列在 collectionDriver.js
中。
保存你的修改——你完成了這個文件!如今你須要一個方式去調用這個文件。
要調用你的 collectionDriver
,首先添加下面一行到 package.json
中的 dependencies
內:
"mongodb":"1.3.23"
在終端執行下列命令:
npm update
這將下載並安裝 MongoDB 包。
在終端執行下列命令:
edit views/data.jade
如今添加下列代碼到 data.jade
中,注意縮進層級:
body h1= collection #objects table(border=1) if objects.length > 0 - each val, key in objects[0] th= key - each obj in objects tr.obj - each val, key in obj td.key= val
這個模版渲染一個集合到一個 HTML 表格中,使其對人類可讀。
添加下列代碼到 index.js
,就在 path = require('path')
那行下面:
MongoClient = require('mongodb').MongoClient, Server = require('mongodb').Server, CollectionDriver = require('./collectionDriver').CollectionDriver;
這裏你包含了來自 MongoDB 模塊的 MongoClient
和 Server
對象以及你新建立的CollectionDriver
。
添加下列代碼到 index.js
,就在最後一行 app.set
的下面:
var mongoHost = 'localHost'; //A var mongoPort = 27017; var collectionDriver; var mongoClient = new MongoClient(new Server(mongoHost, mongoPort)); //B mongoClient.open(function(err, mongoClient) { //C if (!mongoClient) { console.error("Error! Exiting... Must start MongoDB first"); process.exit(1); //D } var db = mongoClient.db("MyDatabase"); //E collectionDriver = new CollectionDriver(db); //F });
上面 A 行假設 MongoDB 實例是本地運行在端口 27017 。若是你已經在其餘地方運行過 MongoDB 服務器,那你就須要修改這些值,但在本教程中就留下它們吧。
B 行建立了一個新的 MongoClient 並調用 C 行的 open
試圖創建一個鏈接。若是你的鏈接嘗試失敗了,那極可能是你尚未啓動你的 MongoDB 服務器。在無鏈接的狀況下,應用將在 D 行退出。
若是客戶端鏈接成功,它就在 E 行打開 MyDatabase
數據庫。一個 MongoDB 實例能夠包含多個數據庫,每個都有惟一的命名空間和惟一的數據。最後,你在 F 行建立CollectionDriver
對象並傳遞一個處理器給 MongoDB 客戶端。
用下列語句替換 index.js
中的頭兩行 app.get
調用:
app.get('/:collection', function(req, res) { //A var params = req.params; //B collectionDriver.findAll(req.params.collection, function(error, objs) { //C if (error) { res.send(400, error); } //D else { if (req.accepts('html')) { //E res.render('data',{objects: objs, collection: req.params.collection}); //F } else { res.set('Content-Type','application/json'); //G res.send(200, objs); //H } } }); }); app.get('/:collection/:entity', function(req, res) { //I var params = req.params; var entity = params.entity; var collection = params.collection; if (entity) { collectionDriver.get(collection, entity, function(error, objs) { //J if (error) { res.send(400, error); } else { res.send(200, objs); } //K }); } else { res.send(400, {error: 'bad url', url: req.url}); } });
這就建立了兩個新路由 /:collection
和 /:collection/:entity
。它們分別調用collectionDriver.findAll
和 collectionDriver.get
方法並返回 JSON 對象、HTML 文檔或一個錯誤。
當你在 Express 中 定義 /collection
,它將明確匹配 「collection」 。然而,若是你定義如 A 行的路由 /:collection
那麼它將匹配任何存儲在 B 行的 req.params
集合中的第一層路徑。在本例中,你使用 C 行的 CollectionDriver
的 findAll
定義的端點去匹配任何到 MongoDB 的 URL 。
若是查詢成功,那麼代碼會在 E 行的頭中檢查,是否請求會接受一個 HTML 結果。若是是,那 F 行就從 data.jade
模版存儲渲染過的 HTML 到應答
中。這將簡單地呈現集合內容到一個 HTML 表格中。
默認狀況下,Web 瀏覽器會在它們的請求中指定它們接受 HTML 。當其餘類型的客戶端請求這個端點,例如 iOS 應用使用 NSURLSession
,這個方法就會在 G 行返回一個機器可讀的 JSON 文檔。 與 H 行, res.send()
會返回由集合驅動器生成的 JSON 文檔和一個成功碼。
這個例子中,對於兩層 URL 指定的位置, I 行將其做爲集合名和實體 _id
對待。以後你在 J 行使用 collectionDriver
的 get()
請求特定的實體。若是那個實體被找到,你就在 K 行將其做爲 JSON 文檔返回。
保存你的工做,重啓你的 Node 實例, 檢查你的 mongod 守護進程是否依然在運行,而後將瀏覽器指向 http://localhost:3000/items ;你將看到以下頁面:
怎麼什麼都沒有?發生了什麼事?
哦,等等——那是由於你尚未添加任何數據呢。是時候了!
從一個空空如也的數據庫裏讀取對象一點兒也不有趣。要測試功能,就要有一個添加實體到數據庫的途徑。
在 CollectionDriver.js
中添加下列新的原型方法,就在 exports.CollectionDriver
行以前:
//save new object CollectionDriver.prototype.save = function(collectionName, obj, callback) { this.getCollection(collectionName, function(error, the_collection) { //A if( error ) callback(error) else { obj.created_at = new Date(); //B the_collection.insert(obj, function() { //C callback(null, obj); }); } }); };
就像 findAll
和 get
,A 行的 save
首先檢索集合對象。以後回調取得提供的實體並再添加一個字段記錄建立的日期(如 B 行所示)。最後,你在 C 行插入修改後的對象到集合裏。insert
同時會自動添加一個 _id
。
添加下列代碼到 index.js
,就在剛纔添加的 get
方法以後:
app.post('/:collection', function(req, res) { //A var object = req.body; var collection = req.params.collection; collectionDriver.save(collection, object, function(err,docs) { if (err) { res.send(400, err); } else { res.send(201, docs); } //B }); });
這就在 A 行爲 POST
動詞建立了一個新的路由,它經過調用你剛剛添加到你的驅動器裏的save()
將 Body 看成一個對象插入到指定的集合裏。當資源被建立後,B 行就返回 HTTP 201 成功碼。
只有最後一塊了。添加下列代碼到 index.js
,就在 app.set
行後面,但在 app.use
或app.get
行以前:
app.use(express.bodyParser());
這會告訴 Express 去解析傳入的 Body 數據;若是它是 JSON,那麼用它建立一個 JSON 對象。經過將這個調用提早,Body 解析將在其餘路由處理器以前調用。這樣 req.body
就能直接做爲 JavaScript 對象傳遞給驅動器。
再次重啓你的 Node 實例,在終端裏執行下列命令,插入一個測試對象到你的數據庫:
curl -H "Content-Type: application/json" -X POST -d '{"title":"Hello World"}' http://localhost:3000/items
你會在控制檯看到記錄的返回信息,以下所示:
如今轉到你的瀏覽器,並從新加載 http://localhost:3000/items ;你就會在表格中看到你插入的項目。
你已經實現了 CRUD 中的 Create 和 Read 操做——還剩下 Update 和 Delete 。這些都比較簡單,遵循與其餘兩個同樣的模式。
添加下列代碼到 CollectionDriver.js
,就在 exports.CollectionDriver
行以前:
//update a specific object CollectionDriver.prototype.update = function(collectionName, obj, entityId, callback) { this.getCollection(collectionName, function(error, the_collection) { if (error) callback(error); else { obj._id = ObjectID(entityId); //A convert to a real obj id obj.updated_at = new Date(); //B the_collection.save(obj, function(error,doc) { //C if (error) callback(error); else callback(null, obj); }); } }); };
update()
函數接受一個對象,並在 C 行使用 collectionDriver
的 save()
方法在集合中更新它。這假設 Body 的 _id
與 A 行指定的路由同樣。B 行添加一個 updated_at
字段做爲對象更新時間。添加一個修改時間戳是一個好主意,有助於理解數據在你的應用程序的生命週期裏是如何改變的。
注意這個更新用新對象操做取代了以前的對象——這裏並無屬性級別的更新支持。
添加下列代碼到 CollectionDriver.js
,就在 exports.CollectionDriver
行以前:
//delete a specific object CollectionDriver.prototype.delete = function(collectionName, entityId, callback) { this.getCollection(collectionName, function(error, the_collection) { //A if (error) callback(error); else { the_collection.remove({'_id':ObjectID(entityId)}, function(error,doc) { //B if (error) callback(error); else callback(null, doc); }); } }); };
delete()
與其餘 CRUD 同樣的操做。 在 A 行,它獲取集合對象,而後在 B 行用提供的 id
調用 remove()
。
如今你須要兩個新的路由來處理這些操做。幸運的是,PUT
和 DELETE
動詞已經存在,因此你能夠用與 GET
同樣的語義建立處理器。
添加以下代碼到 index.js
,就在 app.post()
調用以後:
app.put('/:collection/:entity', function(req, res) { //A var params = req.params; var entity = params.entity; var collection = params.collection; if (entity) { collectionDriver.update(collection, req.body, entity, function(error, objs) { //B if (error) { res.send(400, error); } else { res.send(200, objs); } //C }); } else { var error = { "message" : "Cannot PUT a whole collection" }; res.send(400, error); } });
這個 put
回調遵循同單實體 get
同樣的模式:你在集合上匹配名字和 _id
,如 A 行所示。和post
路由同樣, 在 B 行 put
傳遞來自 Body 的 JSON 對象到 collectionDriver
裏新寫的update()
方法中。
更新的對象將在應答中返回(C 行),因此客戶端能夠解析到任何服務器更新的字段,例如updated_at
。
添加以下代碼到 index.js
,就在剛添加的 put
方法以後:
app.delete('/:collection/:entity', function(req, res) { //A var params = req.params; var entity = params.entity; var collection = params.collection; if (entity) { collectionDriver.delete(collection, entity, function(error, objs) { //B if (error) { res.send(400, error); } else { res.send(200, objs); } //C 200 b/c includes the original doc }); } else { var error = { "message" : "Cannot DELETE a whole collection" }; res.send(400, error); } });
delete
端點很是相似於 put
,如 A 行所示,除了 delete
不須要一個 Body。在 B 行,你傳遞參數給 collectionDriver
裏的 delete()
方法,若是刪除操做成功,那麼你就在 C 行返回一個原始對象和一個 200 應答碼。
若是上述操做中發生任何錯誤,你就返回一個適當的錯誤碼。
保存你的工做,並重啓你的 Node 實例。
在終端執行下列命令,替換 {_id}
爲上一個 POST
調用的返回值:
curl -H "Content-Type: application/json" -X PUT -d '{"title":"Good Golly Miss Molly"}' http://localhost:3000/items/{_id}
你會在終端看到以下應答:
轉到瀏覽器,從新載入 http://localhost:3000/items ;你會在表格中看到你修改的條目:
在終端裏執行下列命令以刪除你的記錄:
curl -H "Content-Type: application/json" -X DELETE http://localhost:3000/items/{_id}
你會看到 curl 收到的響應:
從新載入 http://localhost:3000/items ,我能肯定,你的實體不見了。
就這樣,你使用 Node.js、Express 以及 MongoDB 完成了你的整個 CRUD 模型!
這裏是完成的示例項目,它包含有上面教程裏全部的代碼。
你的服務器如今準備好應對客戶端的鏈接並開始傳輸數據。在本教程的下一部分裏,你將構建一個 iOS 應用來鏈接你的新服務器,並利用一些 MongoDB 和 Express 的炫酷特性。
關於 MongoDB 的更多信息,看看 官方的 MongoDB 文檔 。
若是你有任何問題或評論,可自由地加入下方的討論!