Appcelerate-Titanium-商业应用开发秘籍-全-

Appcelerate Titanium 商业应用开发秘籍(全)

原文:zh.annas-archive.org/md5/f53f886c94048aae4595dbe880edb9fc

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

企业对移动应用的需求从未如此之大。虽然满足这一需求正变得越来越具有商业重要性,但设备和平台的爆炸性增长放大了开发复杂性。自智能手机革命开始以来,移动 JavaScript 框架就已经存在,并且是应对这些挑战的完美平台。认识到这些框架的跨平台特性,企业已经开始依赖它们来构建既经济高效又引人入胜的跨平台解决方案。

Appcelerator 的 Titanium Mobile 凭借其创新的架构、大量的 API 以及专注于提供原生体验的特点而脱颖而出。随着社区的蓬勃发展以及企业合作伙伴数量的不断增长,Titanium 迅速成为最大的跨平台移动框架之一。

Appcelerator Titanium 企业级应用开发食谱展示了如何利用现有的 JavaScript 技能创建原生移动应用。通过一系列示例和辅助代码,涵盖了企业级 Titanium 开发所需的所有方面。您将学习如何使用单个 JavaScript 代码库为 iOS 和 Android 开发跨平台企业级移动应用。

本书从设计模式讨论开始,实际食谱通过企业级移动开发所需的不同主题逐步展开。本书中的每个食谱都是一个独立的课程。请随意挑选您感兴趣的食谱,并将它们作为 Titanium 项目的参考。本书中的食谱展示了如何在设备上处理数据、创建图表和图形,以及与各种 Web 服务交互。后续的食谱讨论了应用安全和原生模块开发,这些提供了额外的资源,以加速您的下一个 Titanium 移动开发项目。

本书涵盖内容

第一章,模式和平台工具,涵盖了 MVC 结构、CommonJS、懒加载以及其他移动开发模式,以帮助提高代码维护性并减少内存占用。

第二章,跨平台 UI,展示了如何从一个共同的代码库中提供 iOS 和 Android 的原生体验,同时创建品牌化的体验。

第三章,使用数据库和管理文件,涵盖了使用 NoSQL、SQLite 和文件系统来管理设备数据的方法。

第四章,与 Web 服务交互,展示了如何与包括 RSS、REST、YQL 和 SOAP 在内的各种不同的 Web 技术进行交互。

第五章, 图表和文档,涵盖了使用图表、文档显示、杂志和画廊布局来可视化数据的方法。

第六章, 使用位置服务,讨论了如何使用按需和后台技术以全球可靠的方式有效地使用地理位置。

第七章, 线程、队列和消息传递,介绍了如何利用队列、意图和线程来分离应用程序责任并提供消息传递功能。

第八章, 基本安全方法,提供了有关登录管理、存储凭证和使用第三方 JavaScript 加密库的最佳实践食谱。

第九章, 使用加密和其他技术进行应用安全,涵盖了在企业应用开发中安全处理应用程序资产、配置和数据所需的食谱。

附录, 钛资源,提供了额外的链接和有关如何开始的详细信息。

您需要这本书的内容

  • Windows, Mac OS X

  • Android SDK 2.3 或更高版本

  • iOS SDK 5.1 或更高版本

  • Titanium SDK 3 或更高版本

本书面向对象

本书是为有一定 JavaScript 知识,并希望快速构建移动企业应用的读者而写的。希望扩展技能并开发跨平台解决方案的资深移动开发者会发现这些实用食谱在开始下一个 Titanium 项目时是一个有用的资源。

惯例

在这本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“此命名空间包含常见的 API,如Ti.UI.ViewTi.UI.TableView。”

代码块设置如下:

//Create our application namespace
var my = {
  ui:{
    mod : require('dateWin')
  },
  tools:{},
  controllers:{}
};

当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:

//Add a global event to listen for log messages
Ti.App.addEventListener('app:log',function(e){
 //Provide Log Message to CommonJS Logging Component
 my.logger.add(e);
});

//Open our sample window
my.ui.mainWindow.open();

新术语重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中显示如下:“本食谱通过使用屏幕断开交互模式来创建额外的空间,以便为添加笔记字段。”

注意

警告或重要提示会以这样的框中出现。

小贴士

小技巧和窍门如下所示。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题非常重要。

如需向我们发送一般反馈,请简单地将电子邮件发送至 <feedback@packtpub.com>,并在邮件主题中提及书籍标题。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们在 www.packtpub.com/authors 上的作者指南。

客户支持

现在您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您在 www.PacktPub.com 的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.PacktPub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/support,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站,或添加到该标题的错误清单中。任何现有的错误清单都可以通过从 www.packtpub.com/support 选择您的标题来查看。

盗版

互联网上对版权材料的盗版是一个跨媒体持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面的帮助。

询问

如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章。模式和平台工具

在本章中,我们将涵盖:

  • 定义应用程序命名空间

  • 实践中的 CommonJS

  • 使用平台指示器

  • 使用 Ti.App.Listener 进行全局日志记录

简介

对于许多人来说,构建钛金属应用程序将是他们第一次接触大型、完整的 JavaScript 项目。无论你是设计一个小型费用跟踪系统还是完整的 CRM 工具,在钛金属中实施适当的设计模式都将提高你应用程序的性能和可维护性。

钛金属的跨平台特性和底层架构影响了众多常见设计模式的应用。在本章中,我们将展示如何应用这些模式以加快开发速度,并在支持多设备时实施最佳实践。

介绍钛金属

Appcelerator 钛金属移动平台是一个使用现代网络技术(如 JavaScript、CSS 和 HTML)构建跨平台原生移动应用程序的平台。钛金属移动平台是由 Appcelerator Inc 开发的开源项目,并许可在 OSI 批准的 Apache 公共许可证(版本 2)下。

钛金属移动项目是 GitHub 上最活跃的项目之一,每天有大量的提交。GitHub 仓库是社区中许多人的焦点,包括模块开发者、需要夜间构建的应用程序开发者以及钛金属项目的个人贡献者。

钛金属生态系统是行业内最大的之一,拥有超过 45 万全球开发者,他们的应用程序运行在世界 10%的设备上。Appcelerator 拥有最大的移动市场之一,为钛金属移动平台提供第三方组件。

钛金属架构

钛金属是一个基于模块的移动开发平台,由 JavaScript 和本地平台代码(Java、Objective-C 和 C++)组成。钛金属的架构目标是提供一个跨平台的 JavaScript 运行时和 API,用于移动开发;这与其他框架构建“原生包装”网络应用程序的方法不同。

钛金属使用 JavaScript 解释器在您的应用程序 JavaScript 代码和底层本地平台之间建立桥梁。这种方法允许钛金属公开大量 API 和本地 UI 小部件,而不会牺牲性能。钛金属的 UI 控件真正是原生的,而不是通过 CSS 进行视觉模拟。因此,当您创建一个Ti.UI.Switch时,它实际上是在 iOS 上使用本地的 UISwitch 控件。

每个钛金属应用程序都组织成分层架构,包括以下主要组件:

  • 您的 JavaScript 代码:在编译时,这将编码并插入到 Java 或 Objective-C 文件中

  • 钛金属的 JavaScript 解释器:在 Android 上为 V8 或 iOS 上的 JavaScriptCore

  • 钛金属 API:这是为在 Java、Objective-C 或 C++中创建的特定平台创建的

  • 原生自定义模块:有大量开源和商业模块可供选择Titanium 架构

在运行时,嵌入您应用程序中的 Titanium SDK 创建了一个原生代码 JavaScript 执行上下文。然后,此执行上下文用于评估您的应用程序 JavaScript 代码。随着 JavaScript 的执行,它将创建代理对象以访问原生 API,如按钮和视图。这些代理是存在于 JavaScript 和原生环境中的特殊对象,充当两个环境之间的桥梁。

例如,如果我们有一个 Ti.UI.View 对象,并将 backgroundColor 更改为 blue,该属性在 JavaScript 中发生变化,然后代理更新了底层原生层中的正确属性,如下所示图所示:

Titanium 架构

构建跨平台应用

Titanium 提供了一个高级跨平台 API,但它不是一个“一次编写,到处运行”的框架。在构建跨平台应用程序时,建议采用“一次编写,适应所有地方”的哲学。使用 Titanium,您可以添加特定于平台的代码来处理每个平台的不同的 UI 要求,同时保持您的业务逻辑 100% 跨平台兼容。

构建最佳跨平台应用程序,Titanium 提供了以下工具:

  • 在运行时识别平台和型号

  • 能够在构建时处理特定于平台和设备的资源

  • 应用平台和设备特定的样式

除了平台工具之外,Titanium API 还旨在协助跨平台开发。每个主要组件,如 MapsContactsFileSystem,都被分离到顶级命名空间 TiTitanium 下的单独组件命名空间中。这些组件命名空间然后有自己的子命名空间来分割特定于平台的行为。

这种分段的例子是 Ti.UI 命名空间,其中包含所有 UI 组件。此命名空间包含常见的 API,例如 Ti.UI.ViewTi.UI.TableView。此外,Ti.UI 命名空间还有特定于平台的子命名空间,例如包含 Ti.UI.iPad.Popover 等控件的 Ti.UI.iPad。同样的设计也适用于非视觉 API,如 Ti.Android 命名空间,它包含 Android 特定的行为。

定义应用程序命名空间

在 Titanium 应用程序开发中,使用命名空间非常重要,因为它有助于组织您的代码,同时不会污染全局命名空间。创建与命名空间或其他作用域条件无关的变量和方法的做法被称为污染全局命名空间。由于这些函数和对象的作用域是全局的,它们将不会在全局命名空间在应用程序关闭时失去作用域之前被收集。这通常会导致内存泄漏或其他不希望出现的副作用。

如何操作...

以下示例展示了如何在app.js中为我们的应用程序创建一个名为my的命名空间,其中包含三个子命名空间,分别称为uitoolscontrollers

var my = {ui:{},tools:{},controllers:{}}

在我们构建食谱的过程中,我们将继续向先前的命名空间添加功能。

实践中的 CommonJS

使用 CommonJS 模块组织您的应用程序代码是 Titanium 开发中的最佳实践。CommonJS 是创建可重用 JavaScript 模块的流行规范,并被 Node.js 和 MongoDb 等几个主要平台和框架所采用。

CommonJS 模块有助于解决 JavaScript 作用域问题,将每个模块放置在其自己的命名空间和执行上下文中。变量和函数在模块内部是局部作用域的,除非明确导出供其他模块使用。

除了帮助解决 JavaScript 作用域问题外,CommonJS 还提供了一个模式,用于公开稳定的接口以供程序使用。信息隐藏设计模式允许模块开发者更新模块的内部结构,而不会破坏公共契约或接口。在 JavaScript 中维护稳定的公共接口是编写可维护代码的关键部分,这些代码将在应用程序和团队之间共享。

Titanium 以类似于 Node.js 的方式实现了 CommonJS,即您使用require方法返回一个 JavaScript 对象,该对象具有属性、函数和其他数据分配给它,这些数据形成了模块的公共接口。

以下截图展示了用于演示本书中将使用的 CommonJS 高级概念的示例应用程序。

实践中的 CommonJS

准备工作

将本食谱中使用的 CommonJS 模块添加到项目中很简单,包括将datahelper.jsdateWin.js文件复制到 Titanium 项目的根目录,如下面的截图所示:

准备工作

如何操作...

以下食谱展示了如何使用 CommonJS 创建UITools模块。在下面的示例中,创建了一个简单的应用程序,允许用户增加或减少日期一天。

创建项目的 app.js

在我们的app.js中,我们创建应用程序命名空间。这些命名空间变量将在示例的后续部分用于引用我们的 CommonJS 模块。

//Create our application namespace
var my = {
  ui:{
    mod : require('dateWin')
  },
  tools:{},
  controllers:{}
};

小贴士

下载示例代码

您可以从您在www.PacktPub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.PacktPub.com/support并注册以直接将文件通过电子邮件发送给您。

然后使用已添加到我们应用程序命名空间的my.ui.mod创建Ti.UI.Window。然后在我们的win对象上调用open方法以打开示例应用程序的主窗口。

my.ui.win = my.ui.mod.createWindow();

my.ui.win.open();

构建 datehelpers 模块

在我们项目的 Resources 文件夹中,有一个 CommonJS 模块 datehelpers.js。此模块包含以下代码:

  1. helpers 方法是在 datahelpers 模块中创建的。默认情况下,此函数是私有的,直到使用 exports 对象暴露它。

    var helpers = function(){
      var createAt = new Date();
    
  2. createdOn 方法被添加到 helpers 函数中。此函数返回 createAt 变量。此函数用于提供时间戳值,以展示模块可以初始化多次。每次为模块创建新会话时,createAt 变量将显示新的初始化时间戳。

    this.createdOn = function(){
        return createAt;
      };
    
  3. addDays 方法被添加到 helpers 函数中。此方法通过 n 参数提供的天数增加提供的日期值。

    this.addDays = function(value,n){
        var tempValue = new Date(value.getTime()); 
        tempValue.setDate(tempValue.getDate()+n);
        return tempValue;
      }
    };
    

module.exports 是在 require 调用结果中返回的对象。通过将 helpers 函数赋值给 module.exports,我们能够使私有的 helpers 函数公开访问。

module.exports = helpers;

日期 Win 模块

同样包含在我们项目的 Resources 文件夹中还有一个 CommonJS 模块 dateWin.js。以下代码部分讨论了此模块的内容。

  1. 使用 require 关键字导入 datehelpers CommonJS 模块。这个模块在 mod 模块级别变量中被导入,以便后续使用。

    var mod = require('datehelpers');
    
  2. createWindow 函数返回 Ti.UI.Window,允许用户与食谱一起工作。

    exports.fetchWindow=function(){
    
  3. 接下来创建 dateHelper 模块的新实例。

      var dateHelper = new mod();
    
  4. 构建 createWindow 函数的下一步是将 currentDateTime 模块属性设置为当前日期/时间的默认值。

      var currentDateTime = new Date();
    
  5. createWindow 函数将返回的 Ti.UI.Window 对象,然后创建。这将用于附加所有我们的 UI 元素。

      var win = Ti.UI.createWindow({
        backgroundColor:'#fff'
      });
    
  6. dateDisplayLabel 用于显示用户增加或减少日期值时 datehelper 模块的结果。

      var dateDisplayLabel = Ti.UI.createLabel({
        text:String.formatDate
        (exports.currentDateTime,"medium"),
        top:120, height:50, width:Ti.UI.FILL, 
        textAlign:'center', color:'#000', font:{fontSize:42}
      });
      win.add(dateDisplayLabel);
    
  7. addButton 在此食谱的后续步骤中被用来调用 datehelper 模块,并给当前模块值添加天数。

      var addButton = Ti.UI.createButton({
        title:"Add Day", top:220, left:5, width:150, height:50
      });
      win.add(addButton);
    
  8. subtractButton 在此食谱的后续步骤中被用来调用 datehelper 模块,并减少当前模块值的日期。

      var subtractButton = Ti.UI.createButton({
        title:"Subtract Day", top:220, right:5, 
      	 width:150, height:50
      });
      win.add(subtractButton);
    
  9. addButtonsubtractButton 点击处理程序中的以下代码显示了如何调用 AddDays 方法来增加模块的 currentDateTime 属性。

      addButton.addEventListener('click',function(e){
    
  10. 以下行演示了如何通过一天来增加 currentDateTime 的值:

        exports.currentDateTime = 
        dateHelper.addDays(currentDateTime,1);
    
  11. 使用新的模块值更新 dateDisplayLabel

        dateDisplayLabel.text = String.formatDate(
        exports.currentDateTime,"medium");
      });    
      subtractButton.addEventListener('click',function(e){
    
  12. 以下代码片段演示了如何通过一天来减少 currentDateTime

        exports.currentDateTime = 
        _dateHelper.addDays(currentDateTime,-1);
    
  13. 使用新的模块值更新 dateDisplayLabel

        dateDisplayLabel.text = String.formatDate(
        currentDateTime,"medium");
      });
      return win;
    };
    

它是如何工作的...

创建模块很简单。你只需将一个 JavaScript 文件添加到你的项目中,并编写你的应用程序代码。默认情况下,任何变量、对象或函数都是私有的,除非你将它们添加到 moduleexports 对象中。moduleexports 对象是由 Titanium 创建的特殊 JavaScript 对象,并由 require 方法返回。

需要

要使用 CommonJS 模块,你必须使用全局可用的 require 函数。此函数有一个单一参数,通过该参数你提供 JavaScript 模块的路径。以下行演示了如何加载位于 Titanium 项目根目录的名为 datehelpers.js 的 CommonJS 模块。

var myModule = require('datehelpers');

注意

当向 require 函数提供绝对路径时,Titanium 将从你的项目的 Resources 文件夹开始。

Titanium 已经优化了模块加载过程,当模块首次使用 require 方法加载时,它将被缓存以避免重新评估模块的 JavaScript。这种方法显著提高了具有共同依赖项的模块的加载性能。如果你需要在加载时管理/修改模块,请记住 JavaScript 不会被重新评估。

属性

向你的 CommonJS 模块添加属性很容易。你只需将一个变量附加到 exports 对象上。

以下代码片段演示了如何创建具有默认值的基本 CommonJS 属性。

exports.myProperty = "I am a property";

也可以创建更复杂的对象属性,例如如下所示:

exports.myObjectProperty = {
  foo:1,
  bar:'hello'
};

你还可以使用私有变量来设置初始属性的值。这通常会使代码更易读和简洁。

创建一个反映你业务需求的局部变量。

var _myObjectProperty = {
  foo:1,
  bar:'hello'
};

然后,你可以将局部变量分配给附加到 exports 对象上的属性,例如如下所示:

exports.myObjectProperty = _myObjectProperty

注意

记住这些只是 exports JavaScript 对象上的属性,可以被任何调用者更改。

函数

在 CommonJS 中创建公共函数很容易,你只需将一个函数添加到 exports 对象中。将 Adddays 方法作为 exports 对象的一部分创建。此函数接受一个日期作为 value 参数和一个整数作为 n 值。

exports.AddDays = function(value,n){

创建一个新变量以避免提供的值被修改。

  var workingValue = new Date(value.getTime());

使用提供的 n 值增加日期。这可以是正数或负数。负数值会减少提供的日期值。

  workingValue.setDate(workingValue.getDate()+n);

返回调整后的新值。

  return workingValue;
};

你也可以将 exports 方法分配给私有作用域的函数。这有助于管理大型或复杂的函数。

创建一个名为 addDays 的局部作用域函数。

function addDays(value,n){
  var workingValue = new Date(value.getTime());
  workingValue.setDate(workingValue.getDate()+n);
  return workingValue;
};

然后,将 addDays 函数分配给 exports.AddDays,使其对模块外部的调用者可见。

exports.AddDays = addDays;

注意

记住这些只是 exports JavaScript 对象上的方法,可以被任何调用者更改。

使用 module.exports 创建实例对象

Titanium 提供了使用 module.exports 创建模块实例对象的能力。这允许你创建与 module.exports 相关联的函数或对象的新实例。这有助于描述一个特定的对象,实例方法代表这个特定对象可以执行的操作。

这种模式鼓励开发者更加模块化地思考,并遵循单一职责原则,因为只有一个对象或函数可以被分配给 module.exports

以下代码片段演示了如何使用此模式创建和调用模块:

  1. 使用 Titanium Studio,创建员工(employee.js)模块文件。

  2. 接下来创建 employee 函数。

    var employee = function(name,employeeId,title){
      this.name = name;
      this.employeeId = employeeId;
      this.title = title;
      this.isVIP = function(level){
        return (title.toUpperCase()==='CEO');
      }
    };
    
  3. 然后将 employee 函数分配给 module.exports。这将使 employee 函数可以通过 require 公开调用。

    module.exports = employee;
    
  4. 使用 require,创建了一个模块的引用并将其分配给 employee 变量。

    var employee = require('employee');
    
  5. 接下来,使用之前创建的 employee 对象的新实例创建了 bobchris 对象。

    var bob = new employee('Bob Smith',1234,'manager');
    var chris = new employee('Chris Jones',001,'CEO');
    
  6. 最后,调用 bobchris 对象上的属性和函数,以演示每个对象的实例信息。

    Ti.API.info('Is ' + bob.name + ' a VIP? ' + bob.isVIP());
    Ti.API.info('Is ' + chris.name + ' a VIP? ' + chris.isVIP());
    

CommonJS 全局作用域反模式

在 Android 和 iOS 上的 CommonJS 实现基本上是相同的,只有一个主要的范围异常。如果您使用的是低于版本 3.1 的 Titanium 框架版本,Android 将所有变量访问范围限定在模块本身,而 iOS 允许模块访问执行上下文中已加载的模块之外的对象。这应该被视为一种反模式,因为它破坏了许多 CommonJS 规范旨在防止的封装原则。

在 Titanium 版本 3.1 中,已经决定弃用 iOS 和 Android 中的全局作用域访问,以支持新的 Alloy.Globals 对象。您可以在 docs.appcelerator.com/titanium/latest/#!/api/Alloy 中了解更多关于 Alloy.Globals 对象的信息。

以下食谱演示了这种常见的反模式,并突出了 iOS 和 Android 在此领域的 CommonJS 实现差异。

//Create our application namespace
var my = {
  tools: require('scope_test'),
  session:{
    foo: "Session value in context"
  }
};

tools 模块上调用 testScope 方法。这演示了 CommonJS 模块作用域反模式是如何工作的。

my.tools.testScope();

包含 testScope 方法的 tools 模块是本食谱代码的一部分,可以在我们项目的 scope.js 文件根目录中找到。此模块包含以下代码:

exports.testScope = function(){
 Ti.API.log
 ("Test Module Scope - Foo =  " + my.session.foo);
 return my.session.foo;
};

在调用 my.tools.testScope() 方法时,会显示出范围访问的反模式。在 iOS 中,my.tools.testScope() 返回 "Session Value in context",因为它可以从当前执行上下文中访问 my.session.foo。在 Android 中,在 Titanium SDK 版本 3.1 之前,未定义的对象通常会被返回,因为模块没有访问 my.session.foo 对象的权限。在 Titanium SDK 版本 3.1 及更高版本中,Android 现在返回 "Session Value in context",因为它可以访问 my.session.foo 对象。

从 Titanium SDK 版本 3.1 开始,在两个平台上对全局作用域的访问已经弃用,并且将在 SDK 的未来版本中删除。如果您之前实现了这种反模式,建议采取纠正措施,因为该功能的弃用将在您的应用程序中引起破坏性更改。

参见

使用平台指示器

处理不同的设备、平台和模型是跨平台开发中最大的挑战之一。Titanium 提供了Ti.Platform命名空间,以帮助您在代码中做出决策,如何处理运行时执行。

在以下菜谱中,我们将逐步介绍如何创建一个包含便利方法的PlatformHelpers CommonJS 模块,这些方法可以解决许多与平台相关的查询。以下截图展示了在 iPhone 和 Android 平台上运行此菜谱的情况。

使用平台指示器

准备工作

Ti.Platform命名空间对于提供平台和设备特定细节很有帮助,但通常您需要更高级别的细节,例如当您的应用在一个平板电脑上运行时,以及它是否是一个 iPad Mini。

将菜谱组件添加到您的项目中

PlatformHelpers模块添加到您的项目中很简单。只需将platform_helpers.js文件复制到您的项目中,如下截图所示:

将菜谱组件添加到您的项目中

如何做...

以下app.js演示了本章前面描述的PlatformHelpers模块。这个示例应用展示了一个包含设备详情的窗口。请在您的所有 Android 和 iOS 设备上尝试一下。

  1. 要创建我们的相同应用,首先我们声明我们的应用命名空间并导入我们的PlatformHelper模块。

    //Create our application namespace
    var my = {
      platformHelpers : require('platform_helpers')
    };
    
    (function(){
    
  2. 接下来,我们创建一个窗口,我们将在此窗口中展示设备信息。

    var win = Ti.UI.createWindow({
      backgroundColor:'#fff'
    });
    
  3. 接下来,我们创建一个空数组来存储我们的设备详情。

    var deviceData = [];
    

    如果您的设备正在模拟器上运行,则会将条目添加到deviceData

    isSimulator属性用于显示模拟器消息,仅当菜谱当前正在平台模拟器或仿真器上运行时。

    if(my.platformHelpers.isSimulator){
      deviceData.push({
        title:'Running on Simulator', child:false
      });
    }
    
  4. 接下来,将平台、操作系统名称和制造商添加到deviceData

    deviceData.push({
      title:'Platform: ' + 
      (my.platformHelpers.isAndroid ? 'Android' : 'iOS'), 
      child:false
    });
    
    deviceData.push({
      title:'osname: ' +  my.platformHelpers.osname, 
      child:false
    });
    
    deviceData.push({
      title:'manufacturer: ' +  
      my.platformHelpers.manufacturer, child:false
    });
    
  5. 以下语句向deviceData添加一个标志,指示当前设备是否为平板电脑形态:

    deviceData.push({
      title:(my.platformHelpers.isTablet ? 
      'Is a Tablet' : 'Is not a Tablet'), child:false
    });
    
  6. 接下来,我们在deviceData数组中添加设备型号,并指定它是否支持后台任务。

    deviceData.push({
      title:'Model: ' + my.platformHelpers.deviceModel, 
      child:false
    });
    
    deviceData.push({
      title:'Backgrounding Support: ' + 
      my.platformHelpers.supportsBackground,
      child:false
    });
    
  7. 屏幕尺寸是最常用的属性。以下代码片段将屏幕的高度和宽度添加到deviceData数组中:

    deviceData.push({
      title:'Height: ' + my.platformHelpers.deviceHeight +
      ' Width: ' + my.platformHelpers.deviceWidth,
      child:false
    });
    
  8. 如果您的应用当前正在 iOS 设备上运行,以下代码片段会将设备类型添加到deviceData数组中,并指定是否启用 retina。

    if(my.platformHelpers.isIOS){
      deviceData.push({
        title:'iOS Device Type: ' + 
        (my.platformHelpers.iPad ? 
        (my.platformHelpers.iPadMiniNonRetina ? 
        'iPad Mini' : 'Is an iPad') : 'iPhone'),
        child:false
      });
    
      deviceData.push({
        title:'Is Retina : ' + my.platformHelpers.isRetina,
        child:false
      });				
    }
    
  9. PlatformHelper模块提供了大量数据。为了最佳显示,我们将使用Ti.UI.TableView和我们在早期代码片段中构建的deviceData数组。

    var tableView = Ti.UI.createTableView({top:0, 
    data:deviceData});
    
      win.add(tableView);
      win.open();
    })(); 
    

它是如何工作的...

设备平台查找将频繁地在您的应用中访问。为了避免通过重复跨越 JavaScript 桥接导致的性能问题,我们将导入我们将要使用的值并将它们分配给我们的 CommonJS PlatformHelpers模块中的属性。

exports.osname = Ti.Platform.osname;
exports.manufacturer = Ti.Platform.manufacturer;
exports.deviceModel = Ti.Platform.model;
exports.deviceHeight = Ti.Platform.displayCaps.platformHeight;
exports.deviceWidth = Ti.Platform.displayCaps.platformWidth;
exports.densitylevel = Ti.Platform.displayCaps.density;
exports.deviceDPI = Ti.Platform.displayCaps.dpi;

对于您正在工作的平台,有一个布尔指示器通常很有帮助。以下代码片段展示了如何创建isAndroidisIOS属性来完成此操作:

exports.isAndroid = exports.osname === 'android';
exports.isIOS = (exports.osname === 'ipad' || 
exports.osname === 'iphone');

模拟器检查

根据您的平台,某些功能可能在该平台的模拟器或仿真器中无法工作。通过使用isSimulator属性,检测您所在的环境并根据情况分支您的代码。

exports.isSimulator = (function(){
  return (Ti.Platform.model.toUpperCase() === 'GOOGLE_SDK' || 
  Ti.Platform.model.toUpperCase()  === 'SIMULATOR' || 
  Ti.Platform.model.toUpperCase()  === 'X86_64')
})();

后台功能

移动应用通常需要在后台执行任务。此配方演示了如何执行基于版本的特性检查,以确定您的应用正在运行的设备是否支持后台处理/多任务处理。

exports.supportsBackground = (function(){

现在,执行以下步骤:

  1. 首先,检查用户是否正在 Android 上运行。如果是,则返回true,因为大多数 Android ROM 默认支持后台处理。

      if(exports.osname === 'android'){
        return true;
      }
    
  2. 接下来,确认该配方是否在 iOS 设备上运行。

      if(exports.osname === 'iphone'){
    
  3. 已检查版本以确保设备正在运行 iOS 4 或更高版本。这确认了操作系统支持后台处理。

        var osVersion = Ti.Platform.version.split(".");
        //If no iOS 4, then false
        if(parseInt(osVersion[0],10) < 4){
          return false;
        } 
    
  4. 最后,该配方确认设备版本高于 iPhone 3GS。这确认了硬件支持后台处理。

        var model = exports.deviceModel.toLoweCase()
        .replace("iphone","").trim();
        var phoneVersion = Ti.Platform.version.split(".");
        if(parseInt(phoneVersion[0],10) < 3){
          return false;
        } 		
      }
      //Assume modern device return true
      return true;
    
    })();
    

    注意

    根据平台,此功能可能被用户关闭。还建议进行二级容量检查。

检测平板电脑

在 Android 和 iOS 上,通用应用开发是一个常见的需求。以下助手提供了一个布尔指示器,如果您的应用正在平板电脑形态上运行,则返回true

//Determine if running on a tablet
exports.isTablet = (function() {

检查设备是否为 iPad 或至少有一个维度大于 700 像素的 Android 设备。

  var tabletCheck = exports.osname === 'ipad' || 
    (exports.osname === 'android' && 
    (!(Math.min(
    exports.deviceHeight,
    exports.deviceWidth
    ) < 700)));
    return tabletCheck;
})();

注意

对于 Android,此检查是否有高度或宽度大于 700 像素。您可能希望根据您的目标设备更改此值。例如,如果您正在针对一些大屏幕的 Android 手机,则需要将默认的 700 像素/点更新为反映它们具有大屏幕但仍被视为手机形态。

一款 4 英寸的 iPhone

随着 iPhone 5 的推出,我们需要注意两种不同的 iPhone 屏幕尺寸。以下代码片段展示了如何创建一个属性来指示您的应用是否运行在具有这种新屏幕尺寸的 iPhone 上。

exports.newIPhoneSize = (function(){

首先,验证该配方是否在 iPhone 设备上运行。

  if(exports.osname !== 'iphone'){
    return false;
  }

接下来,检查尺寸以查看是否有任何维度大于 480 点。

  return (Math.max(
    exports.deviceHeight,
    exports.deviceWidth
  ) > 480);
});

注意

请注意,前面的 newIPhoneSize 方法只有在应用已完全支持 4 英寸显示屏时才会返回 true。否则,它将返回 false,因为您的应用正在以信封模式运行。

iPad

Titanium 允许您在 iOS 上创建通用应用程序。使用以下属性,您可以为 iPad 用户分支代码以显示不同的功能:

exports.iPad = exports.osname === 'ipad';

iPad Mini

iPad Mini 的设计分辨率与第一代和第二代 iPad 相同。虽然设计为无需修改即可运行 iPad 应用,但较小的屏幕尺寸通常需要调整 UI 以适应较小的触摸目标。以下代码演示了如何确定您的应用是否在 iPad Mini 上运行:

exports.iPadMiniNonRetina= (function() {

现在执行以下步骤:

  1. 首先检查配方是否在非视网膜 iPad 上运行。

      if((exports.osname !== 'ipad')||
      (exports.osname === 'ipad' &&
      exports.densitylevel==='high')){
        return false;
      }
    
  2. 接下来验证非视网膜 iPad 不是 iPad 1 或 iPad 2。如果不是这些模块中的任何一个,假设配方正在 iPad Mini 上运行。

      var modelToCompare = exports.deviceModel.toLowerCase();
      return !(
        (modelToCompare==="ipad1,1")||
        (modelToCompare==="ipad2,1")||
        (modelToCompare==="ipad2,2")||
        (modelToCompare==="ipad2,3")||	
        (modelToCompare==="ipad2,4")		
      );
    })();
    

注意

苹果目前不提供 iPad Mini 的平台指示器。此检查使用型号编号,可能不是未来的解决方案。

使用 Ti.App.Listener 进行全局日志记录

Titanium 具有内置的触发和监听应用程序级事件的能力。这个强大的功能非常适合创建松散耦合的组件。这种模式可以提供解耦的日志记录方法,这对于特定情况(如分析、日志记录和处理监控模块)非常有帮助。

此配方详细说明了如何使用 Ti.App.Listener 创建一个解耦的可重用应用程序组件。以下截图展示了此配方在 iPhone 和 Android 平台上运行的情况:

使用 Ti.App.Listener 进行全局日志记录

准备工作

将此配方中使用的 CommonJS 模块添加到项目中非常简单,只需将 logger.jsmainWin.js 文件复制到钛项目根目录,如下截图所示:

准备工作

如何做到这一点...

应用程序级别的事件允许您在应用程序中全局实现发布/订阅模式,即使在执行上下文中也是如此。通过简单地添加如下监听器,您可以从应用程序的任何地方接收消息:

Ti.App.addEventListener('app:myEvent', myFunctionToHandleThisEvent);

在 Titanium 中触发自定义事件非常简单。您只需使用 Ti.App.fireEvent 并提供事件名称和可选的参数负载。此示例展示了如何调用我们之前定义的 app:myEvent 监听器。

Ti.App.fireEvent('app:myEvent',"My parameter objects");

小贴士

建议您使用描述性约定命名事件。例如,app:myEvent 表示这是一个应用程序事件,并在我的 app.js 文件中定义。

使用事件设计全局日志记录

以下配方展示了如何使用应用程序级别的自定义事件在应用程序的不同上下文中实现日志记录。

定义我们的 app.js

app.js 中,我们定义了应用程序命名空间并导入日志和 UI CommonJS 模块。

//Create our application namespace
var my = {
  ui:{
    mainWindow : require('mainWin').createWindow()
  },
  logger : require('logger')
};

在我们的 app.js 中,接下来调用 logger 模块的 setup 方法。这确保我们的数据库日志表被创建。

//Run setup to create our logging db
my.logger.setup();

在我们的 app.js 中定义应用程序级监听器。此监听器将等待触发 app:log 事件,然后使用提供的有效负载调用日志器的 add 方法。

//Add a global event to listen for log messages
Ti.App.addEventListener('app:log',function(e){
 //Provide Log Message to CommonJS Logging Component
 my.logger.add(e);
});

//Open our sample window
my.ui.mainWindow.open();

日志模块

以下代码片段演示了如何创建基本 CommonJS 模块 logger.js,该模块用于我们的全局监听器配方。此模块有两个方法;setup,用于创建我们的数据库对象,以及 add,监听器调用它来记录我们的消息。

创建一个具有日志数据库名称的常量。

var LOG_DB_NAME = "my_log_db";

创建 setup 模块级方法。此方法用于创建 LOG_HISTORY 表,该表将用于记录所有日志语句。

exports.setup=function(){
  var createSQL = 'CREATE TABLE IF NOT EXISTS LOG_HISTORY '; 
  createSQL +='(LOG_NAME TEXT, LOG_MESSAGE TEXT, ';
  createSQL += 'LOG_DATE DATE)';
  //Install the db if needed
  var db = Ti.Database.open(LOG_DB_NAME);
  //Create our logging table if needed
  db.execute(createSQL);
  //Close the db
  db.close();
};

创建 add 模块级方法。此方法用于记录要添加到日志表中的信息。

exports.add=function(logInfo){
  var insertSQL ="INSERT INTO LOG_HISTORY ";
  insertSQL +=" (LOG_NAME,LOG_MESSAGE,LOG_DATE)"; 
  insertSQL +=" "VALUES(?,?,?)";
  var db = Ti.Database.open(LOG_DB_NAME);
  //Create our logging table if needed
  db.execute(insertSQL,logInfo.name,logInfo.message, 
  new Date());
  //Close the db
  db.close();
};

将所有内容整合在一起

以下部分展示了我们的 mainWin.js 模块的内容。此模块返回一个包含单个按钮的窗口,当按钮被按下时,会触发我们的日志事件。

窗口和模块变量设置

fetchWindow 函数返回一个简单的窗口,展示了如何触发应用程序级事件。为了演示,每次都会发送一条新消息,并且我们添加一个名为 _press_Count 的模块级变量,每次点击都会增加。

创建模块级变量 _press_Count 以记录 addLogButton 被按下的次数。

var _press_Count = 0;

fetchWindow 方法返回一个包含此配方 UI 元素的 Ti.UI.Window

exports.fetchWindow=function(){

为所有 UI 元素创建一个 Ti.UI.Window

  var win = Ti.UI.createWindow({
    backgroundColor:'#fff'
  });

创建 addLogButton Ti.UI.Button。此按钮将用于触发用于在此配方中演示全局监听器的 Ti.App.fireEvent

  var addLogButton = Ti.UI.createButton({
    title:"Fire Log Event", top:180, 
    left:5,right:5, height:50
  });

触发应用程序级事件

当点击 addLogButton 按钮时,_press_Count 变量增加一,并创建一个日志对象。以下代码演示了如何调用 Ti.App.fireEvent 方法来触发 app:log 事件,并提供包含我们的日志详细信息的 JavaScript 对象。然后,我们在 app.js 中定义的监听器将接收此消息并记录我们的结果。

  addLogButton.addEventListener('click',function(e){		

用户点击 addLogButton 后采取的第一个动作是将 _pressCount 增加。

    _press_Count ++;

接下来创建包含要提交的日志信息的 logObject

    var logObject = {
      name:'test log',
      message:'example message ' + 
      _press_Count
    };

最后,调用 Ti.App.fireEvent 以触发 app:log 事件。在调用此应用程序全局事件时,还提供了 logObject 作为参数。

    Ti.App.fireEvent('app:log',logObject);
  });    
  win.add(addLogButton);

  //Return window
  return win;
};

注意

应用程序日志记录只是众多示例之一,展示了您如何使用应用程序级事件来解耦应用程序的组件。在整个书籍的阅读过程中,当需要解耦组件交互时,此模式将被多次使用。

更多...

由于应用程序级别的监听器是全局作用域的,你在定义它们的位置和时间需要格外小心。一个一般性的规则是,在应用程序始终可用且永远不会重新加载的区域定义应用程序级别的监听器,例如你的app.js文件,这是一个只加载一次的 CommonJS 文件,或者另一种引导方法。

如果你必须在窗口或其他重复加载的模块内定义它们,确保当对象不再需要时移除监听器。要移除事件监听器,你需要使用与创建时相同的参数调用Ti.App.removeEventListener方法。以下代码片段展示了如何移除之前创建的app:myEvent监听器:

Ti.App.removeEventListener('app:myEvent', myFunctionToHandleThisEvent);

小贴士

如果你没有移除监听器,所有相关的 JavaScript 对象将不符合垃圾回收的条件,这通常会导致内存泄漏。

第二章. 跨平台 UI

在本章中,我们将涵盖:

  • 跨平台 HUD 进度指示器

  • 应用内通知

  • 屏幕断点菜单

  • Metro 样式标签控件

  • 滑出菜单

简介

Titanium 不是一个一次编写到处运行的应用程序框架,而是一个一次编写,针对每个平台修改的框架。现在没有比在应用程序的用户界面中这一点更重要了。出色的用户体验涉及让用户感到熟悉,同时突出显示应用程序运行的设备。Titanium 通过拥抱通用和平台特定的 UI 小部件来实现这一点。

本章中的食谱将演示如何创建有用的 UI 小部件,为您的企业用户提供独特的跨平台体验。每个食谱都设计得易于品牌化和在现有的 Titanium Enterprise 应用程序中使用。

跨平台 HUD 进度指示器

抬头显示HUD)交互模式,以 UIKit 的UIProgressHUD组件命名,是一种向用户提供进度信息的有效方式。这对于企业移动应用尤其如此,因为它们通常与后端系统深度集成。因此,在执行这些调用时,应用程序不会显得缓慢或无响应。建议使用WaitingProgress指示器向用户提供反馈。本食谱演示了如何使用HUD模块来显示一个Waiting指示器,通知您的用户关于应用程序中长时间运行的操作的进度。

准备工作

这个可链式 CommonJS HUD模块,hud.js,提供了原生 iOS 和 Android 进度指示器体验。这是一个简单的食谱,演示了如何使用HUD模块。您可以使用此示例轻松地将模块集成到您的 Titanium 项目中。

准备工作

将 HUD 模块添加到您的项目中

HUD模块添加到您的项目中非常简单。只需将hud.js文件复制到项目中的Resources文件夹,如下截图所示:

将 HUD 模块添加到您的项目中

一旦您将hud.js文件添加到项目中,您需要使用require将模块导入到您的代码中:

//Create our application namespace
var my = {
    hud : require('hud'),
    isAndroid : Ti.Platform.osname === 'android'
};

创建一个示例窗口

为了演示HUD模块,我们创建了一个带有单个按钮的基本窗口。为此,首先我们创建一个Ti.UI.Window并附加一个按钮:

var win = Ti.UI.createWindow({
  backgroundColor:'yellow'
});

var hudButton = Ti.UI.createButton({
  title:'Show HUD', height:50, right:5, left:5, bottom: 40
});
win.add(hudButton);

添加 HUD 监听器

在我们的示例中接下来,我们添加一系列的监听器。以下代码块演示了如何添加一个监听器,当 HUD 窗口关闭时触发:

my.hud.addEventListener('close',function(e){
  Ti.API.info('HUD Window Closed');
});
  1. 您还可以监听一个open事件,当 HUD 窗口打开时将触发:

    my.hud.addEventListener('open',function(e){
      Ti.API.info('HUD Window Open');
    });
    
  2. 当用户在 HUD 窗口上双击时,会触发dblclick事件:

    my.hud.addEventListener(dblclick,function(e){
      Ti.API.info('HUD Window double clicked');
    });
    
  3. 当调用updateMessage来更新 HUD 窗口消息时,会触发hudTextChanged事件:

    my.hud.addEventListener('hudTextChanged',function(e){
      Ti.API.info('Text Changed from ' + e.oldValue + ' to ' + e.newValue);
    });
    
  4. 如果您正在使用 关闭计时器 功能,了解您的窗口是因计时器还是其他方法而关闭的很有帮助。为了帮助确定这一点,您可以订阅 timerClose 事件,如下面的代码片段所示:

    my.hud.addEventListener('timerClose',function(e){
      Ti.API.info('HUD Window Closed by Timer');
    });
    

创建 HUD 指示器

我们现在可以使用之前创建的按钮,来演示如何显示 HUD 窗口。我们为按钮的点击处理程序添加了逻辑,以使用消息文本 请等待... 加载 HUD 模块。然后我们调用 show 方法,该方法将 HUD 窗口显示给用户。

hudButton.addEventListener('click',function(e){
  win.navBarHidden=true;
  my.hud.load('Please Wait...').show();
});

小贴士

在 Android 上,您可能希望隐藏导航栏,以便 HUD 窗口可以占据整个屏幕。如果您不隐藏导航栏,HUD 模块将设置窗口标题与提供的 HUD 显示消息相同的文本。

更新 HUD 消息

通过调用 updateMessage 函数,您可以在任何时间更新 HUD 窗口的文本。以下行演示了如何将文本从 请等待... 更新为 仍在等待...

my.hud.updateMessage('Still waiting...');

关闭 HUD 窗口

有两种方法可以关闭 HUD 模块窗口。第一种也是最常见的方法是调用 hide 函数,如下所示:

my.hud.hide();

关闭 HUD 窗口的第二种方法是,在调用 show 方法之前,在 HUD 模块上使用 addCloseTimer 函数,如下面的代码片段所示。在这个例子中,HUD 窗口将在 5,000 毫秒后关闭。

my.hud.load('Please Wait...').addCloseTimer(5000).show();

小贴士

addCloseTimer 函数有助于处理超时情况。例如,在执行网络调用时,您可能希望在五秒后放弃该过程,以免让用户等待过长时间。

它是如何工作的...

HUD 模块提供了一系列用于创建、更新和维护 HUD 进度窗口的函数。以下列表提供了每个方法的描述以及它们相关的功能:

  • load 函数:此函数用于构建 Ti.UI.WindowTi.UI.ActivityIndicatorTi.UI.Label,这些将在以后用于显示您的进度指示器。请注意,您需要首先调用 load 函数,然后调用 show 函数,以显示 HUD 窗口。

  • show 函数:此函数用于显示 HUD 窗口。请注意,在调用 show 之前,您首先需要调用 load 方法。如果顺序错误,HUD 窗口将不会显示。

  • updateMessage 函数:要更新 HUD 标签,您需要调用此函数,并提供一个包含要显示的更新文本的字符串。

  • hide 函数:调用此函数将关闭 HUD 窗口并移除所有创建的 HUD 对象。

  • addCloseTimer 函数:您可以使用此函数设置一个计时器,在指定的持续时间(以毫秒为单位)后关闭 HUD 窗口。

  • removeCloseTimer 函数:您可以使用此函数关闭使用 addCloseTimer 设置的计时器。

  • addEventListener 函数:HUD 模块支持下一节中详细说明的几个事件。您可以使用此函数订阅这些事件。

  • removeEventListener函数:您订阅的任何事件都可以通过调用此函数来移除。要移除事件,您需要提供与调用addEventListener时相同的名称和回调参数。

    小贴士

    HUD 模块中的所有函数都是可链式的,类似于 jQuery 函数。

应用内通知

常常需要提醒用户某些事情已经发生。Titanium 允许您使用alert函数,就像在网页上做的那样。但是,这种模态警告通常可能限制性较强,不符合您的设计要求。

这个菜谱介绍了如何使用NotifyUI模块创建跨平台的、品牌化的通知窗口。

准备就绪

NotifyUI是一个由单个 JavaScript 文件notify.js和一些用于样式的图像资产组成的 CommonJS 模块。您可以通过配置或更新核心图像文件来控制模块的样式,使其容易适应您现有的品牌要求。

这个菜谱是关于如何使用NotifyUI模块的一个简单演示。您可以使用这个示例快速将模块集成到您的 Titanium 项目中。

以下截图显示了此菜谱的实际操作:

准备就绪

将 NotifyUI 模块添加到您的项目中

NotifyUI模块添加到您的项目中非常简单。只需将NotifyUI文件夹复制到 Titanium 项目的Resources文件夹中,如以下截图所示。这将安装 CommonJS 模块及其所有支持图像。

将 NotifyUI 模块添加到您的项目中

小贴士

为了使这个菜谱按预期工作,必须将NotifyUI文件夹复制到您的 Titanium 项目的Resources文件夹中。

如何操作…

一旦您将NotifyUI文件夹添加到项目中,您需要使用require将模块导入到您的代码中:

//Create our application namespace
var my = {
  notify = require('./NotifyUI/notify'),
  isAndroid : Ti.Platform.osname === 'android'
};

创建示例窗口

为了演示NotifyUI模块,我们创建了一个包含两个按钮的基本窗口:

var win = Ti.UI.createWindow({
  backgroundColor:'#fff'
});

var showNotifyButton = Ti.UI.createButton({
  title:'Show Notification', top: 100, 
  height:50, right:5, left:5
});
win.add(showNotifyButton);

var closeNotifyButton = Ti.UI.createButton({
  title:'Close Notification', top: 200, 
  height:50, right:5, left:5
});
win.add(closeNotifyButton);

添加 NotifyUI 监听器

在我们的示例中接下来,我们添加了一系列监听器。以下代码块演示了如何添加一个监听器,当NotifyUI通知窗口关闭时触发:

my.notify.addEventListener('close',function(e){
  Ti.API.info(Notify Window Closed');
});
  1. 您还可以监听open事件,该事件在通知窗口打开时触发。

    my.notify.addEventListener('open',function(e){
      Ti.API.info('Notify Window Open');
    });
    
  2. 当用户双击通知窗口时,会触发dblclick事件。

    my.notify.addEventListener('dblclick',function(e){
      Ti.API.info('Notify Window double clicked');
    });
    
  3. 当调用updateMessage来更新通知窗口消息时,textChanged事件由NotifyUI模块触发。

    my.notify.addEventListener('textChanged',function(e){
      Ti.API.info('Text Changed from ' + e.oldValue + ' to ' + e.newValue);
    });
    

如果您使用的是关闭计时器功能,了解窗口是通过计时器关闭还是通过其他方法关闭是有帮助的。为了帮助确定这一点,您可以订阅timerClose事件,如下面的代码片段所示:

my.notify.addEventListener('timerClose',function(e){
  Ti.API.info('Notify Window Closed by Timer');
});

显示消息窗口

我们现在可以使用之前创建的按钮来演示如何显示通知窗口。我们为按钮的点击处理程序添加了逻辑来 load 模块,包含消息文本 Hello World 和样式 complete,然后调用 show 方法,该方法向用户显示通知窗口。

showNotifyButton.addEventListener('click',function(e){
  win.navBarHidden = true;
  my.notify.load({
    style:'complete',
    message:'Hello World'
  }).show();
});

更新消息

通过调用 updateMessage 函数,你可以随时更新通知文本。以下演示了如何将文本从 Hello World 更新为 I'm a new message

my.notify.updateMessage("I'm a new message");

关闭消息窗口

有三种方式可以关闭通知窗口。第一种也是最常见的方式是调用 hide 函数,如下面的代码片段所示:

my.notify.hide();

关闭通知窗口的第二种方式是在调用 show 函数之前使用 addCloseTimer 函数,如下面的代码块所示。在这个例子中,窗口将在 5,000 毫秒(5 秒)后自动关闭。

my.notify.load({
  style:'complete',
  message:'Hello World'
}).addCloseTimer(5000).show();

关闭通知窗口的第三种和最后一种方式是让用户双击消息。这个手势将触发模块内部处理关闭操作。

它是如何工作的…

NotifyUI 模块提供了一系列用于创建、更新和维护通知窗口的函数。以下列表提供了每种方法的描述,以及它们相关的功能:

  • load 函数:此函数用于构建 Ti.UI.WindowTi.UI.Label 和相关的通知样式,这些样式将用于稍后显示通知。请注意,你需要首先调用 load 函数,然后调用 show 函数来显示通知窗口。

  • show 函数:此函数用于显示通知窗口。请注意,在调用 show 之前,你需要首先调用 load 方法。如果顺序错误,通知窗口将不会显示。这些顺序操作提供了分离加载和显示操作的灵活性。如果你有多个连续的通知事件,这可能很重要。

  • updateMessage 函数:要更新通知标签,你需要调用此函数,并为其提供一个包含要显示的更新文本的字符串。

  • hide 函数:调用此函数将关闭通知窗口并移除所有创建的 NotifyUI 对象。

  • addCloseTimer 函数:你使用此函数来设置一个计时器,该计时器将在提供的持续时间(以毫秒为单位)后关闭通知窗口。

  • removeCloseTimer 函数:使用此函数可以关闭使用 addCloseTimer 设置的计时器。

  • addEventListener 函数:NotifyUI 模块支持下一节中详细说明的几个事件。你可以使用此函数来订阅这些事件。

  • removeEventListener 函数:你可以通过调用此函数来移除你已订阅的任何事件。要移除一个事件,你需要提供与调用 addEventListener 时相同的参数。

    小贴士

    大多数 NotifyUI 函数都是可链式的,提供了一种类似于 jQuery 提供的模式,便于使用。

内置消息样式

NotifyUI 模块包含以下内置样式:

  • 完成:这会显示一个带有蓝色背景和默认文本“完成”的窗口

    my.notify.load({style:'complete'}).show();
    

    内置消息样式

  • 错误:这会显示一个带有红色背景和默认文本“错误”的窗口

    my.notify.load({style:'error'}).show();
    

    内置消息样式

  • 警告:这会显示一个带有橙色背景和默认文本“警告”的窗口

    my.notify.load({style:'warning'}).show();
    

    内置消息样式

  • 成功:这会显示一个带有绿色背景和默认文本“成功”的窗口

    my.notify.load({style:'success'}).show();
    

    内置消息样式

    小贴士

    所有默认消息文本和文本颜色都可以通过向 load 函数调用提供 messagemessageColor 对象属性来更新。

屏幕断点菜单

企业应用程序通常包含许多功能。提供一种引人入胜的方式来显示这些功能和子菜单可能具有挑战性。一个独特的导航选项是实现屏幕断点菜单,类似于 iOS 文件夹的工作方式。这种导航技术允许您仅在用户请求时呈现额外信息。

在本食谱中,我们将演示如何实现跨平台的屏幕断点动画,并将其链接到菜单视图。

准备工作

本食谱通过使用屏幕断点交互模式来创建额外的空间,用于“添加笔记”字段。您可以轻松调整此食谱以满足更复杂的菜单需求,例如实现高级选项或交互式帮助菜单。

菜单关闭时

当菜单关闭时,所有空间都可以用于其他控件。以下图片显示了屏幕断点菜单在关闭状态下的样子。

菜单关闭时

菜单打开时

当菜单打开时,屏幕被分割,Ti.UI.Window 的底部区域会进行动画处理,暴露出菜单区域。以下示例显示了打开的屏幕断点菜单,并显示了一个 Ti.UI.TextArea 用于记笔记:

菜单打开时

将屏幕断点菜单添加到您的项目中

屏幕断点菜单是一个由单个 JavaScript 文件组成的 CommonJS 模块。要安装它,只需将 breakmenu.js 文件复制到您的项目中,如下面的截图所示:

将屏幕断点菜单添加到您的项目中

如何操作…

一旦将 breakmenu.js 文件添加到您的项目中,您需要使用 require 将模块导入到您的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  breakingMenu : require('breakmenu')
};

创建示例窗口

为了演示屏幕断点菜单,我们创建了一个带有单个按钮的基本窗口。当点击此按钮时,屏幕断点菜单会被激活。

var win = Ti.UI.createWindow({
  backgroundColor:'#fff'
});

//Create button
var button = Ti.UI.createButton({
  title:'Add Notes', left:20, right:20, height:50, top:150
});
win.add(button);

添加屏幕断点菜单监听器

在我们的示例中接下来,我们添加了一系列监听器。以下代码块演示了如何添加一个监听器,当屏幕断开菜单开始时触发,以显示菜单。

my.breakingMenu.addEventListener('breaking',function(e){
  Ti.API.info("Screen is breaking");
});
  1. 当菜单完全显示时,会触发broken事件。

    my.breakingMenu.addEventListener('broken',function(e){
      Ti.API.info("Screen is broken, menu is visible");
    });
    
  2. 当菜单开始关闭时,会触发closing事件。这将花费一秒钟才能完全关闭。

    my.breakingMenu.addEventListener('closing',function(e){
      Ti.API.info('Menu starting to close');
    });
    
  3. 只有当菜单完全隐藏时,才会触发closed事件。

    my.breakingMenu.addEventListener('closed',function(e){
      Ti.API.info('Menu is closed');
    });
    

创建笔记菜单对象

显示Ti.UI.View是屏幕断开菜单的核心。在这个例子中,我们创建了一个包含Ti.UI.ViewTi.UI.TextArea控件的笔记菜单对象。这些控件用于在屏幕断开菜单可见时,向用户提供一个输入笔记的区域。

var notesMenu = {
  value : '', //Our value for the notes screen
  view : function(){
    //Create a view container
    var vwMenu = Ti.UI.createView({
      backgroundColor:'yellow',top:150,
      height:200, width: Ti.UI.FILL
    });
    //Create a textArea for the user to type
    var txtField = Ti.UI.createTextArea({
      width: Ti.UI.FILL,height:Ti.UI.FILL,
      hintText:"Add a note", value : notesMenu.value, backgroundColor:'transparent',font:{fontSize:14,fontWeight:'bold'}
    });
    vwMenu.add(txtField);
    //When text is added, update the menu value
    txtField.addEventListener('change',function(e){
      notesMenu.value = txtField.value; 
    });
    //Return the view so we can later bind for display
    return vwMenu;
  }
};

显示菜单

当样本按钮被按下时,会调用breakScreen函数以显示屏幕断开菜单。

breakScreen函数接受以下参数:

  • 第一个参数是您希望分割以显示菜单的Ti.UI.Window

  • 第二个参数是您希望用作菜单的Ti.UI.View。请注意,这只能是一个Ti.UI.View对象类型。

  • 最后一个参数是settings对象。此对象包含以下属性:

    • breakStartPoint:从屏幕顶部测量的位置,表示断开应该开始的位置。

    • bottomViewHeight:此属性决定下半部分的屏幕高度。此值决定了屏幕下半部分的大小,该部分将沿着屏幕向下动画。

    • slideFinishPoint:从屏幕顶部测量的位置,表示底部视图应该滑动到的位置。

      小贴士

      您需要调整这些settings属性以满足您的屏幕布局需求。

以下代码块实现了所有三个属性:

button.addEventListener('click',function(e){
  //Options object
  var settings = {};
  //Point of the screen where the screen will separate
  settings.breakStartPoint = 150; 
  //Size of the bottom half of screen, ie the sliding part
  settings.bottomViewHeight = 218, 
  //The bottom point you want the screen to slide to. 
  //Measured from top of screen
  settings.slideFinishPoint = 340;			
  //Call function to split the screen & show our menu
  my.breakingMenu.breakScreen(win,notesMenu.view(),settings);
});

Metro 风格标签控件

分页界面是组织您的企业钛金应用的有效方式。随着智能手机和精心设计、引人入胜的 UI/UX 消费者应用的日益普及,企业用户对 UI 丰富的企业应用的需求水平正在提高。您的普通企业用户现在开始期待比传统分页界面提供更丰富的体验。

本食谱演示了如何使用Metro 风格标签控件为您的企业应用创建独特的跨平台体验。此控件允许您以引人入胜的新方式使用分页交互模式,同时满足您的品牌或组织风格要求。

准备工作

本食谱通过创建一个用于组织应用导航的 Metro 风格标签控件的过程进行说明。标签控件设计得易于配置,以满足您的企业品牌和显示需求。

下一个示例显示了位于我们跨平台应用顶部的简单 3 标签导航器。以下各节将指导您创建此样本,并演示如何配置控件以满足您的特定需求。

准备中

将标签控制添加到你的项目中

Metro Tab Control是一个 CommonJS 模块,由一个 JavaScript 文件组成。要安装,只需将tabcontrol.js文件复制到你的项目中,如下面的屏幕截图所示:

将标签控制添加到你的项目中

如何做到这一点...

一旦你将tabcontrol.js文件添加到你的项目中,你需要使用require将模块导入到你的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  tabControl : require('tabcontrol')
};

创建示例窗口

要演示 Metro 样式标签控制,创建一个Ti.UI.Window来附加这个自定义控制,并带有三个标签。

var win = Ti.UI.createWindow({
  backgroundColor:'#fff',title:"Tab Control Example", 
  fullscreen:false, navBarHidden:true
});

创建设置对象

当创建标签控制时,你需要一个tabSettings对象和一个标签数组。tabSettings对象包含创建标签控制所需的所有属性。然后,将这些标签添加到控制中。在创建tabSettings对象时,你可以使用任何有效的Ti.UI.ScrollView属性,因为标签控制是在 Titanium 原生Ti.UI.ScrollView的基础上提供包装的。

tabSettings对象还有一个特定于标签控制的名为selectedCue的对象。selectedCue对象包含selectedBackgroundColorbackgroundColor属性。selectedBackgroundColor属性是在标签被选中时提供视觉提示所使用的颜色。backgroundColor属性是用于视觉提示区域的默认背景颜色。

var tabSettings = {
  top:0, //Position the control a top of win
  selectedCue : {
    selectedBackgroundColor:'red',
    backgroundColor:'transparent',
  }
};

定义标签

你在创建标签控制时定义所有标签。对于这个例子,我们首先创建一个空数组,然后将每个标签定义推入数组中。

var tabs = [];//Create tab collection

标签定义有两个主要部分,即TabLabel部分。在Tab部分,你可以使用在创建Ti.UI.View对象时有效的任何参数。任何额外的参数将被视为Tab对象的自定义属性,一旦标签控制被添加到Ti.UI.Window中,这些属性将可用。

Label部分,你可以使用在创建Ti.UI.Label对象时有效的任何参数。Label部分还有一个特定于标签的属性,称为selectedFontselectedFont对象具有与Ti.UI.Label字体对象相同的属性,但仅在标签被选中时应用。以下示例用于演示字体大小和字重的增加。

小贴士

确保在你的标签定义或TabStrip中包含一个width属性,否则布局将不正确。

var tab0 = {
  Tab : {
    left:1, width:75, height:50
  },
  Label :{
    text:'tab0',
    selectedFont:{
      fontSize:22, fontWeight:'bold'
    },
    font:{
      fontSize:14
    }
  }
};
tabs.push(tab0); //Add tab to collection

要完成这个食谱,使用前面演示的相同模式添加tab1tab2

将标签控制添加到窗口中

在这个食谱的下一步中,需要调用createTabStrip方法,提供我们的设置对象和标签数组。此方法返回一个自定义标签控制的实例,然后将其添加到示例的Ti.UI.Window中,如下面的代码片段所示:

//Create our tab control
var tabControl = my.tabControl.createTabStrip(tabSettings,tabs);
//Add our tab control to the window
win.add(tabControl);

添加标签监听器

Metro Tab Control 是从Ti.UI.ScrollView派生出来的。因此,它继承了其基本监听器,如pinchscrollclick。还有其他事件可供您订阅。

  1. 当用户点击标签时,将触发tabClick事件,提供被点击标签的索引。

    tabControl.addEventListener('tabClick',function(e){
      Ti.API.info("Tab at Index: " + e.index + " was clicked");
    });
    
  2. 当选择标签时,将触发indexChanged事件,提供当前和之前的索引值。

    tabControl.addEventListener('indexChanged',function(e){
      Ti.API.info("Tab Index changed from " + e.oldIndex +" to " + e.index);
    });
    

Slideout Menu

使用 Slideout 菜单进行导航已成为显示导航选项的一种流行方式。这种导航模式由 Facebook 和 Path 应用推广。这种导航模式的优点是它可以有效地展示大量的菜单选项。大多数企业应用都可以从这种类型的导航中受益,因为它增加了功能可发现性。

准备中

在这个菜谱中,我们将演示如何使用 Slideout 菜单创建一个示例应用,通过它访问四个应用视图,以及一个注销按钮。Slideout Menu模块提供了一个可链式 CommonJS 模块,可以轻松管理您的菜单和视觉资产,这些资产可以轻松地进行品牌化。

以下截图显示了在我们的示例跨平台应用中打开的 Slideout 菜单。以下步骤将指导您创建此示例,并演示如何配置控件以满足您的需求。

准备中

安装 Ti.Draggable 模块

在 Android 上运行时,Slideout 菜单使用一个流行的本地模块,称为Ti.Draggable。此模块使用户能够轻松地滑动菜单封面。

要安装此模块,您可以轻松地从菜谱的源代码中复制modules目录,或者从github.com/pec1985/TiDraggable下载Ti.Draggable项目。一旦您有了解压的模块,您将希望将其内容复制到您的项目modules文件夹中,如下面的截图所示:

安装 Ti.Draggable 模块

在您将文件复制到modules目录后,您需要将模块添加到您的项目中。在 Titanium Studio 中,通过点击tiapp.xml文件可以轻松完成此操作。一旦项目配置屏幕加载,点击下面的绿色加号按钮(如下面的截图所示)。

安装 Ti.Draggable 模块

然后,您将看到一个包含您已安装的所有模块的列表。选择ti.draggable [android]模块,如下面的截图所示。选择此模块后,点击OK将引用模块添加到您的项目中。

安装 Ti.Draggable 模块

一旦您添加了模块,在运行之前请确保保存您的项目。保存后,您需要清理您的项目。您可以通过在 Titanium Studio 的项目工具栏选项下选择清理…来完成此操作,如下面的截图所示:

安装 Ti.Draggable 模块

将 Slideout Menu 模块添加到您的项目中

Slideout Menu模块添加到您的项目中很简单。只需将SlideOutMenu文件夹复制到您的 Titanium 项目中的Resources文件夹,如下面的截图所示。这将安装 CommonJS 模块及其所有支持图像。

将 Slideout Menu 模块添加到您的项目中

小贴士

为了使这个食谱按设计工作,SlideOutMenu文件夹必须位于您的 Titanium 项目根目录中。

如何做到这一点...

SlideOutMenu文件夹添加到您的项目后,您需要使用require将模块导入到您的代码中。

//Create our application namespace
var my = {
  views:{},
  menu : require('./SlideOutMenu/MASlidingMenu')
};

定义我们的内容视图

对于这个示例,我们创建了一个 CommonJS 模块,它作为一个视图工厂工作。在您的应用程序中,这些视图将是您希望在屏幕上显示的内容。

my.sampleView = require('sample_views');

以下代码片段构建了一个包含我们由示例视图工厂创建的占位符视图的views对象。

my.views = {
  home : my.sampleView.placeholderView({
    title:'Home',backgroundColor:'blue'}),
  inbox : my.sampleView.placeholderView({
    title:'Home',backgroundColor:'green'}),
  sales : my.sampleView.placeholderView({
    title:'Home',backgroundColor:'yellow'}),
  customers : my.sampleView.placeholderView({
    title:'Home',backgroundColor:'blue'}),
  about : my.sampleView.placeholderView({
    title:'About',backgroundColor:'purple'})	
};

构建我们的菜单项

我们食谱的下一步是定义我们的菜单项。当菜单打开时,菜单将显示一系列菜单项,如下面的代码片段中的menuData所示。

每个菜单项可以包含以下属性:

  • 标题:此属性用于显示菜单项的文本。

  • 细节提示:属性hasDetail确定是否显示更多箭头视觉提示。此属性是可选的,默认为false

  • 视图指针:可选属性视图用于保存当菜单项被按下时应显示的视图的引用。

    var menuData = [
      { title:'Home', hasDetail:true, view: my.views.home },
      { title:'Inbox', hasDetail:true, view: my.views.inbox },
      { title:'Sales', hasDetail:true, view: my.views.sales },
      { title:'Customers', hasDetail:true, 
    view: my.views.customers },
      { title:'About', hasDetail:true, view: my.views.about },
      { title:'Logout' }
    ];
    

打开应用程序窗口

要打开示例窗口,您首先需要调用addMenuItems函数并提供我们之前创建的menuData数组。这将创建模块内的菜单对象。接下来,您需要将addMenuItems函数的结果传递给open方法。这将打开您的主应用程序窗口并显示菜单中的第一个视图。

my.menu.addMenuItems(menuData).open();

添加菜单监听器

Slideout 菜单提供了对buttonclickswitchopenclosesliding事件的监听。以下详细说明了在示例中使用这些事件。

  1. 当菜单项被点击时,会触发buttonclick事件,提供菜单项的索引。

    my.menu.addEventListener('buttonclick', function(e) {
      if (e.index === 2) {
        alert('You clicked on Logout');
      }
    });
    
  2. 当显示视图切换到另一个视图时,会触发switch事件。此事件可用于帮助加载显示视图的内容。

    my.menu.addEventListener('switch', function(e) {
      Ti.API.info('menuRow = ' + JSON.stringify(e.menuRow));
      Ti.API.info('index = ' + e.index);
      Ti.API.info('view = ' + JSON.stringify(e.view));
    });
    
  3. 当用户滑动显示视图以显示菜单时,会触发sliding事件。这提供了视图距离。

    my.menu.addEventListener('sliding', function(e) {
      Ti.API.info('distance = ' + e.distance);
    });
    
  4. 当菜单完全显示时,会触发open事件。此事件有助于跟踪视觉状态。

    my.menu.addEventListener('menuOpened', function(e) {
      Ti.API.info('Menu is open');
    });
    
  5. 当菜单完全关闭时,会触发menuClosed事件。此事件有助于跟踪视觉状态。

    my.menu.addEventListener('menuClosed ', function(e) {
      Ti.API.info('Menu is closed');
    });
    

添加自定义应用程序监听器

我们的示例应用程序还使用了以下自定义应用程序级别事件。这些事件由我们的示例视图触发,用于关闭和切换菜单。

Ti.App.addEventListener('app:toggle_menu',function(e){
  Ti.API.info('App Listener called to toggle menu');
  my.menu.toggle();
});
Ti.App.addEventListener('app:close_menu',function(e){
  Ti.API.info('App Listener called to close menu');
  my.menu.hide();
});

它是如何工作的…

Slideout Menu模块提供了几个函数,帮助您构建应用程序的导航系统。

创建菜单

以下代码片段演示了如何使用addMenuItems辅助函数通过菜单项列表创建菜单。请参阅本食谱的如何做…部分以获取示例。

my.menu.addMenuItems(menuData);

打开菜单容器

以下行演示了如何打开菜单容器并显示第一个菜单项。请参阅本食谱的如何做…部分以获取示例。

my.menu.open();

小贴士

在菜单容器可以打开之前,需要调用addMenuItems函数。

显示菜单

以下行演示了如何完全显示菜单:

my.menu.expose();

切换菜单

在设计您的导航器时,您将希望实现一个按钮,允许用户打开和关闭菜单。以下代码片段显示了toggle方法如何执行此操作:

my.menu.toggle();

关闭菜单

以下行演示了您如何隐藏菜单:

my.menu.hide();

确定菜单状态

您可以使用以下行来确定菜单是否现在已完全显示:

Ti.API.info("Menu is open " + my.menu.isOpen());

访问当前视图

您通常会需要访问当前显示的视图以执行输入验证或其他操作。以下行演示了您如何轻松访问当前视图:

var current = my.menu.currentView();

关闭菜单容器

以下行演示了如何关闭菜单容器及其所有相关显示视图:

my.menu.dispose();

使用示例的全局事件

我们的每个示例视图都可以触发两个有用的自定义应用程序级事件。第一个事件是app:toggle_menu,当您点击菜单图标时被调用。这将根据其当前状态显示或隐藏菜单。

toMenu.addEventListener('click', function(){
  Ti.App.fireEvent('app:toggle_menu');
});

通常,用户希望在菜单完全关闭之前就开始使用屏幕。为了方便起见,每个显示视图将在双击时调用app:close_menu事件。这将自动关闭视图,并允许用户继续在当前屏幕上工作。

view.addEventListener('dblclick',function(e){
  Ti.App.fireEvent('app:close_menu');
});

相关阅读

第三章:使用数据库和管理文件

在本章中,我们将涵盖:

  • 访问您的数据库的 Ti.Filesystem

  • DbTableChecker SQLite 表存在性检查

  • 使用 Dossier 递归处理文件

  • 对 SQLite 数据库进行调优以获得最佳性能

  • 使用 DbLazyProvider 进行数据访问

  • 使用 MongloDb 进行 NoSQL

简介

Titanium 有几个组件旨在帮助处理您的数据需求。此外,Titanium 开源社区还提供了丰富的替代数据处理选项。

本章中的食谱展示了在设备上处理数据时各种有用的方法。从文件系统管理到 SQLite 调优,无论您首选哪种方法,都将有一个方法来协助您的企业数据需求。

访问您的数据库的 Ti.Filesystem

钛金属包含一个强大的数据库 API,它提供了对底层平台 SQLite 实现的便捷访问。通过为您的应用程序的 SQLite 数据库访问Ti.Filesystem,您可以更有效地处理更新、迁移和安装。

DbFileExt模块提供了对您的 SQLite Ti.Filesystem对象的便捷访问。该模块还包括其他方便的方法来帮助处理数据库文件。

准备就绪

DbFileExt模块添加到您的项目中很简单。只需将dbfileext.js文件复制到您的项目中,如下截图所示:

准备就绪

如何操作…

一旦您将dbfileext.js文件添加到您的项目中,您需要使用require将模块导入到您的代码中:

//Create our application namespace
var my = {
  isAndroid : Ti.Platform.osname === 'android',
  dbfileext : require('dbfileext')
};

查找我们的数据库 Ti.Filesystem.File

dbFile函数提供了对您的数据库Ti.Filesystem.File对象的便捷访问。只需提供您希望访问的数据库名称,相关的Ti.Filesystem.File对象将被返回:

Var myDbFile = my.dbfileext.dbFile('testdb');

小贴士

如果向dbFile函数提供了无效的数据库名称,将返回一个新的Ti.Filesystem.File对象。这允许您稍后添加数据库或文件。建议在执行任何操作时使用exists方法。

确定数据库目录

确定您的应用程序的数据库文件夹可能具有挑战性,因为它根据平台而异。利用dbDirectory函数有助于解决这一挑战,前提是您根据应用程序运行的设备指定适当的目录路径:

my.dbfileext.dbDirectory();

文件存在性检查

在打开之前,您通常会需要检查数据库是否已安装。DbFileExt模块通过dbExists函数使这一过程变得简单。只需粘贴数据库名称,就会返回一个布尔值,标识数据库是否已安装:

my.dbfileext.dbExists('testdb');

远程备份 – iOS 特定的属性

在 iOS 5 中,Apple 包含了将文件备份到 iCloud 服务的功能。您可以通过使用 dbRemoteBackup 函数来启用或禁用此功能。当您提供数据库名称和布尔值时,您表示是否希望启用远程备份:

my.dbfileext.dbRemoteBackup('testdb',true);

小贴士

如果在 Android 平台上调用此函数,则不会执行任何操作,因为这涉及到 iOS 特定的功能。

重命名数据库文件

DbFileExt 模块允许您重命名数据库文件。这在进行数据库版本控制或备份时非常有用。要重命名数据库,请将当前数据库名称和新名称作为参数传递给 dbRename 方法,如下所示:

my.dbfileext.dbRename('current name','new name');

列出所有数据库

在处理大型应用程序或需要数据库版本控制的应用程序时,您通常会需要列出应用程序沙盒内安装的所有数据库。dbList 函数提供了应用程序数据库文件夹中每个 SQLite 数据库的名称和本地路径。然后,您可以使用此数组删除任何不需要的数据库文件:

var installedDatabase = my.dbfileext.dbList();

删除数据库文件

dbRemove 函数提供了一个简单且安全的方法来删除任何不需要的数据库文件。提供您希望删除的数据库名称,DbFileExt 模块将文件从您的设备中删除:

my.dbfileext.dbRemove('newtest');

它是如何工作的...

以下一系列测试演示了 DbFileExt 模块在无窗口的 app.js 示例中的应用。

设置我们的测试

首先,我们使用 Ti.Database.open 函数创建一个示例数据库。以下高亮代码演示了如何创建名为 testdb 的数据库:

//Create our application namespace
var my = {
    isAndroid : Ti.Platform.osname === 'android',dbfileext : require('dbfileext')
};

var testDb = Ti.Database.open('testdb');

接下来,我们创建一个 Ti.Filesystem 对象,该对象具有对 testdb 数据库的已知引用。这将在以后用于验证 DbFileExt 模块是否返回正确的值。

var testFileReference = (function(){
  if(my.isAndroid){
    return Ti.Filesystem.getFile(
  }else{
    return testDb.file;
  }
})();

小贴士

iOS 的 Ti.Database 对象有一个文件属性,可以在您的测试中使用。在 Android 上,您需要使用正确的目录和文件名创建文件对象。

查找我们的数据库 Ti.Filesystem

接下来,我们比较 DbFileExt 模块返回的 Ti.Filesystem.File 对象是否与我们的测试文件匹配。无论平台如何,这些都应该始终匹配。

Ti.API.info("Does the module return the same as our test?");
Ti.API.info((testFileReference.nativePath === my.dbfileext.dbFile('testdb').nativePath) ?
"Test Pass" : "Test Failed");

确定数据库目录

SQLite 文件安装的目录因平台而异。例如,在 Android 上位于 data 目录中,而在 iOS 上位于 Private Documents 文件夹中。以下示例演示了如何将设备数据库目录的路径写入 Titanium Studio 的控制台:

Ti.API.info("Your database directory is " + my.dbfileext.dbDirectory());

文件存在检查

DbFileExt 模块最常用的用途是检查数据库是否已经安装。下一个测试比较了 DbFileExt 中的 dbExists 结果和由我们的测试 Ti.Filesystem.File 对象生成的结果:

Ti.API.info("Does the exists test work?");
Ti.API.info((testFileReference.exists() === my.dbfileext.dbExists('testdb')) ? "Both Exist" : "Test Failed");

重命名数据库文件

你可能会因为各种原因需要重命名数据库,最常见的是存档或版本控制。以下突出显示的代码演示了如何将testdb数据库重命名为oldtest。然后对新的和旧的名字都调用了dbExists方法,以演示重命名功能已正确工作。

my.dbfileext.dbRename('testdb','oldtestdb');
Ti.API.info("Does the test db exist?  " + my.dbfileext.dbExists('testdb'));
Ti.API.info("How about the oldtest one? Does that exist?  " + my.dbfileext.dbExists('oldtest'));

列出所有数据库

列出应用中所有可用的数据库通常很有帮助,这对于大型应用或包含第三方组件的应用尤其有用。

为了演示此功能,首先在我们的示例应用中创建几个数据库:

Ti.Database.open('test1');
Ti.Database.open('test2');
Ti.Database.open('test3');

DbFileExt模块的dbList函数用于返回安装到我们的应用中的所有数据库:

var installedDb =  my.dbfileext.dbList();

以下代码片段将installedDb变量提供的数据库列表写入 Titanium Studio 的控制台。这将列出本示例中使用的所有数据库,以及可能也安装在你 Titanium 项目中的任何数据库。

installedDb.forEach(function(db) {
  Ti.API.info("Db Name = " + db.dbName);
  Ti.API.info("nativePath = " + db.nativePath);
});

删除数据库文件

此配方的最终测试演示了如何使用dbRemove函数删除已安装的数据库。以下突出显示的代码片段显示了如何删除我们在示例中之前使用的oldtest数据库:

my.dbfileext.dbRemove('oldtest');
Ti.API.info("db still exists?  " + my.dbfileext.dbExists('oldtest'));

DbTableChecker SQLite 表存在性检查

SQLite 和Ti.Database API 提供了许多强大的功能。此配方演示了如何使用现有的Ti.Database API 和 SQL 语句来检查表是否已经创建。

检查数据库对象是否存在的能力对于企业应用至关重要,用于版本控制和数据迁移目的。此过程的典型用法是在模式迁移期间检查表是否已经存在。例如,如果客户端表在创建表时已经存在,则可能执行特定的数据迁移。

准备工作

此配方依赖于 Titanium 框架的Ti.Database API,无需任何依赖。在下一节中,我们将创建一个简单的app.js文件,演示如何执行表存在性检查。

如何操作…

以下步骤演示了如何在 SQLite 数据库中检查表是否存在。

创建我们的模块

在 Titanium Studio 中,创建一个名为db的模块,名为tablechecker.js。此模块应包含以下代码片段:

exports.tableExists = function (dbName, tableName){
  var conn = Ti.Database.open(dbName);
  var selectSQL ='SELECT name FROM sqlite_master 'selectSQL +=' WHERE type="table" AND name=?';

  var getReader = conn.execute(selectSQL,tableName);
  var doesExist = (getReader.getRowCount() > 0 );

  //Clean-up
  getReader.close();
  conn.close();
  getReader = null;
  conn = null;

  return doesExist;
};

命名空间和应用设置

接下来,在我们的应用程序的app.js文件中,我们创建应用程序命名空间,并使用require方法将CommonJS模块导入到我们的应用程序中:

//Create our application namespace
var my = {
  dbTableChecker : require('dbtablechecker')
};

创建我们的窗口

为了帮助演示此配方,创建了一个包含三个按钮的窗口。这些按钮将允许你创建、测试表是否存在,以及删除myTable示例表。

var win = Ti.UI.createWindow({
  backgroundColor:'#fff',layout:'vertical'
});

测试表是否存在

配方中的第一个按钮演示了如何在dbTableCheckerCommonJS模块中创建的tableExists函数中进行调用:

var tableExistsBtn = Ti.UI.createButton({
    title:'Does Table Exist?', height:50, right:5, left:5, top: 20
});
win.add(tableExistsBtn);

当按钮被按下时,在 CommonJS 模块中调用 tableExists 函数以确定指定的表是否存在。下一个突出显示的代码片段演示了检查 myDatabase SQLite 数据库中是否存在名为 myTable 的表。

tableExistsBtn.addEventListener('click',function(e){
  //Check if our table exists in our database
  var doesExist = my.dbTableChecker.
tableExists('myDatabase','myTable');
  //Alert the user if the table exists or not
  alert('Table "myTable" ' + (doesExist ? ' exists' : "does not exist"));
});

创建一个表格

下一个按钮在本菜谱中用于创建一个用于测试的示例表。在探索本菜谱时,此按钮将允许您多次删除和重新创建示例表。

var makeTestBtn = Ti.UI.createButton({
  title:'Create Test Table', height:50, right:5, left:5, top: 20
});
win.add(makeTestBtn);

以下突出显示的代码演示了调用 my.testers.makeTable 函数来在 myDatabase 数据库中创建名为 myTable 的表:

makeTestBtn.addEventListener('click',function(e){
  //Create a sample table
 my.testers.makeTable('myDatabase','myTable');

  //Alert the user a test table has been created
  alert('Table "myTable" was created.');
});

小贴士

请参阅本菜谱的 如何工作… 部分,以获取有关 my.testers 的更多信息。

删除一个表

本菜谱中的最后一个按钮用于删除示例表。在探索本菜谱时,此按钮将允许您多次删除和重新创建示例表:

var removeTestBtn = Ti.UI.createButton({
  title:'Remove Test Table', height:50, right:5, left:5, top: 20
});
win.add(removeTestBtn);

以下代码片段中突出显示的行演示了如何调用 my.testers.removeTable 函数来删除我们数据库中的名为 myTable 的表:

removeTestBtn.addEventListener('click',function(e){
  //Create a sample table
  my.testers.removeTable('myDatabase','myTable');
  //Alert the user a test table has been removed
  alert('Table "myTable" was removed');
});

小贴士

请参阅本菜谱的 如何工作… 部分,以获取有关 my.testers 的更多信息。

如何工作…

本菜谱使用 SQLite 数据字典和几个辅助方法来支持测试。这些方法的函数性和它们的组合方式在此进行讨论。

测试辅助工具

本菜谱使用两个辅助函数 makeTabledropTable 来管理我们的示例表。这些方法允许 tableExists 方法在无冲突的情况下重复测试。

my.testers = {
  1. makeTable 函数使用 dbName 打开数据库连接。一旦数据库已打开,就会使用提供的 tableName 参数创建一个表(如果不存在):

      makeTable : function(dbName,tableName){
        var conn = Ti.Database.open(dbName);
        var createSql = 'CREATE TABLE IF NOT EXISTS ' 
        createSql += tableName ;
        createSql += '(id INTEGER PRIMARY KEY AUTOINCREMENT,';
        createSql += ' my_column TEXT)';
    
        conn.execute(createSql);
    
        //Clean-up
        conn.close();
        conn = null;
      },
    
  2. removeTable 函数使用 dbName 打开数据库连接。一旦数据库已打开,如果提供的 tableName 参数中指定的表存在,则将其删除:

      removeTable : function(dbName,tableName){
        var conn = Ti.Database.open(dbName);
        var dropSql = 'DROP TABLE IF EXISTS ' + tableName;
    
        conn.execute(dropSql);
    
        Conn.close();
        conn = null;
      }
    };
    

tableExists 方法

dbTableCheck 模块有一个名为 tableExists 的单一方法,如果提供的表名存在于数据库中,则返回一个布尔结果。此检查是通过查询 SQLite 数据库的数据字典来执行的。

exports.tableExists = function (dbName, tableName){
  var conn = Ti.Database.open(dbName);
  1. 以下 SQL 语句将查询 SQLite 数据字典表 sqlite_master,以获取具有指定表名的表列表。? 字符是一个参数,它将由执行方法替换。

      var selectSQL ='SELECT name FROM sqlite_master; selectSQL +=' WHERE type="table" AND name=?';
    
  2. 然后使用 tableName 变量作为其参数执行数据字典查询。返回一个 Ti.Database.DbResultSet 并分配给 getReader 变量:

      var getReader = conn.execute(selectSQL,tableName);
    
  3. 接下来,使用 getRowCount 方法来确定是否有任何行返回。这被转换为一个布尔语句,稍后该方法将返回此语句:

      var doesExist = (getReader.getRowCount() > 0 );
    
  4. DbResultSet 和数据库随后关闭,以减少活动对象的数量:

      getReader.close();
      conn.close();
      getReader = null;
      conn = null;
    
  5. 之前确定的布尔结果由以下方法返回:

      return doesExist;
    };
    

使用 Dossier 递归处理文件

在创建你的 Titanium 企业应用程序时,你通常会需要将目录的内容复制到另一个位置。最常见的两个例子是:实现缓存策略和执行懒加载安装。例如,可以使用 Dossier 创建初始内容缓存,通过将文件从你的应用程序的 Resources 目录复制到 Ti.Filesystem.applicationDataDirectory 下的工作目录。这将使用户能够在后台刷新数据时看到初始内容。

Dossier CommonJS 模块提供了一个跨平台的 API,用于处理这些类型的文件夹操作。下一节将演示如何在你的 Titanium 企业应用程序中安装和使用 Dossier 模块。

准备工作

Dossier CommonJS 模块通过将 dossier.js 文件包含到你的项目中来安装。

将 Dossier 模块添加到你的项目中

dossier 模块添加到你的项目中很简单。只需将 dossier.js 文件和 SampleData 文件夹复制到你的 Titanium 项目中的 Resources 文件夹,如下面的截图所示。这将安装此食谱所需的全部文件。

将 Dossier 模块添加到你的项目中

如何操作…

一旦你将 dossier.js 文件添加到你的项目中,你需要使用 require 来将模块导入到你的代码中:

//Create our application namespace
var my = {
  dossier : require('dossier');
};

创建示例目录

为了演示 Dossier 的复制和移动功能,我们为源目录和目标目录创建了一个 Ti.Filesystem 对象。在下一个片段中,sourceDir 变量包含一个指向我们作为食谱设置的一部分复制的 SampleData 文件夹的目录引用,而 targetDir 指向一个名为 NewSampleData 的新文件夹,该文件夹将创建在你的设备 data 目录下。

var sourceDir = Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, 'SampleData');

var targetDir = Ti.Filesystem.getFile(
  Ti.Filesystem.applicationDataDirectory
  + '/NewSampleData');

递归列出目录内容

当处理动态或可下载的内容时,你通常会需要列出特定目录下的所有内容。listContents 方法允许你递归查询目录的内容,列出目录内的所有文件和子目录。

以下代码片段演示了如何查询我们食谱的 sourceDir 以获取所有文件和文件夹的列表:

var listTargetContents = my.dossier.listContents(sourceDir.nativePath);

listContents 方法返回一个文件浏览器字典对象,以分层格式列出所有文件和子目录,如下面的截图所示:

递归列出目录内容

递归复制目录内容

许多企业应用程序都是内容驱动的。为了改善你的首次安装体验,你可能希望在应用程序中捆绑介绍性内容,并在启动时将捆绑的内容复制到本地缓存中。

使用 dossier 模块的 copy 方法,你可以将一个文件夹的全部内容复制到另一个文件夹。以下代码演示了如何将我们源目录的所有内容复制到我们的新目标目录:

my.dossier.copy(sourceDir.nativePath,targetDir.nativePath);

小贴士

在复制过程中,目标文件夹中现有的内容将被删除。

递归移动目录内容

move 方法创建源目录所有内容的副本到目标文件夹。一旦复制过程成功完成,源目录将被删除。

my.dossier.move(sourceDir.nativePath,targetDir.nativePath); 

小贴士

在移动过程中,目标文件夹中现有的内容将被删除,并替换为源文件夹的内容。此外,在所有文件都已移动后,如果可能,将删除源内容。在源目录为只读的情况下,将保留内容。

参考信息

  • 在此菜谱中使用的 Dossier 模块是一个开源项目,可在 Github 上找到。如果您想了解更多信息或贡献,请访问该项目:github.com/benbahrenburg/Dossier

调整 SQLite 数据库以获得最佳性能

数据访问是钛企业开发中的常见瓶颈。通过正确使用 SQLite 事务,您可以体验到批量插入操作高达 10 倍的性能提升。

SQLite 事务提供可靠的工作单元,允许数据恢复并保持数据库的一致性。默认情况下,每次在 SQLite 数据库上执行插入、更新或删除操作时,在您的语句执行前后都会创建一个隐式事务。这有助于保持数据库的一致状态。然而,对于批量操作,这引入了额外的开销,并且可能会大幅降低您的应用程序性能。

此菜谱演示了如何使用 SQLite 事务在执行批量 SQL 操作和处理大量数据时提高应用程序性能。

准备工作

此菜谱将使用 100,000 行进行性能比较。其中包括以下基本 UI,允许您基准测试您的不同设备。

准备工作

添加 TimeLogger 模块

此菜谱使用 TimeLogger 记录每个性能测试的持续时间。要将 TimeLogger 模块添加到您的项目中,将 timelogger.js 文件复制到您的项目中,如下截图所示:

添加 TimeLogger 模块

如何操作…

此菜谱的第一步是创建应用程序命名空间并导入计时器模块,如下代码片段所示:

//Create our application namespace
var my = {
  timer : require('timelogger')
};

创建我们的测试界面

下一步是创建一个 Ti.UI.Window 用于我们的菜谱。这将用于为我们的性能测试提供一个启动点。

var win = Ti.UI.createWindow({
  backgroundColor:'#fff',layout:'vertical'
});
  1. 现在,创建一个标签来跟踪我们的 No Transactions 性能测试的结果:

    var noTransactionLabel = Ti.UI.createLabel({
        text: "No Transactions: NA",height:20, right:5, left:5, top: 40, textAlign:'left',color:'#000', font:{fontWeight:'bold',fontSize:14}
    });
    win.add(noTransactionLabel);
    
  2. 然后,创建一个标签来跟踪我们的 With Transactions 性能测试的结果:

    var withTransactionLabel = Ti.UI.createLabel({
      text: "With Transactions: NA",
      height:20, right:5, left:5, 
      top: 10, textAlign:'left',
      color:'#000', font:{fontWeight:'bold',fontSize:14}
    });
    win.add(withTransactionLabel);
    
  3. 我们创建的最后一个 UI 元素是一个按钮,当按下时,将启动我们的测试:

    var runTestButton = Ti.UI.createButton({
        title:'Start Performance Test', height:50, right:5, left:5, top: 40
    });
    win.add(runTestButton);
    

基准测试

runTestButton 按钮被按下时,执行一个基准测试,插入 100,000 条记录,既有使用事务范围的情况,也有不使用的情况。然后屏幕会更新每个测试的经过的毫秒数。

  1. 以下代码演示了当按钮的点击事件被触发时,每个测试是如何运行的:

    runTestButton.addEventListener('click',function(e){
    
  2. 首先,运行一个不显式创建事务的测试。此测试返回运行期间经过的毫秒数,并将结果存储在 noTransactions 变量中,如下所示:

    var noTransactions = performanceTest.run(false);
    noTransactionLabel.text = "No Transactions: " + noTransactions + ' ms';
    
  3. 接下来,使用 BEGINCOMMIT 语句创建一个显式事务,运行相同的测试。此测试返回测试期间经过的毫秒数,并将结果存储在 withTransactions 变量中,如下所示:

    var withTransactions = performanceTest.run(true);
    withTransactionLabel.text = "With Transactions: " + withTransactions + ' ms';
    });
    

它是如何工作的...

本食谱使用本节中讨论的辅助对象来执行基准测试操作。

数据库设置

dbTestHelpers 是在基准测试中首先使用的辅助对象。此对象包含设置、创建和管理我们测试中使用的数据库所需的所有代码:

var dbTestHelpers = {
  1. maxIterations 属性控制测试中的迭代次数:

      maxIterations : 100001,
    
  2. createOrResetDb 方法用于返回一个已知且一致的数据库以供测试:

      createOrResetDb : function(){
        return Ti.Database.open("perf_test");
      },
    
  3. 然后调用 resetTestTable 方法来删除并重新创建我们的测试表。这允许我们多次运行测试,同时保持样本大小一致:

      resetTestTable : function(db){
        var dropSql = 'DROP TABLE IF EXISTS TEST_INSERT';
        var createSql = 'CREATE TABLE IF NOT EXISTS ';
        createSql += 'TEST_INSERT ';
        createSql += '(TEST_ID INTEGER, TEST_NAME TEXT, ';
        createSql += 'TEST_DATE DATE)';
        db.execute(dropSql); 
        db.execute(createSql);
      },
    
  4. createSQLStatement 方法返回用于执行我们插入操作的 SQL 语句:

      createSQLStatement : function(){
        var sql = 'INSERT INTO TEST_INSERT ';
        sql += '(TEST_ID, TEST_NAME, TEST_DATE) ';
        sql += 'VALUES(?,?,?)';
        return sql;
      },
    
  5. createDummyObject 函数创建一个唯一的对象,用于插入到每一行:

      createDummyObject : function(iterator){
        var dummy = {
          id:iterator, name : 'test record ' + iterator, date : new Date()
        };

        return dummy;
      }
    };
    

执行测试

performanceTest 对象运行并计时食谱数据库的插入操作。run 方法启动基准测试并提供一个指示器,以确定测试是否要使用事务:

var performanceTest = {
  run : function(useTransaction){
  1. 我们测试的第一步是创建数据库连接并重置我们的表。这是通过调用前面讨论过的 dbTestHelper 方法来完成的:

        var db = dbTestHelpers.createOrResetDb();
        dbTestHelpers.resetTestTable(db);
    
  2. 在我们的数据库设置完成后,下一步是创建我们的 insert 语句和 timer 对象,如下所示:

        var dummyObject = null;
        var insertSQL = dbTestHelpers.createSQLStatement();
        var insertTimer = new my.timer("Insert Timer");
    
  3. 如果已设置 useTransaction 标志,我们随后显式开始一个事务:

        if(useTransaction){
          db.execute('BEGIN;');
        }
    
  4. 在本食谱的下一步中,创建一个循环,将特定数量的记录插入测试表。默认情况下,此测试将插入 100,000 条记录,并计时总持续时间。

        for (var iLoop = 0; iLoop < dbTestHelpers.maxIterations; iLoop++){
          dummyObject = dbTestHelpers.createDummyObject(iLoop);
          db.execute(insertSQL,dummyObject.id,dummyObject.name,dummyObject.date);
        }
      }
    
  5. 如果已设置 useTransaction 标志,我们随后显式“提交”事务:

      if(useTransaction){
        db.execute('COMMIT;');
      }
    
  6. 此方法的最后一步是从我们的计时器对象中检索执行持续时间。此值(以毫秒为单位)随后返回以供后续比较操作:

      var totalInsertTime = insertTimer.getResults().msElapsed;
      db.close();
      //Return total ms elapsed
      return totalInsertTime;
      }
    };
    

参见

使用 DbLazyProvider 进行数据访问

在构建任何企业应用程序时,数据访问是一个常见的挑战。DbLazyProvider模块在Ti.Database API 周围提供了一个轻量级的包装器。此模块为常见操作提供辅助器,例如管理事务和懒加载连接。

懒加载是一种常见的效率模式,它将对象的初始化推迟到需要时。通过懒加载应用程序数据库连接,与创建数据库连接相关的任何内存使用或 I/O 操作都将推迟到需要时。

以下部分演示了如何使用DbLazyProvider模块在您的应用程序中实现懒加载模式,同时保持对数据库事务的控制。

准备工作

DbLazyProvider模块添加到您的项目中很简单。只需将dblazyprovider.js文件复制到您的项目中,如下面的截图所示:

准备工作

如何做到这一点…

此菜谱的第一步是创建应用程序命名空间并导入DbLazyProvider模块,如下所示:

//Create our application namespace
var my = {
  dbProvider : require('dblazyprovider')
};

接下来,创建一个名为MY_TEST的示例表。如果表已存在,任何现有记录将被清除,以便我们可以开始新的测试。

var dbSetup = new my.dbProvider("myDb");
var createSql = 'CREATE TABLE IF NOT EXISTS ';
createSql += 'MY_TEST (TEST_ID INTEGER, ';
createSql += 'TEST_NAME TEXT)';
dbSetup.connect().execute(createSql);
dbSetup.connect().execute('DELETE FROM MY_TEST');
dbSetup.close();

创建我们的测试接口

现在,我们为我们的菜谱创建一个Ti.UI.Window。这将用于为我们的测试提供一个启动点。

var win = Ti.UI.createWindow({
  backgroundColor:'#fff',layout:'vertical'
});

使用事务进行懒加载

insertDemoBtn按钮用于触发我们的菜谱,展示了如何使用DbLazyProvider进行事务处理:

var insertDemoBtn = Ti.UI.createButton({
  title:'Run Inserts', height:50, right:5, left:5, top: 20;
});
win.add(insertDemoBtn);
  1. 点击insertDemoBtn按钮将向我们的表插入 1,000 行:

    insertDemoBtn.addEventListener('click',function(e){
      var maxIteration = 1000;
      var sql = 'INSERT INTO MY_TEST ';
      sql+='(TEST_ID, TEST_NAME) VALUES(?,?)';
      var db = new my.dbProvider("myDb");
      for (var iLoop = 0; iLoop < maxIteration; iLoop++){
    
  2. 以下代码展示了使用带有true事务参数的connect方法来获取用于执行 SQL 语句的Ti.Database对象。这将自动为您在第一次插入时创建一个事务。

       db.connect(true).execute(sql,iLoop,'test ' + iLoop);
    
  3. 当调用close方法时,任何创建的交易将被提交,数据库连接将被关闭:

     db.close();
        alert('Test done ' + maxIteration + ' rows inserted');
      });
    

使用懒加载创建选择语句

并非所有 SQL 语句都从事务中受益。以下代码片段演示了如何在不使用事务的情况下执行选择语句:

  var selectDemoBtn = Ti.UI.createButton({
    title:'Run Select', height:50, right:5, left:5, top: 20
  });
  win.add(selectDemoBtn);
  1. 点击selectDemoBtn按钮创建一个新的DbLazyProvider对象,并运行一个选择语句:

      selectDemoBtn.addEventListener('click',function(e){
        var db = new my.dbProvider("myDb");
        var selectSQL = 'SELECT * FROM MY_TEST ';
    
  2. 突出显示的代码展示了如何使用不带任何参数的connect方法。这将默认避免使用事务。

     var getReader = db.connect().execute(selectSQL);
        var rowCount = getReader.getRowCount();
    
  3. 在使用close方法时,数据库连接将被关闭,所有对象都将设置为 null:

        db.close();
        alert('Rows available ' + rowCount);
      });
    

它是如何工作的…

DbLazyProvider模块是Ti.Database的一个轻量级但强大的包装器。

创建一个新的 DbLazyProvider 对象

要创建一个新的DbLazyProvider对象,只需使用require方法导入模块,并使用以下突出显示的代码片段中的引用创建一个新的对象。这将创建一个数据库名称的新对象包装器。

//Create our application namespace
var my = {
  dbProvider : require('dblazyprovider')
};
//Create a new dbLazy object
var db = new my.dbProvider("myDb");

获取连接对象

DbLazyProvder最常用的方法是connect。此方法将在需要时创建一个新的Ti.Database连接,然后返回数据库对象:

db.connect().execute('Your SQL goes here');

如果您希望 SQL 语句开始一个事务,您只需在调用connect方法时传入一个布尔参数true,如下所示:

db.connect(true).execute('Your SQL goes here');

创建的事务将一直使用,直到在您的DbLazyProvider对象上调用closecommit方法。

开始事务

默认情况下,事务会自动为您处理,使用connectclose方法。您也可以在任何时候显式创建一个事务,使用beginTransaction方法,如下所示:

db.beginTransaction();

结束事务

默认情况下,事务会自动为您处理,使用connectclose方法。您也可以在任何时候显式提交或完成事务,使用commit方法,如下所示:

db.commit();

打开数据库连接

默认情况下,模块将在需要数据库连接之前等待,然后打开Ti.Database对象。如果需要提前建立连接,您可以在任何时候调用open方法:

db.open();

小贴士

您还可以传入一个新的数据库名称来切换数据库引用。

关闭数据库连接

在您的DbLazyProvider对象上使用close方法很重要,因为它会提交任何挂起的交易并关闭您的数据库连接。应在每个事务分组之后或当数据库连接不再需要时调用close方法。

db.close();

使用 MongloDb 的 NoSQL

NoSQL 数据库通常非常适合管理您的应用程序数据,因为它允许您使用对象而不是表来工作。除了使用对象的优点外,在移动设备上使用 NoSQL 通过消除模式管理、数据迁移和其他与维护关系数据模型相关联的常见维护问题,可以降低复杂性。

MongloDb 是 MongoDb 流行的 NoSQL 解决方案的纯 JavaScript 实现。MongloDb模块允许您在 Titanium 应用程序中使用熟悉的 MongoDb 语法查询和持久化对象。本菜谱演示了如何在新的或现有的 Titanium 项目中利用MongloDb模块。

准备工作

为 Titanium 安装 MongloDb 是一个简单的过程。您可以从菜谱的源代码中复制Monglo文件夹,或者自己创建捆绑包。

创建捆绑包是一个三步过程:

  1. 使用 Titanium Studio,在项目的Resources文件夹中创建一个名为Monglo的文件夹。

  2. monglodb.com下载最新的monglodb.js文件到之前步骤中创建的Monglo文件夹。

  3. 从钛存储项目下载最新的index.js文件到Monglo文件夹,该文件可在monglodb.com上找到。

无论您是从食谱源复制了捆绑包还是创建了您自己的,您的 Titanium 项目中Monglo文件夹应类似于以下截图的高亮部分:

准备中

如何操作...

安装 MongloDb 后,您需要使用require将模块导入到您的代码中:

//Create our application namespace
var my = {
  monglo : require('./Monglo/monglodb').Monglo
};

初始化您的数据库

在模块导入之后,需要初始化一个新的数据库实例。以下代码演示了如何创建一个名为myDbMonglo新实例:

my.db = my.monglo('myDb');

添加钛存储提供程序

MongloDb 具有支持广泛存储提供程序的能力。本食谱实现了钛存储提供程序以持久化数据。将存储提供程序与 MongloDb 关联是一个两步过程。首先,我们require钛存储提供程序,如下所示:

var tistore = require('./Monglo/index');

在创建存储提供程序后,提供程序被传递到use方法,如下所示。一旦调用use方法,MongloDb 将使用钛存储提供程序执行所有持久化操作。

my.db.use('store', tistore);

初始化我们的集合

一旦存储提供程序已关联,可以使用collection方法创建持久集合。以下行演示了如何创建一个名为foo的文档集合:

my.db.someCollection = my.db.collection('foo');

当初始化一个命名集合时,任何之前由该集合持久化的文档将自动重新加载。在上一个示例中,当集合初始化时,之前保存在foo文档集合中的任何文档都将被重新加载。

使用事件

MongloDb 提供了用于监控大多数操作的监听器事件。以下片段演示了如何为每个支持的监听器添加事件:

my.db.someCollection.on('insert', function(){
  Ti.API.info("Document Inserted") ;
});
my.db.someCollection.on('update', function(){ 
  Ti.API.info("Document Updated");
});
my.db.someCollection.on('remove', function(){
  Ti.API.info("Document Removed");
});
my.db.someCollection.on('find', function(){ 
  Ti.API.info("Find Used");
});
my.db.someCollection.on('createCollection', function(){ 
  Ti.API.info("Collection Created");
});
my.db.someCollection.on('removeCollection', function(){
  Ti.API.info("Collection Removed");
});

插入文档

下一步是插入三个新的文档。以下片段演示了如何将新文档插入到someCollection中。insert方法有两个参数。第一个参数是要存储的文档对象。第二个参数是一个回调函数,它列出错误并提供文档信息。这在上面的高亮片段中显示:

my.db.someCollection.insert({text: "record 1", 
batchId:'sample_test'}, function ( error, doc )

errordoc对象作为callback函数的一部分返回。error对象包含在插入操作期间遇到的任何问题,而doc对象包含创建的Monglo文档的副本。

  Ti.API.info('Error: ' + JSON.stringify(error));
  Ti.API.info('doc: ' + JSON.stringify(doc));
});
//Create second record
my.db.someCollection.insert({text: "record 2", batchId:'sample_test'}, function ( error, doc ){ });
//Create third record
my.db.someCollection.insert({text: "record 3", batchId:'sample_test'}, function ( error, doc ){ });

使用 find 进行查询

现在已创建三个记录,我们可以使用find函数来搜索所有具有批次 IDsample_test的文档。

my.db.someCollection.find({batchId:'sample_test'}, 
function ( error, cursor ){

find方法返回一个error对象和一个cursor对象。游标的forEach迭代器提供了一个方便的方式来检查返回的每个文档。以下片段演示了如何将cursor中的每个文档作为 JSON 字符串打印到 Titanium Studio 控制台:

  cursor.forEach(function(doc){ 
    Ti.API.info('doc: ' + JSON.stringify(doc));
  });
});

更新文档

与传统数据库类似,MongloDb 提供了使用update方法更改文档的能力。update方法有三个参数。第一个参数用于查找你希望更新的对象。下一个示例展示了如何更新具有等于record 1的文本属性的任何对象。第二个参数是更新语句。示例将每个匹配对象的文本属性更新为updated record 1。最后一个参数是一个回调,它返回一个errordoc对象。

my.db.someCollection.update({text: "record 1"}, {$set: {text: 'updated record 1'}}, function ( error, doc ){ 
  Ti.API.info('Error: ' + JSON.stringify(error));
  Ti.API.info('doc: ' + JSON.stringify(doc));
});

这演示了如何更新本菜谱中插入的第一个文档的文本属性。

使用findOne查询单个文档

findOne方法提供了查询你的集合以获取特定文档的手段。以下代码片段演示了如何查询someCollection以获取我们刚刚更新的文档。然后,单个匹配的文档作为 JSON 字符串打印到 Titanium Studio 控制台。

my.db.someCollection.findOne({text: 'updated record 1'}, function ( error, doc ){ 
  Ti.API.info('Error: ' + JSON.stringify(error));
  Ti.API.info('doc: ' + JSON.stringify(doc));
});

删除文档

本菜谱的最后一部分演示了如何从集合中删除文档。以下代码片段演示了如何删除所有具有批处理 IDsample_test的文档。

my.db.someCollection.remove({batchId:'sample_test'}, function (error) { 
  Ti.API.info('Error: ' + JSON.stringify(error));
});

小贴士

之前的代码片段删除了在此菜谱中创建的所有记录,允许你多次运行示例而不会创建不想要的记录。

参见

本菜谱中展示的所有 NoSQL 示例都使用了 MongloDb 开源项目。请参阅以下内容以了解更多关于此项目的信息:

第四章. 与 Web 服务交互

在本章中,我们将涵盖:

  • 消费 RSS 源

  • 使用 Yahoo Local 创建业务位置地图

  • 在您的应用中使用 Google Analytics

  • 使用 SUDS.js 进行 SOAP 服务调用

  • 使用 LinkedIn 联系人 API

简介

移动设备一直是企业空间和消费者空间中 Web 服务种类和数量增长的主要推动力。作为最终的断开连接客户端,移动应用重新激发了组织对面向服务架构SOA)的兴趣,因为它们希望将现有的系统和功能扩展到移动客户。因此,您很少会找到一个不使用内部或第三方远程服务的企业应用。

本章演示了如何在构建企业应用时使用 SOAP 和 REST 调用与流行的第三方平台进行接口。

消费 RSS 源

在定期更新内容的企业应用中,使用 RSS 和 ATOM 源是很常见的。您可以使用本食谱中展示的技术来发布您组织或第三方的内容。许多公司使用这种方法为员工提供有关其组织或行业的最新新闻和更新。

在本食谱中,我们消费了来自fastcompany.com的 RSS 源,并在Ti.UI.TableView中显示,供用户查看并启动浏览器阅读详细信息。

消费 RSS 源

准备工作

本食谱使用rss2Objects CommonJS 模块。此模块和其他代码资源可以从本书提供的源代码中下载。将此模块安装到您的项目中很简单。只需将rss2objects.js文件复制到您的项目中,如下面的截图所示:

准备工作

如何操作...

一旦您将rss2objects.js文件添加到您的项目中,您需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到您的代码中,如下面的代码块所示:

//Create our application namespace
var my = {
  reader : require('rss2objects'),
  isAndroid : ( Ti.Platform.osname === 'android'),
  rssTestUrl : "http://www.fastcompany.com/rss.xml"
};

为示例应用创建用户界面

本食谱的演示应用提供了两个Ti.UI.Button控件来展示获取 RSS 结果的不同技术,以及一个Ti.UI.TableView来显示文章。

  1. 首先,我们创建我们的Ti.UI.Window来附加所有 UI 元素:

      var win = Ti.UI.createWindow({
        backgroundColor:'#fff'
      });
    
  2. 接下来,创建一个按钮来演示如何使用 YQL 加载 RSS 源:

      var yqlButton = Ti.UI.createButton({
        title:"Load Using YQL",left:0 , width: 140, height:40, top:90
      });
      win.add(yqlButton);
    
  3. 然后添加一个按钮来演示如何使用 XML 解析加载 RSS 源:

      var xmlButton = Ti.UI.createButton({
        title:"Load Using XML",right:0 , width: 140,height:40, top:90
      });
      win.add(xmlButton);
    
  4. 最后,添加一个Ti.TableView来显示 RSS 文章:

      var tableView = Ti.UI.createTableView({
        top:120, bottom: 0, width:Ti.UI.FILL
      });
      win.add(tableView);
    

使用 YQL 读取 RSS 源

使用 Yahoo 的 YQL 平台是读取 RSS 流的便捷方式。YQL 相比传统的 XML 解析有两个额外的好处。第一个好处是 YQL 会为您规范化输出。第二个好处是 Yahoo 会将 RSS 流转换为 JSON。YQL 的缺点是它需要通过 Yahoo 的服务器代理流,这会降低下载速度。

此代码片段演示了如何调用 rss2Objects 模块的 yqlQuery 方法。yqlQuery 方法接受两个参数:第一个是要读取的 RSS 流的 URL,第二个是用于提供流项目结果的回调。

  yqlButton.addEventListener('click',function(e){
    my.reader.yqlQuery(my.rssTestUrl,rssResults);
  });

使用 XML 解析读取 RSS 流

rss2Objects 模块还提供了直接使用 XML 解析 RSS 流的能力。此选项为较大 RSS 流提供了最佳的下载性能。

要直接使用 XML 流,只需在 rss2Objects 模块上调用 query 方法。query 方法接受三个参数:

  • 您想读取的 RSS 流的 URL。

  • 一个可选的点查询语法,允许您提供您想要返回的节点路径。在下一个示例中,提供了 channel.item。这告诉 query 方法只从 channel item 元素列表返回节点。如果您想返回所有对象,只需将一个 null 值作为参数传递。

  • 第三个参数是回调方法,模块将在处理完成后发送结果。

    xmlButton.addEventListener('click',function(e){
      var queryPath = "channel.item";
      my.reader.query(my.rssTestUrl,queryPath,rssResults);
    });
    

显示文章

YQL 和 XML 解析演示都使用下面展示的回调方法。这个函数接收由 rss2objects 模块提供的对象字典,并为显示创建 TableView 行:

  function rssResults(e){
  1. 此函数中的参数 e 提供了来自 rss2Objects 模块的结果。第一步是检查是否成功返回了源。这通过检查 e.success 属性来完成,如下面的代码块所示:

        if(!e.success){
          alert("Sorry we encountered an error");
          tableView.setData([]);
          return;
        };
        var items = e.rssDetails.items;
        var itemCount = items.length;
        var rows = [];
    
  2. 接下来,所有 RSS 项目都会被循环遍历,并为每个项目构建一个 Ti.UI.TableViewRow 定义:

        for (var iLoop=0;iLoop<itemCount ;iLoop++){
          rows.push({
            title: items[iLoop].title,
            article : items[iLoop],
            height: 60,
            hasChild:true,
            color:'#000'
          });
        }
    
  3. 最后,在前面步骤中构建的文章行被添加到 Ti.TableView 以供显示:

        tableView.setData(rows);
      };
    

它是如何工作的…

rss2objects 模块提供了一个简单的 API 用于读取 RSS 流。rss2object.js 模块提供的两个主要方法将在以下章节中详细介绍。

使用 yqlQuery 函数

yqlQuery 方法使用 Titanium 的内置 YQL 提供者将提供的 RSS 流 URL 解析为对象的字典:

exports.yqlQuery = function(url,callback){
  1. onComplete 方法用作回调到 Ti.Yahoo.yql 函数。此方法将处理 YQL 结果,以供消费的标准格式。

      function onComplete(e){
    
  2. 参数 e 是由 YQL 提供的字典(包含 RSS 流查询的结果)。

        var results = {
          success:false,
          rssDetails : {
            url: url, full : null, items:null
          }
        };
    
  3. 检查 e.success 属性以确定 YQL 语句是否生成了错误。在 Android 上,您必须检查 e.data 属性是否为 null,而不是使用 e.success 属性。

        //Determine if we have an error
        results.success =  !((_isAndroid && e.data == null)||(!_isAndroid && (e.data == null || e.success==false)));
    
  4. 如果成功,将 e.data.items 添加到 results.rssDetails.items 数组中,该数组将作为后续的回调方法:

        if(results.success){
          results.rssDetails.items = [];
          var itemCount = e.data.item.length;
          for (var iLoop=0;iLoop<itemCount ;iLoop++){
            results.rssDetails.items.push({
              //Add each field in our items
            });
          }
        }
    
  5. 当调用 yqlQuery 模块方法时,将我们的 YQL 解析的 RSS 源的结果提供给最初使用的 callback 方法:

        callback(results);
      };
    
  6. 以下代码片段演示了一个 YQL 选择查询,该查询将从提供的 URL 参数的 RSS 源返回标题、链接、描述和其他列出的列:

      var yQuery = 'SELECT title, link, description,';
        yQuery += ' pubDate,guid,author,comments,';
        yQuery += ' enclosure,source FROM rss WHERE url =';
        yQuery += '"' + url + '"';
    
  7. 下面的代码块演示了使用 Ti.Yahoo.yql 方法运行查询并将结果发送到提供的 onComplete 回调函数:

      Ti.Yahoo.yql(yQuery, onComplete);
    };
    

使用查询函数

q uery 方法使用 Titanium 的 HTTPClient 和 XML 模块来读取提供的 RSS URL,并将提供的 XML 转换为对象的字典:

exports.query = function(url,pathQuery,callback){
  var workers = {
    results : {
      success:false,
      rssDetails : {
        url:url,full : null,items:null
      }
    },
  1. query 方法完成处理或出现错误时,将调用 whenComplete 函数。whenComplete 方法用于包装 callback 参数并提供查询结果。

        whenComplete : function(){
         callback(workers.results);
        }
      };
    
  2. 此部分菜谱采取的操作是创建一个 HTTP 客户端:

      var xhr = Ti.Network.createHTTPClient();
    
  3. 创建一个 onload 回调来检索 RSS 源提供的 HTTP 响应:

      xhr.onload = function(e) {
    
  4. 当请求返回时,使用 responseXML 属性从 RSS 源中收集 XML 结果:

        var xml = this.responseXML;
    
  5. 然后使用我们的辅助方法将 XML 结果转换为对象:

        var objResults = helpers.parseToObjects (xml);
    
  6. 如果转换的 XML 对象为 null,则使用回调方法通知用户该模块无法读取提供的 RSS 源:

        if(objResults ==null){
          workers.whenComplete();
          return;
        }
    
  7. 如果 XML 成功转换为对象,则使用完整的 XML 结果填充查询结果,并创建一个标志表示查询成功执行。

        workers.results.rssDetails.full = objResults;
        workers.results.success = true;
        workers.results.rssDetails.items = objResults.item;
    
  8. 如果提供了 pathQuery 字符串,则运行查询并使用 queryByPath 方法的输出更新 results 对象。

        if(pathQuery!=null){
          workers.results.rssDetails.items = helpers.queryByPath(wobjResults,pathQuery);
        }
      };
    
  9. 提供一个 onerror 回调来处理 HTTPClient 请求期间生成的错误。

      xhr.onerror = function(e) {
        Ti.API.info(JSON.stringify(e));
        workers.whenComplete();
      };
    
  10. 打开 HTTP 客户端并向提供的 RSS URL 发送一个 GET 请求。

      xhr.open('GET', url);
      xhr.send();
    };
    

    小贴士

    函数调用的顺序很重要。必须先调用 open 方法,然后再调用 send 方法,如前一个代码部分所示。

参见

使用 Yahoo Local 创建商业位置地图

为企业应用提供商店、客户或其他位置列表功能是许多应用的一个常见需求。在这个菜谱中,我们将展示如何使用 Yahoo Local 搜索 API 和 Ti.Map.View 来提供商店定位器。为了演示目的,这个菜谱使用 Yahoo API 提供了美国流行咖啡连锁店星巴克的定位搜索结果。每个位置的搜索结果随后显示在 Ti.Map.View 上,如下面的截图所示:

使用 Yahoo Local 创建商业位置地图

准备工作

此配方使用 Yahoo Search CommonJS 模块。此模块和其他代码资产可以从本书提供的源代码中下载。将此模块安装到您的项目中很简单。只需将 yahoo_search.js 文件添加到您的项目中,如下面的截图所示:

准备中

如何操作...

一旦将 yahoo_search.js 文件添加到您的项目中,您需要在 app.js 文件中创建您的应用程序命名空间,并使用 require 将模块导入到您的代码中,如下面的代码块所示:

//Create our application namespace
var my = {
  search : require('yahoo_search'),
  isAndroid : ( Ti.Platform.osname === 'android')
};

添加您的 API 密钥

我们配方中的下一步是添加您的 Yahoo 开发者 API 密钥。以下代码片段显示了如何将 API 密钥注册到模块中:

my.search.addAPIKey('YOUR_KEY_GOES_HERE');

创建示例应用程序的 UI

此配方使用一个简单的用户界面,包含 Ti.UI.TextFieldTi.Map.ViewTi.UI.Button 来搜索。在这里,我们展示了这些 UI 元素的创建。

  1. 首先,我们创建一个 Ti.UI.Window 来附加我们的视觉元素。

      var win = Ti.UI.createWindow({
        backgroundColor:'#fff'
      });
    
  2. 接下来,我们添加一个文本字段,允许用户输入要搜索的业务名称。

      var txtSubject = Ti.UI.createTextField({
        left:0, width:150, height:40,top:30,
        hintText:'Enter Business Name',
        borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED
      });
      win.add(txtSubject);
    
  3. 接下来,添加一个按钮,允许用户搜索在文本字段中输入的业务名称。

      var searchButton = Ti.UI.createButton({
        title:'Search', right:0, width:150, height:40,top:30	
      });
      win.add(searchButton);
    
  4. 最后,附加一个地图视图来显示我们的搜索结果。

      var mapView = Ti.Map.createView({
        mapType: Ti.Map.STANDARD_TYPE, width:Ti.UI.FILL,
        userLocation:true, top:75,bottom:0
      });
      win.add(mapView);
    

更新地图

updateMap 函数是搜索模块的回调方法。updateMap 提供搜索结果,并将其转换为用户显示。

  function updateMap(e){
  1. 搜索结果作为方法的 e 参数提供。过程的第一步是检查搜索是否成功,通过检查 e.success 属性。

        if(!e.success){
          alert("Sorry we encountered an error");
          return;
        }
    
  2. 在验证搜索成功后,使用循环为结果中提供的每个 e.item 创建地图标注。

        var points = [], itemCount = e.items.length, rightBtn = Ti.UI.iPhone.SystemButton.DISCLOSURE;
        for (var iLoop=0;iLoop<itemCount;iLoop++){
          points.push(Ti.Map.createAnnotation({
            latitude : e.items[iLoop].Latitude,
              longitude : e.items[iLoop].Longitude,
              title : e.items[iLoop].Address,
              subtitle : e.items[iLoop].Phone,
              pincolor : Ti.Map.ANNOTATION_GREEN,
              ClickUrl : items[iLoop].ClickUrl,
              animate:true, rightButton: rightBtn
          }));
        }
    
        var currentRegion = { latitude:e.latitude, longitude:e.longitude, latitudeDelta:0.04, longitudeDelta:0.04};
    
  3. 使用搜索坐标创建新的区域对象,并将其应用于设置地图的查看点。这使用户能够看到添加的所有标记。

          mapView.setLocation(currentRegion);
          mapView.regionFit = true;
    
  4. 最后,将包含所有标注的 points 数组添加到 mapView

        mapView.annotations = points;
      };
    

搜索

当按下 searchButton 按钮时,以下代码片段用于使用设备的坐标执行位置搜索。

  searchButton.addEventListener('click',function(e){
  1. 首先,删除任何现有的地图标记

        mapView.removeAllAnnotations();
    
  2. 如果文本字段的键盘是打开的,则调用 blur 方法来关闭它。

        txtSubject.blur();
    
  3. 为了避免空搜索,检查文本字段以确保它包含一个值。

        if((txtSubject.value+'').length ===0){
          alert('Enter a business to search');
          return;
        }
    
  4. 然后调用模块的 currentLocationQuery 方法,提供在文本字段中输入的业务名称和用作回调的 updateMap 函数。

        my.search.currentLocationQuery(txtSubject.value,updateMap);
      });
    

工作原理...

Yahoo Search CommonJS 模块 (yahoo_search.js) 提供以下公共函数,详细说明如下。

使用 addAPIKey 为您的 Yahoo 服务密钥

Yahoo Local 搜索 API 需要一个开发者密钥。在使用任何查询方法之前,您必须首先使用 addAPIKey 将您的开发者密钥与模块关联。

my.search.addAPIKey('YOUR_KEY_GOES_HERE');

小贴士

您可以通过访问 developer.yahoo.com 并在他们的开发者门户中创建一个项目来获取 Yahoo API 密钥。

使用 geoQuery 方法

geoQuery函数使用提供的纬度、经度和主题执行 Yahoo Local 搜索。下一个示例演示了如何在纽约市的时代广场附近搜索星巴克的位置。当搜索完成后,结果将提供给callback函数。

my.search.geoQuery(40.75773, -73.985708,'starbucks',callback);

使用 currentLocationQuery 方法

currentLocationQuery方法使用您的设备的位置服务来确定您的当前纬度和经度。然后,它为geoQuery函数提供所需的搜索详细信息。下一行代码演示了如何搜索您当前位置附近的星巴克分店。一旦搜索完成,结果将提供给callback函数。

my.search.currentLocationQuery('starbucks',callback);

在您的应用中使用 Google Analytics

Google Analytics 是一种流行的服务,用于衡量和记录网站流量和活动。很可能会使用 Google Analytics 或类似的服务来收集您网站访问者的分析数据。通过钛模块,您可以在您的应用中使用相同的 Google Analytics 平台。这种方法允许您在一个易于使用的仪表板中查看您的移动和网页流量。

在本菜谱中,我们将演示如何将“页面查看”和“操作”事件提交到您的 Google Analytics 仪表板。

在您的应用中使用 Google Analytics

准备工作

本菜谱使用Ti.Google.Analytics CommonJS 模块。此模块和其他代码资源可以从本书提供的源代码中下载,或者通过本菜谱末尾的“另请参阅”部分提供的链接单独下载。将此模块安装到您的项目中很简单。只需将Ti.Google.Analytics.js文件复制到您的项目中,如下面的截图所示:

准备工作

如何操作…

一旦您将Ti.Google.Analytics.js文件添加到您的项目中,您需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到您的代码中,如下面的代码块所示:

//Create our application namespace
var my = {
  gAnalyticsModule : require('Ti.Google.Analytics'),
  isAndroid : (Ti.Platform.osname === 'android'),
  session:{}
};

创建模块的实例

菜谱的下一步需要您拥有一个 Google Analytics 密钥。要获取密钥,请注册您的应用至 www.google.com/analytics。一旦您有了密钥,您需要创建Analytics模块的新实例,并提供您的分析密钥,如下所示:

my.session.ga = new my.gaModule('YOUR_KEY_HERE');

辅助函数

辅助函数有助于提供有关用户设备更有意义的信息。这些函数在每次触发PageView操作时都会在整个菜谱中使用。

  1. 下面的代码片段演示了如何在启动应用程序时将isTablet属性附加到应用程序命名空间。该属性被其他函数用来指示应用程序是在手机还是平板电脑上运行。

    my.isTablet = (function() {
      var tabletCheck = Ti.Platform.osname === 'ipad' || 
        (Ti.Platform.osname === 'android' && 
        (!(Math.min(
          Ti.Platform.displayCaps.platformHeight,
          Ti.Platform.displayCaps.platformWidth
        ) < 700)));
    
      return tabletCheck;
    })();
    
  2. basePage属性以类似于网站路由的方式工作,并将 URL 的第一部分(发送到 Google)设置为设备指示器。在 Google Analytics 中,这将允许您通过设备更好地细分使用模式。

    my.basePage = (function(){
      if(my.isAndroid){
        return ((my.isTablet)? 
        'AndroidTablet' : 'AndroidPhone');
      }else{
        //Return iPhone or iPad
        return Ti.Platform.model;
      }
    })();
    

开始记录事件

下一步是调用start方法。这将使模块开始收集分析请求。start方法接受一个整数值,表示您希望模块多久发送一次排队到 Google 的分析请求。这由模块内部使用间隔计时器处理。

my.session.ga.start(10);

创建我们的示例 UI

本节概述了用于触发和提交 Google Analytics 请求的示例 UI。

  1. 首先,创建一个Ti.UI.Window来锚定所有 UI 控件。

      var win = Ti.UI.createWindow({
        backgroundColor:'#fff', layout:'vertical'
      });
    
  2. 在创建我们的窗口后,添加了两个Ti.UI.Button控件。这些按钮将在后面的菜谱中用于演示如何创建trackEventPageview事件。

      var button1 = Ti.UI.createButton({
          title: 'Log Action', top:40, height:45, width:Ti.UI.FILL
      });
      win.add(button1);
    
      var button2 = Ti.UI.createButton({
        title: 'Open New Page', top:40, height:45, width:Ti.UI.FILL
      });
      win.add(button2);
    

记录一个动作

trackEvent函数允许您向 Google Analytics 发布细粒度的事件跟踪。此方法需要以下参数,这些参数将用于将操作发布到您的 Google Analytics 仪表板:

  • 类别:通常是与之交互的对象(例如,按钮)

  • 动作:交互类型(例如,点击)

  • 标签:用于对事件进行分类(例如,导航按钮)

  • :值必须是非负数。用于传递计数(例如,四次)

下一个片段演示了在按下button1时如何调用trackEvent方法。当按钮的点击事件触发时,trackEvent方法会以button类别、click动作、recipe_button标签和trigger_event值被调用。

  button1.addEventListener('click',function(e){
    my.session.ga.trackEvent('button','click', 'win_button', 'trigger_event');
  });

打开窗口时的 Pageview 函数

使用trackPageview函数来模拟在您的 Google Analytics 仪表板中显示的页面流量或查看次数。使用之前创建的basePage属性,您可以通过以下代码块中显示的约定创建特定于设备的窗口跟踪:

  win.addEventListener('open',function(e){
    my.session.ga.trackPageview(my.basePage + "/Main");
  });
  win.open();

子窗口上的 Pageview 函数

本菜谱的以下部分演示了如何使用trackPageviewtrackEvent方法记录何时以及如何打开子窗口或视图。当用户按下button2并触发点击事件时,Google Analytics 模块用于记录导航过程的每个步骤。

  button2.addEventListener('click',function(e){
  1. 使用trackEvent方法记录按下打开子窗口的按钮:

      my.session.ga.trackEvent('button','click', 'win_button', 'open_win2');
    

    您需要向trackEvent方法提供以下参数:

    • 类别:本示例中提供了button类别值

    • 动作:本示例中提供了click动作值

    • 标签:本示例中提供了win_button标签值

    • :本示例中提供了open_win2

  2. 创建一个新的窗口来演示在子窗口上trackPageview功能。

        var win2 = Ti.UI.createWindow({
          backgroundColor:'yellow', layout:'vertical'
        });
    
  3. win2窗口加载时,会调用trackPageview方法,记录个人已查看该页面。my.basePage用于创建一个路由,显示哪种类型的设备访问了/win2窗口。

        win2.addEventListener('open',function(e){
          my.session.ga.trackPageview(my.basePage + "/win2");
        });
        win2.open({modal:true});
      });
    

参见

  • 这个配方使用 Roger Chapman 的 Titanium Mobile Google Analytics 模块的修改版本。要了解更多关于这个模块的信息,请访问github.com/rogchap/Titanium-Google-Analytics

  • 关于检测不同设备特性的更多信息,请参阅第一章中的使用平台指示器配方,模式和平台工具

使用 SUDS.js 调用 SOAP 服务

在许多企业市场细分领域,SOAP 服务仍然是主导的 Web 服务协议。由于 SOAP 通常通过 HTTP 实现,大多数网络客户端,包括 Titanium 的Ti.Network,都可以有效地与该协议交互。

即使使用 Titanium 的Ti.Network模块与 SOAP 一起工作,信封和 XML 结果也可能具有挑战性,通常需要创建一个 SOAP 信封和大量的 XML 操作。这个配方演示了几个开源模块如何提高你与 SOAP 服务和它们的 XML 结果交互时的生产力。

为了帮助说明如何与 SOAP 服务交互,这个配方使用www.webserviceX.NET天气 SOAP 服务来返回在城市:字段中输入的城市天气结果,如下面的截图所示:

使用 SUDS.js 调用 SOAP 服务

准备中

这个配方使用SUDSXMLToolsCommonJS 模块。这些模块和其他代码资产可以从本书提供的源代码中下载,或者通过此配方末尾的参见部分提供的链接单独下载。安装这些模块很简单,只需将suds2.jsXMLTools.js文件复制到你的 Titanium 项目中,如下面的截图所示:

准备中

如何做到这一点…

一旦你将suds2.jsXMLTools.js模块添加到你的项目中,你需要在app.js文件中创建你的应用程序命名空间,并使用require将模块导入到你的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  suds : require('suds2'),
  xmlTools : require("XMLTools"),
  isAndroid : (Ti.Platform.osname === 'android')
};

SOAP 辅助方法

除了之前导入的两个 CommonJS 模块之外,这个配方还使用soapHelper对象来处理格式化和配置活动。

var soapHelper = {
  1. 配置对象包含suds.js返回 XML 结果所需的所有配置细节:

      config : {
        endpoint:"http://www.webservicex.com/globalweather.asmx";  targetNamespace: 'http://www.webserviceX.NET/',
        includeNS : false,
        addTargetSchema : true 
      },
    
  2. resultParser对象用于将返回的 XML 结果格式化为 JavaScript 对象:

      resultParser : {
    
  3. removeHeader对象用于删除 XML 头节点。Android 要求在parseString函数正确创建 XML 文档对象之前删除头。

        removeHeader : function(text){
          return text.replace(
            '<?xml version="1.0" encoding="utf-16"?>',''); 
        },
    
  4. xmlToObject 函数将 Ti.XML.Document 对象转换为 JavaScript 对象。

        xmlToObject : function(doc){
    
  5. 第一步是获取 GetWeatherResponse 标签的 Ti.XML.Nodelist

    var results = doc.documentElement.getElementsByTagName(
     'GetWeatherResponse');
    
  6. Android 和 iOS 处理转换过程的方式不同。使用 my.isAndroid 属性来分支转换逻辑。

    if(my.isAndroid){
    
  7. 天气服务结果包含一个嵌套的 XML 文档。以下示例演示了如何从 GetWeatherResponse 节点读取嵌入的 XML 文档到新的 Ti.XML.Document。使用 removeHeader 函数来修复 textContent 值,以符合 Android 的 XML Document 格式。

    var docCurrent =Ti.XML.parseString(
      soapHelper.resultParser.removeHeader(
      results.item(0).textContent));
    
  8. 接下来,将 Ti.XML.Document 对象提供给 XMLTools 模块的构造函数,然后使用 toObject 方法将其转换为 JavaScript 对象,如下面的代码片段所示:

          return new my.xmlTools(docCurrent).toObject();
        }else{
    
  9. 在 iOS 上,我们使用 getChildNodes 函数来获取天气子节点:

        var weather =results.item(0).getChildNodes()
          .item(0).getChildNodes();
    
  10. 将天气节点的 XML 字符串加载到 XMLTools 模块的构造函数中,然后使用 toObject 方法将其转换为 JavaScript 对象,如下面的代码块所示:

            var docCurrentFromString = Ti.XML.parseString(soapHelper.resultParser.removeHeader(weather.item(0).textContent));
            return new my.xmlTools(docCurrentFromString).toObject();
          }
        }
      }
    };
    

创建 UI

本节概述了用于调用和显示天气 SOAP 服务结果的示例 UI。

  1. 为所有 UI 元素创建一个新的 Ti.UI.Window

      var win = Ti.UI.createWindow({
        backgroundColor:'#fff', layout:'vertical'
      });
    
  2. 文本字段 txtCity 允许用户输入他们希望显示天气的城市。

      var txtCity = Ti.UI.createTextField({
          value:'New York', hintText:'Enter city',top:10, height:40, left:10, right:10,textAlign:'left', borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED 
      });
      win.add(txtCity);
    
  3. 文本字段 txtCountry 允许用户输入城市所属的国家名称。

      var txtCountry = Ti.UI.createTextField({
        value:'United States', hintText:'Enter country',top:10, height:40, left:10, right:10, textAlign:'left', borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED 
      });
      win.add(txtCountry);
    
  4. goButton 是一个 Ti.UI.Button,用于调用天气 SOAP 服务。

      var goButton = Ti.UI.createButton({
        title:'Find Weather', left:10, top:10
      });
      win.add(goButton);
    
  5. tableView 是一个 Ti.UI.TableView,用于显示 SOAP 服务的输出结果。

      var tableView = Ti.UI.createTableView({
        visible : false, top:10, height:100, width:Ti.UI.FILL
      });
      win.add(tableView);
    

uiHelpers 对象

uiHelpers 对象用于根据应用的不同状态更新 UI 对象,以及使用 SOAP 服务的结果加载 tableView

var uiHelpers = {

updateUI 用于格式化 SOAP 服务返回的对象结果以进行显示。

  updateUI : function(weather){
    var data = [];
    tableView.visible = true;
    data.push({title: "Sky Conditions: " + weather.SkyConditions, color:'#000'});
    data.push({title: "Temperature: " + weather.Temperature, color:'#000'});
    data.push({title: "Time: " + weather.Time});
    tableView.setData(data);	
  };

resetUI 方法用于在 SUDS 调用 web 服务时隐藏 tableView 对用户可见。此方法还用于在 SUDS 调用导致错误时隐藏 tableView

  resetUI :function(){
    tableView.visible = false;
  };
};

调用 SOAP 服务

goButton 上的点击事件用于执行天气 SOAP 服务调用。

goButton.addEventListener('click',function(e){
  1. 首先调用 resetUI 方法来隐藏 tableView,在调用服务的过程中。

      uiHelpers.resetUI()
    
  2. 使用之前在 soapHelper.config 对象中定义的配置信息创建一个新的 sudsClient 实例。

      var sudsClient = new my.suds(soapHelper.config);
    
  3. 然后在 sudsClient 上调用 invoke 方法。提供的第一个参数是 suds 应该调用的 SOAP 方法。

      sudsClient.invoke('GetWeather', 
    
  4. 提供给 sudsClient 的第二个参数是用户请求的城市和国家名称,以检索天气信息。

      {
        CityName: txtCity.value,
        CountryName: txtCountry.value
      }, 
    
  5. invoke 方法的最后一个参数是回调方法 SUDS。此回调方法将提供一个包含服务结果的 Ti.XML.Document。以下示例演示了使用内联函数作为回调方法:

      function(xml) {
    
  6. 内联回调方法将在服务完成后接收一个Ti.XML.Document。一旦接收,结果将使用前面在菜谱中详细说明的resultParser对象解析为 JavaScript 对象。

        var weather = soapHelper.resultParser.xml2Object(xml);
    
  7. Status属性更改在解析的对象上,以确定是否已成功创建了天气对象。

        if(weather.Status==='Success'){
    
  8. 如果服务结果已成功转换为对象,它们将被提供给updateUI方法,以向用户显示。

          uiHelpers.updateUI(weather);
        }else{
    
  9. 如果在调用服务或处理结果时发生错误,我们将向用户发出警报,然后隐藏tableView显示对象。

          alert("Sorry a problem happened");
          uiHelpers.resetUI();
        }
      });
    });
    

参考信息

使用 LinkedIn 联系人 API

LinkedIn 是一个流行的专业社交网络网站。LinkedIn API 为你的应用程序提供了丰富的集成服务。对于面向企业的应用程序,LinkedIn 的某些功能,如消息和联系人,可能至关重要。常见的例子包括为销售代表提供访问应用程序内联系人的权限。

本菜谱演示了如何以可搜索的方式将 LinkedIn 联系人 API 集成到你的 Titanium 应用程序中。

使用 LinkedIn 联系人 API

准备工作

本菜谱使用了几个模块,包括创新的 LinkedIn 模块linkedin_module-min.js。此模块和其他代码资产可以从本书提供的源代码中下载,或者通过本菜谱末尾的“参考信息”部分提供的链接单独下载。设置本菜谱的依赖项很简单。首先,将lib文件夹复制到你的 Titanium 项目的Resources文件夹中,然后,将copy formatters.jslinkedin_module-min.js文件复制到Resources文件夹中,如下面的截图所示:

准备工作

如何操作…

在设置好菜谱的依赖项之后,下一步是在app.js文件中创建你的应用程序命名空间,并使用require将模块导入到你的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  isAndroid : (Ti.Platform.osname === 'android'),
  linkedMod: require('linkedin_module-min'),
  formatters : require('formatters')
};

添加你的 API 密钥和密钥

使用 LinkedIn 模块的第一步是在 www.linkedin.com/secure/developer 创建 LinkedIn 应用程序。一旦注册了您的应用程序,LinkedIn 将为您提供与他们的 API 交互所需的 API 和认证密钥。本食谱中使用的所有 API 都需要 OAuth 1.0a 认证来连接。LinkedIn 模块将通过使用 init 方法注册您的密钥和 API 密钥来为您处理此操作,如下面的示例所示:

my.linkedMod.init('YOUR_SECRET_KEY', 'YOUR_API_KEY');

小贴士

在使用模块的任何功能之前,您必须使用 init 方法设置您的密钥和 API 密钥。

添加权限

默认情况下,如果没有指定权限,您的应用程序将只有读取当前用户基本“数字名片”信息的权利。由于本食谱需要访问用户的联系人信息,我们必须调用 addPermission 方法来请求 r_network 权限,如下面的代码片段所示:

my.linkedMod.addPermission('r_network');

要添加多个权限,只需多次调用 addPermission 方法。以下代码片段显示了如何将完整配置文件访问权限添加到应用程序中:

my.linkedMod.addPermission('r_fullprofile');

小贴士

要查看所有权限的完整列表,请访问 developer.linkedin.com/documents/authentication

创建 UI

本食谱的这一部分概述了如何创建用于显示和搜索 LinkedIn 联系人的 UI。

  1. 首先,创建一个 Ti.UI.Window。这将用于附加所有我们的视觉元素,并在打开事件触发时调用 LinkedIn API。

      var win = Ti.UI.createWindow({
        backgroundColor:'#fff'
      });
    
  2. 然后创建一个 Ti.UI.SearchBar。搜索栏控件提供了一个搜索框,可以过滤 Ti.UI.TableView 的内容。在本食谱中,我们使用搜索栏控件按姓氏过滤用户的 LinkedIn 联系人。

      var search = Ti.UI.createSearchBar({
        barColor:'#385292', showCancel:true, hintText:'Search'
      });
    
  3. 最后添加到 Ti.UI.Window 的 UI 组件是 Ti.UI.TableView,它将用于显示用户的 LinkedIn 联系人。

      var tableView = Ti.UI.createTableView({
        height:Ti.UI.FILL, width:Ti.UI.FILL, search:search, filterAttribute:'lastName'
      });
      win.add(tableView);
    

加载您的 LinkedIn 联系人

下一个代码片段演示了如何使用 LinkedIn 模块在 Ti.UI.Window 窗口加载时加载联系人列表。

  win.addEventListener('open',function(e){
  1. 使用 uiHelper.createWaitMsg 方法向用户显示加载信息。

        uiHelpers.createWaitMsg('Loading please wait...');
    
  2. 使用模块的 getConnections 方法从 LinkedIn API 中查询用户的联系人。在下一个示例中,使用内联函数作为回调,将用户联系人作为 _e 变量返回。

        my.linkedMod.getConnections(function(_e) {
    
  3. 使用 uiHelpers.displayContacts 方法来格式化和应用返回的联系人以进行显示。

          uiHelpers.displayContacts(_e);
        });
      });
    
      win.open();
    

使用 uiHelpers 对象格式化结果

uiHelpers 对象用于格式化 LinkedIn 联系人 API 的结果以进行显示。

var uiHelpers = {
  1. createWaitMsg 函数用于在调用 LinkedIn API 时在 tableView 中显示等待信息。

      createWaitMsg : function(msg){
        tableView.setData([{title:msg}]);
      },
    
  2. displayContacts 方法是用于转换和显示 API 结果的主要方法。

      displayContacts : function(apiResults){
    
  3. 更新 tableView 以提醒用户我们现在正在加载他们的联系人。

        uiHelpers.createWaitMsg('Loading contacts please wait...');
    
  4. 调用convertToObject方法将 LinkedIn XML 结果转换为更易于管理的 JavaScript 对象。

        var resultAsObjects =  my.resultParser.convertToObjects(apiResults);
    
  5. 如果转换结果为null,向用户显示错误消息。

        if(resultAsObjects == null){
          alert('Sorry we ran into an error');
          return;
        }
    
  6. 使用formatter.createContactTableRows函数,将 JavaScript 对象格式化为本食谱截图 earlier 中显示的Ti.UI.TableViewRow布局。然后,使用格式化后的行更新tableView

        tableView.setData(my.formatters.createContactTableRows( resultAsObjects)
        );
      }
    };
    

解析 LinkedIn API 结果

使用resultParser对象将 LinkedIn API 提供的 XML 解析为更易于管理的 JavaScript 对象。

my.resultParser = {
  1. 使用getText函数从提供的Ti.XML.Element返回一个特定键。如果没有找到键,则返回一个 null 值。

      getText : function(item, key) {
        var searchItem = item.getElementsByTagName(key).item(0);
        return ((searchItem == null)? null : searchItem.textContent);
      },
    
  2. 使用getQueryParams函数来返回一个包含提供 URL 的所有查询字符串参数的对象。

      getQueryParams : function(url){
        var params = {};
        url = url.substring(url.indexOf('?')+1).split('&');
        var pair, d = decodeURIComponent;
        for (var i = url.length - 1; i >= 0; i--) {
          pair = url[i].split('=');
          params[d(pair[0])] = d(pair[1]);
        }
        return params;
      },
    
  3. formatUrl函数返回联系人的个人资料 URL。如果无法确定 URL,则提供一个到linkedin.com的链接。

      formatUrl : function(findKey){
        return ((findKey.hasOwnProperty("key")) ? ("http://www.linkedin.com/profile/view?id=" + findKey.key) : "http://www.linkedin.com");
      },
    
  4. 使用getProfileUrl函数来返回联系人的个人资料 URL。由于 LinkedIn API 不提供此信息,通过解析site-standard-profile-request节点以获取关键细节来生成一个 URL。以下代码片段展示了如何构建此 URL:

      getProfileUrl : function(item) {
        var searchItem = item.getElementsByTagName("site-standard-profile-request").item(0);
        if(searchItem == null){
          return null;
        }
        if(searchItem.hasChildNodes()){
          var findKey = my.resultParser.getQueryParams(searchItem.getElementsByTagName('url').item(0).textContent);
          return my.resultParser.formatUrl(findKey);
        }else{
          return null;
        }
      },
    
  5. 使用isPublic函数来确定联系人的信息是否公开。如果个人资料不是公开的,我们不会将其添加到显示的联系人列表中。

      isPublic : function(item){
        return !((my.resultParser.getProfileUrl(item)==null));
      },
    
  6. convertToObjects是负责将 LinkedIn 联系人 XML 转换为 JavaScript 对象的主要方法。

      convertToObjects : function(xmlString){
        var contacts = [];
    
  7. 首先,将 API 提供的 XML 字符串加载到Ti.XML.Document中。

        var doc = Ti.XML.parseString(xmlString);
    
  8. 获取所有在person标签下的 XML 节点。

        var items = doc.documentElement.getElementsByTagName("person");
    
  9. 开始遍历person标签下的每个节点。

        for (var i = 0; i < items.length; i++) {
    
  10. 检查个人资料是否公开。

          if(my.resultParser.isPublic(items.item(i))){
    
  11. 如果个人资料是公开的,则使用来自Ti.XML.Node的属性创建一个 JavaScript 对象。

            contacts.push({
              id : my.resultParser.getText(items.item(i), 'id'), headLine : my.resultParser.getText(items.item(i), 'headline'),
              firstName : my.resultParser.getText(items.item(i), 'first-name'),
              lastName : my.resultParser.getText(items.item(i), 'last-name'),
              pictureUrl : my.resultParser.getText(items.item(i), 'picture-url'),
              profileUrl : my.resultParser.getProfileUrl(items.item(i))
            });
          }
        }
    
  12. 对于拥有大量联系人的个人来说,这个转换过程可能会消耗大量内存。为了帮助减少内存中变量的数量,我们在返回转换后的结果之前将所有临时对象设置为null

        doc = null, xmlString = null;
        return contacts;
      }
    };
    

参见

第五章. 图表和文档

在本章中,我们将介绍以下内容:

  • 打开 PDF 文档

  • 使用 iPad 进行文档签名

  • 从图像或屏幕截图创建 PDF 文档

  • 使用 jsPDF 生成 PDF 文档

  • 使用 RGraph 创建调度图表

  • 使用 Google 仪表板显示信息

简介

通过移动设备的便利性,您的客户和员工能够随时随地访问数据、文档和相关信息。本章提供了配方,以帮助开发一目了然和便携式信息。

根据您的行业,创新的移动文档策略可以提供关键的竞争优势。表格、合同、配方和发票都是移动文档策略可以降低成本并更好地吸引用户注意力的领域示例。在本章中,我们将介绍几种处理跨平台文档的策略。

使用图表和图形创建仪表板,以绘制引人入胜的画面,这是管理团队常见的请求,并且是利用移动技术提供商业价值的一种强大方式。这种价值主张的一个例子是移动性,它使您的营销、销售或运营团队能够以一目了然的方式获取当前市场数据,从而使管理团队能够做出更快的决策。在本章中,我们将展示如何使用 Titanium 创建强大的交互式图表体验。

打开 PDF 文档

在企业级应用开发中,能够打开、查看和交换 PDF 文档是一个常见的要求。本配方讨论了如何使用 openPDF 的CommonJS模块以跨平台的方式查看 PDF 文档。

以下截图展示了该配方在 iPhone 和 Android 设备上的运行情况:

打开 PDF 文档

准备工作

该配方同时使用CommonJS和 Android 原生模块。这些可以从本书提供的源代码中下载,或者通过本配方末尾的另请参阅部分提供的链接单独下载。将这些模块安装到您的项目中很简单。只需将openPDF.js文件和bencoding.android.tools文件夹复制到您的 Titanium 项目中,如下面的截图所示:

准备就绪

在复制模块文件夹后,您需要在 Titanium Studio 中点击您的tiapp.xml文件,并添加对bencoding.android.tools模块的引用,如下面的截图所示:

准备就绪

如何操作…

一旦您已将openPDF.js和原生 Android 模块添加到您的项目中,您需要创建您的应用程序命名空间,并使用require将模块导入到您的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  pdfOpener : require('openPDF'),
  isAndroid : (Ti.Platform.osname === 'android')
};

创建指向 PDF 文件的链接

此食谱的下一步是创建一个变量,包含我们的 PDF 文件的路径。为了示例目的,我们使用一个名为w4.pdf的 PDF 文件,该文件位于我们项目的resources目录中。以下代码片段演示了如何创建此路径变量:

my.pdfFile = Ti.Filesystem.resourcesDirectory + 'w4.pdf';

创建我们的示例 UI

此食谱提供了一个基本的用户界面,以帮助说明如何使用openPDF模块。这个简单的屏幕包含一个按钮,该按钮打开一个对话框,提供用户可以选择以打开 PDF 文件的不同选项。如果此食谱在 iOS 设备上运行,则用户将有一个第二个按钮,演示如何在应用内部打开 PDF 文件。

  1. 我们首先创建一个Ti.UI.Window窗口,将所有 UI 元素附加到该窗口上:

    var win = Ti.UI.createWindow({
        exitOnClose: true, title:"Open PDF Example", backgroundColor:'#fff' 
    });
    
  2. 接下来,我们添加一个标签,指示用户按下哪个按钮:

    var infoLabel = Ti.UI.createLabel({
        text:'Press the button to open a PDF file', top:10, left:10, right:10, height:40, textAlign:'center'
    });
    win.add(infoLabel);
    
  3. 接下来,将启动我们第一个示例的按钮添加到Ti.UI.Window窗口中:

    var goButton = Ti.UI.createButton({
        title: 'open pdf', height: '60dp', width: '150dp', top: '140dp'
    });
    win.add(goButton);
    

在按钮点击时打开 PDF 文件

使用openPDF模块的第一个示例如下面的goButton点击事件代码片段所示:

goButton.addEventListener('click', function(e) {
  1. 首先,调用isSupported方法以确定设备是否支持 PDF 文件。如果支持,则此方法将返回true,如果不支持,则返回false

      if(my.pdfOpener.isSupported()){
    
  2. 接下来,将我们的 PDF 文件的文件路径提供给open方法。然后,将向用户展示一个options对话框,询问他们希望如何查看文件:

        my.pdfOpener.open(my.pdfFile);
      }else{
    
  3. 如果isSupported方法返回false,则可以调用isSupportedDetail方法以获取更多关于为什么无法显示 PDF 文件的信息。如下面的代码片段所示,可以使用isSupportedDetail方法返回的reason属性来提醒用户具体问题。您还可以使用code属性来创建自己的自定义消息。

        alert(my.pdfOpener.isSupportedDetail().reason);
      }
    });
    

在你的 iOS 应用中打开 PDF 文件

iOS 平台内置了对 PDF 文件的支持。openPDF模块利用这个平台,在您的应用中提供创建 PDF 对话框查看器的功能,如下面的截图所示:

在你的 iOS 应用中打开 PDF 文件

  1. 在本节中,进行了一次检查以确保食谱在 iOS 设备上运行。如果是这样,则将按钮goButton2添加到我们的Ti.UI.Window窗口中:

    if(!my.isAndroid){
        var goButton2 = Ti.UI.createButton({
        title: 'open pdf menu', height: 60, width: 150d, bottom: 140
      });
      win.add(goButton2);
    
  2. goButton2点击事件演示了如何在对话框窗口中打开 PDF 文件:

      goButton2.addEventListener('click', function(e) {
    
  3. 接下来,调用isSupported方法以验证设备是否可以读取 PDF 文件:

        if(!my.pdfOpener.isSupported()){
    
  4. 如果设备不支持显示 PDF 文件,则调用isSupportDetail方法以返回无法显示 PDF 文件的原因:

          alert(my.pdfOpener.isSupportedDetail().reason);
          return;
        }
    
  5. 最后,调用openDialog方法以显示 PDF 文件。openDialog方法需要两个参数。第一个是要显示的 PDF 文件的路径。第二个是一个包含一个 UI 对象的配置对象,该对象指定了查看器应显示的视图相对位置。在以下示例中,我们提供之前创建的infoLabel标签作为我们的view对象:

        my.pdfOpener.openDialog(my.pdfFile,
          {view:infoLabel, animated:true}
        );
      });
    }
    

    小贴士

    可以向配置对象添加其他元素,例如 animated 属性,以确定在打开和关闭 PDF 文件时是否应该应用动画。

要程序化地关闭 PDF 对话框,请使用 closeDialog 方法,如下面的代码片段所示:

my.pdfOpener.closeDialog();

关闭窗口时的文件清理

openPDF 模块创建缓存对象和临时文件以协助显示过程。当不再需要 PDF 操作时,调用 dispose 方法很重要:

win.addEventListener('close', function(e) {
  my.pdfOpener.dispose();
});

小贴士

如果您在 iOS 上使用 openDialog 选项,调用 dispose 也会关闭 PDF 对话框。

另请参阅

要了解更多关于本食谱中使用的库和框架的信息,请访问以下链接:

使用 iPad 进行文档签名

平板和其他触摸设备为处理文档提供了一个自然沉浸式环境。通过使用几个开源模块和我们的示例 PDF,本食谱说明了如何为您的组织创建一个文档签名应用程序。

以下截图展示了本食谱在 iPad 上的运行情况:

使用 iPad 进行文档签名

小贴士

本食谱仅适用于 iPad。

准备就绪

本食谱使用 CommonJS 和本地模块。这些模块可以从本书提供的源代码中下载,或者通过本食谱末尾的 另请参阅 部分的链接单独下载。安装过程简单,只需复制几个文件夹和文件。只需将 library.js 文件和 modules 文件夹复制到您的 Titanium 项目中,如下面的截图所示:

准备就绪

在复制 modules 文件夹后,您需要在 Titanium Studio 中点击您的 tiapp.xml 文件,并添加对 ti.paint 模块的引用,如下面的截图所示:

准备就绪

如何操作…

一旦您已将library.js和原生模块添加到项目中,您需要创建应用程序命名空间,并使用require将模块导入到app.js文件中,如下代码片段所示:

//Create our application namespace
var my = {
  paint : require('ti.paint'),
  library : require('library')
};

创建菜谱窗口

以下代码片段演示了如何创建一个Ti.UI.Window窗口来容纳此菜谱中使用的所有控件:

  var win = Ti.UI.createWindow({
    title:"iPad Signature Recipe", backgroundColor:'#fff' 
  });

添加文档视图

添加到Ti.UI.Window的第一个 UI 元素用于显示菜谱的文档。在 iOS 上,Ti.UI.WebView对象内置了对显示 PDF 文件的支持。此示例演示了如何从 Titanium 项目的Resources文件夹中加载单个 PDF 文档。

  var docView = Ti.UI.createWebView({
    top:60, bottom:250, left:10, right:20, url:'w4.pdf'
  });
  win.add(docView);

添加签名视图

接下来添加到Ti.UI.Window窗口的下一个 UI 元素是PaintView。这个UIView是在设置过程中添加的Ti.Paint模块提供的。此视图允许用户通过触摸在屏幕上绘制。以下代码片段演示了如何设置此视图,以便用户可以使用黑色色调和 10 点的笔触大小进行绘制:

  var vwSign = my.paint.createPaintView({
      bottom:60, left:10, right:10, height:140, strokeColor:'#000', strokeAlpha:255, strokeWidth:10, eraseMode:false, borderColor:'#999'
  });
  win.add(vwSign);

添加按钮

下一步是创建用于保存或清除签名的按钮。bClear按钮被添加到屏幕的左下角。此按钮将提供用户清除签名区域的能力。此按钮还可以用于删除已保存的签名。

  var bClear = Ti.UI.createButton({
      title:'Remove Signature', left:10, bottom:10, width:150, height:45
  });
  win.add(bClear);

当点击bClear按钮的事件被触发时,签名将被清除,并且任何已保存的签名文件都将被移除:

  bClear.addEventListener('click',function(e){
  1. vwSign上调用clear方法。这将移除PaintView中的所有内容。

        vwSign.clear();
    
  2. 接下来,在库CommonJS模块上调用deleteSignature方法。这将移除存储的签名图像。

        my.library.deleteSignature();
    
  3. 然后,vwSign启用触摸功能,并通知用户他们的签名已被移除。完成此操作后,用户将能够创建新的文档签名。

        vwSign.touchEnabled = true;
        alert("Signature has been reset.");
      });
    
  4. 接下来,将bSave按钮添加到 iPad 屏幕的右下角。此按钮允许用户保存签名的副本以供以后使用。

      var bSave = Ti.UI.createButton({
        title:'Save Signature', right:10, bottom:10, width:150, height:45
      });
      win.add(bSave);
    
  5. 当点击bSave按钮的事件被触发时,用户的签名图像将被保存到设备上的一个库文件夹中。

      bSave.addEventListener('click',function(e){
    

执行以下步骤以完成保存过程:

  1. 将签名视图的内容转换为图像 blob。然后,将此 blob 提供给库CommonJS模块的saveSignature方法,以便将其持久保存到设备的文档文件夹中。

        my.library.saveSignature(vwSign.toImage());
    
  2. 一旦签名图像已保存到磁盘,签名表面将被设置为只读,因此无法执行任何额外的更新:

        vwSign.touchEnabled = false;
        alert("You have successfully signed this document.");
      });
    

重新加载已保存的签名

本菜谱的最后一部分描述了在打开窗口时,如何检查文档是否已签名并加载存储的签名以供显示。

  win.addEventListener('open',function(e){

首先在library模块上调用isSigned方法。此方法将检查签名文件是否存在于设备的library文件夹中。

    if(my.library.isSigned()){

signatureUrl方法用于向视图提供签名的图像路径以供显示:

      vwSign.image = my.library.signatureUrl();

由于文档已经被签署,因此签名视图随后被禁用,因此在没有清除现有签名的情况下无法进行任何额外的更新。

      vwSign.touchEnabled = false;
    }
  });

  win.open({modal:true});

相关内容

从图像或屏幕截图创建 PDF 文档

移动设备是 PDF 文档的绝佳消费者。但是,创建它们怎么办?许多场景,例如发布食谱或发票,需要设备创建 PDF 文件。实现这一目标的一个简单方法是将图像转换为 PDF 文件。这种方法与 Titanium 强大的图像创建和维护功能配合得很好。

这个食谱演示了如何将 Ti.UI.View 转换为 PDF 文档。还演示了如何使用 Titanium 的 Ti.Media.takeScreenshot API 将应用程序的全屏图像转换为 PDF 文件。这可能对希望“打印”屏幕的消费者有所帮助。

从图像或屏幕截图创建 PDF 文档

准备就绪

这个食谱利用了 bencoding.pdf 原生模块。这个模块及其支持源代码可以从本书提供的源代码中下载,或者通过本食谱末尾的 相关内容 部分的链接单独下载。这个食谱的安装过程很简单,只需将两个 bencoding.pdf 文件夹复制到相应的 modules 文件夹中,如本节末尾的截图所示。如果你的 Titanium 项目当前没有 modules 文件夹,你可以简单地将这个食谱中的完整 modules 文件夹复制到你的项目中。除了这两个原生模块,你还需要复制 sampleUI.js 文件,如下面的截图所示:

准备就绪

在复制 modules 文件夹后,你需要在 Titanium Studio 中点击你的 tiapp.xml 文件,并添加对 bencoding.pdf 模块的引用,如下面的截图所示。请注意,对于 bencoding.pdf,将只出现一个模块条目,但一旦选择,iOS 和 Android 都将被添加到你的 tiapp.xml 文件中。

准备就绪

如何操作...

一旦你将 sampleUI.js 文件和原生模块添加到你的项目中,你需要在 app.js 文件中创建你的应用程序命名空间,并使用 require 将模块导入到你的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
    PDF : require('bencoding.pdf'),sampleUI : require('sampleUI'),isAndroid : Ti.Platform.osname === 'android'
};

为食谱创建 UI

现在,我们创建一个 Ti.UI.Window 窗口来承载食谱的 UI 元素:

var win = Ti.UI.createWindow({
  title:'PDF Screenshot', backgroundColor:'#fff'
});

一旦创建了Ti.UI.Window,样本发票的内容由我们的sampleUI模块提供。sampleUI模块使用几个Ti.UI.ViewTi.UI.Label控件来生成样本发票布局。

var vwContent = my.sampleUI.fetchInvoiceUI();
win.add(vwContent);

从视图创建 PDF

接下来,将makeImageButton按钮添加到菜谱的Ti.UI.Window中。此按钮将稍后用于创建 PDF 样本发票。

var makeImageButton = Ti.UI.createButton({
    title:'Invoice PDF', bottom:5, left:10, width:135, height:50	
});
win.add(makeImageButton);

makeImageButton按钮的点击事件被触发时,将生成一个包含Ti.UI.View内容的 PDF。

makeImageButton.addEventListener('click',function(e){
  1. 生成 PDF 的第一步是从包含发票布局的Ti.UI.View创建一个图像。以下代码片段演示了如何在 iOS 和 Android 上完成此操作:

      var image = ((my.isAndroid)? vwContent.toImage().media : vwContent.toImage());
    
  2. 然后,将发票Ti.UI.View的图像 blob 提供给PDF Converters模块中的convertImageToPDF方法。此方法将提供的图像转换为 PDF Ti.File blob。除了 iOS 上的图像外,您还可以提供用于转换过程的分辨率。以下示例使用分辨率为 100:

      var pdfBlob = my.PDF.createConverters().convertImageToPDF(image,100);
    
  3. 然后,可以使用标准Ti.FileSystem方法将发票 PDF Ti.File blob 保存,如下所示:

      var pdfFile = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory, 'invoice.pdf'
      );
      pdfFile.write(pdfBlob);
    });
    

从截图创建 PDF 文档

可以使用类似的技术来创建包含应用截图的 PDF 文档。执行此操作的第一步是在菜谱的Ti.UI.Window中添加一个名为ssButton的按钮:

var ssButton = Ti.UI.createButton({
    title:'Screen to PDF', bottom:5, right:10, width:135, height:50
});
win.add(ssButton);

当我们点击ssButton按钮时,将进行截图并将其转换为 PDF 文件:

ssButton.addEventListener('click',function(e){

此过程的第一步是使用Ti.Media.takeScreenshot API 对应用进行截图:

  Ti.Media.takeScreenshot(function(event){

截图 API 返回设备屏幕的图像 blob:

    var image = event.media;

然后,将截图图像 blob 提供给PDF Converters模块中的convertImageToPDF方法。此方法将提供的图像转换为 PDF Ti.File blob。除了 iOS 上的图像外,您还可以提供用于转换过程的分辨率。此示例使用分辨率为 96 点:

    var pdfBlob = my.PDF.createConverters().convertImageToPDF(image,96);

然后,可以使用标准Ti.FileSystem方法将截图 PDF Ti.File blob 保存:

    var pdfFile = Ti.Filesystem.getFile(
      Ti.Filesystem.applicationDataDirectory, 'screenshot.pdf'
);
    pdfFile.write(pdfBlob);
  });
});

参见

使用 jsPDF 生成 PDF 文档

在移动设备上创建格式化的 PDF 文档可能很困难。将跨平台添加到等式中只会使这一挑战更加复杂。jsPDF JavaScript 库及其相关的 jsPDF Titanium 模块提供了一种强大且纯 JavaScript 的跨平台方法。

此菜谱演示了如何使用强大的 JavaScript API 创建格式化的 PDF 文档,类似于以下截图:

使用 jsPDF 生成 PDF 文档

准备工作

这个菜谱使用 Titanium 的jsPDF模块来创建 PDF 文件。此模块及其支持源代码可以从本书提供的源代码中下载,或者通过此菜谱末尾的“也见”部分提供的链接单独下载。此菜谱的安装过程简单直接,只需将jsPDFMod文件夹复制到您的 Titanium 项目的Resources文件夹中,如下截图所示:

准备中

如何做…

jsPDFMod文件夹添加到您的项目后,您需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到您的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
    jsPDF : require('./jsPDFMod/TiJSPDF'),isAndroid : Ti.Platform.osname === 'android'
};

创建菜谱的 UI

此菜谱包含一个基本的用户界面,允许用户生成菜谱 PDF 并打开电子邮件对话框。

  1. 创建一个Ti.UI.Window窗口来容纳所有 UI 控件:

        var win = Ti.UI.createWindow({
            backgroundColor: '#eee', title: 'jsPDF Sample',layout:'vertical'
        });
    
  2. 接下来,将goButton按钮添加到菜谱的Ti.UI.Window中:

        var goButton = Ti.UI.createButton({
          height: '50dp', title: 'Email a receipt', left:'20dp', right:'20dp', top:'40dp'
        });
        win.add(goButton);
    
  3. goButton按钮的点击事件通过调用generatePDFExample方法启动 PDF 创建过程:

        goButton.addEventListener('click', function (e) {
          generatePDFExample();
        });
    

使用 jsPDF 创建 PDF 文档

下面的代码片段描述了如何使用generatePDFExample通过jsPDF模块创建 PDF 文档:

    function generatePDFExample(){

下一行演示了如何创建jsPDF模块的新实例。这创建了一个新的虚拟 PDF 文档。

        var doc = new my.jsPDF();

下面的代码片段演示了如何向 PDF 文档对象添加文档属性:

        doc.setProperties({
            title: 'Title', subject: 'This is the subject', author: 'add author', keywords: 'one, two, three', creator: 'your organization'
        });

下面的代码片段演示了如何将图像嵌入到 PDF 文档中。重要的是图像必须包含完整的本地路径,如所示,否则在保存时文档将生成错误:

        var imgSample1 = Ti.Filesystem.resourcesDirectory + 'sample_logo.jpg';
        doc.addImage(imgSample1, 'JPEG', 10, 20, 35, 17);

我们然后使用Helvetica粗体和字体大小32创建一个标题:

        doc.setFont("helvetica");
        doc.setFontType("bold");
        doc.setFontSize(32);
        doc.text(20, 50, 'Your Receipt);

下面的代码片段使用normal字体和大小18创建了一个项目符号菜谱部分:

        doc.setFontType("normal");
        doc.setFontSize(18);
        doc.text(20, 70, '1\. Cargo Container $2399.99');
        doc.text(20, 80, '2\. Storage (5 days) $1299.99');
        doc.text(20, 90, '3\. Loading Fee $699.99');
        doc.text(20, 100, '4\. Transport Insurance $399.99');

创建一个指向我们希望保存recipe.pdf的位置的Ti.File对象:

        var myFile = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'recipe.pdf');

然后将Ti.File对象传递给jsPDF模块的save方法。save方法将生成一个具有先前创建的属性的 PDF 文件:

        doc.save(myFile);

将保存的 PDF 文件的Ti.File引用作为附件提供给Ti.UI.EmailDialog并打开,以便用户可以编写电子邮件:

        var emailDialog = Ti.UI.createEmailDialog();
        emailDialog.addAttachment(myFile);
        emailDialog.open();
      };

也见

  • 要了解更多关于 James Hall 创建的 jsPDF 项目的信息,请访问 GitHub 仓库github.com/MrRio/jsPDF

  • 此菜谱使用 Titanium 的 jsPDF 模块。有关模块的附加文档、示例和指南,请参阅以下链接:

    作者:Malcolm Hollingsworth

    仓库:github.com/Core-13

    赞助组织:Core13;网站:core13.co.uk

使用 RGraph 创建调度图

一图胜千言,在当今快节奏的商业环境中,使用图形来显示信息可以是一个竞争优势。本菜谱演示了您如何使用流行的 RGraph JavaScript 库创建反映项目调度的图表。以下截图显示了完成此菜谱后的样子:

使用 RGraph 创建调度图

准备工作

本菜谱使用 RGraph 和几个非原生组件来显示图表。这些组件可以从本书提供的源代码中下载,或者通过本菜谱末尾的“也见”部分提供的链接单独下载。

设置此菜谱的第一步是将以下截图中的web文件夹(突出显示)复制到项目文件夹的Resources文件夹中。在复制web文件夹后,您需要将teamBuilder.jschartLauncher.jssampleUI.jsscheduleBuilder.js文件复制到 Titanium 项目的Resources文件夹中,如下截图所示:

准备工作

如何操作…

在前面讨论的添加web文件夹和 CommonJS 模块之后,您需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到您的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
    schedule : require('scheduleBuilder'),team : require('teamBuilder'),chartDisplay : require('chartLauncher'),sampleUI : require('sampleUI')
};

为菜谱创建 UI

本菜谱的下一步是创建主Ti.UI.Window窗口。此Ti.UI.Window将用于启动菜谱的功能。

var win = Ti.UI.createWindow({
  backgroundColor: '#fff', title: 'RGraph Sample'
});

在创建Ti.UI.Window窗口后,我们在sampleUI模块上调用fetchDemoView方法。这向用户显示菜谱的说明:

win.add(my.sampleUI.fetchDemoView());

创建调度和分配任务

本部分菜谱演示了如何创建调度,并将任务分配给项目中的个人。

  1. 第一步是为团队成员创建一个调度。这是通过创建一个新的调度对象并提供个人的名字来完成的。以下行演示了如何为理查德创建一个调度:

    var richard = new my.schedule('Richard');
    
  2. 接下来,使用createTask方法为个人创建一个任务。

    richard.addTask({
        taskName:'Website',startDay:15,
        duration:28,percentageComplete:67,
        comment:'Work on website'
    });
    richard.addTask({
        taskName:'Fix Web Servers',startDay:40,
        duration:15,percentageComplete:50,
        comment:'Work with vendor'
    });
    

    createTask方法有以下参数:

    • 任务名称: 包含任务名称的字符串

    • 开始于某天: 这是任务开始的位置(天)

    • 持续时间: 任务所需的天数

    • 完成百分比: 任务的完成百分比

    • 注释: 任务的注释

  3. 最后,将团队成员的调度添加到团队中:

    my.team.add(richard);
    

下面的代码片段演示了如何执行前面讨论的步骤,为另一位团队成员:

var rachel = new my.schedule('Rachel');
rachel.addTask({
    taskName:'Mock-up',startDay:0, 
    duration:28, percentageComplete:50,
    comment:'Create initial product mock-ups'
});
rachel.addTask({
    taskName:'Mobile Images',startDay:40,
    duration:25, percentageComplete:74,
    comment:'Create mobile assets'
});
my.team.add(rachel);

提示

您可以通过以下详细说明的模式添加额外的个人。

启动示例

执行以下步骤以启动我们在上一节中创建的示例:

  1. 第一步是将goButton按钮添加到Ti.UI.Window窗口中,以便用户可以启动图表示例:

    var goButton = Ti.UI.createButton({
        title:'View schedule', bottom:'40dp',left:'25dp',right:'25dp', width:Ti.UI.FILL, height:'60dp'
    });
    win.add(goButton);
    
  2. goButton 按钮的点击事件通过在 chartLauncher 模块上使用 openDialog 方法启动图表示例。openDialog 方法使用之前创建的 my.team 对象生成包含团队预定的甘特图。

        goButton.addEventListener('click',function(e){
    

    在以下片段中展示了调用 openDialog 方法:

          my.chartDisplay.openDialog(my.team);
        });
    

工作原理…

chartLauncher 模块(chartLauncher.js)用于显示团队的预定任务。以下行展示了该模块如何使用 RGraph 创建包含结果的甘特图。首先,将 openDialog 方法添加到 exports 对象中:

exports.openDialog = function(team){

此食谱本部分的下一步是创建一个窗口以向用户显示:

  var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'Sample Chart',barColor:'#000'
  });

在创建 Ti.UI.Window 之后,附加一个 Ti.UI.WebView 对象。RGraph 图表信息包含在 Ti.UI.WebView 显示的 index.html 文件中:

  var webView = Ti.UI.createWebView({
    height:Ti.UI.FILL, width:Ti.UI.FILL, url:'web/index.html'
  });
  win.add(webView)

Ti.UI.Window 打开事件中,将团队的预定信息传递到包含 RGraph 方法的 index.html 文件中:

  win.addEventListener('open',function(e){

在团队对象上调用 createSchedulescreateComments 方法。这些方法格式化预定和评论信息,以便 RGraph 可以显示详细信息。

    var schedules = JSON.stringify(team.createSchedule());
    var comments = JSON.stringify(team.createComments());

Ti.UI.Window 窗口打开后,向 Ti.UI.WebView 添加一个加载事件监听器。这允许在注入信息之前,Ti.UI.WebView 的内容完全加载。

    webView.addEventListener('load',function(x){

使用 evalJS 方法将格式化的团队信息传递到当前在 Ti.UI.WebView 中显示的 addGraph 方法。您可以通过打开此食谱支持的 index.html 文件来查看 addGraph 方法的具体内容。

      webView.evalJS('addGraph(' + schedules + ',' + comments  + ')');
    });
    win.open({modal:true});
  };

参见

  • 此食谱使用强大的 RGraph HTML5/JavaScript canvas 库来创建显示的甘特图。我鼓励您访问他们的网站 rgraph.net,了解更多关于 RGraph 和提供的不同图表选项。

使用 Google 仪表显示信息

仪表是显示速度、状态或简单测量的强大方式。此食谱演示了如何使用 Google 仪表图表控件来显示达成月销售目标的进度。以下截图显示了此完成的食谱在 iOS 和 Android 上的运行情况:

使用 Google 仪表显示信息

提示

在 Android 上运行此食谱时,请注意需要 Android 4.0 或更高版本。

准备工作

此菜谱使用 Google Charts 来显示仪表盘。当将此菜谱添加到您的应用程序时,您需要设计网络依赖项,因为 Google Charts 需要连接来渲染图表信息。这些组件可以从本书提供的源代码中下载,或者通过此菜谱末尾的 另请参阅 部分的链接单独下载。设置此菜谱的第一步是将以下截图中的 web 文件夹复制到您的项目文件夹的 Resources 文件夹中。在复制 web 文件夹后,您需要将 persist.js 文件复制到您的 Titanium 项目的 Resources 文件夹中,如下截图所示:

准备中

如何操作…

在上一节中讨论的将 web 文件夹和 persist.js CommonJS 模块添加到应用程序后,您需要创建您的应用程序命名空间,并使用 require 将模块导入到您的代码中,如下代码片段所示:

//Create our application namespace
var my = {
  persist : require('persist'),
  session:{},
  isAndroid : Ti.Platform.osname === 'android'
};

加载已保存的销售信息

在创建我们的命名空间后,第一步是加载我们保存的销售信息。如果没有保存之前的数据,将提供一个默认值 10。

my.session.sales = my.persist.load();

为菜谱创建 UI

现在我们创建 UI 来显示我们的销售数据。

  1. 首先,创建一个 Ti.UI.Window 窗口来承载所有我们的 UI 元素:

    var win = Ti.UI.createWindow({
        backgroundColor: '#fff', title: 'Sales Gauge', barColor:'#000', layout:'vertical',fullscreen:false
    });
    
  2. 接下来,创建一个 Ti.UI.WebView 来显示 Google 仪表盘。此 Ti.UI.WebView 用于显示一个本地 html 页面,该页面承载 Google Chart 控件。以下代码片段演示了如何从项目的 web 文件夹中加载本地 html 页面(index.html):

    var webView = Ti.UI.createWebView({
      top:10, height:200, width:200, url:'web/index.html'
    });
    win.add(webView)
    
  3. 最后,将一个 Ti.UI.Slider 添加到菜谱的 Ti.UI.Window 中。用户可以使用这个滑块来调整他们的月度销售。

    var slider = Ti.UI.createSlider({
        min:0, max:100, value:my.session.sales, left:10, right:10, height:'30dp', top:10
    });
    win.add(slider);
    

调整销售

当用户调整 Ti.UI.Slider 时,以下方法被用来使用新的销售数字更新 Google 仪表盘。updateSales 方法使用新的销售数字更新 Ti.UI.WebView 的内容:

function updateSales(sales){

传入的 sales 值被放置到会话的 my.session.sales 属性中,以供后续使用:

  my.session.sales = sales;

Ti.UI.WebView 上调用 evalJS 方法,以将新的销售信息提供给示例 index.html 文件中包含的 updateGauge 方法。此方法用于更新 Google 仪表盘。有关更多详细信息,请参阅此菜谱的 index.html 文件内容。

  webView.evalJS('updateGauge(' + my.session.sales + ')');

在更新 Ti.UI.WebView 后,新的销售值被保存到 Ti.App.Properties 中以供后续使用:

  my.persist.save(my.session.sales);
};

菜谱的 Ti.UI.Slider 方法上的更改事件在用户移动滑块时调整 Google 仪表盘:

  slider.addEventListener('change',function(e){

每当触发更改事件时,新的 Ti.UI.Slider 值会被提供给 updateSales 方法,以便在菜谱的 Ti.Ui.WebView 中反映出来。

  updateSales(Math.round(e.value));
});

重新加载已保存的销售信息

每次加载 Ti.Ui.Window 时,以下代码用于显示保存的销售值并初始化 Google 仪表。下一个片段演示了如何在打开 Ti.UI.Window 时重新加载销售信息:

win.addEventListener('open',function(e){

首先,执行网络连接。由于这个食谱使用 Google Charts,显示这些信息需要网络连接:

  if(!Ti.Network.online){
    alert('Network connection required.');
    return;
  }

最后,向 Ti.UI.WebView 添加一个加载事件监听器。一旦加载完成,updateSales 函数被调用以初始化任何之前保存的销售信息:

  webView.addEventListener('load',function(x){
    assist.updateSales(my.session.sales); 
  });

参见

  • 这个食谱使用 Google Charts 来提供显示的仪表。要了解更多关于 Google 的图表工具,请访问他们的开发者网站,google-developers.appspot.com/chart/.

第六章:使用位置服务

在本章中,我们将涵盖:

  • 使用 basicGeo 进行原生地理位置

  • 使用 Ti.GeoProviders 框架进行地理位置

  • 多租户地理位置

  • 计算地址之间的距离

  • 背景地理位置管理

简介

在移动设备普及之前,往往很难确定用户的位置,这使得提供基于位置的服务变得困难。现在,几乎每个应用开发者都可以直接从用户的设备中获取实时地理位置信息。

由于当今员工的全局移动性,为您的企业提供具有位置感知的应用程序与消费市场一样重要,甚至更为重要。地理位置在包括车队管理、货物运输跟踪、销售路线规划或为移动员工提供实时相关资源信息在内的多个领域的企业组织中发挥着重要作用。

通过一系列位置服务示例,本章概述了您可以在您的企业钛金应用中利用的各种不同方法。

使用 basicGeo 进行原生地理位置

安卓和 iOS 都提供了强大的地理位置平台 API,用于查找给定地址的地理纬度和经度。您可以使用basicGeo模块在您的钛金模块中访问这些平台 API。

本食谱讨论了如何在您的钛金应用中利用此模块执行正向和反向地理位置操作。以下截图展示了此食谱在 iPhone 和安卓设备上的运行情况:

使用 basicGeo 进行原生地理位置

准备工作

本食谱使用benCode.basicGeo原生模块。此模块可以从本书提供的源代码中下载,或者通过本食谱末尾的“另请参阅”部分提供的链接单独下载。将这些模块安装到您的项目中很简单。只需将modules文件夹复制到您的项目中,如以下截图所示:

准备工作

在复制modules文件夹后,您需要在 Titanium Studio 中单击您的tiapp.xml文件,并添加对bencoding.basicgeo模块的引用,如下截图所示:

准备工作

如何操作...

一旦您将bencoding.basicgeo模块添加到您的项目中,接下来您需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到您的代码中,以下代码片段展示了如何进行:

//Create our application namespace
var my = {
  basicGeo : require('bencoding.basicgeo'),
  isAndroid : Ti.Platform.osname === 'android'
};

添加可用性助手

随着设备数量的不断增加,每个设备都有不同的功能,根据传感器的能力提供渐进式增强是一个常见需求。basicGeo模块中的可用性功能提供了一系列属性,可以用来确定设备的功能。以下代码片段展示了如何创建Availability代理,以便在后面的食谱中使用:

my.available = my.basicGeo.createAvailability();

添加位置服务目的

在 iOS 上,Apple 要求应用程序访问 GPS 有一个目的或原因。在首次请求时,此消息将用于向用户展示的消息中,以获得使用其设备位置服务的批准。以下代码片段演示了如何将此目的添加到 basicGeo 模块。

my.basicGeo.purpose = 'Demo of basicGeo';

构建菜谱 UI

下面的代码片段描述了如何创建本菜谱早期截图所示的 UI:

  1. 第一步是创建一个 Ti.UI.Window,所有视觉元素都将附加到该窗口。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'benCoding Geo Recipe', 
      barColor:'#000',fullscreen:false
    });
    
  2. 接下来,将一个 Ti.UI.TextField 添加到菜谱的 Ti.UI.Window 中。这个 Ti.UI.TextField 的内容将在稍后本菜谱中详细描述的前向地理定位查找过程中使用。

    var txtAddress = Ti.UI.createTextField({
      hintText:'enter address', height:40, 
      left:5, right:60, top:55
    });
    win.add(txtAddress);
    
  3. 本菜谱的下一步是向菜谱的 Ti.UI.Window 添加一个 Ti.Map.View。这个 UI 组件将用于显示在 txtAddress Ti.UI.TextField 中输入的地址。

    var mapView = Ti.Map.createView({
      top:160, bottom:0, left:5, right:5, 
      userLocation:false
    });
    win.add(mapView);
    

处理地点对象

在执行地理定位查找时,basicGeo 模块返回一个与提供的地址匹配的地点集合。placeHelpers 对象提供了方便的函数来处理从 basicGeo 模块返回的结果。

var placeHelpers = {
  1. address 函数提供了一个从 basicGeo 地点对象中格式化的地址。这在本菜谱的“查找当前位置”功能中使用。

      address :function(place){
        if(my.isAndroid){
    
  2. Android 反向地理定位 API 已经提供了一个格式化的 address 字段。

          return place.address;
        }else{
    
  3. 在 iOS 上,地址信息以行数组的形式提供。以下方法将这些地址行转换为格式化字符串:

          var lines = place.addressDictionary
          .FormattedAddressLines;
          var iLength = lines.length, address = '';
          for (iLoop=0;iLoop < iLength;iLoop++){
            if(address.length>0){
              address += ' ' + lines[iLoop];
            }else{
              address = lines[iLoop];
            }
          }
          return address;
        }
      },
    
  4. addToMap 方法将 basicGeo 反向地理定位提供的地点信息添加到本菜谱中之前创建的 Ti.Map.View

      addToMap: function(place){
        var lat = place.latitude, 
        lng = place.longitude, 
        title = placeHelpers.address(place);
        var pin = Ti.Map.createAnnotation({
          latitude:lat,longitude:lng,
          title:title
        });
        mapView.addAnnotation(pin);
    
  5. 使用 basicGeo 模块的纬度和经度信息创建一个区域。然后调用 setLocation 方法将 Ti.Map.View 缩放到图钉的坐标。

        var region = {latitude:lat,
         longitude:lng,animate:true,
         latitudeDelta:0.04, longitudeDelta:0.04};
         mapView.setLocation(region);
      }
    };
    

查找当前位置

执行以下步骤以查找当前位置:

  1. findButton 展示了如何使用设备的当前坐标执行反向地理定位查找。然后使用这些坐标找到用户的当前地址。

    var findButton = Ti.UI.createButton({
      right:5, top:55, height:40, width:50, title:'Find'
    });
    win.add(findButton);
    
  2. findButton 的点击事件中,在 basicGeo 模块的 currentGeoLocation 代理上调用 getCurrentPlace 方法。

    findButton.addEventListener('click',function(e){
    
  3. 创建了 resultsCallback 方法来处理 getCurrentPlace 方法返回的结果。getCurrentPlace 方法的结果提供给 e 参数。

      function resultsCallback(e){
    
  4. e.success 属性提供了一个标志,用于确定地理定位操作是否遇到错误。

        if(!e.success){
          alert('Sorry we encountered an error.');
          return;
        }
    
  5. e.placeCount 属性提供了返回的地点对象的数量。通常,这取决于准确性,是一个介于 012 之间的数字。如果没有返回地点,则向用户提醒地址未找到。

        if(e.placeCount === 0){
          alert('Unable to find your address.');
          return;
        }
    
  6. 将地点集合中的第一个地点提供给placeHelpers.address方法。此方法提供了一个格式化的地址字符串,然后将其呈现给用户在Ti.UI.TextField txtAddress中。

        txtAddress.value = 
        placeHelpers.address(e.places[0]);
      };
    
  7. 创建了一个新的CurrentGeoLocation代理实例。此代理包含使用设备当前坐标执行地理定位操作的方法。

      var currentGeo = my.basicGeo.createCurrentGeolocation();
    
  8. 如果菜谱在 Android 设备上运行,可以调用setCache方法。这使basicGeo模块能够使用设备缓存的最后最佳位置。这提供了更快的查找速度,但可能导致位置信息不够准确。

      if(my.isAndroid){
        currentGeo.setCache(true);
      }
    
  9. 返回设备当前地址的最终步骤是调用getCurrentPlace方法。此方法使用设备的坐标执行反向地理定位查找,并将地点集合提供给provide回调方法。以下代码片段演示了如何使用resultsCallback作为回调参数调用此方法。

      currentGeo.getCurrentPlace(resultsCallback);
    });
    

前向位置查找

  1. searchTextAddressButton Ti.UI.Button使用原生设备 API 执行前向地理定位查找。

    var addressOnMapButton = Ti.UI.createButton({
      right:5, left:5, height:40, 
      title:'Show address on Map',top:110
    });
    win.add(searchTextAddressButton);
    
  2. 当点击searchTextAddressButton Ti.UI.Button时,会使用在txtAddress Ti.UI.TextField中输入的地址执行前向地理定位查找。

    searchTextAddressButton.addEventListener('click',function(e){
    
  3. 前向地理定位查找的第一步是验证用户是否在txtAddress Ti.UI.TextField中输入了地址。

      if(txtAddress.value.length==0){
        alert('Please enter an address to display');
      }
    
  4. 创建forwardGeoCallback方法来处理forwardGeocoder方法返回的结果。forwardGeocoder方法的结果提供给e参数。

      function forwardGeoCallback(e){
        if(!e.success){
          alert('Sorry we encountered an error.');
          return;
        }
        if(e.placeCount === 0){
          alert('Unable to find address entered.');
          return;
        }
    
  5. addToMap方法正在使用地点集合中的第一个地点创建一个调用。此方法将在Ti.Map.View上创建一个带有地点对象详细信息的标记。

        placeHelpers.addToMap(e.places[0]);
      };
    
  6. 执行前向地理定位查找的下一步是调用forwardGeocoder方法并提供一个回调方法,如下所示:

      var geoCoder = my.basicGeo.createGeocoder();
    
  7. 在示例中,将txtAddress.valueforwardGeoCallback提供给forwardGeocoder方法。前向地理定位查找的结果将如在此菜谱中之前所述提供给forwardGeoCallback函数。

      geoCoder.forwardGeocoder(txtAddress.value,
      forwardGeoCallback);
    });
    

设备能力检查

按照以下步骤执行设备能力检查:

  1. 此菜谱要求设备支持反向和前向地理定位操作。在加载主菜谱的Ti.UI.Window时,会使用在此菜谱中之前创建的Availability对象来提醒用户其设备是否支持运行此菜谱。

    win.addEventListener('open',function(e){
    
  2. 检查reverseGeoSupported属性以确定运行菜谱的设备是否可以支持运行该菜谱。

      if(!my.available.reverseGeoSupported){
    
  3. 如果设备不支持反向地理定位,会提醒用户可能存在的问题。

        if(my.isAndroid){
          alert("Configuration isn't supported.");
        }else{
          alert('iOS 5 or greater is required');
        }
      }	
    });	
    win.open({modal:true});
    

    小贴士

    由于 Android 模拟器缺陷,此菜谱在模拟器中运行时需要 Android 4.0 或更高版本。

参见

使用 Ti.GeoProviders 框架进行地理定位

Ti.GeoProviders 框架提供了一种多提供程序方法来进行反向地理定位。通过多种提供商,Ti.GeoProviders 框架提供了一个通用的 API 来处理 GeoNames.org、Google 和 basicGeo 的地理定位操作。

以下食谱演示了如何使用 Ti.GeoProviders 框架及其相关提供商。以下截图展示了此食谱在 iPhone 和 Android 设备上的运行情况:

使用 Ti.GeoProviders 框架进行地理定位

准备就绪

这个食谱同时使用了 CommonJS 和本地模块。这些可以从本书提供的源代码中下载,或者通过此食谱末尾的 也见 部分提供的链接单独下载。只需将 Ti.GeoProviders 文件夹复制到你的项目 Resources 文件夹中,然后将 modules 文件夹复制到你的项目中,如图所示。最后,将 provider_picker.js 文件复制到 Titanium 项目的 Resources 文件夹中,如图所示:

准备就绪

在复制此处提到的文件和文件夹后,你需要在 Titanium Studio 中点击你的 tiapp.xml 文件,并添加对 bencoding.basicgeo 模块的引用,如图所示:

准备就绪

如何操作...

一旦你将本地和 CommonJS 模块添加到你的项目中,你需要在 app.js 文件中创建你的应用程序命名空间,并使用 require 将模块导入到你的代码中,如下代码片段所示:

//Create our application namespace
var my = {
  picker : require('provider_picker'),
  currentProvider:null,
  providers:[
    require('./GeoProviders/geonames_geoprovider'),
    require('./GeoProviders/bencoding_geoprovider'),
    require('./GeoProviders/google_geoprovider')
  ]
};

添加你的 API 密钥

许多地理信息提供商需要 API 密钥。Ti.GeoProvider 包含 addKey 方法,允许你在进行服务调用之前关联你的 API 密钥。以下代码片段演示了如何将 API 密钥 demo 添加到你的服务调用中。

my.currentProvider.addKey('demo');

添加你的目的

要在 iOS 中使用位置服务,需要提供应用访问 GPS 的目的或原因。在第一次请求时,此消息将显示给用户。以下代码演示了如何使用 addPurpose 方法将此目的添加到 Ti.GeoProviders

my.currentProvider.addPurpose('Demo of Geo Provider');

注意

Android 不需要提供目的;在这种情况下,定义的目的在访问 GPS 时不会被使用。

构建食谱 UI

以下代码片段描述了如何创建此食谱早期截图所示的 UI:

  1. 第一步是创建一个 Ti.UI.Window,所有视觉元素都将附加到这个窗口上。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'Ti.GeoProviders', 
      barColor:'#000',fullscreen:false
    });
    
  2. 接下来,将一个 Ti.Map.View 添加到 Ti.UI.Window 中。这用于绘制 GeoProvider 提供的位置信息。

    var mapView = Ti.Map.createView({
      top:140, bottom:0, width:Ti.UI.FILL,
      userLocation:false	
    });
    win.add(mapView);
    
  3. 然后在 Ti.UI.Window 中添加了一个 picker 控件。此控件包含一个提供者列表和一个回调方法,用于在它们之间切换。当用户更新所选的 picker 时,会调用 lookup.updateProvider 方法来切换活动的 Ti.GeoProvider。有关更多详细信息,请参阅本食谱中的 查找函数 部分。

    var picker = my.picker.createPicker({
      top:30, height:40},lookup.updateProvider
    });
    win.add(picker);
    
  4. findButton 按钮是最后添加到食谱的 Ti.UI.Window 中的 UI 组件。此 Ti.UI.Button 用于运行食谱的反向地理定位查找。

    var findButton = Ti.UI.createButton({
      title:'Find Current Location', 
      left:10, right:10,top:30,height:40
    });
    win.add(findButton);
    

运行反向地理定位

要运行反向地理定位,请执行以下步骤:

  1. 当用户按下 findButton 时,会触发点击事件并运行以下代码片段:

    findButton.addEventListener('click',function(e){
    
  2. 本食谱本部分的第一个步骤是检查网络连接是否可用。需要网络连接来联系 Ti.GeoProvider 网络服务以执行反向地理定位查找。

      if(!Ti.Network.online){
    
  3. 如果没有网络连接可用,用户会收到此要求的提醒,并且查找过程会停止。

        alert("You must be online to run this recipe");
        return;
      }
    
  4. 本食谱本部分的最后一步是调用 Ti.GeoProvidersgetCurrentAddress 方法。该方法将使用 Ti.Geolocation API 获取设备的坐标,然后使用 Ti.GeoProviders 的特定逻辑将地址对象返回给提供的 onSuccess 回调方法。如果在地理定位过程中发生错误,将调用 onError 回调方法并提供错误详情。以下代码片段演示了如何调用 getCurrentAddress 方法:

      my.currentProvider.getCurrentAddress(
      lookup.onSuccess,lookup.onError);
    });
    

查找函数

现在执行以下步骤:

  1. 在本食谱中,使用 lookup 对象来显示 GeoProvider 返回的结果。

    var lookup = {
    
  2. 使用前面讨论的 picker 控件,用户可以更改食谱的 Ti.GeoProvider。当提供者更改时,会使用要加载的新提供者详细信息调用 updateProvider 方法。

      updateProvider : function(providerKey){
    
  3. 根据提供的 providerKeyupdateProvider 方法将切换 my.currentProvider 对象引用。作为此方法的一部分,还将处理特定于提供者的详细信息,例如 API 密钥详情。GeoNames 提供者代码片段演示了在本食谱中如何执行提供者切换。

        my.currentProvider = my.providers[providerKey];
    
        if(my.currentProvider.providerName == 'geonames'){
          my.currentProvider.provider.addKey('demo');
        }
    
  4. addPurpose 等跨提供者方法在 updateProvider 方法的末尾执行,因为它们被所有 Ti.GeoProviders 利用。

        my.currentProvider.addPurpose('Geo Demo');
      },
    
  5. 使用 GeoNames Ti.GeoProvider 提供的纬度、经度和地址信息,使用 addToMap 方法创建一个地图图钉。

      addToMap: function(lat,lng,title){
        var pin = Ti.Map.createAnnotation({
          latitude:lat,longitude:lng,
          title:title
        });
        mapView.addAnnotation(pin);
    
  6. 使用 Ti.GeoProvider 的纬度和经度信息创建一个区域。然后调用 setLocation 方法将 Ti.Map.View 缩放到图钉的坐标。

        var region = {latitude:lat,
        longitude:lng,animate:true,
        latitudeDelta:0.04,
        longitudeDelta:0.04};
        mapView.setLocation(region);
      },
    
  7. 使用 onSuccess 方法来处理来自 Ti.GeoProviders 的成功返回。此方法用于在 Ti.GeoProviders 成功返回后协调所有用户交互。

      onSuccess : function(e){
        if(!e.found){
          alert("Unable to find your location");
          return;
        }
    
  8. 使用generateAddress方法为title变量创建一个值。然后,title变量用于创建地图针。由于Ti.GeoProviders可以包含不同的格式,因此使用generateAddress函数创建用于显示的格式化地址。

        var title = my.currentProvider.generateAddress(e);
        lookup.addToMap(e.latitude,e.longitude,title);
      },
    
  9. 如果在反向地理编码过程中出现问题时,Ti.GeoProviders会使用onError方法返回错误信息。所有错误详情都可以在e参数中访问。

      onError: function(e){
        alert("Error Details:" JSON.stringify(e));
      }
    };
    

参见

要了解更多关于此配方中使用的basicGeo Titanium 模块的信息,您可以查看以下链接:

多租户地理定位

如本章前面所述,Ti.GeoProviders框架提供了一种多提供者方法来处理反向地理编码。多租户组件包括Ti.GeoProviders框架在提供者无法找到合适位置时进行故障转移的能力。这种多租户方法有助于确保您的地理定位功能适用于您的全球移动员工。

以下配方演示了如何使用多租户Ti.GeoProviders框架通过故障转移方法执行反向位置查找。以下截图展示了此配方在 iPhone 和 Android 设备上的运行情况:

多租户地理定位

准备就绪

此配方同时使用 CommonJS 和本地模块。这些模块可以从本书提供的源代码中下载,或者通过此配方末尾的“参见”部分提供的链接单独下载。只需将GeoProviders文件夹复制到您的项目中的Resources文件夹,然后将modules文件夹复制到您的项目中,如下面的截图所示:

准备就绪

复制提到的文件夹后,您需要在 Titanium Studio 中点击您的tiapp.xml文件,并添加对bencoding.basicgeo模块的引用,如下面的截图所示:

准备就绪

如何操作...

一旦您已将原生和 CommonJS 模块添加到您的项目中,接下来您需要在 app.js 文件中创建您的应用程序命名空间,并使用 require 将模块导入到您的代码中,如下代码片段所示:

//Create our application namespace
var my = {
  multiProvider : 
  require('./GeoProviders/reverse_multi_geoprovider')
};

添加提供者

以下代码片段演示了如何通过调用 addProvider 方法将不同的 GeoProviders 添加到 multiProvider 模块中。建议首先添加您认为最能满足您需求的 GeoProviders,因为 multiProvider 将按添加的顺序执行它们。

my.multiProvider.addProvider({
  key : 'demo',
  providerString:'GeoProviders/geonames_geoprovider'
});

my.multiProvider.addProvider({
  providerString:'GeoProviders/bencoding_geoprovider'
});
my.multiProvider.addProvider({
  providerString:'GeoProviders/google_geoprovider'
});

添加目的

在 iOS 上使用位置服务需要提供目的或原因,以便应用程序访问 GPS。在第一次请求时,此消息将展示给用户。以下代码演示了如何使用 addPurpose 方法将此目的添加到多租户提供者:

my.multiProvider.addPurpose('Demo of Geo Provider');

注意

Android 不需要提供目的;在这种情况下,当访问 GPS 时,定义的目的不会被使用。

构建食谱 UI

以下代码片段描述了如何创建在此食谱早期屏幕截图中所显示的 UI。第一步是创建 Ti.UI.Window,所有视觉元素都将附加到该窗口。

var win = Ti.UI.createWindow({
  backgroundColor: '#fff', title: 'Multi-Tenant Geo', 
  barColor:'#000',fullscreen:false
});

接下来,将 Ti.Map.View 添加到 Ti.UI.Window 中;这将用于显示带有设备当前位置和地址详情的地图标记。

var mapView = Ti.Map.createView({
  top:120, bottom:0,width:Ti.UI.FILL,
  userLocation:false	
});
win.add(mapView);

查找辅助方法

现在执行以下步骤:

  1. lookup 对象旨在帮助格式化多租户反向地理编码组件的结果,并以图形方式向食谱用户展示地址信息。

    var lookup = {
    
  2. addToMap 方法创建一个地图标记并添加信息到 Ti.Map.View

      addToMap: function(lat,lng,title){
        var pin = Ti.Map.createAnnotation({
        latitude:lat,longitude:lng,
        title:title	
      });
      mapView.addAnnotation(pin);
    
  3. 使用地图标记坐标创建一个区域,然后调用 Ti.Map.ViewsetLocation 函数。这将使地图缩放到最近添加的标记的坐标。

      var region = {latitude:lat,longitude:lng,
        latitudeDelta:0.04,   
        longitudeDelta:0.04};
        mapView.setLocation(region);
      },
    
  4. 当调用 getCurrentAddress 方法时,提供 onSuccess 方法作为成功回调。getCurrentAddress 方法的结果提供给了 e 参数。

      onSuccess : function(e){
        if(!e.found){
          alert("Unable to find your location");
          return;
        }
    
  5. 调用 getProvider 方法以创建一个引用,用于返回位置结果的提供者。这允许使用特定于提供者的 generateAddress 方法。

        var provider = my.multiProvider.getProvider(
        e.provider.name);
        var title = provider.generateAddress(e);
        lookup.addToMap(e.latitude,e.longitude,title);
      },
    
  6. 当调用 getCurrentAddress 方法时,提供 onError 方法作为错误回调。错误详情提供给了 e 参数。

      onError: function(e){
        alert("Error finding your location");
      }
    };
    

执行多租户反向地理编码查找

现在执行以下步骤:

  1. 本食谱的最后一部分是使用 multiProvider 模块的 getCurrentAddress 方法执行多租户查找。

    var findButton = Ti.UI.createButton({
      title:'Find Current Location', 
      left:10, right:10,top:70,height:40
    });
    win.add(findButton);
    
  2. findButton 的点击事件上执行 multiProvider 查找。

    findButton.addEventListener('click',function(e){
    
  3. 由于反向地理编码需要网络连接,因此反向地理编码过程的第一步是验证网络连接。

      if(!Ti.Network.online){
        alert("You must be online to run this recipe");
        return;
      }	
    
  4. 接着调用getCurrentAddress方法,并提供了成功和错误回调方法。以下代码片段展示了如何使用前面在本食谱中讨论过的lookup.onSuccesslookup.OnError回调方法调用此方法。

      my.multiProvider.getCurrentAddress(
      lookup.onSuccess,lookup.onError);
    });	
    

计算地址之间的距离

在企业应用程序中,使用地理定位服务是很常见的。最常见的地理定位需求之一是计算两点之间的距离。这在规划路线、确定里程、预测配送时间表等方面非常有用。

以下食谱演示了如何计算两个地址之间的距离。这种距离测量使用的是直接距离,而不是像步行或驾驶那样使用的路线计算。以下截图展示了此食谱在 iPhone 和 Android 设备上的运行情况。

计算地址之间的距离

准备工作

本食谱同时使用 CommonJS 和本地模块。这些可以从本书提供的源代码中下载,或者通过本食谱末尾的“也见”部分提供的链接单独下载。只需将forwardGeo.js文件复制到项目的Resources文件夹中,然后将modules文件夹复制到项目中,如图所示:

准备工作

在复制此处提到的文件和文件夹后,您需要在 Titanium Studio 中点击您的tiapp.xml文件,并添加对bencoding.basicgeo模块的引用,如图所示:

准备工作

如何操作...

在您的项目中添加了本地和 CommonJS 模块之后,接下来需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  forward : require('forwardGeo')
};

添加地址信息

在应用程序命名空间中添加了以下startAddressendAddress对象。这些对象将用于创建本食谱的地址信息和坐标状态。

my.startAddress  = {
  needRefresh:true,lat:40.748433, lng:-73.985656,
  address:'350 5th Ave  New York, NY 10118'
};

my.endAddress = {
  needRefresh:false, lat:40.75773, lng:-73.985708,
  address:'1560 Broadway, New York, NY 10036'
};

构建食谱 UI

以下代码片段描述了如何创建本食谱早期截图所示的 UI:

  1. 第一步是创建Ti.UI.Window,所有视觉元素都将附加到该窗口。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'Geo Distance Recipe', 
      barColor:'#000',fullscreen:false
    });
    
  2. 接下来创建txtStartAddress Ti.UI.TextField,允许用户输入起始地址。

    var txtStartAddress = Ti.UI.createTextField({
      hintText:'enter starting address', 
      value: my.startAddress.address,
      height:40, left:5, right:5, top:55,
      borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED
    });
    win.add(txtStartAddress);
    
  3. 接下来创建txtEndAddress Ti.UI.TextField,允许用户输入目的地地址。

    var txtEndAddress = Ti.UI.createTextField({
      hintText:'enter destination address', 
      value: my.endAddress.address,
      height:40, left:5, right:5, top:125,
      borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED
    });
    win.add(txtEndAddress);
    
  4. 在本食谱的后续步骤中,将findButton Ti.UI.Button添加到Ti.UI.Window。此按钮将用于执行距离计算。

    var findButton = Ti.UI.createButton({
      title:'Calculate Distance between', height:40,
      left:5, right:5, top:180
    });
    win.add(findButton);
    

距离和地址方法

本食谱使用geo对象执行距离和地址查找操作。

var geo ={
  1. distanceInUnits使用哈夫曼公式,并计算两组坐标之间直接距离的千米或米。

    distanceInUnits: function(lat1, lng1, lat2, lng2){
      var rOfEarth = 6371; 
      var dLat = (lat2-lat1)*Math.PI/180;  
      var dLon = (lng2-lng1)*Math.PI/180;   
      var a = Math.sin(dLat/2) * Math.sin(dLat/2) +  
      Math.cos(lat1*Math.PI/180) *
      Math.cos(lat2*Math.PI/180) *   
      Math.sin(dLon/2) * Math.sin(dLon/2);   
      var c = 2 * Math.asin(Math.sqrt(a));   
      var distance = rOfEarth * c;
    
  2. 如果距离小于 1 公里,则返回的单位将转换为米。

      return {
        distance: ((distance < 1) ? 
        (distance * 1000) : distance),
        unit: ((distance < 1) ? 'm' : 'km')
      };
    },
    
  3. 使用 findLocations 方法来获取提供的地址的坐标。

    findLocations : function(callback){
    
  4. onFinish 函数是提供给 forwardGeo 函数的回调方法。e 参数提供了起始和结束地址的坐标。

      function onFinish(e){
        if(!e.success){
          alert(e.message);
          return;
        }
    

    forwardGeo e.starte.end 结果分配给 my.startAddressmy.endAddress 属性。然后执行回调方法,以便执行距离计算。

        my.startAddress = e.start;
        my.endAddress = e.end;
        callback();
      };
    
  5. 调用 forwardGeo 方法来获取 my.startAddressmy.endAddress 对象的坐标。如前所述,地理位置结果作为以下代码片段所示提供给了 onFinish 回调方法:

        my.forward.forwardGeo(my.startAddress,
        my.endAddress,onFinish);
      }	
    };
    

查找两个地址之间的距离

当用户按下 findButton 并触发点击事件时,食谱将执行两个输入地址之间的距离计算。

findButton.addEventListener('click',function(e){
  1. 此过程的第一个步骤是确定是否支持正向地理位置。距离计算需要每个地址的坐标。执行正向地理位置查找以获取此信息。

      if(!my.forward.isSupported()){
        alert('Forward Geocoding is not supported');
        return;
      }	
    
  2. 使用 findDistance 方法来调用距离计算方法并格式化提供的结果。

      function findDistance(){
    
  3. 本食谱本部分的第一个步骤是使用每个地址的经纬度信息调用 distanceInUnits 方法。

        var result = geo.distanceInUnits(
        my.startAddress.lat,my.startAddress.lng,
        my.endAddress.lat,my.endAddress.lng);
    
  4. 接下来需要格式化距离计算结果。如果结果是以公里为单位,则 distance 变量将被四舍五入到小数点后三位。如果是米,则将显示完整值。

        if(result.unit=='km'){
          result.distance = 	
          result.distance.toFixed(3);
        }
        distanceLabel.text = result.distance + " " + 
        result.unit + " between addresses";
      };
    
  5. 如果任一地址信息对象需要刷新,则调用 geo.findLocation 方法。findDistance 方法作为回调方法提供给 findLocations 函数,以便在获取坐标后执行距离计算。

      if(my.startAddress.needRefresh || 
      my.endAddress.needRefresh){
        geo.findLocations(findDistance);
      }else{
    
  6. 如果地址信息对象不需要刷新,则直接调用 findDistance 方法来执行距离计算。

        findDistance();
      }	
    });
    

后台地理位置管理

后台地理位置是许多企业应用的重要功能。在后台监控设备位置的能力是一个强大的功能,可用于从个人安全到里程追踪的广泛活动。

以下食谱演示了如何使用 Ti.Geo.Background 框架来启用后台地理位置监控。以下截图展示了该食谱在 iPhone 和 Android 设备上运行的情况。

后台地理位置管理

准备就绪

该食谱使用了一系列 CommonJS 模块。这些模块可以从本书提供的源代码中下载,或者通过本食谱末尾的 另请参阅 部分提供的链接单独下载。只需将 bGeo 文件夹复制到项目中的 Resources 文件夹,如图所示:

准备就绪

更新你的 tiapp.xml 文件

此 recipe 需要更新你的项目 tiapp.xml 文件。第一次更新是为 iOS 设备提供背景地理定位支持。以下突出显示的 UIBackgroundModes 部分说明了此 recipe 所需的条目:

<ios>
  <min-ios-ver>5.0</min-ios-ver>
  <plist>
    <dict>
 <key>UIBackgroundModes</key>
 <array>
 <string>location</string>
 </array>
      <key>NSLocationUsageDescription</key>
      <string>Demo Geo App</string>
    </dict>
  </plist>
</ios>

此 recipe 使用 Android 服务作为保持活跃。以下突出显示的部分是 recipe 创建内部服务所必需的:

<android xmlns:android=
"http://schemas.android.com/apk/res/android">
 <services>
 <service url="bGeo/Ti.Geo.Timer.js" type="interval"/>
 </services>
</android>

如何操作...

The Ti.Geo.Background CommonJS 模块同时利用了位置管理器距离过滤和保持活跃的地理定时器。这确保了当设备移动特定距离或经过指定时间段时,都会记录位置。Ti.Geo.Background 模块管理所有地理定位操作,并维护一个距离过滤器,以便在设备移动超过特定阈值距离时记录坐标。

Ti.Geo.Timer 执行两项活动。首先提供保持 iOS 应用程序活跃所需的保持活跃循环,其次,在预定的时间间隔内,服务记录设备的当前坐标。这确保了即使个人没有移动,也会记录坐标。

以下图表说明了不同 Ti.Geo.Background 组件之间的交互:

如何操作...

命名空间和应用程序设置

一旦你将原生和 CommonJS 模块添加到你的项目中,你需要在 app.js 文件中创建你的应用程序命名空间,并使用 require 将模块导入到你的代码中,如下代码片段所示:

var my = {
  bGeo : require('bGeo/Ti.Geo.Background'),
  isAndroid : Ti.Platform.osname === 'android',
  session:{started:false}
};

背景位置选项

Ti.Geo.Background 模块提供了一系列可选的配置参数,允许你根据需要调整模块。第一个配置参数是 purpose。当 iOS 向用户展示位置服务访问提示时,会使用此参数。

my.bGeo.purpose = "Demo Background Recipe";
  1. distanceFilter 是一个以米为单位的值,表示你希望位置管理器多久触发一次位置变更的警报。以下示例设置为 100,将在用户移动超过 100 米时触发位置变更事件。

    my.bGeo.distanceFilter = 100;
    
  2. trackSignificantLocationChange 设置表示在 iOS 上应使用显著位置变更跟踪。这种地理定位方法通过仅在执行网络更改(如切换蜂窝塔)时触发事件来减少电池影响。

    my.bGeo.trackSignificantLocationChange = true; 
    
  3. minAge 配置是你希望接收位置更新的最小频率阈值,以分钟为单位。在以下示例中,无论个人移动的距离如何,你将不会以每 3 分钟一次的频率接收更新。

    my.bGeo.minAge = 3;
    
  4. maxAge 配置是你希望在没有收到更新时,你希望达到的最大时间阈值(以分钟为单位)。这也是 Ti.Geo.Timer 执行坐标查找时使用的阈值。

    my.bGeo.maxAge = 30;
    

构建 recipe 的 UI

以下代码片段描述了如何创建此 recipe 早期截图所示的 UI:

  1. 第一步是创建 Ti.UI.Window,所有视觉元素都将附加到该窗口上。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'Background Geo Recipe', 
      barColor:'#000',fullscreen:false
    });
    
  2. 下一步是向配方的 Ti.UI.Window 添加 Ti.Map.View。这将用于显示 Ti.Geo.Background 运行期间收集的坐标。

    var mapView = Ti.Map.createView({
      top:80, bottom:0, left:5, right:5, userLocation:false
    });
    win.add(mapView);
    
  3. startStopButton 被添加到 Ti.UI.Window 中。此按钮将用于启动和停止 Ti.Geo.Background 进程。

    var startStopButton = Ti.UI.createButton({
      title:((my.bGeo.active()) ? 'stop' :'start'),
      top:30, height:40,left:5, width:75
    });
    win.add(startStopButton);
    
  4. clearButton 被添加到 Ti.UI.Window 中。此按钮将用于删除 Ti.Geo.Background 进程记录的所有坐标信息。

    var clearButton = Ti.UI.createButton({
      title:'Clear', top:30, height:40,left:85, width:75
    });
    win.add(clearButton);
    
  5. refreshButton 被添加到 Ti.UI.Window 中。此按钮将用于使用 Ti.Geo.Background 进程记录的坐标刷新 Ti.Map.View

    var refreshButton = Ti.UI.createButton({
      title:'Refresh', top:30, height:40,left:165, width:75
    });
    win.add(refreshButton);
    

配方的 assistant 方法

此配方实现了一个 assistant 对象,其中包含用于处理和显示 Ti.Geo.Background 模块收集的坐标的辅助函数。

var assistant = {
  1. addToMapassistant 对象中的第一个方法。此方法为 Ti.Geo.Background 模块收集的每个坐标点添加一个地图标记。

      addToMap : function(e){
        var pin = Ti.Map.createAnnotation({
          latitude:e.latitude,
          longitude:e.longitude	
        });
        mapView.addAnnotation(pin);
        var region = {latitude:e.latitude,
        longitude:e.longitude,
        latitudeDelta:0.04, longitudeDelta:0.04};
        mapView.setLocation(region);
      },
    
  2. assistant 对象中的下一个方法是 locationChangeCallback 方法。此方法作为 Ti.Geo.Background 模块 change 事件的回调方法。变化坐标被提供给方法的 e 参数。然后 locationChangeCallback 方法调用 addToMap 方法以显示新收集的坐标信息。

      locationChangeCallback : function(e){
        assistant.addToMap(e);
      },
    
  3. assistant 对象中的最后一个方法是 locationErrorCallback 方法。此方法作为 Ti.Geo.Background 模块 error 事件的回调方法。错误信息被提供给方法的 e 参数。

      locationErrorCallBack : function(e){
        alert('Error due to ' + e.message);
      }
    };
    

地理位置事件

Ti.Geo.Background 模块有多个事件。在此配方中使用的事件在本节中详细说明。change 事件是本配方中使用的首选方法。每当生成位置变化时,都会触发此事件。以下示例演示了如何订阅此 change 事件,提供回调方法 assistant.locationChangeCallback

my.bGeo.addEventListener('change',
assistant.locationChangeCallback);

在此配方中,error 事件也被用于在模块发生错误时向用户提供警报。以下示例演示了如何订阅 error 事件,提供回调方法 assistant.locationErrorCallback

my.bGeo.addEventListener('error',
assistant.locationErrorCallBack);

背景按钮事件

此配方使用一系列按钮来演示如何调用 Ti.Geo.Background 模块的方法。startStopButton 的点击事件演示了如何启动和停止 Ti.Geo.Background 进程。

startStopButton.addEventListener('click',function(e){
  1. 如果模块已经处于活动状态,则配方将切换状态为 off 并停止模块以记录坐标。

      if(my.bGeo.active(){
        my.bGeo.stop();
      }else{
    
  2. 如果模块处于关闭状态,则配方将切换状态为 on 并启动模块以记录坐标。

        my.bGeo.start();
      }
    
  3. startStopButton 的标题被更新以刷新模块的当前状态。

      startStopButton.title=((my.bGeo.active()) ? 
      'stop' :'start');
    });
    
  4. 使用refreshButtonclick事件来重新加载记录的坐标以在Ti.Map.View中显示。

    refreshButton.addEventListener('click',function(e){
    
  5. 首先移除所有地图标注。

      mapView.removeAllAnnotations();
    
  6. 然后调用readCache方法来返回包含所有记录坐标的数组。

      var results = my.bGeo.readCache();
    
  7. 然后使用assistant.addToMap方法遍历坐标数组,在食谱的Ti.Map.View上创建地图标记。

      for (iLoop=0;iLoop < results.length;iLoop++){
        assistant.addToMap(results[iLoop]);
      }
    });
    
  8. 使用clearButtonclick事件来移除所有记录的坐标信息,并如以下代码片段所示清除Ti.Map.View上的所有标注:

    clearButton.addEventListener('click',function(e){
      my.bGeo.clearCache();
      mapView.removeAllAnnotations();
    });
    

iOS 应用级别事件

iOS 平台不允许在应用处于前台时运行后台服务。以下代码块演示了如何处理这个 iOS 特定场景:

  1. 首先检查确保食谱在 iOS 设备上运行。

    if(!my.isAndroid){
    
  2. 接下来在resumed事件上创建一个应用级别的监听器。这将当应用被置于前台时触发。

      Ti.App.addEventListener('resumed',function(e){
    
  3. 当应用被移入前台时,食谱会检查Ti.Geo.Background模块是否活跃。如果活跃,必须调用模块的paused方法来禁用Ti.App.iOS.BackgroundService,同时保持位置管理器活跃。

        if(my.bGeo.active()){
          my.bGeo.paused();
        }
      });
    
  4. 在 iOS 上管理后台服务的下一步是向paused事件添加一个应用级别的监听器。当应用被置于后台时,将触发paused事件。

      Ti.App.addEventListener('paused',function(e){
    
  5. 以下代码片段演示了如果Ti.Geo.Background模块活跃,如何重启后台服务。

        if(my.bGeo.active()){
          my.bGeo.restart();
        }
      });
    

    小贴士

    如果你想在应用暂停时继续收集坐标,必须在应用暂停时调用重启方法。

  6. 以下代码片段演示了如何在应用关闭时停止Ti.Geo.Background模块。这也会为后台进程提供一个干净的关闭,避免 iOS 在约 10 分钟后终止它们。

      Ti.App.addEventListener('close',function(e){
        my.bGeo.stop();
      });
    }
    

参见

  • 在这个食谱中使用了Ti.Geo.Background模块来提供跨平台的背景位置服务。有关许可、源代码以及了解更多关于这个项目的信息,请访问github.com/benbahrenburg/Ti.Geo.Background

第七章. 线程、队列和消息传递

在本章中,我们将涵盖:

  • 队列多个下载

  • 从一个应用启动另一个应用

  • 跨平台 URL 方案

  • 在 BOOT_COMPLETED 时打开您的 Android 应用

  • 使用 Web Workers 进行 iOS 多线程

简介

大小、复杂性和体积是开发和维护企业应用时常见的问题。Titanium 提供了对几种常见模式的支持,用于分离操作责任模式。

本章讨论了如何在 Titanium 中使用消息传递、队列和多线程功能。然后可以使用这些分步菜谱将这些设计原则融入现有的 Titanium 企业应用中。

队列多个下载

对于企业应用来说,需要从组织的文件服务器下载文档和内容是很常见的。在这种情况下使用队列有助于确保顺序和交付,同时避免与生成多个异步请求相关的常见陷阱。此菜谱使用Ti.Queue CommonJS 模块来创建一个持久、命名的队列以执行文件下载。

为了演示如何实现持久队列,此菜谱将从 Github 下载 5 MB 的样本文件到您的设备。以下截图显示了此菜谱在 iPhone 和 Android 设备上运行时的样子。

队列多个下载

准备中

此菜谱使用Ti.Queue CommonJS 模块。此模块和其他代码资源可以从本书提供的源代码下载,或者通过此菜谱末尾的“另请参阅”部分提供的链接单独下载。将这些安装到您的项目中很简单。只需将Ti.Queue.js文件复制到您的项目中,如下面的截图所示:

准备中

网络连接

此菜谱需要网络连接以从 Github 下载文件。请确保设备或模拟器有适当的网络连接性。

如何操作...

一旦将Ti.Queue模块添加到您的项目中,您需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到您的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  qMod : require('Ti.Queue'),
  jobInfo:{total:0,completed:0}
};

创建菜谱的 UI

以下步骤概述了如何创建此菜谱中使用的 UI:

  1. 首先,创建一个Ti.UI.Window以附加所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'Download Queue', 
      barColor:'#000',layout:'vertical',fullscreen:false
    });
    
  2. 接下来,向Ti.UI.Window添加一个Ti.UI.ProgressBar。这将用于显示队列的状态。

    var progress = Ti.UI.createProgressBar({
      top:30, height:50,min:0,max:3, value:0, left:10, 
      right:10, message:'Tap button to start download'
    });
    win.add(progress);
    
  3. 立即调用我们的Ti.UI.ProgressBarshow方法,以确保它正确地显示在Ti.UI.Window上。

    progress.show();
    
  4. 接下来,向Ti.UI.Window添加一个Ti.UI.Button。此控件将用于触发队列下载逻辑。

    var downloadButton = Ti.UI.createButton({
      title:'Run Download Queue', top:40,
      left:10, right:10, height:50, 
    });
    win.add(downloadButton);
    

创建队列

菜谱的下一步是创建一个命名队列,如下面的代码片段所示。默认情况下,任何命名队列都是持久的,将在会话之间存储所有作业。

//Create a new version of the queue
var queue = new my.qMod("demo");

注意

如果在创建新队列时没有提供名称,则队列不会在应用程序重启之间保存任务。

将任务添加到队列

在创建队列之后,下一步是为队列创建一系列任务以进行管理。以下代码块为队列创建了三个下载任务。

  1. 创建的第一个任务是下载来自 Github 的 ZIP 文件。使用时间戳提供唯一的名称。

    var sample1Name = new Date().getTime();
    queue.enqueue({
      title:'Sample ' + sample1Name,
      url:"https://github.com/benbahrenburg/
      Ti.Queue/blob/master/5MB.zip",
      downloadPath:Ti.Filesystem.applicationDataDirectory + 
      sample1Name + '.zip',
      attempts:0
    });
    
  2. 再次,创建第二个任务以从 Github 下载 ZIP 文件。使用时间戳提供唯一的名称。

    var sample2Name = new Date().getTime();
    queue.enqueue({
      title:'Sample ' + sample2Name,
      url:"https://github.com/benbahrenburg/
      Ti.Queue/blob/master/5MB.zip",
      downloadPath:Ti.Filesystem.applicationDataDirectory + 
      sample2Name + '.zip',
      attempts:0
    });
    
  3. 再次,创建第三个任务以从 Github 下载 ZIP 文件。使用时间戳提供唯一的名称。

    var sample3Name = new Date().getTime();
    queue.enqueue({
      title:'Sample ' + sample3Name,
      url:"https://github.com/benbahrenburg/
      Ti.Queue/blob/master/5MB.zip",
      downloadPath:Ti.Filesystem.applicationDataDirectory + 
      sample3Name + '.zip',
      attempts:0
    });
    

菜谱的辅助函数

assist 对象用于管理下载过程。以下是对 assist 对象如何工作以及如何在您的应用程序中利用它的讨论:

var assist = {
  1. progressSetup 方法用于在初始化新的下载系统时重启进度条。

      progressSetup : function(min,max){
        progress.min = min;
        progress.max = max;
        progress.value = 0;
        progress.message = 'Starting download';
        downloadButton.title = "Downloading... please wait";
      },
    
  2. updateProcess 方法用于更新进度条信息并提醒用户整体下载任务的状态。

      updateProgress : function(value,text){
        progress.value = value;
        progress.message = text;
      },
    
  3. 所有 Ti.Queue 任务完成或生成错误后,调用 whenFinished 方法。此方法用于更新 UI 以提醒用户已处理队列中的任务。

      whenFinish : function(){
        downloadButton.text = "Tap button to start download";
        alert('Finished Download');
        downloadButton.enabled = true;
      },
    
  4. next 方法用于处理 Ti.Queue 中的下一个任务。

      next : function(){
    
  5. next 方法中的此步骤是检查是否有任何任务可用。这是通过在队列上调用 getLength 方法来完成的,以检索队列中当前存储的任务数量。

        if(queue.getLength() == 0){
    
  6. 如果队列中没有剩余的任务,则调用 whenFinish 方法并退出下载过程。

          assist.updateProgress(my.jobInfo.total,
          'Download Completed');
          assist.whenFinish();
          return;
        }
    
  7. 如果队列中有可用的任务,下一步是更新 progressValue 以显示当前处理状态,如下面的代码片段所示。

        var progressValue = 
        (my.jobInfo.total - queue.getLength());
    
  8. 下一步是调用队列上的 peek 方法。这允许我们获取队列中的下一个项目,而不将其从队列本身中弹出。这用于将信息写入 Titanium Studio 控制台进行调试。

        var pkItem = queue.peek();
        Ti.API.info('Peek value: ' + JSON.stringify(pkItem));
    
  9. 下一步是调用队列上的 dequeue 方法。此函数从队列中弹出下一个项目并返回该项目。在下面的代码片段中,调用 dequeue 方法将下一个队列任务提供给 item 变量。

        var item = queue.dequeue();
    
  10. 然后调用 updateProgress 方法以提醒用户下载进度。

        assist.updateProgress(progressValue,'Downloading ' +
        item.title);
    
  11. 接下来调用 download 方法以启动下载过程。为了创建递归函数,将 next 方法作为回调参数传递给 download 方法。

        assist.download(item,assist.next);
    
      },
    
  12. download 方法包含从 Github 下载队列中任务所需的所有逻辑。

      download :function(item,callback){
    
  13. 从 Github 下载文件的第一步是创建 Ti.Network.HTTPClient

        var done = false;
        var xhr = Ti.Network.createHTTPClient();
        xhr.setTimeout(10000);
    
  14. Ti.Network.HTTPClient 收到成功响应时,触发 onload 回调。

        xhr.onload = function(e){
          if (this.readyState == 4 && !done) {
            done=true;
    
  15. 如果响应没有提供 200 状态码,则生成错误。

            if(this.status!==200){
              throw('Invalid http reply status code:' + 
              this.status);
              return;
            }
    
  16. 如果返回了适当的状态代码,则创建一个Ti.Filesytem对象,并将responseData保存到提供的输出路径。

            var saveToFile =
            Ti.Filesystem.getFile(item.downloadPath);
            if(saveToFile.exists()){
              saveToFile.deleteFile();
            }
            saveToFile.write(this.responseData);
            saveToFile = null;
    
  17. responseData被持久化到文件系统后,回调方法被触发。这允许配方递归地遍历队列。

            callback();
          }
        };
    
  18. Ti.Network.HTTPClient收到错误时,触发onerror回调。

        xhr.onerror = function(e){
    
  19. 当收到错误时,通过在assist对象上调用requeue方法,将提供的作业重新排队以进行另一次尝试。

          assist.requeue(item,callback)
        };
    
  20. 通过在Ti.Network.HTTPClient上调用opensend方法,并使用队列项目的url详细信息,开始下载进度,如下代码片段所示:

        xhr.open('GET', item.url);
        xhr.send();
      },
    
  21. 使用requeue方法将项目重新添加到队列中。该配方旨在在将工作视为永久性错误之前提供三次下载尝试。

      requeue :function(item,callback){
        Ti.API.info('requeue is called on download fail.'); 
        Ti.API.info('Allowed to retry 3 times');
    
  22. 该项目的attempt属性被检查以确定工作是否尝试了超过三次。

        if(item.attempts > 3){
    
  23. 如果工作尝试次数超过三次,则项目不会被重新添加到队列中。

          Ti.API.info('Max removed from queue.')
        }else{
    
  24. 如果工作出错次数少于三次,则增加attempts属性,并将项目再次添加到队列中。

          item.attempts++; 
          queue.enqueue(item);
        }
    
  25. 最后,触发callback方法,将下载过程移动到其生命周期的下一个步骤。

        callback();
      }
    };
    

开始下载

当按下downloadButton时,配方开始处理队列中的工作。

downloadButton.addEventListener('click',function(e){
  1. 下载过程的第一步是检查配方是否有网络连接。如果网络不可用,配方将提醒用户退出进程。

      if(!Ti.Network.online){
        alert('This recipe requires a network');
        return;
      }
    
  2. 下载过程的下一步是禁用downloadButton。这避免了用户在作业开始后再次处理按钮。

      downloadButton.enabled = false;
    
  3. 然后,my.jobInfo对象被更新为当前的计数和状态信息。这将用于跟踪整体下载状态。

      my.jobInfo.total = queue.getLength();
      my.jobInfo.completed = 0;
    
  4. 接下来调用processSetup方法,使用正确的minmax值初始化Ti.UI.ProgressBar

      assist.progressSetup(my.jobInfo.completed,
      my.jobInfo.total);
    
  5. 最后,在assist对象上调用next方法。这开始下载过程并创建一个递归循环,直到队列空为止。

      assist.next();
    
    });
    

参见

从一个应用启动另一个应用

大多数移动企业应用都是围绕特定任务设计的,例如时间报告。通过使用 URL 方案或 Android 意图过滤器,您可以在用户的设备上打开并通信不同的应用。例如,您可以为组织的时间报告应用提供一个选项,当员工出差并需要记录额外信息时,打开费用应用。

此配方演示了如何使用原生平台的集成模式启动不同的应用。以下屏幕截图显示了该配方在 iOS 和 Android 设备上运行的情况:

从一个应用启动另一个应用

注意

此菜谱必须在设备上运行,才能完全体验所有功能。需要设备,因为模拟器或仿真器不允许运行来自不同应用商店的应用。

准备工作

此菜谱使用schemeList CommonJS 模块。此模块和其他代码资源可以从本书提供的源代码中下载。在项目中安装这些资源很简单。只需将schemeList.js文件复制到您的项目中,如下面的截图所示:

准备中

网络连接

此菜谱要求在您的设备上安装以下应用:

  • LinkedIn

  • Evernote

  • Google Maps(在 iOS 和 Android 上)

  • 本章后面将讨论的 URL Scheme 示例

如何做到这一点...

在添加schemaList模块后,您需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到您的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  schemes : require('schemeList'),
  isAndroid : Ti.Platform.osname === 'android'
};

iOS 更新 tiapp.xml

添加允许其他 iOS 应用启动您的应用的功能很简单。以下代码片段演示了如何将名为bencoding-linkLauncher的自定义方案添加到此菜谱中。这允许任何 iOS 应用以类似的方式启动此示例,正如本菜谱后面所讨论的。

<ios>
  <plist>
    <dict>

CFBundleURLlTypes节点是一个字典数组,描述了该包支持的 URL 方案。在此节点下,您将列出您的应用将响应的所有 URL 和方案名称。

    <key>CFBundleURLTypes </key>
    <array>
      <dict>

CFBundleURLName是包含此 URL 类型抽象名称的键。这是引用特定类型的主要方式。为确保唯一性,建议您使用 Java 包风格的标识符。

        <key>CFBundleURLName </key>
        <string>bencoding-linkLauncher</string>

CFBundleURLSchemes是包含字符串数组的键,每个字符串都标识由该类型处理的 URL 方案。

        <key>CFBundleURLSchemes</key>
        <array>
          <string>bencoding-linkLauncher</string>
        </array>
      </dict>
     </array>
   </dict>
  /plist>
</ios>

创建应用启动列表

schemeList CommonJS 模块,添加到my.scheme属性中,提供了 iOS 和 Android 方案示例列表。以下几节将描述如何在两个平台上创建方案。

iOS 方案列表

在 iOS 上,URL 方案就像网页上的 URL 一样工作。唯一的区别是应用注册的 URL 方案用作协议。以下步骤提供了具体 URL 格式的详细示例。

  1. getiOSList提供了一个对象数组,设计用于绑定到Ti.UI.TableView,并随后用于启动其他应用。

    exports.getiOSList = function(){
      var apps =[];
    
  2. 最基本的 URL 方案类型仅仅是应用的CFBundleURLName。以下示例展示了 Evernote 和 LinkedIn 的此属性。

      apps.push({
        title:'Evernote',
        url:'evernote://',
        errorMessage:'Check Evernote is installed'
      });
    
      apps.push({
        title:'LinkedIn',
        url:'linkedin://',
        errorMessage:'Check Linkedin is installed'
      });
    
  3. 更复杂的 URL 方案可以使用,如下面的示例所示,它包括about路由的 URL 方案。

      apps.push({
        title:'Recipe Example 1',
        url:'bencoding-linkrecipe://about',
        errorMessage:'Check Link Receiver Recipe is installed'
      });
    
  4. URL 方案也可以包含类似于网页的查询参数,如下面的代码片段所示:

      apps.push({
        title:'Recipe Example 2',    
          url:'bencodinglinkrecipe: //'+'login?user=chris&token=12345',
        errorMessage:'Check Link Receiver Recipe is installed'
      });
    
      return apps;
    };
    

Android 方案列表

Android 有能力以多种方式启动应用。本食谱侧重于如何使用类似于 iOS 的 URL 方案和意图来实现应用集成。

getAndroidList 提供了一个对象数组,这些对象旨在绑定到 Ti.UI.TableView 并用于稍后启动其他应用。

exports.getAndroidList = function(){
  var apps =[];
  1. 与 iOS 应用类似,您可以使用基于路由的 URL 来启动第三方应用。以下代码片段展示了如何构建一个带有 about 路由的 URL。

      apps.push({
        title:'Recipe Example 1',
        type:'url',
        color:'#000',
        url:'bencoding-linkrecipe://com.bencoding/about',
        errorMessage:'Check Link Receiver Recipe is installed'
      });
    
  2. 就像网页一样,您可以使用查询字符串参数构建更复杂的 URL。以下代码片段演示了如何在传递查询字符串信息的同时调用登录界面。

      apps.push({
        title:'Recipe Example 2',
        type:'url',
        color:'#000',
        url:'bencoding-linkrecipe://'+'login?user=chris&token=12345',
        errorMessage:'Check Link Receiver Recipe is installed'
      });
    
  3. 在 Android 上启动应用的一个更受欢迎的方式是使用意图。以下示例展示了如何创建一个意图以启动 LinkedIn 应用。

      var linkedInIntent = Ti.Android.createIntent({
        action: Ti.Android.ACTION_SEND,
     packageName :'com.linkedin.android',
     className:
     'com.linkedin.android.home.UpdateStatusActivity'
      });
    
  4. 一旦创建了 LinkedIn 意图,它将以意图类型添加到应用的数组中。这将被用于稍后启动 LinkedIn 应用。

      apps.push({
        title:'LinkedIn',
        type:'intent',
        color:'#000',
        intentObject :linkedInIntent
      });
    

    注意

    为了让意图启动应用,LinkedIn 应用必须在用户的设备上安装。如果应用未安装,将会生成一个异常。

  5. 在 Android 上启动应用的一个更受欢迎的方式是使用意图。以下示例展示了如何创建一个意图以启动 Evernote 应用。

      var everNoteIntent = Ti.Android.createIntent({
     action: "com.evernote.action.CREATE_NEW_NOTE"
      });
    

    注意

    为了让意图启动应用,Evernote 应用必须在用户的设备上安装。如果应用未安装,将会生成一个异常。

  6. 一旦创建了 Evernote 意图,它将以意图类型添加到应用的数组中。这将被用于稍后启动 Evernote 应用。

      apps.push({
        title:'Evernote',
        type:'intent',
        color:'#000',
        intentObject :everNoteIntent
      });
    
      return apps;	
    };
    

食谱的 UI

本食谱的这一部分是用于启动第三方应用的 UI。

  1. 首先,创建一个 Ti.UI.Window 来附加所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'App Launcher', 
      barColor:'#000',layout:'vertical',fullscreen:false
    });
    
  2. 接下来调用 schemeList CommonJS 模块以返回要启动的应用列表。

    var schemeData = ((my.isAndroid) ? 
    my.schemes.getAndroidList() : my.schemes.getiOSList())
    
  3. 然后,将 schemaData 对象格式化并绑定到 Ti.UI.TableView 以进行显示。

    var tableView = Ti.UI.createTableView({
      data : my.schemes.format(schemeData),
      top:10, bottom:0, width:Ti.UI.FILL
    });
    win.add(tableView);
    

本食谱的最后一部分演示了如何使用在 Ti.UI.TableView 中显示的列表来启动第三方应用。

  1. Ti.UI.TableView 上添加了一个 click 事件。当用户在 Ti.UI.TableView 中的某一行上点击时,此事件将被触发。

    tableView.addEventListener('click',function(e){
    
  2. 事件触发后,第一步是检查食谱正在运行的平台。

      if(my.isAndroid){
    
        try{
    
  3. 如果在 Android 下运行,必须执行检查以确定启动类型是意图还是 URL。

          if(e.rowData.type == "intent"){
    
  4. 如果启动类型是意图,则使用提供的意图调用 Ti.Android.currentActivity.startActivity,如下面的代码片段所示。如果第三方应用已安装在用户的设备上,这将启动该应用。

            Ti.Android.currentActivity.startActivity(
            e.rowData.intentObject);
          }else{
    
  5. 如果启动类型是 URL,则使用 Ti.Platform.openURL 方法通过提供的 url 属性打开应用,如下面的代码片段所示。如果第三方应用已安装在用户的设备上,这将启动该应用。

            Ti.Platform.openURL(e.rowData.url);
          }
        }catch(err){
          alert(e.rowData.errorMessage);
        }
    
      }else{
    

    注意

    如果应用未安装,将会生成一个异常。

  6. iOS 平台通过类似于使用Ti.Platform.openURL方法加载网页的方式,使用 URL 方案启动第三方应用。以下代码片段展示了如何使用schemeList CommonJS 模块提供的url属性启动第三方应用:

        if(Ti.Platform.canOpenURL(e.rowData.url)){
          Ti.Platform.openURL(e.rowData.url);
        }else{
          alert(e.rowData.errorMessage);
        }
      }
    });
    win.open({modal:true});
    

参见

跨平台 URL 方案

iOS 上的 URL 方案和 Android 上的意图过滤器为你提供了一个开放集成点,让你可以将你的应用功能暴露给他人。如果你正在构建一套企业应用,如单独的里程和费用应用,并希望允许它们之间有集成点,这将特别有帮助。

此配方演示了如何在 Titanium 企业应用中创建跨平台 URL 方案。我们将展示如何使用这个开放集成点来访问 Titanium 应用中的功能。

准备就绪

此配方使用demoUITi.SchemeTools CommonJS 库来帮助管理和演示如何创建跨平台 URL 方案。此模块和其他代码资源可以从书中提供的源代码下载。要安装这些模块,只需将它们复制到 Titanium 项目的Resources文件夹中,如下截图所示:

准备就绪

AppLauncher 要求

此配方的另一个要求是 AppLauncher 应用,该应用是在本章前面讨论的从一个应用启动另一个应用配方中创建的。此应用将用于启动此配方中包含的不同 URL 示例。

如何做到...

在添加了demoUITi.SchemeTools模块之后,你需要创建你的应用程序命名空间,并使用require将模块导入到你的app.js中,如下代码片段所示:

//Create our application namespace
var my = {
  isAndroid : Ti.Platform.osname === 'android',
  scheme : require('Ti.SchemeTools'),
  ui : require('demoUI')
};

iOS 更新 tiapp.xml

要使其他应用能够启动此配方,你必须对tiapp.xml文件进行一些更新。以下步骤讨论了如何创建bencoding-linkrecipe自定义 URL 方案:

  1. 首先,打开你的项目tiapp.xml文件,并对ios节点进行以下修改:

    <ios>
      <plist>
        <dict>
    
  2. 接下来,添加CFBundleURLTypes节点。这是一个描述应用程序支持的 URL 方案的字典数组。在此节点下,你将列出你的应用将响应的所有 URL 和方案名称。

        <key>CFBundleURLTypes</key>
          <array>
            <dict>
    
  3. 然后添加CFBundleURLName键。此键包含此 URL 类型的抽象名称。这是引用特定应用的主要方式。为确保唯一性,建议使用 Java 包风格的标识符。

              <key>CFBundleURLName </key>
              <string>bencoding-linkrecipe</string>
    
  4. 最后,添加CFBundleURLSchemes键。这些键包含一个字符串数组,每个字符串都标识由该类型处理的 URL 方案。以下代码片段显示了应用的登录、关于和根活动的 URL:

     <key>CFBundleURLSchemes</key>
     <array>
     <string>bencoding-linkrecipe</string>
     <string>bencoding-linkrecipe://about</string>
     <string>bencoding-linkrecipe://login</string>
     </array>
            </dict>
          </array>                
        </dict>
      </plist>
    </ios>
    

Android 更新 tiapp.xml

要在 Android 上创建自定义 URL 方案,您需要编辑tiapp.xml文件以添加一个 intent 过滤器来监听特定的android:schemeandroid:host

  1. 首先打开tiapp.xml文件并编辑 android 配置节点。

    <android >
    
  2. 接下来,添加manifest节点。

      <manifest>
    
  3. 添加application节点。此节点将用于生成项目的AndroidManifest.xml文件,因此请确保属性正确匹配您的项目。

        <application 
        android:debuggable="false" android:icon=
        "@drawable/appicon" android:label=
        "XUrlScheme" android:name="XurlschemeApplication">
    
  4. 接下来添加应用的根活动。

          <activity 
          android:configChanges="keyboardHidden|orientation" 
          android:label="XUrlScheme" 
          android:name=".XurlschemeActivity" 
          android:theme="@style/Theme.Titanium">
    
  5. 然后添加应用的主要 intent 过滤器。这将用于启动您的应用。

            <intent-filter>
              <action android:name=
              "android.intent.action.MAIN"/>
              <category android:name=
              "android.intent.category.LAUNCHER"/>
            </intent-filter>
    
  6. 接下来添加一个带有自定义 URL 信息的第二个 intent 过滤器。

            <intent-filter>
    
  7. 然后添加包含您的android:schemeandroid:host信息的 data 节点。这些值在Ti.Platform.openURL用于启动 URL 方案时用作协议。以下高亮代码允许您使用类似bencoding-linkrecipe://com.bencoding的 URL 启动应用。

              <data android:scheme="bencoding-linkrecipe" 
              android:host="com.bencoding"/>
    
  8. 接下来必须向 intent 过滤器添加一个类别,以便应用能够正确地暴露给第三方启动器,并在 URL 方案被调用时打开应用。以下高亮代码片段显示了实现自定义 URL 方案所需的类别信息。

              <category android:name=
              "android.intent.category.DEFAULT" />
              <category android:name=
              "android.intent.category.BROWSABLE" />
              <action android:name=
              "android.intent.action.VIEW" />
    
            </intent-filter>
    
          </activity>
        </application> 
      </manifest>
    </android>
    

创建菜谱 UI

这个菜谱有一个简单的 UI,用于演示如何实现不同的 URL 方案功能。

  1. 首先,创建一个Ti.UI.Window;这是应用的根Ti.UI.Window

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'Url Receiver', 
      barColor:'#000',layout:'vertical',fullscreen:false
    });
    
  2. 接下来,如果菜谱在 iOS 设备上运行,将添加一个Ti.UI.Button,允许 URL 接收器应用启动本章前面讨论的从另一个应用启动菜谱中创建的应用启动器应用。

    if(!my.isAndroid){
      var launcherButton = Ti.UI.createButton({
        title:'Press to open AppLauncher Sample', 
        top:30, left:10, right:10, height:45
      });
      win.add(launcherButton);
    
  3. 当点击launcherButton时,如果设备上安装了应用启动器,应用将尝试打开应用启动器。以下高亮代码演示了如何使用Ti.Platform.openURL方法来启动应用。

      launcherButton.addEventListener('click',function(e){
        if(Ti.Platform.canOpenURL(
        "bencoding-linkLauncher://")
        ){
          Ti.Platform.openURL(
          "bencoding-linkLauncher://");
        }else{
          alert("Please install the AppLauncher Recipe");
        }
      });
    }
    

启动辅助函数

这个菜谱使用assist object来帮助启动不同的 URL。

var assist = {
  1. openWindow方法根据提供的 URL 和参数值打开一个特定的窗口。

      openWindow : function(url,params){
        if(url == 'about'){
          my.ui.openAboutWindow();
        }
        if (url =='login'){
          my.ui.openLoginWindow(params);
        }
      },
    
  2. 当应用打开或恢复时,会调用openPageFromUrl方法以确定应用是否是从第三方应用打开的,如果是,提供了哪些路由信息。

      openPageFromUrl : function(){
    
  3. 第一步是确定应用是否是从第三方应用打开的。这是通过检查hasLaunchUrl方法来完成的。

        if(!my.scheme.hasLaunchUrl()){
    
  4. 如果应用不是从另一个应用启动的,菜谱的主页将被显示,并且任何会话变量将被重置。

          my.scheme.session.reset();
          return;
        }
    
  5. 如果应用是由另一个应用启动的,我们首先需要检查启动 URL 是否已更改。这可以避免在请求相同窗口时重新加载应用。这可以通过调用以下代码片段中突出显示的hasChanged方法来完成:

     if(!my.scheme.session.hasChanged()){
          return;
        }
    
  6. 接下来,通过调用getLaunchUrl方法获取要启动的 URL,如下面的代码片段所示。然后,此 URL 将被加载到session变量中供以后使用。

        my.scheme.session.launchUrl = 
        my.scheme.getLaunchUrl();
    
  7. 然后使用getCurrentPage函数获取请求的页面。这将用于确定使用openWindow方法加载哪个页面。

        var requestedPage = my.scheme.getCurrentPage();
    
  8. 接下来,获取任何启动参数。以下代码块演示了如何检查是否提供了启动参数,并将它们解析为格式化的对象。

        my.scheme.session.launchParams = null;
        if(my.scheme.hasParams()){
          my.scheme.session.launchParams = 
          my.scheme.getParams();
        }
    
  9. 最后,使用在前面步骤中创建的requestedPagelaunchParams调用openWindow方法。openWindow方法将随后在应用中打开请求的窗口。

        assist.openWindow(requestedPage,
        my.scheme.session.launchParams);
      }
    };
    

当从另一个应用启动时

为了让食谱启动请求的窗口,必须在恢复和打开事件中添加监听器。以下步骤详细说明了如何将适当的事件监听器添加到您的应用中:

  1. 如果食谱在 iOS 上运行,则添加恢复事件监听器。请注意,在实现您自己的应用时,必须将其添加到app.js中。

    if(!my.isAndroid){
    
      Ti.App.addEventListener( 'resumed', function(e) {
    
  2. 每次应用恢复时,都会调用assist.openPageFromUrl方法。如果没有提供 URL 信息,则将显示主窗口。

        assist.openPageFromUrl();
      });
    
    }
    
  3. 在主窗口的打开事件上添加了一个监听器。此事件将在应用首次启动时触发。

    win.addEventListener('open',function(e){
    
  4. 在首次启动时,会调用assist.openPageFromUrl方法以确定应用是否被第三方应用打开。

      assist.openPageFromUrl();
    
  5. 如果食谱在 Android 上运行,则会在主窗口的resume事件上添加一个监听器。这允许食谱检测在应用在后台运行时是否有第三方应用尝试打开食谱。

      if(my.isAndroid){
        win.activity.addEventListener('resume', function(){
          assist.openPageFromUrl();
        });
      }
    
    });
    
    win.open();
    

它是如何工作的...

跨平台自定义 URL 方案是提供第三方集成点的低成本好方法。以下部分详细说明了使用“从一个应用启动另一个应用”食谱访问不同应用路由的端到端过程,而无需此食谱的示例应用。

启动关于窗口

本节提供了逐步说明,说明您如何在从应用启动器食谱启动时,在 URL 接收器食谱中启动关于窗口。

  1. 打开应用启动器应用程序,并在表格视图中轻触食谱示例 1行,如以下截图中的红色方框所示。启动关于窗口

  2. 应用启动器应用运行以下代码块。

    • 在 iOS 上

      Ti.Platform.openURL('bencoding-linkrecipe://about');
      
    • 在 Android 上

      Ti.Platform.openURL('bencoding-linkrecipe://com.bencoding/about');
      
  3. 然后将在您的设备上启动 URL 接收器食谱。

  4. 然后恢复或打开事件处理程序将解析提供的 URL 信息。当调用getCurrentPage方法时,requestedPage值将被设置为包含about值的字符串。

    var requestedPage = my.scheme.getCurrentPage();
    
  5. 接下来,调用my.scheme.hasParams方法以确定是否在提供的 URL 中传递了任何参数。在这种情况下,没有提供任何附加参数,因此session参数对象保持为null

  6. 然后调用openWindow方法,并打开以下关于窗口:启动关于窗口

启动登录窗口

本节提供了从应用启动器食谱启动 URL 接收器食谱中登录窗口的逐步描述。作为启动过程的一部分,用户和令牌参数由应用启动器食谱提供,然后在 URL 接收器食谱应用中用于完成表单。

  1. 打开应用启动器应用程序,并在表格视图中点击食谱示例 2行,如下截图中的红色方框所示:启动登录窗口

  2. 应用启动器应用程序运行以下代码块。

    • 在 iOS 上:

      Ti.Platform.openURL('bencoding-linkrecipe://login?user=chris&token=12345');
      
    • 在 Android 上:

      Ti.Platform.openURL(
      'bencoding-linkrecipe://com.bencoding/'+
      login?user=chris&token=12345');
      
  3. 然后在您的设备上启动 URL 接收器食谱。

  4. 然后恢复或打开事件处理程序将解析提供的 URL 信息。当调用getCurrentPage方法时,requestedPage值将被设置为包含login值的字符串。

    var requestedPage = my.scheme.getCurrentPage();
    
  5. 接下来,调用my.scheme.hasParams方法以确定是否在提供的 URL 中传递了任何参数。

  6. 下面的参数对象是使用调用 URL 中包含的查询字符串参数创建的。

    { user:"chris",
     token:12345 }
    
  7. 然后调用openWindow方法,提供requestedPageparameter对象。这将打开登录页面,并使用用于完成表单的参数,如下截图所示:启动登录窗口

使用 BOOT_COMPLETED 打开您的 Android 应用

许多企业应用都有始终在后台运行的要求。这些应用最常见的类别包括路线管理、物品跟踪或客户调度应用。在您的 Android Titanium 应用中,您可以使用BOOT_COMPLETED动作的 intent 过滤器来通知您的应用设备已重新启动。BOOT_COMPLETED动作的事件生命周期如下图中所示:

使用 BOOT_COMPLETED 打开您的 Android 应用

准备就绪

此食谱使用bencoding.android.tools本地模块来帮助订阅BOOT_COMPLETED广播。此模块和其他代码资产可以从本书提供的源代码中下载,或者通过此食谱末尾的“也见”部分提供的链接单独下载。要安装此模块,只需将以下截图所示的modules文件夹复制到您的项目中。

示例配方还包括一系列示例tiapp.xmlapp.js文件,展示了处理接收BOOT_COMPLETED广播的不同选项。您需要将以下截图突出显示的文件复制到您的项目中,因为它们将被用于演示不同的可用选项。

准备就绪

复制提到的文件后,您需要在 Titanium Studio 中点击您的tiapp.xml文件,并添加对bencoding.android.tools模块的引用,如下截图所示:

准备就绪

如何操作...

此配方演示了如何实现以下场景:

  • 在接收到BOOT_COMPLETED广播时自动重启您的应用

  • 向接收BOOT_COMPLETED广播的用户发送通知

  • 使用 Titanium 属性配置您的应用如何处理BOOT_COMPLETED广播

必需的 tiapp.xml 更新

当 Titanium Android 应用启动时,Titanium 框架必须首先初始化,然后您的应用才能运行。当应用由其他服务(如BOOT_COMPLETED广播)启动时,您将收到一条消息,表明应用需要重启。为了避免此问题,您必须在您的tiapp.xml文件中添加以下属性:

<property name="ti.android.bug2373.finishfalseroot" type="bool">true</property>
<property name="ti.android.bug2373.disableDetection" type="bool">true</property>
<property name="ti.android.bug2373.restartDelay" type="int">500</property>
<property name="ti.android.bug2373.finishDelay" type="int">0</property>
<property name="ti.android.bug2373.skipAlert" type="bool">true</property>
<property name="ti.android.bug2373.message">Initializing</property>
<property name="ti.android.bug2373.title">Restart Required</property>
<property name="ti.android.bug2373.buttonText">Continue</property>

场景 A – 自动重启

Android.Tools.Receiver模块提供的第一个BOOT_COMPLETED选项是能够重启您的应用。典型用例是在用户重启其设备时重启 Titanium 应用。

小贴士

要使用配方源将此场景部署到您的设备上,将1.auto_start_tiapp.xml文件重命名为tiapp.xml,将1.app.js重命名为app.js。这将使配方应用使用以下场景详情。

自动重启步骤 1:将接收器添加到 tiapp.xml

启用此场景的第一步是将接收器条目添加到我们项目中的 Android 配置节点,该节点位于您的 Titanium 项目根目录下的tiapp.xml文件中。

<receiver android:exported="true" android:name="bencoding.android.receivers.BootReceiver">
  1. 首先,必须在我们的接收器中添加一个 intent 过滤器,这将使我们的应用订阅BOOT_COMPLETED广播。

      <intent-filter>
        <action android:name=
        "android.intent.action.BOOT_COMPLETED"/>
      </intent-filter>
    
  2. 接下来添加一个名为bootType的元数据节点。此节点由Android.Tools模块使用,以确定应采取什么启动操作。以下代码片段演示了如何配置模块以重启应用。

      <meta-data android:name=
      "bootType" android:value="restart"/>
    
  3. 然后添加一个名为sendToBack的元数据节点。此节点由Android.Tools模块使用,以确定如果设置为true,则应用应在后台重启,如果设置为false,则应在前台重启。

      <meta-data android:name=
      "sendToBack" android:value="true"/>
    </receiver>
    
  4. 最后添加了RECEIVE_BOOT_COMPLETED权限。

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    

自动重启步骤 2:测试

以下步骤概述了如何最好地测试此配方场景:

  1. 清理您的 Titanium 解决方案并将其推送到设备。

  2. 确保应用未在您的设备上运行。您可以使用设置中的强制停止选项停止应用。

  3. 重启您的设备。

  4. 在设备重启后,确认配方主屏幕上显示的日期和时间。

场景 B – 重启时通知

Android.Tools.Receiver 模块提供的第二个 BOOT_COMPLETED 选项是在用户重启其设备时创建通知消息的能力。这可以用于向用户提供提醒或说明。

小贴士

要使用配方源将此场景部署到您的设备上,将 2.auto_start_tiapp.xml 文件重命名为 tiapp.xml,将 2.app.js 文件重命名为 app.js。这将使配方应用切换到使用以下场景详情。

重启通知步骤 1:将接收器添加到 tiapp.xml

启用此场景的第一步是将接收器条目添加到我们项目 tiapp.xml 文件中的 Android 配置节点。

<receiver android:exported="true" android:name="bencoding.android.receivers.BootReceiver">
  1. 首先,必须向我们的接收器添加一个意图过滤器,这将使我们的应用订阅 BOOT_COMPLETED 广播。

      <intent-filter>
        <action android:name=
        "android.intent.action.BOOT_COMPLETED"/>
      </intent-filter>
    
  2. 接下来,添加一个名为 bootType 的元数据节点。此节点由 Android.Tools 模块用于确定应采取什么启动操作。以下片段演示了如何配置模块在设备重启时发送通知消息。

      <meta-data android:name=
      "bootType" android:value="notify"/>
    
  3. 然后添加一个名为 title 的元数据节点。此节点包含将要使用的通知标题。

      <meta-data android:name="title" 
      android:value="Title Sample from tiapp.xml"/>
    
  4. 然后添加一个名为 message 的元数据节点。此节点包含将要使用的通知消息。

      <meta-data android:name="message" 
      android:value="Message Sample from 
      tiapp.xml"/></receiver>
    
  5. 最后,添加了 RECEIVE_BOOT_COMPLETED 权限。

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    

重启通知步骤 2:测试

以下步骤概述了测试此配方场景的最佳方法:

  1. 清理您的钛金解决方案并将其推送到设备。

  2. 确保应用在您的设备上没有运行。您可以使用 设置 下的 强制停止 选项停止应用。

  3. 重启您的设备。

  4. 在设备重启后的几秒钟内,您将在通知托盘中看到一条新消息。

场景 C – 属性控制

Android.Tools.Receiver 模块提供的最终 BOOT_COMPLETED 选项是能够重启您的应用。典型用例是在用户重启其设备时重启钛金应用。

小贴士

要使用配方源将此场景部署到您的设备上,将 3.auto_start_tiapp.xml 文件重命名为 tiapp.xml,将 3.app.js 文件重命名为 app.js。这将使配方应用切换到使用以下场景详情。

属性控制步骤 1:将接收器添加到 tiapp.xml

启用此场景的第一步是将接收器条目添加到我们项目 tiapp.xml 文件中的 Android 配置节点。

<receiver android:exported="true" android:name="bencoding.android.receivers.BootReceiver">
  1. 必须向我们的接收器添加一个意图过滤器,这将使我们的应用订阅 BOOT_COMPLETED 广播。

      <intent-filter>
        <action android:name=
        "android.intent.action.BOOT_COMPLETED"/>
      </intent-filter>
    
  2. 接下来,添加一个名为 bootType 的元数据节点。此节点由 Android.Tools 模块用于确定应采取什么启动操作。以下代码片段演示了如何配置模块以使用钛金属性来确定应采取的操作。

        <meta-data android:name="bootType" 
        android:value=" propertyBased"/>
    
  3. 然后添加元数据节点来配置 BOOT_COMPLETED 接收器。每个节点都用于将一个 Titanium 属性映射到一个接收器配置元素。例如,bootType_property_to_reference 包含用于确定 bootType 的属性名称。

    <meta-data android:name="enabled_property_to_reference" android:value="my_enabled"/>
    <meta-data android:name="bootType_property_to_reference" android:value="my_bootType"/>
    <meta-data android:name="sendToBack_property_to_reference" android:value="my_sendtoback"/>
    <meta-data android:name="icon_property_to_reference" android:value="my_notify_icon"/>
    <meta-data android:name="title_property_to_reference" 
    ndroid:value="my_notify_title"/>
    <meta-data android:name="message_property_to_reference" android:value="my_notify_message"/></receiver>
    
  4. 最后,添加 RECEIVE_BOOT_COMPLETED 权限。

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    

属性控制步骤 2:创建 recipe app.js

Android.Tools 模块允许通过 Titanium 属性配置 BOOT_COMPLETED 接收器。以下代码片段(app.js)演示了如何通过简单地更新正确的 Titanium 属性来创建两种不同的配置。

  1. 首先,创建一个 Ti.UI.Window 来附加所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'BOOT_COMPLETED Example', 
      layout:'vertical',fullscreen:false, exitOnClose:true
    });
    
  2. 接下来,创建一个 Ti.UI.Button 来演示如何创建前台重启。

    var button1 = Ti.UI.createButton({
      title:'Foreground Restart',
      top:25, height:45, left:5, right:5
    });
    win.add(button1);
    
  3. 在我们第一个按钮的 click 事件上,用于配置我们的 BOOT_COMPLETED 接收器的属性被更新。

    button1.addEventListener('click',function(e){
    
  4. click 事件中执行的第一项操作是将 BOOT_COMPLETED 接收器设置为启用。

      Ti.App.Properties.setBool('my_enabled',true);
    
  5. 接下来,将 bootType 设置为重启。这将使我们的 Titanium 应用在设备重启时重启。

      Ti.App.Properties.setString("my_bootType", "restart");
    
  6. 然后,将 sendToBack 属性设置为 false。这将使用户在重启其设备时,我们的 Titanium 应用在前台重启。

      Ti.App.Properties.setBool("my_sendtoback",false);
    });
    
  7. 接下来,创建第二个按钮来演示如何配置通知。

    var button2 = Ti.UI.createButton({
      title:'Restart Notification',
      top:25, height:45, left:5, right:5
    });
    win.add(button2);
    
  8. 接下来创建第二个按钮来演示如何在设备重启时生成通知的配置。

    button2.addEventListener('click',function(e){
    
  9. click 事件中执行的操作是将 BOOT_COMPLETED 接收器设置为 enabled

      Ti.App.Properties.setBool('my_enabled',true);
    
  10. 接下来,将 bootType 设置为 notify。这将接收 BOOT_COMPLETED 广播后发送一条消息。

      Ti.App.Properties.setString("my_bootType", "notify");
    
  11. 接下来,创建通知图标资源标识符。此资源标识符将用于在创建通知时创建图标。如果没有提供资源标识符,则默认使用星形图标。

      .App.Properties.setInt("my_notify_icon", 17301618);
    
  12. 最后,将链接到通知标题和消息的属性设置为新的字符串值。这些属性将在设备重启时生成通知时使用。

      Ti.App.Properties.setString(
      "my_notify_title", "Title from property");
      Ti.App.Properties.setString(
      "my_notify_message","Message from property");
    });
    

属性控制步骤 3:测试

以下步骤概述了如何最好地测试此配方场景:

  1. 清理您的 Titanium 解决方案并将其推送到设备。

  2. 打开应用,并按下第一个按钮(button1)。这将更新属性以执行前台重启。

  3. 接下来停止应用;您可以在 设置 下的 强制停止 选项中停止应用。

  4. 重启您的设备。

  5. 设备启动后,配方应用将在前台启动。

  6. 按下第二个按钮(button2)。这将更新属性以在重启时发送通知。

  7. 重启您的设备。

  8. 在设备重启后的几秒钟内,您将在通知托盘中看到一条新消息。

参见

在本食谱中使用了Android.Tools模块来提供BOOT_COMPLETED功能。有关此模块的更多信息,请访问 GitHub 上的项目github.com/benbahrenburg/benCoding.Android.Tools

有关 BootReceiver 的完整文档,请访问github.com/benbahrenburg/benCoding.Android.Tools/tree/master/documentation/bootreceiver.md

使用 Web Workers 进行 iOS 多线程

企业应用程序通常需要执行大量的处理。为了在充分利用有限的设备资源的同时提供最佳的用户体验,您必须在后台线程上执行计算操作。

在本食谱中,我们将讨论如何使用Ti.WebWorkerWrapper模块执行后台计算操作。为了模拟计算任务,对随机数字计算斐波那契序列并在并行中进行处理。以下截图显示了在 iPhone 上运行这些 Web Worker 任务的食谱。

使用 Web Workers 进行 iOS 多线程

注意

本食谱是一个仅适用于 iOS 的食谱,因为它需要 Web Workers,而 Web Workers 目前尚不支持 Titanium Android。

准备就绪

本食谱使用Ti.WebWorkerWrapper CommonJS 模块和fibonacci.js Web Worker。这些模块和其他代码资产可以从本书提供的源代码中下载,或者通过本食谱末尾的“另请参阅”部分提供的链接单独下载。要将Ti.WebWorkerWrapper模块和 Web Worker 安装到您的项目中,只需将Ti.WebWorkerWrapper.jsfibonacci.js文件复制到您的 Titanium 项目中的Resources文件夹,如下截图所示:

准备就绪

如何操作...

一旦您已将Ti.WebWorkerWrapper模块和fibonacci.js文件添加到您的项目中,接下来您需要创建您的应用程序命名空间,并使用require将模块导入到您的app.js文件中,如下代码片段所示:

//Create our application namespace
var my = {
  workerMod : require('Ti.WebWorkerWrapper'),
  isAndroid : Ti.Platform.osname === 'android'
};

创建食谱 UI

本食谱提供了一个基本的 UI,用于启动和跟踪 Web Workers 执行斐波那契序列的过程。以下步骤详细说明了如何创建前面截图所示的主要组件:

  1. 首先创建一个Ti.UI.Window。此窗口将用于附加和显示所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'web Workers', 
      barColor:'#000',layout:'vertical',fullscreen:false
    });
    
  2. 接下来,一系列Ti.UI.Label控件被添加到Ti.UI.Window中。这些控件将用于显示 Web Workers 的进度和结果。

    var worker1 = Ti.UI.createLabel({
      top:20, height:25, left:5, right:5,color:'#000',
      textAlign:'left',text:'Not yet processed - Press Button', 
      font:{fontSize:14, fontWeight:'bold'}
    });
    win.add(worker1);
    

    注意

    使用与前面代码片段中显示的worker1标签相同的模板,添加了三个额外的 Web Worker 标签。

  3. 最后,在Ti.UI.Window中添加了一个Ti.UI.Button。此按钮用于启动本食谱中使用的四个 Web Worker 进程。

    var runButton = Ti.UI.createButton({
      title:'Run Web Worker Test', top:40,
      left:10, right:10, height:50, 
    });
    win.add(runButton);
    

测试函数

本菜谱使用一个 tests 对象来帮助处理时间和显示 Web Worker 斐波那契数列处理的结果。以下部分讨论了 tests JavaScript 对象中的功能。

var tests = {
  1. updateLabel 方法用于将格式化模板应用于每个标签,以显示斐波那契数列回调结果。

      updateLabel : function(label,e){
        label.text = "Seed:" + e.seed + " result:" + 
        e.result + " elapsed:" + e.elapsed + "ms";
      },
    
  2. worker1worker4 方法是作为回调方法提供给每个 Web Worker 的,以便它在处理斐波那契数列时使用。这些回调接收计算过程中的结果。

      worker1 : function(e){
        tests.updateLabel(worker1,e);
      },
      worker2 : function(e){
        tests.updateLabel(worker2,e);
      },
      worker3 : function(e){
        tests.updateLabel(worker3,e);
      },
      worker4 : function(e){
        tests.updateLabel(worker4,e);
      },
    
  3. random 方法用于在给定范围内生成随机数。此方法用于生成发送给斐波那契数列的随机数。

      random : function(from,to){
        return Math.floor(Math.random()*(to-from+1)+from);
      }
    };
    

使用 Web Workers 进行多线程

当用户点击 runButton 按钮时,将运行多线程示例。以下部分演示了如何在后台创建四个 Web Workers。一旦每个 Web Worker 完成处理,结果将提供给本菜谱前面讨论过的测试回调方法。

runButton.addEventListener('click',function(e){
  1. 首先,通过初始化 Ti.WebWorkerWrapper 模块的新实例来创建一个新的 worker 对象。

      var worker1 = new my.workerMod();
    
  2. 接下来,在 worker 对象上调用 start 方法。start 方法需要以下参数:

    • Web Worker 应该执行的 JavaScript 文件路径

    • 作为 Web Worker postMessage 传递的随机数

    • 当 Web Worker 完成处理时使用的回调方法

        worker1.start('fibonacci.js',tests.random(1,50),
        tests.worker1);
      
  3. 使用与 worker1 创建相同的模式创建了三个额外的 Web Workers。

      var worker2 = new my.workerMod();
      worker2.start('fibonacci.js',tests.random(1,50),
      tests.worker2);
    
      var worker3 = new my.workerMod();
      worker3.start('fibonacci.js',tests.random(1,50),
      tests.worker3);
    
      var worker4 = new my.workerMod();
      worker4.start('fibonacci.js',tests.random(1,50),
      tests.worker4);
    
    });
    

参见

第八章.基本安全方法

在本章中,我们将涵盖:

  • 在 Titanium 中实现 iOS 数据保护

  • 使用 JavaScript 进行 AES 加密

  • 使用 Ti.Network.HTTPClient 进行基本身份验证

  • 实现跨平台密码屏幕

  • 在 iOS 上处理受保护的 ZIP 文件

简介

安全性、隐私和知识产权保护是我们这些构建 Titanium 企业应用的人心中的首要任务。Titanium 允许您结合底层平台工具和第三方 JavaScript 库,以帮助您满足您的安全需求。

本章提供了一系列方法,介绍如何利用 JavaScript、Titanium 模块和底层平台,帮助您创建分层安全方法,以协助您实现组织整体安全开发目标。每个配方都旨在提供构建块,帮助您实现您所在行业的现有安全和隐私标准。

在 Titanium 中实现 iOS 数据保护

从 iOS 4 开始,苹果引入了应用使用数据保护功能以在磁盘上存储的数据添加额外安全层的能力。数据保护使用内置的硬件加密来加密设备上存储的文件。当用户的设备被锁定并使用密码锁保护时,此功能可用。在此期间,所有文件都受到保护且无法访问,直到用户明确解锁设备。

注意

当设备被锁定时,没有任何应用可以访问受保护的文件。这甚至适用于创建该文件的应用。

准备就绪

此配方使用securely原生模块以增强安全性功能。此模块和其他代码资产可以从本书提供的源代码中下载。将这些安装到您的项目中很简单。只需将modules文件夹复制到您的项目中,如图所示:

准备就绪

在复制提到的文件夹后,您需要在 Titanium Studio 中点击您的tiapp.xml文件,并将对bencoding.securely模块的引用添加到其中,如图所示:

准备就绪

启用数据保护

此配方需要您的 iOS 设备启用数据保护。您需要一个设备,因为模拟器不支持数据保护。以下步骤涵盖了如何在您的设备上启用此功能:

  1. 前往设置 | 通用 | 密码

  2. 按提示设置密码。

  3. 添加密码后,滚动到屏幕底部并验证是否可以看到如图所示的文本数据保护已启用启用数据保护

iOS 设备浏览器

需要一个第三方 iOS 设备浏览器来验证示例配方应用的数据保护是否已成功启用。本配方讨论了如何使用流行的 iExplorer 应用来验证数据保护。可以使用 iExplorer 应用的评估版本来跟随本配方。有关更多信息以及下载 iExplorer,请访问www.macroplant.com/iexplorer

如何操作...

为了启用 iOS 数据保护,需要将DataProtectionClasscom.apple.developer.default-data-protection键添加到你的tiapp.xml中,如下代码片段所示:

  1. 首先,如果你的项目中还没有包含这个元素,请添加ios配置节点。

    <ios>
      <plist>
        <dict>
    
  2. 然后在dict节点顶部添加以下突出显示的键。

     <key>DataProtectionClass</key>
     <string>NSFileProtectionComplete</string>
     <key>com.apple.developer.
     default-data-protection</key>
     <string>NSFileProtectionComplete</string>
         </dict>
      </plist>
    </ios>
    
  3. 在保存tiapp.xml的更新后,你必须清理你的 Titanium 项目,以便更新生效。这可以在 Titanium Studio 中通过选择项目 | 清理来完成。

创建命名空间和导入

一旦你添加了securely模块并更新了项目中的tiapp.xml,你需要在app.js文件中创建你的应用程序命名空间,并使用require将模块导入到你的代码中,如下代码片段所示:

//Create our application namespace
var my = {
  secure : require('bencoding.securely')
};

创建配方 UI

以下步骤概述了如何创建本配方中使用的 UI:

  1. 首先,创建一个Ti.UI.Window来附加所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', 
      title: 'Data Protection Example', 
      barColor:'#000',layout:'vertical'
    });
    
  2. 接下来,向Ti.UI.Window中添加一个Ti.UI.Button。这将用于触发我们的示例。

    var button1 = Ti.UI.createButton({
      title:'Create Test File',
      top:25, height:45, left:5, right:5
    });
    win.add(button1);
    

创建一个文件以验证数据保护

为了验证应用中是否启用了数据保护,该配方会在Ti.Filesystem.applicationDataDirectory目录中创建一个带时间戳的文件。使用 iOS 设备浏览器,我们可以验证当设备锁定时测试文件是否受到保护。以下步骤描述了配方如何创建这个测试文件:

  1. button1click事件创建一个带时间戳的文件,使我们能够验证应用是否已正确启用了数据保护。

    button1.addEventListener('click',function(e){
    
  2. 接下来,在securely上调用isProtectedDataAvailable方法。这提供了一个布尔结果,指示数据保护允许应用从或向文件系统读写。

      if(!my.secure.isProtectedDataAvailable()){
        alert('Protected data is not yet available.');
        return;
      }
    
  3. 为了确保文件中有一个唯一的标识符,使用当前日期和时间创建了一个令牌。然后,将此令牌添加到以下消息模板中:

      var timeToken = String.formatDate(new Date(),"medium") +
      String.formatTime(new Date());
      var msg = "When device is locked you will not be able";
      msg += " to read this file. Your time token is ";
      msg += timeToken;
    
  4. 第 3 步创建的消息随后被写入到位于Ti.Filesystem.applicationDataDirectory目录中的test.txt文件。如果文件已存在,则将其删除,以便最新的消息可用于测试。

      var testfile = Ti.Filesystem.getFile(
      Ti.Filesystem.applicationDataDirectory, 'test.txt');
      if(testfile.exists()){
        testfile.deleteFile();
      }
      testfile.write(msg);
      testfile = null;
    
  5. 一旦将test.txt文件写入设备,就会向用户显示一条消息,通知他们锁定设备并使用 iOS 设备浏览器来确认数据保护已启用。

      var alertMsg = "Please lock your device.";
      alertMsg+= "Then open an iOS Device Browser.";
      alertMsg+= "The time token you are looking for is ";
      alertMsg+= timeToken;
      alert(alertMsg);
    

它是如何工作的...

在您的 tiapp.xml 文件中添加了 DataProtectionClasscom.apple.developer.default-data-protection 键之后,当设备锁定时,iOS 设备将处理保护您的文件。以下步骤讨论了如何测试此食谱是否正确实现了数据保护:

  1. 验证过程的第一步是在您的 iOS 设备上构建和部署食谱应用程序。

  2. 一旦应用程序已加载到您的设备上,打开应用程序并点击 创建测试文件 按钮。工作原理...

  3. 一旦收到表示测试文件已创建的警报消息,请按主页按钮并锁定您的设备。

  4. 将您的设备连接到已安装 iExplorer 的计算机。

  5. 打开 iExplorer 并导航,以便您可以在设备上查看应用程序。

  6. 在以下截图中的红色方框中标记的 DataProtection 应用程序中选择。然后右键单击位于 Documents 文件夹中的 test.txt 文件,并选择与绿色方框中标记的 Quick Look 相同的选项:工作原理...

  7. 选择 Quick Look 后,iExplorer 将尝试打开 test.txt 文件。由于它受到保护,无法打开,Quick Look 将显示进度指示器,直到达到超时。工作原理...

  8. 然后,您可以解锁您的设备并重复前面的步骤以在 Quick Look 中打开文件。

使用 JavaScript 进行 AES 加密

高级加密标准 (AES) 是美国国家标准与技术研究院(NIST)于 2001 年建立的电子数据加密规范。此加密算法用于美国政府机构保护敏感但未分类的材料。AES 已被企业广泛采用,并已成为许多商业敏感交易的事实上的加密标准。

本食谱讨论了如何在 JavaScript 中实现 AES 并将其集成到您的 Titanium 企业应用程序中。

准备工作

此食谱使用 Ti.SlowAES CommonJS 模块作为 SlowAES 开源项目的包装器。此模块和其他代码资产可以从本书提供的源代码中下载,或者通过此食谱末尾的 另请参阅 部分的链接单独下载。将这些内容安装到您的项目中很简单。只需将 SlowAES 文件夹复制到您的项目中的 Resources 文件夹,如下面的截图所示:

准备就绪

如何操作...

一旦将 SlowAES 文件夹添加到您的项目中,接下来您需要在 app.js 文件中创建您的应用程序命名空间,并使用 require 将模块导入到您的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  mod : require('SlowAES/Ti.SlowAES')
};

创建食谱 UI

本食谱通过一个示例应用程序展示了如何使用 Ti.SlowAES CommonJS 模块,该应用程序使用两个 Ti.UI.TextField 控件进行输入。

  1. 首先,创建一个 Ti.UI.Window 来附加所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'AES Crypto Example',
      barColor:'#000',layout:'vertical',fullscreen:false
    });
    

    创建食谱 UI

  2. 接着,在 Ti.UI.Window 中添加了一个 Ti.UI.TextField 控件,用于收集用户的密钥。

    var txtSecret = Ti.UI.createTextField({
      value:'DoNotTell',hintText:'Enter Secret',
      height:45, left:5, right:5,
      borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED
    });
    win.add(txtSecret);
    
  3. 另外添加了一个 Ti.UI.TextFieldTi.UI.Window 中,用于从用户那里收集要加密的字符串。

    var txtToEncrypt = Ti.UI.createTextField({
      value:'some information we want to encrypt',
      hintText:'Enter information to encrypt',
      height:45, left:5, right:5,
      borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED
    });
    win.add(txtToEncrypt);
    
  4. 接下来,在 Ti.UI.Window 中添加了一个 Ti.UI.Label。这个 Ti.UI.Label 将用于向用户显示加密值。

    var encryptedLabel = Ti.UI.createLabel({
      top:10, height:65, left:5, right:5,color:'#000',
      textAlign:'left',font:{fontSize:14}
    });
    win.add(encryptedLabel);
    
  5. 最后,在 Ti.UI.Window 中添加了一个 Ti.UI.Button。这个 Ti.UI.Button 将在后面的菜谱中用于执行加密测试。

    var btnEncrypt = Ti.UI.createButton({
      title:'Run Encryption Test', top:25, 
      height:45, left:5, right:5
    });
    win.add(btnEncrypt);
    

加密和解密值

本节演示了如何使用 Ti.SlowAES 模块,通过在 txtSecretTi.UI.TextField 中输入的密钥来加密 txtToEncryptTi.UI.TextField 中的内容。一旦完成,加密值随后被解密并与原始输入进行比较。结果以警报消息的形式显示给用户,如下述截图所示:

加密和解密值

btnEncrypt 控件的点击事件触发时执行加密测试,如下述代码片段所示:

btnEncrypt.addEventListener('click',function(x){
  1. 加密过程的第一步是创建 SlowAES 模块的新实例,如以下代码片段所示。

      var crypto = new my.mod();
    
  2. 接下来,使用加密函数,将 txtSecret 控件中提供的密钥用于加密 txtToEncrypt 控件中的值。加密结果随后返回到 encryptedValue,如下述语句所示:

      var encryptedValue = 
      crypto.encrypt(txtToEncrypt.value,txtSecret.value);
    
  3. 然后,将 encryptedLabel.text 属性更新以向用户显示加密值。

      encryptedLabel.text = 'Encrypted:' + encryptedValue;
    
  4. 接着,使用 decrypt 方法演示如何解密之前加密的字符串值。此方法需要加密的字符串值和密钥,如下述代码片段所示:

      var decryptedValue = 
      crypto.decrypt(encryptedValue,txtSecret.value);
    
  5. 最后,将原始输入值与解密值进行比较,以确保我们的加密测试成功。测试结果随后通过消息警报和 Titanium Studio 控制台显示给用户。

      alert((txtToEncrypt.value ===decryptedValue) ? 
      'Encryption Test successfully ran check console for details.': 
      'Test failed, please check console for details.');
    
    });
    

参见

您还可以参考以下资源:

使用 Ti.Network.HTTPClient 进行基本身份验证

大多数企业网站或服务都通过基本访问认证提供内容访问。基本认证是一种 HTTP 用户代理在请求时提供用户名和密码的方法。重要的是要记住,基本认证将您的凭据 base64 编码,而不是加密它们。因此,建议在创建网络连接时使用 HTTPS。

Titanium 通过 SDK 的 Ti.Network 功能完全支持基本认证。本配方描述了如何使用 Ti.Network.HTTPClient 代理通过基本认证头连接到网站。

准备就绪

此配方使用 Ti.BA CommonJS 模块作为 Titanium 的原生 Ti.Network.HTTPClient 的辅助工具。此模块和其他代码资源可以从本书提供的源代码中下载。在您的项目中安装此模块很简单。只需将 Ti.BA.js 文件复制到项目中的 Resources 文件夹,如图所示:

准备就绪

如何操作...

一旦将 Ti.BA.js 文件添加到您的项目中,您需要在 app.js 文件中创建您的应用程序命名空间,并使用 require 将模块导入到您的代码中,如下代码片段所示:

//Create our application namespace
var my = {
  mod : require('Ti.BA')
};

创建配方 UI

此配方使用简单的用户界面来展示如何建立基本的认证网络连接。本节中的代码片段说明了如何构建以下截图所示的基本认证测试应用程序:

创建配方 UI

现在执行以下步骤:

  1. 首先,创建一个 Ti.UI.Window 来附加所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'AES Crypto Example', 
      barColor:'#000',layout:'vertical',fullscreen:false
    });
    
  2. 接下来,将 txtUsername Ti.UI.TextField 添加到 Ti.UI.Window 中。默认值设置为 test,因为它是用于创建我们的模拟测试连接的 browserspy.dk 测试网站的默认值。

    var txtUsername = Ti.UI.createTextField({
      value:'test',hintText:'Enter username',
      height:45, left:5, right:5,
      borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED
    });
    win.add(txtUsername);
    
  3. 然后将 txtPassword Ti.UI.TextField 添加到 Ti.UI.Window 中。默认值设置为 test,因为它是用于创建我们的模拟测试连接的 browserspy.dk 测试网站的默认值。

    var txtPassword = Ti.UI.createTextField({
      value:'test',hintText:'Enter password',
      passwordMask:true,height:45, left:5, right:5,
      borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED
    });
    win.add(txtPassword);
    
  4. 接下来,将 txtUrl Ti.UI.TextField 添加到 Ti.UI.Window 中。默认值设置为 "http://browserspy.dk/password-ok"txtUrl 的值可以更新为任何需要基本认证的服务。为了演示目的,此配方使用 browserspy.dk 测试网站。

    var txtUrl = Ti.UI.createTextField({
      value:'http://browserspy.dk/password-ok.php',
      hintText:'Enter Url',
      height:45, left:5, right:5,
      borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED
    });
    win.add(txtUrl);
    
  5. 最后,将 btnConnect Ti.UI.Button 添加到 Ti.UI.Window 中。在本配方后面的部分,将使用 btnConnect 控件来初始化认证网络连接。

    var btnConnect = Ti.UI.createButton({
      title:'Connect', top:25, height:45, left:5, right:5
    });
    win.add(btnConnect);
    

创建服务连接

在放置好示例 UI 之后,本配方下一步是执行使用基本认证的安全网络连接。以下代码片段用于创建网络连接并显示以下截图所示的结果:

创建服务连接

btnConnect 控件的 click 事件触发时,执行基本认证测试网络连接,如下面的代码片段所示:

btnConnect.addEventListener('click',function(x){
  1. 首先,检查 Ti.Network.online 属性以确保设备有网络连接。如果没有网络连接,则提醒用户并退出函数。

      if(!Ti.Network.online){
        alert('Network connection is required');
        return;
      }
    
  2. 接下来创建 onDone 函数。此函数用作认证网络连接结果的回调。

      function onDone(d){
    
  3. 当在 onDone 函数中时,首先执行的操作是检查 d.success 属性提供的结果。如果此属性为 true,则网络连接成功完成。否则,将 d.error 消息打印到控制台,并通知用户。

        if(d.success){
          Ti.API.info('Results = ' + 
          JSON.stringify(d.results));
          alert('Authenticated successfully');
        }else{
          Ti.API.info('error = ' + JSON.stringify(d.error));
          alert('Authenticated Failed');
        }
      };
    
  4. 然后创建 credentials 对象。此对象包含在创建我们的认证连接时使用的用户名和密码。请注意,这些值应与从屏幕上的 Ti.UI.TextField 控件直接获取的值一样为纯文本。

      var credentials = {
        username:txtUsername.value,
        password:txtPassword.value
      };
    
  5. Ti.BA 模块 允许您配置所有 Ti.Network.HTTPClient 选项,并控制和服务输出格式。以下代码片段演示了如何配置请求以将超时设置为 9 秒,并指定 responseText 为返回的响应。

      var options = {format:'text', timeout:9000};
    
  6. 最后创建 Ti.BA 模块的新实例,并提供以下内容:

    • 方法:执行 GETPOST 操作。在本示例中,提供了 POST 方法。

    • URL:模块用于连接的 URL。

    • 凭据credentials 对象包含用于创建基本认证连接的用户名和密码。

    • 回调:此参数需要提供一个回调方法。在本示例中,将 onDone 方法提供给模块,并将返回连接响应。

    • 选项:这是 Ti.Network.HTTPClient 和要返回的结果类型的配置选项。如果没有提供,则返回模块的默认值。

        var basicAuth = new 
        my.mod('POST',txtUrl.value,credentials, onDone,options);
      });
      

实现跨平台通行码屏幕

密码和通行码屏幕是企业应用中常见的验证工具。这些屏幕提供交互模式,有助于解决认证和确认场景。

此配方演示了如何实现类似于在 iOS 和 Android 网络屏幕上看到的跨平台通行码屏幕。

准备工作

此配方使用 Ti.Passcode CommonJS 模块来实现跨平台通行码屏幕。此模块和其他代码资产可以从本书提供的源代码下载,或者通过此配方末尾的 也见 部分的链接单独下载。在项目中安装此模块很简单。只需将 Ti.Passcode.js 文件复制到项目中的 Resources 文件夹,如下面的截图所示:

准备工作

如何操作...

Ti.Passcode.js文件添加到您的项目后,您需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到您的代码中,如下面的代码所示:

//Create our application namespace
var my = {
  mod : require('Ti.Passcode')
};

创建食谱 UI

本食谱使用Ti.UI.Windows向用户展示信息。本节讨论了创建第一个Ti.UI.Window的代码。本节详细介绍了如何启动Ti.Passcode模块,并提供了验证它的代码。

创建食谱 UI

现在执行以下步骤:

  1. 首先,创建一个Ti.UI.Window来附加所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'Passcode Example', 
      layout:'vertical',fullscreen:false, exitOnClose:true
    });
    
  2. 接下来,将txtPasscode Ti.UI.TextField添加到Ti.UI.Window中。此控件用于收集Ti.Passcode模块将验证的密码。

    var txtPasscode = Ti.UI.createTextField({
      value:'1234',hintText:'Enter Passcode',
      height:45, left:5, right:5,
      borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED
    });
    win.add(txtPasscode);
    
  3. 最后,将bntRunPasscode Ti.UI.Button添加到Ti.UI.Window中。此按钮将在食谱中稍后用于启动密码屏幕。

    var btnRunPasscode = Ti.UI.createButton({
      title:'Open Passcode Screen', top:25, height:45, 
      left:5, right:5	
    });
    win.add(btnRunPasscode);
    

启动密码屏幕

本食谱中的第二个Ti.UI.Window是由Ti.Passcode模块创建的。此Ti.UI.Window负责展示和验证应用程序密码。本节描述了如何使用此模块的显示元素配置、创建和确认您的应用程序密码,以下截图显示了该模块:

启动密码屏幕

当用户点击btnRunPasscode Ti.UI.Button并触发click事件时,会启动密码验证屏幕。以下代码片段讨论了在click事件触发后执行的操作:

btnRunPasscode.addEventListener('click',function(x){
  1. btnRunPasscode按钮的click事件中的第一个代码块是创建我们的配置和其他变量。options对象包含配置Ti.Passcode窗口所需的所有设置。

      var codeWindow  = null, 
      options = {
    
  2. window配置元素允许使用所有标准的Ti.UI.Window属性。以下片段展示了如何在密码验证窗口上设置backgroudnColornavBarHidden属性。

        window:{
          backgroundColor:'#ccc',
          navBarHidden:true
        },
    
  3. view配置元素允许配置Ti.Passcode窗口中显示的大多数组件。以下片段演示了如何设置passcode标题属性和显示错误信息的代码。

        view:{
          title:'Enter application passcode',
          errorColor:'yellow'
        }
      };
    

    提示

    请查阅Ti.Passcode模块以获取完整的配置属性列表。

  4. 接下来,定义onCompleted回调函数。此函数将用作提供给Ti.Passcode模块的回调方法。密码验证结果将作为字典提供给d输入参数。

      function onCompleted(d){
    
  5. d参数是一个包含验证结果的对象。d.success属性提供了一个指示器,表示输入的密码是否与启动时提供的密码匹配。以下代码片段会在用户输入正确的密码或需要再次尝试过程时提醒用户。

        if(d.success){
          alert('Passcode entered is correct');
          codeWindow.close();
        }else{
          Alert('Invalid passcode, please try again');
        }
      };
    
  6. 本食谱这一部分的下一步是创建一个新的密码模块实例。以下代码片段展示了如何完成此操作:

      var passcode = new my.mod();
    
  7. 然后在新的密码实例上调用 createWindow 方法。此方法提供了从 txtPasscode 控制器验证的密码,并提供了之前创建的回调和选项对象。然后此方法返回一个 Ti.UI.Window。一旦输入密码,回调方法 onCompleted 将发送验证结果。

      var codeWindow =
      passcode.createWindow(txtPasscode.value,
      onCompleted,options);
    
  8. createWindow 方法返回的 Ti.UI.Window 然后以模态标志打开。密码 Ti.UI.Window 将保持打开状态,直到被前面讨论的 onCompleted 回调方法关闭。

      codeWindow.open({modal:true});
    });
    

以下截图说明了用户成功输入其密码后,代码的这一部分在设备上的外观:

启动密码屏幕

参见

在 iOS 上处理受保护的 ZIP 文件

受保护的 ZIP 文件是交换、存储和传输企业数据的一种常见方式。ZIP 文件通常用于将多个文件捆绑在一起以进行传输或存储。作为额外的一层安全措施,所有此类文件都应该始终使用强密码进行保护。

以下食谱讨论了如何在 iOS 上创建和解压受保护的压缩文件。

准备工作

本食谱使用 bencoding.zip 本地模块来处理受保护的 ZIP 文件。此模块和其他代码资源可以从本书提供的源代码下载,或者通过本食谱末尾的 参见 部分的链接单独下载。在您的项目中安装此模块很简单。只需将 modules 文件夹复制到项目的根目录,然后将 data 文件夹复制到项目的 Resources 目录中,如图下所示:

准备工作

在复制提到的文件夹后,您需要在 Titanium Studio 中点击您的 tiapp.xml 文件,并添加对 bencoding.zip 模块的引用,如图下所示:

准备工作

如何操作...

一旦您将 modulesdata 文件夹添加到您的项目中,您需要在 app.js 文件中创建您的应用程序命名空间,并使用 require 将模块导入到您的代码中,如下代码片段所示:

//Create our application namespace
var my = {
  zip : require('bencoding.zip')
};

创建食谱 UI

本食谱使用简单的 UI 来说明如何创建(压缩)和解压受保护的 ZIP 文件。本节讨论的代码展示了如何构建如图下所示的食谱 UI:

创建食谱 UI

以下步骤概述了如何创建食谱的 UI:

  1. 首先,创建一个 Ti.UI.Window 以附加所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'Protected Zip Example', 
      barColor:'#000',layout:'vertical'
    });
    
  2. 然后添加 txtPassword Ti.UI.TextField 控件。此控件将用于提供创建受保护 ZIP 文件或解压它们的密码。默认设置为 foo123。这也是包含的示例文件的密码。

    var txtPassword = Ti.UI.createTextField({
      value:'foo123',hintText:'Enter Password',
      height:45, left:5, right:5, passwordMask:true,
      borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED
    });
    win.add(txtPassword);
    
  3. 然后将 btnZip Ti.UI.Button 添加到 Ti.UI.Window。此按钮将用于在讨论本食谱时演示如何创建受保护的 ZIP 文件。

    var btnZip = Ti.UI.createButton({
      title:'Zip with Password', 
      top:25, height:45, left:5, right:5
    });
    win.add(btnZip);
    
  4. 接下来将 btnUnzip Ti.UI.Button 添加到 Ti.UI.Window。此按钮将用于在讨论本食谱时演示如何解压受保护的 ZIP 文件。

    var btnUnzip = Ti.UI.createButton({
      title:'Unzip with Password', top:25, height:45, 
      left:5, right:5	
    });
    win.add(btnUnzip);
    

创建密码保护的 ZIP 文件

本食谱的这一部分演示了如何创建受保护的 ZIP 文件。以下截图显示了本食谱的实际操作,创建受保护的 ZIP 文件并提示用户输出文件路径:

创建密码保护的 ZIP 文件

当用户点击 btnZip Ti.UI.Button 并触发 click 事件时,执行创建的 ZIP 文件。以下代码片段讨论了在 click 事件触发后执行的操作:

btnZip.addEventListener('click',function(x){
  1. 压缩过程的第一步是创建 onZipCompleted 回调方法。当压缩过程完成时,模块将结果发送到这个回调方法:

      function onZipCompleted(d){
    
  2. d 方法参数提供了模块的结果。处理模块结果的第一步是检查 d.success 属性,以查看压缩过程是否成功。如果是这样,用户会收到完成 ZIP 文件的路径提示。否则,用户会收到创建文件时生成的错误提示。

        if(d.success){
          alert('Protected zip created at: ' + d.zip);
        }else{
          alert('failed due to: ' + d.message);
        }
      };
    
  3. 接下来创建 writeToZipinputDirectory 变量。writeToZip 变量包含应用 Documents 目录中 taxforms.ZIP 输出文件的路径。inputDirectory 创建了对在食谱的 准备就绪 部分创建的 Resources/data 的引用。

      var writeToZip = Ti.Filesystem.applicationDataDirectory + 
      '/taxforms.zip';
      var inputDirectory = Ti.Filesystem.resourcesDirectory + 
      'data/';
    
  4. 最后调用 zip 方法,并在此方法中提供在 click 事件处理程序中构建的参数。一旦完成,zip 方法将结果提供给提供的 onZipCompleted 回调方法。

      my.zip.zip({
        zip: writeToZip, 
        password:txtPassword.value,
        files: [inputDirectory + 'w2.pdf',
        inputDirectory + 'w4.pdf'],
        completed:onZipCompleted
      });
    });
    

    小贴士

    files 参数使用 inputDirectory 变量提供了一个文件数组。在本例中,包含的文件是 Resources/data 文件夹中的 w2.pdfw4.pdf 文件,这些文件是在食谱的 准备就绪 部分创建的。

解压受保护的 ZIP 文件

本食谱的这一部分演示了如何解压受保护的 ZIP 文件。以下截图显示了本食谱的实际操作,将受保护文件的文件内容解压到应用的 Documents 目录中:

解压受保护的 ZIP 文件

当用户点击 btnUnzip Ti.UI.Button 并触发 click 事件时,执行解压示例。以下代码片段讨论了在 click 事件触发后执行的操作。

btnUnzip.addEventListener('click',function(x){
  1. 解压过程的第一步是创建 onUnzipCompleted 回调方法。当解压过程完成后,模块会将结果发送到这个回调方法。

      function onUnzipCompleted(e){
    
  2. d 方法参数提供了模块的结果。处理模块结果的第一步是检查 d.success 属性,以查看解压过程是否成功。如果是这样,用户会收到解压文件目录路径的提示。否则,用户会收到生成文件的错误提示。

        if(e.success){
          alert('Unzipped to ' + e.outputDirectory);
        }else{
          alert('failed due to: ' + e.message);
        }
      };
    
  3. 接下来创建 outputDirectoryzipFileName 变量。outputDirectory 变量包含应用 Documents 目录中输出目录的路径。zipFileName 变量创建了对在食谱的 准备就绪 部分创建的 Resources/data/taxform.zip 文件的引用。

      var outputDirectory = 
      Ti.Filesystem.applicationDataDirectory;
      var zipFileName = Ti.Filesystem.resourcesDirectory + 
      'data/taxforms.zip';
    
  4. 最后,调用 unzip 方法,并在此方法中提供在 click 事件处理程序中先前构建的参数。一旦完成,unzip 方法将结果提供给提供的 onUnzipCompleted 回调方法。

      my.zip.unzip({
        outputDirectory:outputDirectory, 
        zip:zipFileName, 
        overwrite:true,
        completed:onUnzipCompleted
      });
    });
    

    注意

    zipFileName ZIP 文件中包含的所有文件都将解压到 outputDirectory 参数提供的目录根目录中。

参见

第九章:使用加密和其他技术进行应用程序安全

在本章中,我们将介绍:

  • 使用安全属性

  • 对象和字符串加密

  • 处理加密文件

  • 在 iOS 上处理受保护的 PDF 文件

  • Android 锁屏监控

简介

企业应用程序通常包含私有或机密信息。因此,在开发企业级 Titanium 应用程序时,加密、文件锁定和安全应用程序生命周期管理是基本要求。核心 Titanium SDK 在此领域提供了有限的功能,例如单向哈希和基本应用程序事件,但要完全满足安全要求,需要第三方模块,如 Securely

在本章中,我们将讨论如何使用 Securely 框架来处理常见的安全编程任务,例如文件和字符串加密。Securely 以跨平台和 Titanium 友好的方式提供对每个平台的安全 API 的访问。通过一系列食谱,我们将演示如何在现有的 Titanium 企业应用程序中利用 Securely 框架。

使用安全属性

Titanium SDK 提供了一个 Ti.App.Properties 对象,它提供了一种方便的方式来持久化用户和应用程序信息。Securely 框架提供了一个熟悉的 API,旨在镜像 Ti.App.Properties,允许您以安全的方式持久化这些信息。本食谱描述了如何使用 Securely.Properties 对象以加密和安全的方式存储、读取和删除数据。

准备工作

本食谱使用 Securely 原生模块。此模块和其他代码资源可以从本书提供的源代码中下载,或者通过本食谱末尾的 另请参阅 部分的链接单独下载。将这些安装到您的项目中很简单。只需将 modules 文件夹复制到您的项目中,如下截图所示:

准备工作

添加模块引用

在复制提到的文件夹后,您需要点击 Titanium Studio 中的 tiapp.xml 文件,并添加对 bencoding.securely 模块的引用,如下截图所示:

添加模块引用

如何操作...

本食谱旨在在 Ti.UI.Window 或单个 Titanium 上下文中的其他组件的上下文中运行。本节中的代码示例演示了如何使用 Securely 的安全属性,使用与 Appcelerator 为 Titanium SDK 的 Ti.App.Properties 类使用的相同测试。有关更多信息,请参阅食谱源代码中包含的 app.js

创建命名空间

在您将 Securely 模块添加到您的项目后,您需要在 app.js 文件中创建您的应用程序命名空间,并使用 require 将模块导入到您的代码中,如下代码片段所示:

//Create our application namespace
var my = {
  secure : require('bencoding.securely'),
  isAndroid : Ti.Platform.osname === 'android',
  testObjects:{
    testList : [
      {name:'Name 1', address:'1 Main St'},
      {name:'Name 2', address:'2 Main St'}	
    ]
  }
};

创建安全属性对象

在创建应用命名空间之后,下一步是创建一个新的属性对象。此对象包含以下属性值,这些值必须在创建时设置:

  • secret:这是一个必需的参数。secret 是用于加密和解密所有属性值的密码。在加密过程中使用的相同 secret 必须在解密过程中使用,否则将返回 null 值。

  • identifier:此参数是可选的。如果没有提供值,iOS 上的捆绑名称或 Android 上的 PackageName 将被使用。identifier 允许您在需要时使用标识符对每个属性进行分段。

  • accessGroup:此参数是仅在 iOS 平台上使用的可选值。访问组可用于在两个或多个应用程序之间共享密钥链项。如果没有提供访问组,密钥链值将仅限于保存值的该应用内部访问。

  • encryptFieldNames:此参数是仅在 Android 平台上使用的可选值。当设置为 true 时,Securely 将使用提供的 secret 对所有属性名称创建 MD5 哈希。

    var properties = my.secure.createProperties({
      secret:"sshh_dont_tell",
      identifier:"myPropertyIdentifier",
      accessGroup:"myAccessGroup",
      encryptFieldNames:false
    });
    

结果比较辅助工具

以下代码片段中显示的 displayResults 函数用于将测试结果与预期值进行比较。根据比较结果,生成适当的消息以呈现给用户。

function displayResults(result, expected) {
  if (result instanceof Array) {
    return displayResults(JSON.stringify(result), 
    JSON.stringify(expected));
  }else{
    if (result === expected) {
      return "Success ("+result+"=="+expected+")";
    } else {
      return "Fail: " + result + "!=" + expected;
    }
  }
};

读取不带默认值的安全属性

每种支持的属性类型都有一个 get 方法。例如,要读取布尔属性,可以调用 getBool 方法并提供一个名称。此 API 与 Titanium SDK 中的 Ti.App.Properties 对象类似,增加了读取和解密安全属性的支持。如果没有存储值可用,将提供 null 或默认值类型。以下代码片段演示了如何读取已保存的安全属性值:

Ti.API.info('Bool: ' + displayResults(properties.getBool('whatever'),
(my.isAndroid ? false : null)));

Ti.API.info('Double: ' + 
displayResults(properties.getDouble('whatever'),
(my.isAndroid ? 0: null)));

Ti.API.info('int: ' + displayResults(properties.getInt('whatever'),
(my.isAndroid ? 0 : null)));

Ti.API.info('String: ' + 
displayResults(properties.getString('whatever'),null));

Ti.API.debug('StringList: ' + 
displayResults(properties.getList('whatever'),null));

注意

在 iOS 上,任何没有保存值的属性都将返回 null 值。由于 Android 的类型系统,如果没有存储值,布尔值将返回 false,数值将返回零。所有其他 Android 值将返回 null,类似于 iOS。

读取带默认值的安全属性

同样,与 Ti.App.Properties 类似,Securely 提供了读取和解密安全属性的能力,并在请求的安全属性没有存储值时提供默认值。

Ti.API.info('Bool: ' + 
displayResults(properties.getBool('whatever',true),true));

Ti.API.info('Double: ' + 
displayResults(properties.getDouble('whatever',2.5),2.5));

Ti.API.info('int: ' + 
displayResults(properties.getInt('whatever',1),1));

Ti.API.info('String: ' + 
displayResults(properties.getString('whatever',"Fred"),"Fred"));

Ti.API.debug('StringList: ' + 
displayResults(properties.getList('whatever'),testList));

注意

默认值在读取过程中不会被加密或持久化。

设置安全属性值

每种支持的属性类型都有一个 set 方法,用于加密和持久化值。例如,要保存和加密布尔属性,可以使用 setBool 方法,并提供属性名称和布尔值。此 API 与 Titanium SDK 中的 Ti.App.Properties 对象类似。Securely 支持直接加密并将值写入安全属性。以下代码片段演示了如何将以下值保存到安全和加密的存储中:

properties.setString('MyString','I am a String Value ');
properties.setInt('MyInt',10);
properties.setBool('MyBool',true);
properties.setDouble('MyDouble',10.6);
properties.setList('MyList',my.testObjects.testList);

为了演示属性是否正确保存,对每个文件调用get方法,并将结果打印到 Titanium Studio 控制台。

Ti.API.info('MyString: '+ properties.getString('MyString'));
Ti.API.info('MyInt: '+ properties.getString('MyInt'));
Ti.API.info('MyBool: '+ properties.getString('MyBool'));
Ti.API.info('MyDouble: '+ properties.getString('MyDouble'));
var list = properties.getList('MyList');
Ti.API.info('List: ' + JSON.stringify(list));

列出安全属性字段名称

可以通过在Securely属性对象上调用listProperties方法来返回属性名称数组。以下代码片段演示了如何使用此方法在 Titanium Studio 控制台中打印names数组的 JSON 表示形式。

if(!properties.hasFieldsEncrypted()){
  var allProperties = properties.listProperties();
  Ti.API.info(JSON.stringify(allProperties));
}

注意

如果启用了字段名称加密,则listProperties方法将返回null。因为字段名称使用单向哈希加密,原始名称不再可用。

删除安全属性

您可以使用Securely属性对象的两种方法来删除属性。removePropertyremoveAllProperties旨在易于使用,并且与 Titanium SDK 的Ti.App.Properties对象上具有相同名称的方法类似。

removeProperty方法将删除提供的存在的安全属性名称。

properties.removeProperty('MyInt');

removeAllProperties方法将删除在创建属性对象时提供的标识符内的所有属性。

properties.removeAllProperties();

注意

在我们的食谱中,remove属性函数在测试结束时被调用,以便每次都能可靠地重现结果。

检查是否存在安全属性

Securely属性对象提供了hasProperty方法,以提供检查安全属性是否存在的能力。如果属性存在,则返回布尔值true,否则提供结果false。此 API 旨在易于使用,因为它与核心 Titanium SDK 中的Ti.App.Properties.hasProperty函数类似。

Ti.API.info("Does MyInt property exist? " + 
properties.hasProperty('MyInt'));

它是如何工作的...

安全属性的基础设施实现方式取决于运行应用程序的平台。尽管Securely提供了一个跨平台 API,但了解每个平台是如何实现及其相关的安全考虑是很重要的。

iOS 上的安全属性

Securely框架将所有属性值作为序列化字符串保存在 iOS Keychain 中。这提供了安全存储,并且由于它是 iOS 的一部分,因此不需要任何依赖项。由于Securely使用 iOS 密钥链服务,建议在将敏感数据存储在安全管理的容器中之前,您的组织审查 Apple 的密钥链文档。

注意

由于使用了 iOS Keychain 服务,因此需要记住,在您的应用程序卸载后,您的安全属性值仍然会在 iOS Keychain 中可用。如果您希望在删除应用程序之前删除所有密钥链项,则必须调用removeAllProperties方法。

Android 上的安全属性

Securely 框架将所有属性值以序列化和 AES 加密字符串的形式保存到 Android 的 SharedPreferences 中。尽管 Android 在 API 级别 14 中引入了原生密钥链支持,但 Securely 模块旨在适应更多设备,并针对 API 级别 8 和更高版本。建议您的企业审查 Securely 中的安全属性实现,以确保其符合您的企业或行业标准与要求。

安全属性考虑事项

默认情况下,在 Android 上,属性名称不会被加密。这可以通过在创建 properties 对象时设置 encryptFieldNames 来启用。由于需要加密所有属性名称,此属性只能在创建新的 properties 对象时设置。当启用字段名称加密时,Securely 将使用提供的 secret 属性值对每个字段名称创建一个 SHA-1 哈希。启用此功能会带来性能考虑。在实现现有的 Titanium 应用程序之前,建议进行回归和性能测试。

参见

对象和字符串加密

在企业级 Titanium 开发周期中,通常需要加密进程中的或持久化的 JavaScript 对象或变量。Securely 框架提供了一个 StringCrypto 代理,它提供了 AES 和 DES 双向加密的便捷方法。

本食谱描述了如何使用 Securely.StringCrypto 对象以安全的方式加密和解密 JavaScript 对象。

注意

Securely 的 AES 和 DES 实现旨在针对特定设备平台。如果需要交换 AES 或 DES 加密数据,访问设备、平台或第三方服务,建议进行测试以验证实现是否匹配。

准备工作

本食谱使用 Securely 原生模块。此模块和其他代码资源可以从本书提供的源代码中下载。将这些安装到您的项目中很简单。只需将 modules 文件夹复制到 Titanium 项目的根目录中。请参阅 Using secure properties 食谱中的 准备工作 部分,了解在继续之前如何设置模块的说明。

如何操作...

此示例设计为在 Ti.UI.Window 或单个 Titanium 上下文中的其他组件的上下文中运行。本节演示了如何使用 SecurelyStringCrypto 方法加密 JavaScript 对象。有关更多信息,请参阅食谱源代码中包含的 app.js

创建命名空间

一旦将 Securely 模块添加到您的项目中,您需要在 app.js 文件中创建您的应用程序命名空间,并使用 require 将模块导入到您的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  secure : require('bencoding.securely'),
  isAndroid : Ti.Platform.osname === 'android'
};

生成密钥

密钥生成是密码学的一个重要部分,为了帮助这个过程,Securely 有两个内置的便捷方法。

生成派生密钥

第一种密钥生成便捷方法是称为 generateDerivedKey。该方法包括用户提供的字符串输入到用于确定密钥的盐算法中。如果需要知道或由另一个访问系统导出种子值,这种方法是有帮助的。以下步骤演示了为 generateDerivedKey 方法提供种子值的两种常见方法:

  • 一种常见的方法是从新的 GUID 创建种子,从中生成密钥。以下代码片段演示了使用 Ti.Platform.createUUID 方法生成 GUID 来实现这一方法:

    var usingGUID = 
    my.secure.generateDerivedKey(Ti.Platform.createUUID());
    

    注意

    generateDrivedKey 方法将使用提供的参数在每次调用时创建一个新的密钥。

  • 另一种不那么随机的密钥生成方法是提供 Titanium 应用程序 GUID 作为种子值。以下代码片段演示了如何使用 Ti.App.guid 从项目的 tiapp.xml 文件中的 GUID 创建派生密钥。

    var usingAppID = my.secure.generateDerivedKey(Ti.App.guid);
    

生成随机密钥

第二种密钥生成便捷方法称为 generateRandomKey。正如其名称所示,生成一个随机的字母数字字符串并将其用作种子值。以下代码片段演示了如何使用 generateRandomKey 方法创建密钥值。

var randomKey = my.secure.generateRandomKey();

创建 stringCrypto 对象

字符串和对象的加密过程中的下一步是创建一个新的 stringCrypto 代理实例。以下代码片段显示了如何创建一个名为 stringCrypto 的新代理对象。

var stringCrypto = my.secure.createStringCrypto();

使用 DES 加密

Securely 支持较旧的 数据加密标准 (DES) 加密算法。对这种算法的支持主要是为了与旧系统进行交互。 wherever possible, the stronger AES encryption should be used instead.

使用 DES 加密

DESEncrypt 方法需要一个密钥和一个要加密的字符串。然后该方法将返回一个 DES 加密字符串。如果在加密过程中生成错误,则返回 null 值。以下演示了如何使用此方法加密 JavaScript 字符串和对象。

var desEncryptedString = stringCrypto.DESEncrypt
(usingGUID,
plainTextString);
var desEncryptedObject = stringCrypto.DESEncrypt
(usingGUID,
JSON.stringify(plainObject));

任何非 JavaScript 字符串元素必须在提供给 DESEncrypt 函数之前首先转换为 JavaScript 字符串。

使用 DES 解密

DESDecrypt 方法用于解密使用 DES 算法加密的字符串。此方法需要一个密钥和一个包含加密值的字符串。DESDecrypt 方法将返回一个包含解密值的字符串。以下代码片段演示了如何使用 DESDecrypt 方法解密字符串和对象。

var desDecryptedString = stringCrypto.DESDecrypt
(usingGUID,
desEncryptedString);

以下代码片段演示了如何使用 JSON.parse 从解密后的 JSON 字符串中重建 JavaScript 对象。

var desDecryptedObject = JSON.parse
(stringCrypto.DESDecrypt(usingGUID,
desEncryptedObject));

使用 AES 加密

AESEncrypt 方法需要一个密钥和一个字符串来进行加密。此方法将返回一个 AES 加密的字符串。如果在加密过程中生成错误,则返回一个 null 值。以下代码片段演示了如何使用此方法加密 JavaScript 字符串和对象。

var aesEncryptedString = stringCrypto.AESEncrypt
(usingGUID,
plainTextString);
var aesEncryptedObject = stringCrypto.AESEncrypt
(usingGUID,
JSON.stringify(plainObject));

注意

任何非 JavaScript 字符串元素必须在提供给 AESEncrypt 函数之前首先转换为 JavaScript 字符串。

使用 AES 解密

AESDecrypt 方法用于解密使用 AES 算法加密的字符串。此方法需要一个密钥和一个包含加密值的字符串。AESDecrypt 方法将返回一个包含解密值的字符串。以下代码片段演示了如何使用 AESDecrypt 方法解密字符串和对象。

var aesDecryptedString = stringCrypto.AESDecrypt
(usingGUID,
aesEncryptedString);

以下代码片段演示了如何使用 JSON.parse 从解密后的 JSON 字符串中重建 JavaScript 对象。

var aesDecryptedObject = JSON.parse
(stringCrypto.AESDecrypt(usingGUID,
aesEncryptedObject));

钛金属对象加密

钛金属 SDK 对象,如 Ti.UI.View,不是真正的 JavaScript 对象,因此无法有效地序列化和加密。要加密钛金属对象,您必须首先将钛金属对象的全部属性复制到一个纯 JavaScript 对象中,然后将 JavaScript 对象转换为 JSON 字符串,如前所述。在解密过程中,可以采取相反的方法来重新创建钛金属对象。

参见

处理加密文件

文件加密是企业移动开发的基本构建块。由于大多数企业应用收集的数据敏感性,建议所有持久化文件都进行加密。

本食谱演示了如何使用Securely框架对文件进行加密和解密。通过使用文件加密示例,我们将提供逐步说明,说明如何在 Titanium 应用程序内部处理本地加密文件。

准备就绪

本食谱使用Securely原生模块。此模块和其他代码资源可以从书中提供的源代码中下载。只需将modules文件夹复制到您的 Titanium 项目根目录。在继续之前,请查阅使用安全属性食谱中的准备就绪部分,了解模块设置说明。

安装Securely模块后,您需要将PlainText.txt文件复制到项目的Resources文件夹中。此文件将由食谱用于创建初始加密文件。

如何做...

一旦将module文件夹和PlaintText.txt示例文件添加到您的项目中,您需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到您的代码中,如下代码片段所示:

//Create our application namespace
var my = {
  secure : require('bencoding.securely'),
  isAndroid : Ti.Platform.osname === 'android'
};

创建 UI

本食谱通过Securely模块和 Titanium 的Ti.Filesystem命名空间,逐步说明如何加密和解密文件。以下截图中的测试框架用于演示如何执行这些加密操作:

创建 UI

现在执行以下步骤:

  1. 创建测试框架的第一步是创建一个Ti.UI.Window,它用于附加所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'File Crypto Example', 
      barColor:'#000',layout:'vertical',fullscreen:false
    });
    
  2. 创建测试框架 UI 的下一步是添加一个名为txtPasswordTi.UI.TextField。此控件用于获取加密和解密操作中使用的密码。

    var txtPassword = Ti.UI.createTextField({
      value:'foo123',hintText:'Enter Password',
      height:45, left:5, right:5, passwordMask:true
    });
    win.add(txtPassword);
    
  3. 创建测试框架 UI 的下一步是添加一个名为btnEncryptTi.UI.Button。此控件将用于启动文件加密过程。

    var btnEncrypt = Ti.UI.createButton({
      title:'Encrypt', top:25, height:45, left:5, right:5	
    });
    win.add(btnEncrypt);
    
  4. 创建测试框架 UI 的最后一步是添加一个名为btnDecryptTi.UI.Button。此控件将用于启动文件解密过程。请注意,当btnEncrypt按钮被点击时启动的加密过程必须首先运行。

    var btnDecrypt = Ti.UI.createButton({
      title:'Decrypt', top:25, height:45, left:5, right:5	
    });
    win.add(btnDecrypt);
    

加密文件

使用btnEncrypt Ti.UI.Buttonclick事件演示文件加密过程。本节描述了如何使用SecurelyAESEncrypt方法,通过 AES 加密算法进行文件加密。

btnEncrypt.addEventListener('click',function(x){
  1. 文件加密过程的第一步是创建一个回调方法以接收AESEncrypt方法的结果。以下onEncryptCompleted方法演示了如何在回调过程中检查提供的结果。

      function onEncryptCompleted(e){
        if(e.success){
          var test = Ti.Filesystem.getFile(e.to);
          Ti.API.info("Test file contents:\n" + 
          (test.read()).text);
        }else{
          alert('failed due to: ' + e.message);
        }
      };
    
  2. 接下来必须创建Securely框架的FileCrypto对象的新实例。

      var fileCrypto = my.secure.createFileCrypto();
    
  3. 然后为输入和输出文件创建Ti.FileSystem.File对象。

      var plainTextFile = Ti.Filesystem.getFile(
      Ti.Filesystem.resourcesDirectory, 
      'PlainText.txt'),
      futureEncrypted = Ti.Filesystem.getFile(
      Ti.Filesystem.applicationDataDirectory, 
      'encryptedFile.txt');
    
  4. 最后,使用以下参数调用AESEncrypt方法:

    • password: password参数是在文件加密过程中使用的密钥。如果您希望解密文件,则必须在稍后提供相同的密码。

    • from: from参数提供了将要加密的文件的nativePath引用。请注意,文件本身不会被加密,而是作为源在to参数提供的路径上生成加密文件的依据。

    • to: to参数提供了生成加密文件的nativePath引用。应用程序必须能够写入此文件路径,否则将生成 IO 异常。

    • completed: completed参数提供了在AESEncrypt方法执行完成后要使用的回调方法的引用。

        fileCrypto.AESEncrypt({
          password:txtPassword.value,
          from:plainTextFile.nativePath,
          to:futureEncrypted.nativePath,
          completed:onEncryptCompleted
        });
      });
      

解密文件

使用btnDecrypt Ti.UI.Buttonclick事件演示文件解密过程。以下部分描述了如何使用SecurelyAESDecrypt方法,通过 AES 加密算法进行文件解密。请注意,用于加密文件的相同密码必须在解密过程中提供。

btnDecrypt.addEventListener('click',function(x){
  1. 文件解密过程的第一步是创建一个回调方法以接收AESDecrypt方法的结果。以下onDecryptCompleted方法演示了如何在回调过程中检查提供的结果:

      function onDecryptCompleted(e){
        if(e.success){
          var test = Ti.Filesystem.getFile(e.to);
          Ti.API.info("Test file contents:\n" + 
          (test.read()).text);
        }else{
          alert('failed due to: ' + e.message);
        }
      };
    
  2. 接下来为输入和输出文件创建Ti.FileSystem.File对象。

      var encryptedFile = Ti.Filesystem.getFile(
      Ti.Filesystem.applicationDataDirectory, 
      'encryptedFile.txt'),
      futureDecrypted = Ti.Filesystem.getFile(
      Ti.Filesystem.applicationDataDirectory, 
      'decryptedFile.txt');
    
  3. 然后创建SecurelyFileCrypto对象的新实例。

      var fileCrypto = my.secure.createFileCrypto();
    
  4. 最后,使用以下参数调用AESDecrypt方法:

    • password: password参数是在文件解密过程中使用的密钥。此密码必须与文件加密过程中提供的密钥匹配。如果密码不同,将向回调方法提供错误。

    • from: from参数提供了将要解密的文件的nativePath引用。请注意,文件本身不会被解密,而是作为源在to参数提供的路径上生成解密文件的依据。

    • to: to参数提供了生成解密文件的nativePath引用。应用程序必须能够写入此文件路径,否则将生成 IO 异常。

    • completed: completed参数提供了在AESDecrypt方法执行完成后要使用的回调方法的引用。

        fileCrypto.AESDecrypt({
          password:txtPassword.value,
          from:encryptedFile.nativePath,
          to:futureDecrypted.nativePath,
          completed:onDecryptCompleted
        });
      });
      

参见

  • 此配方使用Securely模块,有关安装详情,请参阅准备就绪部分中的使用安全属性配方。

  • Securely在 iOS 上使用RNCryptor库进行文件加密。有关文档、许可和源代码,请访问github.com/rnapier/RNCryptor

在 iOS 上处理受保护的 PDF 文件

在大多数组织中,处理和交换 PDF 文件是常见的做法。苹果在 iOS 原生访问中提供了 API,用于锁定和解锁 PDF 文档,这使得为该文件格式实现安全实践变得容易得多。Securely模块公开了这些原生 iOS API,以便您的 Titanium 应用程序可以利用。

此配方演示了如何使用Securely框架来锁定和解锁 PDF 文件。通过使用 PDF Locker 示例,我们将提供逐步说明,说明如何在您的 Titanium 应用程序中从本地设备保护并处理 PDF 文件。

准备就绪

此配方使用Securely原生模块。此模块和其他代码资源可以从本书提供的源代码中下载。在项目中安装这些资源很简单。只需将modules文件夹复制到您的项目中,如图所示。然后,将w4.pdf文件复制到您的项目中的Resources文件夹。此文件将由配方用于创建初始加密文件:

准备就绪

在复制modules文件夹后,您需要更新tiapp.xml引用,如准备就绪部分中的使用安全属性配方中所示。

如何操作...

一旦将module文件夹和w4.pdf样本文件添加到您的项目中,您需要在app.js文件中创建您的应用程序命名空间,并使用require将模块导入到您的代码中,如下代码片段所示:

//Create our application namespace
var my = {
  secure : require('bencoding.securely')
};

创建配方的 UI

此配方介绍了如何使用Securely模块以及 Titanium 的Ti.FileSystem命名空间来锁定或解锁 PDF 文件。以下屏幕截图中的测试工具用于演示如何执行这些安全的 PDF 操作:

创建配方的 UI

现在执行以下步骤:

  1. 创建此测试工具的第一步是创建一个Ti.UI.Window,用于附加所有 UI 元素。

    var win = Ti.UI.createWindow({
      backgroundColor: '#fff', title: 'PDF Protection Example', 
      barColor:'#000',layout:'vertical'
    });
    
  2. 创建测试工具 UI 的下一步是添加一个名为txtPasswordTi.UI.TextField。此控件用于获取加密和解密操作中使用的密码。

    var txtPassword = Ti.UI.createTextField({
      value:'foo123',hintText:'Enter Password',
      height:45, left:5, right:5, passwordMask:true
    });	
    win.add(txtPassword);
    
  3. 创建测试工具 UI 的下一步是添加一个名为btnLockTi.UI.Button。此控件将用于启动 PDF 锁定/保护过程。

    var btnLock = Ti.UI.createButton({
      title:'Lock PDF', top:25, height:45, left:5, right:5
    });
    win.add(btnLock);
    
  4. 创建测试工具 UI 的下一步是添加一个名为btnUnlockTi.UI.Button。此控件将用于启动 PDF 解锁或密码删除过程。

    var btnUnlock = Ti.UI.createButton({
      title:'Unlock PDF', top:25, height:45, left:5, right:5
    });
    win.add(btnUnlock);
    

保护 PDF 文件

使用 btnLock Ti.UI.Buttonclick 事件演示了 PDF 保护或锁定过程。以下部分描述了如何使用 Securelyprotect 方法进行 PDF 锁定:

btnLock.addEventListener('click',function(x){
  1. 文件解密过程的第一步是创建一个回调方法以接收 AESDecrypt 方法的返回结果。以下 onProtected 方法演示了如何在回调过程中检查提供的不同结果。

      function onProtected(e){
        if(e.success){
          alert('Protected PDF file created at: ' + e.to);
        }else{
          alert('failed due to: ' + e.message);
        }
      };
    
  2. 接下来创建 Ti.FileSystem.File 对象用于输入和输出文件。

      var inputFile = Ti.Filesystem.getFile(
      Ti.Filesystem.resourcesDirectory, 
      'w4.pdf'),
      outputFile = Ti.Filesystem.getFile(
      Ti.Filesystem.applicationDataDirectory, 
      'locked.pdf');
    
  3. 然后,必须创建 SecurelyPDF 对象的新实例。

      var pdf = my.secure.createPDF();
    
  4. 最后调用 protect 方法时,使用以下参数:

    • userPassword: userPassword 参数是 PDF 的用户级密码。此字段是必需的。

    • ownerPassword: ownerPassword 参数是 PDF 的所有者级密码。尽管这是可选的,但必须设置此值以对文档进行密码保护。

    • from: from 参数提供了要保护的 PDF 文件的 nativePath 引用。请注意,文件本身并未锁定,而是作为在 to 参数提供的路径生成受保护 PDF 文件的数据源。

    • to: to 参数提供了受保护 PDF 文件应生成的 nativePath 引用。应用程序必须能够写入此文件路径,否则将生成 IO 异常。

    • allowCopy: allowCopy 是一个布尔参数,表示文档在用户密码解锁时是否允许复制。此参数默认为 true,且为可选参数。

    • completed: completed 参数提供了一个回调方法的引用,用于在 protect 方法的执行完成后使用。

        pdf.protect({
          userPassword:txtPassword.value,
          ownerPassword:txtPassword.value,
          from:inputFile.nativePath,
          to:outputFile.nativePath,
          allowCopy:false,
          completed:onProtected
        });
      });
      

解锁 PDF 文件

使用 btnUnlock Ti.UI.Buttonclick 事件演示了解锁或从现有 PDF 文件中移除 PDF 保护的过程。以下步骤描述了如何使用 Securelyunprotect 方法进行 PDF 解锁:

btnUnlock.addEventListener('click',function(x){
  1. 解锁受保护 PDF 文件的第一步是创建一个回调方法以接收 unprotect 方法的返回结果。以下 onUnlock 方法演示了如何在回调过程中检查提供的不同结果。

      function onUnlock(e){
        if(e.success){
          alert('Unlocked PDF file created at: ' + e.to);
        }else{
          alert('failed due to: ' + e.message);
        }
      };
    
  2. 接下来创建 Ti.FileSystem.File 对象用于输入和输出文件。

      var protectedFile = Ti.Filesystem.getFile(
      Ti.Filesystem.applicationDataDirectory, 
      'locked.pdf'),
      unlockedFile = Ti.Filesystem.getFile(
      Ti.Filesystem.applicationDataDirectory, 
      'unlocked.pdf');
    
  3. 然后,必须创建 SecurelyPDF 对象的新实例。

      var pdf = my.secure.createPDF();
    
  4. 最后,使用以下参数调用 unprotect 方法:

    • password: 密码参数是解锁受保护 PDF 文件使用的密钥。此密码必须与锁定文档时使用的所有者密码匹配。

    • from: from 参数提供了受保护 PDF 文件的 nativePath 引用。请注意,PDF 文件本身并未解锁,而是作为在 to 参数提供的路径生成新解锁 PDF 文件的数据源。

    • toto 参数提供了解锁 PDF 文件应生成的 nativePath 引用。应用程序必须能够写入此文件路径,否则将生成 IO 异常。

    • completedcompleted 参数提供了对在执行 unprotect 方法完成后要使用的回调方法的引用。

        pdf.unprotect({
          password:txtPassword.value,
          from:protectedFile.nativePath,
          to:unlockedFile.nativePath,
          completed:onUnlock
        });
      });
      

参见

  • 此菜谱使用 Securely 模块。有关安装详情,请参阅 使用安全属性 菜谱中的准备就绪部分。

Android 锁屏监控

由于钛金 Android 架构的固有性质,确定应用何时被置于后台或锁屏被激活可能具有挑战性。这些动作是跟踪密码和应用程序访问的重要生命周期事件。例如,如果您希望用户在最后进入您的应用后锁定了设备,您可能希望在您的应用中显示登录界面。

以下菜谱演示了如何使用 Securely 框架检查用户是否启用了锁屏图案,并在屏幕锁定或解锁时触发事件。

准备就绪

此菜谱使用 Securely 原生模块。此模块和其他代码资源可以从书籍提供的源中下载。在项目中安装这些资源非常简单。只需将 modules 文件夹复制到您的项目中,如下面的屏幕截图所示:

准备就绪

在复制 modules 文件夹后,您需要更新 tiapp.xml 引用,如 使用安全属性 菜谱中的准备就绪部分所示。

如何操作...

一旦您将 module 文件夹添加到项目中,您需要在 app.js 文件中创建您的应用程序命名空间,并使用 require 将模块导入到您的代码中,如下面的代码片段所示:

//Create our application namespace
var my = {
  secure : require('bencoding.securely')
};

创建菜谱的 UI

此菜谱使用单个 Ti.UI.Window 对象来托管和演示不同的可用锁屏方法和事件。以下代码片段显示了如何创建此对象。

var win = Ti.UI.createWindow({
  backgroundColor: '#fff', title: 'Lock Screen Monitor', 
  fullscreen:false, exitOnClose:true
});

验证是否启用了锁屏图案

此菜谱依赖于用户启用密码或锁屏图案。如果此功能未启用,菜谱将通过简单地提供屏幕从功耗角度被禁用的时刻来继续工作。

以下步骤讨论了如何验证用户是否启用了锁屏功能:

  1. 第一步是创建一个新的 Securely.Platform 代理,如下面的代码片段所示:

      var platform = my.secure.createPlatform();
    
  2. Securely.Platform 代理提供了许多与安全相关的函数。当调用 lockPatternEnabled 方法时,会提供一个布尔值,表示用户是否已在他们的设备上启用了此功能。

      if(!platform.lockPatternEnabled()){
        alert('lock screen is not enabled on this device');
      }
    

    注意

    根据您组织的密码策略,您可能希望如果未实现锁屏,则禁用应用程序。

创建锁屏助手

Securely.LockScreenHelper 代理对象提供了启动监控锁屏活动所需的初始化方法。以下代码片段展示了如何使用此代理来启动监控过程:

  1. 锁屏监控过程的第一步是创建一个新的 Securely.LockScreenHelper 代理,如下面的代码片段所示:

    var lockHelper = my.secure.createLockScreenHelper();
    
  2. 然后调用 startMonitorForScreenOff 方法。这注册了一个广播接收器来监听 ACTION_SCREEN_OFF 广播。

    lockHelper.startMonitorForScreenOff();
    
  3. 然后调用 startMonitorForScreenOn 方法。这注册了一个广播接收器来监听 ACTION_SCREEN_ON 广播。

    lockHelper.startMonitorForScreenOn();
    

屏幕锁定事件

之前描述的 startMonitorScreenOffstartMonitorScreenOn 方法在接收到它们订阅的广播时都会触发全局事件。以下代码片段展示了如何创建应用程序监听器来订阅这些事件:

Ti.App.addEventListener('BCX:SCREEN_OFF',function(e){
  Ti.API.info('Last locked at ' + 
  String.DateFormat(new Date(e.actionTime)));
});

Ti.App.addEventListener('BCX:SCREEN_ON',function(e){
  Ti.API.info('Last unlocked at ' + 
  String.DateFormat(new Date(e.actionTime)));
});

每个事件都提供了信息以帮助管理您的应用程序状态。使用先前的示例代码片段,e 参数由 Securely 提供了两个属性。

  • actionName:这是完整的 Android 意图动作名称。

  • actionTime:这提供了最后事件被调用时的日期/时间,以秒格式。这可以通过 new Date(e.actionTime) 转换为 JavaScript 日期。

使用窗口焦点进行监控

此配方使用示例 Ti.UI.Window 上的 focus 事件来演示如何检查自 Ti.UI.Window 上次获得焦点以来设备是否已被锁定。此模式的一个用途可能是检查是否应该展示内部密码屏幕,或者检查是否需要重新建立会话。

win.addEventListener('focus',function(e){
  1. 调用 wasLocked 方法来确定设备是否已被锁定。

      if(lockHelper.wasLocked()){
    
  2. isShowingLockScreen 方法也可以用来确定设备是否当前向用户展示锁屏界面。

        if(!lockHelper.isShowingLockScreen()){
    
  3. resetMonitorForScreenOff 方法也可以用来重置 wasLocked 返回的值。这在跟踪设备在应用程序会话之间是否被锁定时很有帮助。

          lockHelper.resetMonitorForScreenOff();
    
        }
      }	
    });
    

停止监控

当应用程序不再需要此功能时,重要的是停止监控并移除全局监听器。以下代码片段展示了如何使用 Ti.UI.Windowclose 事件来完成此操作。

win.addEventListener('close',function(e){
  lockHelper.stopMonitoring();
  Ti.App.removeEventListener('BCX:SCREEN_ON',screenON);
  Ti.App.removeEventListener('BCX:SCREEN_OFF',screenOFF); 
});	

注意

可以通过使用 stopMonitorForScreenOffstopMonitorForScreenOn 单独停止监控。要停止所有监控,可以使用 stopMonitoring 便利方法来移除两个接收器。

参见

  • 要了解更多关于 android.intent.action.SCREEN_ONandroid.intent.action.SCREEN_OFF 意图的信息,请访问官方 Android 文档,网址为 developer.android.com/reference/android/content/Intent.html

  • 此配方使用 Securely 模块。有关安装详情,请参阅 Using secure properties 配方的 准备就绪 部分。

附录 A. Titanium 资源

在本附录中,我们将涵盖:

  • 开始使用 Titanium

  • 开始使用 Android

  • 开始使用 iOS

  • Titanium 测试资源

  • 模块和开源

  • Titanium 社区链接

开始使用 Titanium

要创建自己的 Titanium 应用或运行本书中提供的任何示例项目,首先您需要下载 Titanium SDK。这个免费的开发平台可在www.appcelerator.com获取。

Appcelerator 提供了一个免费的 IDE 以帮助开发 Titanium 应用。要了解更多关于 Titanium Studio 的信息以及下载应用程序,请访问developer.appcelerator.com

注意

本书提供的所有示例项目都设计为导入到 Titanium Studio 项目中。

开始使用 Android

为了使用 Titanium 构建 Android 应用,您首先需要安装 Android SDK。要下载 Android SDK 并查看安装文档,请访问developer.android.com/sdk/index.html

安装 Android SDK 后,您必须在 Titanium Studio 中设置 Android 引用并配置默认模拟器。有关如何在 Titanium Studio 中设置 Android 的分步指南,请访问developer.android.com/sdk/index.html

有关最低支持的 SDK 版本和整体兼容性的信息,请访问docs.appcelerator.com/titanium/latest/#!/guide/Titanium_Compatibility_Matrix

开始使用 iOS

Titanium 利用原生平台开发工具链。因此,构建 Titanium iOS 应用需要苹果电脑。

目前有两种方式可以下载 Xcode 和开发所需的 SDK。

  • 在您的苹果电脑上打开 Mac App Store 并搜索 Xcode。这将允许您免费下载苹果 IDE 和 SDK。

  • 苹果开发工具也通过开发者门户developer.apple.com提供。

要将应用发布到苹果 App Store,您必须是苹果 iOS 开发者计划的一部分。要了解更多关于应用分发的信息,请访问developer.apple.com/programs/ios/distribute.html

Titanium 测试资源

Titanium 的测试故事正在迅速发展。以下工具是 Appcelerator 测试平台的开源伴侣。

TiShadow

TiShadow 项目是 Titanium 的基石,被开发者用来解决各种问题。这个成熟且广泛采用的项目作为一个测试工具,内置了对使用 Jasmine 的 BDD 风格测试的支持。TiShadow 不仅仅是一个测试工具,它还提供了从跨平台预览到高级开发者工具的广泛活动支持。

了解更多关于 TiShadow 的信息,请访问 github.com/dbankier/TiShadow

Titanium-Jasmine

Titanium-Jasmine 项目为您的 Titanium 应用程序提供了对 Jasmine 测试框架的支持。这个文档齐全的项目提供了启动所需的所有文档,包括示例应用程序和说明。

了解更多关于 Titanium-Jasmine 的信息,请访问 github.com/guilhermechapiewski/titanium-jasmine

TiJasmine

TiJasmine 项目是由 Titanium SDK 的核心提交者之一发起的一个新的开源项目,它为 Titanium 提供了 Jasmine 测试支持。考虑到 Titanium 的 CommonJS 实现,该项目允许您快速将测试集成到现有的 Titanium 应用程序中。

了解更多关于 TiJasmine 的信息,请访问 github.com/billdawson/tijasmine

模块和开源

Appcelerator 围绕 Titanium 移动平台建立了一个强大的开源社区。Titanium 移动生态系统的大多数内容都是开源的,包括 Titanium SDK 本身。这个开源社区为学习和提升您的 Titanium 开发流程提供了一个极好的资源。以下章节讨论了一些关键的 Titanium 开源项目。

Appcelerator 在 Github

Appcelerator 的 Github 仓库包含了所有开源解决方案,包括由 Appcelerator 提供的 Titanium 移动 SDK。这些仓库提供了 Appcelerator 大多数产品的最新状态,包括新功能和更新。

github.com/appcelerator 查看 Appcelerator 的所有开源解决方案。

Titanium 移动 SDK 在 Github

Titanium 移动 SDK 在 Apache 2 许可证下开源,可在 Github 上获取。Titanium 使用与第三方模块相同的模块 API 构建。因此,查看 Titanium SDK 代码可以了解开发此类模块的实践。代码的可用性为 Titanium 开发者提供了一个强大的学习机会,因为他们能够看到他们的 JavaScript 如何与底层平台交互。

查看 Titanium 移动 SDK 的源代码,请访问 github.com/appcelerator/titanium_mobile

Github 上的 Titanium 模块

钛移动的一个关键特性是支持第三方模块。本书中使用的所有模块都是开源的,可在 GitHub 上找到。除了第三方提供的模块外,Appcelerator 还提供了一大批社区模块,可在github.com/appcelerator/titanium_modules找到。

钛社区链接

钛拥有一个庞大而活跃的开发者社区。以下几节将讨论由 Appcelerator 和钛社区维护的一些流行网站。

文档

Appcelerator 在docs.appcelerator.com/titanium/latest提供了钛移动 SDK 和相关组件的完整文档。

所有 5,000 多个 SDK API 都带有示例文档,可在docs.appcelerator.com/titanium/latest/#!/api找到。

Appcelerator 为平台的大多数功能创建了入门指南,可在docs.appcelerator.com/titanium/latest/#!/guide找到。

支持

developer.appcelerator.com/questions/newest的钛 Q&A 论坛是支持产品的首要门户。这个论坛提供了广泛的支持主题列表,并且经常有钛泰坦和 Appcelerator 的员工访问。

流行的编程 Q&A 网站 Stack Overflow 也有一个钛移动部分,可在stackoverflow.com/questions/tagged/titanium找到。如果你的问题涉及原生平台组件,这可以是一个极好的资源。

Appcelerator 还提供付费支持选项。了解更多信息请访问support.appcelerator.com

其他有用资源

Appcelerator 的开发者博客developer.appcelerator.com/blog提供了新闻、指导和代码示例的优秀来源。

钛 Meetup 社区也在不断发展,你附近的 Meetup 列表可在appcelerator.meetup.com找到。

官方 Appcelerator 推特账号twitter.com/appcelerator是获取新闻的绝佳来源,包括在各个应用商店中推出的新钛应用。

如果你需要帮助处理钛项目,可以在developer.appcelerator.com/devlink找到的 Devlink 程序中找到经验丰富的钛开发者和服务机构的列表。

posted @ 2025-09-24 13:53  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报