单页Web应用 5 构建Model

    设计和构建Model的people对象。Model向Shell和功能模块提供业务逻辑和数据。Model不依赖用户界面,它被分离出来负责逻辑和数据管理。Model自身通过使用Data模块,从Web服务器分离出来。

    希望单页应用使用people对象来管理人员列表,这包括用户以及和我们聊天的人。

    用户登入和登出,添加触摸控件。

5.1 理解Model


     Model自身通过使用Data模块,从Web服务器分离出来。这种分离对开发人员和质量保证都有好处。

     Shell会管理登入,Chat功能模块将管理聊天窗口。Avatar功能模块将管理代表人员的彩色盒子。


  • 为了管理登入和登出的过程,Shell需要知道当前用户,它需要确定“当前用户是谁”的方法,在需要是更改用户。
  • Chat功能模块也需要查看当前用户,以此判断他是否已授权发送或者接收消息。它需要确定正在和用户聊天的人,如果有的话。它需要查询在线人员的列表,这样就可以把他们显示在聊天滑块的左边。最后,它需要发送消息和选择用户进行聊天的方法。
  • Avatar功能模块也需要查看当前用户,以此判断他是否已授权查看头像并与之交互。它也需要当前用户的身份证明,这样它就能把当前用户的头像显示为蓝色。它也需要确定正在和用户聊天的人,这样它就能把这个人的头像显示为绿色。最后,它需要设置和检索当前所有在线人员头像的详细信息的方法。

       模块很多必需的业务逻辑和数据都是重叠的。

  1. 在每个功能模块中构建必需的逻辑和数据
  2. 在不同的功能模块中构建部分逻辑和数据。比如,把Chat当作people对象的拥有者,Avatar是chat对象的拥有者。然后在模块之间互相调用,以便共享信息。
  3. 构建中央Model,合并逻辑和数据。

        第3种做法,目前为止最好的。

        Model不可以假定存在document对象这种浏览器特有的方法。方便单元测试和回归测试。

        Model不提供通用的工具方法。

        Model不直接和服务器通信,Data模块负责

5.2 创建Model和其他文件

           

      创建css/spa.avtr.css

     下载以下3个库到spa/js/jq:

     提供了统一的触摸和鼠标输入

     https://raw.githubusercontent.com/mmikowski/jquery.event.ue/master/jquery.event.ue.js

     使用全局自定义事件时需要这个文件

     https://raw.githubusercontent.com/mmikowski/jquery.event.gevent/master/jquery.event.gevent.js

     提供了客户端数据库,它不是jQuery插件。

     https://raw.githubusercontent.com/typicaljoe/taffydb/master/taffy.js

     创建js/spa.avtr.js js/spa.data.js js/spa.fake.js

     添加浏览器端工具js/spa.util_b.js,在Node.js中不能正常工作,而js/spa.util.js是可以的。

         用来编码和解码HTML中的特殊字符,比如&和<。

5.3 设计people对象

     

    需要识别4种类型的person:当前用户、匿名、正在和用户聊天的、其他在线的

    设计people对象的API:由方法和jQuery全局自定义事件组成。

  • get_user() --- 返回当前用户,如果未登入,则返回匿名person
  • get_db()  --- 获取所有person对象的集合
  • get_by_cid(<client_id>) --- 获取client_id的person对象
  • login(<user_name>) --- 以指定用户名的方法登入用户
  • logout() --- 退出。

    设计people的事件:Model更新数据将广播各个模块。   

    给people对象的API编写文档

5.4 构建people对象

    用Fake模块来向Model提供伪造数据。它是快速开发的关键所在,在完成目标之前,就一直使用伪造数据。

    创建伪造的人员列表:在Model中设置isFakeData标志。spa.fake.getPeopleList

    开始构建people对象:创建person对象的TaffyDB集合。spa.model.initModule()

    完成people对象的构建:login()  logout()  get_user()

    使用有jQuery的Node.js就能够运行这个测试集,无需使用浏览器,请查看附录B   

5.5 在Shell中开启登入和登出的功能

    设计用户登入的体验:    

  1. 如果用户未登入,提示信息为Please Sign-in。点击显示登入对话框。
  2. 当用户填完对话框表单
  3. 移除登入对话框,显示...processing...
  4. 一旦登入过程完成,显示登入用户名

    更新Shell的JavaScript:   熟悉jQuery全局自定义事件的发布-订阅特性。

    更新Shell的样式表

