8.1 数据库的作用
选择数据存储:选择MongoDB作为数据存储,有几个理由:它被证明是可靠的、可扩展的,它有良好的性能,它的定位就是成为通用数据库。它的存储格式是JSON,它的数据管理工具是专门为JSON而开发的。
消除数据转换:传统的Web应用程序:开发人员必须进行SQL->Active Record->JSON的转换,及JSON->Active Record->SQL的转换。
把逻辑放在需要的地方:前后端逻辑代码通用。
8.2 MongoDB简介
一种可扩展的、高性能的、开源的NoSQL数据库:
- 可扩展的、高性能---MongoDB可使用较便宜的服务器进行水平扩展。而使用关系型数据库,扩展数据库唯一的简便方法是购买更好的硬件。
- 面向文档的存储---文档以集合的形式进行存储,类似SQL的表
- 动态schema---关系型数据库需要schema来定义什么数据可以存储在什么表中,而在MongoDB中,在同一个集合中的个人文档,可以有完全不同的结构,在更新文档的时候可以彻底改变文档结构。
面向文档的存储:单一的数据格式具备更加优异的性能。
动态文档结构:对比关系型数据库,必须要明确地定义表和schema,对数据结构的任何更改都需要更改schema。但也有缺点。
开始使用MongoDB: 推荐阅读《MongoDB in Action》
8.3 使用MongoDB驱动程序
package.json
{
"name": "SPA",
"version": "0.0.3",
"private": true,
"dependencies": {
"express": "3.2.x",
"mongodb": "1.3.x",
"socket.io": "0.9.x"
}
}
安装并连接MongoDB:
使用MongoDB的CRUD方法:
shell
> use spa;
switched to db spa
> db.user.insert({
... "name" : "Mike Mikowski",
... "is_online" : false,
... "css_map" : {"top":100, "left":120,
... "background-color":"rgb(136, 255, 136)"
... }
... });
WriteResult({ "nInserted" : 1 })
> db.user.insert({
... "name" : "Mr. Joshua C. Powell, humble humanitarian",
... "is_online" : false,
... "css_map" : {"top":150, "left":120,
... "background-color":"rgb(136, 255, 136)"
... }
... });
WriteResult({ "nInserted" : 1 })
> db.user.insert({
... "name": "Your name here",
... "is_online":false,
... "css_map" : {"top":50, "left":120,
... "background-color":"rgb(136, 255, 136)"
... }
... });
WriteResult({ "nInserted" : 1 })
> db.user.insert({
... "name" : "Hapless interloper",
... "is_onlie": false,
... "css_map":{"top":0, "left":120,
... "background-color":"rgb(136, 255, 136)"
... }
... });
WriteResult({ "nInserted" : 1 })
> db.user.find()
{ "_id" : ObjectId("5a60a67c022fa768214422e7"), "name" : "Mike Mikowski", "is_online" : false, "css_map" : { "top" : 100, "left" : 120, "background-color" : "rgb(136, 255, 136)" } }
{ "_id" : ObjectId("5a60a6d5022fa768214422e8"), "name" : "Mr. Joshua C. Powell, humble humanitarian", "is_online" : false, "css_map" : { "top" : 150, "left" : 120, "background-color" : "rgb(136, 255, 136)" } }
{ "_id" : ObjectId("5a60a718022fa768214422e9"), "name" : "Your name here", "is_online" : false, "css_map" : { "top" : 50, "left" : 120, "background-color" : "rgb(136, 255, 136)" } }
{ "_id" : ObjectId("5a60a759022fa768214422ea"), "name" : "Hapless interloper", "is_onlie" : false, "css_map" : { "top" : 0, "left" : 120, "background-color" : "rgb(136, 255, 136)" } }
向服务器应用添加CRUD操作:
8.4 验证客户端数据
验证对象类型:传给MongoDB的对象需要经过验证。
验证对象: JSV是一款使用JSON schema的验证器。浏览器和服务器都可以使用,所以不必编写或者维护两份单独的验证库。
安装JSV的node模块: npm install JSV@4.0.x
创建JSON schema :user.json
{
"type" : "object",
"additionalProperties" : false,
"properties" : {
"_id" : {
"type" : "string",
"minLength" : 25,
"maxLength" : 25
},
"name" : {
"type" : "string",
"minLength" : 2,
"maxLength" : 127
},
"is_online" : {
"type" : "boolean"
},
"css_map" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"background-color" : {
"required" : true,
"type" : "string",
"minLength" : 0,
"maxLength" : 25
},
"top" : {
"required" : true,
"type" : "integer"
},
"left" : {
"required" : true,
"type" : "integer"
}
}
}
}
}
加载JSON schema:routes.js
创建验证函数:将来自客户端的数据与user的JSON schema进行比较。
/*
* routes.js - module to provide routing
*/
/*jslint node : true, continue : true,
devel : true, indent : 2, maxerr : 50,
newcap : true, nomen : true, plusplus : true,
regexp : true, sloppy : true, vars : false,
white : true
*/
/*global */
// ------------ BEGIN MODULE SCOPE VARIABLES --------------
'use strict';
var
configRoutes,
mongodb = require( 'mongodb' ),
mongoServer = new mongodb.Server(
'127.0.0.1',
27017 //mongodb.Connection.DEFAULT_PORT
),
dbHandle = new mongodb.Db(
'spa', mongoServer, { safe : true }
),
makeMongoId = mongodb.ObjectID;
var fsHandle = require( 'fs' );
var loadSchema, checkSchema;
var JSV = require( 'JSV' ).JSV;
var validator = JSV.createEnvironment(); //创建JSV验证器环境
var objTypeMap = { 'user' : {} }; //声明并赋值允许的对象类型的映射
// ------------- END MODULE SCOPE VARIABLES ---------------
// ------------- BEGIN MODULE INITIALIZATION --------------
dbHandle.open( function () {
console.log( '** Connected to MongoDB **' );
});
//加载Schema
loadSchema = function ( schema_name, schema_path ) {
fsHandle.readFile( schema_path, 'utf8', function ( err, data ) {
objTypeMap[ schema_name ] = JSON.parse( data );
});
};
//验证函数
checkSchema = function ( obj_type, obj_map, callback ) {
var schema_map = objTypeMap[ obj_type ];
var report_map = validator.validate( obj_map, schema_map );
callback( report_map.errors );
};
(function () {
var schema_name, schema_path;
for (schema_name in objTypeMap) {
if (objTypeMap.hasOwnProperty( schema_name ) ) {
schema_path = __dirname + '/' + schema_name + '.json';
loadSchema( schema_name, schema_path );
}
}
}());
// -------------- END MODULE INITIALIZATION ---------------
// ---------------- BEGIN PUBLIC METHODS ------------------
configRoutes = function ( app, server ) {
app.get( '/', function ( request, response ) {
response.redirect( '/spa.html' );
});
app.all( '/:obj_type/*?', function ( request, response, next ) {
response.contentType( 'json' );
if (objTypeMap[ request.params.obj_type ] ) { //是否是user类型
next();
} else {
response.send({error_msg : request.params.obj_type + ' is not a valid object type'});
}
});
app.get( '/:obj_type/list', function ( request, response ) {
console.log(request.params.obj_type);
dbHandle.collection(
request.params.obj_type,
function ( outer_error, collection ) {
console.log(collection);
collection.find().toArray(
function ( inner_error, map_list ) {
response.send( map_list );
}
);
}
);
});
app.post( '/:obj_type/create', function ( request, response ) {
var obj_type = request.params.obj_type;
var obj_map = request.body;
checkSchema( obj_type, obj_map, function ( error_list ) {
if ( error_list.length === 0 ) {
dbHandle.collection(
obj_type,
function ( outer_error, collection ) {
var options_map = { safe: true };
collection.insert(
obj_map,
options_map,
function ( inner_error, result_map ) {
response.send( result_map );
}
);
}
);
} else {
response.send({
error_msg : 'Input document not valid',
error_list : error_list
});
}
});
});
app.get( '/:obj_type/read/:id', function ( request, response ) {
var find_map = { _id: makeMongoId( request.params.id ) };
dbHandle.collection(
request.params.obj_type,
function ( outer_error, collection ) {
collection.findOne(
find_map,
function ( inner_error, result_map ) {
response.send( result_map );
}
);
}
);
});
app.post( '/:obj_type/update/:id', function ( request, response ) {
var
find_map = { _id: makeMongoId( request.params.id ) },
obj_map = request.body
obj_type = request.params.obj_type;;
checkSchema( obj_type, obj_map, function ( error_list ) {
if ( error_list.length === 0 ) {
dbHandle.collection(
obj_type,
function ( outer_error, collection ) {
var
sort_order = [],
options_map = {
'new' : true, upsert: false, safe: true
};
collection.findAndModify(
find_map,
sort_order,
obj_map,
options_map,
function ( inner_error, updated_map ) {
response.send( updated_map );
}
);
}
);
} else {
response.send({
error_msg : 'Input document not valid',
error_list : error_list
});
}
});
});
app.get( '/:obj_type/delete/:id', function ( request, response ) {
var find_map = { _id: makeMongoId( request.params.id ) };
dbHandle.collection(
request.params.obj_type,
function ( outer_error, collection ) {
var options_map = { safe: true, single: true };
collection.remove(
find_map,
options_map,
function ( inner_error, delete_count ) {
response.send({ delete_count: delete_count });
}
);
}
);
});
};
module.exports = { configRoutes : configRoutes };
// ----------------- END PUBLIC METHODS -------------------
8.5 创建单独的CRUD模块
来自Web socket连接的对象也需要拥有验证和管理数据库中文档的所有逻辑。
/*
* routes.js - module to provide routing
*/
/*jslint node : true, continue : true,
devel : true, indent : 2, maxerr : 50,
newcap : true, nomen : true, plusplus : true,
regexp : true, sloppy : true, vars : false,
white : true
*/
/*global */
'use strict';
//加载Schema
var loadSchema, checkSchema, clearIsOnline;
//CRUD
var checkType, constructObj, readObj;
var updateObj, destroyObj;
var fsHandle = require( 'fs' );
var JSV = require( 'JSV' ).JSV;
var validator = JSV.createEnvironment(); //创建JSV验证器环境
var objTypeMap = { 'user' : {} }; //声明并赋值允许的对象类型的映射
var mongodb = require( 'mongodb' );
var mongoServer = new mongodb.Server(
'127.0.0.1',
27017 //mongodb.Connection.DEFAULT_PORT
);
var dbHandle = new mongodb.Db(
'spa', mongoServer, { safe : true }
);
//加载Schema
loadSchema = function ( schema_name, schema_path ) {
fsHandle.readFile( schema_path, 'utf8', function ( err, data ) {
objTypeMap[ schema_name ] = JSON.parse( data );
});
};
//验证函数
checkSchema = function ( obj_type, obj_map, callback ) {
var schema_map = objTypeMap[ obj_type ];
var report_map = validator.validate( obj_map, schema_map );
callback( report_map.errors );
};
//在连接上MongoDB时会执行,确保在服务器启动时,所有的用户都被标记为离线状态
clearIsOnline = function () {
updateObj(
'user',
{ is_online : true },
{ is_online : false },
function ( response_map ) {
console.log( "All users set to offline", response_map );
}
);
};
checkType = function ( obj_type ) {
if (!objTypeMap[ obj_type ] ) {
return ({ error_msg : 'Object type "' + obj_type + '" is not supported.' });
}
return null;
};
constructObj = function ( obj_type, obj_map, callback ) {
var type_check_map = checkType( obj_type );
if ( type_check_map ) {
callback( type_check_map );
return;
}
checkSchema( obj_type, obj_map, function ( error_list ) {
if ( error_list.length === 0 ) {
dbHandle.collection(
obj_type,
function ( outer_error, collection ) {
var options_map = { safe: true };
collection.insert(
obj_map,
options_map,
function ( inner_error, result_map ) {
callback( result_map );
}
);
}
);
} else {
callback({
error_msg : 'Input document not valid',
error_list : error_list
});
}
});
};
readObj = function ( obj_type, find_map, fields_map, callback ) {
var type_check_map = checkType( obj_type );
if ( type_check_map ) {
callback( type_check_map );
return;
}
dbHandle.collection(
obj_type,
function ( outer_error, collection ) {
collection.find( find_map, fields_map ).toArray(
function ( inner_error, map_list ) {
callback( map_list );
}
);
}
);
};
updateObj = function ( obj_type, find_map, set_map, callback ) {
var type_check_map = checkType( obj_type );
if ( type_check_map ) {
callback( type_check_map );
return;
}
checkSchema( obj_type, find_map,
function ( error_list ) {
if ( error_list.length === 0 ) {
dbHandle.collection(
obj_type,
function ( outer_error, collection ) {
collection.update(
find_map,
{ $set : set_map },
{ safe : true, multi : true, upsert : false },
function ( inner_error, updated_count ) {
callback( { updated_count : updated_count } );
}
);
}
);
} else {
callback({
error_msg : 'Input document not valid',
error_list : error_list
});
}
});
};
destroyObj = function ( obj_type, find_map, callback ) {
var type_check_map = checkType( obj_type );
if ( type_check_map ) {
callback( type_check_map );
return;
}
dbHandle.collection(
obj_type,
function ( outer_error, collection ) {
var options_map = { safe: true, single: true };
collection.remove(
find_map,
options_map,
function ( inner_error, delete_count ) {
callback({ delete_count: delete_count });
}
);
}
);
};
module.exports = {
makeMongoId : mongodb.ObjectID,
checkType : checkType,
construct : constructObj,
read : readObj,
update : updateObj,
destroy : destroyObj
};
dbHandle.open( function () {
console.log( '** Connected to MongoDB **' );
clearIsOnline();
});
(function () {
var schema_name, schema_path;
for (schema_name in objTypeMap) {
if (objTypeMap.hasOwnProperty( schema_name ) ) {
schema_path = __dirname + '/' + schema_name + '.json';
loadSchema( schema_name, schema_path );
}
}
}());
console.log('** CRUD module loaded **');
8.6 构建chat模块
开始创建chat模块:lib/chat.js
为什么使用Web socket? 和其他一些在浏览器端使用的近实时通信技术相比, Web socket有一些显著的优点。
- 为维持数据连接,Web socket的数据帧只需要两个字节,而AJAX HTTP调用(用于长轮询)的每一帧经常要发送上千字节的信息(实际数据量取决于cookie的数量和大小)。
- Web socket比长轮询有优势。通常情况下,Web socket使用的网络带宽是长轮询的1%~2%,延迟时间是长轮询的三分之一。Web socket也往往是更加“防火墙友好”的。
- Web socket是全双工的,而大多数的其他解决方法并不是,它们相当于需要两个连接。
- 不像Flash socket,Web socket几乎在所有平台的所有现代浏览器上都能工作,包括像智能手机和平板电脑这样的移动设备。
创建adduser消息处理程序:
- 使用CRUD模块,尝试在MongoDB中按提供的用户名查找用户对象;
- 如果找到请求用户名的对象,则使用找到的对象;
- 如果没找到请求用户名的对象,则使用提供的用户名,创建一个新的用户对象,并把它插入到数据库里面,然后使用这个新创建的对象;
- 在MongoDB中更新用户对象,提示用户在线(is_online:true);
创建updatechat消息处理程序:
- 检查聊天数据,检索接收者;
- 确定预期接收者是否在线;
- 如果接收者在线,则在接收者的socket连接上,向接收者发送聊天数据;
- 如果接收者不在线,则在发送者的socket连接上,向发送者发送新的聊天数据;新的聊天数据要通知发送者;预期的接收者不在线。
创建disconnect消息处理程序:把客户端用户标记为离线状态。然后需要把更新后的在线用户列表广播给所有已连接的客户端。
创建updateavatar消息处理程序:更新头像。
/*
* routes.js - module to provide routing
*/
/*jslint node : true, continue : true,
devel : true, indent : 2, maxerr : 50,
newcap : true, nomen : true, plusplus : true,
regexp : true, sloppy : true, vars : false,
white : true
*/
/*global */
'use strict';
var chatObj;
var socket = require( 'socket.io' );
var crud = require( './crud' );
var emitUserList, signIn, signOut;
var makeMongoId = crud.makeMongoId;
var chatterMap = {}; //把用户ID和socket连接关联起来
//把在线用户列表广播给所有已连接的客户端
emitUserList = function ( io ) {
crud.read(
'user',
{ is_online : true },
{},
function ( result_list ) {
io
.of( '/chat' )
.emit( 'listchange', result_list );
}
);
};
//通过更新用户的状态,登入当前用户
signIn = function ( io, user_map, socket ) {
crud.update(
'user',
{ '_id' : user_map.id },
{ is_online : true },
function ( result_map ) {
emitUserList( io );
user_map.is_online = true;
socket.emit( 'userupdate', user_map );
}
);
chatterMap[ user_map._id ] = socket;
socket.user_id = user_map._id;
};
//
signOut = function ( io, user_id ) {
crud.update(
'user',
{ '_id' : user_id },
{ is_online : false },
function ( result_list ) {
emitUserList( io );
}
);
delete chatterMap[ user_id ];
};
// 使用/chat名字空间,发送adduser,
// updatechat, leavechat,
// disconnect和updateavatar
chatObj = {
connect : function ( server ) {
var io = socket.listen( server );
//Begin to setup
io
.set( 'blacklist', [] ) //黑名单
.of( '/chat' ) //
.on( 'connection', function ( socket ) {
socket.on('adduser', function ( user_map ) {
crud.read(
'user',
{ name : user_map.name },
{},
function ( result_list ) {
var result_map;
var cid = user_map.cid;
delete user_map.cid;
//
if ( result_list.length > 0 ) {
result_map = result_list[0];
result_map.cid = cid;
signIn( io, result_map, socket );
} else {
user_map.is_online = true;
crud.construct(
'user',
user_map,
function ( result_list ) {
result_map = result_list[ 0 ];
result_map.cid = cid;
chatterMap[ result_map._id ] = socket;
socket.user_id = result_map._id;
socket.emit( 'userupdate', result_map );
emitUserList( io );
}
);
}// if
}
);
} );
socket.on('updatechat', function ( chat_map ) {
if (chatterMap.hasOwnProperty( chat_map.dest_id ) ) {
chatterMap[ chat_map.dest_id ]
.emit( 'updatechat', chat_map );
} else {
socket.emit( 'updatechat', {
sender_id : chat_map.sender_id,
msg_text : chat_map.dest_name + ' has gone offline.'
});
}
});
socket.on('leavechat', function () {
console.log(
'** user %s logged out **', socket.user_id
);
signOut( io, socket.user_id );
} );
socket.on('disconnect', function () {
console.log(
'** user %s closed browser window or tab **', socket.user_id
);
signOut( io, socket.user_id );
} );
socket.on('updatechat', function ( avtr_map ) {
crud.update(
'user',
{ '_id': makeMongoId( avtr_map.person_id ) },
{ css_map : avtr_map.css_map },
function ( result_list ) {
emitUserList( io );
}
);
} );
}
);
return io;
}
};
module.exports = chatObj;