使用 Worklight,第 2 部分: 在 IBM Worklight 中开发结构化模块并使用 Encrypted Offline Cache 特性

使用结构、功能性和安全性对您的移动应用程序进行分层

本系列文章将介绍 IBM® Worklight® 平台,向您显示如何构建充分利用各种 IBM 软件产品的移动应用程序。第 2 部分将继续探讨开发一个 Worklight 应用程序的过程,展示构建混合应用程序的一些最佳实践,并着重介绍 Worklight 的 Encrypted Offline Cache 功能。

Carlos Andreu , 软件开发人员, IBM

Carlos Andreu 是 IBM 软件部的一名软件开发人员,目前致力于创建一种用来构建 Hybrid、Android 和 iOS 应用程序的框架。他的兴趣十分广泛,从关注最新趋势和技术博客,到阅读、看电视,再到享受各种音乐。您可以通过 http://dev.yapr.org/carlosandreu 了解有关他的更多信息。



Jeremy Nortey, 软件开发人员, IBM

Jeremy Nortey 是 IBM 软件部从事 Mobile Foundationis 的一名软件开发人员。他为移动解决方案开发软件和质量保证,专门从事 iOS 方面的工作,喜欢在业余时间为 iPhone 构建原生应用程序。他的业余爱好包括足球和跑步。



Raj Balasubramanian, 产品架构师, IBM

Raj Balasubramanian 是 IBM 软件部的一名咨询 IT 架构师。他现在从事客户履约工作,为客户交付应用程序和基础架构相关的项目。他的兴趣十分广泛,从技术到历史以至物理学的各个范畴都有所涉猎。在充裕的闲暇时间里,他愿意与妻子在一起,以及与儿子谈论机器人。您可以通过他的个人博客 Gurukulam 阅读他的技术和个人冒险史。



2012 年 11 月 12 日

免费下载:IBM® Worklight Mobile Platform
下载更多的 IBM 软件试用版,并加入 IBM 软件下载与技术交流群组,参与在线交流。

简介

IBM WorklightIBM Mobile Foundation 的一部分,提供了一个快速构建移动应用程序的强健平台,同时也利用了可跨多个设备平台运行的基于 Web 的技术。本文将继续探讨 第 1 部分 中所中断的开发一个功能齐全的自包含移动应用程序 “Todo” 的过程,这个应用程序使移动用户能够构建并维护一个待办的任务列表。在这个过程中,您将了解到 Encrypted Offline Cache,这是一个 Worklight 客户端运行时的安全措施,可以保护敏感信息免受恶意软件的攻击和设备偷窃。

立即获取 Worklight

立即下载免费的 IBM Worklight Developer Edition 5.0,可无限期使用!

到现在为止,您应该已经在 Eclipse IDE 内设置好了 IBM Worklight Studio,并且应该对如何在 iOS 和 Android 上部署一个简单的 "Hello World" 类型应用程序比较熟悉。我们将以您在 第 1 部分 开始开发的 Todo 应用程序开始。下载在第 1 部分创建的初始应用程序,并将它导入到 Worklight Studio 环境中(您也可以在本文的附带部分 下载 Todo 应用程序项目文件。)


开发这个应用程序

使用样例应用程序

此处描述的样例应用程序只作为示例目的的练习使用。由于大量的应用程序逻辑都使用的是 JavaScript™(特别是使用了 JQuery),因此将突出介绍构造应用程序逻辑来实现更好的可读性、可靠性和一致性的最佳实践,以及使用名称空间等的实践。

图 1 阐述了用户运行 Todo 移动应用程序的一般流程。简而言之,用户将会:

  1. 打开这个应用程序。
  2. 输入密码以便安全存储离线数据,然后轻击 Start 进入到第二个面板。
  3. 在第一个字段内输入任何能代表一个新的 “to do” 项的文本,然后通过轻击 Add Item 按钮将其添加到列表内。
  4. 通过轻击列表内的一项将它标记为 “done”,选中该项。轻击 Remove Done 删除所有标记为 done 的项。
  5. 通过在第二个文本字段内键入某个项目的全部或部分名称,过滤面板上所显示的项目,然后再轻击 Filter Items...