css/spa.shell.css

.spa-shell-head, .spa-shell-head-logo, .spa-shell-head-acct,
.spa-shell-head-search, .spa-shell-main, .spa-shell-main-nav, 
.spa-shell-main-content, .spa-shell-foot, 
.spa-shell-modal {
	position : absolute;
}
.spa-shell-head {
	top     : 0;
	left    : 0;
	right   : 0;
	height  : 40px;
}
.spa-shell-head-logo {
	top     : 4px;
	left    : 8px;
	right   : 32px;
	width   : 128px;
	background  : orange;
}
  .spa-shell-head-logo h1 {
  	font : 800 22px/22px Arial, Helvetica, sans-serif;
  	margin : 0;
  }
  .spa-shell-head-logo p {
  	font : 800 10px/10px Arial, Helvetica, sans-serif;
  	margin : 0;
  }

.spa-shell-head-acct {
	top     : 4px;
	right    : 0;
	height   : 32px;
	width   : 210px;
	line-height: 32px;
	background  : #888;
	color : #fff;
	text-align : center;
	cursor     : pointer;
	overflow   : hidden;
	text-overflow : ellipsis;
}
.spa-shell-head-search {
	top     : 4px;
	right    : 64px;
	height   : 32px;
	width   : 248px;
	background  : blue;
}
.spa-shell-main {
	top     : 40px;
	left    : 0;
	right   : 0;
	bottom  : 40px;
}
.spa-shell-main-content,
.spa-shell-main-nav {
	top     : 0px;
	bottom  : 0px;
}
.spa-shell-main-nav {
	width     : 400px;
	background  : #eee;
	z-index : 1;
}/*使用父类来影响子元素。这大概是CSS的一个最强大的功能,但几乎没被频繁的使用*/
  .spa-x-closed,
  .spa-shell-main-nav {
   	  width     : 0px;
  }
  
.spa-shell-main-content {
	left    : 400px;
	right   : 0;
	background  : #ddd;
}/*缩进派生选择器,紧跟在父选择器的下面*/
  .spa-x-closed,
  .spa-shell-main-content {
	  left     : 0px;
  }
.spa-shell-foot {
	height     : 40px;
	left    : 0;
	right   : 0;
	bottom  : 0;
}

.spa-shell-modal {
	margin-top : -200px;
    margin-left : -200px;
    top     : 50%;
    left :   50%;
	width   : 400px;
	height  : 400px;
	background : #fff;
	z-index  : 2;
	display : none;
}
js/spa.shell.js

