Sencha-Touch2-JavaScript-移动框架-全-

Sencha Touch2 JavaScript 移动框架(全)

原文:zh.annas-archive.org/md5/04504CE3000052C183ADF069B1AD3206

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

自首次发布以来,Sencha Touch 迅速成为开发基于 HTML5 的丰富移动网络应用的黄金标准。Sencha Touch 是第一个允许你在 iPhone、Android、BlackBerry 和 Windows Phone 触摸屏设备上开发看起来和感觉都像本地应用的 HTML5 移动 JavaScript 框架。Sencha Touch 是世界上第一个专门利用 HTML5、CSS3 和 JavaScript 构建最高水平的力量、灵活性和优化的应用程序框架。它专门使用 HTML5 来提供如音频和视频以及本地存储代理等组件,以便离线保存数据。Sencha Touch 在其组件和主题中广泛使用 CSS3,提供一个极其健壮的样式层,让你完全控制应用的外观。

Sencha Touch 让你能够为多个平台设计,而无需学习多种神秘的编程语言。相反,你可以利用你对 HTML 和 CSS 的现有知识,用 JavaScript 快速创建适用于移动设备的丰富网络应用。本书将向你展示如何使用 Sencha Touch 高效地制作吸引人、令人兴奋且易于使用的网络应用,从而让你的访客不断回访。

Sencha Touch 移动 JavaScript 框架教你所有开始使用 Sencha Touch 和构建绝妙的移动网络应用所需的知识。从对 Sencha Touch 的概述开始,本书将引导你创建一个完整的简单应用,然后是用户界面的样式设计,并通过综合示例解释 Sencha Touch 组件列表。接下来,你将学习关于触摸和组件事件的必要知识,这将帮助你创建丰富的动态动画。随后,本书提供有关核心数据包的信息以及处理数据的内容,并以构建另一个简单但强大的 Sencha Touch 应用作为结尾。

简而言之,本书采用逐步讲解和丰富内容,让初学者轻松快速地成为 Sencha Touch 高手。

利用 Sencha Touch,一个针对下一代触摸设备的多平台库。

本书涵盖内容

第一章,用 Sencha Touch 开始,概述了 Sencha Touch 并介绍了设置开发库的基础知识。我们还将讨论编程框架以及它们如何帮助你快速轻松地开发触摸友好的应用。

第二章,创建一个简单应用,首先通过创建一个简单应用来探索 Sencha Touch 的基本元素。我们还将探讨一些更常见的组件,如列表和面板,并指导你如何查找并修复常见错误。

(- )第三章,用户界面样式,探讨了在我们有了简单的应用程序之后,如何使用 CSS 样式来改变个别组件的外观和感觉。然后我们将深入探讨如何使用 Sencha Touch 主题,通过 SASS 和 Compass 来控制整个应用程序的外观。

(- )第四章,组件和配置,详细介绍了 Sencha Touch 的个别组件。我们还将讨论每个组件中的布局使用,以及它们如何用于安排应用程序的不同部分。

(- )第五章,事件和控制器,帮助我们查看 Sencha Touch 事件系统,该系统允许这些组件对用户的触摸做出反应并与彼此通信。我们将涵盖监听器和处理器的使用,并探讨如何监视和观察事件,以便我们可以看到我们应用程序的每个部分正在做什么。

(- )第六章,获取数据,解释了使用表单从用户那里获取信息、验证数据的方法,以及如何存储数据的细节,因为数据是任何应用程序的关键部分。我们还将讨论 Sencha Touch 使用的不同数据格式,以及如何使用 Sencha Touch 的模型和存储来操作这些数据。

(- )第七章,获取数据出去,将讨论使用面板和 XTemplates 来显示数据,因为在我们应用程序中有数据之后,我们需要能够将其取回以显示给用户。我们还将查看如何使用我们的数据创建多彩的图表和图形,使用 Sencha Touch 图表。

(- )第八章,创建 Flickr 查找器应用程序,创建了一个更复杂的应用程序,它基于我们当前的位置从 Flickr 获取照片,使用了我们所学的关于 Sencha Touch 的信息。我们还将借此机会讨论最佳实践,以结构化和组织您的应用程序及其文件。

(- )第九章,高级主题,探讨了如何通过创建自己的 API 来将您的数据与数据库服务器同步。此外,我们还将查看如何在大设备与数据库服务器之间同步数据,将您的应用程序与 Phone Gap 和 NimbleKit 编译。我们还将探讨如何开始成为 Apple iOS 或 Google Android 开发者。

(- )本书所需材料

(- )为了完成本书中的任务,您需要一台计算机,配备以下物品:

  • Sencha Touch 2.1.x

  • (- )Sencha Cmd 3.1.x 或更高版本

  • (- )像 BBEdit、Text Wrangler、UltraEdit、TextMate、WebStorm、Aptana 或 Eclipse 这样的编程编辑器

  • 例如内置的苹果 OSX 网络服务器、微软内置的 IIS 服务器,或者可下载的 WAMP 服务器和软件包等本地的网络服务器。

这些项目的链接在 设置您的开发环境 部分提供,位于 第一章,让我们从 Sencha Touch 开始。当需要时,其他可选但有助于帮助的软件将在特定章节中链接。

本书适合哪些人

如果您想掌握使用 Sencha Touch 移动网络应用程序框架的实践知识,制作适用于移动设备的吸引人的网络应用程序,这本书非常适合您。您应该对 HTML 和 CSS 有些熟悉。如果您是设计师,这本书将为您提供实现想法的技能;如果您是开发者,这本书将通过实际示例为您提供创意灵感。我们假定您知道如何使用触摸屏、触摸事件和移动设备,如苹果 iOS、谷歌 Android、黑莓和 Windows Phone。

约定

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

文本中的代码词汇显示如下:“顶部标题还列出了组件紧邻的 xtype 值。”

代码块设置如下:

var nestedList = Ext.create('Ext.NestedList', {
    fullscreen: true,
    title: 'Minions',
    displayField: 'text',
    store: store
});

当我们希望引起您对代码块中特定部分的关注时,相关的行或项目将被加粗:

<img src="img/my-big-image.jpg">

任何命令行输入或输出都如下所示:

C:\Ruby192>ruby -v
ruby 1.9.2p180 (2011-02-18) [i386-mingw32]

新术语重要词汇以粗体显示。您在屏幕上看到的词汇,例如在菜单或对话框中出现的词汇,在文中会以这种方式显示:“还有一个 选择代码 选项,让您复制代码并将其粘贴到您自己的应用程序中。”

注意

警告或重要说明会以这种盒子形式出现。

提示

技巧和小窍门会以这种方式出现。

读者反馈

我们总是欢迎来自读者的反馈。告诉我们您对这本书的看法——您喜欢什么或者可能不喜欢什么。读者反馈对我们来说非常重要,以便我们开发出您能真正从中获益的标题。

如果您想发送一般性反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在消息主题中提及书籍标题。

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

客户支持

既然您已经成为 Packt 书籍的自豪拥有者,我们就有一系列的事情可以帮助您充分利用您的购买。

下载示例代码

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

错误

虽然我们已经竭尽全力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能将这些错误报告给我们,我们将非常感激。这样做可以避免其他读者感到沮丧,并帮助我们改进此书的后续版本。如果您发现任何错误,请通过访问www.packtpub.com/submit-errata报告,选择您的书籍,点击错误提交表单链接,并输入您错误的详细信息。一旦您的错误得到验证,您的提交将被接受,并且错误将会上传到我们的网站,或者添加到该标题下的现有错误列表中。您可以通过从www.packtpub.com/support选择您的标题来查看现有的错误。

盗版

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

请通过<copyright@packtpub.com>联系我们,附上涉嫌侵权材料的链接。

我们感激您在保护我们的作者和我们的能力,带来有价值的内容方面所提供的帮助。

问题

如果您在阅读书籍过程中遇到任何问题,可以通过<questions@packtpub.com>联系我们,我们会尽最大努力解决问题。

第一章:让我们从 Sencha Touch 开始

随着移动设备、手机和平板电脑的日益普及,消费者迅速转向接受触摸屏操作系统和应用程序。这种普及为开发者提供了丰富的平台选择:苹果的 iOS(包括 iPhone、iPod Touch 和 iPad)、谷歌的 Android、Windows 7 移动版以及更多。不幸的是,这种丰富的平台选择带来了同样丰富的编程语言选择。选择任何单一的语言往往让你锁定使用特定的平台或设备。

Sencha Touch 通过提供基于 JavaScript、HTML5 和 CSS 的框架消除了这一障碍。这些标准得到了大多数现代浏览器和移动设备的支持。使用基于这些标准的框架,你可以将应用部署到多个平台,而无需完全重写你的代码。

本书将帮助你熟悉 Sencha Touch,从基本设置到构建复杂应用。我们还将涵盖一些框架和触摸屏应用的基础知识,并提供如何设置你的开发环境和以多种不同方式部署应用的技巧。

在本章中,我们将介绍以下主题:

  • 框架

  • 移动应用框架

  • 为 Sencha Touch 设计应用

  • 开始使用 Sencha Touch

  • 设置你的开发环境

  • 使用 Sencha Touch 开发应用程序的其他工具

框架

框架是一组可重用的代码,提供一组对象和函数,你可以使用它们来为构建应用程序提供一个起点。框架的主要目标是让你在每次构建应用程序时避免重新发明轮子。

编写良好的框架通过提供一定程度的一致性并轻轻地推动你遵循标准实践,也有助于提高可重用性。这种一致性还使框架更容易学习。可重用性和易于学习的两个关键编程概念是对象继承

大多数像 Sencha Touch 这样的框架都是围绕面向对象编程风格(也称为OOP)构建的。OOP 背后的想法是,代码是围绕简单的基对象设计的。基对象将有一些它可以执行的属性和函数。

例如,假设我们有一个名为wheeledVehicle的对象。我们的wheeledVehicle对象有几个如下列出的属性:

  • 一个或多个轮子

  • 一个或多个座位

  • 转向装置

它还有一些如下列出的功能:

  • moveForward

  • moveBackward

  • moveLeft

  • moveRight

  • stop

这是我们基本的对象。一旦创建了基本对象,我们就可以扩展它以添加更多功能和属性。这允许我们创建更复杂的对象,如自行车、摩托车、汽车、卡车、公共汽车等。这些复杂对象比我们的基本车轮对象多得多,但它们也继承了原始对象的属性和能力。

我们甚至可以覆盖原始函数,例如如果需要,可以让我们的moveForward函数比自行车更快地运行。这意味着我们可以构建许多不同的wheeledVehicles实例,而无需重新创建我们的原始工作。我们甚至可以构建更复杂的对象。例如,一旦我们有了一个通用的汽车,我们只需添加特定模型的新的属性和函数,就可以从大众汽车到法拉利汽车构建出各种车型。

Sencha Touch 也是基于面向对象编程(OOP)的概念。让我们以 Sencha Touch 中的一个例子来说明。在 Sencha Touch 中,我们有一个简单的对象叫做container

container对象是 Sencha Touch 的基本构建模块之一,如其名称所示,它用于包含应用程序视觉区域中的其他项目。其他视觉类,如面板、工具栏和表单面板,都扩展了container类。组件类有许多配置项,用于控制简单事物,例如以下内容:

  • height

  • width

  • padding

  • margin

  • border

配置选项还可以定义更复杂的行为,例如以下内容:

  • layout: 此选项用于确定容器中项目的位置。

  • listeners: 此选项用于确定容器应该关注哪些事件以及听到事件时应该做什么。

component对象拥有一些用于控制其行为和配置的方法。一些简单方法的示例如下:

  • show

  • hide

  • enable

  • disable

  • setHeight

  • setWidth

它还支持更复杂的方法,例如以下内容:

  • query: 此操作用于在容器内搜索特定项目。

  • update: 此操作用于更新容器中的 HTML 或数据内容。

容器有一些属性和事件可供使用和监听。例如,您可以监听以下事件:

  • show

  • hide

  • initialize

  • resize

基本的container对象是 Sencha Touch 中的一个构建块,用于创建其他视觉对象,如面板、标签页、工具栏、表单面板和表单字段。这些子对象或对象继承了容器对象(对象)的所有属性和能力。它们将包括相同的高度、宽度等配置选项,并且知道如何执行容器可以执行的所有操作:显示、隐藏等。

这些子对象也将有额外的、独特的配置和方法。例如,按钮有一个额外的text属性,用于设置它们的标题,当用户轻触按钮时,按钮会发出通知。通过扩展原始对象,创建按钮的人只需要为这些额外的配置和方法编写代码。

从编程角度来看,对象和继承使得我们可以复用大量的工作。这也意味着当我们遇到一个新的框架,如 Sencha Touch 时,我们可以利用我们对基本代码对象的学习来快速理解更复杂的对象。

构建基础

除了提供可复用性,框架还为您提供了一组核心对象和函数,通常用于构建应用程序。这使得您不需要每次开始一个新应用程序时都从头开始。

这些代码对象通常处理用户输入、操作或查看数据的大部分方式。它们还涵盖了应用程序后台发生的常见任务,如管理数据、处理会话、处理不同的文件格式以及格式化或转换不同类型的数据。

框架的目的是预见常见的任务,并为程序员提供预先构建的函数来处理这些任务。一旦您熟悉了 Sencha Touch 等框架提供的广泛对象和函数,您就可以快速、更有效地开发应用程序。

构建基础

有计划地构建

在选择任何框架时,关键之一是要查看其文档。没有文档的框架,或者是文档质量差的框架,使用起来简直就是一种折磨。良好的文档提供了关于框架中每个对象、属性、方法和事件的低层次信息。它还应该提供更一般性的信息,例如代码在各种不同情况下是如何使用的示例。

提供良好的文档和示例是 Sencha Touch 作为框架的两个亮点。在 Sencha 的主网站上,docs.sencha.com,可以找到关于Sencha 文档资源 | Touch的广泛信息。有自 Sencha Touch 1.1.0 版本以来的每个版本的文档。在这本书中,我们使用的是 Version 2.2.1,所以点击Touch 2.2.1链接会带您到相关的文档。您还可以下载文档作为 ZIP 文件。

一个设计良好的框架还维护一套规范和实践。这些可以是很简单的事情,比如使用驼峰命名法为变量命名(例如,myVariable),或者更复杂的注释和文档化代码的做法。这些标准和实践的关键是保持一致性。

一致性使你能够快速学习语言,并直观地知道在哪里找到你问题的答案。这有点像有一个建筑计划;你很快就能理解事物的布局和如何到达你需要去的地方。

框架也会通过提供结构和编码一致性的示例帮助你理解如何构建自己的应用。

在这方面,Sencha 竭尽全力鼓励一致性,遵守标准,并为 Sencha Touch 框架提供广泛的文档。这使得 Sencha-Touch 成为初学者非常有效的首选语言。

构建社区

框架很少是孤立的存在的。开发人员群体往往会聚集在特定的框架周围,形成社区。这些社区是询问问题和学习新语言的绝佳场所。

就像所有社区一样,有许多不成文的规定和习俗。在发帖提问之前,总是花时间浏览一下论坛,以防问题已经被提出并回答过了。

Sencha Touch 拥有一个活跃的开发社区,有一个可以从主要 Sencha 网站 www.sencha.com/forum 访问的论坛(在网站上向下滚动以找到 Sencha Touch 特定的论坛)。

移动应用框架

移动应用框架需要解决与标准框架不同的功能问题。与传统的桌面应用不同,移动设备处理触摸和滑动而不是鼠标点击。键盘是屏幕的一部分,这可能使得传统的键盘导航命令变得困难,甚至不可能。此外,移动设备中有各种屏幕尺寸和分辨率可供选择。因此,框架必须根据屏幕和设备类型调整自身。移动设备的计算能力不如桌面设备,资源也不多,所以移动框架应该考虑这些限制。为了理解这些限制,我们可以先看看不同类型的移动框架以及它们是如何工作的。

原生应用与网页应用

移动应用框架主要有两种基本类型:一种用于构建原生应用,另一种用于构建基于网页的应用,如 Sencha Touch。

原生应用是指直接安装在设备上的应用。它通常能更多地访问设备的硬件(如摄像头、GPS、定位硬件等)和其他设备上的程序,比如通讯录和相册。原生应用的更新通常需要每个用户下载更新后的程序的新副本。

基于 Web 的应用程序,如名字暗示的那样,需要一个公共 Web 服务器,用户需要通过这个服务器来访问应用程序。用户会使用他们移动设备上的浏览器导航到你的应用程序的网站。由于应用程序在 Web 浏览器中运行,它对本地文件系统的访问权限更少,对硬件的访问权限也更少,但它也不需要用户经历复杂的下载和安装过程。基于 Web 的应用程序的更新可以通过对公共 Web 服务器进行一次更新来完成。然后,任何访问该网站的人都会自动更新程序。

基于 Web 的应用程序也可以修改,使其表现得更像原生应用程序,甚至可以通过单独的程序编译,成为一个完整的原生应用程序。

大多数移动浏览器允许用户将应用程序保存到移动设备的桌面。这将创建一个在移动设备主屏幕上的图标。从那里,应用程序可以被启动,并且表现得非常像一个原生应用程序。当从主屏幕图标启动应用程序时,浏览器的导航将不可见。Web 应用程序还可以使用移动设备的内置存储能力,例如使用HTML5 本地存储在设备上存储数据,使应用程序在没有网络连接的情况下离线工作。

原生应用程序与 Web 应用程序

如果你发现自己需要原生应用程序的全部功能,你可以使用 Sencha Cmd 命令行工具或外部编译器,如 PhoneGap(www.phonegap.com/),将你的基于 Web 的应用程序编译成一个完整的原生应用程序,然后你可以在苹果的 App Store 或谷歌 Play 商店上传并销售。我们将在书的后面更详细地讨论这些选项。

基于 Web 的移动框架

基于 Web 的移动框架依赖于运行应用程序的 Web 浏览器。这是一个关键的信息,原因有几个。

基于 Web 的应用程序首先需要考虑的是,Web 浏览器在移动平台之间需要保持一致。如果你之前有任何网站开发经验,你就会知道浏览器兼容性问题非常痛苦。一个网站在不同的浏览器中可能看起来完全不同。在某个浏览器中可以工作的 JavaScript 在另一个浏览器中可能无法工作。人们也倾向于保留不更新老旧的浏览器。幸运的是,对于大多数移动设备来说,这些问题影响不大,对于 iOS 和 Android 来说更不是问题。

苹果的 iOS 和谷歌的 Android 的 Web 浏览器都是基于WebKit引擎。WebKit 是一个开源引擎,它基本上控制着浏览器如何显示页面、处理 JavaScript 以及实现 Web 标准。这意味着你的应用程序应该在这两个平台上的工作方式相同。

然而,不使用 WebKit 的手机设备(如 Windows 手机)将无法使用您的应用程序。好消息是,随着更多浏览器采用 HTML5 标准,这个问题也可能开始消失。

对于基于网页的应用程序的第二个考虑因素是它存放在哪里。原生应用程序被安装在用户的设备上。基于网页的应用程序需要被安装在公共服务器上。用户应该能够将 URL 输入到他们的网页浏览器中,并导航到您的应用程序。如果应用程序只存在于您的电脑上,只有您一个人可以使用它。这对于测试来说很好,但是如果您想让其他人使用您的应用程序,您需要将其托管在公共服务器上。

第三个考虑因素是连通性。如果用户无法连接到互联网,他们将无法使用您的应用程序。然而,Sencha Touch 可以配置为存储您的应用程序及其所有数据在本地。乍一看,这个能力似乎完全解决了连通性问题,但实际上,当用户用多个设备连接到您的应用程序时,它实际上会导致问题。

基于网页的手机框架

基于网页的应用程序存放在互联网上,因此它可以通过任何带有浏览器和网络连接的设备访问。同一个应用程序可以同时用于手机、个人电脑和移动设备。如果数据存储在中心服务器上,这是信息丰富应用的最大优势。这意味着在一个设备上输入的数据可以在另一个设备上访问。

但是,如果一个应用程序存储数据在本地,这将不可能实现,因为在一个移动设备上输入的数据无法在个人电脑上访问。如果用户使用个人电脑查看网站,应用程序将创建另一套本地数据。

幸运的是,Sencha Touch 可以设置为在服务器和其他各种设备之间同步数据。当您的应用程序连接到互联网时,它会同步任何现有的离线数据,并在在线时使用远程服务器存储任何数据。这确保了您的数据可以在所有设备上访问,同时允许您根据需要离线工作。

网页框架和触控技术

标准的网页应用程序框架已经被设计来与鼠标和键盘环境一起工作,但是,移动框架应该使用触控技术进行导航和数据输入。

网页框架和触控技术

以下是一些常见的触控手势:

  • Tap: 屏幕上的一次点击

  • 双击: 在屏幕上快速点击两次

  • Tap Hold: 在设备上点击一次,然后保持手指按压

  • Swipe: 用一根手指从左到右或从上到下在屏幕上移动

  • 捏合或展开: 用两根手指触摸屏幕,然后捏合在一起或分开以撤销动作

  • 旋转:将两个手指放在屏幕上并顺时针或逆时针旋转它们,通常是为了在屏幕上旋转一个对象

最初,这些交互仅在原生应用中得到支持,但 Sencha Touch 使它们在网页应用中也可用。

为移动设备和触控技术设计应用

移动应用需要一些思维上的改变。最大的考虑因素是比例问题。

如果你习惯在 21 英寸显示器上设计应用,处理 3.5 英寸手机屏幕可能会是一种痛苦的经历。手机和移动设备使用各种屏幕分辨率;以下是一些例子:

  • iPhone 5 Retina 显示屏: 1136 x 640

  • iPhone 5: 960 x 640

  • iPhone 4iPod Touch 4: 960 x 640

  • iPhone 4iPod Touch 3: 480 x 320

  • Android 4 手机: 这些支持四种通用尺寸:

    • 大屏幕至少为 960 x 720

    • 小屏幕至少为 640 x 480

    • 普通屏幕至少为 470 x 320

    • 小屏幕至少为 426 x 320

  • HTC 手机: 800 x 480

  • 三星 Galaxy S3: 1280 x 720

  • iPad: 1024 x 768

  • iPad Retina: 2048 x 1536

此外,Android 平板电脑具有各种分辨率和尺寸。为这些不同屏幕尺寸设计应用可能需要一些额外的努力。

设计移动应用时,通常的一个好主意是制作设计草图,以更好地了解比例和应用各种元素将要去的位置。有许多好的布局程序可以帮助你做这件事,如下列出:

触控应用还有一些你需要留意的考虑因素。如果你来自典型的网络开发背景,你可能会习惯使用如悬停等事件。

悬停通常用于网络应用中,以提示用户可以执行某个操作,或者提供工具提示;例如,当用户将鼠标光标悬停在图像或文本上时,通过改变其颜色来显示该图像或文本可以被点击。由于触控应用要求用户与屏幕接触,所以实际上并不存在悬停的概念。用户可以激活或与之交互的对象应该是显而易见的,图标应该清晰标记。

与基于鼠标的应用程序不同,触控应用程序通常也被设计来模仿现实世界的交互。例如,在触控应用程序中翻页通常是通过水平地用手指在页面上滑动来完成的,这与现实世界中的操作非常相似。这鼓励了应用程序的探索,但这也意味着程序员在处理任何潜在的破坏性操作时(如删除一个条目)必须特别小心。

为什么是触控?

在触屏出现之前,应用程序通常限于从外部键盘和鼠标接收输入。这两种方式在移动平台上都不是很理想。即使在全内置键盘的非触控设备中使用,它们也可能占用设备上大量的空间,从而限制了可用的屏幕尺寸。相比之下,基于触控的键盘在不需要时会消失,从而留出更大的屏幕区域用于显示。

移动设备上的滑出式键盘并没有 adverse 影响屏幕尺寸,但它们可能会占用空间并且使用起来不舒服。此外,触控屏键盘允许有特定于应用程序的键盘和按键,比如在网页浏览器中使用的.com键。

键盘和鼠标设备也可能对一些用户造成心理上的 disconnect。在桌面上使用鼠标来控制分离屏幕上的小指针往往会让用户有一种没有完全控制活动的感觉,而直接在屏幕上触摸并移动一个对象则让你成为活动的主体。因为我们通过触摸和用手移动物体与物理世界互动,所以基于触控的应用程序通常提供更直观的用户界面(UI)。

触控技术随着 Windows 8 的问世也开始在桌面电脑领域取得重大突破。随着这项技术的价格降低并变得更加普及,对基于触控的应用程序的需求将继续增长。

开始使用 Sencha Touch

当你开始接触任何新的编程框架时,了解所有可用的资源是个好主意。购买这本书是一个很好的开始,但还有其他一些对你探索 Sencha Touch 框架来说非常有价值的资源。

幸运的是,Sencha 网站为我们提供了丰富的信息,帮助你在开发的每个阶段。

应用程序编程接口(API)

Sencha Touch 应用程序编程接口(API)文档提供了关于 Sencha Touch 中可用的每个对象类的详细信息。API 中的每个类都包括对该类每个配置选项、属性、方法和事件的详细文档。API 通常还包括每个类的简短示例,带有实时预览和代码编辑器。

应用程序编程接口(API)文档可在 Sencha 网站docs.sencha.com/touch/2.2.1/上找到。

如以下屏幕截图所示的副本,也作为 Sencha Touch 框架的一部分包含在内,您将下载此框架来创建您的应用程序:

The API

示例

Sencha 网站还包含了许多示例应用程序供您查看。其中最为有帮助的是厨房水槽(Kitchen Sink)应用程序。下面的屏幕截图展示了厨房水槽应用程序的外观:

示例

厨房水槽应用程序

厨房水槽应用程序提供了以下示例:

  • 用户界面元素,如按钮、表单、工具栏、列表等

  • 动作的动画,如翻页或滑动表单

  • 触摸事件,如轻触、滑动和捏合

  • 处理 JSON、YQL 和 AJAX 的数据

  • 音频和视频的媒体处理

  • 更改应用程序外观的主题

每个示例在上右角都有一个源代码按钮,将显示厨房水槽示例的代码。

厨房水槽应用程序还提供了事件记录器事件模拟器。这些功能将允许您录制、存储并回放设备屏幕上执行的任何触摸事件。

这些模拟器演示了如何在您的应用程序中记录操作以作为现场演示或教程回放,它们还可以用于轻松重复测试功能。

您可以在任何移动设备上或使用苹果的 Safari 网络浏览器在常规计算机上与厨房水槽应用程序互动。厨房水槽应用程序可在 Sencha 网站docs.sencha.com/touch/2.2.1/touch-build/examples/kitchensink/上找到。

厨房水槽应用程序的副本也作为 Sencha Touch 框架的一部分包含在内,您将下载此框架来创建您的应用程序。

学习

Sencha 在网站上还有一个部分,致力于讨论 Sencha Touch 框架的特定方面。这个部分被适当地命名为学习。它包含了许多教程、屏幕录像和指南供您使用。每个子部分都被标记为简单中等困难,这样您就对您将要进入的内容有一个大致的了解。

学习部分可在 Sencha 网站www.sencha.com/learn/touch/上找到。

论坛

Sencha 论坛值得再次提及。这些社区讨论提供了基础知识、错误报告、问答环节、示例、竞赛等内容。论坛是找到日常使用框架的人提供的解决方案的好地方。

设置您的开发环境

现在您已经熟悉了可用的 Sencha Touch 资源,下一步是设置您的开发环境并安装 Sencha Touch 库。

为了开始使用 Sencha Touch 开发应用程序,强烈建议您有一个可以托管应用程序的网络服务器。虽然通过使用网络浏览器查看本地文件夹来开发 Sencha Touch 应用程序是可能的,但没有网络服务器,您将无法在任何移动设备上测试您的应用程序。

在 Mac OS X 上设置网络共享。

如果您正在使用 Mac OS X,您已经安装了一个网络服务器。要启用它,请启动系统偏好设置,选择共享,并启用网络共享。如果您还没有这么做,点击创建个人网站文件夹来为您的主目录设置一个网络文件夹。默认情况下,这个文件夹名为Sites,这是我们构建应用程序的地方:

在 Mac OS X 上设置网络共享

共享面板将告诉您您的网络服务器 URL。记住这个,稍后用得着。

在 Microsoft Windows 上安装网络服务器。

如果您正在运行 Microsoft Windows,您可能正在运行 Microsoft 的互联网信息服务器IIS)。您可以进入控制面板,选择以下任一选项来查看:

如果您没有安装 IIS,或者不熟悉其操作,建议您安装 Apache 服务器,与本书配合使用。这将使我们能够在示例中为 Mac 和 PC 提供一致的指导。

安装 Apache 的最简单方法之一是下载并安装 XAMPP 软件包(www.apachefriends.org/en/xampp-windows.html)。这个软件包包括 Apache 以及 PHP 和 MySQL。这些额外的程序在您的技能增长时会有帮助,让您能够创建更复杂的程序和数据存储选项。

下载并运行 XAMPP 后,系统会提示您运行 XAMPP 控制面板。您还可以从 Windows开始菜单运行 XAMPP 控制面板。您应该点击控制面板中的开始按钮来启动您的网络服务器。如果您从防火墙软件收到通知,您应该选择允许 Apache 连接到互联网的选项。

在 Microsoft Windows 上安装网络服务器

在您安装 XAMPP 的文件夹中,有一个名为htdocs的子目录。这是我们将要设置 Sencha Touch 的网页文件夹。完整路径通常是C:\xampp\htdocs。您的网页服务器 URL 将是http://localhost/;您想要记住这个步骤。

下载并安装 Sencha Touch 框架

在您的网页浏览器中,访问www.sencha.com/products/touch/,然后点击下载按钮。将 ZIP 文件保存到一个临时目录中。

注意

请注意,本书中的所有示例都是使用 Sencha Touch Version 2.1.1 编写的。

您下载的文件解压缩将创建一个名为sencha-touch-version的目录(在我们的案例中,它是sencha-touch-2.1.1)。将此目录复制到您的网页文件夹中并重命名它,删除版本号,只留下sencha-touch

现在,打开您的网页浏览器并输入您的网页 URL,在末尾添加sencha-touch/examples。您应该看到以下 Sencha Touch 演示页面:

下载并安装 Sencha Touch 框架

恭喜您!您已成功安装了 Sencha Touch。

这个演示页面包含了应用程序的示例以及组件的简单示例。

用于使用 Sencha Touch 开发的额外工具

除了配置网页服务器和安装 Sencha Touch 库之外,还有一些开发工具您可能在深入您的第一个 Sencha Touch 应用程序之前想要了解一下。Sencha 还拥有几款其他可能对您的 Sencha Touch 应用有用的产品,还有不少第三方工具可以帮助您开发和部署您的应用。我们不会详细介绍如何设置和使用它们,但这些工具绝对值得一探。

Safari 和 Chrome 开发者工具

编写代码时,能够看到幕后发生的情况通常非常有帮助。与 Sencha Touch 一起工作的最关键工具是 Safari 和 Chrome 开发者工具。这些工具将以多种方式帮助您调试代码,我们将在书中进一步详细介绍它们。现在,让我们快速了解一下以下四个基本工具,它们将在以下章节中解释。

提示

对于 Safari 用户,您可以通过前往编辑 | 偏好设置 | 高级来启用 Safari 开发者菜单。在菜单栏中勾选显示开发复选框。一旦启用此菜单,您可以看到所有可用的开发者工具。对于 Chrome 用户,这些工具可以通过查看 | 开发者 | 开发者工具菜单访问。

JavaScript 控制台

JavaScript 控制台显示错误和控制台日志,这为您提供了出错时的指示。

JavaScript 控制台

注意我们在这里得到两方面的信息:错误和错误发生的文件。你可以点击文件名查看错误发生的确切行。你应该花些时间熟悉 Chrome 或 Safari 中的控制台。你可能会在这里花很多时间。

网络标签页

第二个有用的工具是网络标签页。这个标签页会显示在网页浏览器中加载的所有文件,包括浏览器尝试加载但无法找到的任何文件的错误。

网络标签页

丢失的文件显示为红色。点击一个文件会显示更多详细信息,包括传递给文件的任何数据和返回的数据。

网络检查器

网络检查器允许你检查页面中显示的任何元素的底层 HTML 和 CSS。下面的屏幕截图展示了网络检查器:

网络检查器

网络检查器在寻找应用程序中的显示和定位错误时特别有用。你可以在 Chrome 中点击放大镜或 Safari 中的指针来选择页面上的任何元素并查看显示逻辑。

资源标签页

资源标签页显示浏览器为我们应用程序存储的信息。这包括我们存储在本地任何数据的信息,以及我们为这个应用程序创建的任何饼干的详细信息,如下面的屏幕截图所示:

资源标签页

从这一标签页中,你可以双击一个项目来编辑它,或者右键点击删除它。

随着书籍的进展,我们将更详细地查看这些工具,并展示一些额外的用途和技巧。

Safari 开发者工具的完整讨论可以在developer.apple.com/technologies/safari/developer-tools.html找到。

Chrome 开发者工具的介绍可以在developers.google.com/chrome-developer-tools/找到。

其他 Sencha 产品

Sencha 提供了几款可以加速代码开发甚至扩展 Sencha Touch 功能的产品。

Sencha Cmd

Sencha Cmd 是一个命令行工具,允许你从命令行提示符生成基本的 Sencha Touch 文件。它还可以让你编译应用程序供 Web 使用,或者编译成二进制文件,你可以在各种应用程序商店中销售。

在本书中,我们将多次使用 Sencha Cmd。你可以从以下网站下载它:

www.sencha.com/products/sencha-cmd/download

Sencha Architect

Sencha Architect 是 Sencha Touch 和 ExtJS 应用的集成开发环境IDE)。Sencha Architect 允许您在一个图形化环境中构建应用程序,通过拖放控件到屏幕上。您可以以多种方式排列和操作这些组件,而 Sencha Architect 会为您编写底层代码。您可以从以下网站下载:

Sencha Architect

Sencha Animator

Sencha Touch 带有一些内置动画;但是,对于更复杂的动画,需要一个更强大的应用程序。使用 Sencha Animator 桌面应用程序,您可以创建与 Flash 动画相媲美的专业动画。然而,与 Flash 动画不同,Sencha Animator 动画可以在大多数移动浏览器上运行,使其成为为您的 Sencha Touch 应用程序添加额外魅力完美的选择。您可以在以下网站下载 Sencha Animator:

Sencha Animator

第三方开发者工具

您还可以选择多种开发者工具,这些工具在开发您的 Sencha Touch 应用时可能会有所帮助。

Notepad++

Notepad++是一个代码编辑器,非常适合编写 JavaScript 代码。它具有某些有用功能,如语法高亮、语法折叠、多视图和多语言环境以及多文档。这是一个免费且开源的工具,可在Notepad++上获得。这只适用于 Windows 和 Linux 操作系统。

WebStorm

WebStorm 是一个 IDE(代码编辑器),用于开发使用 JavaScript 等语言的网页应用程序。WebStorm 适用于 Windows、OS X 和 Linux。Webstorm 提供 30 天免费试用,并可选商业、个人和教育用途的许可选项。您可以在以下网站找到它:

WebStorm

Xcode 5

Xcode 5 是苹果完整的开发环境,旨在为任何苹果平台(OS X、iPhone 或 iPad)的开发者提供支持。因此,它包含很多对于编写 Sencha Touch 应用程序来说实际上并不必要的组件。然而,Xcode 5 中包含的一个对于 Sencha Touch 开发者可能非常有用的工具是 iOS 模拟器。使用 iOS 模拟器,您可以在不实际拥有它们的情况下在各种 iOS 设备上测试您的应用程序。

大多数使用 Xcode 5 的用户需要加入苹果开发者计划(比如在应用商店销售应用程序)。然而,iOS 模拟器任何人都可以使用。您可以从以下网站下载 Xcode 5:

Xcode

Android 模拟器

Android 模拟器是 Xcode 5 中 iOS 模拟器的 Android 对应程序。Android 模拟器是免费下载的 Android SDK 的一部分,地址为developer.android.com/guide/developing/devices/emulator.html。Android 模拟器可以配置为模仿许多具体的 Android 移动设备,使你能够跨广泛的设备测试你的应用程序。

YUI 测试

编程的任何一部分都包括测试。YUI 测试是雅虎的 YUI JavaScript 库的一部分,它允许你创建和自动化单元测试,就像 JUnit 对 Java 做的那样。单元测试为特定代码段设置测试用例。然后,如果将来这段代码发生了变化,可以重新运行单元测试,以确定代码是否仍然成功。这非常有用,不仅用于查找代码中的错误,而且用于在发布之前确保代码质量。YUI 测试可以在以下网址找到:

yuilibrary.com/yui/docs/test/

Jasmine

Jasmine 是一个类似于 YUI 测试的测试框架,只不过它是基于行为驱动设计BDD)。在 BDD 测试中,你从规格开始——关于你的应用程序在某些场景下应该做什么的故事——然后编写符合这些规格的代码。YUI 测试和 Jasmine 都达到了测试你代码的相同目标,它们只是以不同的方式做到这一点。你可以在以下网址下载 Jasmine:

pivotal.github.com/jasmine/

JSLint

可能是这个列表中最有用的 JavaScript 工具,JSLint将检查你的代码中的语法错误和代码质量。由 JavaScript 的两位之父之一 Douglas Crockford 编写,JSLint 将详细检查你的代码,这对于在部署代码之前找到错误非常有帮助。你可以在以下网址找到更多信息:

www.jslint.com/lint.html

总结

在本章中,我们介绍了 web 应用程序框架的基础知识以及为什么应该使用 Sencha Touch。我们带你了解如何设置开发环境和安装 Sencha Touch 库。我们还简要了解了移动设备的限制以及如何克服它们。我们还简要了解了在开发移动应用程序时应该注意的事情。我们还探讨了在移动应用程序开发中有用的其他工具:

在下一章中,我们将创建第一个 Sencha Touch 应用程序,在这个过程中,我们将学习如何使用 Sencha Touch 开发和 MVC 框架的基本知识。

第二章:创建一个简单的应用

在本章中,我们将带领大家了解如何在 Sencha Touch 中创建一个简单应用的基础知识。我们将涵盖大多数 Sencha Touch 应用中使用的的基本元素,并查看你可能会在自己的应用中使用的更常见的组件:容器、面板、列表、工具栏和按钮。

本章将涵盖以下主题:

  • 使用 Sencha Cmd 创建基本应用

  • 理解应用的文件和文件夹

  • 修改应用

  • 控制应用的布局

  • 测试和调试应用

  • 更新生产环境中的应用

让我们学习如何设置一个基本的 Sencha Touch 应用。

设置应用

在开始之前,你需要确保你已经根据前章的概要正确地设置了你的开发环境。

注意

根目录

如前章所述,为了允许网络服务器找到它们,你需要将你的应用文件和文件夹放在你本地机器上正确的文件夹中。

在 Mac 机器上,如果你使用网络共享,这将是你家目录下的Sites文件夹。如果你使用 MAMP,位置是/Applications/MAMP/htdocs

在 Windows 上,这将是在前章中描述的安装 XAMPP 后的C:\xampp\htdocs

在本书的其余部分,我们将把这个文件夹称为根目录。

在 Sencha Touch 的先前版本中,你必须手动设置你的目录结构。为了使这个过程变得稍微容易一些并且更加一致,Sencha 现在建议使用 Sencha Cmd 来创建初始应用结构。

使用 Sencha Cmd 入门

正如前章所提到的,Sencha Cmd 是一个命令行工具,它允许你从命令行生成许多基本的 Sencha Touch 文件。

首先,你需要从以下链接下载一个 Sencha Cmd 的副本:www.sencha.com/products/sencha-cmd/download

在 Windows 或 Mac 上,下载安装程序后可以运行它,然后按照安装 Sencha Cmd 的提示操作。

一旦你安装了 Sencha Cmd,你可以以以下方式在你的电脑上打开命令行提示:

  • 在 Mac OS X 上,前往Applications/Utilities并启动终端

  • 在 Windows 上,点击开始 | 运行,然后输入cmd

一旦命令行可用,输入sencha,你应该会看到类似以下内容:

Sencha Cmd 入门

这告诉你命令是否成功,并提供了一些 Sencha Cmd 的基本帮助选项。实际上,我们将使用这个帮助部分列出的第一个命令来生成我们的新应用:

sencha -sdk /path/to/sdk generate app MyApp /path/to/myapp

这个命令有七个部分,所以我们逐一来看看:

  • sencha:这告诉命令行将处理命令的应用程序名称;在这个例子中,Sencha Cmd,或者简称sencha

  • -sdk:这告诉 Sencha Cmd 我们将指定我们 Sencha Touch 库的路径。我们也可以直接将目录更改为我们下载这些库的文件夹,从而省略-sdk部分以及随后的路径信息。

  • /path/to/sdk:这将被替换为我们下载的 Sencha Touch 库文件的实际路径(不是 Sencha Cmd,而是实际的 Sencha Touch 库)。

  • generate:这表明了我们接下来要做什么。

  • app:由于我们将要生成一些东西,那么我们将要生成什么?这一部分的命令回答了这个问题。在这个例子中,我们将要生成一个应用。

  • MyApp:你的应用将被称为这个名字。它还将用于我们稍后介绍的 JavaScript 命名空间。这是任意的,但必须是一个没有空格的单个单词。

  • /path/to/myapp:这将是你的新应用的路径。这个路径应该在我们之前提到的根目录中的一个新文件夹里。

在本章节,我们将要创建一个名为TouchStart的应用。你的路径信息需要反映你个人的设置,但命令应该看起来类似于这样:

sencha -sdk /Users/12ftguru/Downloads/touch-2.2.1 generate app TouchStart /Applications/MAMP/htdocs/TouchStart

根据你的 Sencha Touch 库和根目录的位置调整你的路径。命令一旦执行,你将在终端中看到如下方式出现的一系列信息:

使用 Sencha Cmd 开始

Sencha Cmd 复制它需要的文件并设置你的应用。一旦命令执行,你应该在根目录中有一个名为TouchStart的新文件夹。

打开那个文件夹,你会看到以下文件和目录:

使用 Sencha Cmd 开始

我们将几乎完全与app目录中的文件一起工作,但了解这些文件和目录的每个部分还是值得的:

  • app:本章节我们将详细介绍这个目录,这是我们的所有应用文件所在的地方。

  • app.js:这个 JavaScript 文件设置我们的应用并在应用启动时处理初始化。我们将在下一节更详细地查看这个文件。

  • build.xml:这是一个编译应用程序的配置文件。你可能不需要更改这个文件。

  • index.html:这个文件与任何网站的index.html文件类似。它是浏览器加载的第一个文件。然而,与传统的网站不同,我们应用的index.html文件只加载我们的初始 JavaScript,然后什么都不做。你不需要更改这个文件。

  • packager.json:这个配置文件告诉我们应用如何设置文件以及它们的所在位置。大部分情况下,你可能不需要更改这个文件。

  • packagespackages目录是一个占位符,你可以在这里为你的应用程序安装额外的包。在这个阶段,它基本上是未被使用的。

  • resourcesresources目录包含我们的 CSS 文件和启动屏幕及图标。我们将在下一章关于样式的内容中了解更多关于这个目录的信息。

  • touch:这个目录包含 Sencha Touch 库文件的副本。它绝不应该被修改。

我们也可以通过访问我们的网页目录,在网络浏览器中查看我们新的应用程序。对于 Windows 和 MAMP 用户,这将是在http://localhost/TouchStart,而对于启用了网络共享的 Mac 用户,则是在http://localhost/~username/TouchStart

Sencha Cmd 入门

提示

值得一提的是,Sencha Cmd 本身就有一个内置的网络服务器,你可以用它来查看你的 Sencha Touch 应用程序。你可以使用以下命令启动 Sencha Cmd 网络服务器:

sencha fs web -port 8000 start –map /path/to/your/appfolder 

然后,你可以通过访问http://localhost:8000来打开你的网络浏览器。

有关使用 Sencha Cmd 网络服务器的更多信息,请访问docs.sencha.com/cmd/3.1.2/#!/guide/command.

从已经创建的基本应用程序中,我们可以看到的是位于app/viewMain.js文件的内容。我们可以修改这个文件,并在重新加载页面时看到结果。

在我们开始修改Main.js文件之前,我们需要先查看一下为我们加载所有内容的文件,即app.js

创建 app.js 文件

app.js文件负责设置我们的应用程序,虽然我们不需要经常修改它,但了解它做什么以及为什么这样做是个好主意。

在你的代码编辑器中打开app.js文件;在顶部,你会看到一大段注释(你应该阅读并熟悉它)。在注释下方,代码以:

Ext.Loader.setPath({
    'Ext': 'touch/src'
});

这告诉应用程序我们的 Sencha Touch 库文件位于哪里。

接下来,我们用以下代码定义我们的应用程序:

Ext.application({
    name: 'TouchStart',

    requires: [
        'Ext.MessageBox'
    ],

    views: [
        'Main'
    ],

    icon: {
        '57': 'resources/icons/Icon.png',
        '72': 'resources/icons/Icon~ipad.png',
        '114': 'resources/icons/Icon@2x.png',
        '144': 'resources/icons/Icon~ipad@2x.png'
    },
    isIconPrecomposed: true,

    startupImage: {
        '320x460': 'resources/startup/320x460.jpg',
        '640x920': 'resources/startup/640x920.png',
        '768x1004': 'resources/startup/768x1004.png',
        '748x1024': 'resources/startup/748x1024.png',
        '1536x2008': 'resources/startup/1536x2008.png',
        '1496x2048': 'resources/startup/1496x2048.png'
    },
    launch: function() {
        // Destroy the #appLoadingIndicator element
        Ext.fly('appLoadingIndicator').destroy();

        // Initialize the main view
        Ext.Viewport.add(Ext.create('TouchStart.view.Main'));
    }
});

一口气吃下这么多代码确实有些多,所以让我们一步一步来理解。

第一部分,Ext.Application({…});,为 Sencha Touch 创建了一个新的应用程序。大括号之间的所有内容都是这个新应用程序的配置选项。虽然一个应用程序有很多配置选项,但大多数至少包括应用程序的名称和启动函数。

注意

命名空间

使用他人代码时的一个最大问题就是命名问题。例如,如果你正在使用的框架有一个名为Application的对象,而你又创建了一个名为Application的自定义对象,这两个对象的功能将会发生冲突。Sencha Touch 使用命名空间的概念来防止这些冲突的发生。

在此案例中,Sencha Touch 使用了命名空间Ext。你会在本书的代码中看到这个命名空间被广泛使用。这只是一个消除框架对象和代码以及您自己的对象和代码之间潜在冲突的方式。

Sencha 将自动为您自己的代码设置命名空间,作为新Ext.Application对象的一部分。在此案例中,它将是TouchStart,我们用它来生成我们的应用程序。

Ext也是 Sencha 的 Web 应用程序框架ExtJS的名称之一。Sencha Touch 使用相同的命名空间约定,让开发者熟悉一个库,并容易理解另一个库。

当我们创建一个新应用程序时,我们需要向其传递一些配置选项。这将告诉应用程序如何外观以及要做什么。这些配置选项包含在花括号{}内,并用逗号分隔。第一个选项是:

name: 'TouchStart'

这将我们应用程序的名称设置为引号之间的任何内容。name值不应包含空格,因为 Sencha 也使用这个值为您自己的代码对象创建命名空间。在此案例中,我们称之为应用程序TouchStart

name选项之后,我们有一个requires选项:

requires: [
        'Ext.MessageBox'
    ]

这是我们列出任何文件的地方,这些文件在应用程序启动时就需要。由于我们实际上在文件的底部使用了Ext.Msg.confirm函数,所以我们不得不在这里包含Ext.MessageBox类。

接下来,我们有views部分:

views: [
        'Main'
    ]

本节作为app/view文件夹中Main.js文件的参考。我们也可以在这里为controllersstoresmodels列出清单,但目前,这个骨架应用中唯一的一个是Main.js视图文件。我们将在后面的章节中了解更多关于控制器、模型、存储器和视图的内容。

iconstartupImage部分提供了用于应用程序图标和启动屏幕的图像文件的链接。列出的大小确保了应用程序的图像能够在多种设备上正确显示。

下一个选项是事情开始变得有趣的地方:

launch: function() {
  // Destroy the #appLoadingIndicator element
  Ext.fly('appLoadingIndicator').destroy();

  // Initialize the main view
  Ext.Viewport.add(Ext.create('TouchStart.view.Main'));
}

launch函数在所需的 JavaScript 文件(以及任何列出的视图、模型、存储器和控制器)加载完成后执行。这个函数首先销毁我们的加载指示器(因为我们已经完成加载文件)。然后创建我们的主视图并将其添加到视口。视口是我们向用户显示内容的地方。

在此情况下,TouchStart.view.Main指的是app/view文件夹中的Main.js文件。这就是 Sencha Touch 如何查找文件的方式:

  • TouchStart是我们应用程序的一部分。

  • viewviews文件夹

  • Main是我们的Main.js文件。

让我们 closer 看看这个Main.js文件,看看它是如何创建我们目前看到的骨架应用程序中的所有视觉元素的。

创建 Main.js 文件

Main.js文件位于app/view文件夹中。view文件是我们应用程序的视觉组件。让我们打开这个文件,看看如何创建一个简单的标签面板。

Ext.define('TouchStart.view.Main', {
    extend: 'Ext.tab.Panel',
    xtype: 'main',
    requires: [
        'Ext.TitleBar',
        'Ext.Video'
    ],
    config: {
        tabBarPosition: 'bottom',

        items: [
            …
        ]
    }
});

我们从我们的代码示例中删除了items部分的内容,以使这更容易阅读。

前代码的的第一行和第二行在您在 Sencha Touch 中创建的几乎每个组件中都是通用的。

Ext.define('TouchStart.view.Main', {
    extend: 'Ext.tab.Panel'

第一行定义了组件的全名,格式如下:

  • 应用程序名称(命名空间)。

  • 文件夹名称。

  • 文件名(无扩展名)。

接下来,我们列出我们要扩展的组件;在这个例子中,是一个标签面板。您会在本书中看到这个定义/扩展模式。

您还会注意到,标签面板被称为Ext.tab.Panel。这使得 Sencha Touch 知道该组件是一个本地组件(Ext),位于名为tab的文件夹中的一个名为Panel.js的文件中。这种模式允许 Sencha Touch 加载正确的文件,并使用我们新的配置选项对其进行扩展:

xtype: 'main',
requires: [
   'Ext.TitleBar',
   'Ext.Video'
],
config: {
   tabBarPosition: 'bottom'

我们做的第一件事是为我们的新组件设置一个xtype值。xtype部分是一个简短的名字,允许我们轻松地引用和创建我们组件的副本,而不必使用完整名称。您稍后在本书中会看到一些这样的例子。

我们的骨架应用程序使用了一个TitleBar和一个Video组件,因此我们需要这两个文件。

接下来,我们设置了一个config部分。这个部分用于为我们新组件设置任何自定义设置。在这个例子中,我们将我们的标签栏定位在底部。

现在,我们想看看我们从代码示例中删除的items部分,并研究这个部分对我们的标签面板有什么影响。

探索标签面板。

Ext.tab.Panel被设计为自动为我们做几件事情。最重要的是,对于我们在items部分添加的每一个面板,都会自动为我们在标签面板中创建一个对应的标签。默认情况下,只显示第一个面板。然而,标签面板也会在我们点击面板的标签时自动切换这些面板。

如果您回过头来看我们在浏览器中的应用程序,您还会看到每个标签页都有一个标题和一个图标。这两个config选项是作为当前看起来类似于这样的个别项目设置的:

items: [
   {
      title: 'Welcome',
      iconCls: 'home',
      styleHtmlContent: true,
      scrollable: true,
         items: {
            docked: 'top',
            xtype: 'titlebar',
            title: 'Welcome to Sencha Touch 2'
         },
      html: [
         "You've just generated a new Sencha Touch 2 project. What you're looking at right now is the ",
         "contents of <a target='_blank' href=\"app/view/Main.js\">app/view/Main.js</a> - edit that file ",
         "and refresh to change what's rendered here."
      ].join("")
   },
   {
      title: 'Get Started',
      iconCls: 'action',
      items: [
         {
            docked: 'top',
            xtype: 'titlebar',
            title: 'Getting Started'
         },
         {
            xtype: 'video',
            url: 'http://av.vimeo.com/64284/137/87347327.mp4?token=1330978144_f9b698fea38cd408d52a2393240c896c',
            posterUrl: 'http://b.vimeocdn.com/ts/261/062/261062119_640.jpg'
         }
      ]
   }
]

请注意,我们的items列表用括号括起来,列表中的各个组件用花括号包含。这种嵌套组件结构是 Sencha Touch 的关键部分,您会在本书的各个章节中看到它的使用。

titleiconCls属性控制了每个条目中标签的外观。我们的标题目前设置为WelcomeGetting Started。我们的iconCls配置决定了标签中使用的图标。在这种情况下,我们使用了两个默认图标:homeaction

我们的面板是Welcome面板,它有配置选项,允许我们使用带样式的 HTML 内容并使其可滚动(如果内容大于屏幕大小)。html配置选项里面的文本是我们看作是第一个面板的内容的。

你会注意到我们的面板也有它自己的项目。在这种情况下,有一个titlebar将会被docked在我们的面板的top上,标题是“欢迎使用 Sencha Touch 2”。

我们的第二个Get Started面板里面有 two items:一个像我们第一个面板一样的titlebar和一个video组件,它列出视频的 URL 和另一个posterUrl,这是在用户播放视频前会显示的图片。

正如我们第一个面板中的文本所提到的,我们可以更改这个文件的内容,当我们重新加载页面时,就能看到结果。让我们试一试,看看它是如何工作的。

添加一个面板

我们想要做的第一件事是删除我们标签面板中items括号[ ]之间的所有内容。接下来,我们将添加一个类似的新面板:

items: [
  {
    title: 'Hello',
    iconCls: 'home',
    xtype: 'panel',
    html: 'Hello World'
  }
]

如果我们现在重新加载浏览器,我们看到这个:

添加一个面板

由于我们现在只有一个面板,所以我们只有一个标签。我们还移除了标题栏,所以我们页面顶部没有什么东西。

小贴士

细心的读者还会注意到,我们在这个例子中明确为panel设置了一个xtype值。标签面板会自动假设,如果你没有为它的一个项目指定xtype值,那么它就是个面板。然而,设定组件使用的xtype值是一个好习惯。我们将在第四章,组件和配置中更多地讨论 xtype。

现在,我们的面板非常简单,只包含一行文本。在现实世界中,应用程序很少有这么简单。我们需要一种在我们面板内安排不同元素的方法,这样我们就可以创建现代、复杂的布局。幸运的是,Sencha Touch 有一个内置的配置叫做layout,这将帮助我们实现这一点。

用布局控制外观

布局为您提供了一系列在容器内安排内容的选择。Sencha Touch 为容器提供了五种基本布局:

  • fit:这是一个单一项目的布局,它会自动扩展以占据整个容器。

  • hbox:这使得项目在容器内水平排列。

  • vbox:这使得项目在容器内垂直排列。

  • card:这像是一叠卡片一样排列项目,最初只显示活动卡片。

  • docked:这使得项目在显示区域的顶部或底部或左侧或右侧。

在我们之前的例子中,我们没有声明布局。通常,你总是想要为任何容器声明一个布局。如果你不这么做,容器内的组件在出现时可能不会适当地调整大小。

我们已经看到了最后两种布局。标签面板使用card布局在它的items列表中切换不同的面板。

我们原始的Main.js文件中的标题栏有一个docked属性作为它们配置的一部分。这个配置将它们停靠到屏幕的特定部分。你甚至可以将多个项目停靠到一个面板的四个边之一。

例如,如果我们向我们的当前面板的items部分添加如下内容:

items: [
  {
    xtype: 'titlebar',
    docked: 'top',
    title: 'About TouchStart'
  },
  {
    xtype: 'toolbar',
    docked: 'top',
    items: [
      {
        xtype: 'button',
        text: 'My Button'
      }
    ]
  }
]

这两个栏将以下方式堆叠在一起:

使用布局控制外观

使用适合布局

让我们添加第二个面板来理解我们之前做了什么。在我们第一个面板的闭合花括号后,加上一个逗号,然后添加以下代码:

{
  title: 'Fit',
  iconCls: 'expand',
  xtype: 'panel',
  layout: 'fit',
  items: [
    {
    xtype: 'button',
    text: 'Very Fit'
    }
  ]
}

对于这个面板,我们添加了一个config选项,layout: 'fit',以及一个items部分,里面有一个按钮。

使用适合布局

正如前一个屏幕截图所示,这给了我们第二个标签页,其中包含我们新添加的按钮。由于布局被设置为适合,按钮会扩展以占据所有可用的空间。虽然当你想要一个组件占据所有可用空间时这很有用,但如果你想要嵌套多个组件,它就不会表现得很好。

使用 vbox 布局

vbox布局从上到下堆叠组件。在这个例子中,多个组件将填满可用的屏幕空间。让我们添加另一个面板来看看这是什么样子。像之前一样,在我们最后一个面板的闭合花括号后,加上一个逗号,然后添加以下代码:

{
  title: 'VBox',
  iconCls: 'info',
  xtype: 'panel',
  layout: 'vbox',
  items: [
    {
      xtype: 'container',
      flex: 2,
      html: '<div id="hello">Hello World Top</div>',
      style: 'background:red',
      border: 1
    }, {
      xtype: 'container',
      flex: 1,
      html: '<div id="hello">Hello World Bottom</div>',
      style: 'background:yellow',
      border: 1
    }, {
      xtype: 'container',
      height: 50,
      html: '<div id="footer">Footer</div>',
      style: 'background:green',
    }

  ]
}

正如你所看到的,这个面板有一个layout: 'vbox'的配置和一个三个items的列表。这些项目是我们想要包含在我们panel内的container组件的集合。

container组件是panel的简化版,它没有工具栏或标题栏等元素的选项。

我们的前两个容器有一个叫做flex的配置。flex配置是vboxhbox布局所特有的(我们会在后面马上讲到hbox)。flex配置控制组件在整体布局中占用的比例空间。你也许还注意到最后一个容器没有flex配置。相反,它有height: 50vbox布局会解释这些值来按以下方式布局容器:

  1. 由于我们有一个高度为50的组件,vbox布局将把这个组件的高度留为 50 像素。

  2. vbox布局然后将其他两个组件的flex值作为比例。在这个例子中,2:1。

  3. 最终结果是在屏幕底部的一个 50 像素高的容器。其他两个容器将占据剩余的可用空间。顶部容器也将是中间容器两倍高。

为了使这些大小更清晰,我们还给每个容器添加了一个样式,以颜色背景并使其稍微突出。结果如下:

使用 vbox 布局

这种布局在窗口大小调整时也会缩小和扩大,使其成为适应各种设备尺寸非常有效的布局。

使用 hbox 布局

hbox布局的运作方式几乎与vbox布局相同,不同之处在于hbox布局中的容器是从左到右排列的。

你可以通过复制我们之前的vbox示例并将其粘贴在我们items列表中的最后一个面板之后来添加一个具有hbox布局的面板(不要忘记在项目之间加上逗号)。

接下来,我们需要修改我们新面板中的几个配置:

  • title: 'VBox'设置为title: 'HBox'

  • layout: 'vbox'设置为layout: 'hbox'

  • 在最后一个container中,将height: 50设置为width: 50

当你重新加载页面时,你应该能够点击HBox标签,并看到以下类似屏幕截图:

使用 hbox 布局

你可以嵌套这些基本布局以以任何方式安排你的组件。我们还将介绍一些在第三章中样式化用户界面的方法样式化用户界面

测试和调试应用程序

在测试应用程序时,首先要查找错误控制台的地方。在 Safari 中,从开发菜单中选择显示错误控制台。在 Chrome 中,从查看菜单中选择开发者,然后选择JavaScript 控制台

测试和调试应用程序

解析错误

前一个屏幕截图中的错误控制台告诉我们两件非常重要的事情。首先,我们有一个语法错误:解析错误。这意味着代码中的某个地方,我们做了浏览器无法理解的事情。通常,这可能是因为:

  • 忘记关闭一个括号、方括号或花括号,或者添加了一个多余的

  • 在配置选项之间没有逗号,或者添加了多余的逗号

  • 在变量声明的末尾遗漏了一个分号

  • 没有关闭引号或双引号(也没有在必要的地方转义引号)

第二个重要信息是 /app/TouchStart-4.js: 39。它告诉我们:

  • /app/TouchStart-4.js 是发生错误的文件

  • 39 是发生错误的行

使用这些信息,我们应该能够快速追踪到错误并修复它。

区分大小写

JavaScript 是一种区分大小写的语言。这意味着如果你输入xtype: 'Panel',你将在错误控制台中得到以下内容:

尝试创建一个具有未注册 xtype 的组件:Panel

这是因为 Sencha Touch 期望panel而不是Panel

丢失文件

另一个常见的问题是丢失文件。如果你没有正确地将你的index.html文件指向你的sencha-touch-debug.js文件,你会得到两个不同的错误:

  • 加载资源失败:服务器响应状态为 404(未找到)

  • 引用错误:找不到变量:Ext

第一个错误是关键信息;浏览器找不到您尝试包含的文件之一。第二个错误是由缺少的文件引起的,它简单地抱怨找不到Ext变量。在这种情况下,是因为缺少的文件是sencha-touch-debug.js,它首先设置了Ext变量。

网络检查器控制台

另一个对于调试应用程序非常有用的 Safari 网络检查器功能是控制台。在您的app.js文件中,添加以下命令:

console.log('Creating Application');

在这行Ext.Application之前添加它:

Ext.Application({

您应该在网页检查器的控制台标签中看到创建应用的文本。您还可以向控制台发送变量,以查看它们的 contents:

console.log('My viewport: %o', Ext.Viewport);

如果您在app.js中的这行Ext.Viewport.add(Ext.create('TouchStart.view.Main'));之后放置这个控制台日志,控制台将显示完整的视图和所有嵌套的子组件。如果您有组件显示不正常的原因,这很有用。将对象发送到控制台允许您以 JavaScript 的方式查看对象。

注意

有关 Chrome 开发者工具的更多信息,请访问developers.google.com/chrome-developer-tools/

如果您想了解更多关于使用 Safari 网络检查器调试您应用程序的信息,请访问苹果公司的调试您的网站页面:developer.apple.com/library/safari/#documentation/AppleApplications/Conceptual/Safari_Developer_Guide/DebuggingYourWebsite/DebuggingYourWebsite.html

为生产更新应用程序

当一个应用程序准备好投入生产时,通常需要进行许多步骤来准备和优化您的代码。这个过程包括压缩 JavaScript 以加快加载速度,优化图像,以及删除代码库中实际上您的应用程序不需要的部分。这可能是一个相当繁琐的过程,但 Sencha Cmd 将实际上用一个命令为您完成这个任务。

当您准备好更新您的应用程序以用于生产时,您可以打开您的命令行,并使用cd命令将您的代码根目录移动到:

cd /path/to/my/application

一旦您进入该目录,您可以输入以下命令:

sencha app build

此命令将在其中创建一个build目录,里面有您应用程序的优化版本。您可以测试这个优化版本是否有任何错误。如果您需要更改应用程序,您可以对未优化的代码进行更改,然后再次运行build命令。

一旦您对代码构建感到满意,就可以将应用程序投入生产。

将应用程序投入生产

既然你已经编写了并测试了你的应用程序并为其生产做好了准备,我们需要弄清楚我们的代码将存放在哪里。由于将应用程序投入生产的方法将根据您的设置而有所不同,我们将非常一般性地介绍这个任务。

首先要熟悉将应用程序投入生产的三个基本部分:

  • 网页托管

  • 文件传输

  • 文件夹结构

虽然在本地的 Web 服务器上开发应用程序是可以的,但如果您想让其他人看到它,您需要一个可以持续连接到互联网的公共可访问的 Web 服务器。有许多网页托管提供商,例如 GoDaddy、HostGator、Blue Host、HostMonster 和 RackSpace。

由于我们的应用程序是纯 HTML/JavaScript/CSS,您不需要任何花哨的插件,例如数据库或服务器端编程语言(PHP 或 Java),在您的网页托管账户中。任何能够提供 HTML 页面的账户都足够了。这个决定的关键应该是客户支持。在选择提供商之前,确保检查评论。

托管提供商还将提供有关设置您的域名并将您的文件上传到 Web 服务器的信息。确保为将来参考保留好您的用户名和密码。

为了将您的应用程序复制到您的网页托管账户,你可能需要熟悉一个FTP文件传输协议)程序,例如FileZilla。与托管提供商一样,FTP 程序的选择非常多。它们中的大多数遵循一些基本规范。

一开始,您需要使用 FTP 程序连接到 Web 服务器。为此,您需要以下内容:

  • Web 服务器的名称或 IP 地址

  • 您的网页托管用户名和密码

  • Web 服务器的连接端口

您的网页托管提供商应该在您注册时提供这些信息。

将应用程序投入生产

一旦您连接到服务器,您将看到您本地机器上的文件列表以及您远程 Web 服务器上的文件。您需要将TouchStart文件拖到远程服务器上以进行上传。您的托管提供商还将为您提供这些文件需要去的特定文件夹的名称。该文件夹通常称为httpdhtdocshtmlpublic_html

这让我们考虑上传文件的最后一件事情:文件夹路径。

文件夹路径会影响应用程序定位其文件和资源的方式。当您将应用程序上传到远程 Web 服务器时,它可能会影响应用程序内部如何查看您的文件夹。如果您有从绝对路径引用的任何文件,例如http://127.0.0.1/~12ftguru/TouchStart/myfile.js,那么在您将东西移到 Web 服务器上时,文件将无法工作。

即使相对路径在将文件传输到远程服务器时也可能出现问题。例如,如果你有一个使用路径/TouchStart/myFile.js的文件,而你上传了TouchStart文件夹的内容而不是上传整个文件夹,文件路径将会错误。

如果你发现自己遇到图片缺失错误或其他错误,这是一个需要记住的事情。

再次强调,你的网页托管服务商是你获取信息最好的资源。一定要寻找入门指南文档,并且不要害怕在任何用户论坛寻求帮助,这些论坛你的托管服务商可能会有。

摘要

在这一章,我们使用 Sencha Cmd 创建了第一个简单应用。我们了解了一些 Sencha Touch 组件的基本知识,包括配置和组件之间的嵌套。我们向你介绍了TabPanelPanelContainer组件。此外,我们解释了一些基本的调试方法,并为我们应用的生产准备好了。

在下一章,我们将通过使用 SASS 和 Sencha Touch 库的样式工具为我们的应用创建一个自定义主题。

第三章:用户界面样式

现在我们已经了解了应用程序是如何组合在一起的,接下来我们将看看您可以使用的一些不同的视觉元素来定制您的应用程序。在本章中,我们将:

  • 仔细观察工具栏和按钮,使用布局,以及其他样式和图标来提升用户界面的视觉吸引力

  • 扩展我们之前关于图标的工作;这包括使用 Pictos 图标字体显示新图标

  • 讨论与不同设备和屏幕尺寸一起工作时的一些考虑和捷径

  • 使用 Sass 和 Compass 探索极其强大的 Sencha 主题引擎,以简单 CSS 样式命令创建复杂的视觉皮肤

样式组件与主题

在我们进入本章之前,了解样式化单个组件与创建主题之间的区别非常重要。

几乎 Sencha Touch 中的每一个显示组件都有设置自身样式的选项。例如,panel组件可以这样使用样式:

{ 
 xtype: 'panel',
 style: 'border: none; font: 12px Arial black',
 html: 'Hello World'
}

样式也可以使用如下方式作为对象设置:

{ 
 xtype: 'panel',
style : {
  'border' : 'none',
  'font' : '12px Arial black',
  'border-left': '1px solid black'
} 
html: 'Hello World'
}

提示

您会注意到在style块内部,我们对配置设置的两边都进行了引用。这仍然是 JavaScript 的正确语法,并且使用style块时这是一个非常好的习惯。这是因为许多标准 CSS 样式在其名称中使用连字符。如果我们不对border-left添加引号,JavaScript 会将此读作border减去left,并立即在错误堆中崩溃。

我们还可以为组件设置一个style类,并使用外部 CSS 文件如下定义该类:

{ 
 xtype: 'panel',
 cls: 'myStyle',
 html: 'Hello World'
}

您的外部 CSS 文件可以以如下方式控制组件的样式:

.myStyle {
 border: none;
 font: 12px Arial black;
}

这种基于类的显示控制被认为是最佳实践,因为它将样式逻辑与显示逻辑分开。这意味着当您需要更改边框颜色时,可以在一个文件中完成,而不是在多个文件中寻找单独的style设置。

这些样式选项对于控制个别组件的显示非常有用。还有一些样式元素,如边框、内边距和外边距,可以直接在组件的配置中设置:

{ 
 xtype: 'panel',
 bodyMargin: '10 5 5 5',
 bodyBorder: '1px solid black',
 bodyPadding: 5,
 html: 'Hello World'
}

这些配置可以接受一个数字以应用于所有边,或者是一个 CSS 字符串值,如1px solid black10 5 5 5。数字应不带引号输入,但 CSS 字符串值需要在引号内。

这些小的更改在样式化您的应用程序时可能会有所帮助,但如果您需要做一些更大的事情呢?如果您想要更改整个应用程序的颜色或外观呢?如果想要为按钮创建自己的默认样式呢?

这就是主题和 UI 样式发挥作用的地方。

工具栏和按钮的 UI 样式

让我们快速回顾一下在第二章,创建一个简单应用程序中创建的基本 MVC 应用程序,并使用它开始探索带有工具栏和按钮的样式。

首先,我们将向第一个面板添加一些内容,该面板包含我们的titlebartoolbar你好世界文本。

添加工具栏

app/views中,你会发现Main.js。打开编辑器中的这个文件,看看我们项目列表中的第一个面板:

items: [
  {
      title: 'Hello',
      iconCls: 'home',
      xtype: 'panel',
      html: 'Hello World',
      items: [
         {
            xtype: 'titlebar',
            docked: 'top',
            title: 'About TouchStart'
         }
     ]
  }...

我们将在现有工具栏的顶部添加第二个工具栏。定位items部分,在第一个工具栏的花括号后添加第二个工具栏,如下所示:

{

 xtype: 'titlebar', 
 docked: 'top',
 title: 'About TouchStart'
}, {
 docked: 'top',
 xtype: 'toolbar',
 items: [
  {text: 'My Button'}
 ]}

不要忘记在两个工具栏之间加上逗号。

提示

多余或缺少的逗号

在 Sencha Touch 中工作时,导致解析错误的最常见原因之一是多余或缺少逗号。当你移动代码时,请确保你已经考虑到了任何散落或丢失的逗号。幸运的是,对于这些类型的解析错误,Safari 错误控制台通常会给我们一个关于查看哪一行的好主意。一个更详细的常见错误列表可以在以下网址找到:

javascript.about.com/od/reference/a/error.htm

现在当你查看第一个标签页时,你应该看到我们新的工具栏,以及左侧的新按钮。由于两个工具栏都有相同的背景,它们有点难以区分。所以,我们将使用ui配置选项更改底栏的外观:

{
 docked: 'top',
 xtype: 'toolbar',
 ui: 'light',
 items: [
  {text: 'My Button'}
 ]
}

ui配置是 Sencha Touch 中特定样式集的简写。Sencha Touch 包含几个ui样式,我们将在本章后面向您展示如何创建自己的样式。

添加工具栏

样式按钮

按钮也可以使用ui配置设置,为此它们提供了几个不同的选项:

  • normal:这是默认按钮

  • back:这是一个左侧缩成一点的按钮

  • round:这是一个更急剧圆角的按钮

  • small:这是一个更小的按钮

  • action:这是一个默认按钮的更亮版本(颜色根据主题的活跃颜色而变化,我们稍后会看到)

  • forward:这是一个右侧缩成一点的按钮

按钮还内置了一些ui选项的颜色。这些颜色选项是confirmdecline。这些选项与前面的形状选项结合使用连字符;例如,confirm-smalldecline-round

让我们添加一些新按钮,看看这些按钮在我们的屏幕上看起来如何。在第二个工具栏中找到带有按钮的items列表:

items: [
  {text: 'My Button'}
]

用以下新的items列表替换那个旧的items列表:

items: [
 {
  text: 'Back',
  ui: 'back'
 }, {
  text: 'Round',
  ui: 'round'
 }, {
  text: 'Small',
  ui: 'small'
 }, {
  text: 'Normal',
  ui: 'normal'
 }, {
  text: 'Action',
  ui: 'action'
 }, {
  text: 'Forward',
  ui: 'forward'
 }
]

这将在工具栏顶部产生一系列按钮。正如您所注意到的,我们的所有按钮都靠左对齐。您可以通过在您想要推向右边的按钮前面添加一个spacer xtype 来将按钮移到右边。尝试通过在我们ForwardAction按钮之间添加以下内容来实现:

{ xtype: 'spacer'},

这将使Forward按钮移动到工具栏的右侧:

按钮样式

由于按钮实际上可以任何地方使用,我们可以在我们的标题栏添加一些按钮,并使用align属性来控制它们出现的位置。修改我们第一个paneltitlebar,并添加一个items部分,如下面的代码所示:

{
  xtype: 'titlebar',
  docked: 'top',
  title: 'About TouchStart',
  items: [
    {
      xtype: 'button',
      text: 'Left',
      align: 'left'
    },
    {
      xtype: 'button',
      text: 'Right',
      align: 'right'
    }
  ]
}

现在我们标题栏应该有两个按钮,一个在标题的每一边:

按钮样式

我们还在panel容器中添加一些按钮,以便我们可以看到ui选项confirmdecline的样子。

在我们HelloPanel容器的items部分末尾,位于第二个工具栏后面添加以下内容:

{
 xtype: 'button',
 text: 'Confirm',
 ui: 'confirm',
 width: 100
}, {
 xtype: 'button',
 text: 'Decline',
 ui: 'decline',
 width: 100
}

您可能会注意到,我们的面板按钮和工具栏按钮之间有两个不同之处。第一个是我们在我们面板中声明了xtype:'button',但在我们的工具栏中没有声明。这是因为工具栏假设它将包含按钮,而xtype只有在您使用除按钮之外的内容时才需要声明。面板没有设置默认的xtype属性,所以面板中的每个项目都必须声明一个。

第二个区别是我们为按钮声明了width。如果我们不在面板中使用按钮时声明width,它将扩展到面板的整个宽度。在工具栏上,按钮会自动调整大小以适应文本。

按钮样式

您还会注意到我们面板中的两个按钮粘在一起。您可以通过为每个按钮配置部分添加margin: 5来将它们分开。

这些简单的样式选项可以帮助使您的应用程序更易于导航,并为用户提供了关于重要或潜在破坏性操作的视觉提示。

标签栏

底部的标签栏也理解ui配置选项。在这种情况下,可用的选项是lightdark。标签栏还根据ui选项改变图标的外观;light工具栏将具有深色图标,而dark工具栏将具有浅色图标。

这些图标实际上是名为Pictos的特殊字体的一部分。Sencha Touch 从版本 2.2 开始使用 Pictos 字体,以解决某些移动设备上的兼容性问题,而不是使用图像图标。

注意

来自 Sencha Touch 先前版本的图标遮罩可用,但已在 2.2 版本中被弃用。

您可以在Ext.Button组件的文档中看到一些可用的图标:

docs.sencha.com/touch/2.2.0/#!/api/Ext.Button

如果你对 Pictos 字体感到好奇,你可以通过访问pictos.cc/了解更多相关信息。

Sencha Touch 主题

有时候你希望不仅仅改变一个单个的面板或按钮的外观。Sencha Touch 主题是快速改变应用程序整体外观和感觉的强大方式。我们将在本章后面覆盖主题化过程,但在开始之前我们需要做一些基础工作。需要覆盖的概念信息很多,但你所获得的灵活性将是值得努力的。

我们需要覆盖的第一个工具是 Sencha Touch 中用于使应用程序主题化可能的工具:Sass 和 Compass。

注意

如果你已经熟悉 Sass 和 Compass,你将会更舒适地先安装然后再覆盖概念。你可以跳到设置 Sass 和 Compass部分。

介绍 Sass 和 Compass

Syntactically Awesome Stylesheets (Sass)用于扩展标准 CSS,允许变量、嵌套、混合函数、内置函数和选择器继承。这意味着你的所有常规 CSS 声明都会正常工作,但你也会得到一些额外的福利。

在 Sass 中的变量

变量允许你定义具体的值,然后在样式表中使用它们。变量名称是任意的,以$开始。例如,我们可以使用 Sass 定义以下内容:

$blue: #4D74C1;
$red: #800000;
$baseMargin: 10px;
$basePadding: 5px;

我们可以在 Sass 文件中的标准 CSS 声明中使用以下变量:

.box1 {
border: 1px solid $blue;
padding: $basePadding;
margin: $baseMargin;
}

我们还可以按照以下方式使用基本数学函数:

.box2 {
border: 1px solid $blue;
padding: $basePadding * 2;
margin: $baseMargin / 2;
}

这将创建一个具有两倍内边距和原始盒子一半外边距的盒子。这对于创建灵活、可扩展的布局非常不错。通过更改你的基本值,你可以快速扩展你的应用程序以应对具有多种分辨率和屏幕尺寸的多台设备。

另外,当你决定要更改你使用的蓝色阴影时,你只需要在一个地方更改。Sass 还有许多内置函数用于调整颜色,例如:

  • darken: 这个函数通过百分比使颜色变暗

  • lighten: 这个函数通过百分比使颜色变亮

  • complement: 这个函数返回互补色

  • invert: 这个函数返回反色

  • saturate: 这个函数通过数值来饱和颜色

  • desaturate: 这个函数通过数值来去色

这些函数允许你执行操作,例如:

.pullQuote {
border: 1px solid blue;
color: darken($blue, 15%);
}

还有针对数字、列表、字符串和基本 if-then 语句的函数。这些函数可以帮助你的样式表像你的编程代码一样灵活。

小贴士

Sass 函数

Sass 函数的完整列表可以在sass-lang.com/docs/yardoc/Sass/Script/Functions.html找到。

Sass 中的混合函数

混合函数是 Sass 变量标准的一种变体。避免简单地声明一个一对一的变量,例如以下内容:

$margin: 10px;

相反,你可以使用混合(mixin)来声明一个整个 CSS 类作为变量:

@mixin baseDiv {
 border: 1px solid #f00;
 color: #333;
 width: 200px;
} 

然后你可以把这个混合(mixin)用在 Sass 文件中:

#specificDiv {
 padding: 10px;
 margin: 10px;
 float: right;
 @include baseDiv;
}

这给了你 baseDiv 混合(mixin)组件的所有属性和在 #specificDiv 类中声明的具体样式。

你还可以让你的混合(mixin)使用参数来使其更加灵活。让我们看看我们之前看到的内容的一个替代版本:

@mixin baseDiv($width, $margin, $float) {
 border: 1px solid #f00;
 color: #333;
 width: $width;
 margin: $margin;
 float: $float;
}

这意味着我们可以在 Sass 代码中为 widthmarginfloat 设置值,如下所示:

#divLeftSmall {
 @include baseDiv(100px, 10px, left);
}
#divLeftBig{
 @include baseDiv(300px, 10px, left);
}
#divRightBig {
 @include baseDiv(300px, 10px, right);
}
#divRightAlert {
 @include baseDiv(100px, 10px, right);
 color: #F00;
 font-weight: bold;
}

这给了我们四个带有稍有不同的属性的 div 标签。它们都共享与混合(mixin) baseDiv 类相同的基属性,但它们的 widthfloat 值是不同的。我们也可以通过在我们包含混合(mixin)时像在我们的 #divRightAlert 示例中添加它们来覆盖混合(mixin) baseDiv 的值。

Sass 中的嵌套

Sass 也允许嵌套 CSS 声明。这不仅能让你写出的样式更紧密地反映你的 HTML 结构,而且还能写出更清晰、更容易维护的代码。

在 HTML 中,我们经常嵌套彼此之间的元素以给文档结构。这种的一个常见例子是一个无序列表包含几个列表项,如下所示:

<ul>
 <li>Main List Item 1</li>
 <li>Main List Item 2</li>
</ul>

通常,通过 CSS 样式这个列表,你会分别写 ul 元素的规则和 li 元素的规则。这两个规则在你的 CSS 文件中可能相隔很远,使得调试或修改样式更加困难。

在 Sass 中,我们可以写如下内容:

ul {
 width: 150px;
 border: 1px solid red;

 li {
  margin: 1px;
  border: 1px solid blue;
 }

}

看看我们是怎样在 ul 的样式声明内嵌套 li 元素的样式声明的?嵌套不仅匹配 HTML 文档的结构,而且还能让你知道当需要更新 li 元素时,它是在 ul 元素内的。

当你用 Sass 编译这个时,生成的 CSS 为 ulli 元素有分开的规则:

ul {
 width: 150px;
 border: 1px solid red;
}
ul li {
 margin: 1px;
 border: 1px solid blue;
}

如果你在浏览器中查看这个列表,你会看到一个有红色边框的列表,每个单独的列表项周围还有蓝色边框。

Sass 中的嵌套

使用和号(&)字符引用嵌套层级中的一级也是可能的。这在给嵌套元素添加悬停状态等事物时很有用,或者更一般地说,将你的规则的异常分组在一起。

假设我们想要在鼠标悬停在 li 元素上时改变背景色。我们可以在 li 样式声明内添加 &:hover

ul {
 width: 150px;
 border: 1px solid red;

 li {
  margin: 1px;
  border: 1px solid blue;

  &:hover {
   background-color: #B3C6FF;
  }

 }

}

Sass 编译器将 &:hover 转换为 li:hover

ul li:hover {
 background-color: #B3C6FF;
}

和号(&)特殊字符不必用在规则的开始处。比如说你的设计师有元素 li,当它们位于特殊的 #sidebardiv 组件内时,使用更大的边框。你可以在 ul/li 规则之后写一个单独的规则,或者使用特殊的 & 字符在 li 规则集中添加这个异常:

ul {
 li {
  margin: 1px;
  border: 1px solid blue;

  &:hover {
   background-color: #B3C6FF;
  }
  div#sidebar& {
   border-width: 3px;
  }
 }
}

前面的代码将被翻译成以下规则:

div#sidebar ul li { border-width: 3px; }

你也可以嵌套 CSS 命名空间。在 CSS 中,如果属性全部以相同的前缀开始,比如font-,那么你也可以嵌套它们:

li {
 font: {
  family: Verdana;
  size: 18px;
  weight: bold;
 }
}

一定要记得在命名空间后面加上冒号。编译后,这将变为以下内容:

li {
 font-family: Verdana;
 font-size: 18px;
 font-weight: bold;
}

这个方法适用于任何命名空间 CSS 属性,如border-background-

Sass 中的选择器继承

Sass 中的选择器继承与 JavaScript 中的对象继承类似。同样,一个panel组件扩展了container对象,这意味着一个panel具有container的所有属性和功能,还有一些别的。Sass 让您拥有继承其他对象样式的对象。

假设我们想要为我们的应用程序创建一些消息框元素,一个用于信息性消息,一个用于错误。首先,我们需要定义一个通用框:

.messageBox {
  margin: 10px;
  width: 150px;
  border: 1px solid;
  font: {
   size: 24px;
   weight: bold;
  }
}

现在,在任何我们想要包含.messageBox样式的类中,我们只需使用@extend指令@extend .messageBox;(单独一行):

.errorBox {
 @extend .messageBox;
 border-color: red;
 color: red;
}

.infoBox {
 @extend .messageBox;
 border-color: blue;
 color: blue;
}

然后,在 HTML 中,我们只需使用.errorBox.infoBox类即可:

<div class="infoBox">Here's some information you may like to have.</div>
<div class="errorBox">An unspecified error has occurred.</div>

把所有内容放在一起,你就会看到左边的盒子有一个蓝色的边框和蓝色的文本,右边的盒子有一个红色的边框和红色的文本:

Sass 中的选择器继承

指南针

正如 Sencha Touch 是建立在 JavaScript、CSS 和 HTML 这些低级语言之上的框架一样,Compass 也是建立在 Sass 和 CSS 之上的框架。Compass 为您应用程序的样式提供了一系列可重用的组件。这些包括:

  • CSS 重置:这能强制大多数 HTML 在所有主流网络浏览器中具有一致的外观。

  • 混合:这些允许你为你的 CSS 声明复杂的程序化函数。

  • 布局和网格:这些强制执行宽度和高度标准,以帮助保持跨所有页面的一致布局。

  • 图像雪碧:这允许您自动从多个小图像生成单个图像(这对于浏览器下载来说更快)。CSS 将自动显示您需要的图像部分,隐藏其余部分。

  • 文本替换:这允许您自动交换文档中特定文本片段。

  • 排版:这为在您的网页中使用字体提供了高级选项。

Compass 还将其组件中融入最新的 CSS 最佳实践,这意味着你的样式表将会更简洁、更高效。

Sass + Compass = 主题

Sencha Touch 主题通过提供变量和混合器,其功能性特定于 Sencha Touch,将 Sass 和 Compass 推进了一步。Sencha Touch 的 JavaScript 部分生成大量非常复杂的 HTML,以显示各种组件,如工具栏和面板。而不是学习所有 Sencha Touch 使用的复杂类和 HTML 技巧,你可以简单地使用适当的混合器来改变应用程序的外观。

设置 Sass 和 Compass

如果您决定要创建自己的 Sencha Touch 主题,则不需要安装 Sass 或 Compass,因为它们都包含在 Sencha Cmd 中。

然而,Windows 用户首先需要安装 Ruby。Ruby 用于将 Sass/Compass 文件编译成可用的主题。Linux 和 OS X 用户应该已经在他们的计算机上安装了 Ruby。

在 Windo

rubyinstaller.org/下载 Ruby 安装程序。

提示

我们建议下载版本 1.9.2,因为 Sencha Cmd 可能会与 Ruby 的新版本发生问题。

运行安装程序,并按照屏幕上的说明安装 Ruby。确保检查名为将 Ruby 可执行文件添加到您的 PATH 中的框。这将在以后命令行中为您节省很多输入。

安装完成后,打开 Windows 中的命令行,通过前往开始 | 运行,输入cmd,并按Enter键。这应该会打开命令行。

现在,尝试输入ruby -v。您应该会看到如下内容:

C:\Ruby192>ruby -v
ruby 1.9.2p180 (2011-02-18) [i386-mingw32]

这意味着 Ruby 已经正确安装。

创建自定义主题

接下来我们需要做的是创建我们自己的主题 SCSS 文件。在TouchStart/resources/sass中找到app.scss文件,并复制该文件。将新复制的文件重命名为myTheme.scss

更改文件名后,您需要将主题编译成应用程序可以读取的实际 CSS 文件。为此,我们需要回到命令行,移动到我们的TouchStart/resources/sass目录:

cd /path/to/TouchStart/resources/sass

一旦进入目录,您可以输入以下命令:

compass compile

这将编译我们新的主题,并在resources/css目录下创建一个名为myTheme.css的新文件。

提示

使用compass compile将目录中的任何.scss文件编译。每次更改.scss文件时,您都需要运行此命令。不过,您也可以使用命令compass watch来监视当前文件夹的任何更改,并自动编译它们。

既然我们已经有了新的 CSS 主题文件,接下来需要让应用程序加载它。在 Sencha Touch 的早期版本中,CSS 文件是从index.html文件中加载的。然而,由 Sencha Cmd 生成的应用程序实际上是从我们主TouchStart目录中的app.json文件中加载 CSS 文件的。

打开app.json,查找如下部分:

"css": [
  {
     "path": "resources/css/app.css",
     "update": "delta"
  }
]

将此部分更改为:

"css": [
  {
    "path": "resources/css/myTheme.css",
    "update": "delta"
  }
]

提示

SCSS 和 CSS

请注意,我们目前从css文件夹中包含了一个名为sencha-touch.css的样式表,并且在scss文件夹中有一个匹配的文件,名为sencha-touch.scss。当编译 SCSS 文件时,它们将在您的css文件夹中创建一个新文件。这个新文件将具有.css后缀,而不是.scss

.scss是 Sass 文件的文件扩展名。

如果您在网页浏览器中重新加载应用程序,您将看不到任何变化,因为我们只是为我们的主题复制了文件。让我们看看我们如何改变这一点。打开您的myTheme.scss文件。您应该看到以下内容:

@import 'sencha-touch/default';
@import 'sencha-touch/default/all';

这段代码抓取了所有默认的 Sencha Touch 主题信息。当我们运行compass compilecompass watch时,它会被编译并压缩成一个 CSS 文件,我们的应用程序可以阅读。

最好的部分是我们现在可以用一条代码就改变应用程序的整体颜色方案。

基本颜色

Sencha Touch 主题中的一个关键变量是$base_color。这个颜色及其变体在整个主题中都有使用。为了了解我们的意思,让我们将主题的颜色改为漂亮的森林绿,方法是在我们的myTheme.scss文件的顶部添加以下内容(在所有其他文本之上):

$base_color: #546346;

接下来,我们需要重新编译 Sass 文件以创建我们的myTheme.css文件。如果您正在运行compass watch,当您保存 Sass 文件时这将自动发生。如果没有,您需要像以前一样运行compass compile来更新 CSS(请记住,您需要从resources/sass目录中运行此命令)。

提示

Compass 编译与 Compass 监控

Compass 使用compile命令根据您的 SCSS 文件创建新的样式表。然而,您还可以设置 Compass 监控特定文件的更改,并在添加任何新内容时自动编译文件。这个命令在命令行中如下输入:

compass watch filename

这个命令将一直保持活动状态,直到您的终端关闭。一旦您关闭终端窗口,您需要再次运行该命令,以便让 Compass 监控更改。

在 Safari 中重新加载页面,您应该看到我们应用程序的新森林绿色外观。

请注意,这一行代码为我们的深色和浅色工具栏创建了变体。更改基本颜色还改变了底部的标签栏图标。

这很酷,但如果我们要调整主题的个别部分呢?Sencha Touch 主题通过混合和ui配置选项为我们提供了 exactly 需要。

混合与 UI 配置

如我们之前提到的,Sencha 主题系统是一组预定义的混合和变量,它们被编译成 CSS 样式表。每个组件都有自己的混合和变量来控制样式。这意味着您可以覆盖这些变量或使用混合来定制您自己的主题。

您还可以使用混合(mixins)为ui配置选项创建额外选项(超出我们之前见过的简单的lightdark值)。例如,我们可以在myTheme.sass文件中添加一个新的混合来修改我们工具栏的颜色。

在我们的myTheme.sass文件中,找到如下行:

@import 'sencha-touch/default/all';

在此行之后,添加以下行:

@include sencha-toolbar-ui('subnav', #625546, 'matte');

这行代码告诉 Sass 为工具栏创建一个新的ui选项。我们新的选项将被称为subnav,它将具有#625546的基础颜色。最后一个选项设置了渐变的样式。可用的样式有:

  • flat:无渐变

  • matte:一个细微的渐变

  • bevel:一个中等渐变

  • glossy:一个玻璃样式渐变

  • recessed:一个反转的渐变

你可以在 Sencha Touch 文档的每个组件顶部找到有关这些变量(和任何可用的混合剂)的额外信息:docs.sencha.com/touch/2.2.0/

混合剂和 UI 配置

保存文件后,你需要在命令行使用compass compile命令重新编译样式表。

我们还需要更改 JavaScript 文件中的ui配置选项。在app/view文件夹中找到我们的Main.js文件并打开它。找到我们应用程序中的第二个工具栏,就在我们添加按钮的上方。它应该如下所示:

dock: 'top',
xtype: 'toolbar',
ui: 'light'

你需要将ui:'light'改为ui:'subnav'并保存文件。

然后你可以重新加载页面以查看你的更改。

混合剂和 UI 配置

你还会注意到,工具栏内的按钮也调整了它们的颜色以匹配新工具栏的ui配置。

添加新图标

如我们在本章开头提到的,Sencha Touch 的早期版本使用图标遮罩来创建应用程序中的图标。这导致了一些与浏览器兼容性问题,所以新图标实际上是从 Pictos 图标字体生成的。默认情况下,包含这 26 个图标,但你可以使用icon混合剂添加更多。

注意

Sencha Touch 中可用的默认图标列表可以在docs.sencha.com/touch/2.2.0/#!/api/Ext.Button找到。

Pictos 图标的完整列表可以在pictos.cc/font/找到。

在你的myTheme.sass文件中,找到写着以下内容的行:

@import 'sencha-touch/default/all';

此行之后,请添加以下内容:

@include icon('camera', 'v'); 

icon混合剂有两个参数:你想要引用图标的名称(这是任意的)以及 Pictos 字体中图标的相应字母。第二个参数可以在前面提示中提到的 Pictos 网站上查找。

样式表重新编译后,我们可以在面板中更改iconCls值以使用新图像。

app/Main.js文件中,找到我们的HBox面板的iconCls,目前显示为:

iconCls: 'info',

用以下内容替换该行:

iconCls: 'camera',

保存你的更改并重新加载页面以查看你的新图标。不要忘记在命令行使用compass compile重新编译 Sass 文件。

变量

变量也适用于大多数组件,并用于控制特定的颜色、大小和外观选项。与混合剂不同,变量针对组件的单一设置。例如,button组件包括以下变量的变量:

  • $button-gradient:所有按钮的默认渐变

  • $button-height:所有按钮的默认高度

  • $button-radius:所有按钮的默认边框半径

  • $button-stroke-weight:所有按钮的默认边框厚度

如前所述,您可以在每个组件的顶部找到这些变量(和任何可用的混合)的列表,在 Sencha Touch 文档中docs.sencha.com/touch/2.2.0/

例如,如果我们向我们的myTheme.scss文件添加$button-height: 2em;,然后我们可以重新编译并看到我们工具栏中的按钮现在比之前要大。

变量

您还会注意到我们的小型按钮大小没有改变。这是因为它的 UI 配置(small)已经单独定义,并包括了一个特定的高度。如果您想更改这个按钮的大小,您需要在Main.js文件中删除它的ui配置。

更多 Sass 资源

使用 Sencha Touch 主题中包含的混合和变量,您可以几乎改变界面的任何方面,使其完全按照您想要的方式显示。有许多在线资源可以帮助您深入了解 Sass 和 Compass 的所有可能性。

注意

更多资源

Sencha Touch 主题混合和变量的完整列表可在dev.sencha.com/deploy/touch/docs/theme/找到。

详细了解 Sass,请访问sass-lang.com/

Compass 官网提供了使用 Compass 的网站示例、教程、帮助等内容;您可以访问compass-style.org/

默认主题和主题切换

随着 Sencha Touch 2.2 的推出,现在支持 Blackberry 10 和 Windows Phone 平台。为了帮助您为这些平台样式化您的应用程序,Sencha Touch 2.2 包括两个平台的默认主题。让我们通过创建几个新的主题文件来了解这是如何工作的。

首先,将我们的原始resources/sass/app.scss文件复制两份,并将它们重命名为windows.scssblackberry.scss

在两个文件中,找到以下行:

@import 'sencha-touch/default';
@import 'sencha-touch/default/all';

windows.scss中,将行更改为:

@import 'sencha-touch/windows';
@import 'sencha-touch/windows/all';

blackberry.scss中,将行更改为:

@import 'sencha-touch/bb10';
@import 'sencha-touch/bb10/all';

接下来,您需要运行compass compile以创建新的 CSS 文件。

现在我们可以使用我们的app.json文件根据应用程序运行的平台来切换这些主题。打开app.json文件,再次查找我们的css部分。它应该如下所示:

"css": [
        {
            "path": "resources/css/myTheme.css",
            "update": "delta"
        }
    ]

让我们将其更改为如下所示:

"css": [
 {
  "path": "resources/css/myTheme.css",
  "platform": ["chrome", "safari", "ios", "android", "firefox"],
  "theme": "Default",
  "update": "delta"
 },
 {
  "path": "resources/css/windows.css",
  "platform": ["ie10"],
  "theme": "Windows",
  "update": "delta"
 },
 {
  "path": "resources/css/blackberry.css",
  "platform": ["blackberry"],
  "theme": "Blackberry",
  "update": "delta"
 }
   ]

由于我们大多数人并不富有,我们可能没有每种类型的设备来测试。然而,我们可以在我们应用程序 URL 的末尾添加一个参数,以测试我们的每个主题。例如:

myapplication.com?platform=ie10

这将会在应用程序中自动处理,但我们可以通过向 URL 添加这个参数来测试我们的应用程序。我们应该现在有了基于平台的三种不同的主题。

默认主题和主题切换

我们可以根据这些三个选项之外的条件来制作这些条件主题。可用的平台有:

  • 电话、平板电脑和桌面

  • iOS、Android 和 Blackberry

  • Safari、Chrome、IE 10 和 Firefox

这意味着我们可以根据前面列表中提到的任何平台来更改样式。只需生成新的 Sass/CSS 样式表,并在app.json中包含适当的配置行,就像之前的示例一样。

这类条件样式的微调将帮助您的应用程序在多种设备上保持可读性和易用性。

使用 Sencha.io Src 在不同设备上的图片

如果您的应用程序使用图片,那么您可能需要比前面部分使用的条件样式更健壮的东西。为每个设备创建单独的图片集将是一场噩梦。幸运的是,Sencha 的团队对这个问题的解决办法是一个名为Sencha.io Src的基于 Web 的服务。

Sencha.io Src是 Sencha 的一个独立服务,可以用于任何基于 Web 的应用程序。该服务通过获取原始图片并实时调整大小以适应当前设备和屏幕大小来工作。这些图片也被服务缓存并优化,以便快速、重复交付。要使用Sencha.io Src服务,您需要更改的只是图片的 URL。

例如,一个基本的 HTML 图片标签看起来像这样:

<img src="img/my-big-image.jpg">

使用Sencha.io Src服务的同一个图片标签看起来像这样:

<img src="img/my-big-image.jpg">

这个过程会将您图片的实际 URL 传递给系统进行处理。

注意

Sencha.io Src 中的图片 URL

正如您在示例中看到的,我们使用了一个完整的图片 URL(带有www.mydomain.com/),而不是一个更短的相对 URL(例如/images/my-big-image.jpg)。由于Sencha.io Src服务需要能够直接从主Sencha.io服务器获取文件,所以相对 URL 不起作用。图片文件需要放在一个可以向公众公开的 Web 服务器上,才能正确工作。

Sencha.io Src 在不同设备上的图片

使用这个服务,我们的大图片将根据我们使用的设备屏幕大小调整到全宽,无论设备的大小如何。Sencha.io Src还能保持图片的比例正确,不会出现压缩或拉伸的情况。

使用 Sencha.io Src 指定大小

我们并不总是在我们应用程序中使用全屏图片。我们经常用它们来作为应用程序中的图标和强调元素。Sencha.io Src还允许我们为图片指定特定的高度和/或宽度:

<img src="img/my-big-image.jpg">

在这种情况下,我们已经将需要调整大小的图片宽度设置为320像素,高度设置为200像素。我们还可以只限制宽度;高度将自动设置为正确的比例:

<img src="img/my-big-image.jpg">

提示

需要注意的是Sencha.io Src只会缩小图片;它不会放大它们。如果你输入的值大于实际图片的尺寸,它将 simply display at the full image size. 你的全尺寸图片应始终是你用于展示所需的最大尺寸。

通过公式确定大小

我们还可以使用公式根据设备屏幕大小进行更改。例如,我们可以使用以下代码使我们的照片比屏幕的全宽窄 20 像素:

<img src="img/my-big-image.jpg">

如果你想要在图片周围留出一点边框,这个选项很有用。

通过百分比确定大小

我们还可以使用百分比宽度来设置图片大小:

<img src="img/my-big-image.jpg">

我们 URL 中的x50部分将图片大小设置为屏幕宽度的 50%。

我们甚至可以将这两个元素结合起来创建一个可伸缩的图片库:

<img src="img/my-big-image.jpg">
<img src="img/my-big-image.jpg">

使用公式-20x50-5,我们取原始图片,为边距去掉 20 像素,将其缩小到 50%,然后去掉额外的五像素,以允许两张图片之间有空间。

通过百分比确定大小

更改文件类型

Sencha.io Src提供了一些可能很有用的额外选项。首先,它让你可以实时更改图片的文件类型。例如,以下代码会将你的 JPG 文件转换为 PNG:

<img src="img/my-big-image.jpg">

当向应用程序用户提供多个图片下载选项时,这个选项很有用。

此选项还可以与调整大小选项结合使用:

<img src="img/my-big-image.jpg">

这将把文件转换为 PNG 格式并将其缩放到 50%。

使用Sencha.io Src中可用的功能,您可以自动调整应用程序中的图片大小,并在多种设备上提供一致的外观和感觉。

注意

Sencha.io 是一个免费服务。要获取使用Sencha.io Src的所有功能的完整列表,请访问:

www.sencha.com/learn/how-to-use-src-sencha-io/

总结

在这一章中,我们学习了如何使用ui配置选项来样式化工具栏。我们还讨论了 Sencha Touch 如何使用 Sass 和 Compass 创建一个健壮的主题系统。我们包括了 Sass 和 Compass 的安装说明,并解释了混合模式、变量、嵌套和选择器继承。最后,我们提到了为多种设备设计界面以及使用Sencha.io Src处理自动调整图片大小的方法。

在下一章中,我们将重新深入研究 Sencha Touch 框架。我们将回顾一下我们之前学过的关于组件层次结构的知识。然后,我们将介绍一些更专业的组件。最后,我们会给你一些在 Sencha Touch API 文档中找到所需信息的技巧。

第四章:组件和配置

在本章中,我们将更深入地查看 Sencha Touch 中可用的各个组件。我们将检查布局配置选项以及它们如何影响每个组件。

在本章中,我们将使用简单的基组件作为学习更复杂组件的起点。我们还会稍微谈谈如何在组件创建后访问它们。

最后,我们将总结如何使用 Sencha Touch API 文档来查找每个组件的详细配置、属性、方法和事件信息。

本章将涵盖以下主题:

  • 基组件类

  • 布局重新审视

  • 标签面板和轮播组件

  • 表单面板组件

  • 消息框和弹幕

  • 地图组件

  • 列表和嵌套列表组件

  • 在哪里查找有关组件的更多信息

基组件类

当我们谈论 Sencha Touch 中的组件时,我们通常是指按钮、面板、滑块、工具栏、表单字段和其他我们可以在屏幕上看到的实际项目。然而,所有这些组件都继承自一个具有惊人原创名称的单一基础组件component。这显然可能会导致一些混淆,所以我们将把这个称为Ext.Component

理解最重要的一点是,你并不总是直接使用Ext.Component。它更常作为 Sencha Touch 中所有其他组件的构建块。然而,熟悉基组件类是很重要的,因为只要它能做,所有其他组件都能做。学习这个类可以让你在所有其他事情上有一个巨大的优势。Ext.Component一些最有用的配置选项如下:

  • border

  • cls

  • disabled

  • height/width

  • hidden

  • html

  • margin

  • padding

  • scroll

  • style

  • ui

像我们将在本章后面覆盖的其他组件一样,继承自基组件类,它们都会有这些相同的配置选项。这些配置中最关键的是layout

再次审视布局

当你开始创建自己的应用程序时,你需要充分理解不同的布局如何影响你在屏幕上看到的内容。为此,我们将从演示应用程序开始,展示不同的布局是如何工作的。

注意

为了这个演示应用程序的目的,我们将一次创建不同的组件,作为单独的变量。这样做是为了可读性,不应被视为最佳编程风格。记住,以这种方式创建的任何项目都会占用内存,即使用户从未查看组件:

var myPanel = Ext.create('Ext.Panel', { …

始终创建你的组件,使用xtype属性,在你的主容器内,如下面的代码片段所示,是一个更好的做法:

items: [{ xtype: 'panel', …

这允许 Sencha Touch 在需要时渲染组件,而不是在页面加载时一次性渲染所有组件。

创建一个卡片布局

首先,我们将创建一个简单的应用程序,其包含一个配置为使用card布局的容器:

var myApp = Ext.create('Ext.Application', {
    name:'TouchStart',
    launch:function () {
        var mainPanel = Ext.create('Ext.Container', {
            fullscreen:true,
            layout:'card',
            cardSwitchAnimation:'slide',
            items:[hboxTest]
        });

        Ext.Viewport.add(mainPanel);
    }
});

这设置了一个名为mainPanel的单一容器,具有card布局。这个mainPanel容器是我们将在本节中添加我们布局示例容器的剩余部分的地方。

card布局将其项目安排得类似于卡片堆叠。这些卡片中只有一张是激活的并一次显示。card布局将任何额外的卡片保留在后台,并在面板接收到setActiveItem()命令时仅创建它们。

列表中的每个项目可以通过使用setActiveItem(n)激活,其中n是项目编号。这可能会有些令人困惑,因为项目的编号是基于零的,这意味着你从 0 开始计数,而不是从 1 开始。例如,如果你想要激活列表中的第四个项目,你会使用:

mainPanel.setActiveItem(3);

在此案例中,我们起初只有一个名为hboxTest的单一卡片/项目。我们需要添加这个容器以使我们的程序运行。

创建一个 hbox 布局

在前面的部分的代码中,在var mainPanel = Ext.create('Ext.Container', {行上方,添加以下代码:

var hboxTest = Ext.create('Ext.Container', {
    layout:{
        type:'hbox',
        align:'stretch'
    },
    items:[
        {
            xtype:'container',
            flex:1,
            html:'My flex is 1',
            margin:5,
            style:'background-color: #7FADCF'
        },
        {
            xtype:'container',
            flex:2,
            html:'My flex is 2',
            margin:5,
            style:'background-color: #7FADCF'
        },
        {
            xtype:'container',
            width:80,
            html:'My width is 80',
            margin:5,
            style:'background-color: #7FADCF'
        }
    ]
});

这给了我们一个具有hbox布局和三个子项目的容器。

提示

子项与父项

在 Sencha Touch 中,我们经常发现自己处理非常大量的项目,这些项目被嵌套在容器中,而这些容器又被嵌套在其他容器中。通常,将容器称为其包含的任何项目的父容器是有帮助的。这些项目被称为容器的子项目。

hbox布局将其项目横向堆叠,并使用widthflex值来确定其每个子项目将占据多少横向空间。align: 'stretch'配置导致项目拉伸以填充所有可用的垂直空间。

创建一个 hbox 布局

你应该尝试调整flexwidth值,看看它们如何影响子容器的尺寸。你还可以更改aligncenterendstartstretch)的可选配置选项,以查看可用的不同选项。完成之后,让我们继续向我们的卡片布局添加更多项目。

创建一个 vbox 布局

在我们的var hboxTest = Ext.create('Ext.Container',{行上方,添加以下代码:

var vboxTest = Ext.create('Ext.Container', {
    layout:{
        type:'vbox',
        align:'stretch'
    },
    items:[
        {
            xtype:'container',
            flex:1,
            html:'My flex is 1',
            margin:5,
            style:'background-color: #7FADCF'
        },
        {
            xtype:'container',
            flex:2,
            html:'My flex is 2',
            margin:5,
            style:'background-color: #7FADCF'
        },
        {
            xtype:'container',
            height:80,
            html:'My height is 80',
            margin:5,
            style:'background-color: #7FADCF'
        }
    ]
});

这代码与我们的之前的hbox代码几乎一模一样,一个具有三个子容器的容器。然而,这个父容器使用layout: vboxitems列表中的第三个子容器使用height而不是width。这是因为vbox布局是垂直堆叠其项目,并使用heightflex的值来确定子项目将占据多少空间。在这个布局中,align: 'stretch'配置导致项目伸展以填满水平空间。

现在我们已经有了我们的vbox容器,我们需要将其添加到我们主layoutContainer中的项目。将layoutContainer中的items列表更改为以下内容:

items: [hboxTest, vboxTest]

如果我们现在运行代码,它看起来会和之前一模一样。这是因为我们的卡片布局layoutContainer中只能有一个活动项目。您可以通过向我们的layoutContainer添加以下配置来设置layoutContainer显示我们的新vbox

activeItem: 1,

记住我们的项目是从零开始编号的,所以项目1是我们列表中的第二个项目:items: [hboxTest, vboxTest]

现在您应该能够看到我们应用程序的vbox布局:

创建一个 vbox 布局

hbox一样,您应该花点时间调整flexwidth值,看看它们如何影响容器的大小。您还可以更改aligncenterendstartstretch)的可选配置选项,以查看不同的选项。完成后,让我们继续向我们的card布局添加更多项目。

创建合适的布局

fit布局是最基本的布局,它只是使任何子项目填满父容器。虽然这看起来相当基础,但它也可能有一些 unintended consequences,正如我们在例子中所见。

在我们之前的var vboxTest = Ext.create('Ext.Container', {行上,添加以下代码:

var fitTest = Ext.create('Ext.Container', {
    layout:'fit',
    items:[
        {
            xtype:'button',
            ui:'decline',
            text:'Do Not Press'
        }
    ]
});

这是一个具有fit布局的单容器和按钮。现在,我们只需要在我们的主layoutContainer组件上设置activeItem配置,将activeItem: 1更改为activeItem: 2

如果您现在重新加载页面,您将看到我们所说的 unintended consequences:

创建一个适合布局

正如您所看到的,我们的按钮已经扩展到填满整个屏幕。我们可以通过为按钮(以及我们放置在这个容器中的任何其他项目)声明一个特定的高度和宽度来更改此情况。然而,适合布局通常最适合单个项目,该项目旨在占据整个容器。这使得它们成为子容器的一个很好的布局,在这种情况下,父容器控制整体大小和位置。

让我们看看这可能如何工作。

增加复杂度

在这个例子中,我们将创建一个嵌套容器并添加到我们的卡片堆叠中。我们还将添加一些按钮,以便更容易切换卡片堆叠。

我们两个新容器是我们当前应用程序中已经拥有的变体。第一个是我们hbox布局的副本,有几个小的变化:

var complexTest = Ext.create('Ext.Container', {
    layout:{
        type:'vbox',
        align:'stretch'
    },
    style:'background-color: #FFFFFF',
    items:[
        {
            xtype:'container',
            flex:1,
            html:'My flex is 1',
            margin:5,
            style:'background-color: #7FADCF'
        },
        hboxTest2,
        {
            xtype:'container',
            height:80,
            html:'My height is 80',
            margin:5,
            style:'background-color: #7FADCF'
        }
    ]
});

你可以复制并粘贴我们旧的vboxTest代码,并将第一行更改为说complexTest而不是vboxTest。你还需要删除我们items列表中的第二个容器(包括所有括号)并用hboxTest2替换它。这是我们将在其中嵌套具有自己布局的另一个容器的位置。

现在,我们需要通过复制我们之前的hboxTest代码来定义hboxTest2,并进行一些小的修改。你需要将这段新代码粘贴到你放置complexTest代码的地方;否则,在我们实际定义它之前尝试使用hboxTest2时,你会得到错误:

var hboxTest2 = Ext.create('Ext.Container', {
    layout:{
        type:'hbox',
        align:'stretch'
    },
    flex:2,
    style:'background-color: #FFFFFF',
    items:[
        {
            xtype:'container',
            flex:1,
            html:'My flex is 1',
            margin:5,
            style:'background-color: #7FADCF'
        },
        {
            xtype:'container',
            flex:2,
            html:'My flex is 2',
            margin:5,
            style:'background-color: #7FADCF'
        },
        {
            xtype:'container',
            width:80,
            html:'My width is 80',
            margin:5,
            style:'background-color: #7FADCF'
        }
    ]
});

粘贴代码后,你需要将变量名更改为hboxTest2,并且我们需要为主父容器添加一个flex配置。由于这个容器嵌套在我们的vbox容器中,flex配置需要定义hboxTest2将占据多少空间。

在我们查看这个新的复杂布局之前,让我们通过添加一些按钮来简化我们的工作,以便在各种布局卡之间切换。

定位mainPanel,在它下面,定义items列表的地方,在items列表的最上面添加以下代码:

{
    xtype:'toolbar',
    docked:'top',
    defaults:{
        xtype:'button'
    },
    items:[
        {
            text:'hbox',
            handler:function () {
                mainPanel.setActiveItem(0);
            }
            text:'vbox',
            handler:function () {
                mainPanel.setActiveItem(1);
            }
        },
        {
            text:'fit',
            handler:function () {
                mainPanel.setActiveItem(2);
            }
        },
        {
            text:'complex',
            handler:function () {
                mainPanel.setActiveItem(3);
            }
        }
    ]
}

这段代码在mainPanel的顶部添加了一个工具栏,每个布局卡片都有一个按钮。

提示

在 Sencha Touch 的早期版本中,toolbar项是独立于其他项定义的,并使用一个名为dock的配置来控制其位置。在当前版本中,toolbar组件与其他项一起内联定义,而工具栏的位置则由docked配置控制。

每个按钮都有一个文本配置,作为按钮的标题,还有一个handler配置。handler配置定义了按钮被点击时会发生什么。对于我们每个按钮,我们在代码中使用之前设置的mainPanel变量:

var mainPanel = Ext.create('Ext.Container', {…

这让我们可以使用容器及其card布局可用的任何方法。在每按钮的代码中,我们通过使用以下代码行来设置活动项(哪个标签页是可见的):

mainPanel.setActiveItem(x);

在此情况下,x值将被替换为我们想要激活的项的索引(记住这些是按顺序排列的,从 0 开始,而不是 1)。

注意我们还在mainPanel组件的activeItem初始配置选项中留下了空位。这将控制我们的应用程序启动时显示哪个项。

如果你刷新页面,你应该能够点击按钮并看到我们的各种布局,包括新的复杂布局。

增加复杂性

从这个例子中,您可以看到我们的vbox布局将窗口分为三行。第二行的hbox布局将其分为三列。使用这些嵌套布局类型可以非常容易地创建传统布局,例如电子邮件或社交网络应用程序中使用的布局。

增加复杂性

在这个例子中,我们有一个典型电子邮件应用程序的布局。这个布局可以从概念上分解为以下几个部分:

  • 具有工具栏菜单的应用程序容器和一个称为的单个容器,具有适合布局。

  • 容器将有一个hbox布局和两个子容器,分别称为左侧右侧

  • 左侧容器将有一个flex值为1和一个vbox布局。它将有两个子容器,分别称为邮箱(具有flex3)和活动(具有flex1)。

  • 右侧容器将有一个flex值为3和一个vbox布局。它还将有两个子容器,分别称为消息(具有flex1)和消息(具有flex2)。

构建此类容器布局是一种良好的实践。要查看此容器布局的示例代码,请查看代码包中的TouchStart2b.js文件。创建这些基本布局作为模板以快速启动构建您未来的应用程序也是一个好主意。

现在我们已经更好地了解了布局,让我们来看看我们可以在布局中使用的某些组件。

标签面板和轮播组件

在我们最后一个应用程序中,我们使用按钮和card布局创建了一个可以在不同的子项之间切换的应用程序。虽然应用程序经常需要以这种方式(使用您自己的按钮和代码)进行编程,但您也可以选择让 Sencha Touch 自动设置此操作,使用TabPanelCarousel

创建标签面板组件

当您需要让用户在多个视图之间切换时,TabPanel组件非常有用,例如联系人、任务和设置。TabPanel组件自动生成布局的导航,这使其成为应用程序主要容器的非常有用功能。

在我们第二章的早期示例应用程序中,创建一个简单应用程序,使用了一个简单的TabPanel来形成我们应用程序的基础。以下是一个类似的代码示例:

Ext.application({
    name:'TouchStart',
    launch:function () {
        var myTabPanel = Ext.create('Ext.tab.Panel', {
            fullscreen:true,
            tabBarPosition:'bottom',
            items:[
                {
                    xtype:'container',
                    title:'Item 1',
                    fullscreen:false,
                    html:'TouchStart container 1',
                    iconCls:'info'
                },
                {
                    xtype:'container',
                    html:'TouchStart container 2',
                    iconCls:'home',
                    title:'Item 2'
                },
                {
                    xtype:'container',
                    html:'TouchStart container 3',
                    iconCls:'favorites',
                    title:'Item 3'
                }
            ]
        });
        Ext.Viewport.add(myTabPanel);
    }
});

在这段代码中,Ext.tab.Panel会自动生成一个卡片布局;您不需要声明一个布局。您可能希望为组件声明一个tabBarPosition值。这是您的标签将自动出现的地方;默认情况下在屏幕的顶部。

这将为items列表中的每个子项生成一个大的正方形按钮。按钮还将使用iconCls值分配一个图标给按钮。title配置用于给按钮命名。

提示

有关可用的图标和样式信息,请参阅上一章关于tab panel的更多信息。还应注意的是,这些图标只在tabBarPosition值设置为bottom时使用。

如果你将tabBarPosition值设置为顶部(或者留空),它会使按钮变小且变圆。它还会消除图标,即使你在子项目中声明了iconCls值。

创建一个 TabPanel 组件

Carousel组件与tabpanel类似,但它生成的导航更适合于幻灯片展示等事物。它可能不会像应用程序的主界面那样出色,但它确实作为在一个可滑动的容器中显示多个项目的方式表现良好。

tabpanel类似,Carousel收集其子项目,并自动将它们安排在一个card布局中。实际上,我们实际上可以对我们之前的代码进行一些简单的修改,使其成为一个Carousel组件:

Ext.application({
    name:'TouchStart',
    launch:function () {
        var myCarousel = Ext.create('Ext.carousel.Carousel', {
            fullscreen:true,
            direction:'horizontal',
            items:[
                {
                    html:'TouchStart container 1'
                },
                {
                    html:'TouchStart container 2'
                },
                {
                    html:'TouchStart container 3'
                }
            ]
        });
        Ext.Viewport.add(myCarousel);
    }
});

我们首先使用Ext.create创建了一个新的Ext.carousel.Carousel类,而不是一个新的Ext.tab.Panel类。我们还添加了一个direction配置,可以是horizontal(从左到右滚动)或vertical(向上或向下滚动)。

我们移除了停靠工具栏,因为正如我们将看到的,Carousel不需要它。我们还将每个子项目的图标类和标题移除,原因相同。最后,我们移除了xtype配置,因为Carousel组件会为每个子项目自动创建一个Ext.Container类。

创建一个 Carousel 组件

tabpanel不同,carousel没有按钮,只在底部有一系列圆点,每个子项目都有一个圆点。虽然使用圆点进行导航是可能的,但carousel组件会自动设置以响应触摸屏上的滑动。你可以在浏览器中通过点击并按住鼠标指针,同时水平移动它来复制这个手势。如果你在carousel中声明了一个direction: vertical配置,你还可以垂直滑动以在子项目之间移动。

与章节开头我们的示例中的卡片布局类似,tabpanelcarousel组件都理解activeItem配置。

这让你可以设置应用程序首次加载时显示哪个项目。此外,它们都理解setActiveItem()方法,该方法允许你在应用程序加载后更改选中的子项目。

Carousel组件还有next()previous()方法,允许你按顺序遍历项目。

需要注意的是,由于tabpanelcarousel都继承自Ext.Container,它们也理解容器理解的所有方法和配置。

与容器一样,tabpanelcarousel将是大多数应用程序的主要起点。然而,在某个时候,你可能还想使用另一种容器:FormPanel组件。

创建 FormPanel 组件

FormPanel组件是Ext.Container组件的一个非常特殊的版本,正如名称暗示的那样,它被设计用来处理表单元素。与面板和容器不同,您不需要为formpanel指定布局。它自动使用自己的特殊表单布局。

创建formpanel组件的基本示例如下:

var form = Ext.create('Ext.form.FormPanel', {
 items: [
  {
   xtype: 'textfield',
   name : 'first',
   label: 'First name'
  },
  {
   xtype: 'textfield',
   name : 'last',
   label: 'Last name'
  },
  {
   xtype: 'emailfield',
   name : 'email',
   label: 'Email'
  }
 ]
});

在这个例子中,我们只是创建了一个面板,并为表单中的每个字段添加了项目。我们的xtype告诉表单要创建什么类型的字段。我们可以将此添加到我们的carousel中,替换我们的第一个容器,如下所示:

Ext.application({
    name:'TouchStart',
    launch:function () {
        var myCarousel = Ext.create('Ext.carousel.Carousel', {
            fullscreen:true,
            direction:'horizontal',
            items:[
                form, {
                    html:'TouchStart container 2'
                }, {
                    html:'TouchStart container 3'
                }]
        });
        Ext.Viewport.add(myCarousel);
    }
});

创建 FormPanel 组件

任何曾经在 HTML 中处理表单的人都应该熟悉所有标准的字段类型,因此熟悉标准 HTML 表单的人都会理解以下的xtype属性名称:

  • checkboxfield

  • fieldset

  • hiddenfield

  • passwordfield

  • radiofield

  • selectfield

  • textfield

  • textareafield

这些字段类型在很大程度上与它们的 HTML 同类相匹配。Sencha Touch 还提供了一些特殊的文本字段,可以帮助验证用户输入:

  • emailfield:此字段只接受有效的电子邮件地址,在 iOS 设备上,它会弹出另一个电子邮件地址和 URL 友好型键盘

  • numberfield:此字段只接受数字

  • urlfield:此字段只接受有效的网络 URL,并且还会弹出特殊键盘

这些特殊字段只有在输入有效时才会允许提交操作。

所有这些基本表单字段都继承自主容器类,因此它们具有所有标准的heightwidthclsstyle和其他容器配置选项。

它们还有一些字段特定的选项:

  • label:这是与字段一起使用的文本标签

  • labelAlign:这是标签出现的位置;可以是顶部或左侧,默认为左侧

  • labelWidth:这告诉我们标签应该有多宽

  • name:这对应于 HTML 的 name 属性,这是字段值提交的方式

  • maxLength:这告诉我们字段中可以使用多少个字符

  • required:这告诉我们字段是否为必须的,以便表单能够提交

小贴士

表单字段位置

虽然FormPanel通常是在显示表单元素时使用的容器,但它理解submit()方法,该方法将通过 AJAX 请求或POST提交表单值。

如果您在不是FormPanel组件的东西中包含一个表单字段,您将需要使用您自己的自定义 JavaScript 方法来获取和设置字段的值。

除了标准的 HTML 字段外,Sencha Touch 中还提供了一些特殊字段,包括DatePickersliderspinnertoggle字段。

添加日期选择器组件

datepickerfield组件(这个名称正确吗?)在表单中放置一个可点击的字段,字段右侧有一个小三角形。

你可以在emailfield项之后添加以下代码来向我们的表单中添加一个日期选择器:

{
 xtype: 'datepickerfield',
 name : 'date',
 label: 'Date'
}

当用户点击字段时,将出现一个DatePicker组件,用户可以通过旋转月份、日期和年份轮盘,或通过向上或向下滑动来选择日期。

添加日期选择器组件

datepickerfield还具有configs选项,如下所示:

  • yearFrom:日期选择器的开始年份。

  • yearTo:日期选择器的结束年份。

  • slotOrder:使用字符串数组来设置插槽顺序。默认值为['month', 'day', 'year']

添加滑块、微调器和切换按钮

滑块允许从指定的数值范围内选择一个值。sliderfield值显示一个带有指示器的条,可以通过水平滑动来选择值。这可以用于设置音量、颜色值和其他范围选项。

与滑块类似,微调器允许从指定的数值范围内选择一个值。spinnerfield值显示一个带有数字值和+-按钮的表单字段。

切换按钮允许在 1 和 0 之间进行简单选择(开和关),并在表单上显示一个切换风格的按钮。

在以下组件列表的末尾添加以下新组件:

{
 xtype: 'sliderfield',
 label: 'Volume',
 value: 5,
 minValue: 0,
 maxValue: 10
},
{
 xtype: 'togglefield',
 name : 'turbo',
 label: 'Turbo'
},
{
 xtype: 'spinnerfield',
 minValue: 0,
 maxValue: 100,
 incrementValue: 2,
 cycle: true
}

添加滑块、微调器和切换按钮

我们的sliderfieldspinnerfield具有minValuemaxValue配置选项。我们还向spinnerfield添加了一个incrementValue属性,当点击+-按钮时,它将按2的增量移动。

注意

我们将在第六章中介绍表单的发送和接收数据,获取数据。

消息框和表单组件

在某些时候,您的应用程序可能需要向用户反馈、询问用户问题或提醒用户事件。这就是MessageBoxSheet组件发挥作用的地方。

创建消息框组件

MessageBox组件在页面上创建一个窗口,可用于显示警告、收集信息或向用户展示选项。MessageBox可以通过三种不同的方式调用:

  • Ext.Msg.alert接受一个标题、一些消息文本,以及一个可选的回调函数,当点击警告框的确定按钮时调用。

  • Ext.Msg.prompt带有标题、一些消息文本和一个当按下OK按钮时调用的回调函数。该prompt命令创建一个文本字段并自动添加到窗口中。在此例中,函数接收字段的文本进行处理。

  • Ext.Msg.confirm带有标题、一些消息文本和一个当任一按钮被按下时调用的回调函数。

提示

回调函数

回调函数是一个在用户或代码采取特定行动时自动调用的函数。这是程序员让代码说“当你完成这个,回调我并告诉我你做了什么”的基本方式。这个回调允许程序员根据函数中发生的事情做出额外的决定。

让我们尝试一些例子,从一个简单的消息框开始:

Ext.application({
    name:'TouchStart',
    launch:function () {
        var main = Ext.create('Ext.Container', {
            fullscreen:true,
            items:[
                {
                    docked:'top',
                    xtype:'toolbar',
                    ui:'light',
                    items:[
                        {
                            text:'Panic',
                            handler:function () {
                                Ext.Msg.alert('Don\'t Panic!', 'Keep Calm. Carry On.');
                            }
                        }
                    ]
                }
            ]
        });

        Ext.Viewport.add(main);
    }
});

这段代码设置了一个带有工具栏和单个按钮的简单面板。按钮有一个处理程序,使用Ext.Msg.alert()来显示我们的消息框。

提示

转义引号

在我们的上一个示例中,我们使用字符串Don\'t Panic作为消息框的标题。\告诉 JavaScript 我们的第二个单引号是字符串的一部分,而不是字符串的结束。正如在示例中看到的那样,\在我们的消息框中消失了。

创建一个 MessageBox 组件

现在,让我们在我们的toolbar组件中的items中添加一个第二个按钮,以Ext.Msg.prompt样式的消息框:

{
    text:'Greetings',
    handler:function () {
        Ext.Msg.prompt('Greetings!', 'What is your name?', function (btn, text) {
            Ext.Msg.alert('Howdy', 'Pleased to meet you ' + text);
        });
    }
}

这个消息框有点更复杂。我们创建了一个带有标题、信息和函数的Ext.Msg.prompt类。提示将自动创建我们的文本字段,但我们需要使用函数来确定用户在字段中输入的文本要做什么。

该函数接收按钮的值和文本的值。我们的函数抓取文本并创建一个新的警告框来响应,还包括用户在字段中输入的名称。

创建一个 MessageBox 组件

MessageBoxExt.Msg.confirm类用于用户需要做出决定,或确认系统将要采取的特定行动。

让我们把我们下面的组件添加到toolbar组件的items列表中:

{
 text: 'Decide',
 handler: function() {
  Ext.Msg.confirm('It\'s Your Choice...', 'Would you like to proceed?', function(btn) {
   Ext.Msg.alert('So be it!', 'You chose '+btn);
  });
 }
}

Ext.Msg组件的提示函数类似,确认版本也带有标题、信息和回调函数。回调函数接收用户按下的按钮(作为值btn),然后可以用来确定系统接下来应该采取哪些步骤。

在这种情况下,我们只是弹出一个警告框来显示用户所做的选择。你也可以使用if...then语句来根据点击哪个按钮采取不同的行动。

创建一个 MessageBox 组件

创建一个 Sheet 组件

Sheet组件与Ext.Msg组件类似,通常用于在屏幕上弹出新的信息或选项。它也通过出现在现有屏幕之上来展示这些新信息。与MessageBox一样,在Sheet关闭或以某种方式响应之前,无法进行进一步的操作。

让我们在我们的toolbar组件的items部分添加另一个按钮。这个按钮将弹出一个新的Sheet组件:

{
    text:'Sheet',
    handler:function () {
        var mySheet = Ext.create('Ext.Sheet', {
            height:250,
            layout:'vbox',
            stretchX:true,
            enter:'top',
            exit:'top',
            items:[
                {
                    xtype:'container',
                    layout:'fit',
                    flex:1,
                    padding:10,
                    style:'color: #FFFFFF',
                    html:'A sheet is also a panel. It can do anything the panel does.'
                },
                {
                    xtype:'button',
                    height:20,
                    text:'Close Me',
                    handler:function () {
                        this.up('sheet').hide();
                    }
                }
            ],
            listeners:{
                hide:function () {
                    this.destroy();
                }
            }
        });
    }
}
Ext.Viewport.add(mySheet);
mySheet.show();

这里有很多新东西,但有些应该看起来很熟悉。我们的按钮从按钮要显示的text值开始,然后创建了一个handler值,告诉按钮在点击时应该做什么。

然后我们创建了一个新的Ext.Sheet类。由于Sheet继承自面板,我们有一些熟悉的配置选项,如heightlayout,但我们还有一些新的选项。stretchXstretchY配置将导致Sheet组件扩展到屏幕的整个宽度(stretchX)或高度(stretchY)。

enterexit的值控制了Sheet组件如何在屏幕上滑动到位。你可以使用topbottomleftright

我们的表单使用vbox布局,包含两个项目,一个用于我们的文本的container对象和一个用于用户阅读完毕后隐藏Sheet组件的button对象。button组件本身包含了一段有趣的代码:

this.up('sheet').hide();

当我们提到this关键字时,我们是指button对象,因为函数发生在button本身内部。然而,我们实际上需要到达包含按钮的Sheet,以便在按钮被点击时关闭它。为了做到这一点,我们使用了一个巧妙的小方法,叫做up

up方法基本上会向上遍历代码结构,寻找所需的项。在这种情况下,我们通过xtype进行搜索,并请求搜索中遇到的第一个表单。然后我们可以使用hide()方法隐藏表单。

提示

Ext.ComponentQuery

当你想要获取一个组件,并且已经给它指定了一个 ID,你可以使用Ext.getCmp(),正如我们之前讨论的那样。如果你想要获取多个组件,或者根据它相对于另一个组件的位置来获取一个组件,你可以使用query()up()down()。要隐藏一个位于面板内的工具栏,你可以使用以下代码:

panel.down('toolbar').hide();

此外,要获取您应用程序中所有的工具栏,您可以使用以下命令:

var toolbars = Ext.ComponentQuery.query('toolbar');

一旦我们隐藏了Sheet组件,我们仍然有一个问题。现在Sheet组件是隐藏的,但它仍然存在于页面中。如果我们返回并再次点击按钮,而不销毁Sheet,我们就会不断创建越来越多的新的表单。这意味着越来越多的内存使用,这也意味着你的应用程序最终会走向死亡螺旋。

我们需要做的是确保我们清理好自己的东西,这样表格就不会堆积起来。这让我们来到了我们代码的最后部分和最后的listeners配置:

listeners: {
 hide: {
  fn: function(){ this.destroy(); }
 }
}

监听器监听特定事件,在这个例子中,是hide事件。当hide事件发生时,监听器然后运行fn配置中列出的附加代码。在这个例子中,我们使用this.destroy();来销毁Sheet组件。

在下一章,我们将详细介绍监听器和事件。

提示

关于 this 变量的一点说明

当我们在程序中使用变量this时,它总是指的是当前项目。在前面的例子中,我们在两个不同的地方使用了this,它指的是两个不同的对象。在我们最初的用法中,我们在按钮的配置选项中,所以this指的是按钮。当我们后来将this作为监听器的一部分时,我们在表格的配置中,所以this指的是表格。

如果您发现自己感到困惑,使用console.log(this);可以非常有帮助,以确保您正在 addressing 正确的组件。

你现在应该能够点击表格按钮并查看我们新的表格了。

创建一个表格组件

创建行动表格组件

ActionSheet是标准表格的一种变体,设计用于显示一系列按钮。当您只需要用户做出快速决策,有明显的选择且不需要过多解释时,这是一个很好的选择。例如,删除确认屏幕就是行动表格的一个很好的用途。

让我们在我们的布局中添加一个新的按钮,用于弹出一个用于删除确认的ActionSheet组件:

{
 text: 'ActionSheet',
 handler: function() {
  var actionSheet = Ext.create('Ext.ActionSheet', {
   items: [
   {
    text: 'Delete',
    ui  : 'decline'
   },
   {
    text: 'Save',
    ui  : 'confirm'
   },
   {
    text: 'Cancel',
    handler: function() {
     this.up('actionsheet').hide();
    }
   }
   ],
   listeners: {
    hide: {
     fn: function(){ this.destroy(); }
    }
   }
  });
  Ext.Viewport.add(actionSheet);
   actionSheet.show();
  }
}

ActionSheet对象以与我们的上一个表格示例非常相似的方式创建。然而,行动表格假设其所有项目都是按钮,除非您指定了不同的xtype值。

我们的例子有三个简单的按钮:删除保存取消取消按钮将隐藏ActionSheet组件,其他两个按钮只是装饰。

与我们的上一个示例一样,我们希望在隐藏它时也销毁ActionSheet组件。这可以防止ActionSheet组件的副本在后台堆积并造成问题。

点击我们应用程序中的行动表格按钮现在应该会显示我们创建的行动表格:

创建一个行动表格组件

创建一个 Map 组件

Map组件是一个非常特殊的容器,旨在与 Google Maps API 一起使用。该容器可用于显示 Google Maps 显示的大部分信息。

我们将为这个部分创建一个Map容器的非常基础的例子,但我们将在此返回第九章,高级主题,并介绍一些更高级的技巧。

为了这个例子,让我们创建一个新的 JavaScript 文件:

Ext.application({
 name: 'TouchStart',
 launch: function() {
  var map = Ext.create('Ext.Container', {
  fullscreen: true,
  layout: 'fit',
  items: [
   {
    xtype: 'map',
    useCurrentLocation: true
   }
  ]
  });
  this.viewport = map;
 }
});

在这个例子中,我们只是创建了一个带有单个项目的Container组件。这个项目是一个地图,并且配置了useCurrentLocation: true。这意味着浏览器将尝试使用我们的当前位置作为地图显示的中心。当这种情况发生时,用户总是会被警告,并且会被提供拒绝的选项。

在我们了解这是如何工作的之前,我们需要对我们的标准index.html文件进行一项更改。在包含我们其他 JavaScript 文件的行下面,我们需要包含来自 Google 的一个新文件:

  <!-- Google Maps API -->
  <script type="text/javascript" src="img/js?sensor=true"></script>

这将包括我们使用 Google Maps API 所需的所有函数。

如果您重新加载页面,系统会询问您是否允许当前位置被应用程序使用。一旦您接受,您应该会看到一个新的地图,您的当前位置在中心。

创建一个地图组件

您还可以使用map属性以及mapOptions配置选项来访问 Google Maps 的其他功能。我们将在第九章高级主题中探索一些这些选项,并且进行更详细的讲解。

提示

Google Maps API 文档

完整的 Google Maps API 文档可以在code.google.com/apis/maps/documentation/v3/reference.html找到。

创建列表

Sencha Touch 提供了几种不同的list组件。每个这些list组件都由三个基本部分组成:

  • 列表面板:它负责收集其配置选项中的其他项目。

  • XTemplate:这决定了列表中每一行的显示方式。

  • 数据存储:这里包含将在列表中使用的所有数据。

注意

还应该注意的是,一个存储区可以(并且通常会)与一个模型相关联,以定义存储区的数据记录。然而,也可以简单地将字段作为存储区的一部分定义,这在接下来的例子中我们会这样做。我们将在本书关于数据的章节中介绍模型和存储区。

在我们第一个例子中,我们创建了一个与这个类似的列表对象:

Ext.application({
name: 'TouchStart',
launch: function() {

var myDudeList = Ext.create('Ext.Container', {
 fullscreen: true,
 layout: 'fit',
 items: [
 {
   xtype: 'list',
   itemTpl: '{last}, {first}',
   store: Ext.create('Ext.data.Store', {
    fields: [
     {name: 'first', type: 'string'},
     {name: 'last', type: 'string'}
    ],
    data: [
     {first: 'Aaron', last: 'Karp'},
     {first: 'Baron', last: 'Chandler'},
     {first: 'Bryan', last: 'Johnson'},
     {first: 'David', last: 'Evans'},
     {first: 'John', last: 'Clark'},
     {first: 'Norbert', last: 'Taylor'}
    ]
   })
 }]
});
Ext.Viewport.add(myDudeList);
}
});

我们首先像以前一样创建我们的应用程序。然后我们创建了一个带有列表项目的单个容器。列表项目需要一个数据存储,而数据存储需要一组字段或数据模型。在这个例子中,我们将使用一组字段以简化操作。

fields: [
 {name: 'first', type: 'string'},
 {name: 'last', type: 'string'}
]

这段代码为我们每个数据记录提供了两个潜在的值:firstlast。它还告诉我们每个值的type;在这个例子中,两个都是strings。这使得数据存储知道如何处理数据的排序,并且让 XTemplate 知道数据如何被使用。

在这个示例中,我们设置了itemTpl: '{last}, {first}'。这个itemTpl值作为模板或 Sencha Touch 中的 XTemplate。XTemplate 从存储中的每个记录中获取数据,并告诉列表显示每个数据记录:姓氏,后面跟着一个逗号,然后是名字。我们将在第七章,获取数据外中详细介绍 XTemplates。

创建列表

请注意,目前我们的列表没有按字母顺序排序。我们需要在模型的配置选项下方添加一个排序器到存储中:

sorters: 'last'

这将按last(人的姓氏)值对我们的列表进行排序。

添加分组列表

分组列表也常见于许多应用程序中。通常,分组用于人员或其他字母顺序的物品列表。电话簿或长字母顺序数据列表是分组列表的好地方。分组列表在屏幕上放置一个indexBar组件,允许用户跳转到列表中的特定点。

为了对我们的当前列表进行分组,我们需要向我们的list组件添加两个配置设置。在声明xtype: 'list'下方添加以下代码:

grouped: true,
indexBar: true,

我们还需要向我们的存储添加一个函数,以获取显示我们字母indexBar的字符串。在store组件的sorters配置处替换以下代码:

grouper: {
  groupFn : function(record) {
    return record.get('last').substr(0, 1);
  },
  sortProperty: 'last'
}

这段代码使用record.get('last').substr(0,1)来获取我们联系人的姓氏的第一个字母。这让列表知道当点击indexBar组件上的字母时应该滚动到哪里。

添加分组列表

添加嵌套列表

NestedList组件自动化嵌套数据集的布局和导航。这对于您有一个项目列表和列表中每个项目的详细信息的情况非常有用。例如,假设我们有一个办公室列表,每个办公室都有一组部门,每个部门都由一些人组成。

我们可以首先将此显示为办公室列表。点击一个办公室会带你到该办公室内的部门列表。点击一个部门会带你到该部门的人员列表。

我们需要做的第一件事是一组用于此列表的数据:

var data = {
    text:'Offices',
    items:[
        {
            text:'Atlanta Office',
            items:[
                {
                    text:'Marketing',
                    items:[
                        {
                            text:'David Smith',
                            leaf:true
                        },
                        {
                            text:'Alex Wallace',
                            leaf:true
                        }
                    ]
                },
                {
                    text:'Sales',
                    items:[
                        {
                            text:'Jane West',
                            leaf:true
                        },
                        {
                            text:'Mike White',
                            leaf:true
                        }
                    ]
                }
            ]
        },
        {
            text:'Athens Office',
            items:[
                {
                    text:'IT',
                    items:[
                        {
                            text:'Baron Chandler',
                            leaf:true
                        },
                        {
                            text:'Aaron Karp',
                            leaf:true
                        }
                    ]
                },
                {
                    text:'Executive',
                    items:[
                        {
                            text:'Bryan Johnson',
                            leaf:true
                        },
                        {
                            text:'John Clark',
                            leaf:true
                        }
                    ]
                }
            ]
        }
    ]
};

这是一个相当庞大且看起来很丑的数据数组,但它可以分解为几个简单的部分:

  • 我们有一个名为Offices的主要项目。

  • Offices有一个包含两个项目的列表,Atlanta OfficeAthens Office

  • 这两项各有两个部门。

  • 每个部门有两个人。

这个列表中的每个人都有一个特殊的属性叫做leafleaf属性告诉我们的程序已经到达嵌套数据的末端。此外,我们列表中的每个项目都有一个名为text的属性。这个text属性是我们store中的fields列表的一部分。

然后我们可以创建我们的存储并将其数据添加到其中:

var store = Ext.create('Ext.data.TreeStore', {
 root: data,
 fields: [{name: 'text', type: 'string'}],
 defaultRootProperty: 'items',
 autoLoad: true
});

对于NestedList组件,我们需要使用TreeStore类,并将root配置指向我们之前定义的data数组变量。这将告诉存储器在我们数据的第一组项目中最开始查找的位置。

最后,我们需要创建我们的NestedList

var nestedList = Ext.create('Ext.NestedList', {
    fullscreen: true,
    title: 'Minions',
    displayField: 'text',
    store: store
});

我们将NestedList组件设置为全屏,同时也设置了title值,告诉它要显示哪个字段,最后,我们将其指向我们的存储,以便它可以获取我们创建的数据。

添加嵌套列表

如果你点击嵌套列表,你会注意到点击动作已经被自动添加。这同样适用于上导航和标题。

NestedList组件为在小型屏幕上快速有效地显示层次化数据提供了一个很好的起点。

使用 Sencha Docs 查找更多信息

在本章中,我们覆盖了很多信息,但它只是 Sencha Touch API 文档中可用信息的一小部分。

使用 Sencha Docs 查找更多信息

起初,API 可能会让人感到有些不知所措,但如果你理解了其组织结构,你就可以快速找到所需的信息。这里有一些帮你入门的小贴士。

查找组件

API 的左侧包含五个标签页,内容如下:

  • 主屏幕包含 Sencha Touch 的一般营销信息。

  • 带有列表中每个可用组件的 API 文档。

  • 指南部分,其中包含有关各种组件及其用途的更详细文章。

  • 视频部分,其中包含多个视频演讲,详细介绍布局和 MVC 等主题。

  • 示例部分,其中包含许多 Sencha Touch 组件及其功能的多项示例。

查找组件

如果你点击 API 标签,可以浏览一个组件列表。你还可以在文档页面上方右侧的搜索框中快速查找组件。

当你点击 API 列表中的项目时,标签页将打开屏幕的主要部分,并详细介绍组件的信息。

理解组件页面

单个组件页面顶部的信息为理解组件的工作提供了巨大的跳板。

理解组件页面

快速扫描右侧的组件层次结构,会告诉你组件继承了哪些其他项目。如果你理解了基本组件,如容器和面板,你可以迅速利用这些知识来指导你使用新组件。

顶部标题还列出了组件的xtype值。

在标题下方,有一系列菜单,包括:

  • Config:组件创建时使用的初始选项。

  • 属性:创建组件后您可以从组件中获取的信息

  • 方法:组件创建后知道如何执行的操作

  • 事件:组件创建后关注的事情

  • CSS 变量:可用于样式化组件(仅在某些组件上)的可用的 CSS 变量列表

  • CSS 混合:组件可用的混合列表(仅在某些组件上)

还有一个文本框用于过滤类成员,一个菜单用于控制列表中出现的类成员类型,以及一个按钮用于展开页面上的所有项。

大多数常见组件在页面的顶部都包含示例。当在 WebKit 浏览器(Safari 或 Chrome)中查看时,这些示例包括一个实时预览 / 代码编辑器选项,可以切换。这将显示用户看到的组件,或者是创建组件的实际代码。

正如名称所暗示的,代码编辑器选项实际上可以编辑以测试不同的配置选项。还有一个选择代码选项,它将允许你复制代码并将其粘贴到自己的应用程序中。

这些信息应该为您学习 API 中的任何组件提供了一个起点。

总结

在本章中,我们首先查看了一个基本组件,名为Ext.Component。我们还研究了组件是如何创建的。然后我们详细探讨了容器的布局,展示了它是如何影响容器内部的子项的。

本章还描述了 Sencha Touch 中一些更常见且实用的组件,包括:容器、面板、TabPanel、Carousel、FormPanel、FormItem、MessageBox、Sheet、列表和嵌套列表。我们在章节的最后提供了一些使用 Sencha Touch API 的建议。

在下一章中,我们将介绍 Sencha Touch 中事件的使用。

第五章:事件和控制器

在上一章中,我们详细查看了 Sencha Touch 中可用的组件。然而,仅仅创建组件还不足以构建一个应用程序。组件仍然需要彼此通信,以便我们的应用程序做些真正有用的事情。事件和控制器就在这里发挥作用。

在本章中,我们将探讨 Sencha Touch 中的事件和控制器:它们是什么,为什么我们需要它们,以及它们是如何工作的。我们将讨论如何使用监听器和处理程序使您的应用程序对用户的触摸以及后台发生的事件做出反应。我们还将介绍一些有用的概念,例如可观察的捕获和事件代理。最后,我们将通过查看触摸特定事件和如何从 Sencha Touch API 获取更多信息来完成本章。

本章将涵盖以下内容:

  • 事件

  • 监听器和处理程序

  • 控制器

  • 监听器选项

  • 作用域

  • 移除事件

  • 处理程序和按钮

  • 常见事件

  • 关于事件的其他信息

探索事件

作为程序员,我们倾向于将代码视为一个有序的指令序列,逐行执行。很容易忽视的事实是,我们的代码实际上花费了很多时间坐着等待用户做些什么。它正在等待用户点击一个按钮,打开一个窗口,或者从列表中选择。代码正在等待一个事件的发生。

通常,事件发生在组件执行特定任务之前或立即之后。当任务执行时,事件被广播到系统其余部分,在那里它可以触发特定的代码,或者可以被其他组件用来触发新的动作。

例如,在 Sencha Touch 中,每当点击按钮时,按钮就会触发一个事件。这个点击可以执行按钮内的代码,创建一个新的对话框,或者一个面板组件可以“监听”按钮正在做什么,并在听到按钮触发tap事件时改变其颜色。

由于大多数应用程序都是为了人机交互而设计的,所以说程序的大部分功能都来自于对事件的响应是安全的。从用户的角度来看,事件是使程序实际“做”事情的东西。程序正在响应用户的请求。

除了响应请求外,事件在确保事情按正确顺序发生方面也起着重要的作用。

异步与同步操作

爱因斯坦曾经说过:

时间存在的唯一原因是让一切不同时发生。

虽然这可能看起来像是一个随意的评论,但实际上在编写代码时与之有很大的关联。

在 Sencha Touch 中编写代码时,我们正在指导网络浏览器在用户的屏幕上创建和销毁组件。这个过程的明显限制是,我们既不能在组件创建之前操纵它,也不能在它被销毁之后操纵它。

这看起来在第一眼似乎相当直接。你永远不会在实际创建组件之前写一行试图与组件交谈的代码,那么问题是什么?

这个问题与代码中的异步动作有关。尽管我们的大部分代码将按顺序或以同步方式执行,但有许多情况我们需要发出一个请求并得到回应才能继续。这在基于 web 的应用程序中尤为正确。

例如,假设我们有一行代码,它使用来自 Google 地图的请求来构建一个地图。我们需要等待我们从 Google 那里得到回应并渲染我们的地图,然后我们才能开始在地图上工作。然而,我们不想让我们的应用程序的其他部分在我们等待回应时冻结。因此我们发起一个异步请求,这个请求在后台进行,而我们的应用程序的其他部分继续它的业务。

这种异步请求称为 Ajax 请求。"Ajax"代表异步 JavaScript 和 XML。如果我们配置我们其中一个按钮发出一个 AJAX 请求,用户在应用程序等待回应时仍然可以执行其他操作。

在界面方面,你可能想要让用户知道我们已经发出了请求,并正在等待回应。在大多数情况下,这意味着显示一个加载信息或一个动画图形。

在 Sencha Touch 中使用事件,我们可以通过绑定到 Ajax 组件的beforerequest事件来显示加载图形。由于我们需要知道何时让加载信息消失,因此我们的组件将等待来自 Ajax 请求的requestcomplete事件。一旦这个事件触发,我们就可以执行一些代码来告诉加载信息消失。我们还可以使用requestexception事件来告知用户在请求过程中是否出现错误。

使用这种事件驱动的设计允许你快速响应用户的操作,而不需要让他们等待你的代码需要执行的一些更耗时的请求。你还可以用事件来告知用户关于错误的信息。事件的关键在于让你的其他组件“监听”到这个事件,然后告诉他们如何处理收到的信息。

添加监听器和处理程序

每个 Sencha Touch 组件都能生成一大串事件。 鉴于你应用中可能会有大量的组件,你可以预期会有很多交互。

想象一个有 100 个人的聚会,每个人都在进行着许多不同的对话。现在想象一下,试图从每个对话中提取所有有用的信息。这是不可能的。你必须专注于某个特定的对话,才能收集到有用的信息。

同样的,组件也需要被告知要监听什么,否则我们可怜的聚会参与者很快就会感到不知所措。幸运的是,我们有针对这一点的配置。

listeners配置告诉组件需要关注哪些事件。监听器可以像 Sencha Touch 中的任何其他配置选项一样添加。例如,面板的配置选项可能如下所示:

listeners: {
 singletap: {
  element: 'element',
  fn: function(){ Ext.Msg.alert('Single Tap'); }
 }
}

这个配置选项告诉面板在用户在面板内部元素上单击一次时监听singletap事件。当singletap事件发生时,我们执行fn配置选项中列出的函数(这通常被称为处理程序)。在这种情况下,我们弹出一个带有消息警告Single Tap的消息框。

请注意,我们listeners配置中的项目总是作为一个对象的一部分(无论是否只有一个事件我们正在监听),即使我们只监听一个事件也是如此。如果我们添加第二个事件,它将如下所示:

listeners: {
 singletap: {
  element: 'element',
  fn: function(){ Ext.Msg.alert('Single Tap'); }
 },
 hide: {
  fn: function(){ this.destroy(); }
 }
}

注意

如果事件没有其他属性,你也可以像这样缩短事件声明:hide: function(){ this.destroy(); }

我们还可以从监听器中获取信息并用在我们的处理函数中。例如,singletap事件会返回event对象,被点击的 DOM 元素以及我们如果在面板上有以下监听器的话,还会返回listener对象本身:

listeners: {
  singletap: {
    element: 'element',
    fn: function(event, div, listener) {
      console.log(event, div, listener);
    }
  }
}

当用户在面板内单击时,我们将在控制台上获得一个视图,类似于以下内容:

Adding listeners and handlers

提示

事件参数

您会注意到某些默认值会被传递到我们的事件中。这些默认值可以在每个组件的docs.sencha.com/touch/2.2.1/中找到。

每个事件都将有它自己的默认值。选择一个组件从 Sencha API 文档,然后点击页面顶部的Events查看组件的所有事件。每个事件的描述将包括其默认参数。

从控制台可以看出,我们的event对象包含了一个在单击发生时的 UnixtimeStamp,以及单击本身pageXpageY坐标,还有被单击的div标签的整个内容。您可能还注意到我们的tap事件在我们的调试输出中被称为mouseup事件。在 Sencha Touch 中,singletapmouseup事件是彼此的别名。这保留了与桌面浏览器传统的mouseup事件和移动浏览器singletap事件之间的兼容性。

我们可以在我们函数内部使用所有这些信息。

为了这个例子,我们将创建一个带有红色容器的简单面板。我们的singletap监听器将改变红色盒子的尺寸以匹配我们屏幕上的单击位置,如下代码片段所示:

Ext.application({
 name: 'TouchStart',
 launch: function() {
  var eventPanel = Ext.create('Ext.Panel', {
   fullscreen: true,
   layout: 'auto',
   items: [{
    xtype: 'container',
    width: 40,
    height: 40,
    id: 'tapTarget',
    style: 'background-color: #800000;'
   }],
   listeners: {
    singletap: {
     element: 'element',
     fn: function(event, div, listener) {
      var cmp = Ext.getCmp('tapTarget');
      cmp.setWidth(event.pageX);
      cmp.setHeight(event.pageY);
      console.log(event.pageX, event.pageY);
     }
    }
   }
  });
  Ext.Viewport.add(eventPanel);
 }
});

如果我们打开控制台运行这段代码,我们可以看到我们单击的位置的 x 和 y 坐标会在控制台出现。我们的盒子也会根据这些值来匹配大小。

Adding listeners and handlers

正如您在前面代码中看到的,我们监听了tap事件。然后我们使用Ext.getCmp('tapTarget');获取container组件,并根据从tap事件返回的值改变红色盒子的尺寸:

singletap: {
 element: 'element',
 fn: function(event, div, listener) {
  var cmp = Ext.getCmp('tapTarget');
  cmp.setWidth(event.pageX);
  cmp.setHeight(event.pageY);
 }
} 

这是一个使用 Sencha Touch 事件的基本示例。然而,我们的大多数应用程序通常会做不止一件简单的事情。我们还可以使用 ID 和Ext.getCmp()获取它们。在大型应用程序中,不小心创建具有相同 ID 的组件或在已由 Sencha Touch 使用的 ID 创建组件是非常容易的。这通常会导致应用程序的螺旋死亡和大量扯头发。

提示

作为一种最佳实践,避免为 addressing components 使用 ID 是个好主意。在接下来的几节中,我们将开始向您展示更可靠的方法来引用我们各个组件。

如果我们打算构建比这种“单招马”更复杂的应用程序,我们可能想要开始考虑将我们的事件和动作分离到适当的控制器中,并找到一种更好地引用我们不同组件的方法。

控制器

在第三章 用户界面样式中,我们稍微谈到了模型视图控制器MVC)架构。这种架构将我们的文件划分为数据文件(ModelsStores)、界面文件(Views)以及处理功能(Controllers)的文件。在本节中,我们将重点关注 MVC 的控制器部分。

在最基本层面上,控制器在应用程序中分配监听器和动作。与我们的前一个示例不同,在那里单个组件负责处理事件,控制器将处理我们应用程序中每个组件的事件。

这种劳动分工在创建应用程序时提供了几个不同的优势,如下所述:

  • 当我们知道我们的函数都在控制器中,并且与显示逻辑分离时,代码更容易导航。

  • 控制器为应用程序中各个显示组件提供了一个更简单的通信层。

  • 控制器可以根据功能划分为不同的文件。例如,我们可以有一个用户控制器,它处理用户数据的事件和监听器,还有一个单独的公司控制器,它处理公司数据的事件和监听器。这意味着如果一个用于保存新用户的表单不能正确工作,我们知道要查看哪个文件来尝试找出问题所在。

让我们通过一个例子来看看我们在谈论什么。我们将从使用 Sencha Cmd 生成的基本启动应用程序开始,使用以下命令行:

sencha generate app TouchStart /Path/to/Save/Application

Controllers

路径将根据您的设置而变化,但这将给我们提供我们将添加控制器的基本应用程序。

注意

想回顾一下 Sencha Cmd 和 MVC 的基础知识,请参见第三章,用户界面样式

如果我们查看我们新创建的应用程序的app/controller文件夹,我们会发现它是空的。让我们先在这里创建一个Main.js文件。在新文件中,我们将添加:

Ext.define('TouchStart.controller.Main', {
 extend: 'Ext.app.Controller',

});

这扩展了基本的Ext.app.Controller组件,但其他什么也没做。我们的控制器需要理解一些基本的东西,以便正确地工作;它们如下:

  • 控制器控制了应用程序的哪些部分?

  • 它应该监听哪些组件事件?

  • 当其中一个事件被触发时,它应该做什么?

这个谜题的第一部分是由引用(refs)处理的。

Refs and control

refs部分使用ComponentQuery语法来创建对应用程序中组件的内部引用。ComponentQuery语法允许我们根据 ID、xtype 和其他任何配置选项来查找组件。

例如,在我们的app/view目录中有一个Main.js文件(它是由 Sencha Cmd 自动生成的)。view组件有一个xtype值为main。我们可以像以下这样将这个视图文件添加到我们的控制器中:

Ext.define('TouchStart.controller.Main', {
 extend: 'Ext.app.Controller',
 views: ['TouchStart.views.Main'],
 config: {
  refs: {
   mainView: 'main'
  }
 }
});

这告诉我们的控制器,它控制着TouchStart.views.Main视图文件,并且我们将用一个简写 m(这是我们的选择)来引用这个特定的组件。通过创建这个引用,我们自动为该组件创建了一个 getter 函数。这意味着当我们在控制器中需要引用这个组件的其他地方时,例如如果我们需要向我们的标签面板添加一个新的标签,我们只需使用this.getMainView()来获取组件。

Tip

这里又是大小写可以悄无声息地攻击你的另一个地方。你会注意到,尽管我们用小写的m给我们的引用命名,但 get 函数使用的是大写的M。如果我们给我们的引用命名为mainPanel,get 函数将是this.getMainPanel()。第一个字母总是是大写的。

让我们向我们的基本应用程序添加一些元素,以确切了解这是如何工作的。首先我们需要在Main.js视图文件中添加一个按钮。在我们第一个面板(带有标题的那个)中,将项目部分修改如下以添加一个按钮:

items: [{
 docked: 'top',
 xtype: 'titlebar',
 title: 'Welcome to Sencha Touch 2',
 items: [
  { 
   text: 'Add Tab',
   action: 'addtab',
  }
 ]
}] 

请注意,这次我们没有在这里添加处理程序,但我们确实有一个actionaddtab,我们将用它来在我们的控制器中引用按钮:

Refs and control

回到我们位于app/controller/Main.js文件,我们将添加一个refscontrol部分如下:

Ext.define('TouchStart.controller.Main', {
 extend: 'Ext.app.Controller',
 config: {
 views: ['TouchStart.view.Main'],
  refs: {
   m: 'main',
   addBtn: 'button[action=addtab]'
  },
  control: {
   addBtn: {
    tap: 'addNewTab'
   }
  }
 }
});

现在我们有了按钮的新引用:

addBtn: 'button[action=addtab]'

Tip

需要注意的是,我们按钮上的action配置完全是任意的。我们可以称它为myPurposeInLife: 'addtab',这对组件本身没有任何影响。在这种情况下,我们只是将按钮引用为addBtn: 'button[myPurposeInLife = addtab]'。术语action通常是按惯例使用的,但它不是按钮的默认配置选项。它只是我们稍后将在控制器中使用ComponentQuery查找按钮的值。

现在我们已经有了引用,我们可以在设置控制时使用addBtn。这个control部分是我们为这个特定按钮设置监听器的地方:

 control: {
   addBtn: {
    tap: 'addNewTab'
   }
  }

这个control部分表示我们希望我们的控制器监听addBtn按钮的轻触事件,并在用户轻触按钮时触发addNewTab函数。接下来,我们需要将这个addNewTab函数添加到我们控制器的底部,位于config部分之后(不要忘记在config部分的末尾和新的函数之间加上逗号),如下面的代码片段所示:

addNewTab: function() {
  this.getMainView().add({
   title: 'My New Tab',
   iconCls: 'star',
   html: 'Some words of wisdom...'
  });
 }

这个函数使用我们的this.getMainView()函数来获取我们的主标签面板,并向其添加一个新的标签。现在我们点击按钮,我们应该会看到一个带有星形图标和我们 HTML 文本的新标签:

Refs 和 control

每个控制器文件可以包含任意数量的视图、引用和函数。然而,通常最好将您的控制器根据它们处理的数据类型分成单独的文件(一个用于用户,一个用于公司,另一个用于消息,等等)。这种代码组织完全取决于程序员,但它有助于大大减少寻找问题的难度。

使用 ComponentQuery 引用多个项目

正如我们之前的示例所看到的,refs部分为我们组件提供了简写式的引用名称,而control部分允许我们将监听器和函数分配给我们的组件。尽管我们可以使用control部分将单个函数分配给多个组件,但我们在refs部分包含的项目只能是单数的。我们无法在refs部分为多个组件创建一个单一的引用。

然而,我们可以通过使用Ext.ComponentQuery来解决这个问题。

为了演示这一点,让我们来看一个真实世界的例子:一个带有添加、编辑和删除按钮的条目列表。添加按钮应该始终是可用的,而编辑删除按钮只有在列表中选择了某个项目时才应该是活动的。

使用 ComponentQuery 引用多个项目

我们将创建一个名为PersonList.js的列表,位于view文件夹中,如下面的代码片段所示:

Ext.define('TouchStart.view.PersonList', {
    extend: 'Ext.dataview.List',
    xtype: 'personlist',
    config: {
        itemTpl: '{last}, {first}',
        store: Ext.create('Ext.data.Store', {
            sorters: 'last',
            autoLoad: true,
            fields: [
                {name: 'first', type: 'string'},
                {name: 'last', type: 'string'}
            ],
            data: [
                {first: 'Aaron', last: 'Karp'},
                {first: 'Baron', last: 'Chandler'},
                {first: 'Bryan', last: 'Johnson'},
                {first: 'David', last: 'Evans'},
                {first: 'John', last: 'Clark'},
                {first: 'Norbert', last: 'Taylor'},
                {first: 'Jane', last: 'West'}
            ]
        })
    }
});

这类似于我们在第五章,事件和控制器中创建的列表,只不过我们通过使用Ext.define并扩展Ext.dataview.List对象,将其变成了一个独立的view组件。我们本可以将它简单地作为我们的Main.js视图文件的一部分,但将其分离出来允许我们定义一个自定义的xtypepersonlist,这将使我们在控制器中引用它变得更容易。

注意

为了简化,我们将store作为我们视图的一部分,而不是将其分离到store目录中的单独文件中。我们将在第七章,获取数据和第八章,创建 Flickr 查找器应用程序中讨论如何实现,其中我们将介绍存储和模型。

现在我们已经有了personlist视图,我们需要将其添加到我们的Main.js视图文件中。让我们替换Main.js文件中的第二个面板(其中包含视频链接的那个)。新面板将看起来像这样:

{
    title: 'Advanced',
    iconCls: 'action',
    layout: 'fit',
    items: [{
        docked: 'top',
        xtype: 'toolbar',
        items: [
            {
                text: 'Add',
                action: 'additem'
            },
            {
                text: 'Edit',
                action: 'edititem',
                enableOnSelection: true,
                disabled: true
            },
            {
                text: 'Delete',
                action: 'deleteitem',
                enableOnSelection: true,
                disabled: true
            }
        ]
    },
        { xtype: 'personlist'}
    ]
}

这段代码创建了一个带有fit布局和两个项目的新面板。第一个项目是一个工具栏,固定在面板的顶部。第二个项目(在非常底部)是我们的personlist组件。

工具栏有自己的项目,包括三个带有文本添加编辑删除的按钮。每个按钮都有自己的独立action配置,而编辑删除按钮有一个额外的配置:

enableOnSelection: true

注意

请注意,与action一样,enableOnSelection配置是任意值,而不是按钮组件的默认配置。

单个action配置将允许我们将函数分配给每个按钮。共享的enableOnSelection配置将允许我们用一个引用抓取编辑删除按钮。让我们回到我们的Main.js控制器看看这是如何工作的。

我们首先想要做的是让Main.js控制器知道它负责我们的新personlist视图。我们通过将其添加到控制器中的views列表来实现,如下面的代码片段所示:

views: ['TouchStart.view.Main', 'TouchStart.view.PersonList']

接下来,我们需要在refs部分创建我们的引用,如下面的代码片段所示:

refs: {
    mainView: 'main',
    addBtn: 'button[action=addtab]',
    addItem: 'button[action=additem]',
    editItem: 'button[action=edititem]',
    deleteItem: 'button[action=deleteitem]',
    personList: 'personlist'
}

然后,我们将修改我们的control部分,使其如下所示:

control:{
    addBtn:{
        tap:'addNewTab'
    },
    personList:{
        select:'enableItemButtons'
    },
    addItem:{
        tap: 'tempFunction'
    },
    editItem:{
        tap: 'tempFunction'
    },
    deleteItem:{
        tap: 'tempFunction'
    }
}

在这里,我们将我们的personList组件设置为监听select事件,并在事件发生时触发enableItemButtons函数。我们还为我们的三个按钮的tap事件分配了一个单独的tempFunction函数。

我们的tempFunction在现有的addNewTab函数之后添加,如下所示:

tempFunction:function () {
    console.log(arguments);
}

这只是为了演示目的而暂时使用的函数(我们将在第七章,获取数据和第八章,创建 Flickr 查找器应用程序中更详细地介绍添加、编辑和删除操作)。现在,这个临时函数只是记录发送给它的参数。

提示

在 JavaScript 中,arguments是一个特殊的变量,它包含了传递给函数的许多变量。这对于使用控制台日志来说非常棒,因为你可能不清楚你的函数接收到的变量,它们的顺序,或者它们的格式。

第二个函数将处理我们的列表选择:

enableItemButtons:function () {
     var disabledItemButtons =   Ext.ComponentQuery.query('button[enableOnSelection]');
     Ext.each(disabledItemButtons, function(button) {
        button.enable();
     });
}

正如我们之前所提到的,我们不能简单地为我们的两个禁用按钮创建一个refs列表。如果我们尝试在我们的refs部分使用myButtons: 'button[enableOnSelection]',我们只能得到第一个按钮。

然而,我们可以使用完全相同的选择器Ext.ComponentQuery.query('button[enableOnSelection]');,得到两个按钮作为一个按钮对象的数组。然后我们可以使用Ext.each逐一遍历每个按钮,并在它们上面运行一个函数。

在这种情况下,我们只是在每个按钮上运行button.enable();。现在当列表中选择一个项目时,我们的两个按钮都将被启用。

使用 ComponentQuery 引用多个项目

通过使用Ext.ComponentQuery,一个事件可以轻松地根据它们的属性影响多个组件。

从事件中获取更多内容

既然我们已经了解了事件和控制器是如何结合在一起的,我们需要看看事件的其他用途和可用选项。

自定义事件

虽然 Sencha Touch 组件响应大量的事件,但有时在应用程序内部触发自定义事件可能会有所帮助。

例如,你可以触发一个名为vikinginvasion的自定义事件,这可能会触发你应用程序中的其他操作。在这个例子中,我们将假设我们有一个名为cmp的组件。我们可以通过调用这个组件来触发事件:

cmp.fireEvent('vikinginvasion');

然后,你可以在控制器的control部分为vikinginvasion添加一个监听器,以及一个处理事件的函数。如果我们想为自定义事件添加监听器到名为trebuchet的组件,它可能如下所示:

control: {
 trebuchet: {
  vikinginvasion: 'fireAtWill'
 }
}

你还可以检查一个组件是否具有特定的监听器,使用hasListener()方法:

if(this.getTrebuchet.hasListener('vikinginvasion') {
  console.log('Component is alert for invasion');
} else {
  console.log('Component is asleep at its post');
}

还有许多有用的选项,你可以使用它们来控制监听器如何检查事件。

探索监听器选项

在大多数情况下,监听器可以通过事件名称、处理程序和作用域来配置,但有时你需要更多的控制。Sencha Touch 提供了一系列有用的选项来修改监听器的工作方式;它们包括:

  • delay:这将延迟事件触发后处理程序的执行。它以毫秒为单位给出。

  • single: 这提供了一个一次性处理器,在下一个事件触发后执行,然后将自己移除。

  • buffer:这会导致处理器作为Ext.util.DelayedTask组件的一部分被调度运行。这意味着如果一个事件被触发,我们在执行处理器之前等待一段时间。如果在我们的延迟时间内再次触发相同的事件,我们在执行处理器之前重置计时器(只执行一次)。这在对文本字段的变化事件进行监控时可能很有用——在用户最后一次更改后等待 300 毫秒才触发事件的功能。

  • element:这允许我们在组件内指定一个特定的元素。例如,我们可以在面板的tap事件上指定一个正文。这将忽略附着项的点击,只监听面板正文的点击。

  • target:这将限制监听器仅接收来自目标的事件,并忽略来自其子元素的同类事件。

使用不同的监听器选项,代码可能看起来像以下这样:

this.getTrebuchet.on('vikinginvasion', this.handleInvasion, this, {
 single: true,
 delay: 100
});

这个示例将为vikinginvasion添加一个监听器,并在本作用域中执行一个名为handleInvasion的函数。处理器只会执行一次,在 100 毫秒的延迟后。然后将自己从组件中移除。

如果你在一个控制器内,你可以这样在control部分完成同样的事情:

control:{
 Trebuchet:{
  vikinginvasion: {
   fn: this.handleInvasion,
   single: true,
   delay: 100
  }
 }
}

由于我们在vikinginvasion的事件监听器上设置选项,它变成了自己的配置对象。反过来,我们的handleInvasion函数变成了一个名为fn的配置选项。

这些基本的配置选项在添加监听器时给你带来了相当大的灵活性。然而,在监听器中还有一个可用的附加配置选项,需要稍作解释。它叫做scope

仔细查看作用域

在你的处理函数中有一个特殊的变量叫做this。通常,this指的是触发事件的组件,在这种情况下,scope通常设置为scope: this。然而,在监听器配置中指定scope的不同值是可能的:

Ext.application({
 name: 'TouchStart',
 launch: function() {
  var btn = Ext.create('Ext.Button', {
   xtype: 'button',
   centered: true,
   text: 'Click me'
  });
  var Mainpanel = Ext.create('Ext.Panel', {
   html: 'Panel HTML'
  });
  btn.on({ 
   painted: {
    fn: function() {
     console.log('This should show our button %o', this)
    }
   },
   tap: {
    scope: Mainpanel,
    fn: function() {
     console.log('This should show our main panel %o', this)
    }
   }
  });
  Ext.Viewport.add(btn);
  Ext.Viewport.add(Mainpanel);
 }
});

在此我们创建了一个名为btn的按钮和一个名为Mainpanel的面板。然后附上两个监听器。第一个是在按钮的painted事件上。这个事件在按钮“绘制”(出现在)屏幕上时立即触发。在这种情况下,函数的作用域是button,这是我们可以预期的默认情况。

第二个是在buttontap事件上。tap事件的scopeMainpanel。这意味着,尽管监听器附着在按钮上,但函数将this视为Mainpanel组件,而不是按钮。

虽然scope这个概念可能难以理解,但它是监听器配置中的一个非常实用的部分。

移除监听器

通常,当组件被销毁时,监听器会自动移除。然而,有时您会在组件被销毁之前想要移除监听器。为此,你需要一个你创建监听器时创建的处理函数的引用。

到目前为止,我们一直使用匿名函数来创建我们的监听器,但如果我们想要移除监听器,我们需要稍有不同的方法:

var myPanel = Ext.create('Ext.Panel', {…});

var myHandler = function() {
  console.log('myHandler called.');
};

myPanel.on('click', myHandler);

这是一个好习惯,因为它允许你一次性定义处理函数,并在需要的地方重复使用它们。它还允许你稍后移除处理程序:

myPanel.removeListener('click', myHandler);

提示

在 Sencha 的术语中,on()addListener()的别名,而un()removeListener()的别名,这意味着它们做完全相同的事情。在处理事件时,你可以自由选择使用你喜欢的方法。

还应注意的是,作为控制器control部分添加的监听器永远不会被移除。

使用处理程序和按钮

正如您可能从我们之前的某些代码中注意到的,按钮有一个默认配置称为handler。这是因为按钮的一般目的是被点击或轻触。handler配置只是添加tap监听器的有用简写。因此,下面的两段代码完全相同:

var button = Ext.create('Ext.Button', {
  text: 'press me',
  handler: function() {
    this.setText('Pressed');
  }
})
var button = Ext.create('Ext.Button', {
  text: 'press me',
  listener: {
   tap: {
      fn: function() {
        this.setText('Pressed');
     }
    }
  }
});

接下来,我们将查看一些常见事件。

探索常见事件

让我们看看我们的老朋友Ext.Component,并了解一些我们可以使用的一些常见事件。记住,由于我们的大多数组件将继承自Ext.Component,这些事件将贯穿我们使用的大多数组件。这些事件中的第一个与组件的创建有关。

当 Web 浏览器执行你的 Sencha Touch 代码时,它将组件写入网页作为一系列divspan和其他标准 HTML 标签。这些元素还与 Sencha Touch 中的代码链接在一起,以标准化所有支持 Web 浏览器的组件的外观和功能。这个过程通常被称为渲染组件。在 Sencha Touch 中控制这个渲染的事件称为painted

其他一些常见事件包括:

  • show:当在组件上使用show方法时触发

  • hide:当在组件上使用hide方法时触发

  • destroy:当组件被销毁时触发

  • disabledchange:当通过setDisabled更改disabled配置时触发

  • widthchange:当在组件上调用setWidth时触发

  • heightchange:当在组件上调用setHeight时触发

这些事件为您提供了一种基于组件正在执行或对组件执行的操作来编写代码的方法。

提示

名称以changed结尾的每个事件都是由于config选项已更改而触发的;例如,setWidthsetHeightsetTop。虽然监听这些事件与监听任何其他事件类似,但了解这个约定是有用的。

每个组件还将有一些与之关联的特定事件。有关这些事件的列表,请参阅可用的文档docs.sencha.com/touch/2.2.1。在左侧列表中选择一个组件,然后点击页面顶部的事件按钮。

更多信息

关于事件的信息可以在 Sencha Docs 中找到docs.sencha.com/touch/2.2.1。在左侧列表中选择一个组件,然后在顶部寻找事件按钮。您可以点击事件以跳转到该部分的开始,或者将鼠标悬停在上面以查看完整的事件列表并从中选择特定事件。

点击事件旁边的向下箭头将显示事件的参数列表以及关于如何使用事件的任何可用示例。

另一个了解触摸特定事件的好地方是 Kitchen Sink 示例应用程序(dev.sencha.com/deploy/touch/examples/kitchensink/)。在应用程序中有一个触摸事件部分。这个部分允许您轻触或点击屏幕以查看不同轻触和手势生成的哪些事件。

Sencha Touch 的 WebKit 团队还创建了一个用于 Android 的事件记录器。您可以在www.sencha.com/blog/event-recorder-for-android-web-applications/找到更多信息。

总结

在本章中,我们介绍了事件的基本概述,以及如何使用监听器和处理程序使程序对这些事件做出响应。我们深入探讨了控制器及其如何使用引用和control部分来附加监听器到组件。我们介绍了Ext.ComponentQuery(),用于在事件处理程序中获取组件。我们谈论了自定义事件、按钮中的处理程序,并列出了一些常见事件。

在下一章中,我们将介绍如何在 Sencha Touch 中获取和存储数据,使用 JSON、数据存储、模型和表单。

第六章:获取数据

任何应用程序的关键方面之一是处理数据——将数据输入应用程序,以便您可以操作和存储它,然后再次获取以供显示。我们将用接下来的两章来讨论 Sencha Touch 中的数据处理。本章将重点介绍如何将数据输入您的应用程序。

我们将从讨论用于描述您数据的模型开始。然后,我们将讨论收集数据的读取器以及用于在应用程序中保存数据的存储。一旦我们了解了数据去了哪里,我们将介绍如何使用表单来获取数据。我们将查看如何验证您的数据,并为您提供一些表单提交示例。最后,我们将介绍如何将数据回填到表单中以进行编辑。这将是下一章关于数据的起点,该章节将涵盖如何获取数据以供显示。

本章涵盖了以下主题:

  • 数据模型

  • 数据格式

  • 数据存储

  • 使用表单和数据存储

模型

在 Sencha Touch 应用程序中处理数据的第一步是创建数据的模型。如果您习惯于数据库驱动的应用程序,将模型视为数据库架构会有所帮助;这是一个定义我们将要存储的数据的构造,包括数据类型、验证和结构。这为我们的应用程序的其余部分提供了一个共同的映射,用于理解来回传递的数据。

在 Sencha Touch 2 中,模型还可以用于保存单个数据记录的信息。这意味着我们可以使用已经内置到 Sencha Touch Ext.data.Model组件中的函数来创建、读取、更新和删除单个记录。

基本模型

在最基本的情况下,模型使用Ext.define()描述数据字段,如下所示:

Ext.define('User', {
extend: 'Ext.data.Model',
config: {
  fields: [
    {name: 'firstname', type: 'string'},
    {name: 'lastname', type: 'string'},
    {name: 'username', type: 'string'},
    {name: 'age', type: 'int'},
    {name: 'email', type: 'string'},
    {name: 'active', type: 'boolean', defaultValue: true},
  ]
 }
}

第一行声明我们已经将新模型命名为User,并且我们正在扩展默认的Ext.data.Model。我们在config部分内设置模型的配置选项。

提示

在版本 2 中,模型设置有所变化。我们现在使用Ext.define和扩展,而不是通过旧的模型管理器创建事物。我们还将模型的选项包裹在一个config部分内。在extend设置外,您的模型选项的其余部分应该用这个config部分包裹起来。

config部分内,我们将描述我们的数据字段作为一个fields数组,包括nametype和可选的defaultValue字段。name字段就是我们希望在代码中引用数据的方式。type的有效值是:

  • auto:这是一个默认值,它接受原始数据而不进行转换

  • string:这将数据转换为字符串

  • int:这将数据转换为整数

  • float:这将数据转换为浮点整数

  • boolean:这将数据转换为真或假的布尔值

  • date:这将数据转换为 JavaScript Date对象

defaultValue字段可以用来设置一个标准值,如果该字段没有收到数据,就可以使用这个值。在我们的例子中,我们将active的值设置为true。我们可以在使用Ext.create()创建新的用户实例时使用这个值:

var newUser = Ext.create('User', {
  firstname: 'Nigel',
  lastname: 'Tufnel',
  username: 'goes211',
  age: 39,
  email: 'nigel@spinaltap.com'
});

请注意,我们在新的用户实例中没有为active提供值,所以它只是使用了我们的模型定义中的defaultValue字段。这也可以在用户忘记输入值时帮助用户。我们还可以通过使用validations来验证用户输入的信息。

模型验证

模型验证确保我们得到我们认为得到的数据。这些验证有两个功能。第一个是提供数据输入的指导方针。例如,我们通常希望用户名只包含字母和数字;验证可以强制这个约束,并在用户使用错误字符时通知用户。

第二个是安全性;恶意用户也可以通过表单字段发送可能对我们数据库有害的信息。例如,如果数据库没有得到适当保护,将DELETE * FROM users;作为用户名发送可能会造成问题。始终验证数据是个好主意。

我们可以将validations作为数据模型的一部分来声明,就像我们声明字段一样。例如,我们可以在我们的User模型中添加以下代码:

Ext.define('User', { 
extend: 'Ext.data.Model',
 config: {
  fields: [
    {name: 'firstname', type: 'string'},
    {name: 'lastname', type: 'string'},
    {name: 'age', type: 'int'},
    {name: 'username', type: 'string'},
    {name: 'email', type: 'string'},
    {name: 'active', type: 'boolean', defaultValue: true},
  ],
  validations: [
    {type: 'presence',  field: 'age'},
    {type: 'exclusion', field: 'username', list: ['Admin', 'Root']},
     {type: 'length', field: 'username', min: 3},
    {type: 'format', field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}
  ]
 }
}

在我们的例子中,我们增加了四个验证。第一个测试age值的存在。如果没有age的值,我们会得到一个错误。第二个验证器exclusion测试我们不希望在此字段中看到的值。在这个例子中,我们有一个用户名的列表,我们不希望看到的是AdminRoot。第三个验证器确保我们的用户名至少有三个字符长。最后一个验证器使用正则表达式检查我们的用户名格式。

提示

正则表达式

正则表达式,也称为正则表达式正则表达式,是匹配字符串结构的极其强大的工具。您可以使用正则表达式在字符串中搜索特定的字符、单词或模式。正则表达式的讨论需要一本自己的书,但网上有许多好的资源。

好的教程可以在以下位置找到:

www.zytrax.com/tech/web/regex.htm

一个可搜索的正则表达式数据库可以在以下位置找到:

regexlib.com

一个出色的正则表达式测试器也在此处提供:

www.rexv.org/

我们可以通过使用我们新User实例的validate方法来测试我们的验证:

var newUser = Ext.create('User', {
  firstname: 'Nigel',
  lastname: 'Tufnel',
  username: 'goes211',
  email: 'nigel@spinaltap.com'
});

var errors = newUser.validate();
console.log(errors);

请注意,我们故意这次省略了age字段,以给我们一个错误。如果我们查看我们的控制台,我们可以看到我们返回的Ext.data.Errors对象,如下面的屏幕截图所示:

模型验证

这是我们errors对象的控制台输出。errors对象包括一个名为isValid()的方法,它将返回一个truefalse值。我们可以使用这个方法来测试错误并向用户返回消息,例如:

  if(!errors.isValid()) {
    alert("The field: "+errors.items[0].getField()+ " returned an error: "+errors.items[0].getMessage());
  }

这里,我们测试errors是否有效,如果不有效,则显示第一个错误的信息。然后我们使用getField()getMessage()在用户的警报中显示信息。这些详细的错误信息包含在errors对象的items列表中。在实际使用中可能会有多个错误,因此我们需要遍历items列表以获取所有错误。

我们还可以通过在验证上设置额外的配置选项来更改默认错误消息:

  • exclusionMessage:当我们在字段中得到一个被排除的值时使用。

  • formatMessage:当我们在字段中得到格式不正确的值时使用。

  • inclusionMessage:当我们在字段中没有得到包含的值时使用。

  • lengthMessage:当字段的值不符合我们所需的长度时使用此功能。

  • presenceMessage:当我们在字段中没有保留所需的值时使用。

定制这些错误将帮助用户了解到底出现了什么问题以及需要采取什么措施来解决问题。

模型方法

我们的模型还可以包含可以对模型实例调用的方法。例如,我们可以在User模型的fields列表之后添加一个名为deactivate的方法。

deactivate: function() {
 if(this.get('active')) {
  this.set('active', false);
 }
}

这个函数检查我们当前的active值是否为true。如果是,我们将其设置为false。一旦我们像以前那样创建了newUser,我们可以像以下方式调用该函数:

newUser.deactivate();

这些模型方法为在模型中实现常见功能提供了很好的方式。

提示

CRUD

尽管模型方法可能看起来是一个添加函数以保存我们模型的不错选择,但实际上你真的不需要这样做。这些类型的函数—CreateReadUpdateDestroy—通常被称为不吸引人的缩写CRUD,它们由 Sencha Touch 自动处理。我们将在本章后面稍后再讨论这些功能。

现在我们已经定义了模型的字段、验证和函数,我们需要一种方法来在模型之间传递数据以存储和检索我们的用户。这时代理和读取器就派上用场了。

代理和读取器

在该模型中,代理和读取器合作存储和检索模型要使用的数据。代理告诉模型其数据将存储在哪里,读取器告诉模型正在使用哪种格式来存储数据。

代理主要有两种类型:本地和远程。本地代理在其设备上以两种代理类型之一存储其数据:

  • LocalStorageProxy:通过浏览器将数据保存到本地存储。除非用户删除,否则这些数据在会话之间是持久的。

  • MemoryProxy:本地内存中保存数据。页面刷新时,数据会被删除。

远程代理有两个基本类型:

  • AjaxProxy:将请求发送到当前域内的服务器。

  • JsonP:这会将请求发送到不同域上的服务器(在先前版本中这被称为scripttag代理)。

此外,还有一些特殊化的代理,包括:

  • Direct:这是一种专有的 Sencha 技术,与 Ajax 一样,允许与远程服务器进行异步通信。然而,与 Ajax 不同,Direct不需要保持一个到远程服务器的套接字打开,等待响应。这使得它非常适合任何可能需要服务器长时间响应延迟的过程。有关Direct的更多信息,请访问:

    Ext.direct.Manager api.

  • RestRest代理采用基本代理功能(CreateReadEditDelete),并将这些映射到 HTTP 请求类型(分别是POSTGETPUTDELETE)。这种通信方式在商业 API 中非常常见。有关其他代理的更多信息,请访问:

    Ext.data.proxy.Rest api

    有关 REST 协议本身的更多信息,请访问:

    HTTP 和 REST 的初学者介绍

  • Sql:此代理允许您在本地 SQL 数据库中存储数据。这不应与实际的 SQL 服务器混淆。Sencha Touch SQL 代理将模型数据输出到 HTML5 本地数据库中,使用 WebSQL。

在本章及下一章中,我们将主要处理本地代理。我们将在第九章高级主题中覆盖远程代理和数据同步,高级主题

代理可以作为模型的一部分声明,如下所示:

proxy: {
  type: 'localstorage'
  id: 'userProxy'
}

所有代理都需要一个类型(本地存储、会话存储等);然而,一些代理将需要附加信息,例如localstorage代理所需的唯一 ID。

我们还可以向此代理配置中添加一个读者。读者的任务是告诉我们的代理发送和接收数据时应使用哪种格式。读者理解以下格式:

  • array:一个简单的 JavaScript 数组

  • xml:可扩展标记语言格式

  • json:一种 JavaScript 对象表示法格式

读者作为代理的一部分被声明:

proxy: {
  type: 'localstorage',
  id: 'userProxy',
  reader: {
    type: 'json'
  }
}

小贴士

声明代理和读者

代理和读取器也可以作为数据存储和模型的一部分声明。如果为存储和模型声明了不同的代理,那么调用store.sync()将使用存储的代理,而调用model.save()将使用模型的代理。通常只有在复杂情况下才需要在模型和存储上使用不同的代理。这也可以是令人困惑的,所以最好只在模型中定义代理,除非你确切知道你在做什么。

介绍数据格式

在我们将数据存储前进之前,我们需要简要地查看一下数据格式。Sencha Touch 目前支持的三种数据格式是数组、XML 和 JSON。对于每个示例,我们将查看一个简单的contact模型,其中包含三个字段:ID、姓名和电子邮件 ID,数据将如何显示。

数组

ArrayStore数据格式使用标准的 JavaScript 数组,对于我们这个contact示例,它看起来像这样:

[ 
  [1, 'David', 'david@gmail.com'],
  [2, 'Nancy', 'nancy@skynet.com'],
  [3, 'Henry', 'henry8@yahoo.com']
]

这种数组的一个首要特点是没有字段名包括在 JavaScript 数组中。这意味着如果我们想通过名称在我们的模板中引用字段,我们必须通过使用mapping配置选项来设置我们的模型,使其理解这些字段应该映射到数据数组的哪个位置:

Ext.define('Contact', {
 extend: 'Ext.data.Model',
  config: {
   fields: [
        'id',
        {name: 'name', mapping: 1},
        {name: 'email', mapping: 2}
    ],
    proxy: {
      type: 'memory',
      reader: {
        type: 'array'
      }
    }
   }
});

这设置我们的id字段为数据索引0,这是默认值。然后我们使用mapping配置将nameemail分别设置为数据数组索引12,然后我们可以使用配置设置模板值:

itemTpl: '{name}: {email}'

尽管数组通常用于简单的数据集,但对于更大的或嵌套的数据集,使用简单的 JavaScript 数组结构可能会变得非常难以管理。这就是我们的其他格式发挥作用的地方。

XML

可扩展标记语言XML)对于那些过去曾与 HTML 网页一起工作的人来说,应该是一个熟悉的格式。XML 由一系列嵌套在标签中的数据组成,这些标签标识数据集的每个部分的名字。如果我们把之前的例子转换成 XML 格式,它将如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<contact>
  <id>1</id>
  <name>David</name>
  <email>david@gmail.com</email>
</contact>
<contact>
  <id>2</id>
  <name>Nancy</name>
  <email>nancy@skynet.com</email>
</contact>
<contact>
  <id>3</id>
  <name>Henry</name>
  <email>henry8@yahoo.com</email>
</contact>

注意,XML 总是以版本和编码行开始。如果没有设置这一行,浏览器将无法正确解释 XML,请求将会失败。

我们还包括用于定义各个联系人的标签。这种格式的一个优点是我们现在可以嵌套数据,如下面的代码所示:

<?xml version="1.0" encoding="UTF-8"?>
<total>25</total>
<success>true</success>
<contacts>
  <contact>
    <id>1</id>
    <name>David</name>
    <email>david@gmail.com</email>
  </contact>
  <contact>
    <id>2</id>
    <name>Nancy</name>
    <email>nancy@skynet.com</email>
  </contact>
  <contact>
    <id>3</id>
    <name>Henry</name>
    <email>henry8@yahoo.com</email>
  </contact>
</contacts>

在这个嵌套示例中,我们每个单独的contact标签都嵌套在一个contacts标签内。我们还为我们的totalsuccess值设置了标签。

由于我们有一个嵌套数据结构,我们也需要让读取器知道去哪里寻找我们需要的片段。

reader: {
    type: 'xml',
    root: 'contacts',
    totalProperty  : 'total',
    successProperty: 'success'
}

root属性告诉读取器从哪里开始查找我们的单个联系人。我们在contacts列表之外也设置了一个totalProperty值。这告诉存储器总共有 25 个联系人,尽管存储器只接收前三个。totalProperty属性用于分页数据(即显示 25 个中的 3 个)。

我们contacts列表之外的另一个属性是successProperty。这告诉存储器在哪里检查请求是否成功。

XML 的唯一缺点是它不是原生 JavaScript 格式,因此当系统解析它时会有一些开销。通常,只有在非常庞大或深度嵌套的数组中才会注意到这一点,但对于某些应用程序来说可能是个问题。

幸运的是,我们也可以使用 JSON。

JSON

JavaScript 对象表示法JSON)具有 XML 的所有优点,但由于它是原生 JavaScript 结构,因此与解析 XML 相比,它具有更少的开销。如果我们把我们的数据集看作是 JSON,我们会看到以下内容:

[
  {
    "id": 1,
    "name": "David",
    "email": "david@gmail.com"
  },
  {
    "id": 2,
    "name": "Nancy",
    "email": "nancy@skynet.com"
  },
  {
    "id": 3,
    "name": "Henry",
    "email": "henry8@yahoo.com"
  }
]

我们也可以以与处理 XML 相同的方式嵌套 JSON:

{ 
  "total": 25,
  "success": true,
  "contacts": [
   {
    "id": 1,
    "name": "David",
    "email": "david@gmail.com"
   },
   {
    "id": 2,
    "name": "Nancy",
    "email": "nancy@skynet.com"
   },
   {
    "id": 3,
    "name": "Henry",
    "email": "henry8@yahoo.com"
   }
  ]
}

然后,读取器会像我们的 XML 读取器一样设置,但将类型列为 JSON:

reader: {
    type: 'json',
    root: 'contacts',
    totalProperty  : 'total',
    successProperty: 'success'
}

与之前一样,我们为totalPropertysuccessProperty设置了属性。我们还为读取器提供了一个开始查找我们的contacts列表的地方。

提示

还应注意的是,totalPropertysuccessProperty的默认值分别是totalsuccess。如果你在自己的 JSON 返回值中使用了totalsuccess,你实际上不需要在reader上设置这些配置选项。

JSONP

JSON 还有一种替代格式,称为 JSONP,即带填充的 JSON。这种格式用于你需要从远程服务器获取数据时。我们需要这个选项,因为大多数浏览器在处理 JavaScript 请求时遵循严格的同源策略。

同源策略意味着 web 浏览器只允许 JavaScript 在与 web 页面相同的服务器上运行,只要 JavaScript 在运行。这将防止许多潜在的 JavaScript 安全问题。

然而,有时你会出于正当理由从远程服务器发起请求,例如查询 Flickr web service 的 API。因为你的应用可能不会在flickr.com上运行,你需要使用 JSONP,它简单地告诉远程服务器将 JSON 响应封装在一个函数调用中。

幸运的是,Sencha Touch 为我们处理所有这些事情。当你设置你的代理和读取器时,将代理类型设置为jsonp,并像设置常规 JSON 读取器一样设置你的读取器。这告诉 Sencha Touch 使用Ext.data.proxy.JsonP来执行跨域请求,而 Sencha Touch 处理其余部分。

注释

如果您想看看 JSONP 和Ext.data.proxy.JsonP的实际应用,我们在第八章,创建 Flickr Finder 应用程序中使用两者来构建Flickr Finder应用程序。

虽然我们有多种格式可供选择,但本章余下的例子我们将使用 JSON 格式。

介绍存储

顾名思义,存储用于存储数据。正如我们在前几章所看到的,列表组件需要一个存储来显示数据,但我们也可以使用存储从表单中获取信息并将其保存在我们应用程序的任何地方。

存储、模型和代理一起工作,与传统数据库非常相似。模型为我们数据提供结构(如传统数据库中的架构),代理提供通信层,以便将数据进出存储。存储本身持有数据,并为排序、筛选、保存和编辑数据提供强大的组件接口。

存储还可以绑定到许多组件,如列表、嵌套列表、选择字段和面板,以提供显示数据。

我们将在第七章,获取数据外中覆盖显示、排序和筛选内容,但目前,我们将着手查看使用存储来保存和编辑数据。

简单的存储

由于本章关注的是将数据导入存储,我们将从一个非常简单的本地存储示例开始:

var contactStore = Ext.create('Ext.data.Store', {
  model: 'Contact',
  autoLoad: true
});

这个示例告诉存储使用哪个模型,这反过来定义了存储知道的字段以及存储应该使用的代理,因为存储将采用字段列表和代理从其模型中。我们还设置存储为autoLoad,这意味着一旦创建存储,它就会加载数据。

注意

如果您在存储配置中声明了一个代理,那么将使用该代理而不是模型的代理。在某些情况下这很有用,例如您想要存储关于记录集合的信息,如一组管理员用户。在这种情况下,模型用于存储用户详细信息,但存储用于收集特定类型(管理员用户)的多个用户。

我们还需要确保我们的模型设置正确,以便使用此存储。由于我们在存储中没有列出代理,我们需要确保模型有一个,如果我们想要保存我们的数据:

Ext.define('Contact', {
 extend: 'Ext.data.Model',
  config: { 
   fields: [
        {name: 'id', type:'int'},
        {name: 'name', type: 'string'},
        {name: 'email',  type: 'string'}
    ],
    proxy: {
        type: 'localstorage',
        id: 'myContacts',
        reader: {
          type: 'json'
        }
    }
  }
});

这是一个包含三个项目的简单模型:一个 ID、一个名称和一个电子邮件地址。我们然后像以前一样创建一个新的联系人:

  var newContact = Ext.create('Contact', {
    name: 'David',
    email: 'david@msn.com'
  });

请注意,这次我们没有设置 ID。我们希望存储为我们设置 ID(这与典型数据库中的自动递增类似)。然后我们可以将这个新联系人添加到存储中并保存它:

var addedUser = contactStore.add(newContact);
contactStore.sync();

第一行将用户添加到商店,第二行保存商店的内容。通过将 addsync 功能分开,你可以向商店添加多个用户,然后执行一次保存,如下面的代码所示:

  var newContact1 = Ext.create('Contact', {
    name: 'David',
    email: 'david@msn.com'
  });

  var newContact2 = Ext.create('Contact',
    name: 'Bill',
    email: 'bill@yahoo.com'
  });

var addedContacts = contactStore.add(newContact1, newContact2);
contactStore.sync();

在这两种情况下,当我们向商店添加联系人时,我们设置一个返回变量来获取 add 方法的返回值。这个方法返回一个联系人数组,现在每个 contact 对象都将有一个唯一的 ID。我们可以在我们的同步之后添加几个控制台日志来查看这些值:

console.log(addedContacts);
console.log(addedContacts[0].data.name+': '+addedContacts[0].data.id);
console.log(addedContacts[1].data.name+': '+addedContacts[1].data.id);

这将显示返回两个 contact 对象的数组。它还显示了如何通过使用数组中特定联系人的索引号来获取我们需要的数据。然后我们可以深入到数据中,获取姓名和我们在同步时分配的新 ID。

一个简单的商店

既然我们已经大致了解了如何将数据输入商店的方法,那么让我们来看看如何使用表单来完成它。

表单和商店

在这个例子中,我们将使用与上一个例子相同的商店和模型,但我们将添加一个列表和一个表单,这样我们就可以添加新的联系人并查看我们添加了什么。让我们从列表开始:

this.viewport = Ext.create('Ext.Panel', {
    fullscreen: true,
    layout: 'fit',
    items: [
  {
        xtype: 'toolbar',
        docked: 'top',
        items: [{
            text: 'Add',
            handler: function() {
              Ext.Viewport.add(addNewContact);
              addNewContact.show()
            }
        }]
    },
    {
      xtype: 'list',
      itemTpl: '{name}: {email}',
      store: contactStore
    }]
});

你会得到类似于以下屏幕截图的东西:

表单和商店

这里的大部分代码与之前的例子非常相似。我们有一个带有 list 组件的单个子面板。我们的列表有一个使用与我们的 contact 模型相同的字段名的模板 itemTpl,它决定了它们将如何显示。我们还添加了一个带有我们新 添加 按钮的固定工具栏。

提示

toolbar 组件也发生了变化,与 Sencha Touch 的以前版本不同。在版本 2 中,toolbaritems 列表的一部分,而不是作为一个单独的 dockedItem。此外,toolbar 的位置以前是通过 dock 配置选项来设置的。在 Sencha Touch 2 中,这被改为了 docked。还应该注意的是,如果你尝试使用旧的 dockedItemdock 配置,你不会得到任何错误。你也不会得到工具栏。这可能会导致你扯掉很多头发并说出粗糙的语言。

按钮有一个非常简单的函数,它将一个名为 addNewContact 的 Ext.Sheet 添加到我们的视口,然后显示该表单。现在我们需要实际创建这个表单:

var addNewContact = Ext.create('Ext.Sheet', {
  height: 250,
  layout: 'fit',
  stretchX: true,
  enter: 'top',
  exit: 'top',
  items: […]
});

这给了我们一个新表单,当我们点击 添加 按钮时会出现。现在,我们需要将我们的表单字段添加到我们刚刚创建的表单的 items 部分:

{
  xtype: 'formpanel',
  padding: 10,
  items: [
    {
     xtype: 'textfield',
     name : 'name',
     label: 'Full Name'
    },
    {
     xtype: 'emailfield',
     name : 'email',
     label: 'Email Address'
   }
  ]
}

我们首先创建一个 formpanel 组件,然后将 textfieldemailfield 添加到 formpanelitems 列表中。

专业文本字段

Sencha Touch 使用了如 emailfieldurlfieldnumberfield 等专业文本字段,以控制移动设备使用哪种键盘,如下面的 iPhone 示例所示:

专业文本字段

前述图表中所示的键盘类型如下解释:

  • URL 键盘用点(.)、斜杠(/**)和.com的键替换了传统的空格键。

  • 电子邮件键盘缩短了空格键,并为@和点(.)腾出了空间。

  • 数字键盘最初显示数字键盘,而不是标准的 QWERTY 键盘。

这些特殊字段不会自动验证用户输入的数据。那些验证是通过模型验证处理的。

提示

特殊键盘

安卓和 iOS 拥有略微不同的特殊键盘,因此你可能会在这两者之间找到一些变化。通常,运行你的应用程序通过安卓和 iOS 模拟器,以确保正确使用键盘类型。

将字段映射到模型

你还会注意到我们表单中的每个字段名称与我们contact模型的名称相匹配;这将允许我们轻松创建联系信息并将它们添加到商店中。然而,在我们到达那里之前,我们需要添加两个按钮(保存取消),以告诉表单要做什么。

在我们表单中的emailfield对象之后,我们需要添加以下内容:

{
  xtype: 'button',
  height: 20,
  text: 'Save',
  margin: 10,
  handler: function() {
    this.up('sheet').hide();
  }
  }, {
  xtype: 'button',
  height: 20,
  margin: 10,
  text: 'Cancel',
  handler: function() {
    this.up('sheet').hide();
  }
}

这给了我们在表单底部两个按钮。现在,我们的保存取消按钮做相同的事情:它们调用一个函数来隐藏包含我们表单的弹出窗口。这是一个很好的起点,但我们还需要更多功能来让保存按钮保存我们的数据。

将字段映射到模型

由于我们是很棒的程序员,并且给我们的字段命名以匹配我们的模型,我们只需要在我们按钮处理程序中使用以下代码就可以将我们的表单添加到我们的商店中:

handler: function() {
  var form = this.up('formpanel');
  var record = Ext.create('Contact', form.getValues());
  contactStore.add(record);
  contactStore.sync();
  form.reset();
  this.up('sheet').hide();
 }

第一行使用up方法获取围绕按钮的表单。第二行使用form.getValues(),并将输出直接传递到一个新Contact模型中,使用我们之前示例中的create()方法。然后我们可以将新联系信息添加到商店并同步,就像我们之前做的那样。

我们需要做的最后一点清理工作是通过使用form.reset()来清除所有表单值,然后像之前一样隐藏表单。如果我们不清除字段,下次我们显示表单时数据仍然会存在。

当我们同步商店时,与商店关联的列表将会刷新,我们的新联系信息会出现。

将字段映射到模型

由于这个商店使用本地存储来保存数据,我们的列表在我们退出 Safari 浏览器后仍然会保持原位。当你测试应用程序时,这可能会让你感到有些烦恼,所以让我们来看看如何清除商店中的数据。

清除商店数据

本地存储和会话存储在我们本地计算机上保存信息。由于我们计划在编码时进行大量测试,知道如何清除这类数据而又不删除可能仍然需要的其他数据是个好主意。要清除您本地或会话存储中的数据,请按照以下步骤操作:

  1. 开发菜单中打开网络检查器,并选择资源标签。清除存储数据

  2. 本地存储会话存储部分(取决于您使用的方法),您应该看到您应用程序的数据库。一旦您选择了数据库,您可以删除特定的记录或完全清空数据库。只需在屏幕右侧选择记录,然后点击底部的X以删除记录。

  3. 您还可以通过双击它并更改数字来重置计数器的值。小心不要创建具有相同数字的多个记录。这将造成大问题。

  4. 资源部分完成后,让我们继续使用我们的表单编辑数据。

使用表单编辑

现在我们已经了解了将数据传入存储的基本知识,让我们来看看如何使用对我们当前表单进行一些修改来编辑这些数据。

我们想要添加的第一个是一个itemsingletap监听器到我们的列表上。这将让我们点击列表中的一个项目并弹出包含所选条目的表单,以便我们进行编辑。监听器如下所示:

listeners: {
 itemsingletap: {
  fn: function(list, index, target, record){
   addNewContact.down('form').setRecord(record);
   Ext.Viewport.add(addNewContact);
   addNewContact.show();
  }
 }
} 

我们的itemsingletap监听器将自动返回list的副本、项目的index属性、target元素以及被点击项背后的record。然后我们可以获取我们表单内的表单并在其中设置记录。

经常以这种方式链接函数很有用,特别是如果你需要用到的部分只需使用一次。例如,我们可以这样做:

var form = addNewContact.down('form');
form.setRecord(record);

这样也可以让我们在函数的许多地方使用那个form变量。由于我们只需要用它来设置记录,我们可以将这两行合并为一行:

addNewContact.down('form').setRecord(record);

以下方式将数据加载到我们的表单中:

使用表单编辑

还有一个问题需要解决:我们的保存按钮硬编码到向存储中添加新记录。如果我们现在点击保存,我们最终会得到同一个联系人的多个副本。我们需要对我们的表单进行更改,以便让我们可以根据我们是在编辑还是创建新联系人来切换保存按钮的行为。

切换处理程序

为了更改处理程序,按钮触发保存我们的联系人;我们需要将代码的主体与按钮本身分开。首先,找到我们的保存按钮的处理程序,并将当前函数复制到剪贴板。接下来,我们想要用外部函数的名称替换那个函数:

handler: addContact

我们还将以以下方式向我们的按钮添加一个额外的config选项:

action: 'saveContact'

这将使我们稍后用组件查询更容易地获取我们的按钮。

小贴士

action配置选项是一个完全任意的名称。您不受限于 Sencha 定义的选项。您可以为组件定义任何其他选项,并在处理程序和控制器中像其他任何配置选项一样引用它们。

现在,我们需要为这个处理程序创建一个新的addContact函数。在我们创建addNewContact表单的 JavaScript 文件中,在创建addNewContact表单之前,添加一个名为addContact的新函数,并粘贴我们旧handler函数的代码。它应该如下所示:

var addContact = function() {
  var form = this.up('formpanel');
  var record = Ext.create('Contact', form.getValues());
  contactStore.add(record);
  contactStore.sync();
  form.reset();
  this.up('sheet').hide();
};

这是我们之前在按钮上使用过的表单保存函数,它添加新联系人正好合适。现在,我们需要创建一个类似的函数,当我们在列表中点击它们时更新我们的联系人。

在我们的addContact函数顶部,添加以下代码:

var updateContact = function() {
  var form = this.up('formpanel');
  var rec = form.getRecord();
  var values = form.getValues();
  rec.set(values);
  contactStore.sync();
  form.reset();
  this.up('sheet').hide();
};

这个函数几乎做了我们另一个函数的所有事情。然而,不同的是,它不是获取表单字段并创建一个新的记录,而是使用form.getRecord()从表单本身获取记录。这个记录是我们需要用新信息更新的记录。

然后,我们使用form.getValues()获取表单的当前值。

我们的rec变量现在设置为数据存储中的旧记录。然后,我们可以使用rec.set(values)将该记录传递给新数据,这将用我们当前表单值覆盖存储记录中的旧信息。由于我们没有传递新值,ID 将保持不变。

更新记录后,我们只需执行以下早期所做的操作:

  • sync

  • reset

  • hide

现在我们的两个函数的代码已经就位,我们需要根据用户是否点击了我们列表顶部的添加按钮或选择了列表中的项目来切换保存按钮的处理程序。

让我们从添加按钮开始。在list对象的顶部找到添加按钮的处理程序。我们需要向这个按钮添加一些代码,以更改保存按钮的处理程序:

handler: function() {
  var button = addNewContact.down('button[action=saveContact]');
  button.setHandler(addContact);
  button.setText('Add');
  Ext.Viewport.add(addNewContact);
  addNewContact.show();
}

由于我们的addNewContact表单已经在代码的其他地方定义为一个变量,我们可以使用down()方法获取button并做一些更改。首先,更新处理程序以查看我们的新addContact函数,第二个更改是将按钮的文本更改为创建。然后,我们可以在视口中添加我们的addNewContact表单并调用addNewContact.show(),就像以前一样。

我们的添加按钮现在设置为显示表单并更改按钮的文本和处理程序。

现在,我们需要对列表中的itemsingletap处理程序做类似的事情:

itemsingletap: {
  fn: function(list,index, target, record){
    addNewContact.down('formpanel').setRecord(record);
    var button = addNewContact.down('button[action=saveContact]');
    button.setHandler(updateContact);
    button.setText('Update');
    Ext.Viewport.add(addNewContact);
    addNewContact.show();
  }
}

在这里,我们仍然获取记录并将其加载到表单中,但我们要获取button带有action值为saveContact的元素,并更改处理程序和文本。更改将保存按钮指向我们的updateContact函数,并将文本更改为更新

Switching handlers

从数据存储中删除

如果你还记得之前我们讨论 CRUD 功能的时候,你会发现我们已经成功覆盖了Create(创建)、Read(读取)和Update(更新)。这些操作都是由存储自动完成的,几乎不需要编写任何代码。那么Delete(删除)呢?

结果表明,Delete(删除)与其他存储方法一样简单。我们可以使用两个方法中的任意一个:第一个是remove()—它需要一个记录作为参数—第二个是removeAt,它需要一个索引来确定要删除的记录。我们可以将其中任何一个作为我们编辑表单的一部分,通过在表单底部添加一个新按钮来实现,如下所示:

{
  xtype: 'button',
  height: 20,
  margin: 10,
  text: 'Delete',
  ui: 'decline',
  handler: function() {
    var form = this.up('formpanel');
    contactStore.remove(form.getRecord());
    contactStore.sync();
    form.reset();
    this.up('sheet').hide();
  }
}

使用remove需要存储记录,因此我们从表单面板中获取记录:

contactStore.remove(form.getRecord());

这样就处理了所有基本的Create(创建)、Read(读取)、Edit(编辑)和Delete(删除)功能。只要你记得设置你的模型并匹配你的字段名,存储会自动处理大多数基本操作。

注意

更多信息

Sencha 提供了许多关于使用表单、模型和存储的优秀教程,请访问docs.sencha.com/touch/2.2.1/#!/guide

总结

在本书的第四章,我们介绍了在 Sencha Touch 中构成所有数据基本结构的数据模型。我们查看了代理和读取器,它们处理数据存储与其他组件之间的通信。我们还讨论了在 Sencha Touch 中持有所有数据的存储。最后,我们查看了如何使用表单将数据进出存储,以及如何在数据不再需要时删除数据。

在下一章中,我们将查看一旦我们把数据从存储中取出后可以做的所有其他事情。

第七章:获取数据

在上一章中,我们了解了如何将数据导入 Sencha Touch 数据存储。一旦我们有了数据,下一步就是弄清楚如何从存储中获取数据并在我们的应用程序中使用它。幸运的是,Sencha Touch 有几种内置方法可以帮助我们完成这项任务。在这里,我们将探讨如何使用单个数据记录以及数据存储的完整内容来在我们的应用程序中显示信息。

在本章中,我们将探讨:

  • 使用数据存储进行显示

  • 绑定、排序、过滤、分页和加载数据存储

  • 使用 XTemplates

  • 在 XTemplate 中遍历数据

  • XTemplates 中的条件显示和内联函数

  • 在 XTemplates 中的内联 JavaScript 和成员函数

  • 使用 Sencha Touch 图表显示存储数据

使用数据存储进行显示

能够在应用程序中存储数据只是战斗的一半。您需要能够轻松地将数据重新取出并以有意义的方式呈现给用户。Sencha Touch 中的列表、面板和其他具有数据功能的组件提供三种配置选项来帮助您完成这项任务:storedatatpl

直接绑定存储

数据视图、列表、嵌套列表、表单选择字段和索引栏都旨在显示多个数据记录。这些组件中的每一个都可以配置一个数据存储,从中提取这些记录。我们在书中的早些时候介绍了这种做法:

Ext.application({
    name: 'TouchStart',
    launch: function () {
        Ext.define('Contact', {
            extend: 'Ext.data.Model',
            config: {
                fields: [
                    {name: 'id'},
                    {name: 'first', type: 'string'},
                    {name: 'last', type: 'string'},
                    {name: 'email', type: 'string'}
                ],
                proxy: {
                    type: 'localstorage',
                    id: 'myContacts',
                    reader: {
                        type: 'json'
                    }
                }
            }
        });

        var main = Ext.create('Ext.Panel', {
            fullscreen: true,
            layout: 'fit',
            items: [
                {
                    xtype: 'list',
                    itemTpl: '{last}, {first}',
                    store: Ext.create('Ext.data.Store', {
                        model: 'Contact',
                        autoLoad: true
                    })
                }
            ]
        });

        Ext.Viewport.add(main);

    }
});

存储配置在设置时包括modelautoLoad属性。这将获取存储的所有数据(使用model参数中的代理)并将其拉入列表以供显示。我们现在对此很熟悉,但如果我们只想获取一些数据,或者需要以特定顺序获取数据呢?

结果证明,Sencha Touch 存储可以在首次创建时以及我们需要根据用户更改过滤或排序时进行排序和过滤。

排序器和过滤器

排序器和过滤器可以用多种方式使用。第一种方式是在创建存储时设置默认配置。

var myStore = Ext.create('Ext.data.Store', {
    model: 'Contact',
    sorters: [
        {
            property: 'lastLogin',
            direction: 'DESC'
        },
        {
            property: 'first',
            direction: 'ASC'
        }
    ],
    filters: [
        {
            property: 'admin',
            value: true
        }
    ]
});

我们的sorters组件被设置为一个属性值和方向值的数组。这些按顺序执行,因此我们的示例首先按lastLogin(最新)排序。在lastLogin内,我们按名称(按字母顺序递增)排序。

我们的过滤器列为propertyvalue对。在示例中,我们希望商店只显示admin给我们。商店实际上可能包含非管理员,但在这里我们要求首先过滤掉那些人。

排序器和过滤器可以通过使用以下方法之一在初始加载后进行修改:

  • clearFilter:此方法清除存储上的所有过滤器,给您商店的完整内容。

  • filter:此方法接受一个过滤器对象,与我们在早期配置示例中的对象类似,并使用它来限制所需的数据。

  • filterBy:这个方法允许你声明一个在每个存储项上运行的函数。如果你的函数返回true,该项目将被包含在内。如果它返回false,那么该项目将被过滤掉。

  • sort:这个方法接收一个sort对象,就像我们配置示例中的那些,并使用它来按请求的顺序排序数据。

如果我们使用先前的存储示例,改变sort顺序将如下所示:

myStore.sort( {
    property : 'last',
    direction: 'ASC'
});

筛选必须考虑存储上任何先前的筛选。在我们的当前存储示例中,我们设置为筛选出admin值为false的人。如果我们尝试以下代码,我们将列表中什么也得不到,因为我们实际上告诉存储同时根据新(admin = false)和先前(admin = true)的筛选进行筛选:

myStore.filter( {
    property : 'admin',
    value: false
});

因为admin是一个布尔值,所以我们什么也得不到。我们首先必须清除旧的筛选器:

myStore.clearFilter();
myStore.filter( {
    property : 'admin',
    value: false
});

这个示例将清除存储中的旧'admin'筛选器,并返回一个不是管理员的每个人的列表。

排序和筛选为在数据存储中操作数据提供了强大的工具。然而,还有其他几种情况我们也应该考虑。当你有太多数据时你应该做什么,当你需要重新加载数据存储时你应该做什么?

页面数据存储

在某些情况下,你可能会得到比你的应用程序一次能舒适处理更多的数据。例如,如果你有一个带有 300 个联系人的应用程序,初始加载时间可能会比你真正想要的要长。处理这种情况的一种方法是分页数据存储。分页允许我们按块获取数据,并在用户需要时发送下一个或前一个数据块。

我们可以使用pageSize配置来设置分页:

var myStore = Ext.create('Ext.data.Store', {
    model: 'Contact',
    pageSize: 40,
    proxy: {
        type: 'localstorage',
        id: 'myContacts',
        reader: {
            type: 'json'
        }
    },
    autoLoad: true
});

然后我们可以使用分页功能遍历数据:

myStore.nextPage();
myStore.previousPage();
myStore.loadPage(5);

这段代码先前进一页,再后退一页,然后跳转到第五页。

注意

请注意,页面索引是基于 1 的(也就是说,编号为 1、2、3 等),而不是基于 0 的(也就是说,编号为 0、1、2、3 等,就像数组一样)。

如果我们跳转到第五页并且它不存在,我们的应用程序可能会出现问题(也就是说,它会爆炸!),这意味着我们需要一种好的方法来确定我们实际上有多少页。为此,我们需要知道数据存储中的记录总数。

我们可以尝试使用数据存储的getCount()方法,但这个方法返回的只是存储中当前缓存的记录数。由于我们在分页数据,这意味着我们并没有加载所有可用的数据。如果我们最大页面数为 40,但我们的数据库中有 60 条记录,那么getCount()方法在第一页将返回 40,在加载第二页时将返回 20。

另外,如果您过滤商店的数据,getCount()返回的数字将是匹配过滤器的记录数,而不是商店中记录的总数。我们需要设置商店的读取器以从我们的系统中获取实际的总数。我们还需要告诉商店当数据返回时这些记录将在哪里。

我们可以在reader上为totalPropertyrootProperty设置一个配置,例如以下内容:

var myStore = new Ext.data.Store({
 model: 'Contact',
 pageSize: 40,
 proxy: {
  type: 'localstorage',
  id: 'myContacts',
  reader: {
   type: 'json',
   totalProperty: 'totalContacts',
   rootProperty: 'contacts'
  }
 },
 autoLoad: true
});

这告诉我们的读者在收集的数据中寻找两个额外的属性,分别叫做totalContactsrootProperty。我们从商店拉入的数据也必须设置为在数据字符串中包括这个新属性。如何实现这一点在很大程度上取决于您的数据是如何创建和存储的,但在 JSON 数据数组中,格式将类似于以下内容:

{
"totalContacts: 300,
  "contacts":[…]
}

totalContacts属性告诉我们有多少联系人,rootProperty告诉读者从哪里开始寻找这些联系人。

一旦我们的数据以这种方式设置好,我们可以如下获取总联系人:

var total = myStore.getProxy().getReader().getTotalCount()

然后我们可以除以myStore.getPageSize(),以确定我们数据中的总页数。我们还可以通过myStore.currentPage获取当前页。这两条信息将允许我们显示用户在页面中的当前位置(例如,第 5 页/共 8 页)。现在,我们需要考虑当商店背后的数据发生变化时会发生什么。

在商店中加载更改

当我们使用数据存储从外部源(如文件、网站或数据库)拉取信息时,数据总是有可能在外部源处发生变化。这将导致我们在商店中留下陈旧的数据。

幸运的是,有一个简单的方法可以处理这个问题,即使用商店上的load()函数。load()函数的工作方式如下:

myStore.load({
 scope: this,
 callback: function(records, operation, success) {
  console.log(records);
 }
});

scopecallback函数都是可选的。然而,callback为我们提供了做一些有趣事情的机会,比如比较我们的旧记录和新记录,或者在新技术记录加载后通过视觉方式向用户发出警报。

提示

您还可以在商店中为load事件设置监听器。这将使得商店在任何时候调用基本store.load()函数时都使用这个回调。另外,还有一个名为beforeLoad的事件,顾名思义,在商店加载之前每次都会触发。如果beforeLoad事件返回false,则不会触发load事件。

在加载数据存储时,还需要考虑是否要自动加载(autoLoad)商店作为其创建的一部分,或者稍后加载。一个好的经验法则是只自动加载您知道最初将显示的数据存储。任何后续的数据存储都可以设置在它们所绑定的组件显示时加载。

例如,假设我们有一个系统用户列表,在程序中只偶尔访问。我们可以以如下方式向组件列表本身添加一个监听器:

listeners: {
 show: {
  fn: function(){ this.getStore().load(); }
 }
}

这段代码只有在list组件实际显示时才会加载存储。这种加载存储的方式可以在启动我们的应用程序时节省时间,同时也可以节省内存。然而,需要注意的是,代码也会在组件显示时每次加载存储。如果你预计存储背后的数据会频繁更新,这是可取的;否则,最好手动加载存储。

我们还可以通过使用存储来为多个组件提供数据,例如数据列表和详细面板,从而节省时间和内存。与前面的示例一样,这种策略也有一些注意事项。如果一个组件对存储应用了过滤器、排序或数据加载,它也会影响与此存储绑定的任何其他组件。

数据存储和面板

与列表不同,面板通常显示单个记录,但是我们可以以与列表相同的方式从我们的数据存储中获取这些信息。

让我们从一个章节的开始部分的联系人示例开始;我们将使用firstlast构建一个名字列表,然后添加一个详细面板,显示所选名字的全名、电子邮件地址和电话号码。

我们首先从我们的模型和存储开始:

Ext.define('Contact', {
 extend:'Ext.data.Model',
 config:{
  fields:[
   {name:'first', type:'string'},
   {name:'last', type:'string'},
   {name:'address', type:'string'},
   {name:'city', type:'string'},
   {name:'state', type:'string'},
   {name:'zip', type:'int'},
   {name:'email', type:'string'},
   {name:'birthday', type:'date'}
  ],
  proxy:{
   type:'ajax',
   url:'api/contacts.json',
   reader:{
    type:'json',
    rootProperty:'children'
   }
  }
 }
});
var contactStore = Ext.create('Ext.data.Store', {
 model:'Contact',
 autoLoad:true
});

这给我们firstlast的值,我们将用于初始列表,以及emailbirthdayaddress的信息,我们将用于详细信息。

细心的读者可能已经注意到,我们将模型更改为使用 Ajax 作为api/contacts.json URL 的代理(记住,我们的存储将自动使用这个代理)。这意味着当存储加载时,它将在api文件夹中寻找一个名为contacts.json的本地文件。这个文件作为本书可下载代码文件的一部分提供,其中包含我们整理的一些测试数据。如果你不想下载它,而是想创建自己的文件,该文件的格式如下所示:

{
  "children":[
    {
        "first":"Ila",
        "last":"Noel",
        "email":"ante.ipsum@Sedmalesuada.ca",
        "address":"754-6686 Elit, Rd.",
        "city":"Hunstanton",
        "state":"NY",
        "zip":34897,
        "birthday":"Tue, 16 Oct 1979 04:27:45 -0700"
    }, ...
  ]
}

通过将此存储设置为查看本地文本文件,我们可以通过向文本文件添加新的children来快速添加测试数据。

提示

测试数据是你朋友

无论何时你组装一个应用程序并测试它,你可能需要一些数据以确保事情正常运行。通常,手动将此信息输入文本文件或一遍又一遍地输入数据表单是非常繁琐的。幸运的是,www.generatedata.com/ 是一个网站,可以以多种格式生成随机数据。只需提供字段名称和类型,然后告诉网站你需要多少条记录。点击按钮,你就可以得到准备测试的随机数据。最重要的是,它是免费的!

数据存储和面板

我们的list组件基本上与之前保持不变。由于list使用模板itemTpl: '{last}, {first}'对列表中的每个项目进行格式化,它简单地忽略了addresscitystatezipemailbirthday的值。然而,由于这些值仍然是数据记录的一部分,我们仍然可以获取它们并在我们的面板上使用它们来显示详情。

在我们能够添加详情面板之前,我们需要创建一个main面板并将其设置为使用card布局。这将让我们通过一次轻触就能在列表和详情之间切换:

var main = Ext.create('Ext.Panel', {
 fullscreen:true,
 layout:'card',
 activeItem:0,
 items:[
  {
   xtype:'list',
   itemTpl:'{last}, {first}',
   store:contactStore
  }
 ]
});

我们已经将main面板设置为使用card布局,activeItem组件为0。在这种情况下,项目0是我们的list,它被内置到main面板中。

确保所有组件都包裹在一个应用程序启动函数内,就像我们前几章的例子一样:

Ext.application({
    name:'TouchStart',
    launch:function () {
     //components go here
    Ext.Viewport.add(main);
  }
});

在底部,在launch函数内,我们向Viewport添加了main面板。

一旦你有了数据和main面板,加载页面以确保到目前为止我们所做的一切都是正确的。

数据存储和面板

现在,我们需要添加我们的detailsPanel组件。我们首先在这个第一部分保持简单,并在我们的列表后添加一个新的panel项:

var detailsPanel = Ext.create('Ext.Panel', {
    tpl: '{first} {last}<br>'+
        '{address}<br>'+
        '{city}, {state} {zip}<br>'+
        '{email}<br>{birthday}',
    items: [
        {
            xtype: 'toolbar',
            docked: 'top',
            items: [
                {
                    text: 'Back',
                    ui: 'back',
                    handler: function () {
                        main.setActiveItem(0);
                    }
                }
            ]
        }
    ]
});

我们首先添加一个简单的模板。我们包含一些 HTML 换行符,以更好地布局数据。我们还把模板分成多行以提高可读性,并使用+运算符将字符串连接在一起。然后我们添加一个返回按钮,它将带我们回到主列表。

小贴士

由于我们已经在代码中将main定义为一个变量,我们可以在handler函数内使用它。由于main面板也是我们视口中的第一个面板,我们可以这样获取它:console.log(this.up('viewport').down('panel'));

一旦我们的detailsPanel被定义,我们需要在我们的列表中添加一个listeners部分,以将数据加载到面板中:

listeners:{
 itemtap:{
  fn: function (list, index, target, record) {
   detailsPanel.setRecord(record);
   main.setActiveItem(1);
  }
 }
}

好处是我们实际上并不需要加载任何新东西。列表已经可以访问数据存储中存在的所有额外数据。我们还在itemTap事件中接收记录作为一部分。我们可以获取这个记录,并使用setRecord()函数将其设置在面板上。最后,我们将活动项目设置为我们的detailsPanel组件。当我们轻触列表中的一个项目时,结果如下所示:

数据存储和面板

detailsPanel组件不仅包括我们从列表中的第一个和最后一个名字,还包括地址、电子邮件和出生日期数据。所有这些数据都来自同一个数据存储;我们只是使用模板来选择显示哪些数据。

说到模板,我们的看起来有点单调,而且birthday值比我们真正需要的要具体得多。我们必须想办法让这看起来更好一点。

XTemplates

正如我们从之前的许多例子中看到的那样,XTemplate是一个包含 HTML 布局信息和用于我们数据的占位符的结构。

到目前为止,我们只创建了用于我们的列表和面板的非常基本的模板,这些模板使用了数据值和一些 HTML。在我们的第一个例子中,我们学会了如何使用+运算符,使我们能够将一个非常长的字符串拆分成更小的字符串,以提高可读性。另一种这样做的方法是将这些模板设置为独立的组件:

var myTemplate = new Ext.XTemplate(
  '{first} {last}<br>',
  '{address}<br>',
  '{city}, {state} {zip}<br>',
  '{email}<br>',
  '{birthday}'
);

这将创建一个与之前完全相同的模板。这是 Sencha Touch 网站上大多数示例的编写方式,所以知道这两种方法都是好的。

一旦我们有了组件模板,我们就可以将其添加到我们的面板中,并与tpl: myTemplate一起使用。

以下两个方法在处理复杂模板时为您提供更好的可读性:

tpl: new Ext.XTemplate(
    '<div style="padding:10px;"><b>{first} {last}</b><br>',
    '{address}<br>',
    '{city}, {state} {zip}<br>',
    '<a href="mailto:{email}">{email}</a><br>',
    '{birthday}</div>'
);

或者:

tpl: '<div style="padding:10px;"><b>{first} {last}</b><br>'+
    '{address}<br>'+
    '{city}, {state} {zip}<br>'+
    '<a href="mailto:{email}">{email}</a><br>'+
    '{birthday}</div>'

这两种方法提供相同的结果。

XTemplates

我们也可以使用相同类型的 XTemplates 给我们的主列表添加一些样式。例如,将以下代码作为我们列表的itemTpl组件将会在列表中的每个名字旁边放置一个可爱的猫图片:

var listTemplate = new Ext.XTemplate(
    '<div class="contact-wrap" id="{first}-{last}">',
    '<div class="thumb" style= "float: left;"><img src="img/36" title="{first}"></div>',
    '<span class="contact-name">{first} {last}</span></div>'
);

在这个例子中,我们只是添加了一个 HTML 组件来布局每行数据,然后使用一个随机图像生成服务(在这个例子中,placekitten.com)放置任何 36x36 的猫图片,它将位于左侧我们的名字旁边(你也可以用它来显示联系人的照片)。

XTemplates

到目前为止,我们仍然只是在使用基本的 HTML;然而,XTemplates 的功能要比这强大得多。

操作数据

XTemplates 还允许我们在模板中以多种方式直接操作数据。我们首先可以做的事情就是清理那个丑陋的生日值!

由于在我们的模型中birthday值被列为一个date对象,因此我们可以在模板中将其当作一个对象来处理。我们可以用以下内容替换我们模板中的当前生日行:

  'Birthday: {birthday:date("n/j/Y")}</div>'

这将使用我们的birthday值,以及格式化函数datedate函数使用字符串"n/j/Y"birthday转换为更易读的格式。这些格式化字符串可以在 Sencha Touch API 的日期页面上找到。

操作数据

Sencha Touch 包括许多可以以这种方式使用的格式化函数。这些函数包括:

  • date:这个函数使用指定的格式化字符串对date对象进行格式化(格式化字符串可以在 Sencha Touch API 的日期页面上找到)。

  • ellipsis:这个函数将字符串截断到指定长度,并在末尾添加(注意被认为是总长度的部分)。

  • htmlEncodehtmlDecode:这两个函数将 HTML 字符(&<>')转换为 HTML 或从 HTML 转换回来。

  • leftPad:这个函数用指定的字符填充字符串的左侧(适用于用前导零填充数字)。

  • toggle:这个实用函数会在两个交替的值之间切换。

  • trim:这个函数会删除字符串开头和结尾的空格。它会保留字符串中的空格。

基本的函数可以被用在我们的 XTemplate 的 HTML 内部来格式化我们的数据。然而,XTemplate 还有一些额外的小技巧。

循环数据

在列表视图中,itemTpl 组件的 XTemplate 会自动应用到列表中的每个项目上。然而,你也可以使用下面的语法手动地循环你的数据:

'<tpl for=".">',
'{name}</br>',
'</tpl>' 

当你使用 <tpl> 标签时,它告诉 XTemplate 我们正在退出 HTML 的领域,在模板内部做一些决策。在这个例子中,<tpl for="."> 告诉代码开始一个循环,并使用我们数据的根节点。闭合的 </tpl> 标签告诉循环停止。

由于我们可以拥有既包含 XML 又包含 JSON 的复杂嵌套数据,因此除了根节点之外,在其他的地点循环数据也会很有帮助。比如说,假设我们有一个包含州的数据数组,而每个州又包含一个城市数据数组。我们可以像下面这样循环这个数据:

'<tpl for=".">',
'{name}</br>',

'<tpl for="cities">',
'{name}</br>',
'</tpl>' 

'</tpl>' 

我们的第一个 <tpl> 标签开始循环我们的州数据,打印出名称。在打印出名称之后,它会寻找个体州内部的名为 cities 的子数组。这次,当我们使用变量 {name} 时,它处于我们的子循环中,所以它会打印出州内的每个城市的名称,然后继续循环到下一个循环中的下一个州。

注意

注意,当我们在 <tpl> 标签内部使用字段名称时,我们不会像这样使用花括号:{cities}。由于我们处于模板的 HTML 部分之外,Sencha Touch 假定 "cities" 是一个变量。

我们甚至可以通过添加另一个循环来访问每个城市中嵌套的数组,例如 postal codes

'<tpl for=".">',
'{name}</br>',

'<tpl for="cities">', 
'{name}</br>',

'<tpl for="cities.postal">',
'{code}</br>',
'</tpl>' 

'</tpl>' 

'</tpl>' 

在这个例子中,我们使用了 <tpl for="cities.postal"> 来表示我们将会在 cities 数据数组内部的 postal codes 数据数组中循环。我们的其他数组循环像以前一样执行。

循环内的编号

当你在循环内部工作时,能够计算循环的次数通常很有帮助。你可以通过在你的 XTemplate 中使用 {#} 来做到这一点:

'<tpl for=".">',
'{#} {name}</br>',
'</tpl>' 

这将会在循环中的每个名字旁边打印当前的循环次数。这对于嵌套数据也会以类似的方式工作:

'<tpl for=".">',
'{#} {name}</br>',

'<tpl for="cities">',
'{#} {name}</br>',
'</tpl>' 

'</tpl>' 

第一个 {#} 会在主循环中显示我们的位置,第二个 {#} 会在 cities 循环中显示我们的位置。

循环中的父数据

当我们在嵌套数据的情况下,能够从子循环内部访问父级属性也会很有帮助。你可以通过使用 parent 对象来实现这一点。在我们的包含州、城市和国家的嵌套示例中,这看起来像这样:

'<tpl for=".">',
'{name}</br>',

'<tpl for="cities">',
'{parent.name} - {name}</br>',

'<tpl for="cities.postal">',
'{parent.name} - {code}</br>',
'</tpl>' 

'</tpl>' 

'</tpl>' 

当我们在cities循环中时,{parent.name}将显示该城市的州名。当我们我们在cities.postal循环中时,{parent.name}将显示与那个邮政编码相关联的城市名称。使用这种{parent.fieldname}语法,我们可以从当前子项中访问父项的任何值。

条件显示

除了循环,XTemplates 还为您模板中提供了一些有限的条件逻辑。在 Sencha Touch 的 2.x 版本中,我们现在可以访问完整的if...then...else...功能。例如,我们可以在我们的州和城市中使用if语句,只显示人口超过 2,000 的城市:

'<tpl for=".">',
  '{name}</br>',
  '<tpl for="cities">',
    '<tpl if="population &gt; 2000">',
      '{name}</br>',
    '</tpl>',
  '</tpl>',
'</tpl>'

如果我们想要根据多个人口目标对城市进行颜色编码,那么我们可以像这样使用if...then...else

'<tpl for=".">',
  '{name}</br>',
  '<tpl for="cities">',
    '<tpl if="population &gt; 2000">',
      '<div class="blue">{name}</div>',
    '<tpl elseif="population &gt; 1000">',
      '<div class="red">{name}</div>', 
    '<tpl else>', 
      '<div>{name}</div>',
    '</tpl>',
  '</tpl>',
'</tpl>'

条件显示

现在,你可能已经自己在问自己为什么我们使用&gt;&lt;而不是><。原因是我们的条件语句中的任何东西都需要进行 HTML 编码,以便 XTemplate 正确解析它。这可能一开始有点令人困惑,但需要记住的关键事情如下:

  • 使用&gt;而不是>

  • 使用&lt;而不是<

  • 使用==作为等于符号。然而,如果你要比较一个字符串值,你必须转义单引号,例如这样:'<tpl if="state == 'PA'">'

  • 如果您想将"编码为条件语句的一部分,那么您需要将其编码为&quot;spam&quot;

算术功能

除了条件逻辑,XTemplates 还支持以下基本算术功能:

  • 加法(+

  • 减法(-

  • 乘法(*

  • 除法(/

  • 模数——一个数除以另一个数的余数(%

例如:

'<tpl for=".">',
  '{name}</br>',
  '<tpl for="cities">',
      '{name}</br>',
  'Population: {population}</br>',
  'Projected Population for next year: {population * 1.15}</br>',
  '</tpl>',
'</tpl>'

这给我们初始的人口值,接着是当前人口的 1.15 倍的预测人口。数学函数包含在我们变量的花括号中。

内联 JavaScript

我们还可以通过将代码放在括号和花括号组合中来执行任意的内联代码作为 XTemplate 的一部分:{[…]}。在此代码中还可以访问一些特殊属性:

  • values:此属性保留当前作用域中的值

  • parent:此属性保留当前父对象的价值

  • xindex:此属性保留您当前所在的循环索引

  • xcount:此属性保留当前循环中的项目总数

让我们通过一个例子来阐述这些属性。我们可以确保我们的州和城市名称是大写的,并且列表中城市的颜色交替,通过使用以下的 XTemplate:

'<tpl for=".">',
  '{[values.name.toUpperCase()]}</br>',
  '<tpl for="cities">',
  '<div class="{[xindex % 2 === 0 ? "even" : "odd"]}">',
      '{[values.name.toUpperCase()]}</br>',
  '</div>',
  '</tpl>',
'</tpl>'

在这个例子中,我们使用{[values.name.toUpperCase()]}将州和城市的名称强制为大写。我们还使用{[xindex % 2 === 0 ? "even" : "odd"]}根据当前计数除以 2 的余数(取模运算符)交替行颜色。

即使有了编写内联 JavaScript 的能力,有许多情况下你可能需要更加健壮的东西。这就是 XTemplate 成员函数发挥作用的地方。

XTemplate 成员函数

一个 XTemplate 成员函数允许你将一个 JavaScript 函数附加到你的 XTemplate 上,然后通过调用this.function_name在模板内部执行它。

这些函数添加到模板的末尾,一个模板可以包含多个成员函数。这些成员函数被一对花括号括起来,这与监听器的风格类似:

{
myTemplateFunction: function(myVariable) {
  ...
 },
myOtherTemplateFunction: function() {
  ...
 }
}

我们可以使用这些成员函数向我们的模板返回附加数据。让我们使用我们之前的州和城市示例,看看我们如何根据我们数据的多个量在较大的城市旁边放置一个特殊图标。

'<tpl for=".">',
  '{name}</br>',
  '<tpl for="cities">',
      '<div>{name} <tpl if="this.isLargeCity(values)"><img src="img/bigCity.png"></tpl></div>',
    '</tpl>',
  '</tpl>',
'</tpl>',
{
isLargeCity: function(values) {
  if(values.population >= 5000 && values.hasAirport && values.hasHospital) {
   return true;
  } else {
   return false;
  }
 }
}

在这个例子中,我们创建了一个名为isLargeCity的成员函数,在其中传递我们的数据。由于我们的函数可以执行任何我们想要的 JavaScript,我们可以使用结果来控制模板。然后我们可以在模板中调用函数{[this.isLargeCity(values)]},根据数据记录中的值打印我们的bigCity.png图片。

XTemplate 成员函数

我们还可以使用成员函数来帮助我们检查数据是否存在或不存在。这在控制我们的模板时非常有用。例如,让我们从一个包含姓名、地址和电子邮件的联系人模板开始,类似于以下内容:

var myTemplate = new Ext.XTemplate(
  '<div style="padding:10px;"><b>{first} {last}</b><br>',
  '{address}<br>',
  '{city}, {state} {zip}<br>',
  '<a href="mailto:{email}">{email}</a><br>',
  'Birthday: {birthday:date("n/j/Y")}</div>'
);

如果我们没有addresscitystate的数据,我们最终会有一些空行和一个多余的逗号。由于根据我们的模型,zip变量是一个integer,如果我们没有为它存储值,它将显示为0

XTemplate 成员函数

我们需要一种方法来检查在将这些项目打印到屏幕之前我们是否有这些项目的数据。

空函数

结果证明,原生 JavaScript 在检测空值方面非常有问题。根据函数的不同,JavaScript 可能会返回以下内容:

  • 空值

  • 未定义

  • 空数组

  • 空字符串

对于我们大多数人来说,这些都是差不多一样的东西;我们没有得到任何东西。然而,对于 JavaScript 来说,这些返回值是非常不同的。如果我们尝试用if(myVar == '')来测试数据,并且我们得到nullundefined或空数组,JavaScript 将返回false

幸运的是,Sencha Touch 有一个方便的小函数叫做isEmpty()。这个函数将测试 null、undefined、空数组和空字符串,所有这些都在一个函数中。然而,Sencha Touch 没有一个相反的函数来测试有数据,这是我们真正想要测试的。多亏了模板成员函数,我们可以编写自己的函数。

var myTemplate = new Ext.XTemplate(
  '<div style="padding:10px;"><b>{first} {last}</b><br>',
  '<tpl if="!Ext.isEmpty(address)">',
    '{address}<br>',
    '{city}, {state} {zip}<br>',
  '</tpl>',
  '<a href="mailto:{email}">{email}</a><br>',
  'Birthday: {birthday:date("n/j/Y")}</div>'

甚至不需要为这个数据检查编写成员函数。我们可以在我们的模板中添加<tpl if="!Ext.isEmpty(address)">,并与我们的模板并列检查地址。Ext.isEmpty函数类获取地址数据,确定它是空的还是包含数据,分别返回truefalse。如果address不为空,我们打印出地址,如果为空,我们什么都不做。

使用 XTemplate.overwrite 更改面板内容

在我们之前的示例中,我们已经将 XTemplate 作为我们面板或列表的一部分声明,使用tplitemtpl。然而,在列表或面板显示之后,编程地覆盖一个模板也可能很有帮助。您可以通过声明一个新的模板,然后使用面板或列表的overwrite命令将模板和数据结合,覆盖面板或列表的内容区域来实现。

var myTemplate = new Ext.XTemplate(
'<tpl for=".">',
  '{name}</br>',
  '<tpl for="cities">',
      '- {name}<br>',
    '</tpl>',
  '</tpl>',
'</tpl>'
);

myTemplate.overwrite(panel.body, data);

我们的overwrite函数将一个元素(ExtHTML)作为第一个参数。所以,我们不仅需要使用面板,还需要使用面板的body元素作为panel.body。然后,我们可以为新的模板提供来自数据存储的一个记录或一个值数组作为第二个参数。

虽然 XTemplates 对于显示我们的数据非常强大,但它们仍然非常文本化。如果我们想以更有色彩的方式显示数据会怎样?让我们来看看 Sencha Touch Charts,了解我们如何做到这一点。

Sencha Touch Charts

到目前为止,我们只是查看了数据存储和记录作为显示文本数据的方式,但随着 Sencha Touch Charts 的发布,我们现在能够以图形数据的形式在我们的应用程序中显示复杂的数据。

Sencha Touch Charts

这些新组件使用数据存储来显示各种图表和图类型,包括以下类型:

  • 饼图

  • 柱状图

  • 折线图

  • 散点图

  • 蜡烛图

  • OHLC(开盘价、最高价、最低价、收盘价)

  • 气泡图

虽然对图表组件的全面探索值得一本单独的书,但我们将提供一个这些组件如何与数据存储交互的概述,并希望激发你的好奇心。

安装 Sencha Touch Charts

截至版本 2.1,Sencha Touch Charts 已集成到 Sencha Touch 中,不再需要单独下载。在撰写本文时,图表包许可作为开源 GPLv3 许可的一部分提供,或作为 Sencha Complete 或 Sencha Touch Bundle 的一部分提供。

一个简单的饼图

让我们从一个简单的 JavaScript 文件开始,用于我们的图表示例,从数据存储开始:

Ext.application({
 name: 'TouchStart',
 launch: function() { 
  var mystore = Ext.create('Ext.data.JsonStore', {
   fields: ['month', 'sales'],
   data: [
    {'month': 'June', 'sales': 500},
    {'month': 'July', 'sales': 350},
    {'month': 'August', 'sales': 200},
    {'month': 'September', 'sales': 770},
    {'month': 'October', 'sales': 170}
   ]
  });
 }
});

我们的存储声明了两个字段类型,monthsales,我们的数据数组持有五组monthsales值。这将输入到极坐标图中,在本例中,是一个饼图。在存储定义之后,我们添加如下内容:

Ext.create('Ext.chart.PolarChart', {
  background: 'white',
  store: mystore,
  fullscreen: true,
  innerPadding: 35,
  interactions: ['rotate'],
  colors: ["#115fa6", "#94ae0a", "#a61120", "#ff8809", "#ffd13e"],
  legend: {
   position: 'right',
   width: 125,
   margin: 10
  },
  series: [
   {
    type: 'pie',
    xField: 'sales',
    labelField: 'month',
    donut: 25,
     style: {
      miterLimit: 10,
      lineCap: 'miter',
      lineWidth: 2
     }
   }
  ]
});

就像我们的其他面板组件一样,Ext.chart.PolarChart类需要一些标准的配置,如heightwidthfullscreen。它还有一些特殊的配置,如innerPadding,这是坐标轴和系列之间的填充,以及background,这是图表背后的背景颜色。chart组件还需要一个store配置选项,我们将将其设置为我们之前创建的mystore组件。

interactions部分允许我们指定一些视觉工具,使用户能够与图表互动。每种图表都有它自己的一组交互。当前的交互包括:

  • panzoom:此交互允许我们在坐标轴之间平移和缩放

  • itemhighlight:此交互允许我们突出显示系列数据点

  • iteminfo:此交互允许我们在弹出面板中显示数据点的详细信息

  • rotate:此交互允许旋转饼图和雷达系列

接下来是我们图表的legend配置。这为我们的所有图表值提供了颜色编码的参考。我们可以使用一个位置配置来指定图例在纵向和横向模式下应如何显示。

最后的部分是我们的series配置。在我们的示例中,我们设置了:

  • 我们将看到的类型的图表

  • 图表将使用哪个xfield来确定饼图扇区的大小

  • 要用于饼图扇区的labelField的值

  • 饼图中心孔洞的大小

  • 图表的整体风格

当我们加载所有内容时,我们的图表如下所示:

一个简单的饼图

如果您点击图例上的任何月份,您可以将其在图表中打开或关闭。这个功能无需任何额外代码即可自动发生。我们的交互设置还允许我们点击并拖动以旋转图表。

这种饼图非常适合非常简单的单系列数据,但如果我们有几年的数据呢?让我们看看柱状图可能如何显示这种数据。

柱状图

对于我们的柱状图,让我们用以下内容替换我们的图表数据存储:

var mystore = Ext.create('Ext.data.JsonStore', {
 fields: ['month', '2008', '2009', '2010'],
 data: [
   {'month': 'June', '2008': 500, '2009': 400, '2010': 570},
   {'month': 'July', '2008': 350, '2009': 430, '2010': 270},
   {'month': 'August', '2008': 200, '2009': 300, '2010': 320},
   {'month': 'September', '2008': 770, '2009': 390, '2010': 670},
   {'month': 'October', '2008': 170, '2009': 220, '2010': 360}
 ]
});

这个数据集有我们需要显示的多系列数据(五个月,每个月有三年的数据)。一个有效的柱状图需要为每个月显示一行,并在同一个月内为每个年份显示不同的柱状图。

接下来,我们需要将我们的PolarChart更改为CartesianChart,如下所示:

Ext.create("Ext.chart.CartesianChart", {
 fullscreen: true,
 background: 'white',
 flipXY: true,
 store: mystore,
 interactions: ['panzoom'],
 legend: {
  position: 'right',
  width: 80,
  margin: 10
 },
 axes: [
  {
   type: 'numeric',
   position: 'bottom',
   grid: true,
   minimum: 0
  },
  {
   type: 'category',
   position: 'left'
  }
 ],
 series: [
  {
   type: 'bar',
   xField: 'month',
   yField: ['2008', '2009', '2010'],
   axis: 'bottom',
   highlight: true,
   showInLegend: true,
   style: {
    stroke: 'rgb(40,40,40)',
    maxBarWidth: 30
   },
   subStyle: {
    fill: ["#115fa6", "#94ae0a", "#a61120"]
   }
  }
 ]
}); 

就像我们的饼图一样,柱状图组件也需要backgroundfullscreen、数据store以及panzoom交互的配置选项。这个选项使我们能够在坐标轴之间进行平移和缩放。

然后是我们之前的图例,后面跟着一个新配置选项叫做axes。由于柱状图沿着 x 轴和 y 轴进行操作,我们需要指定每个轴输入的是什么类型的数据(在这个例子中,是bottomleft轴)。

首先是我们每年的销售数据。这些数据是数值型的,位于底部,并命名为sales。我们还指定了我们的最小值应该是多少(这个数字将出现在柱状图的最左端,通常会是0)。

下一个轴是我们的分类数据(也将用于我们的图例)。在这个例子中,我们的positionleft,我们的title一年的月份。有了这个,我们就完成了axes配置。

最后是我们的series配置,将其设置为柱状图。与我们之前的饼图示例不同,后者只跟踪单个点的销售数据,而柱状图跟踪两个不同点的销售数据(monthyear),因此我们需要分配我们的xFieldyField变量,并声明一个轴位置。这个位置应该与您显示数值数据所在的轴匹配(在我们的案例中,数据在 y 轴上,位于底部)。我们最后使用showInLegend来显示我们的图例。

最终的图表应该如下所示:

柱状图

注意

图表是使用存储显示数据的一种非常灵活的方式。我们在这里实在没有时间一一讲解,但你可以通过docs.sencha.com/touch/2.2.0/#!/guide/drawing_and_charting探索 Sencha Touch Charts 的所有功能。

总结

在本章中,我们探讨了数据存储可以用来显示简单和复杂数据的方式。我们谈论了绑定、排序、分页和加载数据存储。然后我们通过使用数据存储与列表和面板结合的方式进行讲解。

我们还讲解了如何使用 XTemplates 来控制存储和记录中的数据布局。我们探讨了如何在 XTemplate 中操作和遍历我们的数据,以及如何使用条件逻辑、算术和内联 JavaScript。我们在讨论 XTemplates 时,通过讨论成员函数及其用途来结束。我们通过查看如何使用 Sencha Touch Charts 包以图形化的方式显示我们的存储数据来结束本章。

在下一章中,我们将探讨如何将我们前几章的所有信息整合到一个完整的应用程序中。

第八章:创建 Flickr Finder 应用

到目前为止,我们已经单独或在小型的简单应用中查看了 Sencha Touch 组件。在本章中,我们将使用 Sencha Touch 创建一个结构良好且更详细的应用。我们将尝试利用我们前几章的所有技能来创建一个允许我们搜索靠近我们位置的照片的应用。本章将包括:

  • 介绍模型视图控制器MVC)设计模式

  • 设置更健壮的文件夹结构

  • 设置主要应用文件

  • 使用 Flickr API

  • 注册组件

  • 设置SearchPhotos组件

  • 设置SavedPhotos组件

  • 给应用添加最后的润色以发布

生成基本应用

这个应用的基本想法是使用 Flickr API 来发现靠近我们位置的照片。我们还将增加保存有趣照片的功能,以便我们以后想看时能够找到。

当你第一次创建一个应用时,最好先勾勒出界面草图。这让你对需要构建的各个部分有一个大致的了解,同时也允许你像用户一样遍历各种屏幕。它不需要很漂亮;它只需要给你一个创建应用的所有部分的基本概念。

目标应该是非常基础的,比如这样:

生成基本应用

接下来,你希望通过纸质界面点击你的方式,就像你会在一个真实应用中那样,思考每次点击会将用户带到哪里,可能缺少什么,以及可能对用户造成困惑的地方。

我们的基本应用需要能够显示照片列表以及单张照片的特写。当我们点击列表中的照片时,我们需要显示更大的特写照片。我们还需要一种在查看完照片后返回列表的方法。

当我们看到喜欢照片时,我们需要能够保存它,这意味着我们需要一个保存照片的按钮,以及一个保存照片的单独列表和一个保存照片的特写视图。

一旦我们对草图感到满意,我们就可以开始编写代码,将我们的纸质原型转变为类似这样的东西:

生成基本应用

介绍模型视图控制器

在我们开始构建应用之前,我们应该花些时间谈论一下结构和组织。虽然这可能看起来像是应用哲学的乏味偏离,但实际上,这是你应用中最关键的考虑因素之一。

首先,考虑一下单片应用程序,所有内容都集中在一个巨大的文件中。这似乎很疯狂,但你会遇到成百上千个以这种方式编写的应用程序。试图调试这种东西是一场噩梦。想象一下在一个 750 行长的组件数组中找到缺失的闭合花括号。糟糕!

那么问题变成了如何逻辑地分割文件。

如本书前面所讨论的,模型-视图-控制器(MVC)架构根据代码的功能组织应用程序文件:

  • 模型描述你的数据。

  • 视图控制数据如何显示。

  • 控制器通过从用户那里获取输入并告诉视图和模型根据用户的输入如何响应来处理用户交互。

Sencha Touch 还使用存储库,描述组件之间数据存储和传输的情况。当我们把应用程序的这些部分分开时,意味着你应用程序的每个部分都将有这些部分的单独文件。让我们来看看这个结构是怎样的:

介绍模型视图控制器

这是一个基本的应用程序骨架,由 Sencha Cmd 输出,我们将用它来开发我们的FlickrFindr项目。在根目录中,我们有文件夹:

  • app:这个文件夹包含我们的主要应用程序文件。

  • :这个文件夹用于存放我们可能需要的任何外部库。

  • 资源:这个文件夹包含我们的 CSS、SASS、图标和各种图片文件。

  • 触摸:这个文件夹包含 Sencha Touch 库的副本。

在我们的app目录中,我们有自己的文件:

  • 控制器:我们的控制器将包含我们应用程序的功能。

  • 表单:我们的表单将控制我们使用的任何表单的外观。

  • 模型:我们的模型将描述我们使用的数据。

  • 配置文件:我们的配置文件将包含不同类型设备的显示信息。本书不涉及这部分内容,但详细解释可以在 Sencha 网站上的docs.sencha.com/touch/2.2.1/#!/guide/profiles找到。

  • 存储库:我们的存储库决定应用程序数据如何存储。

  • 视图:我们的视图控制应用程序的外观。

通过这种方式分割文件,跨应用程序重用代码要容易得多。例如,假设你构建了一个具有模型、存储库、控制器以及用户视图的应用程序。如果你想创建另一个需要处理用户的新应用程序,你只需将模型、存储库、视图和控制器的单个文件复制到你的新应用程序中即可。如果所有文件都被复制过去,那么用户代码应该和在前一个应用程序中一样正常工作。

如果我们构建一个单体应用程序,您必须浏览代码,提取出片段,并将它们重新组装到新应用程序中。这将是一个缓慢且痛苦的过程。通过按功能分离我们的组件,项目间复用代码要容易得多。

分割组件

我们需要考虑的下一件事是应用程序如何被拆分成独立的 MVC 部分。例如,如果您的应用程序跟踪人和他们拥有的汽车,您可能会为人和汽车分别有一个模型和控制器。您还可能为汽车和人都有多个视图,如添加编辑列表详情

在我们的应用程序中,我们将处理两种不同类型的数据。第一种是照片的搜索数据,第二种是我们保存的照片。

如果我们把这个分解成模型、存储、视图和控制器,我们得到类似于以下的内容:

分割组件

我们的控制器通过功能分为保存照片搜索照片

由于它们处理相同类型的数据,我们的每个控制器都可以使用相同的照片模型,但它们将需要不同的存储(搜索照片保存照片),因为它们各自使用不同的实际数据集。

对于视图,我们的搜索需要一个搜索列表的列表视图和一个搜索详情视图。保存的照片也需要一个保存列表视图和一个用于编辑/添加保存详情的视图。

现在我们已经清楚地了解了我们需要的文件,是时候开始构建我们的应用程序了。

使用 Sencha Cmd 建立基础

在 Sencha Touch 的 1.0 版本中,应用程序的设置过程非常手动且耗时。然而,Sencha Cmd 的引入允许我们用一条命令生成大多数核心应用程序文件。

Sencha Cmd 是一组命令行工具,在 Sencha Touch 中执行许多基本任务,例如:

  • 生成一个可以用作应用程序基础的应用程序骨架

  • 生成控制器、表单和模型

  • 构建您的应用程序以“最小化”和压缩 JavaScript 和图片以供生产应用程序使用

  • 将您的应用程序作为可以在 App Store 中销售的独立二进制文件构建

Sencha Cmd 有许多其他用途和配置选项。有关这些信息,请参阅docs.sencha.com/touch/2.2.1/#!/guide/command

对于这个项目,我们将主要使用generate命令来构建我们的基本应用程序、控制器和模型。然而,首先我们需要安装所有内容。

安装 Sencha Cmd

Sencha Cmd 是我们从 Sencha Touch 代码下载的独立文件,可以在以下网址找到:www.sencha.com/products/sencha-cmd/download。这个下载文件支持 Windows、OS X 和 Linux(32 位和 64 位)。解压缩下载的文件后,你可以双击它来安装 Sencha Cmd。

注意

对于这本书,我们使用的是 Sencha Cmd 版本 3(至少需要版本 3.1.2 或更高版本)。详细的安装说明可以在这里找到。

一旦你安装了 Sencha Cmd,你可以按照以下方式打开计算机上的命令行:

  • 在 Mac OS X 上,前往应用程序并启动终端

  • 在 Windows 上,前往开始 | 运行 并输入cmd

一旦你的终端打开,输入sencha。你应该会在终端中看到如下截图:

安装 Sencha Cmd

现在我们已经安装了 Sencha Cmd,是时候为我们的应用程序生成骨架了。

首先,你需要切换到你的 Sencha Touch 文件安装的目录(不是我们刚刚下载的 Sencha Cmd 文件,而是你原始的 Sencha Touch 2.1 文件):

cd /path/to/Sencha-touch-directory/

Sencha Cmd 将使用来自这个 Sencha Touch 目录的文件来生成我们的应用程序。从这里,我们使用generate命令如下:

sencha generate app FlickrFindr /path/to/www/flickrfindr

当然,你需要根据你自己的开发环境调整前面的路径,以创建我们在介绍模型视图控制器部分展示的目录结构的基本应用程序。

除了我们之前提到的文件夹,你还会看到许多已经创建的文件。你可以查阅 Sencha Cmd 文档,了解所有文件的详细信息,但现在我们只需要关注一个名为app.js的文件。

app.js文件在我们应用程序启动时加载,并处理我们的基本设置。如果你查看文件顶部的注释,你应该会看到类似这样的内容:

Ext.Loader.setPath({
    'Ext': 'touch/src'
});

这为我们的 Sencha Touch 框架副本设置了路径,Sencha Cmd 在我们执行generate app命令时,将其复制到了一个touch目录中。

接下来的几行设置了我们的应用程序命名空间(name)和处理我们需要的文件:

Ext.application({
    name: 'FlickrFinder',
    requires: [
        'Ext.MessageBox'
    ],
    views: [
        'Main'
    ],

requires部分列出了我们应用程序需要的任何内部或外部库。Ext.MessageBox组件是默认包含在内的。我们还有一个views部分,可以列出我们应用程序需要的任何视图。还可以为模型、存储和控制器添加单独的部分。我们稍后会涉及到这些。

接下来的几部分将涉及到我们的应用程序图标和启动屏幕(startupImage)。这些图像可以通过替换现有图像或更改 URL 来指向新的图像文件进行修改。

我们app.js文件的最后一部分是launch函数。所有组件加载完毕后会被调用:

    launch: function() {
        // Destroy the #appLoadingIndicator element
        Ext.fly('appLoadingIndicator').destroy();

        // Initialize the main view
        Ext.Viewport.add(Ext.create('FlickrFindr.view.Main'));
    }

这个函数移除了加载指示器并显示我们的主窗口。FlickrFindr.view.Main文件在我们的views文件夹中;我们很快就会修改它。

提示

请注意,我们示例中的文件名是FlickrFindr.view.Main,这告诉应用程序这个文件叫做Main.js,并且它位于我们的app/view文件夹中。

如果我们有很多视图,我们可以将它们分成views文件夹内的目录。例如,我们可以为我们的应用程序创建searchsaved文件夹。在这种情况下,我们search文件夹的Details.js视图将是FlickrFindr.view.search.Details,而我们的saved文件夹的Details.js视图将是FlickrFindr.view.saved.Details

我们稍后会回到这个文件,但现在,只需熟悉一下内容。

既然我们已经知道我们的应用程序应该如何布局,那么在真正开始之前我们还有一项任务要完成。我们需要从 Flickr 获取一个 API 密钥。

使用 Flickr API

大多数流行的网络应用程序都为其他应用程序提供了一个API应用程序编程接口)。这个 API 的工作方式与我们的 Sencha Touch 框架几乎相同。API 提供了一系列可以用来从远程服务器读取,甚至写入数据的方法。

这些 API 通常需要一个密钥才能使用。这使得服务能够跟踪谁在使用服务并限制对系统的任何滥用。API 密钥通常是免费且容易获得的。

访问 Flickr API 网站www.flickr.com/services/api/,寻找API 密钥这个短语。点击链接,使用提供的表单申请 API 密钥。当您收到 API 密钥时,它将是一个由数字和小型字母组成的 32 个字符长的字符串。

每次您向 Flickr API 服务器发送请求时,您都需要传输这个密钥。我们稍后会谈到这部分。

Flickr API 涵盖了超过 250 个方法。其中一些需要您使用 Flickr 账户登录,但其他方法只需要 API 密钥。

出于我们的目的,我们将使用一个单一的 API 方法,称为flickr.photos.search,该方法无需登录。此方法根据某些标准寻找照片。我们将使用设备的当前纬度和经度来获取距离我们当前位置指定距离内的照片。

我们的搜索结果以一大堆 JSON 格式返回,我们需要对其进行解码以显示。

一旦我们有了 API 密钥,我们就可以开始设置我们的模型、存储、视图和控制器。

向基本应用程序添加内容

首先,让我们看看我们生成的原始应用程序。目前,如果您将应用程序加载到您的网络浏览器中,您应该会看到类似这样的内容:

添加到基本应用程序

我们正在查看的这个视图来自app/view/Main.js文件,这是一个带有两个子面板的标签面板。我们将用一个更简单的标签面板替换这段代码:

Ext.define('FlickrFinder.view.Main', {
    extend: 'Ext.tab.Panel',
    xtype: 'main',
    requires: [
        'FlickrFinder.view.SearchPanel',
        'FlickrFinder.view.SavedPanel'
    ],
    config: {
        tabBarPosition: 'bottom',
        items: [
           { xtype: 'searchpanel'},
           { xtype: 'savedpanel'}
        ]
    }
});

就像我们正在替换的代码一样,这个组件扩展了一个标签面板并设置了一个xtypemain

在这个上下文中,xtype可能会有些令人困惑,因为Ext.tab.Panel已经有了xtypetabpanel。然而,由于我们正在扩展标签面板,我们实际上是在创建一个新的组件,这意味着我们可以为这个组件设置xtypextype完全是任意的,但必须在所有组件(包括 Sencha 自己的组件)中唯一。使用main作为xtype是您应用程序开始容器的一般约定。通过设置自定义xtype,我们可以很容易地从我们稍后创建的控制器中找到容器。

在我们的requires部分,我们列出了两个新的视图,一个是FlickrFinder.view.SearchPanel,另一个是FlickrFinder.view.SavedPanel。在这个代码块的稍下方,你将看到在items部分列出的两个xtype值,searchpanelsavedpanel,它们对应于这两个必需的文件。我们接下来需要创建这些文件。

在你的文本编辑器中,你需要在你的app/view文件夹中创建两个新的面板文件。第一个叫做SearchPanel.js,代码将如下所示:

Ext.define('FlickrFinder.view.SearchPanel', {
    extend: 'Ext.Panel',
    xtype: 'searchpanel',
    config: {
        title: 'Search', 
        iconCls: 'search',
        html: 'Search Panel'

    }
});

就像我们的Main.js文件一样,我们首先将文件名设置在命名空间内为FlickrFinder.view.SearchPanel。然而,这次我们扩展了Ext.Panel组件而不是标签面板。

我们还设置了xtypesearchpanel,这应该与我们在app/view/Main.jsitems部分中的值相匹配。

最后,我们用titleiconClshtml设置了我们的config部分。

接下来,我们在app/view中创建了一个名为SavedPanel.js的第二个面板。这个面板的代码几乎与我们的上一个面板一样:

Ext.define('FlickrFinder.view.SavedPanel', {
    extend: 'Ext.Panel',
    xtype: 'savedpanel',
    config: {
        title: 'Saved',
        iconCls: 'favorites',
        html: 'Saved Panel'
    }
});

正如你所看到的,我们只是将单词search替换为单词saved。一旦这个第二个面板保存完毕,你可以重新加载页面来看这个:

添加到基本应用程序

我们现在有了一个带有两个基本面板的标签面板,我们可以在这两个面板之间相互切换。

目前,我们的应用程序还做不了太多事情,但我们可以解决这个问题。让我们先为我们的应用程序添加一对控制器。

使用 Sencha Cmd 生成控制器

就像我们的入门应用程序一样,我们也可以使用 Sencha Cmd 生成控制器。为此,你需要回到你的终端程序,并从那里切换到你的应用程序目录(而不是 Sencha 目录,就像我们生成应用程序时那样)。切换到这个目录让 Sencha Cmd 知道在哪里创建控制器文件:

cd /path/to/www/myapp
sencha generate controller SearchPhotos
sencha generate controller SavedPhotos

这将为我们的控制器创建两个启动文件,并在app.js中添加对这些文件的引用。如果您在运行这些命令后打开app.js,现在应该看到一个controllers部分,如下所示:

    controllers: [
        'SearchPhotos',
        'SavedPhotos'
    ]

如果你打开其中一个新控制器文件,你应该看到类似这样的内容:

Ext.define('FlickrFinder.controller.SearchPhotos', {
    extend: 'Ext.app.Controller',
    config: {
        // refs: {
        //     TODO: add refs here
        // },
        // control: {
        //     TODO: add event handlers here    
        // }
    },

    // Called when the Application is launched, remove if not needed
    launch: function(app) {

    }
});

通过首先创建这两个空的控制器,我们然后可以将任何模型、视图和存储区添加到我们的两个controller文件中,并保持app.js不变(因为它已经包括了我们的控制器)。

注解

在某些 Sencha Cmd 的旧版本中,使用generate controller命令会创建文件,但不会向app.js添加controllers部分。最好检查并确保controllers部分被添加,否则您的文件将无法加载。这将导致错误和很多扯头发的情况。

关于包含文件的一些简要说明

当你把代码分离到单独的文件中时,框架需要具备基本的理解,了解需要包含哪些文件才能使应用程序运行。

app.js和我们controller文件都可以包括模型、视图和存储区段。这些部分可以在任意一组文件中指定,但最佳实践是在app.js中包括controllers,并让各个控制器包括模型、存储和视图。

其他组件可以包含一个required部分来包含文件。例如,在我们的main.js视图中,我们要求两个面板视图(savedPanelsearchPanel),这些视图在我们的main.js文件中的items部分使用。requires部分用于任何直接在组件中使用的依赖项。

我们在创建我们的模型和存储时将看到这个例子。

创建 Photo 数据模型

我们的搜索和保存的照片都将处理相同的信息集。这意味着我们可以创建一个单一共享的模型,称为 Photo。

我们的照片数据将在一定程度上受到我们从 Flickr API 能够获取的数据的限制。然而,我们还希望将图片作为搜索结果的一部分显示出来。这意味着我们需要查看 Flickr API,并了解在应用程序中显示 Flickr 图片需要什么。

如果我们查看www.flickr.com/services/api/misc.urls.html,我们会发现 Flickr 中的Photo Source URLs具有以下结构:

http://farm{farm-id}.static.flickr.com/{server-id}/{id}_{secret}.jpg

这意味着,为了显示每张照片,我们需要以下内容:

  • farm-id:这个变量指示图片所在的服务器群组

  • server-id:这个变量指示图片所在的特定服务器

  • id:这个变量指示图片的唯一 ID

  • secret:这个变量指示 Flickr API 用于路由请求的代码

这些都是我们作为flickr.photos.search请求的一部分收到的所有变量。我们还收到照片的标题,我们可以将其作为显示部分使用。现在,我们将使用这些信息来创建我们的模型。

在模型目录中,创建一个名为Photo.js的新文件:

Ext.define('FlickrFindr.model.Photo', {
    extend: 'Ext.data.Model',
    config: {
        fields: [
            { name: 'id', type: 'int' },
            { name: 'owner', type: 'string' },
            { name: 'secret', type: 'string' },
            { name: 'server', type: 'int' },
            { name: 'farm', type: 'int' },
            { name: 'title', type: 'string' }
        ]
    }
});

我们首先定义我们新的模型并扩展Ext.data.Model。接下来,我们提供字段定义的一系列name type值。如果您留空类型,Sencha Touch 会试图自己找出答案,但尽可能指定类型是个好主意。

既然我们已经定义了一个共享的Photo模型,接下来我们需要从我们的SearchPhotos组件开始设置个体组件。

制作 SearchPhotos 组件

为了搜索照片,我们将需要一个存储和两个视图(列表和详细信息)。当我们的应用程序启动时,SearchPhotos.js控制器将确定用户的当前位置。控制器然后根据该位置加载存储并在我们的列表组件中显示照片。当用户在列表中点击一个项目时,控制器将抓取我们的详细视图并使用它来显示有关照片的更多信息。

让我们先创建我们的数据存储。

创建 SearchPhotos 存储

数据存储负责与 Flickr API 联系并获取我们的列表中的照片。我们还需要包括一些基本的分页信息和对我们共享模型文件的引用。

app/store中创建一个名为SearchPhotos.js的新文件,并添加以下代码:

Ext.define('FlickrFindr.store.SearchPhotosStore', {
    extend: 'Ext.data.Store',requires: 'FlickrFindr.model.Photo',config: {model: 'FlickrFindr.model.Photo',autoLoad: false,pageSize: 25,proxy: {type: 'jsonp',url: 'http://ycpi.api.flickr.com/services/rest/',callbackKey: 'jsoncallback',limitParam: 'per_page',reader: {type: 'json',root: 'photos.photo',totalProperty: 'photos.total'}}}
});

在这里,我们定义了FlickrFindr.store.SearchPhotos存储,并扩展了标准的Ext.data.Store存储。由于我们正在使用我们之前创建的Photo模型,我们还需要将其添加到我们的requires部分。我们将使用一个jsonp代理来进行此存储。

如果您记得从第六章,获取数据,这个代理类型用于处理对不同服务器的请求,这与 JSONP 类似。这些跨站请求需要回调函数来处理服务器返回的数据。然而,与 JSONP 不同,jsonp代理将几乎自动处理回调功能为我们。

我们说几乎是因为 Flickr 的 API 期望回调变量作为:

jsoncallback = a_really_long_callback_function_name

默认情况下,存储将此变量作为:

callback = a_really_long_callback_function_name

幸运的是,我们可以通过设置以下配置选项来改变这一点:

callbackParam: 'jsoncallback'

在前面的代码片段中的下一部分设置了用于与 Flickr API 联系的 URL,即url: 'api.flickr.com/services/rest/'。这个 URL 对 Flickr API 的任何请求都相同。

我们需要向 Flickr API 发送许多其他参数以获取我们需要的内容,但我们会稍后在控制器中处理。

一旦我们返回数据,我们就将其传递给读取器:

reader: {
 type: 'json',
 root: 'photos.photo' ,
 totalProperty: 'photos.total'
}

由于我们从 Flickr API 得到的响应是 JSON 格式,我们需要在reader函数中设置type: 'json'。我们还需要告诉reader函数在从 Flickr 返回的json数组中寻找照片的开始位置。在这个例子中,root: 'photos.photo'是正确的值。我们需要的最后一件事是totalProperty,它告诉读者我们从 Flickr 返回的总照片数。我们将使用这个值进行分页。

现在我们已经设置了数据模型和存储,我们还需要两个视图:SearchPhotoList视图和SearchPhotoDetails视图。

创建 SearchPhotos 列表

我们需要为应用程序中的SearchPhotos部分创建两个视图:一个列表组件和一个用于详细信息的面板。我们将从创建列表组件开始。

在我们的views文件夹中创建一个SearchPhotoList.js文件。这将是我们的两个SearchPhotos视图中的第一个。每个视图代表一个 Sencha Touch 显示组件。在这个例子中,我们将使用Ext.dataview.List类进行显示和 XTemplate 来控制列表的布局。

在文件顶部,我们的 XTemplate 看起来如下:

var SearchResultTpl = new Ext.XTemplate(
    '<div class="searchresult">',
    '<img src="img/{[this.getPhotoURL("s", values)]}" height="75" width="75"/>',
    ' {title}</div>',
    {
    getPhotoURL: function(size, values) { /* Form a URL based on Flickr's URL specification: http://www.flickr.com/services/api/misc.urls.html */
        size = size || 's';
        var url = 'http://farm' + values.farm + '.static.flickr.com/' + values.server + '/' + values.id + '_' + values.secret + '_' + size + '.jpg';
        return url;
    }
});

我们的 XTemplate 的第一部分为我们提供了将要填充日期的 HTML。我们首先声明一个带类searchresultdiv标签。这给了我们一个类,我们可以稍后用来指定哪个照片结果被点击。

接下来,我们有一个图片标签,需要包含我们要在列表中的 Flickr 图片的 URL。我们可以在 XTemplate 的 HTML 中组装这个字符串,但我们打算通过将其变成 XTemplate 上的一个函数来增加一些灵活性。

Flickr 在使用照片时为我们提供了多种尺寸选项。我们可以作为我们 Flickr 图片 URL 的一部分传递以下任何一个选项:

  • s:这指的是小尺寸的正方形,75 x 75 像素

  • t:这指的是缩略图,最长边为 100 像素

  • m:这指的是小尺寸的图片,最长边为 240 像素

  • -:这指的是中等尺寸的图片,最长边为 500 像素

  • z:这指的是更大尺寸的图片,最长边为 640 像素

  • b:这指的是大尺寸图片,最长边为 1024 像素

  • o:这指的是原始图片,根据源格式是 JPG、GIF 还是 PNG

我们想要设置我们的函数,接收这些选项之一以及我们的模板值,并创建 Flickr 图片 URL。我们的函数首先查看是否传递了尺寸的值,如果没有,我们将其设置为默认的s,即size = size || 's';

接下来,我们使用我们的 XTemplate 值和相应的尺寸组装 URL,最后返回 URL 供我们的 XTemplate HTML 使用。这将让我们为我们的每张图片创建一个缩略图。

现在,我们需要定义我们的列表,并向其传递我们之前创建的 XTemplate 和存储。在我们 XTemplate 的定义之后,添加以下代码:

Ext.define('FlickrFindr.view.SearchPhotoList', {
    extend: 'Ext.dataview.List',
    alias: 'widget.searchphotolist',
    requires: [
        'FlickrFindr.store.SearchPhotosStore'
    ],
    config: {
        store: 'SearchPhotosStore',
        itemTpl: SearchResultTpl
    }
});

这里我们定义并扩展,就像我们对待其他组件一样。

由于我们使用SearchPhotosStore来填充我们的列表,我们还需要在requires部分中包含它。在config部分,我们有我们的list组件的基本storeitemTpl配置。

创建导航视图

现在我们已经有了SearchPhotoList,我们需要将其添加到SearchPhotos面板上,但在我们这样做之前,我们需要谈谈我们的功能。

列表和详情视图的应用程序非常普遍:这包括一个带有有限信息的条目列表,用户可以选择查看关于该项目的更详细页面。详情页通常包括一个回到主列表的按钮或链接。实际上,这种功能如此普遍,以至于 Sencha 有一个内置组件来处理它,称为导航视图

导航视图的运作方式类似于卡片布局,其中容器内的只有一个项目是可见的。然而,导航布局有两个特殊功能:

  • push:此函数将新组件添加到导航视图中,并通过动画过渡显示新组件

  • pop:此函数从导航视图中删除组件,并通过动画过渡显示上一个组件

导航视图还会每次向其添加新容器时添加一个返回按钮。这允许您深入嵌套数据,并使布局本身控制您的导航。我们将使用这种视图来控制列表和详情视图之间的流程,这意味着我们需要对我们的SearchPanel.js文件进行一些更改。

首先,找到如下行:

extend: 'Ext.Panel',

然后将其更改为:

extend: 'Ext.navigation.View',

这个单一的更改把我们现有的面板变成了一个导航视图。

接下来,我们需要将我们的 HTML 从旧的面板中移除,并添加我们的ShowPhoto列表。通过直接将其添加到导航视图中,它将成为面板加载时我们首先看到的内容。为此,更改该行:

html: 'Search Panel'

为:

items: {
 xtype: 'searchphotolist',
 title: 'Photos Near You'
}

稍后在我们控制器中,我们将了解如何将详情面板添加到导航视图中,但首先我们需要创建一个详情视图。

创建 SearchPhotoDetails 视图

如我们之前所说,当用户在列表中点击一个照片时,我们希望能够显示照片的大图,并给用户提供将此照片添加到照片收藏列表的机会。

我们将从一个非常基础的面板开始:

Ext.define('FlickrFinder.view.SearchPhotoDetails', {
 extend: 'Ext.Panel',
 xtype: 'searchphotodetails',
 config: {
  tpl: '<div class="photoDetails"><h1>{title}</h1><img src="img/{id}_{secret}_b.jpg"></div>',
  padding: 10,
  scrollable: {
   direction: 'vertical',
   directionLock: true
  },  
  items: [
   {
     xtype: 'button',
     action: 'savephoto',
     text: 'Add To Saved Photos',
     width: 250,
     margin: '0 0 10 0'
    }
  ]
 }
});

现在我们已经有了我们的两个视图,我们需要编辑我们的控制器以使视图真正工作。

创建 SearchPhotos 控制器

我们的SearchPhotos控制器需要实现以下功能:

  • 获取用户的位置

  • 从 Flickr 加载接近该位置的照片

  • 允许用户翻页查看照片

  • 允许用户从列表中选择照片并查看大图

前两个部分将在控制器launch函数内完成,以便在应用程序启动时自动执行。其他部分将由我们附加到组件事件的单独函数处理。

在那之前,我们想要包括我们的存储和视图文件,并设置一些引用,以便更容易地从控制器内部引用组件。

打开controller文件夹中的SearchPhotos.js文件。如果你跟随我们一起使用 Sencha Cmd 生成控制器文件,你会看到refscontrols的占位符。在这些占位符上方并在config部分内,我们需要添加我们的视图和我们的存储:

Ext.define('FlickrFindr.controller.SearchPhotos', {
    extend: 'Ext.app.Controller',
    config: {
        views: [
            'FlickrFindr.view.SearchPhotoList',
            'FlickrFindr.view.SearchPhotoDetails',
            'FlickrFindr.view.SearchPanel',
            'FlickrFindr.view.Main'
        ],
        stores: [
            'FlickrFindr.store.SearchPhotosStore'
        ]

viewsstores部分列出了我们应用程序中此控制器将进行通信的部分。接下来,我们将添加一些引用,以便更容易从控制器内部访问这些部分。在refs部分(在我们的stores部分之后),添加以下内容:

refs: {
 SearchPhotoList:'searchphotolist',
 Main: 'main',
 SearchPanel: 'searchpanel'
}

refs部分由一系列名称:目标对组成。名称是任意的,目标可以是xtype或者一个有效的Ext.ComponentQuery()字符串。

注意

Ext.ComponentQuery让你使用类似于标准 CSS 选择器的语言来搜索特定的组件。有关详细信息,请参阅docs.sencha.com/touch/2.2.0/#!/api/Ext.ComponentQuery

这些refs将允许我们从控制器内的任何地方抓取组件,使用this.getName()。例如,如果我们需要从控制器内部向我们的searchpanel添加一个组件,我们可以简单地使用以下代码:

var panel = this.getSearchPanel();
panel.add(myComponent); 

注意

需要注意的是,在使用this.getName()时,名称总是大写的。所以如果我们设置我们的引用为searchPhotoList:'searchphotolist'(有一个小写的"s"),我们仍然需要使用this.getSearchPhotoList()(有一个大写的"S")来返回组件。

现在我们已经有了我们的引用,我们将跳过当前的controls部分,并设置我们的launch函数。

设置 launch 函数

我们的launch函数是我们查找用户位置并与 Flickr API 联系以获取我们的照片的地方。我们需要做的第一件事是设置一些默认值——以防用户拒绝分享他们的位置,或者我们可以确定他们的位置。

launch: function() {
 var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, -1);

 // Set some defaults.
 var easyparams = {
  "min_upload_date": Ext.Date.format(dt, "Y-m-d H:i:s"),
  "lat": 40.759017,
  "lon": -73.984059,
  "accuracy": 16,
  "radius": 10,
  "radius_units": "km" ,
  "method": "flickr.photos.search",
  "api_key": Your_API_Key_Goes_Here,
  "format": "json"
 };

前一个代码片段的第一部分通过使用当前日期并从中减去一年来创建一个新的日期。这给了我们正好是一年前的日期。我们将使用这个日期来获取仅在过去一年内发布的照片。

我们的easyparams变量是一组默认参数,如果我们无法获取有效的用户位置,我们将发送给 Flickr 的 API。这些包括我们的最小上传日期、纬度和经度(我们的默认值是纽约,位于时代广场中央)。我们还包括准确度、半径和半径单位的值,以定义我们的搜索应该有多宽。

最后,我们有一个方法(这是我们将会使用的 API 方法),你的 Flickr API 密钥,以及返回数据的格式(在这个例子中,是 JSON)。如在使用 Flickr API部分 previously noted 所提到的,你需要获取你自己的 API 密钥并在这里使用它。

既然我们已经有一些默认值,让我们看看是否可以用Ext.util.Geolocation获取用户的地理位置。

使用 Ext.util.Geolocation

Ext.util.Geolocation组件允许我们使用网络浏览器检索用户的地理位置。这个类是基于大多数现代浏览器内置的 Geolocation API 规范。当这个组件被调用时,用户会被提示并询问他们是否愿意与应用程序分享他们的位置。如果他们确认,组件将返回用户当前位置的纬度和经度。

在我们的默认easyparams定义之后,添加以下代码以访问 Geolocation 组件:

var me = this;
 var geo = Ext.create('Ext.util.Geolocation',{
  autoUpdate: false,
  timeout: 10000,
  // 10 second timeout
  listeners: {
   locationupdate: function(geo) {
    // Use our coordinates.
    easyparams = {
    "min_upload_date": Ext.Date.format(dt, "Y-m-d H:i:s"),
    "lat": geo.getLatitude(),
    "lon": geo.getLongitude(),
    "accuracy": 16,
    "radius": 10,
    "radius_units": "km",
    "method": "flickr.photos.search",
    "api_key": me.getApplication().api_key,
    "format": "json"
    };

    var store = me.getSearchPhotoList().getStore();
    store.getProxy().setExtraParams(easyparams);
    store.load();
  },
  locationerror: function(geo, bTimeout, bPermissionDenied, bLocationUnavailable, message) {
   Ext.Msg.alert('Unable to set location.');
   var store = me.getSearchPhotoList().getStore();
   store.getProxy().setExtraParams(easyparams);
   store.load();
  }
 }
});

geo.updateLocation();

这部分有点长,所以我们一次看一部分。

我们首先创建一个Ext.util.Geolocation组件的新实例。我们将autoUpdate设置为false,这使得组件不会试图不断更新我们的位置。想法是组件会在应用程序打开时仅触发一次(这也让我们不会过度消耗用户的电池寿命)。接下来,我们将timeout value设置为 10000 毫秒(10 秒)。这意味着一旦用户确认我们被允许访问他们的位置,组件将在 10 秒内尝试获取位置信息,然后在超时并报告错误。

对于Ext.util.Geolocation的配置就这样了,但现在我们需要为处理组件返回的数据设置监听器。

我们从Ext.util.Geolocation中获得了事件反馈的两个基本可能性:

  • locationupdate: 如果我们为用户返回了一个有效的地理位置,就会得到这个。

  • locationerror: 如果发生了一些事情,我们无法为用户获取有效的地理位置,就会得到这个。

这两个事件都返回我们的Geolocation对象,附带了一些新数据。在locationUpdate的情况下,我们返回:

  • accuracy: 这给出了最后检索到的纬度和经度坐标的准确度级别。

  • altitude: 这给出了指定位置上椭圆体以上的最后检索到的米高度。

  • altitudeAccuracy: 这给出了高度坐标的最后检索到的准确度级别,以米为单位。

  • heading: 这给出了托管设备的最后检索到的旅行方向,以 0 到 359 之间的非负度数表示,相对于真北顺时针计数(如果速度为零,则报告NAN

  • latitude: 这给出了最后检索到的地理坐标,以度为单位。

  • longitude: 这给出了最后检索到的经度坐标,以度为单位。

  • speed: 这给出了设备最后检索到的当前地面速度,以每秒米为单位。

对于这个应用程序,我们只对我们将传递给我们的easyparams对象的纬度和经度感兴趣。从前面的示例代码:

easyparams = {
    "min_upload_date": Ext.Date.format(dt, "Y-m-d H:i:s"),
    "lat": geo.getLatitude(),
    "lon": geo.getLongitude(),
    "accuracy": 16,
    "radius": 10,
    "radius_units": "km",
    "method": "flickr.photos.search",
    "api_key": me.getApplication().api_key,
    "format": "json"
    };

这和我们的默认对象一样,但我们的位置有了准确的坐标。然后我们可以获取我们的数据store,添加easyparams对象,以便它随我们的请求发送,并调用商店的load函数来获取照片。

var store = me.getSearchPhotoList().getStore();
store.getProxy().setExtraParams(easyparams);
store.load();

这会导致照片出现在我们的SearchPhotoList组件中。

如果无法为用户获取位置,locationError监听器将会触发。它 simply 提示用户我们无法获取位置,然后加载我们的默认easyparams设置,以纽约的位置为准。

我们launch函数做的最后一件事是使用geo.updateLocation();调用我们的Geolocation对象的updateLocation函数。

此时,你应该能够启动应用程序并看到靠近你位置的一组照片。

使用 Ext.util.Geolocation

现在我们已经有了基本的列表,我们可以上下滑动来滚动。然而,在完成我们的控制器之前,我们需要添加一些更多功能。

正如你可能已经注意到的,我们可以滚动,但还不能查看任何详细信息。此外,我们从搜索结果中只加载了前 25 张照片。我们需要有一种方法来告诉列表,我们想要点击一个项目来查看详细信息,滑动一个项目来浏览我们的照片列表。然而,结果证明,我们并不想告诉列表任何东西。我们实际上想要监听它。

监听列表

我们的列表在响应用户互动时会发送许多有用的活动。我们最关心的是itemswipeitemtap这两个事件。我们需要在我们的控制器中监听这些事件,并编写在事件发生时执行的函数。让我们从itemtap事件开始。

为了监听一个事件,我们需要将其添加到controllercontrols部分,如下面的代码片段所示:

SearchPhotoList: {
 itemtap: 'showSearchPhotoDetails'
}

因为我们之前在controller中提到了SearchPhotoList:'searchphotolist',我们可以使用缩写SearchPhotoList来表示我们正在为我们的列表添加监听器。

在这里,我们指定当我们的列表触发itemtap事件时,我们想要执行一个名为showSearchPhotoDetails的函数。接下来,我们需要将该函数添加到我们的controller中。

controller中的launch函数后加一个逗号,然后添加以下内容:

showSearchPhotoDetails: function(list, index, target, record) {
  var panel = Ext.create('FlickrFindr.view.SearchPhotoDetails', {
    title: record.get('title'),
    record: record
  });
  this.getSearchPanel().push(panel);
}

这个函数创建了我们SearchPhotoDetails面板的新实例,并根据被点击的列表项设置了它的titlerecord(记录作为项点击事件的一部分传递)。

提示

Sencha Touch 文档可在此处找到:docs.sencha.com/touch/2.2.0/,它将显示给定事件传递的任何值列表。找到您的组件,然后从事件列表中选择一个事件。事件的右侧将显示传递给事件的值列表。点击蓝色的展开三角形,将提供有关这些事件值的详细信息。

通过设置记录,我们也设置了将被我们的详情模板用于显示的数据。

最后,我们将新面板推送到我们的SearchPanel导航视图组件。记住,由于我们在controller中为此添加了ref,我们可以使用this.getSearchPanel()来获取它。一旦我们将新面板推送到我们的导航视图,列表将被隐藏,而带有返回按钮的新面板将显示出来。试试看。

监听列表

如果你点击返回按钮,详情面板会自动从堆栈中移除,列表会再次显示。

接下来,我们需要处理itemswipe函数,以便它加载项目的下一页或上一页。在这种情况下,我们还需要做一点数学运算,以确保我们不会尝试翻页到列表的开始或结束。我们还需要探索一下,以获取我们从事件中需要的信息。

首先,让我们通过修改controls部分,将我们的监听器添加到controller中,使其看起来像这样:

control: {
 SearchPhotoList: {
  itemtap: 'showSearchPhotoDetails',
  itemswipe: 'pageChange'
 }
}

接下来,我们需要在我们的showSearchPhotoDetails函数之后添加我们的pageChange函数。我们将使用这个函数来确定用户滑过的方向,这样我们才知道是否应该在我们的分页中向前或向后。

从 Sencha 文档中,我们可以看到itemswipe事件返回以下内容:

  • this:这是我们列表组件

  • index:这是被滑过的项目的索引

  • target:这是被滑过的元素或DataItem

  • record:这是与项目相关的记录

  • e:这是事件对象

  • eOpts:这是传递给Ext.util.Observable.addListeneroptions对象

用户滑动的方向存储在事件对象e中。我们可以通过将event对象输出到控制台日志来获取我们需要的值。所以首先,让我们让我们的pageChange函数看起来像这样:

pageChange: function(list, index, target, record, e, eOpts) {
 console.log(e);
}

如果我们重新加载我们的应用程序并滑动我们列表中的一个项目,我们应该在控制台看到e的列表。点击列表旁边的展开三角形,查看以下所有详细信息:

监听列表

从前面屏幕截图的详情中,我们可以看到我们从事件中获得了很多信息,但我们需要的只是方向。这意味着我们可以在我们的函数中测试e.direction,以查看我们需要翻页列表照片的方向。

pageChange: function(list, index, target, record, e, eOpts) {
 console.log(e);
 var store = this.getSearchPhotoList().getStore();
 if(e.direction == 'right') {
  if(store.currentPage != 1) {
   store.previousPage();
  }
 } else {
  var total = store.getTotalCount();
  var page = store.getPageSize();
  if(store.currentPage <= Math.floor(total/page)) {
   store.nextPage()
  ;}
 }
}

首先,通过获取列表并调用getStore()来获取我们的商店。接下来,我们测试我们的滑动方向是否是向。如果滑动方向是向左,我们是在翻页回退。如果我们的当前页面是1,我们不想后退。如果我们的页面大于1,我们使用store.previousPage();进行后退翻页。

如果我们向右滑动,我们需要确保在尝试翻到下一页之前,我们不在最后一页。我们通过获取商店中的total照片数量和pageSize来实现。通过将照片总数除以页数并四舍五入(Math.floor),我们可以得到最后一页的编号。然后,我们将该编号与currentPage进行比较,以决定是否需要翻到下一页。现在,您应该能够通过在项目之间左右滑动来导航列表的页面。

现在我们已经可以查看我们的照片的全尺寸,让我们设置一个savedphoto组件,允许我们保存任何我们喜欢的照片的链接。

构建 SavedPhotos 组件

我们的SavedPhotos组件将需要存储我们搜索结果中单张照片的信息。我们还需要一个用于保存照片的列表视图和一个详细视图,就像我们之前的SearchPhotosListSearchPhotoDetails模型一样。

创建 SavedPhotos 商店

由于我们的SavedPhotosSearchPhotos组件存储的是相同类型的数据,我们不需要创建一个新的模型。我们只需使用我们的Photo.js模型。然而,我们确实需要一个单独的数据存储,一个可以本地存储我们的Photo模型的存储。

让我们在我们的app/store文件夹中创建一个名为SavedPhotosStore.js的新文件,并添加以下代码:

Ext.define('FlickrFindr.store.SavedPhotosStore', {
    extend: 'Ext.data.Store',
    requires: 'FlickrFindr.model.Photo',
    config: {
        model: 'FlickrFindr.model.Photo',
        autoLoad: true, 
        pageSize: 25,
        storeId: 'SavedPhotosStore',
        proxy: {
            type: 'localstorage',
            id: 'flickr-saved'
        }
    }
});

在这里,我们只需创建一个FlickrFindr.store.SavedPhotosStore类,并扩展Ext.data.store。我们还重用了我们的FlickrFindr.model.Photo模型,并将其设置为我们所需文件的一部分。我们还希望这个商店在应用程序启动时加载(autoLoad: true),并将其页面大小设置为25。由于它正在获取本地数据,这对应用程序的加载不会造成很大的负担。

对于这个商店,我们将包含一个storeId,这样我们可以在稍后的控制器中获取商店。我们将我们的代理设置为本地存储数据,并为代理分配一个id组件,flickr-saved,用于存储我们的数据。

当我们完成SavedPhotosStore.js文件后,我们需要将其添加到我们的SavedPhotos.js控制器中。打开controller文件,在config部分添加以下内容:

stores: [
 'FlickrFindr.store.SavedPhotosStore'
]

这将确保我们的商店被加载。接下来,我们需要为列表和详细信息设置两个视图。

创建 SavedPhoto 视图

对于SavedPhoto视图,我们需要一个列表和一个详细视图。这些视图将非常类似于我们为SearchPhotosListSearchPhotoDetails模型已经拥有的视图。实际上,我们可以先复制这两个文件,并稍微调整一下我们的布局。

views文件夹中,复制SearchPhotoList.js文件,并将其重命名为SavedPhotoList.js。你还需要将所有出现的SearchPhotosearchphoto替换为SavedPhotosavedphoto(记住 JavaScript 是大小写敏感的)。你的代码应如下所示:

var SavedResultTpl = new Ext.XTemplate(
 '<div class="savedresult">',
 '<img src="img/{[this.getPhotoURL("s", values)]}" height="75" width="75"/>',
 ' {title}</div>',
 {
  getPhotoURL: function(size, values) { 
  size = size || 's';
  var url = 'http://farm' + values.farm + '.static.flickr.com/' + values.server + '/' + values.id + '_' + values.secret + '_' + size + '.jpg';
  return url;
 }
    });

Ext.define('FlickrFindr.view.SavedPhotoList', {
 extend: 'Ext.dataview.List',
 alias: 'widget.savedphotolist',
 requires: [
  'FlickrFindr.store.SavedPhotosStore'
 ],
 config: {
  store: 'SavedPhotosStore',
  itemTpl: SavedResultTpl
 }
}); 

你会注意到我们在这个文件中创建了原始SearchResultTpl模板的副本。如果我们愿意,我们完全可以重用SearchPhotos.js文件中的FlickrFindr.view.SearchResultTpl类。重用模板是可以的,但这允许我们选择更改保存照片列表的外观。

除了这些,文件与我们的SearchPhotosList.js文件基本相同。

注意

虽然可能看起来有点冗余,拥有两个如此相似的文件,但应该注意的是,它们都从不同的数据存储中读取,并且需要被控制器以不同的方式处理。这也给了我们稍后调整不同视图外观的机会。

对于我们的SavedPhotoDetails视图,我们将采取类似的步骤。将SearchPhotoDetails.js文件复制到你的views文件夹中,并将其重命名为SavedPhotoDetails.js。这个文件将显示一张保存的照片。然而,与我们的搜索照片详情不同,这个保存的照片详情面板将获得一个Remove按钮而不是Save按钮。

你需要修改文件,将Save按钮更改为以下内容:

Ext.define('FlickrFindr.view.SavedPhotoDetails', {
 extend: 'Ext.Panel',
 xtype: 'savedphotodetails',
 config: {
  tpl: '<div class="photoDetails"><h1>{title}</h1><img src="img/{id}_{secret}_b.jpg"></div>',
  padding: 10,
  scrollable: {
  direction: 'vertical',
  directionLock: true
 },
 items: [
  {
   xtype: 'button',
   action: 'removephoto',
   text: 'Remove From Saved Photos',
   width: 250,
   margin: '0 0 10 0'
  }
 ]
 }
});

这与我们之前创建的SearchPhotoDetails文件非常相似;我们只是交换了名称并将我们的Add按钮更改为Remove按钮。我们将在控制器中为这些按钮添加功能。

首先,像我们对待SearchPhotosList一样,我们需要在SavedPanel.js文件中添加SavedPhotosList,并将其更改为扩展Ext.navigation.View而不是Ext.Panel

打开SavedPanel.js并修改代码,使其看起来像这样:

Ext.define('FlickrFindr.view.SavedPanel', {
    extend: 'Ext.navigation.View',
    xtype: 'savedpanel',
    config: {
        title: 'Saved',
        iconCls: 'favorites',
        items: {
            xtype: 'searchphotolist',
            title: 'My Saved Photos'
        }
    }
});

一旦我们有了这两个视图,我们还需要将它们添加到我们的SavedPhotos.js控制器中。打开app/controller/SavedPhotos.js文件,在config部分添加以下代码:

views: [
 'FlickrFindr.view.SavedPhotoList',
 'FlickrFindr.view.SavedPhotoDetails'
]

现在我们可以开始连接其他的控制器。我们将首先回到我们的SearchPhotos.js控制器来连接Add按钮。

完成 SearchPhotos 中的 Add 按钮

打开controller文件夹中的SearchPhotos.js;让我们给我们的Save按钮添加一个控制。在control部分,在我们的SearchPhotoList控制下方,我们添加按钮的控制如下:

'button[action=savephoto]': {
  tap: 'savePhoto'
 }

接下来,我们需要在我们的上一个函数定义之后添加我们的savePhoto函数:

savePhoto: function(btn) {
 var rec = btn.up('searchphotodetails').getRecord();
 var store = Ext.data.StoreManager.lookup('SavedPhotosStore');
 rec.save({
  callback: function() {
  store.load();
  this.getMain().setActiveItem(1);
  }
 }, this);
}

我们需要两件东西来让这个功能工作:来自我们的详情面板的记录和 Saved Photos 存储,这样我们就可以在记录保存后加载SavedPhotoList

我们通过使用按钮上的up函数查找我们的searchphotodetails面板,然后使用getRecord();获取记录。我们使用StoreManager通过其唯一 IDlookup了商店。

接下来,我们使用了模型的save()函数来使用模型的代理(而不是商店的代理)保存模型。然后我们使用回调函数在模型成功保存后加载商店并切换我们的视图。

提示

你会注意到我们还在save函数的末尾设置了一个选项this。作为save函数的一部分,我们可以设置我们callback函数的作用域,这可能会让你想起书早先的部分。通过将作用域设置为this,当我们在函数内部引用thisthis.getMain())时,我们是在谈论控制器,而不是函数本身。

现在既然你已经设置了我们的函数,你应该能够重新加载应用程序并保存照片。我们还需要能够访问已保存照片的详细信息,并删除我们不再想要的照片。

更新SavedPhotos控制器

在我们SavedPhotos控制器内部,我们需要添加一些引用和控件,就像我们在SearchPhotos控制器中做的那样。

打开SavedPhotos.js文件,像这样修改refscontrols部分:

refs: {
 SavedPhotoList:'savedphotolist',
 SavedPanel: 'savedpanel'
},
control: {
 SavedPhotoList: {
  itemtap: 'showSavedPhotoDetails',
  itemswipe: 'pageChange'
 },
 'button[action=removephoto]': {
   tap: 'removePhoto'
 }
}

这给了我们列表和面板的refs(我们不需要main的),以及三个将几乎以与我们的SearchPhotos函数相同方式工作的controls

让我们从showSavedPhotoDetails函数开始,在config部分后添加以下内容:

showSavedPhotoDetails: function(list, index, target, record) {
 var panel = Ext.create('FlickrFindr.view.SavedPhotoDetails', {
  title: record.get('title'),
  record: record
 });
 this.getSavedPanel().push(panel);
}

与我们的上一个showSearchPhotosDetails函数非常相似,这个创建了我们SavedPhotoDetails视图的新副本,分配了一个标题和记录,然后将其推到我们的SavedPanel中。

接下来,我们有我们的pageChange函数。你可以从我们的SearchPhotos.js控制器中复制并粘贴这个函数:

pageChange: function(list, index, target, record, e, eOpts) {
        console.log(e);
        var store = this.getSavedPhotoList().getStore();
        if(e.direction == 'right') {
            if(store.currentPage != 1) {
                store.previousPage();
            }
        } else {
            var total = store.getTotalCount();
            var page = store.getPageSize();
            if(store.currentPage <= Math.floor(total/page)) {
                store.nextPage();
            }
        }
    }

我们之前代码片段中需要更改的只有一行,即第三行,我们在那里获取我们的SavedPhotoListstore。除此之外,这个函数实现了与我们的另一个控制器相同的结果;它检测用户的滑动,并在结果之间前后翻页。

我们需要的最后一片是我们的removePhoto函数。这个会有些不同。当我们从我们的已保存照片列表中删除一张照片时,我们需要从我们的SavedPanel导航视图中pop掉详细信息视图,而不是更改视图:

removePhoto: function(btn) {
 var rec = btn.up('savedphotodetails').getRecord();
 var store = Ext.data.StoreManager.lookup('SavedPhotosStore');
 rec.erase({
  callback: function() {
   store.load();
   this.getSavedPanel().pop();
  }
 }, this);
}

对于这个函数,我们使用了erase()方法从我们的本地存储中删除记录。然后像以前一样加载商店,并使用pop()函数删除我们的详细信息视图。当这个视图被删除时,我们的SavedPanel导航视图将自动切换回SavedPhotosList

打磨你的应用程序

现在既然我们已经完成了我们的应用程序,我们将会想要添加一些最后的润色,真正让我们的应用程序焕发光彩,并给完成的产品增加一层专业性。好消息是,所有这些都可以很容易地快速实施。

添加应用程序图标和启动屏幕

如我们在第一章中提到的Let's Begin with Sencha Touch,用户可以导航到你的网络应用程序,然后选择将其保存到他们移动设备的桌面。

添加应用程序图标和启动屏幕

在我们当前的应用程序中,当某人以此方式安装它时,默认的 Sencha 图标会被显示。然而,你可以修改在主屏幕上显示的默认图标。

references文件夹包含你的应用程序将用于各种设备的所有图标。它还包括一个启动文件夹,其中包含应用程序在各种设备上使用的启动屏幕图像。

这些图片都可以编辑以自定义应用程序的外观。只需确保你将它们保存为相同的格式、大小和名称。

改进应用程序

我们的应用程序仍有很大的改进空间,但我们将这一点留给读者作为额外的加分项。你可能想要尝试的一些事情如下:

  • 允许用户在保存时重新命名照片

  • 添加一个专家搜索功能,你可以手动设置你的位置或扩大搜索半径

  • 更改主题并使 XTemplates 更具吸引力

  • 添加保存位置以及照片的能力

尝试使用我们在本章中介绍的 MVC 组织技巧来扩展应用程序并提高你的技能。

总结

在本章中,我们向你介绍了 MVC 设计模式。我们谈论了建立一个更健壮的文件夹结构,并创建了你的主要应用程序文件。我们以 Flickr API 的概览开始了我们的应用程序,并探讨了如何注册我们的各种模型、视图和控制器组件。然后我们为SearchPhotosSavedPhotos组件设置了我们的组件。我们最后给出了几点关于为你的应用程序添加收尾工作的建议,并谈论了几件你可能想要添加到应用程序中的额外内容。

在下一章中,我们将介绍一些高级主题,例如构建你自己的 API、使用清单系统创建离线应用程序,以及使用 PhoneGap 等程序编译应用程序。

第九章:高级主题

在本章中,我们将探讨几个高级主题,旨在为构建 Sencha Touch 应用程序指出正确的方向,例如以下内容:

  • 与你的服务器通信

  • 离线工作

  • 编译你的应用程序

  • 进入市场

与你的服务器通信

到目前为止,我们一直使用本地存储作为在运行我们程序的设备上直接创建数据库的方法。虽然这非常有用,但在某些方面它也可能是有局限的,如下所述:

  • 如果设备上存储了任何数据,你无法从另一台设备查看它

  • 如果设备被盗窃/损坏/丢失或无法使用,你也会丢失其数据

  • 分享选项限于传输数据的副本

  • 数据协同编辑不可用

每一个这些问题都可以通过将数据存储在外部数据库中得到解决,比如 MySQL、PostgreSQL 或 Oracle。这些数据库可以运行在与我们的应用程序相同的服务器上,并处理来自不同设备的多个连接。由于所有设备都联系同一个中央数据库,跨设备共享数据变得更容易实现。

不幸的是,Sencha Touch 框架不能直接与这些类型的外部数据库进行通信。为了使用 Sencha Touch 应用程序与外部数据库,我们需要使用第三方 API 或创建我们自己的。积极的一面是,这意味着我们可以使用任何我们想要的数据库来存储我们的数据。然而,这也意味着我们将需要编写一些代码以便将 Sencha 与外部数据库连接起来。

使用你自己的 API

在之前的章节中,我们已经学习了如何使用外部 API 来处理来自如 Flickr 和 Google 服务等数据。外部 API 使得获取存储在这些各种服务数据库中的数据成为可能,但当你需要将数据传入和传出你自己的数据库服务器时该怎么办呢?

结果表明,使用 Sencha Touch 最好的方式是创建你自己的 API。为了做到这一点,我们需要退一步,更多关于 API 是什么以及它做什么的话题进行讨论。

在最为基础的层面上,API 充当了应用程序的存储部分与界面部分之间的翻译者。前端向 API 请求数据(比如说,联系人的列表),API 从数据库中提取信息。API 然后将那些数据翻译成 JSON 或 XML,并将其发送回前端进行展示。

虽然这对于一个应用程序来说可能看起来是一种不必要的分离,但实际上它有许多好处。首先,它允许后端和前端用不同的编程语言编写。这对我们来说很重要,因为尽管 JavaScript 是一种创建界面的好语言,但它并不是与更强大的数据库系统(如 MySQL、PostgreSQL、Microsoft SQL Server 和 Oracle)通信的好工具。API 的代码可以用一种对数据库友好的语言,如 PHP、RUBY 或 PERL 来创建。

注意

我们将使用 PHP 作为我们的示例,但 API 语言的选择完全取决于你。当我们覆盖 PHP 方面的事情时,我们也会非常一般化。我们的目标是传达概念,而不是提供特定的 PHP 代码。

第二个好处是,多个应用程序可以使用 API 来访问数据。这使得在用户之间共享数据变得容易得多,也使得向完全不同的应用程序提供相同的数据集成为可能(正如 Flickr API 所做的那样)。我们甚至不需要关心前端是用哪种编程语言编写的,因为 API 处理翻译。

让我们重新审视一下我们的FlickrFindr存储器,探索这是如何工作的:

Ext.define('FlickrFindr.store.SearchPhotosStore', {
    extend: 'Ext.data.Store',
    requires: 'FlickrFindr.model.Photo',
    config: {
        model: 'FlickrFindr.model.Photo',
        autoLoad: false,
        pageSize: 25,
        proxy: {
            type: 'jsonp',
            url: 'http://api.flickr.com/services/rest/',
            callbackKey: 'jsoncallback',
            limitParam: 'per_page',
            reader: {
                type: 'json',
                root: 'photos.photo',
                totalProperty: 'photos.total'
            }
        }
    }
});

我们将这个存储器指向一个特定的 URL(api.flickr.com/services/rest/),现在,在我们控制器的监听部分,我们还发送我们的位置、半径和准确性设置:

listeners: {
   locationupdate: function(geo) {
      // Use our coordinates.
      easyparams = {
        "min_upload_date": Ext.Date.format(dt, "Y-m-d H:i:s"),
        "lat": geo.getLatitude(),
        "lon": geo.getLongitude(),
        "accuracy": 16,
        "radius": 10,
        "radius_units": "km",
        "method": "flickr.photos.search",
        "api_key": me.getApplication().api_key,
        "format": "json"
      };
      var store = me.getSearchPhotoList().getStore();
        store.getProxy().setExtraParams(easyparams);
        store.load();
      },
}

每个这些参数都作为一组POST变量发送到 Flickr API URL。Flickr 然后使用我们在前面的代码中提供的变量执行flickr.photos.search功能。API 然后将这些结果组装成 JSON 格式并传递给我们。这被称为 REST 请求。

REST

REST代表代表性状态转移,这是一个过于复杂的说法,意思是我们要使用已经内置到 HTTP 中的标准方法来进行通信。这些方法允许 HTTP 通过POSTPUTDELETEGET传输数据。

Sencha Touch Version 2.1 代理Ext.data.proxy.Rest是一个严格的 REST 实现,使用这四个单独的方法来处理 CRUD 功能:

  • POST处理新记录的创建

  • GET处理记录的读取

  • PUT处理现有记录的更新

  • DELETE处理记录的删除

提示

Ext.data.proxy.Ajax代理与Ext.data.proxy.Rest代理类似,但只使用POSTGET。如果你正在使用的 API 需要更严格的 REST 符合性,请确保使用 REST 代理。

如果你曾经在 Web 上处理过表单,你可能对GETPOST很熟悉。两者都是向网页传递额外变量的方法。例如,GET使用 URL 传递其变量,如下所示:

http://www.my-application.com/users.php?userID=5&access=admin

这会将userID=5access=admin发送到网页进行处理。

POSTPUTDELETE 变量作为 HTTP 请求的一部分发送,并不出现在 URL 中。然而,它们传输的数据与键值对相同。

设计你的 API

在你开始编码之前,先考虑一下你希望你的 API 如何工作是一个好主意。API 可能会很快变得复杂,花些时间弄清楚你的 API 将会和不会做什么可以帮助你在构建应用程序时节省大量时间。

不同的程序员对于如何构建 API 有不同的哲学观点,所以我们这里提出的方法只是可能的方法之一。

Sencha Touch 的模型和代理带有几个方法,特别是 CRUD 函数(创建、读取、更新和删除),它们与 API 调用非常相符。这使它们成为一个很好的起点。首先,列出你认为需要的每个模型的方法。对于每个模型,你需要创建、读取、更新和删除函数。

然后,你应该仔细查看模型,看看哪些可能需要额外的 API 方法。一个好的例子是 user 模型。你肯定需要基本的 CRUD 方法,但也许还需要一个认证方法来让用户登录,以及可能一个检查权限的额外方法。

随着你的进展,你可能会发现你需要为特定的模型添加额外的 API 方法,但标准的 CRUD 函数应该会在你设计你的 API 时给你一个很好的起点。

创建模型和 store

在这个例子中,我们将使用 Bookmarks 模型的一个变种和我们上一章的 FlickrFindr 应用程序中的 store。

由于我们的 Bookmarks 组件现在将从数据库中获取,模型中需要一些额外的选项。我们不再像以前那样使用 SearchResults 模型,而是使用一个新的模型,例如以下的一个:

Ext.define('FlickrFindr.model.Bookmark', {
  extend: 'Ext.data.Model',
  fields: [
    {
    name: 'id',
    type: 'int'
  },
    {
    name: 'owner',
    type: 'string'
  },
    {
    name: 'secret',
    type: 'string'
  },
    {
    name: 'server',
    type: 'int'
  },
    {
    name: 'farm',
    type: 'int'
  },
    {
    name: 'title',
    type: 'string'
  }
  ],
  proxy: {
        type: 'rest',
        url : '/api/bookmarks.php'
    }
});

在这里,我们在我们的模型中添加了一个 rest 代理和 url 值。这将允许我们直接从模型中保存、编辑和删除。

例如,要保存一个新的书签,我们可以在 Sencha Touch 中调用以下代码:

var bookmark = Ext.create('FlickrFindr.model.Bookmark', {id: 6162315674, owner: 15638, secret:'d94d1629f4', server:6161, farm:7, title:'Night Sky'});
bookmark.save();

这段代码将执行一个 HTTP POST 请求到 /api/bookmarks.php,使用我们的所有 bookmark 变量作为键值对。

同样,我们可以取一个现有的书签,修改一些它的信息,然后调用 bookmark.save()。如果我们这样做在一个现有的书签上,模型会将变量作为 PUT 请求的一部分发送到 /api/bookmarks.php

正如你所预期的,调用 bookmark.destroy() 会将我们的变量作为 DELETE 请求的一部分发送到 /api/bookmarks.php

我们还需要以类似的方式修改我们的已保存照片 store:

Ext.define('FlickrFindr.store.SavedPhotosStore', {
  extend: 'Ext.data.Store',
  requires: 'FlickrFindr.model.Bookmark',
  config: {
    model: 'FlickrFindr.model.Bookmark',
    storeID: 'BookmarkStore',
    autoload: true,
    proxy: {
      type: 'rest',
      url: '/api/bookmarks.php',
      reader: {
        type: 'json',
        root: 'children'
      }
    }
  }
});

与本章前面讨论的 store 相比,这个 store 的主要区别是代理配置。我们使用相同的 /api/bookmarks.php 文件来处理我们的请求。在这种情况下,store 在联系 /api/bookmarks.php 文件时将使用 GET 请求方法。

我们的reader有一个名为children的根属性。这表示接收到的数据应该如下所示:

{
"total": 2,
  "children":[
    {
        "id":"6162315674",
        "owner":"Noel",
        "secret":"d94d1629f4",
        "server":"6161",
        "farm":7,
        "title":"Night Sky"
    },
    {
        "id":"6162337597",
        "owner":"Noel",
        "secret":"f496834m347",
        "server":"6161",
        "farm":7,
        "title":"Ring of Fire"
    }
  ]
}

我们的存储将开始在children数组内寻找记录,并使用默认变量total来获取记录总数。

发起请求

一旦我们的模型和存储理解了如何发起这些请求,我们的基于 PHP 的 API 文件就必须决定如何处理它们。这意味着我们必须将我们的bookmarks.php文件设置为处理这些请求。在很高的层次上,这意味着执行类似于以下代码的操作:

<?PHP
$action = $_SERVER['REQUEST_METHOD'];

if($action == 'GET') {
  // read - return a list of bookmarks as JSON
} else if($action == 'POST') {
  // add a new user
} else if($action == 'PUT') {
  // save the edit of an existing user
} else if($action == 'DELETE') {
  // delete an existing user
}
?>

<?PHP?>标签 simply denote the beginning and end of PHP code.

$action = $_SERVER['REQUEST_METHOD'];行获取了request方法,然后根据该结果决定我们的代码决策(addeditreaddelete)。

注意

我们不想深入到代码特定的例子中,因为代码将根据您希望用于 API 的语言和数据库而有很大的不同。您需要查阅特定于 API 编程语言的指南,以学习如何适当地与您选择的数据库进行交互。

在执行addeditdelete功能时要注意的一点是,传递给这些功能的数据将作为记录数组的形式到来,例如以下内容:

{"records":[{"id":6162315674,"owner":"46992422@N08","secret":"d94d1629f4","server":6161,"farm":7,"title":"foo"}]}

这表明对于任何addeditdelete选项,您将需要遍历每个记录的值并对每个记录进行数据库更改。虽然您可以通过records[0].id直接访问记录,但遍历值可以让您利用数据存储一次性同步多个更改的能力。

当您的 API 返回操作结果时,Sencha Touch 期望您返回最初发送到 API 中的完整记录(或记录)。例如,如果您创建了一个新记录,API 在成功保存后应该将该记录作为结果的一部分返回。如果您修改了几个记录并保存它们,如果它们正确地被保存了,API 应该返回所有被修改的记录。这是因为您的 API 可能会对记录进行额外的更改,这些更改应该在您的 JavaScript 代码中反映出来。返回完整记录确保了您的 JavaScript 应用程序与 API 所做的任何更改保持最新。

例如,我们可以向存储中添加多个书签,而不是像我们代码中早期那样直接使用模型创建它们。当我们在存储中调用sync()函数时,它将把数据作为书签数组发送到我们的 API:

{"records":[
 {"id":6162315674 
  "owner":"46992422@N08",
  "secret":" your_secret_here ",
  "server":6161,
  "farm":7,
  "title":"foo"},
 {"id":"6162337597",
  "owner":"Noel",
  "secret":"your_secret_here",
  "server":"6161",
  "farm":7,
  "title":"Ring of Fire"}
]}

这样,如果我们允许 API 中循环,我们就不用担心请求来自模型还是存储。从接收角度来看,API 只需要担心请求是POSTadd)、PUTedit)、GETread)还是DELETEdelete)。

然而,有时我们需要直接与 API 通信,也许还需要得到更完整的响应。这时 Ajax 请求就能派上用场。

API 中的 Ajax 请求

在与外部数据库合作时,我们经常需要对其他模型进行数据更改。我们可能还需要接收比当前版本 Sencha Touch 数据存储更复杂的响应。在这些情况下,我们可以使用 Ajax 请求对象直接将数据发送到我们的后端进行处理。

例如:

Ext.Ajax.request({
    url: '/api/bookmarks.php',
    method: 'GET',
    params: {
        id: '6162337597'
    },
    success: function(result, request) {
        var json = Ext.decode(result.responseText);
console.log(json.bookmark);
    },
failure: function(response, opts) {
        console.log('server-side failure with status code ' + response.status);
   } 
});

之前的代码向/api/bookmarks.php发送了一个直接的GET请求,并将id 6162337597值作为请求的一部分。API 然后可以使用这些信息抓取一个特定的书签,并以 JSON 格式将其返回给 Ajax 请求。

成功或失败由返回适当的 HTTP 状态码来表示。如果你返回一个成功的消息,简单地输出 JSON 将返回一个可接受的状态码。要表示失败,你会返回一个 400 或 500 范围内的错误代码;在 PHP 中,它可能如下所示:

<?PHP
header("Status: 400 Bad Request – Invalid Username");
?>

你需要查阅你喜欢的 API 编程语言的文档,了解如何发送 HTTP 响应头。

注意

要查看 HTTP 状态码的列表,请访问restpatterns.org/HTTP_Status_Codes

离线工作

不可避免地,使用你应用程序的人会发现他们没有互联网接入。在传统的网络应用程序中,这通常意味着应用程序无法访问和使用。但是,通过一些周密的计划,你可以使你的移动应用程序能够在离线状态下使用。

同步本地和远程数据

首先需要考虑的是你的数据:用户即使在离线状态下也需要哪些数据?让我们用一个简单的通讯录例子来说明。你可能会有一个用于联系人的模型和一个查询远程通讯录服务器的存储,也许还有一个列表视图来显示联系人:

Ext.define('Contact', {
  extend: 'Ext.data.Model',
  config: {
    fields: [
      {name: 'id', type: 'int'},
      {name: 'firstname', type: 'string'},
      {name: 'lastname', type: 'string'},
      {name: 'email', type: 'string'}
      ]
  }
});

Ext.define('ContactStore', {
  extend: 'Ext.data.Store',
  config: {
    model: 'Contact',
    proxy: {
        type: 'jsonp',
      url: 'http://mycontactserver.com/api',
    },
    autoLoad: true
  }
});

Ext.define('ContactView', {
  extend: 'Ext.dataview.List',
  xtype: 'contactview',
  config: {
    store: 'ContactStore',
    itemTpl: '{firstname} {lastname} – {email}'
  }
});

注意

这是一个非常简单的例子,我们省略了创建index.html文件或把列表添加到视口中,即使这两个行动都是使这个应用程序真正工作的必要条件。

你会注意到我们的应用程序使用了jsonp代理,如果我们只是想从远程服务器加载其数据,这是可以的。如果我们想让我们的应用程序离线工作,我们必须提供一些本地存储。另外,当用户重新上线时,我们想让他们能够从远程服务器检索到更新的联系人信息。

这意味着我们需要两个存储:我们当前的存储,它使用一个jsonp代理,以及一个新的存储,用于在本地存储中保持数据的副本,以便我们在离线时使用。新的存储如下所示:

Ext.define('OfflineContactStore', {
  extend: 'Ext.data.Store',
  config: {
    model: 'Contact',
    proxy: {
        type: 'localstorage',
      id: 'contacts'
   },
    autoLoad: true
  }
});

我们接下来的任务是确保离线商店拥有在线商店的最新数据。我们通过给在线商店的 load 事件添加一个监听器来实现。每次在线商店加载新数据时,我们将更新离线商店。离线商店作为在线数据的缓存以如下方式工作:

Ext.define('ContactStore', {
    extend: 'Ext.data.Store',
    config: {
        model: 'Contact',
        proxy: {
            type: 'jsonp',
            url: 'http://mycontactserver.com/api',
            reader: {
                type: 'json'
            }
        },
        autoLoad: true,
        listeners: {
            load: function() {
                var offlineContacts = Ext.StoreMgr.get('OfflineContactStore');

                offlineContacts.each(function(record) {
                    offlineContacts.remove(record);
                });
                offlineContacts.sync();

                this.each(function(record) {
                    offlineContacts.add(record.data);
                });

                offlineContacts.sync();

            }
        }
    }
});

load 事件在在线商店成功加载新数据时被调用。在我们的处理程序中,我们首先检索离线商店并清空它(否则,每次加载在线商店时我们都会复制数据)。然后,我们使用在线商店的 .each() 函数遍历每一条记录,将该记录的数据添加到离线商店。

提示

.each() 函数

.each() 函数是 store 提供的,它允许你为 store 中的每一条记录调用一个函数。这个函数将单独的记录作为一个参数。这允许你逐条而不是单独查询所有记录执行操作。

现在,每当在线商店更新时,离线商店也会更新。更重要的是,当在线商店无法更新时,离线商店中仍然会有数据。由于即使在线商店没有数据,离线商店总是有数据可以显示,因此我们应该将离线商店作为列表的商店使用,这样我们总是向用户显示一些内容。所以,我们将 ContactView 更改为如下内容:

Ext.define('ContactView', {
  extend: 'Ext.dataview.List',
  config: {
    store: 'OfflineContactStore',
    tpl: '{firstname} {lastname} – {email}'
  }
});

我们的在线商店在应用启动时仍然会自动加载,尽管它不再绑定到我们的列表了。如果用户在线,两个商店中的所有数据都将更新。

当然,还有其他实现同一目标的方法。你可以使用 Ext.List 组件的 bindStore 函数在两个商店之间切换,或者使用在线商店的 jsonp 代理 exception 事件来发现你何时离线。或者,你可以查看 window.navigator.onLine 变量的值来确定你的在线状态并相应地设置你的商店。我们将在本章后面讨论 jsonp 代理的 exception 事件和 window.navigator.onLine 变量。

清单

既然我们已经确保了数据可以离线使用,我们还需要确保应用的其他部分也可以离线使用。这包括我们所有的 JavaScript 代码、HTML、样式和图片。如果用户已经离线,除非他们有本地副本可以从中工作,否则他们将无法加载我们的应用。Application Cache 就在这时发挥作用。

HTML5 为指示 web 浏览器将应用的哪些部分存储为离线使用提供了一个机制。这不是 Sencha Touch 提供的功能,但无论如何你都应该熟悉这个概念。

注意

如果你正在使用 Sencha Cmd 来管理你的应用开发过程,cache.manifest 文件将会自动为你创建。

manifest 文件是一种你指定要缓存的文件的方式。让我们为我们的简单地址簿应用程序创建一个。打开一个空文本文件并添加以下代码:

CACHE MANIFEST
# Simple Address Book v1.0

CACHE:
index.html
app/app.js
css/my-app.css
lib/resources/css/sencha-touch.css
lib/sencha-touch.js

# Everything else requires us to be online.
NETWORK:
*

然后,将文件保存为 cache.manifest。所有以井号(#)开头的行都是注释,将被忽略。

在前面的代码片段中,CACHE: 术语之后的第一个部分是移动设备应该为离线使用保存的文件列表。如果你有任何图片或其他文件,你也应该在这里列出。

NETWORK: 部分列出了所有只能在线访问的文件。星号(*)表示 CACHE: 部分未列出的所有内容都应仅在线可用。

注意

大多数浏览器将离线存储限制在 5 MB。这包括你在 manifest 中列出的文件以及任何在本地存储存储中的数据。因此,如果你有一个特别大的应用程序,你可能需要选择性地允许你的应用程序离线执行。

为了让浏览器了解你的 manifest 文件,你必须在你 的 index.html 文件中添加对它的引用。然而,这并不是我们链接 CSS 或 JavaScript 文件的方式。相反,我们给 html 标签的开启标签添加一个属性,如下所示:

<html manifest="cache.manifest">

现在,当你启动你的浏览器时,你应该在开发者控制台的应用缓存中看到你的文件列表(点击资源标签,然后点击应用缓存),如下图所示:

Manifests

设置你的网页服务器

最初,你可能会发现你的 manifest 文件没有正常工作。通常,这意味着你的网页服务器没有配置好以按照移动浏览器期望的方式来提供 manifest 文件。

网页服务器使用MIME 类型来告诉浏览器如何处理某些文件。MIME 类型可能相当复杂,但对于 manifest 文件,你只需要在你的服务器中添加 MIME 类型。你应该查阅你的网页服务器的文档以获取指导,但我们将以 Apache 网页服务器为例。

对于 Apache,你应该在你的 httpd.conf 文件中添加以下 MIME 类型:

AddType text/cache-manifest .manifest

然后,重启你的网页服务器以使更改生效。

对于 IIS,你将需要使用管理界面来添加 MIME 类型。

注意

查看以下链接以设置你的网页服务器:

关于设置 Apache 的更多信息:httpd.apache.org/docs/current/mod/mod_mime.html

关于设置 IIS 的更多信息:technet.microsoft.com/en-us/library/cc753281(WS.10).aspx

更新你的缓存应用程序

一旦你的应用程序被本地缓存,移动设备就不再向你的服务器查询以下载你的应用程序文件。这意味着,当你发布你的应用程序的更新或新版本时,那些已经缓存了你应用程序的用户将不会收到更新。

强制用户下载代码新版本的方法的唯一途径是更新本身 manifest 文件。这就是为什么我们在前面的代码片段顶部添加了以下几行:

CACHE MANIFEST
# Simple Address Book v1.0

只需更新版本号并保存文件如下:

CACHE MANIFEST
# Simple Address Book v1.1

这将更改 manifest 文件,强制所有缓存副本重新下载 manifest 中CACHE:部分的所有文件。

注意

如果你想要了解更多关于应用缓存和 manifest 文件的信息,请查看在www.html5rocks.com/en/tutorials/appcache/beginner/应用缓存初学者指南

界面考虑

让用户知道他们在线工作也很重要。大多数设备在状态栏中有一个在线图标,但即便如此,用户下线时也不总是显而易见的。你可能希望在将应用程序置于离线模式时让他们知道。

警告你的用户

在我们的通讯录示例中,我们有一个在线商店,它更新了一个离线商店的数据。离线商店持有用户在Ext.List类中看到的数据。然而,我们从不明确地告诉用户他们何时下线。在我们第一个例子中,我们不自己跟踪在线或离线状态,因为应用程序在任何模式下都能工作。

如果我们想要告诉用户应用程序何时下线,最可靠的方法是等待在线商店的请求超时。在代理中,让我们添加一个timeout组件和一个在timeout发生时调用的函数:

proxy: {
        type: 'jsonp',
        url: 'http://mycontactserver.com/api',

 timeout: 2000,
 listeners: {
 exception:function (this, response, operation, eOpts)  {
 if(operation.error == 'timeout')  {
Ext.Msg.alert('Offline Mode', 'Network unreachable, we have entered offline mode.');
 }

 }
 }
}

exception函数只有在超时发生后才会被调用。Sencha Touch 中的超时以毫秒为单位列出,因此在这个例子中,2000意味着两秒钟。如果商店在两秒钟内没有从服务器获得响应,用户将看到一个警告,通知他们应用程序已经下线。

这里是一个添加其他离线逻辑的好地方:

  • 如果你在你的商店中设置了轮询,以便它每隔一段时间自动刷新一次,你可能希望关闭它。

  • 如果有特殊的离线 UI 元素,你可以在這裡启用它们。

  • 如果你有很多离线逻辑,你可能会想要把代码放在一个单独的函数里,这样你就不必在代理配置中寻找它了。

如果你在使用前一章讨论的 MVC 结构,控制器将是这种逻辑的好地方。

提示

如果你通过 Sencha Cmd 或其他方法编译你的应用程序,而不是作为网络应用程序运行,你可能可以访问由Ext.device.Connection对象引发的onlinechange事件。查看 API 文档以了解关于使用Ext.device.Connection对象更多信息。

更新你的 UI

让用户视觉上知道他们处于离线模式的一种方法是改变应用的颜色或样式。虽然为离线模式设置一个完全不同的主题可能过于夸张,但有一种方便的方法可以指定一个离线样式表。

让我们创建一个名为my-app-offline.css的文件,并将其保存在我们的css文件夹中。在文件中,放置以下代码:

.x-list .x-list-item {
  color: #f00;
}

这将把contact-list文本变成红色。现在,我们需要在离线时加载它。

应用缓存清单文件(cache.manifest)可以有一个名为FALLBACK:的部分,用于当某个特定文件无法访问时替代另一个文件。让我们在cache.manifest文件的底部添加以下内容:

FALLBACK:
css/my-app.css css/my-app-offline.css

你还需要将CACHE:部分中的css/my-app.css行更改为引用css/my-app-offline.css,如下所示:

CACHE MANIFEST
# Simple Address Book v1.2

CACHE:
index.html
app/app.js
css/my-app-offline.css
lib/resources/css/sencha-touch.css
lib/sencha-touch.js

# Everything else requires us to be online.
NETWORK:
*

FALLBACK:
css/my-app.css css/my-app-offline.css

index.html文件中,你应该保留css/my-app.cssstyle标签中,因为当我们在线时,这个文件会被加载。然而,当我们在离线时,清单告诉我们的移动浏览器隐式地使用css/my-app-offline.css

更新你的 UI

正如我们在之前的截图中看到的,现在,当你的应用处于离线状态时,它会自动使用my-app-offline.css而不是my-app.css。你也可以用这个方法来提供一个离线版本的图片,甚至是 JavaScript 文件,如果你想完全隔离在线和离线功能的话。需要注意的是,这个方法如果有人在使用你的应用时在线然后离线是行不通的,比如说,他们穿过一个隧道失去了信号。在这种情况下,你希望使用事件监听方法将用户切换到离线模式。

检测离线模式的其他方法

如本章前面所提到的,有两种检测离线模式的备用方法:navigator.onLineonline/offline浏览器事件。

变量navigator.onLine如果浏览器在线则为true,如果浏览器不在线则为false。在前面章节中讨论的exception函数中,我们可以添加以下代码来检查它并相应地更改我们的消息:

exception:function () {
  if (navigator.onLine) {
 Ext.Msg.alert('Network Error', 'We have an Internet connection, but there is a problem communicating with the server.');
  } else {
    Ext.Msg.alert('Offline Mode', 'No Internet Connection, we have entered offline mode.');
  }
}

另外,我们可以为浏览器的onlineoffline事件设置监听器,如下所示:

window.addEventListener("offline", function(e) {
alert("Application is offline.");
});
window.addEventListener("online", function(e) {
alert("Application is online.");
});

你会注意到我们这里没有使用 Sencha Touch 的事件管理概念。这是因为 Sencha Touch 没有为onlineoffline事件提供自定义事件,所以我们不得不使用浏览器的事件监听函数。

注意

并非所有桌面浏览器都支持navigator.onLineonline/offline事件,所以如果你也让桌面用户可以使用你的应用,你应该使用超时异常和清单缓存技术。

进入市场

Sencha Touch 应用程序为开发者提供了一种使用现有网络技术触达广大用户的方法。用户可以通过网络访问应用程序,甚至可以将它们保存到设备上以供离线使用。尽管这种灵活性非常有价值,但你也许还希望通过苹果和 Android 上可用的各种应用程序商店分发你的应用程序。

在本节中,我们将看看一些可用的选项和发布编译应用程序可能遇到的潜在障碍。

编译你的应用程序

编译后的应用程序是指在所述设备上本地运行的应用程序。对于苹果的 iOS 产品,这意味着 Objective C,对于谷歌的 Android 操作系统,这意味着 Java。iOS 和 Android 都使用自己的软件开发工具包SDK)来创建这些本地应用程序。

一个 SDK 在功能上类似于 Sencha Touch 的框架,但它更加复杂且与特定平台(iOS 或 Android)紧密相关。由于本地应用程序是唯一可以在 Android 和 iOS 的各种应用程序商店中销售的类型,我们需要一种将我们的 Sencha Touch JavaScript 翻译成 SDK 可以使用的 JavaScript 的方法。幸运的是,Sencha Touch 开发者有几个选项可以将他们的基于 JavaScript 的应用程序翻译成这两种语言并创建编译后的应用程序。最受欢迎的两个翻译程序是 Sencha Cmd 和 PhoneGap。

Sencha Cmd 和 PhoneGap 都使用专门的命令行工具,允许你将现有代码放入 iOS 或 Android 的 SDK 中。这两个工具都广泛使用 Xcode 和 Android SDK 库将你的代码翻译成编译后的应用程序。我们在注册开发者账号部分看看如何获取这些 SDK。

除了将你的 Sencha Touch 应用程序翻译成本地应用程序之外,Sencha Cmd 和 PhoneGap 还允许你访问设备的一些本地功能。这些功能包括访问文件系统、摄像头以及设备上的声音和振动选项。

让我们来看看 Sencha Cmd 和 PhoneGap 翻译程序。

Sencha Cmd

如果你一直在用这本书和 Sencha Cmd 一起工作,那么它很可能是编译应用程序的最佳选择。通过编译应用程序,你可以访问 iOS 或 Android 设备上的更多功能。这些功能包括以下几点:

  • 摄像头:此功能允许你使用摄像头拍照或访问之前拍摄的照片。

  • 连接:此功能允许你查看设备是否在线以及正在使用哪种类型的连接。

  • 联系人:此功能允许访问搜索、排序和过滤设备上的联系人。

  • 地理定位:此功能允许访问设备的地理定位 API(这是浏览器地理定位功能的更健壮实现)。

  • 通知:此功能在设备上显示简单通知。这些通知出现在操作系统级别,而不仅仅是应用程序级别。

  • 方向:此功能收集设备的朝向反馈。

  • 推送:此功能向设备发送推送通知(仅限 iOS)。

这些功能通过一个名为Ext.device的对象访问。例如,Ext.device.Camera.capture(...)方法允许您从相机或相册中抓取图片,并将其用于您的应用程序。

有关本地打包的逐步指南,请参阅docs.sencha.com/cmd/3.1.2/#!/guide/native_packaging

PhoneGap

与 Sencha Cmd 类似,PhoneGap 通过一个名为navigator的全局对象提供广泛的本地功能。这个对象允许您使用 JavaScript 调用您的 JavaScript 中的命令,例如以下命令:

navigator.camera.getPicture(...)
navigator.compass.getCurrentHeading(…)

第一个命令在设备上打开相机,并允许您的应用程序拍照。照片作为数据字符串返回给您的应用程序,您可以在 JavaScript 中对其进行操作。

第二个函数返回设备的方向,以度为单位。这在游戏中有时候非常有用,因为游戏可以通过倾斜设备来操作。

PhoneGap 还提供了以下功能:

  • 加速度计:此功能获取设备运动传感器的信息。

  • 相机:此功能使用设备的相机拍照。

  • 捕获:此功能捕获音频和视频。

  • 指南针:此功能确定设备所指的方向。

  • 连接:此功能检查网络状态并获取蜂窝网络信息。

  • 联系人:此功能与内置的联系人数据库一起使用。

  • 设备:此功能收集设备特定信息。

  • 事件:此功能监听设备上的原生事件。

  • 文件:此功能读写本地文件系统。

  • 地理定位:此功能收集更详细的地理位置信息。

  • 媒体:此功能回放音频文件。

  • 通知:此功能创建设备通知。

  • 存储:此功能直接在设备上存储数据。

PhoneGap 还为您提供了在 Blackberry、WebOS 和 Symbian 平台编译应用程序的选项。

注意

以下链接有更多关于 PhoneGap 的资源,请查看:链接

docs.phonegap.com/en/edge/

其他选项

PhoneGap 还推出了一项基于云的服务,用于编译应用程序,名为PhoneGap Build(build.phonegap.com/). 这个独特的服务消除了为每个希望编译的平台下载 SDK 的必要性。文件只需上传到构建服务,系统就会为您指定的平台生成应用程序。

Sencha Architect是 Sencha Touch 和 ExtJS 的图形应用程序构建器。Architect 现在具有直接在应用程序中编译 iOS 和 Android 应用程序的功能。更多信息可以在docs.sencha.com/architect/2/#!/guide/deploy找到。

像这些选项中的任何一个一样,您需要成为您想要编译的平台上的授权开发者。这可能是一个相当漫长的过程,所以让我们看看涉及哪些内容。

注册开发者账户

为了将您的应用程序发布到 Apple Store 或 Google Play,您必须注册它们的相应开发者账户。这两个商店都会向您收取成为开发者的费用,并要求您提供大量的个人信息。它们需要这些信息有几个原因。首先,它们需要知道您是谁,这样您才能收到在它们的商店中销售的应用程序的付款。其次,如果您的应用程序出现问题,它们需要知道如何联系您。最后,如果有人试图用您的应用程序做坏事,它们需要能够找到您。当然,您不会这么做!

注册开发者账户

您还需要下载并安装适合该商店的适当 SDK,以便能够适当地打包您的应用程序。

成为 Apple 开发者

要成为 Apple 开发者,首先您必须前往developer.apple.com/programs/register/

您需要提供您的现有 Apple ID,或者注册一个新的 ID,填写一些详尽的个人资料信息,同意一些法律文件,然后进行电子邮件验证。从那时起,您将能够访问 Apple 开发者中心。对我们这些移动开发者来说,最感兴趣的两个点是iOS 开发者中心iOS 配置门户

iOS 开发者中心是您可以下载 iOS SDK(称为Xcode)、阅读文档、查看示例代码和教学视频以及关于 iOS 开发的一些地方。

iOS 配置门户是您将应用程序添加到 Apple Store 或发布应用程序测试版本的地方。

小贴士

为了使用 Xcode 或将在 Apple Store 上发布您的应用程序,您的计算机必须运行 OS X。Windows 和 Linux 计算机无法运行 Xcode 或发布到 Apple Store。

成为 Android 开发者

注册 Android Market 的过程非常相似。首先,前往market.android.com/publish/signup

在那里,您会被要求填写更多的个人资料信息并支付开发者注册费用。您还应该下载 Android SDK,位于developer.android.com/sdk/index.html,尽管与 Apple 的 SDK 不同,Android 的 SDK 可以在 Windows、OS X 和 Linux 上运行。

安卓开发者仪表板还包含指南、参考资料和教学视频的链接。

总结

在本章中,我们向有抱负的 Sencha Touch 开发者介绍了一些高级主题。我们首先讨论了如何创建自己的 API 以与数据库服务器进行通信。我们介绍了与服务器发送和接收数据的 REST 通信方法,并讨论了构建自己的 API 的一些选项。

注意

更多关于创建 API 的资源如下:

如何创建 API:www.webresourcesdepot.com/how-to-create-an-api-10-tutorials/

创建以 API 为中心的 Web 应用程序:net.tutsplus.com/tutorials/php/creating-an-api-centric-web-application/

然后我们讨论了如何使用清单和应用程序缓存将您的应用程序离线。我们谈论了在应用程序离线时警告用户的最佳实践以及如何使用 Sencha Touch 和设备的 Web 浏览器检测 Internet 连接的可用性。

注意

更多关于如何使应用程序离线的资源如下:

让 Sencha Touch 应用程序离线:

www.sencha.com/learn/taking-sencha-touch-apps-offline/

HTML 清单属性:

www.w3schools.com/tags/att_html_manifest.asp

我们以使用 Sencha Cmd 和 PhoneGap 编译您的应用程序来进入应用程序市场的方式结束了本章。我们还讨论了成为苹果或安卓开发者以便您可以在市场上销售应用程序的过程。

注意

更多关于构建 Sencha Touch 应用程序的资源:

使用 Sencha Cmd 增强 iOS Sencha Touch 应用程序:

docs.sencha.com/cmd/3.1.2/

使用 PhoneGap 构建 Sencha Touch 应用程序:

docs.phonegap.com/en/edge/

posted @ 2024-05-23 14:40  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报