图 1. 样例应用程序面板
图 1. 样例应用程序面板

向应用程序添加该功能的第一步是将代码分为在应用程序内具有特定职责的模块。Todo 将具备三个模块(参见图 2):

  • Constant 模块 获取并设置将要在应用程序中使用的各种常量。
  • List 模块 负责为当前会话存储列表以及处理事件,比如向列表内添加新项、将项目标记为 done,以及删除标记为 done 的项目。
  • Vault 模块 的任务是对列表进行加密和解密。这里需要利用 Worklight 的 Encrypted Offline Cache 特性。
图 2. 模块及其交互
图 2. 模块及其交互

为了构造您的代码,将会使用 JavaScript 领域内常见且流行的 Module Pattern(参见 参考资料)。清单 1 显示了一个模块的框架。您可以为包含在名称空间 (MYAPP) 内的一个对象 (Module1) 分配一个自动执行函数(或立即函数)。一个好的做法是传递全局对象(对于大多数客户端 JavaScript 的窗口)和供应商库(比如 jQuery,以便您可以在模块内使用 $,即便是在模块外,有其他东西也使用了 $,比如 Prototype.js 或另一个库)之类的内容。另一个不错的做法是列出依赖项,并将局部变量分配给这些依赖项,这是因为访问局部变量要比搜索键值对快。

比如:

var eoc = WL.EncryptedCache; eoc.open()

就好过让 JavaScript 搜索 WL 对象,然后再搜索指向另一个对象的 EncryptedCache 键,另一个对象包含您每次需要在其中一个模块内调用该函数的 open 函数。

清单 1. Module Pattern 框架
MYAPP.Module1 = (function (global, $) { 
//List dependencies: jQuery 1.7.2

//List private variables

//List private functions
var _init = function () {
console.log("I'm ready!");
}

//List one time initialization procedures 

//public API
return {
init : _init
};

}(window, jQuery)); //MYAPP.Module1

这里有必要提及一些明智的样式约定。在这些代码样例中,大写字符用于表示常量,在私有函数前面添加下划线字符以区别于私有变量。您可以将这些约定扩展为利用 “single var pattern”,其中单变量 (var) 在每个需要变量的函数顶部声明。这么做的原因之一是 JavaScript 在执行代码时会将变量 “提升” 至顶部(清单 2),原因之二是忘记使用 var 有时会产生不想要的结果,比如使用同一个名称覆盖了一个全局变量。

清单 2. 提升的示例
//Example of Hoisting 
myname = "global";

