单页Web应用 3 开发Shell

    单页应用客户端关注的是构建桌面应用,而不是传统网站。

    shell是架构的必需组件。先开发包含功能容器的页面布局,然后让Shell来渲染它们。

3.1 深刻理解Shell

    Shell是单页应用的主控制器。只是架构的一部分,是我们从很多商业项目中提炼出来的。

    首先编写Shell模块是有好处的,因为它是架构的中枢。它是功能模块和业务逻辑以及通用浏览器接口(像URI或cookie)之间的协调者。当用户点击了后退按钮、登录或做了其他事情而改变了应用状态,这些状态可以使用书签来标记,Shell会协调这些改变。


   Shell负责以下事情:

  • 渲染和管理功能容器
  • 管理应用状态
  • 协调功能模块

3.2 创建文件和名字空间

       每个JavaScript名字空间都会有一个对应的JavaScript文件,并且使用自执行匿名函数,以免污染全局名字空间。我们也会建立平行的CSS文件结构。这种约定能加快开发、提升质量和简化维护。随着项目中添加更多的模块和开发人员,它的价值就会增加。

       创建文件结构


     安装jQuery和插件:http://jquery.com/download/   

            jquery-3.2.1.js

            jQuery的uriAnchor插件提供了管理URI的锚组件的工具方法。https://github.com/mmikowski/urianchor

     编写应用的HTML文件:spa.html,为什么不把脚本文件放到body容器的最后面呢?,这样静态的HTML和CSS在JavaScript加载完成前就能显示,通常会使页面渲染更快。然而,单页应用不是这样工作的。它们使用JavaScript来生成HTML,因此将脚本放置在头部之外,并不能更快地渲染页面。相反,我们把所有的外部脚本放在head区块中,以便改进组织结构和易读性。

<!doctype html>
<html>
<head>
  <title>SPA Starter</title>
  <!-- stylesheets -->
  <link rel="stylesheet" href="css/spa.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.shell.js" ></script>
  <script>
    $(function () { 
    	spa.initModule( $('#spa') );
    });
  </script>
</head>
<body>
  <div id="spa"></div>
</body>
</html>
    创建CSS根名字空间: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: 10px ; }
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         : 8px;
	left        : 8px;
	bottom      : 8px;
	right       : 8px;

	min-height: 500px;
	min-width: 500px;
	overflow : hidden;

	background-color : #fff;
	border-radius    : 0 8px 0 8px;
}

/** 其他模块,以spa-x-作为前缀*/
.spa-x-select {}
.spa-x-clearfloat {
	height      : 0       !important;
	float       : none    !important;
	visibility  : hidden  !important;
	clear       : both    !important;
}

    创建JavaScript根名字空间:js/spa.js 希望添加一个初始化应用的方法,确保代码通过JSLint的验证。

/*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 ) {
		$container.html( 
			'<h1 style="display:inline-block; margin:25px;">'
			+ 'hello world!'
			+ '</h1>'
		);
	};
	return { initModule : initModule };  //返回spa名字空间中的对象,只导出了initModele方法
}());

    安装JSLint: npm install -g jslint

    jslint js/spa.js

    打开spa.html

3.3 创建功能容器

      Shell创建并管理着功能模块要用到的容器。


      选取策略:单独在layout.html中开发功能容器的HTML和CSS。再把代码移至Shell的CSS和JavaScript文件。这种做法通常是最快速的,是开发初始布局的最高效方法,因为不用担心与大多数其他代码交互,就能进行工作。先编写HTML,然后添加样式。

      编写Shell的HTML:layout.html

<!doctype html>
<html>
<head>
  <title>HTML Layout</title>
  <link rel="stylesheet" href="css/spa.css" type="text/css" />
  <style>
    .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-chat,
    .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;
    }
    .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-chat {
    	bottom  : 40px;
    	right   : 0;
    	width   : 300px;
    	height  : 15px;
    	background : red;
    	z-index  : 1;
    }
    .spa-shell-modal {
    	margin-top : -200px;
        margin-left : -200px;
        top     : 50%;
        left :   50%;
    	width   : 400px;
    	height  : 400px;
    	background : #fff;
    	z-index  : 2;
    }
  </style>
</head>
<body>
  <div id="spa">
    <div class="spa-shell-head">
      <div class="spa-shell-head-logo">logo</div>
      <div class="spa-shell-head-acct">acct</div>
      <div class="spa-shell-head-search">search</div>
    </div>
    <div class="spa-shell-main">
      <div class="spa-shell-main-nav">nav</div>
      <div class="spa-shell-main-content">content</div>      
    </div>
    <div class="spa-shell-foot">foot</div>
    <div class="spa-shell-chat">chat</div>
    <div class="spa-shell-modal">modal</div>
  </div>
</body>
</html>

      使用Tidy工具来验证以下HTML,确保没有错误。http://http://infohound.net/tidy  在线版

      编写Shell的CSS:编写支持流式布局(布局的流动性)的CSS,除了一些最极端的尺寸,内容的宽度和高度会完全自适应填充浏览器窗口。避免使用任何边框,因为它们会更改CSS盒子的尺寸。这在快速原型开发过程中会引入不必要的麻烦。一旦对容器的展示满意了,必要是可以再回过头来添加边框。

3.4 渲染功能容器

     让Shell来渲染容器,而不是使用静态的HTML和CSS。

     将HTML转换为JavaScript

     在JavaScript中添加HTML模板: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 = {
		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-chat"></div>'
		  + '<div class="spa-shell-modal"></div>'
	},
	stateMap = { $container  : null },  /*在整个模块中共享的动态信息*/
	jqueryMap = { },
	setJqueryMap, initModule;
	// 将创建和操作页面元素的函数放在"DOM Methods"区块中

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

     编写Shell的样式表:css/spa.shell.css的文件中使用spa-shell-*选择器。

