Ext-js-精要-全-
Ext.js 精要(全)
原文:
zh.annas-archive.org/md5/b56e5de812264811a409c0a207343cf4译者:飞龙
前言
Ext JS 5 是一个重量级的 JavaScript 框架,被数百万开发者用于构建丰富和交互式的 Web 应用。其众多的组件和高级数据包使其特别适合企业级软件。该框架鼓励良好的架构,并且具有极高的可定制性。
Ext JS 精华旨在为您提供快速理解 Ext JS 的方法。本书以简洁但全面的方式涵盖了框架最重要的方面,确保您在使用其众多功能时取得成功。
本书围绕一个示例应用编写,充满了关于框架如何工作、应用架构、数据处理以及提供的众多组件的实用见解。
本书涵盖的内容
第一章, 了解 Ext JS,介绍了 Ext JS 框架及其优势和劣势,包括它非常适合的应用类型。然后我们将使用 Sencha Cmd 创建我们的示例项目。
第二章, 掌握框架的构建块,涵盖了 Ext JS 的类系统,我们将学习如何定义、创建和配置类。继承和重写等概念也将被涉及。
第三章, 响应用户和应用交互,帮助我们探索用户如何与我们的应用交互,包括事件如何触发和监听。
第四章, 架构 Ext JS 应用,涵盖了 MVC 和 MVVM 模式以及如何使用它们来架构您的应用。
第五章, 为您的 UI 建模数据结构,详细介绍了如何使用 Ext JS 数据包建模数据结构。然后我们将介绍如何将数据加载和保存到外部数据源。
第六章, 将 UI 组件组合成完美的布局,介绍了 Ext JS 布局系统,并深入解释了主要的布局类型,每个类型都附有示例。
第七章, 构建常见的 UI 组件,介绍了框架提供的主要 UI 组件,包括树、网格、表单和数据视图。组件生命周期和组件查询语法也得到涵盖。
第八章, 使用 SASS 创建独特的视觉效果,展示了如何为您的 Ext JS 应用创建主题,并涵盖了创建自定义主题、使用 SASS 混入和组件 UI 进行定制。
第九章, 可视化应用程序数据,探讨了如何使用 Ext JS 图表包可视化数据,并提供了所有主要图表类型的示例。
第十章, 使用单元和 UI 测试保证代码质量,详细探讨了编写可测试代码的技术,并通过实际操作介绍测试框架 Siesta。
阅读本书所需的条件
为了跟随本书的内容,我们建议您准备好您的 IDE 以及带有一些开发者工具的浏览器——我们推荐 Google Chrome 及其开发者工具。
您还需要一个可用的本地 Web 服务器来运行示例项目。
示例项目需要 Ext JS 5 SDK,该 SDK 可在 Sencha 网站上获取 www.sencha.com/products/extjs。我们还将使用 Sencha Cmd,它可以从 www.sencha.com/products/sencha-cmd 下载。
Siesta 测试工具也被介绍,并可以从 www.bryntum.com/products/siesta 下载。
适合阅读本书的对象
如果你是一位希望开发丰富互联网应用的开发者,那么这本书适合你。本书假设你已有软件开发经验,并能够快速使用这个令人惊叹的框架。
术语
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“ext文件夹包含 Ext JS 框架代码,我们的新应用将使用这些代码。”
代码块设置如下:
Ext.define('BizDash.config.Config', {
}, function(){
console.log('Config class created');
});
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
Ext.define('BizDash.config.Config', {
singleton: true,
}, function() {
console.log('BizDash.config.Config defined');
});
任何命令行输入或输出如下所示:
Sencha app build
新术语和重要词汇以粗体显示。屏幕上出现的单词,例如在菜单或对话框中,在文本中显示如下:“点击下一步按钮将您带到下一屏幕。”
注意
警告或重要注意事项以如下框中显示。
提示
小技巧和窍门如下所示。
读者反馈
读者的反馈总是受欢迎的。请告诉我们您对本书的看法——您喜欢或不喜欢的地方。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中受益的标题。
要向我们发送一般反馈,请简单地发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com下载您购买的所有 Packt 出版物的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误清单部分。
要查看之前提交的错误清单,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在错误清单部分。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过链接版权@packtpub.com 与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
问答
如果您对本书的任何方面有问题,您可以通过链接问题@packtpub.com 与我们联系,我们将尽力解决问题。
第一章. 了解 Ext JS
“应用”一词已经彻底改变了我们获取信息和相互以及与组织互动的方式。设计和开发应用给软件工程师带来了许多挑战,这些挑战可能会阻碍我们快速发布这些应用并保持开发过程的敏捷性。
框架和库的帮助现在比以往任何时候都更好,并且随着 Sencha 的最新发布,Ext JS 5,我们可以为我们的用户提供更丰富、更实时、更吸引人的体验。
Ext JS Essentials旨在涵盖 Ext JS 的所有主要主题。到本书结束时,你将了解:
-
如何使用 Ext JS 构建丰富且响应式的应用程序
-
基于模型-视图-控制器(MVC)和模型-视图-视图模型(MVVM)模式的 Web 应用架构
-
框架的基本要素,包括类、事件和用户交互
-
Ext JS 的核心布局、小部件和组件
-
数据包的主要概念和双向数据绑定
-
使用图表可视化数据集的方法
-
你可以使用来增强你的开发体验的工具
-
如何使用 SASS 自定义界面时的起点
-
你可以做什么来测试你的应用程序
本章将提供一个框架的高级概述,并解释为什么你应该投入时间和精力去学习其技术、技术和工具。
预计你已经具备编程背景,但你将发现你需要掌握的知识,以便充分利用框架。本书中使用的示例将围绕一个单一的应用程序,到本章结束时,你的开发环境将准备就绪,应用程序也将运行!
什么是 Ext JS
Sencha Ext JS 是一个应用开发平台,它允许你使用 Web 技术和标准构建丰富的用户体验。Ext JS 非常适合构建单页应用程序,并提供实现这一目标所需的所有工具。然而,如果你只想使用框架的一小部分,你可以只包含你需要的控件和类。你可以直接在网页上嵌入这些控件,就像嵌入 jQuery 组件一样。
与其他应用开发框架不同,Ext JS 具有一些独特的技巧。首先,它被专门设计为跨平台和跨浏览器工作。与跨浏览器开发相关的许多头痛问题已经为我们解决。只需付出很少的努力,你的应用程序就可以在以下浏览器上运行:
-
Internet Explorer 8 及以上版本
-
Firefox
-
Chrome
-
Safari 6 及以上版本
-
Opera 12 及以上版本
Sencha 也没有忘记 iOS 和 Android。从 Ext JS 5 开始,你将能够让你的应用在 iOS 6+和 Android 4.1+上流畅运行。
其次,Ext JS 鼓励在 Web 应用中使用最佳实践和架构模式。Ext JS 是一个面向对象的框架,具有清晰定义的结构和命名约定。Ext JS 支持 MVC 和 MVVM 架构,但你并不受 Sencha 的限制;自由地覆盖和扩展框架以满足你的需求。
最后,该框架得到了详尽且文笔优美的文档支持,以及一个杰出的用户社区,通常可以在 Sencha 论坛或 StackOverflow 上找到。Sencha 市场或 GitHub 上也有可用的插件社区。资源都在那里,等你去发现。去寻找吧,如果你找不到什么,就提出来。
Ext JS 不是什么
在你继续之前,花几秒钟时间考虑一下 Ext JS 不是什么可能是有益的。如果你想要为移动设备优先设计,它就不适合你。Sencha 最近在框架上取得了重大进展,以提供触摸支持,但它仍然不是最适合手机的。另一方面,Sencha Touch 是一个专门为移动应用设计的框架。
Ext JS 也不太适合用于网站,除非你有特定的用例。例如,如果你在寻找网站上的下拉菜单或标签面板,你可能更愿意查看其他框架,如 jQuery。我们并不是说 Ext JS 不能做这些事情,但该框架有较大的开销,可能对于这些简单的用例来说过于复杂。
由于框架复杂且包含大量组件,有时定制组件和/或应用的外观和感觉以满足你的需求可能会变得繁琐。做好这件事需要时间、耐心和知识,但结果会证明一切。
用例
Ext JS 被全球数千家组织使用,向用户交付 Web 应用。其架构模式和组件,如网格,使其特别适合商业相关应用。
通常,开发者使用 Ext JS 进行以下操作:
-
带实时数据的交易应用
-
企业数据管理应用
-
内部网络应用
-
可视化应用
-
面向消费者的 Web 应用
-
原生封装的桌面应用
-
数据捕获和监控系统
-
仪表板和门户
显然,这个列表并不全面,但它应该能给你一个关于其他人如何在现实世界中使用 Ext JS 的概念。
Ext JS 5 的新特性
Ext JS 5 是应用开发人员和最终用户向前迈出的又一步。其新特性使其成为世界上最先进的多设备 JavaScript 框架之一。以下是该框架主要增强功能的总结。
触摸支持
从一个代码库,你现在可以构建真正的跨平台应用。这次 Ext JS 的发布引入了触摸能力,使你能够在触摸屏设备上交付你的桌面应用。这结束了用户在平板电脑(如 iPad)或触摸屏笔记本电脑上所感受到的挫败感。
Sencha 提供了一个名为 Neptune Touch 的主题,更适合在触摸设备上使用。这是通过增加屏幕上可触摸组件的大小来实现的。
一个 简洁 的 Nept*une Touch 版本为你提供了更多选择,作为你应用程序的起点。
架构改进
进一步的效率和架构改进有助于使框架更加响应,并更好地构建应用程序。这里最显著的改进是新 MVVM 架构模式,它使我们能够使用更少的应用程序逻辑来开发 Ext JS。第三章,对用户和应用交互做出反应,将更深入地探讨 MVVM,并解释如何使用它来制作应用程序。
响应式布局
Ext JS 5 提供了让你的应用程序在桌面和平板电脑上获得最佳观看体验的能力,无论方向如何变化,都使用新的响应式配置系统。
组件增强
Sencha 对框架中的组件进行了多项增强。例如,网格能够直接在网格单元格中添加小部件。这将非常适合数据可视化和灵活的用户体验。图表包已升级,以支持金融图表,并进行了触摸优化。
你需要了解的内容
开始使用 Ext JS 不需要大量的前期知识,因为有大量的示例和资源可以帮助你。然而,具备一般的编程和面向对象编程经验将对你大有裨益。如果你恰好了解 JavaScript(以及 JSON),那就更好了。
本书将涵盖 MVC 和 MVVM 架构模式,但如果你从另一个框架中获得了相关知识,这将使学习更加容易。同样适用于应用主题:我们将演示在 Ext JS 应用中使用 SASS,但不会详细讲解这项技术。
开发环境
Ext JS 的开发环境只需要一个基本的文本编辑器、一个本地网络服务器和一个用于查看输出的网络浏览器。话虽如此,还有一些工具(其中一些是免费提供的),可以使你的体验更加出色。我们建议熟悉你浏览器内置的工具和插件:
-
Chrome 中的开发者工具
-
Firefox 中的 Firebug
-
Internet Explorer 中的开发者工具
-
Opera 中的 Dragonfly
-
Safari 中的开发者工具
最后,值得注意的是,近年来,对 集成开发环境(IDEs)的 JavaScript 支持有了很大的提升。Ext JS 与 JetBrains IntelliJ Idea(或如果你需要更基础的选择,WebStorm),Eclipse,以及 Spket 等其他环境配合得特别出色。
项目介绍
为了使我们在 Ext JS 框架中的旅程尽可能具有信息性和相关性,我们将从头到尾开发一个真实世界的应用程序。我们将涵盖应用程序构建过程的每个步骤,并将我们遇到的每个新概念、小部件和类都纳入这个应用程序中,以传授可以直接使用的实用知识。
我们的应用程序将是一个业务仪表板应用程序,将以多种不同的格式展示各种信息;它将允许用户创建和操作数据结构,并创建定制的界面和感觉。
到本书结束时,我们的应用程序将看起来像下面的截图,并将包括以下功能:
-
MVVM 架构
-
以各种图表显示的数据
-
交互式数据网格
-
定制数据视图
-
双向数据绑定表单

