單頁Web應用 4 新增功能模組
功能模組向單頁應用提供了精心定義和有作用域限制的功能。除了聊天滑塊之外,還有其他功能模組的例子,包括圖片檢視器、賬戶管理面板或者是使用者集中放置圖形物件的工作臺。
和第三方模組的做法很像:精心定義的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
};
}());