.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-chat,
.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-chat {
	bottom  : 40px;
	right   : 0;
	width   : 300px;
	height  : 15px;
	background : red;
	z-index  : 1;
}
.spa-shell-modal {
	margin-top : -200px;
    margin-left : -200px;
    top     : 50%;
    left :   50%;
	width   : 400px;
	height  : 400px;
	background : #fff;
	z-index  : 2;
}

     指示应用使用Shell:js/spa.js,以便使用Shell,

var spa = (function ( ) {
	// 初始化
	var initModule = function ( $container ) {
		spa.shell.initModule( $container );
	};
	return { initModule : initModule };  //返回spa名字空间中的对象,只导出了initModele方法
}());
     打开spa.html
3.5 管理功能容器

    编写展开或收起聊天滑块的方法:需求如下

  1. 开发人员能够配置滑块运动的速度和高度。
  2. 创建单个方法来展开或者收起聊天滑块
  3. 避免出现竞争条件,即滑块可能同时在展开和收起。
  4. 开发人员能够传入一个可选的回调函数,会在滑块运动结束时调用。
  5. 创建测试代码,以便确保滑块功能正常。

        修改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 = {
		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-chat"></div>'
		  + '<div class="spa-shell-modal"></div>',
		chat_extend_time    : 1000,
		chat_retract_time   : 300,
		chat_extend_height  : 450,
		chat_retract_height : 15
	},
	stateMap = { $container  : null },  /*在整个模块中共享的动态信息*/
	jqueryMap = { },
	setJqueryMap, initModule, toggleChat;
	// 将创建和操作页面元素的函数放在"DOM Methods"区块中

	// Begin DOM Methods
	setJqueryMap = function () {    
	    //缓存jQuery集合,几乎我们编写的每个Shell和功能模块都应该有这个函数
	    //可以大大地减少jQuery对文档的遍历次数,能够提高性能。
		var $container = stateMap.$container;
		jqueryMap = { 
			$container : $container,
			$chat : $container.find('.spa-shell-chat')
		};
	};
	toggleChat = function ( do_extend, callback ) {
		var px_chat_ht = jqueryMap.$chat.height();
		var is_open = px_chat_ht === configMap.chat_extend_height;
		var is_closed = px_chat_ht === configMap.chat_retract_height;
		var is_sliding = ! is_open && !is_closed;
		// 避免出现竞争条件,即同时在展开和收起,如果已经在运动中,则拒绝执行
		if (is_sliding) { return false; }
		// Begin extend chat slider
		if (do_extend) {
			jqueryMap.$chat.animate (
				{ height : configMap.chat_extend_height },
				configMap.chat_extend_time,
				function () {
					if (callback) { callback( jqueryMap.$chat ); }
				}
			);
			return true;
		}
		// Begin retract chat slider
		jqueryMap.$chat.animate (
	        { height : configMap.chat_retract_height },
			configMap.chat_retract_time,
			function () {
				if (callback) { callback( jqueryMap.$chat ); }
			}
		);
		return true;
	};
	initModule = function ( $container ) {
		stateMap.$container  = $container;
		$container.html( configMap.main_html );
		setJqueryMap();
		// test toggle
		setTimeout( function () { toggleChat( true ); }, 3000 );
		setTimeout( function () { toggleChat( false ); }, 8000 );
	};
	return { initModule : initModule };
} ());
     打开spa.html

     给聊天滑块添加点击事件处理程序:需求如下

  1. 设置提示信息文字来提示用户操作,比如“Click to retract”
  2. 添加点击事件处理程序来调用toggleChat
  3. 点击事件处理程序绑定到jQuery事件上。

