文章: 基于HTML5和Javascript的移动应用架构
如果你认为你能够无视终端用户的移动化需求,那请记住:当个人电脑刚出现时,企业中的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元素以启用缓存特性:
<!DOCTYPE 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"], function (util) { return { date : function(){ var date = new Date(); return 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= "stockListContainer" data-role= 'content'> <ul data-role= "listview" id= 'tcStockList' data-inset= "true" data-filter= "true"></ul> </div>
列表视图的stock条目定义在 stock-list-item.html中。其使用了Backbone模板机制将stock模型(JSON对象)的数据展示出来。具有模板元素的HTML列出如下:
<a href= '#<%=ticker%>'> <h4><%= ticker %></h4> <p><%= name %></p> <p>Price: <%= 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'], function($, Backbone, _, StockListCollection, StockListView, stockListTemplate) { var list = {}; return Backbone.View.extend({ id : 'stock-list-page',
当一个视图/控制器实例被创建时,initialize函数被调用以定义事件和初始化控制器的模型,该模型可以是单独的对象或者对象的集合。
请注意在 stockListPage.js示例的视图/控制器的初始化函数中,有一个 StockListCollection对象被创建。集合也是Backbone支持的对象,其提供了为视图管理JavaScript对象模型“集合”的方式。当控制器被调用时,initialize() 方法就被执行。当实例被创建时,它利用jQuery选择符为按钮绑定事件处理函数。下面列出了初始化函数的代码片段:
var list = {}; return Backbone.View.extend({ id : 'stock-list-page', initialize : function() { this.list = new StockListCollection(); $("#about").on("click", function(e){ navigate(e); e.preventDefault(); e.stopPropagation(); return false; }); $("#add").on("click", function(e){ navigate(e); e.preventDefault(); e.stopPropagation(); return false; }); },
通过将jQuery选择符和表单事件对应到方法名来将事件和视图/控制器方法关联起来。下面的代码描述了和stock列表页面的添加按钮相关的事件和处理函数。请注意代码中的导航命令,我们将在下一节中讨论他们。
events: { "click #about" : "about", "click #add" : "add", }, about : function(e) { window.stock.routers.workspaceRouter.navigate("#about", true); return false; }, add : function(e) { window.stock.routers.workspaceRouter.navigate("#add", true); return false; },
render() 方法被传递到一个实例时视图/控制器HTML模板被呈现。stockListPage.js的render函数列在下面。你能够看到它是如何编译模板,接着展现绑定到控制器的el属性的HTML模板。this.el属性就是控制器在DOM中插入HTML代码的地方。接着,请注意另一个视图/控制器是如何被实例化和呈现的。StockListView控制器用于呈现stock的JQueryMobile列表视图。
render : function(eventName) { var compiled_template = _.template(stockListTemplate); var $el = $( this.el); $el.html(compiled_template()); this.listView = new StockListView({ el : $('ul', this.el), collection : this.list }); this.listView.render(); return this; }, }); });
导航(Navigation)
控制器视图间的导航是Backbone提供的另一个机制。Backbone暗含着这种“导航”的意思,其提供了可扩展的Backbone.Router对象来定义导航路由。下面列出了示例应用程序的路由代码:
define(['jquery', 'backbone', 'jquerymobile' ], function($, Backbone) { var transition = $.mobile.defaultPageTransition; var WorkspaceRouter = Backbone.Router.extend({ // bookmarkMode : false, id : 'workspaceRouter', routes : { "index" : "stockList", "stockDetail" : "stockDetail" }, initialize : function() { $('.back').on('click', function(event) { window.history.back(); return false; }); this.firstPage = true; }, defaultRoute: function() { console.log('default route'); this.runScript("script","stockList"); }, stockDetail: function() { require(['view/stockDetailView'], function (ThisView) { var page = new 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 : function() { require(['view/stockListPage'], function (ThisView) { var page = new ThisView(); $(page.el).attr({ 'data-role' : 'page', 'data-add-back-btn' : "false" }); page.render(); $(page.el).prependTo($('body')); $.mobile.changePage($(page.el), { transition : 'flip' }); }); }, }); return new 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 : function() { require(['view/stockListPage'], function (ThisView) { var page = new 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'], function($, Backbone, _, StockListItem) { return Backbone.Collection.extend({ model : StockListItem, url : 'http://localhost:8080/khs-sherpa-jquery/sherpa?endpoint=StockService&action=quotes&callback=?', initialize : function() { $.mobile.showPageLoadingMsg(); console.log('findScripts url:' + this.url); var data = this.localGet(); if (data == null) { this.loadStocks(); } else { console.log('local data present..'); this.reset(data); } }, loadStocks : function() { var self = this; $.getJSON( this.url, { }).success( function(data, textStatus, xhr) { console.log('script list get json success'); console.log(JSON.stringify(data.scripts)); self.reset(data); self.localSave(data); }).error( function(data, textStatus, xhr) { console.log('error'); console.log("data - " + JSON.stringify(data)); console.log("textStatus - " + textStatus); console.log("xhr - " + JSON.stringify(xhr)); }).complete( function() { console.log('json request complete'); $.mobile.hidePageLoadingMsg(); }); }, localSave : function(data) { var d = JSON.stringify(data); localStorage.setItem('STOCKS', d); }, localGet : function() { var d = localStorage.getItem('STOCKS'); data = JSON.parse(d); return data; } }); });
当集合对象被实例化时(在示例应用程序中,这在视图/控制器创建实例时发生),所指定的URL属性被用于通过jQuery AJAX机制调用服务器端的JSONP端点(endpoint)。该端点返回JSON格式的stock对象,该对象被映射到 stockListItem模型集合。下面是利用Backbone.Model定义StockItemModel的代码:
define(['jquery','backbone', 'underscore'], function($, Backbone, _) { return Backbone.Model.extend({ initialize : function() { } }); });
对于只有强类型语言编程经验的读者,可能会感觉返回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'], function(Backbone, WorkspaceRouter) { "use strict"; $( function(){ window.tc = { routers : { workspaceRouter : WorkspaceRouter }, views : {}, models : {}, ticker: null }; var started = Backbone.history.start({pushState: false, root:'/HTML5BackboneJQMRequireJS/'}); window.tc.routers.workspaceRouter.navigate("#index", {trigger: true}); }); });
服务器端的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中文站投稿或者参与内容翻译工作,请邮件至 [email protected]。也欢迎大家通过新浪微博( @InfoQ)或者腾讯微博( @InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。