使用 Sencha Cmd 创建我们的应用程序
Sencha Cmd 是一个命令行工具,它自动化了与 Ext JS 和 Sencha Touch 应用程序创建、开发和部署相关的各种任务。它具有大量功能,其中许多将在第三章(Chapter 3. Reacting to User and Application Interactions)中详细讨论,Reacting to User and Application Interactions。在本节中,我们将讨论如何安装 Sencha Cmd,使用它来生成我们的业务仪表板应用程序,并为其生产部署做好准备。
安装 Sencha Cmd
Sencha Cmd 是一个跨平台工具,并为每个主要平台提供了不同的变体,但它依赖于必须首先安装的一些依赖项。这些是:
-
Java 运行时环境 v1.7
-
Ruby
-
这在 OS X 上是预安装的。
-
对于 Windows,可以从
rubyinstaller.org/downloads/获取。 -
对于 Ubuntu,使用
sudo apt-get install ruby2.0.0下载。
-
安装好这些后,前往 Sencha 网站(www.sencha.com/products/sencha-cmd/download)下载与你的操作系统相关的包。遵循安装程序中的说明,你应该可以开始使用了。
为了验证安装是否成功,打开一个新的终端或命令提示符窗口,并运行命令 sencha。你应该会看到 Sencha Cmd 帮助文本出现,列出可用的命令。
下载框架
在我们创建应用程序之前,我们必须首先从 Sencha 网站下载 Ext JS 框架(www.sencha.com/products/extjs/#try)。将此存档提取到合适的位置。我们现在可以创建我们的应用程序了。
应用程序、包和工作区
当涉及到构建你的应用程序时,有三个主要实体需要理解:应用程序、包和工作区。
应用程序 是一个完整的产品,它将所有功能性和特性汇集在一起。每个应用程序都有自己的 index.html 页面,并且通常独立存在。
包 是一个自包含的代码片段,旨在在应用程序之间共享,它可以是工作区本地的,也可以通过 Sencha 包管理器和远程仓库进行分发。一个包的例子可能是一个自定义 UI 组件。
最后,工作区 是一个特殊的文件夹,它将多个应用程序和包分组在一起,允许它们共享通用代码和框架实例。
生成我们的工作区
我们将使用 Sencha Cmd 的 generate 命令来创建我们的基本应用程序结构,但我们将首先创建一个工作区。
首先,我们打开一个终端/命令提示符窗口,导航到我们之前提取的 Ext JS 框架文件夹。然后我们运行以下命令:
sencha generate workspace /your/workspace/path
这告诉 Sencha Cmd 在指定的文件夹中生成一个新的工作区,如果它还不存在,则会创建。工作区的内容可以在下面的截图中看到:

.sencha 文件夹是一个隐藏文件夹,其中包含 Sencha Cmd 所使用的配置文件。你只有在自定义构建过程或对应用程序进行大量自定义时才需要深入这个文件夹。
ext 文件夹包含 Ext JS 框架代码,我们的新应用程序将使用这些代码。
最后,空的 packages 文件夹将成为我们选择包含的任何包的家园。这可能包括用户扩展、主题,或者仅仅是应用程序之间共享的通用代码。
生成我们的应用程序
现在我们已经为我们的应用程序创建了一个可以居住的工作区,我们可以使用 generate app 命令来创建它,并将应用程序的名称和工作区中应用程序文件夹的路径传递给它:
sencha generate app BizDash /your/workspace/folder/BizDash
此命令将在名为 BizDash 的文件夹内创建一个基本的应用程序和文件夹结构,如下面的截图所示。我们将在下一节中解释这个文件夹内所有文件夹和文件的内容。

这种结构将构成我们业务仪表板应用程序的基础。
你可以用浏览器导航到 index.html 文件,看看它的样子。你应该看到一个带有标签栏和按钮的简单应用程序。
准备生产
显然,我们离应用程序的生产还有很长的路要走,但从一开始就正确设置这些事情非常重要,这样我们就可以在需要时快速部署,并且使这个过程持续运行。
当我们现在查看我们的应用程序并监控开发者工具中的文件请求时,我们会看到大量的活动——总计超过 350 个请求和 6 MB 的数据传输!这对最终用户来说远非理想,因此,我们希望将这些请求合并成一个单一的压缩文件。
我们通过使用 Sencha Cmd 构建应用来完成这项工作。这个过程将结合应用使用的所有类文件(即我们之前提到的 350 个请求)以及其他构建任务。有两种类型的构建:测试和生产。测试构建将结合代码但不会压缩,而生产构建将完全压缩。
我们使用以下命令为生产构建应用:
sencha app build production
这将我们的构建应用输出到 build/production/BizDash 文件夹。如果你在浏览器中加载这个页面,你会看到请求的数量已经减少到 6 个,总大小仅为 1.3 MB。好多了!
除了连接和压缩我们的 JavaScript 之外,这个过程还将编译我们的 SASS 样式,为旧浏览器生成图像精灵,并构建缓存清单文件。
部署建议
虽然我们已经节省了超过 5 MB,但仍有进一步的优化应该进行,以确保我们的应用尽可能快地加载。这里我们将概述其中的一些方法,但为了更完整的列表,请查看 Google 的 PageSpeed 或 Yahoo 的 YSlow,它们将分析应用并提出建议。
GZip
GZipping 允许压缩发送到浏览器的内容,使其下载速度更快。这是一个可以在大多数 Web 服务器上启用的设置。
压缩和连接
我们已经讨论了这两个过程,它们由 Sencha Cmd 处理。然而,当将第三方库和框架包含到你的应用中时,记住这一点很重要。
图片优化
在页面重量方面,JavaScript 的大小受到很多重视,但通常,移除单个图片可以将页面大小减半。如果无法移除图片,请确保它们已经完全优化,可以使用 ImageOptim 等工具。
我们应用的结构
让我们退一步,理解 Sencha Cmd 在我们的应用文件夹中创建的内容,以及我们的应用代码将属于何处:
-
.sencha: 我们应用的.sencha文件夹与工作区中找到的类似。其中的文件使我们能够对应用本身及其构建过程进行细粒度控制。 -
app:app文件夹是我们将花费大部分时间的地方,因为它包含我们所有的 JavaScript 源代码。默认情况下,每个主要类类型都有自己的文件夹,包括控制器、模型、存储和视图。根据应用需求,可以随时在此处添加新文件夹。Ext JS 5 向框架引入了一个新的架构概念,称为 MVVM,这将在后续章节中进一步讨论。当使用这种结构时,我们将我们的 ViewModels 和 ViewControllers 包含在
view文件夹中。 -
Application.js: 这个文件是定义应用程序的地方,也是它将从那里启动的地方。在这个文件中,我们将定义我们想要加载的控制器、存储和视图,以及当浏览器和框架准备好时我们想要运行的代码。你会注意到在根级别还有一个
app.js文件。这个文件通常不需要编辑,并且任何“应用程序”自定义设置都应该添加到Application.js中。 -
overrides: 我们想要对框架代码进行的任何覆盖都可以添加到这里。 -
resources: 这个文件夹将包含我们应用程序将使用的任何资产(图像、图标、字体等)。当应用程序构建时,这些资产都会被复制到我们的生产build文件夹中。 -
sass:sass文件夹将是我们所有自定义 SASS 样式规则的存放地,这些规则将在 Sencha Cmd 构建过程中被编译。 -
app.json: 我们的app.json文件包含大量针对应用程序的配置选项,可以用来配置诸如包含在构建中的 JavaScript 和 CSS 文件、AppCache 详细信息以及活动主题等。 -
build.xml: 这个文件允许我们挂钩到自动化构建过程的每个步骤,并添加我们自己的步骤。如果我们想要定制过程以适应我们的工作流程,这非常有用。 -
bootstrap.css、bootstrap.js和bootstrap.json: 这三个文件是启动应用程序所必需的,但它们是由 Sencha Cmd 构建过程生成的,因此不应手动编辑。
工作原理
到目前为止,我们已经创建了一个可工作的骨架应用程序,准备用我们的业务逻辑和用户界面来填充,但框架实际上是如何工作的,它为我们提供了哪些其他框架没有的功能呢?
引导启动过程
使用框架,我们的应用程序的启动和运行过程很简单,框架负责在需要时按正确顺序包含所有资产。我们只需定义哪些类依赖于哪些其他类,框架就会为我们构建这个关系图。
JavaScript 到 HTML
Ext JS 管理整个用于向用户显示用户界面的 HTML。我们主要处理界面组件的 JavaScript 配置,这些组件随后被渲染为 HTML。通过这种方式,我们能够从复杂的 HTML 和 CSS 中抽象出来,这些是需要在所有平台上完美渲染丰富小部件所必需的。
事件系统
Ext JS 类使用一个事件系统,这使得它们能够无缝地相互通信。这使保持耦合度低变得容易,并且非常适合简化处理 JavaScript 的异步特性。
管理数据
框架的一个大优势是它允许你有效地对数据结构进行建模并在你的应用程序中管理数据。创建关联数据模型、读取和保存数据到各种来源以及直接将这些数据源绑定到界面组件的支持,使框架非常强大。
浏览器 API 交互
有许多浏览器 API,Ext JS 会代表我们抽象和交互,以简化并统一我们使用它们的方式。例如,使用这种方法,在将数据保存到服务器 API 或 LocalStorage 数据存储之间切换只是一个简单的配置更改。
路由
Ext JS 5 引入了一种新的路由系统,使我们能够在我们的单页 Web 应用程序中启用返回按钮,并直接访问应用程序的特定区域。
摘要
本章主要介绍了 Ext JS 框架的设置场景,并解释了如何使用它来创建令人难以置信的 Web 应用程序。它的用例多种多样,通过使用它,你可以确保你的项目建立在坚实的基础之上,拥有创建可靠、可维护且最重要的是功能强大的应用程序的功能。
我们还创建了本书项目应用的基础,这将在本书的每一章中扩展。到那时,我们将开发一个真实、活跃的应用程序。我们认为这种方法对于保持课程的相关性和实用性至关重要,确保你可以立即开始创建自己的应用程序。
第二章 掌握框架的构建块
Ext JS 类系统是框架的基础,为我们提供了一个面向对象的结构来构建我们的应用程序。本章将介绍类系统的基本原理,以及我们如何使用它来定义应用程序的构建块。
我们将在本章讨论以下主题:
-
基本面向对象原则,如继承,是如何被使用的
-
如何使用 Ext.Loader 类动态加载我们的类
-
如何覆盖类方法
-
如何使用 Ext JS 的配置模型
定义类
我们应用程序的第一个类将是一个配置类,用于存储我们应用程序的各种选项。我们使用 Ext.define 方法定义一个类,如下所示:
Ext.define('BizDash.config.Config', {
},
function(){
console.log('Config class created');
});
第一个参数是一个定义类名的字符串,第二个参数是一个包含所有类成员的对象,第三个是一个可选的回调函数,当类被定义时执行。
类名必须遵循严格的命名约定,以便它们可以被 Ext.Loader 类自动加载。这个类在需要时自动加载类,并使用它们的完全限定名在目录结构中找到它们。
-
我们类名的第一部分——
BizDash——是我们应用程序的名称,也是我们的根命名空间。默认情况下,它映射到我们应用程序文件夹内的app文件夹。 -
第二部分——
config——是我们类的子命名空间,用于将我们的类组织到文件夹中。这个名称映射到app文件夹内的一个子文件夹。我们可以创建任何深度的命名空间,以便我们以对应用程序有意义的方式组织我们的代码。 -
最后,
Config是我们类的名称,并形成了我们类定义将驻留的文件名。
以下屏幕截图显示了我们的完全限定类名与其目录结构的关系:

标准的 Sencha 命名约定规定,所有子命名空间都应该小写,所有类名都应该使用大驼峰式。例如,BizDash.view.users.UserForm 优于 BizDash.view.Users.userForm。
我们现在将使用 Ext.create 方法实例化这个类。这个方法接受一个类的完全限定名和一个对象,该对象的属性和值将被用来配置这个类。我们将在本章后面讨论如何添加我们自己的配置选项。
在网页浏览器中打开应用程序,并在开发者工具的控制台中运行以下代码:
Ext.create('BizDash.config.Config', {});
运行代码后,我们应该看到一个类似于以下屏幕截图的控制台:

在屏幕截图中,我们可以看到三件事:
-
通过
GET请求自动加载到页面中的源文件 -
来自
Ext.define回调的控制台日志 -
我们的新类实例在控制台中记录
您会注意到,我们能够在不在我们的应用程序或 HTML 页面中引用它的情况下实例化我们的BizDash.config.Config类。这是由Ext.Loader类的魔法处理的,我们将在下一节中讨论。
Ext.Loader 背后的魔法
在上一节中,我们看到了一个例子,说明了类源文件如何根据需要自动加载到应用程序中。这是由Ext.Loader类处理的,它分析每个类的依赖关系,并确保所有依赖的类都通过 AJAX 加载。有关如何将此与创建生产构建相关联的更多详细信息,请参阅第一章的使用 Sencha Cmd 创建我们的应用程序部分,了解 Ext JS。
类的定义过程
以下图表展示了类定义的过程,从调用Ext.define方法开始,以及Loader类如何融入这一过程:

定义依赖项
确保所有必要的类都已加载到页面上的最佳方式是使用requires配置选项构建一个依赖关系树。此选项接受一个完全限定的类名数组,然后所有这些类名在类定义被认为完成之前都加载到页面中。在加载一个依赖类后,这个过程会重复进行,新类及其所有依赖项都会被加载,直到所有所需的类都存在。
我们可以通过在新的类BizDash.config.Constants中引入一个依赖项来扩展我们的Config类:
Ext.define('BizDash.config.Constants', {
},
function(){
console.log('BizDash.config.Constants defined.');
});
我们希望我们的Config类能够使用这个类,因此我们必须确保在定义类时它能够被实例化。我们通过在类定义中添加requires配置来实现这一点,告诉框架自动加载它:
Ext.define('BizDash.config.Config', {
requires: ['BizDash.config.Constants']
},
function(){
console.log('BizDash.config.Config defined');
});
在这一行设置后,我们可以使用Ext.create再次实例化我们的类,并看到Constants类被自动加载:
Ext.create('BizDash.config.Config', {});

Loader 路径
默认情况下,应用程序的根命名空间(在我们的案例中是BizDash)映射到app文件夹。这意味着任何子命名空间直接映射到app文件夹内的一个文件夹,类名映射到 JavaScript 文件本身。保持这些命名约定非常重要,以便可以找到这些源文件。
如果您希望应用程序从不同的位置加载文件,您可以通过在Application.js文件的顶部添加对Ext.Loader.setConfig方法的调用,为加载器指定一个自定义路径和命名空间约定。
默认的paths对象将Ext和应用程序命名空间映射,如下面的代码片段所示:
Ext.Loader.setConfig({
paths: {
Ext : '../ext/src',
BizDash : 'app'
}
});
您可以通过添加自己的命名空间作为键(这可以是一个命名空间的多个部分;例如,Custom.namespace),以及正确的文件夹路径作为值来自定义它:
Ext.Loader.setConfig({
paths: {
Ext : '../ext/src',
BizDash : 'app',
'Custom.path' : '..CustomClasses/path'
}
});
Ext.Loader 和 Sencha Cmd
正如我们所见,Sencha Cmd 可以将所有所需的类文件连接成一个单一的文件。这是通过生成这个依赖图并按应用程序启动时加载的顺序组合所有源文件来实现的。这确保了只有实际使用的类被包含在应用程序的最终源文件中,使其比始终包含整个框架小得多。
我们的依赖根
在我们的示例中,我们通过Ext.create调用强制加载了Config配置,这会同步加载源文件。通过依赖这种方式来加载类文件,依赖树仅在运行时建立和满足。这意味着 Sencha Cmd 将无法创建一个完全连接的源文件,并且将被迫在运行时加载缺失的类。
为了解决这个问题,我们应该在Application.js文件中将我们的根类作为所需的类包含在内。此文件是应用程序的入口点,因此从这里我们可以建立依赖图,直到应用程序启动前的极端部分。
为了确保我们的BizDash.config.Config类在应用程序启动时加载并准备好使用,我们会在Application.js文件中的requires数组中添加它:
Ext.define('BizDash.Application', {
extend: 'Ext.app.Application',
name: 'BizDash',
requires: [ 'BizDash.config.Config' ],
...
});
添加类成员
到目前为止,我们已经探讨了定义和创建类以及将这些类加载到我们的页面中的方法。现在,我们将探讨如何通过添加类成员(如属性、方法和静态成员)使它们变得有用。
属性
可以通过在类的定义对象中包含一个键值对来向类中添加公共属性。以下代码示例展示了我们如何向我们的Config类添加一个版本号属性:
Ext.define('BizDash.config.Config', {
requires: ['BizDash.config.Constants'],
version: '0.0.1-0'
},
function(){
console.log('BizDash.config.Config defined');
});
这个属性可以通过在Config类的一个实例中使用点符号来访问;例如:
var config = Ext.create('BizDash.config.Config', {});
// logs 0.0.1-0 console.log(config.version);
方法
可以以与属性相同的方式添加方法。我们可以在Config类中添加一个getBuildNumber方法,它将从版本属性中提取构建号,如下所示:
Ext.define('BizDash.config.Config', {
requires: ['BizDash.config.Constants'],
version: '0.0.1-0',
getBuildNumber: function(){
var versionSplit = this.version.split('-');
return versionSplit[1];
}
},
function() {
console.log('BizDash.config.Config defined');
});
此方法可以按正常方式执行:
// logs "0"
console.log(config.getBuildNumber());
注意,默认情况下,方法是在类实例的作用域中执行的,这使得我们可以访问版本属性。
静态成员
Ext JS 类系统为我们提供了在类中包含静态属性和方法的能力,以消除创建类实例的需要。这些可以在分配给statics配置选项的对象内部添加。
以下示例展示了添加到BizDash.config.Constants类的名为ENVIRONMENT的静态属性,用于跟踪我们是否处于开发或生产环境:
Ext.define('BizDash.config.Constants', {
statics: {
ENVIRONMENT: 'DEV'
}
},
function(){
console.log('BizDash.config.Constants defined.');
});
我们现在可以使用以下代码访问这个静态属性:
// logs "DEV"
console.log(BizDash.config.Constants.ENVIRONMENT);
Ext JS 的命名约定指出,静态属性名称应该始终为大写。
子类中的静态成员
如果您希望类的静态属性在所有子类中可用,则必须使用inheritableStatics属性来定义。
单例
作为使用静态方法的替代,可以将类定义为单例,在成功定义后,将实例化类并将其分配回类的属性。这可以通过简单地给类添加 singleton: true 配置来实现:
Ext.define('BizDash.config.Config', {
singleton: true,
...
},
function() {
console.log('BizDash.config.Config defined');
// logs "true"
console.log(BizDash.config.Config.isInstance);
});
在所有类实例中都存在的 isInstance 属性的记录显示,一旦定义,BizDash.config.Config 属性现在成为了该类的一个实例。
扩展类
Ext JS 框架的架构意味着功能是通过继承构建和共享的。让我们的自定义类或视图继承另一个或现有的框架类变得极其容易。
例如,当定义您的视图类时,您可能会从基础 Ext.Component 类或其子类之一扩展,例如 Ext.panel.Panel 或 Ext.grid.Panel。
我们通过包含 extend 配置并给出要扩展的类的名称来定义我们类的超类。在以下示例中,我们将从 Ext.util.Observable 类扩展我们的 Config 类,使其能够触发自己的自定义事件。这将使我们的 Config 类能够访问这个类的所有方法和属性,例如 on 方法:
Ext.define('BizDash.config.Config', {
extend: 'Ext.util.Observable',
...
}, function() {
// logs "function(){..}"
console.log(BizDash.config.Config.on);
});
Ext JS 文档在其文档页面的右上角显示了每个类的继承树,如下所示,对于 Ext.panel.Panel 类:

当省略时,就像我们原始的 Config 类一样,类将默认从 Ext.Base 类扩展,这是所有框架类的基础基类。
覆盖类
有时有必要在不直接修改原始源的情况下更改类的功能。我们可以通过定义一个新的类以及 overrides 配置选项来告诉 Ext JS 要修改哪个类。
当改变框架行为时(例如,修复框架代码中的错误)应始终使用这种技术,因为 Ext JS 代码不应直接修改。当未来进行框架升级时,这可能会给你带来麻烦。
在以下示例中,我们将覆盖我们的 getBuildNumber 方法,使其返回带有前缀 Build Number: 的构建号。您可能已经看到 Sencha Cmd 已经在我们的应用程序结构中创建了一个 overrides 文件夹。这就是我们将放置我们的类覆盖文件的地方。
在这个文件夹内,我们创建一个新的 config 目录来映射我们的主应用文件夹结构,并在一个名为 Config.js 的文件中定义一个新的名为 Overrides.config.Config 的类。

override 的结构与正常的类定义相同,如下所示:
Ext.define('Overrides.config.Config', {
override: 'BizDash.config.Config',
});
我们现在可以添加对 getBuildNumber 方法的覆盖,该方法使用 callParent 方法来执行原始方法,并将其输出与我们的标签结合:
Ext.define('Overrides.config.Config', {
override: 'BizDash.config.Config',
getBuildNumber: function() {
return 'Build Number: ' + this.callParent(arguments);
}
});
需要我们的覆盖类
为了将此覆盖包含到我们的应用程序中并使更改生效,我们需要告诉应用程序我们的覆盖类所在的位置,以便Ext.Loader知道在哪里找到它们。我们通过向sencha.cfg文件中添加overrides路径来实现,这样 Sencha Cmd 就可以更新应用程序的依赖项。我们通过在BizDash/.sencha/app/sencha.cfg文件的底部添加以下行来完成此操作:
app.overrides=${app.dir}/overrides
然后,我们可以在BizDash文件夹内运行sencha app refresh命令来重新生成应用的引导文件。
如果我们现在重新加载我们的应用程序并调用getBuildNumber方法,我们应该看到输出为构建号:0。

虽然这个覆盖正在覆盖我们自己的自定义方法之一,但可以遵循相同的过程来覆盖任何您想要更改行为的框架自己的类。
针对框架版本进行覆盖
现在可以使用兼容性配置来针对特定的框架或包版本进行覆盖。这可以接受一个包含版本号的单一字符串,一个将使用逻辑或运算符匹配版本字符串的数组,或者一个使用and或or键来创建更复杂匹配的对象。以下示例显示了如何使用这些:
/* matches Ext JS 5.0.0 compatibility: '5.0.0'
matches Ext JS 5.0.0 OR Ext JS 4.2.1 compatibility: ['5.0.0', '4.2.1']
matches the 'Ext JS' package with 5.0.0 and 'Sencha Core' with 5.0.0 */
compatibility: {
and: [
'extjs@5.0.0',
'sencha-core@5.0.0'
]
}
如果兼容性选项匹配,则包含覆盖;否则不包含。
关于可以使用哪些版本表达式的详细信息,请参阅Ext.checkVersion方法的文档。docs.sencha.com/extjs/5.1/5.1.0-apidocs/#!/api/Ext-method-checkVersion。
配置类
Ext JS 提供了一种创建可配置属性的有用方法,它为我们提供了一个自动生成的过程来获取和设置它们的值,并且还可以执行在其他区域中所需的任何更新;例如,在 UI 中反映更新值。
当一个类被定义时,在config对象中找到的任何属性将完全封装在其他类成员之外,并且会为其提供自己的获取器和设置器方法。
例如,如果我们将Config类的version属性移动到config对象中,如以下代码片段所示,类将获得两个新方法,分别命名为getVersion和setVersion。请注意,我们必须创建一个constructor函数并调用initConfig方法,以便类系统初始化这些新方法。我们还必须更新我们的getBuildNumber方法,该方法使用this.version引用版本号,改为使用新的获取器方法getVersion。
Ext.define('BizDash.config.Config', {
extend: 'Ext.util.Observable',
singleton: true,
requires: ['BizDash.config.Constants'],
config: { version: '0.0.1-0' },
constructor: function(config){
this.initConfig(config);
this.callParent([config]);
},
getBuildNumber: function() {
var versionSplit = this.getVersion().split('-');
return versionSplit[1];
}
}, function() {
...
});
在 Ext JS 5 中,当扩展Ext.Component类或其子类时,现在不需要调用initConfig,因为它是在内部调用的。
设置配置值
当给 config 选项赋予新值时,生成的设置器方法比仅仅将给定的值分配给类中的属性要复杂得多。它引入了 applier 和 updater 方法的概念,这些方法是可选的,如果存在,则按顺序在设置器方法中调用。
应用器函数应使用与获取器和设置器相同的约定命名,名称前缀为 apply,属性名称的首字母大写;例如,applyVersion。此方法用于在将值存储在类中之前按需转换给定的值。可能的用法可能包括查找值以获取其实例(例如,从其 ID 获取存储实例)。此方法必须返回一个值;否则属性将不会被更新。
更新器函数遵循相同的命名模式(例如,updateVersion),并在值经过应用器转换并在类中设置后调用。此函数主要用于更新 UI 以反映组件内的最新值。
以下图展示了在调用生成的设置器方法时此过程是如何工作的:

以下示例展示了我们如何使用和应用此方法以确保版本号的格式;以及一个更新方法来触发自定义事件,允许应用的其他区域通知变化:
Ext.define('BizDash.config.Config', {
extend: 'Ext.util.Observable',
singleton: true,
requires: ['BizDash.config.Constants'],
config: {
version: '0.0.1-0',
...
},
...
applyVersion: function(newVersion, oldVersion){
return newVersion;
},
updateVersion: function(newVersion, oldVersion){
this.fireEvent('versionchanged', newVersion, oldVersion);
}
}, function() { ... });
覆盖默认值
当创建新的类实例时,可以通过简单地将属性和所需值添加到传递给 Ext.create 方法的配置对象中来轻松覆盖配置属性的默认值。以下代码片段显示了如何设置版本选项(假设 Config 类没有被设置为单例):
var config = Ext.create('BizDash.config.Config', {
version: '0.2.0-0'
});
// logs "0.2.0-0"
console.log(config.getVersion());
平台特定配置
Ext JS 5 引入了根据应用程序运行的平台配置类的不同能力的能力,因此我们可以根据平台的能力定制体验。
这是通过使用 platformConfig config 选项来完成的,给它一个配置对象数组。这些配置选项必须包含一个平台属性,该属性将用于在当前平台上找到适当的配置。
此选项应包含一个字符串数组,描述配置应针对的平台。这可以是一个或多个以下选项:手机、平板、桌面、iOS、Android、Blackberry、Safari、Chrome 或 IE10。
如果这些平台中的任何一个与当前平台匹配,则其他属性将合并到类配置中。可能存在多个规则评估为真,在这种情况下,所有匹配规则的性质都将应用。
以下代码展示了我们如何根据平台配置我们的 Config 类:
Ext.define('BizDash.config.Config', {
extend: 'Ext.util.Observable',
singleton: true,
requires: ['BizDash.config.Constants'],
config: {
version: '0.0.1-0',
isPhone : false,
isTablet : false,
isDesktop: false
},
platformConfig: [ {
platform: ['phone'],
isPhone : true
},
{
platform: ['tablet'],
isTablet: true
},
{
platform : ['desktop'],
isDesktop: true
}]
...
});
我们可以使用它们的 getter 方法访问这些属性。以下代码在笔记本电脑上运行,将输出true给isDesktop属性,对于其他两个属性输出false:
// logs "true", "false", "false"
console.log(BizDash.config.Config.getIsDesktop());
console.log(BizDash.config.Config.getIsPhone());
console.log(BizDash.config.Config.getIsTablet());
概述
在本章中,我们涵盖了 Ext JS 类系统的所有方面,包括:
-
定义和实例化类
-
添加属性、方法和静态成员
-
扩展其他类和覆盖框架行为
-
配置类并使用应用器和更新器方法
我们还演示了如何使用Ext.Loader类确保我们的类文件在需要时被加载到应用程序中。
下一章将讨论 Ext JS 如何处理事件,这些事件既来自用户输入,也来自其他类。
第三章。响应用户和应用交互
使用 Ext JS 开发有效的应用程序需要深入了解事件驱动编程以及如何根据用户和应用交互执行操作。Ext JS 是一个事件驱动框架,并使用事件来控制应用程序的流程。
事件可以通过用户输入、框架内部或我们自己的应用程序代码来触发。例如,当用户用鼠标点击按钮时,按钮的实例将触发一个点击事件。然后我们可以将监听器附加到这个事件,并在它被触发时执行我们的处理程序代码。看看下面的图示:

本章将详细探讨 Ext JS 中的事件。本章涵盖的主题包括:
-
监听事件
-
抬起自定义事件
-
将事件处理程序附加到组件上
-
通过鼠标、键盘和触摸屏监听用户输入
背景
使用 Ext JS 构建的应用程序将在其生命周期中的许多地方使用事件。即使你不知道,在后台,当发生有趣的事情时,框架将触发事件。作为开发者,我们希望我们的应用程序代码能够对这些事件做出响应,无论是处理某些事情还是向用户反馈发生了什么。
Ext JS 实现了一个Ext.mixin.Observable类,它提供了一个发布事件的通用接口。让我们看看我们有哪些选项来监听事件。
在配置对象中定义事件处理程序
定义监听器的一种常见方法是使用listeners配置选项,它允许我们定义一个包含事件处理程序的对象。这个对象应该在 Ext JS 组件的config对象中定义。
让我们直接跳转到在第一章中生成我们的应用程序时自动创建的BizDash.view.main.Main类,了解 Ext JS。
我们将绑定一个事件监听器到 Tab Panel 组件的afterrender事件。当事件被触发时,该函数将被执行。在这种情况下,处理程序的输出是一个简单的控制台消息:
Ext.define('BizDash.view.main.Main', {
extend: 'Ext.container.Container',
...
items: [{
region: 'center',
xtype: 'tabpanel',
listeners: {
afterrender: function(component, eOpts) {
console.log('Center tabpanel has rendered.')
}
}
}]
});
同时也可以将处理程序附加到多个事件上。这个例子中,Tab Panel 组件触发的事件既有beforerender又有afterrender处理程序:
Ext.define('BizDash.view.main.Main', {
extend: 'Ext.container.Container',
...
items: [{
region: 'center',
xtype: 'tabpanel',
listeners: {
afterrender: function(component, eOpts) {
console.log('Center tabpanel has rendered.')
},
beforerender: function(component, eOpts) {
console.log('Center tabpanel before rendering.')
}
}
}]
});
按钮处理程序
该框架提供了一个快捷方式来定义一个处理程序到按钮,因为它高度可能你希望你的按钮能够响应鼠标点击。在这个情况下,handler被绑定到视图的ViewController中的onClickButton方法,BizDash.view.main.MainController:
Ext.define('BizDash.view.main.Main', {
extend: 'Ext.container.Container',
...
items: [{
...
tbar: [{
text: 'Button',
handler: 'onClickButton'
}]
}]
});
被认为是一个好的实践,将业务逻辑放在视图之外,而不是将其放在全局控制器或 ViewController 中。
on 方法
或者,我们可以使用on方法,这是addListener方法的别名。这个方法来自混合的Ext.mixin.Observable类,并允许我们在类实例化后向类或组件添加监听器:
Ext.define('BizDash.view.main.MainController', {
extend: 'Ext.app.ViewController',
...
init: function () {
var button = this.getView().query('button[text="Button"]')[0];
button.on('mouseover', 'onMouseOver');
button.on({ mouseover: 'onMouseOver' });
},
onMouseOver: function () {
console.log('Button Mouseover Event Fired');
}
});
前面的例子展示了如何使用 Ext JS 组件查询在我们的应用程序中搜索按钮,并添加一个绑定到我们 ViewController 中onMouseOver方法的mouseover监听器。我们将在第七章构建常见 UI 小部件中更详细地介绍组件查询。
与监听器的config对象一样,on方法也接受一个可选的参数集,允许一次性分配多个事件处理器。通过将一个 JavaScript 对象作为第一个参数提供,其中包含指定事件名称和处理函数的键值对,可以一次性分配所有监听器。通过在这个对象中定义一个scope属性,处理函数将在这个指定对象的范围内执行(或者在函数内部this所引用的对象):
Ext.define('BizDash.view.main.MainController', {
extend: 'Ext.app.ViewController',
...
init: function () {
var button = this.getView().query('button[text="Button"]')[0];
button.on({
mouseover: 'onMouseOver',
mouseout: 'onMouseOut',
scope: this
});
},
onMouseOver: function () {
console.log('Button Mouseover Event Fired');
},
onMouseOut: function () {
console.log('Button Mouseout Event Fired');
}
});
作用域默认为触发事件的那个对象。在我们的例子中,那就是按钮。如果您需要处理函数在不同的作用域中执行,可以使用scope选项来自定义。前面的例子展示了如何使用this引用来改变作用域,使其变为BizDash.view.main.MainController的作用域。
监听器选项
您还可以配置许多监听器选项。例如,您可以缓冲连续快速触发的事件,或者将事件目标指向特定的元素而不是整个组件。在组件构建期间,这种事件委托非常有用,可以将 DOM 事件监听器添加到组件的元素上,这些元素将在组件渲染后存在。
Ext.define('BizDash.view.main.MainController', {
extend: 'Ext.app.ViewController',
...
onClickButton: function () {
Ext.Msg.confirm('Confirm', 'Are you sure?', 'onConfirm', this);
this.getView().getButton().disable();
},
...
init: function () {
var button = this.getView().query('button[text="Button"]')[0];
button.on({
mouseover: 'onMouseOver',
mouseout: 'onMouseOut',
click: {fn: 'onClickButton', single: true},
scope: this
});
},
...
});
此示例展示了添加到点击事件的single选项。此选项在第一次触发后自动删除点击事件。我们进一步增强了onClickButton方法,使其禁用按钮。
Ext.mixin.Observable类的文档包含有用的示例和有关配置事件的更多信息。您可以在docs.sencha.com/extjs/5.1/5.1.0-apidocs/#!/api/Ext.mixin.Observable找到它。
触发事件
Ext.mixin.Observable类还提供了一种触发事件的方法,无论是框架事件还是自定义事件。fireEvent方法将触发我们需要的任何事件,并将参数传递给处理函数以供消费。以下示例展示了如何触发一个自定义的confirmed事件,将choice参数传递到按钮上,并将其绑定到onConfirmed处理函数:
Ext.define('BizDash.view.main.MainController', {
extend: 'Ext.app.ViewController',
onConfirm: function (choice) {
if (choice === 'yes') {
var button = this.getView().getButton();
button.fireEvent('confirmed', choice)
}
},
onConfirmed: function(choice){
console.log('The CONFIRMED event was fired');
},
init: function () {
button.on({
mouseover: 'onMouseOver',
mouseout: 'onMouseOut',
click: {
fn: 'onClickButton',
single: true
},
confirmed: 'onConfirmed',
scope: this
});
}
});
在元素上监听事件
由于某些事件,例如点击,并非所有组件都可用,因此可以直接将事件处理器附加到任何元素。Ext.dom.Element 类是一个框架类,它包装 DOM 元素,并将所有底层 DOM 事件传递出去,其文档包含这些事件的完整列表。
Ext.define('BizDash.view.main.MainController', {
extend: 'Ext.app.ViewController',
init: function () {
var el = this.getView().getEl();
el.on('tap', function() {
console.log('The Viewport was tapped/clicked.');
});
}
});
上述示例展示了如何在整个 Viewport 上监听触摸事件。
事件委托
然而,事件处理器是内存泄漏的常见原因,如果不小心管理,可能会导致性能下降。我们创建的事件处理器越多,出现此类问题的可能性就越大;因此,当我们不需要时,应尽量避免创建大量处理器。
事件委托是一种技术,在父元素上创建单个事件处理器,利用浏览器会将任何在其子元素上引发的事件冒泡到父元素的事实。如果原始事件的目标与委托的选择器匹配,则将执行事件处理器;否则,不会发生任何操作。
这意味着我们不需要为每个单独的子元素附加事件处理器,而只需在父元素上创建一个处理器,然后在处理器内部查询实际被点击的子元素,并相应地做出反应。为了实现这一点,我们使用 listeners 配置中可用的委托选项。
以下示例展示了如何使用包含多个链接的元素进行事件委托:
/* assume navigationEl is an Ext.Element instance containing multiple <a> tags */
navigationEl.on('click', function(e){
/* Handle a click on any element inside the 'navigationElement'. Use e.getTarget to determine which link was clicked.*/
}, {
delegate: 'a'
});
鼠标事件
框架可以处理的鼠标事件有 mousedown、mousemove、mouseup、mouseover、mouseout、mouseenter 和 mouseleave。Ext.event.Event 类处理跨浏览器和跨设备的差异,以确保我们的应用程序在所有支持的浏览器上表现一致。
键盘事件
Ext.event.Event 类还提供了一系列键常量:
var constants = {
BACKSPACE: 8,
TAB: 9,
NUM_CENTER: 12,
ENTER: 13,
RETURN: 13,
SHIFT: 16,
...
}
例如,我们可以使用 Ext.event.Event.ENTER 获取回车键的代码。
键盘映射
Ext.util.KeyMap 类用于将键盘按键绑定到处理函数。使用此功能,用户可以使用键盘控制应用程序:
Ext.define('BizDash.view.main.MainController', {
extend: 'Ext.app.ViewController',
init: function () {
var map = new Ext.util.KeyMap({
target: this.getView().getEl(),
key: Ext.event.Event.ENTER,
fn: this.onEnterPress,
scope: this
});
},
onEnterPress: function() {
console.log('ENTER key was pressed');
}
});
触摸事件
我们不仅经常需要支持多个浏览器,而且大多数时候我们需要我们的应用程序对设备不可知。Ext JS 使我们能够支持使用其他类型指针的用户,例如鼠标、笔或手指。
Ext JS 5 提供了对 touchstart、touchmove 和 touchend 事件的支持。
事件归一化
为了支持触摸屏设备,框架会自动将触摸屏事件转换为等效的鼠标事件,这称为事件归一化。
作为开发者,我们不需要担心额外的编码。我们只需考虑鼠标使用的事件即可。例如,mousedown 将无缝转换为 touchdown 和 pointerdown。
手势
虽然归一化可以节省我们编码,但我们仍然需要理解用户在我们应用程序上执行的手势。Ext JS 几乎为我们做了所有繁重的工作。它将解释点击、滑动、拖动和双击等手势,并在任何元素上为我们触发事件,以便我们监听。
为了做到这一点,框架基于 Sencha Touch 手势系统,该系统解释了三个主要事件的序列和时机:touchstart、touchmove 和 touchend。Ext JS 5 将这些转换为等效的指针和鼠标事件(例如,pointerdown 或 mousedown),以便手势无论在哪种输入设备上都能被理解。
例如,像点击和滑动这样的手势对触摸和鼠标输入都适用。
摘要
在本章中,我们学习了如何在 Ext JS 应用程序中处理事件。你现在应该对以下内容感到更加熟悉:
-
监听事件
-
触发自定义事件
-
将事件处理器附加到组件上
-
通过鼠标、键盘和触摸屏监听用户输入
下一章将在你对类和事件的知识基础上,涵盖整个应用程序及其架构。Ext JS 5 现在提供了对 MVVM 以及 MVC 的支持。虽然本章的重点是这些范式,但我们也会探讨在开发周期早期值得考虑的其他因素。
第四章。构建 Ext JS 应用程序架构
无论您是仅开发一个应用程序还是计划开发多个,提前考虑您的应用程序架构都是一个很好的主意。架构是您应用程序的内部结构以及其中使用的编程模式。
最终,遵循常用模式为您提供了应用程序中的连续性和一致性。这给我们带来了四个主要优势:
-
框架和您的应用程序更容易学习
-
在应用程序之间切换所需时间更少
-
应用程序之间的代码共享是可能的
-
您可以在构建和测试工具之间获得一致性
由于本章涵盖了使用 Ext JS 的应用程序架构,我们将要探讨的核心主题包括:
-
Sencha Cmd 以及它如何帮助我们构建应用程序
-
模型-视图-控制器(MVC)架构模式
-
新引入的 模型-视图-视图模型(MVVM)架构模式
-
使用事件驱动模型进行跨类通信
-
考虑将您的应用程序离线以及离线优先设计
Ext JS 5 现在提供了对 MVC 和 MVVM 应用程序架构的支持。本质上,这两种模式将应用程序分割,从而在组织良好的文件系统中产生井然有序的代码。
充分利用 Sencha Cmd
我们在第一章中描述了如何使用 Sencha Cmd 开始,了解 Ext JS,但您可以使用它做更多的事情。在本节中,我们将探讨一些其强大的命令以及它们如何加快并改进我们的工作流程。
生成应用程序组件
Sencha Cmd 可以通过生成 MVC/MVVM 组件来帮助加快开发过程,并使我们能够专注于应用程序的逻辑而不是编写重复的代码。
生成模型
要将模型添加到您的应用程序中,将 /path/to/MyWorkspace/BizDash 设置为当前目录并运行 Sencha Cmd,如下所示:
cd /path/to/MyWorkspace/BizDash
sencha generate model User Name:string,Email:string,TelNumber:string
此命令在 model 目录中的 User.js 文件中添加一个 model 类。文件看起来像这样:
fields: [
{
name: 'Name',
type: 'string'
},
{
name: 'Email',
type: 'string'
},
{
name: 'TelNumber',
type: 'string'
}
]
});
生成视图
以相同的方式将视图添加到您的应用程序中:
cd /path/to/MyWorkspace/BizDash
sencha generate view location.Map
这将生成以下文件:
-
app/view/Location/:实现新视图的类的文件夹 -
Map.js:新视图 -
MapModel.js:新视图的Ext.app.ViewModel -
MapController.js:新视图的Ext.app.ViewController
上述代码为 Map.js 生成的输出如下:
Ext.define("BizDash.view.location.Map",{
extend: "Ext.panel.Panel",
controller: "location-map",
viewModel: {
type: "location-map"
},
html: "Hello, World!"
});
ViewController (MapController.js) 是:
Ext.define('BizDash.view.location.MapController', {
extend: 'Ext.app.ViewController',
alias: 'controller.location-map'
});
在这种情况下,除了视图名称之外没有必需的参数。然而,如果您愿意,可以添加一个基类:
cd /path/to/MyWorkspace/MyApp
sencha generate view -base Ext.tab.Panel location.Map
这将改变视图类使用的扩展为 Ext.tab.Panel。
生成控制器
在 Ext JS 5 中,由 Sencha Cmd 生成的每个视图都有一个默认的 Ext.app.ViewController,因此大多数情况下不需要基于 Ext.app.Controller 生成全局控制器。如果您需要一个新的控制器,您可以使用与模型和视图相同的基本方式生成:
cd /path/to/MyWorkspace/BizDash
sencha generate controller Location
这将在控制器目录中生成一个名为 Location.js 的文件,其内容如下:
Ext.define('BizDash.controller.Location', {
extend: 'Ext.app.Controller'
});
升级您的应用程序
随着框架增强、功能和错误修复的实施,您可能会发现自己处于希望将应用程序升级到框架新版本的境地。您可以使用以下命令来完成此操作:
sencha app upgrade path/to/new/framework
此命令将升级 Sencha Cmd 框架和应用程序使用的框架。有关升级应用程序的完整说明,请参阅框架文档。
值得注意的是,使用 Sencha Cmd 无法撤销升级操作。我们建议在开始之前,确保您能够在版本控制系统中撤销升级更改。
刷新应用程序元数据
以下命令重新生成包含动态加载器和类系统 bootstrap 数据的元数据文件。每次添加、重命名或删除类时,都必须执行此操作。
sencha app refresh
触发自动构建
watch 命令非常有用,因为它会监视您的代码库中的更改(编辑、删除等),并触发应用程序的重建以加快开发过程。
当您运行以下命令时,将启动一个 Web 服务器以托管应用程序:
sencha app watch
注意
Web 服务器的默认端口是 1841。
MVC 和 MVVM
本章的下一部分将重点关注与 Ext JS 应用程序配合最佳的建筑模式。当 Sencha 在 Sencha Touch 的早期版本中引入 MVC 时,它是 Web 应用程序中 MVC 的先驱。从那时起,随着 Web 应用程序变得更大、更复杂、更难以维护,MVC(以及 MVVM)在 Web 开发社区中获得了动力和普及。这些应用程序架构的主要目的之一是为您的代码库提供结构和一致性。如今,大多数主要框架都支持它们,Ext JS 也是如此。
我们将解释 MVC 和 MVVM 是什么,它们的优缺点,以及它们在典型的 Ext JS 应用程序中的工作方式。
什么是 MVC?
模型-视图-控制器(MVC)是编写软件的架构模式。它将应用程序的用户界面分为三个不同的部分,有助于将代码库组织成基于功能的逻辑信息表示。在 Ext JS 应用程序中,这种范式的结果是拥有组织良好的代码和文件系统。
解释缩写
有时,MVC 实现在不同应用程序之间可能略有不同,但一般来说,架构的每个部分都有特定的职责。在 MVC 架构中,程序中的每个对象都是一个模型、一个视图或一个控制器。
模型
模型代表我们在应用程序中计划使用的数据。它描述了数据的通用格式——在大多数情况下是简单的字段,但它也可能包含业务规则、验证逻辑、转换、格式化规则和各种其他功能。
视图
视图从视觉上向用户展示数据。它使用 Ext JS 的标准 JSON 配置通过扩展框架组件/小部件来定义。例如,一个典型的视图可能是一个网格、一个表单或一个图表。
可能会有多个视图以不同的方式显示相同的数据。例如,尽管图表和网格在视觉上不同,但它们共享相同的数据。
注意
最佳实践规定,视图中几乎不存在任何业务逻辑。
控制器
控制器是 MVC 应用程序的核心部分。它是一个单一的结构,响应应用程序中的事件,并在模型和视图之间委派命令。由于 MVC 中关注点的清晰分离,控制器充当一个全局的消息总线,监听配置组件上的事件。
以下图表将为你提供一个更清晰的画面:

将这些内容整合起来
在你的应用程序中,用户将与视图交互,这些视图通常包含存储在模型中的数据。控制器在监控视图中的各种交互并更新模型或视图方面发挥着关键作用。控制器几乎包含应用程序的所有业务逻辑,使得视图和模型之间大部分互不干扰。你可以将其视为应用程序的发布-订阅模型。
Ext JS 有特定的类来管理控制器和模型,即Ext.app.Controller和Ext.data.Model。视图应通过扩展框架小部件来定义。
定义模型是一个好主意,因为它包含了你计划在应用程序中使用的数据。只需通过基本的字段配置扩展Ext.data.Model类,就可以开始使用。
依据此原则,通过扩展组件或小部件来创建视图。尽量避免在视图中放置业务逻辑;相反,将逻辑放在控制器中。例如,视图中的一个按钮不应包含事件逻辑——它应该放在控制器内部。
最后,通过扩展Ext.app.Controller来创建控制器。控制器可能不知道视图,在许多情况下,在你可以进行更多工作之前,你需要手动获取视图的引用。Sencha 关于 refs(docs.sencha.com/extjs/5.1/5.1.0-apidocs/#!/api/Ext.app.Controller-cfg-refs)和组件查询(docs.sencha.com/extjs/5.1/5.1.0-apidocs/#!/api/Ext.ComponentQuery-method-query)的文档详细解释了这一点。
Ext JS 的命名规范和目录结构
Sencha 使用明确的命名规范来保持所有文件在一起。例如,正如在第二章中解释的,掌握框架的构建块,类BizDash.view.Main与你的文件系统中的一个位置相关联。在这种情况下,文件被命名为Main.js,位于view目录中。
确保将你的模型存储在model目录中,视图存储在view目录中,控制器存储在controller目录中。
使用 MVC 的优缺点
使用 MVC 有许多优点,包括:
-
应用之间的连贯性,这减少了学习时间
-
应用之间共享代码的便利性
-
在使用 MVC 时,使用 Sencha 的构建工具构建优化应用的能
然而,MVC 架构最大的优势在于它帮助开发者避免编写过大且难以维护的文件。通过明确划分应用每个组件的责任,类被存储在一致且易于工作的目录结构中。
可惜,MVC 架构确实有其缺点。在 Ext JS 的 MVC 方法中,控制器是全局作用域的,这导致需要额外的业务逻辑来获取对视图、模型和其他对象的引用。控制器可以编写为在任何时候监视任何对象,因此任何给定的控制器可能既有视图 A 的逻辑,也有视图 B 的逻辑,这在大应用中会导致额外的混淆。
单元测试是 MVC 应用中反复出现的问题。在 MVC 模型中,视图和控制器应该是松散耦合的,但测试控制器需要了解更大的应用。很多时候,单元测试需要启动整个应用来测试单个组件。这显然是繁琐的、耗时的,并且容易出错。
注意
不要陷入构建具有相对较少控制器、每个控制器跨越数千行代码的应用程序的陷阱。最终,这会导致性能不佳和长期维护问题。
什么是 MVVM?
虽然 MVC 架构确实有一些主要优点,但其缺点也需要解决。模型-视图-视图模型(MVVM)架构是一个解决方案。Sencha 仅在 Ext JS 5 中引入了 MVVM 架构支持,因此你无法将这些原则应用于你的 Ext JS 4 或 Sencha Touch 2 应用。
Ext JS 5 仍然支持 MVC,因此从 Ext JS 4 升级到 5 不会破坏你的应用。Sencha 决定在 Ext JS 5 中支持 MVVM 架构,以解决可维护性和测试的缺点。
解决关注点
理论上,全局控制器非常有用,但如您所见,在实际中它们可能很难管理。MVVM 通过引入一个名为ViewModel的新类来克服这一点,该类管理特定于视图的数据。它是通过数据绑定来做到这一点的。这意味着我们编写的代码更少,更容易维护,并且测试起来更容易。
解释缩写
就像 MVC 一样,MVVM 是另一种用于编写软件的架构模式。它基于 MVC 模式,因此其中很多内容应该都很熟悉。
模型
MVVM 架构中模型的原则与 MVC 中相同。
视图
与模型一样,视图在 MVVM 中与 MVC 中相同。唯一的区别是我们必须为视图设置数据绑定。这是通过向视图中添加 ViewModel 来完成的:
Ext.define("BizDash.view.location.Map",{
extend: "Ext.panel.Panel",
viewModel: {
type: "location-map"
}
});
与 MVC 不同,MVVM 架构紧密地将视图与其关联的 ViewModel 和 ViewController 耦合在一起。
ViewModel
ViewModel 是 MVVM 与 MVC 之间的关键区别。在 MVC 中,使用事件,我们的控制器负责管理模型和视图之间的通信。在 MVVM 中,框架在幕后进行大量工作,并使用数据绑定来完成这项工作。
数据绑定是一种将用户界面与业务逻辑连接起来的机制。例如,当 UI 中的值发生变化时,模型中的底层数据值也会发生变化。
引入 ViewModel 的结果是模型和框架比以前做更多的工作,这最大限度地减少了操纵视图所需的应用程序逻辑。
看一下以下图表:

一个典型的 ViewModel 可能看起来像这样:
Ext.define('BizDash.view.location.MapModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.location-map',
data: {
name: 'Map Location'
}
});
ViewModel 在模型的数据和其视觉表示之间提供了一个桥梁。它们与视图紧密相关,并提供了它们所代表的数据。
业务逻辑
然而,仍然存在应用程序逻辑的问题。有两个选项,但通常,您的业务逻辑应该放在 ViewController 中。
ViewController
ViewController 非常类似于 Controller,因为它仍然通过监听事件来使用发布-订阅模型。它与之不同的地方在于它与视图的耦合方式。
ViewController 的定义如下:
-
直接关联到视图
-
具有一对一的关系
-
为每个视图实例创建
这大大减少了开销,因为您的应用程序没有那么多事件和组件引用在浮动。由于 ViewController 与引用它的视图相关联,内存泄漏和状态管理更容易识别和维护。

您的 ViewController 最初看起来可能如下所示:
Ext.define('BizDash.view.location.MapController', {
extend: 'Ext.app.ViewController',
alias: 'controller.location-map'
});
在此处定义的别名可以在视图定义的控制器配置中使用,以将两个类关联起来。
控制器
然而,您仍然可以使用控制器来处理您应用程序的消息总线。它们将继续像 MVC 架构中一样在多个视图中监听事件。
使用事件进行跨类通信
您的 Ext JS 应用程序利用事件来处理用户交互,但也很容易让您的类通过事件相互通信。
Ext JS 在处理事件方面非常成熟,因为这是框架自始至终的核心。它使用观察者模式允许您的类发布事件,并让其他类订阅这些事件。一旦发布类触发事件,您的订阅类就会立即触发其逻辑。这种范式是异步和模块化的。
正如我们在第二章中所述,掌握框架的构建块,Ext JS 中的事件驱动逻辑由Ext.mixin.Observable类处理。
将您的应用程序离线
在构建应用程序时,开发者经常争论设计一个能够离线工作的系统的问题。Ext JS 作为一个框架,旨在模仿在标准桌面环境中可能看到的组件和小部件。它为我们提供了构建真正丰富应用程序和类似甚至优于传统桌面应用程序的体验所需的工具。对于 Web 开发者来说,问题是用户已经习惯了使用那些被设计为“首先离线”的软件。许多移动或平板电脑应用程序也是如此。
以电子邮件客户端为例:即使您的数据连接丢失,桌面客户端仍然可以继续工作。您可以阅读电子邮件,搜索,组织文件夹,做很多事情。另一方面,基于 Web 的等效客户端可能表现不佳。自己试试看。
为什么我们应该设计离线优先?
除了确保网络始终是,并且继续是交付应用程序的有效解决方案之外,考虑将您的应用程序离线还有很多优势:
-
最重要的事实是我们离线了。这可能是无意为之,也可能不是我们所希望的,但连接性是间歇性的,并不总是可以保证的。这可能会导致您的用户丢失数据或无法完成他们的工作。
-
我们没有无处不在的互联网。我们必须在我们的移动和固定线路网络提供商的限制内工作。
-
移动、远程工作和在移动中访问系统比以往任何时候都更受欢迎。这一趋势仍在继续,大多数开发者在开发新应用程序时都需要考虑这一点。
-
性能得到了极大的提升,因为用户在本地做了大量工作,而您的服务器则可以用于其他更重要的任务。
-
可靠性和信任度得到了提升,因为您的用户将应用程序视为不带有通常缺陷的东西。
-
最后,鲁棒性得到了提升,例如,您的服务器宕机并不一定意味着应用程序会宕机。
总结来说,采用离线优先的方法将使您能够提供更好的用户体验。
我们能做些什么呢?
不同的应用有不同的方法,每个方法的难度等级也不同。按照从简单到复杂的顺序,常见的方法包括:
-
警告用户他们处于离线状态
-
提供用户缓存的数据库
-
允许用户进行最小程度的交互
-
允许用户与复杂应用进行完全交互
我们如何做到这一点?
在网络应用中有许多实现方式,为您的应用选择正确的方法并不简单。
-
原生打包是一个选项,类似于混合移动应用。可以使用嵌入式 WebKit、Cordova 或 Chrome/Firefox 应用等工具打包您的网络应用。
-
使用网络应用清单来定义应用中存在的详细信息和 API。
-
使用 AppCache 有助于浏览器缓存文件和资源。
-
如果您需要进行后台处理,例如数据同步,ServiceWorkers 非常有用;对于许多 JavaScript 开发者来说,这些可能是理想的。
-
LocalStorage 适用于以键值对的形式存储数据。Ext JS 为使用 LocalStorage 提供了出色的支持,但要注意其浏览器强制的存储限制——通常约为 5 MB。
-
IndexedDB 或 WebSQL 是在客户端存储应用程序数据的其他方法。WebSQL 已被弃用,但 IndexedDB 是一个非常可行的替代方案,并且 Ext JS 提供了出色的支持。
离线架构
有很多需要考虑的因素,显然,一个离线网络应用需要不同的架构。
你应该总是为最坏的情况做准备,并希望最好的情况发生。
解决这个问题的方法之一是将所有状态放在客户端,并在可能的情况下同步它。本质上,你希望设计你的应用程序以在用户的硬盘上下载和存储文件到缓存,并与本地存储的数据交互。在后台,本地存储的数据通过某种形式的代理与服务器同步。
同步数据
与后端同步数据可能是采用离线优先原则开发应用程序中最棘手的部分。编写同步协议是一个困难且耗时的过程。你应该考虑使用框架和工具,如 Hoodie、PouchDB 和 remoteStorage.io 来减轻同步引起的问题。
为了获得最佳结果,我们建议您遵循以下指南:
-
经常做,并且尽可能快地做
-
尽可能地传输最少的数据
-
准备好不可靠的数据网络
-
制定策略来管理冲突
摘要
本章重点介绍了应用程序架构的原则以及我们在 Ext JS 5 应用程序中可用的某些工具。我们涵盖了:
-
使用 Sencha Cmd 生成模型、视图和控制台
-
MVC 和 MVVM 架构模式
-
跨类通信
-
与离线数据一起工作
下一章将通过详细介绍数据包来进一步加深你的知识。对数据建模和存储的扎实理解将使你开发高级网络应用时处于有利地位。
第五章。为您的 UI 模型化数据结构
准确表示您系统的数据对于每个应用程序都至关重要。能够以逻辑和组织的方式访问和操作数据集对于创建可维护的代码库是必不可少的。
Ext JS 拥有一个全面的数据包,这使得这项任务变得非常简单。它具有大量功能,包括:
-
数种数据字段类型
-
自动值转换
-
验证规则
-
关联,包括一对一、一对多和多对多
-
抽象的读写机制,包括 AJAX、LocalStorage 和 REST
在本章中,我们将演示如何创建业务仪表板应用程序所需的数据结构,如何将这些数据模型通过关联连接起来,以及如何从后端读取和写入数据。
以下图表展示了我们将代表的应用程序中的数据实体及其关系:

主要关系如下:
-
一个 位置 可以包含一个或多个 产品
-
一个 产品 可以位于一个或多个 位置
-
一个 产品 可以有多个 销售
-
一个 销售 可以涉及一个或多个 产品
-
一个 用户 可以有多个 销售
-
一个 用户 可以有多个 消息
定义模型
数据模型定义为常规的 Ext JS 类,并应扩展 Ext.data.Model 类。模型类通常位于应用程序的模型文件夹中,并且可以有任意数量的子命名空间,以允许对相关模型进行分组。
我们将首先在 model 文件夹中名为 Product.js 的文件中定义我们的产品模型:
Ext.define('BizDash.model.Product', {
extend: 'Ext.data.Model',
fields: [
{
name: 'Name',
type: 'string'
},
{
name: 'Description',
type: 'string'
},
{
name: 'Quantity',
type: 'int'
},
{
name: 'Price',
type: 'float'
}
]
});
为了节省一些按键,您可以使用 Sencha Cmd 在命令行中生成模型。有关如何操作的详细信息,请参阅第四章,构建 Ext JS 应用程序架构,第四章。
字段
产品模型中的 fields 属性定义了附加到模型每份数据的名称和类型。fields 数组中的每个对象都是 Ext.data.Field 类的配置对象,该类提供了各种选项,用于解释和存储字段的方式。
主要配置如下:
-
name:这是字段的名称。在从数据源读取数据并稍后检索时用作映射键。 -
type:这是分配的值应解析成的数据类型。可能的值包括string、int、float、date、boolean和auto。如果设置为auto,则不会进行自动转换。 -
mapping:这允许从与字段名称不匹配的属性中提取字段的值。
字段验证
数据结构的验证逻辑可以直接嵌入到定义中,从而确保相同的逻辑不会散布在您的应用程序的各个部分。这可以通过 validators 选项来实现,该选项允许您定义应应用于每个字段的验证规则。
我们可以向产品模型添加验证规则以确保每个字段的数据正确。我们通过定义一个对象来完成此操作,其中每个键都引用一个字段的名称。此属性的值可以是一个简单的字符串,指代验证类型的名称,一个配置验证的对象,或者一个对象数组,以便可以应用多个规则。
下面的代码片段显示了用于以这种方式验证字段的配置:
-
Name字段通过存在(即不为空)并且至少有三个字符长是有效的 -
Quantity字段必须存在 -
Price字段大于 0
...
validators: {
Name: [
{
type : 'presence'
},
{
type : 'length',
min : 3
}
],
Quantity: 'presence',
Price: {
type: 'range',
min: 0
}
}
...
type 配置定义了我们想要应用的规则:默认情况下,可能的选项有 presence、length、format、inclusion、exclusion、range 和 email。每个选项的详细信息可以在 Ext.data.validator.* 命名空间中找到。
添加的任何其他属性都用作选项来定制特定的验证器;例如,min 定义了 Name 必须具有的最小长度。
我们可以使用 getValidation 方法在模型实例(有时称为 record)上应用验证规则。此方法将返回 Ext.data.Validation 类的实例,然后我们可以查询它以确定哪些字段无效并检索错误消息。
自定义字段类型
尽管 Ext JS 有五种字段类型可以覆盖大多数场景,但最新版本引入了创建自己的自定义字段类型的能力,该类型可以内置转换、验证和序列化。此功能可以帮助减少具有相同字段类型的模型之间的代码重复。
在我们的产品模型中,我们为 Price 字段定义了一个验证规则,以确保它具有正值。我们必须将此规则应用到每个包含货币值的字段的所有模型上。这将是一个理想的候选者,用于创建一个自定义字段类型,以便可以共享此配置。
自定义字段类型声明的方式与其他类相同,应扩展 Ext.data.field.Field 类(或其子类之一,如果您想基于其现有功能进行构建)。我们将扩展 Ext.data.field.Number 类型,它为我们提供了确保值是数字的逻辑。
我们添加到产品模型的验证规则可以添加到此类中:
Ext.define('BizDash.model.field.Money', {
extend: 'Ext.data.field.Number',
alias: 'data.field.money',
validators: [
{
type: 'range',
min : 0
}
],
getType: function() {
return 'money';
}
});
现在可以通过将任何字段指定为类型 money 来使用此自定义字段类型。产品模型的 Price 字段将被重写如下,并且将自动应用我们指定的验证规则:
...
{
name: 'Price',
type: 'money'
}
...
自定义数据转换器
字段的值可以在存储之前自动处理,以便根据其他字段的值对其进行修改,或者将其值解析为不同的类型;例如,将接收到的货币值“USD10.00”拆分为其两个组成部分:货币和值。
实现这一点的第一种方法是向字段的配置中添加一个convert属性,并为其分配一个函数。这个函数将接收字段的值以及它所存储的记录实例,并应该返回要存储的处理后的值。
应该注意的是,记录实例可能根据字段填充的顺序而不完整,因此不能保证其他字段值将可用。
以下示例展示了我们如何实现一个StockValue字段,该字段使用当前的Quantity和Price值来计算股票的总价值:
{
name : 'StockValue',
type : 'money',
convert: function(val, rec) {
return rec.get('Quantity') * rec.get('Price');
},
depends: ['Price', 'Quantity']
}
我们还包含了depends配置,它在我们使用的StockValue字段和其convert函数中的字段之间创建了一个依赖关系。这意味着当Price或Quantity字段更新时,StockValue字段会重新计算,确保一切保持同步。
另一种方法是使用calculate选项,它的工作方式非常相似,但设计用于仅用于计算字段而不是操作实际值。我们可以使用calculate重写StockValue字段,如下所示:
{
name : 'StockValue',
type : 'money',
calculate: function(data) {
return data.Quantity * data.Price;
}
}
calculate函数接受一个包含记录数据对象的参数,可以用来访问其他字段。与convert函数一样,它应该返回要存储的值。
通过使用calculate,会根据函数的内容自动确定依赖关系。因此,不需要显式的depends配置。
与存储一起工作
存储是一组模型实例的集合,允许对这些模型进行操作(例如,排序、过滤、搜索等)。它们还提供了一个与后端交互的平台。许多 Ext JS 组件可以绑定到数据存储,并处理许多响应存储中数据变化的管道工作。
在本节中,我们将讨论如何构建一个简单的存储以及如何对其进行简单的数据操作;如何使用链式存储创建数据集的不同视图;最后,如何使用 TreeStores 存储层次数据。
简单存储
要定义一个存储,你必须扩展Ext.data.Store类,并使用一个model类来配置它,该类将包含一个集合。以下存储定义显示了一个包含用户记录集合的存储:
Ext.define('BizDash.store.Users', {
extend: 'Ext.data.Store',
model: 'BizDash.model.User'
});
我们现在可以将我们的存储添加到Application.js文件中的stores配置选项。这将导致文件被加载并自动实例化,使其可以通过Ext.getStore方法访问。然后我们可以使用add方法添加一些示例数据:
// Application.js
stores: [
'Users'
],
launch: function() {
var usersStore = Ext.getStore('Users');
usersStore.add([
{
Name : 'John',
Email : 'john@gmail.com',
TelNumber: ' 0330 122 2800',
Role : 'Administrator'
},
{
Name : 'Sarah',
Email : 'sarah@hotmail.com',
TelNumber: ' 0330 122 2800',
Role : 'Customer'
},
{
Name : 'Brian',
Email : 'brian@aol.com',
TelNumber: ' 0330 122 2800',
Role : 'Supplier'
},
{
Name : 'Karen',
Email : 'karen@gmail.com',
TelNumber: ' 0330 122 2800',
Role : 'Administrator'
}
]);
}
现在我们有一个填充了数据的存储,我们可以开始探索各种查询和操作数据的方法。
存储统计
我们经常需要知道存储中记录的数量,这可以通过getCount方法轻松完成:
usersStore.getCount() // returns 4
检索记录
获取记录实例的最简单方法是基于其(基于 0 的)索引访问它。为此,我们使用getAt方法,它返回该位置的Ext.data.Model子类(如果没有找到则返回 null):
var userRecord = usersStore.getAt(0); // { Name: "John" ... }
查找特定记录
为了根据字段的值查找特定记录,我们使用findRecord方法,其最简单形式接受一个字段名称和要匹配的值。这将返回第一个匹配的记录,如果未找到则返回 null:
var userRecord = usersStore.findRecord('Name', 'Karen');
默认情况下,这将搜索所有记录,并将搜索值查找在记录值的开头。它将不区分大小写,并允许部分匹配。这些选项可以通过将以下参数传递给findRecord方法来更改:
-
字段名称(字符串):要匹配的字段
-
搜索值(字符串/数字/日期):要查找的值
-
起始索引(数字):开始搜索的索引
-
任意匹配(布尔值):如果匹配搜索值在任意位置(而不仅仅是开头)
-
区分大小写(布尔值):如果匹配大小写
-
精确匹配(布尔值):如果匹配整个值
如果你希望检索记录的索引而不是实际的记录实例,可以使用find方法,它将返回记录的位置,如果未找到则返回-1。
复杂搜索
如果你的搜索条件比简单的字段匹配更复杂,你可以使用自己的自定义匹配函数来引入多个标准。例如,你可能想通过名称和电话号码查找用户。
你可以使用findBy方法做到这一点,传递一个函数,该函数将有一个参数,即record实例,如果被认为匹配则返回 true,如果不匹配则返回 false。此函数将为存储中的每个记录执行一次,直到找到匹配项:
var userRecord = usersStore.findBy(function(record){
return record.get('Name') === 'John' && record.get('TelNumber') === '0330 122 2800';
});
筛选商店
商店可以在任何时候通过类似的基本或复杂查询进行筛选。这样做,将导致商店仅暴露其原始数据集的子集。可以在任何时候移除过滤器以再次恢复完整的数据集。
我们可以使用addFilter方法通过配置筛选商店,它接受单个Ext.util.Filter实例或Ext.util.Filter实例数组或配置对象。这些应指定正在筛选的字段名称和要比较的值。以下示例筛选商店以仅包含名为“Brian”的用户:
console.log(usersStore.getCount()); // 4
usersStore.addFilter({
property: 'Name',
value: 'Brian'
});
console.log(usersStore.getCount()); // 1
类似于findBy方法,我们还可以使用一个允许构建更复杂查询的函数进行筛选。以下示例显示了商店通过名称和电子邮件进行筛选:
console.log(usersStore.getCount()); // 4
usersStore.filterBy(function(record){
return record.get('Name') === 'John' && record.get('Email') === 'john@swarmonline.com';
});
console.log(usersStore.getCount()); // 1
当商店被筛选时,所有查询操作(find、getAt等)都在筛选后的数据集上执行,而不会搜索任何筛选项。要使商店回到未筛选状态,只需调用clearFilter方法:
console.log(usersStore.getCount()); // 1
usersStore.clearFilter();
console.log(usersStore.getCount()); // 4
基于配置的筛选
所有的前述示例都显示了存储程序化地进行过滤。当配置存储时,也可以定义一个默认过滤器,并将其应用于添加到存储中的所有新记录。这可以是一个完整的Ext.util.Filter配置对象,包含简单的属性/值组合或更复杂的filterFn:
Ext.define('BizDash.store.Users', {
extend: 'Ext.data.Store',
model: 'BizDash.model.User',
filters: [
{
property: 'Name',
value: 'John'
}
]
});
对存储进行排序
我们还可以通过使用sort或sortBy方法来更改存储记录的排序顺序。与过滤和查找类似,这些方法允许在单个或多个字段中进行简单排序,或者使用函数进行更复杂的排序。请注意,如果没有提供排序选项,记录将保持添加时的顺序。
对名称进行简单排序可以如下所示:
console.log(usersStore.getAt(0).get('Name')); // John
usersStore.sort('Name', 'ASC');
console.log(usersStore.getAt(0).get('Name')); // Brian
console.log(usersStore.getAt(3).get('Name')); // Sarah
usersStore.sort('Name', 'DESC');
console.log(usersStore.getAt(0).get('Name')); // Sarah
console.log(usersStore.getAt(3).get('Name')); // Brian
要执行更复杂的排序,你可以提供一个对象来配置Ext.util.Sorter类的实例。以下示例通过每个用户的姓名的逆序(例如,nhoJ,haraS)进行排序:
usersStore.sort({
sorterFn: function(a, b) {
var aName = a.get('Name').split('').reverse().join(''),
bName = b.get('Name').split('').reverse().join('');
return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0));
},
direction: 'ASC'
});
基于配置的排序
再次强调,我们可以在添加每条新记录后重新应用默认排序器。这应该是一个Ext.util.Sorter配置对象:
Ext.define('BizDash.store.Users', {
extend: 'Ext.data.Store',
model: 'BizDash.model.User',
sorters: [
{
property: 'Name',
direction: 'DESC'
}
]
});
分组
存储的记录也可以通过特定的字段或字段的组合进行分组,这在以分组网格显示数据时非常有用。我们使用group方法如下,传入Role字段作为分组字段,以及ASC来确定组的排序方向:
usersStore.group('Role', 'ASC');
通过调用此方法,存储保持不变。这意味着我们可以像以前一样访问所有记录,但现在它还提供了对Ext.util.GroupCollection实例的访问,这是一个包含每个实例中一组分组记录的Ext.util.Group实例集合。
我们可以使用getGroups方法来查询分组数据:
var groups = usersStore.getGroups();
console.log(groups.getCount()); // 3 groups: Administrator, Supplier, Customer
console.log(groups.getAt(0).getGroupKey()); // Administrator
console.log(groups.getAt(1).getGroupKey()); // Customer
console.log(groups.getAt(2).getGroupKey()); // Supplier
console.log(groups.getAt(0).getCount()); // 2
console.log(groups.getAt(0).getAt(0).get('Name')); // John
console.log(groups.getAt(0).getAt(1).get('Name')); // Karen
除了接受这些参数外,我们还可以传递一个Ext.util.Grouper配置对象来定义更复杂的分组设置。在以下示例中,我们根据每个用户的电子邮件地址域进行分组(注意,我们使用clearGrouping方法来重置任何现有的分组):
usersStore.clearGrouping();
usersStore.group({
groupFn: function(rec){
var email = rec.get('Email'),
emailSplit = email.split('@'),
domain = emailSplit[1];
return domain;
},
direction: 'DESC'
});
groups = usersStore.getGroups();
console.log(groups.getCount()); // 3 groups: hotmail.com,
//gmail.com, aol.com
console.log(groups.getAt(0).getGroupKey()); // hotmail.com
console.log(groups.getAt(1).getGroupKey()); // gmail.com
console.log(groups.getAt(2).getGroupKey()); // aol.com
console.log(groups.getAt(0).getCount()); // 1
console.log(groups.getAt(0).getAt(0).get('Name')); // Sarah
基于配置的分组
分组也可以通过在存储定义中使用groupField和groupDirection选项一起,或者单独使用grouper选项来完成。以下代码片段显示了使用每种组合在Role上对存储进行分组:
Ext.define('BizDash.store.Users', {
extend: 'Ext.data.Store',
model: 'BizDash.model.User',
grouper: [
{
property: 'Role',
direction: 'ASC'
}
]
});
此外,还有另一个示例:
Ext.define('BizDash.store.Users', {
extend: 'Ext.data.Store',
model: 'BizDash.model.User',
groupField: 'Role',
groupDir: 'ASC'
});
连锁存储
连锁存储是 Ext JS 5 的新增功能,允许我们创建对同一底层数据存储的不同视图,而不会影响基础数据或其他连锁存储。
假设我们想在usersStore中为每个角色显示数据,我们希望一个网格连接到usersStore并带有过滤器Role=Administrator,下一个网格带有Role=Supplier,依此类推。不幸的是,在 Ext JS 5 之前,无法仅使用一个存储来完成此操作,因此,我们必须创建多个存储,包含相同数据的多个副本,并分别管理它们。链式存储解决了这个问题,并允许我们附加到基本数据存储,并应用任何过滤器或排序器到数据的视图中,而不会影响基本存储或任何其他相关联的链式存储。
下面的图解解释了这个概念:

关于链式存储的关键事项包括:
-
记录实例在所有链式存储和基本存储之间共享
-
对任何记录所做的任何更新都将传播到所有相关联的存储。
回到我们的角色示例,我们现在可以通过定义三个链式存储来解决此问题,这些存储可以绑定到单独的网格或数据视图中。
我们使用source配置选项将它们链接到基本数据存储,该选项可以接受存储实例或存储 ID。然后我们可以向链式存储添加任何排序器或过滤器,就像对常规存储所做的那样。
我们在store/users/文件夹中创建这些存储,以保持事物有序:
// store/users/Admins.js
Ext.define('BizDash.store.users.Admins', {
extend: 'Ext.data.ChainedStore',
config: {
source: 'Users',
filters: [
{
property: 'Role',
value: 'Administrator'
}
]
}
});
// store/users/Customers.js
Ext.define('BizDash.store.users.Customers', {
extend: 'Ext.data.ChainedStore',
config: {
source: 'Users',
filters: [
{
property: 'Role',
value: 'Customer'
}
]
}
});
// store/users/Suppliers.js
Ext.define('BizDash.store.users.Suppliers', {
extend: 'Ext.data.ChainedStore',
config: {
source: 'Users',
filters: [
{
property: 'Role',
value: 'Supplier'
}
]
}
});
然后,我们在Application.js中的stores配置中添加新的存储,并为每个创建一个实例,查看每个实例的记录数:
var adminStore = Ext.create('BizDash.store.users.Admins');
var customerStore = Ext.create('BizDash.store.users.Customers');
var supplierStore = Ext.create('BizDash.store.users.Suppliers');
console.log(usersStore.getCount()); // 4
console.log(adminStore.getCount()); // 2
console.log(customerStore.getCount()); // 1
console.log(supplierStore.getCount()); // 1
TreeStores
Ext.data.TreeStore类是一个专业存储,它扩展自常规的Ext.data.Store类,用于管理层次化数据。此类存储必须用于绑定到树面板和其他组件,其中数据需要具有层次结构。
TreeStores 的创建方式与常规存储相同,只有一个主要区别。TreeStore 管理的模型集合必须扩展自Ext.data.TreeModel类,而不是通常的Ext.data.Model。
Ext.data.TreeModels
TreeStore 的模型必须扩展Ext.data.TreeModel类的原因是每个模型实例都必须添加额外的属性和方法,以便正确管理模型的层次结构,并允许树组件正确显示它们。这些额外的属性和方法来自Ext.data.NodeInterface类,其成员都应用于 TreeStore 中的每个模型实例。
我们将通过首先定义将要存储的模型来创建一个简单的树示例。我们的数据将代表我们应用程序的导航结构,并形成我们菜单的基础:
Ext.define('BizDash.model.NavigationItem', {
extend: 'Ext.data.TreeModel',
fields: [
{
name: 'Label',
type: 'string'
},
{
name: 'Route',
type: 'string'
}
]
});
如果你需要在你的 Application.js 文件中使用这个类,创建这个模型的实例并检查其内容。你会发现模型现在有超过 20 个额外的数据字段。这些字段都用于描述每个节点以实现各种目的,例如跟踪其位置、状态和外观。
默认情况下,这些字段的 persist 配置设置为 false,因此它们不会包含在任何由它们发起的保存操作中。

如果你进一步查看,你还会看到添加到模型中的许多额外方法。这些方法可以用来管理节点及其子节点,遍历树结构,并查询其在层次结构中的位置。其中一些更有用的方法在此处详细说明:
-
appendChild: 这会将指定的节点(或节点集合)添加为当前节点的最后一个子节点 -
insertChild: 这将在指定位置插入新的节点 -
removeChild: 这将从子节点集合中移除指定的节点 -
eachChild: 这将对每个子节点执行一个函数 -
findChild: 这将找到第一个匹配给定属性/值的子节点 -
isLeaf: 这将确定当前节点是否为没有子节点的叶子节点
创建一个 TreeStore
我们将创建一个简单的 TreeStore,名为 BizDash.store.Navigation,它将包含 BizDash.model.NavigationItem 模型实例的集合。此存储库扩展了基类 Ext.data.TreeStore:
Ext.define('BizDash.store.Navigation', {
extend: 'Ext.data.TreeStore',
model: 'BizDash.model.NavigationItem'
});
我们将此存储库包含在 Application.js 文件中的 stores 数组中,它将自动加载和实例化,并且可以通过调用 Ext.getStore('Navigation') 来访问。
填充 TreeStore
TreeStores 必须始终有一个 根节点,它是存储中所有子节点的根父节点。这个节点通常总是隐藏的,实际上在树视图中从未真正显示。为了以编程方式(即不是从外部数据源——我们将在下一节中讨论这一点)填充我们的 TreeStore,我们可以使用 setRoot 方法添加完整的层次结构,或者使用 appendChild 或 insertChild 方法单独添加子节点。
我们将首先使用 setRoot 方法,该方法从根节点向外构建 NavigationItem 实例的层次结构:
var navigationStore = Ext.getStore('Navigation');
navigationStore.setRoot({
Label : 'Root',
children: [
{
Label: 'Home',
Route: '/home'
},
{
Label : 'Users',
Route : '',
children: [
{
Label: 'Manage Users',
Route: '/manage-users',
leaf : true
},
{
Label: 'Add User',
Route: '/add-user',
leaf : true
}
]}
]
});
这将产生以下结构:

注意,我们自己的字段(Label 和 Route)与一些 Ext.data.NodeInterface 字段(子节点和叶子节点)结合使用;我们这样做是为了表示节点之间的父子关系,以及表示哪些节点没有子节点。
一旦存在根节点,我们也可以开始使用 appendChild 方法添加节点,如下面的代码片段所示:
// append a node at the same level as 'Home' and 'Users'
navigationStore.getRoot().appendChild({
Label: 'Orders',
Route: '/orders'
});
// find the 'Users' node and append a node to it
navigationStore.getRoot().findChild('Label', 'Users', true).appendChild({
Label: 'Import Users',
Route: '/import-users'
});
将数据放入你的应用程序中
到目前为止,我们只处理了硬编码的数据,而没有处理从外部源加载数据和保存数据的真实世界示例,无论是 REST 端点、本地数据库还是第三方 API。Ext JS 支持多种不同的方式来帮助加载数据和持久化,包括 AJAX 和 LocalStorage。
Ext.Ajax
虽然这与存储间接相关,但我们首先将更一般地讨论 AJAX 请求以及我们如何执行它们以调用服务器后端。
Ext.Ajax是Ext.data.Connection类的单例实例,它为我们提供了一个非常简单的接口,用于向服务器发出 AJAX 调用并处理响应。我们将主要关注request方法,它启动此调用并允许我们指定如何进行调用。
简单 AJAX 调用
我们将首先演示如何向一个静态 JSON 文件发出简单的 AJAX 请求。我们只需向请求方法传递一个配置对象,告诉框架在哪里发出 AJAX 请求,在这种情况下指定url属性:
Ext.Ajax.request({
url: 'user.json'
});
如果我们在 Ext JS 应用的控制台中运行此代码,我们应该在网络选项卡中看到请求正在发出。目前,我们正在忽略给我们提供的响应,所以现在我们将包括一个回调函数来处理接收到的数据。
如你所知,AJAX 调用是异步的,这意味着在请求进行时,其余的代码将继续执行,而不会等待它完成。这意味着我们必须在回调函数中处理响应,该函数将在收到响应时执行。我们通过指定success属性来实现这一点,该属性的函数将接收两个参数:一个响应对象和一个选项对象。
在我们的成功处理程序中,我们将解码收到的 JSON 字符串并将输出记录到控制台:
Ext.Ajax.request({
url: 'user.json',
success: function(response, options){
var user = Ext.decode(response.responseText);
console.log(user);
}
});
处理错误
显然,我们不能仅仅依赖于所有 AJAX 请求都成功完成的童话案例,所以我们必须包括一些替代方案,以防请求失败(即返回非 200 响应代码)。我们可以通过指定failure配置并定义一个在请求失败时将执行的函数来实现这一点。以下示例在发生错误时控制台记录响应状态代码:
Ext.Ajax.request({
url: 'user.json',
success: function(response, options){
var user = Ext.decode(response.responseText);
console.log(user);
},
failure: function(response, options){
console.log('The request failed! Response Code: ' + response.status);
}
});
你可以通过修改url属性为一个不存在的地址来尝试此操作,以强制产生 404 错误。
其他有用的配置
你可以向request方法传递很多配置选项。以下是一些这些选项的简要说明:
-
params: 这是一个对象,其键/值对将随请求一起发送 -
method: 这是发送请求的方法(如果没有params,默认为GET,如果有则为POST) -
callback: 在请求之后,无论成功与否,都会调用这里定义的函数 -
timeout: 这定义了请求的超时时间,以秒为单位 -
headers: 这定义了随请求一起发送的头部信息
代理
现在我们已经讨论了执行简单的 AJAX 请求,这些请求可以愉快地用来填充存储,我们将继续解释代理,它为我们提供了简单机制,使我们的存储能够与数据源通信。
存储或模型可以通过代理进行配置,该代理提供了一层抽象,覆盖了每个可能的数据源的具体细节(无论是外部服务器还是 LocalStorage)。这种抽象允许我们提供简单的配置,并让存储负责处理与数据源通信的复杂性。
代理与两种其他类类型相关联:Reader和Writer。Reader类负责解释接收到的数据并正确解析它,以便它可以转换为模型实例。另一方面,Writer类负责收集要保存的数据。
以下图表显示了这些类是如何相互关联的:

AJAX 代理
最常见的代理类型是 AJAX 代理(Ext.data.proxy.Ajax),它允许我们通过 AJAX 调用将数据加载和保存到服务器端点。我们将向用户存储添加一个 AJAX 代理来从简单的 JSON 文件加载数据:
Ext.define('BizDash.store.Users', {
extend: 'Ext.data.Store',
model: 'BizDash.model.User',
proxy: {
type : 'ajax',
url : 'users.json',
reader: {
type : 'json',
rootProperty: 'rows'
}
}
});
让我们分解这段代码。我们首先添加一个proxy配置选项和一个config对象。我们指定我们想要的proxy类型(我们很快会讨论其他类型)——在这种情况下是一个 AJAX 代理,相当于Ext.data.proxy.Ajax类。接下来,我们指定从哪里加载数据的url。最后,我们通过给代理一个reader配置来告诉代理如何解释结果。类型告诉它我们将接收 JSON 数据,因此我们想使用Ext.data.reader.Json类。rootProperty告诉读取器在接收到的 JSON 对象中查找数据记录的哪个属性。
此配置将从包含以下数据的users.json文件加载数据:
{
"success": true,
"results": 4,
"rows": [
{
"Name" : "John",
"Email" : "john@gmail.com",
"TelNumber": "0330 122 2800",
"Role" : "Administrator"
},
{
"Name" : "Sarah",
"Email" : "sarah@hotmail.com",
"TelNumber": "0330 122 2800",
"Role" : "Customer"
},
{
"Name" : "Brian",
"Email" : "brian@aol.com",
"TelNumber": "0330 122 2800",
"Role" : "Supplier"
},
{
"Name" : "Karen",
"Email" : "karen@gmail.com",
"TelNumber": "0330 122 2800",
"Role" : "Administrator"
}
]
}
我们现在可以将用户的加载方法称为存储,并且它将向users.json文件发起一个 AJAX 请求,并使用四个记录填充自身。以下代码将在完成加载后加载存储并记录加载的记录:
Ext.getStore('Users').load(function(records, operation, success){
console.log(Ext.getStore('Users').getCount()); // 4
console.log(records); // [ ...record instances... ]
});
LocalStorage 代理
另一种类型的代理是localstorage代理,它允许我们在浏览器中加载和保存 LocalStorage 中的数据。配置存储与 LocalStorage 通信非常简单,并且它遵循与 AJAX 代理相同的模式,如下所示:
Ext.define('BizDash.store.Users', {
extend: 'Ext.data.Store',
model: 'BizDash.model.User',
proxy: {
type: 'localstorage',
id : 'users'
}
});
再次,我们指定了代理类型,在这种情况下,将使用Ext.data.proxy.LocalStorage类。我们还指定了一个id,它将用于识别 LocalStorage 中属于此存储的项目。这个id必须在所有代理中是唯一的。
我们将首先演示保存记录,以便在 LocalStorage 中有一些数据可以加载。我们可以在添加一些记录后通过调用sync方法来保存存储的记录:
Ext.getStore('Users').add([
{
Name : 'John',
Email : 'john@gmail.com',
TelNumber: ' 0330 122 2800',
Role : 'Administrator'
},
{
Name : 'Sarah',
Email : 'sarah@hotmail.com',
TelNumber: ' 0330 122 2800',
Role : 'Customer'
},
{
Name : 'Brian',
Email : 'brian@aol.com',
TelNumber: ' 0330 122 2800',
Role : 'Supplier'
},
{
Name : 'Karen',
Email : 'karen@gmail.com',
TelNumber: ' 0330 122 2800',
Role : 'Administrator'
}
]);
Ext.getStore('Users').sync();
如果你打开开发者工具的资源选项卡并检查 LocalStorage 区域,你会看到基于我们提供的id的键的记录。
存储的项有三种类型:
-
<id>-counter,包含存储记录的数量 -
<id>,包含存储记录 ID 的逗号分隔列表 -
每条记录的 JSON 编码数据都存储在一个键中,其格式为
<id>-<Record ID>

现在我们已经将记录保存在 LocalStorage 中,我们可以调用存储的load方法,就像我们使用 AJAX 代理做的那样,以检索这些已保存的记录并重新填充存储。
REST 代理
许多 API 都是 RESTful 的,因此 Ext JS 提供了一个代理,使得与它们的集成变得极其简单。通过定义 REST 代理,我们的 CRUD 请求将执行到具有正确方法类型的端点。我们可以通过将类型更改为rest来更新我们的用户存储以使用 REST 代理(Ext.data.writer.Rest类):
proxy: {
type : 'rest',
url : 'users',
reader: {
type : 'json',
rootProperty: 'rows'
}
}
现在我们有了我们的存储,通过使用 REST 代理,我们可以更新记录之一并同步存储,看到执行检索记录的GET请求和执行更新数据的PUT请求:
Ext.getStore('Users').getAt(0).set('Email', 'john@hotmail.com');
Ext.getStore('Users').sync();
这里是它的截图:

数据关联
我们应用程序的数据模型之间始终存在关联,这些关联必须在我们的客户端应用程序中表示,以便它们可以轻松且一致地进行操作。Ext JS 提供了在模型类型之间建模一对一、一对多和多对多关系的功能。
一对多
当单个实体拥有多个不同类型的实体时,存在一对一关系。例如,一个作者拥有许多书籍或一个食谱拥有许多成分。在我们的示例项目中,我们将模拟用户和销售之间的关联——一个用户可以有多个销售,而每个销售只有一个用户。

