功能模块向单页应用提供了精心定义和有作用域限制的功能。除了聊天滑块之外,还有其他功能模块的例子,包括图片查看器、账户管理面板或者是用户集中放置图形对象的工作台。
和第三方模块的做法很像:精心定义的API和强隔离性。可在多个项目之间很容易地重用模块。
4.1 功能模块策略
模块有自己的视图、控制器和Shell在它们之间共享的部分模型。
功能模块的例子包括在工作台上处理草图的spa.wb.js、管理账户功能的spa.acct.js(像登入或登出)和用于聊天界面的spa.chat.js。
与第三方模块的比较:参考《Third-Party JavaScript》
包括博客评论姓(DisQus或者LiveFyre)、广告型的(DoubleClick或者ValueClick)、分析型的(Google或者Overture)、分享型的(AddThis或ShareThis)和社交服务型的(赞)。它们都非常流行,因为网站管理员可以把这些高质量的功能添加到他们的网站里面,和自己来开发这些功能相比。
精心编写的第三方模块具有以下共同特征:
- 在自己的容器内渲染
- 提供了精心定义的API,以便控制它们的行为
- 通过将自己的JavaScript、数据和CSS精心地隔离,避免污染主页面
它的缺点也有多。
我们的功能模块没有使用第三方模块,向Shell提供一致的配置、初始化和调用的API。通过使用唯一的和协调的JavaScript和CSS名字空间,功能之间相互隔离,除了共享的工具方法外,不允许任何外部调用。
像第三方模块一样来开发自己的模块,还有一个巨大的优势:我们处于一种有利的情况,Web应用的非核心功能使用第三方模块,然后在时间和资源允许时,有选择性地使用自己的功能模块来替换它们,这样就能更好地集成、运行更快、侵入性更小,或者是以上全部的好处。
功能模块和分型MVC模式:分形是一种模式,它在所有层级上显示为自相似性。我们的单页应用架构在多个层级上采用重复的MVC模式,所以我们把它叫做”分形模型-视图-控制器“,或者是FMVC。
应用被分割为两部分:服务器采用MVC模式向客户端提供数据;采用MVC的单页应用允许用户查看浏览器的模型,并与之交互。服务器的模型是从数据库获取的数据,而视图是要发送给浏览器的数据表现,控制器是协调数据管理和同浏览器通信的代码。在客户端,模型包括从服务器接收到的数据,视图是用户界面,控制器是协调客户端数据和界面的逻辑。
几乎所有的现代网站都适用这种模式,即便开发人员没有意识到这一点。比如,一旦开发人员把DisQus或者LiveFyre的评论模块添加到他们的博客中,他们就添加了另外一个MVC模式。
4.2 创建功能模块文件
规划文件结构:
- 为Chat模块创建一个有名字空间的样式表
- 为Chat模块创建一个有名字空间的JavaScript模块js/spa.chat.js,js/spa.model.js
- 为浏览器端的模型创建一个桩文件(stub)css/spa.chat.css
- 创建一个提供通用程序的共用模块,供其他所有模块使用js/spa.util.js。
- 修改浏览文档,引入新的文件。
- 删除用来开发布局的文件。
桩文件:css/spa.chat.css,桩是一个故意没有完成的或者是占位用的资源
文档加载约定:根-》核心工具方法-》Model-》浏览器端工具方法-》Shell-》功能模块
为什么自己的库要放在最后加载,因为防止第三方库声明名字空间spa.model。
4.3 设计方法API
功能模块之间的相互调用是不允许的。功能模块的唯一数据源或者功能只能来自Shell,在配置和初始化期间以参数的形式传给模块的公开方法。
锚接口模式:
Chat的配置API:JS中所有复杂数据类型(对象、数组和函数)传递的是引用。
- 一个提供”修改URI锚中的chat参数"的功能的函数
- 一个提供“发送和接收消息(来自model)”的方法的对象
- 一个提供“与一系列用户(来自Model)交互”的方法的对象。
- 许多行为设置,比如滑块打开时的高度,滑块的打开时间以及滑块的关闭时间。
js/spa.chat.js中API规范, configModele,
配置和初始化的级联:所有的模块都有公开的initModule方法。只在需要支持设置时,才会提供configModule方法。
4.4 实现功能API
样式表:css/spa.chat.css
修改Chat:API实现。js/spa.chat.js
4.5 添加经常使用的方法
重置方法(removeSlider)和窗口尺寸变化的方法(handleResize)
如果用户登出的时候,彻底移除聊天滑块。需要删除Chat添加的DOM容器,依次释放初始化和配置信息。
窗口有些情况下不能工作,需要一些计算。
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 4</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" />
<!-- third-party javascript -->
<script src="js/jq/jquery-3.2.1.js" ></script>
<script src="js/jq/jquery.uriAnchor.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.shell.js" ></script>
<script src="js/spa.chat.js" ></script>
<script>
$(function () {
spa.initModule( $('#spa') );
});
</script>
</head>
<body>
<div id="spa"></div>
</body>
</html>
css/spa.css
/** 重置大多数选择器,我们不信任浏览器的默认行为 */
* {
margin: 0;
padding: 0;
-webkit-box-sizing : border-box;
-moz-box-sizing : border-box;
box-sizing : border-box;
}
h1, h2, h3, h4, h5, h6, p { margin-bottom: 6pt ; }
o1, ul, dl { list-style-position : inside ; }
/** 希望确保跨平台应用有一致的外观。 */
body {
font : 13px 'Trebuchet MS', Verdana, Helvetica, Arial, sans-serif;
color : # 444;
background-color: #888;
}
a {
text-decoration: none;
}
a:link, a:visited { color : inherit; }
a:hover { text-decoration : underline; }
strong {
font-weight: 800;
color : #000;
}
/** 通常使用根名字作为元素选择器,定义选择器的名字空间*/
#spa {
position : absolute;
top : 0;
left : 0;
bottom : 0;
right : 0;
min-height: 15em;
min-width: 35em;
overflow : hidden;
background : #fff;
}
/** 其他模块,以spa-x-作为前缀*/
.spa-x-select {}
.spa-x-clearfloat {
height : 0 !important;
float : none !important;
visibility : hidden !important;
clear : both !important;
}
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 : 4px;
right : 32px;
width : 128px;
background : orange;
}
.spa-shell-head-acct {
top : 4px;
right : 0;
height : 32px;
width : 64px;
background : green;
}
.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 : 250px;
background : #eee;
}/*使用父类来影响子元素。这大概是CSS的一个最强大的功能,但几乎没被频繁的使用*/
.spa-x-closed,
.spa-shell-main-nav {
width : 0px;
}
.spa-shell-main-content {
left : 250px;
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;
}
css/spa.chat.css
/* Chat feature styles
*/
.spa-chat {
position: absolute;
bottom: 0;
right: 0;
width: 25em;
height: 2em;
background: #fff;
border-radius: 0.5em 0 0 0;
border-style: solid;
border-width: thin 0 0 thin;
border-color: #888;
box-shadow: 0 0 0.75em 0 #888;
z-index : 1 ;
}
.spa-chat-head, spa-chat-closer {
position: absolute;
top : 0;
height: 2em;
line-height: 1.8em;
border-bottom: thin solid #888;
cursor : pointer;
background : #888;
color: white;
font-family: arial, helvetica, sans-serif;
font-weight: 800;
text-align: center;
}
.spa-chat-head {
left: 0;
right: 2em;
border-radius: 0.3em 0 0 0;
}
.spa-chat-closer {
right: 0;
width: 2em;
}
.spa-chat-closer:hover {
background: #800;
}
.spa-chat-head-toggle {
position: absolute;
top: 0;
left: 0;
width: 2em;
bottom: 0;
border-radius: 0.3em 0 0 0;
}
.spa-chat-head-title {
position: absolute;
left: 50%;
width: 16em;
margin-left: -8em;
}
.spa-chat-sizer {
position: absolute;
top: 2em;
left: 0;
right: 0;
}
.spa-chat-msgs {
position: absolute;
top: 1em;
left: 1em;
right: 1em;
bottom: 4em;
padding: 0.5em;
border : thin solid #888;
overflow-x: hidden;
overflow-y: scroll;
}
.spa-chat-box {
position: absolute;
height: 2em;
left: 1em;
right: 1em;
bottom: 1em;
border : thin solid #888;
background: #888;
}
.spa-chat-box input[type=text] {
float: left;
width: 75%;
height: 100%;
padding: 0.5em;
border : 0;
background: #ddd;
color: #404040;
}
.spa-chat-box input[type=text]:focus {
background: #fff;
}
.spa-chat-box div {
float: left;
width: 25%;
height: 2em;
line-height: 1.9em;
text-align: center;
color: #fff;
font-weight: 800;
cursor: pointer;
}
.spa-chat-box div:hover {
background-color: #444;
color: #ff0;
}
.spa-chat-head:hover .spa-chat-head-toggle {
background: #aaa;
}
js/spa.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
*/
var spa = (function ( ) {
// 初始化
var initModule = function ( $container ) {
spa.shell.initModule( $container );
};
return { initModule : initModule }; //返回spa名字空间中的对象,只导出了initModele方法
}());
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
*/
spa.shell = (function () {
var configMap = {
anchor_schema_map : {
chat : {opened : true, closed : true }
},
main_html : String()
+ '<div class="spa-shell-head">'
+ '<div class="spa-shell-head-logo"></div>'
+ '<div class="spa-shell-head-acct"></div>'
+ '<div class="spa-shell-head-search"></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 = { },
setJqueryMap, initModule,
copyAnchorMap, changeAnchorPart,
onHashchange, setChatAnchor, onResize;
// 将创建和操作页面元素的函数放在"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
};
};
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 );
//
$(window)
.bind( 'hashchange', onHashchange )
.trigger( 'hashchange' );
};
return { initModule : initModule };
} ());
js/spa.util.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
*/
spa.util = (function () {
var makeError, setConfigMap;
makeError = function ( name_text, msg_text, data ) {
var error = new Error();
error.name = name_text;
error.message = msg_text;
if (data) {
error.data = data;
}
return error;
};
setConfigMap = function (arg_map) {
var input_map = arg_map.input_map;
var settable_map = arg_map.settable_map;
var config_map = arg_map.config_map;
var key_name, error;
for ( key_name in input_map ) {
if (input_map.hasOwnProperty( key_name ) ) {
if (settable_map.hasOwnProperty( key_name ) ) {
config_map[key_name] = input_map[key_name];
} else {
error = makeError( 'Bad Input', 'Setting config key |' + key_name + '| is not supported');
throw error;
}
}
}
};
return {
makeError : makeError,
setConfigMap : setConfigMap
};
}());
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
*/
spa.model = (function () {
return {};
}());
js/spa.chat.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
*/
/*spa.chat名字空间*/
spa.chat = (function () {
var configMap = {
main_html : String()
+ '<div class="spa-chat">'
+ '<div class="spa-chat-head">'
+ '<div class="spa-chat-head-toggle"> + </div>'
+ '<div class="spa-chat-head-title">'
+ 'Chat'
+ '</div>'
+ '</div>'
+ '<div class="spa-chat-closer">x</div>'
+ '<div class="spa-chat-sizer">'
+ '<div class="spa-chat-msgs"></div>'
+ '<div class="spa-chat-box">'
+ '<input type="text"/>'
+ '<div>send</div'
+ '</div>'
+ '</div>'
+ '</div>',
settable_map : {
slider_open_time : true,
slider_close_time : true,
slider_opened_em : true,
slider_closed_em : true,
slider_opened_title : true,
slider_closed_title : true,
chat_model : true,
people_model : true,
set_chat_anchor :true
},
slider_open_time : 250,
slider_close_time : 250,
slider_opened_em : 18,
slider_closed_em : 2,
slider_opened_title : 'Click to close',
slider_closed_title : 'Click to open',
slider_opened_min_em : 10, //最小高度
window_height_min_em : 20,
chat_model : null,
people_model : null,
set_chat_anchor : null
},
stateMap = {
$append_target : null,
position_type : 'closed',
px_per_em : 0,
slider_hidden_px : 0,
slider_closed_px : 0,
slider_opened_px : 0
},
jqueryMap = {
},
setJqueryMap, configModule, initModule,
getEmSize, setPxSizes, setSliderPosition,
onClickToggle, removeSlider, handleResize;
//Begin utility methods
getEmSize = function ( elem ) {
return Number(
getComputedStyle( elem, '' ).fontSize.match(/\d*\.?\d*/)[0]
);
};
//Begin DOM method
setJqueryMap = function() {
var $append_target = stateMap.$append_target;
var $slider = $append_target.find('.spa-chat');
jqueryMap = {
$slider : $slider,
$head : $slider.find('.spa-chat-head'),
$toggle : $slider.find('.spa-chat-head-toggle'),
$title : $slider.find('.spa-chat-head-title'),
$sizer : $slider.find('.spa-chat-sizer'),
$msgs : $slider.find('.spa-chat-msgs'),
$box : $slider.find('.spa-chat-box'),
$input : $slider.find('.spa-chat-input input[type-text]')
};
};
setPxSizes = function () {
var px_per_em, opened_height_em, window_height_em;
px_per_em = getEmSize( jqueryMap.$slider.get(0) );
window_height_em = Math.floor( ( $(window).height() / px_per_em ) + 0.5 );
opened_height_em = window_height_em > configMap.window_height_min_em
? configMap.slider_opened_em
: configMap.slider_opened_min_em;
//opened_height_em = configMap.slider_opened_em;
stateMap.px_per_em = px_per_em;
stateMap.slider_closed_px = configMap.slider_closed_em * px_per_em;
stateMap.slider_opened_px = opened_height_em * px_per_em;
jqueryMap.$sizer.css ( {
height : (opened_height_em - 2) * px_per_em
});
};
handleResize = function () {
if (!jqueryMap.$slider) { return false; }
setPxSizes();
if (stateMap.position_type === 'opened') {
jqueryMap.$slider.css({
height : stateMap.slider_opened_px
});
}
return true;
};
setSliderPosition = function (position_type, callback) {
var height_px, animate_time, slider_title, toggle_text;
if (stateMap.position_type === position_type) {
return true;
}
switch (position_type) {
case 'opened':
height_px = stateMap.slider_opened_px;
animate_time = configMap.slider_open_time;
slider_title = configMap.slider_opened_title;
toggle_text = '=';
break;
case 'hidden':
height_px = 0;
animate_time = configMap.slider_open_time;
slider_title = '';
toggle_text = '+';
break;
case 'closed':
height_px = stateMap.slider_closed_px;
animate_time = configMap.slider_close_time;
slider_title = configMap.slider_closed_title;
toggle_text = '+';
break;
default : return false;
}
// animate slider position change
stateMap.positiong_type = '';
jqueryMap.$slider.animate(
{ height : height_px },
animate_time,
function () {
jqueryMap.$toggle.prop('title', slider_title);
jqueryMap.$toggle.text( toggle_text );
stateMap.position_type = position_type;
if (callback) { callback( jqueryMap.$slider ); }
}
);
return true;
};
//Begin event handlers
onClickToggle = function ( event ) {
var set_chat_anchor = configMap.set_chat_anchor;
if (stateMap.position_type === 'opened') {
set_chat_anchor('closed');
} else if (stateMap.position_type === 'closed') {
set_chat_anchor('opened');
}
return false;
};
//Begin public methods
configModule = function ( input_map ) {
spa.util.setConfigMap({
input_map : input_map,
settable_map : configMap.settable_map,
config_map : configMap
});
return true;
};
//
removeSlider = function () {
if (jqueryMap.$slider) {
jqueryMap.$slider.remove();
jqueryMap = {};
}
stateMap.$append_target = null;
stateMap.position_type = 'closed';
// unwind key configurations
configMap.chat_model = null;
configMap.people_model = null;
configMap.set_chat_anchor = null;
return true;
};
initModule = function ( $append_target ) {
$append_target.append( configMap.main_html );
stateMap.$append_target = $append_target;
setJqueryMap();
setPxSizes();
jqueryMap.$toggle.prop('title', configMap.slider_closed_title);
jqueryMap.$head.click( onClickToggle );
stateMap.position_type = 'closed';
return true;
};
//这两个方法几乎是所有功能模块的标配方法
return {
setSliderPosition : setSliderPosition,
configModule : configModule,
initModule : initModule,
removeSlider : removeSlider,
handleResize : handleResize
};
}());