/*jslint    browser : true, continue : true,
    devel : true, indent : 2, maxerr : 50,
    newcap : true, nomen : true, plusplus : true,
    regexp : true, sloppy : true, vars : true,
    white : true
*/
/*global $, spa */
spa.shell = (function () {
	'use strict';
	var configMap = {
		anchor_schema_map : { 
			chat : {opened : true, closed : true }
		},
		main_html : String() 
		  + '<div class="spa-shell-head">'
		    + '<div class="spa-shell-head-logo">'
		      + '<h1>SPA</h1>'
		      + '<p>javascript end to end</p>'
		    + '</div>'
		    + '<div class="spa-shell-head-acct"></div>'
		  + '</div>'
		  + '<div class="spa-shell-main">'
		    + '<div class="spa-shell-main-nav"></div>'
		    + '<div class="spa-shell-main-content"></div>'
		  + '</div>'
		  + '<div class="spa-shell-foot"></div>'
		  + '<div class="spa-shell-modal"></div>',
		resize_interval : 200
	},
	stateMap = { 
		anchor_map  : {},
		resize_idto : undefined
	},  /*在整个模块中共享的动态信息*/
	jqueryMap = { };
	var
	setJqueryMap,  initModule, 
	copyAnchorMap, changeAnchorPart, 
	onHashchange,  setChatAnchor, onResize,
	onTapAcct, onLogin, onLogout;
	// 将创建和操作页面元素的函数放在"DOM Methods"区块中
	copyAnchorMap = function () {
		return $.extend( true, {}, stateMap.anchor_map );
	};
	changeAnchorPart = function (arg_map) {
		var anchor_map_revise = copyAnchorMap();
		var bool_return = true;
		var key_name, key_name_dep;
		KEYVAL:
		for (key_name in arg_map) {
			if (arg_map.hasOwnProperty( key_name )) {
				if (key_name.indexOf('_') === 0) { continue KEYVAL; }
				anchor_map_revise[key_name] = arg_map[key_name];
				key_name_dep = '_' + key_name;
				if (arg_map[key_name_dep]) {
					anchor_map_revise[key_name_dep] = arg_map[key_name_de];
				} else {
					delete anchor_map_revise[key_name_dep];
					delete anchor_map_revise['_s' + key_name_dep];
				}
			}
		}
		//
		try {
			$.uriAnchor.setAnchor( anchor_map_revise );
		} catch (error) {
			$.uriAnchor.setAnchor( stateMap.anchor_map, null, true );
			bool_return = false;
		}
		return bool_return;
	};
	onHashchange = function (event) {
		var anchor_map_previous = copyAnchorMap();
		var anchor_map_proposed, _s_chat_previous;
		var _s_chat_proposed, s_chat_proposed;
		var is_ok = true;
		//
		try {
			anchor_map_proposed = $.uriAnchor.makeAnchorMap(); 
		} catch (error) {
			$.uriAnchor.setAnchor( anchor_map_previous, null, true );
			return false;
		}
		stateMap.anchor_map = anchor_map_proposed;
		//
		_s_chat_previous = anchor_map_previous._s_chat;
		_s_chat_proposed = anchor_map_proposed._s_chat;
		//
		if ( ! anchor_map_previous 
			|| _s_chat_previous !== _s_chat_proposed ) {
			s_chat_proposed = anchor_map_proposed.chat;
		    switch (s_chat_proposed) {
		    	case 'opened':
		    	  is_ok = spa.chat.setSliderPosition('opened');
		    	break;
		    	case 'closed':
		    	  is_ok = spa.chat.setSliderPosition('closed');
		    	break;
		    	default:
		    	is_ok = spa.chat.setSliderPosition('closed');
		    	delete anchor_map_proposed.char;
		    	$.uriAnchor.setAnchor( anchor_map_proposed, null, true );
		    }
		}
		if (!is_ok) {
			if (anchor_map_previous) {
				$.uriAnchor.setAnchor( anchor_map_previous, null, true );
				stateMap.anchor_map = anchor_map_previous;
			} else {
				delete anchor_map_proposed.chat;
				$.uriAnchor.setAnchor( anchor_map_proposed, null, true );
			}
		}
		return false;
	};
	// Event handler
	onResize = function () {
		if (stateMap.resize_idto) { return true; }
		spa.chat.handleResize();
		stateMap.resize_idto = setTimeout(
			function () {
				stateMap.resize_idto = undefined;
			}, configMap.resize_interval 
		);
		return true;
	};

	// Begin DOM Methods
	setJqueryMap = function () {    
	    //缓存jQuery集合,几乎我们编写的每个Shell和功能模块都应该有这个函数
	    //可以大大地减少jQuery对文档的遍历次数,能够提高性能。
		var $container = stateMap.$container;
		jqueryMap = { 
			$container : $container,
			$acct      : $container.find('.spa-shell-head-acct'),
			$nav       : $container.find('.spa-shell-main-nav')
		};
	};

    //点击账户元素
    onTapAcct = function ( event ) {
    	var acct_text, user_name, user = spa.model.people.get_user();
    	if (user.get_is_anon() ) {
    		user_name = prompt( 'Please sign-in' );
    		spa.model.people.login( user_name );
    		jqueryMap.$acct.text( '... processing ...');
    	} else {
    		spa.model.people.logout();
    	}
    	return false;
    };
    //更新用户区
    onLogin = function ( event, login_user ) {
    	jqueryMap.$acct.text( login_user.name );
    };
    //
    onLogout = function ( event, logout_user ) {
    	jqueryMap.$acct.text( 'Please sign-in' );
    };

    setChatAnchor = function (position_type) {
    	return changeAnchorPart(
    		{ chat : position_type }
    	);
    }
	initModule = function ( $container ) {
		stateMap.$container  = $container;
		$container.html( configMap.main_html );
		setJqueryMap();

		$.uriAnchor.configModule( {
			schema_map : configMap.anchor_schema_map
		});
		// configure and initialize feature modules
		spa.chat.configModule( {
			chat_model : spa.model.chat,
			set_chat_anchor : setChatAnchor,
			people_model : spa.model.people
		} );
		spa.chat.initModule( jqueryMap.$container );
		//
		$.gevent.subscribe( $container, 'spa-login', onLogin );
		$.gevent.subscribe( $container, 'spa-logout', onLogout );
		jqueryMap.$acct 
		   .text( 'Please sign-in' )
		   .bind( 'utap', onTapAcct );
		//
		$(window) 
		   .bind( 'hashchange', onHashchange )
		   .trigger( 'hashchange' );
	};
	return { initModule : initModule };
} ());
js/spa.model.js