配置代理和数据源
我们将首先定义一个代理和简单的静态数据源,以便将我们的模型连接起来,这样它们就可以被填充。我们使用以下代码指向两个简单的 JSON 文件来完成此操作:
Ext.define('BizDash.model.Sale', {
extend: 'Ext.data.Model',
...
proxy: {
type : 'ajax',
url : 'sale.json',
reader: {
type: 'json'
}
}
...
});
// sale.json
[
{
"id": 1,
"userId": 1,
"productId": 1,
"Date" : "2014-08-04T14:41:17.220Z",
"Quantity" : 1,
"TotalCost": 9.99
},
{
"id": 2,
"userId": 1,
"productId": 1,
"Date" : "2014-08-03T14:41:17.220Z",
"Quantity" : 2,
"TotalCost": 19.98
}
]
Ext.define('BizDash.model.User', {
extend: 'Ext.data.Model',
...
proxy: {
type : 'ajax',
url : 'user.json',
reader: {
type: 'json'
}
}
...
});
// user.json
{
"id" : 99,
"Name" : "Joe Bloggs",
"Email" : "joe@gmail.com",
"TelNumber": " 07777777777",
"Role" : "Salesman"
}
定义关联
现在我们已经建立了基础设施,允许数据加载到每个模型中,我们可以定义这两个实体之间的关系。这可以通过两种方式完成:hasMany和reference。
hasMany 配置
hasMany配置选项是这些关联一直以来的创建方式,并允许我们定义相关的模型,以及每个模型的名称和数据源。这种方法允许对关联的细节有更明确的控制,使其更适合更定制的情况:
Ext.define('BizDash.model.User', {
extend: 'Ext.data.Model',
...
hasMany: [
{
model: 'BizDash.model.Sale',
name: 'sales'
}
],
...
});
hasMany配置接受一个关联定义数组,并应包括一个模型选项,定义关联数据表示的模型名称,以及一个名称选项,该选项将用于访问关联数据。
要通过定义的代理访问关联数据,我们使用用户模型的静态load方法来加载 ID 为1的用户,并指定一个在成功加载时执行的回调函数:
BizDash.model.User.load(1, {
success: function(userRecord) {
}
});
我们现在可以加载与该用户关联的销售记录。我们通过调用sales方法(根据我们在hasMany配置中给出的name配置命名)来完成此操作,这将返回一个包含我们的销售记录实例的Ext.data.Store实例。然后我们调用该存储的load方法,这将访问sale.json文件并返回相关的销售记录。在回调函数中,我们可以看到加载的数据项:
BizDash.model.User.load(1, {
success: function(userRecord) {
userRecord.sales().load(function() {
console.log('User: ', userRecord.get('Name')); // Joe Bloggs
console.log('Sales: ', userRecord.sales().getCount()); // 2
});
}
});
引用配置
在 Ext JS 5 中定义关联的新方法是在模型字段上使用reference配置,这将把模型中的外键链接到相关模型类型。这极大地简化了关联的构建,并且意味着关联的两个方向都可以轻松访问。
以下代码展示了添加到BizDash.model.Sale模型的reference配置,将其链接到BizDash.model.User模型:
Ext.define('BizDash.model.Sale', {
extend: 'Ext.data.Model',
fields: [
...
{
name: 'userId',
type: 'int',
reference: 'BizDash.model.User'
},
...
]
});
我们现在可以使用相同的代码来加载用户及其关联的销售记录:
BizDash.model.User.load(1, {
success: function(userRecord) {
userRecord.sales().load(function(){
console.log('User: ', userRecord.get('Name')); // Joe Bloggs
console.log('Sales: ', userRecord.sales().getCount()); // 2
});
}
});
这种技术还使得访问与用户关联的销售记录变得容易。我们利用生成的getUser方法,这是由于我们创建的引用而添加到销售记录中的。调用此方法将根据销售中定义的外键(userId)加载相关的用户模型:
var saleRecord = Ext.create('BizDash.model.Sale', {
id : 1,
userId : 1,
productId: 1,
Date : new Date(),
Quantity : 1,
TotalCost: 9.99
});
saleRecord.getUser(function(userRecord){
console.log(userRecord.get('Name')) // Joe Bloggs
});
探索请求
每次我们在关联中进行load或getUser调用时,都会向我们的服务器资源发送一个 AJAX 请求。在我们的案例中,我们只有简单的静态 JSON 文件,但在现实生活中,我们会有一个适当的服务器实现,它会根据请求的销售或用户返回正确的数据。如果我们查看开发者工具的网络选项卡,我们可以看到对user.json和sale.json文件的请求传递了参数,然后我们可以使用这些参数从我们的服务器数据库检索正确数据。
以下截图显示了 ID 为 1 的用户加载过程。它将 ID 值作为查询字符串参数传递:

当加载与该用户相关的销售时,服务器会收到一个稍微复杂一些的 JSON 字符串,其中包含应用于检索正确数据的筛选器细节。在我们的服务器端代码中,我们会解析这个字符串并在数据库查询中使用它。

多对多
另一种可以建模的关系类型是多对多。在我们的应用中,我们想要定义产品和位置之间的多对多关系,这意味着一个产品可以存储在多个位置,而一个位置也可以拥有多个产品。例如,产品 1 可以位于仓库 1 和仓库 2,而仓库 1 可以存储产品 1 和产品 2。
配置代理和数据源
正如我们之前所做的那样,我们将首先定义一个代理和简单的静态数据源,以便将我们的模型连接起来,从而使它们可以被填充。我们使用以下代码指向两个简单的 JSON 文件:
Ext.define('BizDash.model.Product', {
extend: 'Ext.data.Model',
...
proxy: {
type : 'ajax',
url : 'product.json',
reader: {
type: 'json'
}
}
...
});
// product.json
{
"id" : 1,
"Name" : "Product 1",
"Description": "Product 1 Description",
"Quantity" : 10,
"Price" : 9.99
}
Ext.define('BizDash.model.Locations', {
extend: 'Ext.data.Model',
...
proxy: {
type : 'ajax',
url : 'location.json',
reader: {
type: 'json'
}
}
...
});
// location.json
[
{
"id" : 1,
"Name" : "Location 1",
"Row" : 20,
"Shelf": 10
},
{
"id" : 2,
"Name" : "Location 2",
"Row" : 11,
"Shelf": 22
}
]
定义关联
定义这种关系的最简单方法是在关联(产品与位置)两边的模型中添加manyToMany配置选项,并指定应该位于另一边的模型名称:
Ext.define('BizDash.model.Product', {
extend: 'Ext.data.Model',
...
manyToMany: [
'Location'
]
...
});
Ext.define('BizDash.model.Location', {
extend: 'Ext.data.Model',
...
manyToMany: [
'Product'
]
...
});
加载关联数据
就像一对一关联一样,通过定义这个链接,Ext JS 将在产品和位置模型中分别生成名为locations和products的新方法。这些方法将返回一个Ext.data.Store实例,并包含我们的关联数据:
BizDash.model.Product.load(1, {
success: function(productRecord) {
productRecord.locations().load(function() {
console.log(productRecord.locations().getCount()); // 2
});
}
});
从位置记录中,可以以相同的方式加载相关的产品。
保存数据
现在我们能够成功地将数据加载到我们的应用程序中,我们可以继续将我们在客户端应用程序中对这些数据的更改持久化。在大多数应用程序中,当对数据存储或记录进行更改时,我们希望将此更改持久化回数据源,无论是服务器端数据库还是 LocalStorage 存储。如前所述,Ext JS 为我们提供了Ext.data.writer.Writer类(及其子类)来管理需要写入的字段。
在其最简单和标准的形式中,存储将在调用sync方法时保存所做的任何更改,而无需进一步添加到我们的proxy配置中。
如果我们继续使用我们的用户存储示例,我们可以更新一个record字段,然后调用sync方法:
Ext.getStore('Users').getAt(0).set('Email', 'john@hotmail.com');
Ext.getStore('Users').sync();
如果我们检查开发者工具的网络标签页,我们将看到向我们的users.json文件发送POST请求,传递更改的字段和记录的 ID:

同样,我们可以添加或删除记录,并调用sync方法,这将触发一个等效的POST请求:
Ext.getStore('Users').add({
Name: 'Stuart',
Email: 'stuart@gmail.com',
Role: 'Customer',
TelNumber: ' 0330 122 2800'
});
Ext.getStore('Users').sync();
这是请求的截图:

在记录添加后,前面的请求显示了所有字段作为POST参数被发送:
Ext.getStore('Users').removeAt(0)
Ext.getStore('Users').sync();

当记录被删除时,id字段将被发送到服务器。
CRUD 端点
显然,前面的例子有点不完善,因为我们正在将更改 POST 到静态 JSON 文件。默认情况下,Ext JS 将发送所有更改到代理中指定的 URL,无论它们是添加、更新还是删除。当存在针对这些操作的单独端点时,这通常是不希望的。我们可以轻松配置我们的代理以使用api配置属性联系不同的端点。我们的proxy配置可以重写如下:
proxy: {
type : 'ajax',
url : 'users.json',
api : {
create : 'user-add.php',
read : 'users.json',
update : 'user-update.php',
destroy: 'user-delete.php'
},
reader: {
type : 'json',
rootProperty: 'rows'
}
}
如果我们重新运行添加、更新和删除操作,我们将看到对我们在api配置中指定的每个 PHP 文件的调用。
数据写入者
数据写入器使我们能够控制如何构建存储的数据以发送保存,并为我们提供了各种选项来自定义此过程。默认情况下,代理将使用 JSON 写入器(Ext.data.writer.Json),这将导致要保存的数据被编码为 JSON 字符串。或者,可以使用 XML 写入器(Ext.data.writer.Writer),这将数据作为 XML 传输。
要定义一个写入器,我们使用代理的 writer 配置,并给它一个自己的配置对象。type 属性确定使用哪个写入器类——要么是 JSON,要么是 XML。
我们可以配置我们的用户存储使用 JSON 写入器,如下所示:
proxy: {
...
writer: {
type: 'json'
}
}
现在,我们将查看写入器类提供的几个配置选项,以允许我们自定义如何以及发送哪些数据到服务器。
在我们之前的更新示例中,我们看到了编辑的字段(在这种情况下是 Email 字段)被发送,以及记录的 ID,到服务器。有时,我们可能希望无论是否已编辑,都发送所有字段到服务器。我们可以通过使用 writeAllFields 配置轻松完成此操作,如下所示:
proxy: {
...
writer: {
type: 'json',
writeAllFields: true
}
}
这是此更新请求的截图:

有时,在数据被发送到服务器之前,你可能想要对数据集进行一些额外的处理。我们可以通过使用 transform 配置来定义这个额外的处理步骤,给它一个在写入过程中执行的功能。这个函数将接受将要发送的 data 对象,并应返回处理后的 data 对象。以下示例显示了如何确保给定的电子邮件地址始终是小写:
proxy: {
...
writer: {
type : 'json',
writeAllFields: true,
transform : {
fn: function(data, request) {
data.Email = data.Email.toLowerCase();
return data;
}
}
}
}
摘要
在本章中,我们探讨了如何使用 Ext JS 对应用程序的数据结构进行建模的细节。我们涵盖了:
-
定义模型
-
将数据加载和保存到服务器和 LocalStorage
-
通过关联定义模型之间的关系
-
如何在数据存储中处理数据
我们将在本书的其余部分利用所有这些课程,随着我们的示例应用程序逐渐成形,并开始将其与数据源和数据可视化集成。
第六章。将 UI 小部件组合成完美的布局
布局对于您应用程序的外观和可用性至关重要。UI 小部件可以组合和排列成无数种不同的配置,以创建简单和复杂的应用程序布局。
Ext JS 小部件需要一个布局来管理其在屏幕上的尺寸和位置。该框架提供了大量的不同布局,这些布局提供了简单的配置和灵活性,以生成应用程序。
本章将涵盖与布局最常见的话题。具体如下:
-
组件如何在布局中配合
-
对最常见布局的示例和解释,即:
-
边界布局
-
适应布局
-
HBox 和 VBox 布局
-
-
如何设计对用户屏幕尺寸做出响应的布局
布局及其工作方式
在 Ext JS 中,容器具有布局来管理其子组件的尺寸和位置。传统上,开发者会将对 DOM 元素应用一组 CSS 规则来构建所需的屏幕布局。Ext JS 通过允许我们在容器/组件中定义布局配置,并使用 JavaScript 配置尺寸和位置来为我们处理大部分工作。
默认情况下,容器配置了自动布局,这使得子组件在全宽范围内自然流动,与常规 HTML 页面中的 DIV 元素非常相似。
以下是由框架提供的布局列表。将多个布局组合在一起是可能的,也是常见的;这样可以使子容器或组件适当地定位和调整大小。例如,标签面板(卡片布局)有多个子容器,其中每个容器可能具有不同的布局。每个布局都有不同的配置选项来控制您的应用程序如何渲染。默认情况下,Ext JS UI 小部件已配置了组件布局,您可能需要了解这一点。
-
绝对布局:使用 X 和 Y 坐标,绝对布局固定容器在屏幕上的位置。此布局允许重叠。
-
手风琴布局:手风琴布局提供在屏幕上创建手风琴式面板堆叠的能力。
-
锚点布局:这是一种布局,允许将包含的元素相对于容器的尺寸进行锚定。
-
边界布局:边界布局允许您将容器附加到中央区域的边界,从而提供北、南、东和西区域。此布局具有内置的区域折叠和调整大小的行为。
-
卡片布局:卡片布局提供一组可以前后移动的容器。卡片布局非常适合用于向导式组件或标签式组件。
-
居中布局:居中布局的内容在其容器内居中。
-
列布局:列布局非常适合以多列形式展示您的界面。
-
Fit: 这是一个非常常见的布局,fit 布局将小部件拉伸到父容器的大小和位置。fit 布局中只能有一个项目。
-
HBox: 与列布局类似,这种布局将组件水平排列。它提供了一些有用的配置来拉伸和定位子组件。
-
VBox: 与 HBox 布局类似,这种布局将组件垂直排列,一个接一个。它提供了一些有用的配置来拉伸和定位子组件。
-
Table: 尽管表格在开发者中不太受欢迎,但表格布局仍然可能很有用。表格布局中的内容将被渲染为 HTML 表格。
布局系统的工作原理
正如我们所知,布局负责容器子组件的大小和位置。正确渲染屏幕需要所有子组件的大小和位置都被计算出来,以便更新 DOM。框架通过updateLayout方法来完成这项工作。此方法会递归地遍历所有子组件,并计算适当的定位和大小。
注意
对于熟悉框架先前版本的开发者,updateLayout方法在 Ext JS 4.1 中替换了doLayout方法。
框架通过调用updateLayout方法自动处理大小和位置的调整。例如,当浏览器窗口调整或缩放,或者你添加或删除组件时,框架将执行必要的计算以确保你的组件在屏幕上正确显示。
有一些情况下,手动调用updateLayout是有益的。布局组件可能是一个资源密集型任务,如果你知道你将要对组件进行多次更新,那么考虑将布局批处理成一个可能是有价值的。例如,连续添加三个组件会触发三次对updateLayout的调用(它会递归遍历所有子组件)。通过使用suspendLayout标志,我们可以防止我们的应用程序在准备好之前更新 DOM。当我们准备好时,只需将suspendLayout标志设置为 false,然后手动调用容器的updateLayout方法即可。
组件布局
组件还有一个布局,它定义了它如何定义其内部子项的大小和位置。组件布局是通过使用componentLayout配置选项来配置的。
在大多数情况下,你不需要componentLayout配置,除非你打算编写具有复杂布局要求的自定义组件。
使用边界布局
如果你希望创建具有桌面风格的用户界面体验,那么边界布局就是你的选择。

