如果你认为你能够无视终端用户的移动化需求,那请记住:当个人电脑刚出现时,企业中的 IT 部门也曾对它们有抵制情绪。实际情况会怎么样呢?移动设备的激增正在促使 IT 部门做出改变,他们必须支持移动设备,并紧接着开发出友好的移动设备应用程序。随着用户对移动设备越来越熟悉,他们对在移动设备浏览器中访问的应用程序的要求也越来越高。
向用户提供强大的移动应用程序交互体验可以通过开发内建的应用或者基于 HTML5 和 JavaScript 的网页应用。内建的应用程序有利于提供更加丰富的用户体验,但你需要为不同类型的操作系统构建相应的应用程序,这是相当耗时和昂贵的。HTML5 和 JavaScript 使开发独立于设备的用户界面成为可能。也就是说可以使用 JavaScript 组件库来创建用户界面,这些组件库利用 HTML5 元素呈现交互性的用户界面微件(Widget)。HTML5 具有很多支持移动性和富用户界面开发的新特性。
本文介绍了用以实现基于 JavaScript/HTML5 的移动设备富客户端应用程序的框架和结构。用户界面和导航元素全都是宿主于浏览器(browser-resident)的组件,而应用程序服务器的唯一角色就是提供让用户界面访问的 JSON 格式的数据。因为本文的意图是提供一些框架和应用程序结构的参考,所以示例只实现了最基本的特性。
移动设备应用开发考量
开发桌面浏览器应用程序的许多方法都可以被应用到基于移动设备浏览器的应用程序开发中。然而,开发移动设备应用程序存在一些开发桌面浏览器应用程序时不需要考虑的问题和挑战。将这些困难逐个击破是很重要的。
看看屏幕分辨率的情况。随着屏幕尺寸越来越小,这就要求进行不同的用户界面设计以优化应用程序的交互体验。HTML5 以及相应的移动设备 UI 微件(比如 jQuery Mobile)提供了使用 JavaScript 创建特定于移动设备的用户界面的跨设备方法。下面的屏幕截图是使用 jQuery Mobile HTML5 创建的列表视图:
这个移动 UI(mobile UI)是使用 HTML5 的 role 标识符和 CSS 创建的。下面列出了该列表视图的 HTML 标签代码片段。请注意 data-role 属性,其用于让 jQuery Mobile 呈现友好的移动设备用户界面组件。
<div id="stockListContainer" data-role='content'> <ul data-role="listview" id='tcStockList' data-inset="true" data-filter="true"></ul> </div>
连接性是另一个要考虑的问题。即使目前 3G 网路和 WIFI 盛行,也并不能保证连接性每时每刻都正常。幸运的是,HTML5 具有缓存特性,能让网站资源缓存在本地并在无连接的状态下被使用。可以通过添加下面所示的代码到根级别的 HTML 元素以启用缓存特性:
<html manifest="cache.manifest"> <body> ... </body> </html>
文本形式的 Manifest 文件定义了要被缓存的资源和不需要缓存的资源路径,以及当请求的资源不存在时应呈现什么。通过相关 JavaScript API,还能控制缓存资源的更新和通知机制。下面是一个缓存特性的 manifest 文件示例:
CACHE MANIFEST # 2012-27-18:v1 # Explicitly cached resources. CACHE: index.html css/stylesheet.css Images/logo.png scripts/main.js # White Listed Resources, requires connectivity, will bypass cache NETWORK: http://api.twitter.com # offline.html will be displayed, if *.HTML are not available. FALLBACK: *.html /static.html
另外,HTML5 提供了将服务端数据缓存到本地以支持无连接状态下操作和提高性能的机制。一种 HTML5 的键值对本地存储机制能够存储字符串或者字符串形式的 JSON 对象。使用 JavaScript 的localStorage对象可以访问本地存储。下面的示例描述了怎样将Stock对象Backbone.Collection以 JSON 字符串的形式存储和提取。
var d = JSON.stringify(data); localStorage.setItem('STOCKS', d); var d = localStorage.getItem('STOCKS'); data = JSON.parse(d);
我们还能让应用程序优雅的通知用户“连接不可用”,在启用 HTML5 的浏览器中,期间的远程数据访问请求将会按键值对的本地存储标准排列于本地。当连接恢复时,本地存储的对象会被更新到服务器。
网络带宽是另一个要考虑的地方。利用 AJAX,以及使用 JavaScript 对象标记法(JSON)的方式传递服务端数据,可以最小化访问服务器的次数和负载量。相比诸如 XML 或者基于 SOAP 的传统的交互协议,JSON 格式更加高效和简洁。
HTML5 应用程序架构
JavaScript 从来都没有试图成为一般目的(general purpose)的应用程序开发语言。它的最初意图是通过允许在不用访问远程服务器的情况下动态地呈现和改变 HTML 来提高浏览器中应用程序的用户体验。利用 JavaScript 可以极大地提高应用程序的性能和交互体验。移动设备不具备桌面电脑或者笔记本中的浏览器所具有的计算能力和数据传输带宽,所以应该在客户端尽可能地利用 JavaScript 和 HTML5 元素来实现富用户界面,并将客户端与服务器之间的双向数据传输量降低到最小。
这是将原本在服务端 Web 应用程序代码(JSP/ASP/PHP)中逻辑运算出来的动态 HTML 元素分离到客户端了。按照这种新的拓扑结构,服务端代码处理权限验证和数据访问请求,而用户交互和大多数应用程序逻辑则在客户端浏览器中执行。以下图表描述了这种拓扑结构:
值得一提的另外一点是,JavaScript 是弱类型的动态语言,具有闭包和将代码块作为数据类型等特性,从而提供了很多编程上的灵活性。但与此同时,也可能导致出现难以维护的代码。因此,用 JavaScript 编写应用程序时一定要小心谨慎。下面的列表列出了一般需要考虑的机制:
- 导航(Navigation)
- 远程数据访问
- 验证 / 授权
- 视图和应用模型间的解耦(MVC 模式)
- 模块化 / 打包
- 依赖管理
- 日志 / 跟踪
- 异常处理
在 Java 领域,有许多框架帮助我们构建层级的应用程序架构。在 JavaScript 领域,也存在相应的框架。下面列出的 JavaScript 框架可以用来构建模块化的、层级的和面向对象的 JavaScript 应用程序架构,以使应用程序更加可维护和稳定。
Backbone 允许以面向对象的方式将模型视图控制器(MVC)模式应用到 JavaScript 应用程序开发中。Backbone 将 HTML5 用户界面、控制器以及对象模型隔离开来,并提供了用户界面间相互导航的标准机制。
这是一个用于加载 JavaScript 文件和模块的框架。可以用该框架来加载和验证 JavaScript 模块 / 函数所依赖的其他 JavaScript 代码。开发者可以断言错误以在 JavaScript 模块或程序库没有被加载时获得依赖信息。
Underscore.js 提供了在对象集合上的进行操作的公用方法。它还提供了 HTML 模板特性。
JQuery 库用来访问和操作 HTML DOM 元素。
这是一个 HTML5 用户界面组件库,提供了基于 HTML5 的一系列 UI 控件。其包括事件处理机制、界面样式和界面操控。
这是一个允许通过 HTTP 请求访问远程 Java 对象的框架。它提供了基于票据(token)的验证机制,以及自动将 Java 对象包装为 JSON 对象的机制。它默认启用了支持跨域的 JSONP 选项。
JavaScript 目录结构
JavaScript 没有提供组织编程元素的标准方法,其只是纯文本的.js 文件。其他编程语言具有相关组织机制,比如 Java 中的包或者 C#中的命名空间。将所有函数和对象定义在一个臃肿的文件中会导致难以维护,特别是当应用程序的大部分逻辑是由 JavaScript 所编写时。因此,可以在文件系统的根文件夹下定义其他文件夹以将 JavaScript 源码按其职责存放于相应的区域。
一种方式是建立文件夹以存放应用程序的 MVC 模式的 JavaScript 元素。上图示例描述了这样的目录结构,其假设相关文件夹存在于 web 服务器 / 应用服务器的文件根目录下。
模块化支持
JavaScript 没有用以分离源代码元素的内建机制。其他一些语言则有相关机制,比如 Java 使用包的概念,而 C#使用命名空间的概念。然而,应用程序可以利用目录结构来建立相互依赖的模块。这样,应用程序和代码框架的模块化有利于维护和重用。
Require.js框架提供了相关机制以有效地分离和模块化 JavaScript 文件,以及定义、引用和访问所依赖的模块。
Require 框架使用了异步模块定义(Asynchronous Module Definition)框架来定义和加载依赖的功能。下面列出了用以加载模块的 Require/AMD 函数:
define([modules], factory function);
每个模块是一个定义了 JavaScript 对象的独立 JavaScript 文件。当以上被调用时,相关模块会被加载,同时一个对象实体被创建并传递给 factory 函数以让开发者使用。下面的示例使用 Require 框架的 define() 函数来加载所依赖的公用模块。
define(["util"], <b>function</b> (util) { <b>return</b> { date<b>:</b> <b>function</b>(){ var date = new Date(); <b>return</b> util.format(date); } }; });
Require 框架还具有用于最小化文件加载次数以提高性能的特性。
Backbone MVC
该框架使用 JavaScript 来实现流行的 MVC 设计模式。典型的 web 应用程序使用通用编程语言(general purpose language)在服务器端利用动态 HTML 生成技术来实现 MVC 模式,比如 JSP/ASP 或者某些 HTML 模板引擎。 该框架提供了相关组件或抽象用以处理用户输入以及将应用程序对象模型应用到动态 HTML 机制。Backbone 框架提供了在 JavaScript 中应用这些机制的方式,而不是在服务器端生成 HTML 标签。
HTML 模板
Backbone 视图只是一个应用了 HTML5 的 role 属性的静态 HTML 文件。Backbone 提供了模板机制,其在试图 / 控制器机制生成视图时将模型属性应用到 HTML。应用程序示例使用 HTML5 的 role 属性定义了一个 JQuery Mobile 列表视图。下面列出了 HTML5 视图stock-list.html:
<div id=<i>"stockListContainer"</i> data-role=<i>'content'</i>> <ul data-role=<i>"listview"</i> id=<i>'tcStockList'</i> data-inset=<i>"true"</i> data-filter=<i>"true"</i>></ul> </div>
列表视图的 stock 条目定义在stock-list-item.html中。其使用了 Backbone 模板机制将 stock 模型(JSON 对象)的数据展示出来。具有模板元素的 HTML 列出如下:
<a href=<i>'#<%=ticker%>'</i>> <u><h4><</u>%= ticker %></h4> <u><p><</u>%= name %></p> <u><p></u>Price: <u><</u>%= price %></p> </a>
请注意,上面的模板表达式和 JSP/ASP 的相类似,<%= %> 用以标识将被替换成 JSON 格式的模型对象的相关属性值的位置。HTML 模板位于模板文件夹中。
视图 / 控制器
Backbone 框架通过路由到 Backbone 控制器实现来呈现 HTML 视图。控制器将 JavaScript 对象绑定到 HTML 视图并告诉框架呈现视图,同时定义和处理事件。用 Backbone 的术语,视图就是控制器,创建 Backbone 视图是通过扩展框架所提供的Backone.View对象来达到的。
下面是stockListPage.js视图控制器示例的部分代码,其使用require.js框架加载了所需要的 JavaScipt 代码文件(.js 文件)。该代码调用了返回 Backbone 视图控制器函数的 JavaScript 函数define([modules,…], controller())。请注意该函数是怎样扩展Backbone.View对象的。Require 框架的 Define 函数的优势在于它用于加载视图控制器实现所需要的依赖模块。请注意模型和 HTML 模板模块是怎样提供给视图 / 控制器模块对象的:
define([ 'jquery', 'backbone', 'underscore', 'model/stockListCollection', 'view/stockListView', 'text!template/stock-list.html'], <b>function</b>($, Backbone, _, StockListCollection, StockListView, stockListTemplate) {<b>var</b> <u>list</u> = {}; <b>return</b> Backbone.View.extend({ id : 'stock-list-page',
当一个视图 / 控制器实例被创建时,initialize 函数被调用以定义事件和初始化控制器的模型,该模型可以是单独的对象或者对象的集合。
请注意在stockListPage.js示例的视图 / 控制器的初始化函数中,有一个StockListCollection对象被创建。集合也是 Backbone 支持的对象,其提供了为视图管理 JavaScript 对象模型“集合”的方式。当控制器被调用时,initialize() 方法就被执行。当实例被创建时,它利用 jQuery 选择符为按钮绑定事件处理函数。下面列出了初始化函数的代码片段:
<b>var</b> <u>list</u> = {}; <b>return</b> Backbone.View.extend({ id : 'stock-list-page', initialize : <b>function</b>() { <b>this</b>.list = <b>new</b> StockListCollection(); $("#about").on("click", <b>function</b>(e){ navigate(e); e.preventDefault(); e.stopPropagation(); <b>return</b> <b>false</b>; }); $("#add").on("click", <b>function</b>(e){ navigate(e); e.preventDefault(); e.stopPropagation(); <b>return</b> <b>false</b>; }); },
通过将 jQuery 选择符和表单事件对应到方法名来将事件和视图 / 控制器方法关联起来。下面的代码描述了和 stock 列表页面的添加按钮相关的事件和处理函数。请注意代码中的导航命令,我们将在下一节中讨论他们。
events: { "click #about" : "about", "click #add" : "add", }, about : <b>function</b>(e) { window.stock.routers.workspaceRouter.navigate("#about",<b>true</b>); <b>return</b> <b>false</b>; }, add : <b>function</b>(e) { window.stock.routers.workspaceRouter.navigate("#add",<b>true</b>); <b>return</b> <b>false</b>; },
render() 方法被传递到一个实例时视图 / 控制器 HTML 模板被呈现。stockListPage.js 的 render 函数列在下面。你能够看到它是如何编译模板,接着展现绑定到控制器的 el 属性的 HTML 模板。this.el 属性就是控制器在 DOM 中插入 HTML 代码的地方。接着,请注意另一个视图 / 控制器是如何被实例化和呈现的。StockListView 控制器用于呈现 stock 的 JQueryMobile 列表视图。
render : <b>function</b>(eventName) { <b>var</b> compiled_template = _.template(stockListTemplate); <b>var</b> $el = $(<b>this</b>.el); $el.html(compiled_template()); <b>this</b>.listView = <b>new</b> StockListView({ el : $('ul', <b>this</b>.el), collection : <b>this</b>.list }); <b>this</b>.listView.render(); <b>return</b> <b>this</b>; }, }); });
导航(Navigation)
控制器视图间的导航是 Backbone 提供的另一个机制。Backbone 暗含着这种“导航”的意思,其提供了可扩展的 Backbone.Router 对象来定义导航路由。下面列出了示例应用程序的路由代码:
define(['jquery', 'backbone', 'jquerymobile' ], <b>function</b>($, Backbone) { <b>var</b> <u>transition</u> = $.mobile.defaultPageTransition; <b>var</b> WorkspaceRouter = Backbone.Router.extend({ // bookmarkMode : false, id : 'workspaceRouter', routes : { "index" : "stockList", "stockDetail" : "stockDetail" }, initialize : <b>function</b>() { $('.back').on('click', <b>function</b>(event) { window.history.back(); <b>return</b> <b>false</b>; }); <b>this</b>.firstPage = <b>true</b>; }, defaultRoute: <b>function</b>() { console.log('default route'); <b>this</b>.runScript("script","stockList"); }, stockDetail: <b>function</b>() { require(['view/stockDetailView'], <b>function</b> (ThisView) { <b>var</b> page = <b>new</b> ThisView(); $(page.el).attr({ 'data-role' : 'page', 'data-add-back-btn' : "false" }); page.render(); $(page.el).prependTo($('body')); $.mobile.changePage($(page.el), { transition : 'slide' }); }); }, stockList : <b>function</b>() { require(['view/stockListPage'], <b>function</b> (ThisView) { <b>var</b> page = <b>new</b> ThisView(); $(page.el).attr({ 'data-role' : 'page', 'data-add-back-btn' : "false" }); page.render(); $(page.el).prependTo($('body')); $.mobile.changePage($(page.el), { transition : 'flip' }); }); }, }); <b>return</b> <b>new</b> WorkspaceRouter(); });
我们使用了 Require 的 Define 函数来将 Query、Backbone 和 jQuery Mobile 的实例提供给重写的路由函数 / 方法。当路由实例被创建后,接着传递 ID 和函数名以初始化路由,该函数将在导航到该“路由”时被执行。上面的示例中有两个路由:#index 和#stockDetail。请注意为这些路由所定义的函数。
可以使用下面的表达式调用路由对象以导航到所定义的视图 / 控制器:
<aRouter>.navigate("#index");
路由器函数创建了Backbone.View的实例,然后调用了 render 函数。有个示例扩展了用以呈现 jQuery Mobile 的 stock 列表视图的BackBone.Router 函数,下面的代码片段来自于该示例。请注意代码里 Require 框架是怎样创建 view/stockListPage Backbone 视图控制器的实例,接着使用 JQuery 设置页面属性,最后呈现页面的。
// Router navigation funtion stockList : <b>function</b>() { require(['view/stockListPage'], <b>function</b> (ThisView) { <b>var</b> page = <b>new</b> ThisView(); $(page.el).attr({ 'data-role' : 'page', 'data-add-back-btn' : "false" }); page.render(); $(page.el).prependTo($('body')); $.mobile.changePage($(page.el), { transition : 'flip' }); }); },
集合 / 模型
Backbone 提供了相关的集合对象用于管理多个Backbone.Model对象。视图 / 控制器对象具有引用单个或多个 JavaScript 对象的属性。下面代码描述了引用视图 / 控制器呈现的多个StockListItem模型对象的Backbone.Collection对象:
define(['jquery', 'backbone', 'underscore', 'model/stockItemModel'], <b>function</b>($, Backbone, _, StockListItem) { <b>return</b> Backbone.Collection.extend({ model : StockListItem, url : 'http://localhost:8080/khs-sherpa-jquery/sherpa?endpoint=StockService&action=quotes&callback=?', initialize : <b>function</b>() { $.mobile.showPageLoadingMsg(); console.log('findScripts url:' + <b>this</b>.url); <b>var</b> data = this.localGet(); <b>if</b> (data == <b>null</b>) { <b>this</b>.loadStocks(); } <b>else</b> { console.log('local data present..'); <b>this</b>.reset(data); } }, loadStocks : <b>function</b>() { <b>var</b> self = <b>this</b>; $.getJSON(<b>this</b>.url, { }).success(<b>function</b>(data, textStatus, xhr) { console.log('script list get json success'); console.log(JSON.stringify(data.scripts)); self.reset(data); self.localSave(data); }).error(<b>function</b>(data, textStatus, xhr) { console.log('error'); console.log("data - " + JSON.stringify(data)); console.log("textStatus - " + textStatus); console.log("xhr - " + JSON.stringify(xhr)); }).complete(<b>function</b>() { console.log('json request complete'); $.mobile.hidePageLoadingMsg(); }); }, localSave : <b>function</b>(data) { <b>var</b> d = JSON.stringify(data); localStorage.setItem('STOCKS', d); }, localGet : <b>function</b>() { <b>var</b> d = localStorage.getItem('STOCKS'); data = JSON.parse(d); <b>return</b> data; } }); });
当集合对象被实例化时(在示例应用程序中,这在视图 / 控制器创建实例时发生),所指定的 URL 属性被用于通过 jQuery AJAX 机制调用服务器端的 JSONP 端点(endpoint)。该端点返回 JSON 格式的 stock 对象,该对象被映射到stockListItem模型集合。下面是利用 Backbone.Model 定义 StockItemModel 的代码:
define(['jquery','backbone', 'underscore'], <b>function</b>($, Backbone, _) { <b>return</b> Backbone.Model.extend({ initialize : <b>function</b>() { } }); });
对于只有强类型语言编程经验的读者,可能会感觉返回 JSON 格式的字符串到模型对象的 JavaScript 特性很神奇。
Backbone.Model 对象具有将数据保存和同步到服务器的多个方法。Backbone.Collection 对象同样具有将数据保存和同步到服务器的多个方法,同时还具有函数式编程类型操作的特性。读者可以点击这里进一步了解。
本地存储
其他添加到扩展了 Backbone.Collection的StockListCollection示例的方法提供了利用 HTML5 本地存储机制保存和恢复对象的功能。上面集合中的相关方法是localSave() 和localGet() 。从服务器获取了 Stock 对象的集合之后,HTML5 应用程序就能在断开连接的状态下运行。示例利用了键值对的本地 session 存储机制。HTML5 还提供了本地关系型存储机制,其被称为 webSQL。然而,有关该规范的工作进行缓慢,webSQL 的支持性还不完全,只依赖于它是危险的。请从这里查看关于webSQL 规范的更多详细信息。
应用程序引导/ 启动
当所定义的样式表以及下面标签的资源被加载后,标准的 Index.html便开始工作了:
<script data-main="main.js src="libs/require/require.js"/>
这调用了用于配置和加载所需的 JavaScript 程序库的Main.js。Require 框架提供了利用基本 URL 地址将 JavaScript 程序库映射到简单名称的机制。因为 libs 文件夹位于根目录之下,所以不需要基本 URL 路径。请看下面的示例代码片段:
require.config({ paths : { 'backbone' : 'libs/AMDbackbone-0.5.3', 'underscore' : 'libs/underscore-1.2.2', 'text' : 'libs/require/text', 'jquery' : 'libs/jquery-1.7.2', 'jquerymobile' : 'libs/jquery.mobile-1.1.0-rc.2' }, baseUrl : '' });
这个启动函数使用 require 函数来配置 jQuery Mobile 属性和加载App.js脚本以导航到#index并显示 stock-list-item.html 页面。
下面列出了 App.js 脚本,其实例化工作空间路由实例,启动 backbone,然后导航到#index页面。
define(['backbone', 'router/workspaceRouter'], <b>function</b>(Backbone, WorkspaceRouter) { "use strict"; $(<b>function</b>(){ window.tc = { routers : { workspaceRouter : WorkspaceRouter }, views : {}, models : {}, ticker: <b>null</b> }; <b>var</b> <u>started</u> = Backbone.history.start({pushState:<b>false</b>, root:'/HTML5BackboneJQMRequireJS/'}); window.tc.routers.workspaceRouter.navigate("#index", {trigger:<b>true</b>}); }); });
服务器端的 JSON Endpoints
使用了 khsSherpa JSON 框架配置的应用程序服务器提供了访问端点的 URL,这些端点提供了列表和单个 Stock 对象的创建、读取、更新和删除方法。该框架将 HTTP 请求中的参数传递给 Java Endpoint 的方法调用中,并负责 Java 对象与 JSON 字符串间的序列化转换。
示例项目意图被发布成 JEE WAR 组件。该 WAR 同时包含最初发布时与客户端浏览器集成在一起的静态 HTML/JavaScript,同时还包含可以驱动 HTML5 接口的 Sherpa JSON Java 应用程序服务端。
示例中的 Java 服务端点提供 JSON 格式的 stock 相关对象。通过 HTTP GET 方法调用服务端点。
示例应用程序只需要一个用于获取 Stock 对象的服务端点。现实中的应用程序往往要求权限验证以及更多的端点以支持 CRUD 操作,而该框架支持这些要求。更多该框架的特性描述,请参见 Github 。
结论
虽然示例应用程序功能简单,但已经达到介绍基于浏览器应用程序 MVC 框架的目的。减少对应用程序服务器动态 HTML 代码生成接口的需求是本文所介绍的应用程序架构的关键特性。JavaScript 不是一门自然的通用目的编程语言,然而,移动设备数量的爆炸性增长,以及 HTML5 的广受欢迎和浏览器插件技术的日渐冷落,正在促使 JavaScript 和 HTML5 成为构建移动设备的富浏览器应用程序的切实可行之途径。
GitHub 上有完整的示例应用程序的源代码。
关于作者
David Pitt是一名高级解决方案架构师,同时是 Keyhole Software 公司的管理合伙人。在 25 年的 IT 从业经验中,David 在最近 15 年主要从事于帮助企业 IT 部门采用面向对象技术的工作。从 1999 年起,他一直致力于带领和指导软件开发团队使用基于 Java (JEE) 和.NET (C#) 的技术。David 创作了很多技术文章,同时是一本广受欢迎的介绍其架构设计模式思想的 IBM WebSphere 书籍的合著人。
查看英文原文: Mobile Application Architecture with HTML5 and Javascript
感谢贾国清对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论