function func() {
alert(myname); //returns: "undefined" 
var myname = "local";
alert(myname); //returns: "local"

有一种称为闭包的基础结构能够增强这些模块(参见 参考资料)。在 JavaScript 中,闭包指的是一个函数的局部变量,它在该函数返回后仍然有效。这与 JavaScript 内由函数定义的范围界定紧密相关。比如,清单 1 展示的就是一个正在运行的闭包的示例。其中,init 是一个私有函数,不能在分配给 MYAPP.Module1 的自动执行函数之外进行访问,但是您可以返回一个引用该私有函数的对象;这样一来,您就可以轻松访问它,比如 MYAPP.Module1.init()。而这一切的实现都要归功于闭包。

JavaScript 是一种面向事件的编程语言,类似于几乎在每个编程模式的书籍中都会介绍的 Observer Pattern。在 JavaScript 脚本中,举几个例子,每次当用户进行滚动、单击 HTML 标记或悬停在链接上时,都会触发事件。事件可以被侦听,而且您也可以触发自己的事件。

关注点分离是另一个需要关注的重要实践,它意味着使标记、样式和应用程序逻辑保持分离。您的 HTML 代码应该只描述应用程序的结构;HTML 中内联的 JavaScript 函数并不需要。可以使用事件驱动的编码风格来实现这一点,其中,反而会观察需要具有内联调用的特定元素,以便察看有无触发必要操作的特定事件。这就使得在 JavaScript 中只有一个地方具有应用程序逻辑,而不是在整个 HTML 的多个地方都具有这个逻辑。同样地,应该避免编写 HTML 作为 JavaScript 的一部分,而是应该使用模板(参见 参考资料)。


Constant 模块

从定义您的名称空间开始(清单 3)。对于样例 Todo 应用程序,调用名称空间 “TD”。名称空间本质上就是一个能保存其他对象的对象,所有对象均属于您的应用程序。随着应用程序的发展,TD 有可能已经在全局名称空间内定义。如果还没有,可以使用单一的 var 模式来创建一个 TD 对象。这个名称空间函数将会确保方法调用中的一致性:调用 TD.namespace(TD.module1) 将会向 TD 添加一个名为 module1 的对象。您将了解如何声明 TD 名称空间,并在需要时使用它来级联 TD 名称空间下的对象。

清单 3. 名称空间函数
/**********************************************************************************
* TD NAMESPACE
**********************************************************************************/
var TD = TD || {};

TD.namespace = function (ns_string) { 
var parts = ns_string.split('.'),
parent = TD, 
i;

// strip redundant leading global 
if (parts[0] === "TD") {
parts = parts.slice(1); 
}

for (i = 0; i < parts.length; i += 1) {
// create a property if it doesn't exist
if (typeof parent[parts[i]] === "undefined") {
parent[parts[i]] = {}; 
}
parent = parent[parts[i]]; 
}

return parent; 
};

在图 3 中,可以看到 TD.Constant(Constant 模块)并不具备任何私有成员。它只通过 Public API(它是使用对象字面化创建的一个简单的键值对)返回值。同样,Todo items key 用于 Encrypted Offline Cache。encryptdecrypt 值是简单的布尔值,可最终告知 Vault 模块是否需要加密或解密。

图 3. 模块元素
图 3. 模块及其交互

List 模块

如图 1 和清单 4 所示,应用程序的第二个面板实际上是应用程序的主页。它包含:

  • 标头上的一个徽标。
  • 一个用来添加新项的文本字段(id="new-todo")。
  • 用来添加新项的一个按钮(id="add")。
  • 用来删除标记为 done 的全部项目的一个按钮(id="delete")。
  • 用来添加此项目列表的一个无序列表(id="todo-list")。
  • 用来在列表为空时显示文本的 div 标记(id="no_items")。

此外,还有一点需要注意,通过向 <ul> 标记添加 data-filter=’true’ 属性可以免费获得数据过滤(参见 参考资料)。(http://jquerymobile.com/demos/1.1.0/docs/lists/index.html)。

清单 4. 主面板 HTML
<!-------------------------- MAIN PAGE  -------------------------->
<div data-role="page" id="main" >

<!-- <div data-role="header"><h1>Todo App</h1></div> -->
<img class="centerimg" src="images/logonobg.png"/>

<div data-role="content" >

<input type="text" name="new-todo" id="new-todo" 
placeholder="Write your tasks here..." />

<div class="controls" data-role="controlgroup" data-type="horizontal">
<a href="#" id="add" data-role="button" data-icon="plus">Add New</a> 
<a href="#" id="delete" data-role="button" data-icon="delete">Delete Done</a>
</div>

<ul id="todo-list" data-role="listview" data-filter="true" data-inset="true" >
</ul><!-- /todo-list -->

<div class="center" id="no_items"></div>

</div>
<!-- /content -->
</div>
<!-- /page -->

对于您的任务列表,您将会把项目存储在一个列表数组内。例如:

[{content: “myTodo Item 1", done: false},
{content: “myTodo Item 2", done: true}].

这只是一个具有键值对(JavaScript 对象)的简单数组,由一个字符串表示项目名称,一个布尔值来跟踪项目是否完成。

如图 3 所示,TD.List(List 模块)包含一个定义为 (_encrypt) 的私有方法,该方法用来加密列表(一个名为 ‘list’ 的私有数组),它从公共 API 调用 Vault 模块的加密方法。还有一个 _item_template,它是一个从模板生成的函数,用来显示在您的 HTML 文档内定义的项;您需要为这个键和列表数组传递一个具有 ‘item’ 的对象。

一个练习

作为练习,您可能想执行更为详细的类型检查(比如,检查 item.done 是否会返回一个 Boolean、检查空值,等等)。

私有函数 add 的作用是检查 item.content 是非空还是未定义,以及在调用 item.done 时是否会有结果返回。请记住,默认情况下没有分配到值的变量将会返回 “undefined”。

清单 5. _add 函数
_add =  function (item) {
if (item.content !== 'undefined' && item.done !== 'undefined' && item.content.length > 0)
{
list.push(item);
}
},// add item

_refresh_list 函数在这个列表数组上动态运行一个映射函数,并向数组内存储的每个对象添加一个新元素。函数的其他作用是清空列表,将基于此列表生成的模板结果添加到 HTML 内的这个无序列表 (ul),并刷新视图。后一个方法调用就是 jQuery Mobile,特定于刷新列表的样式设计。

清单 6. _refresh_list 函数
_refresh_list = function () {

$.map(list, function (item, i) {
item.index = i;
});

list_ul.empty();
list_ul.append(_item_template({item : list}));
list_ul.listview('refresh');

if (list.length < 1) {
no_items.text('No items.');
} else {
no_items.text('');
}

}; // refresh list

另一个练习

您或许想要另辟蹊径,使用不同的数据结构和不同的算法来生成 ID 并保持跟踪。目的是无需使用映射函数来迭代整个列表并生成 ID,也无需在每次添加或删除项目时清空和重新添加整个列表。

接下来,需要将操作附加到事件。如果您熟悉 jQuery,可能习惯于查看或使用像 bind()、live() 和 delegate() 这样的调用。在本例中,唯独使用了 on(),因为在 jQuery 1.7.0 后,那些 bind、live 和 delegate 方法都只是 on() 的包装器。而且,$(this) 被 “缓存” 到一个名为 $this 的局部变量,因为在理想情况下,想要尽可能少地使用该 jQuery 函数并限制投入 DOM (Document Object Module) 的时间量,因为这是代价很大的操作。注意,我们切换这个布尔值来描述项目是否完成,并添加或删除在样式表(.css 文件)内定义的名为 “done” 的类。最后,对列表进行 encrypt() 以实现持久化,即便您在单击一个项目并将其标记为 done 后立即关闭应用程序也是如此。

清单 7. TD.List 内的事件附件
add_btn.on('click', function () {

_add({content : new_item.val(), 
done: false });

new_item.val('');
_refresh_list();

_encrypt();

}); // add btn

list_ul.on('click', 'li', function () {

var $this = $(this),
$item = $this.find('a'),
index = $item.attr('id'), 
current_item = list[index];

current_item.done = !(current_item.done);

if (current_item.done) {
$item.addClass('done');
} else {
$item.removeClass('done');
}

_encrypt();

}); // list item

delete_btn.on('click', function () { 
var i = list.length;

while (i--) {
if (list[i].done) {
list.remove(i);
}
}

_refresh_list();

_encrypt();

});// delete btn

对删除按钮也应用了类似的处理,其代码可以在 Worklight Studio 内看到。

List 模块的公共 API 非常简单;您可以获取正在使用的当前列表、设置一个新列表并使用它,以及刷新当前的设置列表。可以通过调用 TD 名称空间来调用这些方法,其次是调用模块的名称,最后是调用由模块返回的对象中的其中一个键。

例如:

TD.List.get(), TD.List.set([]), TD.List.refresh()

都是有效的,而:

TD.List._refresh_list(), TD.List._add()

则不是,因为这些函数只能在此模块内调用(记住,函数创建范围),或由特权成员(比如在模块内返回的对象)调用。

清单 8. TD.List 公共 API
//public API
return {
get : function () {
return list;
},
set : function (new_list) {
list = new_list;
},
refresh : function () {
_refresh_list();
}
};

Vault 模块

TD.Vault 模块是 Worklight 的 Encrypted Offline Cache (EOC) 功能的包装器。EOC 使用 HTML5 在本地存储上构建,它是一种不使用 cookie 存储持久性数据的方式。EOC 提供了打开和关闭、在打开时读取和写入值的方法。EOC 背后的安全性细节超出了本文的讨论范围,但简言之,EOC 利用用户密码来加密数据。因此,与这个特定模块相关的 HTML 代码是一个页面(图 1 中的第一个面板),包含了:

  • 一个具有徽标和应用程序名的标头图像。
  • 一个密码字段 (id=’passcode’)。
  • 一个按钮 (id=’send_passcode’),用来提交该密码。
清单 9. 密码页面的 HTML
<!-------------------------- PASSCODE PAGE  -------------------------->
<div data-role="page">

<img class="centerimg" src="images/logonobg.png"/>

<div data-role="content">

<div class="center" id="invalid_passcode"></div>

<ul data-role="listview" data-inset="true">
<li data-role="fieldcontain">
<label for="passcode">Enter Passcode:</label> 
<input type="text" name="passcode" id="passcode" value="" placeholder="Your passcode" />
</li>
</ul>

<a id="send_passcode" href="#" data-role="button" data-theme="a">Start</a>
</div>
<!-- /content -->

</div>
<!-- /page -->

通过将一些全局变量传递给自动执行的函数,比如,jQuery(重定义为 $)和 WL(Worklight 的名称空间,重定义为 WL),启动 JavaScript 代码。然后 “缓存” 依赖项,方法是给它们分配局部变量。这就确保了您可以轻松调用 wl.EncryptedCache.open(而不是将 WL 名称空间以参数的形式传递到模块),然后在一个名为 eoc 的局部变量内缓存 wl.EncryptedCache。接下来,诸如 KEY 这样的私有变量存储用户输入的口令,还有一些 DOM 调用保存为密码按钮、密码字段,以及能够在密码无效时显示一个错误消息的 div(清单 10)。

清单 10. TD.Vault 模块、依赖项和私有变量
TD.namespace('TD.Vault');
TD.Vault = (function ($, wl) { 
//dependencies
var eoc = wl.EncryptedCache,
log = wl.Logger,
list_obj = TD.List,
CONST = TD.Constant,

//private variables
KEY = "",
send_passcode_btn = $('#send_passcode'),
passcode_fld = $('#passcode'),
invalid_passcode_div = $('#invalid_passcode'),

最后一个练习

您可能想要查阅 Worklight 文档,并根据 onErrorHandler 回调函数返回的状态代码实现特定于错误的处理。

接下来,您将会有一些私有变量,比如 error,它只在您收到错误回调时进行记录。_setData_getData 方法指定如何获取并设置将要加密和解密的数据(清单 11)。

清单 11. _error、_setData 和 _getData 函数
//private functions
_error = function () {
log.debug("error");
},

_setData = function (new_list) {
if (new_list) {
list_obj.set(new_list);
list_obj.refresh();
} 
},

_getData = function () {
return list_obj.get();
},

事件定义与 TD.List 中的事件定义类似,在 TD.List 中需要附加 send_passcode_btn on click 来只将用户在启动应用程序时输入的密码分配给私有变量 KEY,前提是输入长度至少为 1 个字符(清单 12)。

清单 12. 发送密码按钮事件附件
send_passcode_btn.on('click', function () {

var passcode = passcode_fld.val();

if (passcode.length > 0) {
KEY = passcode;

_decrypt();

$.mobile.changePage("#main", { transition: CONST.DEFAULT_TRANSITION });

} else {
passcode_fld.val('');
invalid_passcode_div.text('Invalid Passcode.');
}

});

接下来,通过调用私有变量 decrypt() 解密 EOC 的内容。这会用一个常量 (CONST.DECRYPT) 调用 _open,这表明要在打开加密的缓存后进行解密。_open 函数触发事件 eoc-open 并向 _read() 发送操作 (decrypt)。之后,您使用用户提供的密钥尝试读取加密的缓存。此时,用 parseJSON 解析回 JavaScript 数组(该数组包含一个键值对,用来描述数组的每个索引上的某一项)的数据调用 setData。如果从缓存中有所收获,那么就会在 List 模块上调用 set 来将取回的列表设置为新列表。最后,通过从 TD.List 的公共 API 调用 refresh 来刷新列表 HTML 元素。

清单 13. _close、 _write、 _read、 _encrypt、 _decrypt 函数和 ‘eoc-open’ 事件侦听器
_close = function () {
var onCompleteHandler = function () { 
$.publish('eoc-closed'); 
};

//function(onCompleteHandler, onErrorHandler)
eoc.close(onCompleteHandler, _error);
},

_write = function () {
var data = JSON.stringify(_getData());

//function(key, data, onCompleteHandler, onErrorHandler)
eoc.write(CONST.TODO_ITEMS_KEY, data, _close, _error); 
},

_read = function () {
var onCompleteHandler = function (data) {
_setData($.parseJSON(data)); 
_close(); 
};

//function(key, onCompleteHandler, onErrorHandler)
eoc.read(CONST.TODO_ITEMS_KEY, onCompleteHandler, _error);
},

_encrypt = function () {
_open(CONST.ENCRYPT);
},

_decrypt = function () {
_open(CONST.DECRYPT);
};

$.subscribe("eoc-open", function (e, action) {
if (action) { // == CONST.ENCRYPT
_write();
} else { // == CONST.DECRYPT
_read();
}
});

解密遵循几乎相同的步骤,只是在您打开缓存后的动作将会调用 _write 而非 _read。之后您就可以运行 _getData 来获取当前列表,用 JSON.stringify 将它转变为一个字符串并存储在缓存内。在每次加密和解密后关闭缓存。

图 4 和图 5 显示了在 iPhone 模拟器上运行的完成后的应用程序。

图 4. 在 iPhone 模拟器上运行的完成后的应用程序 (Passcode Page)
图 4. 在 iPhone 模拟器上运行的完成后的应用程序 (Passcode Page)
图 5. 在 iPhone 模拟器上运行的完成后的应用程序 (Main Page)
图 5. 在 iPhone 模拟器上运行的完成后的应用程序 (Main Page)

结束语

这个有关 IBM Worklight 的介绍性系列的第 2 部分继续利用了第 1 部分 Worklight 开发环境设置,并添加了构建 Todo 样例应用程序的功能。在这个过程中,我们了解了您的结构化代码将会如何为应用程序的开发、表现和维护提供益处,还了解了如何通过使用 Worklight 的 Encrypted Offline Cache 功能来持久化设备上的数据(任务列表)。本系列的总结部分将会添加与适配器的服务器端连接性,从而完成这个样例应用程序。


下载

描述名字大小
样例应用程序项目文件todo-app-part2.zip18.5MB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=移动开发, WebSphere
ArticleID=845369
ArticleTitle=使用 Worklight,第 2 部分: 在 IBM Worklight 中开发结构化模块并使用 Encrypted Offline Cache 特性
publish-date=11122012