/*jslint    browser : true, continue : true,
    devel : true, indent : 2, maxerr : 50,
    newcap : true, nomen : true, plusplus : true,
    regexp : true, sloppy : true, vars : true,
    white : true
*/
/*global TAFFY $, spa */
spa.model = (function () {
	'use strict';
	var configMap = {
		anon_id : 'a0'
	};
	var stateMap = {
		anon_user      : null,
		cid_serial     : 0,
		people_cid_map : {},
		people_db      : TAFFY(),
		user           : null
	};
	var isFakeData = true;
	var personProto, makePerson, people, initModule;
	var makeCid, clearPeopleDb, completeLogin, removePerson;
	personProto = {
		get_is_user : function () {
			return this.cid === stateMap.user.cid;
		},
		get_is_anon : function () {
			return this.cid === stateMap.anon_user.cid;
		}
	};
	makeCid = function () {
		return 'c' + String( stateMap.cid_serial++ );
	};
	clearPeopleDb = function () {
		var user = stateMap.user;
		stateMap.people_db = TAFFY();
		stateMap.people_cid_map = {};
		if (user) {
			stateMap.people_db.insert( user );
			stateMap.people_cid_map[ user.cid ] = user;
		}
	};
	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;
		// When we add chat, we should join here
		$.gevent.publish( 'spa-login', [stateMap.user] );
	};
	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; }
		//加入Taffy数据库
		stateMap.people_cid_map[cid] = person;
		stateMap.people_db.insert( person );
		return person;
	};
	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;
	};

	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; };
		login = function ( name ) {
			// 模拟SocketIO
			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
			});
			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;
			$.gevent.publish('spa-logout', [user]);
			return is_removed;
		};
		//导出people对象的所有公开方法
		return {
			get_by_cid    : get_by_cid,
			get_db        : get_db,
			get_user      : get_user,
			login         : login,
			logout        : logout
		};
	}());

	initModule = function () {
		var i, 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; i<people_list.length; i++) {
				person_map = people_list[i];
				makePerson( {
					cid     : person_map._id,
					css_map : person_map.css_map,
					id      : person_map.id,
					name    : person_map.name
				});
			}
		}
	};
	return {
		initModule : initModule,
		people     : people
	};
}());
js/spa.fake.js

/*jslint    browser : true, continue : true,
    devel : true, indent : 2, maxerr : 50,
    newcap : true, nomen : true, plusplus : true,
    regexp : true, sloppy : true, vars : true,
    white : true
*/
/*global $, spa */
spa.fake = (function () {
	'use strict';
	var getPeopleList, fakeIdSerial, makeFakeId, mockSio;
	fakeIdSerial = 5;
	//模拟服务端IO字符串
	makeFakeId = function () {
		return 'id_' + String( fakeIdSerial++ );
	};
	getPeopleList = function () {
	   return [
	   	 { name : 'Berry', _id : 'id_01', 
	   	   css_map : { top:20, left: 20,
	   	   	 'background-color' : 'rgb(128, 128, 128)'
	   	   }
	   	 },
	   	 { name : 'Mike', _id : 'id_02', 
	   	   css_map : { top:60, left: 20,
	   	   	 'background-color' : 'rgb(128, 255, 128)'
	   	   }
	   	 },
	   	 { name : 'Pebbles', _id : 'id_03', 
	   	   css_map : { top:100, left: 20,
	   	   	 'background-color' : 'rgb(128, 192, 192)'
	   	   }
	   	 },
	   	 { name : 'Wilma', _id : 'id_04', 
	   	   css_map : { top:140, left: 20,
	   	   	 'background-color' : 'rgb(192, 128, 128)'
	   	   }
	   	 }
	   ]; 
    };
    //在mockSio闭包里面创建on_sio方法
    mockSio = (function () {
    	var on_sio, emit_sio, callback_map = {};
    	on_sio = function ( msg_type, callback ) {
    		callback_map[ msg_type ] = callback;
    	};
    	emit_sio = function ( msg_type, data ) {
    		// adduser
    		if ( msg_type === 'adduser' && callback_map.userupdate ) {
    			setTimeout( function () {
    				callback_map.userupdate(
    					[{
    						_id     : makeFakeId(),
    						name    : data.name,
    						css_map : data.css_map
    					}]
    				);
    			}, 3000);
    		}
    	};
    	return { emit : emit_sio, on : on_sio };
    }());
    return { 
    	getPeopleList : getPeopleList,
    	mockSio       : mockSio
     };
}());

