Model 数据模型模块
该模块属于该书项目的数据模块,其中使用了三个插件,两个 jQuery 事件插件,一个数据库管理插件,分别是
gevent
:主要用来实现用户自定义的事件;ue
:该插件和移动端触摸事件有关;taffyDb
:数据库管理插件
以上三个插件目前只是使用到了 gevent
和 taffyDb
两个,第一个主要用来实现用户登入登出和用户更新等自定义事件,其实现原来有待研究,第二个就是数据库管理插件了,这个插件用起来还是不那么复杂的,并且也具备一定的灵活性,后面代码中会稍微研究下;
该模块涉及到的一些对象和函数列表:
personProto
:【对象类型】,用户原型,主要用来创建用户对象的,这里创建对象的方式使用了原生的接口:Object.create
,然后personProto
作为参数,其实也就是后面创建的person
对象都是该原型的子对象,实现了原型继承;people
:【对象类型】,通过立即执行的匿名函数方式实现的对象,用途是管理所有用户数据和一些有关用户操作的东西;makePerson
:【函数类型】,这个就是用来创建用户的函数,里面包含用户属性赋值,和信息的缓存处理;makeCid
:【函数类型】,创建唯一的用户ID;clearPeopleDb
:【函数类型】清空用户数据;completeLogin
:【函数类型】用户登录成功之后的回调函数;removePerson
:【函数类型】用户登出之后,删除当前用户登录状态;initModule
:模块初始化。
这个模块并没有类似 chat
模块的 configModule
函数,原因在于这是数据模块,相对独立的模块,不需要直接与 shell
模块发生联系,并且注册方式是在 shell
模块注册之前完成:
// 加载数据模型模块
spa.model.initModule();
// 加载 shell 模块
spa.shell.initModule( $container );
首先来看下,数据模块用户的相关的基本内容吧,包括:用户相关属性,创建;
在这之前来简单看下 configMap
和 stateMap
基本配置:
这里面只有个匿名用户的ID,由于目前来说还用不着和具体模块发生联系,配置也没什么
configMap = { anon_id : 'a0' }
状态配置:
这里配置主要跟用户有关联,匿名用户对象,
cid
,用户管理对象people
的东西,以及user
这个代表当前登录的用户stateMap = { anon_user : null, cid_serial : 0, people_cid_map : {}, // 根据 cid 缓存的人物对象 people_db : TAFFY(), // 创建空数据集合 user : null }
用户:person
用户关键字使用:person
,至于 people
是对所有用户进行管理的对象
用户属性
id
:这里id
应该分为三种(匿名用户ID,用户客户端ID,用户服务端ID),目前还没涉及到服务器一块,因此目前所用到的只有前两者,对应属性:anon_id
和cid
;name
:这个肯定必须的要了,也不用过多解释,地球人都知道;cid
:id
中的第二类,其值组合根据自己需要可以自定义,该书使用的是简单的:'c' + cid_serial
方式,类似:c0, c1, c2 … 字符串;css_map
: 这个属性主要用来定义对应用户在聊天窗口中显示时的一些基本样式;
用户对象处理函数
makeCid
:生成cid
makeCid = function () { return 'c' + String( stateMap.cid_serial++ ); };
创建用户对象:
makePerson
// spa.model.js /* 这里面主要做的事情有: 1. `Object.create` 创建子类即用户对象 2. 用户属性初始化 3. 缓存新建对象至 `stateMap.people_cid_map` 4. 将新建用户插入数据库保存 `stateMap.people_cid_map`:这个对象用来根据 cid 来保存该对象, 日后可通过 cid 直接获取到对应的用户对象,不过这里直接用这个去缓存用户, 貌似针对小量用户还是可以行,如果在用户数量一多,估计所需的存储空间还是不容忽视的。 这里不太明白作者用意,当然作为测试来说无可厚非,若果真正到开发中,这个用户缓存是否 需要就要考量下了,更何况这个缓存感觉必要性不是很大; */ makePerson = function ( person_map ) { var person, cid = person_map.cid, css_map = person_map.css_map, id = person_map.id, name = person_map.name; if ( cid === undefined || !name ) { throw 'client id and name required'; } person = Object.create( personProto ); person.cid = cid; person.name = name; person.css_map = css_map; if ( id ) { person.id = id; } // 缓存新建对象 stateMap.people_cid_map[ cid ] = person; // 插入到 taffy 数据库中 // 这里的 people_db 事实上就是通过 taffyDb 插件创建的,然后通过该插件下的 // 数据插入操作执行用户保存行为 stateMap.people_db.insert( person ); return person; };
既然有创建就得有删除:
removePerson
这个接口作用还是比较明确且简单的,由于数据的存储就两个地方,一个是:
stateMap.people_cid_map
缓存,一个是:taffyDb
数据库的保存,只要删除这两个地方的用户数据就OK。removePerson = function ( person ) { if ( !person ) { return false; } if ( person.id === configMap.anon_id ) { return false; } stateMap.people_db({ cid: person.cid }).remove(); if ( person.cid ) { delete stateMap.people_cid_map[ person.cid ]; } return true; };
完成登录:
completeLogin
完成登录所做的事情,主要是将当前登录的用户信息保存到
stateMap.user
当中,这个接口属于回调函数,即:用户登录,服务器接受到登录请求,并且返回成功登录的响应之后去调用,比较需要注意的是最后一句:$.gevent.publish
,这句意义在于用户登录完成之后发布登录完成的自定义下消息:spa-login
,而后,可通过捕获这个事件做出一定的响应。completeLogin = function ( user_list ) { var user_map = user_list[ 0 ]; delete stateMap.people_cid_map[ user_map.cid ]; stateMap.user.cid = user_map._id; stateMap.user.id = user_map._id; stateMap.user.css_map = user_map.css_map; stateMap.people_cid_map[ user_map._id ] = stateMap.user; // 第一个参数:发布的消息名称,第二个:与消息相关的数据数组 $.gevent.publish( 'spa-login', [ stateMap.user ] ); };
用户管理对象:people
这个对象用来管理用户数据以及用户行为等,比如:获取用户数据库,获取当前登录用户,用户登入登出行为等;提供了一套获取 stateMap
中数据的一些接口。
get_by_cid
:通过cid
获取用户对象,其实也就是从stateMap.people_cid_map
中获取;get_db
:与stateMap.people_db
对应;get_user
:与stateMap.user
对应;login
:用户登录行为;logout
:用户登出行为。
登录实现通过 Socket.io
来实现,不太知道是什么东西,估计是充当数据通信管道类似的东西,据说是‘全双工’形式的双向通信,值得研究研究。
// spa.model.js
people = (function () {
var
get_by_cid, get_db, get_user, login, logout;
get_by_cid = function ( cid ) {
return stateMap.people_cid_map[ cid ];
};
get_db = function () {
return stateMap.people_db;
};
get_user = function () {
return stateMap.user;
};
// 这里目前采用的是利用 `spa.fake.js` 模块模拟 `socket` 通信实现虚拟登录
// 后面会单独研究下虚拟登录的实现原理,为学习 Socket.io 做个准备
login = function ( name ) {
// Socket.io
var sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
stateMap.user = makePerson({
cid : makeCid(),
css_map : { top: 25, left: 25, 'background-color': '#8f8' },
name : name
});
// 监听后端登录过程中的 userupdate 消息
sio.on( 'userupdate', completeLogin );
sio.emit( 'adduser', {
cid : stateMap.user.cid,
css_map : stateMap.user.css_map,
name : stateMap.user.name
} );
};
// 登出行为只是做了简单的数据清楚操作
logout = function () {
var
is_removed, user = stateMap.user;
// 注销后成为匿名用户
is_removed = removePerson( user );
stateMap.user = stateMap.anon_user;
return is_removed;
};
return {
get_by_cid : get_by_cid,
get_db : get_db,
get_user : get_user,
login : login,
logout : logout
};
}());
模块初始化:initModule
模块初始化和以往差不多,主要完一些数据的初始化工作,由于最初状态应该是没有用户登录的情况,因此当前应该采用匿名用户方式,即需要初始化下匿名用户对象:stateMap.anon_user
,后面的是模拟数据部分;
通过在spa.js
中,shell
模块初始化之前调用进行数据初始化
initModule = function ( $container ) {
// 加载数据模型模块
spa.model.initModule();
// 加载 shell 模块
spa.shell.initModule( $container );
};
通过初始化方式也可知,数据模型模块是相对独立与 shell
模块的
initModule = function () {
var
i, len, people_list, person_map;
stateMap.anon_user = makePerson({
cid : configMap.anon_id,
id : configMap.anon_id,
name : 'anonymous'
});
stateMap.user = stateMap.anon_user;
// 模拟数据
if ( isFakeData ) {
people_list = spa.fake.getPeopleList();
for ( i = 0, len = people_list.length; i < len; i++ ) {
person_map = people_list[i];
makePerson({
cid : person_map._id,
id : person_map._id,
css_map : person_map.css_map,
name : person_map.name
});
}
}
};
登录模拟
模拟登录的代码:
login = function ( name ) {
// Socket.io
var sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
stateMap.user = makePerson({
cid : makeCid(),
css_map : { top: 25, left: 25, 'background-color': '#8f8' },
name : name
});
// 监听后端登录过程中的 userupdate 消息
sio.on( 'userupdate', completeLogin );
sio.emit( 'adduser', {
cid : stateMap.user.cid,
css_map : stateMap.user.css_map,
name : stateMap.user.name
} );
};
下面来看下是如何通过模拟 Socket.io
来实现模拟登录的,看下其过程究竟是如何的,先去绘个图先,还是比较喜欢流程图形式,清晰明了,还能理顺思路。
上图虽然完成了,可是总感觉挺别扭,总感觉少了什么,或哪里错了,囧囧囧!!!
主要步骤:
创建管道
var sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
上面一句,考虑了虚拟数据和真实数据的情况,
isFakeData
是判断依据,这里是模拟数据,因此是直接通过spa.fake.mockSio
来获取管道对象。Client 请求
客户端角度属于发送登录请求方,主要以下步骤
发起请求,发送之前需要确定登录用户信息,并将其缓存,通过下面方式完成
stateMap.user = makePerson({ cid : makeCid(), css_map : { top: 25, left: 25, 'background-color': '#8f8' }, name : name });
监听
userupdate
消息在这里监听消息,同时为此消息注册响应句柄,即更新用户信息,登录成功后需要调用的回调
// 监听后端登录过程中的 userupdate 消息 sio.on( 'userupdate', completeLogin );
发送
adduser
消息该消息会携带用户数据,发送给服务器,告诉服务器去更新当前登录用户信息,成功之后服务器响应更新结果给
SIO
,SIO
拿到结果,判断是否成功,成功则触发userupdate
消息的处理函数:completeLogin
sio.emit( 'adduser', { cid : stateMap.user.cid, css_map : stateMap.user.css_map, name : stateMap.user.name } );
Server 响应
setTimeout
3秒的动作,就是模拟服务器3秒后给出了响应,然后触发userupdate
告诉客户端执行后续操作// 模拟向服务器发送消息,并且在响应成功后调用回调 emit_sio = function ( msg_type, data ) { // 用 userupdate 去响应 adduser 事件 if ( msg_type === 'adduser' && callback_map.userupdate ) { setTimeout(function () { callback_map.userupdate([{ _id : makeFakeId(), name : data.name, css_map : data.css_map }]); }, 3000 ); } };
另外需要注意的是:
callback_map
这个东西是消息的处理函数容器,比如:userupdate
对应的completeLogin
,直接调用:callback_map.userupdate
其实就是执行了completeLogin
。
总结
总的感觉,从模拟 SIO 角度来说,登录过程并不是很复杂,简单来说就是:
- 客户端携带用户数据发送请求;
- SIO 添加相应的更新消息
userupdate
,并发送添加用户消息adduser
给服务器; - 服务器收到消息,进行更新,将结果响应给 SIO;
- SIO,拿到结果,通知客户端;
- 客户端执行登录成功的回调。
其实可以更简单点:
想想所有的 C/S 请求都可以简化成上图(尴尬~~),所谓大道至简嘛(哈哈 - -!)
而对于模型来说,目前主要两个对象:person
和 people
,用户对象和用户管理对象,前者包含了单个用户的特性,后者拥有管理用户数据及行为的接口。