面向安卓的-HTML5-和-JavaScript-学习手册-全-
面向安卓的 HTML5 和 JavaScript 学习手册(全)
零、简介
欢迎来到学习 Android 的 HTML5 和 JavaScript】。这本书将为 Android 操作系统 4.0 版本的 Android 浏览器(称为冰淇淋三明治)提供 HTML5、JavaScript 和 CSS3 的介绍。这本书将带你了解如何利用最好的移动网络技术和方法来开发可靠的移动网站,不仅仅是为 Android,也为其他平台。
这本书不是关注现成的框架和库,而是关注普通 JavaScript、CSS 和 HTML5 的使用,希望一旦你完成这本书,你将有足够的能力使用普通 JavaScript for mobile,以及 JavaScript mobile web 框架。
这本书是给谁的
这本书是为任何有网络开发或原生移动应用开发经验并想掌握移动网络的人准备的。你需要一些 JavaScript/ActionScript 或其他编程语言的知识。
这本书的结构
这本书分为九章。
- Chapter 1 (Introduction): This chapter will guide you to build your development environment.
- Chapter 2 (Introduction of creating mobile network applications for Android): This chapter will give you a deeper understanding of the history behind the mobile network and the differences between it and desktop-based websites. It will take you through several case studies of existing mobile websites and explain how to improve or change them to make them easier for users.
- Chapter 3 (HTML5) and Chapter 4 (Start your project with HTML5): These chapters will introduce you to some new HTML5 tags, which are specifically for mobile devices. This chapter will also show you how to encode video and audio for mobile devices and embed them with HTML5. After you finish the HTML5 chapter, the seminar will take you through the form of movie reminding mobile web application to create the HTML foundation of mobile web application.
- Chapter 5 (CSS3 for Mobile) and Chapter 6 (laying the foundation of CSS3): These chapters will show you some new CSS3 mobile compatible features, such as transformation, animation, shadow and fillet. You will also learn how to use SASS, a CSS3 precompiler. This seminar will guide you to design your mobile web application using SASS and best practices while using the precompiler.
- Chapter 7 (Mobile JavaScript) and Chapter 8 (JavaScript: model, view and controller): These chapters will show you how to use JavaScript to enhance your mobile application. There are no libraries in this chapter, such as jQuery, Sencha or jQuery Mobile. The Getting Started with JavaScript chapter will show you how to use ordinary JavaScript to build a basic framework and interact with canvas and audio. The seminar will take you to enhance mobile web applications by adding pagination and communicate with third-party APIs through JSONP.
- Chapter 9 (Testing and deploying your mobile Web application): This chapter will show you how to test your application with QUnit and deploy it with Capistrano.
下载代码
本书中所示示例的代码可在 Apress 网站[www.apress.com](http://www.apress.com)
上获得。在该书的信息页面上的源代码/下载选项卡下可以找到一个链接。该选项卡位于页面相关标题部分的下方。
联系作者
如果你有任何问题或意见,或者发现你认为我应该知道的错误,你可以通过[gavin@justanotherdeveloper.co.uk](http://gavin@justanotherdeveloper.co.uk), [tweet @fishrodgavin](http://tweet@fishrodgavin) or visit [
www.justanotherdeveloper.com](http://www.justanotherdeveloper.com).
联系作者
一、入门指南
在 2008 年 9 月发布第一款 Android 手机和 2007 年 6 月发布第一款 iPhone 手机之前,移动网络浏览器的标准化并没有立即推进。播放视频需要 Flash mobile 或低质量的 3GP 版本的视频。开发人员避免使用 JavaScript,因为 JavaScript 在大多数移动 web 浏览器上默认是禁用的,而其他浏览器根本不支持 JavaScript。一个这样的开发者,登录 stackoverflow.com,评论说使用 JavaScript 是“一场噩梦”。。。就像在 90 年代使用网络浏览器一样,但是经理们对未来充满期待。” 1
移动网站只是灰度手机上 WAP 时代的无线标记语言(WML)页面,如摩托罗拉 V50,但带有一点色彩。从那以后没有太大的变化,大多数移动网站仍然保持着同样的从上到下的线性信息流,交互性不强。这种设计风格有三个原因。
- WAP/GPRS 和 EDGE 都是速度很慢的协议,不能处理大量文件的网站,所以设计和内容被限制为快速传递网站及其信息。
- 旧手机的分辨率和长宽比很糟糕,以至于你几乎不能在屏幕上显示任何内容。
- 传统上,你使用一个球或按键来浏览移动网站。上下滚动似乎比左右滚动更自然。
1 Stackoverflow.com,由安娜卡塔发布,[
stackoverflow.com/questions/316876/using-javascript-in-mobile-webapplication#316920](http://stackoverflow.com/questions/316876/using-javascript-in-mobile-webapplication#316920)
。
我们现在不再依赖基于硬件的控件来浏览移动设备上的内容。随着每一款新的平板电脑和手机的发布,屏幕的尺寸、质量、分辨率、像素密度/PPI 和颜色深度都在增加。我们看到桌面浏览器引擎,如 WebKit 和 Geko,被插入到网络浏览器中,如移动 Safari、Android 浏览器和 Firefox,就在我们的移动设备上。这有助于开发人员制作出令人惊叹的移动网站,这些网站在目前流行的 Android 和 iOS 手机以及平板设备上的外观和感觉都是一致的。
此外,最新的手机浏览器也支持 GPU 加速。这意味着移动网络应用可以变得更加精致和互动,因为大多数渲染现在可以卸载到图形处理器上(这在几年前还是闻所未闻的)。
考虑到 Adobe axing Flash Mobile 的最新发布,以及将更快的 CPU 和 ram 塞入移动设备的持续竞争,现在不仅是进入移动 web,而且是 HTML5、CSS3 和 JavaScript 的最佳时机。
作为一名移动 web 开发人员,您现在有机会在现有 web 标准的基础上为小型笔记本电脑开发近乎原生的应用。
然而,不要被愚弄;就标准化而言,移动网络世界还有很长的路要走。所以,在这本书里,我会给你一些防御性的编程技巧,帮助你在开发移动网络时避免常见的错误和误解。
开始之前,你需要一台平板电脑和/或一台基于 Android 的移动设备来测试应用。您还需要一个坚实的开发环境来工作。
选择测试设备
虽然不是必需的,但手边有一个实体的 Android 设备,如手机和平板电脑,会有很大帮助。你可以使用 Android SDK 或普通的网络浏览器来测试你的移动网络应用。然而,这也有缺点。众所周知,Android SDK 启动极其缓慢,运行缓慢;在桌面浏览器上进行测试不允许你在设计和构建的平台上测试你的 web 应用。
与其他移动操作系统不同,Android 遭遇了开发者最可怕的噩梦,即设备碎片化。设备碎片可能由以下一些因素引起。
- 不止一个设备供应商为单一操作系统生产设备。
- 每种设备都有不同的硬件规格和限制。
- 加速计
- 全球(卫星)定位系统
- 陀螺仪
- 屏幕分辨率
- 像素密度
- 中央处理器
- 随机存取存储
- 旧设备不支持具有最新功能的最新操作系统,例如具有最新 API 和渲染引擎的最新默认浏览器。
正因为如此,挑选一款人人都有的设备进行测试变得极其困难。为了客观地看待这个问题,请参见表 1-1 中截至 2011 年 12 月 Android 设备与行业其他设备的对比统计。
表 1-1 描绘了一幅清晰的画面,Android 设备厂商为 Android 用户生产了各种各样的设备。
理想情况下,你应该挑 12 个安卓设备(6 个手机,6 个平板)。还要考虑以下标准。
- 高端设备(450 美元或以上)
- 最近六个月内释放
- 12-18 个月前发布
- 一款中档设备(150-449 美元)
- 最近六个月内释放
- 12-18 个月前发布
- 低端设备(不到 150 美元)
- 最近六个月内释放
- 12-18 个月前发布
你应该以这种方式挑选设备有两个主要原因。
- 设备功能会因价格而异。例如,通常情况下,你不会在低于 100 美元的设备中看到双核 CPU。然而,你还是应该迎合那些没有最新最棒的人。这将允许您针对能力较低的设备进行测试,并确保您的移动 web 应用将适度降级。
- 设备契约以 12 个月、18 个月和现在的 24 个月为周期结束。这是用户升级手机和设备供应商发布新硬件的理想时机。记住这一点,您应该选择购买用户将在 2-3 个月后升级的设备。同样,这将帮助您针对设备进行测试,并确保您的移动 web 应用能够正常降级。
如果你只能选择一个设备,选择最新最好的。这个装置本身可以让你使用一年多一点。如果您的目标是每年升级一次设备,那么您将会收集大量的旧设备进行测试,并且这些设备与您的用户将要使用的设备相同或相似。
出于这本书的目的,我将使用 HTC Desire HD、华硕 Eee Pad 和三星 Galaxy 智能手机。
设置您的开发环境
既然您已经选择了要测试的设备,现在是时候设置您的开发环境了。
我选择的操作系统是 Mac OS X 狮子;但是,其他平台的设置过程非常相似。
我选择了开源或免费的应用来开发。所有的应用都可以在 Mac、Windows 或 Linux 上运行。
Aptana
Aptana 是一个用于 web 开发的集成开发环境(IDE)。IDE 不同于常规的文本编辑器,如 TextMate 或 BBEDIT,也不同于网站编辑器,如 Dreameweaver。它们将提供开箱即用的开发所需的一切,并且可以扩展以适应您特定的开发风格或平台。
Aptana 基于 Eclipse,因此可以支持大多数(如果不是全部)Eclipse 插件;它将管理您的虚拟 Android 测试环境,执行代码完成,验证您的代码,并为您部署它。
要下载 Aptana,请前往[
aptana.com/](http://aptana.com/)
。您将看到图 1-1 中所示的下载选项。
图 1-1。 Aptana 下载选项
选择图 1-1 所示的“单机版”,点击下载按钮。安装它并继续安装 Android SDK。
注意:你可以改变 Aptana 中编辑器的外观来适应你的偏好(例如,你可能希望你的 IDE 有一个或暗或亮的主题)。为此,只需转到首选项。“首选项”窗口将会打开。使用左上角的过滤器并键入主题。点按搜索栏下方菜单中的“主题”选项。默认将是 Aptana Studio,但选择任何你喜欢的主题并点击 OK。
Android SDK
Android SDK 将允许您创建虚拟的 Android 环境,以便使用不同的硬件配置和 SDK/OS 版本进行开发。Eclipse 有一个插件,允许您管理、创建和配置虚拟 Android 设备,并从 Aptana 中启动它们。
在安装 ADT 之前,您需要在 Aptana 中启用 Eclipse Helios 更新站点。这包含了 Eclipse 的 Android ADT 插件的依赖项。
要启用 Eclipse Helios 更新站点,请从苹果任务栏转到 Aptana Studio 3,然后选择首选项安装/更新
可用软件站点。将出现一个类似于图 1-2 的屏幕。
图 1-2。 启用日食太阳神更新网站
要为 Aptana 安装 ADT,请转到[
developer.android.com/sdk/eclipse-adt.html#downloading](http://developer.android.com/sdk/eclipse-adt.html#downloading)
。
按照说明操作。成功安装 ADT 后,Aptana 将重新启动,您将看到类似于图 1-3 的屏幕。
图 1-3。 初始 ADT 启动屏幕
保留所有默认选项,然后单击下一步>。您可以决定是否要向 Android 发送使用数据,然后单击“完成”。接受最后一个屏幕上的所有选项,并再次单击 Finish。ADT 将开始下载最新的 SDK,这将需要几分钟时间。
现在已经安装了 ADT,您可以安装所有的 SDK 来测试您的 Android web 应用。Android ADT 可以在窗口菜单底部找到,如图图 1-4 所示。
图 1-4。Aptana的新安卓菜单
转到 Android SDK 管理器。你会看到一个 Android SDKs 下载列表,如图图 1-5 所示。展开所有 Android 版本,并确保为每个 Android 版本勾选以下选项。
- 谷歌公司的谷歌 API。
- SDK 平台
- 三星电子的 GALAXY Tab
图 1-5。Android SDK 管理器
点击安装按钮开始下载和安装过程。
在随后的屏幕上选择全部接受,然后单击安装。你应该会看到一个类似于图 1-6 的窗口。安装 SDK 的过程可能需要相当长的时间,这取决于您的计算机的能力和您的互联网速度。
图 1-6。Android SDK 管理器安装包
完成这些步骤后,您就可以用 Android SDK 的每个版本来测试您的移动网络应用了。
萨斯
SASS 是一个 CSS 预处理器。它允许您嵌套 CSS 规则,在 CSS 中使用变量,重用 CSS 块(比如用 mixins 设置一组元素的边框半径),并允许 CSS 规则继承其他规则。
SASS 将贯穿本书来编写 CSS。为了让 SASS 工作,需要安装 SASS Ruby gem。
这对于使用终端的 OS X 来说相当简单。终端可以在应用工具中找到。
打开“终端”后,输入以下命令:
sudo gem install sass
输入您的密码,等待 SASS gem 安装完成。要测试 SASS 是否已成功安装,请输入:
sass –v
如果 SASS 已经成功安装,您将看到 SASS 的版本号。要在 Windows 或 Linux 上安装,在 SASS 的下载页面[
sass-lang.com/download.html](http://sass-lang.com/download.html)
上有安装程序和说明。如果您没有安装 Ruby,您必须先安装它。从[
rubyinstaller.org/downloads/](http://rubyinstaller.org/downloads/)
下载并安装。安装 Ruby 后,从程序 Ruby【版本】
运行它,用 Ruby 启动命令提示符。从那里,运行“gem install sass”。
Apache
为了在开发环境之外的 Android 设备上测试移动网站,需要一个 web 服务器。Mac OS X 预装了 Apache,所以只需要打开它。
为此,进入系统偏好设置共享并启用网络共享,如图 1-7 所示。单击创建个人网站文件夹按钮。这将为您创建一个文件夹,将您的网页内容储存在您的 Mac 帐户中,以便在网页浏览器中查看。要测试它,请单击按钮上方的链接。这将打开带有欢迎页面的网站。
图 1-7。??【OS X 狮子】上启用网络共享
总结
既然您的开发环境已经设置好了,您就可以开始为 Android 编写和测试移动网站了。这将为您提供一个坚实的平台来开发小型和大型的移动 web 应用。
二、为 Android 创建移动 Web 应用
既然您的开发环境已经设置好了,那么您一定渴望深入研究一些代码!
在你开始之前,本章将带你了解移动网络与传统桌面环境相比的基本原理。
如果您可以一次构建和部署一个应用,并让它立即在所有设备上可用(不仅仅是 Android),生活将会简单得多。移动网络旨在解决这个问题。原生应用有其优势,当它们需要大量的图形处理、CPU 和 RAM,以及访问 Android 操作系统的几乎所有方面时,它们就会发挥作用。
像 Mozilla 这样的浏览器供应商正试图改变这种情况,并向网络标准倾斜。通过利用 Android 的原生 API,并通过浏览器中的 JavaScript APIs 向 web 开发人员提供这些 API,我们有可能在不久的将来开发出与原生应用开发人员相同的 API。与此同时,将 HTML5 引入移动设备有助于填补我们等待的空白,并提供一个坚实的基础。诸如 PhoneGap、Rhomobile 和 Appcelerator 等多种基于手机网络的应用框架将取代未来浏览器目前为我们提供的草案规范。
通过认可 web 标准,我们应该可以说,我们为 Android 手机和平板电脑部署的相同 web 应用现在和将来也可以在 iOS 和 Windows Phone 7 手机和平板电脑上工作。
本章将带你了解一些关于移动网站设计和开发的基本原则。
-
What’s different about the mobile web?
您将了解移动网络与桌面的不同之处,以及如何确保移动用户从他们可用的控件(他们的手指)中获得最佳体验!
-
Catering to your audience
在这里,您将了解受众如何影响您设计和布局移动网站,如何区分内容的优先级,以及如何为目标受众提供最佳功能。
-
Web vs. native apps
如果你对是否开发纯本地应用、混合应用或纯 web 应用犹豫不决,那么这将带你了解每种解决方案的优缺点。
-
The first line of code: Hello World
这最后一节将带您了解应用的构建模块,例如设置 ANT 进行自动部署,以及构建和压缩 SASS/CSS 文件和 JavaScript。
移动网络有什么不同?
迎合 3.654 亿永久连接用户的潜在受众使移动网络成为最令人兴奋的开发平台之一。为桌面环境创建 web 应用是令人满意的。但是,用户只能使用一个指点设备和一个键盘来与您的作品进行交互。移动网络带来了一个全新的可能性世界。移动设备充当交互元素的空白画布,用户只需触摸即可与之交互。作为一名开发人员,你可以通过占据整个屏幕,让用户沉浸在你的移动网络应用的世界中,来创造一种更加亲密的体验。
不幸的是,尽管移动网络带来了现实世界中的所有优势,但在平台继续发展的同时,您也将面临桌面环境中同样的开发和用户体验绊脚石。
物体/特征检测
移动网络上可供开发者使用的 API 的碎片化可能是一个问题。解决跨浏览器 API 差异的最常见解决方案是使用 JavaScript 来检测浏览器或设备,并根据使用的浏览器提供不同的样式表或执行特定的 JavaScript 片段。这种方法被称为用户代理(UA)嗅探或浏览器嗅探。清单 2-1 显示了一个通用的 UA 嗅探脚本。
清单 2-1。 用于 UA 嗅探的 JavaScript 代码
`// Get the user agent string
var browser = navigator.userAgent;
// Check to see whether Firefox is not in the string
if(browser.match(/Firefox/) === null){
// If it's not Firefox, send the user to another page
window.location.href = "sendstandardmessage.html";
} else {
// If it is, use the Mozilla SMS API to send an SMS
navigator.mozSms.send("01234567891", "My Message");
}`
UA 嗅探可能有什么问题?虽然您将为 Firefox 提供支持,并为其他浏览器提供后备,但您将无法支持可能与 Firefox 具有相同 API 的浏览器。
这个特殊的 API 也只在 Firefox 11+中可用,所以您还需要确保该版本包含在 UA 嗅探脚本中。
当您开始增加浏览器检测脚本的粒度时,由于必须不断更新嗅探代码以适应新的浏览器和版本,您也降低了可维护性并增加了复杂性。不知不觉中,您的 JavaScript 库变成了不可维护的意大利面条代码。
更好的方法是通过物体检测。修改后的代码可以在清单 2-2 中看到。首先,我们发现 SMS API 是否存在。如果它不存在,我们将用户发送到另一个页面;如果是的话,我们就可以发送短信了。
清单 2-2。 用于对象检测的 JavaScript 代码
`// Check to see whether navigator.mozSms is an object (if it exists)
if (typeof navigator.mozSms === "object"){
// If it does, send a message using the built-in SMS API
navigator.mozSms.send("01234567891", "My Message");
} else {
// If it doesn't, send the user to another location
window.location.href = "sendstandardmessage.html";
}`
对象检测的方法还允许我们为浏览器特定的 API 提供回退。Firefox 11 nightlies 目前只支持 SMS API,但未来可能会有其他浏览器和其他设备通过不同的方法或类支持相同的实现。
我们可以使用一个类将这变成我们应用的一个特性。我们可以在一个方法中委托消息的发送,如清单 2-3 所示。这在理论上应该允许我们使用自己的 API 在应用中发送消息。当浏览器供应商将 SMS API 添加到他们的浏览器中时,我们只需要将该方法添加到单个位置,而不是在整个应用中查找和替换它。
清单 2-3。 使用委托发送消息,用我们自己的 Web 服务作为后备
`var Message = function Message(message, recipient){
this.message = message;
this.recipient = recipient;
this.sendSMS = function sendSMS(recipient){
if(typeof navigator.mozSms === "object"){
// Send SMS using the user's mobile phone
navigator.mozSms.send(this.recipient, this.message);
} else if (typeof navigator.otherSms === "object") {
// Use another browser's SMS implementation
navigator.otherSms.sendMessage(this.message, this.recipient);
} else {
// If sending via the user's mobile isn't possible,
// send the message using a third-party web service
this.ajaxSend(this.recipient, this.message);
}
}
function ajaxSend(recipient, message){
// Send the SMS using a web-based SMS gateway via Ajax
}
}
var messageInst = new Message("my message!", "01234567891");
messageInst.sendSMS();`
正如你从清单 2-3 中看到的,无论浏览器的功能是什么,我们都可以使用对象检测来确保用户获得相同或相似的体验,无论设备的功能是什么。
使用 JavaScript 检测这些利基特性非常容易。但是如何测试 CSS3 或 HTML5 的功能,并为 CSS3 动画和 3D 转换等功能提供向后兼容性呢?
一个名为 Modernizr 的 JavaScript 库可以帮助您实现这一点。它使用相同的对象检测方法来检测用户 web 浏览器的 HTML/CSS/JavaScript 功能。
它通过向 HTML 标签添加类来修改 DOM(文档对象模型),以便为您自己的 CSS 和 JavaScript 特性检测提供挂钩。图 2-1 显示了这在 haz.io 中的作用。这将在第七章中详细介绍。
图 2-1。 使用 Modernizr 检测 haz.io 上的特征
屏幕尺寸和像素密度
在开发移动 web 应用时,您可能希望创建一个对平板设备和移动设备具有相同功能的应用,但呈现不同的视图或布局,以利用设备的额外空间或方向。媒体查询有助于促进这一点。
使用媒体查询和弹性设计的组合,您可以生成响应用户显示的视图,而不是检测用户的设备类型并为其提供视图。这就是所谓的响应式网页设计。
这种开发方法比根据用户使用的设备的类型来决定用户应该如何浏览你的网站要优雅得多。相反,你应该关注可用的空间和可用的像素密度。
像素密度是一个概念,它允许具有相同物理尺寸屏幕的移动设备根据每平方英寸可用的像素数量而改变分辨率。
Android 设备分为三类像素密度:
- 低的
- 中等
- 高的
这对您的移动 web 应用有什么影响?当您为普通网站制作图像时,您制作的单个图像不能在所有屏幕类型上缩放和工作,因为布局将随图像本身缩放以适合固定宽度或弹性布局。
对于移动网站,您通常会创建一个移动应用来适应整个视窗,并具有相同的尺寸,而不管设备的像素密度如何。
例如,如果您为低像素密度屏幕制作 500 像素宽的图像,它在高密度屏幕上会显得更小。这是因为 500 px 在高密度屏幕上不会像在低密度屏幕上占用那么多空间。
移动浏览器的解决方案是根据目标密度放大或缩小图像。例如,如果您为中等密度的屏幕开发应用,浏览器将为低密度屏幕缩小图像,为高密度屏幕放大图像。无论以哪种方式缩放图像,都会导致开销,放大图像时会出现像素化,缩小图像时会出现潜在的失真。
为了解决这个问题,我们可以专门为高密度屏幕开发应用,并允许手机缩小图像。就 CPU/GPU 和网络活动而言,这可能非常昂贵。这两个因素都会对渲染时间产生影响,并且可能会影响下载不必要资源的用户口袋。或者,我们可以使用媒体查询来确保为正确的显示类型提供正确的内容。为此,您必须将viewport
元标签的target-densitydpi
属性设置为device-dpi
,并使用媒体查询导入特定于像素密度的样式表,如清单 2-4 所示。
清单 2-4。 使用媒体查询像素密度–特定样式
`// Set the viewport to match the devices pixen density and width
// Pull in the main stylesheet
// Pull in high, medium, and low stylesheets to provide pixel density
// specific images
正如你在清单 2-4 中看到的,每一类显示器的像素比率如下。
- 低:0.75
- 中等:1.0
- 高:1.5
我们使用通用的移动样式表,以便在设备不匹配任何像素比率的情况下,我们可以提供备用图像。然后,我们使用每个像素密度类别的样式表来覆盖图像。
像素密度可能是一个难题,因为这意味着对于您在应用中使用的每个图像,您必须生成两个不同大小的图像。这也意味着,即使您今天创建了最高像素密度的图形,明天您也可能不得不为另一台像素密度更高的显示器重新导出所有内容。在选择图形包来创建你的移动网页设计时,一定要记住这一点。
迎合你的观众
记住你为谁写你的应用和他们用什么来和你的工作互动一样重要。第一步是确保你理解你的用户会用你的应用做什么。为此,您必须对其进行分类。
对你的应用进行分类将有助于你根据你的类别中的其他应用是如何设计的以及它们有什么样的特性来制定通用的交互规则。这听起来像是复制,但是它将帮助用户根据他们以前的经验快速直观地了解如何使用您的应用,从而在最少的时间内启动并运行它。
重要的是要记住,你可以建立在这些规则之上,你不必坚持它们。只要你能让你的用户打开你的移动网络应用,玩几分钟,然后马上说“我明白了”,你就完成了你的工作。
移动 web 应用有许多种类,但大多数都可以归为以下几类。
- 基于任务
- 社会的
- 娱乐
基于任务
基于任务的应用本质上非常简单。它们是为日常使用而制造的省时设备。这可以是从查找火车时刻到查找最近的酒吧或酒吧在哪里的任何事情。
有几次,我站在伦敦滑铁卢火车站的中央,盯着火车时刻表,看起来茫然和困惑,只是拿出手机启动火车时刻应用,以更快地找到火车时刻。
重要的是要记住,如果一个用户不能用你的应用在最短的时间内完成一项任务,他们会关闭你的浏览器窗口,另找一个能更快完成同样任务的浏览器。
对于基于任务的应用,有两条基本信息可以用来帮助用户更快地执行任务。
- 用户在哪里?
- 他们用的是什么设备?
这两条关键信息对您的应用来说很容易获得,了解它们将会带来很大的不同。
找出用户的物理位置和他们正在做什么将有助于你在用户访问你的移动网络应用时先发制人。
举例来说,如果你正在创建一个旅程规划器,有几件关于你的用户的事情你应该考虑。
- 用户在哪里?他们的网络连接是否有限(例如,3G/EDGE 或更糟的 GPRS)。
- 用户在移动吗?他们有时间边走边填表,用拇指输入数据吗?
这些因素不仅会影响交互元素(如输入表单)的呈现方式,还会影响如何编写代码来减少用户完成任务所需的工作量。
在图 2-2 和图 2-3 中,您可以看到在创建基于位置的工具时,了解和使用用户的位置并理解他们的情况会带来多大的不同。
图 2-2。 TFL 手机网站用户旅程
在图 2-2 中,你可以看到 TFL 旅程规划移动网站。上面的用户旅程描述了最坏的情况。该用户在移动中,容易犯数据输入错误。因此,为了完成任务,用户必须通过两次额外的页面加载,加载更多的表单字段。
有两个额外的页面来帮助用户验证有什么问题?两个额外的页面相当于超过 3G 的 4 秒以上的加载时间。您还必须考虑用户处理页面和响应页面所需的时间。
我们如何改进 TFL 移动网站?
- 增加反馈回路。我们可以在用户使用自动完成功能输入出发地/目的地位置时向他们提供建议。然后,他们可以选择一个适合他们的建议来预填充旅程规划表单字段。
- 我们可以使用用户的当前位置作为他们旅程的起点/终点的建议。
- 如果我们使用本地存储,我们还可以向用户建议最近目的地的列表。例如,如果我们知道他们刚刚计划了去某个地方的旅行,那么当他们重新打开移动 web 应用时,很有可能会想知道如何返回。
图 2-3。 BUSit 移动网站用户旅程
图 2-3 显示了一个来自 busitlondon.co.uk 的好例子。在第一次启动移动网络应用时,它会尝试查找您的当前位置。当用户键入开始和结束位置时,它会建议用户使用 Google Maps API 和自动完成来选择选项。您还可以随时选择用户的当前位置。
在你计划好你的目的地后,它会给你建议路线。所有这些信息都包含在一个页面上,无需重新加载页面。用户可以轻松地更改或修改视图,而不必等待图形(除了地图切片)加载。这提供了更多的“本地应用”的外观和感觉。
社交
社交应用的主要目标是促进与朋友或其他感兴趣的人联系和交流的能力。与社交移动网络应用交互所花费的时间通常比使用基于工具的应用所花费的时间要多得多。
社交媒体应用的主要目标通常有三个。
- 用户通过访问来消费内容。
- 用户通过访问来贡献内容。
- 用户访问参与。
这三个基本规则支撑着当今几乎所有的社交移动应用。如果用户不贡献内容,就没有内容可供其他用户消费和参与。
仅仅因为用户在社交移动网络应用上花费更多的时间,并不意味着完成一项任务(比如分享内容)的途径应该与基于任务的应用有任何不同。应该考虑用户情况的相同因素。应该是既容易分享内容,又容易消费内容。
举个例子,Twitter 和脸书在功能集上截然不同,但这两个应用在移动网络上的主要目标都是让用户更容易消费、贡献和参与。
图 2-4 显示了脸书触摸式移动网站的三个屏幕(左侧)。登录后,您会看到脸书新闻提要,因此您可以立即使用内容。您还可以看到三个清晰而独特的按钮来分享内容,如您的状态、照片和当前位置(签到)。顶部还有一个工具栏,以模式菜单或弹出菜单的形式为您提供与您相关的内容和更新(好友请求、消息和通知)。更多的功能在隐藏菜单中,这为添加更多的次要功能和动作留下了余地,而不会弄乱应用的其余部分。
图 2-4。 脸书 Touch 和 Twitter 移动网站让分享和消费内容变得很容易。
Twitter 的核心功能可以在它的顶部工具栏中找到。共享内容的清晰操作按钮以蓝色突出显示,带有独特的图标。登录后,如果用户使用过 twitter 网站,就会知道这是一个分享内容的按钮。同样的设计模式现在在 Twitter 的桌面、移动和网络版本中引起了共鸣。
娱乐
基于娱乐的应用主要是为了满足某种形式的无聊而创建的。解决方案有多种形式,从显而易见的游戏到交付音乐和视频内容。娱乐应用通常被设计成让用户沉浸在应用的环境中。这甚至可以通过当今移动网络上最基本的 HTML5 游戏来实现。
网络应用与本地应用
在过去的几年中,一个很大的争论和讨论的原因是,是将一个项目构建为一个本地应用还是一个移动 web 应用。两者各有利弊。但是,重要的是要记住,您选择的解决方案应该基于特定项目的需求和您作为开发人员的能力。最重要的是,选择能最快完成项目的解决方案!
有几个因素将帮助您决定是创建移动 web 应用还是本地应用。
- 您是否已经知道如何为目标平台开发
- 您的应用是依赖网络连接还是某种形式的在线存储的动态数据
- 您的应用依赖哪种类型的设备功能(例如,GPS、加速度计、陀螺仪、地址簿、日历、密集的 CPU/GPU 操作)
- 您的项目现在或将来是否有机会将功能移植到其他平台(例如 iOS、Blackberry、Windows Phone、desktop)
- 您发布应用的频率,以及您将如何处理用户不在他们的设备上更新您的应用的情况
- 时间和预算
如果您已经知道如何使用 web 标准进行开发,那么移动 web 应用可能是最好的解决方案。但是,如果您已经可以针对目标平台进行开发,那么开发一个原生应用可能会更有优势。然而,这将稍微关闭一个可以在其他平台上运行的应用的大门,因为除非您使用一个跨平台的应用框架,如 Marmalade,否则需要为所有平台重新创建相同的应用。
制作一个移动 web 应用是一种经济有效的方法,在将应用本地化之前,可以在所有平台上对其进行测试或原型制作。通过使用分析,你可以看到你应该把哪些平台作为原生应用的目标。通过进行用户研究,您可以看到创建一个具有特定于平台的特性的原生应用是否对您的用户有利。
如果您的应用依赖于无法通过 web 浏览器访问的 API,如电话簿、日历、陀螺仪或加速度计,那么移动 web 应用可能是不可能的,因为这些 API 目前无法通过大多数移动 web 浏览器访问。
如果您的应用依赖于动态数据,那么使用 web 标准开发应用可能是一个明智的选择,因为您可以使用 Ajax 通过网络向应用快速交付内容。您还可以使用移动 web 应用缓存和存储文件,这样当没有网络连接时,您的应用仍然可以脱机使用。
如果您经常为移动应用提供更新,您可能会遇到用户没有像您希望的那样经常更新到最新版本的问题。通过创建一个移动网络应用,你可以简单地将更新推送到你的网络服务器,你的所有用户将立即拥有你的应用的最新版本。
在图 2-5 中,你可以看到 Twitter 原生应用(左)和移动网络应用(右)展示了作为原生应用的社交应用和作为移动网络应用的区别。如你所见,没有真正的区别。移动 web 应用中要放弃的主要特性是使用第三方本地应用共享内容的能力。Twitter 还取消了在移动网络应用上分享照片的功能。对象/特征检测可以提供在某些设备上上传照片的能力。
图 2-5。 Twitter 原生应用(左)和 Twitter 移动网络应用(右)
到目前为止,本节收集的信息应该有助于您决定是使用本地网站还是移动网站。
然而,还有第三种选择。多种基于 web 的电话应用框架,如 PhoneGap、Appcelerator 和 Rhomobile,将允许您用 XHTML/JavaScript 和 CSS 构建应用,但利用一些可能只适用于本地 web 应用的 API。
这些框架为你开发应用提供了一个 web 视图,并通过使用 JavaScript 作为两者之间的桥梁,为移动 API 提供了一个代理。图 2-6 显示了多种手机网络应用框架的结构。
图 2-6。 一个多手机网络应用框架的结构
以这种方式部署您的移动 web 应用会给您带来新的机会。我们知道,在某个时候,移动网络浏览器将提供 API 来与第三方应用进行交互,并利用移动设备的硬件,如 CPU/GPU 和摄像头。所以继续开发浏览器是有意义的。然而,多电话基于网络的应用框架有助于将本地应用可用的 API 和服务带到网络应用中。
通过以这种方式构建应用,您可以一次构建并部署一个功能有限的移动 web 应用。然后,您可以在基于 web 的多电话应用框架内使用对象/功能检测来逐步增强该应用,就像增强本地应用一样。这让你两全其美。
第一行代码:Hello World
现在是你写第一行代码的时候了。在这个 Hello World 应用中,您只需创建一个带有“Hello World!”并显示在 Android 虚拟设备上。
设置
首先打开 Aptana Studio。您将需要创建一个新项目,因此转到文件新建
Web 项目。
您将看到一个类似于图 2-7 中的屏幕。输入项目名称,然后单击完成。我选择了第二章作为我的。
图 2-7。 Aptana 的新 Web 项目向导
这将在 Aptana 中创建新的空项目。新项目将出现在左侧的应用浏览器面板中。
HTML
为移动网络编写与为桌面网络应用编写没有什么不同。我们将从创建一个基本的 HTML5 文档开始。
创建新文件的方式与创建新文件夹大致相同,只是选择文件而不是文件夹。将该文件命名为index.html
。确保该文件存在于项目的根目录中是很重要的。下面的代码将构成我们的 HTML 文件的基础。
清单 2-5。Hello World 的 HTML 源代码!
`
Hello World!
`如果您不熟悉清单 2-5 中的一些 HTML 元素,第一行是新的 HTML5 doctype。在 HTML5 中,您不需要指定 DTD,这通常可以在 XHTML 1.1 页面中找到。清单 2-6 展示了 XHTML 1.1 文档类型声明和 HTML5 文档类型声明的区别。
清单 2-6。XHTML 1.1 Doctype 声明和 HTML5 Doctype 声明的区别
`
`如您所见,现在不需要搜索或记忆 DTD 路径的位置,也不需要指定 HTML 版本。
在 HTML 标签中,我添加了两个属性:<html lang="en-GB" dir="ltr">.
lang
将指定文档中使用的语言,dir
决定阅读方向。从左到右dir
被设置为ltr
,英国人lang
被设置为en-GB
。
前进到 head 元素,有两个 meta 标签,如清单 2-7 中的所示。
清单 2-7。源代码中的元元素
<meta charset="UTF-8" /> <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=no; target-densitydpi=device-dpi;"/>
第一个 meta 标记指定了文档中使用的字符集。这通常应该是 UTF 8,这将涵盖大多数语言字符。
第二个 meta 标记专门用于控制移动网站上的布局或视口。有了这个 meta 标签,我们可以使用width
属性将页面的宽度设置为等于、小于或大于视窗(浏览器屏幕的可视区域)。
您还可以使用这个标签来控制用户通过initial-scale
和maximum-scale
属性放大您的 web 应用的程度。
user-scalable
属性是一个标志,用于允许或禁止用户通过挤压或点击来放大或缩小您的移动 web 应用。
最后,target-densitydpi
属性用于指示网页应该如何根据用户屏幕的像素密度进行缩放。将此属性设置为device-dpi
将阻止图像自动放大高像素密度的设备或缩小低像素密度的设备。这有助于防止设备缩放图像时常见的图像像素化。在第三章中,你会发现如何使用媒体查询来防止图像在高/中、低密度设备上像素化。清单 2-8 显示了 viewport meta 标签的完整定义。
清单 2-8。 全视区元标签定义
<meta name="viewport" content=" height = [pixel_value | device-height] , width = [pixel_value | device-width ] , initial-scale = float_value , minimum-scale = float_value , maximum-scale = float_value , user-scalable = [yes | no] , target-densitydpi = [dpi_value | device-dpi | high-dpi | medium-dpi | low-dpi] " />
清单 2-9 显示了<title />
标签,它包含了页面的标题。
清单 2-9。 标题标签
<title>My First Mobile Web App</title>
最后,如清单 2-10 所示,在主体中,有一个包含文本“Hello World!”的<h1 />
标签。
清单 2-10。 标题和链接标签
`
Hello World!
`测试
在继续之前,您应该使用 Aptana 中的 Android SDK 创建一个 Android 虚拟设备(AVD ),以测试您的网站并查看其进度。出于本章的目的,您将创建一个功能最少的简单 AVD。
首先进入窗口 AVD 管理器,如图图 2-8 所示。
图 2-8。 创建新的 Android 虚拟设备
当 AVD 对话窗口出现时,点击位于窗口右侧的新建。
在创建新的 Android 虚拟设备(AVD)对话框中,使用以下参数。
- 名称:我的测试
- 目标:Android 4.0–API 级别 14
- SD 卡:大小:100 兆字节
- 快照:已启用
- 皮肤:内置:WVGA800
- 硬件:
- 抽象液晶密度:240
- 最大虚拟机应用堆大小:24
- 设备 ram 大小:1024
设置好所有选项后,单击创建 AVD 按钮。您的新 AVD 将出现在 Android 虚拟设备管理器中。选择它并单击开始。将会出现一个新的对话框,您应该在其中接受默认值并单击 Launch。众所周知,avd 启动和运行极其缓慢。还有其他选择,但不会在本书中讨论。
几分钟后,您应该有一个虚拟 Android 设备启动并运行。单击互联网图标启动浏览器。
现在,您需要将应用部署到 web 服务器上。在第三章的中,您将找到更多关于自动部署应用的信息,但是现在您可以使用 Aptana 将项目导出到适当的文件夹中。转到文件
你现在可以使用 AVD 的内置浏览器和你在第一章 ( [
your-ip-address/~username/Chapter-2/](http://your-ip-address/~username/Chapter-2/)
)中记下的网址访问网站。如果一切正常,你应该会看到 AVD 屏幕上的图 2-9 中显示的内容。
图 2-9。 你好世界!
总结
在这一章中,你应该已经了解了三种不同类型的 web 应用:基于任务的、社交的和娱乐的。
您应该了解用户如何与您的应用交互。除了本书之外,你应该了解在开发移动 web 应用时如何考虑用户的潜在情况,以及这将如何影响你的功能、设计和用户体验。
这一章应该让你深入了解 JavaScript 开发的最佳实践,以及响应式设计的皮毛。
最后,这一章应该已经给了你一些关于是否将你的项目作为一个本地的,web 的,或者多手机的基于 web 的应用框架项目来开始。
三、HTML5
随着生产跨平台移动应用的需求,HTML5 对移动行业来说从未如此重要。它是创建简单但功能丰富的应用的最佳候选之一,这些应用可以构建和部署一次,以支持当今所有主要的智能手机和平板设备。
对基于 HTML5 的应用的常见误解是,它们可能很慢,没有响应,并且达不到用户对原生移动应用的预期速度和质量。这仅仅是对的一半,你可能已经从上一章看到了;这取决于正在构建的应用的类型。例如,App Store 上提供的英国《金融时报》应用似乎是一个原生应用。然而,如果你仔细观察,你会发现英国《金融时报》的应用只是英国《金融时报》的移动网络应用(app.ft.com
),包装在原生应用的网络视图中。
从图 3-1 中可以看到,iPhone 和 Android 的应用看起来都很相似。抛开 UI 带来的几个特定于平台的增强,它们实际上是同一个应用。
注意:使用 PhoneGap 等多种基于手机网络的应用框架,构建一个网络应用并将其展示给各种应用市场并没有错。它增加了您的应用的曝光率,并使您的用户更容易访问它。如果 App Store 的条款和条件变得对你不利,以这种方式制作你的应用也可以为你提供一个即时的解决方案。
图 3-1。 原生《金融时报》安卓应用(左)和 iOS 网络应用(右)
在这一章中,你将学习 HTML5 的关键基础知识,以及如何在移动网络中利用它。
您还将了解如何为移动设备编码视频和音频内容,以及有助于向用户交付这些内容的服务类型。
本章将更深入地介绍如何使用媒体查询来根据屏幕属性设计内容的样式。
最后,您将了解新的表单元素,以及如何提示某些类型的输入数据来影响浏览器中的键盘。
有什么新消息?
HTML5 在 HTML4/XHTML1.1 的基础上有了重大飞跃。它提供了新的 HTML 标签,如header
、footer
、hgroup
、nav
、section
和article
,进一步改进了我们标记文档的方式。这使我们能够生产更有意义和机器可读的内容。比如我们现在可以用<
。除此之外,HTML5 还带来了 API 访问的标准化,比如地理定位、画布、web 套接字和 web 存储。
HTML5 规范中有许多新的变化,但在这一章中,我们将重点关注适用于移动设备的变化。
HTML5 规范中的变化将在提供的代码示例中显而易见。但你可能会问自己,这有什么意义?不管你是否使用新的 HTML5 元素,你的用户都会看到同样的东西。有几个原因可以解释为什么做出这样的改变会对你的用户产生影响。
- 您可以生成更清晰、更易于维护的代码。
- 机器消费者会更容易阅读和理解你的代码。机器包括搜索引擎机器人、浏览器插件和依赖于理解文档内容结构的功能。
- 您不必在文档中定义那么多的类和 id。您可以更多地依靠 cascade 来为您完成大部分工作。
注意:虽然示例中没有显示<body />
、<html />
或<head />
标签,但是所有元素都可以放在文档的主体中,除非另有说明。
<条/ >
<article />
元素用于表示页面上的独立内容,比如博客文章、新闻文章或评论。原则上,一篇文章应该包含自己的页眉、内容和页脚。您还可以在元素中嵌套关于文章作者的信息。您还可以将文章元素嵌套在另一个文章元素中,以帮助进一步组织内容,例如文章评论。
图 3-2 显示了一个<article />
元素相对于一个 HTML5 文档的位置。清单 3-1 显示了一些基本 HTML5 元素的结构,以及<article />
元素在这个层次结构中的位置。
图 3-2。 <article />
元素(以灰色突出显示)与在移动网站文档中找到的元素相关
清单 3-1。html 5 中一篇文章的拟议结构
`
Article Title
Created by Daniel Carpenter on
Article Content
清单 3-1 中显示的元素似乎有意义。<header />
元素包含与文章相关的所有标题信息,比如标题、作者和发布时间。请注意,文章中的内容不需要包装在另一个元素中。最后,<footer />
包含关于作者的信息,它嵌套在一个<address />
元素中。
将此与清单 3-2 中的进行比较,它显示了在以前的 HTML 版本中您可能是如何编写的。
清单 3-2。html 4 和 Prior 中一篇文章的拟议结构
`
Article Title
Created by Daniel Carpenter on
March 15th 2012
Article Content
正如你从清单 3-2 中看到的,这个标记没有真正明显的结构。有许多 div 都有与之相关的类;然而,创建这样的文档并没有真正的标准。
<暂且不提/ >
<aside />
元素可以用来表示与网站主要内容无关的内容,比如 tweets、相关链接、标签和导航元素。这些通常出现在文件的左侧或右侧,如图 3-3 中的所示。
图 3-3。 具有<aside />
元素的文档结构(以灰色突出显示)
我们可以根据屏幕大小隐藏 aside 元素,并在用户单击按钮显示它时显示它,从而利用 aside 元素。这种设计模式可以在 facebook mobile web 应用中找到,并将在第四章的研讨会中进行更深入的探讨。
清单 3-3 展示了应该如何使用 aside 元素,而清单 3-4 展示了如何在 HTML4 中编写相同的代码。
清单 3-3。html 5中提出的结构
`
从清单 3-3 中可以看出,我们使用<aside />
元素为网站导航,因为它存在于<section class="content" />
元素定义的内容部分之外。<aside />
元素将被浮动在内容的左边。
为 HTML4 编写的相同标记看起来像清单 3-4 中的。
清单 3-4。 提议在 HTML4 中搁置结构
`
</body> `正如您所看到的,有几种有用的方法来寻呼移动设备。虽然这些例子是分开展示的,但是您可以将它们组合起来。例如,您可以结合容器和 Ajax 方法来分离应用的不同部分和功能。您还可以使用 Ajax 通过 JSON/XML 和本章中提到的任何方法来动态加载内容和数据,从而生成新的动态视图。
下一节将带您完成创建 MoMemo 应用的第一个阶段。
创建应用
创建可用的移动 web 应用的关键在于规划。确定移动 web 应用的关键功能,以及用户如何获得重要的功能和数据,将有助于您决定如何使用分页技术、设计和 UI 来实现应用本身。如果你不喜欢计划,这可能是一个费力又无聊的任务,但它会帮助你在开始开发和设计之前解决问题。
策划 MoMemo
这个过程的第一步是用一句话来定义应用。在用一句话定义一个应用的时候,你应该尽量避免包括特性或者技术细节。句子应该简单描述应用及其目标。对于 MoMemo,应用定义如下:
MoMemo 是一个应用,允许用户快速记下他们在电影院看到的电影预告片,并在电影上映时得到提醒。
下一步是定义必须具备的特性,这些特性将有助于满足应用的主要目标。MoSCoW(必须有,应该有,可能有,不会有)可以是定义应用核心特性和功能的好方法。它将允许您定义核心功能(必须具备),提供附加值的功能(应该具备),如果在项目结束时还有剩余时间可以实现的功能(可能具备),以及由于时间或资金限制而无法实现但可以在不久的将来实现的功能(不会具备)。这将有助于防止范围蔓延和“永无止境的项目”综合症,即开发人员不断谈论一个应用及其极长的不可能的功能列表,但从未实际创建它。
要使 MoMemo 成功,应用必须:
- 允许用户在个人列表中快速添加和删除电影
- 允许用户观看列表中的电影
它应该:
- 在用户键入时提供电影建议列表
- 显示关于电影的信息,包括
- 摘要
- 发布日期
- 演员表
它可以:
- 允许用户观看电影预告片
- 允许用户播放电影中的声音剪辑
- 允许用户在社交网络上共享添加到其列表中的项目
- 查看电影备忘录时,向用户显示最近电影院的地图
不会的:
- 电影上映时向用户发送通知
- 允许用户在看过电影后对电影进行分级
- 允许用户邀请其他用户去电影院看电影
既然已经定义了核心特性和功能,我们就可以开始基于必须具备、应该具备和可能具备的特性集来创建用户旅程了。
首先,我们应该从必备类别中构建我们的核心特性集。图 4-1 显示了应用的核心功能应该如何运行。用户应该启动应用,并提出了他们已经添加的电影列表。从这里,他们可以添加或删除列表。然后它们将被带回到电影列表。
图 4-1。 应用的主要特性
现在,您可以在此基础上开始添加应有的功能。在图 4-2 中,可以看到只增加了电影信息功能。我们仍然需要在用户输入时提供一个电影建议列表,但是这个电影建议列表将是 Add Movie 特性的一个特性,而不是 Movie Info 特性。
图 4-2。 应用的二级特性
最后,您可以添加可能拥有的或增值的功能,如图图 4-3 所示。
图 4-3。 增值增值功能
正如您从图 4-3 中看到的,电影信息功能有三个子功能,允许您在电影信息主功能之间导航。这增加了应用的复杂性,并建议电影信息应该潜在地被分解到它自己的页面或卡片组中。
现在我们已经清楚地了解了应用当前应该如何运行,我们可以开始创建 UI 了。
创建用户界面和 HTML
如果你曾经专门为 Android 开发过一个应用(原生或网络),你会知道一些设计原则不同于你对 iOS 或 Windows Mobile 等其他移动操作系统的预期。例如,在谷歌 Galaxy Nexus 和三星 Galaxy Tab 上,系统栏(导航栏和组合栏)位于屏幕底部,并且在使用 Android 浏览器时总是活跃或可见的。一个好的设计原则是不要把工具栏堆叠在系统栏的顶部;这将防止用户无意中点击系统按钮,而他们实际上是想与你的应用进行交互。
为了方便用户使用该应用,有必要为用户提供一种清晰的方式来添加和查看他们的电影,同时也为您提供在未来添加新功能的能力。
LinkedIn 提供了一个很好的清晰的例子。正如你从图 4-4 中看到的,很明显,移动网络应用的主要用途是搜索人和查看最近的更新。如果你想访问更多的功能,在搜索栏旁边的“在”图标下有一个隐藏的工具栏。如果你想更新你的 LinkedIn 状态,你可以点击右上角的信息气球图标。
图 4-4。 LinkedIn 为整洁的登陆/主页提供的解决方案
在应用的每个页面上都可以看到这个顶栏。在设计任何基于移动设备的网站时,你应该记住,它可以在各种屏幕尺寸上以横向或纵向模式浏览。
注:迄今为止,还没有已知的方法可以将网络浏览器的方向锁定为横向或纵向。所以当你设计一个移动网络应用时,你应该考虑到方向会改变。
创建电影列表
MoMemo 应用的 UI 围绕着屏幕顶部的搜索栏。图 4-5 和图 4-6 显示了应用的电影列表部分,包括平板电脑和移动设备的任务栏。
图 4-5。 电影列表为风景写字板
从图 4-5 中可以看到,在浏览之前添加到列表中的电影时,可以访问搜索和添加电影的功能。用户也很清楚,这会将项目添加到列表中,而不是搜索现有的列表,因为提交表单的按钮被标记为 add 而不是 search。
图 4-6 以同样的方式显示信息,但屏幕更小;但是,列表项稍大一些,以适应用户手指敲击准确度可能较低的情况。虽然列表项被捆绑在一起,但是用户点击以查看关于电影的更多信息的目标是相当大的。将任务栏放在顶部还允许用户自然轻松地浏览他们保存的电影列表,而不必担心意外激活应用的另一部分。应用的两个 UI 模型在 HTML 方面是相同的;但是,我们可以使用 CSS 媒体查询来确定特定的显示大小和方向。您还可以利用流体布局来确保应用对方向和屏幕大小的变化做出正确的反应。
图 4-6。 人像手机电影列表
用 HTML 标记这一点非常简单。首先,在项目的根目录下创建一个名为application
的文件夹。在该文件夹中,再创建三个名为css
、img
和js
的文件夹。css
文件夹将存储你的 CSS/SASS,img
将存储你所有的图片和精灵,js
将存储你所有的库和应用 JavaScript。
您还需要在js
中创建两个名为lib
和app
的文件夹,在js/app/
文件夹中创建一个名为bootstrap.js
的文件,在css
文件夹中创建一个名为mobile.scss
的文件。
在application
文件夹中创建一个名为index.html
的新文件;清单 4-11 中的代码将有助于引导应用。
清单 4-11。 初始引导 HTML
`
你可以看到,有一个div
围绕着名为shoe
的甲板。这将有助于包含出现在每个页面上的全局元素,例如顶部任务栏,并且如果应用需要在未来扩展,它将拥有多个面板。除了与赌场和扑克牌相关的命名约定之外,您可以使用任何层次命名约定。
注意:这只是一个命名约定,我采用它是为了让我和其他开发人员更容易理解我的应用的结构。这也使得在编写 CSS 和 JavaScript 来挂钩移动 web 应用的功能时,语义更加清晰。您可以使用任何您希望的 id 或类,或者您可以使用 suite 并使用我的。只要确保它们是有意义的。
你会注意到 CSS 没有链接到mobile.scss
。这是因为 SCSS 文件需要被 SASS 编译并转换成 CSS。一旦编译完成,就会出现mobile.css
文件。打开mobile.scss
文件按 Shift + CMD + R 然后按 1。这将把 SASS 文件编译成 CSS 文件。(SASS 将在第五章的中介绍。)
是时候为应用创建标题了。在清单 4-12 中显示的代码应该被添加到<div id="shoe">
元素中,但是在<div id="deck">
元素之前。
清单 4-12。 标题代码
`
Mo Memo
这将简单地创建一个标题和表格供用户搜索,如图 4-7 所示。使用 CSS,这将被放置在屏幕的顶部。
图 4-7。 没有样式的任务栏
现在是时候添加第一张卡片了,电影列表卡片。这很简单,通过创建一个无序的数据列表来实现,如清单 4-13 中的代码所示。
清单 4-13。 保存的电影列表
`
`在 HTML5 中,可以用href
标签包围块级元素。这使得将列表项的全部内容链接到另一个资源变得更加容易。
从图 4-8 中可以看出,这个页面看起来相当无聊。下一个研讨会将涉及使用 CSS 样式的应用。
图 4-8。 完整电影列表页面
电影搜索和添加
有了电影列表功能,现在是时候介绍搜索和添加电影的任务了。这可以通过两种方式之一来实现。
- 用户搜索一部电影,并得到一个列表。从这个列表中,用户点击电影,然后将他们带到电影信息屏幕。从这个屏幕上,用户可以将电影添加到列表中并返回到电影列表。
- 第二个选项是向用户提供建议,允许他们点击适合他们的建议,然后单击 add 按钮。然后,用户可以在以后查看电影信息。
这两种选择都没有错,但最优的还是在于用户在搜索电影时所处的情境。要回答这个问题,你需要参考莫斯科对这个项目的要求。必备要求之一是“允许用户快速在个人列表中添加和删除电影”。用户很可能会打开应用,搜索电影,添加电影,关闭应用,并在以后更详细地查看电影列表。图 4-9 和 4-10 展示了基于此的平板电脑和移动设备的搜索功能。
图 4-9。 平板电脑电影列表
搜索和显示搜索结果将是 JavaScript 的任务(在第八章的中介绍)。
图 4-10。 手机电影列表
电影信息
MoMemo 应用的最后一部分是电影信息部分。它有几个子功能,包括电影简介,剪辑,演员阵容,配乐和最近的电影院。您可以在单独的卡片上呈现这些信息,但是当您在大屏幕设备(如平板电脑)上查看小部分内容(如概要)时,最终会有很多空白空间。为了解决这个问题,您可以将所有内容放在同一个视图中,但允许用户在纵向设备上侧滚至内容,以利用垂直空间,并在横向模式下正常滚动。图 4-11 和 4-12 显示了这应该如何呈现。
图 4-11。 人像移动设备上的电影信息
图 4-12。 平板设备上的电影信息
虽然两个视图的显示略有不同,但内容是相同的,并且可以使用 CSS 媒体查询重新定位,以适应设备的方向。创建一个id
为card-movie_info
的新卡,并添加来自清单 4-14 的 HTML。
清单 4-14。 电影信息头
`
My Movie Title
Released: Monday 10th March 2012
这将为标题创建标记,根据设备的方向,可以使用 CSS 以不同的方式显示该标记。您使用hgroup
对发布日期信息进行分组,该信息不应该包含在h2
元素中。
清单 4-15 显示了大纲块,它将只包含文本。有一个div
,其中一类内容围绕着块内的内容,但不包括主标题。这是为了使内容可以滚动,但标题始终在视图中。
清单 4-15。 剧情简介块
`
Hello world, this is my synopsis
清单 4-16 显示了铸造块。从设计上看,演员名单应该可以在它的区块内滚动;但是,标题应该始终保持在顶部。这个块还显示了列表将被标准化,以减少 CSS 中的臃肿。
清单 4-16。 施展封锁
`
Cast List
Actor Name
然后你移动到视频块,如清单 4-17 所示。在两种线框中,视频都以网格格式显示,但它们是灵活的,因为一行可以包含两个或四个视频,这使得使用表格不灵活。为此,您可以选择使用常规列表,并根据设备的方向使用 CSS 对其进行格式化。
清单 4-17。 视频块
`
Video Clips
Clip name - 00:38
音轨块非常简单,因为它在两个方向上都很相似,在平板电脑和移动设备上都是如此。这显示在清单 4-18 中。
清单 4-18。 配乐块
`
Soundtrack
Title | Artist | |
---|---|---|
A Ridiculously Long Track Title | Track Artist | |
A Ridiculously Long Track Title | Track Artist |
如您所见,每行的第一列都有一个canvas
元素。我们将使用 HTML 画布来生成播放按钮和动画进度条。
最后,清单 4-19 显示了最近的电影院街区。这由一个带有类map
的div
组成。谷歌地图应用编程接口将用于这项任务。
清单 4-19。 最近的电影院街区
`
Closest Cinemas
为 MoMemo 创建标记的工作到此结束。任务栏如何对应用做出反应将在 JavaScript 的第八章中讨论。
如果您看到类似于图 4-13 中所示的内容,请不要惊慌。你将在第五章中学习如何使用 SASS 生成模块化 CSS。
图 4-13。 三星 Galaxy 标签上的完整标记
您可能希望做的最后一件事是开始实现应用的离线缓存功能。这将允许用户在没有接收信号时浏览他们的电影列表。
第一步是将manifest
属性添加到html
标签中,如清单 4-20 所示。
清单 4-20。 应用清单属性
`
`现在在应用目录的根目录下创建一个名为momemo.cache
的文件。在这个文件中,添加清单 4-21 中的代码。
清单 4-21。 缓存清单文件
CACHE MANIFEST index.html js/app/bootstrap.js css/mobile.css
这将确保缓存index.html
、bootstrap.js
和mobile.css
文件以供离线查看。随着应用的构建,更多的文件和规则将被添加到缓存清单文件中。
总结
从本章中,你应该已经了解了如何在移动 web 应用中管理分页,以及如何根据项目的需求选择合适的分页策略。您还应该了解如何开始构建应用——从想法到需求,从 IA/线框到用 HTML 编写基础代码,以及设备的方向和屏幕大小将如何影响您设计应用。
在第五章中,你接下来将学习 CSS 如何改变你的风格、动画和提高你的移动网络应用的性能,以及 SASS 如何帮助组织你的 CSS 规则和产生一组结构化的 CSS 文件。
五、移动 CSS3
移动开发最令人兴奋的一个方面是在最新的智能手机上通过浏览器支持 CSS3。在 CSS3 之前,我们依赖于使用 JavaScript 来提供令人眼花缭乱的动画和过渡,只需将样式应用于 DOM 元素,如父元素中的最后一个元素或交替的表格行。
在这一章中,你将学习一些 CSS3 的新特性,比如动画和过渡。您将了解 CSS3 如何提供与最基本的动画概念相似的特性,称为关键帧。
您将学习如何在移动 web 应用中导入新的字体,这将为您的受众提供更广泛的字体集。您还将了解 CSS3 的一些关键特性,如文本阴影、选择器、渐变和新的边框属性。此外,您将简要地接触 CSS 媒体查询,这将帮助您应用基于屏幕分辨率和像素密度的样式。
最后,您将看到语法上非常棒的样式表(SASS)形式的 CSS 预编译器的强大功能,通过它您将了解如何简化您的 CSS 工作流并减少编码时间。
特定于供应商的属性
在撰写本文时,许多 CSS3 属性,如border-radius
和opacity
,已经被标准化。然而,浏览器制造商可以开发他们自己的新 CSS 属性的实现。为了避免语法差异引起的冲突,尚未标准化的新 CSS 属性通常会以供应商前缀开头。例如,在border-radius
的标准化之前,在 CSS3 中有几种可能的方法来声明它。
-moz-border-radius
-o-border-radius
-webkit-border-radius
border-radius
正如你所看到的,这个列表中的最后一个声明是现在的标准化版本,对于基于 Gecko 的浏览器(Firefox),特定于供应商的实现以-moz-
为前缀;对于 Opera,以-o-
为前缀;对于基于 Webkit 的浏览器(Chrome、Android 浏览器、Dolphin),以-webkit-
为前缀。
还有更多特定于厂商的前缀,但一般来说,对于 Android 来说,-moz-
、-o-
和-webkit-
应该足够了。始终包括标准实现是很重要的。
有一些方法可以避免在需要时声明所有四个 CSS 属性,我将在本章后面的“CSS 预编译器(SASS)”一节中对此进行解释。
CSS 动画和过渡
CSS3 引入了 DOM 元素的 CSS 过渡和转换。您可以使用这些来代替传统的动画 DOM 元素的方法,方法是使用 JavaScript 中的计时器来操纵它们的 CSS 属性。你可能会问自己,为什么我要用 CSS 做动画而不是 JavaScript?当然,CSS 应该用于样式,JavaScript 应该用于交互。事实是,通过使用 CSS3 制作动画,您可以将大量经常使用 JavaScript 传递给设备 CPU 的繁重工作卸载到设备的 GPU(如果它有 GPU 的话)。这可以使动画更加流畅。
过渡
CSS 过渡允许您在两种 CSS 样式之间创建过渡。您可以通过创建一个 CSS 样式并向其添加另一个样式来调用转换。CSS 转换将处理两种状态之间的变化。
在 CSS3 中创建一个过渡非常简单。首先你创建你的div
元素。
<div class="test"></div>
接下来,为 CSS 元素创建一个样式。在这个样式中,将width
和height
设置为100px
,将position
设置为absolute
,因为您将把元素移动到页面上的不同位置。您也可以通过将border-radius
设置为50px
来将正方形变成圆形。您还明确地将top
和left
位置设置为0px
,将background-color
位置设置为blue
。
.test { width: 100px; height: 100px; position: absolute; top: 0px; left: 0px; border-radius: 50px; background-color: blue; }
这将呈现出类似于图 5-1 中所示的图像。
图 5-1。 渲染一个 CSS 圆圈
现在你需要为球设置下一个状态。这就像创建一个具有不同属性的新样式一样简单。
.second-position { left: 50%; background-color: yellow; }
正如您所看到的,新的属性将圆设置为位于屏幕的中间,其background-color
为yellow
。将这个 CSS 类添加到测试中div
。
<div class="test second-position"></div>
现在你会看到一个类似于图 5-2 中所示的屏幕。
图 5-2。 测试分区最终位置
最后要做的是给.test
类添加一个过渡。这将规定应该如何过渡和过渡什么属性,以及过渡的时间。
过渡属性目前是特定于供应商的,并且,像往常一样,包含所有供应商属性是一种好的做法。下面的代码将为test
元素的所有属性创建一个转换。
.test { width: 100px; height: 100px; position: absolute; top: 0px; left: 0px; border-radius: 50px; background-color: blue; transition: all 2s; -moz-transition: all 2s; -webkit-transition: all 2s; -o-transition: all 2s; }
为了让转换工作,您需要动态地将second-position
类添加到元素中。您可以使用 JavaScript 来做到这一点。下面的脚本将搜索类名为test
的第一个元素,并将second-position
类追加到其中。您应该将它放在测试元素的下面,如图所示。
`
当您将页面加载到移动设备上时,圆圈应该会在屏幕的中心出现,并逐渐变为黄色。
还可以通过指定属性、持续时间、计时函数和延迟来控制应该转换哪些属性,如下面的示例所示。
[-moz-|-o-|-webkit-]transition: property transition-duration transition-timing- function transition-delay [, property duration timing-function delay]
使用这种简单的方法,您可以指定任意多的属性来制作动画。表 5-1 列出了可能的值。
例如,您可能想要在颜色过渡开始五秒钟后开始过渡左侧位置,并减缓左侧位置。在这种情况下,您可以使用下面的代码。
.test { width: 100px; height: 100px; position: absolute; top: 0px; left: 0px; border-radius: 50px; background-color: blue; transition: left 5s ease-out 5s, background-color 5s ease 0s;
-moz-transition: left 5s ease-out 5s, background-color 5s ease 0s; -webkit-transition: left 5s ease-out 5s, background-color 5s ease 0s; -o-transition: left 5s ease-out 5s, background-color 5s ease 0s; }
动画
有时,您可能希望对动画有更多的控制。例如,如果您可以从一个位置到另一个位置制作动画,同时在动画中的某些点更改某些 CSS 属性,这不是很好吗?这被称为关键帧。如果您有 flash 动画方面的经验,您会更好地了解如何在 Flash 时间轴中对对象进行重大更改,并在它们之间创建补间动画。关键帧现在在 CSS 中可用。与往常一样,在撰写本文时,这是特定于供应商的,因此为了兼容,请使用所有可用的供应商。在这个演示中,您将在屏幕上制作一个圆圈的动画,并使其弹跳。
在开始创建弹跳球动画之前,请看图 5-3 中所示的动画。
图 5-3。 期望的动画序列
从图 5-3 中的动画序列可以看出,其目的是模仿一个弹跳的球。CSS 关键帧功能允许您指定您希望以百分比增量制作动画的 CSS 样式。我们可以使用图 5-3 中所示的信息来创建关键帧规则。
首先,使用@keyframes
规则和关键帧的名称创建一个新的关键帧定义,如下面的代码所示。
@keyframes bouncyball { }
接下来,使用百分比标记和 CSS 样式指定动画附加元素的开始位置。
@keyframes bouncyball { 0% { top: 0px; left: 0px; } }
这里,您已经指定了关联的元素应该从左上角开始。
接下来,指定动画中的各个分段。以图 5-3 为指导,有 0%、12.5%、25%、37.5%、50%、62.5%、75%、87.5%、100%的 CSS 规则。
@keyframes bouncyball { 0% { bottom: 100%; left: 0px; } 12.5% { bottom: 0px; left: 12.5%; } 25% { bottom: 50%; left: 25%; } 37.5% { bottom: 0px; left: 37.5%; } 50% { bottom: 25%; left: 50%; } 62.5% { bottom: 0px; left: 62.5% } 75% { bottom: 12.5%; left: 75% } 87.5% { bottom: 0px; left: 87.5% } 100% { bottom: 0px; left: 100% } }
现在是时候为你的球创建一个新的 CSS 规则了。下面的代码将从一个正方形创建一个圆,并将动画应用到元素。
.ball { background: black; width: 100px; height: 100px; position: absolute; border-radius: 50px; animation: bouncyball 2s ease-in-out; -moz-animation: bouncyball 2s ease-in-out; -webkit-animation: bouncyball 2s ease-in-out; }
这个例子中的动画 CSS 属性是用速记写的,并且同样是特定于供应商的。表 5-2 列出了动画属性按顺序取的参数。
当您将动画加载到设备上时,它应该会自动播放。它不是一个非常平滑的弹跳球,但这只是为了证明 CSS 可以成为动画的一个非常强大的工具,不费吹灰之力。您还可以使用 JavaScript 来动态编写 CSS 动画脚本。对于更密集的动画,也有 HTML5 画布。
CSS3 新特性
除了动画、转换和过渡,CSS3 规范还有几个值得注意的新特性。在本节中,您将学习如何使用@font-face
通过导入字体文件将新的字体引入到您的移动 web 应用中。
您还将学习如何使用几个新的边框样式元素,比如border-radius
(这将允许您在元素上创建圆形边框,而不需要额外的标记或 JavaScript)、box-shadow
和border-image
。您还将学习如何根据文档的大小创建可缩放的 CSS3 渐变,而不需要重复的背景图像并节省带宽。
这一节还介绍了几个新的 CSS3 选择器,它们使基于状态和层次的 DOM 元素样式化变得更加容易。
@font-face
@font-face
是 CSS3 的一项新的标准化功能,允许您使用网页安全字体列表之外的字体(如 Arial 和 Times New Roman 等字体,它们通常在大多数设备上都能找到)。这给了你更多的自由去创造你的字体。在@font-face
之前,使用非网页安全字体的非标准化方法包括 cufon(一种利用 Canvas 和 SVG 的技术)、sIFR(虽然现在不再保留,但 sIFR 使用了 Flash)和标准 CSS 图像替换(一种利用预先渲染的文本图像作为屏幕上应显示文本的背景图像的方法)。
重要的是要记住,虽然你对你使用的字体有完全的自由,但你必须确保字体确实与你的内容和受众相关。同样重要的是要记住,有些字体适用于标题,但不适用于正文,因为在较小的字体下会变得不可读(见图 5-4 )。例如,对于正文来说,漫画 Sans 是一个糟糕的字体选择。
漫画无字体是独一无二的:在全世界都被使用,它是一种不想成为铅字的字体。它看起来很普通,是手写的,对于我们认为有趣和自由的事情来说是完美的。对玩具店的遮阳篷来说很好,但对新闻网站、墓碑和救护车的侧面来说就不那么好了。”
www.bbc.co.uk/news/magazine-11582548
图 5-4。 Hello World with a web 字体
在网络上使用@font-face
有几个注意事项。最大的问题是关于许可。为了在浏览器中呈现字体,字体必须是可下载的。当使用可能附有许可证的购买字体时,这可能会带来潜在的问题。在您的项目中使用 web 字体之前,您应该检查许可证是否允许使用@font-face
下载或交付字体。如果你不能使用你想要的字体,你可以使用谷歌字体目录,使用开源网络字体中的任何字体(见图 5-5 )。谷歌还提供了一种便捷的方法来嵌入托管在其服务器上的网络字体。
图 5-5。 谷歌网页字体
网络字体的第二个警告是它们的文件大小。使用单一的 web 字体不会对加载时间产生太大的影响,但是如果您使用多种活动的 web 字体或具有多种字体样式的 web 字体,您可能会遇到页面加载时间缓慢的问题。因此,只包含 web 应用所需的字符集和字体样式很重要,这样可以减少字体的负载。
Android 浏览器足够智能,只在页面上实际使用时加载一个字体族。例如,如果您定义一个h4
元素来使用 web 字体,那么除非该元素存在于页面上,否则 web 字体不会下载,即使 CSS 类中有该字体的定义。
在撰写本文时,Android 浏览器仅支持 TTF 和 SVG 字体,这是两种最大的未压缩字体格式。其他格式包括 EOT 和 WOFF。当声明@font-face
支持其他浏览器和时,包含所有字体格式是很重要的,这样当 Android 浏览器开始支持其他格式时,它们可以被加载而不需要改变你的代码。顺序应该是大小优先(从最小的开始),因为 Android 浏览器将选择第一个可用的格式来使用。万一 Android 可能已经包含了你想在设备上使用的字体,你也可以先指定字体的本地名称。如果找到了该字体,就不需要从网上加载和下载该字体。
图 5-6。Android 版谷歌浏览器中的字体负载
@font-face
声明用于声明新字体。CSS 文档中的每个新字体声明都使用@font-face {}
。
@font-face { font-family: "MyFont"; src: url(’/path/to/my/font.otf’); }
从这里开始,你可以定义font-family
来引用 CSS 中的字体。最后,声明字体的来源。这可以是服务器上的路径,也可以是远程服务器上的字体。
然后,您可以使用传统方法在 CSS 中的任何地方自由使用该字体系列。
h1 { font-family: "MyFont"; }
下面的代码示例展示了如何使用@font-face
的完整声明。
@font-face { font-family: "My Font With Spaces"; src: local("My Font With Spaces"), url("/path/to/fonts/my-font-with-spaces.woff") format("woff"), url("/path/to/fonts/my-font-with-spaces.eot") format("embedded-opentype"), url("/path/to/fonts/my-font-with-spaces.svg") format("svg"), url("/path/to/fonts/my-font-with-spaces.ttf") format("truetype"); font-style: normal; font-weight: normal; }
文本阴影和文本描边
允许你使用 CSS 在文本后面创建不同数量的阴影。text-stroke
允许您在文本的内侧边缘绘制轮廓。text-shadow
和text-stroke
也可以用在@font-face
字体上。
要在文本周围创建一个基本的阴影,只需在 CSS 中添加text-shadow
属性。属性接受下列值和格式。
text-shadow: horizontal-offset vertical-offset blur color;
例如,下面的 CSS 样式将产生类似于图 5-7 所示的结果。
h1 { text-shadow: 10px 10px 10px #000000; }
也可以使用负数表示阴影的位置。这将使水平偏移的阴影向左偏移,垂直偏移的阴影向上偏移。
通过以像素为单位指定笔画宽度及其颜色来定义text-stroke
属性。text-stroke
属性接受以下格式的值。
text-stroke: width color;
它的用法与text-shadow
非常相似,如下面的代码片段所示。
h1 { text-stroke: 1px #000000; }
图 5-7。 文字阴影效果(左)和笔画效果(右)
选择器
选择器允许您使用 CSS 对 DOM 元素应用样式。通常有两种类型的选择器:常规的 CSS 类和元素和 ID 选择器,如.elementclass
、#elementid
和element
。还有赝选者,比如:link
、:visited
、:hover
、:active
。
CSS3 引入了几个新的选择器,允许您根据属性值、输入状态和元素在 DOM 中的位置来选择元素。
有用的表单选择器
表单选择器将使您能够根据表单输入的状态或类型来设计它们的样式。在 CSS3 之前,您需要手动将类分配给文本、复选框、单选按钮、提交字段和按钮,因为没有明确的方法将样式应用于这些字段。这是因为它们都是<input />
元素,所以任何为输入元素创建全局样式的尝试都会使所有字段类型的样式完全相同。
使用 CSS3,您现在可以使用新的属性选择器将样式应用于特定的输入类型。表 5-3 给出了属性选择器格式。
您可以更改属性和值以匹配任何元素。例如,要选择表单中的所有文本字段,可以使用下面的 CSS。
input[type="text"] { border: 1px solid #000000; }
这将在所有文本元素周围创建一个单像素的边框。
您也可以使用表 5-4 中给出的伪选择器选择所有被选中、启用或禁用的元素。
你可以组合和链接 CSS 选择器。例如,如果您想要选择所有被禁用的文本表单字段,您可以使用下面的 CSS。
input[type="text"]:disabled { opacity: 0.5; }
替代 JavaScript 的有用选择器
使用 JavaScript 选择另一个元素的最后一个子元素,并对其应用一个类来移除浮动元素的边距或填充,这是很常见的。如果您有一个多行的三列布局,您也可以使用 JavaScript 选择元素中的每三个子元素,并对其应用类。有了 CSS3,您不再需要这样做。
您可以使用:last-child
伪类选择元素的最后一个子元素。例如,如果您想选择一个ul
中的最后一个li
,您可以使用下面的 CSS。
ul li:last-child { margin-right: 0px; }
您也可以做同样的事情来选择任何元素的第 n 个子元素。使用:nth-child
、:nth-last-child
、:nth-of-type
、:nth-last-of-type
可以根据下级指标和下级类型及指标进行选择,如表 5-5 所示。
例如,如果您想选择一个ul
中的每三个li
并使文本变成灰色,您可以使用下面的 CSS 样式。
ul li:nth-child(3) { color: #CCCCCC; }
正如您所看到的,有许多新的 CSS 选择器可以使您的移动 web 应用的样式更加简单。还有更高级的选择器可供选择。
渐变
CSS3 渐变允许您向元素添加背景渐变,而无需使用重复的图像。这可以节省带宽,并允许您根据屏幕大小和方向创建可缩放的渐变背景。目前,CSS3 渐变是特定于供应商的。每个供应商似乎都有自己的方式来产生 CSS3 渐变。这一节将重点介绍 WebKit 的实现。
有两种类型的渐变你可以在 CSS3 中使用:线性和径向。线性渐变将从屏幕的一侧流向另一侧,径向渐变将从中心点向外发散,如图图 5-8 所示。
图 5-8。 线性(左)和径向(右)渐变
线性渐变
线性渐变具有以下语法,必须使用background
属性作为背景应用。
.box { background: -webkit-linear-gradient(start, start-color, end-color); }
您可以将起始位置指定为单个位置(左、上、右、下)或这些位置的组合。例如,要从左下角开始线性渐变,可以使用下面的代码。
.box { background: -webkit-linear-gradient(bottom left, green, red); }
图 5-9 显示了这个代码片段的结果。
图 5-9。 从左下角开始的线性渐变
您也可以用度数来指定渐变的起点。例如,将起点设置为45deg
将与将起点设置为bottom left
具有相同的结果。
.box { background: -webkit-linear-gradient(45deg, green, red); }
除了标准的双色渐变,您还可以在渐变背景中使用多种颜色。您只需在位置后指定更多颜色。例如,下面的代码将使用线性渐变创建一面爱尔兰国旗,如图 5-10 所示。
.box { background: -webkit-linear-gradient(left, green, white, orange); }
图 5-10。 使用 CSS3 渐变创建爱尔兰国旗
CSS3 渐变也支持色标。色标允许您指定渐变在渐变线上的停止位置。例如,你可以在 CSS3 中创建一个真正的爱尔兰国旗,没有任何渐变,使用停止。为了做到这一点,您应该指定绿色将在元素的 33%(三分之一)处停止,然后白色将在 33%处开始并在 33%处停止。这将在绿色和白色之间创建一条直接的颜色线,而不是渐变。从这里开始,您将使用另一种白色,并指定在屏幕的 66%停止;最后是橙色,它将在 66%处停止,形成另一条颜色线。
代码如下所示,你可以在图 5-11 中看到结果。
.box { background: -webkit-linear-gradient(left, green 33.3%, white 33.3%, white 66.6%, orange 66.6%); }
图 5-11。 使用 CSS3 渐变颜色创建爱尔兰国旗
径向梯度
径向渐变比线性渐变稍微复杂一些。您可以指定渐变的起始位置及其形状。径向渐变具有以下语法。
.box { background: -webkit-radial-gradient(center, [circle|elipse] [closest-side|closest-corner|farthest-side|farthest-corner|contain|cover], start-color, stop-color); }
您可以以像素为单位指定中心位置,或者指定左侧和顶部位置的百分比。第二个参数接受 shape 关键字,它可以是圆形或椭圆形。第二个参数也接受一个 size 关键字,它们是closest-side
、closest-corner
、farthest-side
、farthest-corner
、contain
和cover
。最后,渐变还接受十六进制、关键字、RGB 或 RGBA 颜色作为开始和结束颜色。
例如,你可以使用以下代码用 CSS3 制作一面日本国旗,其结果可以在图 5-12 中看到。
.box { background: -webkit-radial-gradient(center, circle contain, red, white); }
图 5-12。 带有放射状渐变的日本国旗
您可以使用与线性渐变示例中相同的颜色停止技术来删除径向渐变上的渐变,并创建一个完整的圆。您可以使用以下代码来实现这一点,图 5-13 显示了结果。
.box { background: -webkit-radial-gradient(center, circle contain, #C00C00 70%, white 70%); }
图 5-13。 移除径向渐变的日本国旗
边框
有了 CSS3,你现在可以应用新的边框样式,比如border-radius
和box-shadow
。
边界半径
属性允许你在元素上创建圆角。在具备这种能力之前,为了制作具有圆角的灵活元素,您可以使用几个图像来模拟圆角,或者使用 JavaScript 助手,如 Curvy Corners,它将生成大量的div
元素,并将它们定位以模拟圆角。
border-radius
允许你使用 CSS3 生成圆角,无需任何图像或 JavaScript 的额外帮助。它现在是 CSS3 规范的一部分,使用下面的 CSS 可以创建一个圆角边框。
.box { border-radius: 10px; }
这将创建一个半径为 10 像素的边界。你也可以使用下面的语法指定你的元素的每个角的半径,其结果你可以在图 5-14 中看到。
.box { border: 1px solid #000000; border-top-left-radius: 5px; border-top-right-radius: 10px; border-bottom-left-radius: 15px; border-bottom-right-radius: 20px; width: 100px; height: 100px; }
图 5-14。边境半径
箱形阴影
属性允许你在块级元素上创建阴影。当设计需要不同大小的投影时,这很方便。现在,您可以使用几行 CSS 代码,而不是使用几个图像来创建不同的阴影样式。
box-shadow
属性具有以下格式。
box-shadow: horizontal-offset vertical-offset blur spread color inset;
horizontal-offset
和vertical-offset
属性以像素为单位指定阴影的位置,blur
以像素为单位设置模糊量,spread
以像素为单位设置阴影扩散,color
设置阴影的颜色,inset
设置阴影应该在元素的内部还是外部。inset
属性的值为 inset 或 nothing。
例如,下面的 CSS 将产生类似于图 5-15 的结果。
.box { width: 100px; height: 100px; border: 1px solid #000000; box-shadow: 10px 10px 20px 5px #000000; }
图 5-15。 框影
box-shadow
的值与text-shadow
的作用相同,如果指定负偏移值,阴影将呈现在屏幕的左上方。
CSS 媒体查询
CSS 媒体查询允许您根据特定条件获取 CSS 样式。这些条件可以包括表 5-3 中所示的条件。
创建媒体查询背后的想法不一定是构建针对特定设备的媒体查询(例如,不专门针对平板电脑或手机),而是迎合特定的屏幕尺寸并调整内容以适应它。
通过这样做,您可以确保您的 CSS 应用于可用空间,而不是目标设备。我们称之为响应式网页设计。
丹尼尔·文的网站([
danielvane.com/](http://danielvane.com/)
)展示了一个响应式网页设计的很好的例子。通过提供所有视窗尺寸的样式、最大 480 像素的显示样式和最大 768 像素的显示样式,网站可以适当地响应任何手机或平板设备上的可用空间,如图 5-16 和图 5-17 所示。
图 5-16。 Daniel Vane 的响应式网站(平板电脑在左边,手机在右边)
图 5-17。 Daniel Vane 的纵向响应网站(左边是平板电脑,右边是手机)
安迪·克拉克和基思·克拉克设计了一套媒体查询,你可以用它来定位逐渐变大的显示器。这背后的想法是用颜色和排版为最小的屏幕尺寸设计样式,然后以特定的屏幕增量逐步增强网站,直到屏幕尺寸超过 992 像素。该组媒体查询还包括针对具有高像素密度的目标显示器的媒体查询。
`
`您应该检查他们的 GitHub 项目,查看在[
github.com/malarkey/320andup/](https://github.com/malarkey/320andup/)
找到的这组规则的更新。
CSS 预编译器(SASS)
如果你过去有过使用 CSS 的经验,你就会知道它的一些局限性。例如,您不能定义可能影响 CSS 显示方式的变量,也不能重用代码元素。随着应用的增长,在 CSS 中生成和维护一个长的继承链也是一件痛苦的事情,如下面的代码所示,其中一个元素中有几个元素需要类似的样式。
`/**
* A common way to style a block in CSS
**/
.block {
/** style your block here **/
}
.block h1.heading {
/** style your header here **/
}
.block ul.alternating {
/** style your block ul here **/
}
.block ul.alternating li {
/** style your alternating li here /
}
.block ul.alternating li a {
/ style your li link here **/
}
/** and the story continues **/`
语法上令人敬畏的样式表(SASS)通过使用嵌套、变量、混合和选择器继承来帮助摆脱这种麻烦。SASS 不是 CSS,需要编译器将其编译成 CSS。
从前面的 CSS 可以看出,很多代码是重复的。不幸的是,没有办法以一种浏览器可以识别的方式删除大块,但有一种方法可以做到这一点,即你编写的 CSS 更容易维护和移植。这在 SASS 中被称为嵌套。
在本节中,您将学习如何使用 SASS 来生成有组织的、可重用的、具体的 CSS。您将了解 SASS 如何改进您的开发工作流程并改变您对 CSS 的看法。
您还将了解 SASS 如何消除在整个样式表中使用类似 CSS 样式的大量重复工作,并为面向对象的 CSS 铺平道路,这是一种思考 CSS 和 HTML 之间关系的方式,它将每个设计元素视为其自己独立的设计对象。
筑巢
嵌套允许你嵌套 CSS 样式。例如,前面的嵌套 SASS 代码如下所示。
`/**
* The SASS way to style a block in CSS
**/
.block {
/** style your block here **/
h1.heading {
/** style your header here **/
}
ul.alternating {
/** style your block ul here /
li {
/ style your alternating li here **/
a {
/** style your li link here **/
}
}
}
}
/** and the story continues **/`
这段代码更容易维护。如果您要更改块的类名,只需在嵌套样式中更改一次类名。如果您需要添加更多的元素,您只需要在适当的位置添加另一个您想要样式化的类或元素。例如,如果您想在标题中设置链接的样式,您可以使用首选的 SCSS 格式执行以下操作。
`.block {
/** style your block here **/
h1.heading {
/** style your header here /
a {
/ style your heading link here **/
}
}
ul.alternating {
/** style your block ul here **/
li {
/** style your alternating li here **/
a {
/** style your li link here **/
}
}
}
}`
编译
前面的代码需要编译成 CSS,以便网络浏览器能够理解。不要将 SASS 文件直接链接到 HTML 文档中;相反,您可以链接生成的 CSS 文件。您可以使用内置工具直接从 Aptana Studio 编译 SASS 文件。
要在 Aptana Studio 中编译 SASS 文件,在项目中的任意位置创建一个名为mobile.scss
的新文件(之后可以删除它)并添加以下代码。
`.test {
background: #000000;
.test2 {
background: #FFFFFF;
}
}`
点击命令 SASS 编译 Sass。这将在 SCSS 文件所在的位置生成一个新的 CSS 文件。您需要刷新应用浏览器才能看到新文件。编译 SASS 的捷径是 cmd+Shift+r(Windows 和 Linux 上的 CTRL + Shift + r)。当出现图 5-18 中所示的对话框时,按 1。
图 5-18。 使用 cmd + shift + r 命令编译 SASS】
新的 CSS 文件出现后,打开它。您应该会看到下面的代码。
.test { background: #000000; } .test .test2 { background: #FFFFFF; }
偏音
一个大的 SASS 文件可能会变得很难维护,并且需要很长的滚动时间!在 Aptana Studio 中,可以使用代码折叠来显示和隐藏 SASS 样式,以便于浏览,如图图 5-10 所示。
图 5-19。 代码折叠在阿普塔纳与 SCSS 的文件中
虽然这很方便,但 SASS 也支持使用常规 CSS 中相同的@import
语法从外部 SASS 文件导入部分样式表。SASS 实现和常规样式表中的实现之间的区别在于,SASS 将在编译时拉入文件,而不是使用 HTTP 请求将所有文件一个接一个地加载到常规 CSS 文件中。这为在编译时导入特定于对象或节的分部提供了空间。下面的代码显示了一个示例。
`/** mobile.scss **/
@import "partials/tablet";
@import "partials/phone";
/** partials/_tablet.scss **/
.test-tablet {
background: url(’../themes/mytheme/common/logo.png’) no-repeat top left
FFFFFF;
}
/** partials/_phone.scss **/
.test-phone {
background: url(’../themes/mytheme/common/logo.png’) no-repeat top left
FFFFFF;
}`
编译完成后,CSS 将如下所示。
`.test-tablet {
background: url("../themes/mytheme/common/logo.png") no-repeat top left
white; }
.test-phone {
background: url("../themes/mytheme/common/logo.png") no-repeat top left
white; }`
从这个例子中可以看出,每个部分的文件名都应该以 _(下划线)为前缀,导入中的引用应该包含相对文件夹和部分名称,而不包含 _ 前缀或 SCSS 文件名。您可能会注意到,SASS 还会将编译后的 CSS 中的#FFFFF
转换为white
。
变量和插值
您最终必然会产生基于颜色/主题的样式表(例如,同一个样式表可能引用相同的图像,但是来自不同的图像文件夹,或者具有不同的颜色主题)。
传统上,您会使用 PHP、Python 或。NET 动态生成这些样式表。SASS 通过使用变量消除了这种需要。
SASS 中变量的行为与其他语言中的行为非常相似。它们可以是任何类型(字符串、CSS 属性值、整数、像素、em、%)并且可以添加到 SCSS 样式中以对样式表进行全局更改。
例如,以 partials 部分中的示例代码为例,我们可以对其进行修改,以便您可以从主(移动)样式表中更改主题文件夹和颜色。
`/** mobile.scss **/
color: #000000;
@import "partials/tablet";
@import "partials/phone";
/** partials/_tablet.scss **/
.test-tablet {
background: url(’../themes/#{color;
}
/** partials/_phone.scss **/
.test-phone {
background: url(’../themes/#{color;
}`
正如你在mobile.scss
中看到的,你用一串"bentley"
定义了一个主题变量。然后在下面的线上定义一个黑色。@import
则用来导入分音。在每个分部中,您会注意到背景声明被修改如下。
background: url(’../themes/**#{$theme}**/common/logo.png’) no-repeat top left **$color**;
有两种方法可以将变量添加到 SASS 文件中。要将变量添加为 CSS 字符串的一部分,如背景图像路径,请使用以下语法。
#{$myvariable}
这就是所谓的插值,你也可以用它来改变一个 CSS 属性而不是它的值。例如,border-#{$position}-radius:
其中position
是由变量定义的位置。
第二种方法是简单地使用$myvariable
重复变量名。这是您在定义颜色、宽度或高度等 CSS 属性值时应该使用的内容。
混合蛋白
SASS 更受欢迎的特性之一是 mixins。Mixins 允许您在一个地方定义一段代码,并在 SASS 样式表的任何地方使用它。例如,您可能有一个跨浏览器渐变的大 CSS 声明,如下面的代码所示。
.myelement { background: rgb(206,220,231); background: -moz-linear-gradient(-45deg, rgba(206,220,231,1) 0%, rgba(89,106,114,1) 100%); background: -webkit-gradient(linear, left top, right bottom, color-stop(0%,rgba(206,220,231,1)), color-stop(100%,rgba(89,106,114,1))); background: -o-linear-gradient(-45deg, rgba(206,220,231,1) 0%, rgba(89,106,114,1) 100%); background: -ms-linear-gradient(-45deg, rgba(206,220,231,1) 0%, rgba(89,106,114,1) 100%); background: linear-gradient(-45deg, rgba(206,220,231,1) 0%, rgba(89,106,114,1) 100%); }
代码太多了。如果你想用在别的地方呢?最有效的方法是简单地将更多的类添加到您想要使用它的定义中。
.myelement, .mysecondelement { background: rgb(206,220,231); background: -moz-linear-gradient(-45deg, rgba(206,220,231,1) 0%, rgba(89,106,114,1) 100%); background: -webkit-gradient(linear, left top, right bottom, color-stop(0%,rgba(206,220,231,1)), color-stop(100%,rgba(89,106,114,1))); background: -o-linear-gradient(-45deg, rgba(206,220,231,1) 0%,rgba(89,106,114,1) 100%); background: -ms-linear-gradient(-45deg, rgba(206,220,231,1) 0%,rgba(89,106,114,1) 100%); background: linear-gradient(-45deg, rgba(206,220,231,1) 0%,rgba(89,106,114,1) 100%); }
您可以使用 mixin 来定义渐变,并使用以下代码将其包含在您的样式中。
`@mixin specialgradient {
background: rgb(206,220,231);
background: -moz-linear-gradient(-45deg,
rgba(206,220,231,1) 0%, rgba(89,106,114,1) 100%);
background: -webkit-gradient(linear, left top, right bottom,
color-stop(0%,rgba(206,220,231,1)), color-stop(100%, rgba(89,106,114,1)));
background: -o-linear-gradient(-45deg, rgba(206,220,231,1) 0%,
rgba(89,106,114,1) 100%);
background: -ms-linear-gradient(-45deg, rgba(206,220,231,1) 0%,
rgba(89,106,114,1) 100%);
background: linear-gradient(-45deg, rgba(206,220,231,1) 0%,
rgba(89,106,114,1) 100%);
}
my-first-element {
@include specialgradient;
}
my-second-element {
@include specialgradient;
}`
然而,这将是一个坏主意,因为产生的 CSS 将在样式表中包含两次渐变,这增加了膨胀,并不是我们想要的。选择器继承应该是这方面的首选。当您有一大块 CSS 将在其他 CSS 规则中重复时,或者更好的是,当您有在整个样式表中重复的 CSS 时,例如特定于供应商的样式(例如,渐变和边框图像)需要为每个浏览器多次定义相同的 CSS 时,Mixins 就很方便了。
为了实现这一点,您可以将参数传递到 mixins 中。现在,您可以使用以下代码在一行中的任意位置生成 CSS 渐变。
`@mixin gradient(stop, \(degrees) {
background: rgba(\)start, 1);
background: -moz-linear-gradient(start 0%, start), color-stop(100%, degrees, stop 100%);
background: -ms-linear-gradient(start 0% degrees, stop 100%);
}
my-first-element {
@include gradient(rgba(206,220,231,0.5), rgba(89,106,114,1), -45deg);
}
my-second-element {
@include gradient(rgba(206,220,231,1), rgba(89,106,114,1), -45deg);
}`
如您所见,首先定义一个名为gradient
的 mixin,它有三个参数:$start
、$stop
和$degrees
。在这个 mixin 中,首先为不支持渐变的设备定义标准背景。使用rgba
SASS 函数定义背景颜色的值。在这里,您显式地将背景色设置为没有 alpha 透明度的起始色。使用下面几行,您只需将开始颜色、结束颜色和度数传递给适当的供应商渐变声明。现在,您可以使用@include gradient(start-color, finish-color, degrees);
在样式表的任何地方使用参数来绘制渐变。生成的 CSS 如下所示。
`#my-first-element {
background: #cedce7;
background: -moz-linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0%, #596a72
100%);
background: -webkit-gradient(linear, left top, right bottom,
color-stop(0%, rgba(206, 220, 231, 0.5)), color-stop(100%, #596a72));
background: -o-linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0%, #596a72
100%);
background: -ms-linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0% #596a72
100%);
background: linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0%, #596a72
100%); }
my-second-element {
background: #cedce7;
background: -moz-linear-gradient(-45deg, #cedce7 0%, #596a72 100%);
background: -webkit-gradient(linear, left top, right bottom,
color-stop(0%, #cedce7), color-stop(100%, #596a72));
background: -o-linear-gradient(-45deg, #cedce7 0%, #596a72 100%);
background: -ms-linear-gradient(-45deg, #cedce7 0% #596a72 100%);
background: linear-gradient(-45deg, #cedce7 0%, #596a72 100%); }`
注意#my-first-element
中的 CSS 在第一个描述符中将背景色作为常规的十六进制颜色,其余的是 RGBA 颜色。此外,即使在 mixin 调用中使用 RGBA 设置了停止颜色,它也是一种十六进制颜色,因为不透明度设置为 1,而开始颜色设置为 0.5。SASS 将选择最有效的方式输出您的颜色。
选择器继承
当然,在整个 SASS 文件中使用 mixins 是很诱人的,即使 CSS 可能完全相同。选择器继承允许您在放置于 SASS 文件中的规则中使用相同的 CSS 规则。例如,在 CSS 中可以使用下面的代码。
.my-element-one, .my-element-two, .my-element-three { /** insert common CSS style here **/ }
虽然效率很高,但是很容易忘记哪些 CSS 规则与一组规则相关联。您可能需要在文档中搜寻,以找到那组规则以及与之相关联的元素、类和 id。更令人困惑的是,样式可能位于单独的 CSS 文件中。
选择器继承有助于克服这个问题。选择器继承允许您生成与刚才所示相同的代码,但是以一种对开发人员更加友好的方式。
使用 mixins 一节中的例子,您可以定义一种类型的渐变,并在 SASS 文件中的任何地方根据相关规则使用它,而不会在 CSS 文件中多次生成结果渐变。
.block { @include gradient(rgba(206,220,231,0.5), rgba(89,106,114,1), -45deg); } .sidebar-block { border-radius: 10px; @extend .block; }
如您所见,.sidebar-block
与.block
规则相似,除了圆形的边框。生成的 CSS 如下所示。
`.block, .sidebar-block {
background: #cedce7;
background: -moz-linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0%, #596a72
100%);
background: -webkit-gradient(linear, left top, right bottom,
color-stop(0%, rgba(206, 220, 231, 0.5)), color-stop(100%, #596a72));
background: -o-linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0%, #596a72
100%);
background: -ms-linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0% #596a72
100%);
background: linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0%, #596a72
100%); }
.sidebar-block {
border-radius: 10px; }`
您可以看到 SASS 已经将border-radius
属性分离出来,并将其放在自己的.sidebar-block
CSS 规则中。
您还可以将链接的类添加到 block 元素中,它将为.sidebar-block
和.block
规则生成边界案例。
`/**
* mobile.scss
*/
.block {
@include gradient(rgba(206,220,231,0.5), rgba(89,106,114,1), -45deg);
}
.block.wide {
width: 100px;
}
.sidebar-block {
border-radius: 10px;
@extend .block;
}
/**
* mobile.css
*/
.block, .sidebar-block {
background: #cedce7;
background: -moz-linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0%, #596a72
100%);
background: -webkit-gradient(linear, left top, right bottom,
color-stop(0%, rgba(206, 220, 231, 0.5)), color-stop(100%, #596a72));
background: -o-linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0%, #596a72
100%);
background: -ms-linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0% #596a72
100%);
background: linear-gradient(-45deg, rgba(206, 220, 231, 0.5) 0%, #596a72
100%); }
.block.wide, .wide.sidebar-block {
width: 100px; }
.sidebar-block {
border-radius: 10px; }`
总结
从这一章开始,你应该对 CSS3 的新特性有了很好的理解。您应该准备好一个小工具箱,从中可以进行扩展,包括如何执行基本动画,如何为动画“补间”和创建关键帧,以及如何将它们应用到元素。您还应该了解,大多数浏览器都支持 CSS3 的一些特性,但它们仍处于草案阶段,这就是为什么有时您需要多次编写相同的代码。
您还应该对 SASS 有深入的了解,以及它如何通过大幅减少您必须编写的代码量来提高您的生产率。
六、奠定 CSS3 基础
在上一章中,您重点学习了 CSS3 的一些新特性,以及如何使用 SASS 使您的生活变得更加轻松。在这一章中,你将把这些新知识应用到实践中,开始创建你的移动网络应用的可视化基础。momemo 应用中的大多数元素,如搜索、观看和喜欢的电影,都是用 JavaScript 处理和生成的,因此这些元素的样式将在第八章中讨论。
在您开始创建任何应用之前,您通常需要完成费力的引导任务。这需要设置好一切,比如你将要构建的应用的框架。虽然这是一个非常卑微和无聊的任务,但把它做好是很重要的,因为应用的其余部分可以从坚实的基础中受益。
在这一章中,你将学习如何利用 SASS 中的片段来组织你的 CSS 文件,这样就不会影响加载时间。您还将创建应用的基本框架,包括创建样式表以提高高分辨率显示器上的图像质量,以及创建应用的基本布局。
您需要下载应用的映像包,并将其放在应用映像(img)文件夹中。
井井有条
让我们从在应用文件夹中创建相关文件夹开始。在应用文件夹的 CSS 文件夹中,创建两个名为 mixins 和 partials 的文件夹,并在 CSS 文件夹中创建一个名为 mobile.scss 的新的 sass 文件。你的文件夹结构应该类似于下面的图 6-1 。
图 6-1。 CSS 文件夹结构
这个文件夹结构将允许你把你的表单、布局和排版的 CSS 分成单独的 SASS 文件。mobile.scss 文件只是一个主 SASS 文件,它将提取所有部分。这意味着,如果您想为只有版式的旧移动设备创建一个样式表,您可以创建一个新的主 SASS 文件,并只获取版式 SASS 文件,而不必复制任何 CSS。
打开 mobile.scss 文件并添加以下 SASS 代码:
`@import 'mixins/animations';
@import 'mixins/gradient';
@import 'mixins/box-sizing';
@import 'partials/reset';
@import 'partials/typography';
@import 'partials/layout';
@import 'partials/forms';
@media only screen and (-webkit-min-device-pixel-ratio : 1.5),
only screen and (min-device-pixel-ratio : 1.5) {
@import 'partials/highres';
}`
如第五章所示,这将在编译 SASS 文件时导入适当的 SASS 文件。
您还会注意到,在上面的代码中有一个媒体查询。此媒体查询将允许您为高分辨率设备获取高分辨率图形。在媒体查询中,您可以看到没有显式添加 CSS,而是导入了 highres 部分。这有助于防止任何 CSS 被添加到主 mobile.scss 文件中。mobile.scss 文件应该被简单地看作是一个 SASS 文件,用来把所有的东西放在一起,理想情况下应该只包含媒体查询和导入。
在编译 mobile.scss 文件之前,您需要创建适当的 SASS 文件。
- mixin/_ animations . SCS
- mixins/_ box-size . scss
- mixins/_gradient.scss
- partials/_forms.scss
- partials/_highres.scss
- partials/_layout.scss
- partials/_reset.scss
- partials/_ 印刷术. scss
继续创建它们,记住 SASS partials 需要在文件名的开头有一个 _(下划线),以便导入时被识别。
您需要创建如图 6-2 中所示的空文件。
图 6-2。 萨斯巴勒斯
您会注意到有一个名为 _reset.scss 的文件。如果您不熟悉 Eric Meyer 的 reset css 文件,reset 样式表用于创建跨浏览器 css 样式的公平竞争环境。这是因为浏览器可以为某些元素设置不同的默认样式,比如不同的边距、填充和字体大小。重置样式表为最常用的 HTML 元素设置新的可预测默认值。要注意的是,有些,特别是 Eric Meyer 的会重新设置字体,这样它们就没有任何风格了,这是有用的,但是你必须记住,对于有默认字体风格的元素,比如<pre />
会有和<body />
一样的字样。
在 mixins 文件夹中,你会看到几个看起来像 css 属性的文件,如 _animation.scss 和 _gradient.scss。这些文件有助于通过使用 mixins 创建属性的通用版本来消除一些特定于供应商的 CSS 对主 SASS 文件的污染。您可以开始向这些文件添加内容。
打开 empty _animations.scss 文件。这个 mixin 将用于创建动画,并应用于所有供应商。如果创建了一个新的特定于供应商的动画属性,它可以被添加到一个地方,而不是跨多个 SASS 文件添加。将以下代码添加到打开的文件中。
@mixin animation ($values) { animation: $values; -moz-animation: $values; -webkit-animation: $values; }
如您所见,它只是作为基于标准的 Mozilla 和 webkit 动画属性的代理,接受一组属性,然后将它们传递给供应商特定的动画属性。保存文件并关闭它。
打开 empty _box-sizing.scss 文件。这个 mixin 提供了对盒子大小的支持。关于 CSS 中的灵活布局,最令人沮丧的一个问题是,当您将元素设置为 100%宽(父元素的宽度)并带有填充时,浏览器通常会将填充添加到元素的宽度,即使宽度被指定为 100%,因此结果是您的元素会因您添加的填充量而过度拉伸,有时会将元素稍微推出屏幕或其父元素之外。框大小属性通过以下方式帮助克服这一问题:
- 使用内容框值时,从元素的宽度和高度中排除任何填充、边距或边框
- 使用填充框值时,包括元素宽度和高度的任何填充
- 当使用边框值时,包括任何填充和边框宽度以及元素的宽度和高度
@mixin box-sizing ($value) { -moz-box-sizing: $value; -webkit-box-sizing: $value; box-sizing: $value; }
同样,这个 mixin 只是通过将值传递给属性来充当供应商特定属性的代理。
最后,打开 _gradient.scss 文件,并向其中添加以下代码。
@mixin gradient($start, $stop, $degrees) { background: rgba($start, 1); background: -moz-linear-gradient($degrees, $start 0%, $stop 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, $start), color-stop(100%, $stop)); background: -o-linear-gradient($degrees, $start 0%,$stop 100%); background: -ms-linear-gradient($degrees, $start 0% $stop 100%); background: linear-gradient($degrees, $start 0%, $stop 100%); }
你可能在上一章已经看过这个 mixin 了。它只是为供应商特定的渐变代码创建 CSS 渐变。它比其他 mixins 稍微复杂一点,因为在编写本文时,每个供应商都有自己的 CSS 渐变实现,这使得接受单个值并将其传递给供应商属性变得不可能。
创造分音
混音创建完成后,现在是创建分音的时候了。如前所述,通过在常规 CSS 文件中使用传统的@import,partials 将有助于将 CSS 的不同部分分成不同的文件,而不会对最终用户产生影响,这将对加载时间产生很大影响。
您可以从打开 partials 目录中的 empty _reset.scss 文件开始。您不必手动将下面的代码键入这个 SASS 文件,您可以从 Eric Mayar 的网站Meyer web . com/Eric/thoughts/2011/01/03/reset-revisited/
复制它。下面列出的代码仅供您参考。
`/* http://meyerweb.com/eric/tools/css/reset/
v2.0b1 | 201101
NOTE: WORK IN PROGRESS
USE WITH CAUTION AND TEST WITH ABANDON */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
/* remember to define visible focus styles!
:focus {
outline: ?????;
} */
/* remember to highlight inserts somehow! */
ins {
text-decoration: none;
}
del {
text-decoration: line-through;
}
table {
border-collapse: collapse;
border-spacing: 0;
}`
保存并关闭文件。您需要添加代码的下一个文件是 _typography.scss 文件。在 Aptana Studio 中打开它。_typography.scss 文件将简单地设置文本在应用中的显示方式。正如您在下面的代码中看到的,您将简单地设计主体和标题的样式。
`body {
font-size: 0.75em;
font-family: Arial, Helvetica, sans-serif;
}
h1, h2, h3, h4 {
font-family: 'Arimo', sans-serif;
font-weight: bold;
margin-bottom: 0.5em;
font-size: 1em;
}
h3 { font-size: 1.25em; }
h2 { font-size: 1.5em; }
h1 { font-size: 1.9em; }`
你用 em 代替像素作为字体大小。将正文的字体大小设置为 0.75em 相当于 12px,如上面的代码片段所示,其中正文的字体大小被声明为 em。1em 相当于浏览器的默认字体大小,即 16px。要计算出 10px 在 em 中应该是多少,您可以使用 10 / 16,它等于 0.625,因此 10px 应该是 0.625em。em 是有用的,因为值是相对的。例如,如果您将 div 的字体大小设置为 0.75em (12px),然后将其中的任何元素设置为 1em,则该字体大小将与父元素的字体大小相关。所以子元素中的 1em 变成了离父元素 0.75em。试图找出 em 的字体大小可能是一场噩梦,riddle.pl/emcalc/
有一个解决方案,允许你建立一个基于 DOM 的像素字体大小树,网络应用会为你将它们转换为 EM,并考虑父元素的字体大小。
保存并关闭 _ topology . scss 文件。打开 _layout.scss 文件。_layout.scss 文件将控制应用中元素的位置、尺寸、颜色和一般布局。
首先要做的是对 body,html,#shoe 和。应用的甲板元素。您可以在顶部设置它们的样式,这样它们就可以在样式表的后面被覆盖。
body, html, #shoe, .deck { height: 100%; width: 100%; overflow: hidden; margin: 0px; }
正如你所看到的,高度和宽度已经被设置为 100%,所以它跨越了屏幕的宽度和高度。溢出:隐藏;添加了,以便元素之外的任何内容都被截断,不会影响布局,为了更好地测量,添加了 0px 边距,以防止元素之间出现任何间隙。
接下来要做的是对#card-movie_search_results 卡片进行样式化。进行搜索时,卡片应显示在页面上所有元素的上方。您可以通过设置 z 索引来实现这一点。z-index 指示元素在元素堆栈中的位置。设置较高的数字通常会将元素放在堆栈的顶部。这种情况下用 50。
`/**
* Individual Card Styles
*/
card-movie_search_results {
z-index: 50;
}`
下一步是设置卡片组和卡片样式。如您所见,SASS 嵌套在这里用于嵌套卡片组中不同的卡片状态。当呈现 SASS 文件时,将生成适当的 CSS。您需要将甲板位置设置为相对位置。这将允许卡片组中绝对定位的卡片相对于父卡片组而不是整个视窗定位。
`/**
* Deck styles
*/
.deck {
position: relative;
}`
现在需要用。卡牌类也是。每张牌的宽度和高度都应该相同,但是要放在屏幕之外,这样用户一开始就看不到它。当。活动类被添加到任何。卡元素,它应该被带回到视图中。这可以通过将初始左侧位置设置为负值来实现,负值相当于卡片的宽度,在本例中为-100%。当您希望卡片重新出现在视图中时,会为设置 0px 的位置。主动造型。
`/**
* Deck styles
*/
.deck {
position: relative;
** .card {**
** height: 100%;**
** width: 100%;**
** left: -100%;**
** position: absolute;**
** }**
** .card.active {**
** left: 0px;**
** }**
}`
接下来要设计的是屏幕条。滚动条将位于屏幕的顶部和底部。这些需要以统一的方式设计,以便用户可以很容易地找到它们。正如你在下面看到的,渐变混合被用来创建一个 CSS3 渐变作为这个元素的背景。
`/**
* Header taskbar styles
*/
.screenbar {
@include gradient(#7D9DCE, #ABC1E1, 90deg);
}`
任务栏相当复杂,因为它包含应用的徽标、搜索栏和清除按钮。任务栏需要和屏幕一样宽,搜索栏需要灵活,这样无论屏幕大小如何,它都能占据大部分空间。
从下面的代码中可以看到,你将任务栏的字体颜色设置为白色,溢出区被设置为隐藏,这样它将包围所有浮动的元素。任务栏也有 10px 填充,底部有红色边框。
header#taskbar { color: #FFFFFF; overflow: hidden; padding: 10px; border-bottom: 1px solid #BF2628; }
现在,您需要设计应用的品牌/徽标。为此,您可以使用 header (h1)元素,然后使用图像替换技术来显示徽标。这可以通过将 h1 的宽度和高度设置为与徽标相同的宽度和高度来实现,将 text-indent 属性设置为一个高的负任意值,以便文本位于屏幕之外,在这种情况下使用-10000px。最后,将徽标的背景设置为徽标。
h1 元素也浮动在任务栏的左侧,以便搜索表单可以占据剩余的可用空间。
`header#taskbar {
...
** h1.branding {**
** margin: 0px;**
** float: left;**
** width: 73px;**
** height: 32px;**
** text-indent: -10000px;**
** overflow: hidden;**
** background: url('../img/momemo.png') no-repeat top left;**
** }**
}`
接下来要做的是设置清除搜索链接。您使用与前面相同的图像替换技术来替换清除搜索链接中的文本。这次这个按钮被浮动到右边并隐藏起来,这样它就不会马上被看到。
`header#taskbar {
...
h1.branding {
...
}
** .clear-search {**
** float: right;**
** width: 35px;**
** height: 35px;**
** display: none;**
** overflow: hidden;**
** text-indent: -10000px;**
** background: url('../img/clear.png') 50% 50% no-repeat;**
** }**
}`
最后要添加到 _layout.scss 文件中的是 searchactive 覆盖。当您添加 css 类时。searchactive 添加到 header#taskbar 元素,它将显示 clearsearch 按钮,并通过向 add-movie 表单添加右边距来为它提供足够的空间。这可以防止。searchactive buttom 从下拉到新行。
`header#taskbar {
...
}
header#taskbar.searchactive {
** .clear-search {**
** display: block;**
** }**
** form#add-movie {**
** margin-right: 40px;**
** }**
}`
您的 final _layout.scss 文件应该类似于下面的代码。
`header#taskbar {
color: #FFFFFF;
overflow: hidden;
padding: 10px;
border-bottom: 1px solid #BF2628;
h1.branding {
margin: 0px;
float: left;
width: 73px;
height: 32px;
text-indent: -10000px;
overflow: hidden;
background: url('../img/momemo.png') no-repeat top left;
}
.clear-search {
float: right;
width: 35px;
height: 35px;
display: none;
overflow: hidden;
text-indent: -10000px;
background: url('../img/clear.png') 50% 50% no-repeat;
}
}
header#taskbar.searchactive {
.clear-search {
display: block;
}
form#add-movie {
margin-right: 40px;
}
}`
接下来要做的是设计表单的样式。所以打开 _forms.scss 文件。首先要做的是为所有表单元素设置框的大小,使添加的任何填充或边框成为整体宽度的一部分。下面一行将使用盒子大小的 mixin 来实现这一点。
input, select, textarea, button { @include box-sizing(border-box); }
然后,您需要设置文本输入的样式,您可以使用新的 CSS3 属性选择器来完成这一工作,而不是像以前那样向每个文本输入元素添加 CSS 类。从下面的代码片段中可以看到,下面的文本输入有 1 像素的黑色边框和 5 像素的填充,而提交输入只有 10 像素的填充。应用中没有使用提交按钮,所以对它进行样式化还没有意义。
input[type="text"] { border: 1px solid #000000; padding: 5px; }
目前,只有一个输入元素应该跨越其父元素的整个宽度。将来您可能想要添加更多这样的元素,所以将它转换成一个可以重用的 CSS 类是一个好主意。
input.full-width { width: 100%; }
通过在搜索表单中添加 80px 的左边距(大于或等于徽标的宽度),表单中的任何内容都将出现在徽标旁边。
form#add-movie { margin-left: 80px; }
这是一个比浮动 add-movie 表单好得多的解决方案,因为如果不使用 JavaScript 来计算它的大小,它将不再能够拥有其父任务栏元素的全部宽度。
下面的代码简单地设计了搜索字段的样式。正如你所看到的,这里第一次使用了背景尺寸。background-size 属性允许您指定背景应该有多大,以像素为单位,或者以背景所添加到的元素的百分比来表示。
input.search { padding-left: 30px; background: url('../img/search.png') 5px 50% no-repeat transparent; background-size: auto 50%; border: none; border-bottom: 1px solid #BF2628; color: #FFFFFF; font-size: 1.5em; }
背景尺寸属性接受宽度和高度,这两个属性可以是不同的单位。例如,在本例中,宽度设置为自动,高度设置为 50%。这允许高度为元素高度的 50%,但宽度将根据背景图像的高度成比例调整,这样它就不会出现扭曲。
以下样式使用供应商特定的伪样式。-webkit-input-placeholder 和-moz-placeholder 允许您设置输入元素上使用的占位符文本的样式。例如,搜索框的背景在蓝色背景上是透明的,所以默认的灰色几乎看不见。文本需要是白色的,因此占位符伪对象允许您自定义占位符文本的呈现方式。
input.search::-webkit-input-placeholder, input.search::-moz-placeholder { color: rgba(255, 255, 255, 0.5); }
虽然这在 Android 4 上不会立即可见,但下面的样式会在搜索框中显示一个加载指示器,同时在后台搜索电影。
input.search.loading { background-image: url('../img/loading.gif'); }
最终的表单 SASS 文件应该类似于下面的代码。
`input, select, textarea, button {
@include box-sizing(border-box);
}
input[type="text"] {
border: 1px solid #000000;
padding: 5px;
}
input.full-width {
width: 100%;
}
form#add-movie {
margin-left: 80px;
}
input.search {
padding-left: 30px;
background: url('../img/search.png') 5px 50% no-repeat transparent;
background-size: auto 50%;
border: none;
border-bottom: 1px solid #BF2628;
color: #FFFFFF;
font-size: 1.5em;
}
input.search::-webkit-input-placeholder, input.search::-moz-placeholder {
color: rgba(255, 255, 255, 0.5);
}
input.search.loading {
background-image: url('../img/loading.gif');
}`
保存并关闭文件。最后,您需要打开 _highres.scss 文件。这个文件将简单地用于替换任何高分辨率显示的图形,使它们看起来清晰。将以下代码添加到文件中。
`header#taskbar {
h1.branding {
background-image: url('../img/momemo.png');
background-size: 73px 32px;
}
}`
正如你所看到的,这里需要使用背景尺寸,因为尽管高分辨率文件的分辨率是低分辨率图像的两倍,CSS 在用作背景图像时仍然会使用图像的全尺寸。这将确保背景图像缩小到正确的像素大小。不使用这个和使用它的区别可以在下面的图 6-3 中显示。
图 6-3。 高密度显示器上的高分辨率图像(下图)与低分辨率图像(上图)
在 Aptana 中自动编译 Sass
直到现在,你还没有在 Aptana Studio 中编译过任何 SASS。在前一章中,您看到了如何使用 SASS 的内置 SASS 编译器命令来编译 SASS 文件。每当你想对你的 SASS 文件进行修改时,这会变得很麻烦。您可以通过使用 SASS 命令行自动编译您的 SASS 文件来解决这个问题。为了做到这一点,在 Aptana Studio 的应用浏览器中点击你的应用文件夹,然后点击命令图标,它看起来像一个齿轮,可以在图 6-4 中看到。
图 6-4。 命令菜单
点击打开终端菜单项。这将打开类似于图 6-5 的终端视图。
图 6-5。 终端视图
在终端视图中,输入以下命令并按回车键。
sass --watch css/*.scss
这将查找您的 SASS 文件中的任何更改,并自动为您生成 CSS 文件。你应该会看到类似于图 6-6 的东西。
图 6-6。 萨斯-手表输出
这还会在您的部分文件中查找更改,然后用新的更改自动覆盖 mobile.css。
每次打开 Aptana Studio 时,您都需要运行这个命令,并且您还应该始终保持这个终端视图打开。
现在你的 CSS 文件已经成功生成,在 Aptana Studio 中运行你的网站,右击 index.html 进入运行为 ** JavaScript Web 应用**。它将在 Firefox 中启动,访问移动设备地址栏中显示的 URL。你现在应该会看到类似于图 6-7 的东西。
图 6-7。 Momemo 带 CSS
如果你没有看到新的样式,回到 Aptana Studio,点击 CSS 文件夹,按键盘上的 F5 刷新它。应该会出现新的 mobile.css 文件。刷新你的手机上的网页,一切都应该看起来像它应该的样子。
总结
虽然这一章很短,但是您应该对如何真正利用 SASS 中的 partilals 和 mixins 以及如何为开始在之上构建 CSS/SASS 打下基础有了更深入的了解。
现在,您应该知道如何在主 SASS 文件(mobile.scss)的 CSS 媒体查询中使用片段。
六、移动 JavaScript
自 1999 年第一款消费者 WAP 手机 Nokia 7110 问世以来,移动 JavaScript 已经走过了漫长的道路。从完全不支持到完全支持,仅仅 10 年时间,JavaScript 已经让我们的移动网络体验变得更加互动、有趣和令人满意。
今天的问题是,有了这么多的 JavaScript 支持,我们如何利用它,使它不引人注目,并为我们的用户提供良好和流畅的体验?
本章将指导你如何将 JavaScript 集成到你的项目中,使用不同类型的库来使你更容易的开发出可以在任何平台上运行的移动网络应用。您还将了解新的 HTML5 JavaScript APIs(如地理定位)、存储,以及如何利用它使用 HTML5 Canvas 元素为 Android 绘制基于矢量的图形。
面向对象的 JavaScript
JavaScript 是一种处理移动网站中用户交互的极好的语言。就像你为桌面 web 编写 JavaScript 一样,你也可以利用同样的设计模式和方法为移动设备编写。您可以用两种方式之一编写 JavaScript。其中一个方法是程序性的,如下面的代码所示。
function sayHelloWorld(foo){ alert(foo); }
第二种方法是面向对象的,如下所示。
`var World = function(){
this.say = function say(hello){
alert(hello);
}
}
var myworld = new World();
myworld.say('Hello');`
正如您所看到的,您可能需要为面向对象的方法编写更多的代码,但是有几个好处。
- 面向对象的方法允许扩展您的代码。
- 面向对象的方法可以更有组织性。
- 面向对象的方法允许封装,这意味着对象中的变量或属性可以是公共的或私有的。
- 面向对象的方法允许您将对象传递给其他对象。这通常被称为对象依赖性。
注意:在基于类的语言中,比如 Java、Objective-C 和 PHP,类在被使用new ClassName
实例化之前是一个对象。一个对象是一个类被实例化后的实例。JavaScript 有创建对象的基本方法,不幸的是,它不完全支持封装、继承、抽象和开箱即用的接口。您可能需要创建自己的方法和实践来实现这一点。JavaScript 也是一种基于对象的语言,所以尽管感觉像是在创建类,但实际上是在代码中为对象创建结构。
前面的两个代码片段具有相同的结果;然而,面向对象的方法将World
视为一个对象,并将该对象中的函数this.say
视为可以在其上执行的方法。
面向对象的方法还允许您创建一个对象的多个实例。例如,你可以创建几个World
实例,通过修改前面的代码,你可以开始创建只存在于每个World object
范围内的实例变量,比如它的名字。
`var World = function(_name){
var name = _name;
this.greet = function(guest){
alert('Hello ' + guest + ' my name is ' + name);
}
}
var venus = new World('Venus');
var mars = new World('Mars');
venus.greet('Antony');
venus.greet('Dan');`
从前面的例子中,您可以看到,要在 JavaScript 中创建一个对象,就像创建一个函数一样简单。使用函数的参数,您创建了所谓的构造函数。构造函数是一种在实例化时向对象传递参数的方法。这些参数通常用于将变量赋给对象本身的属性。
在普通的面向对象中,属性可以声明为 public 或 private。在 JavaScript 中,属性没有这样的简化。因此,属性可以是实例变量(私有)或公共属性(公共)。在这个实例中,name
属性是一个实例变量,这意味着您不能使用例如venus.name
从对象外部访问它。这通常被称为封装。对象的属性是一个变量,既可以在对象范围内使用this.propertyname
访问,也可以在对象外部使用object.propertyname
访问。例如,如果您试图从对象外部访问name
实例变量,您将得到undefined
作为输出。
您还可以创建对象方法,即可以从对象内部或外部使用this
或从外部分配给实例化对象的变量来访问的函数。使用前面的例子,this.greet
是一个公共对象方法,可以在对象外部访问。
从这里,您可以看到对象允许更易维护的代码。对象的属性存在于全局名称空间之外,因此可以避免变量名和特定对象的其他细节发生冲突。除此之外,您还可以为应用对象创建自己的名称空间。这有助于避免其他 JavaScript 库覆盖它们。下面的例子展示了为对象创建应用级名称空间的最基本的方法。
`var app = app || {};
app.world = function(_name){
var name = _name;
this.greet = function(guest){
alert('Hello ' + guest + ' my name is ' + name);
}
}`
这允许您在单独的文件中创建属于您的应用的类。前面的代码示例中的第一行声明了全局名称空间中的变量app
,如果它已经存在,则将app
全局变量赋给它。如果它不存在,它会为您创建一个空对象,以便开始填充您的对象。如果您在开发过程中以这样一种方式组织您的对象,将它们保存在单独的文件中,然后在生产中合并,这将非常方便。您甚至可以更进一步,根据功能命名您的对象。
这些是现代移动浏览器支持的面向对象 JavaScript 的基础。
您以这种方式编码(而不是,例如,创建 jQuery 插件),因为它将您的应用代码与特定于供应商的代码分开,并减少了对第三方代码的依赖。您可以更进一步,遵循模型视图控制器(MVC)模式,将您的用户交互与您的域逻辑(真实世界的对象)和呈现给用户的结果视图分离开来。
与设计模式和面向对象一样,JavaScript 也是一种事件驱动的语言。这意味着在运行时,在应用的一个部分触发的事件可以在应用的完全不同的部分触发一段代码。
最简单的形式是,事件可以通过用户交互来触发。在移动领域,这通常被视为触摸事件,用户使用手指通过浏览器与你的应用进行交互。浏览器注册该事件,然后将该事件及其信息(如事件源自的元素)传递给应用中的任何订阅者。对于桌面环境,这些被称为鼠标事件。
处理触摸事件
JavaScript 在处理桌面事件方面毫不逊色,在移动领域也是如此。事件可以由用户级事件(如触摸和拖动)或设备级事件(如方向变化或设备位置变化)组成。最基本的事件是用户级事件。可以在任何 DOM 元素上跟踪它们。对于移动设备,有四种主要的触摸事件:
touchstart
touchend
touchmove
touchcancel
当用户触摸屏幕上的元素时,touchstart
事件将被触发。当用户触摸屏幕上的某个元素后,将手指从该元素上移开时,将触发touchend
事件。touchmove
事件将跟踪用户的动作,并随着每个动作触发事件。当用户通过移动到目标边界之外并释放屏幕来取消触摸事件时,将触发touchcancel
事件。这一事件似乎无法预测。
为了响应事件,您必须使用element.addEventListener(event, callbackfunction);
为它们创建事件监听器。该方法采用事件名称(touchstart
、touchend
等)。)和回调函数。有时,您可能希望阻止触发事件的默认操作。例如,如果您向链接添加一个事件监听器,您可能不希望该链接在被点击时打开一个新页面。为此,您必须向名为e
的回调函数添加一个参数,并在回调函数结束时调用e.preventDefault()
。这也将防止元素滚动和干扰touchmove
事件,如下面的代码片段所示。
`
这段代码将用黑色填充屏幕,用白色文本包含用户手指的当前坐标以及用户是否正在触摸屏幕。您可以通过点击传递给touchmove
事件监听器的事件的触摸列表来获取当前坐标。你可以从列表中获取第一次触摸,并使用clientX
和clientY
来检索 X 和 Y 坐标,就像这样:
e.touches[0].clientX, e.touches[0].clientY
如您所见,您可以通过调用e.preventDefault()
来防止文档滚动。
当用户触摸并将手指抬离屏幕时,将调用 touchstart 和 touch end 的其他两个事件侦听器。
图 7-1。 检测触摸和移动
获取用户的位置
当您知道用户需要将他们的当前位置输入到应用中时,获取用户的位置会很方便。这有助于查找和搜索他们周围的事物,如事件、地点和其他人。定位 API 非常简单,利用了移动设备内置的 GPS 芯片。
要获得用户的位置,可以使用下面的代码。它是异步的和非阻塞的,所以当设备搜索用户位置时,您可以继续在前台或后台处理 JavaScript 事件。
var showCurrentPosition = function(position){ alert('Lat: ' + position.coords.longitude + ' Lon: ' + position.coords.latitude); }
这是在移动设备具有用户的位置之后将被调用的功能。传递回回调函数的参数是一个扩展了Coordinates
接口的对象,其属性如表 7-1 所示。
要检索设备的坐标,只需在设备上查询用户的位置,如下所示:
navigator.geolocation.getCurrentPosition(showCurrentPosition);
如果用户尚未授权您的应用访问他们的位置,他们将首先被要求批准位置请求。图 7-2 显示了这个对话框的样子。
图 7-2。 位置请求
这就出现了一个问题,因为你应该预料到一些用户可能不希望共享他们当前的位置,并可能点击拒绝按钮;或者可能只是检索用户当前位置的问题。这可以用一个错误事件处理程序来处理,它是getCurrentPosition()
方法的第二个参数。为了处理检索用户当前位置的错误,您必须创建一个错误处理程序,它将接受 error 对象。
`var handleLocationError = function(error){
alert(error.message);
}
navigator.geolocation.getCurrentPosition(showCurrentPosition,
handleLocationError);`
错误对象是PositionError
接口的一部分,其属性如表 7-2 所示。
您应该使用PositionError
常量PERMISSION_DENIED
、POSITION_UNAVAILABLE
和TIMEOUT
来适当地处理错误,而不是依赖错误消息或将错误代码与硬编码的整数进行比较。下一个代码示例展示了如何使用handleLocationError
函数和switch
语句来处理错误。
var handleLocationError = function(error){ switch(error.code){ case error.PERMISSION_DENIED: /** * Handle permission denied response here, * potentially display a dialog to the user */ var confirmed = confirm("We really need your location!"); if(confirmed){ navigator.getCurrentPosition(showCurrentPosition, handleLocationError); } break; case error.POSITON_UNAVAILABLE: /** * Handle position unavailable response here, * potentially display a dialog to the user and * ask them to enter their location manually */ var tryagain = confirm("Sorry, something serious is wrong, would you like to try again?"); if(tryagain){
navigator.getCurrentPosition(showCurrentPosition, handleLocationError); } break; case error.TIMEOUT: /** * Appologizies to the user for the delay and attempts * to retrieve their location again */ navigator.geolocation.getCurrentPosition(showCurrentPosition, handleLocationError); break; } }
这些都是非常简单的错误处理程序,如果发生错误,可以对它们进行扩展,为用户提供更好的体验。
然后,您可以将坐标传递给地图服务,如谷歌地图,以显示用户的当前位置。下面的例子使用 Google Maps 静态 API 来生成用户当前位置的图像,并显示在移动设备上。
<img src="/map.jpg" id="map" alt="Map" />
``
结果如图 7-3 中的所示。当然,您也可以订阅用户当前位置的重大变化。您可以使用navigator.geolocation.watchPosition
方法来完成这项工作。这将监听用户当前位置的重大变化,并在每次用户位置变化时调用回调函数。watchPosition
方法与getCurrentPosition
方法采用相同的参数。
图 7-3。 显示用户在谷歌地图上的当前位置
用画布画画
HTML5 Canvas 允许您使用 JavaScript 绘制基于矢量的形状。HTML5 Canvas 元素没有提供太多固有的功能,但是它为您开始绘制对象提供了一个基础。把它想象成你设备的白板。下一个练习将带您了解如何创建画布,如何开始使用 JavaScript 绘制基本形状,以及如何制作它们的动画。
首先,在这个名为canvas
的章节文件夹中创建一个新文件夹。在canvas
文件夹根目录下创建一个js
文件夹,包含一个名为canvas.js
的新 JavaScript 文件和一个index.html
文件,内容如下。
`
这将创建一个id
为play
的画布元素,宽 100 像素,高 100 像素。永远不要尝试使用 CSS 来调整 Canvas 元素的大小,因为它不会像预期的那样工作。这个 HTML 也将链接到canvas.js
文件。
打开canvas.js
文件,它将被用来控制你的画布。您将使用面向对象的 JavaScript 来创建和控制播放按钮。
在这个例子中,您将需要两个对象:一个track
对象(它将模拟实际的音轨)和一个playButton
对象(它将控制音轨进度的显示和音轨的播放/暂停)。track
对象应负责以下内容:
- 记录轨道的总长度
- 保持曲目的当前状态(播放/暂停/停止)
- 如果曲目正在播放,则保留曲目的当前时间
- 播放、暂停和停止轨道
本例中的playButton
对象将负责以下内容:
- 绘制播放按钮
- 显示播放进度
- 显示轨道回放状态
- 通过显示播放或停止符号来表示轨道的状态
- 通过移动播放头来表示音轨的播放进度
受 iTunes 播放控件的启发,如图 7-4 所示,你将创建类似的东西。
图 7-4。 iTunes 预览播放控件
首先,在您的canvas.js
文件中创建两个对象,如下面的代码片段所示:
`var app = app || {};
app.playButton = function(id, track){
}
app.track = function(length){
}`
如您所见,playButton
的构造函数带有一个id
,它将是画布元素的 ID,还有一个track
,它将是app.track
类的实例。track
构造器简单地以秒为单位获取轨道的长度。
由于需要首先实例化track
,您将从创建track
类的代码开始。首先,在名为this.state
的track
类中创建一个新属性,如下所示:
app.track = function(length){ ** this.state = {** ** STOPPED: 0,** ** PLAYING: 1,** ** PAUSED: 2** ** };** }
state
属性包含可用于确定应用当前状态的变量。另一种方法是将当前状态存储为字符串(例如,播放、暂停或停止)。当您在应用中添加更多状态或更改状态名称时,这可能会有问题。通过这样做,改变应用的状态就像使用state = this.state.STOPPED
一样简单。这也很有帮助,因为当你键入this.state.
时,代码补全会向你显示可能的状态,这比你必须挖掘你的代码来找出可用的状态更好更有效。
接下来,在track
类的范围内定义几个变量,如下面的代码片段所示:
`app.track = function(length){
...
** var length = (length * 1000),**
** currentTime = 0,**
** interval,**
** _self = this,**
** state = this.state.STOPPED,**
** updateInterval = 1000 / 30;**
}`
在 JavaScript 中,可以在一行中声明变量,用逗号分隔它们。这也适用于移动设备。
您的第一个变量length
,通过乘以1000
,将传递给该类的音轨长度从秒转换为毫秒。您还将currentTime
设置为0
,并声明一个名为interval
的变量。interval
变量负责保存对区间的引用,以重复调整轨道的定时。
这看起来很奇怪,但是你也声明了一个名为_self
的变量,并将this
赋给它。这创建了一个全局变量,这样事件监听器在对象范围之外调用的任何回调事件仍然能够访问父类,因为this
将在回调事件或目标的范围内,而不是父类(在本例中是track
)。
然后声明应用的当前状态,并将其默认状态设置为this.state.STOPPED
。
最后,您创建一个名为updateInterval
的新变量,它将用于设置时间每秒更新的次数。例如,如果您想每秒更新间隔 500 次,您可以将updateInterval
设置为updateInterval = 1000 / 500
。增加该时间将对性能产生影响,因为这会影响画布动画的帧速率。
您将需要更新currentTime
。setCurrentTime
是一个私有方法,允许您设置播放头的当前时间。它还将回调任何使用_self.callbacks.didUpdateTime.call(_self, currentTime);
将其自身指定为该方法回调的函数或方法。call
是一个允许你在另一个对象范围内调用一个函数的方法。这将允许回调函数在其代码中使用this
,并且this
将是对进行回调的对象的引用,而不是回调函数的父对象。call
的第一个参数是您想要从中传递范围的对象。之后的参数是回调方法将接受的参数。
接下来,您必须创建名为updateTime
的私有方法。这将更新音轨的当前播放时间。该方法还检查currentTime
是否达到了总轨迹长度。如果有,那么它将停止跟踪。
`app.track = function(length){
...
** var setCurrentTime = function(time){**
** currentTime = time;**
** _self.callbacks.didUpdateTime.call(_self, currentTime);**
** };**
** var updateTime = function(){**
** if(currentTime < length){**
** setCurrentTime(currentTime + updateInterval);**
** } else {**
** _self.stop();**
** }**
** };**
}`
你会注意到这里使用了_self
。这不是一个全局 JavaScript 变量,而是您之前声明的_self
变量。updateTime
在跟踪类/对象的范围之外被调用,所以_self
维护一个对它的引用。这被更好地称为关闭。
接下来,您将声明几个 getter 和 setter。创建它是为了能够访问对象范围之外的私有变量。当您不希望对象更改另一个对象的属性时,这很方便。例如,currentTime
不应该在对象之外被操作,但是外部对象应该能够找出音轨的当前播放时间。使用不带 setter 的 getter 可以防止外部对象更改该值。
`app.track = function(length){
...
** this.getCurrentTime = function(){**
** return currentTime;**
** };**
** this.getLength = function(){**
** return length;**
** };
this.getState = function(){**
** return state;**
** };**
}`
本例中的 getters 将简单地返回私有变量;但是,您可以定义一个 getter,比如getCurrentTimeInSeconds
,它将修改返回值,以便函数返回以秒为单位的回放时间。例如:
this.getCurrentTimeInSeconds = function(){ return (currentTime / 1000); }
接下来,您必须定义轨道的控件,例如播放、暂停和停止。
`app.track = function(length){
...
** this.stop = function(){**
** window.clearInterval(interval);**
** state = _self.state.STOPPED;**
** setCurrentTime(0);**
** _self.callbacks.didStop.call(_self);**
** };**
** this.play = function(){**
** if(state != _self.state.PLAYING){**
** interval = window.setInterval(updateTime, updateInterval);**
** state = _self.state.PLAYING;**
** _self.callbacks.didStartPlaying.call(_self);**
** }**
** };**
** this.pause = function(){**
** window.clearInterval(interval);**
** state = _self.state.PAUSED;**
** _self.callbacks.didPause.call(_self);**
** };**
}`
this.stop
将停止跟踪,并使用window.clearInterval(interval)
清除间隔计时器。stop
方法还会使用state = _self.state.STOPPED
将轨道的当前状态设置为0
或STOPPED
。该方法还将重置当前时间,并调用didStop
回调方法。
this.play
将通过检查当前状态来查看曲目是否正在播放。如果曲目没有播放,那么它将创建一个新的间隔计时器。window.setInterval
采用两个参数:回调方法和以毫秒为单位的间隔时间。如果您希望分配一个从设置初始间隔的函数中获取参数的回调,您可以使用以下方法:
var globalParam = 'foo'; window.setInterval(function(){ callbackFunction.call(this, globalParam); }, intervaltime);
记住globalParam
必须用var
声明,这样它才能存在于闭包中。
最后,定义默认的回调函数。
app.track = function(length){ ... ** this.callbacks = {** ** didUpdateTime: function(time){},** ** didStartPlaying: function(){},** ** didPause: function(){},** ** didStop: function(){}** ** };** };
如您所见,这些都是空函数。这允许你调用回调函数,即使它们没有被赋值。有四个回调函数:this.callbacks.didUpdateTime
、this.callbacks.didStartPlaying
、this.callbacks.didPause
和this.callbacks.didStop
。
现在是时候开始创建播放按钮并深入画布了!在开始之前,了解 Canvas 的实际工作方式是很重要的。为了在画布上绘图,您需要获得它的上下文。如果你不熟悉什么是上下文,它就像一个隐藏的空间,你可以在那里画画。在您完成绘制后,上下文将呈现给用户。Canvas API 中目前只有一个上下文,所有的形状都绘制在它上面。在理想的情况下,您应该有几个上下文,在它们上面绘制单独的组件,并将每个上下文合并成一个上下文。目前,这是不可能的,将在本章中进一步解释。
画布上下文在一个基于坐标的系统上工作,从左上角开始,如图 7-5 所示。
图 7-5。 画布网格
首先,您需要定义几个全局变量。
app.playButton = function(id, track){ ** var canvas = document.getElementById(id),** ** context = canvas.getContext('2d'),** ** track = track,** ** _self = this;** }
如您所见,您通过使用getElementById
获得了 Canvas 元素。然后通过使用canvas.getContext('2d')
获得画布上下文,这将返回一个 2d 画布上下文供您绘制。然后显式声明 track 变量,并再次为任何回调方法将_self
定义为this
。
通过为 canvas 元素创建新的属性,可以更容易地计算画布的某些方面。这是使用以下代码完成的:
`app.playButton = function(id, track){
...
** canvas.center = {**
** x: (canvas.offsetHeight / 2),**
** y: (canvas.offsetHeight / 2)**
** };
canvas.dimensions = {**
** width: (canvas.offsetWidth),**
** height: (canvas.offsetHeight)**
** };**
}`
这将允许您快速检索中心坐标以及画布的宽度和高度,而无需将它们存储在全局变量中。例如,您可以简单地使用canvas.center.x
来获取画布的中心 x 坐标。
接下来,您将需要为跟踪更新其计时器和跟踪暂停分配回调。
`app.playButton = function(id, track){
...
** track.callbacks.didUpdateTime = function(time){**
** _self.draw();**
** };**
** track.callbacks.didPause = function(){**
** _self.draw();**
** }**
}`
如您所见,两个回调都只是调用了playButton
类中的draw
方法。
接下来,您需要创建播放控制方法。这将用于通过播放按钮播放和停止曲目。这也允许其他对象或功能通过播放按钮开始或停止音轨。
`app.playButton = function(id, track){
...
this.togglePlay = function(){
** switch(track.getState()){**
** case track.state.STOPPED:**
** case track.state.PAUSED:**
** _self.play();**
** break;**
** case track.state.PLAYING:**
** _self.stop();**
** break;**
** }**
** };
this.play = function(){**
** track.play();**
** };**
** this.stop = function(){**
** track.pause();**
** };**
}`
如你所见,有一个方法叫做this.togglePlay
。切换播放方法将检查音轨的状态。如果停止或暂停,会触发play
方法;如果它正在播放,就会触发stop
方法。这些条件包含在一个switch
语句中。为了减少混乱,switch
语句是使用if
语句的一个很好的选择。该声明由以下内容组成:
switch(**value**){ case **condition**: /** condition code **/ break; case **condition**: /** condition code **/ break; default: /** default code **/ break; }
如你所见,这需要一个value
。每个case
代表一个condition
来与value
进行比较。如果condition
匹配,则执行case
内的代码,然后跳出switch
。如果没有一个condition
匹配,您可以使用default
指定一个默认的动作。最佳实践是只比较switch
语句中的整数值。
随着togglePlay
方法的完成,this.play
和this.stop
方法都作为暂停或播放音轨的包装器。
音轨的完整代码如下:
`app.track = function(length){
this.state = {
STOPPED: 0,
PLAYING: 1,
PAUSED: 2
};
var length = (length * 1000),
currentTime = 0,
interval,
_self = this,
state = this.state.STOPPED,
updateInterval = 1000 / 30;
var setCurrentTime = function(time){
currentTime = time;
_self.callbacks.didUpdateTime.call(_self, currentTime);
};
var updateTime = function(){
if(currentTime < length){
setCurrentTime(currentTime + updateInterval);
} else {
_self.stop();
}
};
this.getCurrentTime = function(){
return currentTime;
};
this.getLength = function(){
return length;
};
this.getState = function(){
return state;
};
this.stop = function(){
window.clearInterval(interval);
state = _self.state.STOPPED;
_self.setCurrentTime(0);
_self.callbacks.didStop.call(_self);
};
this.play = function(){
if(state != _self.state.PLAYING){
interval = window.setInterval(updateTime, updateInterval);
state = _self.state.PLAYING;
_self.callbacks.didStartPlaying.call(_self);
}
};
this.pause = function(){
window.clearInterval(interval);
state = _self.state.PAUSED;
_self.callbacks.didPause.call(_self);
};
this.callbacks = {
didUpdateTime: function(time){},
didStartPlaying: function(){},
didPause: function(){},
didStop: function(){}
};
};`
现在该画停止按钮了。draw 方法在this.draw
方法中被调用,上下文取自类中的私有变量。
绘制停止图标
停止按钮为 20px × 20px,应为实心矩形。要绘制任意比例的矩形,可以使用context.fillRect()
方法。fillRect
方法采用表 7-3 中所示的四个参数。
要绘制一个 20px × 20px 的简单矩形,可以使用以下代码:
context.fillStyle = '#000000'; context.fillRect(0, 0, 20, 20);
这将产生一个类似于图 7-6 所示的矩形。
图 7-6。 一个 20px × 20px 的长方形
context.fillStyle
将任何新闭合形状的填充设置为黑色或#000000,如矩形或圆形。
在为停止符号在播放按钮上绘制矩形的代码中,您需要考虑停止符号相对于画布的位置。您需要将停止符号直接放在画布的中心。要使停止符号居中,您需要计算停止符号左上角的 x 和 y 偏移量。要计算这一点,您需要将画布的宽度和高度分成两半,以得到偏移量。然后,您可以从形状的中心减去画布中心,得到 x 和 y 坐标。这个方法简单地将要绘制的形状的中心与画布的中心对齐。
下面的代码将使停止图标相对于画布本身的大小居中。
`app.playButton = function(id, track){
...
** this.drawStop = function(){**
** var width = 20,**
** height = 20,**
** x = canvas.center.x - (width / 2),**
** y = canvas.center.y - (height / 2);**
** context.beginPath();**
** context.fillStyle = '#A0A0A0';**
** context.fillRect(x, y, width, height);**
** };**
}`
正如您所看到的,您将停止图标的width
和height
声明为20
,以便以后可以引用它们。您还可以通过获得画布的中心 x 坐标减去矩形宽度的一半来计算 x 坐标。这将使停止图标水平居中。
接下来,y 坐标的设置方式也差不多,将画布(其中心)的高度减半,并减去停止图标高度的一半。然后将停止图标垂直放置在画布的中心,如图图 7-7 所示。结合这两种计算方法将使停止图标在画布中完全居中。
图 7-7。 将停止图标沿 x 轴和 y 轴居中
在开始在画布上绘制新形状之前,调用context.beginPath()
是个好主意。这将在上下文中创建一个新路径,以便您开始绘制。这相当于在你在纸上画一个新的形状之前,把你的笔从纸上拿开。
接下来,你需要设置你要画的形状的fillStyle
。2D 上下文 API 有几种绘图方法。API 最基本的方法和属性如表 7-4 和表 7-5 所示。
设置好样式属性后,您现在可以使用context.fillRect()
绘制停止图标。
定义的方法将在当前上下文中绘制一个矩形。接下来,您需要创建一个方法来绘制播放按钮。
绘制播放图标
播放按钮稍微复杂一些。要绘制播放按钮,您需要下拉到上下文中的绘制路径。
为了绘制路径,您使用了moveTo
和lineTo
方法。这些方法允许你在不画线的情况下移动到某一点,在两点之间画一条线。
`app.playButton = function(id, track){
...
** this.drawPlay = function(){**
** var width = 20,**
** height = 20,**
** x = canvas.center.x - (width / 2),**
** y = canvas.center.y - (height / 2);**
** context.beginPath();**
** context.moveTo(x, y);**
** context.lineTo(x + width, y + (height / 2));**
** context.lineTo(x, (y + height))**
** context.fillStyle = '#A0A0A0';**
** context.fill();**
** };**
}`
首先,将播放图标的width
和height
设置为20
(即 20px × 20px)。然后,将中心点设置为画布宽度的一半,减去播放按钮宽度的一半。您也可以对 y 轴进行同样的操作,就像您对停止图标所做的一样。
为了绘制 play 按钮,您需要在画布上预先映射三个点,以绘制起点和终点的直线。图 7-8 显示了在 20px × 20px 绘图环境中的大概坐标。
图 7-8。 画一个三角形
如你所见,三角形有三个点:(0,0),(20,10),和(0,20)。就像你在正方形上做的一样,你必须根据它的宽度和高度来计算三角形上的点应该在哪里。你知道第一个点应该从 0,0 开始。第二个点应位于 x =形状宽度,y =形状高度/ 2 的位置。记住这一点,第三个也是最后一个点应该定位在 x =原点 x,y =造型高度的地方。这将创建一个等边三角形。下面的代码将创建这个:
`var width = 20, height = 20, startx = 0, starty = 0;
context.beginPath();
context.fillStyle = '#A0A0A0';
context.moveTo(startx, starty);
context.lineTo((startx + width), (starty + (height / 2)));
context.lineTo(startx, (starty + height));
context.fill();`
正如你所看到的,你画了两条线形成等边三角形。您不必绘制另一条线来连接最终位置和原始位置。通过调用context.fill()
,你将自动闭合原点和终点之间的间隙,并用context.fillStyle
颜色填充矩形。前面的方法也考虑了绘制形状的起点。你可以改变startx
或starty
的值,它将总是在那个位置画一个等边三角形。
随着图标的创建,现在是时候设置播放头了。播放头简直就是一个逐渐打开的圆圈。半圆下面是另一个圆圈,有对比色帮助区分音频播放的进度,如图图 7-9 所示。
图 7-9。 回放头
绘制播放头
Canvas 的问题是你不能单独为每个形状制作动画。移动或激活画布元素需要完全重绘画布。在 Canvas 中制作动画需要在 JavaScript 中跟踪每个对象的状态,然后在每次调用draw
时渲染它。这是一个漫长而费力的过程,但是通过正确的实现,可以减少耗时..
要跟踪跟踪的进度,您首先需要计算出其当前进度的百分比。
`app.playButton = function(id, track){
...
** this.draw = function(){**
** var percentage = 100 - ((track.getCurrentTime() / track.getLength())**
*** 100);**
** };**
}`
这简单地计算为(当前时间/长度)* 100。这将给你一个介于 1 和 100 之间的可预测的数字。您需要返回一个百分比,其中 100%表示音轨开始时,0%表示音轨结束时。为此,您只需从 100 中减去已播放的百分比,即可得到剩余曲目的百分比。
下一步是根据剩余曲目的百分比计算播放头的角度。你知道 2 * π (PI)的结果将等于以弧度表示的整圆的角度。0 * π (PI)会得出 0,会得出一个空圆。
`app.playButton = function(id, track){
...
this.draw = function(){
var percentage = 100 - ((track.getCurrentTime() / track.getLength()) *
100);
** var endradians = (percentage * (2 / 100)) * Math.PI;**
};
}`
图 7-10 显示了 PI 计算的重要位置。
图 7-10。 n * π弧度
如您所见,0 从圆的右侧开始,2 也将导致相同的位置。如果将圆弧的起始位置设置为 0,结束位置设置为 2 * PI,结果将是空的,因为圆由于角度为 0 而没有圆周。
您将定期重绘画布,因此每次重绘时都需要清除画布,以防止在新的上下文形状下面显示以前的上下文形状。这可以通过调用context.clearRect(0, 0, canvas width, canvas height);
来实现。这将在整个画布上绘制一个清晰的矩形。您不必担心矩形绘制后存在的内存或形状,因为只有当前上下文保存在内存中。
`app.playButton = function(id, track){
...
this.draw = function(){
var percentage = 100 - ((track.getCurrentTime() / track.getLength()) *
100);
var endradians = (percentage * (2 / 100)) * Math.PI;
** context.clearRect(0, 0, canvas.dimensions.width,**
canvas.dimensions.height);
};
}`
下一步是画一个圆圈,作为播放按钮的背景。画一个完整的圆,然后用黑色填充,就可以做到这一点。
`app.playButton = function(id, track){
...
this.draw = function(){
var percentage = 100 - ((track.getCurrentTime() / track.getLength()) *
100);
var endradians = (percentage * (2 / 100)) * Math.PI;
context.clearRect(0, 0, canvas.dimensions.width,
canvas.dimensions.height);
** /****
** * Draw the play button backdrop**
** /*
** context.beginPath();**
** context.fillStyle = '#000000';**
** context.arc(canvas.center.x, canvas.center.y,**
** canvas.center.x - 10, 0, 2 * Math.PI, false);**
** context.fill();**
};
}`
下一步是画一个没有填充的圆,并对其应用一个笔画,以提供当游戏头移动时显示的背景。
`app.playButton = function(id, track){
...
this.draw = function(){
var percentage = 100 - ((track.getCurrentTime() / track.getLength()) *
100);
var endradians = (percentage * (2 / 100)) * Math.PI;
context.clearRect(0, 0, canvas.dimensions.width,
canvas.dimensions.height);
/**
* Draw the play button backdrop
*/
context.beginPath();
context.fillStyle = '#000000';
context.arc(canvas.center.x, canvas.center.y,
canvas.center.x - 10, 0, 2 * Math.PI);
context.fill();
** /****
** * Draw the background for the play head**
** /*
** context.beginPath();
context.arc(canvas.center.x, canvas.center.y,**
** canvas.center.x - 20, 0, 2 * Math.PI);**
** context.lineWidth = 5;**
** context.strokeStyle = "#FFFFFF";**
** context.stroke();**
};
}`
最后,这是一个根据音轨的当前播放位置绘制播放头的例子。代码与为播放头绘制背景的代码相同,除了结束角度被设置为代码中先前声明的endradians
来表示轨道的进度。
`app.playButton = function(id, track){
...
this.draw = function(){
var percentage = 100 - ((track.getCurrentTime() / track.getLength()) *
100);
var endradians = (percentage * (2 / 100)) * Math.PI;
context.clearRect(0, 0, canvas.dimensions.width,
canvas.dimensions.height);
/**
* Draw the play button backdrop
*/
context.beginPath();
context.fillStyle = '#000000';
context.arc(canvas.center.x, canvas.center.y,
canvas.center.x - 10, 0, 2 * Math.PI);
context.fill();
/**
* Draw the background for the play head
*/
context.beginPath();
context.lineWidth = 5;
context.strokeStyle = "#FFFFFF";
context.arc(canvas.center.x, canvas.center.y,
canvas.center.x - 20, 0, 2 * Math.PI);
context.stroke();
/**
*** Draw the progress head**
/
context.beginPath();
context.lineWidth = 5;
context.strokeStyle = "#A8A8A8";
context.arc(canvas.center.x, canvas.center.y,
** canvas.center.x - 20, 0, endradians);*
context.stroke();
};
}`
该方法的最后一步是决定每次重绘画布时是在按钮上绘制停止图标还是播放图标。这是通过一个switch
语句实现的。
`app.playButton = function(id, track){
...
this.draw = function(){
var percentage = 100 - ((track.getCurrentTime() / track.getLength()) *
100);
var endradians = (percentage * (2 / 100)) * Math.PI;
context.clearRect(0, 0, canvas.dimensions.width,
canvas.dimensions.height);
/**
* Draw the play button backdrop
*/
context.beginPath();
context.fillStyle = '#000000';
context.arc(canvas.center.x, canvas.center.y,
canvas.center.x - 10, 0, 2 * Math.PI);
context.fill();
/**
* Draw the background for the play head
*/
context.beginPath();
context.lineWidth = 5;
context.strokeStyle = "#FFFFFF";
context.arc(canvas.center.x, canvas.center.y,
canvas.center.x - 20, 0, 2 * Math.PI);
context.stroke();
/**
* Draw the progress head
*/
context.beginPath();
context.lineWidth = 5;
context.strokeStyle = "#A8A8A8";
context.arc(canvas.center.x, canvas.center.y,
canvas.center.x - 20, 0, endradians);
context.stroke();
/**
*** Decide whether to draw the play or the stop button**
*/
switch(track.getState()){
case track.state.PAUSED:
case track.state.STOPPED:
this.drawPlay();
break;
case track.state.PLAYING:
this.drawStop();
break;
}
};
}`
可以看到,如果曲目状态是暂停或停止,会绘制播放图标;如果曲目正在播放,则会绘制停止图标。
playButton
的完整代码如下。您会注意到,在代码示例的底部,有一个事件侦听器为画布绑定触摸事件。这将触发togglePlay()
方法。
`app.playButton = function(id, track){
var canvas = document.getElementById(id),
context = canvas.getContext('2d'),
track = track,
_self = this;
canvas.center = {
x: (canvas.offsetHeight / 2),
y: (canvas.offsetHeight / 2)
};
canvas.dimensions = {
width: (canvas.offsetWidth),
height: (canvas.offsetHeight)
};
/**
* Track callback methods
*/
track.callbacks.didUpdateTime = function(time){
_self.draw();
};
track.callbacks.didPause = function(){
_self.draw();
}
/**
* Track controls
*/
this.togglePlay = function(){
switch(track.getState()){
case track.state.STOPPED:
case track.state.PAUSED:
_self.play();
break;
case track.state.PLAYING:
_self.stop();
break;
}
}
this.play = function(){
track.play();
};
this.stop = function(){
track.pause();
};
this.drawStop = function(){
var width = 20,
height = 20,
x = canvas.center.x - (width / 2),
y = canvas.center.y - (height / 2);
context.beginPath();
context.fillStyle = '#A0A0A0';
context.fillRect(x, y, width, height);
};
this.drawPlay = function(){
var width = 20,
height = 20,
x = canvas.center.x - (width / 2),
y = canvas.center.y - (height / 2);
context.beginPath();
context.moveTo(x, y);
context.lineTo(x + width, y + (height / 2));
context.lineTo(x, (y + height))
context.fillStyle = '#A0A0A0';
context.fill();
};
this.draw = function(){
// Draw the progress bar based on the
// current time and total time of the track
var percentage = 100 - ((track.getCurrentTime() / track.getLength()) *
100);
var endradians = (percentage * (2 / 100)) * Math.PI;
context.clearRect(0, 0, canvas.dimensions.width,
canvas.dimensions.height);
context.beginPath();
context.fillStyle = '#000000';
context.arc(canvas.center.x, canvas.center.y,
canvas.center.x - 10, 0, 2 * Math.PI);
context.fill();
context.beginPath();
context.arc(canvas.center.x, canvas.center.y,
canvas.center.x - 20, 0, 2 * Math.PI);
context.lineWidth = 5;
context.strokeStyle = "#FFFFFF";
context.stroke();
context.beginPath();
context.arc(canvas.center.x, canvas.center.y,
canvas.center.x - 20, 0, endradians);
context.lineWidth = 5;
context.strokeStyle = "#A8A8A8";
context.stroke();
switch(track.getState()){
case track.state.PAUSED:
case track.state.STOPPED:
this.drawPlay();
break;
case track.state.PLAYING:
this.drawStop();
break;
}
};
canvas.addEventListener('touchend', function(e){
_self.togglePlay();
e.preventDefault();
});
this.draw();
};`
图 7-11。 最终播放按钮
存储数据
传统上,要在移动 web 应用中持久化数据(如用户名),需要将这些信息存储在 cookie 中。cookies 的问题在于,虽然它们非常适合存储少量数据,但对于大量数据,例如 JavaScript 对象,它们很快就变得难以管理。当您希望存储应用的当前状态时,这可能是完美的,以便当用户返回时,他们可以从他们离开的地方继续,就像本机应用一样。
不幸的是,本地存储不支持存储对象,只支持字符串值。但是您可以使用 JSON.stringify 将对象转换为字符串,然后使用 JSON.parse 将它们转换回对象。
要使用本地存储来存储数据,只需使用localStorage
API。这些包括表 7-6 中所示的属性和方法。
例如,如果您想存储一个包含用户名、电子邮件地址和联系电话号码的对象,您可以创建如下内容:
var user = {name: "John Seagate", email: "john.seagate@hello.com", contactNumber: "012345678910"} localStorage.setItem('user', JSON.stringify(user));
要检索该项,您可以使用以下代码:
var user = JSON.parse(localStorage.getItem('user'));
移动 JavaScript 库
JavaScript 库可以帮助减轻任何类型的前端开发的负担。它们可以帮助提供一致的 API,从 DOM 操作一直到厨房水槽。本章将使用三个库作为示例。
- 李荣
- 移动 jQuery
- 煎茶触摸
图 7-12。【jQuery Mobile】(左),和的 Sencha Touch(右)
XUI 是专门针对移动设备的,它提供了类似 jQuery 的语法和轻量级的 DOM 操作,简单的 API 抽象来生成和处理 Ajax 请求,并执行基本的基于 JavaScript 的动画。它有一个很像 jQuery 的插件架构,所以你可以扩展 XUI 来满足你的需要,并为你的项目创建额外的插件。
jQuery Mobile 和 Sencha Touch 都是重量级产品,从某种意义上来说,它们不仅提供了一个你可能称之为“普通 JavaScript”的抽象,而且还提供了一个你可以轻松构建移动 web 应用的框架。
jQuery mobile 可以帮助您快速构建项目的原型,然后进一步对它们进行皮肤处理。它的用户界面依赖于传统的简单 HTML (POSH)。然后用 CSS 和 JavaScript 增强了这种时髦。jQuery 通过使用 CSS 媒体查询来改变布局,可以在基于手机和平板电脑的设备上运行。
Sencha Touch 提供了一种更复杂、功能更全的开发方法。你不用 HTML 写卡片或页面。相反,您通过 JavaScript 配置每个页面并提供内容。Sencha Touch 提供了许多 UI 增强和小部件,包括离线存储数据的能力和通过使用代理开箱即用的在线。这允许您通过一个通用界面存储和检索数据,并指定您在配置中使用的存储类型。
jQuery 和 Sencha Touch 都很棒;然而,您最终创建的是用 jQuery mobile 或 Sencha 思维方式构建的应用。使用移动库来完成一个基于移动的项目并没有错,但是在选择一个框架或库的时候,你应该注意你应该寻找的东西。
文件大小
确保任何库都具有较小的占用空间是很重要的。不幸的是,一些移动运营商不提供无限制的数据计划,所以确保访问你的移动网站不会对你的用户的口袋产生大的影响是很重要的。同样重要的是要记住,尽管 3G 和 LTE 提供相对较高的数据速度,但并非所有用户都可以随时访问 3G 或 LTE。这对加载时间有影响,因为 500kB 的库以及整个应用的图像和 CSS 素材可能需要几秒钟才能通过 3G/LTE 下载;这可能需要更长的时间。因此,迎合最小公分母,这将是优势,在这种情况下。
文件数量
在撰写本文时,移动浏览器可以发出的请求数量非常有限。这意味着,如果您有许多资源要下载到浏览器,这会影响加载时间。您可以通过确保您使用的 JavaScript 库提供以下功能来克服这个问题:
- 源的缩小和连接版本
- 图标和按钮等东西的 sprite 表
- 内容交付网络(CDN)托管版本的库
活动
您应该查看库的最新更新是什么时候,以及新版本发布的频率(发布周期)。这对您的开发有影响,如果您是一个关注 bug 修复和主要发布版本的人,您可能会发现自己一直在用最新的发布版本更新代码;或者更糟糕的是,如果这个库被废弃并且不再被维护,这意味着你将不得不学习一个新的库或者维护刚刚被废弃的库。
CSS3 支持
许多库现在正在更新他们的代码,以利用 GPU 对 CSS3 动画和过渡支持的增强。您应该检查您的库,以确保它支持 CSS3 过渡和动画,或者具有针对 JavaScript 动画和过渡的路线图。这将为您的应用提供性能增强。
总结
从这一章开始,你应该对 JavaScript 和移动给你带来的新功能有了更深的理解。
您应该能够区分过程 JavaScript 和面向对象 JavaScript 之间的区别,并且将对象相互传递是一个比在全局名称空间中调用函数好得多的概念和原则。
您还应该对 JavaScript 中的作用域有一个简单的了解,以及如何通过闭包和使用_self = this
在对象之间维护它。
您还应该对如何在 JavaScript 中处理触摸事件有一个简单的了解,通过利用这一点,您可以生成更加丰富的应用。
这一章非常详细地谈到了画布。你应该了解 HTML5 Canvas 是如何工作的,以及上下文和绘图 API 背后的想法,如arc
、fillRect
,以及样式 API,如lineStyle
和fillStyle
。
八、JavaScript:模型、视图和控制器
有许多开发设计模式。MVC(模型视图控制器)是一个真正脱颖而出并可以应用于几乎所有编程语言的工具。MVC 将应用分解为不同的责任层。
作为开发人员,我们在没有真正了解项目最终将如何发展或成长的情况下,就开始着手一个项目,这种情况太普遍了。例如,我们使用 Ajax 从外部资源获取数据,然后在同一个代码块中用 HTML 简单地呈现这些数据。如果您想在应用的另一部分使用相同的 HTML,但是出于不同的目的,使用不同的数据源,会发生什么情况呢?最快的方法是复制粘贴代码并修改变量。
当你开始以这种方式将更多的特性加入到你的应用中时,它可能看起来更像是你用果冻和巧克力而不是代码和逻辑构建的应用。这听起来很有趣,但关键是如果你从一开始就以一种以后可以很容易地构建的方式构建你的应用,那么将来添加更多的特性将花费更少的时间和金钱。
实现这一点的一部分是为应用的某些方面标准化或创建规则。这可以使代码写起来更长,但更容易被开发人员而不是你自己使用。通过采用 MVC,你采用了一种易于理解的工作方法。你的技能水平不应该决定你是否应该学习设计模式。你可以用你喜欢的任何方式实现 MVC 然而,本章将只向你展示一种在 JavaScript 中使用 MVC 的方法。
通过这一章,你将学习如何创建和实现你自己的 MVC 框架。您将了解什么是模型,以及它们如何作为应用的生命线。您将了解控制器如何帮助绑定和管理应用中的事件,以及如何构建视图以供重用。
注意:在开始之前,你需要一个烂番茄开发者账户。要创建一个,请前往[
developer.rottentomatoes.com](http://developer.rottentomatoes.com)
并按照步骤创建一个开发者帐户并获得一个 API 密钥。
清理你的代码
在你开始为这一章写任何代码之前,你需要清理你为第四章的创建的应用的根目录下的index.html
。大多数 HTML,比如收藏夹列表和电影预览列表,现在将使用 JavaScript 生成。我们将在这一章中涉及相当多的内容;为了详细关注所有这些问题,寻找电影院和播放音轨将从最终的功能列表中删除。
打开index.html
并确保你的 HTML 看起来像下面的代码。
`
Mo Memo
`
正如您在前面的代码中看到的,对<div />
id 和类有几处更改。个别卡片上的内容也被删除了。这是因为,在本章中,你将学习如何用 JavaScript 创建可重用的 HTML 片段,称为视图。这将允许您将 HTML 和视图逻辑放在应用的主代码之外,放在它自己的可维护文件中。
MVC 和 JavaScript 初级读本
JavaScript 基于 ECMAScript 标准。我们有幸在二十世纪末看到了 JavaScript 的流行,当时 dynamicdrive.com 网站将 JavaScript 带到了网络的前沿。然后,正如 Web 2.0 是几年前流行的 Web 技术(如 Ajax、JavaScript、CSS 和 HTML)和概念(包括 API、RSS、社交媒体和大规模内容生产和消费)集合的流行语一样,d HTML 成为利用 JavaScript、DOM 操纵和 CSS(例如,制作雪花漂浮在其上的网页)的流行语。
很快,JavaScript 变得越来越流行,在拥有 Java 和 C/C++等语言的企业软件开发经验的专业开发人员手中,JavaScript 成熟了。然而,浏览器在实现上不一致;JavaScript 开发人员经常在仇恨中编码,他们知道其他浏览器对 Ajax/XMLHttpRequest 的实现与当时流行的 Internet Explorer 完全不同(与我们今天看到的相反)。即使像绑定事件或选择元素这样简单的任务也可能是一种痛苦,因为您必须做两次——一次是针对 Internet Explorer,一次是针对其他所有人。
后来,我们有了 MooTools、DoJo、JQuery 和 YUI 等库,让我们摆脱了许多繁重的工作。这些解决了许多浏览器不一致的问题,为我们提供了一种执行简单任务(比如 DOM 选择和操作)的方法,这种方法适用于所有浏览器。例如,与其编写几行代码来创建一个兼容 Internet Explorer 和 Firefox 的 Ajax 请求,不如用 jQuery 一次覆盖两者,如下所示。
$.ajax('/my/data/provider.json');
不幸的是,留给我们的是一群新开发人员,他们完全有理由相信 jQuery、DoJo、MooTools 或 YUI 是真正的 JavaScript——因为这是他们被教导或自学的方式。
混合了库代码的过程化 JavaScript 成为了标准,用意大利面条式的代码来填充$(document).ready(function(){});
最终会变得难以为不断增长的 web 应用所维护。
这些工具令人惊叹,功能强大,但人们很容易依赖它们,却不明白它们到底是如何工作的,也不明白为什么你应该或不应该在手机上使用其中一个。如果您主要使用 jQuery,那很好。我从 2007 年/1.1 版本开始作为开发人员使用 jQuery,从 2005 年开始使用 script.aculo.us(虽然我不愿意承认)。但是,我是先学 JavaScript 的,甚至在开始使用之前,我就翻看了 jQuery 和 script.aculo.us 的代码。
这在移动中甚至更重要,因为库中的大部分代码几乎不会被您的移动 web 应用使用。至少,您可能会使用 DOM 选择器、遍历、事件绑定和 Ajax,这些只占库代码的一小部分。也就是说,对于一个已经在供需矛盾中挣扎的网络来说,把整个事情拉下来是没有意义的。
相反,有时候开发自己的迷你 JavaScript 库或框架实际上更有益。你永远不会是一个新手来学习如何做这件事;事实上,最好现在就开始习惯自己的方式。
框架只是一种处理代码的方法、标准或实践。您创建了一个框架来分离应用的重要部分,以便在应用增长时更容易管理它们。该框架还管理数据如何流经您的应用。您将主要看到 JavaScript 对象从一个方法传递到另一个方法进行表示。这些对象通常代表某种形式的实体,被称为模型。模型通常会被传递给一个方法或函数,然后显示给用户。在 MVC 中,处理和操纵模型以进行展示的方法被称为控制器,生成包含模型的 HTML 的代码被称为视图。图 8-1 显示了 MVC 框架是如何构建的。
图 8-1。JavaScript 的 MVC 图
模型
MVC 中的 M 代表模型。模型是应用的一部分,它规定了如何处理不同类型的数据。模型只是一个 JavaScript 对象,可以表示某种类型的实体。例如,您可以在应用中为用户创建一个模型,如下所示:
var user = function user(){}
提示:你会注意到我已经命名了这个函数,并把它赋给了一个变量。这实质上创建了一个命名函数,而不是一个匿名函数。这在很多情况下都很有用,比如调试,因为您可以在堆栈跟踪中看到方法名。
用户通常有一些属性,比如名字、密码和宠物。最好通过使用实例变量(只能从对象/模型内部访问的变量)来创建这些属性,然后创建特权 getters 和 setters 来修改或检索这些值。这允许您围绕这些属性创建规则。例如,您可以有一个密码设置器,它接受一个纯文本密码,然后在对象中加密它的值。通过省略 getter,还可以防止另一段代码从用户对象中检索用户的密码。下一个代码示例展示了用户模型的演变。
`var user = function user(name, password, pet){
var _name = null,
_password = null,
_pet = null,
_self = this;
this.setName(name);
this.setPassword(password);
this.setPet(pet);
name = null;
password = null;
pet = null;
/**
* Returns the user's name
/
this.getName = function(){
return _name;
}
/*
* Sets the user's name
*/
this.setName = function(name){
_name = name;
}
/**
* Sets the user's password and encrypts it before assignment
/
this.setPassword = function(password){
_password = password.encrypt(); // .encrypt() doesn’t really exist!
}
/*
* Returns the user's favorite pet
*/
this.getPet = function(){
return _pet;
}
/**
* Sets the user's favorite pet
*/
this.setPet = function(pet){
_pet = pet;
}
}`
如您所见,这里有大量的代码来实现看起来很少的功能。但是,这个想法是,您可以在应用的任何地方使用这个新的用户模型,无论您向它提供什么数据,它都会在整个应用中以可预测的方式运行。
使用模型的美妙之处在于,您可以在它们之间创建关系。例如,使用前面的例子,每个用户都有一只宠物,但是找到一种方法来描述这只宠物不是很好吗?为此,您可以创建一个宠物模型。
您可以很容易地将宠物属性添加到用户模型中,但是如果您将来需要更详细地描述宠物,您最终会得到一个混乱的用户模型。拥有一个独立的模型允许你在将来创建新的宠物属性,而不会破坏你的应用的完整性。宠物模型如下。
var pet = function pet(name, type){ var _name = null, _type = null;
` this.setName(name);
this.setType(type);
/**
* Gets the pet's name
*/
this.getName = function(){
return _name;
}
/**
* Sets the pet's name
*/
this.setName = function(name){
_name = name;
}
/**
* Gets the pet's type
*/
this.getType = function(){
return type;
}
/**
* Sets the pet's type
*/
this.setType = function(type){
_type = type;
}
}`
如您所见,宠物模型遵循与用户模型完全相同的结构。要一起使用这些,您可以执行以下操作。
`var sue = new user('Suzanne', 'password', null); // First create a new user with
no pet
var jack = new pet('Jack', 'dog'); // Create a new pet
sue.setPet(jack); // Assign the new pet to the user
/**
* By calling getPet, you now have access to all of the pet's methods and
* attributes from the user
*/
alert(sue.getName() + 'has a favorite ' + sue.getPet().getType() + ' called ' +
sue.getPet().getName());`
你可能想更进一步,允许用户拥有许多宠物。您可以通过在用户模型中创建一组宠物来实现这一点。您必须创建几个新方法来从用户对象外部管理 pets 数组。
addPet
将单个宠物添加到宠物数组中。- 从宠物数组中获取一个特定索引的宠物。
- 使用索引值删除单个宠物。
- 使用宠物数组设置宠物数组,覆盖现有的宠物数组。
getPets
检索分配给用户对象的所有宠物。
用户对象的新变化如下。
`/**
* Now that you can have multiple pets, it doesn't make sense to add it to
* the constructor
*/
var user = function user(name, password){
var _name = null,
_password = null,
_pets = [], // The default value is now an array instead of null
_self = this;
this.setName(name);
this.setPassword(password);
// favoritePet is not part of the constructor anymore, so it doesn't need to
be set
name = null;
password = null;
...
/**
* Adds a pet to the pet array
*/
this.addPet = function(pet){
// You can add object validation here before adding to the pet array
_pets.push(pet);
}
/**
* Gets a pet from the array at a specific index
*/
this.getPet = function(index){
return _pets[index];
}
/**
* Removes a pet from the array
/
this.removePet = function(index){
/*
* Splice can remove items from an array. It accepts a start index
* and number of items
*/
_pets.splice(index, 1);
}
/**
* Sets the pet array
/
this.setPets = function(pets){
/*
* Clear the pets array, using Array.length = 0 will remove
* every element in the array as apposed to creating a new array
* using _pets = [];
*/
_pets.length = 0;
/**
* Instead of completely replacing the pets array with the new array,
* each pet should go through the same validation in the addPet method.
* Instead of duplicating any validation code, it makes sense to just
* call the addPet method for every pet using a for loop.
*/
for(var i = 0; i < pets.length; i++){
_self.addPet(pets[i]);
}
}
/**
* Gets the pet array
*/
this.getPets = function(){
return _pets;
}
}`
从前面的代码中可以看出,除了setPets
方法之外,大多数方法都是显而易见的。从setPets
代码中,您可以看到您必须首先使用_pets.length = 0
清除 pets 数组。这比使用_pets = []
给_pets
变量分配一个新的空数组要慢;然而,它将简单地删除所有的数组元素,而不是创建一个新的空数组。不是将传递给方法的 pets 数组分配给 pets 对象中的_pets
数组,而是遍历新数组中的每个宠物并调用addPet
方法。这样做的原因是为了确保任何新宠物仍然通过用于将宠物添加到用户对象的相同代码,该代码可能包含验证或修改每个宠物对象。要使用新代码,您可以做一些类似于下面的 JavaScript 代码的事情。
`var user = new user('Suzanne', 'password');
var pet1 = new pet('Jack', 'dog');
var pet2 = new pet('Snoop', 'dog');
user.appPet(pet1);
user.addPet(pet2);
var message = user.name + ' has ' + user.getPets().length + ' pets. ' +
user.name + ' has';
for(var i = 0; i < user.getPets().length; i++){
message += ' a ' user.getPet(i).getType() + ' called ' +
user.getPet(i).getName();
}
alert(message);`
这应该会输出类似“Suzanne 有两只宠物。她有一只叫杰克的狗,一只叫史努比的狗。
重要的是要记住,你的模型只是 JavaScript 对象,所以你可以添加任何方法来操作其中的变量或者以某种方式输出东西。
MoMemo 内部没有那么多模型。解释模型如何相互协作的最好方式是通过类图。
尽管 JavaScript 是一种无类语言,但是您仍然可以使用类类比来描述如何通过在对象中创建构造函数、方法和实例变量来形成对象。
类图显示了一个类将拥有的方法和属性,以及它们如何与其他类交互。
注意:为了使本书尽可能简单,我将只讲述如何阅读基本的 UML 类图,包括属性、方法和常见的关联。如果你想学习更多关于 UML 和不同可用图表的知识,请随意查看[www.agilemodeling.com/essays/umlDiagrams.htm](http://www.agilemodeling.com/essays/umlDiagrams.htm)
。
MoMemo 的基本类图如图图 8-2 所示。
图 8-2。 类图中的班级
从图 8-2 中可以看到,每个框的顶部都显示了一个名称(如Movie
、Actor
等)。),它代表每个类的名称。在框名的正下方是几行前缀为-符号的行。这些是类的属性。属性名称前的符号指示属性应该是公共(+)还是私有(-)。您可以在 UML 类图中为属性指定类型以及其他属性。
就在属性下面,有一行后跟一个方法名。在图 8-2 的例子中,你可以看到Movie
的唯一方法是isFavorite,
,它返回一个布尔值并确定电影是否是一个收藏。
类旁边的黑色菱形表示Movie
类与Actor
和Video
类之间存在关联。黑色菱形告诉您关联是复合的,这意味着Movie
类拥有一个Actor
和Video
,并且在这个应用的上下文中,没有Movie
,Actor
和/或Video
就不能存在。复合关联还表示,如果父代(Movie
,在本例中)死亡,那么子代(Actor
/ Video
)将不再存在。重要的是要记住,当创建类之间的关联时,它们是在应用的上下文中创建的。还有其他类型的关联,在表 8-1 中列出。
在关联线旁边,您会看到 1 或 0...这叫做多重性。它指示每个类之间的关联类型。1 表示只有一个关联对象 0..表示有零个或多个关联对象。
在图 8-2 的例子中,一个Actor
只有一个Movie
,但是一个Movie
有零个或多个Actor
,一个Movie
有零个或多个Video
,但是一个Video
只能有一个Movie
。
通常,UML 图也不会显示属性的 getters 和 setters。使用前面的模型,您可以开始为 MoMemo 创建您的模型。
先说最小最不重要的型号,VideoSource
。
视频源模式
视频源模型是 MoMemo 应用中使用的所有模型中最简单的。视频源模型用于存储视频的不同视频格式。与其将 webm、mp4 和 ogv 属性添加到Video
模型中,不如将视频与视频源相关联以获得灵活性。
想象一下,一个新的移动网络浏览器刚刚发布,支持几种新的格式。如果你想支持这些格式,你需要修改Video
模型,这会对你的应用产生负面影响。通过创建与视频源的关联,如果出现新的格式,您不必担心修改代码,因为您可以简单地为新的视频格式创建新的实例,然后将它们添加到与Video
模型的关联中。为了使生活更容易,你可以在你的视图中创建一个循环(这将在本章中进一步讨论),这样你就不必修改应用来支持新的视频格式。
首先,在js/app/model/
文件夹中为视频源模型创建一个名为videosource.js
的新 JavaScript 文件。您现在可以开始使用下面的代码来定义您的模型的结构。
`var app = app || {};
app.model = app.model || {};
/**
* A video source used within a video
* You must add this object to a video once instantiated
* @param {String} url
* @param {app.type.format} format
*/
app.model.videosource = function appModelVideoSource(url, format){
// Your implementation goes here
}`
如您所见,该模型的声明方式与 pets 模型基本相同,只是您对所有模型都使用了app.model
名称空间。
构造函数接受视频的 URL 和格式,可以是 webm、ogv、mp4 等等。
接下来要做的是声明实例变量。简单重述一下,实例变量是只存在于对象实例范围内的变量。除非为实例变量创建了 getter 或 setter,否则实例变量在实例外部是不可访问或修改的。
`...
app.model.videosource = function appModelVideoSource(url, format){
/**
* The video source's instance variables
*/
var _url,
_format,
_self = this;
}`
如您所见,只有两个实例变量对应于构造函数参数。实例变量以下划线(_
)为前缀,这样它们就不会与通过构造函数传递的变量冲突。您还使用_self = this
声明了对实例的引用,因为this
关键字引用的是特权方法而不是对象。
接下来要做的事情是创建一个实例化方法,该方法在模型的末尾被调用。代码如下。
`...
app.model.videosource = function appModelVideoSource(url, format){
...
/**
* Set the instance variables using the constructor's arguments
*/
this.init = function(){
this.setUrl(url);
this.setFormat(format);
}
// Insert getters and setters
this.init();
}`
如您所见,init
方法只是调用通过构造函数传递的属性的设置器。
注意:通过构造函数传递的变量在构造函数和包含在构造函数中的特权方法的范围内。这意味着可以在特权方法中使用和修改构造函数参数。当在特权方法中创建新变量时,使用var
声明它们以防止特权方法修改构造函数参数是很重要的。
这可以防止代码重复,因为在从对象外部修改实例变量时,您可能会执行其他操作,如错误检查或根据条件更改值。
您可能会想,为什么不简单地调用 setters,而不像前面的代码示例那样在 JavaScript 对象的顶部用init
方法包装它们呢?简单的答案是 setters 还没有声明,所以调用这些方法会产生一个 JavaScript 错误。
当您在 JavaScript 中声明一个普通的命名函数时,解释器将在调用该函数时寻找它,而不管它在脚本中的位置。然而,当你把一个函数赋给一个变量时,你必须等到赋值发生后才能调用这个函数。
要解决这个问题,您必须将所有初始化代码放在对象的末尾,或者将它包装在顶部的方法中,并从底部调用它。我选择了后者,因为 getters 和 setters 往往会产生大量的白噪声,并且滚动大量看起来毫无意义的代码来找到您的主代码有点烦人。您可以看到在前面的例子中,init
方法被调用到对象的末尾。
接下来要做的是创建 getters 和 setters。这是一个非常简单的任务。简单回顾一下,getters 返回一个实例变量,setters 给实例变量赋值,因为对象外部的任何东西都不能从对象外部修改实例变量。
getters 和 setters 非常简单,您可以在下面的代码中看到。
`...
app.model.videosource = function appModelVideoSource(url, format){
...
/**
* Getters and setters
*/
/**
* Gets the url of the video source
* @return {String}
*/
this.getUrl = function(){
return _url;
}
/**
* Sets the url of the video source
* @param {String} url
*/
this.setUrl = function(url){
_url = url;
}
/**
* Gets the mimetype of the video source
* @return {app.type.format}
*/
this.getFormat = function(){
return _format;
}
/**
* Sets the mimetype of the video source
* @param {app.type.format} format
*/
this.setFormat = function(format){
_format = format;
}
...
}`
这就结束了VideoSource
模式。如你所见,这很简单。接下来,您将创建Video
模型,这也是相对简单的。
视频模式
Video
模型也相对简单,除了它与VideoSource
模型有一个复合关联,其中它可以由许多视频源组成。
在js/app/model
中创建一个名为video.js
的新的空文件。这将包含新的视频模型。
构造函数接受一个标题、以毫秒为单位的长度和一个指向 posterframe 或 preview frame 的 URL,这将是一个图像。
`var app = app || {};
app.model = app.model || {};
/**
* A video associated with a movie
* You must add video sources in order for videos to play
* @param {String} title
* @param {Integer} length
* @param {String} posterframe
*/
app.model.video = function appModelVideo(title, length, posterframe){
// Code implementation goes here
}`
这个对象的实例变量是title
、length
、posterframe
和一个源数组。从下面的代码片段中可以看出,sources
变量不是构造函数参数的一部分。
app.model.video = function appModelVideo(title, length, posterframe){ /** * The video's instance variables */ var _title, _length, _posterframe, _sources = [], self = this; }
您可以为_sources
实例变量创建访问器。因为它对于对象的直接实例的函数不是强制性的,所以它不需要在构造函数中。
_sources
实例变量的访问器由一个方法组成,该方法用于添加单个源实例、基于索引从数组中移除和获取实例、检索整个数组或者用新数组覆盖该数组。在下面的代码片段中可以看到这些访问器。
`app.model.video = function appModelVideo(title, length, posterframe){
...
/**
* Gets all of the video sources used for embedding video
* in POSH
* @return {Array}
*/
this.getSources = function(){
return _sources;
}
/**
* Sets all video sources using an array
* @param {Array} sources
*/
this.setSources = function(sources){
/**
* Clears the sources array
*/
__sources.length = 0;
/**
* Rather than setting the sources all in one go,
* you use the addSource method, which can handle
* any validation for each source before it's
* added to the object
*/
for(var i = 0; i < sources.length; i++){
_self.addSource(sources[i]);
}
}
/**
* Adds a source to the sources array
* @param {app.model.videosource} source
*/
this.addSource = function(source){
_sources.push(source);
}
...
}`
正如您所看到的,访问器遵循了本章前面显示的 pet 示例中的相同实现。
您可能还想在_length
setter 上添加某种类型的过滤,以便将任何值解析为整数,从而防止模型中出现任何问题。您可以在 setter 的实现中使用parseInt
方法。这将确保传递给模型的任何值都是整数或零。在下面的代码片段中可以看到 setter。
`app.model.video = function appModelVideo(title, length, posterframe){
...
/**
* Sets the length of the video in milliseconds
* @param {Integer} length
/
this.setLength = function(length){
/*
* Use parseInt here just to ensure the length
* is an integer. If it's not, then it will
* return NaN. The isNaN method will check to
* see whether the value is not a number.
*/
_length = parseInt(length);
if(isNaN(_length)){
_length = 0;
}
}
...
}`
您现在可以完成Video
模型,它应该看起来像下面这样。
`var app = app || {};
app.model = app.model || {};
/**
* A video associated with a movie
* You must add video sources in order for videos to play
* @param {String} title
* @param {Integer} length
* @param {String} posterframe
*/
app.model.video = function appModelVideo(title, length, posterframe){
/**
* The video's instance variables
*/
var _title,
_length,
_posterframe,
_sources = [],
_self = this;
/**
* Set the instance variables using the constructor's arguments
*/
this.init = function(){
this.setTitle(title);
this.setLength(length);
this.setPosterframe(posterframe);
}
/**
* The getters and setters
*/
/**
* Gets the title of the video
* @return {String}
*/
this.getTitle = function(){
return _title;
}
/**
* Sets the title of the video
* @param {String} title
*/
this.setTitle = function(title){
_title = title;
}
/**
* Gets the length of the video in milliseconds
* @return {Integer}
*/
this.getLength = function(){
return _length;
}
/**
* Sets the length of the video in milliseconds
* @param {Integer} length
/
this.setLength = function(length){
/*
* Use parseInt here just to ensure the length
* is an integer. If it's not, then it will
* return NaN. The isNaN method will check to
* see whether the value is not a number.
*/
_length = parseInt(length);
if(isNaN(_length)){
_length = 0;
}
}
/**
* Gets all of the video sources used for embedding video
* in POSH
* @return {Array}
*/
this.getSources = function(){
return _sources;
}
/**
* Sets all video sources using an array
* @param {Array} sources
*/
this.setSources = function(sources){
_sources.length = 0;
/**
* Rather than setting the sources all in one go,
* you use the addSource method, which can handle
* any validation for each source before it's
* added to the object
*/
for(var i = 0; i < sources.length; i++){
_self.addSource(sources[i]);
}
}
/**
* Gets the source at a specific index
* @param {Integer} index
* @return {app.model.videosource} source
*/
this.getSource = function(index){
return _sources[index];
}
/**
* Removes a source at a specific index
* @param {Integer} index
*/
this.removeSource = function(index){
_sources.splice(index, 1);
}
/**
* Adds a source to the sources array
* @param {app.model.videosource} source
*/
this.addSource = function(source){
_sources.push(source);
}
this.init();
}`
完成Video
模型后,您可以进入最后一个小模型,即Actor
模型。
演员模特
Actor
模型是另一个简单的对象,它只包含Movie
中参与者的名字和角色。在js/app/models/
中创建一个名为actor.js
的新文件。这里没有太多要解释的,所以继续创建模型。它应该有两个通过构造函数设置的名为_name
和_role
的实例变量。您的完整代码将如下所示。
`var app = app || {};
app.model = app.model || {};
/**
* The actor object handles the actors for a movie
* Actors should only be included in a full movie listing
* @param {String} name
* @param {String} role
*/
app.model.actor = function appModelActor(name, role){
/**
* The actor's instance variables
*/
var _name,
_role,
_self = this;
/**
* Set the instance variables using the constructor's arguments
*/
this.init = function(){
this.setName(name);
this.setRole(role);
}
/**
* Getters and setters
*/
/**
* Returns the full name of the actor
* @return {String}
*/
this.getName = function(){
return _name;
}
/**
* Sets the actor's full name
* @param {String} name
*/
this.setName = function(name){
_name = name;
}
/**
* Gets the role of the actor in
* relation to the associated film
* @return {String}
*/
this.getRole = function(){
return _role;
}
/**
* Sets the actor's role in relation
* to the associated film
* @param {String} role
*/
this.setRole = function(role){
_role = role;
}
this.init();
}`
电影模式
Movie
模型是 MoMemo 应用中最大的模型之一。它与Actor
和Video
模型有复合关联,所以如果电影被销毁,关联的对象实例也会被销毁。关联的属性也是数组,所以您需要为数组对象创建添加和删除方法,UML 类图中没有列出这些方法。
首先,在js/app/model/
中创建movie.js
模型文件。Movie
模型将模仿烂番茄 API 的一些可用属性。
注意:本书只涉及部分烂番茄原料药。您可以在[
developer.rottentomatoes.com/](http://developer.rottentomatoes.com/)
找到关于 API 能力的更多信息。
下面的代码片段显示了需要为其创建访问器的实例变量。
`var app = app || {};
app.model = app.model || {};
/**
* A movie model used for all movies within the application
*
* @alias app.model.movie
* @constructor
* @param {String} title
* @param {String} rtid
* @param {String} posterframe
* @param {String} synopsis
*/
app.model.movie = function appModelMovie(title, rtid, posterframe, synopsis) {
/**
* The video's instance variables
*/
var _title,
_rtid,
_posterframe,
_synopsis,
_releaseDate,
_videos = [],
_actors = [],
_rating,
_favorite = false,
_self = this;
}`
您完整的Movie
模型应该看起来像附录中列出 A-1 的中的代码片段。
这个模型的代码非常简单。对于对应用非常重要的东西,验证传递给访问器的值是有意义的,这样可以防止应用在错误的类型传递给模型时崩溃。
验证
不幸的是,JavaScript 不支持所谓的类型提示。类型提示是指定方法可以接受的参数类型的过程。其他语言中的类型提示将允许您在方法声明中指定方法的参数应该是某种类型。例如,在 PHP 中,您可以指定参数应该是数组或对象类型,如下面的代码片段所示。
function doSomething(MyObject $object, array $myarray){ /** some implementation **/ }
这减轻了执行大量验证来检查传递给方法的对象是否是某种类型的需要,因为在执行时,如果不是,方法将自动抛出异常或错误。
为了解决这个问题,您需要验证传递给访问器的参数。最常见的验证类型是根据类型进行验证。您可以检查一个值是对象、数组、数字还是字符串。
您可能希望不止一次地使用这些验证器,所以创建一个对象来存储所有的验证方法是值得的。为此,在js/app/utility
文件夹中创建一个名为validator.js
的新 JavaScript 文件(如果该文件夹不存在,则创建它)。
您将创建一个无构造器的对象。这将允许您使用对象的方法,而不必实例化它。使用下面的代码片段开始创建您的验证对象。
`var app = app || {};
app.utility = app.utility || {};
/**
* Validator object has static methods
* to check to easily validate values
*/
app.utility.validator = {}`
有了 validator 工具,我们就可以开始创建验证方法了。
伊西普西伊普西伊普西伊普西伊普西伊普西伊普西伊普西伊普西伊普西伊普西伊普西伊普西伊普西伊普西伊普西
第一个简单的验证方法是检查一个值是否为空。在验证器中创建一个名为isEmpty
的新方法,如下面的代码示例所示。
`...
app.utility.validator = {
/**
* Checks to see whether a value is empty or not
* Returns true if it is, or false if it isn't
* @param {String|Object} value
* @return {Bool}
*/
isEmpty: function(value){
return false;
}
}`
如您所见,isEmpty
方法接受单个参数,即要验证的值。默认情况下,它将返回 false,除非实现的其余部分返回 true。
使用下面的验证代码来完成验证器。
`...
app.utility.validator = {
/**
* Checks to see whether a value is empty or not
* Returns true if it is, or false if it isn't
* @param {String|Object} value
* @return {Bool}
*/
isEmpty: function(value){
if(value == '' || value == null || value === false){
return true;
}
return false;
}
}`
从前面的实现中,您可以看到,如果值为空字符串、null 或 false,条件语句将返回 true。
注:如你所见,我已经用===
比较了值与 false。一个==
比较松散地比较值。例如,0 == false
将返回 true。0 不一定是空值,所以使用0 === false
会返回 false,因为 0 和 false 是两种完全不同的类型。
isTypeOf
下一个验证器将检查一个值是否是一个对象类型。这是一个比较简单的方法。它使用instanceof
来查看一个对象是否是另一个对象的实例。该方法接受值和类型。但是,类型不能是字符串,而是原始对象,如下面的代码片段所示。
`...
app.utility.validator = {
...
/**
* Checks to see whether a value is a type of object
* Returns true if it is, or false if it isn't
* @param {Object} value
* @param {Object} type
* @return {Bool}
*/
isTypeOf: function(value, type){
if(value instanceof type){
return true;
}
return false;
}
}`
这很好,但是当您想要检查像布尔值、字符串和数字这样的基本类型时会发生什么呢?创建一个新的方法来检查这些将会令人困惑并且有点烦人。要解决这个问题,您可以使用typeof
操作符。这将检查变量的类型。因为字符串、数字和布尔值都是基本类型,所以可以通过允许 type 参数接受字符串来检查它们。为此,只需添加以下代码。
`...
app.utility.validator = {
...
/**
* Checks to see whether a value is a type of object
* Returns true if it is, or false if it isn't
* @param {Object} value
* @param {Object} type
* @return {Bool}
*/
isTypeOf: function(value, type){
// First check to see if the type is a string
if(typeof type == "string"){
// If it is, we're probably checking against a primative type
if(typeof value == type){
return true;
}
} else {
// We're dealing with an object comparison
if(value instanceof type){
return true;
}
}
return false;
}
}`
从前面的代码中可以看出,主要的变化是isTypeOf
方法检查类型参数是否是字符串类型。如果它是一个字符串,假设你想检查一个原始类型;否则,您将检查对象类型。如果原始类型与您需要的类型相同,它将返回 true。
这绝不是一个防弹的解决方案。有一些库可以做更好的类型检查。其思想是,当您开始需要更多的验证技术时,您可以在需要时添加验证对象。如果您创建的验证方法不完全符合特定的边缘情况,您可以进行调整以进行补偿。有了这些,我们现在可以基于 UML 类图在模型的访问器方法中强制类型。
对模型进行验证
当您对模型应用验证时,您有两种选择:要么在验证失败时静默失败,要么抛出一个异常并允许调用者处理它。我更喜欢后者。最令人沮丧的事情莫过于当应用中发生了非常糟糕的事情,而您对此一无所知,因此无法在代码中对其做出反应。
抛出和处理异常
异常应该被认为是由用户代码抛出的严重错误。您不应该在编写的每一段代码中都抛出异常,因为您必须将来自调用者的代码包装在调用它的代码的try
/ catch
块中。浏览器的控制台中将出现一个未捕获的异常。抛出异常相当简单。它由 throw 操作符和一个字符串、布尔值、整数或(信不信由你)一个对象组成。
`// Throwing a string
if(true !== false){
throw "True definitely isn't equal to false"
}
// Throwing an object
if(true !== false){
throw {
message: "True definitely isn't equal to false",
type: "pointless_exception",
code: 1000
}
}`
如果你想知道抛出了什么类型的异常,通常最好抛出一个对象。正如您在前面的代码中看到的,您可以在其中放入几乎任何东西,包括代码、类型和消息。如果你抛出一个原始类型的异常(字符串、布尔值、整数),除了你之外,没有人能够弄清楚这个异常是什么,它意味着什么,最重要的是,如何处理它。通过使用对象,您能够检查代码或类型,而不是尝试比较复杂的字符串。
下一步是在抛出异常后捕捉它。这是一项简单的任务。为了测试这一点,您可以将您的条件语句包装在一个函数中,并从try
/ catch
块中调用它,只是为了向您展示它在现实世界中是如何工作的。
`function doSomething(){
if(true !== false){
throw {
message: "True definitely isn't equal to false",
type: "pointless_exception",
code: 1000
}
}
}
try {
doSomething();
} catch (e){
alert(e.message);
}`
如果您尝试这段代码,您会发现抛出了异常,e.message
将为您检索消息并发出警告。
重要的是要明白,只有当没有其他方法来处理代码块中的错误时,才应该使用异常。
下一节将向您介绍如何强化模型,以便设置器在传递错误类型的值时抛出异常。为了便于阅读,已经省略了 getters。
强化模型
新的验证规则(在下面的代码中以粗体显示)只是检查类型。如果值不属于某个类型,它将抛出一个验证异常,然后中断该方法。这可以防止实例变量被无效值设置。
此外,为了方便起见,app.utility.validator
对象被分配给了 validator 变量。这使得访问验证方法变得稍微容易一些,而不是必须不断地键入极长的名称空间。因为它是一个实例变量,所以特权方法也可以访问它。
`app.model.videosource = function appModelVideoSource(url, format){
/**
* The video source's instance variables
*/
var _url,
_format,
_self = this,
validator = app.utility.validator;
...
/**
* Sets the url of the video source
* @param {String} url
*/
this.setUrl = function(url){
// Check to see whether the value is a primitive string type
** if(!validator.isTypeOf(url, "string")){**
** throw {**
** message: "The url property in the videosource model requires**
a 'string' type",
** type: "validation_exception"**
** }**
** return;**
** }**
_url = url;
}
...
/**
* Sets the mimetype of the video source
* @param {app.type.format} format
*/
this.setFormat = function(format){
** // Check to see whether the value is an app.type.format**
** if(!validator.isTypeOf(format, app.type.format)){**
** throw {
message: "The format property in the videosource model requires**
a 'app.type.format' type",
** type: "validation_exception"**
** }**
** return;**
** }**
_format = format;
}
this.init();
}`
基于这个例子,您现在应该能够根据本书中的代码注释向您的 setters 添加验证。请随意为每个型号添加它们。
创建新类型
JavaScript 中目前可用的类型非常好。然而,有时您可能会发现自己处于这样一种情况,您想要将一个结构化的对象传递给一个模型,而这个模型本身不一定是模型。这些通常被称为类型,并且在整个应用中具有相同的结构。例如,您可能希望创建一个新的位置类型来存储位置信息,并以可预测的方式检索它。
从模型的注释中,您会看到应用中有两个定制类型:format
和releaseDate
。format
简单地允许你存储一个视频格式,这样很容易得到 mime 类型,文件格式(如 mp4,webm 等。),以及格式的名称以供参考。releaseDate
类型只是存储电影的影院发行日期和 DVD 发行日期。
要创建新类型,只需在js/app/type
目录下创建两个新文件,分别名为format.js
和releaseDate.js
。(如果该目录不存在,您应该创建它。)
在format.js
文件中,添加以下代码。
`var app = app || {};
app.type = app.type || {};
/**
* The media type, can be used to
* define mime types of objects
* @param {String} name
* @param {String} format
* @param {String} mime
*/
app.type.format = function(name, format, mime){
/**
* The media's instance variables
*/
var _name,
_format,
_mime,
_self = this;
/**
* Set the instance variables using the constructor's arguments
*/
this.setName(name);
this.setFormat(format);
this.setMime(mime);
/**
* Getters and setters
*/
/**
* Gets the name of the media type
* @return {String}
*/
this.getName = function(){
return _name;
}
/**
* Sets the name of the media type
* @param {String} name
*/
this.setName = function(name){
_name = name;
}
/**
* Gets the format of the media (e.g., webm, ogv)
* @return {String}
*/
this.getFormat = function(){
return _format;
}
/**
* Sets the format of the media
* @param {String} format
*/
this.setFormat = function(format){
_format = format;
}
/**
* Gets the mime type of the media
* @return {String}
*/
this.getMime = function(){
return _mime;
}
/**
* Sets the mime type of the media
* @param {String} mime
*/
this.setMime = function(mime){
_mime = mime;
}
}`
正如您所看到的,只有三个实例变量叫做mime
、format
和name
。使用访问器,这就是创建新类型所需的全部内容。您可以用同样的方式创建发布类型,将下面的代码添加到releaseDate.js.
`var app = app || {};
app.type = app.type || {};
/**
* The movie release date
* The constructor takes the cinema release date and dvd release date
* @param {Date} cinema
* @param {Date} dvd
*/
app.type.releaseDate = function(cinema, dvd){
/**
* The release date instance variables
*/
var _dvd,
_cinema;
/**
* Sets the instance variables using setters
*/
this.setDvd(_dvd);
this.setCinema(_cinema);
/**
* Gets the DVD release date
*/
this.getDvd = function(){
return _dvd;
}
/**
* Sets the DVD release date
*/
this.setDvd = function(dvd){
_dvd = dvd;
}
/**
* Gets the cinema release date
*/
this.getCinema = function(){
return _cinema;
}
/**
* Sets the cinema release date
*/
this.setCinema = function(cinema){
_cinema = cinema;
}
}`
同样,该模型的工作方式与format
型完全相同。
应用工具
正如您从 validator 工具中看到的,工具允许您将不一定属于控制器、模型、视图或类型的代码放入它自己的对象中。工具对象通常会执行某种重复的动作。在这个应用中,除了 validator 工具之外,还有其他三个工具。这些是:
deck.js
—此工具用于管理一副牌中的牌。它允许你显示和隐藏卡片。layout.js
—该工具允许您在设备方向改变时刷新布局。有些元素需要动态的宽度和高度,这个工具提供了便利。jsonp.js
—该工具允许您对支持 JSONP(带填充的 JavaScript 对象符号)格式的 web 服务进行跨站点请求。
管理甲板
卡片组管理器是一个简单的对象,用于显示和隐藏卡片。创建这个工具是为了避免在应用的主代码中重复输入类名。这是因为类名可能会改变,这意味着必须在整个应用中更新类名,并重新测试所有代码。这样做的另一个原因是,您可能想要更改卡片显示或隐藏的方式。例如,您可以使用 CSS3 动画来翻转卡片、淡出卡片等等。
在js/app/utility
中创建一个名为deck.js
的新文件,其中包含以下代码。
`var app = app || {};
app.utility = app.utility || {};
app.utility.deck = (function(){
// Keep all of the cards in a local scope
var _cards = document.getElementsByClassName('card');
// Return an object with methods
return {
// Shows a card by adding the active class
showCard: function(id){
document.getElementById(id).classList.add('active');
},
// Hides a card by removing the active class
hideCard: function(id){
document.getElementById(id).classList.remove('active');
},
/*
* Hides all cards by iterating through the card list
* and removing the active classname
*/
hideAllCards: function(){
for(var i = 0; i < _cards.length; i++){
_cards[i].classList.remove('active');
}
}
}
})();`
这个对象只是公开一些方法,这些方法允许您在具有特定 ID 的元素中添加或移除活动类名。还有一种方法便于隐藏所有的卡片。_cards
实例变量保存了该副牌中所有牌的列表。
正如您所看到的,函数本身被放在括号中,而不是创建一个匿名函数,函数本身返回一个带有方法的对象。这被称为揭示模块模式。它不同于 JavaScript 中创建对象的正常方式,它创建一个函数并使用this.methodName
来创建方法。当脚本加载时,函数中的代码会自动执行,并返回一个对象。
var myObject = (function(){ return { sayHi: function(){ alert('Hi!'); } } })();
这意味着您不必使用new
操作符创建一个新对象。您可以简单地通过使用对象的赋值变量(在本例中为myObject
)来调用该对象,因为构造函数已经执行过了。然后,您可以像访问任何对象一样访问“公共”方法。
myObject.sayHi();
这种方法的缺点是没有明显的方法来创建一个新对象并将其赋给一个新变量。
发送跨站点请求
有时候,您可能需要从外部 web 服务而不是自己的 web 服务中获取数据。为此,您通常会使用 Ajax。如果另一个服务器支持跨源资源共享(CORS),您将能够发出远程 Ajax 请求。不幸的是,并不是所有的 web 服务都支持这一点。
为了解决这个问题,一些 web 服务支持 JSONP。JSONP 允许您向 web 服务发送回调参数。通常,您会收到一个 JSON 对象,作为 JSON 请求的一部分,就像下面这样。
{ " name": " Dave", "occupation": " General Manager" }
使用 JSONP,由于创建请求所用的方法,这些数据变得不可访问。
对于 Ajax 请求,您发送请求并创建一个回调方法作为事件监听器。使用 JSONP,您实际上将请求作为一个<script />
嵌入到 web 页面中。作为脚本源代码的一部分,您将追加一个回调方法。产生的 HTML 看起来像下面的代码片段。
<script src="http://myservice.com/staff/101/**?callback=showProfile**" async="async"></script>
这将加载脚本,web 服务将结果包装在方法中,响应如下所示。
showProfile({ "name": "Dave", "occupation": " General Manager" });
然后将数据作为参数执行showProfile
功能。
创建一个 JSONP 请求可能是一项费力的任务,而这个任务jsonp.js
可以为您处理。
首先在js/app/utility/
中创建一个名为jsonp.js
的新 JavaScript 文件。添加以下代码。
`var app = app || {};
app.utility = app.utility || {};
app.utility.jsonp = function(url, callbackmethod){
/**
* Create a new _src variable to append the callback param to the url
*/
var _src = url + '&callback=' + callbackmethod;
/**
* Create the script element
*/
var _script = document.createElement('script');
/**
* Set the source of the script element to be the same as the one specified
above
*/
_script.src = _src;
/**
* To prevent the script from blocking other requests, load it
* asynchronously where possible
*/
_script.async = "async";
/**
* Once the script has loaded, the function will execute and the
* script tag can be removed from the head of the document
*/
_script.onload = _script.onreadystatechange = function(load){
var script = document.head.removeChild(load.target);
script = null;
}
/**
* This privileged method will send the request by appending the script to
the
* DOM
*/
this.send = function(){
document.head.appendChild(_script);
}
}`
这个工具本身并不复杂。它只是创建一个脚本元素并将其嵌入到文档中。脚本加载完成后,onload
事件监听器会自动将它从 DOM 中移除。
提出要求,就像打电话一样简单。
var request = new app.utility.jsonp(“http://myservice.com/staff/101/”, “showProfile”); request.send();
JSONP 的问题是,与 Ajax 不同,您不能取消请求。因此,如果您经常发送对自动完成字段的请求,您可能会发现发送了几个请求,但是它们并没有按照您期望的顺序返回。如何处理这个问题将在本章后面的电影控制器中讨论。
控制布局和处理大小调整
有时,您可能会发现单独使用 CSS 无法产生您想要的布局。例如,页面上可能有三个高度不可预测的元素。使用 JavaScript 来处理这个问题应该被视为最后的手段,因为在页面加载和布局工具调整元素尺寸之间可能会有延迟。
该工具的代码看起来与其他对象略有不同。它使用一个自执行函数,这样一旦脚本完成加载,它就会自动执行。在js/app/utility/
中创建一个名为layout.js
的新文件,其中包含以下代码。
`var app = app || {};
app.utility = app.utility || {};
app.utility.layout = (function(){
/**
* This method will adjust the height of all decks
* so that there is space at the top for the taskbar,
* which has an unpredictable height
*/
var fixdeckheight = function(){
/**
* First loop through each deck
/
[].forEach.call(document.getElementsByClassName('deck'), function(el){
/*
* And set the height of the deck by subtracting the height of
* the taskbar from the height of the document body
*/
el.style.height = (document.body.offsetHeight –
document.getElementById('taskbar').offsetHeight) + 'px';
});
};
/**
* Create a timeout variable, as it may take a while
* for the new sizes to update in some browsers
/
var timeout;
/*
* Add an event listener to the window so that when
* it's resized, it will clear the timeout
/
window.addEventListener('resize', function(){
// Clear the timeout just in case it's set, to prevents multiple calls
clearTimeout(timeout);
/
* Set the timeout to 100ms and execute fixdeckheight at the end of
* the timeout
*/
timeout = setTimeout(function(){ fixdeckheight(); }, 100);
});
// Call fixdeckheight for the first time
fixdeckheight();
})();`
从代码注释中可以看出,自执行函数包含一个名为fixdeckheight
的方法。考虑到任务栏的大小,这只是将面板的高度设置为视口的大小。事件监听器将监听 resize 事件的触发,并调用fixdeckheight
函数。使用了一个计时器,因为当resize
事件被触发时,视窗大小并不立即可用。
景色
当我第一次在 MVC for JavaScript 中遇到视图时,我假设视图只是在 HTML 网页上定义的一段 HTML,就像 PHP 中的 MVC 一样。从那以后,我对观点的看法发生了变化。对于 JavaScript MVC 中的视图,我能给出的最好解释是,它是一段 HTML,将在您的应用中重用。
视图可能包含某种逻辑,但不多。视图背后的思想是,在模型或控制器中,HTML 的任何部分都是不可见的。考虑到这一点,视图通常是一段 HTML,封装在一个 JavaScript 对象中,在呈现之前有修改它的逻辑。控制器将创建一个新的视图对象,并将适当的变量传递给构造函数。视图对象的一个例子如下。
`var view = function(name){
/**
* Create a root element. This allows you to add to it using innerHTML
* so that you don't need to manually create new DOM elements for large
* chunks of HTML.
*/
var _rootElement = document.createElement('div');
/**
* You can use innerHTML here to add content to the root element. As you can
see,
* rather than concatenating a very long string, an array is used. This is
* cleaner and easier to read than a long string. .join('') is used to merge
the
* array into a string with no spaces.
*/
_rootElement.innerHTML = [
'
Hello, my name is ', name, '
'].join('');
this.render = function(){
return _rootElement;
}
}`
如您所见,构造函数接受了一个名为name
的参数。您可以拥有任意数量的参数,这些参数可以在视图本身中使用。
第二行创建一个新元素,并将其赋给_rootElement
实例变量。这很有用,因为对于添加到根元素的任何新元素,不需要 DOM 解析器来遍历 DOM。
第三行代码使用数组设置根元素的innerHTML
。这比使用长字符串更可取,因为它更容易阅读和维护。join('')
用于将数组合并成一个字符串,以便innerHTML
可以接受它。
视图中唯一有特权的方法是this.render
,它只是返回_rootElement
。您可以在实例化视图后添加方法来操作视图。要检索完整的视图,可以从控制器中对对象运行render
方法,这将返回一个 DOM 对象。
重要的是要记住,在一副牌中使用视图并不一定是完整的牌;它们也可以是在其他视图中使用的局部视图。例如,在 MoMemo 中,有两种类型的电影列表:最喜欢的电影和电影搜索结果。
列表本身的表示可能有所不同,但是因为各个电影行是相同的,所以为每个电影列表项复制 HTML 是没有意义的。这就是在视图中使用视图的方便之处。我们的控制器不需要知道每种类型的列表是如何呈现的;视图对此负责。
我们可以使用原始 HTML 文件中使用的 HTML 来为我们创建视图。MoMemo 应用中有三个视图:movielistitem
、movielist
和movie
。我们接下来会讨论这些。
电影列表项视图
电影列表项视图非常简单。不必在将来为各种电影列表创建相同的电影列表项,电影列表项视图可用于不同的电影列表中,以避免必须为每个视图重写 POSH。
在js/app/view
中创建一个名为movielistitem.js
的新文件。(如果该文件夹不存在,则创建它。)
将以下代码添加到文件中。
`var app = app || {};
app.view = app.view || {};
/**
* Creates a new view for a movie list item
* @param {app.model.movie} movie
*/
app.view.movielistitem = function(movie){
var _movie = movie,
_rootElement = document.createElement('li');
_rootElement.innerHTML = [
'<a data-controller="movies" data-action="find"
data-params="{"id": "', movie.getRtid() ,'"}"
class="more" href="movie/view/", movie.getRtid() ,'">',
'
'
'
'
', movie.getTitle(), '
','
', movie.getSynopsis(), '
',''
].join('');
this.render = function(){
return _rootElement;
}
}`
如您所见,它遵循与常规视图相同的标准结构。根元素是一个列表项(li
),视图接受一个电影模型。在 POSH 的数组中,您可以看到数组值分隔符(逗号)被用于来分隔电影访问器和 POSH 本身。这比使用串联字符串更容易阅读和维护。在电影的链接中,您可以看到已经创建了各种数据属性。
<a data-controller="movies" data-action="find" data-params="{"id": "', movie.getRtid() ,'"}" class="more" href="movie/view/", movie.getRtid() ,'">
params
将用于应用的事件委托,以触发控制器事件/动作并将参数传递给它们。
代码的其余部分非常简单,创建一个列表项来保存电影的标题、概要和预览图像/海报帧。
电影列表视图
电影列表视图只是一个无序的列表,用于保存各种电影。在这个应用中,电影列表视图一次用于收藏夹列表,一次用于搜索结果列表。这再次概括了使用视图的好处。如果没有创建这个视图,就必须在应用的两个地方创建和维护相同的 POSH。
在名为movielist.js
的js/app/view/
文件夹中创建一个新文件,并添加以下代码。
`var app = app || {};
app.view = app.view || {};
/**
* Creates a new view based on the search results
* @param {Array} results
*/
app.view.movielist = function(results){
var _results = results,
_rootElement;
// Create the root UL element
_rootElement = document.createElement('ul');
_rootElement.classList.add('list');
_rootElement.classList.add('movie-list');
for(var i = 0; i < results.length; i++){
var itemView = new app.view.movielistitem(results[i]);
_rootElement.appendChild(itemView.render());
}
this.render = function(){
return _rootElement;
}
}`
如您所见,视图构造函数接受一组结果。在这个视图中没有进行错误检查来验证数组中的每个模型,但是您可以使用 validator 工具来执行这个操作。从代码中,您可以看到根元素是一个无序列表。
_rootElement = document.createElement('ul'); _rootElement.classList.add('list'); _rootElement.classList.add('movie-list');
从这里开始,添加几个类用于以后的样式化。
然后遍历数组中的每个模型,并用该模型创建一个新的movielistitem
对象。然后,通过调用appendChild
方法并呈现列表项来添加movielistitem
就很简单了。
for(var i = 0; i < results.length; i++){ var itemView = new app.view.movielistitem(results[i]); _rootElement.appendChild(itemView.render()); }
结果是一个用电影列表填充的无序列表。
电影观
当用户从他们的收藏夹或搜索中点击一个电影项目时,电影视图简单地保存电影的完整视图。它比其他视图稍微复杂一点,因为它侧边滚动,这样用户可以获得关于电影的更多信息。当您想要为一个模型添加新的内容块时,这种方法特别有效。然而,这是有限制的,因为添加太多的块和永远的侧滚对用户来说是痛苦的。
由于这是一个相当大的视图,我们将逐行浏览。
首先,用下面的代码在js/app/view/movie.js
中创建标准视图对象布局。
`var app = app || {};
app.view = app.view || {};
/**
* Creates a new view for a movie list item
* @param {app.model.movie} movie
*/
app.view.movie = function(movie){
var _rootElement = document.createElement('div');
_rootElement.innerHTML = [].join('');
this.render = function(){
return _rootElement;
}
}`
如您所见,根元素是一个div
。应该创建的第一个部分是电影视图的标题。这包括海报框架、电影标题、从收藏夹中添加/删除电影的按钮以及电影上映日期。
`...
app.view.movie = function(movie){
...
_rootElement.innerHTML = [
'
'
'',
'<a href="#" class="btn-favorite add" data-controller="favorites"
data-action="add" data-params='{"id": "', movie.getRtid() ,'",
"title": "',
escape(movie.getTitle()) ,'", "synopsis": "',
escape(movie.getSynopsis()) ,
'", "posterframe": "', movie.getPosterframe() ,'"}'>favorite',
'
', movie.getTitle(),'
','
Cinematic Release - ',
movie.getReleaseDate().getCinema().getDate(), '/',
movie.getReleaseDate().getCinema().getMonth() + 1 , '/',
movie.getReleaseDate().getCinema().getFullYear() ,'
'',
'
].join('');
...
}`
正如你所看到的,电影标题<header />
包含了海报帧,它跨越了标题的宽度。它还包含电影信息,如标题和上映日期。Date
对象返回一个从零开始的月份数,所以您需要将它的值加 1 以获得正确的月份数。
movie.getReleaseDate().getCinema().getMonth() + 1 , '/',
下一个要创建的是电影内容块。这些块包含电影信息,如概要、演员和视频。
`...
app.view.movie = function(movie){
...
_rootElement.innerHTML = [
...
'
'
'
'
'
', movie.getSynopsis() ,'
','
'
'
'
'
Cast List
','
'
'
'
'
'
Video Clips
','
'
'
'
'
].join(‘’);
...
}`
对这一部分来说,豪华是相当简单的;它只是输出电影的概要。演员表和视频剪辑仍然为空,因为稍后会填充这些内容。
最后,您需要创建一个页脚导航按钮来返回到收藏夹屏幕。
`...
app.view.movie = function(movie){
...
_rootElement.innerHTML = [
...
'
].join('');
...
}`
接下来的几行代码将检查一部电影是否在用户的收藏夹中。如果是,它将改变收藏夹按钮中的状态、数据属性和文本。
`...
app.view.movie = function(movie){
...
// Check to see whether the movie is in the user's favorites
if(movie.isFavorite()){
var _favoriteButton = _rootElement.querySelector('a.btn-favorite');
_favoriteButton.setAttribute('data-action', 'remove');
_favoriteButton.classList.remove('add');
_favoriteButton.classList.add('remove');
_favoriteButton.textContent = 'un-favorite';
}
...
}`
最后但同样重要的是,您需要遍历电影中的所有演员,并将他们添加到演员列表中。
`...
app.view.movie = function(movie){
...
for(var i = 0; i < movie.getActors().length; i++){
var actor = movie.getActor(i);
var element = document.createElement('li');
element.innerHTML = [
'
', actor.getName(), '
',
'', actor.getRole(), '
].join('');
_rootElement.querySelector('#block-cast ul.list').appendChild(element);
}
...
}`
自举和控制器
与其他 MVC 框架不同,控制器不一定要分配给 JavaScript 中的路由或 URI/URL。在本书中,控制器也可以分配给事件。控制器管理应用中的数据流,将信息传递给视图,然后负责使用 DOM 操作将信息呈现给用户。控制器还负责处理用户事件。在我以前为 JavaScript 创建 MVC 框架的尝试中,事件是在控制器之外处理的,这变得很混乱。常见事件(比如绑定到链接来触发控制器动作)应该在一个地方处理;但是,其他独特的事件(如窗体事件)应该由控制器本身绑定。还有其他更有效的方法,但是对于为这本书开发的应用来说,这样做是有意义的。其他替代方法包括使用名为Sammy.js
的库,它可以用来创建基于 hashbangs 的 restful URLs(例如index.html#/mypage
)。Sammy 可以获取这些 URL,然后根据路由规则执行 JavaScript。如果你想了解更多关于Sammy.js
的信息,请前往[
sammyjs.org](http://sammyjs.org)
。
控制器的构造非常简单;它只是一个具有特权方法的对象,作为事件动作。控制器可以与应用中的特定实体相关联。在 MoMemo 中,有一个控制器管理电影,一个控制器管理您的收藏夹。
自举
bootstrap 是一个大对象,因为它处理将链接事件委托给应用的各个部分。它负责初始化所有的控制器,并成为从应用的其余部分访问控制器的一个地方。
首先用下面的代码在js/
中创建新文件bootstrap.js
。
`var app = app || {};
app.bootstrap = (function(){
})();`
首先声明一个名为_controller
的实例变量。
var app = app || {};
`app.bootstrap = (function(){
/**
* Create the controller object
* You explicitly declare the movies and favorites
* controllers
*/
var _controller = {
movies: null,
favorites: null
}
})();`
这将保存控制器对象。显式地创建它是一个好主意,这样您就知道哪些控制器应该存在于您的应用中。
下一步是为整个文档创建一个 click 事件侦听器。这将拾取任何点击,并检查被点击的元素是否是与触发控制器内的动作的请求的链接。
`var app = app || {};
app.bootstrap = (function(){
...
/**
* Add a click event listener over the entire document
* It will delegate clicks for controllers to the
* controller and action
*/
document.addEventListener("click", function(event){
});
})();`
当您点击屏幕上的一个项目时,事件监听器不一定会返回您点击的链接;它很可能会返回链接本身中的一个元素。因为您希望从链接本身获取数据,所以您需要遍历 DOM 树来获取链接并将其设置为目标,或者如果用户没有点击链接,则将目标设置为 null。
为此,您需要创建一个while
循环。这将把目标分配给当前目标的父目标,并逐步循环遍历 DOM 树。如果目标是一个链接,包含数据控制器属性,并且有一个数据动作属性,那么它将跳出while
循环并继续执行。如果 DOM 元素是 HTML 元素(在 DOM 树的顶端),它将跳出 while 循环,并将目标赋给一个空值,因为没有找到链接。
var app = app || {};
`app.bootstrap = (function(){
...
/**
* Add a click event listener over the entire document
* It will delegate clicks for controllers to the
* controller and action
*/
document.addEventListener("click", function(event){
var target = event.target;
/**
* Crawl up the DOM tree from the target element until
* the link surrounding the target element is found
*/
while(target.nodeName !== "A" && target.getAttribute('data-controller')
== null && target.getAttribute('data-action') == null){
// We've reached the body element break!
if(target.parentNode.nodeName == 'HTML'){
target = null;
break;
}
// Assign the target.paretNode to the target variable
target = target.parentNode;
}
});
})();`
如果你有一个目标,你现在需要调用控制器的动作并传递参数(如果有的话)给它。
`var app = app || {};
app.bootstrap = (function(){
...
/**
* If there's a target, then process the link action
*/
if(target){
/**
* You have the target link, so it makes sense to prevent the
* link from following through now.
* This will allow any JavaScript to fail silently!
*/
event.preventDefault();
}
...
})();`
首先要做的是防止浏览器处理该事件。这将防止浏览器试图加载一个死链接。然后,您需要从元素中获取控制器、动作和参数数据属性。
`var app = app || {};
app.bootstrap = (function(){
...
/**
* If there's a target, then process the link action
*/
if(target){
...
// Get the controller, action, and params from the element
var controller = target.getAttribute('data-controller'),
action = target.getAttribute('data-action'),
params = target.getAttribute('data-params');
}
...
})();`
然后,您需要验证控制器和操作是否存在。检查控制器是否存在于_controller
实例变量中,以及动作是否存在于控制器中,如下面的条件语句所示。
`var app = app || {};
app.bootstrap = (function(){
...
/**
* If there's a target, then process the link action
/
if(target){
...
/
* Check to see whether the controller exists in
* the bootstrap and the action is available
*/
if(typeof _controller[controller] === 'undefined'
|| typeof _controller[controller][action] === 'undefined'){
// If they don't exist, throw an exception
throw "Action " + action + " for controller " + controller + " doesn't
appear to exist";
return;
}
}
...
})();`
您可以像访问数组一样访问对象中的属性。除了传递一个索引,你还可以传递一个字符串或者一个变量。例如:
_controller["movie"]["show"]
和打电话一样
_controller.movie.show
除了您现在可以使用变量在第一个示例的括号内进行调用。
传递给控制器动作的参数需要从字符串转换成 JSON 对象。您可以使用JSON.parse()
方法来实现这一点,如下面的代码所示。
`var app = app || {};
app.bootstrap = (function(){
...
/**
* If there's a target, then process the link action
*/
if(target){
...
// Check to see whether the params exist
if(params){
try {
// If they do, then parse them as JSON
params = JSON.parse(params);
} catch (e) {
/*
* If there's a parsing exception, set the
* params to be null
*/
params = null;
return;
}
}
}
...
})();`
如果 JSON 验证失败,将抛出一个异常。您需要捕捉这个并将params
设置为空。
最后要做的是执行控制器动作。
`app.bootstrap = (function(){
...
/**
* If there's a target, then process the link action
/
if(target){
...
/*
* Execute the controller within the context of the target.
* This will allow you to access the original element from
* the controller action. Also pass the parameters from the
* data-params attribute.
*/
_controller[controller][action].call(target, params);
}
...
})();`
因为应用使用localStorage
来存储电影收藏夹,所以您需要初始化本地存储变量。
`app.bootstrap = (function(){
...
/**
* Set up the local storage by checking to see whether
* the favorites item exists
*/
if(!localStorage.getItem('favorites')){
// if it doesn't, create an empty array and assign it to the storage
var favorites = [];
localStorage.favorites = JSON.stringify(favorites);
}
})();`
如前所述,您需要创建访问控制器并初始化它们的方法。
getController
方法接受一个字符串参数,表示控制器的名称及其命名空间。它将使用点字符(fullstop)将字符串分成几部分,然后遍历各个名称空间,逐渐将它构建成一个对象。
`app.bootstrap = (function(){
...
return {
/**
* Create an accessor for the controller,
* which accepts a string representation of the
* controller's namespace
*/
getController: function(name){
/**
* Split the string into an array using the .
* character to separate the string
*/
var parts = name.split('.');
/**
* Initially set the returned controller to null
*/
var returnController = null;
/**
* If the number of parts is greater than 0
/
if(parts.length > 0){
/*
* Set the return controller to the parent object
/
returnController = _controller;
/*
* Loop through each part, gradually assigning the
* action to the return controller
*/
for(var i = 0; i < parts.length; i++){
returnController = returnController[parts[i]];
}
}
/**
* Return the controller
*/
return returnController;
},
/**
* Initializes all of the controllers. You might not want to do this
* automatically, so you can use the initScripts method to execute it.
*/
initScripts: function(){
_controller.movies = new app.controller.movies();
_controller.favorites = new app.controller.favorites();
_controller.favorites.list();
}
}
})();`
init
方法将简单地初始化所有的控制器并加载第一个控制器。
为了方便起见,下面显示了完整的引导程序。
`var app = app || {};
app.bootstrap = (function(){
/**
* Create the controller object
* You explicitly declare the movies and favorites
* controllers
*/
var _controller = {
movies: null,
favorites: null
}
/**
* Add a click event listener over the entire document
* It will delegate clicks for controllers to the
* controller and action
*/
document.addEventListener("click", function(event){
var target = event.target;
/**
* Crawl up the DOM tree from the target element until
* the link surrounding the target element is found
*/
while(target.nodeName !== "A" && target.getAttribute('data-controller')
== null && target.getAttribute('data-action') == null){
// We've reached the body element break!
if(target.parentNode.nodeName == 'HTML'){
target = null;
break;
}
// Assign the target.paretNode to the target variable
target = target.parentNode;
}
/**
* If there's a target, then process the link action
*/
if(target){
/**
* You have the target link, so it makes sense to prevent the link from
following through now.
* This will allow any JavaScript to fail silently!
*/
event.preventDefault();
// Get the controller, action, and params from the element
var controller = target.getAttribute('data-controller'),
action = target.getAttribute('data-action'),
params = target.getAttribute('data-params');
// Check to see whether the controller exists in the bootstrap and the
action is available
if(typeof _controller[controller] === 'undefined'
|| typeof _controller[controller][action] === 'undefined'){
// If they don't exist, throw an exception
throw "Action " + action + " for controller " + controller + " doesn't appear to exist";
return;
}
// Check to see whether the params exist
if(params){
try {
// If they do, then parse them as JSON
params = JSON.parse(params);
} catch (e) {
// If there's a parsing exception, set the params to be null
params = null;
return;
}
/**
* Execute the controller within the context of the target.
* This will allow you to access the original element from
* the controller action. Also pass the parameters from the
* data-params attribute.
*/
_controller[controller][action].call(target, params);
}
});
/**
* Set up the local storage by checking to see whether
* the favorites item exists
*/
if(!localStorage.getItem('favorites')){
// if it doesn't, create an empty array and assign it to the storage
var favorites = [];
localStorage.favorites = JSON.stringify(favorites);
}
return {
/**
* Create an accessor for the controller,
* which accepts a string representation of the
* controller's namespace
*/
getController: function(name){
/**
* Split the string into an array using the .
* character to separate the string
*/
var parts = name.split('.');
/**
* Initially set the returned controller to null
*/
var returnController = null;
/**
* If the number of parts is greater than 0
/
if(parts.length > 0){
/*
* Set the return controller to the parent object
/
returnController = _controller;
/*
* Loop through each part, gradually assigning the action to the
* return controller
/
for(var i = 0; i < parts.length; i++){
returnController = returnController[parts[i]];
}
}
/*
* Return the controller
*/
return returnController;
},
/**
* Initializes all of the controllers. You might not want to do this
* automatically, so you can use the initScripts method to execute it.
*/
initScripts: function(){
_controller.movies = new app.controller.movies();
_controller.favorites = new app.controller.favorites();
_controller.favorites.list();
}
}
})();`
电影控制器
电影控制器相当复杂,因为它通过烂番茄 API 处理电影搜索,并在用户通过 JSONP 输入时提供结果列表。
首先,用下面的代码在js/app/controller
中创建一个名为movies.js
的新控制器。
`var app = app || {};
app.controller = app.controller || {};
app.controller.movies = function(){
this.init = function(){}
this.init();
}`
所有控制器都包含一个在对象结束时执行的init
方法。
您将需要声明几个实例变量。
_searchfield
—包含搜索字段的 DOM 元素_searchform
—包含搜索表单的 DOM 元素_searchresultcard
—包含搜索结果卡的 DOM 元素_searchTimeout
—包含搜索超时定时器_viewScrolls
—包含视图的 iScroll 对象_searchScroll
—包含搜索结果的 iScroll 对象
使用以下代码声明这些实例变量。
`var app = app || {};
app.controller = app.controller || {};
app.controller.movies = function(){
var _self = this,
_searchfield = document.querySelector('#add-movie input[name="query"]'),
_searchform = document.getElementById('add-movie'),
_searchresultscard = document.getElementById('card-movie_search_results'),
_searchTimeout,
_viewScrolls = [],
_searchScroll = null;
this.init = function(){}
this.init();
}`
绑定搜索表单
您将创建的第一个方法会将事件侦听器绑定到搜索表单。
`var app = app || {};
app.controller = app.controller || {};
app.controller.movies = function(){
...
/**
* Binds the search form
*/
this.bindSearchForm = function(){
}
...
}`
当用户关注搜索字段时,如果文本框中有搜索查询,您会希望使用 deck 工具向用户显示当前结果。
`var app = app || {};
app.controller = app.controller || {};
app.controller.movies = function(){
...
/**
* Binds the search form
/
this.bindSearchForm = function(){
/*
* Here you add an event listener to the search filed using
* the focus event listener. If there's a value, then show the
* results.
*/
_searchfield.addEventListener('focus', function(){
if(this.value.length > 0){
app.utility.deck.showCard('card-movie_search_results');
}
});
}
...
}`
您需要绑定的下一个事件是表单的提交。这是为了防止用户意外地按下 Android 键盘上的 go 按钮并通过浏览器提交表单。相反,将执行搜索,应用将不再等待输入超时。
`var app = app || {};
app.controller = app.controller || {};
app.controller.movies = function(){
...
/**
* Add an event listener to the submission of the form.
* This will prevent the form from being submitted
* and sent to another page. Instead, we capture the
* event and trigger the search action.
*/
_searchform.addEventListener('submit', function(e){
e.preventDefault();
// Clear the _searchTimeout timeout
clearTimeout(_searchTimeout);
var value = _searchfield.value;
if(value.length > 0){
_self.search(value);
}
});
...
}`
最后,您需要将事件绑定到实际的输入字段。这将从字段中获取值,清除searchtimeout
(如果设置了的话),检查值的字符串长度是否大于零,然后设置一个超时来执行搜索。
`var app = app || {};
app.controller = app.controller || {};
app.controller.movies = function(){
...
_searchfield.addEventListener('input', function(){
/**
* This is the value of the input field
*/
var value = this.value;
/**
* This will clear the search timeout
*/
clearTimeout(_searchTimeout);
/**
* You don't want to run search straight after every
* key press. This will set a timeout of 1 second
* (1000 ms) before the search function is called.
*/
if(value.length > 0){
document.getElementById('taskbar').classList.add('searchactive');
} else {
document.getElementById('taskbar').classList.remove('searchactive');
}
_searchTimeout = setTimeout(function(){
_self.search(value);
}, 1000);
});
...
}`
对输入设置超时的原因是,用户一输入字母,请求就不会立即被发送。因为您不能取消 JSONP 请求,并且请求可能会以随机的顺序返回,所以最好通过设置一秒钟的计时器来避免同时发出大量请求的情况。
接下来可以看到 bind 方法的完整代码。
`/**
* Binds the search form
*/
this.bindSearchForm = function(){
/**
* Here you add an event listener to the search filed using
* the focus event listener. If there's a value, then show the
* results.
*/
_searchfield.addEventListener('focus', function(){
if(this.value.length > 0){
app.utility.deck.showCard('card-movie_search_results');
}
});
/**
* Add an event listener to the submission of the form.
* This will prevent the form from being submitted
* and sent to another page. Instead, we capture the
* event and trigger the search action.
*/
_searchform.addEventListener('submit', function(e){
e.preventDefault();
clearTimeout(_searchTimeout);
var value = _searchfield.value;
if(value.length > 0){
_self.search(value);
}
});
_searchfield.addEventListener('input', function(){
/**
* This is the value of the input field
*/
var value = this.value;
/**
* This will clear the search timeout
*/
clearTimeout(_searchTimeout);
/**
* You don't want to run search straight after every key press.
* This will set a timeout of 1 second (1000 ms) before the
* search function is called.
*/
if(value.length > 0){
document.getElementById('taskbar').classList.add('searchactive');
} else {
document.getElementById('taskbar').classList.remove('searchactive');
}
_searchTimeout = setTimeout(function(){
_self.search(value);
}, 1000);
});
}`
执行搜索
搜索操作将用于根据搜索值执行对烂番茄的 JSONP 请求。
`var app = app || {};
app.controller = app.controller || {};
app.controller.movies = function(){
...
this.search = function(query){
// Check to see whether the query length is longer than 0 characters
if(query.length > 0){
/*
* Encode the query so that it can be passed
* through the URL
*/
query = encodeURIComponent(query);
/**
* Create a new JSONP request
*/
var jsonp = new app.utility.jsonp(
'http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=
YOURAPIKEY&q=' + query, 'app.bootstrap.getController(
"movies").showSearchResults');
/**
* Send the request
*/
jsonp.send();
/**
* Add the loading class to the search field
*/
_searchfield.classList.add('loading');
}
}
...
}`
您需要用您的烂番茄 API 密钥替换 JSONP 请求 URI 中的YOURAPIKEY
。如您所见,您使用引导程序来获取电影控制器,并在结果加载后执行showSearchResults
。
显示结果
showSearchResults
动作/事件接受一个烂番茄结果集。
注意:如果您想查看烂番茄 API 结果集中的所有值,请查看位于[
developer.rottentomatoes.com/docs/read/json/v10/Movies_Search](http://developer.rottentomatoes.com/docs/read/json/v10/Movies_Search)
的 API 文档。
接下来,它将遍历结果,并根据数据创建要在应用中使用的电影模型。然后它将为结果创建视图,然后用结果 HTML 替换搜索结果div
的内容。
`var app = app || {};
app.controller = app.controller || {};
app.controller.movies = function(){
...
/**
* Shows the search results in the search results card
*/
this.showSearchResults = function(rtresults){
/**
* This is the Rotten Tomatoes API data.
* The following code will process the data
* returned and convert it to models
* that the application will understand.
* You could wrap these API calls into
* a separate library, but for now having
* them in the controller will suffice.
*/
// First, create an empty array to hold the results
var results = [];
// Next, loop through the results from Rotten Tomatoes
for(var i = 0; i < rtresults.movies.length; i++){
var rtmovie = rtresults.movies[i];
// For every result you create a new movie object
var title = rtmovie.title || '', rtid = rtmovie.id, posterframe =
rtmovie.posters.original || '', synopsis = rtmovie.synopsis || '';
results.push(new app.model.movie(title, rtid, posterframe, synopsis));
}
// Create the view using the data
var view = new app.view.movielist(results);
// Set the contents of the search results div
_searchresultscard.innerHTML = '';
_searchresultscard.appendChild(view.render());
// Controlling page needs to be handled by it's own utility or class
_searchresultscard.classList.add('active');
_searchfield.classList.remove('loading');
results = null;
// Check to see whether the search scroll is null
if(_searchScroll !== null){
// If it isn't, destroy it
_searchScroll.destroy();
_searchScroll = null;
}
// Initialize the search scroll for the results card
_searchScroll = new iScroll(_searchresultscard);
}
...
}`
如您所见,一个名为 iScroll 的 JavaScript 库处理滚动功能。蜂巢下面的安卓浏览器不支持溢出,隐藏在 CSS 中,所以用 iScroll 来方便这个。您需要下载最新版本的 iScroll,并将其放在js/lib/cubiq
文件夹中(如果该目录不存在,请创建它)。将文件命名为iscroll.js
。
观看电影
为了观看电影,您需要从烂番茄请求电影信息,然后用控制器中的事件处理程序处理结果。将以下代码添加到控制器中。
`var app = app || {};
app.controller = app.controller || {};
app.controller.movies = function(){
...
this.find = function(data){
// Check to see whether the ID exists in the action params/data
if(typeof data.id === 'undefined'){
throw "No ID supplied to find action in view controller";
return;
}
// Create a new JSONP request
var jsonp = new app.utility.jsonp(
'http://api.rottentomatoes.com/api/public/v1.0/movies/' +
data.id + '.json?apikey=YOURAPIKEY,
'app.bootstrap.getController("movies").view');
// Send the request
jsonp.send();
}
this.view = function(rtresult){
// Check to see whether an object has been returned
if(!app.utility.validator.isTypeOf(reresult, 'object')){
// If it's not an object, don't show the movie
return;
}
// Create a new movie object
var movie = new app.model.movie(rtresult.title,
rtresult.id, rtresult.posters.original, rtresult.synopsis),
// Get the movie info card
viewcard = document.getElementById('card-movie_info');
/**
* Set the DVD and cinema release dates
*/
var releaseDate = new app.type.releaseDate(
new Date(rtresult.release_dates.theater),
new Date(rtresult.release_dates.dvd));
movie.setReleaseDate(releaseDate);
/**
* Set the movie's rating
*/
movie.setRating(rtresult.mpaa_rating);
/**
* Check to see whether the movie is in the user's favorites
* by looping over the favorites localStorage object
*/
var _favorites = JSON.parse(localStorage.favorites);
for(var i = 0; i < _favorites.length; i++){
if(_favorites[i].id == movie.getRtid()){
/**
* If a match is found, set the
* favorite flag to true
*/
movie.setFavorite(true);
}
}
/**
* Add actors to the movie
*/
for(var i = 0; i < rtresult.abridged_cast.length; i++){
var cast = rtresult.abridged_cast[i],
character = (typeof cast.characters === 'undefined') ? '' :
cast.characters[0];
var actor = new app.model.actor(cast.name, character);
movie.addActor(actor);
}
// Create the movie view
var view = new app.view.movie(movie);
viewcard.innerHTML = view.render().innerHTML;
// Intialize iScroll
_viewScrolls.push(new iScroll(viewcard.querySelector('.movie-content'),
{vScroll: false, vScrollbar: false}));
[].forEach.call(viewcard.getElementsByClassName('block'), function(el){
_viewScrolls.push(new iScroll(el, {hScroll: false, hScrollbar:
false}));
});
/**
* Add an event listener to the window. If it resizes,
* reset the iScroll so that it adjusts to the new size.
*/
window.addEventListener('resize', function(){
setTimeout(function(){
_searchScroll.refresh();
for(var i = 0; i < _scrolls.length; i++){
_viewScrolls[i].refresh();
}
}, 100);
});
/**
* Hide all of the cards
*/
app.utility.deck.hideAllCards();
/**
* Show the movie info card
*/
app.utility.deck.showCard('card-movie_info');
}
...
}`
最后,您需要将this.bindSearchForm();
调用添加到init
方法中。
`var app = app || {};
app.controller = app.controller || {};
app.controller.movies = function(){
...
this.init = function(){
this.bindSearchForm();
}
...
this.init();
}`
收藏夹控制器
收藏夹控制器比电影控制器简单得多。它将简单地处理列出用户的收藏夹,在localStorage
中添加和删除用户收藏夹中的项目。
首先用下面的代码在js/app/controller
中创建新文件favorite.js
。
`var app = app || {};
app.controller = app.controller || {};
app.controller.favorites = function(){
var _listScroll = null;
this.init = function(){}
this.init();
}`
_listScroll
实例变量将保存用于收藏夹滚动的 iScroll 对象。
列出收藏夹
您需要创建的第一个动作/事件是list
动作。这将为用户列出所有收藏夹。
首先在收藏夹控制器中创建一个名为list
的新动作。
`var app = app || {};
app.controller = app.controller || {};
app.controller.favorites = function(){
...
this.list = function(){
}
...
}`
接下来你需要做的是从localStorage
中抓取收藏夹。正如您在引导文件中看到的,您用一个空数组创建了一个空的 favorites localStorage
属性。这将允许您获取应用的收藏夹,而不必检查该属性是否存在,然后在整个代码中创建它。创建一个localStorage
工具来存储你的数据可能是个好主意;这将不在本书中讨论,因为您将只在应用的三个地方存储和检索localStorage
属性。
`var app = app || {};
app.controller = app.controller || {};
app.controller.favorites = function(){
...
this.list = function(){
// Get the favorites from local storage
var _favorites = JSON.parse(localStorage.favorites),
// Create an empty movies variable
_movies = [],
// Get the favoritesList card from the DOM
_favoriteslist = document.getElementById('card-favorite_list');
}
...
}`
从代码中可以看出,您还创建了一个空的 movies 数组。这将用于将从用户收藏夹创建的所有电影模型保存到代码中。您还将获得最喜爱的卡片列表 DOM 元素。
接下来要做的是遍历从localStorage
中检索到的每个收藏夹。for
循环可能看起来有点奇怪,因为每次运行for
循环时,迭代器都会减去一个值。这实质上是反转数组,所以最后/最近的元素首先循环。
`var app = app || {};
app.controller = app.controller || {};
app.controller.favorites = function(){
...
this.list = function(){
...
/**
* Loop through each of the favorites backward
* to ensure that the most recent favorite
* is displayed at the top of the list
*/
for(var i = _favorites.length; i > 0; i--){
var _favorite = _favorites[i - 1];
// Push the movie model to the movies array
_movies.push(new app.model.movie(unescape(_favorite.title),
_favorite.id, _favorite.posterframe, unescape(_favorite.synopsis)))
}
...
}
...
}`
从代码中可以看出,unescape
方法用于对每个收藏夹属性所用的字符进行转义。这是因为add
方法对每个属性值进行了转义,以便将对象存储在本地存储中。
下一步是从电影数组中创建一个电影视图。
`var app = app || {};
app.controller = app.controller || {};
app.controller.favorites = function(){
...
this.list = function(){
...
/**
* Create a new movielist view with the _movies model
*/
var view = new app.view.movielist(_movies);
// Set the contents of the search results div
_favoriteslist.innerHTML = '';
// Append the view to the favorites list
_favoriteslist.appendChild(view.render());
...
}
...
}`
最后,您需要为视图创建一个新的 iScroll 对象并显示它。这将允许比 Honeycomb 更老的设备上的用户能够滚动长列表。
`var app = app || {};
app.controller = app.controller || {};
app.controller.favorites = function(){
...
this.list = function(){
...
// Destroy the listScroll if it exists
if(_listScroll !== null){
_listScroll.destroy();
_listScroll = null;
}
// Create a new one
_listScroll = new iScroll(_favoriteslist);
// Hide all of the cards
app.utility.deck.hideAllCards();
// Show only the favorites card
app.utility.deck.showCard('card-favorite_list');
...
}
...
}`
您的最终列表方法应该类似于下面的代码。
`var app = app || {};
app.controller = app.controller || {};
app.controller.favorites = function(){
...
this.list = function(){
// Get the favorites from local storage
var _favorites = JSON.parse(localStorage.favorites),
// Create an empty movies variable
_movies = [],
// Get the favoritesList card from the DOM
_favoriteslist = document.getElementById('card-favorite_list');
/**
* Loop through each of the favorites backward
* to ensure that the most recent favorite
* is displayed at the top of the list
*/
for(var i = _favorites.length; i > 0; i--){
var _favorite = _favorites[i - 1];
// Push the movie model to the movies array
_movies.push(new app.model.movie(unescape(_favorite.title),
_favorite.id, _favorite.posterframe, unescape(_favorite.synopsis)))
}
/**
* Create a new movielist view with the _movies model
*/
var view = new app.view.movielist(_movies);
// Set the contents of the search results div
_favoriteslist.innerHTML = '';
// Append the view to the favorites list
_favoriteslist.appendChild(view.render());
// Destroy the listScroll if it exists
if(_listScroll !== null){
_listScroll.destroy();
_listScroll = null;
}
// Create a new one
_listScroll = new iScroll(_favoriteslist);
// Hide all of the cards
app.utility.deck.hideAllCards();
// Show only the favorites card
app.utility.deck.showCard('card-favorite_list');
}
...
}`
添加收藏夹
完成电影列表后,您现在需要一个操作来将收藏夹添加到列表中。这是一个非常简单的动作,因为它将从localStorage
开始遍历用户的收藏夹,以检查它是否存在。如果没有,它将继续将电影添加到用户的收藏夹,并更改调用它的按钮的状态,这样用户就可以将电影从收藏夹中删除,而不必刷新页面。
`var app = app || {};
app.controller = app.controller || {};
app.controller.favorites = function(){
...
this.add = function(data){
// Get the movie data
var _movie = data;
// Load the favorites from localStorage
var _favorites = JSON.parse(localStorage.favorites);
/**
* Check to see whether the movie
* is already in the user's favorites
*/
for(var i = 0; i < _favorites.length; i++){
if(_favorites[i].id == _movie.id){
return;
}
}
/**
* Change the button’s attributes
*/
if(this.nodeName == 'A'){
this.setAttribute('data-action', 'remove');
this.classList.remove('add');
this.classList.add('remove');
this.textContent = 'un-favorite';
}
// Push the movie to the favorites array
_favorites.push(_movie);
// Save it back to localStorage
localStorage.favorites = JSON.stringify(_favorites);
}
...
}`
移除收藏夹
现在添加已经就绪,您将需要编写从用户的收藏夹中删除项目的操作。这是一个非常简单的过程,方法看起来类似于add
动作。不同之处在于,当你循环检查一个收藏夹是否已经存在时,如果该收藏夹存在于数组中,你将删除它。代码可以在这里看到。
`var app = app || {};
app.controller = app.controller || {};
app.controller.favorites = function(){
...
this.remove = function(data){
// Get the ID of the favorite to remove
var _id = data.id;
// Get the user's favorites from localStorage
var _favorites = JSON.parse(localStorage.favorites);
// Loop through the favorites
for(var i = 0; i < _favorites.length; i++){
// If there's a match
if(_favorites[i].id == _id){
// Remove the item from the favorites using splice
_favorites.splice(i, 1);
}
}
// Save the changed favorites object back to localStorage
localStorage.favorites = JSON.stringify(_favorites);
/**
* Change the add/remove favorites button
* so that it will either add/remote the item
* from the favorites
*/
if(this.nodeName == 'A'){
this.setAttribute('data-action', 'add');
this.classList.remove('remove');
this.classList.add('add');
this.textContent = 'favorite';
}
}
...
}`
设计内容的样式
即使你为 JavaScript 框架付出了这么多努力,MoMemo 看起来还是很难看。在第六章中,我们提到了使用 SASS 来创建一个基本的工作框架。我们现在将把它付诸实践,进一步设计应用的各种元素。
我们将从设计电影列表开始,因为这是用户将与之交互的第一件事。
设计电影列表
MoMemo 应用中有两个地方会用到电影列表。其中一个地方将出现在搜索结果中,第二个将出现在主屏幕的收藏夹列表中。由于有两个列表,您可能希望对这些进行不同的样式化。我们可以分解列表的公共样式,比如每个列表项的大小和它的公共子元素在“全局”列表类中的位置,这些可以使用特殊性来覆盖。
首先,为列表创建全局样式。
`.list {
margin: 0;
padding: 0;
li {
padding: 10px;
overflow: hidden;
height: 82px;
display: block;
border-bottom: 1px solid #CCCCCC;
background: #FFFFFF;
.preview-image {
float: left;
width: 60px;
height: 82px;
text-align: center;
margin-right: 10px;
}
}
}`
.list
样式将为所有要被样式化的列表提供一个框架。如果您知道某个特定的设计组件可能会多次使用,但看起来不一定相同,那么这是设计 web 应用的好方法。您可以只创建一个电影列表样式,然后使用特殊性来覆盖它,但是在您的新样式中可能有您不想要的某些 CSS 样式,这将意味着需要额外的代码来重置它们。将前面代码片段中的 SASS 样式添加到css/partials/_layout.scss
SASS 文件中 deck 样式的正下方。
重新编译 SASS 文件,并在手机浏览器中重新加载 web 应用,然后搜索电影。结果应该看起来像图 8-3 。
图 8-3。 香草搜索结果
接下来要做的是设计电影列表的样式,使它看起来更好一些。在电影列表的视图中,您可以看到有多个类被分配给它,如下所示。
_rootElement.classList.add('list'); _rootElement.classList.add('movie-list');
这些提供了 CSS 样式可以锁定的基本钩子。下面的 SASS/CSS 样式将对每个列表项中的附加内容进行样式化,比如截断大纲,使其不会超出列表项的高度。
`.movie-list {
li {
background: #A5CCEB;
border-bottom-colour: #FFFFFF;
.more {
display: block;
height: 100%;
overflow: hidden;
text-decoration: none;
h2 {
margin: 0 0 10px;
color: #BF2628;
}
p {
margin: 0;
color: #000000;
}
}
}
li:nth-child(odd) {
background: #97B2D9;
}
}`
将前面的代码添加到 SASS 文件中的.list
样式下。如果你把它放在.list
样式之前,你想要覆盖的样式将被放置在.movie-list
样式下面的样式覆盖,在这个例子中是.list
。如你所见,有一个li:nth-child(odd)
样式,它将样式化每一个奇怪的列表项。这将使每个奇数列表项的背景颜色不同,以帮助用户区分列表中的不同项,并使他们更容易找到点击的位置。
您的最终movie-list
和list
SASS 应该类似于下面的代码。
`/**
* Standard list
*/
.list {
margin: 0;
padding: 0;
li {
padding: 10px;
overflow: hidden;
height: 82px;
display: block;
border-bottom: 1px solid #CCCCCC;
.preview-image {
float: left;
width: 60px;
height: 82px;
text-align: center;
margin-right: 10px;
}
}
}
/**
* Movie list
*/
.movie-list {
li {
background: #A5CCEB;
border-bottom-color: #FFFFFF;
.more {
display: block;
height: 100%;
overflow: hidden;
text-decoration: none;
h2 {
margin: 0 0 10px;
color: #BF2628;
}
p {
margin: 0;
color: #000000;
}
}
}
li:nth-child(odd) {
background: #97B2D9;
}
}`
准备就绪后,重新加载您的移动浏览器并执行另一次搜索。结果视图将类似于图 8-4 。现在,您可以开始设置电影视图的样式了。
图 8-4。 最终搜索结果查看
设定电影视图的样式
电影视图与电影列表视图略有不同,因为它稍微复杂一些。这个想法是你可以使用 iScroll 来侧滚浏览内容。当内容对于视窗的尺寸来说太长时,用户可以向下滚动。
为了让用户理解有更多的内容,每个内容块的宽度必须小于屏幕的大小,因此下一个内容元素从左侧或右侧突出一点。
我们还将使用 CSS 使海报图像在标题中具有动画效果。这将使视图更有趣一点。
让我们从设计标题开始。在movie-list
样式下声明一个新的样式,如下面的代码所示。
`.movie-header {
position: relative;
overflow: hidden;
height: 20%;
}`
这段代码将相对于它的父元素定位电影标题,任何绝对定位的元素都将包含在.movie-header
中。height
被设置为20%
,这将确保它在平板设备上看起来和在移动设备上一样大。overflow
已经被设置为hidden
以防止海报在元素之外可见,因为它相当大。
在电影风格中,我们可以开始设计海报。在.movie-header
样式中创建一个新的样式,如下面的代码所示。
`.movie-header {
...
.poster {
position: absolute;
top: 0%;
@include animation(posteranimation 10s ease 0 infinite alternate);
}
}`
这将把海报图像放在元素的顶部。你可以看到它附带了一个动画。我们一会儿会谈到这一点。现在,是时候对电影标题元素进行样式化了。在.movie-header
样式中添加以下代码。
`.movie-header {
...
.movie-title {
position: absolute;
bottom: 0px;
background: rgba(255, 255, 255, 0.75);
padding: 5px;
bottom: 0;
left: 0;
width: 100%;
@include box-sizing(border-box);
}
}`
这将使电影标题位于电影标题的bottom
处。它将占据页眉的width
的100%
,并具有略微透明的白色背景色,以便即使在深色海报图像上也能看到文本。你可以看到我们还使用了box-sizing
技巧来确保填充不会影响元素的指定宽度。
在.movie-title
中,您还需要设计收藏夹按钮的样式。这可以使用下面突出显示的代码来完成。
.movie-header { ... .movie-title {
` ...
.btn-favorite {
float: right;
padding: 10px;
color: #FFFFFF;
background: #7D9DCE;
font-weight: bold;
border-radius: 5px;
text-decoration: none;
border: 1px solid #A5CCEB;
}
}
}`
这将创建一个蓝色按钮,浮动在电影标题的右边。你可能还想稍微修改一下上映日期,这样它就能从电影的标题中脱颖而出。接下来您可以看到新增内容。
`.movie-header {
...
.movie-title {
...
.movie-release-date {
text-transform: uppercase;
font-weight: bold;
}
}
}`
完整的电影标题样式应该类似于下面的代码。
`.movie-header {
position: relative;
overflow: hidden;
height: 20%;
.poster {
position: absolute;
top: 0%;
@include animation(posteranimation 10s ease 0 infinite alternate);
}
.movie-title {
position: absolute;
bottom: 0px;
background: rgba(255, 255, 255, 0.75);
padding: 5px;
bottom: 0;
left: 0;
width: 100%;
@include box-sizing(border-box);
.btn-favorite {
float: right;
padding: 10px;
color: #FFFFFF;
background: #7D9DCE;
font-weight: bold;
border-radius: 5px;
text-decoration: none;
border: 1px solid #A5CCEB;
}
.movie-release-date {
text-transform: uppercase;
font-weight: bold;
}
}
}`
刷新您的移动浏览器,通过搜索电影并录制来观看。它现在应该看起来像图 8-5 。
图 8-5。 电影片头
下一个任务是为电影本身设计实际内容的样式。因为这些块相对来说是相同的,所以实现这一点不需要太多代码。
让我们从设计.movie-content
元素开始。
.movie-content { height: 80%; width: 100%; padding-bottom: 40px; @include box-sizing(border-box); }
这里没什么特别可看的。我们只是将height
设置为屏幕高度的80%
,以适应电影标题 20%的高度。
还有一个40px
的padding-bottom
允许电影页脚位于底部,后面没有内容出现。
容纳所有块元素的块容器需要是屏幕的宽度×元素的数量——每个块元素的宽度之差。这在 SASS 中很容易做到,因为您可以创建一个变量来保存每个 block 元素的宽度,然后创建一个等式来设置容器元素的宽度,如下所示。
$blockWidth: 33%; $blocks: 3; ... .block-container { width: (100% * $blocks) - (100% - 33%); ...
我们现在可以设置块及其内容的样式。这部分真的很简单,唯一复杂的事情是确保块的宽度是根据先前设置的变量设置的。将以下代码添加到movie-content
样式中。
`.movie-content {
...
.block-container {
blocks: 3;
width: (100% * $blocks) - (100% - 33%);
height: 100%;
.block {
width: 33%;
float: left;
height: 100%;
font-size: 1.3em;
line-height: 2em;
.content {
@include box-sizing(border-box);
}
h3 {
padding: 10px 10px 0 10px;
}
.content {
padding: 10px;
}
}
}
}`
刷新你的浏览器,你的网络应用应该看起来像图 8-6 。
图 8-6。 电影大片造型
您需要为标题动画创建关键帧。所有这些只是反复上下移动海报图像。我们使用百分比,以便根据屏幕大小,图像将按比例移动。将以下代码添加到您的 SASS 文件中。
@keyframes posteranimation { 0% { top: 0%; } 100% { top: -80%; } }
`@-moz-keyframes posteranimation {
0% { top: 0%; }
100% { top: -80%; }
}
@-webkit-keyframes posteranimation {
0% { top: 0%; }
100% { top: -80%; }
}`
最后但同样重要的是,我们需要设计电影页脚的样式,这是一个非常简单的样式。它会将页脚定位在视图的底部。页脚也可以包含一个后退按钮,需要用一个图像的风格。将附录中的清单 A-2 中的代码放在 SASS 文件的末尾。
你最终的电影视图应该看起来像图 8-7 。
图 8-7。 最终电影信息页面
把所有这些放在一起
准备好所有的 JavaScript 和 SASS 文件后,现在是时候更新 HTML 以利用所有的新代码了。
这是一个非常简单的过程。打开index.html
文件,将下面的代码添加到底部,就在结束 body 标签之前。
... <!-- Load all of the JavaScript dependencies -->
`
你还需要从谷歌的字体目录中加载一种新字体。这是一个足够简单的任务。在<head />
标签中,在mobile.css
链接声明之后添加以下代码。
`
...`
重新加载您的移动网络浏览器,一切都应该像预期的那样工作。
连接、缩小和缓存
尽管拥有多个 JavaScript 文件对于开发和调试非常有用,但是在生产过程中向用户发送每个 JavaScript 文件并不是一个好主意,因为这可能会在应用中造成加载瓶颈。为了解决这个问题并获得最佳性能,最好将所有 JavaScript 文件连接成一个 JS 文件,就像我们使用 SASS 处理 CSS 文件一样。
为了提高性能,您还可以缩小 JavaScript 和 CSS 文件。这是一个删除尽可能多的未使用数据(如空格和回车)以创建一个更紧凑的文件的过程。
为了进一步提高性能,您需要更新缓存的清单文件。这将允许您的应用在用户的移动设备上存储 JavaScript 和图像,减少了每次加载页面时不断从服务器获取它们的需要。这也允许用户在没有连接到网络或服务器的情况下使用 web 应用的一部分。
串联
可以通过几种方式连接 JavaScript。最流行的是使用服务器端脚本自动合并所有文件,并将最终结果缓存在服务器上。最不流行的方法是通过将所有文件复制到一个 JavaScript 文件中来手动合并所有文件。本节将介绍如何手动连接应用的 JavaScript 文件。
在js
目录下创建一个名为app.dev.js
的新文件。首先,按照以下顺序将代码从应用的 JavaScript 文件复制并粘贴到app.dev.js
中,列表顶部的 JavaScript 文件出现在文件的顶部。
js/lib/eligrey/classlist.js
js/lib/cubiq/iscroll.js
js/app/utility/validator.js
js/app/utility/layout.js
js/app/utility/deck.js
js/app/utility/jsonp.js
js/app/type/format.js
js/app/type/releaseDate.js
js/app/model/actor.js
js/app/model/movie.js
js/app/model/video.js
js/app/model/videosource.js
js/app/view/movie.js
js/app/view/movielistitem.js
js/app/view/movielist.js
js/app/controller/movies.js
js/app/controller/favorites.js
js/app/bootstrap.js
保存文件并从index.html
底部删除当前的 JavaScript 文件列表。
下面的代码片段显示了这应该是什么样子。
` ...
如果你刷新你的手机浏览器,一切都应该正常工作。
缩小
缩小/缩小是从代码中删除尽可能多的空白和注释的过程。这听起来很傻,但是额外的数据会占 JavaScript 文件大小的很大一部分。
在 Aptana 中右键单击app.dev.js
文件并选择 Properties,查看其大小。它的重量应该在 54,000 字节左右,大约是 53KB。您可以通过缩小脚本来运行文件,从而进一步减小生产文件的大小。
就像连接一样,你也可以在服务器端自动缩小你的 JavaScript 或者使用 YUI 压缩器。对于本书中的例子,您将使用在[
jscompress.com](http://jscompress.com)
找到的在线 JavaScript 压缩工具。
在js/
中创建一个名为app.min.js
的新文件。这将包含您的生产就绪的精简代码。从app.dev.js
复制代码,粘贴到[
jscompress.com](http://jscompress.com)
的“Javascript 代码输入”文本框中。然后按下“压缩 Javascript”按钮。
复制压缩输出并粘贴到app.min.js
中。保存文件,然后在应用浏览器中右键单击该文件并选择属性。您应该会看到文件大小大大减小,从大约 54,000 字节减少到大约 24,318 字节。这意味着文件大小减少了大约一半。
您还可以利用- style compress 选项,使用 SASS 来压缩您的 CSS 文件。为此,从应用文件夹打开终端并输入以下命令。
sass ./css/*.scss ./css/mobile.min.css --style compress
这将输出一个缩小版本的 CSS 文件到 CSS 目录。要使用它,将index.html
头部 CSS 样式表的href
从mobile.css
改为mobile.min.css
。您的新头部现在应该看起来像下面的代码。
`
`
缓存
所有文件保存技术都可以完美地减少应用在每次页面加载时占用的带宽。现在,如果用户只需要请求那些千载难逢才发生一次变化的素材,这不是很好吗?您可以使用缓存清单来做到这一点。
我们在第四章中简要提到了这一点。缓存可能是有用的,但是当你需要清除它的时候也可能是痛苦的。幸运的是,应用缓存有一个 JavaScript API,允许您动态清除它。
您不仅可以缓存来自 web 应用的文件,还可以缓存从外部网站获取的文件,例如用户最喜欢的电影图像。但是,请注意,在某些设备上,应用的缓存可能会被限制在一定的大小。
打开您在第四章中创建的momemo.cache
文件。我们知道应用文件(比如图像、JavaScript 文件和 CSS)需要缓存,所以更新momemo.cache
以便缓存下面的文件。
index.html
css/mobile.min.css
js/app.min.css
img/
您的清单文件现在应该类似于下面的代码。
`CACHE MANIFEST
We'll make these files cachable
CACHE:
index.html
css/mobile.min.css
js/app.min.js
img/momemo.png
img/momemo.png
img/back.png
img/clear.png
img/loading.gif
img/search.png`
可以看到,每个需要缓存的文件都被明确指定了。不幸的是,您不能使用通配符,因为浏览器缓存将在页面加载之前缓存清单文件中的所有文件。它不知道哪些文件存在于您的服务器上,所以使用通配符(*)不会有任何效果。
我们缓存缩小的 CSS 和 JS 文件,而不是没有缩小的 JavaScript 和 CSS 文件。这将防止在您需要为开发而更改文件时遇到挫折,这样您就不必手动更改清单文件来引入已更改的文件。
使用缓存清单,您还可以指定哪些文件需要网络连接。我们当然希望烂番茄空气污染指数有最新的数据。为此,您必须将文件的 URL 或位置放在清单文件中的网络定义下。您的新缓存文件应该如下所示。
`CACHE MANIFEST
We'll make these files available offline
CACHE:
index.html
css/mobile.min.css
js/app.min.js
img/momemo.png
img/momemo.png
img/back.png
img/clear.png
img/loading.gif
img/search.png
These files require a network connection
NETWORK:
http://api.rottentomatoes.com/`
如果你缓存一个文件,你必须记住它必须存在于服务器上;否则,应用缓存不会缓存您的任何文件。
要重新加载缓存,只需更改/修改缓存文件。
Android 版 Chrome 的调试
如果你有创建网站的经验,你就会知道在 IE6 中调试任何东西是多么令人沮丧。没有 JavaScript 控制台、DOM 检查器、分析器等等。到目前为止,对于移动设备来说也是如此,因为还没有一种本地方法可以轻松地运行和调试移动 web 应用。最后,Chrome for Android 引入了一种聪明的方法来调试你的移动网络应用,就像你的桌面网络应用一样。
使用 Chrome for Android,您可以从控制台启动远程调试会话,并使用电脑上的 web inspector 与 Android 设备上的网页进行交互。不幸的是,这仅适用于 Android 4+(冰淇淋三明治),因为 Chrome 仅支持该版本的 Android。
要做到这一点,去你手机上的 Play Store,下载免费的谷歌 Chrome 浏览器。请记住,只有在 Android 4+上才能找到该应用。
你需要启用 Web 调试,因此在应用中转至设置开发人员工具启用 USB Web 调试。
将您的 Android 设备插入电脑,并在 Aptana 中启动终端。导航到 Android SDK 目录,它通常在~/android-sdks/platform-tools/
中。运行以下命令。
./adb forward tcp:9222 localabstract:chrome_devtools_remote
这是一个端口转发,将允许您从计算机的浏览器访问 Chrome 检查器。
在 Chrome for Android 中打开任何网页,并在您的桌面浏览器中转至以下 URL:localhost:9222。你会看到一个类似于图 8-8 的屏幕。
图 8-8。 Chrome 页面调试选择
选择突出显示的页面,将出现类似于图 8-9 的屏幕。
图 8-9。 调试控制台
如果您现在选择调试控制台中的某个元素或将鼠标悬停在该元素上,并查看您的移动设备,该元素应该会高亮显示。你可以看到它的大小和属性,如图图 8-10 所示。您也可以双击 CSS 规则来更改它们的值,它们将出现在设备上。
图 8-10。 高亮元素
你甚至可以调出 JavaScript 控制台,在手机上键入将直接影响页面的 JavaScript 代码,如图图 8-11 和图 8-12 所示。
图 8-11。 JavaScript 控制台
图 8-12。 Chrome 警报
这是一个非常好的工具。如果你想快速调试或者测试你的 CSS 的变化,直接在浏览器中调整它们要快得多,而不是不断地保存和重新加载你的移动网络应用。
注:由于 Android 版 Chrome 仍处于测试阶段,你最终调试的方式可能会改变。要获得最新的说明,请前往[
developers.google.com/chrome/mobile/docs/debugging](https://developers.google.com/chrome/mobile/docs/debugging)
。
总结
这是一个非常深入的章节,讲述了在过程语言的世界之外,使用 JavaScript 可以真正实现什么。
您现在应该已经真正掌握了 MVC,以及如何使用它来提供一个坚实的工作框架。把它背后的原则带在身边真的很重要,因为这将帮助你进一步理解其他 JavaScript 框架和设计模式,它们可以让你的生活变得更加轻松。
本章还向您介绍了如何在工具对象中将代码组合在一起,以减少代码重复。
您还应该对 JavaScript 对象和范围有更好的理解,以及如何利用这一点。
九、测试和部署您的移动 Web 应用
信不信由你,测试和部署你的移动 web 应用是开发周期中最重要的,但却被忽视的方面之一。
最基本的测试和部署方法是使用 FTP(文件传输协议)客户端将您的移动 web 应用上传到面向公众的 web 服务器。通常情况下,你可以在手机的网络浏览器中测试上传的应用,通过“玩”来确保一切正常。如果有任何问题,那么您在本地机器上进行修改,测试它,然后重新加载更改后的文件。
这适用于小型应用和非常小的团队;然而,随着您的应用和开发团队的成长,彻底测试功能的每个方面,并跟踪代码更改,变得非常耗时。
自然的“快速修复”进展是开始使用文件共享软件或服务,如 AFP(苹果文件共享协议)、Samba 或 Dropbox 来管理组代码,并在开发团队之间共享项目。这最终会变得很麻烦,熟悉这种技术的人都很清楚——文件冲突并不罕见,解决它们极其困难和费力。也没有代码所有权或责备方法。如果开发人员破坏了一部分功能,没有人可以指责,代码需要更长的时间来修复或回滚。
持续集成原则有助于解决这个问题。持续集成包括 SCM(源代码控制管理)来管理团队中的代码、自动化测试、环境测试和自动化部署。这些原则中的一些可能看起来有些陌生,但将在本章中得到全面阐述。没有一个项目或开发团队太大或太小而不能利用持续集成。
本章首先解释这些持续集成元素是什么。然后,您将得到一个实践练习,展示如何创建单元测试,如何使用 SCM 系统 Git,以及如何使用 Capistrano 将您的应用部署到生产服务器上。
源代码管理
源代码控制管理是持续集成的核心。到目前为止,有几种 SCM 实现,包括以下几种:
- 饭桶
- SVN(阿帕奇颠覆)
- 水银的
SCM 提供了一种存储源文件版本的方法。SCM 通过在初始提交/保存时存储原始文件来做到这一点。然后,SCM 在每次后续提交时只存储每个文件之间的变化/差异。这样可以节省磁盘空间和带宽,因为除非文件是新的,否则每次提交时不会保存整个文件。
当您使用 SCM 存储项目时,它们仍然可以从您的计算机上访问,就像任何其他文件一样。不同之处在于,对于基于 SCM 的项目,您可以提交任何文件更改(包括图像、视频等)。)以便在需要时可以对它们进行版本控制和恢复或比较。
要使用 SCM 提交变更,您需要一个 SCM 客户端,比如 Git 或 SVN。配置管理系统通常也会存储额外的文件作为你项目的一部分。在使用过 Git 和 SVN 之后,Git 是我首选的 SCM,因为它在根目录中只存储了一个可以轻松删除的文件夹;相比之下,SVN 会在你的项目的每个文件夹中存储.svn
文件夹,这可能会被证明是一个痛苦的删除。
目前有两种类型的供应链管理:集中式和分布式。集中式系统将所有代码存储在中央服务器上。当开发人员提交时,更改在服务器上合并,而不是在开发机器上。
SVN 是一个集中的供应链管理系统。分布式系统没有中央服务器。提交可以在任何开发人员的机器上进行。如果一个开发人员想要共享他或她的代码,另一个开发人员可以将存储库克隆到他们的机器上,这将包含来自原始存储库的每一个变更。
Git 是一个分布式供应链管理系统。通常情况下,Git 存储库会有一个主要的远程存储库,所有的开发人员都可以在其中进行修改。Git 等系统的优势在于,您可以在没有网络连接的情况下处理项目,但仍然可以在处理项目时将更改提交到您的本地存储库。一旦您有了网络访问权,您就可以提取并合并任何变更,测试它们,然后将您合并的变更推送到远程存储库。如果您有一个中央构建系统,这尤其有用。
SCM 还为您提供了在每次提交时存储注释的能力,这样当您浏览项目提交历史时,您可以看到谁提交了对哪些文件的更改以及每次提交背后的原因。像 Git 这样的配置管理系统默认需要提交注释,而 SVN 不需要。Git 在开发社区中非常受欢迎,这也是它成为本章重点的原因。
分支和标记
大多数 SCM 系统遵循分支和标记的方法。Git 中的 master 分支是主要的代码库;这通常是生产就绪的,包含项目代码的最新版本。当需要添加新特性时,通常会从主分支创建一个分支。这些通常都有代号;一些开发团队使用流行卡通节目中的名字,如 Peter、Meg 或 Stewie,而另一些使用行星名,如 Saturn、Jupiter 或 Mars。您可以使用任何您喜欢的命名约定,只要每个分支都有一个唯一的名称。重要的是创建分支时所做的注释。必须明确分支是干什么的,应该包含什么内容。
分支只是主分支的副本或快照,因此任何新特性都不会干扰生产就绪代码。通常,主分支会定期与新分支合并。这是为了确保对主代码库的任何更改都与分支中的功能更改兼容。在新分支中的特性被实现和测试之后,它将与主分支合并,为生产做好准备。
标签只是您希望保留以供将来参考的项目主分支的重要快照。这些通常是项目的重要版本。如果其他开发人员经常在主分支上工作,而您想要进行工作部署,这可能会很有用。
测试
对于新的 web 开发人员来说,测试您的移动 web 应用是开发周期中最容易被忽视的方面之一。大多数新的移动网站开发者将简单地把他们的网站加载到移动设备上,然后玩玩看它是否能工作。随着您添加更多功能,这可能会很费力,而且您真的不知道代码内部发生了什么。随着您开始编写更多面向对象的代码,您开始看到应用的复杂性在增长(以一种有组织的方式)。您可以基于您输入的内容和您期望得到的内容来测试每个代码单元。
例如,在前面的章节中,您提到了在应用中创建模型来存储数据。表示的完整性和应用背后的逻辑实际上依赖于这些模型如何通过 getters、setters 和其他基于模型的方法接受和输出数据。你可以写一套测试,基于每一个代码单元,包括你输入的内容和你期望得到的回报。这种测试方法被称为单元测试。单元测试允许您测试应用中的每个方法。您为项目的每个方面编写的单元测试越多,您就应该对应用的工作越有信心。这就是所谓的代码覆盖率。请记住,单元测试将只覆盖一定比例的代码,您的目标应该是至少 80%的代码覆盖率。
通过创建单元测试,您可以一次运行一系列针对每个目标 web 浏览器的测试。这应该给你信心,你的代码工作正常。这在发布新的浏览器或浏览器版本时尤其有用,因为您可以通过在新浏览器中运行单元测试来确保您的 JavaScript 代码与新浏览器兼容。
部署您的应用
您的应用可以通过多种方式部署。最常见的方法是通过 FTP(文件传输协议)或 SFTP(安全文件传输协议)进行部署。通常情况下,您会有一个存放生产代码的生产服务器,一个测试最新集成代码的开发服务器,以及一个本地开发服务器。您可能还想创建其他环境,例如预生产服务器,它将模拟生产服务器的确切配置,并且您可能想创建一个临时服务器,在将最终代码投入生产之前,客户端或测试人员将在其中测试您的最终代码。
管理所有这些环境及其代码库可能会有问题,并且每次提交到几个环境时手动部署代码更改可能会变得费力,并且容易出现人为错误。您可能还必须为每个环境执行任务,比如 CSS 预编译、JavaScript 和 CSS 缩小和连接,等等。
您可以将大部分工作交给部署应用,比如 Capistrano。Capistrano 将允许您为每个环境编写部署脚本,并使用一个命令将您的应用部署到任何环境。Capistrano 还允许您将任何更改回滚到以前的工作版本,对于每个部署,Capistrano 将存储每个版本的副本,以便您可以随时回滚。
持续集成服务器
将所有这些应用和实践结合在一起的粘合剂是一个良好的持续集成环境。持续集成环境将检测应用代码中的变化,并自动构建和部署它,以及执行其他任务。这意味着您可以专注于开发世界级的 web 应用,而将重复的部署和测试任务留给持续集成服务器。
本书选择的持续集成服务器是 Atlassian 的 Bamboo。选择这个产品是因为它易于安装,有许多插件,易于设置,并且与 Atlassian 的其他流行软件开发工具兼容,如 JIRA、Crucible 和 FishEye。
您的第一个持续集成项目
第一次创建一个持续的集成项目可能是相当费力的。您将首先在 Aptana Studio 3 中创建一个新项目。为此,打开 Aptana Studio,进入文件 新建
** Web 项目**。将项目命名为
ci
。
在项目中创建一个名为js
的新文件夹。在这个文件夹中,创建两个名为app
和tests
的文件夹。在app
文件夹中,通过转到文件 新建
文件创建一个名为
calculator.js
的新的空 JavaScript 文件。你不会添加任何东西。TDD(测试驱动开发)声明您必须首先编写您的单元测试,以便它们在您编写代码之前失败。这意味着您所有的预期结果都是在测试中编写的,所以当您编写代码时,您的代码能够满足它们。这是一个很好的练习。当您在单元测试中编写逻辑规则时,如何实现最终代码并不重要,只要输出满足单元测试即可。
这种工作方法确实有助于你写出更简洁的代码,因为你的代码现在只会满足预期的输出。
编写你的第一个单元测试
您可以选择编写自己的单元测试框架,或者使用现成的框架。这本书的选择是 QUnit。它由 jQuery 社区开发,并定期更新。不仅如此,它还可以从浏览器中运行,或者可以使用一个名为 PhantomJS 的程序从命令行运行。
要为计算器设置一个 QUnit 单元测试,首先在包含以下 HTML 的tests
文件夹中创建一个名为calculator.html
的新文件。
`
calculator unit tests
l`
这段代码代表了在 QUnit 中运行一个基本的空单元测试所需的 HTML。如您所见,它不包含任何特定于移动设备的标记。
在body
标签中,您可以看到有几个带有 id 的 HTML 元素。这些在表 9-1 中描述。
您需要在移动网络浏览器中运行测试。为此,必须首先配置 Aptana,使其内部服务器绑定到计算机的 IP 地址,而不是其本地 IP 地址 127.0.0.1。为此,请转到 Aptana Studio 3 首选项
Aptana Studio
Web 服务器
内置。您将看到一个屏幕和下拉菜单,允许您选择内部 web 服务器的 IP 地址应该绑定到什么,如图图 9-1 所示。
图 9-1。 内置服务器偏好
选择一个与你的电脑的 IP 地址匹配的(通常是最上面的)点击OK
,退出 Aptana Studio,并再次启动它以使更改生效。
Aptana 完成启动后,您可以在 web 浏览器中运行应用,方法是在 Aptana Studio 应用浏览器中右键单击calculator.html
,然后选择以 身份运行 JavaScript Web 应用。这将启动 Firefox。将 Firefox 地址栏中的 URL 输入移动设备的默认浏览器。你应该看到标题栏下面的栏是绿色的,0/0 测试已经运行,如图 9-2 所示。现在是时候开始编写一些单元测试了。
图 9-2。 在浏览器中运行 QUnit 测试
在 Aptana Studio 中打开calculator.html
,如果它还没有打开的话。单元测试只是运行几个断言,检查方法或属性的结果值是否与基于可预测输入的期望值相匹配。本章中的计算器例子很简单,因为很容易预测 1+1 应该总是等于 2。没有其他变量会影响预期结果,所以 2 应该总是预期结果。当结果不是 2 时,您就知道应用在某个地方出了问题。
在calculator.html
中,在结束的body
标签之前创建一个新的script
标签。
` ...
** **
** **
`
在script
标签中,您可以开始编写您的第一组单元测试。尽管计算器代码还没有写出来,但是您可以开始规定代码中应该存在什么方法和属性,以及它们在单元测试中应该如何表现。一个基本的计算器应该做到以下几点:
- 增加
- 减去
- 划分
- 乘;成倍增加;(使)繁殖
计算器通常会采用初始值来执行这些方法,并且应该返回每个方法的结果。你也应该能够清除计算器。在此基础上,应实施以下方法:
add
subtract
divide
multiply
clear
getResult
然后,您可以将应用的描述转换成单元测试。
因此,为了创建适当的单元测试,在构造函数上的第一个单元测试的<script />
标记中结束下面的代码和注释。
`test('calculator constructor', function(){
/**
* Specify how many assertions this test will run
* If assertions do not run for any reason, this
* test will fail
*/
expect(1);
/**
* You create a new calculator instance and set
* initial value to 10
*/
var calculator = new app.calculator(10);
/**
* You then assert that 10 is being held as the
* current result
*/
equal(calculator.getResult(), 10, 'the result should equal 10 with no
operation');
});`
如您所见,测试方法是 QUnit 的一部分,并接受描述和回调函数。当测试执行时,回调函数被调用,测试被执行。在测试中,您可以看到几种方法。指定测试中将运行多少个断言。您还实例化了 calculator 类,以便它可以在测试中使用。每个测试创建的任何变量都会被销毁,并且不能在另一个测试中使用。但是,它们可以在测试中的任何断言中使用。最后,equal()
是一个断言。断言只是获取一个结果,并检查结果或返回值是否与期望值匹配。在这种情况下,equal
断言检查新计算器的初始结果是否为 10。
这就是单元测试的全部内容。有各种各样的断言可用。参见表 9-2 中的重要列表。
正如你所看到的,断言往往遵循相同的模式,有一个实际值(通常是属性或方法的直接返回值),和一个期望值(你指定的值和描述断言的消息)。
断言方法可以改变,所以最好查阅 QUnit 文档,可以在[
docs.jquery.com/QUnit](http://www.docs.jquery.com/QUnit)
找到。
如果您现在通过刷新手机浏览器中的网页来运行第一个测试,您可以看到绿色条现在应该是红色的,如图 9-3 所示。
图 9-3。 单元测试失败
正如您所看到的,失败的测试还会告诉您测试失败的地方和原因,这对调试非常有用。
有了第一个断言,您现在可以使用接下来的代码来完成计算器其余部分的单元测试。
``
如您所见,有许多断言,但这是为了确保涵盖应用的每个方面。您可以在单元测试中进行更深入的研究,比如确保当无效值(比如字母)被传递给方法时抛出错误;然而,这不在本章讨论范围之内。
单元测试完成后,现在是时候为计算器编写代码了。由于这一节的重点是创建单元测试,所以不会详细解释计算器 JavaScript 代码。代码注释应该有助于解释一下。
`var app = app || {};
app.calculator = function(_initialValue){
/**
* The current result of the calculator
*/
var _result = _initialValue;
/**
* Gets the current result of the calculator
*/
this.getResult = function(){
return _result;
}
/**
* Adds a value to the current result and returns the new value
*/
this.add = function(value){
_result = _result + value;
return _result;
}
/**
* Subtracts a value from the current result and returns the new value
*/
this.subtract = function(value){
_result = _result - value;
return _result;
}
/**
* Multiplies a value from the current result and returns the new value
*/
this.multiply = function(value){
_result = _result * value;
return _result;
}
/**
* Divides a value from the current result and returns the new value
*/
this.divide = function(value){
_result = _result / value;
return _result;
}
}`
现在,当您在移动浏览器中运行单元测试时,当您重新加载移动 web 浏览器时,结果栏应该变成绿色,如图 9-4 所示。
您可以点击任何单元测试结果来查看运行的断言。
图 9-4。 通过单元测试
单元测试就绪后,现在是时候为您的计算器项目创建一个本地 Git 存储库了。
使用 Git 和 GitHub
现在您的项目中已经有了一些文件,是时候创建一个本地存储库了。正如本章前面提到的,Git 是一个分布式配置管理系统。这允许您在没有网络连接的情况下提交到本地存储库,然后将您的更改推送到远程存储库,比如托管在 github.com 上的存储库。
创建本地存储库很容易;只需点击 Aptana Studio 中应用浏览器上方的命令图标,然后点击初始化 Git 库。这将在后台运行适当的命令git init
,以便您可以开始检入文件。
注意:重要的是要记住,Git 作为一个系统,不会将空文件夹提交到存储库中。如果您有一个需要作为项目的一部分签入的空文件夹,则应该在其中创建一个空文件。Git 将获取空文件并签入文件夹。
初始化 Git 存储库不会自动签入任何新文件。为此,再次点击命令图标。现在 Git 存储库已经初始化,您将在下拉菜单中看到一些新命令。向下滚动到提交菜单项并点击它。将出现一个新窗口,如图图 9-5 所示。
图 9-5。Git 提交窗口
从图 9-5 中可以看到,有四个主框:提交更改框(顶部)、未登台更改框(左下角)、提交消息框(右下角)和登台更改框(右下角)。“提交更改”框显示了“未暂存的更改”框中所选文件与存储库中当前版本之间的差异。“未转移的更改”框显示自上次提交以来已更改的所有文件。这些文件有三种状态:
- 白色:新文件
- 红色:删除的文件
- 绿色:文件已更改
“提交”消息框允许您添加提交消息;您需要在每次提交时添加一条提交消息。“阶段更改”框显示了将通过当前提交提交的所有文件。为了提交文件,您必须将它们从“未分段更改”框移到“分段更改”框中。通常情况下,您会希望提交所有文件。点击未分级变更框旁边的>>
按钮。这将自动将所有文件移动到“暂存更改”框中。输入提交消息并点击Commit
按钮。这将把所有的更改提交给本地存储库。
当你在 Aptana Studio 中改变文件时,你会看到它们的颜色会在应用浏览器中改变,如图图 9-6 所示。
图 9-6。 改变了应用浏览器中的文件
文件或文件夹旁边的星号(*)表示更改。根据您的 Aptana Studio 主题,文件的背景也会改变颜色,以指示文件的当前状态。如果您想了解特定主题的颜色含义,请查看 Aptana Studio 首选项窗格中的主题首选项,并查找未登台文件和登台文件元素,如图 9-7 所示。
图 9-7。 检查暂存/未暂存文件的配色方案
现在,您的文件已经提交到本地存储库,现在是时候将它推送到远程存储库了。
使用远程存储库总是好的,即使你是自己为一个项目工作。这样做的主要原因是,如果您的计算机发生问题,您不仅可以对您的项目进行持续备份,还可以对您之前的所有提交和更改进行备份。
本书选择的远程存储库是 GitHub。毫无疑问,它是当今最流行的存储库服务之一,为成百上千的开源开发项目提供免费(但公开)的项目源代码托管空间。
首先,去 github.com 注册一个名为ci
的免费账户和公共存储库。按照说明为本地机器设置 Git SSH 密钥。
- MAC:??]
- 视窗:
[
help.github.com/win-set-up-git/](http://help.github.com/win-set-up-git/)
- Linux:??]
在您设置好 SSH 密钥并通过终端登录 github.com 成功测试之后,返回到您在 github.com 的项目页面。你应该会看到类似于图 9-8 的东西。
图 9-8。 默认 GitHub 项目页面
您可以使用 Aptana Studio 内置的 Git 客户端,而不是使用命令行将您的项目推送到 GitHub。为此,复制项目的 GitHub 远程 URI。该地址将类似于git@github.com:gavinwilliams-fishrod/ci.git
。它是从下一步部分的底部数第二个 URI。
在 Aptana Studio 中,前往命令 更多
添加遥控...如图图 9-9 所示。
图 9-9。 添加远程存储库
会出现一个新的对话框,如图图 9-10 所示。将 GitHub 远程 URI 粘贴到远程 URI 盒中,并按下OK
按钮。
图 9-10。 添加 Git 远程对话框
虽然 Aptana Studio 对它刚刚做的事情不会说得太多,但它刚刚在您的项目中添加了一个远程存储库,别名为origin
。当你现在打开命令菜单时,有几个新的活动项,如推和拉,如图图 9-11 所示。
图 9-11。 新激活的远程命令
不幸的是,Aptana Studio 中的 Git 客户端不会自动推送到新的远程存储库。为了解决这个问题,您需要通过项目浏览器进行第一次推送。进入窗口 显示视图
项目浏览器。右键点击项目浏览器中的
ci
项目,进入团队 推送至远程
原点。你的项目现在应该在 GitHub 上,你现在可以使用命令菜单中的推和拉命令。重要的是尽可能多的承诺,并在项目工作结束时推动你的改变。
转到你在 github.com 的项目,你应该看到你的项目已经被推送到 GitHub 了。
使用 Git 和 GitHub 可以做很多事情,以至于有很多文章和书籍讲述如何真正利用这个系统。您可以在[
help.github.com/](http://help.github.com/)
了解更多信息。
既然您已经了解了使用 GitHub 的基本知识,那么是时候了解 Capistrano 了,它是 web 应用的首选部署平台。
与卡皮斯特拉诺打成一片
Capistrano 是一个部署平台,有助于消除一些重复的部署任务。对于一个小型的移动 web 应用,Capistrano 可以被看作是用一把大锤在一块木头上钉钉子。随着应用的增长,最终会有更多的环境和细节需要在应用中配置,Capistrano 突然感觉像是一股新鲜空气。
在这一节中,您将关注使用 Capistrano 将您的应用简单地部署到生产环境中。
本书的首选主机提供商是 theserve.com,首选服务器操作系统是 CentOS 5;但是,您可以自由选择使用任何提供 SSH 访问的主机。
按照第一章中的安装指南,你应该已经安装了 Ruby。卡皮斯特拉诺是一种红宝石。要安装它,从应用浏览器进入命令 打开终端。终端窗口应该在窗口的右边打开。输入以下命令来安装 Capistrano、Capistrano Rsync With Remote Cache 和 Capistrano multimedia:
- 视窗:
gem install capistrano capistrano_rsync_with_remote_cache capistrano-ext
- MAC/Linux:??]
在安装了 Capistrano 和所有需要的 gems 之后,你现在可以开始你的项目了。返回应用浏览器,确保没有项目被选中/高亮显示,然后进入命令 打开终端。一个终端窗口将会打开,如图 9-12 所示。
图 9-12。 终端窗口
这将确保任何运行的命令都将在项目的根目录下运行。为了验证这一点,确保ci
显示在命令行的某处,如图 9-12 中的所示。
为了使用 Capistrano,你需要创建一组配置文件。Capistrano 可以通过一个名为capify
的命令行工具自动为您完成这项工作。要完成您的项目,请到您的终端并运行命令capify
。您将看到类似于图 9-13 的输出。
图 9-13。 资本化输出
正如您所看到的,在您的项目中已经创建了几个文件和文件夹。通过单击应用浏览器并按键盘上的 F5 键来刷新应用浏览器,以查看更改。新文件如图 9-14 所示。
图 9-14。 新卡皮斯特拉诺文件
在配置 capistrano 之前,您应该配置您的生产服务器,以便您可以使用无密码登录将您的 capistrano 项目部署到它上面。作为其中的一部分,您将需要使用在设置 Git 时生成的公共 rsa 密钥。这使得 capistrano 无需干预即可运行。将id_rsa.pub
文件的内容复制到您的剪贴板,并使用您的服务器的 SSH 用户名和密码登录到您的新生产服务器。在图 9-15 中显示的命令,从 Aptana 的终端或 Mac OSX 的终端运行应该可以促进这一点。
图 9-15。 在 Mac 上使用 Terminal.app 登录远程服务器
如果您运行的是 Windows,您可以使用 PuTTy 应用([www.chiark.greenend.org.uk/~sgtatham/putty/](http://www.chiark.greenend.org.uk/~sgtatham/putty/)
)登录到您的远程服务器。
您需要使用命令cd ~/
切换到当前用户的主目录,如图 9-16 中的所示。
图 9-16。 切换到主目录
应该有个文件夹叫.ssh
。您可以通过运行命令ls
来检查文件夹是否存在。使用命令cd .ssh
进入该目录。如果文件夹不存在,需要使用mkdir .ssh
命令创建目录,然后使用cd .ssh
进入目录,如图图 9-17 所示。
图 9-17。 检查和创建。ssh 目录
进入目录后,如果还没有一个名为authorized_keys
的文件,您需要创建一个新文件。使用ls
命令检查文件是否已经存在。
要创建authorized_keys
文件,如果它不存在,只需运行命令touch authorized_keys
。这将创建一个空文件,如图图 9-18 所示。
图 9-18。 检查并创建授权密钥文件
然后,您需要将计算机的公钥添加到authorized_keys
文件中。使用vi authorized_keys.
编辑文件如果authorized_keys
文件中已经有内容,您需要使用箭头键向下移动到它的底部,然后按住 Shift + I (insert)。一旦在底部,将公钥粘贴到文件中,如图图 9-19 所示。
图 9-19。 添加公钥
您现在需要保存文件。按键盘上的 Esc 键,然后按住 Shift +:。然后键入wq
(写退出),并按下键盘上的回车键。你应该会看到类似于图 9-20 中所示的东西。
图 9-20。 在 vi 中写文件
最后,您需要为新文件设置适当的权限。使用命令cd ../
返回主目录。.ssh
文件夹需要所有者读/写/执行权限。为此,运行命令chmod 0700 ./.ssh
。
接下来,authorized_keys
文件需要所有者的读/写权限。为此,运行命令chmod 0600 ./ssh/authorized_keys
,如图图 9-21 所示。
图 9-21。 设置权限。ssh 文件夹
通过在命令行中执行exit
命令,注销您的远程服务器。你现在应该可以不用密码重新登录,如图 9-22 所示。
图 9-22。 没有密码的 SSH
设置了无密码 SSH 之后,现在是时候配置 Capistrano 以部署到生产服务器了。
回到 Aptana Studio,双击config
文件夹中的新deploy.rb
文件。这包含项目的部署配置。它是用 Ruby 编写的,但是为了理解和配置它,你不一定要了解很多 Ruby。
默认的deploy.rb
文件是专门为 Ruby 项目设置的,这并不是您想要使用的。首先,删除文件的内容。deploy.rb
文件中新的第一行将是应用名。这可以使用下面一行 Ruby 进行配置:
set :application, "continuousintegration" # The application name
这暂时没有真正的功能用途,但是您可以在您的配置任务和选项中使用这个变量。
接下来,您需要使用以下 Ruby 代码包含多阶段和 Capistrano-offroad gems:
require 'capistrano/ext/multistage' require 'capistrano-offroad'
这将包括允许您从单个命令行部署和控制多个环境的适当代码。除了提供标准的 web 友好部署配置,Capistrano-offroad 还允许您在 Ruby 项目之外使用 Capistrano。
接下来,您需要用下面几行代码配置 Capistrano 多级:
set :stages, %w(production) set :default_stage, "production"
stages
行指出了您希望部署到的环境。您可以拥有任意多个环境,只要它们由空格分隔,如下所示:
set :stages, %w(production staging development testing preprod)
stage 环境的名称应该用作配置文件的名称,这一点在本章中有进一步的介绍,您可以随意命名环境,只要它只包含字母字符,不包含空格或特殊字符。如果您从 cap 命令中排除 stage 名称,变量default_stage
将设置默认的 stage 环境。
接下来,指定要用于部署的越野模块。越野默认值模块将覆盖许多默认的 capistrano 挂钩,例如创建共享文件/文件夹定义,Capistrano 将在修订之间共享这些定义,而不是覆盖它们。对于日志、配置选项和用户数据来说,这通常很方便。如果需要,您可以重新实现这些钩子,但是对于没有服务器端代码的应用来说,包含这些钩子是没有意义的。
offroad_modules 'defaults'
现在您必须配置 Git。Capistrano 将从 Git/GitHub 中签出最新的指定分支,以便可以上传。:repository
变量是项目的 GitHub URI,它将用于从 GitHub 检查最新的代码。
set :repository, "git@github.com:gavinwilliams-fishrod/ci.git" # The git repo URI set :scm, :git # Tells capistrano to use GIT set :branch, "master" # Tells capistrano which branch to use
:scm
变量只是告诉 Capistrano that 将被用作 SCM 系统。
您可以通过修改:branch
配置选项来指定使用哪个分支。例如,要使用名为 special 的分支,请使用以下设置:
set :branch, "special" # Tells capistrano which branch to use
下一个变量将告诉 Capistrano 如何部署应用。在这种情况下,将使用rsync_with_remote_cache
。这将克隆 Git 存储库,然后使用 rsync 将其部署到生产服务器。如果您的服务器的防火墙阻止传入的 Git 流量,这将非常方便。
set :deploy_via, :rsync_with_remote_cache # Tells capistrano to deploy via rsync
Rsync 是一个应用,将比较文件夹,并同步它们。您还可以对远程服务器上的远程文件夹使用 rsync。
在撰写本文时,rsync_with_remote_cache
有一个错误,该错误阻止您从带有空格的文件夹中同步。要解决这个问题,您需要使用以下配置变量指定一个替代的临时文件位置:
set :local_cache, '/tmp/ci/' # The directory where you want to store the rsync cache
最后,以下 Capistrano 配置选项从其他配置选项中获取变量:
role(:web) { domain } # Your HTTP server, Apache/etc role(:app) { domain } # This may be the same as your
Web` server
role(:db) { domain } # This is where Rails migrations will run
set :keep_releases, 5 # Tells capistrano how many releases to keep`
为了给您一个概述,完整的配置文件应该如下所示:
`set :application, "continuousintegration" # The application name
require 'capistrano/ext/multistage'
require 'capistrano-offroad'
set :stages, %w(production)
set :default_stage, "production"
offroad_modules 'defaults'
set :repository, "git@github.com:gavinwilliams-fishrod/ci.git" # The location of the
git repo, this is the read-only url
set :scm, :git # Tells capistrano to use GIT
set :branch, "master" # Tells capistrano which branch to use
set :deploy_via, :rsync_with_remote_cache # Tells capistrano to deploy via rsync
# Or: accurev
, bzr
, cvs
, darcs
, subversion
, mercurial
, perforce
,
subversion
or none
set :local_cache, '/tmp/ci/' # Set this to a directory where you would like to store the
rsync cache
role(:web) { domain } # Your HTTP server, Apache/etc
role(:app) { domain } # This may be the same as your Web
server
role(:db) { domain } # This is where Rails migrations will run
set :keep_releases, 5 # Tells capistrano how many releases to keep`
如前所述,Capistrano 多级 gem 用于允许您从命令行控制和部署到多个环境。此配置文件仅为生产而设置。在 Aptana Studio 中,进入应用浏览器,在config
文件夹中创建一个名为deploy
的新文件夹。在deploy
文件夹中,新建一个名为production.rb
的文件,如图图 9-23 所示。
图 9-23。 生产阶段配置文件
在 Aptana Studio 中打开production.rb
文件,添加以下配置选项:
set :domain, "ci.fishrod.co.uk" # The domain name of the application
前面一行代码设置了应用的域名,它将用于登录服务器。
set :deploy_to, "~/application/" # The path to deploy the application
:deploy_to
设置应用应部署到的远程服务器上的路径。因为这个实例中 SSH 用户的 web 路径位于用户的主目录中,所以使用~作为参数。如果您的 web 路径在其他地方,您应该使用该路径。比如在大多数空白服务器上,它会存在于/var/www/application/
下。首先,您需要在服务器上创建应用文件夹。
set :user, "ci.fishrod.co.uk" # The SSH user for your website set :deploy_group, "ci.fishrod.co.uk" set :use_sudo, false # Tells capistrano not to run commands as root
:user
将设置用于登录您的远程服务器的用户。:deploy_group
将设置 Capistrano 将任何上传文件的权限设置到哪个组。:use_sudo
将停止 Capistrano 作为根用户上传和更改文件。
Capistrano 完全配置好之后,现在是时候为部署设置您的生产服务器了。
为此,请再次登录到您的远程服务器,然后转到您的 web 服务器的文档根文件夹上的文件夹。例如,您的根目录可能是/var/www/html
,所以您应该转到您的/var/www
目录。如果你在共享主机上,你应该去~/
,那将是你的主目录。
在该目录下新建一个名为application
的目录,并删除你的文档根文件夹,如图 9-24 所示。您的文档根文件夹可能被称为htdocs
、html
或public_html
;在本章中,它将被称为public_html
。
图 9-24。 创建应用目录,删除文件根目录
在完成 Capistrano 配置之前,您需要在远程服务器上设置 Capistrano 文件夹。为此,请返回 Aptana Studio 并打开终端视图。运行以下命令:
cap production deploy:setup
这将登录到您的远程服务器,并为您创建适当的文件和文件夹。您应该会看到类似于图 9-25 中的输出。
图 9-25。??【卡皮斯特拉诺】部署:设置输出
现在您可以部署您的应用了。运行以下命令可以做到这一点:
cap production deploy
如果一切顺利,您将看到类似于图 9-26 所示的最终输出。
图 9-26。 最终 Capistrano 部署输出
最后要做的是在 web 根目录和当前版本之间创建一个符号链接。这允许您在服务器上保留代码的修订版,如果在部署过程中出现任何问题,Capistrano 可以回滚。
为此,返回到连接到您的服务器的终端窗口(用于创建应用文件夹和删除public_html
文件夹的窗口)并运行以下命令:
ln -s application/current/ public_html
运行ls
命令,验证符号链接已经创建,如图图 9-27 所示。
图 9-27。 新建公共 _html 符号链接
现在,使用以下 URL 在远程服务器上导航到您的单元测试,当然,用您自己的域替换yourdomain
:[
yourdomain/js/tests/calculator.html](http://yourdomain/js/tests/calculator.html)
。
如您所见,Capistrano 是一个强大的工具。它不仅可以用于进行部署,还可以用于在远程服务器上运行命令,而不必直接登录到它们。这对于重新编译 SASS 文件或者连接和缩小 JavaScript 文件非常有用。Capistrano 更好的一点是,因为它是从命令行运行的,所以可以集成到 Hudson 或 Bamboo 等持续集成服务器中。
总结
从这一章开始,你应该对持续集成有一个基本的了解,以及它如何影响你测试和部署你的应用。虽然这本书没有涵盖如何实现持续集成服务器,但这是一个值得研究的课题,即使是作为一个单独的开发人员。当您有最后一分钟的代码更改时,您可以确信当您签入代码时,一旦它到达生产环境,它已经被完全测试。您还可以相信,由于自动化,如果它第一次成功部署,它应该会一次又一次地成功部署,除非您破坏了应用的某些元素。在这种情况下,您失败的测试将指出哪段代码没有按预期工作。
仅通过这一章,你就应该对如何使用 QUnit 创建 JavaScript 单元测试有了一个真正的立足点。还有其他更先进的测试产品,如 Test Swarm ( [
github.com/jquery/testswarm](https://github.com/jquery/testswarm)
)、Jasmine([
pivotal.github.com/jasmine/](http://pivotal.github.com/jasmine/)
)和 Selenium ( [
seleniumhq.org](http://seleniumhq.org)
或[
code.google.com/p/selenium/wiki/AndroidDriver](http://code.google.com/p/selenium/wiki/AndroidDriver)
)。重要的是要记住,尽管 TDD 起初看起来像是一项费力的任务,但它确实允许您思考您的代码及其构造方式,这有助于产生更干净、更精简的 JavaScript。
您应该对如何通过 Aptana Studio 使用 Git 和 GitHub 有一个基本的了解,以及它不仅对您一个单独的开发人员有好处,对最终与您一起工作的其他人也有好处。
最重要的是,您现在应该知道如何设置 Capistrano,这是一个强大的部署应用,主要用于部署 rails 应用。值得看一看 Capistrano 文档,以探索它的所有功能。在[
github.com/capistrano/capistrano/wiki/](https://github.com/capistrano/capistrano/wiki/)
找到它。
这本书应该已经让你走上了移动网络开发的正确道路。有些主题可能看起来有点超前;然而,我觉得,随着行业发展如此之快,保持领先地位并尽可能多地挑战自己总是很重要的。希望你从这本书中学到的一些实践和原则能让你对移动网络产生兴趣。现在,您应该已经掌握了为 Android 构建相当先进的移动 web 应用所需的所有知识。
十、附录 A
A-1 清单
`var app = app || {};
app.model = app.model || {};
/**
* A movie model used for all movies within the application
*
* @alias app.model.movie
* @constructor
* @param {String} title
* @param {String} rtid
* @param {String} posterframe
* @param {String} synopsis
*/
app.model.movie = function appModelMovie(title, rtid, posterframe, synopsis) {
/**
* The video's instance variables
/
var _title,
_rtid,
_posterframe,
_synopsis,
_releaseDate,
_videos = [],
_actors = [],
_rating,
_favorite = false,
_self = this;
/*
* Getters and setters
*/
this.init = function(){
/**
* Set the instance variables using the constructor's arguments
*/
this.setTitle(title);
this.setRtid(rtid);
this.setPosterframe(posterframe);
this.setSynopsis(synopsis);
}
/**
* Returns the movie title
* @return {String}
*/
this.getTitle = function(){
return _title;
}
/**
* Sets the movie title
* @param {String} title
*/
this.setTitle = function(title){
_title = title;
}
/**
* Returns the Rotten Tomatoes reference ID
* @return {String}
*/
this.getRtid = function(){
return _rtid;
}
/**
* Sets the Rotten Tomatoes reference ID
* @param {String} rtid
*/
this.setRtid = function(rtid){
_rtid = rtid;
}
/**
* Gets the posterframe URL/Path
* @return {String}
*/
this.getPosterframe = function(){
return _posterframe;
}
/**
* Sets the posterframe URN/Path
* @param {String} posterframe
*/
this.setPosterframe = function(posterframe){
_posterframe = posterframe;
}
/**
* Gets the synopsis as a string with no HTML formatting
* @return {String}
*/
this.getSynopsis = function(){
return _synopsis;
}
/**
* Sets the synopsis, a string with no HTML must be passed
* @param {String} synopsis
*/
this.setSynopsis = function(synopsis){
_synopsis = synopsis;
}
/**
* Gets all videos associated with the movie
* @return {Array}
*/
this.getVideos = function(){
return _videos;
}
/**
* Sets all videos associated with the movie
* @param {Array}
*/
this.setVideos = function(videos){
_videos.length = 0;
/**
* Rather than setting the videos all in one go,
* you use the addVideo method, which can handle
* any validation for each video before it's
* added to the object
*/
for(var i = 0; i < videos.length; i++){
_self.addVideo(videos[i]);
}
}
/**
* Adds a video to the movie
* @param {app.model.video} video
/
this.addVideo = function(video){
/*
* You can add any video validation here
* before it's added to the movie
*/
_videos.push(video);
}
/**
* Gets all actors associated with the movie
* @return {Array}
*/
this.getActors = function(){
return _actors;
}
/**
* Gets an actor at a specific index
* @param {Integer} index
* @return {app.model.actor}
*/
this.getActor = function(index){
return _actors[index];
}
/**
* Sets all actors associated with the movie
* @param {Array}
*/
this.setActors = function(actors){
_actors.length = 0;
/**
* Rather than setting the actors all in one go,
* you use the addActor method, which can handle
* any validation for each actor before it's
* added to the object
*/
for(var i = 0; i < actors.length; i++){
_self.addActor(actors[i]);
}
}
/**
* Adds an actor to the movie
* @param {app.model.actor} actor
/
this.addActor = function(actor){
/*
* You can add any actor validation here
* before it's added to the movie
*/
_actors.push(actor);
}
/**
* Sets the release date
*/
this.setReleaseDate = function(releaseDate){
_releaseDate = releaseDate;
}
/**
* Gets the release date
* @return {app.type.releaseDate}
*/
this.getReleaseDate = function(){
return _releaseDate;
}
/**
* Gets the movie rating
* @return {String}
*/
this.getRating = function(){
return _rating;
}
/**
* Sets the movie rating
* @param {String} rating
*/
this.setRating = function(rating){
_rating = rating;
}
/**
* Checks to see whether the movie
* is in the user's favorites list
* @return {Bool}
*/
this.isFavorite = function(){
return _favorite;
}
/**
* Sets whether the movie is in the
* user's favorites list
* @param {Bool} value
*/
this.setFavorite = function(value){
_favorite = value;
}
this.init();
}`
清单 A-2
`.footer {
height: 40px;
width: 100%;
text-align: center;
position: absolute;
bottom: 0;
.back {
height: 100%;
display: block;
background: url('../img/back.png') no-repeat 10px 50%;
text-indent: -10000px;
}
}`
最终的 SASS 文件应该类似于下面的代码。
`body, html, #shoe, .deck {
height: 100%;
width: 100%;
overflow: hidden;
margin: 0px;
}
/**
* Individual Card Styles
*/
#card-movie_search_results {
z-index: 50;
}
/**
* Deck styles
*/
.deck {
position: relative;
.card {
height: 100%;
width: 100%;
left: -100%;
position: absolute;
}
.card.active {
left: 0px;
}
}
/**
* List styles
*/
/**
* Standard list
*/
.list {
margin: 0;
padding: 0;
li {
padding: 10px;
overflow: hidden;
height: 82px;
display: block;
border-bottom: 1px solid #CCCCCC;
.preview-image {
float: left;
width: 60px;
height: 82px;
text-align: center;
margin-right: 10px;
}
}
}
/**
* Movie list
*/
.movie-list {
li {
background: #A5CCEB;
border-bottom-color: #FFFFFF;
.more {
display: block;
height: 100%;
overflow: hidden;
text-decoration: none;
h2 {
margin: 0 0 10px;
color: #BF2628;
}
p {
margin: 0;
color: #000000;
}
}
}
li:nth-child(odd) {
background: #97B2D9;
}
}
/**
* Header taskbar styles
*/
.screenbar {
@include gradient(#7D9DCE, #ABC1E1, 90deg);
}
header#taskbar {
color: #FFFFFF;
overflow: hidden;
padding: 10px;
border-bottom: 1px solid #BF2628;
h1.branding {
margin: 0px;
float: left;
width: 73px;
height: 32px;
text-indent: -10000px;
overflow: hidden;
background: url('../img/momemo.png') no-repeat top left;
}
.clear-search {
float: right;
width: 35px;
height: 35px;
display: none;
overflow: hidden;
text-indent: -10000px;
background: url('../img/clear.png') 50% 50% no-repeat;
}
}
header#taskbar.searchactive {
.clear-search {
display: block;
}
form#add-movie {
margin-right: 40px;
}
}
/**
* Movie view
/
/*
* Animations for the poster header
*/
@keyframes posteranimation {
0% { top: 0%; }
100% { top: -80%; }
}
@-moz-keyframes posteranimation {
0% { top: 0%; }
100% { top: -80%; }
}
@-webkit-keyframes posteranimation {
0% { top: 0%; }
100% { top: -80%; }
}
.movie-header {
position: relative;
overflow: hidden;
height: 20%;
.poster {
position: absolute;
top: 0%;
@include animation(posteranimation 10s ease 0 infinite alternate);
}
.movie-title {
position: absolute;
bottom: 0px;
background: rgba(255, 255, 255, 0.75);
padding: 5px;
bottom: 0;
left: 0;
width: 100%;
@include box-sizing(border-box);
.btn-favorite {
float: right;
padding: 10px;
color: #FFFFFF;
background: #7D9DCE;
font-weight: bold;
border-radius: 5px;
text-decoration: none;
border: 1px solid #A5CCEB;
}
.movie-release-date {
text-transform: uppercase;
font-weight: bold;
}
}
}
.movie-content {
height: 80%;
width: 100%;
padding-bottom: 40px;
@include box-sizing(border-box);
.block-container {
width: 280%;
height: 100%;
.block {
width: 33%;
float: left;
height: 100%;
font-size: 1.3em;
line-height: 2em;
.content {
@include box-sizing(border-box);
}
h3 {
padding: 10px 10px 0 10px;
}
.content {
padding: 10px;
}
}
}
}
.footer {
height: 40px;
width: 100%;
text-align: center;
position: absolute;
bottom: 0;
.back {
height: 100%;
display: block;
background: url('../img/back.png') no-repeat 10px 50%;
text-indent: -10000px;
}
}`
From Becci Buckley on
This is a great article Dan, it might need some work 😄