MongoHQ服务是类似于Amazon S3的云服务,只不过它专注于在云中托管MongoDB实例。可以访问http://mongohq.com来注册服务。小于16MB的数据库是免费的,可以用它来运行本章的示例。在线用户界面易于使用,让你可以快速地浏览数据。
设置好MongoHQ账户之后,应该使用在线的用户界面创建名为Lifestream的数据库。你会得到数据库服务器的名称和端口号,对于每个MongoHQ数据库,这些信息是不同的。还必须输入访问数据库的用户名和密码。在线界面将提供数据库登录的详细信息。
在下面的示例中,会把完整的Lifestream应用程序所需的所有文件放在一起。首先,需要得到配置信息,测试到MongoHQ服务的连接,并验证最基本的功能可以实际工作。在第9章,构建了图片上传功能。现在,可以将它放到一边,集中精力完成用户注册功能。介绍将用户添加到系统的基本概念是引出其余功能必不可少的。在本示例中专注于核心功能,下面将完成一个非常简单的用户注册功能。这里不使用密码,而是为每个物理设备分配一个唯一的令牌。最终,将使用OAuth令牌进行用户注册,但现在要实现一个唯一的设备令牌。下面是具体操作步骤。
(1) 进入lifestream/server文件夹(继续使用和第8章、第9章一样的文件夹结构),运行下面的npm模块安装命令。
npm install connect
npm install mongodb
npm install knox
npm install uuid
npm install oauth
npm install url
npm install request
npm install cookies
前面已经安装了一些模块,npm将报告已安装的版本信息或将模块升级到最新的版本。
(2) 新建一个名为config.js的文件来存储服务器的配置信息,将下面的代码插入到文件中,用自己的配置信息替换突出显示的内容。
exports.mongohq = {
username:''YOUR_DB_USERNAME',
password: 'YOUR_DB_PASSWORD',
name: 'YOUR_DB_NAME',
host: 'YOUR_DB_HOST',
port: YOUR_DB_PORT
}
exports.amazon = {
s3bucket: 'YOUR_S3_BUCKET_NAME',
keyid: 'YOUR_AWS_KEY_ID',
secret: 'YOUR_AWS_SECRET'
}
exports.twitter = {
keyid: 'YOUR_TWITTER_KEY_ID',
secret: 'YOUR_TWITTER_SECRET'
}
exports.facebook = {
keyid: 'YOUR_FACEBOOK_KEY_ID',
secret: 'YOUR_FACEBOOK_SECRET'
}
exports.server = 'YOUR_IP_ADDRESS'
exports.max_stream_size = 100
代码片段位于lifestream/server/config.js
(3) 使用下面的更新版本替换lifestream/server文件夹下common.js文件的内容。
var util = exports.util = require('util')
var connect =exports.connect = require('connect')
var knox = exports.knox = require('knox')
var uuid =exports.uuid =require('node-uuid')
var oauth =exports.oauth =require('oauth')
var url =exports.url =require('url')
var request =exports.request = require('request')
var Cookies =exports.Cookies = require('Cookies')
var config = exports.config =require('./config.js')
// JSON functions
exports.readjson = function(req,win,fail) {
var bodyarr= [];
req.on('data',function(chunk){
bodyarr.push(chunk);
})
req.on('end',function(){
varbodystr = bodyarr.join('');
util.debug('READJSON:'+req.url+':'+bodystr);
try {
varbody = JSON.parse(bodystr);
win&& win(body);
}
catch(e){
fail&& fail(e)
}
})
}
exports.sendjson = function(res,obj){
res.writeHead(200,{
'Content-Type': 'text/json',
'Cache-Control': 'private, max-age=0'
});
var objstr= JSON.stringify(obj);
util.debug('SENDJSON:'+objstr);
res.end(objstr );
}
// mongo functions
var mongodb = require('mongodb')
var mongo = {
mongo:mongodb,
db: null,
}
mongo.init = function( opts, win, fail ){
util.log('mongo: '+opts.host+':'+opts.port+'/'+opts.name)
mongo.db =
newmongodb.Db(
opts.name,
newmongodb.Server(opts.host, opts.port, {}),
{native_parser:true,auto_reconnect:true});
mongo.db.open(function(){
if(opts.username ) {
mongo.db.authenticate(
opts.username,
opts.password,
function(err){
if(err) {
fail && fail(err)
}
else {
win && win(mongo.db)
}
})
}
else {
win&& win(mongo.db)
}
},fail)
}
mongo.res = function( win, fail ){
returnfunction(err,res) {
if( err ){
util.log('mongo:err:'+JSON.stringify(err));
fail&& 'function' == typeof(fail) && fail(err);
}
else {
win&& 'function' == typeof(win) && win(res);
}
}
}
mongo.open = function(win,fail){
mongo.db.open(mongo.res(function(){
util.log('mongo:ok');
win&& win();
},fail))
}
mongo.coll = function(name,win,fail){
mongo.db.collection(name,mongo.res(win,fail));
}
exports.mongo = mongo
代码片段位于lifestream/server/common.js
这个common.js的更新版本支持MongoDB验证,这需要使用MongoHQ的云数据库服务。
(4) 在文件夹lifestream/server下新建一个名为server.mongo.js的文件,将下面的代码插入到该文件中。
var common = require('./common.js')
var config = common.config
var mongo = common.mongo
var util = common.util
var connect = common.connect
var knox = common.knox
var uuid = common.uuid
var oauth = common.oauth
var url = common.url
var request = common.request
var Cookies = common.Cookies
// API functions
function search(req,res){
var merr = mongoerr400(res)
mongo.coll(
'user',
function(coll){
coll.find(
{username:{$regex:new RegExp('^'+req.params.query)}},
{fields:['username']},
merr(function(cursor){
var list = []
cursor.each(merr(function(user){
if( user ) {
list.push(user.username)
}
else {
common.sendjson(res,{ok:true,list:list})
}
}))
})
)
}
)
}
function loaduser(req,res) {
var merr = mongoerr400(res)
finduser(true,['username','name','following','followers','stream'],
req,res,function(user)
{
var userout =
{ username: user.username,
name: user.name,
followers: user.followers,
following: user.following,
stream: user.stream
}
common.sendjson(res,userout)
})
}
function register(req,res) {
var merr = mongoerr400(res)
mongo.coll(
'user',
function(coll){
coll.findOne(
{username:req.json.username},
merr(function(user){
if( user ) {
err400(res)()
}
else {
var token = common.uuid()
coll.insert(
{ username: req.json.username,
token: token,
followers: [],
following: [],
stream: []
},
merr(function(){
common.sendjson(res,{ok:true,token:token})
})
)
}
})
)
}
)
}
// utility functions
function finduser(mustfind,fields,req,res,found){
var merr = mongoerr400(res)
mongo.coll(
'user',
function(coll){
var options = {}
if( fields ) {
options.fields = fields
}
coll.findOne(
{username:req.params.username},
options,
merr(function(user){
if( mustfind && !user ) {
err400(res)
}
else {
found(user,coll)
}
})
)
}
)
}
function mongoerr400(res){
return function(win){
return mongo.res(
win,
function(dataerr) {
err400(res)(dataerr)
}
)
}
}
function err400(res,why) {
return function(details) {
util.debug('ERROR 400 '+why+' '+details)
res.writeHead(400,''+why)
res.end(''+details)
}
}
function collect() {
return function(req,res,next) {
if( 'POST' == req.method ) {
common.readjson(
req,
function(input) {
req.json = input
next()
},
err400(res,'read-json')
)
}
else {
next()
}
}
}
function auth() {
return function(req,res,next) {
var merr = mongoerr400(res)
mongo.coll(
'user',
function(coll){
coll.findOne(
{token:req.headers['x-lifestream-token']},
{fields:['username']},
merr(function(user){
if( user ) {
next()
}
else {
res.writeHead(401)
res.end(JSON.stringify({ok:false,err:'unauthorized'}))
}
})
)
}
)
}
}
var db = null
var server = null
mongo.init(
{
name: config.mongohq.name,
host: config.mongohq.host,
port: config.mongohq.port,
username: config.mongohq.username,
password: config.mongohq.password,
},
function(res){
db = res
var prefix = '/lifestream/api/user/'
server = connect.createServer(
connect.logger(),
collect(),
connect.router(function(app){
app.post( prefix+'register', register)
,app.get( prefix+'search/:query', search)
}),
auth(),
connect.router(function(app){
app.get( prefix+':username', loaduser)
})
)
server.listen(3009)
},
function(err){
util.debug(err)
}
)
代码片段位于lifestream/server/server.mongo.js
(5) 在文件夹lifestream/server下新建一个名为accept.mongo.js的文件,将下面的代码插入到该文件中。
var common = require('./common.js')
var config = common.config
var util = common.util
var request = common.request
var assert = require('assert')
var eyes = require('eyes')
var urlprefix = 'http://'+config.server+':3009/lifestream/api'
var headers = {}
function handle(cb) {
return function (error, response, body) {
if( error ) {
util.debug(error)
}
else {
var code = response.statusCode
var json = JSON.parse(body)
util.debug(' '+code+': '+JSON.stringify(json))
assert.equal(null,error)
assert.equal(200,code)
cb(json)
}
}
}
function get(username,uri,cb){
util.debug('GET '+uri)
request.get(
{
uri:uri,
headers:headers[username] || {}
},
handle(cb)
)
}
function post(username, uri,json,cb){
util.debug('POST '+uri+': '+JSON.stringify(json))
request.post(
{
uri:uri,
json:json,
headers:headers[username] || {}
},
handle(cb)
)
}
module.exports = {
api:function() {
var foo = (''+Math.random()).substring(10)
var bar = (''+Math.random()).substring(10)
// create and load
;post(
null,
urlprefix+'/user/register',
{username:foo},
function(json){
assert.ok(json.ok)
headers[foo] = {
'x-lifestream-token':json.token
}
;get(
foo,
urlprefix+'/user/'+foo,
function(json){
assert.equal(foo,json.username)
assert.equal(0,json.followers.length)
assert.equal(0,json.following.length)
;post(
null,
urlprefix+'/user/register',
{username:bar},
function(json){
assert.ok(json.ok)
headers[bar] = {
‚x-lifestream-token':json.token
}
;get(
bar,
urlprefix+'/user/'+bar,
function(json){
assert.equal(bar,json.username)
assert.equal(0,json.followers.length)
assert.equal(0,json.following.length)
// search
;get(
null,
urlprefix+'/user/search/'+foo.substring(0,4),
function(json){
assert.ok(json.ok)
assert.equal(1,json.list.length)
assert.equal(json.list[0],foo)
;}) // search
;}) // get
;}) // post
;}) // get
;}) // post
}
}
代码片段位于lifestream/server/accept.mongo.js
这是一个验收测试,用于测试运行中的服务器。每个测试案例都在前一个测试案例的回调函数中运行,要确保测试按顺序运行。
(6) 安装expresso测试框架,需要用它来运行accept.mongo.js脚本。
npm install expresso
(7) 打开一个新的终端窗口,并启动服务器。
node server.mongo.js
21 Mar 13:39:47 - mongo: flame.mongohq.com:27044/lifestream
(8) 打开另一个新的终端窗口,运行验收测试。
expresso accept.mongo.js
DEBUG: POST http://192.168.100.112:3009/
lifestream/api/user/register: {"username":"707915425"}
DEBUG: 200: {"ok":true,
"token":"0C257205-AB94-4768-9FCC-A1B1321AD2A5"}
DEBUG: GET http://192.168.100.112:3009/
lifestream/api/user/707915425
DEBUG: 200: {"username":"707915425",
"followers":[],"following":[],"stream":[]}
...
服务器和验收测试都会生成调试输出,按顺序显示HTTP请求和响应。
(9) 转到MongoHQ网站,检查user集合的内容。在user集合中,应该看到两个文档
警告:验收测试需要一个真实的网络连接,因为服务器必需和远程的MongoHQ服务通信,以存储和检索数据。这就是它被称为验收测试而非单元测试的原因。根据定义,验收测试要有外部依赖。
示例说明
本章中的应用程序是一个完整的应用程序,包含许多不同的功能,它依赖很多npm模块。前面几章已经用过大多数的模块。以前没有用过的模块包括url、request和 cookies模块,这些模块都是处理HTTP请求的辅助模块。
本章还介绍了使用config.js文件存储服务器配置的概念。这只是前面章节中所使用的keys.js文件的扩展。创建用于生产的应用程序时,从实现中分离出配置,并且不在代码中嵌入配置的设置是一个好主意。
在本章的前面注册了MongoHQ,在前面的章节中也应该有Amazon、Twitter和Facebook的键,可以使用这些键来填写设置。
本章的common.js文件包括了前几章的所有实用功能。这些实用功能让你很容易在HTTP API中处理JSON的请求和响应,以及使用MongoDB的API。还有一个额外的功能。为了使用MongoHQ服务,需要登录到数据库。可以使用下面的代码。
mongo.db.open(function(){
if( opts.username ) {
mongo.db.authenticate(
opts.username,
opts.password,
function(err){
...
为了更便于管理,在本章中添加新功能时,服务器端的代码会存储在单独的server.*.js文件中。该示例的文件名为server.mongo.js。通过本章的介绍可以比较这些文件,以帮助理解。服务器端的代码遵循前面章节中使用的结构。首先,有主要的API函数,然后是一些实用功能,之后是connect模块配置。
在本示例中,实现了搜索函数、用户注册及获取用户详细信息的函数。搜索函数使用MongoDB的正则表达式搜索功能,寻找一个与给定前缀相匹配的用户名。这仅仅是在应用程序中实现用户搜索功能的一个简单方法。代码使用mongoerr400实用函数处理MongoDB出现错误,如果出现问题,将HTTP 400状态码返回给所有客户端。在本节的后面会解释这是如何工作的。下面的代码解释了搜索函数的工作原理。
coll.find(
{username:{$regex:new RegExp('^'+req.params.query)}},
{fields:['username']},
merr(function(cursor){
var list = []
cursor.each(merr(function(user){
if( user ) {
list.push(user.username)
}
else {
common.sendjson(res,{ok:true,list:list})
}
这段代码中的第一行粗体行显示了在MongoDB中如何使用正则表达式查询。它遵循标准的MongoDB查询语法。查询的值是作为HTTP请求的参数提供的,由传递到函数的req对象暴露。
第二行粗体行显示了如何限制从MongoDB结果返回的字段。这样,可以避免返回每个用户的所有数据。如果只是想要匹配的用户名列表,返回数据的所有字段就是资源浪费。
查询的结果作为cursor对象返回。为了使用该对象,要为它的each函数提供一个回调。对于结果集中的每一项,都会调用回调函数。这和传统的SQL数据库游标工作的方式非常相似。当所有的项都被返回之后,将得到一个空(null)的对象,这是停止的信号。if语句检查用户参数是否为空,如果为空,返回JSON结果。否则,它持续追加用户名到JSON结果中。
loaduser函数将大部分的工作交给finduser实用函数。这个函数不返回纯粹的数据库结果,因为这样做可能会公开内部系统的细节,如MongoDB的id字段。相反,loaduser函数只返回指定的数据集。以这种方式显式地过滤数据可能看起来有点偏执,但它是一个很好的安全经验法则。
继续向下阅读脚本文件,在实用函数部分的finduser实用函数,完成实际到数据库中寻找用户的工作。关键的代码是调用集合对象的findOne函数,执行用户搜索。
coll.findOne(
{username:req.params.username},
用户名被指定为HTTP请求的参数。在API使用的URL结构中,对于针对用户的请求,用户名必须是URL路径的一部分。
注册函数与finduser函数非常相似,不同之处在于:如果用户不存在,它会执行一个操作。如果无法找到给定的用户名,说明用户不存在,可以注册。注册是由下面的insert操作执行的。
var token = common.uuid()
coll.insert(
{ username: req.json.username,
token: token,
followers:[],
following:[],
stream: []
},
token是一个特殊的字段,用来验证用户的身份。uuid模块提供了一种方式,可以生成一个长的、随机的、唯一的字符串,特别适合作为令牌。因为这个示例的重点放在构建应用程序,而不是用户管理功能,所以没有实现密码系统。相反,代码采用了一条捷径。注册使用了先到先得的机制。令牌返回到客户端应用程序,客户端永久保存它。此令牌可以用来访问API。实际上它是一个永久的登录令牌。在生产环境中不应使用这种设计,但在这里可以用它模拟用户管理的逻辑,从而演示注册和认证,以及后来与Facebook和Twitter的集成。在开发过程中,需要删除全部现有的登录。可以通过删除并重新安装应用程序来实现这一点。如果在浏览器中测试示例,则只需要从本地存储系统中删除user项。
用户名的值来自于标准Node请求对象的json属性,这看起来相当奇怪。json属性是collect实用函数注入请求对象中的自定义属性,它包含了请求提交的任何JSON内容的解析值。collect函数截获HTTP的POST请求,通过使用common.readjson函数取得其内容。
function collect() {
return function(req,res,next) {
if( 'POST' == req.method ) {
common.readjson(
req,
function(input) {
req.json = input
next()
},
err400(res,'read-json')
)
}
else {
next()
}
}
}
collect函数特殊的另一个原因是,它实际上是connect模块中间件函数。中间件函数可以对HTTP请求做一些处理,然后将请求向前传递给其余的服务器。它处于请求的中间,因此而得名。connect模块是中间件函数的堆栈,每个函数都对请求做了一些工作。在前面的章节中,使用标准的router中间件定义自己的URL终点。在本示例中,建立了自己的中间件!
要定义connect中间件函数,需要编写一个函数,使用一些配置参数(collect还没有用到),并返回一个函数。函数接受三个参数:请求、响应和一个特殊的next函数。这是体现JavaScript强大功能的另一个示例:可以使用函数来动态构建另一个函数。
collect中间件函数的实际工作是在动态函数中完成的。检查POST请求,读取JSON信息,并设置req对象的自定义json属性。处理完后,调用特定的next函数。这样connect知道中间件已经完成处理工作,可以将请求传递到下一阶段进行处理。
如果出现错误该如何处理?因为正在建立一个可以独立于应用程序使用的API,所以需要确保很好地遵循HTTP协议。这意味着,如果是因为输入而产生错误,就需要返回一个400 BadRequest的状态代码。可以使用err400实用函数来执行这项任务,该函数创建了一个函数来完成实际工作。这样,就可以为代码不同的部分定义相应的错误消息。此外,mongoerr400函数针对MongoDB的错误创建了一个特殊的错误处理函数。正如在代码中看到的,可以使用这些函数,通过调用它们来为每个顶层API函数创建自定义的错误函数,如下所示。
var merr = mongoerr400(res)
注意:在本示例中的错误处理代码总是返回400 Bad Request的状态代码。严格地说,如果是因为你而导致的错误(例如,如果数据库连接中断),应该返回一个500 Internal Server Error的状态代码。collect中间件函数不是该服务器代码中唯一的中间件函数。还有一个auth中间件函数用来处理用户身份验证。只有登录的用户才可以调用某些API。这可以防止其他用户访问他人的私人资料。auth中间件函数处于这些API调用之前,用于检查请求是否来自已经登录的用户。这就是为什么代码的connect部分被分成两个路由器部分:第一个是未经验证的动作,如登记和查询;第二个是已通过验证的动作,如获取用户的详细信息或关注其他用户。
auth函数与finduser函数类似,都是通过用户名查找用户。它也需要一个自定义的HTTP头X-Lifestream-Token,其中包含的注册令牌必须与用户存储的令牌匹配。如果验证失败,返回HTTP401状态代码,表示这是未经授权的访问。否则,调用next函数,请求开始处理。下面的代码执行令牌搜索。
coll.findOne(
{token:req.headers['x-lifestream-token']},
{fields:['username']},
merr(function(user){
if( user ) {
next()
}
else {
res.writeHead(401)
res.end(JSON.stringify({ok:false,err:'unauthorized'}))
}
})
)
最后一部分代码,在创建到MongoDB数据库的连接后,设置了connect中间件堆栈。这些按顺序放在一起的函数实现了API结构的中间件。
不应该只是手动测试该服务器。也应该使用一套标准测试来验证API操作正常与否。可以通过构建验收测试实现这一点。使用Node的expresso模块,构建单元和验收测试。虽然应该创建单元测试和验收测试,但该示例的重点是验收测试。单元测试和验收测试之间的区别是什么呢?验收测试依赖于外部资源,而单元测试则不是。为了测试服务器可以正常使用MongoHQ(外部资源)工作,需要验收测试。
验收测试的代码位于accept.mongo.js脚本中。主服务器运行时,在一个单独的终端中运行该脚本。expresso模块将运行,测试任何被放置在特殊exports变量中的函数。在本示例中的代码只有一个主要测试:api函数。此函数包含了一组测试,按顺序运行并执行API的操作。
在此使用了common.js文件以避免重复代码。handle、get和post函数是实用函数,用来跟踪HTTP的请求,当它们到达时输出结果。因此,可以运行测试,看看直接会发生什么,这对于调试是非常有用的。
测试本身是API调用的序列。运行测试时,会注册两个用户,会请求它们的数据,并执行了一个搜索。
;post(
null,
urlprefix+'/user/register',
{username:foo},
function(json){
...
headers[foo] = {
'x-lifestream-token':json.token
}
;get(
foo,
urlprefix+'/user/'+foo,
function(json){
...
;post(
null,
urlprefix+'/user/register',
{username:bar},
function(json){
...
headers[bar] = {
'x-lifestream-token':json.token
}
;get(
bar,
urlprefix+'/user/'+bar,
function(json){
...
// search
;get(
null,
urlprefix+'/user/search/'+foo.substring(0,4),
使用约定格式化代码,避免了很多恼人的缩进。因为每个测试必须在前一个测试的回调中执行,所以通常会在屏幕右侧结束代码缩进。为了避免这种情况,可以在行开始的地方使用分号(;)字符。这使你能重置缩进级别。要确保正确关闭了所有的括号,这样它们和注释在结尾以相反的顺序列出。还有其他的方法,通过使用各种库来解决这个格式的问题。它们在更复杂的情况是有用的,但在目前这种情况下,有一个简单的线性执行流程与约定,很容易就可以保持代码相对整洁。
《移动云计算应用开发入门经典》试读电子书免费提供,有需要的留下邮箱,一有空即发送给大家。 别忘啦顶哦!