单页Web应用 8 服务器数据库

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;

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值