js/spa.util_b.js

/*jslint    browser : true, continue : true,
    devel : true, indent : 2, maxerr : 50,
    newcap : true, nomen : true, plusplus : true,
    regexp : true, sloppy : true, vars : true,
    white : true
*/
/*global $, spa, getComputedStyle */
spa.util_b = (function () {
   'use strict';
   var configMap = {
   	regex_encode_html   : /[&"'><]/g,
   	regex_encode_noamp  : /["'><]/g,
   	html_encode_map     : {
   		'&'  : '&',
   		'"'  : '"',
   		"'"  : ''',
   		'>'  : '>',
   		'<'  : '<'
   	}
   };
   var decodeHtml, encodeHtml, getEmSize;
   configMap.encode_noamp_map = $.extend( {}, configMap.html_encode_map );
   delete configMap.encode_noamp_map['&']; //

   //把浏览器实体(如&)转换成显示字符(如&)。
   decodeHtml = function (str) {
   	return $('<div/>').html(str || '').text();
   };
   //把特殊字符(如&)转换成HTML编码值(如&).
   encodeHtml = function ( intput_arg_str, exclude_amp ) {
   	var input_str = String( input_arg_str ),
   	regex, lookup_map;
   	if (exclude_amp) {
   		lookup_map = configMap.encode_noamp_map;
   		regex = configMap.regex_encode_noamp;
   	} else {
   		lookup_map = configMap.html_encode_map;
   		regex = configMap.regex_encode_html;
   	}
   	return input_str.replace(regex, function (match, name) {
   		return lookup_map[ match ] || '';
   	});
   };
   //计算em单位的像素大小
   getEmSize = function (elem) {
   	  return Number( getComputedStyle( elem, '').fontSize.match(/\d*\.?\d*/)[0]);
   };
   //导出所有的公开方法
   return {
      decodeHtml : decodeHtml,
      encodeHtml : encodeHtml,
      getEmSize  : getEmSize
   };
}());
spa.html

<!doctype html>
<html>
<head>
  <!-- ie9+ rendering support for latest standards -->
  <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <title>SPA Chapter 5-6</title>
  <!-- third-party stylesheets -->

  <!-- our stylesheets -->
  <link rel="stylesheet" href="css/spa.css" type="text/css" />
  <link rel="stylesheet" href="css/spa.chat.css" type="text/css" />
  <link rel="stylesheet" href="css/spa.shell.css" type="text/css" />
  <link rel="stylesheet" href="css/spa.avtr.css" type="text/css" />

  <!-- third-party javascript -->
  <script src="js/jq/jquery-3.2.1.js" ></script>
  <script src="js/jq/jquery.uriAnchor.js" ></script>
   <script src="js/jq/jquery.event.gevent.js" ></script>
    <script src="js/jq/jquery.event.ue.js" ></script>
     <script src="js/jq/taffy.js" ></script>

  <!-- our javascript -->
  <script src="js/spa.js" ></script>
  <script src="js/spa.util.js" ></script>
  <script src="js/spa.model.js" ></script>
  <script src="js/spa.data.js" ></script>
  <script src="js/spa.fake.js" ></script>
  <script src="js/spa.util_b.js" ></script>
  <script src="js/spa.shell.js" ></script>
  <script src="js/spa.chat.js" ></script>
  <script src="js/spa.avtr.js" ></script>
  <script>
    $(function () { 
    	spa.initModule( $('#spa') );
    });
  </script>
</head>
<body>
  <div id="spa"></div>
</body>
</html>

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值