3.6 管理应用状态

     浏览器控件        桌面控件

      回退按钮              撤销

      前进按钮              重做

      标签                     另存为

      查看历史              撤销历史

    选取一个策略来管理历史控件:P80

  1. Susan访问了我们的单页应用,点击聊天滑块来打开它。
  2. 她将单页应用添加为书签,然后浏览了其他网站
  3. 之后,她决定回到我们的应用,于是点击了她的书签。

     URI的锚组件http://localhost/apa.html#!chat=open

     回到顶部的链接。跳转,可书签化

     锚组件的一个独特功能是,在它改变的时候,浏览器不会重新加载页面。锚组件是只给客户端使用的控件,它是保存应用状态的理想地方。

     使用uriAnchor插件

	onClickChat = function (event) {
		if ( toggleChat( stateMap.is_chat_retracted )) {
			$.uriAnchor.setAnchor( {
				chat : (stateMap.is_chat_retracted ? 'open' : 'closed' )
			});
		}
		return false;
	};
     点击滑块时,URI会改变
     使用锚来驱动应用状态:

/*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 : {open : 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-chat"></div>'
		  + '<div class="spa-shell-modal"></div>',
		chat_extend_time    : 1000,
		chat_retract_time   : 300,
		chat_extend_height  : 450,
		chat_retract_height : 15,
		chat_extended_title : 'Click to retract',
		chat_retract_title  : 'Click to extend'
	},
	stateMap = { 
		$container  : null,
		anchor_map  : {},
		is_chat_retracted : true
	},  /*在整个模块中共享的动态信息*/
	jqueryMap = { },
	setJqueryMap, initModule, toggleChat, 
	copyAnchorMap, changeAnchorPart, onHashchange, 
	onClickChat;
	// 将创建和操作页面元素的函数放在"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;
		//
		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 'open':
		    	toggleChat(true);
		    	break;
		    	case 'closed':
		    	toggleChat(false);
		    	break;
		    	default:
		    	toggleChat(false);
		    	delete anchor_map_proposed.char;
		    	$.uriAnchor.setAnchor( anchor_map_proposed, null, true );
		    }
		}
		return false;
	};

	// Begin DOM Methods
	setJqueryMap = function () {    
	    //缓存jQuery集合,几乎我们编写的每个Shell和功能模块都应该有这个函数
	    //可以大大地减少jQuery对文档的遍历次数,能够提高性能。
		var $container = stateMap.$container;
		jqueryMap = { 
			$container : $container,
			$chat : $container.find('.spa-shell-chat')
		};
	};
	toggleChat = function ( do_extend, callback ) {
		var px_chat_ht = jqueryMap.$chat.height();
		var is_open = px_chat_ht === configMap.chat_extend_height;
		var is_closed = px_chat_ht === configMap.chat_retract_height;
		var is_sliding = ! is_open && !is_closed;
		// 避免出现竞争条件,即同时在展开和收起,如果已经在运动中,则拒绝执行
		if (is_sliding) { return false; }
		// Begin extend chat slider
		if (do_extend) {
			jqueryMap.$chat.animate (
				{ height : configMap.chat_extend_height },
				configMap.chat_extend_time,
				function () {
					jqueryMap.$chat.attr(
						'title', configMap.chat_extended_title
					);
					stateMap.is_chat_retracted = false;
					if (callback) { callback( jqueryMap.$chat ); }
				}
			);
			return true;
		}
		// Begin retract chat slider
		jqueryMap.$chat.animate (
	        { height : configMap.chat_retract_height },
			configMap.chat_retract_time,
			function () {
				jqueryMap.$chat.attr(
					'title', configMap.chat_restracted_title
				);
				stateMap.is_chat_retracted = true;
				if (callback) { callback( jqueryMap.$chat ); }
			}
		);
		return true;
	};
	onClickChat = function (event) {
		/*
		if ( toggleChat( stateMap.is_chat_retracted )) {
			$.uriAnchor.setAnchor( {
				chat : (stateMap.is_chat_retracted ? 'open' : 'closed' )
			});
		}*/
		changeAnchorPart( {
			chat: ( stateMap.is_chat_retracted ? 'open' : 'closed' )
		});
		return false;
	};
	initModule = function ( $container ) {
		stateMap.$container  = $container;
		$container.html( configMap.main_html );
		setJqueryMap();
		// test toggle
		//setTimeout( function () { toggleChat( true ); }, 3000 );
		//setTimeout( function () { toggleChat( false ); }, 8000 );
		stateMap.is_chat_retracted = true;
		jqueryMap.$chat
		   .attr('title', configMap.chat_restracted_title)
		   .click( onClickChat );
		$.uriAnchor.configModule( {
			schema_map : configMap.anchor_schema_map
		});
		$(window) 
		   .bind( 'hashchange', onHashchange )
		   .trigger( 'hashchange' );
	};
	return { initModule : initModule };
} ());

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值