边框布局是一种面向应用程序的布局,支持多个嵌套面板,可以通过点击区域标题或折叠图标来折叠区域,以及通过点击和拖动区域之间的分隔条来调整区域大小。
我们 BizDash 应用程序的构建块之一将是一个带有边框布局的视口。在这里,我们将学习如何使用可配置的最大区域数量(北部、南部、东部、西部和中心)创建一个简单的边框布局。西部和东部区域将是可折叠的,东部区域将预先折叠。我们将演示南部和西部区域的调整大小。这四个边框将围绕中心区域,无论您的配置如何,中心区域对于边框布局的正常工作都是必需的。
从视口开始
视口将自身渲染到文档的body中,并自动消耗可视区域。它代表整个可视浏览器区域,并自动使用浏览器窗口的 100%宽度和高度(当然,减去地址栏、开发者工具等)。我们的视口将使用边框布局来管理其子容器的尺寸和位置。
在我们的应用程序中,我们在app.js中将autoCreateViewport属性设置为BizDash.view.main.Main。这会自动将我们的主要视图容器设置为视口。
Ext.application({
name: 'BizDash',
extend: 'BizDash.Application',
autoCreateViewport: 'BizDash.view.main.Main'
});
配置边框布局
我们的主要视图采用以下配置:
Ext.define('BizDash.view.main.Main', {
extend: 'Ext.container.Container',
xtype: 'app-main',
controller: 'main',
viewModel: {
type: 'main'
},
layout: {
type: 'border'
},
items: [
{
region: 'north',
margins: 5,
height: 100,
},
{
title: 'West',
xtype: 'panel',
region: 'west',
margins: '0 5 0 5',
flex: 3,
collapsible: true,
split: true,
titleCollapse: true,
tbar: [{
text: 'Button'
}]
bind: {
title: '{name}'
},
html: 'This area is commonly used for navigation, for example, using a tree component.',
{
title: 'Center',
region: 'center',
xtype: 'tabpanel',
items:[{
title: 'Tab 1',
html: 'Content appropriate for the current navigation'
}]
},
{
title: 'East',
region: 'east',
margins: '0 5 0 5',
width: 200,
collapsible: true,
collapsed: true
},
{
title: 'South',
region: 'south',
margins: '0 5 5 5',
flex: .3,
split: true
}]
});
如其名所示,边框布局创建了一个围绕中心组件的组件布局。因此,边框布局的要求之一是必须指定一个项目作为中心。
中心区域,这是为了使边框布局正常工作而必须包含的区域,会自动扩展以消耗布局中其他区域留下的空余空间。它是通过为高度和宽度都预定义了一个弹性值1来实现的。
北部和南部区域采用高度或弹性配置。在我们的应用中,北部区域的高度固定为 100 像素,而南部区域的弹性值为 3。南部和中心区域的高度是基于浏览器窗口中剩余的高度计算的。在这里,南部区域的高度略小于中心区域高度的三分之一。西部和东部区域则采用宽度或弹性配置。
我们通过在所需区域的配置中指定collapsed、collapsible、split和titleCollapse来添加更多功能。它们执行以下操作:
-
collapsed:如果设置为true,则表示该区域将开始折叠(区域需要是Ext.panel.Panel才能折叠) -
collapsible:如果设置为true,则允许用户通过点击添加到标题中的切换工具来展开/折叠面板 -
titleCollapse:如果设置为true,则无论用户在面板标题的哪个位置点击,都会使面板折叠 -
split:如果设置为true,则允许用户通过点击和拖动区域之间的分隔条来调整区域大小。
使用 fit 布局
在 Ext JS 中,fit 布局非常适合您想要一个容器将组件扩展以填充其父容器的情况。fit 布局易于使用且无需配置。
Ext.define('BizDash.view.main.Main', {
extend: 'Ext.container.Container',
xtype: 'app-main',
controller: 'main',
viewModel: {
type: 'main'
},
layout: {
type: 'border'
},
items: [{
...
},
{
title: 'South',
region: 'south',
margins: '0 5 5 5',
flex: .3,
split: true,
layout: 'fit',
items: [{
xtype: 'component',
html: 'South Region'
}]
}]
});
main类有一个边界布局,但南区域需要 fit 布局来对其子面板进行配置。
fit 布局通过在父容器中定义layout配置选项为fit来工作。这告诉 Ext JS 子项应该扩展以填充从其父项可用的整个空间。
值得注意的是,fit 布局只适用于父容器的第一个子项。如果您定义了多个项,第一个将被显示(因为它会扩展到其父项的剩余空间),而其他项将不可见。
使用 HBox 布局
HBox 布局允许您以类似于列布局的方式在容器内水平对齐组件。然而,它更为高级,因为它允许您配置额外的属性,例如列的高度等。

让我们创建一个包含三列的概览小部件来显示事件、消息和笔记:
Ext.define('BizDash.view.dashboard.Overview', {
extend: 'Ext.container.Container',
xtype: 'app-overview',
controller: 'overview',
viewModel: {
type: 'overview'
},
layout: {
type: 'hbox',
align: 'stretchmax'
},
items: [
{
flex: .3,
title: 'Today\'s Events'
},
{
flex: .3,
title: 'Messages'
},
{
width: 200,
title: 'Notes'
}
]
});
定义一个 HBox 布局确保 Ext JS 水平定位每个子项,在我们的仪表板应用程序中呈现出列的样式。
我们在align配置选项上配置了stretchmax,这意味着所有子项将自动拉伸到最高子项的高度。align配置选项控制子项在 HBox 布局中的垂直对齐方式。有效值如下:
-
begin:这是默认值。HBox 布局中的所有项都将垂直对齐到容器的顶部 -
middle:所有项都将垂直对齐到容器的中间(或中心) -
stretch:每个项都将垂直拉伸以适应容器的高度 -
stretchmax:这将垂直拉伸所有项到最大项的高度,创建一个统一的外观,而无需为每个项单独定义高度
VBox 布局与这个布局的配置非常相似,但选项略有不同,因为它们是为了控制水平对齐而设计的。
通过定义高度为 200 并配置align: 'stretchmax',所有其他面板的高度都将拉伸到 200 像素。
HBox 布局中的宽度
HBox 可以有宽度定义的两种方式:固定宽度和弹性宽度。
固定宽度
子容器可以通过在对象中定义宽度配置来固定其宽度。具有固定宽度的容器将保留其宽度尺寸,即使浏览器窗口大小调整也是如此。因此,它们不是流动的。
如果您需要您的布局对窗口大小调整等更加响应,那么就需要一个flex配置。
弹性宽度
flex配置选项在父容器中相对地水平拉伸子项目。例如,考虑一个 flex: 1 的容器和一个 flex: 3 的容器。在这些情况下,剩余父空间的 25%分配给第一个容器,75%的空间分配给另一个。
弹性值按以下方式计算:
((Container Width – Fixed Width of Child Components) / Sum of Flexes) * Flex Value.
将项目打包在一起
对于 HBox 布局的另一个有用配置选项是pack。
pack配置选项控制子项目如何打包在一起。如果项目没有拉伸到父容器的完整宽度,则可以使用此选项将它们对齐到左侧、中间或右侧。有效值如下:
-
start:这是默认值。它将所有项目对齐到父容器的左侧。 -
center:这将对齐所有项目到容器的中间。 -
end:它将所有项目对齐到容器的右侧。
VBox 布局包含相同的pack配置,但它被设计用来配置子项目的垂直打包。
使用 VBox 布局
VBox 布局与 HBox 布局非常相似。唯一的区别是 VBox 允许你在容器中垂直对齐组件。就像 HBox 布局一样,这个布局是通过设置子组件的固定宽度使用width配置,或者通过使用flex配置自动计算宽度来配置的。

对齐和打包
VBox 布局有一些有用的配置选项,在此处进行描述。
align: 字符串
align配置选项控制子项目在 VBox 布局中的水平对齐方式。有效值如下:
-
begin:这是默认值。VBox 布局中的所有项目在容器中水平对齐到左侧,并使用它们的width配置来定义它们的宽度。 -
middle:所有项目都水平对齐到容器的中间(或中心)。 -
stretch:每个项目都水平拉伸以适应容器的宽度。 -
stretchmax:这将所有项目水平拉伸到最大项目的宽度,创建一个统一的外观,而无需为每个项目单独定义宽度。
pack: 字符串
pack配置选项控制子项目如何打包在一起。如果项目没有拉伸到父容器的完整高度(即没有 flex 值),则可以使用此选项将它们对齐到顶部、中间或底部。有效值如下:
-
start:这是默认值。它将所有项目对齐到父容器的顶部。 -
center:这将对齐所有项目到容器的中间(或中心)。 -
end:这将对齐所有项目到容器的底部。
响应式布局
那些只有桌面电脑和笔记本电脑上才有网络浏览器的日子已经一去不复返了。如今,它们可以在各种硬件上找到,如手机、平板电脑、电视和汽车等。自 2007 年 iPhone 发布以来,一种新的网络类型——移动网络,已经越来越受欢迎,用户的期望也发生了变化。用户期望无论屏幕大小如何,都能访问网络应用并获得出色的体验。
直到 Ext JS 5 的出现,很难满足用户的不同需求。然而,Ext JS 5 已经采纳了流行的响应式设计趋势,为开发者提供了一种根据展示屏幕的大小和分辨率来适应应用布局的实用方法。
Ext.mixin.Responsive 和 Ext.plugin.Responsive
现在有一个新的响应式混合和插件,它添加了一个 responsiveConfig 选项。
在我们的边框布局中,我们希望根据屏幕方向改变区域展示的方式。将以下 规则 的 responsiveConfig 添加到主视图中,告诉应用程序根据屏幕方向将导航面板添加到北部或西部区域:
Ext.define('BizDash.view.main.Main', {
extend: 'Ext.container.Container',
xtype: 'app-main',
controller: 'main',
viewModel: {
type: 'main'
},
layout: {
type: 'border'
},
items: [
{
xtype: 'panel',
bind: {
title: '{name}'
},
html: 'This area is commonly used for navigation, for example, using a tree component.',
width: 250,
split: true,
tbar: [
{
text: 'Button'
}
],
plugins: 'responsive',
responsiveConfig: {
landscape: {
region: 'west'
},
portrait: {
region: 'north'
}
}
},
{
region: 'center',
xtype: 'tabpanel',
items:[
{
title: 'Tab 1',
html: 'Content appropriate for the current navigation'
}]
}]
});
在这个例子中,responsiveConfig 定义了两个规则:一个用于横向,另一个用于纵向。当应用程序满足规则时,该规则对象中定义的配置将被应用到组件上:
responsiveConfig: {
landscape: {
region: 'west'
},
portrait: {
region: 'north'
}
}
例如,如果我们的屏幕方向是横向,那么框架将 region: 'west' 应用到面板上。这等同于以下代码:
{
xtype: 'panel',
bind: {
title: '{name}'
},
html: 'This area is commonly used for navigation, for example, using a tree component.',
width: 250,
split: true,
tbar: [
{
text: 'Button'
}
],
region: 'west'
}
ResponsiveConfig 规则
我们必须定义包含条件或多个条件的规则,根据这些条件应用规则。这些规则可以是任何有效的 JavaScript 表达式,但以下值被认为是范围内的:
-
landscape:当方向为横向或在桌面设备上时返回 true。 -
portrait:当方向为纵向时返回 true,但在桌面设备上始终为 false。 -
tall:如果高度大于宽度,则返回 true。 -
wide:如果宽度大于高度,则返回 true。 -
width:这定义了视口的宽度。 -
height:这定义了视口的宽度。
宽度和高度特别有用,因为这些给我们提供了一种以类似于媒体查询中的断点的方式定义我们自己的值。
以下示例展示了如何创建一个规则,当纵向设备宽度小于 400 像素时将应用此规则:
responsiveConfig: {
'portrait && width < 400': {
...
}
}
摘要
在本章中,我们详细探讨了如何使用 Ext JS 定义屏幕上组件的布局。我们涵盖了:
-
布局管理器是如何工作的
-
我们可用的布局概述
-
如何使用边框、fit 和 HBox/VBox 布局的示例
-
设计响应式布局以应对不同的屏幕尺寸和设备类型
我们将在本书的其余部分利用所有这些课程,因为我们的示例应用程序开始变得生动,并且我们开始将其小部件集成到其中。
在下一章中,我们将演示如何为我们的示例应用程序创建常见的 UI 小部件。这些小部件将利用本章中我们所学到的某些布局。
第七章. 构建常见的 UI 小部件
吸引开发者使用 Ext JS 的最大特点之一是开箱即用的丰富 UI 小部件。它们可以轻松集成,并且每个小部件提供的吸引力和一致性视觉效果也是一个很大的吸引力。没有其他框架能在这一方面竞争,这也是 Ext JS 在大型 Web 应用领域领先的一个巨大原因。
在本章中,我们将探讨 UI 小部件如何适应框架的结构,它们如何相互交互,以及我们如何检索和引用它们。然后,我们将深入探讨组件的生命周期以及它在应用程序生命周期中将要经历的各个阶段。最后,我们将以数据网格、树、数据视图和表单的形式将第一个 UI 组件添加到我们的 BizDash 应用程序中。
UI 小部件的解剖结构
Ext JS 中的每个 UI 元素都扩展自基本组件类Ext.Component。这个类负责将 UI 元素渲染到 HTML 文档中。它们通常由父组件使用的布局来设置尺寸和定位,并参与自动组件生命周期过程。
您可以将Ext.Component的实例想象成用户界面中的一个单独部分,就像您在构建传统 Web 界面时可能会想到 DOM 元素一样。
每个Ext.Component的子类都基于这个简单的事实,并负责生成更复杂的 HTML 结构或组合多个Ext.Component以创建更复杂的界面。
然而,Ext.Component类不能包含其他Ext.Component。要组合组件,必须使用Ext.container.Container类,该类本身扩展自Ext.Component。这个类允许多个组件在其中渲染,并且它们的尺寸和定位由框架的布局类管理(有关更多详细信息,请参阅第六章 Chapter 6,将 UI 小部件组合成完美的布局)。
组件和 HTML
使用组件创建和操作 UI 需要一种与您在创建带有 jQuery 等库的交互式网站时可能习惯的略有不同的思维方式。
Ext.Component类从底层 HTML 提供了一层抽象,使我们能够封装额外的逻辑来构建和操作这个 HTML。这个概念与其他库允许您操作 UI 元素的方式不同,也为新开发者提供了一个需要克服的障碍。
Ext.Component类为我们生成 HTML,我们很少需要直接与之交互;相反,我们操作组件的配置和属性。以下代码和截图显示了简单Ext.Component实例生成的 HTML:
var simpleComponent = Ext.create('Ext.Component', {
html : 'Ext JS Essentials!',
renderTo: Ext.getBody()
});

如你所见,创建了一个简单的 <DIV> 标签,它被赋予了某些 CSS 类和一个自动生成的 ID,并在其中显示了 HTML 配置。
这个生成的 HTML 是由 Ext.dom.Element 类创建和管理的,它包装了一个 DOM 元素及其子元素,为我们提供了许多辅助方法来查询和操纵它。渲染后,每个 Ext.Component 实例都将元素实例存储在其 el 属性中。然后你可以使用这个属性来操纵代表组件的底层 HTML。
如前所述,el 属性将在组件被渲染到 DOM 中之前不会被填充。你应该在 afterrender 事件监听器中将依赖于更改组件原始 HTML 的逻辑放入,或者覆盖 afterRender 方法。
以下示例显示了组件渲染后如何操纵底层 HTML。它将元素的背景色设置为红色:
Ext.create('Ext.Component', {
html : 'Ext JS Essentials!',
renderTo : Ext.getBody(),
listeners: {
afterrender: function(comp) {
comp.el.setStyle('background-color', 'red');
}
}
});
重要的是要理解,深入挖掘和更新 Ext JS 为你创建的 HTML 和 CSS 是一个危险的游戏,当框架尝试自己更新内容时,可能会导致意外结果。通常有一个 框架方式 来实现你想要包含的操纵,我们建议你首先使用它。
我们总是建议新开发者开始时尽量不要与框架过多对抗。相反,我们鼓励他们遵循其约定和模式,而不是强迫它以他们可能以前在开发传统网站和 Web 应用时的方式做事。
组件生命周期
当一个组件被创建时,它会遵循一个重要的生命周期过程,了解这一点很重要,以便了解事情发生的顺序。通过理解这一系列事件,你将更好地了解你的逻辑将适合何处,并确保你在正确的点上对组件有控制权。
创建生命周期
当一个新的组件通过将其添加到现有容器中实例化和渲染到文档时,会遵循以下过程。当一个组件被显式显示(例如,没有添加到父组件,如浮动组件)时,会包含一些额外的步骤。这些步骤在以下过程中用 * 表示。
构造函数
首先,执行类的构造函数,这会依次触发其他所有步骤。通过重写这个函数,我们可以为组件添加任何所需的设置代码。
处理配置选项
下一步要处理的是类中存在的配置选项(有关详细信息,请参阅第二章,掌握框架的构建块)。这涉及到每个选项的 apply 和 update 方法被调用(如果存在),这意味着值现在可以通过 getter 获取。
初始化组件
现在会调用 initComponent 方法,通常用于应用配置并执行任何初始化逻辑。
render
一旦添加到容器中,或者当调用 show 方法时,组件就会被渲染到文档中。
boxready
在这个阶段,组件已经被渲染并由其父布局类布局,并且以初始大小准备好。此事件仅在组件的第一次布局时发生。
activate (*)
如果组件是一个浮动项,则激活事件将会触发,表明该组件是屏幕上的活动组件。当组件被带回焦点时,例如在 Tab 面板中选中标签时,这也会触发。
show (*)
与前一步类似,当组件最终在屏幕上可见时,show 事件会被触发。

销毁过程
当我们从视图中移除组件并想要销毁它时,它将遵循一个销毁序列,我们可以使用这个序列来确保充分清理,以避免内存泄漏等问题。框架会为我们处理大部分清理工作,但重要的是我们也要整理好我们实例化的任何额外事物。
hide (*)
当组件被手动隐藏(使用 hide 方法)时,此事件将会触发,并且可以在此处包含任何额外的隐藏逻辑。
deactivate (*)
与激活步骤类似,当组件变得不活跃时,此事件会被触发。与激活步骤一样,这将在浮动和嵌套组件被隐藏且不再是焦点下的项目时发生。
destroy
这是拆解过程的最后一步,在组件及其内部属性和对象被清理时实现。在这个阶段,最好移除事件处理器、销毁子类,并确保释放任何其他引用。

组件查询
Ext JS 自带一个强大的系统来检索组件引用,称为组件查询。这是一个类似于 CSS/XPath 的查询语法,允许我们针对应用中的广泛集合或特定组件进行定位。例如,在我们的控制器中,我们可能想要找到一个类型为 MyForm 的组件中的“保存”按钮。
在本节中,我们将演示组件查询语法以及如何使用它来选择组件。我们还将详细介绍如何在 Ext.container.Container 类中使用它来限定选择范围。
xtypes
在深入探讨之前,了解 Ext JS 中的 xtypes 概念非常重要。xtype 是 Ext.Component 的简称,允许我们识别其声明性组件配置对象。例如,我们可以使用以下代码创建一个新的 Ext.Component 作为 Ext.container.Container 的子组件,并使用 xtype:
Ext.create('Ext.Container', {
items: [
{
xtype: 'component',
html : 'My Component!'
}
]
});
使用 xtypes 可以在需要时延迟实例化组件,而不是一开始就创建所有组件。
常见的组件 xtypes 包括:
| 类别 | xtypes |
|---|---|
Ext.tab.Panel |
tabpanel |
Ext.container.Container |
container |
Ext.grid.Panel |
gridpanel |
Ext.Button |
button |
xtypes 以与元素类型(例如,div、p、span等)在 CSS 选择器中的作用相同的方式构成了我们的组件查询语法的基石。我们将在以下示例中大量使用这些。
样例组件结构
我们将使用以下样本组件结构——一个包含子标签页面板、表单和按钮的面板——来执行我们的示例查询:
var panel = Ext.create('Ext.panel.Panel', {
height : 500,
width : 500,
renderTo: Ext.getBody(),
layout: {
type : 'vbox',
align: 'stretch'
},
items : [
{
xtype : 'tabpanel',
itemId: 'mainTabPanel',
flex : 1,
items : [
{
xtype : 'panel',
title : 'Users',
itemId: 'usersPanel',
layout: {
type : 'vbox',
align: 'stretch'
},
tbar : [
{
xtype : 'button',
text : 'Edit',
itemId: 'editButton'
}
],
items : [
{
xtype : 'form',
border : 0,
items : [
{
xtype : 'textfield',
fieldLabel: 'Name',
allowBlank: false
},
{
xtype : 'textfield',
fieldLabel: 'Email',
allowBlank: false
}
],
buttons: [
{
xtype : 'button',
text : 'Save',
action: 'saveUser'
}
]
},
{
xtype : 'grid',
flex : 1,
border : 0,
columns: [
{
header : 'Name',
dataIndex: 'Name',
flex : 1
},
{
header : 'Email',
dataIndex: 'Email'
}
],
store : Ext.create('Ext.data.Store', {
fields: [
'Name',
'Email'
],
data : [
{
Name : 'Joe Bloggs',
Email: 'joe@example.com'
},
{
Name : 'Jane Doe',
Email: 'jane@example.com'
}
]
})
}
]
}
]
},
{
xtype : 'component',
itemId : 'footerComponent',
html : 'Footer Information',
extraOptions: {
option1: 'test',
option2: 'test'
},
height : 40
}
]
});
使用 Ext.ComponentQuery 的查询
Ext.ComponentQuery类用于执行组件查询,其中query方法是主要使用的。此方法接受两个参数:一个查询字符串和一个可选的Ext.container.Container实例,用作选择的根(即,只有层次结构中此组件以下的组件将被返回)。该方法将返回一个组件数组或一个空数组,如果没有找到任何组件。
我们将通过几个场景,并使用组件查询来找到一组特定的组件。
基于 xtype 查找组件
正如我们所见,我们使用 xtypes,就像在 CSS 选择器中使用元素类型一样。我们可以使用其 xtype——panel来选择所有Ext.panel.Panel实例:
var panels = Ext.ComponentQuery.query('panel');
我们还可以通过包含一个用空格分隔的第二个 xtype 来添加层次结构的概念。以下代码将选择所有是Ext.panel.Panel类后代的Ext.Button实例:
var buttons = Ext.ComponentQuery.query('panel buttons');
我们也可以使用>字符将其限制为直接是panel后代的按钮。
var directDescendantButtons = Ext.ComponentQuery.query('panel > button');
基于属性查找组件
基于属性的值选择组件很简单。我们使用 XPath 语法来指定属性和值。以下代码将选择具有saveUser动作属性的按钮:
var saveButtons = Ext.ComponentQuery.query('button[action="saveUser"]);
基于 itemIds 查找组件
ItemIds 通常用于检索组件,并且在ComponentQuery类中对性能进行了特别优化。它们只在其父容器内是唯一的,而不是全局唯一,就像id配置一样。要基于 itemId 选择一个组件,我们需要在 itemId 前加上一个#符号:
var usersPanel = Ext.ComponentQuery.query('#usersPanel');
基于成员函数查找组件
也可以根据该组件的函数的结果来识别匹配的组件。例如,我们可以选择所有值有效的文本字段(即,当调用isValid方法返回true时):
var validFields = Ext.ComponentQuery.query('form > textfield{isValid()}');
作用域组件查询
我们之前的所有示例都会搜索整个组件树以找到匹配项,但通常我们可能希望将搜索限制在特定的容器及其后代中。这可以帮助减少查询的复杂性并提高性能,因为需要处理的组件更少。
Ext 容器有三个方便的方法来做这件事:up、down和query。我们将逐一介绍这些方法及其功能。
up
此方法接受一个选择器,并将遍历层次结构以找到单个匹配的父组件。这可以用来找到按钮所属的网格面板,以便对其执行操作:
var grid = button.up('gridpanel');
down
这将返回第一个匹配给定选择器的子组件:
var firstButton = grid.down('button');
查询
query方法与Ext.ComponentQuery.query类似,但它是自动限定在当前容器的作用域内。这意味着它将搜索当前容器的所有子组件,并返回所有匹配的组件作为一个数组。
var allButtons = grid.query('button');
使用树表示分层数据
现在我们已经了解并理解了组件、它们的生命周期以及如何获取它们的引用,我们将继续介绍更具体的 UI 小部件。
树面板组件允许我们以反映数据结构和关系的方式显示分层数据。
在我们的应用程序中,我们将使用树面板来表示我们的导航结构,使用户能够看到应用程序的不同区域是如何链接和组织的。
绑定到数据源
与所有其他数据绑定组件一样,树面板必须绑定到一个数据存储——在这个特定情况下,它必须是一个Ext.data.TreeStore实例或子类,因为它利用了添加到这个专业存储类中的额外功能。
我们将使用在第五章中创建的BizDash.store.Navigation TreeStore 来绑定到我们的树面板。
定义树面板
树面板定义在Ext.tree.Panel类中(它有一个xtype为treepanel),我们将扩展它来创建一个名为BizDash.view.navigation.NavigationTree的自定义类:
Ext.define('BizDash.view.navigation.NavigationTree', {
extend: 'Ext.tree.Panel',
alias: 'widget.navigation-NavigationTree',
store : 'Navigation',
columns: [
{
xtype : 'treecolumn',
text : 'Navigation',
dataIndex: 'Label',
flex : 1
}
],
rootVisible: false,
useArrows : true
});
我们通过使用其storeId配置树绑定到我们的 TreeStore,在这种情况下,是 Navigation。
树面板是Ext.panel.Table类的一个子类(类似于Ext.grid.Panel类),这意味着它必须有一个列配置。这告诉组件显示树的部分值。在一个简单的、传统的树中,我们可能只有一个列显示项目及其子项;然而,我们可以定义多个列,并在每一行显示额外的字段。如果我们显示文件和文件夹,并希望有额外的列来显示每个项目的文件类型和文件大小,这将非常有用。
在我们的示例中,我们只将有一个列,显示标签字段。我们通过使用treecolumn xtype 来实现这一点,它负责渲染树的导航元素。如果没有定义treecolumn,组件将无法正确显示。
treecolumn xtype 的配置允许我们定义要使用的数据模型字段(dataIndex)、列的标题文本(text),以及列应该填充水平空间的事实(flex:参见第六章中的使用 VBox 布局和使用 HBox 布局部分,将 UI 小部件组合成完美布局,了解更多关于这个概念的信息)。
此外,我们将 rootVisible 设置为 false,因此数据的根被隐藏,因为它除了将其他数据组合在一起外没有实际意义。最后,我们将 useArrows 设置为 true,因此带有子项的项目使用箭头而不是 +/- 图标。

显示表格数据
网格组件是开发者选择 Ext JS 的最大原因之一。它的性能、功能和灵活性使其成为一个强大的特性。在本节中,我们将通过创建一个网格来显示我们的产品数据的示例来操作。
产品数据
在我们深入创建网格组件之前,我们必须创建一个数据存储,它将保存我们的网格将显示的产品数据。在第五章,为您的 UI 建模数据结构中,我们定义了表示我们的产品数据的数据模型,但我们没有创建一个产品存储来保存产品模型实例的集合。为此,我们在项目的 store 文件夹中创建了一个名为 Products.js 的新文件,其中包含以下类定义:
Ext.define('BizDash.store.Products', {
extend: 'Ext.data.Store',
model: 'BizDash.model.Product',
autoLoad: true,
proxy: {
type : 'ajax',
url : 'products.json',
reader: {
type : 'json',
rootProperty: 'rows'
}
}
});
这是一个简单的存储定义,唯一的新配置是 autoLoad: true 设置,它将在存储实例化后立即使用定义的代理执行加载。为了使此存储自动实例化,我们将它包含在 Application.js 文件的 stores 配置中。
我们的 products.json 文件包含一些简单的产品数据,如下所示:
{
"success": true,
"rows": [
{
"id" : 1,
"Name" : "Product 1",
"Description": "Product 1 Description",
"Quantity" : 1,
"Price" : 9.99
},
{
"id" : 2,
"Name" : "Product 2",
"Description": "Product 2 Description",
"Quantity" : 5,
"Price" : 2.99
},
{
"id" : 3,
"Name" : "Product 3",
"Description": "Product 3 Description",
"Quantity" : 1000,
"Price" : 5.49
}
]
}
产品网格
要创建一个网格,我们使用 Ext.grid.Panel 组件,它扩展自 Ext.panel.Panel 类,因此可以像简单面板一样使用(例如,它可以有停靠项,给定尺寸等)。
我们将在项目视图文件夹下的一个名为 product 的新文件夹中的 ProductGrid.js 文件中定义我们的产品网格。我们给它一个基本的配置:
Ext.define('BizDash.view.product.ProductGrid', {
extend: 'Ext.grid.Panel',
xtype: 'product-ProductGrid',
store: 'Products',
columns: [
{
text: 'Name',
dataIndex: 'Name'
},
{
text: 'Description',
dataIndex: 'Description',
flex: 1
},
{
text: 'Quantity',
dataIndex: 'Quantity'
},
{
text: 'Price ',
dataIndex: 'Price'
}
]
});
你会认出 Ext.define 结构与我们在整本书中使用的一样,其中定义了类名和父类。xtype 配置允许我们定义一个字符串,当懒加载产品网格时可以使用。
网格组件只有两个必需的配置,我们在前面的示例中已经包含:包含要显示的数据的存储和一个定义显示什么数据的列数组。
存储可以是实际的存储引用或用于查找存储实例的存储 ID——在这种情况下,是我们之前定义的产品存储。
我们的列定义包含一个配置对象数组,这些对象将被用来实例化 Ext.grid.column.Column 类(或其子类,例如 Date、Template、Number 等)。text 属性将定义列的标题文本,而 dataIndex 是映射到存储记录中的字段。在我们的例子中,我们选择显示所有产品的字段。
最后,我们可以在我们的应用程序中包含新的 ProductGrid 并看到它的实际效果。我们必须首先在 Application.js 的 views 数组中引入新的组件:
...
views: [
'product.ProductGrid'
]
...
然后,我们可以在主视图的 items 集合中使用 xtype,将其放置在中心区域:
...
{
region: 'center',
xtype : 'product-ProductGrid'
}
...
重新加载我们的应用程序应该显示一个包含三行数据的网格:

自定义列显示
到目前为止,我们的网格只是简单地显示模型中持有的数据值,这对于简单值来说是可以的,但某些数据将受益于更高级的格式化。
在格式化列值时,有多种实现相同效果的方法。我们将介绍两种主要选项:列渲染器和模板列。
列渲染器
我们将首先通过自定义 数量 列的样式来给我们的网格添加一些颜色,当数量开始变低时。我们将当数量降至 3 或以下时使数字变红,当数量在 7 到 3 之间时变橙。
列 renderer 是一个函数,它允许我们在数据值显示之前对其进行操作(而不影响底层存储的值)。我们向列定义中添加 renderer 属性,并给它一个具有以下参数的函数:
-
value: 绑定模型字段的值
-
metaData: 正在被渲染的单元格的额外属性,例如
tdCls、tdAttr和tdStyle -
record: 当前行的记录
-
rowIndex: 当前行的索引
-
colIndex: 当前列的索引
-
store: 绑定到网格的存储
-
view: 网格视图
渲染器函数应该返回一个字符串,然后将在单元格中显示。以下代码显示了我们的 renderer 函数:
{
text : 'Quantity',
dataIndex: 'Quantity',
renderer : function(value, metaData, record, rowIndex, colIndex, store, view) {
var colour = 'black';
if(value <= 3){
colour = 'red';
} else if(value > 3 && value <= 7){
colour = 'orange';
}
return '' + value + '';
}
}

模板列
如我们之前提到的,还有各种其他列类型提供了额外的功能。模板列允许我们定义一个与行记录合并的 Ext.XTemplate。如果我们想在单元格中包含更复杂的 HTML,这尤其有用。
下一个示例展示了我们如何在 Price 之外包含 StockValue 字段。我们首先在列定义中添加一个 xtype: 'templatecolumn' 属性,然后定义一个 tpl 字符串,该字符串将被转换为 Ext.XTemplate 实例:
{
xtype: 'templatecolumn',
width: 200,
text: 'Price ',
dataIndex: 'Price',
tpl: '£{Price} (£{StockValue})'
}

网格小部件
在网格中渲染复杂组件一直是 Ext JS 开发者的一个愿望,而现在随着网格小部件的引入,这已经变得容易得多。这些是在网格单元格内渲染并绑定到模型字段的组件。网格小部件的例子包括按钮、迷你图表、表单字段等等。
我们打算在我们的网格中添加一个简单的按钮小部件,以便查看每个产品的详细信息。我们首先在我们的网格列数组中添加一个新的widgetcolumn。小部件选项定义了显示的小部件类型:
{
xtype : 'widgetcolumn',
width : 100,
text : 'Action',
widget: {
xtype : 'button',
text : 'Details'
}
}
这将在每一行渲染一个按钮,然后我们可以将其连接到点击事件以打开产品详情视图(有关此操作的更多详细信息,请参阅下一节)。

我们可以使用listeners配置来挂钩小部件的事件,就像我们会在应用程序的任何其他地方使用组件一样。以下代码片段显示了如何将一个简单的click处理程序附加到按钮上:
{
xtype : 'widgetcolumn',
width : 100,
text : 'Action',
widget: {
xtype : 'button',
text : 'Details',
listeners: {
click: function(btn){
var rec = btn.getWidgetRecord();
console.log('Widget Button clicked! - ', rec.get('Name'));
}
}
}
}
使用表单输入数据
网络应用的一个关键方面是表单。能够将数据输入到我们的系统中是必不可少的,因此 Ext JS 提供了适用于所有类型输入的表单组件,这些组件可以轻松地绑定到我们的数据模型和相关视图。
在本节中,我们将扩展我们的产品网格,并允许用户通过一个简单的表单来编辑产品。
定义表单
我们就像定义任何其他视图一样定义我们的产品表单——通过创建一个文件(在这种情况下命名为 view/product/ProductForm.js)并在其中调用Ext.define。我们扩展了Ext.form.Panel类,并给它一个 xtype 为 product—ProductForm:
Ext.define('BizDash.view.product.ProductForm', {
extend: 'Ext.form.Panel',
xtype: 'product-ProductForm'
});
在我们的表单中,我们希望有输入字段来输入产品的名称、描述、数量和价格。我们可以使用多种特定的form字段类型来适应大多数数据类型:text、number、textarea、combobox、time、file、date、html等等。查看Ext.form.field.*命名空间以获取所有可能性。
我们将使用text、textarea和number字段来构建我们的表单。我们通过它们的 xtypes 和fieldLabel来配置每个字段。此外,我们将为我们的textarea指定一个显式的高度,并告诉我们的Price字段它可以有2位的decimalPrecision:
Ext.define('BizDash.view.product.ProductForm', {
extend: 'Ext.form.Panel',
xtype: 'product-ProductForm',
items: [
{
xtype: 'textfield',
fieldLabel: 'Name'
},
{
xtype: 'textarea',
fieldLabel: 'Description',
height: 100
},
{
xtype: 'numberfield',
fieldLabel: 'Quantity'
},
{
xtype: 'numberfield',
fieldLabel: 'Price',
decimalPrecision: 2
}
]
});
现在我们已经准备好了输入字段,我们还将添加两个按钮:保存和取消。我们使用bbar配置,这是一个添加底部停靠工具栏的快捷方式,并定义了两个按钮:
...
items: [ ... ],
bbar : [
{
xtype: 'button',
text: 'Save'
},
{
xtype: 'button',
text: 'Cancel'
}
]
...
显示我们的表单
我们希望在用户点击网格中产品旁边的详情按钮时打开我们的表单,并希望它预先填充该产品的信息。
我们首先创建一个名为product.ProductGridController的 ViewController,它将被附加到ProductGrid视图上:
Ext.define('BizDash.view.product.ProductGridController', {
extend: 'Ext.app.ViewController',
alias: 'controller.ProductGrid'
});
这个类扩展了Ext.app.ViewController类,并给它一个别名ProductGrid。我们使用这个别名通过在ProductGrid类中添加controller: 'ProductGrid'来将其与ProductGrid视图关联起来。
我们现在将使用 ProductGridController 来监听点击 详情 按钮的事件,并创建并显示我们的 ProductForm。
在上一节中添加到 详情 按钮的点击处理程序必须替换为我们想要在事件发生时执行的 ViewController 方法的名称。我们将称之为 onDetailsClick:
// ProductGrid.js
{
xtype : 'widgetcolumn',
width : 100,
text : 'Action',
widget: {
xtype : 'button',
text : 'Details',
listeners: {
click: 'onDetailsClick'
}
}
}
ViewController 的 onDetailsClick 方法将实例化一个 ProductForm 实例,并添加一些额外的配置来调整大小和浮动,使其位于现有的 UI 之上,然后显示它:
onDetailsClick: function(btn) {
var rec = btn.getWidgetRecord(),
productForm = Ext.create('BizDash.view.product.ProductForm', {
floating: true,
modal : true,
closable: true,
center : true,
width : 500,
height : 300
});
productForm.show();
}
如果我们运行我们的应用,我们将看到表单以模态窗口的形式显示在我们的网格上方。

填充我们的表单
目前,我们的产品表单已显示,但它没有填充我们点击的产品详情。为了做到这一点,我们必须通过 ViewModel 将每个表单字段绑定到产品模型。
我们首先创建一个具有非常简单配置的 ProductFormModel 类。该类扩展了 Ext.app.ViewModel 类,并赋予了一个别名 ProductForm。
我们还定义了 data 属性,它包含要绑定的数据,以及一个 rec 属性,它将是我们想要显示的产品模型实例的引用:
Ext.define('BizDash.view.product.ProductFormModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.ProductForm',
data: {
rec: null
}
});
然后将这个类与 ProductForm 类通过其别名关联起来:
// ProductForm.js
viewModel: {
type: 'ProductForm'
}
现在这两者已经绑定在一起,我们可以利用每个表单字段的 bind 选项来告诉框架使用 ViewModel 中的值填充字段。我们也可以这样做表单的标题属性,以便显示产品名称:
...
bind: {
title: '{rec.Name}'
},
items: [
{
xtype : 'textfield',
fieldLabel: 'Name',
bind : '{rec.Name}'
},
{
xtype : 'textarea',
fieldLabel: 'Description',
height : 100,
bind : '{rec.Description}'
},
{
xtype : 'numberfield',
fieldLabel: 'Quantity',
bind : '{rec.Quantity}'
},
{
xtype : 'numberfield',
fieldLabel : 'Price',
decimalPrecision: 2,
bind : '{rec.Price}'
}
]
...
最后一个拼图是给 ViewModel 提供正确的产品模型实例的引用。我们在 ProductGridController 的 onDetailsClick 方法中这样做:
// ProductGridController.js
...
productForm.getViewModel().setData({
rec: rec
});
...
现在,当我们打开表单时,字段将被预先填充。你还会注意到,当你编辑产品名称时,例如,网格和表单标题将自动实时更新。非常聪明。
持久化更新
目前,我们的 保存 和 取消 按钮没有任何作用,所以让我们将它们连接起来。
我们首先定义当按钮被点击时会发生什么。我们使用与产品网格的 详情 按钮相同的模式,并为点击事件分配一个与 ViewController 中的方法相对应的方法名:
// ProductForm.js
bbar: [
{
xtype: 'button',
text: 'Save',
listeners: {
click: 'onSave'
}
},
{
xtype: 'button',
text: 'Cancel',
listeners: {
click: 'onCancel'
}
}
]
现在,我们必须创建一个名为 ProductFormController 的 ViewController。就像我们对 ProductGrid 所做的那样,我们必须通过添加 controller: 'ProductFormController' 来告诉视图 ViewController 的存在。ViewModel 和 ViewController 也必须由视图 要求,因此它们是可用的:
requires: [
'BizDash.view.product.ProductFormController',
'BizDash.view.product.ProductFormModel'
],
在 ViewController 的处理程序中,我们使用 getView 方法来获取表单的引用,以便我们可以对其操作。onSave 方法将获取产品记录的引用(通过 ViewModel)并提交它(或者,如果你有一个合适的后端设置,你会调用 save 方法)。onCancel 方法在产品模型上调用 reject 方法,因此所做的任何更改都将被撤销。然后这两个方法都会关闭表单并销毁它。
//ProductFormController.js
onSave: function(btn){
var productModel = this.getView().getViewModel().getData().rec;
productModel.commit();
this.closeForm();
},
onCancel: function(btn){
var productModel = this.getView().getViewModel().getData().rec;
productModel.reject();
this.closeForm();
},
closeForm: function(){
var productForm = this.getView();
productForm.close();
productForm.destroy();
}
数据绑定视图
到目前为止,我们已经看到了如何在树和网格中显示数据集,但如果我们想要在显示方式上获得更多灵活性怎么办?
数据视图给我们这种自由,并允许我们为数据集中的每个项目定义自定义 HTML 来显示,同时保留强大的自动绑定设置。然后我们可以将我们自己的自定义 CSS 应用到这个 HTML 上,以按我们的意愿进行样式设计。
我们将通过一个示例来演示,创建一个用于我们系统中用户的 数据视图。
定义我们的用户数据视图
与往常一样,我们创建一个新的视图类—BizDash.view.user.UsersView—扩展基本框架类 Ext.view.View:
Ext.define('BizDash.view.user.UsersView', {
extend: 'Ext.view.View',
alias: 'widget.user-UsersView'
});
数据视图需要三个属性才能完全功能:一个 store、一个 tpl 和一个 itemSelector。
存储
与我们之前查看的网格和树完全相同,这是组件将要绑定到的数据源。对这个存储或其记录所做的任何更改都将自动反映在视图中。
在这种情况下,我们将使用用户存储,并使用 storeId 进行配置:
Ext.define('BizDash.view.user.UsersView', {
extend: 'Ext.view.View',
alias: 'widget.user-UsersView',
store: 'Users'
});
模板
数据视图基于为绑定数据存储中的每个记录渲染 HTML 的概念。tpl 配置定义了将合并到记录中以生成该 HTML 片段的 Ext.XTemplate。没有这个,屏幕上不会渲染任何内容。
我们想显示每个用户的姓名、角色和照片。我们使用以下模板字符串定义数据视图:
tpl: [
'<tpl for=".">',
' <div class="user-item">',
' <img src="img/{Photo}" />',
' <div class="name">{Name}</div>',
' <div class="role">{Role}</div>',
' </div>',
'</tpl>'
].join('')
我们首先使用 tpl 标记标签和 for 属性。这告诉模板遍历它给出的数组,并将它内部的 HTML 与每个记录依次合并。我们使用 {{...}} 符号标记数据占位符,其名称与模型的字段名称匹配。
项目选择器
最后,为了使我们的视图组件能够识别单个记录的 HTML,我们必须给它一个可以识别单个节点的 CSS 选择器。这用于允许在单个项目上引发事件(例如,点击、双击等)。
我们已经给包装 div 分配了 user-item 类,我们可以将其用作唯一选择器:
...
itemSelector: 'user-item'
...
视图样式
如果你展示我们的数据视图,它看起来可能有点简陋,所以我们需要添加一些样式来使其看起来更美观。
我们通过在 sass/src 文件夹中创建一个名为 UsersView.scss 的 .scss 文件,并复制视图文件夹的结构来实现这一点,这样 SASS 就会自动被选中。在这种情况下,我们在 sass/src/view/user/UsersView.scss 中创建它:
.user-item
{
float: left;
width: 120px;
text-align: center;
margin: 20px;
img {
width: 100%;
}
.name
{
font-size: 1.2em;
font-weight: bold;
}
.role
{
font-size: 0.8em;
color: #CCC;
}
}
执行sencha app build命令并刷新后,您应该会看到一个样式更佳的数据视图,如下所示:

摘要
在本章中,我们学习了 Ext JS 组件是如何相互配合的,以及它们在创建和销毁时遵循的生命周期。
然后,我们转向使用一些最受欢迎和最有用的组件。在我们的 BizDash 应用程序中,我们探索了以下组件的详细信息:
-
树形结构
-
网格
-
表单
-
数据视图
除了解释这些组件的主要特性和配置选项外,我们还在一个简单的 MVVM 架构中将它们链接在一起,利用双向数据绑定和事件监听。
在下一章中,我们将介绍 Ext JS 主题以及如何自定义它们以使您的应用程序独特。
第八章. 使用 SASS 创建独特的视觉和感觉
自从 Ext JS 从其开始以来,在设计方面取得了长足的进步,并且将新设计应用于框架的容易程度也得到了提高。
它原始的蓝色主题是使其对开发者非常有吸引力的事物,但现在已经过时且过度使用。Ext JS 5 现在包括六个主题和一系列选项来自定义它们以及创建你自己的。
在本章中,我们将讨论如何:
-
将不同的主题应用到你的应用程序
-
创建你自己的自定义主题
-
使用 SASS 变量自定义基本应用程序视觉
-
创建自定义组件 UI
将主题应用到你的应用程序
当你使用 Sencha Cmd 生成新的 Ext JS 应用程序时,它将自动使用海王星主题。这是最新主题之一,看起来非常干净和现代:

如前所述,Ext JS 包含了总共六个主题,如下所示:
-
海王星
-
海王星触摸(海王星的手势友好版本)
-
清晰
-
清晰触摸(清晰的手势友好版本)
-
经典
-
灰色
在以下屏幕截图中查看这些主题:

如果你导航到项目中的 ext 目录并查看 packages 文件夹,你会看到所有可用的主题包。
配置新主题
你可以轻松地更改应用程序使用的主题,只需进行一次配置更改。在你的应用程序文件夹中,打开 app.json 文件。此文件用于配置应用程序的不同方面以及它的加载和构建方式。
你应该在文件顶部附近看到一个名为 theme 的项目。我们将编辑此属性的值并将其更改为 ext-theme-crisp:
"theme": "ext-theme-crisp"
为了使此更改生效,我们必须使用以下命令从 BizDash 文件夹重新构建应用程序:
sencha app build
这将重新生成应用程序的 CSS 文件,并包含新主题的样式。
在浏览器中刷新应用程序现在应该显示应用了清晰主题的效果:

创建自定义主题
现在我们知道了如何切换应用程序的主题,我们将继续创建我们自己的自定义主题。
主题架构
如您从我们对 Ext JS 包夹的探索中注意到的,主题被设计为与 Sencha Core 和 Sencha Charts 一样作为包。这意味着它们可以在应用程序之间移植,并且独立于我们的应用程序代码。
主题包也是按照继承层次结构构建的,它们建立在常见的主题包之上。以下图表显示了每个包如何相互关联以及基本样式如何在它们之间共享:

生成主题包
首先,我们必须使用 Sencha Cmd 生成一个空白主题包。为此,我们打开终端并导航到我们的 BizDash 项目文件夹。
接下来,我们运行以下命令来生成一个基本的新主题:
sencha generate theme bizdash-theme
我们使用generate命令,你可能还记得在本书前面的内容,但这次我们告诉它生成一个名为 bizdash-theme 的主题。
执行后,我们应该在我们的工作区包文件夹中看到一个新文件夹:

主题的解剖结构
在我们的新主题包中,应该有我们在常规代码包中期望的所有文件和文件夹。我们最感兴趣的是处理以下内容:
-
package.json:这是定义所有包的详细信息和配置的地方 -
sass/var:这将是我们放置所有 SASS 变量覆盖的地方 -
sass/src:这是我们定义单个组件样式的位置 -
sass/etc:这是放置任何与组件文件无直接关联的杂项 SASS 文件的位置
跨浏览器样式
Ext JS 的一个巨大好处是它对旧浏览器的支持。那么,我们这些闪亮的新主题如何应对这些旧浏览器呢?Ext JS 在渲染不同浏览器时非常聪明,并为不支持 CSS3 属性(如渐变和圆角)的浏览器使用不同的样式。这些旧浏览器会得到图像精灵来显示这些设计特性,因此,完全相同的设计可以在所有浏览器中复制。
在 Sencha Cmd 构建过程中,一个包含所有框架组件的示例页面在一个无头浏览器中渲染,并对其进行了快照。然后,这个快照被切割成所需的精灵,并按需使用。
主题继承
正如我们之前提到的,主题扩展其他主题,并在每个级别定义的样式上构建。默认情况下,我们的新主题将扩展 ext-theme-classic 主题,并且看起来与它完全相同。
我们可以通过在 IDE 中打开包的package.json文件来更改我们新主题的基础主题。在这个文件中,我们可以更新extend属性为我们想要扩展的主题的名称。我们可以将其更改为扩展 Crisp 主题:
"extend": "ext-theme-crisp"
应用新主题
现在我们已经设置了一个基本主题,我们可以以应用内置主题相同的方式将其应用到我们的应用程序中。我们修改应用程序的app.json文件,并将包含的主题名称更改为 bizdash-theme:
"theme": "bizdash-theme"
在重新构建应用程序并刷新浏览器后,我们将看到我们的应用程序显示我们的新主题(尽管目前它看起来就像 Crisp 主题)。
基本主题定制
现在我们已经设置了一个自定义主题,我们想要开始让它变得独特,并从基础主题继承的默认设置中脱离出来。
主题变量
Ext JS 主题是用 SASS 创建的,并将大量设计控制权交给了 SASS 变量。通过修改这些变量,我们可以非常容易地对我们的主题进行基本修改。
SASS 是一种 CSS 预处理器,它引入了一种更简洁、更功能化的 CSS 编写方式。它引入了变量、嵌套规则、选择器继承和 mixin 等概念,这些概念在编写大型应用程序的 CSS 时非常有用。
框架中的每个组件都有自己的变量集,这将修改特定组件的外观。我们可以在文档中找到变量列表,包括该组件定义的方法、配置和事件。

要在我们的主题中定义这些变量之一,我们必须在主题包的sass/var文件夹中创建一个新的 SCSS 文件。这些 SCSS 文件应与我们的 JavaScript 文件结构相匹配。例如,为Ext.button.Button类定义变量应放在名为sass/var/button/Button.scss的文件中。在sass/src文件夹中创建新样式时应遵循此模式。
现在,我们将演示如何使用 SASS 变量自定义 UI 的一些区域。
更改主颜色
一个常见的场景是我们希望将主题的主颜色更改为与我们的企业颜色相匹配。这可以通过$base-color变量轻松完成,该变量可以分配任何有效的 HTML 颜色代码。
此变量是Ext.Component类的一部分,因此必须在名为sass/var/Component.scss的文件中定义。要将基本颜色更改为红色,请在文件中包含以下代码:
$base-color: #FF0000 !default;
!default后缀将允许在扩展此主题的主题中覆盖变量。
如果我们重新构建我们的应用程序并刷新浏览器,我们将看到这对我们应用程序的影响:

更改字体大小
我们可以通过包含带有新大小值的$font-size变量来快速更改应用程序中使用的字体大小。以下代码将字体大小增加到 16 px:
$font-size: 16px !default;
更改按钮颜色
我们已经看到,更改$base-color变量导致我们的按钮以新颜色渲染。我们可以通过覆盖$button-default-background-color变量来为按钮选择不同的颜色。
我们创建一个名为sass/var/button/Button.scss的新 SCSS 文件,并添加以下代码来更改颜色:
$button-default-background-color: #0000FF !default;

自定义组件 UI
Ext JS 组件可以在创建时通过提供不同的ui配置来单独自定义。这改变了添加到组件中的 CSS 类,使它们具有不同的外观,而与其他类型的组件分离。例如,你可能希望操作按钮为绿色,取消按钮为灰色,但所有其他按钮为默认颜色。
定义 UI
我们通过包含一个 SASS 混合并使用我们所需的颜色和设置来配置它来定义一个 UI。SASS 混合是一组组合在一起的样式规则,因此可以在多个地方重复使用,并通过传递参数值来定制。
我们将创建本章前面提到的两个 UI,用于我们的ProductForm的save和cancel按钮。
我们首先在bizdash-theme包文件夹内的sass/src/button文件夹中创建一个Button.scss文件。这个文件夹结构反映了Ext.button.Button组件类,因此在sencha app build过程中将被拾取和编译。
在此文件中,我们包含了extjs-button-small-ui混合:
@include extjs-button-small-ui( );
然后我们定义 UI 的名称,该名称将用于将样式应用于按钮。我们使用$ui参数名称,并将其命名为action:
@include extjs-button-small-ui(
$ui: 'action'
);
接下来,我们定义按钮的背景颜色、文本颜色和边框颜色:
@include extjs-button-small-ui(
$ui: 'action',
$background-color: #008000,
$color: #FFFFFF,
$border-color: transparent
);
我们可以为cancel按钮重复此操作,并将类似的混合添加到Button.scss文件中:
@include extjs-button-small-ui(
$ui: 'cancel',
$background-color: #EBEBEB,
$color: #000000,
$border-color: transparent
);
现在这些都已经就绪,我们使用BizDash文件夹内的sencha app build命令重新构建应用程序。
应用 UI
现在我们有两个 UI 样式准备应用于我们应用程序中的按钮。要应用这些样式,我们使用ui配置选项。添加后,此选项将添加一个新 CSS 类,该类是在上一步中由我们的混合生成的,并将其添加到组件中:
在ProductForm类中,我们的button配置变为:
...
bbar : [
{
xtype : 'button',
text : 'Save',
ui : 'action',
listeners: {
click: 'onSave'
}
},
{
xtype : 'button',
text : 'Cancel',
ui : 'cancel',
listeners: {
click: 'onCancel'
}
}
]
...
在我们的浏览器中刷新应用程序后,我们可以看到新的按钮样式已经就绪。检查 DOM 显示已添加到每个按钮的新 CSS 类:

以下截图显示了新的 CSS 类:

其他 UI
框架中的大多数组件都有混合,允许我们定义不同的组件样式。现在我们将演示如何为Ext.Panel组件创建一个替代 UI。
如果你在 Ext JS 文档中找到Ext.Panel,你将在CSS Mixins下拉菜单下看到一个条目。我们将使用此条目来定义我们自己的 UI,我们首先在文件夹sass/src/panel/Panel中创建一个Panel.scss文件。

我们将此内容包含在我们的Panel.scss文件中,并开始配置我们想要的样式。您可以在文档中查看所有可用的选项,但我们将自定义页眉背景颜色、边框颜色和边框半径,如下所示:
@include extjs-panel-ui(
$ui: 'product',
$ui-border-color: #78CCFC,
$ui-border-radius: 5px,
$ui-header-background-color: #78CCFC
);
这个 UI 选项可以以与我们处理按钮完全相同的方式添加到ProductForm中,使用ui配置:
...
ui: 'product',
...

摘要
在本章中,我们探讨了如何通过使用自定义主题来定制我们的 Ext JS 应用程序的外观和感觉。我们探讨了主题是如何构建的以及它们是如何相互继承的。
我们还展示了如何通过使用全局 SASS 变量和组件混合来创建和自定义一个新的主题。
下一章将专注于通过使用图表在我们的应用程序中可视化数据。
第九章。可视化应用程序的数据
本章将演示如何使用图表和图形来可视化您的数据。它将重点介绍最受欢迎的图表类型以及如何将数据源与它们集成。我们还将讨论如何将可视化集成到其他组件中,例如网格。
我们将涵盖与图表相关的最常见主题。以下是一些:
-
理解可视化技术和 Ext JS 中图表的工作原理
-
常见图表的示例和说明,包括:
-
折线图
-
柱状图
-
饼图
-
-
将这些可视化集成到其他组件中,例如网格
图表组件的解剖结构
Ext JS 提供了一个非常灵活且功能丰富的图表工具包,用于向用户直观地展示数据。为了充分利用图表包,了解图表的解剖结构是值得的。
系列
在 Ext JS 中,系列基本上是图表类型。系列是图表中最复杂的一部分,负责处理元素如何动画、显示或隐藏。此外,系列还处理数据的标签。
例如,如果应用程序需要柱状图,那么您需要关注的是柱状图或柱状图 3D 系列类。
Ext JS 5 默认提供的以下图表:
-
柱状图(以及 3D 变体)
-
OHCL/蜡烛图
-
面积图
-
饼图(以及 3D 变体)
-
折线图
-
仪表盘
-
散点图
-
雷达图
-
极坐标图
轴
与任何其他图表一样,您通常需要为图表组件定义轴。数值轴和类别轴是最常用的。如果需要非标准的东西,框架提供了一系列布局和分段数据的方法。
标签
图表标签只是显示数据点标签的一种方式。Ext JS 使我们能够自定义这些标签的显示和动画。
交互
交互简单地指的是用户如何与图表交互。例如,使用交互可能允许用户单击并拖动图表的一部分来放大数据。
创建折线图
折线图是最常用的图表类型之一,最适合表示趋势数据,这些数据通常定期更新,并需要实时分析。
Ext JS 图表包的一个巨大优势是我们可以在实时中处理数据。这可以通过多种方式实现:
-
简单地定期轮询以获取更新
-
将存储绑定到
WebSocket并从服务器端推送更新
为了继续我们的 BizDash 商业仪表板的主题,我们将创建一个折线图,显示目前有多少人正在访问我们的网站。
创建存储和模型
正如我们在第五章中看到的,“为您的 UI 建模数据结构”,我们需要首先定义一个模型来表示存储检索的个别记录。
在我们深入了解创建折线图之前,我们需要创建一个存储和模型。在我们的应用程序中还没有WebSiteVisitor模型。所以,让我们创建一个:
// app/model/WebSiteVisitor.js
Ext.define('BizDash.model.WebSiteVisitor', {
extend: 'Ext.data.Model',
fields: [
{
name: 'Time',
type: 'int'
},
{
name: 'Visitors',
type: 'int'
}
]
});
// app/store/WebSiteVisitors.js
Ext.define('BizDash.store.WebSiteVisitors', {
extend: 'Ext.data.Store',
model: 'BizDash.model.WebSiteVisitor',
proxy: {
type : 'ajax',
url : 'visitors.json',
reader: {
type : 'json',
rootProperty: 'rows'
}
}
});
轮询服务器获取新数据
要让我们的存储每 10 秒轮询服务器一次,并使用setInterval或类似方法绘制该时刻的访问次数,是非常直接的:
setInterval(function(){
store.load({
addRecords: true
});
}, 10000);
addRecords配置传递给存储的load方法。通过将此属性设置为true,通过加载操作检索的新记录将附加到现有数据集,而不是替换已经存在的记录,这是默认行为。
图表的配置本身非常简单。我们将在一个新的视图中这样做,以保持与我们的应用程序的其他部分一致:
Ext.define('BizDash.view.chart.SiteVisits', {
extend: 'Ext.chart.CartesianChart',
xtype: 'chart-SiteVisits',
config: {
animate: true,
store : 'WebSiteVisitors',
series : [
{
type : 'line',
smooth: false,
axis : 'left',
xField: 'Time',
yField: 'Visitors'
}
],
axes : [
{
type : 'numeric',
grid : true,
position : 'left',
fields : ['Visitors'],
title : 'Visitors',
minimum : 0,
maximum : 200,
majorTickSteps: 5
},
{
type : 'numeric',
position : 'bottom',
fields : 'Time',
title : 'Time',
minimum : 0,
maximum : 20,
decimals : 0,
constrain : true,
majorTickSteps: 20
}
]
}
});
这就是它的样子:

我们创建的折线图设置在一个相当标准的方式。两个数值轴绑定到WebSiteVisitor模型内的整数字段。线条本身是通过Ext.chart.series.Line类显示的,它通过xField和yField属性与数据相关联。
在柱状图中展示数据
柱状图是向用户展示定量数据的一种极其有用的方式。我们将快速演示如何创建柱状图,并从服务器异步加载数据。
与折线图一样,我们需要一个模型和存储来开始:
// app/model/BarChart.js
Ext.define('BizDash.model.BarChart', {
extend: 'Ext.data.Model',
fields: [
{
name: 'name',
type: 'string'
},
{
name: 'value',
type: 'int'
}
]
});
// app/store/BarChart.js
Ext.define('BizDash.store.BarChart', {
extend: 'Ext.data.Store',
model: 'BizDash.model.BarChart',
proxy: {
type : 'ajax',
url : 'path/to/your/datasource',
reader: {
type : 'json',
rootProperty: 'data'
}
},
autoLoad: true
});
在这个虚构的例子中,从服务器返回的数据必须有一个name和value属性,因为我们已经将这些定义为模型中的字段。
在本例中定义的存储用作我们数据的客户端缓存。通过将BarChart模型关联到存储,我们确保数据在图表组件中正确表示。
现在存储已经准备好了,我们只需要一个组件来绑定它。现在我们将创建一个新的视图BizDash.view.chart.BarChart,其中包含一个带有柱状系列的笛卡尔图表。这为我们提供了基本的图表功能,我们可以添加轴和系列,并将存储绑定到它。
轴用于定义图表的边界,在本例中,创建水平和垂直轴。系列负责在图表上渲染数据点。在这里,我们使用了Ext.chart.series.Bar类来创建一个简单的柱状图。在柱状图配置中,xField和yField必须包含我们在模型中定义的相同名称的字段。
Ext.define('BizDash.view.chart.BarChart', {
extend: 'Ext.chart.Chart',
xtype: 'chart-BarChart',
config: {
animate: true,
store : 'BarChart',
axes : [
{
type : 'numeric',
position: 'left',
fields : ['value'],
title : 'Value'
},
{
type : 'category',
position: 'bottom',
fields : ['name'],
title : 'Name'
}
],
series : [
{
type : 'bar',
axis : 'bottom',
xField: 'name',
yField: 'value'
}
]
}
});
这就是图表现在的样子:

在 Ext JS 中创建饼图
饼图是一种非常常见的图表类型,非常适合表示比例数据,其中饼图的每一块等于该块相对于整个数据集总和的百分比。
在本节中,我们将演示如何创建一个饼图,以展示我们仓库中产品(按库存水平)的分布情况。
我们已经在本书中定义了产品存储和模型(第五章,为您的 UI 建模数据结构,和第七章,构建常见 UI 小部件)。作为提醒,以下是我们定义的内容:
Ext.define('BizDash.model.Product', {
extend: 'Ext.data.Model',
fields: [
{
name: 'Name',
type: 'string'
},
{
name: 'Description',
type: 'string'
},
{
name: 'Quantity',
type: 'int'
},
{
name: 'Price',
type: 'float'
}
]
});
Ext.define('BizDash.store.Products', {
extend: 'Ext.data.Store',
model: 'BizDash.model.Product',
autoLoad: true,
proxy: {
type : 'ajax',
url : 'products.json',
reader: {
type : 'json',
rootProperty: 'rows'
}
}
});
现在我们已经组织好了存储和模型,我们需要为我们的饼图创建一个新的视图:
Ext.define('BizDash.view.chart.StockLevelPie', {
extend: 'Ext.chart.PolarChart',
xtype: 'chart-StockLevelPie',
config: {
animate : true,
store : 'Products',
interactions: 'rotate',
series : {
type : 'pie',
label : {
field : 'Name',
display: 'rotate'
},
xField: 'Quantity',
donut : 30
}
}
});
惊人的是,就这么简单。您可以在以下图像中看到输出:

与其他具有极坐标的图表一样,Ext.chart.PolarChart类为我们特定的图表类型提供了基础设施和画布以进行渲染。
我们使用饼图系列类型,让Ext.chart.series.Pie类处理绑定存储中的记录,并将其转换为一系列精灵以形成图表。此系列将存储中的每个记录转换为饼图的切片。
此系列类型最重要的配置是xField。这告诉系列我们的模型字段中哪个字段包含用于计算每个记录切片大小的数值。
我们通过使用labels属性配置Ext.chart.Label混合,并将其应用于Ext.chart.series.Pie类,为图表添加标签。这些选项允许我们配置标签的位置和样式。通过选择display属性的旋转值,标签将定位在切片的长度上。在讨论图表的外观时,也许值得强调的是,donut配置是饼图中中间挖空的孔的半径(以百分比表示)。
在网格中集成可视化
网格小部件是 Ext JS 最知名的功能之一,该框架现在支持小部件列的概念。小部件列允许您嵌入一个小部件,与标准组件不同,它是一个轻量级组件,渲染和更新速度快。小部件非常适合网格,在网格中您可能同时显示数十个小部件。
Ext JS 5 引入了多个轻量级网格小部件,但毫无疑问,Sparkline 小部件是展示网格中图表的完美小部件。它是一个超轻量级的图表,旨在用最小的空间表示一系列值。
有许多不同的 Sparkline 可以工作:
-
柱状图
-
线形图
-
饼图
-
项目符号
-
范围图
-
三态
-
箱线图
为了演示 Sparkline 的实际应用,我们将回到第七章,构建常见 UI 小部件,并利用我们的product网格:
Ext.define('BizDash.view.product.ProductGrid', {
extend: 'Ext.grid.Panel',
xtype: 'product-ProductGrid',
store: 'Products',
columns: [
{
text: 'Name',
dataIndex: 'Name'
},
{
text: 'Description',
dataIndex: 'Description',
flex: 1
},
{
text: 'Quantity',
dataIndex: 'Quantity'
},
{
text: 'Price ',
dataIndex: 'Price'
}
]
});
一个 Sparkline 图必须附加到单个数据字段,该字段包含将要绘制的点数组。为了演示这个功能,我们将向我们的 Product 模型添加一个HistoricSales字段,该字段将保存我们将要绘制的数据:
Ext.define('BizDash.model.Product', {
extend: 'Ext.data.Model',
...
fields: [
...
{
name: 'HistoricSales',
type: 'auto',
defaultValue: [4, 9, 12, 66, 9]
}
]
});
这个网格已经绑定了一个存储和模型,所以只需将一个新的 widgetcolumn 添加到 columns 数组中,如下所示:
Ext.define('BizDash.view.product.ProductGrid', {
extend: 'Ext.grid.Panel',
columns: [
...
{
text : 'Historic Sales',
xtype : 'widgetcolumn',
dataIndex: 'HistoricSales',
widget : {
xtype: 'sparklineline'
}
}
]
});
widget 配置允许我们指定将在列中渲染的 sparkline 的 xtype(可能的值包括 sparklinebar、sparklinepie、sparklinebullet 和 sparklinediscrete)。以下截图显示了如何渲染此线形图:

摘要
在本章中,我们探讨了将图表可视化添加到我们的 Ext JS 应用程序中的细节。我们:
-
介绍了图表创建的基础知识
-
理解了图表的组成部分
-
建立了一个折线图
-
建立了一个柱状图
-
建立了一个饼图
-
将可视化集成到网格中
下一章将深入探讨如何通过教授你如何编写可测试的 JavaScript 和单元测试来测试你的 Ext JS 应用程序。
第十章.通过单元和 UI 测试确保代码质量
在我们添加新功能、重构现有功能以及最终发布产品时,对我们的代码质量和功能充满信心是至关重要的。
如果没有自动检查我们代码的流程,我们总会担心我们的更改会对应用程序的其他部分产生什么影响。引入错误和发现错误之间的时间延长,导致解决它们的成本大幅上升。
本章将重点介绍我们如何确保我们的代码具有高标准,并且始终按预期工作。我们将探讨以下主题:
-
编写可测试 JavaScript 的最佳实践
-
将测试置于开发流程核心的开发方法
-
Siesta 测试框架简介
-
如何编写单元测试
-
如何编写 UI 测试
-
如何将测试集成到开发工作流程中
编写可测试的 JavaScript
JavaScript 一直是一种难以测试的语言。这很大程度上是由于它被使用的非结构化性质以及将其撒入页面以添加小效果和功能片段的倾向。这意味着隔离小段代码以进行测试变得极其困难,因此,往往根本不进行测试。
现在,JavaScript 被用来编写严肃的应用程序,开发者正在基于框架,如 Ext JS,为他们的项目引入更严格的架构。这种结构使得测试功能单元变得更加容易,并为我们提供了更简单的场景来工作和构建测试。
尽管使用 Ext JS 编写应用程序已经使你朝着更可测试的代码迈出了重要一步,但还有一些良好的实践需要遵循,以确保事情更加简单,我们将在本节中探讨这些内容。
单一职责
通过确保你的类和方法只负责单一的功能部分,意味着测试该功能变得更加容易,因为输入和输出更加清晰。
例如,有一个方法,在按钮点击时,将更新应用程序的三个部分,这个方法将非常复杂。为了测试这个单一的方法,我们需要所有三个区域都可用,并在每个区域检查适当的结果。
如果我们将这些提取为三个独立的方法,我们就可以单独测试每个方法,并专注于该函数及其结果。
可访问的代码
如果我们的代码库中的函数是私有的(即,例如,通过闭包无法从外部代码访问),那么我们将无法测试它或模拟它。这种方法使得测试这些私有函数变得不可能,并且测试依赖于它们的函数变得非常困难,因为它们可能需要被模拟。
因此,有时有必要将这些私有函数公开化,以帮助测试。
嵌套回调
深层嵌套的回调函数非常难以测试,因为它们都是私有的,并且依赖于许多条件来对它们进行测试。这与之前提到的使代码可访问性的观点密切相关。与其嵌套大量的回调,不如考虑将它们提取到成员函数中。以下示例展示了处理 async 调用的函数无法进行测试:
Ext.define('MyClass', {
doAction: function(){
Ext.Ajax.request({
url: 'action.php',
success: function(response){
OtherClass.async(response.value, function(newValue){
AnotherClass.async(newValue, function(){
// finished!
});
});
}
});
}
});
这可以通过使用可以单独测试的成员函数来重构。此外,代码更加整洁,避免了大量的嵌套回调。
Ext.define('MyClass', {
doAction: function(){
Ext.Ajax.request({
url: 'action.php',
success: this.onActionSuccess
});
},
onActionSuccess: function(response){
OtherClass.async(response.value, this.onOtherClassAsync);
},
onOtherClassAsync: function(newValue){
AnotherClass.async(newValue, this.onAnotherClassSync);
},
onAnotherClassSync: function(){
// finished!
}
});
将事件处理程序与操作分离
当将功能附加到事件时,我们通常在同一个函数内执行操作。以下示例展示了按钮点击处理程序,它基于网格中选定的行向用户发送删除请求:
onDeleteButtonClick: function(btn) {
var grid = btn.up('gridpanel'),
selectedUserModel = grid.getSelection()[0];
Ext.Ajax.request({
url : 'deleteUser.php',
params : {
userID: selectedUserModel.get('userID')
},
success: this.onUserDeleteSuccess,
failure: this.onUserDeleteFailure,
scope : this
});
}
这段代码的问题在于,为了测试删除用户,我们需要有一个网格和一个选定的行可用,这设置起来很麻烦。更好的做法是从处理程序中提取操作,这样就可以在不需要设置代码的情况下测试操作。这还有一个额外的优点,即可以从不同的上下文中执行;例如,从删除键处理程序中执行。
onDeleteButtonClick: function(btn) {
var grid = btn.up('gridpanel'),
selectedUserModel = grid.getSelection()[0];
this.doUserDelete(selectedUserModel.get('userID'));
},
doUserDelete: function(userID){
Ext.Ajax.request({
url : 'deleteUser.php',
params : {
userID: userID
},
success: this.onUserDeleteSuccess,
failure: this.onUserDeleteFailure,
scope : this
});
}
测试框架
有许多 JavaScript 测试框架都可以很好地与 Ext JS 应用程序一起工作。它们各自提供了不同的做事方式和不同的功能集。在本节中,我们将讨论 Jasmine (jasmine.github.io/) 和 Siesta (www.bryntum.com/products/siesta/),并在后续章节中详细解释 Siesta。
Jasmine
Jasmine 是一个简单、免费、BDD 风格的框架,在业界广泛使用,包括 Sencha,用于开发 Ext JS 和 Sencha Touch。
这个框架在概述测试时采用了一种非常描述性的方法:
describe("Test Suite Title", function() {
var a;
it("Spec Description", function() {
a = MyClass.doSomething();
expect(a).toBe(true);
});
});
测试套件和规范只是简单的函数,它们应该执行测试代码,然后断言结果是否正确。
describe 方法让我们定义一个测试套件,它可以包括多个规范(或实际上嵌套的测试套件)。规范是通过 it 方法定义的,它接受规范的描述和一个形成测试的函数。在这个函数中,我们设置并执行测试,并分析其结果以确保达到了正确的结果。
Jasmine 有一个测试 harness 页面,它执行所有包含的测试套件并显示结果。它也可以使用像 PhantomJS 这样的工具无头执行。这使得它非常适合与 CI 流程集成。
Siesta
Siesta 是一个强大的测试框架,专注于测试 Ext JS 和 Sencha Touch 应用程序。它允许我们为我们的应用程序编写单元和 UI 测试,还内置了一个强大的事件记录器,用于记录和测试与特定 UI 元素的交互。由于有 Sencha 的倾向,它还提供了辅助方法来帮助测试常见组件,并支持 Sencha 风格构造,如组件查询和 Ext.Loader。
这种 Sencha 专注使 Siesta 成为测试 Ext JS 应用程序的理想选择,因为它对框架的了解,使得任务变得更加简单。一个额外的优势是,它是用 Ext JS 编写的,并使用自身进行测试,因此开发者每天都在使用他们的产品,这为这个框架提供了高度的开发者关注。
Siesta 允许您在一个测试套件中包含多个规范,每个规范测试应用程序的一个区域:
StartTest(function(t) {
t.diag("MyClass Test");
var a = MyClass.doSomething();
t.is(a, true, 'MyClass.doSomething returned true');
t.done();
});
传递给 StartTest 的函数包含所有测试逻辑,包括测试步骤和断言。传递给此函数的对象 t 给我们提供了访问大量断言方法的权限,我们将在稍后探讨。
我们建议使用 Siesta 作为您的测试框架,因为它与 Ext JS 的紧密集成,这使得快速有效地实施测试变得极其容易。
编写单元测试
Siesta 允许我们创建单元测试套件,这使我们能够测试非 UI 逻辑。我们将为 BizDash.config.Config 类编写一些简单的测试来测试其方法。这个类的简化版本如下所示:
Ext.define('BizDash.config.Config', {
extend: 'Ext.util.Observable',
singleton: true,
config: { version: '0.0.1-0' ... },
...
getBuildNumber: function() {
var versionSplit = this.getVersion().split('-');
return versionSplit[1];
},
applyVersion: function(newVersion, oldVersion){
return newVersion;
},
updateVersion: function(newVersion, oldVersion){
if(this.hasListeners) {
this.fireEvent('versionchanged', newVersion, oldVersion);
}
}
});
测试项目结构
我们将首先将 Siesta 框架文件添加到我们的项目文件夹中;这些可以从 Siesta 网站下载(www.bryntum.com/products/siesta/)。
注意
Siesta 提供了一个 Lite 版本,可以免费使用,但功能有限。标准许可证则为您提供了访问额外功能和支持的权利。
然后,我们创建一个测试文件夹,我们将在这里保存我们的测试套件。我们的文件夹结构应该看起来像以下图片:

接下来,我们将为 Config 类创建一个测试套件。我们将模仿应用程序的文件夹结构,并创建一个与被测试的类同名但以下划线为前缀的文件夹。在这种情况下,我们最终得到以下结构:

通过遵循这个约定,测试会根据它们的类分组,因此可以轻松导航。这纯粹是一个建议的结构,如果你的测试套件愿意遵循完全不同的模式,也可以。
创建测试工具
要运行 Siesta 测试,我们必须有一个测试定义脚本(就像我们在上一节中创建的那样)以及一个加载 Siesta 框架和定义脚本的 HTML 页面。
我们的测试定义应该包含以下代码,并且位于 index.js 文件中:
var Harness = Siesta.Harness.Browser.ExtJS;
Harness.configure({
title: 'Config Tests',
preload: [
'../../../MyWorkspace/build/testing/BizDash/resources/BizDash-all.css',
'../../../MyWorkspace/build/testing/BizDash/app.js' ]
});
Harness.start(
{
group: 'Config',
items: [
]
}
);
第一部分配置 Siesta 测试套件标题,并告诉它预加载我们应用程序的构建版本(在运行sencha app build之后创建的版本)。
接下来,我们告诉测试工具开始运行我们的测试,我们将以组的形式结构化这些测试。目前我们没有要运行的测试,但当我们有测试时,我们将引用items数组中的文件。
我们的 HTML 页面非常简单,只是加载了 Siesta 和 Ext JS 框架以及我们的测试定义脚本。
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="http://cdn.sencha.io/ext/gpl/4.2.0/resources/css/ext-all.css">
<link rel="stylesheet" type="text/css" href="../../siesta/resources/css/siesta-all.css">
<script type="text/javascript" src="img/ext-all.js"></script>
<script type="text/javascript" src="img/siesta-all.js"></script>
<script type="text/javascript" src="img/index.js"></script>
</head>
<body>
</body>
</html>
添加第一个测试
我们首先将为getBuildNumber方法添加一个测试。添加一个名为010_getBuildNumber.t.js的新文件。
我们在这里使用的命名约定不是必需的,但在 Siesta 文档中广泛使用。编号允许测试以特定顺序运行,名称指代正在测试的方法或功能。.t.js扩展用于识别测试文件。
在此文件中,我们调用StartTest方法:
StartTest(function(t) {
t.diag("Config.getBuildNumber");
t.done();
});
接下来,我们调用getBuildNumber方法并检查返回的结果是否正确:
StartTest(function(t) {
t.diag("Config.getBuildNumber");
var buildNumber = BizDash.config.Config.getBuildNumber();
t.expect(buildNumber).toEqual('0');
t.done();
});
最后,我们必须在index.js的items数组中引用这个新文件:
Harness.start(
{
group: 'Config',
items: [
'010_getBuildNumber.t.js'
]
}
);
执行测试
要运行我们的测试,我们只需在浏览器中打开index.html,然后点击屏幕左下角的运行所有按钮。Siesta 将执行列出的每个测试并显示结果,如下面的截图所示:

扩展测试
我们可以添加尽可能多的断言和测试到这个初始文件中,但通常将不同的测试类型放在不同的文件中是明智的,以保持事物有序并确保一个干净的环境,不受先前测试的污染。
我们现在将添加一些新的测试文件来测试一些边缘情况。每个这些文件都应该添加到index.js文件中,以便它们包含在测试运行中。
我们下一个规范020_getBuildNumber_emptyString.t.js将测试当版本号设置为空字符串时的输出。我们使用toBeUndefined方法来检查输出:
StartTest(function(t) {
t.diag("Config.getBuildNumber");
BizDash.config.Config.setVersion('');
var buildNumber = BizDash.config.Config.getBuildNumber();
t.expect(buildNumber).toBeUndefined();
t.done();
});
最后,我们将使用我们糟糕的getBuildNumber方法实现来演示如何测试当version配置为 null 时,方法应该抛出异常。为此,我们使用toThrow断言,并将要测试的方法传递给它:
StartTest(function(t) {
t.diag("Config.getBuildNumber");
BizDash.config.Config.setVersion(null);
t.expect(BizDash.config.Config.getBuildNumber).toThrow();
t.done();
});
框架将自动调用我们传递给expect的方法,如果它抛出异常,则通过测试。
测试 UI 交互
Siesta 的一个主要优点是它能够测试真实的 UI 交互,允许我们创建基于应用程序实际渲染方式和真实用户如何与之交互的自动化、可重复的测试。
在本节中,我们将测试我们在第七章中创建的产品网格,构建常用 UI 小部件。我们将查看测试其渲染方式,然后继续测试网格行上的详情按钮的点击。
测试单元格内容
首先,我们将测试每个单元格是否根据其绑定的存储中的数据渲染正确的值。这将捕获字段名称更改和网格列配置中的任何问题。
我们首先创建一个新的测试套件,就像我们在上一节中做的那样,并创建一个名为010_GridCellContents.t.js的新测试文件。我们的文件夹结构将如下所示:

请注意更新你的index.html和index.js文件中的所有路径,以适应新的位置。
在这些测试套件中,我们将包括整个app.js文件,这意味着应用程序将以你打开浏览器时的方式启动。这意味着我们可以立即与 UI 交互。
如果你展开 Siesta 界面右侧的 DOM 面板,它将显示测试期间出现的 UI。如果你在测试运行时观察此区域,你将看到实时发生的交互。
设置预期数据
了解我们的网格正在渲染什么数据非常重要,这样我们就可以检查它是否正确。因此,我们的第一步是将已知测试数据填充到我们的产品存储中:
StartTest(function(t) {
t.diag("Product Grid Contents");
var productsStore = Ext.getStore('Products');
productsStore.removeAll();
productsStore.add([
{
"id" : 1,
"Name" : "Product 1",
"Description": "Product 1 Description",
"Quantity" : 1,
"Price" : 9.99
},
{
"id" : 2,
"Name" : "Product 2",
"Description": "Product 2 Description",
"Quantity" : 5,
"Price" : 2.99
},
{
"id" : 3,
"Name" : "Product 3",
"Description": "Product 3 Description",
"Quantity" : 1000,
"Price" : 5.49
}
]);
});
检查单元格内容
现在,我们可以使用通过t参数提供的内置matchGridCellContent方法。此方法接受一个网格实例(或用于找到它的组件查询),单元格行和列,预期的值以及测试的描述。
我们可以添加一个测试来检查第一列(包含产品名称)是否具有正确的值,基于我们添加到我们存储中的数据:
// test Row 0, Cell 0
t.matchGridCellContent('product-ProductGrid', 0, 0, 'Product 1', 'Cell 0, 0 contents are correct');
// test Row 1, Cell 0
t.matchGridCellContent('product-ProductGrid', 1, 0, 'Product 2', 'Cell 1, 0 contents are correct');
模拟点击
接下来,我们将测试在点击网格行中的一个“详情”按钮后的产品编辑过程。我们希望在这次交互之后测试以下内容:
-
创建并显示一个新的产品表单
-
表单已填充正确的记录详情
-
编辑“名称”字段后,网格实时更新
-
点击“保存”按钮后,表单被隐藏并提交记录
我们首先创建一个新的测试文件(020_ProductEdit.t.js),并包含与上一节中相同的初始存储填充代码。
现在,我们需要获取包含“详情”按钮的单元格的引用。我们通过使用getCell方法来实现,传递给它一个组件查询以找到网格和行/单元格索引:
var cell = t.getCell('product-ProductGrid', 0, 4);
对于这个测试,我们将按顺序执行多个操作,这些操作可能是异步的。我们可以使用回调方法来链接这些项,但我们将使用链式方法,这使得这个过程更容易。
首先,我们将引发对单元格的点击,然后确认已创建了一个ProductForm组件。我们使用cqExists(组件查询存在)方法来完成此操作:
...
t.chain( function (next) {
// click the button
t.click(cell);
// check ProductForm is created
t.cqExists('product-ProductForm', 'Product Form is displayed');
next();
}
...
);
我们传递给chain方法的每个函数都传递一个next方法,应在链的末尾调用此方法,以便执行链中的下一个项。
现在,我们想要检查表单是否已从所选记录中填充了正确的值。首先,我们在链中添加一个 10 毫秒的小暂停,以便让表单有时间填充,然后我们为每个字段调用fieldHasValue方法。我们通过组件查询传递给它,以找到所需的字段,我们期望它拥有的值,以及测试的描述:
...
{
waitFor: 'Ms',
args: 10
},
function (next) {
t.fieldHasValue('product-ProductForm textfield[fieldLabel="Name"]', 'Product 1', 'Name field has correct value');
t.fieldHasValue('product-ProductForm textfield[fieldLabel="Description"]', 'Product 1 Description', 'Description field has correct value');
t.fieldHasValue('product-ProductForm textfield[fieldLabel="Quantity"]', '1', 'Quantity field has correct value');
t.fieldHasValue('product-ProductForm textfield[fieldLabel="Price"]', '9.99', 'Price field has correct value');
next();
}
...
下一步我们想要测试的是当一个字段被更新时会发生什么。为此,我们在链中添加另一个步骤并更新名称字段。然后我们检查网格是否相应更新,并且模型实例现在被标记为脏(即,有一个等待提交的更改):
function(next){
var nameField = Ext.ComponentQuery.query('product-ProductForm textfield[fieldLabel="Name"]')[0];
nameField.setValue('Updated Product 1');
t.matchGridCellContent('product-ProductGrid', 0, 0, 'Updated Product 1', 'Cell 0, 0 contents are correct');
t.expect(Ext.getStore('Products').getAt(0).dirty).toEqual(true);
next();
}
最后,我们测试保存过程。我们模拟点击保存按钮并测试产品表单是否已关闭,并且记录的更改已被提交。最后,我们调用 done 方法来告诉测试框架我们的测试已完成:
function(){
t.click('>>button[text="Save"]');
t.cqNotExists('product-ProductForm');
t.expect(Ext.getStore('Products').getAt(0).dirty).toEqual(false);
t.done();
}
前缀>>应用于表示组件查询字符串。如果省略,查询将被解释为 CSS 样式选择器。
事件记录器
在前面的例子中,我们在测试中执行了非常简单的步骤。然而,为每个按钮和交互顺序手动编码所有组件查询可能会变得繁琐。Siesta Standard 提供了一个事件记录器,允许我们记录一系列交互并生成将这些步骤包含在测试中所需的代码。值得注意的是,此代码很少“原样”使用;相反,它通常在添加到测试脚本之前进行修改和增强。
要启动事件记录器,请点击测试结果窗口中的视频摄像头图标,这将显示一个空的步骤列表,如下截图所示:

我们将使用事件记录器来测试产品表单的取消过程。我们首先运行ProductEdit测试,以便我们有一个可以与之交互并记录的界面。然后我们点击记录按钮(小红色圆圈)并开始与 DOM 面板中的界面进行交互。
我们将执行以下操作:
-
点击详情按钮。
-
编辑产品表单中的名称字段。
-
点击取消按钮。
我们完成这些后,将点击停止按钮,步骤将填充到网格中。

你会看到每个动作都显示为一行,包括动作类型、动作的目标(在这种情况下是用于定位元素的组件查询或输入的文本),以及动作的偏移量。
如果你想要更改目标,你可以点击单元格并从提供的列表中选择不同的查询,或者手动修改它。
一旦你对步骤满意,你可以生成要包含在测试规范中的代码。只需点击生成代码按钮,代码就会显示出来,可以复制并粘贴到我们的测试文件中。
记录器的输出通常使用更简洁、声明性的语法来表示链式步骤。这两种语法都是完全有效的,并且会产生相同的结果。
在大多数情况下,这足以让我们开始,并且我们可以向链中添加更多步骤,这些步骤将在每个动作之后检查正确的结果。
以我们的例子为例,我们可以在记录的步骤周围构建以下测试断言:
t.chain( { click : "button[text=Details] => .x-btn-button", offset : [10, 13] },
function(next) {
t.cqExists('product-ProductForm', 'Product Form is displayed');
next();
},
{
click : "product-ProductForm[title=Product 1] textfield[inputType=text] => .x-form-text", offset : [127, 10] },
{
action : "type", text : " Updated"
},
function(next) {
t.matchGridCellContent('product-ProductGrid', 0, 0, 'Product 1 Updated', 'Cell 0, 0 contents are correct');
t.expect(Ext.getStore('Products').getAt(0).dirty).toEqual(true);
next();
},
{
click : "button[text=Cancel] => .x-btn-inner", offset : [16, 1] },
function() {
t.cqNotExists('product-ProductForm');
t.expect(Ext.getStore('Products').getAt(0).dirty).toEqual(false);
t.done();
}
);
测试自动化和集成
到目前为止,我们只使用 Siesta 界面在浏览器中运行了我们的 Siesta 单元测试,这对于开发来说非常好。然而,我们通常希望将这些测试作为自动化构建过程的一部分来运行。
我们可以非常容易地从命令行使用 PhantomJS 或 WebDriver 运行我们的测试套件。现在我们将演示如何使用 WebDriver 从命令行运行跨浏览器测试。
WebDriver 是一个允许跨浏览器测试自动化的工具,它随 Siesta 一起打包,使得它非常容易工作。在其最简单的形式中,我们可以运行我们刚刚创建的测试,这些测试必须可以从服务器或本地主机访问,并在 Google Chrome 中使用以下代码运行:
siesta/bin/webdriver http://localhost/tests/view/product/_ProductGrid/index.html --browser=chrome
通过从我们的根项目目录执行此代码,将启动一个 Google Chrome 实例,运行测试,并在终端/命令提示符窗口中打印结果。

您可以通过使用--browser=*选项将其扩展到包括 FireFox、Safari、Chrome 和 Internet Explorer。这将依次打开每个浏览器并执行测试——非常适合跨浏览器测试。
测试报告
在每次运行后,也可以生成一个测试报告,例如将其包含在构建资产中。为此,我们在命令的末尾包含--report-format=json和--report-file=BuildReport.json选项,以保存测试结果的 JSON 版本。
摘要
本章重点介绍了我们如何编写更好、更可测试的 JavaScript 代码,然后转向如何使用测试框架来执行对代码的重复性测试。
我们深入探讨了 Siesta 测试框架,并展示了如何编写简单的单元测试以及更复杂的 UI 测试,这些测试可以像用户与他们交互一样测试我们的应用程序。
最后,我们看到了如何从命令行运行我们的测试套件,以测试构建过程中的跨浏览器有效性。


浙公网安备 33010602011771号