HTML5-iPhone-Web-应用开发-全-

HTML5 iPhone Web 应用开发(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

自 90 年代以来,Web 应用程序已经走过了很长的路,当时静态 HTML 页面被传送到客户端。在那些日子里,Web 页面采用严格的客户端-服务器模型,其中大部分处理都在服务器上进行,客户端只是以很少或没有交互方式呈现信息。这样的信息只能通过速度非常慢的桌面计算机访问。

那些日子已经过去,我们现在以前所未有的方式相连。从可以在地铁上打电话的手机,到在空中 3 万英尺处呈现您最喜欢的报纸最新文章的平板电脑;我们现在处于一个数字时代,信息通过创新技术可以轻松获取。然而,我们仍然在努力创建技术和物理世界之间无缝互动。

尽管我们有设备对我们的触摸敏感,可以检测我们的位置,并具有监测我们生命信号的能力,但仍然取决于我们创建将改变世界的应用程序。创建这些应用程序需要大型团队、复杂的业务角色和昂贵的开支。

在短暂的时间内,开发这些应用程序对许多企业家来说是一个挑战,他们希望推动变革。一个分散的移动市场,直到今天仍在持续,导致了有限的开发资源。我们看到这些技术的进步增加了,但很少有人了解或甚至对学习所有这些语言感兴趣,感到有必要创建跨平台应用程序。

然而,只是时间问题,一个单一的平台将到来并永远改变世界。HTML5 及其在各种设备上的实现,帮助推动了传递创新和改变世界所需的力量。在我们的应用程序中利用这项技术,可以推动硬件的极限,同时创建许多用户都可以享受的东西,无论他们喜欢使用什么设备。

多年来,我意识到设备不可知的应用程序将成为常态。我们已经看到竞争对手采用这些标准,对他们的成功几乎没有影响;事实上,可以说它产生了相反的效果。因此,这本书的写作目的是为了向您提供基于开放标准的应用程序创建技术,以及成功创建设备不可知软件的方法。

本书涵盖的内容

第一章 应用程序架构,帮助您了解如何为 iPhone Web 应用程序开发创建标准架构。我们将根据本书的需要定制标准的 HTML5 移动样板。

第二章 集成 HTML5 视频,帮助您了解在 Web 应用程序中实现 HTML5 视频播放器的基础知识。我们将审查规范并实现一个公开的 API 来利用它。

第三章 HTML5 音频,解释了 HTML5 音频 API 的实现。我们将创建一个利用第二章相同原则的可重用组件的音频播放器。

第四章 触摸和手势,帮助您了解触摸和手势事件,包括相似之处和不同之处。我们将讨论一些示例,更重要的是,规范如何正确整合我们应用程序的用户体验。

第五章 理解 HTML5 表单,解释了 HTML5 表单的新功能,最终理解它在我们的 iOS Web 应用程序中的用途。我们将审查新的输入、它们的交互以及 iOS 操作系统所期望的行为。

第六章,“位置感知应用程序”,将以地理定位作为关键点,从规范到在 Safari iOS 浏览器中的完整实现。我们将创建一个利用此功能的示例,并演示我们如何在自己的应用程序中利用它。

第七章,“单页应用程序”,充满了有关如何在应用程序中创建无缝体验的信息。我们将讨论 MVC 设计模式的原则,并创建一个充分利用其潜力的示例。

第八章,“离线应用程序”,将涵盖诸如缓存、历史和本地存储等关键主题。我们将介绍基本知识,并透露细节,以便我们创建真正的离线应用程序。

第九章,“清洁和优化代码原则”,将使我们暂时绕过开发过程,以完善我们的技艺。我们将讨论最佳实践、行业支持的技术以及改进我们的代码以使应用程序整体受益的方法。

第十章,“创建原生 iPhone Web 应用程序”,回顾了我们如何创建之前学到的原生应用程序。应用相同的技术,我们将基于开放标准创建原生应用程序。

您需要为本书做好准备。

本书旨在为 iOS 提供 Web 应用程序开发解决方案。考虑到这一点,您将需要一部 iPhone 和/或 iPad,最好是一台安装有 Mac OS X 10.8 及以上版本的苹果电脑。您肯定需要一个文本编辑器或您选择的集成开发环境,包括安装了 iOS 模拟器的 Xcode 4 及以上版本。最后,您将在最现代的 Web 浏览器中测试您的应用程序,包括 Safari。

本书适合对象

本书适用于初学者到中级开发人员,他们专门从事 iOS 的 Web 应用程序开发。本书从入门级材料开始,每章都会深入讨论每个主题。所涵盖的主题将让您对如何处理开发过程以及实现这些目标所需的步骤有一个良好的理解。

约定

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

文本中的代码单词显示如下:“尽管我们之前已经编写了这段代码,让我们简要回顾一下MediaElement类的结构。”

代码块设置如下:

<div class="audio-container">
    <audio controls preload>
        <source src="img/nintendo.mp3" type='audio/mpeg; codecs="mp3"'/>
        <p>Audio is not supported in your browser.</p>
    </audio>
    <select>
        <option value="sample1.mp3" selected>Sample1</option>
        <option value="sample2.mp3">Sample2</option>
        <option value="sample3.mp3">Sample3</option>
    </select>
</div>

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:“然后通过单击以 zip 格式下载按钮来下载 zip 文件。”

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会以这样的方式出现。

第一章:应用程序架构

在本章中,我们将为我们的 iPhone 应用程序创建一个标准架构。我们将以 HTML5 移动锅炉板为基础,并根据本书中的几个项目的需求进行定制。从在 HTML5 中标记我们的内容到创建 JavaScript 框架,我们将创建静态页面,帮助我们专注于 iPhone Web 应用程序开发的基础。

在本章中,我们将涵盖:

  • 实施 HTML5 移动锅炉板

  • 创建初步架构

  • 自定义我们的框架

  • 创建语义标记

  • 结构化我们的样式表

  • 响应式设计原则

  • 建立我们的 JavaScript 架构

  • 路由到移动站点

  • 主屏幕图标

  • 介绍我们的构建脚本

  • 部署我们的项目

实施 HTML5 移动锅炉板

当您开始开发时,始终要从一个可以塑造成项目需求的基本框架开始。在许多情况下,我们在工作的地方或者为我们自己的个人项目开发这些框架。然而,开源社区为我们提供了一个可以在项目中使用的优秀框架——HTML5 移动锅炉板。这个框架基于著名的 HTML5 锅炉板,并针对移动进行了优化,包括精简的 HTML 模板;使用Zepto,以及针对移动进行了优化的工具和辅助功能。

下载并安装 HTML5 移动锅炉板

我们需要采取的第一步是下载 HTML5 移动锅炉板,位于这里:

html5boilerplate.com/mobile/

一旦下载了锅炉板,您应该从解压的存档文件中看到以下结构:

下载和安装 HTML5 移动锅炉板

初步目录结构

下一步是将这些文件放在您选择的目录中。例如,我已经将我的文件放在 Mac 上的以下目录中:

/Users/alvincrespo/Sites/html5iphonewebapp

接下来,我们将使用一个构建系统,帮助我们创建多个环境,简化部署过程,并在我们想要为测试和/或生产优化我们的网站时使事情变得更容易。

根据 HTML5 移动锅炉板的文档,有两种不同类型的构建系统,如 Node Build 脚本和 Ant Build 脚本。在本书中,我们将使用 Ant Build 脚本。我建议使用 Ant Build 脚本,因为它已经存在一段时间,并且具有我在项目中使用的适当功能,包括 CSS Split,它将帮助拆分锅炉板附带的主 CSS 文件。

集成构建脚本

要下载 Ant Build 脚本,请转到以下链接:

github.com/h5bp/ant-build-script

然后,通过单击Download as zip按钮下载 zip 文件。下载 Ant Build 脚本后,将文件夹及其内容复制到您的项目中。

一旦您的 Ant Build 脚本目录完全转移到您的项目中,将包含构建脚本的目录重命名为build。此时,您的项目应该已经完全设置好,以便在本书的其余应用程序中使用。我们将在本章后面介绍如何使用构建脚本。

创建我们的应用程序框架

对于每个项目,创建一个适应项目需求的框架是很重要的。重要的是要考虑项目的每个方面。从所需的文档到团队的优势和劣势,建立一个坚实的基础对我们构建和相应调整是很重要的。

修改锅炉板

现在,我们将修改我们的锅炉板,以满足我们将要构建的项目的需求。为简单起见,我们将从文件夹中删除以下项目:

  • CHANGELOG.md

  • crossdomain.xml

  • README.md

  • /doc (目录)

现在,目录已经整理好了,是时候看一下一些样板代码,并根据本书项目的需求进行定制了。

定制我们的标记

首先,用你喜欢的文本编辑器打开应用程序。一旦我们用我们选择的编辑器打开了应用程序,让我们看看index.html

索引文件需要进行清理,以便专注于 iPhone Web 应用程序的开发,并且需要删除 Google Analytics 等未使用的项目。所以让我们删除一些对我们来说不必要的代码。

查找以下代码:

<!DOCTYPE html>
<!--[if IEMobile 7 ]>    <html class="no-js iem7"> <![endif]-->
<!--[if (gt IEMobile 7)|!(IEMobile)]><!--> <html class="no-js"> <!--<![endif]-->

提示

下载示例代码

你可以从你在www.packtpub.com的帐户中下载你购买的所有 Packt 图书的示例代码文件。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,直接将文件发送到你的邮箱。

并将其修改为:

<!DOCTYPE html>
<html class="no-js">

我们在这里所做的是移除 IE Mobile 的检测。虽然这对其他项目可能有帮助,但对于我们来说,它并不能真正帮助我们创建一个完全兼容 iPhone 的应用程序。然而,我们还需要删除一个IEMobile特定的 meta 标记:

<meta http-equiv="cleartype" content="on">

之前的 meta 标记打开了cleartype(一种帮助字体呈现的实用程序)对 IE 移动的支持。这对我们来说并不是必要的,也不是我们应用程序的要求。

现在我们已经从页面中删除了一些不必要的标记,我们可以开始启用一些将增强我们应用程序的功能。查找以下 meta 标记并启用它们,删除周围的注释:

<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">

这些指令告诉我们的应用程序可以在全屏模式下运行,并将状态栏设置为黑色。

我们还可以从文档的<head>中删除以下代码:

<!-- This script prevents links from opening in Mobile Safari. https://gist.github.com/1042026 -->
<!--
        <script>(function(a,b,c){if(c in b&&b[c]){var d,e=a.location,f=/^(a|html)$/i;a.addEventListener("click",function(a){d=a.target;while(!f.test(d.nodeName))d=d.parentNode;"href"in d&&(d.href.indexOf("http")||~d.href.indexOf(e.host))&&(a.preventDefault(),e.href=d.href)},!1)}})(document,window.navigator,"standalone")</script>
-->

一旦我们删除了之前的脚本,你的标记现在应该看起来像下面这样:

<!DOCTYPE html>
<head>
    <meta charset="utf-8">
    <title></title>
    <meta name="description" content="">
    <meta name="HandheldFriendly" content="True">
    <meta name="MobileOptimized" content="320">
    <meta name="viewport" content="width=device-width">
    <link rel="apple-touch-icon-precomposed" sizes="144x144" href="img/touch/apple-touch-icon-144x144-precomposed.png">
    <link rel="apple-touch-icon-precomposed" sizes="114x114" href="img/touch/apple-touch-icon-114x114-precomposed.png">
    <link rel="apple-touch-icon-precomposed" sizes="72x72" href="img/touch/apple-touch-icon-72x72-precomposed.png">
    <link rel="apple-touch-icon-precomposed" href="img/touch/apple-touch-icon-57x57-precomposed.png">
    <link rel="shortcut icon" href="img/touch/apple-touch-icon.png">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <link rel="stylesheet" href="css/normalize.css">
    <link rel="stylesheet" href="css/main.css">
    <script src="img/modernizr-2.6.1.min.js"></script>
</head>

现在,我们可以专注于清理我们的正文。幸运的是,我们只需要删除一件事情——Google Analytics,因为我们不会专注于 iPhone Web 应用的跟踪。

为此,找到以下代码并删除它:

<!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
<script>
    var _gaq=[["_setAccount","UA-XXXXX-X"],["_trackPageview"]];
    (function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.async=1;
    g.src=("https:"==location.protocol?"//ssl":"//www")+".google-analytics.com/ga.js";
    s.parentNode.insertBefore(g,s)}(document,"script"));
</script>

页面上应该只有以下脚本:

<script src="img/zepto.min.js"></script>
<script src="img/helper.js"></script>

一旦我们完成了上述步骤,我们的标记应该变得简洁明了,如下所示:

<!DOCTYPE html>
<html class="no-js">
<head>
    <meta charset="utf-8">
    <title></title>
    <meta name="description" content="">
    <meta name="HandheldFriendly" content="True">
    <meta name="MobileOptimized" content="320">
    <meta name="viewport" content="width=device-width">

    <link rel="apple-touch-icon-precomposed" sizes="144x144" href="img/touch/apple-touch-icon-144x144-precomposed.png">
    <link rel="apple-touch-icon-precomposed" sizes="114x114" href="img/touch/apple-touch-icon-114x114-precomposed.png">
    <link rel="apple-touch-icon-precomposed" sizes="72x72" href="img/touch/apple-touch-icon-72x72-precomposed.png">
    <link rel="apple-touch-icon-precomposed" href="img/touch/apple-touch-icon-57x57-precomposed.png">
    <link rel="shortcut icon" href="img/touch/apple-touch-icon.png">

    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">

    <link rel="stylesheet" href="css/normalize.css">
    <link rel="stylesheet" href="css/main.css">
    <script src="img/modernizr-2.6.1.min.js"></script>
</head>
    <body>

        <!-- Add your site or application content here -->

        <script src="img/zepto.min.js"></script>
        <script src="img/helper.js"></script>
    </body>
</html>

从这里开始,我们应该检查每个项目的样式表和脚本,并在开始项目之前尽可能优化它。然而,我们将使用的这个样板已经由社区优化,并得到了许多开发人员的支持,并且对于我们在这里使用的情况,样式和脚本都已经准备就绪。如果你感兴趣,我鼓励你查看normalize.css文件,其中包含了重置页面的优秀指令。还有必要审查已经使用这个样板增强了以支持移动设备的main.css文件。

现在,我们将继续建立我们的框架。

定制我们的框架

对于开发人员来说,为他们正在进行的每个项目建立一个框架都是至关重要的,无论项目大小如何。当然,你的框架也应该根据项目的要求进行调整。在本节中,我们将建立一个简单的框架,以便在本书的使用过程中使用。

我们已经根据我们的需求整理了样板,现在我们将继续扩展样板,包括对我们将构建的应用程序至关重要的文件。

第一个应用程序将基于 HTML5 视频规范(dev.w3.org/html5/spec-author-view/video.html)。在该应用程序中,我们将为我们的视频播放器创建一个特定的功能,包括播放、暂停和全屏功能。所以让我们创建一个专门针对这个应用程序的目录;我们将这个目录称为video

在这个目录中,我们将创建一个index.html文件,并从index.html文件的主页复制内容。

现在我们已经创建了我们的视频部分,让我们在我们的css目录中创建一个video.css文件。

然后,在我们的/js文件夹中创建一个App目录。在/js/App目录中,让我们创建一个App.js文件。稍后,我们将详细解释这个文件是什么,但现在它将是我们的主要应用程序命名空间,基本上封装了我们应用程序的全局功能。

最后,在/js/App目录中创建一个App.Video.js文件,其中将包含我们视频应用程序的功能。

现在,您将为我们的每个应用程序重复之前的步骤;包括视频、音频、触摸、表单、位置、单页和离线。最终,您的目录结构应该包括以下新目录和文件:

/audio
    index.html
/css
    audio.css
    forms.css
    location.css
    main.css
    normalize.css
    singlepage.css
    touch.css
    video.css
/forms
    index.html
/js
    /App/App.Audio.js
    /App/App.Forms.js
    /App/App.js
    /App/App.Location.js
    /App/App.SinglePage.js
    /App/App.Touch.js
    /App/App.Video.js
/location
    index.html
/offline
    index.html
/singlepage
    index.html
/touch
    index.html
/video
    .index.html

此时,我们应该修复对依赖项的引用,比如我们的 JavaScript 和样式表。所以让我们打开/video/index.html

让我们修改以下行:

<link rel="stylesheet" href="css/normalize.css">
<link rel="stylesheet" href="css/main.css">
<script src="img/modernizr-2.6.1.min.js"></script>

将先前的标记更改为以下内容:

<link rel="stylesheet" href="../css/normalize.css">
<link rel="stylesheet" href="../css/main.css">
<script src="img/modernizr-2.6.1.min.js"></script>

提示

请注意,我们在每个依赖项中添加了../。这本质上是告诉页面向上一级并检索适当的文件。我们还需要对 apple-touch-icon-precomposed 链接、快捷图标和页面底部的脚本进行同样的操作。

我们的框架现在几乎完成了,只是它们还没有连接起来。现在我们已经把一切都组织好了,让我们开始把一切连接起来。它看起来可能不太好看,但至少它将能够工作并朝着一个完全功能的应用程序迈进。

让我们从主index.html文件/ourapp/index.html开始。一旦我们打开了主index.html文件,让我们在<body>元素内创建一个基本的站点结构。我们将给它一个类名为"site-wrapper",并将其放在注释Add your site or application content here的下方:

<body>
    <!-- Add your site or application content here -->
    <div class="site-wrapper">

    </div>
    <script src="img/zepto.min.js"></script>
    <script src="img/helper.js"></script>
</body>

在包含我们站点的包装器中,让我们使用新的 HTML5<nav>元素来语义化地描述将存在于所有应用程序中的主导航栏:

<div class="site-wrapper">
<nav>      
</nav>
</div>

还没有什么特别的,但现在我们将继续使用无序列表元素,并创建一个没有样式的导航栏:

<nav>
    <ul>
        <li>
            <a href="./index.html">Application Architecture</a>
        </li>
        <li>
            <a href="./video/index.html">HTML5 Video</a>
        </li>
        <li>
            <a href="./audio/index.html">HTML5 Audio</a>
        </li>
        <li>
            <a href="./touch/index.html">Touch and Gesture Events</a>
        </li>
        <li>
            <a href="./forms/index.html">HTML5 Forms</a>
        </li>
        <li>
            <a href="./location/index.html">Location Aware Applications</a>
        </li>
        <li>
            <a href="./singlepage/index.html">Single Page Applications</a>
        </li>
    </ul>
</nav>

如果我们复制在/video/index.html中创建的代码并测试页面,您会发现它不会正确工作。对于所有子目录,如视频和音频,我们需要将相对路径从./更改为../,以便我们可以向上一级文件夹。考虑到这一点,nav元素在其他应用程序中将如下所示:

<nav>
    <ul>
        <li>
            <a href="../index.html">Application Architecture</a>
        </li>
        <li>
            <a href="../video/index.html">HTML5 Video</a>
        </li>
        <li>
            <a href="../audio/index.html">HTML5 Audio</a>
        </li>
        <li>
            <a href="../touch/index.html">Touch and Gesture Events</a>
        </li>
        <li>
            <a href="../forms/index.html">HTML5 Forms</a>
        </li>
        <li>
            <a href="../location/index.html">Location Aware Applications</a>
        </li>
        <li>
            <a href="../singlepage/index.html">Single Page Applications</a>
        </li>
    </ul>
</nav>

现在,我们可以将/video/index.html中的导航复制到其余的应用程序文件或我们之前创建的index.html文件中。完成后,我们将拥有一个连接良好的单一站点。

信不信由你,我们这里有一个非常简单的网站。我们的页面已经设置了基本的标记和通用样式。此时,我们需要一个将我们的页面连接在一起的导航。然而,我们几乎没有涉及一些重要的方面,包括应用程序的语义标记,我们将在下一节中讨论。

创建语义标记

语义标记之所以重要,原因有几个,包括搜索引擎优化、创建可维护的架构、使代码易于理解以及满足无障碍要求。然而,您应该熟悉使用与您的内容相关的标记来构建页面的结构。HTML5 规范中有一些新元素,有助于简化这个过程,包括<header><nav><footer><section><article><aside>元素。这些元素中的每一个都有助于描述页面的各个方面,并轻松识别应用程序的组件。在本节中,让我们从我们的视频应用程序开始构建我们的应用程序。

创建页眉

首先,让我们给我们的主索引页面一个标题和一个描述我们所在页面的页眉。让我们在应用程序的/index.html中打开主index.html文件。

找到<title>标签,并在其中输入iPhone Web Application Development – Home。请注意,我们在这里使用连字符。这很重要,因为它使用户更容易扫描页面内容,并有助于特定关键字的排名。

您现在应该在文档的<head>标签中有以下<title>

<title>iPhone Web Application Development - Home</title>

现在,我们希望页面的内容也反映标题,并提醒用户他们在我们网站上的进度。我们想要做的是创建一个描述他们所在部分的页眉。为了实现这一点,让我们在之前创建的导航之前放置以下代码。然后您的代码应如下所示:

<hgroup>
    <h1>iPhone Web Application Development</h1>
    <h2>Home</h2>
</hgroup>
<nav>...</nav>

<hgroup>元素用于对一个部分的多个标题进行分组。标题的等级基于<h1><h6>,其中<h1>的等级最高,<h6>的等级最低。因此,突出显示的文本将使我们的<h1>内容高于我们的<h2>

还要注意,我们尚未使用<section>元素。但是,这个页面确实通过 W3C 标记验证服务(validator.w3.org/)进行验证。

我们可以通过将我们的<hgroup><nav>元素包装在<header>元素中来进一步描述页面,以提供页面的介绍性帮助。完成此操作后,您的代码应如下所示:

<header>
    <hgroup>... </hgroup>
    <nav>... </nav>
</header>

通过先前的代码,我们最终为我们的页面提供了一些结构。我们用一个主页眉描述我们的页面,用一个子页眉描述页面。我们还为页面提供了导航菜单,允许用户在应用程序之间导航。

创建页脚

现在让我们添加一个包含本书名称和版权日期的<footer>

<footer>
    <p>iPhone Web Application Development &copy; 2013</p>
</footer>

先前的代码基本上将与最近的分区祖先相关联。因此,页脚将与其前面的内容相关联,我们稍后会填充。此时,您的内容应该如下所示:

<div class="site-wrapper">
    <header>
        <hgroup>...</hgroup>
        <nav>...</nav>
    </header>
    <footer>...</footer>
</div>

清理部分

您可能想知道为什么我们不立即为包含<header><footer>元素的<div>元素使用<section>元素。在这种情况下,这并不一定有用,因为我们并没有创建一个元素内容会在大纲中列出的页面。这是 W3C 的建议,每个开发人员在决定使用<div>还是<section>元素时都应该意识到。最终,这取决于内容本身和团队希望创建的大纲。

现在我们已经为我们的页面创建了基本结构,我们可以继续为我们的其他应用程序做同样的事情。如果您希望查看最终版本,本书提供的代码将为您完成这些工作。

有了这个想法,我们将继续进行应用程序开发,确保在合适的时候使用语义代码。

构建我们的样式表

样式在我们构建的任何应用程序中都非常重要,特别是因为它是用户体验的第一个方面。在这一部分,我们将开始适当地构建我们的样式。

全局样式

首先,让我们打开位于CSS目录中的main.css文件。打开此文件后,您将看到默认的样式。在这一点上,让我们跳过这些内容,以创建我们自己的样式。随着我们继续开发我们的应用程序,我们将审查这些样式。

main.css中找到以下行:

/* ==========================================================================
   Author's custom styles
========================================================================== */

在这条注释之后,我们希望包括我们之前编写的语义代码的全局样式。

首先定义全局站点样式,比如背景颜色:

html{
    background: #231F20;
    border-top: 10px solid #FDFF3A;
    border-bottom: 5px solid #FDFF3A;
    width: 100%;
}

在之前的样式中,我们做了一些样式选择,比如设置背景颜色和一些边框。这里重要的部分是 HTML 元素的宽度被定义为 100%。这基本上允许我们的所有内容扩展到手机宽度的 100%。

定义我们的全局字体

然后我们需要在页面上定义整体字体。目前这只是基本的,可以根据我们的应用程序继续扩展设计,但现在先看看以下样式:

h1, h2, p, a {
    font-family: Arial, Helvetica, sans-serif;
    text-decoration: none;
}

h1, h2 {
    color: #A12E33;
    font-weight: bold;
    margin: 0;
    padding: 0;
}

h1 {
    font-size: 18px;
}

h2 {
    font-size: 14px;
    font-weight: normal;
}

p {
    color: #F15E00;
    font-size: 12px;
}

a,
a:visited {
    color: #F19C28;
}

在之前的代码中,你可以看到我们是从更高的层次向下工作的,这是对层叠样式表的基本理解。我们首先通过使用特定的字体系列并且没有装饰来定义我们的标题、锚点和段落。

当我们继续定义之前的样式时,我们开始更具体地定义每一个,标题没有填充或边距,有特定的颜色。然后,当我们继续往下看,我们可以看到每种类型的标题都有特定的字体大小,我们也对段落和锚点做同样的处理。

我们的页面布局

一旦我们定义了一些字体和站点样式,我们就为包含我们内容的<div>元素包含一些基本布局信息:

.site-wrapper {
    padding: 5px 10px 10px;
}

由于我们的元素自动缩放到屏幕宽度的 100%,我们告诉内容在顶部有5px的填充,在左右各有10px的填充,在底部有10px的填充。或者,我们可以写以下样式:

    padding-top: 5px;
    padding-left: 10px;
    padding-right: 10px;
    padding-bottom: 10px;

前者被称为快捷属性设置,被认为是最佳实践。

使用:before:after添加内容

由于我们还希望确保我们的第二个标题以某种形式有所区别,我们可以使用 CSS3 伪类选择器和属性来定义之前和之后的内容,如下所示:

hgroup h2:before,
hgroup h2:after {
    content: " :: ";
}

注意

请记住,Safari 3.2 及以上版本支持:before:after伪选择器。

之前的选择器针对<hgroup>元素内的<h2>元素,并在其之前和之后添加我们在属性中定义的内容,就像:before:after伪类选择器一样。

为我们的导航添加样式

接下来,让我们为我们的导航添加一些样式,使其看起来更加易用。

nav ul {
    padding: 0;
}

nav li {
    list-style: none;
}

nav a {
    display: block;
    font-size: 12px;
    padding: 5px 0;
}

在这里,我们去掉了<ul>元素的填充,然后移除了每个列表元素的默认样式选项。最后,我们通过将字体大小设置为12px并在每个锚点的顶部和底部添加填充来确保每个锚点正确显示,以便在 iPhone 上轻松选择。

最后,我们将为我们的页脚添加一些样式。

footer p {
    text-align: center;
}

非常简单,我们将段落在页脚中居中对齐。由于我们在字体部分定义了段落的默认样式,所以样式被应用了。

当之前的样式被正确应用时,你的结果应该类似于以下显示:

为我们的导航添加样式

响应式设计原则

响应式设计是我们移动应用程序的关键。考虑到许多移动体验现在超过了桌面上的体验,我们必须创建适应不断发展的技术环境的应用程序。幸运的是,HTML5 移动样板自带了我们可以修改的初步样式。

媒体查询的拯救

首先,让我们在css目录中打开main.css文件。

接下来,向文件底部滚动,你应该看到以下样式:

/* ==========================================================================
   EXAMPLE Media Queries for Responsive Design.
   Theses examples override the primary ('mobile first') styles.
   Modify as content requires.
========================================================================== */

@media only screen and (min-width: 800px) {
}

@media only screen and (-webkit-min-device-pixel-ratio: 1.5),
       only screen and (min-resolution: 144dpi) {}

尽管这些样式让我们起步,但对于 iPhone 开发,我们需要更多的定制。第一个媒体查询是专门针对平板设备的,第二个媒体查询帮助我们针对分辨率更高的设备,比如 iPhone 4。

我们想要做的是让这个更简单一些。因为我们只针对 iPhone,这就是我们可以用来替换之前代码的内容:

/* iPhone 4 and 5 Styles*/
@media only screen and (-webkit-min-device-pixel-ratio: 2) { }

先前的代码将针对 iPhone 4 和 5。我们通过检查设备上的-webkit-min-device-pixel-ratio属性来特别针对这两个设备,如果为真,意味着我们可以提供高清图形。

我们想要检查的另一个方面是我们在index.html页面中设置的视口设置。幸运的是,我们之前已经清理过这个,它应该有以下内容:

<meta name="viewport" content="width=device-width">

先前的代码片段基本上会根据设备的宽度调整我们的内容。

在这一点上,我们应该为以后在我们的应用程序中实现响应式样式做好准备。现在我们的样式已经为我们的应用程序设置好,并且足够通用以扩展,让我们开始添加脚本背后的框架。

响应式图像

图像是任何应用程序的极其重要的部分。它有助于展示产品的特点,并且举例说明您希望用户理解的信息。然而,今天各种各样的设备需要内容正确响应。除此之外,我们需要能够提供适合体验的内容,这意味着我们需要为高分辨率设备量身定制,以便最高质量的内容传达给受众。

有多种技术可以提供适当的内容。但是,您选择的技术取决于项目的要求。在这部分,我们将回顾根据内容和/或容器调整图像大小的传统响应式网页设计原则。

流体图像

在这种技术中,开发人员将所有图像的最大宽度设置为 100%。然后我们定义图像的容器相应调整。

流体宽度图像

要实现全宽度图像,我们可以这样做:

<body>
<img src="img/batman.jpeg" alt="Its Batman!">
</body>

标记很简单,基本上我们将图像包装到一个扩展所需全宽度的元素中。在这种情况下,body 的宽度将扩展到 100%。

接下来,我们将定义图像的样式如下:

img {
    max-width: 100%;
}

通过这简单的 CSS 声明,我们告诉我们的图像将其最大宽度设置为包含内容的 100%。这将根据设备宽度的变化自动调整图像大小,这对于使网站对用户设备响应是至关重要的。

全宽图像

在这种情况下,我们希望图像保持其全宽,但我们也需要相应地裁剪它。

为了实现这一点,我们可以简单地创建一个带有classdiv,在这种情况下我们添加一个overflow类:

<div class="overflow"></div>

然后我们可以创建保持图像全宽并根据内容调整大小的样式:

overflow {
    background: transparent url('img/somgimg.jpg') no-repeat 50% 0;
    height: 500px;
    width: 100%;
}

这有点复杂,但基本上我们使用background属性附加图像。关键在于确保使用 50%将其居中。高度属性只是为了显示图像,宽度告诉容器与其内容相关的 100%。

这是我们在实现传统响应式设计时使用的两种技术。当我们创建视频和图像库时,我们将在以后实现这些技术。

建立我们的 JavaScript 架构

在为应用程序建立 JavaScript 架构时,有很多要考虑的事情,包括近期或短期内可能的变化、安全性、易用性和实施、文档等等。一旦我们能回答我们所提出的各种问题,我们就可以决定采用哪种模式(模块、外观和/或中介等)。我们还需要知道哪种库或框架最适合我们,比如jQueryZepto.jsBackbone.jsAngular.js

幸运的是,为了在 iPhone 上提供有效的应用程序,我们将保持简单明了。我们将利用Zepto.js作为我们支持的库以保持轻量级。然后我们将通过创建遵循模块化模式的自定义 JavaScript 框架来构建 Zepto。

构建我们的应用功能

首先,让我们在我们喜欢的文本编辑器中打开我们的应用程序目录。

接下来,打开我们之前在 JavaScript 目录中创建的App.js文件。App.js文件应该是完全空的,不应该被包含在任何地方。这是我们将开始编写框架的地方。

给我们的应用程序命名空间

如果你是 JavaScript 的新手,你很可能大部分时间都是在全局作用域中编写代码——也许大部分 JavaScript 都是放在 script 标签中。虽然这可能实现了你的一些目标,但在开发大规模应用程序时,我们希望避免这样的做法。我们希望给我们的应用程序命名空间是为了可维护性、效率和可移植性。

让我们首先检查App命名空间;如果存在,我们将使用其中的内容,如果不存在,那么我们将创建一个空对象。以下代码展示了我们如何实现这一点:

var App = window.App || {};

立即调用的函数表达式

太棒了!我们正在检查App命名空间,现在让我们定义它。在检查后,让我们包含以下代码:

App = (function(){}());

先前的代码正在做几件事情,让我们一步一步来。首先,我们将App命名空间设置为所谓的立即调用的函数表达式IIFE)。我们实质上是创建了一个由括号包裹并在闭括号后立即调用的函数。

当我们使用之前的技术或 IIFE 时,我们创建了一个新的执行上下文或作用域。这有助于创建自包含的代码,希望不会影响站点上的其他代码。它保护我们,并帮助我们有效地遵循模块化模式。

让我们通过传入 window、document 和 Zepto 对象来扩展先前的功能,如下所示:

App = (function(window, document, $){
}(window, document, Zepto));

我知道这可能有点令人困惑,但让我们花点时间来思考一下我们在这里做什么。首先,我们在名为windowdocument$的函数中设置了一些参数。然后,在调用此方法时,我们传入了windowdocumentZepto。记住,我们之前讨论过这会创建一个新的作用域或执行上下文?嗯,这对我们很有用,因为现在我们可以传入任何可能是全局的对象的引用。

这对我们有什么用呢?嗯,想象一下,如果你想一遍又一遍地使用实际的Zepto对象,那将会有点累人。虽然输入Zepto并不难,但你可以将其命名空间为美元符号,保持简单。

使用严格模式

好的,我们已经设置好了我们的模块。现在让我们继续扩展它,包括use strict指令:

App = (function(window, document, $){
    'use strict';
}(window, document, Zepto));

这个指令通过改变 JavaScript 的运行方式来帮助我们调试我们的应用程序,允许某些错误被抛出而不是悄悄失败。

默认选项

默认选项是给你的代码库提供一些可扩展性的好方法。例如,如果我们想要自定义或缓存与应用程序相关的元素,那么以下是我们将使用的默认值:

var _defaults = {
'element': document.body,
    'name': 'App',
    'videoOptions': {},
    'audioOptions': {},
    'touchOptions': {},
    'formOptions': {},
    'locationOptions': {},
    'singlePageOptions': {}
};

让我们简要地看一下这些默认值。首先,我们将创建一个defaults变量,其中包含了我们应用程序的所有默认值。在其中,我们已经定义了一个默认位置,用于引用我们应用程序的'element'默认设置为document.body——这样就可以获取我们在DOM文档对象模型)中的 body 元素。然后,我们为我们的应用程序创建一个自定义名称叫做'App'。之后,我们创建了视频、音频、触摸、表单、位置和单页面应用程序的空对象——以后会逐渐扩展这些空对象。当我们继续阅读本书时,这些空对象将被扩展。

定义构造函数

现在我们需要在use strict指令之后定义我们的构造函数。这个构造函数将接受一个名为options的参数。然后我们将用参数options扩展默认值,并存储这些设置,以便以后可以检索。最后,我们将把'element'选项作为Zepto对象进行缓存。

function App(options) {
    this.options = $.extend({}, _defaults, options);
    this.$element = $(this.options.element);
}

这是先前代码的完成情况。首先,我们使用关键字this,它是对将要成为 App 实例的引用。因此,this是对象本身的上下文。希望这不会太令人困惑,并且随着我们的进行会变得清晰。在这种情况下,我们使用this来定义一个对象options,它将包含_defaults和我们传递给构造函数的任何自定义选项的合并内容。

注意,当我们将空对象或{}作为第一个参数传递给$.extend()时,我们告诉Zepto_defaultsoptions合并到一个新对象中,因此不会覆盖_defaults对象。当我们需要在将来对默认选项进行某种检查时,这是有用的。

一旦我们定义了选项,我们就使用this.$element缓存元素,其中$element前面只是为了我的参考,这样我就可以立即识别 Zepto 对象与普通 JavaScript 对象。

原型

好的,我们已经创建了我们的App命名空间,构建了一个 IIFE 来包含我们的代码,并定义了我们的构造函数。现在,让我们开始创建一些可以被访问的公共方法,使其更加模块化。但在我们这样做之前,让我们尝试理解 JavaScript 的prototype

prototype视为可以随时访问、修改和更新的活动对象。它也可以被视为指针,因为 JavaScript 将继续沿着链路向下查找对象,直到找到对象或返回undefined。原型只是一种将功能扩展到任何非普通对象的方法。

为了使事情变得更加混乱,我提到非普通对象具有原型。这些非普通对象将是数组、字符串、数字等。普通对象是我们简单地声明一个空对象,如下所示:

var x = {};

x变量没有原型,它只是一个键/值存储,类似于我们的_defaults对象。

如果您还没有理解原型,不要担心,这一切都是关于动手实践和获取一些经验。所以,让我们继续前进,让我们的应用程序开始工作。

此时,您的App.js文件应该如下所示:

var App = window.App || {};
App = (function(window, document, $){
    'use strict';
    var _defaults = {
        'element': document.body,
        'name': 'App',
        // Configurable Options for each other class
        'videoOptions': {},
        'audioOptions': {},
        'touchOptions': {},
        'formOptions': {},
        'locationOptions': {},
        'singlePageOptions': {}
    };
    function App(options) {
        this.options = $.extend({}, _defaults, options);
        this.$element = $(this.options.element);
    }
}(window, document, Zepto));

定义公共方法

现在我们需要通过在原型中输入来创建一些公共方法。我们将创建一个getDefaults方法,它返回我们的默认选项;toString将覆盖原生的toString方法,以便我们可以返回一个自定义名称。然后我们将创建初始化方法来创建我们的其他应用程序,我们将分别命名这些方法为initVideoinitAudioinitLocalizationinitTouchinitFormsinitSinglePage

App.prototype.getDefaults = function() {
    return _defaults;
};

App.prototype.toString = function() {
    return '[ ' + (this.options.name || 'App') + ' ]';
};

App.prototype.initVideo = function() {
    App.Video.init(this.options.videoOptions);
    return this;
};

App.prototype.initAudio = function() {
    App.Audio.init(this.options.audioOptions);
    return this;
};

App.prototype.initLocalization = function() {
    App.Location.init(this.options.locationOptions);
    return this;
};

App.prototype.initTouch = function() {
    App.Touch.init(this.options.touchOptions);
    return this;
};

App.prototype.initForms = function() {
    App.Forms.init(this.options.formOptions);
    return this;
};

App.prototype.initSinglePage = function() {
    App.SinglePage.init(this.options.singlePageOptions);
    return this;
};

此时,我们有几种方法可以在创建App实例时公开访问。首先,让我们回顾我们之前实现的代码,特别是这一行代码,它被复制,但根据init方法进行了定制:

App.Touch.init(this.options.touchOptions);

对于我们创建的每个init方法,我们都调用适当的应用程序,例如App.TouchApp.FormsApp.Video等。然后我们传递在构造函数中定义的选项,例如this.options.touchOptionsthis.options.formOptionsthis.options.videoOptions等。

请注意,我们尚未为 Video、Forms、Touch 等创建这些类,但我们将很快创建这些类。

返回我们的构造函数/函数

App.js中我们需要做的最后一件事是返回构造函数。因此,在之前定义的所有公共方法之后,包括以下代码:

return App;

这段代码虽然简单,但非常重要。让我们看一个简化版本的App.js,以更好地理解正在发生的事情:

App = (function(){
    function App() {}
    return App;
}());

如前所述,我们正在创建一个App命名空间,该命名空间设置为立即调用的函数表达式。当我们这样做时,在这个函数内部创建了一个新的作用域。

这就是为什么我们可以有一个名为App的函数或构造函数,而没有冲突或错误。但是如果您回忆起来,我们的函数App也是一个对象,就像 JavaScript 中的所有东西一样都是对象。这就是为什么当我们返回我们的函数App时,App命名空间被设置为构造函数。这样一来,您就可以创建多个App的实例,同时将代码集中在一个新的不可触及的范围内。

集成自定义模块模板

现在,为了将我们的架构其余部分放在一起,我们需要打开 JavaScript 目录中的每个其他App文件(/js/App)。

当我们打开这些文件时,我们需要粘贴以下模板,这是基于我们为App.js编写的脚本:

var App = window.App || {};

App.Module = (function(window, document, $){
    'use strict';

    var _defaults = {
        'name': 'Module'
    };

    function Module(options) {
        this.options = $.extend({}, _defaults, options);

        this.$element = $(this.options.element);
    }

    Module.prototype.getDefaults = function() {
        return _defaults;
    };

    Module.prototype.toString = function() {
        return '[ ' + (this.options.name || 'Module') + ' ]';
    };

    Module.prototype.init = function() {

        return this;
    };

    return Module;

}(window, document, Zepto));

当我们每个模板都放入后,我们必须将Module更改为适当的类型,即视频、音频、位置等。

一旦您完成了粘贴部分并更改了名称,基本的 JavaScript 架构就设置好了。

包含我们的脚本

最后需要处理的一项事项是将这个基本架构包含到每个index.html文件中。为了做到这一点,您需要在页面底部粘贴以下代码,就在helper.js包含之后:

<script src="img/App.js"></script>
<script src="img/App.Audio.js"></script>
<script src="img/App.Forms.js"></script>
<script src="img/App.Location.js"></script>
<script src="img/App.SinglePage.js"></script>
<script src="img/App.Touch.js"></script>
<script src="img/App.Video.js"></script>
<script src="img/main.js"></script>

我们基本上包含了框架的每个脚本。这里重要的是始终首先包含App.js。原因在于App.js创建了App对象并直接修改它。如果您在所有其他脚本之后包含它,那么App.js将覆盖其他脚本,因为它直接影响了App对象。

初始化我们的框架

我们需要处理的最后一项事项是main.js,其中包括我们应用程序的初始化。我们通过将我们的代码包装在 IIFE 中,然后将实例暴露给window对象来实现这一点。我们使用以下代码来实现这一点:

(function(window, document) {
    'use strict';

    var app = new App({
        'element': document.querySelector('.site-wrapper')
    });

    window.app = app;

}(window, document));

我们之前看到的是将 IIFE 分配给对象。这里我们看不到,因为这不是必要的。我们只是想确保我们的代码不会影响其余的代码,大多数情况下不会发生,因为这个项目的简单性。然而,作为最佳实践,我尽量在大多数情况下将我的代码自包含起来。

前面代码的不同之处在于我们在这里看到了我们框架的初始化:

var app = new App({
    'element': document.querySelector('.site-wrapper')
});

我们通过使用new关键字创建App的新实例,然后将一个对象传递给它,该对象将合并到我们之前编写的默认选项中。

注意

querySelector是一个附加到文档对象的 JavaScript 方法。该方法接受一个我们通常在 CSS 中使用的选择器,解析 DOM,并找到适当的元素。在这种情况下,我们告诉我们的应用程序将自己包含到具有site-wrapper类的元素中。

当我们最终初始化我们的应用程序时,我们将app附加到window对象上:

window.app = app;

这基本上使它可以在我们的应用程序中的任何地方访问,通过将其附加到window对象上。

我们现在已经完成了应用程序的框架。虽然我们没有在页面上操纵任何内容,也没有附加与用户输入相关的任何事件,但我们现在有了一个遵循最佳实践、高效、有效且易于访问的编码的坚实基础。

路由到移动站点

除非我们正在制作一个完全响应式的站点,其中站点的样式会根据设备的尺寸而变化,否则我们很可能需要对站点进行某种重定向,以便转到我们站点的移动友好版本。

幸运的是,这可以很容易地通过几种方式实现。虽然我不会详细介绍我们可以实现这一点的方式,但以下是一些在决定如何前进时可能有所帮助的技术。

提示

由于本书面向前端,将路由到移动站点的过程将简要涵盖 PHP 和 htaccess。我们总是可以在前端执行此过程,但出于 SEO 和页面排名的目的,应该避免这样做。

通过 PHP 进行重定向

在 PHP 中,我们可以进行以下类型的重定向:

<?php
    $iphone = strpos($_SERVER['HTTP_USER_AGENT'], "iPhone");
    if ($iphone) {
        header('Location: http://mobile.site.com/');
    }
?>

在这个例子中,我们正在创建一个变量$iPhone,并给它一个布尔值,true 或 false。如果在用户代理中找到iPhone,这可能是或可能不是最好的技术,然后我们告诉页面使用 PHP 中的header()方法进行重定向。

再次说明,还有其他方法可以实现这一点,但这将让你立即开始并运行起来。

通过 htaccess 进行重定向

我们还可以检测 iPhone,并通过在服务器上使用htaccess文件放置这些指令来进行重定向:

RewriteEngine on
RewriteCond %{HTTP_USER_AGENT} iPhone
RewriteRule .* http://mobile.example.com/ [R]

在这个例子中,我们正在启用重写引擎,创建一个重写条件,检查用户代理中是否有iPhone文本,然后如果条件满足就创建一个重写规则。

实质上,如果我们想要重定向到我们网站的移动版本,我们需要能够检测设备的类型,而不是它的尺寸,然后适当地进行重定向。

主屏幕图标

如果您正在创建一个应用程序,应该模仿成为本机应用程序的感觉,或者只是增加 Web 应用程序的体验,那么拥有代表您的应用程序的书签图标是一个好主意。

目前,我们支持在我们的index.html文件中使用以下标记:

<link rel="apple-touch-icon-precomposed" sizes="144x144" href="img/touch/apple-touch-icon-144x144-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="img/touch/apple-touch-icon-114x114-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="img/touch/apple-touch-icon-72x72-precomposed.png">
<link rel="apple-touch-icon-precomposed" href="img/touch/apple-touch-icon-57x57-precomposed.png">
<link rel="shortcut icon" href="img/touch/apple-touch-icon.png">

这些指令告诉 Safari 我们有适合相应设备的主屏幕图标。从上到下,我们支持视网膜显示屏、第一代 iPad 和非视网膜 iPhone、iPad Touch,甚至 Android 2.1+。

简单地说,我们有一个应用程序,用户可以将其添加到主屏幕的书签中,从而可以立即从主屏幕访问 Web 应用程序。

介绍我们的构建脚本

早些时候,我们安装了我们的构建脚本以及 HTML5 移动样板。现在,我们将通过为我们的目的定制它来进一步探索构建脚本。我们需要确保我们的样式、脚本、图像和标记都经过优化以进行部署。我们还需要设置多个环境来彻底测试我们的应用程序。

配置我们的构建脚本

让我们从为我们的需求配置构建脚本开始,这样我们将拥有一个为我们工作并立即启动的自定义构建脚本。

缩小和连接脚本

首先,让我们确保我们的脚本被连接和缩小。因此,让我们打开所有我们的index.html文件,并在页面底部用以下注释包装所有我们的脚本:

<!-- scripts concatenated and minified via ant build script-->
<script src="img/script.js"></script>
<!-- end scripts-->

先前的注释被ant任务或构建脚本用来查找所有正在使用的 JavaScript 文件,将它们连接并进行缩小。该过程还将在新优化的 JavaScript 文件上使用时间戳,以打破服务器上的缓存。

缩小和连接样式

默认情况下,Ant 构建脚本会缩小和连接我们的样式。但是,如果我们想保留标识应用程序特定部分的注释,比如视频或音频部分,那么我们需要做一些事情来保留这些注释。

注释可以用来标识一个部分,并且可以写成以下形式:

/*!
  Video Styling
*/

为每个样式表写上先前的注释。

然后,我们需要将每个样式表添加到项目属性中,以便可以通过 YUI 压缩器对每个样式表进行缩小。为此,我们需要打开位于/build/config目录中的project.properties文件。

然后找到以下行:

file.stylesheets  =

一旦我们找到了那一行,让我们按照以下方式添加所有我们的css文件:

file.stylesheets  = audio.css,forms.css,location.css,singlepage.css,touch.css,video.css

请注意,每个文件后面没有空格。这对于构建脚本的处理是必要的。

这是我们目前需要做的所有优化样式。

创建多个环境

通常,一个项目将在开发、测试和生产环境上运行。测试环境应该在配置方面最接近生产环境,这样我们就可以有效地重现可能出现的任何问题。

为了正确构建我们的环境,让我们通过构建我们的项目的过程。首先,让我们打开终端,这是一个允许你通过命令行界面与任何 Unix 风格计算机的操作系统进行交互的程序。

导航我们的目录

一旦终端启动并运行,我们必须导航到我们的项目。以下是一些可以帮助你导航的命令:

cd /somesite

上一个命令意味着我们正在从当前目录切换到Somesite目录,相对于你现在的位置。

cd ../somesite

这个命令告诉我们要更改目录,但是使用../向上一级,然后进入somesite目录。

举个更容易理解的例子,我的项目存在于/Sites/html5iphonewebapp。所以我可以使用以下命令进入我的项目:

cd /Users/somuser/Sites/html5iphonewebapp

这个命令将我的目录更改为我正在开发这个应用程序的项目。

构建我们的项目

一旦我们进入了项目目录,我们就可以开始构建我们的项目。默认情况下,Ant Build 脚本会创建一个生产环境,优化整个过程的所有部分。

ant build

这个命令告诉我们要构建我们的项目,并且如解释的那样,在一个名为publish的目录中创建我们的生产版本。当你运行该命令时,你会注意到你的终端会更新,让你知道构建过程中的哪个步骤。

一旦构建完成,你的目录结构应该类似于以下截图:

构建我们的项目

publish目录代表生产环境。你还会看到一个中间目录已经被创建;这是你的测试环境。

然而,假设你想要完全控制构建,并且想要手动创建你的环境,那么可以在终端中执行以下操作:

ant build -Denv=dev

这个命令,ant build –Denv=, 让我们定义我们想要构建的环境,并相应地执行。

我们现在有一个准备好进行构建的项目。在这个过程中有很多步骤,所以我鼓励你练习这个过程,以便为你和/或你的团队开发一个适合你们的良好架构和部署过程。

总结

在本章中,我们看到了如何为我们的项目使用 HTML5 移动样板,从下载默认包到根据我们的需求进行定制。我们还采取了一些简单的步骤来为我们的 JavaScript、CSS 和 HTML 建立一个坚实的架构。作为一个额外的奖励,我们还介绍了包括构建过程并为我们的项目进行定制。然后我们快速回顾了 JavaScript 应用程序的最佳实践,并给出了一些关于如何将用户引导到一个单独的移动站点的建议。我们现在已经准备好深入开发移动 Web 应用程序了。

第二章:集成 HTML5 视频

媒体分发对于任何 Web 应用程序都是必不可少的;提供改变用户感知的丰富体验。很多时候,我们被要求在网站上放一张静态图片,而其他时候,我们被要求包含视频画廊,允许用户通过某种独特的导航轻松切换视频。以前,我们可以使用 Flash 和其他基于插件的技术来实现这一点,但随着 HTML5 视频的广泛支持,我们现在有能力在不需要下载插件的情况下提供视频。

需要记住的一件事是,HTML5 视频和音频共享相同的规范。这是因为它们都被认为是媒体元素。这意味着视频和音频共享一些属性和方法,使得在我们的应用程序中实现它们更容易。

无论如何,让我们开始学习如何配置我们的服务器以正确地传送我们的视频。

在本章中,我们将涵盖:

  • 配置我们的服务器以进行视频分发

  • 实施 HTML5 视频

  • 监听 HTML5 视频事件

  • 创建一个完整的 JavaScript 视频库

  • 自定义 HTML5 视频控件

配置服务器

在实施视频之前,我们需要确保服务器知道我们将提供哪些媒体类型。现在这样做有助于避免以后出现网络错误时不知道原因的头痛。所以让我们开始吧。

视频格式

首先,我们需要知道我们将提供哪些文件类型。在我们的示例中,我们将使用 MP4,但允许支持的文件类型总是一个好主意。确保你的视频有 WebM、OGV 和 MP4 格式。但首先,在我们继续之前,让我们先了解一下这些格式。

提示

我们不会深入解释广泛支持的不同类型,但请记住,Theora、WebM 和 H.264/MPEG-4 是最广泛支持的格式。Theora 和 WebM 都是免费的,WebM 的开发得到了 Google 的支持。由于担心专利问题,Theora 在浏览器中的实现一直滞后,而 WebM 由于其免版税和开放的视频压缩功能,得到了 Mozilla Firefox、Google 和 Opera 的广泛支持。

当涉及到 H.264 时,情况变得有点棘手。尽管它是一个高质量、速度快、视频压缩的标准格式,但专利使其受到限制。因此,它在流行浏览器中的支持一直滞后。最终,每个浏览器都开始支持这种格式,但不是没有争议。

视频格式指令

接下来,根据服务器类型,我们需要包含特定的指令来允许我们的文件类型。在这个例子中,我们使用的是 Apache 服务器,因此以下语法:

AddType video/ogg .ogv
AddType video/mp4 .mp4
AddType video/webm .webm

前面的代码将被添加到服务器上的.htaccess文件或httpd.conf文件中。无论哪种方式,AddType指令都会告诉服务器它应该和可以提供哪些类型。因此,当我们逐行进行时,我们可以看到我们正在添加video/ogg类型和扩展名.ogv,我们也为 MP4 和 WebM 这样做。

采取这些初始步骤有助于我们在使用 HTML5 在网站上实现视频时避免任何网络问题。如果你想知道我们如何使用这些类型,不用担心,下一节我们将详细介绍。

一个简单的 HTML5 视频

我们一直渴望在我们的 Web 应用程序中做一些酷炫的东西,所以让我们开始吧。让我们从在我们的网站上以最简单的方式包含一个视频开始,而不涉及任何复杂的交互!

单一视频格式

首先,让我们打开位于Chapter 2项目的video子目录中的index.html文件。如果你跳过了第一章,不要担心,Chapter 2的源文件会跟随并帮助你继续前进。

一旦我们有了我们的index.html文件,我们希望在内容区域中包含video元素,在<header>元素之后。这很简单,我们可以这样做:

<video src="img/testvid.mp4" controls preload></video>

前面的代码类似于图像元素。我们定义了一个src属性,指示浏览器在哪里找到视频,然后我们定义了controlspreload属性,这些属性指示浏览器显示默认的本机控件并预加载视频。简单吧?

支持多种格式

这就是我们在网站上放置视频所需要的一切,但当然,事情并不总是那么简单。正如我们之前讨论的,浏览器可以支持我们指定的格式中的一个或一个都不支持。当然,现在我们有很好的浏览器支持,但我们要确保我们的应用程序是稳固的,所以我们需要确保我们传递适当的文件。为了做到这一点,我们可以修改上面的代码如下:

<video poster="testvid.jpg" controls preload>
    <source src="img/testvid.webm" type='video/webm'/>
    <source src="img/testvid.ogv" type='video/ogg'/>
    <source src="img/textvid.mp4" type='video/mp4'/>
    <p>Fallback Content</p>
</video>

在这里,我们介绍了一个新属性,posterposter属性是我们在视频开始时显示的图像,或者在视频无法加载时显示的图像。当我们将源元素移动到video元素内部时,事情变得有点复杂。但是,如果我们检查一切,我们基本上是在定义多个源视频及其类型。然后浏览器将选择适当的视频进行显示。可能会让你困惑的是包含Fallback Content文本的段落元素。如果一切都失败了,或者浏览器不支持 HTML5 视频,这就是它的作用。

如果这有点令人困惑,不要太担心,因为 iPhone 的移动 Safari 支持 MP4,而且这对你的应用程序来说已经足够了。所以如果我们想保持简单,我们可以在我们的 iPhone 应用程序中使用以下代码,这也正是我们在本书中所做的:

<video src="img/testvid.mp4" controls preload>
    <p>Video is not supported in your browser.</p>
</video>

现在我们的应用程序中有一个简单的视频播放,我们可能想要捕捉视频的事件。

监听 HTML5 视频事件

很可能你会想要完全控制你的应用程序,或者至少监视可能发生的事情。出于各种原因,你通常会发现自己附加事件或监听事件。从跟踪到增强体验,事件是我们如何在页面上驱动交互的方式。使用 HTML5 视频,我们可以使用本机浏览器从头到尾监视视频的状态。你有机会监听视频何时加载完成以及用户何时暂停视频。

让我们回顾一下我们可以使用的事件。你会发现,我们用于视频的事件也可以转移到音频上。这是因为,正如我们之前学到的那样,视频和音频元素都被归类为 HTML5 规范中的媒体元素。这是我们可以使用的事件表:

事件名称 条件
loadedmetadata 已确定媒体资源的持续时间和尺寸。
loadeddata 现在可以首次渲染媒体数据。
canplay 媒体数据的播放可以恢复。
seeking 媒体资源的寻找属性已设置为 true。
seeked 媒体资源的寻找属性已设置为 false。
play 元素未暂停。当play()方法已返回或autoplay属性已导致元素开始播放时触发。
ended 已到达媒体资源的结尾并且播放已停止。
pause pause()方法已返回,元素已暂停。
timeupdate 媒体资源的播放位置以某种方式发生了变化。
volumechange 当音量或静音属性发生变化时触发。

规范定义了更多的事件,但这些是我们将从先前简单实现中监听的事件。所以让我们开始吧。

视频标记回顾

首先,打开video目录中的index.html文件。在这个文件中,您必须确保您的内容看起来像下面这样:

<div class="site-wrapper">
    <header>
        ....
    </header>
    <div class="gallery">
                    <video src="img/testvid.mp4" controls preload></video>
    </div>
    <footer>
        ...
    </footer>
</div>

不要注意省略号,这只是为了使代码在文本中更短。您要确保的是,您有来自上一节的简单的<video>元素实现。

附加视频事件

现在开始有趣的部分。让我们开始扩展我们的 JavaScript 以包括监听器。让我们打开位于App文件夹下/js目录中的App.Video.js文件。如果您没有从我们的架构章节一直跟着做,不用担心,对您来说重要的是要理解我们为应用程序创建了一个结构,App.Video.js文件将包含视频应用程序的所有功能。

找到App.Video类的构造函数。这应该在您的文本编辑器的第 16 行,并且当前应该看起来像下面这样:

function Video(options) {
    // Customizes the options by merging them with whatever is passed in
    this.options = $.extend({}, _defaults, options);

    //Cache the main element
    this.$element = $(this.options.element);
}

再次回顾一下,我们将一个称为options的对象传递给我们的构造函数。从这里,我们创建一个名为options的属性,用于Video的实例,这个属性将使用 Zepto 的 extend 方法设置为选项和默认值的扩展或合并版本。然后,我们缓存通过合并选项发送的元素。这可能有点令人困惑,但在 JavaScript 应用程序中,这是一个非常公认的模式。

由于我们已经验证了我们的构造函数存在并且运行良好,现在我们想要添加先前的监听器。我们可以使用本地的addEventListener方法轻松地做到这一点,如下所示:

this.options.element.addEventListener('canplay', function(e){ 
    console.log('video :: canplay'); 
});

this.options.element.addEventListener('seeking', function(e){ 
    console.log('video :: seeking'); 
});

this.options.element.addEventListener('seeked', function(e){ 
    console.log('video :: seeked'); 
});

this.options.element.addEventListener('ended', function(e){ 
    console.log('video :: ended'); 
});

this.options.element.addEventListener('play', function(e){ 
    console.log('video :: play'); 
});

this.options.element.addEventListener('pause', function(e){ 
    console.log('video :: pause'); 
});

this.options.element.addEventListener('loadeddata', function(e){ 
    console.log('video :: loadeddata'); 
});

this.options.element.addEventListener('loadedmetadata', function(e){ 
    console.log('video :: loadedmetadata'); 
});

this.options.element.addEventListener('timeupdate', function(e){ 
    console.log('video :: timeupdate'); 
});

这里有几件事情需要注意。首先,我们使用this.options.element而不是缓存版本的this.$element。我们这样做是因为我们实际上想要元素而不是Zepto对象。其次,我们调用addEventListener并传递两个参数。第一个参数是一个字符串,定义了我们要监听的事件。第二个参数是一个回调函数,每当我们在参数一中指定的事件触发时都会被调用。

提示

请注意,我们正在使用console.log()方法。它类似于alert(),但没有那么烦人。它有助于更好地调试,并输出到一个控制台,让我们跟踪所有的日志输出。在继续之前,使用这种方法是调试我们的应用程序和测试功能的好方法。

您的构造函数现在应该如下所示:

function Video(options) {
    // Customizes the options by merging them with whatever is passed in
    this.options = $.extend({}, _defaults, options);

    // Cache the main element
    this.element = options.element;
    this.$element = $(this.options.element);

    this.options.element.addEventListener('canplay', function(e){ 
        console.log('video :: canplay'); 
    });

    this.options.element.addEventListener('seeking', function(e){ 
        console.log('video :: seeking'); 
    });

    this.options.element.addEventListener('seeked', function(e){ 
        console.log('video :: seeked'); 
    });

    this.options.element.addEventListener('ended', function(e){ 
        console.log('video :: ended'); 
    });

    this.options.element.addEventListener('play', function(e){ 
        console.log('video :: play'); 
    });

    this.options.element.addEventListener('pause', function(e){ 
        console.log('video :: pause'); 
    });

    this.options.element.addEventListener('loadeddata', function(e){ 
        console.log('video :: loadeddata'); 
    });

    this.options.element.addEventListener('loadedmetadata', function(e){ 
        console.log('video :: loadedmetadata'); 
    });

    this.options.element.addEventListener('timeupdate', function(e){ 
        console.log('video :: timeupdate'); 
    });
}

初始化我们的视频

现在我们已经定义了一个初步的视频类,我们需要初始化它。所以让我们继续打开main.js,我们的初始化代码应该在那里。它应该看起来像这样:

(function(window, document) {
    'use strict';

    // Create an instance of our framework
    var app = new App({
        // Custom Option, allowing us to centralize our framework
        // around the site-wrapper class
        'element': document.querySelector('.site-wrapper')
    });
    // Expose our framework globally
    window.app = app;
}(window, document));

我们在上一章中创建了这个,但让我们简要地回顾一下。在这里,我们创建了一个闭包,传递了windowdocument对象。在内部,我们设置解释器严格地读取我们的代码。然后我们创建了App类的一个实例,然后将其暴露给window对象。

现在我们需要添加Video类的初始化。为此,让我们在声明App的新实例之后放入以下代码片段,如下所示:

new App.Video({
    'element': document.getElementsByTagName('video')[0]
});

这个片段创建了App.Video类或Video类的一个新实例,并传入一个包含元素的简单对象。我们通过使用附加到document对象的getElementsByTagName方法来检索元素。我们告诉方法查找所有的视频元素。有趣的部分是[0],它告诉查找结果只获取返回的数组中的第一个元素。

如果我们加载页面并测试视频,我们应该在控制台中看到我们之前定义的日志输出,类似于以下的截图:

初始化我们的视频

视频日志输出

我们已经开始了Video类的初步工作,从事件到初始化。然而,如果我们要使它可重用于我们的应用程序,并且如果我们想要扩展其功能,我们需要稍微整理一下。因此,让我们花一些时间创建一个完全功能的 JavaScript 视频库,它将在我们的 iPhone 网络应用程序中工作。

创建一个 JavaScript 视频库

目前,我们有一个非常简单的Video类,它缓存一个元素,然后附加了多个由 HTML5 媒体元素规范定义的事件。我们已经定义了视频播放器的基本要素,现在需要进一步抽象,以便更好地重用和管理。遵循一些约定并创建一个灵活的框架将帮助我们更快更有效地移动。

首先,让我们考虑一些可能需要从这个类中得到的东西:

  • 一个附加适当事件的事件方法

  • 可以定义的回调方法,例如onPlayonPauseonEnded

  • 可以从实例外部调用的公共方法

  • 类似于 jQuery 的可链接方法,您可以依次调用一个方法,例如fadeIn().fadeOut().show().hide()

拥有一个抽象类行为的项目列表是建立一个坚实框架或库的正确方向。现在让我们开始创建回调。

集中我们的事件

首先,让我们解决如何为我们的Video类附加事件。以前,我们将这些事件添加到构造函数中,虽然这是一种不错的技术,但可以通过指定一个处理事件附加到Video对象实例的函数来改进。

那么,让我们在Video类中创建一个名为attachEvents的私有方法,该方法只能在App.Video闭包或 IIFE 中访问。当我们创建我们的attachEvents方法时,我们应该将所有的事件处理程序放在其中。然后我们希望在初始化this.$element之后调用attachEvents方法。完成后,您的代码应该如下所示:

function Video(options) {
    this.options = $.extend({}, _defaults, options);

    // Cache the main element
    this.element = options.element;
    this.$element = $(this.options.element);

    attachEvents();
}

function attachEvents() {
    // All your event handlers go here
}

提示

在之前的代码中,attachEvents()函数将包含我们之前创建的事件处理程序。为了简洁起见,我现在省略了它们。

现在,如果我们运行这段代码,很可能会遇到一些错误。这实际上是正常的,被称为作用域问题。为了解决这个问题,首先我们需要了解幕后发生了什么。

JavaScript 中的作用域

如果您是 JavaScript 的新手,作用域很可能会在早晚困扰您。如果您在 JavaScript 中处于中级或高级水平,您可能仍然会遇到作用域问题。这是完全正常的,我们都会遇到这种情况。无论如何,让我们拿出当前的Video类并分析一下,以便在上下文中理解作用域。

JavaScript 具有函数级作用域,这意味着每次创建新函数时,都会创建一个新的作用域。作用域可能会相当令人困惑,但通过实践会变得更容易。现在把作用域看作是对当前位置的引用,它知道自己和它的环境,但不知道在它内部新创建的作用域。如果听起来令人困惑,当你开始时可能会有些困惑。但让我们通过一些代码来更好地理解一下。

所以,让我们从全局范围开始:

// Global Scope
var x = 10;
(function($){ 
    // New Scope
    console.log(x);
}(Zepto));

在这个例子中,App.Video的简化版本中,我们可以看到全局作用域在闭包周围。当我们创建一个闭包时,会创建一个新的作用域。这里很酷的一点是,闭包外的任何东西都可以被访问到。因此,当我们在闭包内部执行console.log时,我们应该得到10

每当你创建一个新的函数作用域时,你可以传递参数,本质上是给你发送的值命名空间。在这种情况下,我们传入Zepto,并告诉新的函数作用域在该作用域内将美元符号定义为Zepto的实例。希望这能更清楚地解释作用域,如果不清楚,不要担心;理解这个概念需要时间和耐心。

因此,我们事件处理程序的问题在于attachEvents内的新函数作用域没有对this.options的引用。由于新的作用域,关键字this相对于窗口对象,而不是Video对象。它没有引用的原因是因为我们的构造函数是一个完全不同的作用域,它们之间没有交流。为了解决这个问题,我们可以使用.call()方法,它将改变this关键字的引用,以反映Video函数作用域。可以通过修改attachEvents的调用来实现:

attachEvents.call(this);

如果你现在运行你的代码,你不应该得到任何错误。如果有的话,看看代码的最终版本,进行比较并找出问题所在。

暴露功能

在本章的后面,我们将探索自定义用户界面,帮助我们覆盖视频播放器的默认功能。然而,为了做到这一点,我们需要确保一些功能是公开的。在 JavaScript 中,为了使方法在闭包之外公开,我们需要将方法附加到class的原型上——在这种情况下是Video

我们已经看到我们的所有类中都暴露了两个方法;这些包括getDefaults和重写函数toString。让我们通过添加playpausestopmuteunmutefullscreen方法来扩展原型。

Video.prototype.play = function() {
    return this;
}

Video.prototype.pause = function() {
    return this;
}

Video.prototype.stop = function() {
    return this.pause();
}

Video.prototype.mute = function() {
    return this;
};

Video.prototype.unmute = function() {
    return this;
};

Video.prototype.fullscreen = function() {
    return this;
}

我相信你已经注意到这些方法中缺少了代码,没关系。我们想要理解的是,我们可以扩展Video原型,并且可以通过在return this行中返回实例来为我们的方法添加链式调用。

让我们开始为我们的方法添加功能,从play开始:

Video.prototype.play = function() {
    this.element.play();

    return this;
}

在这里,我们通过调用play方法获取了我们在Video构造函数中缓存的元素。你可能想知道这个play方法是从哪里来的?嗯,HTML5 规范为媒体元素(包括视频和音频)定义了一个play方法。因此,我们可以使用this.element.play()来调用这个方法。我们可以用同样的方法来调用pause方法:

Video.prototype.pause = function() {
    this.element.pause();
    return this;
}

再次,我们有一个由 HTML5 规范定义的暂停媒体元素的方法。当我们定义一个stop方法时,事情变得有点混乱:

Video.prototype.stop = function() {
    return this.pause();
}

和以前一样;我们实际上没有做任何改变。让我解释一下,规范没有定义stop方法,所以我们需要创建一个方法来提供这个功能。但这并不太困难,因为我们已经定义了一个执行类似操作的pause方法。所以我们需要做的就是调用this.pause(),因为这是Video的一个实例,我们已经定义了一个pause方法。这里的巧妙之处在于我们不需要返回this,因为暂停方法已经返回了this,所以我们只需要返回调用pause方法的结果。我知道这有点令人困惑,但随着时间的推移,如果这是你第一次这样做,它会变得清晰起来。

现在,我们来看看我们的muteunmute方法:

Video.prototype.mute = function() {
    this.element.muted = true;
    return this;
};
Video.prototype.unmute = function() {
    this.element.muted = false;
    return this;
};

这些方法的唯一区别在于我们在视频元素上设置了一个属性为false。在这种情况下,我们将静音属性设置为truefalse,取决于你调用的方法。

这里的事情变得有点复杂:

Video.prototype.fullscreen = function() {
    if (typeof this.element.requestFullscreen === 'undefined') {
        this.element.webkitRequestFullScreen();
    } else {
        this.element.requestFullscreen();
    }
    return this;
}

这有点复杂,可能有点令人沮丧。相信我,行业内的许多人都感到痛苦。我们需要理解的是,我们正在处理的浏览器 Safari 是运行在一个名为 WebKit 的开源网络浏览器引擎上。

WebKit 非常受欢迎并得到广泛支持。问题在于,虽然它在实现最新和最好的功能方面做得很好,但其中许多是实验性的,因此它们具有前缀。我们在 CSS(层叠样式表)中经常看到这一点,使用-webkit。但在 JavaScript 中,我们也面临相同的问题,webkit[standardMethodName]

虽然这可能很棒,但我们需要确保我们对剥离该前缀的新版本具有向后兼容性。这就是为什么在上一个方法中,我们对标准方法名称进行检查,如果不存在,我们使用-webkit前缀。否则,我们使用标准版本。

集成回调

回调在任何库或框架中都非常有用,您可能已经在使用 jQuery 或其他一些流行框架时看到过类似的东西。实质上,回调是在方法完成后调用的方法。例如,在Zepto方法中,fadeout接受两个参数,第一个是速度,第二个参数是在淡出完成时调用的函数。可以如下所示:

$('.some-class').fadeout('fast', function(){
    // Do something when fading is complete
});

在上一个代码中的第二个参数不仅是一个回调函数,还是一个匿名函数。匿名函数只是一个没有名称的函数。在这种情况下,它在每次fadeOut()效果完成时执行。我们可以将上一个代码重写如下:

$('.some-class').fadeOut('fast', someFadeOutFunc);
function someFadeOutFunc(){
    // Do something when fading is complete
}

由于我们创建了一个名为someFadeOutFunc的方法,当fadeOut完成时,我们只需调用该函数,而不是创建一个新函数。从架构的角度来看,这更有效和可管理。

创建回调的第一步是定义我们在代码中可能需要回调的位置。在这种情况下,我们可能希望为视频播放器中采取的每个操作创建一个回调,因此我们将创建以下回调:

  • onCanPlay

  • onSeeking

  • onSeeked

  • onEnded

  • onPlay

  • onPause

  • onLoadedData

  • onLoadedMetaData

  • onTimeUpdate

  • onFullScreen

好的,现在我们知道我们的代码中需要哪些回调,让我们在attachEvents方法之前的构造函数中实现它们:

this.callbacks = {
    'onCanPlay': function(){ },
    'onSeeking': function(){},
    'onSeeked': function(){},
    'onEnded': function(){},
    'onPlay': function(){},
    'onPause': function(){},
    'onLoadedData': function(){},
    'onLoadedMetaData': function(){},
    'onTimeUpdate': function(){},
    'onFullScreen': function(){}
};

我们在这里所做的是将一个名为callbacks的属性附加到Video的实例上。该属性包含一个对象,该对象为我们想要实现的每个回调设置了键/值对,值是一个空的匿名函数。

扩展回调

尽管我们可以在类中使用回调,但问题在于它们不具有可扩展性,这意味着使用您的Video类的开发人员将无法扩展您的回调。为了使它们具有可扩展性,我们需要将它们放在我们的_defaults对象中:

var _defaults = {
    'element': 'video',
    'name': 'Video',
    'callbacks': {
        'onCanPlay': function(){ },
        'onSeeking': function(){},
        'onSeeked': function(){},
        'onEnded': function(){},
        'onPlay': function(){},
        'onPause': function(){},
        'onLoadedData': function(){},
        'onLoadedMetaData': function(){},
        'onTimeUpdate': function(){},
        'onFullScreen': function(){}
    }
};

缺点是现在我们需要使用this.options.callbacks来访问我们想要的回调。通过在我们的构造函数中执行以下操作,可以轻松解决这个问题:

this.callbacks = this.options.callbacks;

这仍然允许我们访问回调,但只能从扩展对象中访问。

使用回调

现在我们有了回调,并且已经使它们具有可扩展性,我们可以进入并将它们集成到我们的事件处理程序中。但首先,我们需要将我们的事件处理程序作为私有方法放在这个Video类中,并按以下方式调用我们的自定义回调:

function onCanPlay(e, ele) {
    this.callbacks.onCanPlay();
}

function onSeeking(e, ele) {

    this.callbacks.onSeeking();
}

function onSeeked(e, ele) {

    this.callbacks.onSeeked();
}

function onEnded(e, ele) {

    this.callbacks.onEnded();
}

function onPlay(e, ele) {

    this.callbacks.onPlay();
}

function onPause(e, ele) {

    this.callbacks.onPause();
}

function onLoadedData(e, ele) {
    this.callbacks.onLoadedData();
}

function onLoadedMetaData(e, ele) {
    this.callbacks.onLoadedMetaData();
}

function onTimeUpdate(e, ele) {
    this.callbacks.onTimeUpdate();
}

在这一点上,我们已经完全将我们的回调集成到我们的库中。现在,我们只需要通过修改attachEvents处理程序来调用它们,如下所示:

function attachEvents() {
        var that = this;
        this.element.addEventListener('canplay', function(e){ onCanPlay.call(that, e, this);  });
        this.element.addEventListener('seeking', function(e){ onSeeking.call(that, e, this); });
        this.element.addEventListener('seeked', function(e){ onSeeked.call(that, e, this);  });
        this.element.addEventListener('ended', function(e){ onEnded.call(that, e, this);  });
        this.element.addEventListener('play', function(e){ onPlay.call(that, e, this);  });
        this.element.addEventListener('pause', function(e){ onPause.call(that, e, this);  });
        this.element.addEventListener('loadeddata', function(e){ onLoadedData.call(that, e, this);  });
        this.element.addEventListener('loadedmetadata', function(e){ onLoadedMetaData.call(that, e, this);  });
        this.element.addEventListener('timeupdate', function(e){ onTimeUpdate.call(that, e, this);  });
    }

这里实施了一些概念。首先,我们用之前定义的实际私有方法替换了console.logs。其次,我们使用call方法通过传入that来更改private方法的范围,然后将eventelement作为参数发送进去。

将所有内容联系起来

我们拥有一切所需的东西,如事件处理程序、公开功能、回调,甚至可链接的方法。这都很好,但现在我们需要让它起作用。这就是魔法发挥作用的地方。

要验证,您的Video类应该如下所示:

var App = window.App || {};

App.Video = (function(window, document, $){
    'use strict';

    var _defaults = { ... };

    // Constructor
    function Video(options) {
        this.options = $.extend({}, _defaults, options);

        this.element = options.element;
        this.$element = $(this.options.element);

        this.callbacks = this.options.callbacks;

        attachEvents.call(this);
    }

    // Private Methods
    function attachEvents() { ... }

    // Event Handlers
    function onCanPlay(e, ele) { ... }
    function onSeeking(e, ele) { ... }
    function onSeeked(e, ele) { ... }
    function onEnded(e, ele) { ... }
    function onPlay(e, ele) { ... }
    function onPause(e, ele) { ... }
    function onLoadedData(e, ele) { ... }
    function onLoadedMetaData(e, ele) { ... }
    function onTimeUpdate(e, ele) { ... }

    // Public Methods
    Video.prototype.getDefaults = function() { ... };
    Video.prototype.toString = function() { ... };
    Video.prototype.play = function() { ... }
    Video.prototype.pause = function() { ... }
    Video.prototype.stop = function() { ... }
    Video.prototype.mute = function() { ... };
    Video.prototype.unmute = function() { ... };
    Video.prototype.fullscreen = function() { ... }

    return Video;

}(window, document, Zepto));

注意

请注意,上一段代码中的省略号表示应该有功能。由于页面数量的限制,我们只能展示到目前为止代码的简要摘要。如果您需要查看已完成的工作,请查看前面的部分或查看本书附带的源代码。

如果您的文件看起来像这样,那就太完美了!如果它看起来不太像这样,不要担心,这就是为什么我们在这本书中附上了源代码。在这一点上,我们已经准备好在我们的页面上初始化这个库了。

让我们打开main.js文件;该文件应该位于js目录下。我们需要进行以下添加:

new App.Video({
    'element': document.getElementsByTagName('video')[0],
    'callbacks': {
        'onCanPlay': function(){ console.log('onCanPlay'); },
        'onSeeking': function(){ console.log('onSeeking'); },
        'onSeeked': function(){ console.log('onSeeked'); },
        'onEnded': function(){ console.log('onEnded'); },
        'onPlay': function(){ console.log('onPlay'); },
        'onPause': function(){ console.log('onPause'); },
        'onLoadedData': function(){ console.log('onLoadedData'); },
        'onLoadedMetaData': function(){ console.log('onLoadedMetaData'); },
        'onTimeUpdate': function(){ console.log('onTimeUpdate'); },
        'onFullScreen': function(){ console.log('onFullScreen'); }
    }
});

让我们快速浏览一下。首先,我们创建一个新的App.Video实例,传入一个参数——一个简单的对象。其次,我们传入的对象包含两个对象:我们想要在页面上的video元素,以及一个覆盖默认值的回调对象。第一个参数使用内置方法getElementsByTagName来获取video元素的所有实例,然后我们使用[0]获取找到的第一个实例。这是因为该方法返回一个数组。第二个参数callbacks包含我们想要在App.Video实例上调用的函数回调。在这些方法中,我们只想要记录被调用的方法。

从这里开始,当实例被初始化时,我们定义的Video库将合并我们传入的简单对象,并从那里开始。几乎就像魔术一样,除了我们已经创建了它。

最后要注意的一点是,确保我们只在视频页面上初始化视频。如果我们在应用程序的非视频页面上,这段代码将产生一个错误。这是因为没有视频元素,我们也没有添加错误检测。这是一个很好的功能,但本书不涵盖这部分。因此,让我们在main.js中做以下操作:

if(document.querySelector('video') !== 'null') {
    new App.Video({
        'element': document.getElementsByTagName('video')[0],
        'callbacks': {
            ...
        }
    });
}

在前面的代码中,我们将我们的初始化代码包装在一个if语句中,检查我们是否在视频页面上。我们进行检查的方式是使用文档对象上的内置方法querySelector。这个方法接受一个 CSS 类型的选择器,在这种情况下,我们发送video选择器,告诉它获取所有video元素的实例。如果返回的结果不是 null,那么我们就初始化。

现在我们不需要对标记做任何事情,这段代码将运行,我们应该没问题。如果由于某种原因您遇到任何错误,请查看本书附带的源代码。接下来,让我们考虑覆盖视频播放器的默认控件,以便更好地控制功能。

自定义 HTML5 视频控件

我们可能希望对视频控件有更多的输入,从样式到视频功能,比如添加停止按钮。为了做到这一点,我们需要稍微修改我们的标记。我们应该对视频做以下操作:

<div class="video-container">
    <video src="img/testvid.mp4" controls preload>
        <p>Video is not supported in your browser.</p>
    </video>
</div>

我们在这里所做的只是在video元素周围添加了一个包含div的类,并给它添加了一个video-container的类。现在我们想要为video元素添加一些响应式样式,所以让我们打开video.css并添加以下样式:

video {
    display: block;
    width: 100%;
    max-width: 640px;
    margin: 0 auto;
}

.video-container {
    width: 100%;
}

第一个选择器将应用于页面上的所有video元素,并告诉每个元素相对于其容器具有 100%的宽度,但最大宽度为640px。边距属性有助于使其在页面或容器中居中。下一个选择器video-container只指定宽度为 100%。这种样式将相应地调整播放器的大小;您可以通过调整浏览器大小来查看。

在这个例子中,我们将使用锚元素来使用基本控件。请记住,您可以使用任何类型的样式或标记来设计您的控件,只要记住我们已经在我们的Video类中公开了视频播放,所以为了简洁起见,我们将演示如何使用锚元素来实现这一点。

在我们的video-container中,我们想要附加以下标记:

<div class="video-controls">
    <div class="vc-state">
        <a class="vc-play vc-state-play" href="#play">Play</a>
        <a class="vc-pause vc-state-pause" href="#pause">Pause</a>
    </div>
    <div class="vc-track">
        <div class="vc-progress vc-track-progress"></div>
        <div class="vc-handle vc-track-handle"></div>
    </div>
    <div class="vc-volume">
        <a class="vc-unmute vc-volume-unmute" href="#volume">Volume On</a>
        <a class="vc-mute vc-volume-mute" href="#volume">Volume Off</a>
    </div>
    <a class="vc-fullscreen" href="#fullscreen">Fullscreen</a>
</div>

前面的标记是我们将用于控件的标记。它们非常直观,但让我们回顾一下这里做出的一些决定。首先,我们有一个带有video-controls类的周围div,以帮助定义我们所有控件的存在位置。其次,每种类型的控件都以vc为前缀,代表视频控件。第三,在这个例子中,我们有四种类型的控件,即状态、轨道、音量和全屏控件。最后一点是,其中一些控件具有显示/隐藏功能,例如,播放和暂停只有在其他控件取消时才会显示。

对于样式,我们可以将以下样式添加到video.css文件中:

.video-controls {
    margin: 12px auto;
    width: 100%;
    text-align: center;
}

.video-controls .vc-state,
.video-controls .vc-track,
.video-controls .vc-volume,
.video-controls .vc-fullscreen {
    display: inline-block;
    margin-right: 10px;
}

.video-controls .vc-fullscreen {
    margin-right: 0;
}

.video-controls .vc-state-pause,
.video-controls .vc-volume-unmute {
    display: none;
}

在这一部分的样式中,我们将所有视频控件样式自包含到video-controls类中。这有助于以模块化的方式维护样式。再次遵循响应式设计原则,我们告诉控件宽度为 100%。然后,每种类型的控件都设置为显示为内联块,类似于float。最后,我们告诉所有默认控件,它们不应该在初始时显示,所以设置为display: none。现在,我们需要为我们的控件添加交互性。

首先,让我们创建一个遵循整个框架的App.VideoControls类:

var App = window.App || {};

App.VideoControls = (function(window, document, $){
    'use strict';

    var _defaults = { };

    function VideoControls(ele, options) {
        this.options = $.extend({}, _defaults, options);
        this.ele = ele;
        this.$ele = $(ele);

        this.init();
    }
    return VideoControls;

}(window, document, Zepto));

正如你所看到的,这里并没有太大的区别。唯一的区别是现在有一个被调用的init方法。这是为了将初始化功能分离到其他地方,以便构造函数不完全被代码填满。现在我们需要添加以下默认值:

var _defaults = {
    // Supported Features
    'features': ['play', 'pause', 'fullscreen', 'mute', 'unmute', 'playpause'],
    // State of the controls
    'state': 'paused',
    // State of the sound
    'sound': 'unmuted',
    // Customizable Classes or Classes associated with Elements
    'classes': {
        'state': {
            'holder': 'vc-state',
            'play': 'vc-state-play',
            'pause': 'vc-state-pause'
        },
        'track': {
            'holder': 'vc-track',
            'progress': 'vc-track-progress',
            'handle': 'vc-track-handle'
        },
        'volume': {
            'holder': 'vc-volume',
            'mute': 'vc-volume-mute',
            'unmute': 'vc-volume-unmute'
        }
    },
    // Customizable Events or Dispatched Events
    'events': {
        'onPlay': 'videocontrols:play',
        'onPause': 'videocontrols:pause',
        'onFullScreen': 'videocontrols:fullscreen',
        'onMute': 'videocontrols:mute',
        'onUnmute': 'videocontrols:onUnmute'
    }
};

作为对这些默认值的回顾,第一个默认值是一个特性数组,允许开发人员进入这段代码来自定义我们需要初始化的内容。第二个默认值保持控件的状态,即播放、暂停等。第三个是专门用于声音的状态。类默认值允许我们使用自定义类,因此使用这个videocontrols类的开发人员不受我们在标记中定义的类的限制。最后一个是事件默认值,定义了我们想要分发的自定义事件。通过将其包含在我们的默认值中,开发人员现在也可以自定义这些事件。

注意

正如你所注意到的,构建一个可以在任何类型的网络应用程序中重复使用和正确实现的视频播放器需要很多工作。尽管一开始非常困难,但付出努力最终会有所帮助。现在我们可以以更模块化的方式添加和删除功能。

由于创建模仿原生控件的自定义控件需要大量的代码,我们决定将其余的功能,包括显示/隐藏和触发自定义事件,留在源代码中供您审查。不过不用担心,所有内容都有注释,如果您有问题,我鼓励您给我发电子邮件或向您的同事寻求帮助。

现在,我们想要实现控件和视频播放器之间的通信。但首先,我们需要清理一下main.js文件。因此,让我们从main.js中删除以下代码:

if(document.querySelector('video') !== 'null') {
    new App.Video({
        'element': document.getElementsByTagName('video')[0],
        'callbacks': {
            ...
        }
    });
}

我们不希望这段代码出现在main.js中,因为它将在本书中构建的所有应用程序之间共享,所以我们需要将其抽离出来。因此,我们在js/App目录中创建了另一个名为App.VideoController.js的 JavaScript 文件。这个文件也包含在本书的源代码中。

请打开本书附带的App.VideoController.js文件,并找到initControls方法;它应该看起来像下面这样:

VideoController.prototype.initControls = function() {
    // Remove Default control
    // Comment this out if you want native controls
    $(videoEle).removeAttr('controls');

    controlsEle = document.querySelector('.video-controls');

    controls = new App.VideoControls(controlsEle);

    $(controlsEle).
         on('videocontrols:play', function(){
            video.play();
        }).
        on('videocontrols:pause', function(){
            video.pause();
        }).
        on('videocontrols:fullscreen', function(){
            video.fullscreen();
        }).
        on('videocontrols:mute', function(){
            video.mute();
        }).
        on('videocontrols:onUnmute', function(){
            video.unmute();
        });

    return this;
}

让我们简要回顾一下这个方法中正在发生的事情,以便更好地理解它。首先,我们告诉我们的video元素隐藏它的控件。这是通过移除controls属性来实现的。然后我们将我们的controls div 缓存在controlsEle中。接下来,我们初始化我们的App.VideoControls类,并将其传递给缓存的controls div。最后,我们为缓存的视频控件添加监听器,并监听我们在App.VideoControls默认值中定义的自定义事件。这些监听器然后通过告诉实例video运行适当的函数来调用我们在App.Video中公开的方法。

我们需要处理的最后一个问题是初始化整个程序。由于我们在main.js中删除了初始化,我们需要在其他地方开始它。最好的地方应该是在特定的index.html上,即video/index.html。因此,让我们打开这个文件,并在页面底部包含以下脚本,就在main.js包含之后。

<script>
    new App.VideoController(true);
</script>

这是最后需要处理的事项。当我们运行我们的页面时,我们应该有一个完全功能的视频播放器,它可以使用我们定制的控件。

总结

给自己一个大大的鼓励,因为你已经取得了相当大的成就!你不仅拥有了一个带有定制控件的视频播放器,而且还建立了一个符合 HTML5 规范并在 iPhone 上运行的稳固视频库。我们已经研究了 HTML5 规范的视频集成,创建了一个使用原生控件的简单视频播放器,构建了一个完全功能和模块化的视频库,用一个控件类扩展了视频库,定制了我们的体验,并最终创建了一个控制器类,将视频和定制控件连接起来。在这个过程中,我们花了一些时间来理解 JavaScript 中的作用域、原型和回调的有用性。如果在本章教授的概念中的任何时候你遇到了一些困难,请通过本书查看源代码,并且一如既往地,实践是完美的。下一章应该会更容易,因为我们将把我们在这里学到的概念应用到音频上。

第三章:HTML5 音频

在上一章中,我们讨论了媒体分发的重要性,以及 HTML5 如何改变了在浏览器中提供音频和视频内容的方式。我们特别讨论了 HTML5 视频实现,但我们也讨论了MediaElement规范,该规范涵盖了视频和音频都使用的常见 API。

在本章中,我们将进一步研究规范并将其抽象化,使其可重用于音频和视频。但在此之前,我们将通过一个简单的示例讨论服务器配置,然后继续进行更高级的实现,包括动态音频播放器和自定义控件。

在本章中,我们将学习以下内容:

  • 集成一个简单的 HTML5 音频示例

  • 配置我们的服务器

  • MediaElement抽象

  • 扩展MediaElementAPI 以支持音频

  • 创建动态音频播放器

  • 自定义音频控件

服务器配置

在开始使用 HTML5 音频元素之前,我们需要配置我们的服务器,以允许特定的音频格式适当播放。首先,让我们花点时间了解适当的音频格式。

音频格式

对 HTML5 音频播放的支持与视频元素的支持类似,因为每个浏览器出于某种原因支持不同类型的格式。以下是一些展示支持情况的表格:

  • 以下是与桌面浏览器相关的细节:
桌面浏览器 版本 编解码器支持
Internet Explorer 9.0+ MP3,AAC
Google Chrome 6.0+ Ogg Vorbis, MP3, WAV
Mozilla Firefox 3.6+ Ogg Vorbis, WAV
Safari 5.0+ MP3,AAC,WAV
Opera 10.0+ Ogg Vorbis, WAV
  • 以下是与移动浏览器相关的细节:
移动浏览器 版本 编解码器支持
Opera Mobile 11.0+ 设备相关
Android 2.3+ 设备相关
Mobile Safari(iPhone,iPad,iPod Touch) iOS 3.0+ MPEG,MPG,MP3,SWA,AAC,WAV,BWF,MP4,AIFF,AIF,AIFC,CDDA,32G,3GP2,3GP,3GPP
Blackberry 6.0+ MP3,AAC

正如我们所看到的,各种浏览器,无论是移动还是桌面,都支持多种格式类型。幸运的是,这本书侧重于 iPhone 网络应用程序,所以对于我们的目的,我们只关注传递大多数浏览器支持的 MP3 格式。现在,我们需要确保我们的服务器可以播放 MP3。

音频格式指令

为了提供正确的 MIME 类型,我们需要配置我们的 Apache 服务器。为此,我们希望将以下指令添加到一个.htaccess文件中:

AddType audio/mpeg mp3
AddType audio/mp4 m4a
AddType audio/ogg ogg
AddType audio/ogg oga
AddType audio/webm webma
AddType audio/wav wav

当然,对于我们的目的,我们只需要 MPEG/MP3,但允许这些格式是个好主意,以便在支持其他浏览器时考虑可扩展性。

简单的 HTML5 音频集成

在页面上包含音频非常简单。我们只需在页面中包含以下标记,就可以立即拥有一个音频播放器:

<audio controls>
    <source src="img/mymusic.mp3" type='audio/mpeg; codecs="mp3"'/>
    <p>Audio is not supported in your browser.</p>
</audio>

简单的 HTML5 音频集成

音频元素

前面的例子指定了一个带有控件属性的音频元素,告诉浏览器具有用于播放的本机控件的音频播放器。在这个元素内部,有一个源元素和一个段落元素。源元素指定音频的来源和类型。源元素上的src属性是音频的相对位置,type属性指定了源的 MIME 类型和编解码器。最后,我们有一个段落元素,以防音频元素不受支持。

这个例子非常适合演示在我们的页面上拥有媒体有多么容易,除非它并不总是那么简单。大多数时候,我们希望完全控制我们的组件,有时需要利用指定的 API。我们在上一章中已经讨论过这些概念,并且开发了一个广泛的 Video 类,我们可以在这里使用。在下一节中,我们将退一步,抽象我们迄今为止编写的代码。

MediaElement 抽象

我们已经讨论过音频和视频在 HTML5 规范中共享相同的 API。在本节中,我们将讨论将我们编写的视频 JavaScript 抽象化,以便我们可以重用它来进行音频播放。

创建 App.MediaElement.js

  1. 首先,在我们的js目录中创建一个新的 JavaScript 文件,命名为App.MediaElement.js

  2. 接下来,将App.Video.js的内容复制到新的App.MediaElement.js文件中。

在这一步中,我们希望确保我们的文件反映了MediaElement命名空间,因此我们将把Video一词重命名为MediaElement

一旦我们把所有东西都重命名为MediaElement,我们可能想要删除默认元素及其名称,因为它们对于这样一个抽象类来说是不必要的。除了这些默认值,我们也不需要公共的fullscreen方法或onFullScreen回调。

当我们进行以上更改时,我们的文件应该如下所示:

var App = window.App || {};
App.MediaElement = (function(window, document, $){
  'use strict';

  var _defaults = {
'callbacks': {
...
}
  };

  function MediaElement(options) { ... }
  function attachEvents() { ... }

MediaElement.prototype.onCanPlay = function(e, ele) { ... }
MediaElement.prototype.onSeeking = function(e, ele) { ... }
MediaElement.prototype.onSeeked = function(e, ele) { ... }
MediaElement.prototype.onEnded = function(e, ele) { ... }
MediaElement.prototype.onPlay = function(e, ele) { ... }
MediaElement.prototype.onPause = function(e, ele) { ... }
MediaElement.prototype.onLoadedData = function(e, ele) { ... }
MediaElement.prototype.onLoadedMetaData = function(e, ele) { ... }
MediaElement.prototype.onTimeUpdate = function(e, ele) { ... }
MediaElement.prototype.getDefaults = function() { ... ;
MediaElement.prototype.toString = function() { ... };
MediaElement.prototype.play = function() { ... }
MediaElement.prototype.pause = function() { ... }
MediaElement.prototype.stop = function() { ... }
MediaElement.prototype.mute = function() { ... };
MediaElement.prototype.unmute = function() { ... };

  return MediaElement;

}(window, document, Zepto)); 

尽管我们之前已经编写了这段代码,让我们简要回顾一下MediaElement类的结构。这个类包含可以访问的公开方法,比如onCanPlayonSeekingonEnded。当我们传递的元素分派了适当的事件时,这些方法将被调用。我们正在监听的事件在attachEvents中,它们包含共享的 API 事件,比如canplayseekingended等等。

这个类基本上只包含在音频和视频媒体之间共享的 API。如果我们想要扩展它以实现特定功能,比如全屏,我们将扩展MediaElement的实例,或者使用 JavaScript 继承来为App.Video类。

提示

在本书中,我们不涵盖真正的 JavaScript 继承。鉴于我们希望全面审查 iPhone 网页应用程序开发的 HTML5,我们不会深入讨论 JavaScript 架构的更高级细节。

初始化 App.MediaElement.js

为了初始化App.MediaElement.js,我们可以这样做:

new App.MediaElement({
    'element': someElement,
    'callbacks': {
        'onCanPlay': function(){ console.log('onCanPlay'); },
        'onSeeking': function(){ console.log('OVERRIDE :: onSeeking'); },
        'onSeeked': function(){ console.log('OVERRIDE :: onSeeked'); },
        'onEnded': function(){ console.log('OVERRIDE :: onEnded'); },
        'onPlay': function(){ console.log('OVERRIDE :: onPlay'); },
        'onPause': function(){ console.log('OVERRIDE :: onPause'); },
        'onLoadedData': function(){ console.log('OVERRIDE :: onLoadedData'); },
        'onLoadedMetaData': function(){ console.log('OVERRIDE :: onLoadedMetaData'); },
        'onTimeUpdate': function(){ console.log('OVERRIDE :: onTimeUpdate'); }
    }
});

在上述代码中,我们创建了一个MediaElement的新实例,并传递了一个对象,该对象与MediaElement构造函数的默认值合并。请记住,element将始终引用音频或视频元素。我们可以选择覆盖默认的回调,也可以不覆盖,因为它们是可选的。

注意

请注意,我们正在传递所有的回调。这是因为自从编写本书以来,Zepto.js包含一个 bug,如果将布尔值 true 作为第一个参数传递,它不会进行对象的深复制。

现在我们准备在这个页面上使用这个类与我们为此页面开发的音频类一起。

扩展音频的 MediaElement API

现在我们有了一个抽象的MediaElement类,我们希望在其基础上构建,以实现音频播放。从我们已经建立的基本模板开始,我们将创建一个包含此页面所有功能的App.Audio类;从创建一个MediaElement的实例,到创建一个下拉菜单来切换曲目和管理每个曲目的音量。

基本模板

我们可以通过遵循我们之前建立的模式来建立一个基本模板。以下是一些代码,您可以用作模板的起点:

var App = window.App || {};

App.Audio = (function(window, document, $){
  'use strict';

  var _defaults = {
    'element': 'audio',
    'name': 'Audio'
  };

  function Audio(options) {
    this.options = $.extend({}, _defaults, options);

        this.element = this.options.element;
        this.$element = $(this.element);

        attachEvents.call(this);
  }

    function attachEvents() { }

  Audio.prototype.getDefaults = function() { ... };

  Audio.prototype.toString = function() { ... };

  return Audio;

}(window, document, Zepto));

这里没有什么新东西,我们使用了之前使用过的相同模式;建立一个App.Audio类,一个包含Audio构造函数的 IIFE,包含处理事件的相同attachEvents方法,以及一些扩展Audio的原型方法(getDefaultstoString)。我们继续使用Zepto并将windowdocument传递给 IIFE 作为引用,然后自包含我们的代码。

创建一个 MediaElement 的实例

在我们的构造函数中,我们需要做两件事。一是,我们需要获取页面上的音频元素并对其进行缓存。二是,我们需要根据页面上的元素创建或初始化一个基于 MediaElement 的实例。

查找和缓存音频元素

要找到音频元素并将其缓存,我们可以这样做:

this.audioElement = document.getElementsByTagName('audio')[0];
this.$audioElement = $(this.audioElement);

请记住,this关键字是指返回给App.Audioaudio实例。然后我们在this上创建一个名为audioElement的属性,该属性设置为页面上找到的第一个音频元素。

注意

请注意,getElementsByTagName存在于文档中,接受一个参数,即一个字符串。这个方法获取页面上与该标签匹配的所有元素,并以数组的形式返回。在这种情况下,我们在页面上只有一个音频元素,所以我们得到一个包含一个找到的元素的数组。因此,我们使用[0]来获取该数组中的第一个实例。

一旦我们有了音频元素,我们将其缓存为Zepto对象,以便我们只使用一次Zepto,从而提高我们应用程序的性能。我在大多数项目中都这样做,因为我发现自己经常使用 Zepto 的许多内置方法,特别是用于创建事件侦听器。但是,如果在您的情况下发现它没有用处,可以跳过这一步。

初始化 MediaElement

现在我们有了音频元素,我们可以按照上一节中编写的代码来初始化MediaElement。因此,您不必翻回去,这是我们可以使用的代码:

this.mediaElement = new App.MediaElement({
    'element': this.audioElement,
    'callbacks': {
        'onCanPlay': function(){ ... },
        'onSeeking': function(){ ... },
        'onSeeked': function(){ ... },
        'onEnded': function(){ ... },
        'onPlay': function(){ ... },
        'onPause': function(){ ... },
        'onLoadedData': function(){ ... },
        'onLoadedMetaData': function(){ ... },
        'onTimeUpdate': function(){ ... }
    }
});

这与我们之前编写的代码相同,回调中的省略号应包含我们编写的console.log。您应该注意到的一件事是,我们将this.audioElement,我们缓存的音频元素,传递给MediaElement的实例。此外,我们现在已经创建了对MediaElement实例的引用,即this.mediaElement。现在我们可以从稍后将创建的App.Audio实例中公开控制音频。

在这一点上,我们已经建立了一个完全功能的音频播放器,基于我们抽象类MediaElement。然而,目前没有太多事情发生;我们只是有一个可以工作和可扩展的设置,但它并不是独一无二的。这就是我们动态音频播放器将发挥作用的地方。

动态音频播放器

因此,在这一点上,我们有一个扩展了我们的MediaElement对象的音频类,具有公开的事件,因此可以用来创建动态内容。现在,让我们来玩一些,创建一个可以切换曲目的动态音频播放器。

选择元素

最初,当我们在第一章中创建这个应用程序时,应用程序架构,我们创建了一个由锚点标签和列表元素包含的导航。虽然这在桌面上和可能 iPad 上都可以完美运行,但对于 iPhone 等较小的屏幕设备来说并不适用。因此,select元素会弹出一个原生组件,允许您轻松导航并选择选项。

苹果的开发者文档建议我们在应用程序中使用select元素,因为它已经被优化为 iOS 中的自定义控件。这非常有用,因为它允许我们遵循 iOS 的 Web 应用程序设计指南。

现在让我们继续实施。首先,我们需要确保将select元素添加到我们的页面中。现在,您应该有以下标记:

<div class="audio-container">
    <audio controls preload>
        <source src="img/sample.mp3" type='audio/mpeg; codecs="mp3"'/>
        <p>Audio is not supported in your browser.</p>
    </audio>
</div>

我们需要做的是在audio标签之后添加select元素,如下所示:

<div class="audio-container">
    <audio controls preload>
        <source src="img/nintendo.mp3" type='audio/mpeg; codecs="mp3"'/>
        <p>Audio is not supported in your browser.</p>
    </audio>
    <select>
        <option value="sample1.mp3" selected>Sample1</option>
        <option value="sample2.mp3">Sample2</option>
        <option value="sample3.mp3">Sample3</option>
    </select>
</div>

The select element

选择元素

在上述代码中,我们添加了一个包含多个选项的选择元素。这些选项具有value属性,而第一个选项还包含一个selected属性。value 属性应包含您在资产中拥有的曲目,而 selected 属性告诉select在页面加载时自动选择该选项。

注意

在这个例子中,我们假设所有的音频都是 MP3 格式。在您的情况下可能会有所不同,如果是这样,我们需要在我们将要编写的代码中构建逻辑来处理这个逻辑。由于这将引入复杂性,我们专注于处理具有 MP3 MIME 类型的音频轨道。

切换音轨

现在我们在页面上有一个select元素,以 iOS 建议的方式列出了几个音轨,我们现在希望根据用户输入使我们的播放器动态。为此,我们需要创建一个事件监听器来处理change事件。

change 事件监听器

select元素有一个特定的事件可以监听,即change事件。这在Zepto和我们缓存的音频元素实例中相当容易实现。要添加监听器,让我们进入App.Audio中的attachEvents方法,并添加以下代码:

var that = this;
this.$element
    on('change', 'select', function(e) { onSelectChange.call(that, e); });

首先,我们创建了一个名为that的变量,它指的是音频的实例。然后,我们获取在构造函数中创建的缓存元素,并委托来自页面上任何select元素的change事件。当change事件触发时,我们调用匿名函数,即on方法中的第三个参数。在这个匿名函数内部,我们调用一个方法,我们还没有创建,叫做onSelectedChange,并将事件或e引用传递给它。

注意

我们正在使用 Zepto 的on方法。这个方法可以接受类似于 jQuery 的on方法的各种参数,但在这种情况下,我们发送我们想要监听的事件,它应该来自哪个元素,最后是应该被调用的函数。除此之外,我们的匿名函数正在调用我们之前讨论过的方法,但本质上它改变了this的引用为音频。

change 事件处理程序

一旦我们为change事件创建了监听器,我们需要定义处理程序。我们还没有创建这个,但它涉及一些相当复杂的功能。最初,现在我们通过MediaElement实例有了一个 API,这应该相当容易。然而,页面上只有一个音频元素,所以我们需要能够使用该元素进行播放。因此,在我们的处理程序中,我们需要做以下事情:

  • 创建对缓存音频元素的临时引用

  • 停止音频的播放,即使它没有在播放

  • 将缓存的音频元素克隆到临时引用

  • 从 DOM 中删除音频元素

  • 删除缓存的媒体元素、音频元素和 Zepto 音频元素

  • 更改克隆的音频元素的源

  • 将克隆的音频元素附加到 DOM

  • 重新创建缓存的媒体元素、音频元素和 Zepto 音频元素

是的,这听起来是为了保持页面上的单个音频元素而要做很多工作,但要做到这一点的代码很少,涉及一些复制和粘贴,因为我们已经写过了。所以,让我们写一些魔法!

在事件处理程序部分,我们想要包含以下方法:

function onSelectChange(e) {
    var $tempAudioElement;
    // Stop the song from playing
    this.mediaElement.stop();
    // Store the element temporarily
    $tempAudioElement = this.$audioElement.clone();
    // Now remove the element
    this.$audioElement.remove();
    // Remove from memory
    //-----
    delete this.mediaElement;
    delete this.audioElement;
    delete this.$audioElement;
    //-----

    // Change the temporary audio source
    $tempAudioElement.
        find('source').
            attr('src', '../assets/' + e.target.selectedOptions[0].value);

    // Now attach it to the DOM
    this.$element.prepend($tempAudioElement);
    // Reset the audioElement
    this.audioElement = document.getElementsByTagName('audio')[0];
    this.$audioElement = $(this.audioElement);
    // Reset the mediaElement
    this.mediaElement = new App.MediaElement({
        'element': this.audioElement,
        'callbacks': {
            'onCanPlay': function(){ ... },
            'onSeeking': function(){ ... },
            'onSeeked': function(){ ... },
            'onEnded': function(){ ... },
            'onPlay': function(){ ... },
            'onPause': function(){ ... },
            'onLoadedData': function(){ ... },
            'onLoadedMetaData': function(){ ... },
            'onTimeUpdate': function(){ ... }
        }
    });
}

如果我们继续在浏览器中运行代码,我们应该能够在音轨之间切换而没有问题。如果您遇到问题,请参考提供的源代码。

无论如何,前面的代码确实实现了我们想要的效果。如果我们仔细分析代码,我们可以看到当我们停止播放时,我们实质上是在利用MediaElement类。这是一个很好的例子,说明了现在通过抽象化处理媒体元素(如音频和视频)是多么容易。还要注意,我们使用了相当多的 Zepto 方法,包括cloneremoveprependattr。这些都是有用的方法,这正是我们缓存音频元素的原因。

您可能会问自己在我们前面的代码中delete部分是做什么的。基本上,这有助于垃圾收集;它告诉 JavaScript 引擎我们不再需要它,所以你可以重新收集它。是的,我们可以在将新音频元素前置之后将它们设置为新值,但这是一种确保从 JavaScript 引擎中重新开始并不留下任何猜测的方法。

我们编写的代码存在一个问题,那就是重复创建audioElement$audioElementmediaElement对象。由于我们之前在构造函数中定义了这个功能,我们可以重构以确保我们的功能都位于一个位置——这就是下一节要讨论的内容。如果你已经理解了这段代码的重构意义,你可以跳过这部分。

重构我们的代码

由于我们在两个地方有相同的代码,我们开始引入了一些冗余。为了使我们的应用程序更易管理,我们应该将相同的功能集中到一个位置。这样做并不复杂,比你想象的要简单。

对于我们的重构,我们只需要编写一个方法,一个setAudioElement方法。这个方法应该是私有的,只能在Audio类内部使用,它应该只包含创建对audioElement$audioElementmediaElement对象的引用所需的代码。

为此,在我们的私有方法部分创建以下方法:

function setAudioElement() {
    return this;
}

现在从构造函数中复制以下代码,并粘贴到setAudioElement中:

this.audioElement = document.getElementsByTagName('audio')[0];
this.$audioElement = $(this.audioElement);

this.mediaElement = new App.MediaElement({
        'element': this.audioElement,
        'callbacks': {
            'onCanPlay': function(){ ... },
            'onSeeking': function(){ ... },
            'onSeeked': function(){ ... },
            'onEnded': function(){ ... },
            'onPlay': function(){ ... },
            'onPause': function(){ ... },
            'onLoadedData': function(){ ... },
            'onLoadedMetaData': function(){ ... },
            'onTimeUpdate': function(){ ... }
        }
});

一旦我们完成了这个,让我们在构造函数中调用setAudioElement

function Audio(options) {
    // Customizes the option
    this.options = $.extend({}, _defaults, options);
    //Cache the main element
    this.element = this.options.element;
    this.$element = $(this.element);
    // Sets the audio element objects
    setAudioElement.call(this);
    attachEvents.call(this);
}

如果我们现在运行我们的应用程序,它应该像平常一样运行,就好像我们没有改变任何东西。现在我们需要替换select处理程序中的重复代码,以调用相同的方法:

function onSelectChange(e) {
    ....
    // Now attach it to the DOM
    this.$element.prepend($tempAudioElement);

   setAudioElement.call(this);
}

现在我们已经做好了所有需要的重构,让我们在 iPhone 模拟器上运行应用程序。当页面运行并在音轨之间切换时,你不应该遇到任何问题。这里没有什么令人惊讶的,但很酷的是,现在你有一个通用的代码集中在一个位置。这就是重构的本质,它有助于实现可维护的代码库。

初始化我们的 Audio 类

到目前为止,我们专注于Audio类的开发。这很好,但现在我们需要初始化所有这些代码。

为此,打开index.html文件,找到Audio页面。它应该位于/audio/index.html。一旦打开了该文件,滚动到源代码底部,并在所有其他脚本之后添加以下脚本:

<script>
    new App.Audio({
        'element': document.querySelector('.audio-container')
    });
</script>

这与我们初始化App.Video的方式有些不同,因为我们现在传入元素,而App.Video在其中查找视频元素。这种差异背后的原因是为了展示我们如何以不同的方式初始化我们的类。你可以自行决定如何初始化一个类。每种方式都有其优缺点,但了解替代方案并选择最适合你的代码风格和项目需求的方式是很好的。

现在我们有一个动态音频播放器运行在一个抽象的MediaElement类上。除此之外,我们还创建了一个对于这个目的有效的 UI,并执行了预期的操作。但是,如果我们想要更清晰地控制音频,除了默认界面提供的内容之外呢?在下一节中,我们将发现如何使用之前创建的MediaElement类来控制我们的音频。

自定义 HTML5 音频控件

在这一节中,我们将介绍如何自定义音频播放器的控件。正如我们在上一章讨论的视频播放器中所看到的,创建自定义体验可能非常有用。对于本书来说,我们保持了相当简单的方式,并将继续遵循这种模式,以便我们可以讨论原则并让你快速入门。对于音频,自定义控件甚至更简单,特别是因为我们无法控制音量,这将在下一节中进一步讨论。

创建自定义媒体控件

首先,让我们从audio元素中删除controls属性。这样做后,你应该有以下标记:

<audio preload>
    <source src="img/sample1.mp3" type='audio/mpeg; codecs="mp3"'/>
    <p>Audio is not supported in your browser.</p>
</audio>

现在我们需要向标记添加自定义控件。我们可以继续做与上一章相同的事情,只是这次我们用一个 media-controls 类来抽象它,并简单地只有一个播放和暂停按钮。这也应该放在audio元素之后。完成后,标记应该是这样的:

<div class="media-controls">
    <div class="mc-state">
        <button class="mc-play mc-state-play">Play</button>
        <button class="mc-pause mc-state-pause">Pause</button>
    </div>
</div>

当您在 iPhone 模拟器上查看应用程序时,它应该是这样的:

创建自定义媒体控件

自定义控件

您会注意到的是,现在页面上没有显示音频元素。这是因为我们已经去掉了controls属性。不要太担心;这是 iOS 上预期的行为。通常,您会为音频播放器创建所有控件,但现在我们只做播放和暂停。作为奖励,您可能还想要一首曲目,但这是一个更大讨论的内容,不适合本书的范围。

为我们的自定义控件添加交互性

这就是所有魔术发生的地方。我们现在将连接我们已经构建的交互性到MediaElement类,以定制我们的体验。

首先,让我们去我们的App.Audio JavaScript 文件中找到attachEvents方法。为了简短和简单起见,让我们在我们的change事件监听器之后包含以下代码片段:

this.$element.
    find('.media-controls').
        on('click', '.mc-play', function() { that.mediaElement.play(); }).
        on('click', '.mc-pause', function(){ that.mediaElement.pause(); });

前面的代码使用缓存的$element来查找媒体控件,然后相应地将时钟事件附加到播放和暂停按钮上。在每个事件监听器内部,我们使用在setAudioElement方法中创建的mediaElement的实例来调用playpause方法。

注意

需要注意的一点是,我们的事件监听器使用that来引用mediaElement的实例。如果您还记得,我们在attachEvents方法的顶部创建了that变量,以便在事件监听器内部有一个this的引用。正如我们之前解释过的,JavaScript 具有函数作用域,因此当我们创建我们的事件监听器时,该函数创建了一个新的作用域,将this的关系设置为事件作用域。在幕后,Zepto 将this设置为目标元素,这可能是playpause元素。

这就是我们需要的一切,以制作自定义控件来播放和暂停我们的音频。如果我们现在测试应用程序,我们应该能够在曲目之间切换,播放我们的曲目,并暂停曲目。

顺序播放

在这一部分,我们将看看如何构建一个初步的播放列表。虽然这一部分更多是额外材料,但在创建某种音乐播放器应用程序时,有音乐播放列表是很有用的。起初,可能很难理解我们如何做到这一点,特别是考虑到我们需要用户输入来启用播放,但这实际上并不是问题。因为加载和播放方法是在第一首歌曲上启动的,我们只需切换源,加载它,然后播放曲目。所以让我们一步一步地进行。

标记

我们实际上不希望默认按顺序播放音乐,这应该是基于良好的用户体验设计由用户发起的。因此,让我们为用户添加另一个按钮来启用或禁用此功能:

<div class="mc-state">
    <button class="mc-play mc-state-play">Play</button>
    <button class="mc-pause mc-state-pause">Pause</button>
    <button class="mc-sequential mc-sequential-off mc-state-sequential">Sequential Off</button>
</div>

在前面的代码中,我们所做的只是在播放和暂停按钮之后添加了另一个按钮。这个按钮包含了我们需要的适当的三个类和文本Sequential Off,因为我们只希望用户在需要时启用此功能。

当您的标记都设置好后,您应该有以下界面:

标记

顺序按钮

JavaScript

这里有一些工作要做,但并不是太复杂。以下是我们要做的清单:

  • 为顺序播放创建默认设置,并将其设置为 false

  • 创建一个handleOnAudioEnded方法,带有Audio类的参数

  • 在媒体元素初始化的onEnded回调中调用handleOnAudioEnded方法

  • handleOnAudioEnded方法中,我们应该检查顺序播放是否已启用

  • 如果启用了顺序播放,我们希望更新选择菜单并重新加载音频元素

  • 最后,我们希望监听新的顺序按钮的点击事件以启用或禁用此功能,同时也更新按钮的状态

所以,首先,让我们创建顺序的默认设置:

var _defaults = {
    'element': 'audio',
    'name': 'Audio',
    'sequential': false
};

没有太疯狂的事情,我们只是添加了一个名为sequential的默认设置,并将其设置为false。接下来,我们想创建包含我们之前列出的功能的handleOnAudioEnded方法:

function handleOnAudioEnded(Audio) {
    if(Audio.options.sequential) {
        var $select = Audio.$element.find('select'), $next;

        // Go to next in playlist
        $next = $select.
            find('option[selected]').
                removeAttr('selected').
                    next().
                        attr('selected', 'selected');

        // Change the Selected Index
        $select[0].selectedIndex = $next.index();

        // Must be made on the audio element itself
        Audio.audioElement.src = '../assets/' + $select.val();
        Audio.audioElement.load();
        Audio.audioElement.play();
    }
}

如果你不理解前面的代码,不要担心,只需考虑以下几点:

  • 我们传递的唯一参数是Audio的一个实例

  • 然后我们检查sequential是否已启用

  • 一旦我们确认我们想要顺序播放,我们创建两个变量:$select,它缓存了选择元素,和$next,它将缓存播放列表中的下一首歌曲。

  • 然后我们设置$next元素,同时从当前选项中删除selected属性

  • 通过将selectselectedIndex设置为select中的下一个选项来更新select菜单

  • 最后,我们直接更新音频元素的源,加载该源,并将状态设置为播放

这个方法处理我们想要播放的下一个源的播放。我们可能可以通过在MediaElement类中添加更改源、加载和播放的功能来改进这一点,但我会把这个决定和需要扩展的功能留给你。我们也可能在类级别(Audio)缓存select,而不是每次想要顺序播放时都这样做。

注意

请注意,我们还没有添加任何错误检查。目前,这段代码没有检查我们是否到达列表的末尾。也许我们希望它循环,或者也许我们希望通知用户播放列表已经完成?我们可以在这里执行许多用例,但你明白我的意思,也就是说,如果我们愿意,我们可以在我们的应用程序中拥有一个播放列表。

接下来,当我们将callbacks传递给媒体元素的初始化时,我们希望调用我们创建的前面的方法。你可能还记得,我们把这个放在我们的setAudioElement中,因此我们希望更新初始化如下:

this.mediaElement = new App.MediaElement({
    'element': this.audioElement,
    'callbacks': {
        ...
        'onEnded': function(){ handleOnAudioEnded(that); },
        ...
    }
});

我们在这里所做的就是通过调用handleOnAudioEnded来更新onEnded方法,并传入that,它是对Audio类实例的引用。现在,我们只需要为用户想要顺序播放时添加事件监听器,这可以在我们的attachEvents方法中添加:

this.$element.
    find('.media-controls').
        on('click', '.mc-play', function() { that.mediaElement.play(); }).
        on('click', '.mc-pause', function() { that.mediaElement.pause(); }).
        on('click', '.mc-sequential', function(e) { handleSequentialClick(e, that); });

前面的代码基本上显示了我们已经向我们的顺序按钮添加了一个click事件监听器,它所做的就是调用handleSequentialClick方法,该方法接受一个事件和我们之前创建的that变量的音频实例。注意我们还没有创建handleSequentialClick方法吗?好吧,这就是它:

function handleSequentialClick(e, Audio) {
    var $this = $(e.target);

    if(!Audio.options.sequential) {
        Audio.options.sequential = true;
        $this.
            removeClass('mc-sequential-off').
            addClass('mc-sequential-on').
            text('Sequential On');
    } else {
        Audio.options.sequential = false;
        $this.
            removeClass('mc-sequential-on').
            addClass('mc-sequential-off').
            text('Sequential Off');
    }
}

简而言之,这个方法只是将默认的sequential选项更新为truefalse,根据先前的状态切换值。该方法还切换按钮和内部文本的类,根据用户的交互更新用户。

iOS 注意事项

到目前为止,我们已经为视频和音频元素定制了许多体验。这对桌面设备来说非常完美,但在处理触摸设备(如 iPhone 和 iPad)时,我们需要考虑一些要点。好消息是,这些是所有 iOS 设备上一致的要点,因此应该是我们需要考虑的事情。

音量

我们可以为音频和视频元素设置音量从01,并且我们可以在我们的MediaElement库中保持音量的状态。这是整体架构的良好实践。然而,在 iOS 上,音量在用户的物理控制下——几乎任何设备上我们与之交互的音量按钮。

根据苹果的文档(developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW11):

在 iOS 设备上,音量始终在用户的物理控制下。音量属性在 JavaScript 中不可设置。读取音量属性始终返回 1。

基本上,我们无法设置音量属性;它将始终返回1。这是为了我们不操纵用户的音量,因此只能通过用户的音量控制按钮设置。

自动播放

在我们的应用程序中,我们还看到了自动播放的一个例子,在我们的select中选择不同的音轨后播放音频。这在桌面上运行得很完美,但在 iOS 上不太好。这是有原因的,基本上是为了保护用户的蜂窝数据使用。这是苹果的设计决定,也是我们在其他设备上可能看到的东西。

根据苹果的文档(developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW8):

自动播放被禁用以防止未经请求的蜂窝下载。

它还指出(developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW4):

在 iOS 的 Safari 浏览器上(包括 iPad 在内的所有设备),用户可能在蜂窝网络上并且按数据单位收费,预加载和自动播放都被禁用。直到用户启动它,才会加载数据。这意味着 JavaScript 的 play()和 load()方法也在用户启动播放之前无效,除非 play()或 load()方法是由用户操作触发的。换句话说,用户启动的播放按钮有效,但 onLoad="play()"事件无效。

同时播放

你可能会问为什么我们没有涉足更复杂的体验,包括同时播放多个视频或音轨。嗯,这也有一个很好的理由,基本上是因为 iOS 限制了一次只能播放一个音频或视频流。这也归结于我们不想在页面上使用比必要更多的蜂窝数据。

根据苹果的文档(developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW10):

目前,所有运行 iOS 的设备都限制为一次只能播放一个音频或视频流。在 iOS 设备上,目前不支持同时播放多个视频——并排、部分重叠或完全叠加。同时播放多个音频流也不受支持。

在开发支持音频和视频媒体播放的 iOS Web 应用程序时,应考虑更多因素。我们可以在这里继续讨论这些内容,但我鼓励您访问苹果的文档* iOS 特定注意事项*(developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html)来审查所有必要的注意事项。先前提到的文档片段应该涵盖了您在开发本书的视频和音频部分时遇到的一些问题,但了解所有可能出现的问题总是好的。

总结

在本章中,我们从 iOS 上音频播放的角度审查了媒体元素 API。从将先前的代码抽象成MediaElement类,使其可重用于音频和视频,到自定义音频元素的控件,我们创建了一个动态音频播放器,它以模块化的方式工作和构建。除了创建音频播放器,我们还审查了在 iOS 设备上必须考虑的注意事项,比如音量控制和同时播放的限制。我希望本章能帮助您开始尝试音频,并帮助您了解,通过抽象化我们的代码,我们可以 consoli 代码并专注于提供在我们的应用程序中至关重要的功能。在下一章中,我们将转向如何使用触摸和手势来创建超越可点击按钮的独特用户体验。

第四章:触摸和手势

创建 iPhone 网页应用程序默认涉及触摸交互。这是显而易见的,幸运的是,苹果已经通过默认将点击映射到触摸事件,很好地帮助我们快速上手。然而,如果我们想要一个幻灯片向用户的滑动做出反应怎么办?或者,如果我们想要在用户在应用程序的定义区域内捏合时放大照片,而不影响页面的布局怎么办?嗯,这都取决于我们作为开发者。

在本章中,我们将讨论触摸事件和手势,并利用这项技术构建一个对用户的触摸和手势响应的幻灯片。这里的大部分概念都是基础的,以帮助您理解这些在传统网页开发中不常见的新事件。然而,我们还将深入一些更高级的功能,使用捏合手势来放大和缩小图像。但首先,我们需要对我们的应用进行一些调整,重新组织我们的导航,以便它不会占用大部分屏幕空间,然后我们将开始深入研究触摸和手势。

在本章中,我们将涵盖:

  • 简化我们的导航

  • 创建响应式相册

  • 监听和处理触摸事件

  • 解释触摸事件

  • 响应手势

  • 将触摸事件扩展为插件

简化导航

我们的导航目前占据了一些严重的屏幕空间,尽管它对我们之前的示例有效,但它在本书的其余示例中效果不佳。所以,首先我们需要清理这个应用程序,以便专注于我们应用程序的实际内容。我们将清理我们的标记以使用select组件。然后我们将添加交互性,使我们的select元素实际上在页面之间切换。

在开始编码之前,在我们的 JavaScript 目录中创建一个App.Nav.js文件。创建文件后,让我们在页面底部包含它,使用以下脚本标签:

<script src="img/App.Nav.js"></script>

导航标记和样式

在本章的这一部分,我们将重新设计我们应用程序的导航。在大多数情况下,我们希望确保在设备上使用原生控件,因此这里的目标是为用户提供在 iOS 中使用自定义选择控件的能力,但同时给我们提供相同的灵活性来自定义外观和感觉,同时具有相同的交互。我们将修改标记,查看自定义控件,然后模拟相同的体验。

基本模板

首先,让我们摆脱我们在导航中使用的锚标签。一旦我们移除了这些链接,让我们创建一个select元素,其中包含选项,并使值指向适当的页面:

<nav>
    <select>
        <option value="../index.html">Application Architecture</option>
        <option value="../video/index.html">HTML5 Video</option>
        <option value="../audio/index.html">HTML5 Audio</option>
        <option value="../touch/index.html" selected>Touch and Gesture Events</option>
        <option value="../forms/index.html">HTML5 Forms</option>
        <option value="../location/index.html">Location Aware Applications</option>
        <option value="../singlepage/index.html">Single Page Applications</option>
    </select>
</nav>

在上述代码中,我们用select元素的选项替换了锚标签。每个选项都有一个值,指向特定的页面,选项中包含章节名称。由于我们已经移除了锚标签,我们需要调整样式。

样式化选择组件

这里我们没有太多需要做的,只需移除我们之前设置的样式。虽然这并非必需,但最佳实践是,您总是希望移除未使用的样式。这有助于通过降低页面加载来提高应用程序的性能。

所以让我们移除以下样式:

/* --- NAVIGATION --- */
nav ul {
    padding: 0;
}
nav li {
    list-style: none;
}
nav a {
    display: block;
    font-size: 12px;
    padding: 5px 0;
}

现在,我们需要添加模仿锚标签默认操作的交互性。

导航交互

模仿锚标签的默认行为非常简单。让我们从创建一个基本模板开始,就像我们在之前的章节中所做的那样,然后缓存导航并添加切换页面的行为。所以让我们开始吧!

基本模板

以下是我们的默认模板。和以前一样,这只是一个简单的 IIFE,为我们的导航建立了一个类。这个闭包接受windowdocumentZepto对象,并将Zepto对象别名为美元符号。

var App = window.App || {};

App.Nav = (function(window, document, $){

  var _defaults = {};

  function Nav() {}

  return Nav;

}(window, document, Zepto));

缓存我们的导航

现在,我们可以每次需要时使用 Zepto 在 DOM 中查找导航。但是遵循我们的最佳实践,我们可以缓存导航,并在闭包范围内包含一个变量,该变量可以被私有和公共方法使用。

var _defaults = {},
  $nav;

function Nav() {
  $nav = $('nav');
}

在前面的代码中,我们创建了一个$nav变量,它包含在闭包范围内,因此我们现在可以在闭包中包含的所有方法中引用它。然后在构造函数中,我们将变量设置为nav元素。

监听和处理 change 事件

现在开始有趣的部分。我们需要监听select元素的 change 事件何时被触发。我们以前为我们的音频播放器做过这个。但是,我们将简要介绍如何在这里做这个,以防您之前没有跟进。

首先,让我们调用一个我们将在下面定义的attachEvents方法:

function Nav() {
  $nav = $('nav');

  attachEvents();
}

现在我们正在调用attachEvents方法,我们需要创建它。在这个方法中,我们想要监听 change 事件,然后处理它:

function attachEvents() {
  $nav.
    on('change', 'select', handleSelectChange);
}

在前面的代码中,我们使用 Zepto 的on方法告诉缓存的导航监听select元素上的 change 事件,该元素包含在导航中。然后我们分配一个我们尚未创建的方法handleSelectChange。这个方法是一个处理程序,我们将在下面定义。

最后,我们需要定义我们的处理程序。这个处理程序所需要做的就是根据select元素的更改值切换页面。

function handleSelectChange(e) {
  window.location = this.value;
}

前面的处理程序接受事件参数,但实际上我们并没有使用它。您可以删除此参数,但通常我喜欢保留处理程序接受的参数。无论如何,我们都在告诉窗口对象通过将window.location设置为select元素已更改为的值来切换位置。

注意

请注意,我们使用this.value来设置窗口对象的位置。在这种情况下,this指的是选择元素本身或事件目标元素。

初始化导航

最后,我们需要做的就是初始化这个类。因为这个导航理论上将出现在我们应用程序的每个页面上,所以我们可以在创建此调用后立即创建一个App.Nav的新实例。因此,让我们在App.Nav.js的末尾添加以下代码:

new App.Nav();

这就是我们需要模仿以前锚标签行为的全部内容。完成这些后,我们现在有足够的屏幕空间来进行触摸事件。接下来,让我们讨论 iPhone 上的触摸事件和手势。

触摸和手势事件

在 iPhone 上处理触摸事件很容易;但是,当您开始深入研究事件何时被触发以及在某些情况下如何解释它们时,会有一些“陷阱”。幸运的是,手势也可以很容易地通过GestureEvent对象实现。在本节中,我们将总体上讨论触摸和手势,获得对这些用户体验背后技术的基本理解,以便在下一节中,我们可以成功地创建一个可滑动的幻灯片放映。

触摸事件

触摸事件包括移动设备接收的一个或多个输入。在本书中,我们将重点放在我们可以以多种方式处理的最多两个手指事件上。iOS 在解释这些输入方面做得很好;但是,元素可以是可点击的或可滚动的,如苹果的开发者文档所述(developer.apple.com/library/ios/#documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html#pageTitle):

可点击元素是链接、表单元素、图像映射区域或任何其他具有 mousemove、mousedown、mouseup 或 onclick 处理程序的元素。可滚动元素是任何具有适当溢出样式、文本区域和可滚动的 iframe 元素的元素。由于这些差异,您可能需要将一些元素更改为可点击元素,如“使元素可点击”中所述,以在 iOS 中获得所需的行为。

此外,您可以像在“阻止默认行为”中描述的那样关闭 iOS 上 Safari 的默认行为,并直接处理自己的多点触摸和手势事件。直接处理多点触摸和手势事件使开发人员能够实现类似原生应用程序的独特触摸屏界面。阅读“处理多点触摸事件”和“处理手势事件”以了解更多关于 DOM 触摸事件的信息。

这是必须牢记的,因为根据我们需要的功能类型,某些元素的默认行为会有所不同。如果我们想要修改这种功能,我们需要通过将某些事件附加到这些元素来覆盖默认行为,就像之前描述的那样。通过阻止默认功能并用我们自己的功能覆盖它,我们可以创建非常符合我们需求的体验。一个例子是创建一个全屏视差体验,在滚动时播放动画。

一旦我们知道我们想要的行为类型,就有一些重要的事情需要记住。例如,事件是有条件的,因此根据用户交互,某些手势可能不会生成任何事件。让我们来看看其中一些事件。

滚动时

一个有条件事件的很好例子是用户滚动页面。在这种交互中,滚动事件只有在页面停止移动并重绘时才会触发。因此,在大多数视差驱动的网站上,页面上的默认行为会被阻止,并实现自定义滚动解决方案。

触摸并保持

当用户触摸可点击元素并按住手指时,会显示一个信息气泡。但是如果您希望捕捉此手势,那就没那么幸运了。根据官方苹果文档,在这种类型的交互期间不会分派任何事件。

双击缩放

在这种交互中,用户双击屏幕,页面会放大。你可能会认为会有一个针对这种交互的事件,但是我们没有任何可以关联的事件。

如果我们记住了之前讨论的例外情况,我们应该能够正确地开发我们的应用程序并正确处理我们的触摸事件。现在我们需要知道我们可以关联哪些事件进行触摸,以及如何适当地监听和处理它们。

支持的触摸事件及其工作原理

苹果官方文档正式列出了在 iOS 上支持的所有事件,包括以下触摸和手势事件以及它们的支持情况:

事件 生成 有条件 可用
gesturestart 不适用 iOS 2.0 及更高版本
gesturechange 不适用 iOS 2.0 及更高版本
gestureend 不适用 iOS 2.0 及更高版本
touchcancel 不适用 iOS 2.0 及更高版本
touchend 不适用 iOS 2.0 及更高版本
touchmove 不适用 iOS 2.0 及更高版本
touchstart 不适用 iOS 2.0 及更高版本

根据前面的列表,我们已经拥有了在 iPhone 上使用移动 Safari 制作复杂用户体验所需的一切。如果您担心这些事件是如何处理的,根据苹果的开发文档(developer.apple.com/library/ios/#documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html),无需担心,这些事件的传递方式与任何其他浏览器相同。

鼠标事件按照您在其他网络浏览器中期望的顺序传递(...)。如果用户点击一个不可点击的元素,不会生成任何事件。如果用户点击一个可点击的元素,事件按照以下顺序到达:mouseover、mousemove、mousedown、mouseup 和 click。只有在用户点击另一个可点击的项目时,mouseout 事件才会发生。此外,如果页面内容在 mousemove 事件上发生变化,那么序列中的后续事件都不会发送。这种行为允许用户在新内容中点击。

现在我们对单指触摸事件有了很好的理解,包括异常和它们的工作方式,我们应该花一些时间来理解手势。

手势

从技术上讲,手势是触摸事件,因此前面的信息也适用于单点触摸事件,因为平移、缩放和滚动都被视为手势。但是,手势也是可以被不同解释的复杂交互。根据苹果的文档(developer.apple.com/library/ios/#documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html),我们可以结合多点触摸事件来创建自定义手势;

通常,您会实现多点触摸事件处理程序来跟踪一个或两个触摸。但您也可以使用多点触摸事件处理程序来识别自定义手势。也就是说,自定义手势不是已经识别的手势(...)

我们从前面的部分的图表中看到,我们可以监听手势,从而创建自定义体验;然而,关于手势和普通触摸事件的一件令人困惑的事情是它们发生的时间。但这并不是一个谜,因为苹果的文档(developer.apple.com/library/safari/#documentation/UserExperience/Reference/GestureEventClassReference/GestureEvent/GestureEvent.html#//apple_ref/javascript/cl/GestureEvent)为我们提供了以下信息:

(...)对于双指多点触摸手势,事件按照以下顺序发生:

1. finger 1 的 touchstart。当第一根手指触摸表面时发送。

2. gesturestart。当第二根手指触摸表面时发送。

3. finger 2 的 touchstart。当第二根手指触摸表面时立即发送 gesturestart 后发送。

4. 当前手势的 gesturechange。当两根手指在仍然触摸表面的情况下移动时发送。

5. gestureend。当第二根手指从表面抬起时发送。

6. finger 2 的 touchend。当第二根手指从表面抬起时立即发送 gestureend 后发送。

7. finger 1 的 touchend。当第一根手指从表面抬起时发送。

根据前面的信息,我们可以得出触摸和手势事件是相辅相成的。这使我们能够在前端做一些有趣的事情,而不需要猜测。但是,我们该如何做到这一点呢?好吧,下一节通过创建一个对触摸和手势都有响应的照片库来解决这个问题。

创建一个响应式的照片库

如果我们专注于我们在传统移动应用程序中已经看到的小功能片段,比如交互式幻灯片放映,我们将更好地理解触摸和手势事件。我们到处都看到这个,一个带有下一个和上一个按钮的幻灯片放映,但也可以从左到右或从右到左滑动。按钮很容易,附加触摸事件也相当简单;然而,在移动 Safari 中,滑动不是开箱即用的,所以我们需要构建它。所以让我们首先布置我们的画廊,然后进行样式设置。

画廊标记和样式

与任何幻灯片画廊一样,创建一个良好的结构是至关重要的。这种结构应该易于遵循,如果我们想要模块化,就不需要太多的元素。

基本画廊幻灯片列表

让我们从非常基本的东西开始。首先,让我们创建一个带有gallery类的div

<div class="gallery"></div>

从这里开始,我们希望有一个内容区域,其中包含所有幻灯片。你可能会问为什么我们不把幻灯片直接放在父画廊容器中,原因是这样我们可以通过其他功能扩展我们的画廊,比如播放和暂停按钮,而不会影响幻灯片本身的结构。

所以让我们在我们的画廊内创建另一个带有gallery-content类的div,就像这样:

<div class="gallery">
    <div class="gallery-content">
    </div>
</div>

现在我们有了一个画廊的内容区域,我们想要创建一个包含我们图像的幻灯片的无序列表。当我们最终这样做时,我们的gallery标记应该是这样的:

<div class="gallery">
    <div class="gallery-content">
        <ul>
            <li>
                <img src="img/sample-image1.jpg" alt="…">
            </li>
            <li>
                <img src="img/sample-image2.jpg" alt="…">
            </li>
            <li>
                <img src="img/sample-image3.jpg" alt="…">
            </li>
            <li>
                <img src="img/sample-image4.jpg" alt="… ">
            </li>
        </ul>
    </div>
</div>

提示

当你看到前面的标记时,可能会震惊于我在image标记上留下了alt属性的内容。是的,这是一个不好的做法,但我在这里这样做是为了更快地移动。然而,在你的应用程序中不应该这样做,始终为你的图像提供一个带有相关内容的alt属性。

现在我们有了一个基本的标记结构,我们应该开始为这个幻灯片秀设置样式,但要记住,前面的标记并不是最终的解决方案。我在其他网站上看到了一些非凡的工作,那很酷,但我们想在这里保持简单,并为你提供一个基础。我鼓励你进行实验和尝试新的东西,但不要让前面的标记成为你的最终解决方案。在我们开始样式化之前,让我们退一步,了解为什么我们有一个内容区域。

添加简单的画廊控件

我们不想为内容区域增加复杂的样式。如果我们这样做,这可能会导致一些混乱的样式,"修复我们的标记"。因此,出于这个原因,我们创建了一个内容区域,现在要向我们的幻灯片秀添加一个controls组。

所以让我们遵循同样的原则;让我们创建一个带有gallery-controls类的div,其中包含两个锚标记,一个用于下一个按钮,另一个用于上一个按钮。

<div class="gallery-controls">
    <a href="#next">&raquo;</a>
    <a href="#previous">&laquo;</a>
</div>

现在,内容区域和控件是两个可以独立控制的区域。当我们开始为我们的画廊设置样式时,你会看到这样做对我们来说是多么容易。现在,请相信我,这将使你更容易控制你的画廊。但现在,让我们开始样式化!

使图像具有响应性

我们在本书的第一章已经介绍了响应式设计,希望你能理解这些原则。但如果你不理解,这一章应该给你一个很好的想法,让我们确保我们的应用程序不仅在 iPhone 上工作,而且在其他触摸设备上也能工作。

所以我们希望我们的画廊存在于我们网站的移动和桌面版本上,这是一个非常理想的功能,因为现在你正在构建一个可重用的、设备无关的组件。但这也会让事情变得困难,不考虑资产管理,我们需要计算我们的图像必须有多大。好吧,对于这个例子,我们希望我们的图像能够缩放到幻灯片的宽度的 100%,我们希望幻灯片占据我们屏幕宽度的 100%,并且两侧有 12 像素的填充。

为了实现这一点,我们可以简单地将所有图像的宽度设置为 100%,并让我们的画廊在两侧应用 12 像素的填充,如下所示:

img {
  width: 100%;
}

.gallery {
  margin: 12px 0 0 0;
  padding: 0 12px;
}

注意

请注意,我们的画廊已经占据了屏幕宽度的 100%,减去我们在两侧给它的填充。因此你在.gallery中看不到width: 100%的属性。另外,要考虑到我们在画廊顶部添加了 12 像素的填充,以便给它一些与主导航的空间。最后但同样重要的是,我们在这里使用了简写属性,这样我们就不用使用 padding-left,margin-top 等。这不仅使我们的代码更短,而且更容易理解和维护。

这就是使用 CSS 制作响应式画廊所需的全部内容,其余的样式将通过 JavaScript 应用。有些人可能会对此感到反感,但这是一个相当常用的技术,因为我们需要知道设备的宽度才能正确设置我们的画廊以实现响应式使用。但在开始之前,让我们先完成我们的画廊样式。

为我们的画廊添加样式

现在让我们在 CSS 中完成我们画廊的样式。其中一些样式仍然适用于响应式应用,但前面的部分有助于定义原则。不过不用担心,我会逐一介绍这个应用的每个部分的样式,以便你能彻底理解。

首先,让我们确保我们的画廊内容在宽度上扩展到 100%,并且因为最终我们的幻灯片将左浮动,我们希望父容器有一个高度;所以让我们添加一个overflow: hidden的属性。当你完成后,你的样式应该是这样的:

.gallery .gallery-content {
  width: 100%;
  overflow: hidden;
}

接下来,我们要确保无序列表在幻灯片左浮动时也有一个高度,这样这个高度就会应用到画廊内容上。不仅如此,因为我们想要根据用户交互来动画显示无序列表左右移动,所以我们需要确保位置和起始的left值已经定义。当你完成应用这些样式后,它应该看起来像这样:

.gallery .gallery-content > ul {
  left: 0;
  margin: 0;
  overflow: hidden;
  padding: 0;
  position: relative;
}

提示

在这里,我们还将marginpadding的值设为0。这主要是为了重置,以免以后出现任何布局问题。Normalize.css默认为无序列表应用了一些paddingmargin,这是好的,但对于我们的应用来说并不是必要的,所以我们清除了这些值。

现在,让我们专注于样式化我们幻灯片的控件。下一步主要是设置样式,以便我们在容器内浮动元素时不会遇到任何问题;就像我们之前为gallery内容和无序列表所做的那样。所以让我们确保我们的控件的overflow设置为hidden

.gallery .gallery-controls {
  overflow: hidden;
}

由于我们的控件现在设置为hidden当元素溢出时,我们可以相应地浮动我们的下一个和上一个按钮,使它们位于幻灯片的适当侧面。

.gallery .gallery-controls a[href="#next"] {
  float: right;
}

.gallery .gallery-controls a[href="#previous"] {
  float: left;
}

这就是为你的幻灯片做基本样式所需的全部内容。不幸的是,它看起来仍然不够漂亮,这是因为我们需要使用 JavaScript 来确定屏幕尺寸,为幻灯片应用宽度,并为无序列表应用总体宽度。然而,这里还有一件事情可以带来严重的性能优化,那就是使用 CSS3 过渡。

注意

在我们继续之前,重要的是要注意,我们的 CSS 选择器是从gallery``div中级联的。这是一个很好的做法,因为它允许你将样式分隔开来。我们所做的基本上是为我们的画廊创建默认样式,如果有人想要自定义它,他们可以在.gallery之前添加自己的类来覆盖这些样式,从而使画廊更加可定制。这是一个基本的 CSS 基本原则,但我想指出它的重要性,以显示创建模块化样式的重要性。

使用 CSS3 过渡

CSS3 过渡对我们的应用程序非常重要。不仅因为它让我们的工作变得更容易,而且因为它为我们提供了性能优化。默认情况下,移动 Safari 使用硬件加速进行 CSS3 过渡;这意味着硬件将处理这些过渡的渲染,因此我们不需要手动处理。传统上,我们需要使用 JavaScript 来做到这一点,因为这样我们就无法获得性能优化,但现在我们可以通过 CSS3 过渡来实现。所以让我们使用它们!

这是一个基本的画廊,我们希望保持它简单。所以让我们只是将我们的过渡添加到无序列表中。毕竟,无序列表是我们希望在用户滑动或从控件发起操作时进行动画处理的内容。为此,我们将使用transition属性,并使用简写来定义我们要动画处理的属性、持续时间以及要使用的过渡时间函数,也就是所谓的缓动方法。

.gallery .gallery-content > ul {
  left: 0;
  margin: 0;
  overflow: hidden;
  padding: 0;
  position: relative;

  -webkit-transition: left 500ms ease;
  -moz-transition: left 500ms ease;
  -ms-transition: left 500ms ease;
  -o-transition: left 500ms ease;
  transition: left 500ms ease;
}

我们在这里做的唯一一件事就是向我们的无序列表添加了transition属性。这个属性告诉无序列表在 500 毫秒内动画处理left属性,并使用默认的缓动方法。

提示

在这里,我们定义了五个过渡属性,每个属性都添加了浏览器厂商的前缀,而最后一个是支持的标准属性。这样做是为了使我们的画廊可以在各种设备上使用。是的,这有点复杂和混乱,但鉴于浏览器厂商已经给这个属性添加了前缀,并且现在才开始使用非前缀版本,这是一个必要的恶。

画廊互动

我们幻灯片秀的核心在于它的互动性;从下一个和上一个按钮、可滑动的内容和富有动画的显示——我们的幻灯片秀依赖于 JavaScript。在这一部分,我们深入探讨了我们的幻灯片秀是如何工作的;使用我们的基本框架,我们将构建一个高效的Gallery类,实现之前所述的目标。实际上,我们的画廊应该只具有允许其在某个方向上调整大小和播放的功能。但是,像往常一样,这需要一些设置工作,然后我们将一切连接起来。所以让我们开始吧!

基本模板

首先,我们将创建我们的Gallery类。这个类应该设置与我们构建的任何其他类的方式相同。但是,如果你没有按顺序阅读本书,我们只需要检查App命名空间,然后在其下创建一个Gallery类。包裹在闭包中,我们将有一些默认值和一个Gallery函数,并在闭包声明的末尾返回它。正如我们之前提到的,我们将有以下内容:

var App = window.App || {};

App.Gallery = (function($) {

    var _defaults = {};

    function Gallery() {}

    return Gallery;

}(Zepto));

这里唯一不同的是我们只传入了Zepto对象。以前,我们传入了windowdocument,但对于这个类,我们不需要这两个对象,所以我们将它限制在 Zepto 库中。

现在我们所需要的就是缓存我们将要重复使用的元素,而且它们需要在闭包中可用,以便它们在私有和公共方法中可用。

缓存画廊

在我们的应用程序中,缓存对象非常有帮助,特别是因为它提高了性能,使我们的应用程序非常高效。通过减少我们在 DOM 中需要做的查找次数,我们可以加快处理速度,并创建一个不太容易出错的应用程序。

不仅我们想要缓存某些元素,而且我们希望它们也在闭包中可用,以便所有方法都可以访问它们。要做到这一点,我们只需要在上面的构造函数中添加缓存变量,就像这样:

var _defaults = {},
    $gallery,
    $slides,
    $slidesContainer,
    $slidesLength,
    $galleryControls,
    slidesWidth,
    galleryWidth;

在上面的代码中,我们可以看到画廊、它的幻灯片、幻灯片容器、幻灯片数量、画廊控件、幻灯片和画廊宽度将被缓存。然而,此时我们还没有缓存任何东西。所以让我们开始给它们分配应该有的值。

初始化值的最佳位置应该是在构造函数中,或者在创建一个画廊的实例时。构造函数应该先缓存我们在整个运行应用程序中需要的值。此外,每个变量在语义上描述了它应该持有的内容,这样可以更容易地理解发生了什么。让我们来看看下面的函数:

function Gallery() {
    $gallery = this.$el = $('.gallery');

    $slides = $gallery.find('li');

    $slidesContainer = $gallery.find('.gallery-content > ul');

    $galleryControls = $gallery.find('.gallery-controls');

    $slidesLength = $slides.length;
}

从这个函数中,我们可以得出结论,我们缓存了画廊,然后从中确定了所有其他值。例如,我们使用$gallery来查找所有幻灯片或列表项。这非常有用,因为我们所做的是告诉我们的应用程序从gallery元素开始,然后深入其中找到适当的值。否则,我们通常会从文档的顶部开始,然后向下进行,这在 DOM 查找方面非常昂贵。

这是过程中的一个关键步骤,因为其他所有事情都应该很容易。所以让我们开始连接一些交互!

连接我们的控件

首先,我们希望用户能够点击下一个和上一个按钮。但是,我们现在不希望发生任何事情,我们只是想捕获这些事件。和往常一样,让我们从小处开始,然后逐步扩大,我们想要的是有一个可以使用的基础。

附加事件

我们之前已经讨论过如何附加事件,在本章中也是一样。首先创建一个attachEvents方法,从画廊中查找下一个和上一个按钮,然后调用play方法。当你写完代码时,你应该有类似这样的东西:

function attachEvents() {
    $galleryControls
        on('click', 'a[href="#next"]', play).
        on('click', 'a[href="#previous"]', play);
}

这里没有什么不同。我们使用缓存的$galleryControls变量,并告诉它监听来自下一个和上一个按钮的click事件。当click事件来自指定的元素时,然后调用我们的play方法。如果我们现在运行我们的代码,除了可能会因为play不存在而出现错误之外,什么也不会发生。但我们不要这样做;相反,在所有设置代码完成后,我们将在构造函数中调用我们的attachEvents方法:

function Gallery() {
  // our previous code 

    attachEvents();
}

这里没有什么疯狂的,我们只是调用attachEvents,一个私有方法。你是否注意到,即使它是一个私有方法,我们仍在使用$galleryControls?这是因为该变量存在于闭包范围内,因此这样可以更容易地管理变量,而不会污染程序的全局范围。如果你还不明白这里发生了什么,不要担心。随着时间和实践,这将变得清晰,事情将变得更容易。

现在,我们还有一个问题。没有play方法,所以让我们创建它!

处理我们的事件

因为我们的play方法不存在,所以我们的应用程序失败了;所以我们需要编写它。但它应该做什么?对于这个应用程序,我们希望它确定画廊应该播放的方向。然后我们希望它根据画廊当前位置的左右动画。你可能会说,这听起来比你想象的要容易。但实际上是这样的。所以让我们一步一步来。

再次缓存变量

是的,我们希望尽可能缓存。再次强调,我们正在为 iPhone 创建一个移动应用程序,由于移动设备的性质,我们需要尽可能进行优化。但我们应该缓存什么?好吧,我们将首先检查方向,然后操作无序列表的当前左侧位置。为了防止查找这些值,让我们在方法的顶部声明一个currentLeftPos和方向,如下所示:

function play(e) {
    var currentLeftPos, direction;
}

简单!现在,让我们确定这些值。确定方向的简单方法是基于所点击元素的值。在这种情况下,我们可以检查#next 或#previous,即href属性的值。为了使其更简单,我们可能还想删除井号,以防我们将来想公开此方法并允许自己传递nextprevious。所以让我们这样做:

function play(e) {
    var currentLeftPos, direction;

    direction = $(this).attr('href');

    direction = direction.substr(1, direction.length);
}

提示

不要太担心这里的细节,但基本上,由于play是一个事件处理程序,this已经成为目标事件,这将是我们的锚标签。这是我们如何可以从这些元素中获取href值的方式。同时,不要对那里进行的字符串操作太紧张。基本上,我们使用了substr,这是一个内置的string方法,并传递了1,这样它就从位置 1 开始获取字符串的其余部分。这就是我们如何能够从href属性中获取单词“next”或“previous”的方式。

很好,到这一点上我们已经确定了方向。现在我们想要获取无序列表的最新左位置。为了做到这一点,我们可以在设置方向之后添加以下代码:

function play(e) {

  // Previous code

    currentLeftPos = parseInt($slidesContainer.css('left'), 10);
}

注意

请注意,我们使用了parseInt,这是一个内置的数字方法,它接受一个整数作为其第一个参数,然后将基数作为其第二个参数。我们这样做是因为当我们请求left属性的值时,我们得到类似0px的东西,而我们希望我们使用的值是一个整数,而不是一个字符串。因此,parseInt通过将0px解释为0的整数来帮助我们。

现在是时候创建我们应用程序的神奇部分了。这部分有点复杂,但最终将帮助我们实现我们想要的效果。但首先让我们专注于让我们的应用程序在下一个行动呼叫时移动。为了做到这一点,我们希望将无序列表的左位置设置为当前左位置减去单个幻灯片的宽度。为了做到这一点,我们可以在设置currentLeftPos之后简单地编写以下代码:

function play(e) {
    // Previous code

    // Next
    $slidesContainer.css({ 
    'left': currentLeftPos + -(slidesWidth) + 'px' });
}

前面的代码将完全按照我们的要求执行;但是,我们遇到了一些问题。首先,这将始终运行,即使点击了“previous”按钮。其次,没有检查当你到达画廊的最末端时。这可以很容易地添加到我们的应用程序中,就像这样:

function play(e) {
    // Previous code

    // Next
    if (direction === 'next') {
        if (Math.abs(currentLeftPos) < (galleryWidth - slidesWidth)) {
            $slidesContainer.css({
                'left': currentLeftPos + -(slidesWidth) + 'px'
            });
        }
    }
}

提示

您可能已经注意到我们在currentLeftPos上使用了Math.abs。这是因为我们将得到一个负数作为我们的值,而且由于我们不想使数学或比较复杂化,我们只需使用Math.abs将其转换为正整数。保持简单!

在这个调整后的代码中,我们检查方向,寻找next,然后检查当前左位置是否小于画廊宽度减去单个幻灯片的宽度。这有助于防止可能出现的任何错误。

现在开始实现我们的previous功能。在这一步中,我们将按照相同的步骤进行;我们将确保我们要向previous方向前进,然后我们将进行比较,以确保我们不会低于0标记,最后我们将在条件满足时执行代码。当我们完成实现这个功能时,我们应该有以下代码:

function play(e) {
    // Previous code

    // Previous
    if (direction === 'previous') {
        if (Math.abs(currentLeftPos) > 0) {
            $slidesContainer.css({
                'left':  currentLeftPos + slidesWidth + 'px'
            });
        }
    }
}

唯一的区别是我们正在与静态数字0进行比较。这是为了防止任何会在我们的画廊中引起视觉错误的正值。然后,我们不是对我们的数字取反,而是使用正确的值以便将其加到负数上,从而呈现Previous操作的外观。

最后,我们的play方法应该是这样的:

function play(e) {
    var currentLeftPos, direction;

    direction = $(this).attr('href');

    direction = direction.substr(1, direction.length);

    currentLeftPos = parseInt($slidesContainer.css('left'), 10);

    // Next
    if (direction === 'next') {
        if (Math.abs(currentLeftPos) < (galleryWidth - slidesWidth)) {
            $slidesContainer.css({
                'left': currentLeftPos + -(slidesWidth) + 'px'
            });
        }
    }

    // Previous
    if (direction === 'previous') {
        if (Math.abs(currentLeftPos) > 0) {
            $slidesContainer.css({
                'left':  currentLeftPos + slidesWidth + 'px'
            });
        }
    }
}

我们完成了吗?是的!尽管我们只是在切换无序列表的左位置值,但我们实际上是在进行动画,因为如果你记得,我们已经告诉我们的元素在 CSS 中过渡左属性。看看使用 CSS3 属性是多么简单和有效?通过简单的声明,我们已经能够最小化代码,并制作出高度优化的版本。

现在,我们的画廊的核心已经完成,让我们使其响应式!

画廊响应性

我们要稍微绕个弯,但这是值得的努力!在这一步中,我们将研究如何使我们的画廊对用户设备的宽度做出响应。所以让我们开始设置我们的样式。

设置画廊样式

在这里,我们将设置所有必要的样式,使我们的画廊具有响应性。我们需要做一些事情。首先,让我们使用Gallery函数的prototype创建一个公共的setStyles方法:

Gallery.prototype.setStyles = function() {

    return this;
};

如你可能已经注意到的,前面的方法返回了Gallery的实例,因此允许你链接你的方法。接下来,获取单个幻灯片的宽度。这个宽度是其所在容器的 100%,因此应该与画廊本身的宽度相同。为了获取这个宽度,我们可以在setStyles中进行以下操作:

Gallery.prototype.setStyles = function() {

    slidesWidth = $slides.width();

    return this;
};

现在,我们可以通过将幻灯片的数量乘以每个幻灯片设置的宽度来确定画廊的完整宽度,这是我们在上一步中已经确定的。当我们这样做时,我们得到以下代码:

Gallery.prototype.setStyles = function() {

    slidesWidth = $slides.width();

    galleryWidth = slidesWidth * $slidesLength;

    return this;
};

以下步骤可能会令人困惑,但它非常重要,因为我们需要手动设置每个幻灯片的宽度,以便将它们浮动在一起。因此,我们现在需要做的是将slideWidth值应用到每个幻灯片上,如下所示:

Gallery.prototype.setStyles = function() {

    slidesWidth = $slides.width();

    galleryWidth = slidesWidth * $slidesLength;

    $slides.width(slidesWidth);

    return this;
};

现在,我们还可以使用计算画廊宽度来设置幻灯片容器的宽度。同样,我们需要这样做,以便保持一个具有左浮动幻灯片的画廊。因此,我们将设置幻灯片容器的宽度,然后将所有幻灯片左浮动。当我们编写这些要求时,你的setStyles方法将如下所示:

Gallery.prototype.setStyles = function() {

    slidesWidth = $slides.width();

    galleryWidth = slidesWidth * $slidesLength;

    $slides.width(slidesWidth);

    $slidesContainer.css({'width': galleryWidth});

    $slides.css({'float': 'left'});

    return this;
};

这就是以响应式方式设置我们的画廊样式所需的全部步骤。然而,这里有一个问题;样式无法重置,这是为了在设备的方向或宽度发生变化时适当地确定幻灯片和容器的宽度而需要的。让我们进行一些设置工作,以便进行重置。

为了做到这一点,我们将简单地将我们的功能包装在一个方法中,然后将其传递给一个公共的resetStyles方法。在这种技术中,我们实质上是在发送一个回调,当resetStyles功能完成时将被执行。目前,你的代码应该产生以下结果:

Gallery.prototype.setStyles = function() {

    this.resetStyles(function(){
        slidesWidth = $slides.width();

        galleryWidth = slidesWidth * $slidesLength;

        $slides.width(slidesWidth);

        $slidesContainer.css({'width': galleryWidth});

        $slides.css({'float': 'left'});
    });

    return this;
};

正如你所看到的,我们最初为setStyles创建的所有功能都被包装在一个匿名函数中,也被称为回调,当resetStyles运行完成时将被调用。为了全面了解情况,让我们继续创建我们的resetStyles函数。

重置画廊样式

重置元素的样式实际上并不复杂,所以我们将直接进入这个方法。查看下面应该在你的reset方法中的代码。

Gallery.prototype.resetStyles = function(callback) {
    $slides.attr('style', null);

    $slidesContainer.attr('style', null);

    $slides.attr('style', null);

    if (typeof callback !== 'undefined') {
        callback.call(this);
    }

    return this;
};

不会太疯狂吧?我们基本上只是删除 Zepto 在我们使用 JavaScript 设置元素样式时应用的内联样式,或者我们在setStyles方法中所做的事情。当我们删除这些样式时,然后检查是否有回调方法并执行该方法。这是一个很好的做法,因为假设我们需要出于任何其他原因重置我们画廊的样式;我们不想无缘无故地创建不必要的函数。

初始化画廊样式

我们需要做的最后一件事是初始化我们的样式。为此,让我们在Gallery构造函数中初始化代码时调用setStyles

function Gallery() {
  // our previous code 

   this.setStyles();
    attachEvents();
}

当我们最终设置好我们的样式时,我们的应用程序在纵向模式下应该如下所示:

初始化画廊样式

响应式画廊

在横向模式下,我们的应用程序应该如下所示:

初始化画廊样式

响应式画廊

提示

不幸的是,你的应用程序不会看起来或行为像这些截图中显示的应用程序;这是因为现在没有任何连接,我们甚至还没有初始化我们的任何代码。但是,如果你确实想立即进行操作并查看我们是如何做的,你可以在本章的最后一节之前查看我们的结论。如果你按照这些步骤,你应该会得到一个类似于我们刚刚看到的应用程序。

从技术上讲,我们的画廊现在已经完全构建好了,我们现在可以使用下一个和上一个按钮完全与之交互。但现在,让我们开始等待已久的有趣的触摸事件!

扩展触摸功能的画廊

默认情况下,我们可以将触摸交互包含在Gallery类中,但这不具有可重用性,也无法应用于应用程序的其他部分。因此,在本节中,我们将创建一个名为Swipe的新类,它将包含检测特定模块上滑动手势所需的一切。

基本模板

与以往编写的其他类似,我们始终希望从基本框架开始。我们可以编写以下基本模板来开始:

var App = window.App || {};

App.Swipe = (function(window, document, $){

  var _defaults = {};

  function Swipe(options) {
    this.options = $.extend({}, _defaults, options);
  }

    return Swipe;

}(window, document, Zepto));

Swipe类与我们的Gallery类有些不同,它接受windowdocumentZepto对象。另一个不同之处在于Swipe构造函数接受一个名为options的参数,用于覆盖我们即将设置的默认值。

默认选项和模块化滑动事件

Swipe类内部有几件事情要做。首先,我们希望确保它仅适用于特定容器,而不是整个文档。然后,我们希望能够缓存某些值,如触摸的初始 x 位置和结束 x 位置。这些缓存的值也应该在闭包作用域中可用,以便它们在所有方法中都可用。

以下是我们想要的默认值和将在闭包作用域中可用的缓存值:

var _defaults = {
  'el': document.body,
  '$el': $(document.body)
},
el,
$el,
delta,
initXPos,
endXPos,
threshold = 30;

在前面的代码中,我们基本上是在说默认元素,滑动功能,应该附加到文档的body元素。从这里开始,我们确保可以在闭包作用域中访问这些缓存的元素。最后,我们设置一些变量,将存储关于我们将要监听的触摸手势的信息。

现在在我们的构造函数中,我们要覆盖这些默认值,并确保一些这些初始值将存在于全局作用域中:

function Swipe(options) {
  this.options = $.extend({}, _defaults, options);
  $el = this.$el = this.options.$el = $(this.options.el);
  threshold = this.options.threshold || threshold;

  this.init();
}

在这里,我们使用 Zepto 的extend方法创建一个新对象,其中包含将选项参数合并到默认对象中。然后,我们确保闭包作用域包含了滑动类将附加到的缓存元素。最后,我们检查是否传递了自定义阈值,并覆盖默认的 30。在所有这些之后,我们在构造函数的末尾调用一个初始化方法,以便Swipe类自动启动。

监听触摸事件

现在我们需要将适当的事件附加到Swipe类。这些事件将基于我们之前介绍的触摸事件,但它们将以模拟滑动手势的方式使用。为了实现这一点,我们首先需要监听touchstarttouchendtouchmove事件,并为每个事件分配事件处理程序。我们可以在我们从构造函数调用的init方法中完成所有这些。

因此,首先让我们在Swipeprototype上创建我们的init方法,并确保在方法的末尾返回实例:

Swipe.prototype.init = function() {

  return this;
};

在这个方法中,我们希望监听前面提到的触摸事件,并确保它们有事件处理程序。为此,我们将使用 Zepto 的on方法,并将事件附加到我们缓存的元素上:

Swipe.prototype.init = function() {
  this.options.$el.
    on('touchstart', handleTouchStart).
    on('touchend', handleTouchEnd).
    on('touchmove', handleTouchMove);

  return this;
};

在前面的代码中,我们将事件作为字符串传递给on方法的第一个参数,然后分配一个尚未创建的事件处理程序。您还会注意到这些方法是可链接的,允许我们一次附加多个事件。这就是为什么我们在公共方法的末尾返回this,以便我们可以允许自己同步调用的原因。

处理触摸事件

现在我们需要创建我们分配给每个监听器的事件处理程序。我们将逐个处理处理程序,以便解释如何从这些触摸事件中创建滑动手势。我们首先要看的是touchstart处理程序。

当我们把手指放在手机上时,我们想要做的第一件事是存储手指的初始 x 位置。要访问这些信息,事件触发时会有一个touches数组。因为我们只想使用第一个触摸,所以我们需要访问touches数组中的第一个触摸。一旦我们得到第一个触摸,我们就可以使用该对象上的pageX属性来获取 x 位置。这就是handleTouchStart的功能将会是什么样子:

function handleTouchStart(e) {
    initXPos = e.touches[0].pageX;
}

正如你所看到的,handleTouchStart方法接受一个参数,即事件对象。然后我们将initXPos设置为事件对象上touches数组中第一个触摸的pageX属性。这可能听起来很混乱,但基本上我们只是访问我们需要的对象,以便保存您触摸的初始 x 值。

接下来,我们想要创建handleTouchMove事件处理程序。这个处理程序将包含与handleTouchStart相同的概念,但我们想要更新结束的 x 位置,而不是初始的 x 位置。可以在以下代码中看到:

function handleTouchMove(e) {
  e.preventDefault();
    endXPos = e.changedTouches[0].pageX;
}

这里有一些我将解释的不同之处。首先,我们阻止了触摸移动的默认行为。这是为了阻止发生任何奇怪的行为,通常建议在我们想要创建独特体验时使用,比如可滑动的画廊。

你会注意到的另一个区别是我们正在查看事件的changedTouches对象。这是因为move事件不包含touches对象。尽管有点有争议,但这有助于跟踪每次触摸和该特定触摸的更改属性。因此,如果我有多次触摸,那么我的changedTouches对象将适当地包含每次更改的触摸。

到目前为止,我们所做的只是设置初始和结束的 x 位置。现在我们需要使用这些值来创建一个delta值,然后使用它来触发左右方向的滑动。这就是我们的handleTouchEnd事件处理程序将为我们做的事情。

这是handleTouchEnd应该包含的代码:

function handleTouchEnd(e) {
    endXPos = e.changedTouches[0].pageX;
    delta = endXPos - initXPos;

    if(delta > threshold) {
        $el.trigger('SwipeLeft');
    }

    // The *-1 converts the threshold to a negative integer
    if(delta < threshold*-1) {
        $el.trigger('SwipeRight');
    }
}

现在让我们逐行查看这段代码。首先我们做的和handleTouchMove一样,就是设置结束的 x 位置。接下来,我们设置我们的delta值,即通过从初始 x 位置中减去结束 x 位置得到的差值。现在我们进行比较;如果delta大于阈值,那么触发一个名为SwipeLeft的自定义事件。我们的下一个比较有点更加混乱,但基本上我们检查delta值是否小于负阈值。这是为了检测向右方向的滑动。

我们的Swipe类现在已经完成。我们已经创建了监听我们触摸事件的必要功能,然后模拟了一个手势,我们可以将其连接起来。但实际上我们还没有将它连接到我们的画廊,这是整个过程中的最后一步。因为现在你已经达到了这一点,所以应该感到自豪,因为现在将会发生容易的事情!

把所有东西放在一起

好的,到目前为止我们有一个画廊和使用触摸事件检测滑动手势的能力。但现在,没有什么真正连接在一起,实际上我们还没有初始化我们的Gallery类,所以现在什么都不应该工作。但这就是最后一部分的内容;我们将会初始化我们的Gallery类,添加Swipe功能,然后对我们的滑动事件做出反应。

JavaScript

我们要做的第一件事是打开我们的App.Touch.js文件,你还记得这个文件与我们的触摸页面的功能相关,因此这个文件将包含我们所有的初始化。当我们打开这个文件时,转到init方法,或者如果还没有创建,那么创建并初始化一个Gallery的实例:

Touch.prototype.init = function() {
  var that = this;

  // Initializing Gallery
  this.gallery = new App.Gallery();

  return this;
};

现在我们已经初始化了我们的Gallery类,画廊应该立即初始化。但请记住,我们还没有修改我们的标记以包含这个文件。所以即使在这一点上,你也看不到你劳动的成果。但让我们确保我们继续设置工作。在下一步中,我们想要初始化我们的Swipe类,并确保它将自己设置为gallery元素:

Touch.prototype.init = function() {
  // Previous code

  // Initializing Swipe
  this.swipe = new App.Swipe({
    'el': document.querySelector('.gallery')
  });

  return this;
};

现在,即使在这一点上,我们的画廊也不会响应滑动事件。这是因为我们的滑动功能只检测触摸并分派我们之前设置的自定义事件,所以我们需要做的是在画廊上监听这些事件,然后告诉它播放下一个或上一个幻灯片:

Touch.prototype.init = function() {
  // Previous code

  // Listen to the swipe and then trigger the appropriate click
  this.swipe.$el.
    on('SwipeLeft', function(){
      that.gallery.$el.find('a[href="#previous"]').trigger('click');
    }).
    on('SwipeRight', function(){
      that.gallery.$el.find('a[href="#next"]').trigger('click');
    });

  return this;
};

在前面的代码中,我们监听由我们的滑动实例分派的SwipeLeftSwipeRight事件。当任一事件被分派时,根据事件,我们模拟点击上一个或下一个按钮。通过这种方式,我们能够让用户看起来在整个画廊中滑动,同时消除任何复杂性。

当你完成编写你的init方法时,它应该是这样的:

Touch.prototype.init = function() {
  var that = this;

  // Initializing Gallery
  this.gallery = new App.Gallery();

  // Initializing Swipe
  this.swipe = new App.Swipe({
    'el': document.querySelector('.gallery')
  });

  // Listen to the swipe and then trigger the appropriate click
  this.swipe.$el.
    on('SwipeLeft', function(){
      that.gallery.$el.find('a[href="#previous"]').trigger('click');
    }).
    on('SwipeRight', function(){
      that.gallery.$el.find('a[href="#next"]').trigger('click');
    });

  return this;
};

标记

需要处理的最后一项是页面上的标记 - 包括的脚本。为了简化事情并最终使您的应用程序正确运行,以下是您需要在页面上包含的内容:

    <script src="img/zepto.min.js"></script>
    <script src="img/helper.js"></script>
    <!-- BEGIN: Our Framework -->
    <script src="img/App.js"></script>
    <script src="img/App.Nav.js"></script>
    <script src="img/App.Gallery.js"></script>
    <script src="img/App.Swipe.js"></script>
    <script src="img/App.Touch.js"></script>
    <!-- END: Our Framework -->
    <script src="img/main.js"></script>
    <script> touch = new App.Touch(); </script>

与其他页面相比,这里的不同之处在于我们只包括我们需要的项目,包括App.Nav.jsApp.Gallery.jsApp.Swipe.jsApp.Touch.js。与其他页面相比,我们正在包括整个框架,但对于这个页面或任何以后的页面,我们实际上不需要这样做。需要注意的一点是,我们还创建了一个全局的触摸对象,它被设置为我们App.Touch类的一个实例。这样我们可以在调试器中轻松地引用它,但这应该被替换为App.touch,这样它就不会污染全局命名空间。

我们到达了终点!在这一点上,你应该有一个完全功能的画廊,可以进行滑动交互。现在给自己一个鼓励吧;这是一个漫长的旅程,但我希望你能欣赏到我们已经创建了可重用的、模块化的代码,它是完全自包含的。除此之外,我们的画廊是完全响应式的,可以适应用户的设备,让他们能够一致地享受体验。

总结

在本章中,我们重新设计了我们的主导航,讨论了触摸和手势事件的基本原理,然后使用一个响应式的照片画廊实现了这两种类型的事件,这将适应用户的设备。我们还讨论了如何附加这些事件,并根据幻灯片放映的要求适当地处理它们。从现在开始,你应该对如何使用触摸事件在 iPhone 上创建独特体验有很好的理解,以及在其他移动设备上也是如此。接下来,让我们来看看在 iPhone 上处理 HTML5 表单时会有一些特殊的交互。

第五章:了解 HTML5 表单

在本章中,我们将使用最新的 HTML5 技术来查看表单,包括新的输入类型和表单属性。我们将简要回顾一些我们将在示例表单中使用的新输入类型。然后,我们将讨论规范中的一些新属性,同时专门针对移动设备查看autocapitalize属性。在深入研究我们的示例表单之前,我们考虑 iOS 设备上的表单布局以及与这些表单交互时出现的限制。最后,我们创建一些示例表单,开发一些简单的验证,然后专门为 iOS 和支持 WebKit 的浏览器样式化我们的表单。

一旦我们审查了所有这些功能,并且已经浏览了我们的示例表单,我们应该对 HTML5 表单以及它们与为 iOS 开发 Web 应用程序有何关联有了扎实的理解。

以下是本章将涵盖的主题:

  • 新的 HTML5 输入类型

  • 新的 HTML5 表单特定属性

  • iPhone 的表单布局

  • 表单验证

  • iOS 的表单样式

因此,让我们首先来看一下新的标准 HTML5 输入类型。

HTML5 输入类型

HTML5 引入了几种新的输入类型,加快了应用程序的开发。总共有 13 种新的输入类型在 HTML5 规范中引入,包括日期时间、本地日期时间、日期、月份、时间、周、数字、范围、电子邮件、网址、搜索、电话和颜色。不幸的是,这些新输入中只有 10 种在 iOS 上受支持,但不用担心,因为类型会自动默认为文本。这对我们帮助不大,但它确实允许我们为我们需要但不受支持的类型创建 polyfill。无论如何,以下是 iOS 上支持的所有输入类型的详细说明:

输入类型 描述
--- ---
按钮 代表没有额外语义的按钮。
复选框 代表可以切换的状态或选项。
日期 代表将元素的值设置为表示日期的字符串的控件。
日期时间 代表将元素的值设置为表示全局日期和时间(带有时区信息)的字符串的控件。
本地日期时间 代表将元素的值设置为表示本地日期和时间(不带时区信息)的字符串的控件。
电子邮件 代表编辑电子邮件地址列表的控件。
文件 代表文件项目列表,每个项目包括文件名、文件类型和文件主体(文件的内容)。
隐藏 代表用户不打算检查或操作的值。
图像 代表 UA 从中启用用户交互地选择一对坐标并提交表单的图像,或者用户可以从中提交表单的按钮。
月份 代表一个控件,用于将元素的值设置为表示月份的字符串。
数字 代表一个精确的控件,用于将元素的值设置为表示数字的字符串。
密码 代表用于输入密码的单行纯文本编辑控件。
单选按钮 代表从项目列表中选择一个项目的选择(单选按钮)。
范围 代表一个不精确的控件,用于将元素的值设置为表示数字的字符串。
重置 代表重置表单的按钮。
搜索 代表用于输入一个或多个搜索词的单行纯文本编辑控件。
提交 代表提交表单的按钮。
电话 代表用于输入电话号码的单行纯文本编辑控件。
文本 代表输入元素值的单行纯文本编辑控件。
时间 代表将元素的值设置为表示时间(不带时区信息)的字符串的控件。
url
week

这些详细信息可在以下网址找到:

尽管我们可以在这里尝试许多输入,但我们只会专注于新的emailnumberdatetimerange类型。本书中的示例表单还将包含常规类型,包括textpasswordsubmit

现在我们对支持的内容有了很好的了解,并且有了适合我们需求的类型的信息参考,让我们继续审查我们也可以利用的属性。

HTML5 表单属性

在 HTML5 中有许多属性可供我们使用,但为了简化这部分,我们将专注于我们可以在输入和表单上使用的新属性。以下属性在最新的 HTML5 规范中定义,除了autocapitalize外,在 iOS 上也得到支持:

输入属性 描述
autocapitalize 指定文本元素的自动大写行为。
autocomplete 指定元素是否表示用户输入的输入控件(以便用户代理可以稍后预填充表单)。
min 元素值的预期下限。
max 元素值的预期上限。
multiple 指定元素允许多个值。
placeholder 一个短提示(一个词或短语),旨在帮助用户输入控件的数据。
required 指定元素是表单提交的必需部分。

您可以在以下网址找到这些属性的详细信息:

提示

并非所有表单属性都列在上表中;只列出了 HTML5 规范中定义的最新支持的属性。这是为了让我们对最新和最好有一个很好的了解。然而,如果您想获得更广泛的支持,我鼓励您查看上述详细信息的来源,并对规范中每个属性进行彻底的解释。

我们现在对 iOS 支持的最新属性有了基本的了解。我们现在可以简要地回顾一些设计考虑,然后直接进入一些示例 HTML5 表单,看看最新的输入类型和属性如何一起工作,以简化我们的开发过程。

iPhone 的表单布局

在这一部分,我们简要介绍了在为 iOS 创建表单时的一些设计考虑。您可能对表单的设计有或没有完全控制;然而,为了更容易理解可能出现的限制,以下表格有助于展示我们在处理表单时所拥有的有限屏幕空间。希望这将帮助您解释这些限制,以便进行调整。让我们来看看以下表格:

UI 控件 像素尺寸
状态栏 高度 20 英寸
URL 文本字段 高度 60 英寸
表单助手 高度 44 英寸
键盘 竖屏高度 216 英寸,横屏高度 162 英寸
按钮栏 竖屏高度 44 英寸,横屏高度 32 英寸

有关这些控件的详细信息可以在developer.apple.com/library/safari/#documentation/AppleApplications/Reference/SafariWebContent/DesigningForms/DesigningForms.html找到。

根据这些值,当这些控件出现时,我们需要调整我们的表单以适应特定的尺寸。例如,如果所有这些控件都出现,除了按钮栏,而我们有 480 像素的可用高度,那么我们的屏幕房地产最终将达到惊人的高度 140 像素。

正如你所看到的,为 iOS 创建可用的表单是一个挑战,但并非不可能。有一些有趣的技术可以用来适应我们应用程序中的表单。但最好的技术是简单。确保你不要一次要求用户提供大量信息;所以不要要求姓名、电子邮件、密码和密码确认以及出生日期,而只要求用户名、密码和电子邮件地址。保持简单在我们的应用程序中有很大帮助,并有助于改善用户体验。

我们现在对为 iOS 设计表单时出现的限制有了相当的了解,但现在让我们跳入功能性,看看我们如何创建一些简单的表单。

示例 HTML5 表单

现在我们将仔细研究一些代码,包括标记、脚本和样式。其中一些你可能已经知道,大部分重点将放在新的 HTML5 输入和属性上。我们将看看它们如何被实现到表单中,它们对 UI 控件的影响,以及如何将这项新技术应用到我们的脚本中。但首先,让我们做一些设置工作,以确保我们的页面保持一致。

设置工作

我们需要做的第一件事是打开我们的表单页面的index.html文件。一旦打开了这个文件,你会看到我们最初在本书开始时创建的旧模板。随着我们的应用程序的发展,我们必须更新这个模板以反映这些变化,所以让我们做以下任务:

  • 在我们的主要样式之后包含表单样式(forms.css

  • 更新导航以反映我们的新菜单

  • 包括我们的导航脚本(App.Nav.js)和我们的表单脚本(App.Forms.js

包括我们的表单样式

目前,我们的页面没有任何样式,但我们应该包括我们的页面特定样式表。当我们这样做时,我们的头部应该是这样的:

    <!DOCTYPE html>
    <html class="no-js">
    <head>
        [PREVIOUS META TAGS]

        <link rel="stylesheet" href="../css/normalize.css">
        <link rel="stylesheet" href="../css/main.css">
        <link rel="stylesheet" href="../css/forms.css">
        <script src="img/modernizr-2.6.1.min.js"></script>
    </head>

更新导航

与上一章一样,我们需要更新我们的导航以反映新的选择菜单。这有助于为我们的应用程序节省屏幕房地产。当我们更新我们的导航时,我们的标记将更新为以下代码:

<nav>
    <select>
        <option value="../index.html">Application Architecture</option>
        <option value="../video/index.html">HTML5 Video</option>
        <option value="../audio/index.html">HTML5 Audio</option>
        <option value="../touch/index.html">Touch and Gesture Events</option>
        <option value="../forms/index.html" selected>HTML5 Forms</option>
        <option value="../location/index.html">Location Aware Applications</option>
        <option value="../singlepage/index.html">Single Page Applications</option>
    </select>
</nav>

包括我们的导航和表单脚本

现在我们的导航已经就位,让我们包含导航脚本,同时让我们包含我们的表单的页面特定脚本:

<script src="img/zepto.min.js"></script>
<script src="img/helper.js"></script>
<!-- BEGIN: Our Framework -->
<script src="img/App.js"></script>
<script src="img/App.Nav.js"></script>
<script src="img/App.Forms.js"></script>
<!-- END: Our Framework -->
<script src="img/main.js"></script>

正如你所看到的,我们只包含了这个页面所需的必要脚本。

表单

我们将在页面上开发三种不同的表单,包括登录、注册和个人资料表单。它们非常基本,大部分将演示表单的实现。在每段代码之后,我们将审查新的输入并提供一些关于它们如何影响我们的标记和用户界面的背景信息。在这部分,不要担心整体结构;也就是说,不要担心表单的包含div或带有标题的部分。结构不会被讨论,大部分是作为指导线给你的。所以,让我们从我们的登录表单开始。

登录表单

以下是我们登录表单的结构。仔细审查这一点,主要关注“表单”元素以及它如何利用“自动大写”属性,然后看看我们如何在用户名和密码字段上实现了必填属性:

<!-- BEGIN: LOGIN CONTAINER -->
<form autocorrect="off" autocapitalize="off">
    <div class="error-messaging"></div>
    <label for="login-username">Username</label>
    <input name="username" id="login-username" type="text" placeholder="johndoe" required>
    <label for="login-password">Password</label>
    <input name="password" id="login-password" type="password" required>
    <input type="submit" value="Submit">
</form>
<!-- END: LOGIN CONTAINER -->

当我们看最终产品时,由于我们还没有为我们的表单设置样式,它应该看起来有点像这样:

登录表格

我们的登录表格

如你所见,我们在“表单”元素上将“自动大写”设置为关闭。这基本上告诉移动 Safari 不要对其中的任何输入进行大写。我们可以很容易地在每个单独的输入上设置这个属性为“关闭”,但为了简化这个演示,我们将其保留在“表单”元素上。

这里还有一件很酷的事情是,我们在用户名和密码上都设置了“必填”。这很棒,因为除非填写了这些字段,否则不会提交表单。在过去,我们需要设置一个“必填”的类,然后用 JavaScript 进行检查;现在有了 HTML5,我们就不需要了。

提示

我知道你们中的一些人可能会感到震惊,但在 iOS 中,你不会收到任何关于字段是否必填的通知。根据开发者文档,它不受支持。那么为什么在这里提到它呢?因为如果我们真的想要支持多个移动设备,包含这个属性仍然是一个好主意,这样我们的应用程序就会对设备友好,如果苹果选择在未来支持它,我们的应用程序就是未来的。再次强调,这需要你和可能是你的团队来权衡,但拥有这个属性符合 HTML 5 规范——只是在 iOS 上不受支持而已。

我们还可以看到“占位符”属性被用来为我们的文本输入应用一些默认文本。请记住,“占位符”就是一个占位符。它并不设置我们输入的值,所以值仍然是空的。

注册表格

现在我们转向我们的注册表格。在这个表格中,我们将收集用户的姓名、用户名、电子邮件、密码和确认密码。再次强调,不要关注结构。集中精力关注“自动更正”属性在“表单”元素上的实现,然后关注“电子邮件”输入类型的使用。

<!-- BEGIN: REGISTER CONTAINER -->
<form autocorrect="off" autocapitalize="off">
    <div class="error-messaging"></div>
    <div class="field">
        <label for="register-name">Name</label>
        <input name="name" id="register-name" type="text" placeholder="John Doe">
    </div>
    <div class="field">                    
        <label for="register-username">Username</label>
        <input class="required" name="username" id="register-username" type="text" placeholder="johndoe">
    </div>
    <div class="field">
        <label for="profile-email">Email</label>
        <input class="required" type="email" id="profile-email" autocorrect="off">
    </div>
    <div class="field">
        <label for="register-password">Password</label>
        <input class="required" named="password" id="register-password" type="password">
    </div>
    <div class="field">
        <label for="register-password-confirm">Confirm Password</label>
        <input class="required" named="password" id="register-password-confirm" type="password">
    </div>
    <input type="submit" value="Register">
</form>
<!-- BEGIN: REGISTER CONTAINER -->

当我们完成了这一部分和一些初步的样式后,我们的表单会看起来像这样:

注册表格

我们的注册表格

在这个表格中,我们已经关闭了所有表单字段的“自动更正”。再次强调,我们可以逐个元素地进行设置,但为了简化操作,我们选择将其添加到“表单”元素中。

最后要考虑的一点是使用输入类型“电子邮件”。当我们开始使用一些定制的输入类型时,我们的用户界面会相应调整。例如,当我们点击“电子邮件”输入类型时,我们会看到控件会改变以包括@符号:

注册表格

电子邮件输入类型

现在,让我们更仔细地看看其他输入类型是如何影响我们的用户界面的。

个人资料表格

以下表单是登录和注册表单的一种组合,带有一些额外的字段。然而,有一些区别,所以让我们专注于改变的部分。在这个例子中,我们会看到我们已经将“自动大写”更改为“句子”,并且只在我们想要应用的字段上将“自动更正”设置为“关闭”。除此之外,我们开始使用“日期时间”、“数字”和“范围”输入类型。我们做出的最后一个改变是使用类而不是属性来应用“必填”字段——这将在我们脚本的实现中进一步解释。现在,先审查标记,然后继续阅读解释。

<!-- BEGIN: PROFILE UPDATES -->
<form autocapitalize="sentences">
    <div class="error-messaging"></div>
    <h2>Basic Information</h2>
    <div class="field">
        <label for="profile-name">Name</label>
        <input name="name" id="profile-name" type="text" placeholder="John Doe">
    </div>
    <div class="field">
        <label for="profile-username">Username</label>
        <input name="username" id="profile-username" type="text" placeholder="johndoe" autocorrect="off">
    </div>            
    <div class="field">
        <label for="profile-dob">Date of Birth</label>
        <input type="datetime" id="profile-dob">
    </div>            
    <div class="field">
        <label for="profile-email">Email</label>
        <input type="email" id="profile-email" autocorrect="off">
    </div>
    <h2>Personal Information</h2>
    <div class="field">
        <label for="profile-age">Age</label>
        <input type="number" id="profile-age">
    </div>
    <div class="field">
        <label for="profile-city">City</label>
        <input type="text" id="profile-city" placeholder="Boston">
    </div>
    <div class="field">
        <label for="profile-state">State</label>
        <select name="state" id="profile-state">
            <!-- OPTIONS GO HERE -->
        </select>
    </div>
    <div class="field">
        <label for="profile-zip">ZipCode</label>
        <input type="number" min="0" id="profile-zip">
    </div>
    <h2>Professional Information</h2>
    <div class="field">
        <label for="profile-skills-markup">HTML5</label>
        <input type="range" min="0" max="5" id="profile-skills-markup">
    </div>
    <div class="field">
        <label for="profile-skills-styles">CSS3</label>
        <input type="range" min="0" max="5" id="profile-skills-styles">
    </div>
    <div class="field">
        <label for="profile-skills-scripts">JavaScript</label>
        <input type="range" min="0" max="5" id="profile-skills-scripts">
    </div>
    <h2>Bio Information</h2>
    <label for="profile-bio">About Yourself</label>
    <textarea id="profile-bio" name="about"></textarea>
    <div class="field">
        <label for="register-password">Password</label>
        <input class="required" named="password" id="register-password" type="password">
    </div>
    <p>Provide your password to confirm.</p>
    <input type="submit" value="Update Profile">
</form>

我们的最终产品在样式化后会是这样的:

个人资料表格

我们的个人资料表格

在这个例子中,我们在form元素上将autocapitalize设置为sentences。这有助于我们,因为现在我们已经明确定义了我们希望大写的内容,即只有句子。这在苹果的文档中有描述,可以在那里进一步探索。至于autocorrect,我们在各个项目上设置它,因为我们可能希望在textarea上进行校正。同样,我们可以选择在form元素上将autocorrect设置为off,然后在textarea中将其设置为on,但这是一个选择的问题,完全取决于您作为开发人员。现在让我们来回顾一下几种输入类型。

日期时间类型

在这个例子中,我们使用datetime来处理出生日期字段。这很棒,因为我们的 UI 完全符合我们的期望,以提供准确的信息:

日期时间类型

日期时间输入类型

数字类型

number输入类型也可以操作我们的 UI,以便我们在控件中有一组默认的数字选择:

数字类型

我们的数字输入类型

范围类型

range输入类型是我们表单中非常有用的控件。同样,这种类型提供了一个自定义的 UI,允许我们使用系统默认值,而不是 JavaScript,来提供我们所需的数值类型:

范围类型

范围输入类型

现在我们已经完成了对 HTML5 中一些新的输入字段和属性以及它们如何影响我们的 iOS Web 应用程序 UI 的审查。接下来是使用 JavaScript 来验证我们的表单。同样,这将是非常基础的,并且将介绍我们如何设置一个可重用的表单组件,不会直接与这些新的输入和属性联系起来。这是因为这些自定义输入和属性是规范的一部分,旨在加快开发速度,因此您对使用脚本进行验证的需求应该是有限的。无论如何,让我们继续前进,快速看一下我们的脚本。

表单验证

在这一部分,我们回顾了为这个页面编写的 JavaScript。没有什么真正新颖或突破性的东西;它明确旨在演示如何使用我们在本书中开发的框架来创建自包含的代码,以验证多个表单并使您更容易扩展。所以让我们开始通过回顾基本模板。

基本模板

以下是我们一直在使用的基本模板。使用标准的命名空间技术,扩展App命名空间的Form类将包含我们所有的功能。

var App = window.App || {};

App.Form = (function(window, document, $){
    'use strict';

    var _defaults = {
            'element': 'form',
            'name': 'Form'
        };

    function Form(options) {
        // Customizes the options by merging them with whatever is passed in
        this.options = $.extend({}, _defaults, options);

        this.init();
    }

    //----------------------------------------------------
    //  Private Methods
    //----------------------------------------------------

    //----------------------------------------------------

    //----------------------------------------------------
    //  Event Handlers
    //----------------------------------------------------

    //----------------------------------------------------

    //----------------------------------------------------
    //  Public Methods
    //----------------------------------------------------
    Form.prototype.getDefaults = function() {
        return _defaults;
    };

    Form.prototype.toString = function() {
        return '[ ' + (this.options.name || 'Form') + ' ]';
    };

    Form.prototype.init = function() {
        // Initialization Code

        return this;
    };

    return Form;

}(window, document, Zepto));

请记住,代码是包含在立即调用的函数表达式或 IIFE/闭包中的自包含的。当我们初始化App.Form时,Form构造函数将被调用,我们的公共方法init将初始化我们在其中编写的任何代码。所以让我们从那里开始,附加适当的事件。

初始化我们的表单

我们需要初始化我们的表单,但我们不需要为每个表单创建一个新对象。我们可以通过事件驱动来处理验证,然后使用我们为每个输入写的属性来处理验证。但让我们来看看我们的事件设置。

附加事件

首先,让我们执行事件附加:

this.$element.
  on('submit', 'form', handleFormSubmission);

this.$cache.loginFormContainer.
  on('click', 'a[href="#forgot-password"]', handleForgotPasswordClick).
  on('click', 'a[href="#register"]', handleRegisterClick);

this.$cache.registerFormContainer.
  on('click', 'a[href="#login"]', handleLoginClick);

在上面的代码中,我们有一些事情要做。首先,我们要查找页面上任何表单的提交。然后,当我们提交表单时,我们将调用handleFormSubmission方法,我们将在一会儿编写。以下的事件监听器基本上是登录和注册按钮的显示/隐藏。

这里没有什么新的或突破性的东西,基本上我们只是做一些设置工作,如果需要的话随时可以回来。关键在于,我们没有为每个表单创建一个新的对象实例,而是将我们的代码概括为只监听每个表单上的submit事件。现在让我们创建或设置我们的处理程序,然后编写它们的功能。

事件处理程序

现在,让我们来看一下事件处理程序。

function handleFormSubmission(e) {
  e.preventDefault();

  // Code goes here
}

function handleForgotPasswordClick(e) {
  e.preventDefault();

  // Code goes here
}

function handleRegisterClick(e) {
  e.preventDefault();

  // Code goes here
}

function handleLoginClick(e) {
  e.preventDefault();

  // Code goes here
}

在这里我们并没有做任何新的事情,我们所做的唯一步骤是为我们的代码设置桩,以便我们知道每个功能的位置。从这里开始,我们看一下每个表单提交的验证代码。我们不会看每个表单的显示/隐藏功能,但是你可以查看本书附带的源代码,以了解它是如何工作的。

验证我们的输入

我们将看一下handleFormSubmission方法,并逐步了解我们如何验证我们的字段。如果你在任何步骤感到困惑,不要担心。我们都曾经历过这种情况,我自己有时也会在表单验证和如何在项目中处理它方面遇到困难。

首先,让我们开始缓存我们将要使用的变量:

function handleFormSubmission(e) {
  var $target, errors, $required, fields, $errorText, i, required_fields_length;
}

这些变量描述了它们自己,这是一个标准的做法,因为我们想要理解发生了什么,因此给我们的变量附上有意义的名称是必不可少的。

现在,我们需要阻止表单的默认行为;这意味着我们暂时不想提交表单。为了做到这一点,让我们做以下操作:

function handleFormSubmission(e) {
  var $target, errors, $required, fields, $errorText, i, required_fields_length;
  e.preventDefault();
}

我们添加了e.preventDefault,它告诉事件阻止浏览器中的默认行为。接下来,我们想要定义目标,清空任何先前的错误消息,创建一个空的错误对象,然后找到所有必填元素。可以使用以下代码完成:

function handleFormSubmission(e) {
  // Previous code
  $target = $(e.target);
  $target.find('.error-messaging').empty();
  errors = { 'required': [], 'invalid': [] };
  $required = $target.find(':required');
}

注意

请注意,我们的errors对象包含两个数组:一个required数组和一个invalid数组。这个errors数组将跟踪出了什么问题;例如,如果一个字段是required并且值是empty,那么我们将在error对象内填充required数组,但如果一个输入已经填写但不合法,那么我们将在errors对象内填充invalid对象。

现在,记得当我们添加了required类但没有添加required属性到我们的个人资料表单时吗?前面的代码就无法捕捉到这一点,所以我们会遇到问题。为了防止这种情况发生,我们可以这样做:

function handleFormSubmission(e) {
  // Previous code
  if ($required.length === 0) {
    $required = $target.find('.required')
  }
}

这段代码有助于解决我们在required类上的问题,但确实存在一个逻辑缺陷。你能找到这个缺陷吗?我会留给你作为一个谜题来解决。这个过程的下一步是找到所有的form元素,然后找到required字段并检查它们是否已经填写:

function handleFormSubmission(e) {
  //Previous code
  fields = $target[0].elements;

  i = 0, required_fields_length = $required.length;
  for (i; i < required_fields_length; i++) {
  if ($required[i].value === '') {
      errors.required.push($($required[i]).prev('label').text() + ' is required.');
    }
  }
}

在这一点上,我们基本上在error对象内填充我们的invalid数组。如果字段为空,我们收集与该字段关联的标签的值,并附加一个定制的消息,将呈现给用户。

注意

不幸的是,特定的验证不会被覆盖,比如电子邮件、数字和其他限制。然而,这里有足够的空间让你探索并添加到这段代码中,希望这足以让你理解验证、要求以及如何在代码中处理这些用例。

最后一步是检查错误,如果存在错误,将这些错误呈现给用户,以便他们相应地进行更正:

function handleFormSubmission(e) {
  //Previous code
  if (errors.required.length === 0 && errors.invalid.length === 0) {
    console.log('Form Requirements and Validations Passed');
    return;
  } else {
    $errorText = $('<ul />');

    if (errors.required.length !== 0) {
      $errorText.append('<li>' + errors.required.join('</li><li>') + '</li>');
    }

    if (errors.invalid.length !== 0) {
      $errorText.append('<li>' + errors.invalid.join('</li><li>') + '</li>');
    }

    $target.find('.error-messaging').append($errorText);
  }
}

我们的检查非常简单,我们基本上检查error对象内的invalidrequired数组是否为空。如果是,我们希望继续提交——在这种情况下将是一个 AJAX 调用。否则,我们希望创建一个包含错误的无序列表,然后将它们附加到表单上,以便用户在没有页面刷新的情况下了解出了什么问题。

希望这一部分帮助你理解验证表单的方法。有了 HTML5 规范的最新支持,浏览器已经处理了大部分工作。这加快了开发速度,减少了定制组件的开发,并帮助我们专注于交付。现在作为一个额外的功能,我们继续进行表单的样式设计。

iOS 的表单样式

在本节中,我们将研究如何为我们的表单进行样式设置。如果我们目前在 iOS 设备上甚至桌面浏览器上测试我们的表单,它看起来并不漂亮。事实上,你可能会对它的丑陋感到有点不满。因此,让我们对其进行样式设置,让每个人都满意。我们将从帮助实现良好外观的基本样式开始。然后,我们将考虑如何使用 CSS3 功能自定义我们的组件。

基本样式

样式化表单非常容易。我们可以简单地使用元素本身,但有一个“陷阱”。您可能注意到我们在一个选择器中指定了[type="datetime"]。这是因为datetime输入类型在 iOS 上显示为选择菜单类型的 UI,因此典型的输入选择器不适用。否则,在基本样式中并没有太多真正突出的地方,它基本上给了我们在之前讨论过的表单中使用的输入类型中看到的样式。

/*!
  Forms Styling
*/

label {
    color: #FFF;
    font-family: 'Helvetica', 'Arial', sans-serif;
    font-size: 12px;
    display: block;
    margin: 10px 0 5px 0;
}

input, select, input[type="datetime"], textarea {

    font-size: 13px;

    display: block;
    margin: 0;
    padding: 5px 8px;
}

input[type="submit"] {
    margin: 10px 0;
}

.form-container {
   display: none;
   margin: 15px 0;
}

.form-container.active {
  display: block;
}

form h2 {
    margin: 10px 0 5px 0;
}

.error-messaging ul {
  list-style: square outside;
  margin: 5px 0 0 0;
  padding: 0 0 0 12px;
}

.error-messaging li {
    color: #A12E33;
    font-family: 'Helvetica', 'Arial', sans-serif;
    font-size: 12px;  
}

自定义样式

这就是许多魔术发生的地方。在本节中,我们使用自定义的 CSS3 样式来自定义我们的组件。以下样式将自定义我们的输入、选择,并给我们一个更加风格化的表单,与我们当前的样式相匹配。在审查样式时,您可能需要记住的一些事情是使用 CSS3 的gradient属性作为backgroundborder-radius的使用。

/*!
  Forms Styling
*/

label {
    color: #FFF;
    font-family: 'Helvetica', 'Arial', sans-serif;
    font-size: 12px;
    display: block;
    margin: 10px 0 5px 0;
}

input, select, input[type="date-time"], textarea {

    background: rgb(69,72,77);
    background: -moz-linear-gradient(top, rgba(69,72,77,1) 0%, rgba(0,0,0,1) 100%);
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(69,72,77,1)), color-stop(100%,rgba(0,0,0,1)));
    background: -webkit-linear-gradient(top, rgba(69,72,77,1) 0%,rgba(0,0,0,1) 100%);
    background: -o-linear-gradient(top, rgba(69,72,77,1) 0%,rgba(0,0,0,1) 100%);
    background: -ms-linear-gradient(top, rgba(69,72,77,1) 0%,rgba(0,0,0,1) 100%);
    background: linear-gradient(to bottom, rgba(69,72,77,1) 0%,rgba(0,0,0,1) 100%);

    font-size: 13px;
    color: #e5e5e5;

    border: 1px solid #000918;

    -moz-border-radius: 3px;
    -webkit-border-radius: 3px;
    -ms-border-radius: 3px;
    -o-border-radius: 3px;
    border-radius: 3px;

    display: block;
    margin: 0;
    padding: 5px 8px;

    -moz-box-shadow: 1px 1px 1px #333;
    -webkit-box-shadow: 1px 1px 1px #333;
    -ms-box-shadow: 1px 1px 1px #333;
    -o-box-shadow: 1px 1px 1px #333;
    box-shadow: 1px 1px 1px #333;
}

input[type="text"], 
input[type="number"], 
input[type="email"], 
input[type="datetime"],
input[type="password"],
textarea {
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #42422F), color-stop(0.09, #444));
}

input[type="submit"] {
    margin: 10px 0;
}

.form-container {
   display: none;
   margin: 15px 0;
}

.form-container.active {
  display: block;
}

form h2 {
    margin: 10px 0 5px 0;
}

.error-messaging ul {
  list-style: square outside;
  margin: 5px 0 0 0;
  padding: 0 0 0 12px;
}

.error-messaging li {
    color: #A12E33;
    font-family: 'Helvetica', 'Arial', sans-serif;
    font-size: 12px;
}

当我们应用前面的样式时,我们得到以下 UI:

自定义样式

范围输入类型

正如您所看到的,我们给我们的表单赋予了全新的外观和感觉,并且很容易地对选择组件进行了样式设置,这在桌面浏览器上并不容易做到。在这些样式之上,我建议您查看-webkit-appearance属性,它基本上允许您进一步自定义您的表单,并在组件的样式方面提供更多的控制。然而,此时您应该已经有了一个坚实的基础,可以为 iOS 构建 HTML5 表单。

摘要

在本章中,我们回顾了最新的 HTML5 输入类型和属性,特别是针对我们的示例应用程序。然后,我们讨论了 iOS 上表单的布局及其限制。最后,我们开发了一些表单,并附加了一个非常基本的验证脚本,利用了这些最新的输入和属性。作为一个额外的奖励,我们还讨论了如何为 WebKit 浏览器(包括 iOS 上的移动 Safari)定制我们的表单样式。

现在,我们应该对 iPhone 和 iPad 上的表单有了坚实的掌握,以及如何利用最新的 HTML5 技术为我们带来好处。本章帮助演示了表单的使用以及我们需要考虑的因素,以便创建用户友好的表单。除此之外,我们现在将进入下一章的位置感知,并将使用在这里学到的一些概念来扩展体验。

第六章:位置感知应用程序

地理位置是当今应用程序中广泛请求的功能,为用户提供准确的位置特定信息。在本章中,我们将回顾 HTML5 规范中的地理位置 API。有了这些知识,我们将继续构建一个包装器,使我们能够轻松地利用这一功能。一旦我们彻底了解了如何获取用户的位置,我们将利用一个简单的应用程序来使用我们新发现的知识,该应用程序使用谷歌地图 API。在本章结束时,您应该对地理位置规范有透彻的了解,有一个其实现的简单示例,并且作为奖励,您应该获得了使用谷歌地图 API 的一些经验。因此,让我们开始探索规范。

在本章中,我们将涵盖:

  • 地理位置规范

  • 检索用户当前位置

  • 监视用户的位置

  • 处理地理位置错误

  • 谷歌地图 API

  • 将谷歌地图与地理位置联系起来

  • 自定义谷歌地图

地理位置规范

基于位置的服务已经存在了相当长的时间,并且随着时间的推移而发展。实质上,这些服务努力提供功能,允许在各种类型的程序中使用时间和位置。然而,直到现在,前端还没有一个有用的工具集。因此,W3C万维网联盟)试图标准化从客户端设备检索地理位置的 API,无论是您的台式电脑、手机还是平板电脑。

实施

地理位置 API 定义了与托管实现的设备相关联的位置信息的高级接口,例如纬度和经度。API 本身对底层位置信息源是不可知的。

(如dev.w3.org/geo/api/spec-source.html#introduction所述。)

浏览器实现地理位置 API 的常见方式涉及全球定位系统GPS)、IP 地址、WIFI 和蓝牙 MAC 地址以及基本用户输入。由于这些技术工作的方式各不相同,以及浏览器供应商选择实施规范的程度不同,无法保证此 API 将返回用户或设备的位置。因此,作为开发人员,您需要确保用户也意识到这一限制,并向所有相关方解释合理的期望。

范围、安全性和隐私

在实现地理位置到我们的应用程序时,我们唯一需要担心的是脚本。无需提供任何标记,也无需查询或点击某些外部资源或 API。地理位置的实现严格限于脚本方面,并直接与正在使用的设备相关联。还有一点需要知道的是,位置是以世界大地测量系统坐标或纬度和经度的形式提供的。

在暴露用户位置时,还必须考虑安全性和隐私问题。从用于检索和存储此信息的安全方法到如何在其他方之间分发它,每个实施它的设备都必须提供一种保护用户隐私的机制。因此,W3C 规范要求考虑以下问题:

  • 需要用户的许可才能发送位置信息。

  • 只有在必要时才能请求位置信息。

  • 用户必须批准重新传输位置信息。

  • 持有此信息的一方必须向用户披露他们正在收集位置数据,包括其目的、安全性、可访问性、共享(如果数据将与其他方共享)以及此类数据将被存储的时间长度。

提示

请记住,为移动 Safari 编写的应用程序无法直接访问设备。它们只能查询浏览器代表它们访问设备。因此,您的应用程序正在请求浏览器获取特定信息,浏览器会为您完成工作,但您永远不会与设备本身进行一对一的通信。

总的来说,该规范考虑了与其他方分享个人信息(如地理位置)时出现的问题。然而,这些考虑并未考虑到当用户无意中授予权限或用户决定改变主意时可能出现的复杂性。因此,该规范提出了以下建议:

缓解和深入的防御措施是实施责任,而不是由本规范规定。然而,在设计这些措施时,建议实施者启用用户对位置共享的意识,并提供易于访问的接口,以启用撤销权限。

(如在 www.w3.org/TR/geolocation-API/#implementation_considerations 中提到的。)

考虑到这些问题和考虑,我们现在简要地描述 API。在接下来的部分中,我们将看看 API 是如何构建的,特别是看看在本章构建的应用程序中将使用的部分。

API 描述

在本章的这一部分,您可能会想知道为什么我们还没有看代码,尽管这是一个合理的担忧,但我的目标是帮助您彻底理解 Geolocation API,并指导您了解实际的 W3C 规范。因此,在本章中,我们将查看定义 Geolocation 规范的四个接口或公开行为,包括 GeolocationPositionOptionsPositionCoordinatesPositionError 接口。如果您对此提供的任何信息感到困惑,不用担心。请将本节视为可以帮助您增加对该主题的了解的参考资料。

Geolocation 接口

Geolocation 对象用于确定设备的位置。当我们实例化 Geolocation 对象时,会使用用户代理算法来确定位置,然后创建并填充一个 position 对象。如果我们查看 W3C 规范,Geolocation 被定义为:

interface Geolocation { 
    void getCurrentPosition(PositionCallback successCallback,
            optional PositionErrorCallback errorCallback,
            optional PositionOptions options);

    long watchPosition(PositionCallback successCallback,
            optional PositionErrorCallback errorCallback,
            optional PositionOptions options);

    void clearWatch(long watchId);
};

(如在 www.w3.org/TR/geolocation-API/#geolocation 中所见。)

先前的代码不是 JavaScript,而是 API 或 接口定义语言 (IDL) 的描述。如果它令人困惑,不用担心,当我第一次看规范页面时,我也有同样的感觉。然而,您在这里看到的是 Geolocation 对象的描述。当您阅读先前的代码时,您应该收集以下信息:

有三种方法:

  • getCurrentPosition,接受三个参数,其中两个是可选的

  • watchPosition,接受三个参数,其中两个是可选的

  • clearWatch,接受一个参数

现在您应该知道与 Geolocation 对象关联的有三个方法,每个方法都有一个特定的目的,如函数名称所述。因此,让我们来看看这三种方法,从 getCurrentPosition 开始,您可能已经猜到它获取设备的当前位置或尝试获取。

getCurrentPosition 方法

如前所述,此方法接受三个参数,其中两个是可选的。第一个参数应该是一个成功请求的 callback 方法。第二个和第三个参数是完全可选的。如果定义了第二个参数,那么它是另一个当发生错误时的 callback 方法。最后一个参数是由 PositionsOptions 接口定义的 options 对象。

watchPosition 方法

watchPosition方法也接受三个参数,与getCurrentPosition方法的参数相同。唯一的区别是,这个方法将持续触发successCallback,或者第一个参数,直到调用clearWatch方法。请记住,只有在位置发生变化时,successCallback才会触发,因此不依赖于任何时间选项。此方法还返回一个长值,用于定义观察操作,这是用clearWatch方法清除的。

clearWatch 方法

正如我们已经讨论过的,clearWatch用于停止watchPosition设置的过程。要使用这个方法,我们必须使用watchPosition返回的长值,并将其作为参数发送给clearWatch

PositionOptions 接口

我们已经看到PositionOptions对象用于向getCurrentPositionwatchPosition方法传递可选参数。这个对象由 W3C 定义如下:

interface PositionOptions {
    attribute boolean enableHighAccuracy;
    attribute long timeout;
    attribute long maximumAge;
};

(见www.w3.org/TR/geolocation-API/#position-options。)

从中我们应该得出的结论是,我们可以创建一个包含enableHighAccuracytimeoutmaximumAge键/值对的对象。这个对象在我们的 JavaScript 代码中看起来像下面这样:

var positionOptions = {
    'enableHighAccuracy': false,
    'timeout': Infinity,
    'maximumAge': 0
};

但是这些值代表什么呢?幸运的是,这一切都在规范中定义了。不过,别担心,这里有每个选项的简单解释。

enableHighAccuracy 选项

这个选项基本上是向设备提示应用程序希望接收到最好的可能结果。默认设置为false,因为如果设置为true,可能会导致响应时间变慢和/或增加功耗。请记住,用户可能会拒绝此功能,设备可能无法提供更准确的结果。

超时选项

超时被定义为等待成功回调被调用的时间,以毫秒为单位。如果获取位置数据的时间超过这个值,那么将调用错误回调,并发送PositionError代码TIMEOUT。默认情况下,该值设置为Infinity

最大年龄选项

最大年龄选项是指使用缓存位置的年龄不大于此选项设置的时间。默认情况下,此属性设置为0,因此每次都会尝试获取新的位置对象。如果此选项设置为Infinity,则每次都返回缓存位置。

现在我们了解了这些选项,我们可以将这个对象作为第三个参数传递给getCurrentPositionwatchPosition方法。API 的一个简单实现看起来可能是这样的:

var positionOptions = {
    'enableHighAccuracy': false,
    'timeout': Infinity,
    'maximumAge': 0
};

function successCallback(position) {}

function errorCallback(positionError) {}

// Get the current position
navigator.geolocation.getCurrentPosition(successCallback, errorCallback, positionOptions);

// Watch for position changes
navigator.geolocation.watchPosition(successCallback, errorCallback, positionOptions);

现在我们知道如何自定义对地理位置 API 的调用,但是当成功调用时,数据是什么样子的呢?或者,错误返回是什么样子的?了解这些对于开发地理位置 API 的良好封装非常有用。所以让我们来看一下坐标和位置错误接口。

位置接口

位置接口只是设备实现地理位置 API 返回的信息的容器。它返回一个Coordinates对象和Timestamp。这在 W3C 规范中描述如下:

interface Position {
    readonly attribute Coordinates coords;
    readonly attribute DOMTimeStamp timestamp;
};

(见www.w3.org/TR/geolocation-API/#position。)

在我们到目前为止讨论的内容中,位置接口在getCurrentPosition方法的successCallback中发挥作用。如果你还记得,这个方法接受一个名为options的参数,它是之前定义的position对象。实际上,如果我们想要记录坐标和时间戳,我们可以这样做:

function successCallback(position) {
    console.log(position.coords);
    console.log(position.timestamp);
}

返回的时间戳表示为DOMTimeStampcoords对象包含地理坐标和其他信息,由Coordinates接口定义。

Coordinates 接口

正如我们之前讨论过的,getCurrentPositionwatchPositionsuccessCallback返回一个包含Coordinates对象的position对象。这个Coordinates对象包含多个属性,这些属性在下表中描述:

属性 描述
latitude 十进制度的地理坐标。
longitude 十进制度的地理坐标。
altitude 位置的高度,以米为单位。如果不存在则为 null。
accuracy 经度和纬度的精度,以米为单位。如果不存在则为 null。必须是非负实数。
altitudeAccuracy 海拔精度,以米为单位。如果不存在则为 null。必须是非负实数。
heading 行进方向,以度为单位(0° ≤ heading ≤ 360°),顺时针方向。如果不存在则为 null。如果静止则值必须为 NaN。
speed 当前速度的大小,以米/秒为单位。如果不存在则为 null。必须是非负实数。

(见www.w3.org/TR/geolocation-API/#coordinates。)

既然我们知道了通过Coordinates接口可用的属性,我们可以通过以下实现访问这些属性。

function successCallback(position) {
    console.log(position.coords);
    console.log(position.coords.lattitude);
    console.log(position.coords.longitude);
    console.log(position.timestamp);
}

正如您所见,我们可以通过position.coords对象访问属性。这样,我们可以非常容易地访问用户的当前位置并将其与其他 API 绑定,这正是我们很快将要使用 Google Maps API 做的事情。最后,让我们讨论PositionError接口,以便我们知道如何在应用程序中高效处理错误。

PositionError 接口

getCurrentPositionwatchPosition方法出现错误时,PositionError接口就会发挥作用。该接口描述了发送到我们的错误处理程序或回调的代码和消息。W3C 将PositionError接口解释如下:

interface PositionError {
    const unsigned short PERMISSION_DENIED = 1;
    const unsigned short POSITION_UNAVAILABLE = 2;
    const unsigned short TIMEOUT = 3;
    readonly attribute unsigned short code;
    readonly attribute DOMString message;
};

(见www.w3.org/TR/geolocation-API/#position-error。)

前面的代码描述了作为对象发送到错误处理程序的两个属性,这两个属性分别是codemessage

code属性可以是以下三个常量之一,

  • PERMISSION_DENIED(错误代码 1):用户选择不让浏览器访问位置信息。

  • POSITION_UNAVAILABLE(错误代码 2):浏览器无法确定设备的位置。

  • TIMEOUT(错误代码 3):获取位置信息的总时间已超过 PositionOptions 接口中指定的超时属性。

第二个参数message将是一个描述问题的 DOM 字符串或字符串。

在我们的实现中,我们可以这样做:

function errorCallback(positionError) {
    if (positionError.code === 3) {
        console.log("A Timeout has occurred");
        console.log("Additional Details: " + positionError.message);
    }
}

正如您所见,我们可以很容易地使用PositionError接口确定错误,并根据提供的代码自定义我们的错误消息。在这一点上,您应该已经有了一个坚实的基础,可以在其上构建。现在我们将简要讨论一些将地理位置 API 实现到我们的应用程序中的用例,然后开始构建本书的应用程序。您可以略过下一节,因为它只会给您提供有关地理位置如何实现或已经实现的想法。

用例

在我们开始构建应用程序之前,我想回顾一些可以将地理位置信息实现到我们的应用程序中的情况。这将是简短而有用的,但它将帮助您构思如何高效地实现此功能。这些大部分已经在 W3C 规范中,但我希望这将让您更深入地了解规范的用处,以及在探索新功能时为什么一定要查看它。

兴趣点

我们一直对我们周围的环境感兴趣,无论是食物、啤酒还是娱乐。所以如果我们能列出与用户正在访问的内容相关的可能的兴趣点,那不是很酷吗?我们可以使用地理位置 API 来实现这一点。通过找到用户的当前位置并利用第三方供应商的开放 API,我们可以轻松地找到用户所在地区的相关信息并呈现相关信息。

路由导航

我们以前已经看到这样的情况发生了很多次,手机上的原生应用程序也是如此。甚至可能您的手机预装了这个功能,许多人在之前支付了数百美元。现在,使用 HTML5 地理位置 API,我们可以使用currentPosition方法构建这个功能,并将其与 Google Maps 之类的东西绑定在一起,以便我们可以向用户呈现路线。如果我们愿意,甚至可能使用watchPosition方法制作一个实时应用程序,尽管在构建应用程序时可能会遇到 API 访问限制,所以请记住这一点。

最新信息

该应用程序中的另一个有用功能是向用户提供最新信息。如果我们从后端系统公开 API,这将很容易实现,但如果我们进一步根据用户的当前位置在我们自己的应用程序之外实现信息,会怎么样呢?例如,如果我住在波士顿,去西雅图旅行,我可能想知道西雅图发生了什么,而不是波士顿,所以我的应用程序可能应该处理这种情况。使用 HTML5 地理位置 API,我们可以很容易地实现这一点,而不会有太多复杂性。

我们现在对地理位置 API 有了扎实的理解,从理论理解到简单实现,我们已经了解了关于地理位置和如何使用它的一切。使用案例也已经定义,以帮助我们找到一种将其集成到我们的应用程序中的方法,很可能你会发现在应用程序中使用这项技术的新颖和创新的方式。就目前而言,让我们为指出用户当前位置的简单使用案例场景做好准备,使用 Google Maps API。所以让我们开始吧。

谷歌地图 API

在我们开始使用 Google Maps 实现地理位置之前,我们需要做一些相当简单的设置工作。您可能已经知道,Google Maps 提供了一个 API,您可以利用它将他们的地图实现到您的应用程序中,这样您就可以轻松地显示与用户输入相关的信息,甚至更好的是,他们的当前位置。然而,出于几个原因,我们需要使用谷歌的 API 密钥来授权我们的应用程序,并跟踪从您的应用程序发出的请求。在本节中,我们将介绍设置工作,并希望能够快速帮助您。

API(s)

首先,您需要知道与地图相关的几个 API,包括 JavaScript v3、Places、iOS SDK、Android API、Earth API 等。对于我们的目的,我们将使用 JavaScript API v3;请注意,我们将使用 API 的第 3 版。如果您想了解更多关于几个 API 的信息,您可以访问以下页面:

developers.google.com/maps/

获取 API 密钥

如果您一直在关注,您会注意到我们的应用程序需要一个 API 密钥。谷歌为此提供了以下理由:

使用 API 密钥可以让您监视应用程序的地图 API 使用情况,并确保 Google 在必要时可以联系您的应用程序。如果您的应用程序的地图 API 使用超过使用限制,您必须使用 API 密钥加载地图 API 以购买额外的配额。

(如developers.google.com/maps/documentation/javascript/tutorial#api_key所示。)

激活服务

现在让我们开始创建 API 密钥。首先,在以下 URL 登录到您的 Google 账户:

code.google.com/apis/console

一旦我们在之前的 URL 登录,我们选择服务选项卡。

激活服务

服务选项卡

服务选项卡中,我们看到了 Google 提供的所有服务。在这个列表中,我们需要激活 Google Maps API v3。它应该看起来像这样:

激活服务

未激活的 Google Maps API

当您单击关闭按钮时,服务将激活,并应该如下所示:

激活服务

激活 Google Maps API

Google Maps API v3 服务现在已在您的 Google 账户下激活。下一步是检索将在我们的 Geolocation API 实现中使用的密钥。

检索密钥

现在,服务已在我们的 Google 账户下激活,让我们获取密钥——最后一步。为此,请切换到左侧导航中的API 访问选项卡。

检索密钥

API 访问选项卡

当我们访问这个页面时,我们将看到一个简单 API 访问部分,其中包含我们生成的密钥。这是您要用来授权您的 Google Maps 实现的密钥。除了密钥,您还会注意到它将列出引用者、激活时间以及激活密钥的人(您)。在所有这些信息的右侧,您还会注意到一些选项。这些选项包括生成新密钥、编辑引用者,以及最终删除生成的密钥。

提示

请注意,您还可以设置 OAuth 2.0 客户端 ID,这将保护您的应用程序。如果您将处理敏感信息,这绝对是推荐的,因为您将处理用户位置。然而,OAuth 的设置和使用超出了本书的范围,但我建议您花些时间学习这种新的身份验证方法,并在您自己的应用程序中实现它,一旦您在 API 方面有了坚实的基础。

有了 API 密钥,我们现在已经准备好开始使用 Google Maps 实现 Geolocation。接下来的部分将利用我们学到的知识,并使用我们可用的简单方法在页面上放置 Google 地图。在这方面,我希望它能激发您对 Google Maps API 的兴趣,因为它经过时间的发展,是一个几乎可以在任何应用程序中使用的优秀框架。现在让我们开始开发一些很酷的东西。

Geolocation 和 Google Maps

如果您从本章的开头一直跟随下来,您应该对 Geolocation API 有了全面的了解,并且已经设置好了您的 Google 账户以便利用 Google Maps JavaScript API。如果您一直没有跟随,也没关系,因为本节主要是为了展示如何实现这两种技术。本节将准备我们应用程序中的位置页面,然后快速转移到使用 Google Maps 实现 Geolocation。

标记准备

在上一章中,我们做了一些设置工作来启动我们的应用程序;我们将在这里遵循相同的设置工作,以确保我们所有的页面都是一致的。因此,让我们打开与本书附带的源文件中的location相关的标记页面/location/index.html。当我们在文本编辑器中打开这个页面时,让我们对标记进行以下更新:

  • 更新导航以反映选择菜单。

  • 包括location.css文件,该文件将为此页面提供特定的页面样式。

  • 从页面底部删除未使用的脚本。

  • 包括App.Location.js

  • 在包含main.js之后初始化App.Location

一旦我们进行了这些更新,您的标记应该如下所示:

<!DOCTYPE html>
<html class="no-js">
<head>
    <meta charset="utf-8">
    <title></title>
    <meta name="description" content="">
    <meta name="HandheldFriendly" content="True">
    <meta name="MobileOptimized" content="320">
    <meta name="viewport" content="width=device-width">

    <!-- IOS THUMBS -->

    <!-- APPLE META TAGS -->

    <link rel="stylesheet" href="../css/normalize.css">
    <link rel="stylesheet" href="../css/main.css">
    <link rel="stylesheet" href="../css/location.css">
    <script src="img/modernizr-2.6.1.min.js"></script>
</head>
    <body>
        <!-- Add your site or application content here -->
        <div class="site-wrapper">
            <header>
                <hgroup>
                    <h1>iPhone Web Application Development</h1>
                    <h2>Location Aware Apps</h2>
                </hgroup>
                <nav>
                    <select>
                        <!-- OPTIONS HERE -->
                    </select>
                </nav>
            </header>
            <footer>
                <p>iPhone Web Application Development &copy; 2013</p>
            </footer>
        </div>

        <script src="img/zepto.min.js"></script>
        <script src="img/helper.js"></script>
        <!-- BEGIN: Our Framework -->
        <script src="img/App.js"></script>
        <script src="img/App.Nav.js"></script>
        <script src="img/App.Location.js"></script>
        <!-- END: Our Framework -->
        <script src="img/main.js"></script>
        <script> new App.Location({ 'element': document.body }); </script> 
    </body>
</html>

注意

请注意,在应该存在更多标记的地方添加了注释。与这些部分相关的标记在提供的书籍源代码中。请在那里查找更多关于这些部分应该存在什么的信息。

现在我们已经将标记调整到了先前页面的一致布局,我们准备开始为位置感知定制此应用程序。该过程的下一步是准备标记,以便我们将构建的附加功能。为此,我们需要做以下事情:

  • 包括 Google Maps API JavaScript。

  • 包括我们将要构建的Geolocation包装器。

  • 创建一个包含我们地图的div

当我们按照先前的指示进行操作时,我们的标记将如下所示:

<!DOCTYPE html>
<html class="no-js">
<head>
    <meta charset="utf-8">
    <title></title>
    <meta name="description" content="">
    <meta name="HandheldFriendly" content="True">
    <meta name="MobileOptimized" content="320">
    <meta name="viewport" content="width=device-width">

    <!-- IOS THUMBS -->

    <!-- APPLE META TAGS -->

    <link rel="stylesheet" href="../css/normalize.css">
    <link rel="stylesheet" href="../css/main.css">
    <link rel="stylesheet" href="../css/location.css">
    <script src="img/modernizr-2.6.1.min.js"></script>
</head>
    <body>
        <!-- Add your site or application content here -->
        <div class="site-wrapper">
            <header>
                <hgroup>
                    <h1>iPhone Web Application Development</h1>
                    <h2>Location Aware Apps</h2>
                </hgroup>
                <nav>
                    <select>
                        <!-- OPTIONS HERE -->
                    </select>
                </nav>
            </header>
            <div id="map_canvas"></div>
            <footer>
                <p>iPhone Web Application Development &copy; 2013</p>
            </footer>
        </div>

        <script src="img/js?key=YOUR_API_KEY&sensor=SET_TO_TRUE_OR_FALSE"></script>
        <script src="img/zepto.min.js"></script>
        <script src="img/helper.js"></script>
  <script src="img/Geolocation.js"></script>
        <!-- BEGIN: Our Framework -->
        <script src="img/App.js"></script>
        <script src="img/App.Nav.js"></script>
        <script src="img/App.Location.js"></script>
        <!-- END: Our Framework -->
        <script src="img/main.js"></script>
        <script> new App.Location({ 'element': document.body }); </script> 
    </body>
</html>

正如您所看到的,这并没有太大的区别。我们在这里所做的是包含一个包含 Google Maps JavaScript 的新脚本。然后我们包含另一个名为Geolocation.js的脚本,它将存在于/js/中,最后我们创建一个 ID 为map_canvasdiv,它存在于页眉和页脚之间。

提示

请注意,您需要将在上一节中创建的 API 密钥包含在 Google Maps JavaScript URL 字符串中,用您之前提供的密钥替换YOUR_API_KEY。还要记住,您必须将传感器参数设置为 true 或 false。传感器参数告诉 Google Maps 应用程序使用传感器(例如 GPS)来获取用户位置。

好的,现在我们的标记已经准备好了。我们在这里不需要做任何其他事情,所以现在我们将转向 JavaScript,首先创建我们的Geolocation包装器,然后将其实现到我们的App.Location类中。让我们看看如何在我们的应用程序中更轻松地利用地理位置。

地理位置包装器

在大多数情况下,我们不希望为每种用例反复重写相同的方法。因此,我们创建了包装器,抽象了某些技术的功能,以便我们可以在应用程序中轻松使用它们。这就是我们现在要做的事情,抽象地理位置 API,以便我们可以在 Google Maps API 中使用它。

让我们开始创建一个Geolocation.js文件,放在我们的JavaScript目录中。您可能已经注意到,这不会存在于App命名空间下;这是因为它是任何应用程序都可能使用的抽象类。对于我们的目的,我们只想要获取用户的当前位置,并且希望能够在整个应用程序中使用这些信息,因此我们将其设置为全局对象。

这是我们的Geolocation类的基本模板:

(function($){

    var _self, _defaults, _callbacks;

    // Default options
    _defaults = {};

    // Stores custom callbacks
    _callbacks = {};

    /**
        @constructor
    */
    function Geolocation(options) {
        this.options = $.extend({}, _defaults, options);

        _self = this;
    }

    Geolocation.prototype.toString = function() {
        return "[object " + this.constructor.name + "]";
    }

    // Exposess the Geolocation Function
    window.Geolocation = new Geolocation();

}(Zepto));

这与我们先前编写的任何代码都没有什么不同,只是我们用以下代码公开了这个类:

window.Geolocation = new Geolocation();

我们基本上只是初始化Geolocation对象并将其设置为window对象,而不是返回Geolocation对象,这使其成为全局对象。您还会注意到添加了一个名为_callbacks的闭包作用域变量,它将包含用户在扩展地理位置功能时可以覆盖的回调。现在让我们通过包括用于检索当前位置的默认值以及一个将保存地理位置 API 返回的所有数据的一般属性对象来进一步扩展这一点:

    // Default options
    _defaults = {
        'currentPositionOptions': {
            'enableHighAccuracy': false,
            'timeout': 9000,
            'maximumAge': Infinity
        },
        'props': {}
    };

当我们检索用户位置时,将使用这些选项。目前,让我们将这些保留为原样,并创建一个回调,用户可以在地理位置 API 发生成功或错误时覆盖:

    // Stores custom callbacks
    _callbacks = {
        'getCurrentPositionCallback': function(){}
    };

我们很快将看到如何实现这一点,但现在这将是一个默认方法,用于执行回调。接下来,让我们检查设备/浏览器是否实际支持地理位置 API:

    /**
        @constructor
    */
    function Geolocation(options) {
        this.options = $.extend({}, _defaults, options);

        if(navigator.geolocation) {
            this.geolocation = navigator.geolocation;
        }

        _self = this;
        _self.props = this.options.props;
    }

这是一个相当简单的地理位置支持检查,基本上我们只是在 Geolocation 上创建一个叫做geolocation的属性,如果存在 API 就会设置它。这样,我们就不必在类内部每次都写navigator.geolcation。而且,这样做将更容易在以后检查地理位置功能是否存在。在这一点上,我们准备从 Geolocation API 中公开getCurrentPosition方法。

Geolocation.prototype.getCurrentPosition = function(callback) {
    if (typeof callback !== 'undefined') {
        _callbacks.getCurrentPositionCallback = callback;
    }

    if (typeof this.geolocation !== 'undefined') {
    this.geolocation.getCurrentPosition(currentPositionSuccess, currentPositionError, _self.options.currentPositionOptions);

        return this;
    }

    return false;
};

之前的方法是公共的并且可访问,因为我们已经将它附加到了 Geolocation 的原型上。这个方法将接受一个参数,一个在 Geolocation API 的getCurrentPosition调用成功或失败时将被调用的回调函数。这个方法检查参数是否不是未定义的,然后根据发送的内容重新分配。然后我们检查在构造函数中设置的geolocation属性;如果它不是未定义的,我们就调用 Geolocation API 上的getCurrentPosition方法并发送适当的参数。然后我们返回我们的Geolocation类的实例。如果geolocation属性未定义,我们返回一个 false 的布尔值,因此开发人员在使用这个方法时也可以进行错误检查。

提示

请注意,我们正在传递两个未定义的方法currentPositionSuccesscurrentPositionError,这些方法将很快被定义。但是,也请注意,我们将之前定义的默认属性作为它的第三个参数发送到这个方法中。通过这样做,我们使开发人员能够轻松地进一步定制地理位置功能的体验。当我们开始开发App.Location.js文件时,你会看到定制这些值是多么容易。

在这一点上,唯一剩下的就是创建之前的回调。所以让我们创建以下successCallback

function currentPositionSuccess(position) {
    _self.props.coords = position.coords;
    _self.props.timestamp = position.timestamp;

    _callbacks.getCurrentPositionCallback.call(_self, _self.props);
}

最后一个回调被称为,你可能已经猜到了,当我们成功获取用户位置时调用。根据 W3C 规范的定义,这个方法接受一个参数——一个包含坐标和时间戳的Position对象。我们使用构造函数中定义的props属性来公开返回的信息。一旦所有这些信息都被检索和设置,回调getCurrentPositionCallback被调用并传递检索到的属性。

提示

请注意,我们还将回调中的this的含义更改为 Geolocation 实例的含义,通过将_self作为第一个参数传递来调用。

最后,让我们创建我们的错误回调:

    function currentPositionError(positionError) {
        _callbacks.getCurrentPositionCallback.call(_self, positionError);
    }

这个回调,根据 W3C 规范的定义,接受一个参数,一个带有错误代码和简短消息的PositionError对象。然而,我们所要做的就是使用回调并传递这些信息,类似于successCallback中所做的。不同的是,这里我们只是传递PositionError对象,以便在这个包装器之外创建自定义消息。

有了这个,我们就完成了对地理位置 API 的简单包装。现在我们可以轻松地从App.Location.js中调用 API。所以让我们继续扩展App.Location对象,并开始使用带有地理位置的 Google Maps API。

使用 Google Maps 的地理位置

所以我们现在准备开始使用App.Location来实现使用 Google Maps 的地理位置。我们将使用本书中一直使用的相同样板来将我们的Geolocation包装器与 Google Maps API 连接起来。让我们开始打开提供的App.Location.js,当你打开它时,它应该看起来类似于以下代码:

var App = window.App || {};

App.Location = (function(window, document, $){
    'use strict';

    var _defaults = {
        'name': 'Location'
    }, _self;

    function Location(options) {
        this.options = $.extend({}, _defaults, options);

        this.$element = $(this.options.element);
    }

    Location.prototype.getDefaults = function() {
        return _defaults;
    };

    Location.prototype.toString = function() {
        return '[ ' + (this.options.name || 'Location') + ' ]';
    };

    Location.prototype.init = function() {
        // Initialization Code

        return this;
    };

    return Location;

}(window, document, Zepto));

如果您按顺序阅读本书,这里没有什么新内容。但是作为回顾,我们在App对象下声明了一个名为Location的新命名空间。这个命名空间将包含我们位置页面的所有功能,因此它非常适合作为 Google 地图和地理位置功能之间的控制器。因此,让我们从缓存地图元素开始,创建一个闭包作用域的Location实例引用,然后对其进行初始化。构造函数应该如下所示:

function Location(options) {
    this.options = $.extend({}, _defaults, options);

    this.$element = $(this.options.element);

    // Cache the map element
    this.$cache = {
        'map': this.$element.find('#map_canvas')
    };

    _self = this;

    this.init();
}

在这里,我们在Location实例上创建了一个$cache属性,这个$cache属性将包含对map元素的引用,因此可以使用这个属性进行访问。然后我们创建了一个闭包作用域的 self 变量,引用了Location实例。最后,我们通过调用实例原型上的init方法来初始化我们的代码。

在这个过程中的下一步是使用我们的Geolocation包装器来获取用户的当前位置。我们将把这段代码添加到initialize方法中,如下所示:

Location.prototype.init = function() {
    // Initialization Code
    Geolocation.getCurrentPosition(function(args){
        if(args.toString() !== '[object PositionError]') {
            _self.initGoogleMaps();
        } else {
            console.log("An ERROR has occurred: " + args.message);
        }
    });

    return this;
};

在这里,我们最终可以看到我们的Geolocation包装器的实现,以及它在应用程序中集成的简易程度,因为Geolocation类已经处理了验证和验证设置。这其中的重要部分是我们的回调实际上处理了错误;通过检查PositionError的对象类型,我们能够继续集成 Google 地图或记录返回的错误。当然,我们处理错误的方式应该更加详细,但对于这种情况,它有助于确定在我们的应用程序中采用这种方法有多么简单。

现在,让我们看看如何通过查看之前调用的initGoogleMaps方法来实现 Google 地图的成功回调:

Location.prototype.initGoogleMaps = function() {
    this.latlng = new google.maps.LatLng(Geolocation.props.coords.latitude, Geolocation.props.coords.longitude);

    this.options.mapOptions.center = this.latlng;

    this.map = new google.maps.Map(this.$cache.map[0], this.options.mapOptions);

    this.marker = new google.maps.Marker({
        'position': this.latlng,
        'map': this.map,
        'title': 'My Location'
    });

    this.infowindow = new google.maps.InfoWindow({
        'map': this.map,
        'position': this.latlng,
        'content': 'My Location!',
        'maxWidth': '140'
    });

这里发生了很多事情,但信不信由你,我们几乎已经完成了。所以让我们一步一步地进行。

首先,我们将latlng属性设置为 Google Maps API 的LatLng类的一个新实例。这个class构造函数返回一个表示地理点的对象。尽管我们已经从 Geolocation API 中获得了坐标,但我们需要确保创建一个 Google 地图的LatLng实例,因为它将在接下来的方法中使用。

现在,在继续之前,我们需要暂时绕过一下。Google Maps API 非常广泛和可定制,允许我们在几乎每个区域自定义地图的外观和感觉。为了更深入地探索这一点,让我们在默认设置上创建一个mapOptions对象,它将为我们的地图定制移动端的外观:

var _defaults = {
    'name': 'Location',
    'mapOptions': {
        'center': '',
        'zoom': 8,
        'mapTypeId': google.maps.MapTypeId.ROADMAP,
        'mapTypeControl': true,
        'mapTypeControlOptions': {
            'style': google.maps.MapTypeControlStyle.DROPDOWN_MENU
        },
        'draggable': true,
        'scaleControl': false,
        'zoomControl': true,
        'zoomControlOptions': {
            'style': google.maps.ZoomControlStyle.SMALL,
            'position': google.maps.ControlPosition.TOP_LEFT
        },
        'streetViewControl': false
    }
}, _self;

现在,我们不会深入讨论这一点,但请记住,有许多选项可供您探索和优化,以适用于我们的 iPhone Web 应用程序。我鼓励您访问以下网址并探索这些选项,以便熟悉可用的内容:

developers.google.com/maps/documentation/javascript/reference#MapOptions

让我们回到之前描述的initGoogleMaps方法。在初始化LatLng类之后,我们现在在刚刚创建的mapOptions对象上定义了 center 属性。这个属性设置为LatLng的实例:

this.options.mapOptions.center = this.latlng;

现在我们已经定义了创建 Google 地图所需的所有属性,我们初始化了 Google Maps API 的Map类:

this.map = new google.maps.Map(this.$cache.map[0], this.options.mapOptions);

这个方法接受我们在 JavaScript 中创建并缓存的div元素作为它的第一个参数。第二个参数将是我们创建的options对象。我们在mapOptions对象上设置center属性的原因是因为地图的初始化将显示用户的位置。我们现在已经完成了地理定位和 Google Maps API 的实现。

总结

在本章中,我们回顾了由 W3C 定义的地理定位规范。然后,我们利用这些信息构建了一个包装器,以便我们可以轻松地利用 API。作为一个额外的奖励,我们还回顾了 Google Maps API,创建了一个访问密钥,然后使用我们的地理定位包装器来确定用户的位置并将其显示给用户。现在你应该对确定用户位置并有效使用它有了很好的理解。在下一章中,我们将进入单页应用程序开发,利用我们学到的概念并使用一些额外的开源库进行扩展。

第七章:单页面应用程序

到目前为止,我们已经开发了包含相关静态内容的单独页面。在本章中,我们将通过深入研究单页面应用程序开发来提高水平。我们在许多网络应用程序中都见过这种情况,包括 Pandora、Mint 和 NPR。我们将介绍单页面应用程序开发的基础知识,从 MVC、Underscore 和 Backbone 的介绍到使用我们示例应用程序创建架构和利用本章第一部分教授的方法。完成本章后,您应该对单页面应用程序背后的概念有扎实的理解,这将使您能够继续扩展这些知识,并帮助您在构建复杂应用程序的道路上指引您。所以让我们首先学习 MVC。

在本章中,我们将涵盖:

  • MVC 架构

  • 介绍Underscore.js

  • 介绍Backbone.js

  • 创建单页面应用程序

模型-视图-控制器或 MVC

模型-视图-控制器MVC)是编程中广泛使用的设计模式。设计模式本质上是解决编程中常见问题的可重用解决方案。例如,命名空间立即调用函数表达式是本书中经常使用的模式。MVC 是另一种模式,用于帮助解决分离表示和数据层的问题。它帮助我们将标记和样式保持在 JavaScript 之外;保持我们的代码有组织、清晰和可管理——这些都是创建单页面应用程序的基本要求。因此,让我们简要讨论 MVC 的几个部分,从模型开始。

模型

模型是一个对象的描述,包含与之相关的属性和方法。想想构成一首歌的内容,例如曲目的标题、艺术家、专辑、年份、时长等。在本质上,模型是您的数据的蓝图。

视图

视图是模型的物理表示。它基本上显示模型的适当属性给用户,页面上使用的标记和样式。因此,我们使用模板来填充我们的视图所提供的数据。

控制器

控制器是模型和视图之间的中介。控制器接受操作,并在必要时在模型和视图之间传递信息。例如,用户可以编辑模型上的属性;当这样做时,控制器告诉视图根据用户更新的信息进行更新。

关系

在 MVC 应用程序中建立的关系对于遵循设计模式至关重要。在 MVC 中,理论上,模型和视图永远不会直接交流。相反,控制器完成所有工作;它描述一个动作,当该动作被调用时,模型、视图或两者都相应地更新。这种类型的关系在下图中得以建立:

关系

这个图解释了传统的 MVC 结构,特别是控制器和模型之间的通信是双向的;控制器可以向模型发送/接收数据,视图也可以如此。然而,视图和模型永远不会直接交流,这是有充分理由的。我们希望确保我们的逻辑得到适当的包含;因此,如果我们想要为用户操作正确地委派事件,那么这段代码将放入视图中。

然而,如果我们想要有实用方法,比如一个getName方法,可以适当地组合用户的名字和姓氏,那么这段代码将包含在用户模型中。最后,任何涉及检索和显示数据的操作都将包含在控制器中。

从理论上讲,这种模式有助于我们保持代码组织良好、清晰和高效。在许多情况下,这种模式可以直接应用,特别是在像 Ruby、PHP 和 Java 这样的许多后端语言中。然而,当我们开始严格将其应用于前端时,我们将面临许多结构性挑战。同时,我们需要这种结构来创建稳固的单页应用程序。接下来的章节将介绍我们将用来解决这些问题以及更多问题的库。

Underscore.js 简介

我们在示例应用程序中将使用的库之一是Underscore.js。由于 Underscore 提供了许多实用方法,而不会扩展内置的 JavaScript 对象,如StringArrayObject,因此 Underscore 在过去几年变得非常流行。虽然它提供了许多有用的方法,但该套件还经过了优化并在许多最受欢迎的 Web 浏览器中进行了测试,包括 Internet Explorer。出于这些原因,社区广泛采用了这个库并不断支持它。

实现

在我们的应用程序中实现 Underscore 非常容易。为了让 Underscore 运行,我们只需要在页面上包含它,如下所示:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">
    </head>
    <body>
        <script src="img/jquery.min.js"></script>
        <script src="img/underscore-min.js"></script>
    </body>
</html>

一旦我们在页面上包含 Underscore,我们就可以使用全局范围内的_对象访问库。然后,我们可以通过_.methodName访问库提供的任何实用方法。您可以在线查看 Underscore 提供的所有方法(underscorejs.org/),其中所有方法都有文档并包含它们的实现示例。现在,让我们简要回顾一些我们将在应用程序中使用的方法。

_.extend

Underscore 中的extend方法与我们从Zepto中使用的extend方法非常相似(zeptojs.com/#$.extend)。如果我们查看 Underscore 网站上提供的文档(underscorejs.org/#extend),我们可以看到它接受多个对象,第一个参数是目标对象,一旦所有对象组合在一起就会返回。

将源对象的所有属性复制到目标对象中,并返回目标对象。它是按顺序的,因此最后一个源将覆盖先前参数中相同名称的属性。

例如,我们可以获取一个Song对象并创建一个实例,同时覆盖其默认属性。可以在以下示例中看到:

<script>
    function Song() {
        this.track = "Track Title";
        this.duration = 215;
        this.album = "Track Album";
    };

    var Sample = _.extend(new Song(), {
        'track': 'Sample Title',
        'duration': 0,
        'album': 'Sample Album'
    });
</script>

如果我们记录Sample对象,我们会注意到它是从Song构造函数继承而来,并覆盖了默认属性trackdurationalbum。虽然我们可以使用传统的 JavaScript 来提高继承的性能,但使用extend方法可以帮助我们专注于交付。我们将在本章后面看看如何利用这种方法在我们的示例应用程序中创建基本架构。

_.each

当我们想要迭代ArrayObject时,each方法非常有用。实际上,这是我们可以在Zepto和其他流行库如jQuery中找到的另一种方法。尽管每个库的实现和性能略有不同,但我们将使用 Underscore 的_.each方法,以便我们可以在不引入新依赖项的情况下保持应用程序的架构。根据 Underscore 的文档(underscorejs.org/#each),使用_.each与其他实现类似:

对元素列表进行迭代,依次将每个元素传递给迭代器函数。如果传递了上下文对象,则迭代器绑定到上下文对象。迭代器的每次调用都使用三个参数:(element,index,list)。如果列表是 JavaScript 对象,则迭代器的参数将是(value,key,list)。如果存在本地 forEach 函数,则委托给本地 forEach 函数。

让我们看一个在前一节中创建的代码中使用_.each的示例。我们将循环遍历Sample的实例,并记录对象的属性,包括曲目、持续时间和专辑。由于 Underscore 的实现允许我们像数组一样轻松地循环遍历Object,因此我们可以使用这种方法来迭代我们的Sample对象的属性:

<script>
    function Song() {
        this.track = "Track Title";
        this.duration = 215;
        this.album = "Track Album";
    };

    var Sample = _.extend(new Song(), {
        'track': 'Sample Title',
        'duration': 0,
        'album': 'Sample Album'
    });

    _.each(Sample, function(value, key, list){
        console.log(key + ": " + value);
    });

</script>

我们的日志输出应该是这样的:

track: Sample Title
duration: 0
album: Sample Album

正如你所看到的,使用 Underscore 的each方法与数组和对象非常容易。在我们的示例应用程序中,我们将使用这种方法来循环遍历对象数组以填充我们的页面,但现在让我们回顾一下我们将在 Underscore 库中使用的最后一个重要方法。

_.template

Underscore 已经让我们非常容易地将模板集成到我们的应用程序中。默认情况下,Underscore 带有一个简单的模板引擎,可以根据我们的目的进行定制。实际上,它还可以预编译您的模板以便进行简单的调试。由于 Underscore 的模板化可以插入变量,我们可以利用它来根据需要动态更改页面。Underscore 提供的文档(underscorejs.org/#template)有助于解释在使用模板时我们有哪些不同的选项:

将 JavaScript 模板编译为可以用于渲染的函数。用于从 JSON 数据源呈现复杂的 HTML 片段。模板函数既可以插入变量,使用<%= ... %>,也可以执行任意的 JavaScript 代码,使用<% ... %>。如果您希望插入一个值,并且它是 HTML 转义的,请使用<%- ... %>。当您评估一个模板函数时,传递一个数据对象,该对象具有与模板的自由变量对应的属性。如果您正在编写一个一次性的模板,可以将数据对象作为模板的第二个参数传递,以便立即呈现,而不是返回一个模板函数。

前端的模板化一开始可能很难理解,毕竟我们习惯于查询后端,使用 AJAX,并检索标记,然后在页面上呈现。如今,最佳实践要求我们使用发送和检索数据的 RESTful API。因此,理论上,您应该使用正确形成的数据并进行插值。但是,如果不是在后端,我们的模板在哪里?很容易,在我们的标记中:

<script type="tmpl/sample" id="sample-song">
    <section>
        <header>
            <h1><%= track %></h1>
            <strong><%= album %></strong>
        </header>
    </section>
</script>

因为前面的脚本在浏览器中有一个已识别的类型,所以浏览器避免读取此脚本中的内容。而且因为我们仍然可以使用 ID 来定位它,所以我们可以获取内容,然后使用 Underscore 的template方法插入数据:

<script>
    function Song() {
        this.track = "Track Title";
        this.duration = 215;
        this.album = "Track Album";
    };

    var Sample = _.extend(new Song(), {
        'track': 'Sample Title',
        'duration': 0,
        'album': 'Sample Album'
    });

    var template = _.template(Zepto('#sample-song').html(), Sample);

    Zepto(document.body).prepend(template);

</script>

运行页面的结果将是以下标记:

<body>
    <section>
        <header>
            <h1>Sample Title</h1>
            <strong>Sample Album</strong>
        </header>
    </section>
    <!-- scripts and template go here -->
</body>

正如您所看到的,模板中的内容将被预先放置在主体中,并且数据将被插入,显示我们希望显示的属性;在这种情况下,歌曲的标题和专辑名称。如果这有点难以理解,不要太担心,当行业开始转向运行原始数据(JSON)的单页面应用程序时,我自己也很难理解这个概念。

目前,这些是我们将在本章中一直使用的方法。鼓励您尝试使用Underscore.js库,以发现一些更高级的功能,使您的生活更轻松,例如_.map_.reduce_.indexOf_.debounce_.clone。但是,让我们继续学习Backbone.js以及如何使用这个库来创建我们的应用程序。

介绍 Backbone.js

为了给我们的单页面应用程序添加结构,我们将使用Backbone.js,这是一个轻量级的框架,帮助我们应用 MVC 设计模式。Backbone.js是许多 MVC 类型框架之一,它帮助前端开发遵循将数据与视图或特别是 DOM 分离的最佳实践。除此之外,我们的应用程序可能会变得非常复杂。Backbone.js有助于缓解这些问题,并让我们快速上手。因此,让我们开始讨论 MVC 如何应用于这个框架。

MVC 和 Backbone.js

有许多种类型的 JavaScript 框架以不同的方式应用 MVC,Backbone 也不例外。Backbone 实现了ModelsViewsCollectionsRouters;它还包括一个EventHistorySync系统。正如你所看到的,Backbone 没有传统的 Controller,但我们可以将Views解释为控制器。根据 Backbone 的文档(backbonejs.org/#FAQ-mvc):

(…)在 Backbone 中,View 类也可以被视为一种控制器,分派源自 UI 的事件,HTML 模板作为真正的视图。

这种 MVC 实现可能有点令人困惑,但我们的示例应用程序将有助于澄清问题。现在让我们深入了解 Backbone 模型、视图和集合。在接下来的部分中,我们将介绍 Backbone 的每个部分是如何实现的,以及我们将用来构建应用程序的部分。

Backbone 模型

在任何 MVC 模式中,模型都是至关重要的,包含数据和逻辑,包括属性、访问控制、转换、验证等。请记住,我们每天都在编写模型,事实上,我们在本书中创建了许多模型(MediaElementVideoAudio等)。Backbone 模型类似于样板,它提供了我们否则必须自己构建的实用方法。

让我们以以下代码为例:

function Song() {
    this.track = "Track Title";
    this.duration = 215;
    this.album = "Track Album";
};

Song.prototype.get = function(prop) {
    return this[prop] || undefined;
}

Song.prototype.set = function(prop, value) {
    this[prop] = value;

    return this;
}

var song = new Song();

song.get('album');
// "Track Album"

song.set('album', 'Sample Album');
// Song

song.get('album');
// "Sample Album"

在上面的示例中,我们创建了一个Song模型,与前一节中一样,它有几个属性(trackdurationalbum)和方法(getset)。然后我们创建了Song的一个实例,并使用创建的方法来获取和设置album属性。这很棒;然而,我们需要手动创建这些方法。这不是我们想要做的;我们已经知道我们需要这些方法,所以我们只想专注于数据和扩展它。这就是 Backbone 模型发挥作用的地方。

让我们分析以下模型:

var SongModel = Backbone.Model.extend({
    'defaults': {
        'track': 'Track Title',
        'duration': 215,
        'album': 'Track Album'
    }
});

var song = new SongModel();

song.get('album');
// "Track Album"

song.set('album', 'Sample Album');
// SongModel

song.get('album');
// "Sample Album"

上面的代码展示了我们快速开始编写应用程序的方式。在幕后,Backbone 是一个命名空间,并且有一个附加到它的模型对象。然后,使用 Underscore 的extend方法,我们返回一个Backbone.Model的副本,其中附加了默认属性,赋值给变量SongModel。然后我们做同样的事情,使用getset,期望的输出在注释中。

如你所见,使用 Backbone 很容易入门,尤其是如果你只是想要一种方法来组织你的数据,而不是为每个应用程序构建自定义功能。现在让我们看看 Backbone 中的视图,以及它如何帮助我们将数据与 UI 分离。

Backbone 视图

Backbone 视图与模型有些不同,它们更多的是为了方便。如果我们查看 Backbone 的文档并比较ViewsModels部分,我们会发现 Views 更加简洁,但在组织我们的应用程序时也很有用。为了看到它们为什么仍然有用,让我们看下面的代码:

var $section = $('section');

$section.on('click', 'a', doSomething);

function doSomething() {
    // we do something here
}

通常,这是我们在页面上缓存元素并为特定用户交互委托事件的方式。但是,如果可以减少设置工作呢?在下面的代码中,我们将上面的代码转换为典型的 Backbone 视图设置。

var SongView = Backbone.View.extend({
    'el': document.querySelector('section'),

    'events': {
        'click a': 'doSomething'
    },

    'doSomething': function(e){
        console.log($(e.currentTarget).attr('href'));
    }
});

var view = new SongView();

正如您所看到的,Backbone 为您处理了设置工作。它在幕后为您缓存了所选元素并代理了事件。实际上,您在您的端上需要做的只是设置,然后快速进行下一步;现在您会注意到您的开发时间减少了,而您的效率增加了,这只是进入 Backbone 的初步步骤。当我们将模型和视图连接在一起时,魔术就会发生。要看到这一点,请看以下代码:

var SongModel = Backbone.Model.extend({
    'defaults': {
        'track': 'Track Title',
        'duration': 215,
        'album': 'Track Album'
    }
});

var song = new SongModel();

var SongView = Backbone.View.extend({
    'el': document.querySelector('section'),

    'events': {
        'click a': 'doSomething'
    },

    'initialize': function() {
        this.model.on('change:track', this.updateSongTitle, this);

        this.$el.$songTrack = this.$el.find('.song-track');
        this.$el.$songTrack.text(this.model.get('track'));
    },

    'doSomething': function(e){
        console.log($(e.currentTarget).attr('href'));
    },

    'updateSongTitle': function() {
        this.$el.$songTrack.text(this.model.get('track'));
    }
});

var view = new SongView({
    'model': song
});

song.set('track', 'Sample Track');
// The DOM Updates with the right value

在这段代码片段中,我们最终将单个模型连接到一个视图。我们这样做的方式是将模型的实例传递给视图的实例:

var view = new SongView({
    'model': song
});

当我们这样做时,我们将模型和视图关联起来。但我们还需要对该模型进行一些操作,通常我们希望显示与其关联的数据。因此,在这个例子中,我们创建了一个initialize方法,它被调用作为构造函数。在这个方法中,我们使用 Backbone 内置的事件系统来跟踪与模型的track属性相关的任何更改,并相应地调用updateSongTitle。在此过程中,我们通过将this作为第三个参数传递来更改事件处理程序的上下文,然后缓存显示歌曲轨道的元素。

最后,当您更改歌曲的track属性的实例时,DOM 会相应地更新。现在我们已经有了构建应用程序所需的基础。但让我们来看看 Backbone 集合,了解如何跟踪数据如何增加应用程序的效率。

Backbone 集合

到目前为止,我们已经使用了单个模型,这很好,但在大多数情况下,我们使用数据集。这就是 Backbone 集合存在的原因,用于管理有序的模型集。Backbone 集合还与 Underscore 的方法相关联,使我们可以轻松高效地处理这些集合,而无需进行任何设置工作。

让我们看看以下代码:

var SongModel = Backbone.Model.extend({
    'defaults': {
        'track': 'Track Title',
        'duration': 215,
        'album': 'Track Album'
    }
});

var SongCollection = Backbone.Collection.extend({
    'model': SongModel
});

var SongView = Backbone.View.extend({
    'el': document.querySelector('section'),

    'events': {
        'click a': 'doSomething'
    },

    'initialize': function() {
        this.collection.on('change', this.updateDetected, this);
    },

    'doSomething': function(e){
        console.log($(e.currentTarget).attr('href'));
    },

    'updateDetected': function() {
        console.log("Update Detected");
    }
});

var collection = new SongCollection();

for (var i = 0; i < 100; i++) {
    collection.add(new SongModel());
}

var view = new SongView({
    'collection': collection
});

这个示例代码与上一节中生成的代码非常相似。不同之处在于我们创建了一个SongCollection,它接受SongModel类型的模型。然后我们创建了这个集合的一个实例,通过我们的for循环向其中添加了 100 个模型,最后将集合附加到我们的视图上。

我们的视图也发生了变化,我们将change事件附加到我们的集合上,并创建了一个更通用的监听器,每当集合中的模型更新时都会被调用。因此,当我们执行以下代码时,视图会告诉我们有东西被更新了:

collection.models[0].set('album', 'sample album');
// "Update Detected"

服务器端交互

看到 Backbone 应用程序如何连接到服务器并不容易,特别是因为前端代码中有很多事情要做。但是,如果您查看 Backbone.js 网站提供的文档(backbonejs.org/#Sync),我们知道模型包含了操纵数据的所有功能。事实上,模型连接到数据库并可以与之同步。

Backbone.sync 是 Backbone 每次尝试从服务器读取或保存模型时调用的函数。默认情况下,它使用(jQuery/Zepto)。ajax 来进行 RESTful JSON 请求并返回 jqXHR。您可以覆盖它以使用不同的持久化策略,例如 WebSockets、XML 传输或本地存储。

但是,模型并不是唯一可以连接到服务器的对象。随着文档的继续阅读,模型或集合可以开始同步请求并相应地与之交互。这与传统的 MVC 实现有些不同,特别是因为集合和模型可以与数据库交互。为了更好地显示 Backbone 对 MVC 的实现,提供的图像有助于显示不同类型对象之间的关系:

服务器端交互

这基本上就是我们之前创建的东西;一个视图、模型和控制器。实现略有不同,但我们可以看到演示层和数据之间有明显的分离,因为视图从不直接与数据库交互。如果这有点令人困惑,那是因为它确实如此,这是另一种复杂性的层次,一旦理解,将有助于引导您编写优雅的代码。

您现在已经准备好使用UnderscoreBackboneZepto创建一个单页应用程序。但是,有一个问题。这些库可以加快我们的开发速度并提高效率,但实际上并没有为我们的应用程序提供一个坚实的结构。这就是我们在示例应用程序中要解决的问题。接下来,我们将讨论单页应用程序所需的架构、实现和优化。

我们的示例应用程序

我们现在已经介绍了Underscore.jsBackbone.js,并且对这些库提供的内容以及它们如何帮助应用程序开发有了很好的理解。然而,我们仍然需要一种结构化应用程序的方式,以便它们可以轻松扩展,最重要的是,可以管理。因此,在本章的这一部分,我们将开始构建一个示例应用程序,将所有内容联系在一起,并帮助您快速构建单页应用程序。

应用程序架构

我们的示例应用程序将做两件事。一是允许我们查看用户信息,例如个人资料和仪表板。二是具有可以使用 HTML5 音频媒体元素收听的歌曲播放列表。我们可以将这些要求视为几乎是两个应用程序:一个用于管理用户数据的用户应用程序,另一个用于管理媒体播放的应用程序。但它们将相关联,以便用户将有与他们相关的歌曲播放列表。

基本示例架构

让我们开始实现前面的架构。首先,我们知道将有两个应用程序,类似于我们的App对象,因此让我们从定义这些开始:

  • js/Music/

  • js/User/

  • 在 JavaScript(js)文件夹中,我们应该创建前面提到的两个文件夹:MusicUser。这两个文件夹将分别包含用户和音乐应用程序的代码。为了帮助管理我们的 backbone 文件,我们将为每个创建modelsviewscollections文件夹。

  • js/Music/

  • views/

  • models/

  • collections/

  • js/User/

  • views/

  • models/

  • collections/

太棒了!现在我们可以开始创建一个主 JavaScript 文件,其中将包含每个应用程序的命名空间;每个命名空间分别为UserMusic

  • js/Music/

  • views/

  • models/

  • collections/

  • Music.js

  • js/User/

  • views/

  • models/

  • collections/

  • User.js

现在,我们的大多数视图都将具有非常熟悉的功能。例如,将有一个全局导航栏,其中包含三个链接,每个链接将启动每个部分的隐藏/显示,隐藏当前部分并显示下一个部分。我们不一定希望一遍又一遍地编写相同的代码,因此最好有一个基本视图,我们的应用程序可以从中继承。为此,我们将在我们的App文件夹中创建一个名为views的文件夹:

  • js/App/

  • views/

  • BaseView.js

好的,这基本上是我们这个示例应用程序的 JavaScript 框架。当然,还有其他设置方式,也许它们甚至更好—这很好。对于我们的目的,这符合要求,并有助于展示我们应用程序中的一些结构。现在,让我们开始查看我们的标记。

应用标记

让我们打开与本章相关的index.html文件;它应该位于/singlepage/index.html。现在,如果我们还没有这样做,让我们从更新站点的全局导航开始,这是我们之前为其他章节所做的。如果您需要参考资料,请查看本书提供的上一章的完成源代码,并根据需要更新标记。

更新后,我们的标记应该看起来像这样:

<!DOCTYPE html>
<html class="no-js">
<head>
    <!-- Meta Tags and More Go Here -->

  <link rel="stylesheet" href="../css/normalize.css">
  <link rel="stylesheet" href="../css/main.css">
    <link rel="stylesheet" href="../css/singlepage.css">
  <script src="img/modernizr-2.6.1.min.js"></script>
</head>
  <body>
    <!-- Add your site or application content here -->
        <div class="site-wrapper">
            <header>
                <hgroup>
                    <h1>iPhone Web Application Development</h1>
                    <h2>Single Page Applications</h2>
                </hgroup>
                <nav>
                    <select>
                        <!-- Options Go Here -->
                    </select>
                </nav>
            </header>
            <footer>
                <p>iPhone Web Application Development &copy; 2013</p>
            </footer>
        </div>

        <!-- BEGIN: LIBRARIES / UTILITIES-->
    <script src="img/zepto.min.js"></script>
        <script src="img/underscore-1.4.3.js"></script>
        <script src="img/backbone-0.9.10.js"></script>
    <script src="img/helper.js"></script>
        <!-- END: LIBRARIES / UTILITIES-->
        <!-- BEGIN: FRAMEWORK -->
        <script src="img/App.js"></script>
        <script src="img/App.Nav.js"></script>
        <!-- END: FRAMEWORK -->
  </body>
</html>

现在,让我们开始修改这段代码以适应我们的应用程序。首先,让我们在标题后面添加一个div,类名为content

 <div class="site-wrapper">
    <header>
        <hgroup>
            <h1>iPhone Web Application Development</h1>
            <h2>Single Page Applications</h2>
        </hgroup>
        <nav>
            <select>
                <!-- Options Go Here -->
            </select>
        </nav>
    </header>
    <div class="content"></div>
    <footer>
        <p>iPhone Web Application Development &copy; 2013</p>
    </footer>
</div>

当我们完成这些工作后,让我们修改脚本,包括我们之前创建的整个应用程序。这意味着我们包括了MusicUser应用程序脚本,以及BaseView。我们的标记脚本部分应该看起来像这样:

<!-- BEGIN: LIBRARIES / UTILITIES-->
<script src="img/zepto.min.js"></script>
<script src="img/underscore-1.4.3.js"></script>
<script src="img/backbone-0.9.10.js"></script>
<script src="img/helper.js"></script>
<!-- END: LIBRARIES / UTILITIES-->
<!-- BEGIN: FRAMEWORK -->
<script src="img/App.js"></script>
<script src="img/App.Nav.js"></script>
<script src="img/BaseView.js"></script>
<!-- END: FRAMEWORK -->
<!-- BEGIN: MUSIC PLAYLIST APPLICATION -->
<script src="img/Music.js"></script>
<script src="img/SongModel.js"></script>
<script src="img/SongCollection.js"></script>
<script src="img/SongView.js"></script>
<script src="img/PlayListView.js"></script>
<script src="img/AudioPlayerView.js"></script>
<!-- END: MUSIC PLAYLIST APPLICATION -->
<!-- BEGIN: USER APPLICATION -->
<script src="img/User.js"></script>
<script src="img/UserModel.js"></script>
<script src="img/DashboardView.js"></script>
<script src="img/ProfileView.js"></script>
<!-- END: USER APPLICATION -->
<script src="img/main.js"></script>
<script> Backbone.history.start(); </script>
<!-- END: BACKBONE APPLICATION -->

注意

请注意,我们已经启动了 Backbone 历史 API。虽然我们还没有全面讨论这一点,但这对于保持应用程序状态至关重要。Backbone 中历史 API 的实现细节超出了本书的范围,但对于那些希望利用 Backbone 进行离线存储的人来说,这是非常鼓励的。现在,请记住这对于路由是至关重要的。

创建模板

现在我们的标记接近完成,但我们还剩下应用程序的其余部分将由什么组成;这就是模板化将发挥作用的地方。下一步是包括我们应用程序所需的模板,包括音频播放器视图、播放列表、歌曲、仪表板、个人资料和共享导航视图。那么如何在静态 HTML 页面上指定模板呢?像这样:

<script type="tmpl/Music" id="tmpl-audioplayer-view">
    <section class="view-audioplayer">
        <header>
            <h1>Audio Player</h1>
        </header>
        <div class="audio-container">
            <audio preload controls>
                <source src="img/<%= file %>" type='audio/mpeg; codecs="mp3"'/>
                <p>Audio is not supported in your browser.</p>
            </audio>
        </div>
    </section>
</script>

您可能想知道为什么这不会在浏览器中引起任何验证错误或代码执行错误。好吧,为了帮助澄清事情,我们的script标签的type属性是一个不受支持的 MIME 类型,因此浏览器会忽略script块中的所有内容(www.whatwg.org/specs/web-apps/current-work/multipage/scripting-1.html#script-processing-prepare)。因为块内的代码不会被执行,所以我们可以包含我们的 HTML 模板以供以后使用。请记住,我们已经附加了一个 ID,我们可以使用 Zepto 来定位这个元素。还要注意音频元素的来源,特别是<%= file %>。这将由 Underscore 的template方法用于插入模板本身传递的数据。我们很快就会讨论到这一点,但现在知道这就是我们可以设置模板的方式。

好的,现在我们知道如何创建模板,让我们在包含我们应用程序脚本之前实现以下模板。我们可以包括音频播放器的前一个模板,然后我们可以包括以下模板:

<!-- Playlist View -->
<script type="tmpl/Music" id="tmpl-playlist-view">
    <section class="view-playlist">
        <header>
            <h1><%= name + "'s" %> Playlist</h1>
            <% print(_.template($('#tmpl-user-nav').html(), {})); %>
        </header>
        <ul></ul>
    </div>
</script>

在播放列表视图模板中,我们有一些非常有趣的东西。看一下h1标签后面的代码。我们在这里看到 Underscore 库的template方法;它接受一个参数,这个参数将是模板#tmpl-user-nav的 HTML 字符串,我们还没有定义,第二个参数是一个空对象。这个例子展示了在模板中使用模板的用法,有点像潜行,但希望不会太可怕。请记住,我们提到我们的应用程序中将包含全局导航;前面的方法帮助我们编写一次代码,保持我们的代码清洁、可管理和高效。

现在,我们的播放列表仍然不包含歌曲列表。这是因为它将是动态的,基于歌曲数据集;这就是为什么在播放列表视图中有一个空的无序列表。但我们的歌曲会是什么样子呢?传统上,我们只需在 JavaScript 中创建一个列表(li)元素,但是使用模板,我们不再需要这样做——我们可以将标记保留在逻辑之外:

<!-- Individual Song View -->
<script type="tmpl/Music" id="tmpl-song-view">
    <li class="view-song">
        <strong><%= track %></strong>
        <em><%= artist %></em>
    </li>
</script>

现在看看将标记保留在脚本之外是多么容易?在这个模板中,我们遵循相同的基本原则:定义一个包含标记的脚本块,并创建将插值到其中的标记,以包含我们想要的数据。在这种情况下,我们希望将曲目和艺术家输出到它们自己的元素中。现在让我们创建用户的仪表板:

<script type="tmpl/User" id="tmpl-user-dashboard">
    <section class="view-dashboard">
        <header>
            <h1><%= name + "'s" %> Dashboard</h1>
            <% print(_.template($('#tmpl-user-nav').html(), {})); %>
        </header>
    </section>
</script>

再次,和以前一样。实际上,我们正在重复使用在播放列表视图中显示全局导航的相同方法。到目前为止,你已经注意到每个模板都有一个特定的 ID,并且根据约定,我们已经根据其应用程序定义了每个script块的类型,例如tmpl/User用于用户应用程序,tmpl/Music用于音乐应用程序。现在让我们来看一下结合了前面两种方法的个人资料视图。

<script type="tmpl/User" id="tmpl-user-profile">
    <section class="view-profile">
        <header>
            <h1><%= name + "'s" %> Profile</h1>
            <% print(_.template($('#tmpl-user-nav').html(), {})); %>
        </header>
        <dl>
            <dt>Bio</dt>
            <dd><%= bio %></dd>
            <dt>Age</dt>
            <dd><%= age %></dd>
            <dt>Birthdate</dt>
            <dd><%= birthdate.getMonth() + 1 %>/<%= birthdate.getDate() %>/<%= birthdate.getFullYear() %></dd>
        </dl>
    </section>
</script>

在这个视图中,全局导航被打印出来,并且数据被插值。正如你所看到的,模板中可以做任何事情。但它也可以是我们应用程序的全局导航这样简单的东西:

<script type="tmpl/User" id="tmpl-user-nav">
    <a href="#dashboard">Dashboard</a>
    <a href="#profile">Profile</a>
    <a href="#playlist">Playlist</a>
</script>

在这个最后的例子中,没有发生复杂的事情,实际上就是我们一直期待的全局导航,结果是——它只是标记。现在,你可能会想为什么不在 DOM 中创建所有这些,隐藏它,然后使用ZeptojQuery中的内置选择器引擎填充所需的信息。老实说,这是一个很好的问题。但是有一个主要原因,性能。使用这些引擎是昂贵的,甚至是内置方法querySelectorquerySelectorAll。我们不想触及 DOM,因为这是一个繁重的操作,特别是对于处理大数据集的大规模应用程序。最终,仅仅为了数据填充或存储而进行 DOM 操作是混乱的。不要这样做,将 DOM 用于数据而不是最佳实践。

我们的模板已经完成,这就结束了我们应用程序的标记。现在我们转向有趣的部分,我们的脚本。接下来的部分将会相当复杂和相当具有挑战性,但我保证当我们完成时,你将成为一个单页应用程序的专家,并且准备快速创建你自己的应用程序。第一次总是艰难的,但坚持下去,你将会收获回报。

应用程序脚本

在本节中,我们将介绍使我们的应用程序工作所需的脚本。我们将从审查BaseView开始,这个视图包含了继承视图(PlayListViewProfileViewDashboardView)中的共享功能。然后我们将创建我们的音乐和用户应用程序,每个应用程序都有它们相对应的模型、视图和集合。

BaseView

让我们开始查看我们的脚本,从我们在App命名空间下创建的BaseView文件开始(js/App/views/BaseView.js)。在这个文件中,我们将创建BaseView类,它将扩展 Backbone 的通用View类。BaseView将如下所示:

(function(window, document, $, Backbone, _){

  var BaseView = Backbone.View.extend({

  });

  // Expose the User Object
  window.App.BaseView = BaseView;

}(window, document, Zepto, Backbone, _));

这个类遵循了我们在之前章节中编写的其他 JavaScript 的完全相同的模式,这里唯一的区别是包括了BackboneUndescore,以及我们如何使用window.App.BaseView = BaseView来公开BaseView类。

现在,请跟着我。我们将创建几种方法,这些方法将包含在扩展BaseView类的任何对象中。这些方法将包括showhideonProfileClickonPlaylistClickonDashboardClickonEditClick。正如你可能已经猜到的,其中一些方法将是事件处理程序,用于导航到我们应用程序的某些部分。查看以下代码以了解实现:

(function(window, document, $, Backbone, _){

  var BaseView = Backbone.View.extend({
    'hide': function() {
      this.$template.hide();
    },

    'show': function() {
      this.$template.show();
    },

    'onProfileClick': function(e) {
      e.preventDefault();

      User.navigate('profile/' + this.model.get('username'), { 'trigger': true });
    },

    'onPlaylistClick': function(e) {
      e.preventDefault();

      Music.navigate('playlist', { 'trigger': true });
    },

    'onDashboardClick': function(e) {
      e.preventDefault();

      User.navigate('dashboard', { 'trigger': true });
    },

    'onEditClick': function() {
      console.log('onEditClick');
    }
  });

  // Expose the User Object
  window.App.BaseView = BaseView;

}(window, document, Zepto, Backbone, _));

现在,你可能注意到这里写的对象尚未创建,比如$templateUserMusic对象。我们将在几个步骤后返回到这一点,但请记住,this.$template将指的是扩展BaseView的实例,而UserMusic对象将是使用内置的 backbone 方法navigate来改变我们应用程序在 URL 中的位置并存储用户交互历史的路由器。为了更好地理解这个类BaseView是如何被使用的,让我们开始创建Music.jsMusic对象的代码(js/Music/Music.js)。

音乐应用程序

现在让我们开始创建我们应用程序的第一部分,音乐应用程序。音乐和用户应用程序都是分开的,以增加更高级别的可维护性和重用性。从音乐应用程序开始,我们将创建适当的路由器、集合、模型和视图。

路由器

我们的音乐应用程序始于Music.js文件中定义的Music类,该文件位于js/Music/目录下。在这个文件中,我们将扩展 Backbone 的Router类,包含我们音乐应用程序的路由、用于模型和集合的示例数据对象,以及当请求播放列表时的事件处理程序。首先,让我们从定义类开始:

(function(window, document, $, Backbone, _){

  var Music = Backbone.Router.extend({
    // Application Routes
    'routes': {
      'playlist': 'setupPlaylist',
      'playlist/:track': 'setupPlaylist'
    }
  });

  // Expose the Music Object
  window.Music = new Music();

}(window, document, Zepto, Backbone, _));

按照我们在BaseView类中建立的模式,我们在Backbone中扩展Router类,并定义一些默认路由。这两个路由包括一个常规播放列表路由和一个包含播放列表和曲目编号的替代路由。当调用这两个路由时,都将调用我们接下来将定义的setupPlaylist方法:

'setupPlaylist': function(track){
  if (!this.songCollection) {
    // Create song collection on the instance of Music
    this.songCollection = new this.SongCollection(this.songs);
  }

  if (!this.playListView) {
    // Create song list view on the instance of Music
    this.playListView = new this.PlayListView({
      'el': document.querySelector('.content'),
      'collection': this.songCollection,
      'model': new User.UserModel()
    });
  } else {
    this.playListView.show();
    this.playListView.audioPlayerView.show();
  }

  if (track) {
    this.playListView.updateTrack(track);
  }
}

如果这段代码让你有点畏首畏尾,那没关系,它实际上非常简单。首先,我们检查是否已经使用Music的实例初始化了一个songCollection对象。如果没有,我们将使用一组歌曲的示例数据对象来创建一个。接下来,我们做同样的事情,检查playListView对象是否已经创建;如果没有,我们继续创建它。否则,我们只是显示播放列表和与之相关的音频播放器。最后,我们检查是否传递了曲目编号(与我们创建的第二个路由相关);如果有曲目编号,我们将更新playListView以反映所选的曲目。

让我们专注于playListView的初始化:

this.playListView = new this.PlayListView({
  'el': document.querySelector('.content'),
  'collection': this.songCollection,
  'model': new User.UserModel()
});

尽管我们尚未正式创建PlayListView类,但我们可以回顾它是如何初始化的。在这种情况下,我们在Music的实例上附加了一个playListView属性,即this.playListView。这个属性将是PlayListView的一个实例(new PlayListView({}))。这个PlayListView的新实例将接受一个普通对象,其中包含三个属性:一个定义为el的元素,一个集合,以及一个UserModel的实例,这个实例尚未定义。

这里我们需要做的最后一件事是包括一个initialize方法,该方法将创建一个示例数据对象(this.songs),并监听播放列表路由的调用。当我们调用播放列表路由或导航到它时,我们希望同时隐藏个人资料和仪表板;我们将在routes监听器中手动执行这一操作:

'initialize': function() {
  this.songs = [{
      'duration': 251,
      'artist': 'Sample Artist',
      'added': new Date(),
      'track': 'Sample Track Title',
      'album': 'Sample Track Album'
    }, {
      'duration': 110,
      'artist': 'Sample Artist',
      'added': new Date(),
      'track': 'Sample Track Title',
      'album': 'Sample Track Album'
    }, {
      'duration': 228,
      'artist': 'Sample Artist',
      'added': new Date(),
      'track': 'Sample Track Title',
      'album': 'Sample Track Album'
    }
  ];

  this.on('route:setupPlaylist', function() {
    // This should be more dynamic, but fits our needs now
    // ---
    if (User.profileView) {
      User.profileView.hide();
    }

    if (User.dashboardView) {
      User.dashboardView.hide();
    }
    // ---
  });
},

好的,我们在这里创建了initialize方法,当创建Music的实例时会调用这个方法。这很好,因为在这个方法中,我们可以处理任何设置工作,比如创建示例数据对象。示例数据对象是一个对象数组,然后将被SongCollection类转换为模型:

'setupPlaylist': function(track){
  if (!this.songCollection) {
    // Create song collection on the instance of Music
    this.songCollection = new this.SongCollection(this.songs);
  }
  // Some code defined after
}

看起来很熟悉吧?现在我们正在收尾。我们还没有创建SongCollection类,但是 Backbone 的文档中指出,如果将数组传递给集合,它会自动转换为集合中指定的模型(将在未来的步骤中描述)。

这个initialize方法做的最后一件事是,在播放列表的路由上定义一个监听器(this.on('route:setupPlaylist', function() {});)。事件处理程序然后隐藏了已经创建的个人资料和仪表板。另外,请注意,我们使用route:setupPlaylist指定了路由,但我们也可以使用route来监听任何路由。

我知道这是很多东西要消化的,但我们现在将从这个Music类开始连接这些点,从集合开始,然后转向模型,最后是视图。这个类是其他所有需要构建的东西的基础,以便拥有一个完全功能的音乐应用程序,并提供我们开发的蓝图。

集合

我们音乐应用程序的集合很简单。遵循我们之前所做的基本模板,我们将创建一个包含SongCollection类的闭包。然后我们将定义SongCollection应该保持的模型类型。最后,我们将把这个类暴露给我们的Music对象。

当我们完成了实现这些要求后,我们的类看起来是这样的:

(function(window, document, $, Backbone, _){

  var SongCollection = Backbone.Collection.extend({
    'model': window.Music.SongModel
  });

  window.Music.SongCollection = SongCollection;

}(window, document, Zepto, Backbone, _));

看起来多简单啊?现在我们知道这个集合只跟踪SongModel类型的模型,并且如果传递一个数组,它将把包含的对象转换为SongModel类型。这就是这个类现在要做的全部。当然,您可以扩展它并尝试使用几种方法,比如比较器,这个类可以利用;但现在,这就是我们需要的全部。

模型

我们的SongModel将描述我们试图跟踪的数据类型。这个模型还将包含一个单一的方法,该方法将以秒为单位的持续时间作为属性,并将其以分钟返回。当然,我们有选择在模型初始化时准备我们的模型,但现在我们将保持简单。

SongModel,当写出来时,将是这样的:

(function(window, document, $, Backbone, _){

  var SongModel = Backbone.Model.extend({
    'defaults': {
      // in seconds
      'duration': 0,
      'artist': '',
      'added': 0,
      'track': '',
      'album': ''
    },

    'initialize': function() {

    },

    'getDurationInMinutes': function() {
      var duration = this.get('duration');

      if (duration === 0) {
        return false;
      }

      return this.get('duration') / 60;
    }
  });

  window.Music.SongModel = SongModel;

}(window, document, Zepto, Backbone, _));

从前面的代码中,我们可以推断出SongModel将具有属性durationartistaddedtrackalbum。每个属性的默认值都是空的String0。我们还可以注意到,每个模型都将有一个名为getDurationInMinutes的方法,可以被调用,并返回该模型的持续时间(以分钟为单位)。同样,SongModel类遵循相同的基本架构和最佳实践,返回给Music对象。最后,我们准备好查看这个音乐应用程序的视图。

视图(们)

在这一部分,我们将审查三个单独的视图,包括播放列表、歌曲和音频播放器视图。每个视图呈现音乐应用程序的一个单独部分,除了播放列表,它还呈现音频播放器和每个单独的歌曲。所以,让我们从播放列表视图开始。

播放列表视图

我们希望播放列表视图做一些事情,但我们将一步一步来。首先,让我们创建PlayListView类,它将扩展我们已经创建的BaseView类。

(function(window, document, $, Backbone, _){

  var PlayListView = App.BaseView.extend({
    // Code goes here
  });

  // Expose the PlayListView Class
  window.Music.PlayListView = PlayListView;

}(window, document, Zepto, Backbone, _));

接下来,我们希望PlayListView类引用正确的模板。

(function(window, document, $, Backbone, _){

  var PlayListView = App.BaseView.extend({
    'template': _.template($('#tmpl-playlist-view').html())
  });

  // Expose the PlayListView Class
  window.Music.PlayListView = PlayListView;

}(window, document, Zepto, Backbone, _));

通过将模板作为属性包含进来,我们可以很容易地使用this.template来引用它。请记住,在这个阶段我们还没有处理模板,我们只是简单地使用了 Underscore 的template方法来检索标记。接下来,我们想要为用户点击歌曲时定义一个事件监听器。

(function(window, document, $, Backbone, _){

  var PlayListView = App.BaseView.extend({
    'template': _.template($('#tmpl-playlist-view').html()),

    'events': {
      'click .view-song': 'onSongClicked'
    }
  });

  // Expose the PlayListView Class
  window.Music.PlayListView = PlayListView;

}(window, document, Zepto, Backbone, _));

在这一步中,我们告诉视图将我们创建的所有事件委托给视图的元素。在这个事件对象中,我们监听一个带有类名.view-song的元素上的点击事件。当点击这个元素时,我们想要调用onSongClicked事件处理程序。让我们接下来定义这个事件处理程序。

(function(window, document, $, Backbone, _){

  var PlayListView = App.BaseView.extend({
    'template': _.template($('#tmpl-playlist-view').html()),

    'events': {
      'click .view-song': 'onSongClicked'
    },

    'onSongClicked': function(e) {
      var $target = $(e.currentTarget);

      this.$el.find('.active').removeClass('active');

      $target.addClass('active');

      Music.navigate('playlist/' + ($target.index() + 1), { 'trigger': true });
    }
  });

  // Expose the PlayListView Class
  window.Music.PlayListView = PlayListView;

}(window, document, Zepto, Backbone, _));

在前面的代码中定义的事件处理程序切换活动类,然后告诉Music路由器导航到播放列表路由,告诉它触发路由事件并传递曲目的索引。通过这样做,我们的路由被调用,传递了一个曲目,播放列表更新了。然而,我们仍然没有定义updateTrack方法。让我们在我们的类中包含以下方法:

(function(window, document, $, Backbone, _){

  var PlayListView = App.BaseView.extend({
    'template': _.template($('#tmpl-playlist-view').html()),

    'events': {
      'click .view-song': 'onSongClicked'
    },

    'onSongClicked': function(e) {
      var $target = $(e.currentTarget);

      this.$el.find('.active').removeClass('active');

      $target.addClass('active');

      Music.navigate('playlist/' + ($target.index() + 1), { 'trigger': true });
    },

    'updateTrack': function(track) {
      this.audioPlayerView.render(track);

      this.setActiveSong(track || 1);
    }
  });

  // Expose the PlayListView Class
  window.Music.PlayListView = PlayListView;

}(window, document, Zepto, Backbone, _));

现在我们有了updateTrack方法,这本质上是告诉音频播放器的视图渲染它收到的曲目。不幸的是,我们的代码还没有准备好运行,因为我们还没有创建这个方法。另外,下面的方法setActiveSong也没有定义,所以我们现在需要这样做:

(function(window, document, $, Backbone, _){

  var PlayListView = App.BaseView.extend({
    'template': _.template($('#tmpl-playlist-view').html()),

    'events': {
      'click .view-song': 'onSongClicked'
    },

    'onSongClicked': function(e) {
      var $target = $(e.currentTarget);

      this.$el.find('.active').removeClass('active');

      $target.addClass('active');

      Music.navigate('playlist/' + ($target.index() + 1), { 'trigger': true });
    },

    'setActiveSong': function(track) {
      this.$el.find('.active').removeClass('active');

      this.$el.find('.view-song').eq(track - 1).addClass('active');

      return this;
    },

    'updateTrack': function(track) {
      this.audioPlayerView.render(track);

      this.setActiveSong(track || 1);
    }
  });

  // Expose the PlayListView Class
  window.Music.PlayListView = PlayListView;

}(window, document, Zepto, Backbone, _));

我们现在创建了setActiveSong方法,基本上是根据 URL 的曲目编号切换活动类。我们可能可以推断并在这里为歌曲创建一个通用的切换,但目前这满足了标准。但我们还没有完成,我们仍然需要初始化这个类并适当地渲染它。让我们看看这个类现在需要什么:

(function(window, document, $, Backbone, _){

  var PlayListView = App.BaseView.extend({
    // code before

    'initialize': function() {
      this.render();
    },

    'render': function() {
      var i = 0,
        view,
        that = this;

      // Create the template
      this.$template = $(this.template(this.model.attributes));

      // Append the template
      this.$el.append(this.$template);

      // Create the audio player
      if(!this.audioPlayerView) {
        this.audioPlayerView = new Music.AudioPlayerView({
                      'el': this.el.querySelector('.view-playlist'),
                      'model': new User.UserModel()
                    });
      }

      this.collection.each(function(element, index, list){
        var view  = new Music.SongView({
          'el': that.$template.find('ul'),
          'model': element
        });
      });

      return this;
    },

    // code after 
  });

  // Expose the PlayListView Class
  window.Music.PlayListView = PlayListView;

}(window, document, Zepto, Backbone, _));

前面的代码完成了这个类,但在我们继续之前,让我们看看这里发生了什么。首先,我们定义了一个initialize方法。这个方法将在创建这个类的实例后被调用,因此render方法也将被调用。通常,在 Backbone 中,render方法确切地做了函数被调用的事情——渲染视图。

定义的render方法做了一些事情;首先,它使用传入的模型编译我们的模板。之前我们看到了以下代码:

// Create song list view on the instance of Music
this.playListView = new this.PlayListView({
  'el': document.querySelector('.content'),
  'collection': this.songCollection,
  'model': new User.UserModel()
});

正如我们所看到的,创建了一个新的UserModel并将其传递给PlayListView,并且这个实例用于填充播放列表的模板。一旦编译完成,我们使用 Zepto 的append方法附加编译后的模板。你可能会问,它附加到什么上面?好吧,这个类的上面初始化正在寻找一个类为content的元素,我们在页面的标题元素之后定义了它。因此,PlayListView将附加到这个类为contentdiv上。

当模板附加完成后,我们检查音频播放器视图是否已经创建。如果没有,那么我们就创建它:

if(!this.audioPlayerView) {
  this.audioPlayerView = new Music.AudioPlayerView({
    'el': this.el.querySelector('.view-playlist'),
    'model': new User.UserModel()
  });
}

最后,一旦检查音频播放器视图,我们就可以开始有趣的事情了。在最后一部分中,我们循环遍历发送过来的集合,这是SongCollection的一个实例,与Music.js中创建的相同数据。当我们遍历集合中的每个模型时,我们每次都创建一个SongView的实例,将编译模板的无序列表元素传递给它,并传递当前模型。

现在,如果这没有让你大吃一惊,我不知道还有什么能让你大吃一惊。无论如何,我希望你仍然能接受这个挑战,因为我们还有两个视图需要看一看:AudioPlayerViewSongView。不过不要失去希望,我们已经度过了最大的挑战,准备好迎接新的挑战。

音频播放器视图

接下来我们要构建我们的AudioPlayerView。这个视图需要使用我们之前创建的模板,用曲目编号填充它,并在直接访问 URL 时加载它,例如/#playlist/2。我们还需要覆盖扩展的BaseView上的一个方法,需要被覆盖的方法是onDashboardClick。这是因为它要求我们隐藏播放列表,然后导航到仪表板。所以在最基本的层面上,这个类将如下所示:

(function(window, document, $, Backbone, _){

  var AudioPlayerView = App.BaseView.extend({
    'template': _.template($('#tmpl-audioplayer-view').html()),

    'events': {
      'click a[href="#dashboard"]': 'onDashboardClick'
    },

    'initialize': function(){
      this.render();
    },

    'render': function(file){
      // Put our rendering code here
    },

    'onDashboardClick': function() {
      this.hide();
      Music.playListView.hide();

      User.navigate('/dashboard', { 'trigger': true });
    }
  });

  window.Music.AudioPlayerView = AudioPlayerView;

}(window, document, Zepto, Backbone, _));

正如我们所看到的,前面段落中列出的所有要求都已经在AudioPlayerView的基类中得到满足。然而,我们需要渲染出这个视图,并用 URL 提供的数据填充它。为了做到这一点,我们需要编写我们的render方法如下:

'render': function(file){
  var audioElement;

  if (file) {
    audioElement = this.$el.find('audio')[0];

    // Must be made on the audio element itself
    audioElement.src = '../assets/' + 'sample' + (file || 1) + '.mp3';
    audioElement.load();
    audioElement.play();

    return this;
  }

  this.$template = $(this.template({ 'file': 'sample' + (file || 1) + '.mp3', 'name': this.model.get('name') }));
  this.$template.find('audio')[0].volume = 0.5;

  this.$el.find('header').after(this.$template);

  return this;
},

与我们为播放列表视图编写的先前的render方法类似,render方法检查是否传入了文件或数字。如果有,我们将使用传入的内容填充我们的模板中的音频元素。接下来,我们编译我们的模板,然后将音量设置为0.5,并将播放器附加到PlayListView的标题后面。如果我们回顾一下我们如何初始化这个类,我们会注意到音频播放器视图委托给了播放列表视图元素(在PlayListView内部):

this.audioPlayerView = new Music.AudioPlayerView({
  'el': this.el.querySelector('.view-playlist'),
  'model': new User.UserModel()
});
歌曲视图

我们音乐应用程序的最后一部分是SongView。让我们快速回顾一下这个视图的要求并看看它的实现。对于这个视图,我们再次想设置我们的模板。当我们初始化这个视图时,我们希望在传入的模型上附加一个事件处理程序,因此如果模型被更新,视图将自动渲染更新。这个视图的render方法应该基本上使用模型的属性编译模板,然后将自己附加到为这个视图设置的元素上。

当我们完成了前面的要求实现后,视图应该看起来有点像这样:

(function(window, document, $, Backbone, _){

  var SongView = App.BaseView.extend({
    'template': _.template($('#tmpl-song-view').html()),

    'initialize': function() {
      // Listen to when a change happens on the model assigned this view
      this.listenTo(this.model, 'change', this.render);

      this.render();
    },

    'render': function() {
      this.$el.append(this.template(this.model.attributes));

      return this;
    }
  });

  // Expose the SongView
  window.Music.SongView = SongView;

}(window, document, Zepto, Backbone, _));

正如我们所看到的,我们遵循了先前视图实现中设定的标准。唯一的区别是在模型的更改事件上添加了事件侦听器。让我们回顾一下PlayListView中这个视图是如何初始化的:

this.collection.each(function(element, index, list){
  var view  = new Music.SongView({
    'el': that.$template.find('ul'),
    'model': element
  });
});

现在我们完全理解了音乐应用程序是如何工作的。在这一点上,我们的页面可以仅通过这种实现来运行;但是,我不建议这样做,因为我们还没有创建用户应用程序,错误将会出现。但是我们现在知道,我们的路由定义了应用程序中的操作,视图是实现模型和集合的表示层。模型是我们应用程序的核心,以可管理的方式包含我们需要的所有数据。最后,集合帮助我们管理模型的更大数据集,因为我们可以将这些传递到视图中,视图本身可以管理这些数据的呈现,这对于大型应用程序来说是理想的。

这个过程的下一步是开发用户应用程序,但希望事情会变得更容易一些。就像我们在上一部分中所做的那样,我们将从路由开始,然后逐步进行到集合、模型和视图。

用户应用程序

用户应用程序将遵循我们创建的音乐应用程序相同的流程。同样,我们将涵盖路由、模型和视图的实现。当我们完成这一部分时,我们将拥有各自独立运行的子应用程序,从而增加了我们单页应用程序的可维护性和效率。

路由

我们的用户应用程序的路由将与音乐应用程序非常相似。我们将定义仪表板和个人资料的路由。我们还将抽出时间创建单页应用程序的主页路由。该路由将包含设置仪表板和个人资料的适当方法。它还将包含主页方法,该方法将调用仪表板路由。在路由的initialize方法中,我们将监听这些路由并隐藏其他视图。

(function(window, document, $, Backbone, _){

  var User = Backbone.Router.extend({
    // Application Routes
    'routes': {
      '': 'home',
      'dashboard': 'setupDashboard',
      'profile/:user': 'setupProfile'
    },

    'initialize': function() {

    },

    'home': function() {

    },

    'setupDashboard': function() {

    },

    'setupProfile': function(name) {

    }
  });

  // Expose the User Object
  window.User = new User();

}(window, document, Zepto, Backbone, _));

在前面的代码中,我们遵循我们的标准,为用户应用程序创建基本模板。接下来,让我们看看initialize方法将包含什么:

'initialize': function() {
  var that = this;

  this.on('route:setupDashboard route:setupProfile', function(){
    if(Music.playListView) {
      Music.playListView.hide();
    }
  });

  this.on('route:setupDashboard', function(){
    if (that.profileView) {
      that.profileView.hide();
    }
  });

  this.on('route:setupProfile', function(){
    if (that.dashboardView) {
      that.dashboardView.hide();
    }
  });
},

我们路由的initialize方法满足了我们列出的要求,通过为我们创建的路由创建事件侦听器。每个侦听器都隐藏了我们不想看到的部分,但是我们如何看到我们想要的应用程序的实际部分呢?这就是setup方法发挥作用的地方。

'setupDashboard': function() {
  if (!this.dashboardView) {
    this.dashboardView = new this.DashboardView({
                'model': this.model = new this.UserModel(),
                'el': document.querySelector('.content')
              });
    this.setupDashboard();
    return;
  }

  this.dashboardView.show();
},
'setupProfile': function(name) {
  if (!this.profileView) {
    this.profileView = new this.ProfileView({
                'model': this.model = new this.UserModel(),
                'el': document.querySelector('.content')
              });
    return;
  }

  this.profileView.show();
}

这些方法基本上是相同的。它们检查视图是否已经在路由实例上创建(例如this.dashboardViewthis.profileView),如果已经创建,我们只显示该视图。然而,如果视图尚未创建,我们初始化适当的视图,然后再次调用该setup方法(递归),以便我们可以显示它,因为现在视图已经存在。

提示

你可能已经注意到,我们正在创建一个新的UserModel,并将其传递给我们的许多视图。目前这样做是可以的,因为我们想要测试应用程序的核心部分。但从理论上讲,一个UserModel将在整个应用程序中被初始化和维护。完成本章后,你可以尝试解决这个问题!

我们需要做的最后一件事是为我们的应用程序包含主页方法:

'home': function() {
  this.navigate('dashboard', { 'trigger': true });
},

当你访问/singlepage/index.html时,将调用这个路由。根据Backbone.js库的文档,空路由指的是应用程序的主页状态。虽然我们可以将setupDashboard方法定义为回调,但这是为了说明我们可以在需要时立即从一个路由转到另一个路由。也许我们可以在这里做一些预处理,甚至创建之前提到的单个UserModel

集合

因为我们在这个应用程序中只处理一个用户,所以不需要创建集合。哦!你以为这会变得更加困难吗?好吧,别抱太大希望;我们仍然需要考虑模型和视图。

模型

与 Backbone 中的任何模型一样,我们只是描述了将在整个应用程序中处理的数据。对于我们的UserModel来说也是如此,它将包含实例的默认属性,并在初始化时通过组合first_namelast_name属性来设置人的姓名。

为了满足这些要求,我们的UserModel将被定义如下:

(function(window, document, $, Backbone, _){

  var UserModel = Backbone.Model.extend({
    'defaults': {
      // in seconds
      'first_name': 'John',
      'last_name': 'Doe',
      'bio': 'Sample bio data',
      'age': 26,
      'birthdate': new Date(1987, 0, 2),
      'username': 'doe'
    },

    'initialize': function() {
      this.attributes.name = this.get('first_name') + ' ' + this.get('last_name');
    }
  });

  window.User.UserModel = UserModel;

}(window, document, Zepto, Backbone, _));

这就是我们模型的全部内容。我们只是为用户定义了默认值,并在创建实例时设置了名称。现在我们将看一下我们的DashboardViewProfileView——这个应用程序的最后两个部分。

视图

用户应用程序将包含两个视图,包括DashboardViewProfileView。正如我们已经建立的那样,每个视图都扩展了我们之前创建的BaseView。为了适应我们的体验,我们需要做一些改变,但总体上这与我们的音乐应用程序视图的实现非常相似。

仪表板视图

与我们之前定义的视图一样,DashboardView将包含用于显示我们仪表板的模板,包含与此视图相关的事件,然后渲染模板。你会注意到这里我们的事件将使用在BaseView中定义的事件处理程序,因为BaseView的事件处理程序满足了导航到另一个视图的基本要求,而路由监听器处理了隐藏功能。

(function(window, document, $, Backbone, _){

  var DashboardView = App.BaseView.extend({
    'template': _.template($('#tmpl-user-dashboard').html()),

    'events': {
      'click a[href="#profile"]': 'onProfileClick',
      'click a[href="#playlist"]': 'onPlaylistClick'
    },

    'initialize': function() {

      this.render();
    },

    'render': function() {
      if (!this.$template) {
        this.$template = $(this.template(this.model.attributes));

        this.$el.prepend(this.$template);
      }

      return this;
    }
  });

  window.User.DashboardView = DashboardView;

}(window, document, Zepto, Backbone, _));

这个视图的代码非常简单;我们以前见过这种模式,现在在这里重复。因为我们在BaseView中定义了事件处理程序,所以我们不需要在这里重新定义它们。至于render方法,它会检查模板的创建,如果存在,就会用用户的数据填充模板,这些数据是在创建User.js中的DashboardView实例时传递的。

这就是我们为仪表板视图需要做的全部;就像我承诺的那样,一旦一般设置完成,它就相当容易。接下来让我们来看看我们应用程序的最后一部分:个人资料视图。

个人资料视图

我们的个人资料视图将与仪表板视图完全相同,因为我们有一个模板、一些事件和一个render方法。就像以前一样,我们不需要定义事件处理程序,因为BaseView在这个过程的开始时已经处理了隐藏视图的基本要求。

(function(window, document, $, Backbone, _){

  var ProfileView = App.BaseView.extend({
    'template': _.template($('#tmpl-user-profile').html()),

    'events': {
      'click a[href="#dashboard"]': 'onDashboardClick',
      'click a[href="#edit"]': 'onEditClick'
    },

    'initialize': function() {

      this.render();
    },

    'render': function() {
      if (!this.$template) {
        this.$template = $(this.template(this.model.attributes));

        this.$el.prepend(this.$template);
      }

      return this;
    } 
  });

  window.User.ProfileView = ProfileView;

}(window, document, Zepto, Backbone, _));

这就是全部内容。如果我们现在运行页面,我们将得到一个完全可访问的应用程序,其默认视图为仪表板视图。然后,您可以通过导航到个人资料和播放列表视图与应用程序进行交互。当您这样做时,应用程序会更改 URL 并保留您的活动历史记录,让您可以轻松地前进和后退。相当不错,对吧?以下是一些屏幕截图,展示最终应用程序的外观:

提示

您可能想知道这个应用程序的样式。幸运的是,本书的源代码已经为您编写了所有这些内容。我们不会讨论样式,因为它实际上并没有涵盖任何移动特定的内容,而是更多地是对我们在这里构建的应用程序进行视觉增强的展示。

个人资料视图

这个应用程序在 iOS 模拟器中运行的屏幕截图展示了我们编写的应用程序的仪表板视图。在这个视图中,我们看到我们的常规页眉和页脚,包括书名和作为导航的选择控件。在内容区域内,我们看到我们的仪表板模板呈现了约翰·多的仪表板和链接到播放列表、个人资料和返回到仪表板。

个人资料视图

在这里,我们展示了播放列表和歌曲视图,展示了音频控件和在曲目之间切换的能力。我们可以看到模板在模板内的呈现(播放列表内的音轨)。通过这个例子,我们可以看到控件(模型、视图和控制器)的分离如何帮助我们区分逻辑和用户界面。

个人资料视图

在这个屏幕截图中,我们看到在播放列表页面下选择并播放的音轨。看起来似乎没有太多事情发生,但在幕后,我们已经创建了一个可重复使用的应用程序,允许用户在不刷新页面的情况下进行交互。

个人资料视图

在这最后一个屏幕截图中,我们看到了个人资料视图,显示了约翰·多的简短传记、年龄和出生日期。在播放列表和个人资料的过渡期间,我们没有看到页面刷新,而是内容更新。分析 URL,我们可以看到历史记录已被保留,因此,允许我们使用原生返回按钮在单页应用程序中进行操作。

总结

给自己一个鼓励吧;我们终于到达了本章的结尾!这是一次愉快的旅程,希望不会太糟糕。在这一点上,您现在已经准备好着手开发单页应用程序了。从理解 MVC 设计模式到实施,利用 Backbone 和 Underscore 等库,您现在可以扩展这个基础,开发与 API 相结合并为用户创造动态美妙体验的复杂应用程序。

第八章:离线应用程序

在本章中,我们将介绍离线应用程序开发的基础知识。具体来说,我们将讨论应用程序清单,包括其优缺点,并看看我们如何处理离线交互。然后我们将深入研究如何使用localStorageIndexedDB在客户端临时存储数据。本章的内容将以基本示例进行补充,这将帮助您快速上手。因此,让我们首先来看看应用程序清单对我们有什么好处。

本章将涵盖以下主题:

  • 应用程序清单

  • 处理离线交互

  • localStorageIndexedDBAPI

应用程序缓存

应用程序缓存,又称为AppCache,允许您定义应该在离线使用期间缓存和可用的资源。这基本上使您的 Web 应用程序在用户离线时可用,因此失去网络连接甚至刷新页面都不会影响用户的连接,他们仍然能够与您的应用程序进行交互。让我们首先来看看应用程序缓存清单是什么样子。

清单文件

我们的应用程序清单文件包含了有关哪些资源将被文件缓存的信息。它明确告知浏览器您希望离线使用的资源,使其可供离线使用,同时通过缓存加快页面加载速度。以下代码展示了本章附带示例的缓存清单的基本示例:

 CACHE MANIFEST

index.html

# stylesheets
../css/normalize.css
../css/main.css
../css/offline.css

# javascripts
../js/vendor/modernizr-2.6.1.min.js
../js/vendor/zepto.min.js
../js/helper.js
../js/App/App.js
../js/App/App.Nav.js
../js/App/App.Offline.js
../js/main.js

在上面的例子中,发生了一些事情。首先,我们使用全大写单词CACHE MANIFEST来标识缓存清单。这一行是必需的,并且被浏览器读取为确切的缓存清单。接下来的一行定义了一个我们想要缓存的文件,index.html。现在,有一件事情我们需要记住;这个清单文件是相对于我们所在的位置的。这意味着我们的文件位于index.html所在的目录中(例如,offline.appcache位于localhost/,就像index.html一样)。

接下来,我们发现我们可以在我们的清单文件中包含注释,只需在前面加上井号(#stylesheets)。这有助于我们跟踪这个文件中发生的事情。从这里开始,我们定义我们想要相对于正在查看的页面定义的样式表和脚本。此时,我们正在查看一个真正的清单文件,并对其进行分解以理解它。随着我们在本章中的进展,我们将回到这个文件,看看它如何影响本章中构建的示例。

清单实现

为了有效地使用我们的缓存清单,我们需要能够将其与当前页面关联起来。但是为了做到这一点,我们还需要设置我们的服务器以正确处理清单,通过发送正确的 MIME 类型。每种类型的服务器处理方式都有些不同,指令可能看起来不同,但它们都能实现相同的结果——发送与该文件相关联的正确类型的标头。

在 Apache 中,我们的配置看起来可能是这样的:

AddType text/cache-manifest .appcache

正如您所看到的,我们已经定义了所有类型为.appcache的文件以text/cache-manifest的内容类型进行传递。这使得浏览器能够正确解释文件,因此浏览器将其关联为cache-manifest。尽管这很好,但我们还没有完成。为了完成,我们需要让我们的页面定义这个文件,以便它能够正确关联。

将我们的缓存清单与我们的页面相关联,我们需要在我们的 HTML 标签上设置manifest属性,如下所示:

<html manifest=”offline.appcache”>

我们现在已经定义了我们的应用程序缓存清单,并将其与相关页面一起交付,但我们只是简要地涉及了这项技术。要充分理解其功能,我们需要看到它的使用。因此,我们将继续创建一个简单的示例,让我们使用到目前为止学到的知识。

一个简单的例子

本章的示例将基于上一章。因此,我们不会详细介绍用于创建此示例的结构、样式或脚本。但是,它将被简化为一个基本的个人资料视图,让您能够理解离线应用程序,而无需从前几章获取额外的知识。首先,让我们看看我们的标记。

标记

与本书一起提供的源代码包含了您开始本章目标所需的一切。因此,让我们看看这个示例的实质,并检查这个示例将如何运行,从标记开始。如果您打开位于offline目录中的索引文件,您会注意到我们的内容应该看起来像这样:

<div class=”site-wrapper”>
    <section class=”view-profile”>
        <header>
            <h1>John’s Profile</h1>
            <a href=”#edit”>Edit</a>
        </header>
        <dl>
            <dt>Bio</dt>
            <dd>This is a little bit about myself; I like iphone web apps and development in general.</dd>
            <dt>Age</dt>
            <dd>26</dd>
            <dt>Birthdate</dt>
            <dd>January 1st, 1987</dd>
        </dl>
        <form>
            <div class=”field”>
                <label for=”bio”>Bio</label>
                <textarea id=”bio”>
                    This is a little bit about me; I like iphone web apps and development in general.
                </textarea>
            </div>
            <div class=”field”>
                <label for=”age”>Age</label>
                <input id=”age” type=”number” value=”26”>
            </div>
            <div class=”field”>
                <label>Birthdate</label>
                <input type=”date” value=”1987-01-01”>
            </div>
        </form>
    </section>
</div>

与任何网页应用程序一样,特别是本书中编写的应用程序,这只是整体应用架构的一部分,其中包括页眉、页脚、样式表和脚本。但是,前面的标记描述了一个显示用户信息的个人资料视图,包括简短的个人简介、年龄和出生日期。除了这些信息外,还有一个表单,允许您更新这些信息。

这个应用程序的体验如下。首先,当用户加载页面时,他们应该看到与他们相关的所有信息。其次,他们将有选择使用文本编辑的超链接来编辑这些信息。当单击超链接时,将出现一个表单,允许用户编辑他们的信息。相应地,编辑超链接将更新为查看个人资料。最后,当用户单击查看个人资料时,表单将隐藏,用户信息的显示将重新出现。

JavaScript

这并不像听起来那么复杂。事实上,用于创建页面功能的脚本依赖于以下脚本:

var $viewProfile = $(‘.view-profile’),
    $form = $viewProfile.find(‘form’),
    $dl = $viewProfile.find(‘dl’),
    $p = $(‘<p />’);

function onEditClick(e) {
    e.preventDefault();
    e.stopImmediatePropagation();

    $form.show();
    $dl.hide();
    $(this).text(‘View Profile’).attr(‘href’, ‘#view’);
}

function onViewClick(e) {
    e.preventDefault();
    e.stopImmediatePropagation();

    $form.hide();
    $dl.show();
    $(this).text(‘Edit’).attr(‘href’, ‘#edit’);
}

$viewProfile.
    on(‘click’, ‘a[href=”#edit”]’, onEditClick).
    on(‘click’, ‘a[href=”#view”]’, onViewClick);

让我们看看前面的代码中发生了什么,以确保没有人迷失方向。首先,我们缓存适合我们页面的元素。通过这种方式,我们通过不是每次需要查找东西时都遍历 DOM 来优化性能。然后,我们定义了onEditClickonViewClick事件处理程序,它们显示或隐藏适当的内容块,然后更新与之相关的锚点标签的texthref属性。最后,我们将click事件附加到缓存的$viewProfile元素上。

注意

请注意,前面的 JavaScript 是本章书籍附带源代码的一部分。为了更好地解释正在构建的应用程序的实质,我们已经删除了闭包和Offline类。当然,您可以选择使用前面的代码,或者继续使用本书的源代码。无论您选择哪种方式,期望的结果都将是一个根据当前状态显示或隐藏内容的应用程序。

当执行前面的代码并加载页面时,应用程序的状态如下:

JavaScript

初始应用程序状态和应用程序编辑状态

现在,我们已经建立了应用程序与交互时应该看起来的坚实基础,我们希望确保它被缓存。通过实施我们应用程序开头给出的技术,我们现在应该有一个可以离线运行的应用程序。

请记住,我们需要将应用程序缓存清单放在与正在构建的示例应用程序相同的目录中。因此,我们的应用程序清单需要存在于离线目录中,与我们的index.html文件一起。如果您查看本章的源代码,您应该会看到我们的清单和源文件的结构和布局的工作示例。在正确配置的服务器上运行此应用程序将使我们的页面离线呈现。但问题是,我们如何调试这样的东西呢?好吧,这就是下一节要解决的问题。

调试缓存清单

调试我们的离线应用程序非常重要,毕竟,如果我们的应用程序依赖于网络连接,我们必须提供一个成功的替代体验给我们的用户。然而,调试离线应用程序并不容易。有几个原因,但主要是基于应用程序缓存接口的实现。

当 Safari 重新访问您的站点时,站点将从缓存中加载。如果缓存清单已更改,Safari 将检查声明缓存的 HTML 文件,以及清单中列出的每个资源,以查看它们是否有任何更改。

如果文件与先前版本的文件在字节上完全相同,则认为文件未更改;更改文件的修改日期不会触发更新。您必须更改文件的内容。(更改注释就足够了。)

这可以在苹果文档中找到:developer.apple.com/library/safari/#documentation/iPhone/Conceptual/SafariJSDatabaseGuide/OfflineApplicationCache/OfflineApplicationCache.html#//apple_ref/doc/uid/TP40007256-CH7-SW5

在浏览器中调试

根据先前的文档,我们可以通过清除具有更新资源的缓存来改进调试过程。这可能只是在我们的代码中更新注释,但为了确保正确的资产被缓存,我们可以使用现代浏览器和调试器工具来查看被缓存的资产。查看以下截图,了解如何测试您的资产是否被缓存:

在浏览器中调试

Safari 开发者工具 - 资源

Safari 中的开发者工具(如前面的截图所示)通过提供一个资源选项卡来帮助我们调试应用程序缓存,允许我们分析多个域的应用程序缓存。在这个例子中,我们可以看到与我们示例应用程序域相关的资源。当我们审查应用程序缓存时,我们可以看到与缓存相关的文件列表在右侧。此外,我们还可以看到文件的位置和用户的状态;在这种情况下,我们是在线且空闲。

在浏览器中调试

Chrome 开发者工具 - 资源

Chrome 开发者工具同样有助于显示与应用程序缓存相关的信息。虽然用户界面有点不同,但它包含了审查与您的应用程序缓存相关的所有必要组件。此视图还包括您的应用程序的在线状态;在这个例子中,我们不在线且空闲。

使用 JavaScript 进行调试

应用程序缓存也可以使用 JavaScript 进行调试,幸运的是,应用程序缓存清单的实现非常容易与之交互。我们可以监听多个事件,包括progresserrorupdateready。当我们监听这些事件时,我们可以选择实现一个补充体验,但在这里,我们只是记录事件。

window.applicationCache.addEventListener(‘cached’, handleCacheEvent, false);
window.applicationCache.addEventListener(‘checking’, handleCacheEvent, false);
window.applicationCache.addEventListener(‘downloading’, handleCacheEvent, false);
window.applicationCache.addEventListener(‘error’, handleCacheError, false);
window.applicationCache.addEventListener(‘noupdate’, handleCacheEvent, false);
window.applicationCache.addEventListener(‘obsolete’, handleCacheEvent, false);
window.applicationCache.addEventListener(‘progress’, handleCacheEvent, false);
window.applicationCache.addEventListener(‘updateready’, handleCacheEvent, false);

function handleCacheEvent(e) {
  console.log(e);
}

function handleCacheError(e) {
  console.log(e);

}

在上面的脚本中,我们监听规范定义的事件(www.whatwg.org/specs/web-apps/current-work/#appcacheevents),并调用handleCacheEventhandleCacheError方法。在这些方法中,我们只是记录事件本身;但是,如果我们愿意,我们可以创建一种替代体验。

在应用程序缓存过程的实施过程中还有许多可以使用的操作方法。例如,我们可以使用update方法手动更新缓存。

window.applicationCache.update();

尽管前面的方法对我们可能有所帮助,但请记住,只有内容本身发生了变化,缓存才会更新。实际上,update方法触发了下载过程(www.whatwg.org/specs/web-apps/current-work/#application-cache-download-process),这并不会告诉浏览器获取最新的缓存。 swapCache方法是另一个可以用来调试我们的应用程序的操作调用,它将缓存切换到最新版本。

window.applicationCache.swapCache();

请记住,如果我们进行此调用,资产不会自动更新。获取更新后的资产的方法是刷新页面。根据规范,实现我们需要的更简单的方法是执行location.reload()www.whatwg.org/specs/web-apps/current-work/#dom-appcache-swapcache)。

到目前为止,我们对应用程序缓存清单有了一个很好的了解,包括它的功能、实施细节以及最终如何调试它。现在我们需要知道如何使用前面的方法以及更多方法来处理离线交互。当我们对这两个方面有了很好的理解后,我们就能够创建利用这项技术的简单离线应用程序。

处理离线应用程序

到目前为止,我们已经学会了如何使用应用程序清单接口在客户端缓存我们的文件,以加快网站速度,同时使其在用户离线时可用。然而,这种技术并没有考虑到用户交互时应该做些什么。在这种情况下,我们需要确保我们的应用程序具有可用的部分,以使应用程序在失去连接时无缝运行。

一个简单的用例

在我们继续之前,让我们定义一个简单的用例,说明为什么处理离线应用对用户和我们都有用。假设我们有一个名叫约翰的用户,约翰正在通勤上班,目前正在 iPhone 上的 Web 应用程序中更新他的个人资料。通勤途中的网络连接有时不太稳定,有时会失去连接。他希望能够在上班途中继续使用应用程序,而不是等到上班后再使用。

考虑到我们今天生活的世界,一个不稳定的交互可能会让公司失去一个客户,我们肯定希望能够优雅地处理这种情况。这并不意味着我们会在用户离线时为其提供所有服务,那是不合理的。这意味着我们需要告知用户他们处于离线状态,由于这个原因,某些功能目前被禁用。

检测网络连接

现在,你可能会问:“如何检测网络连接?”嗯,实际上很简单。让我们看一下下面的代码:

var $p = $(‘<p />’);
if(!navigator.onLine) {
  $p.
    text(‘NOTICE: You are currently offline. Your data will sync with the server when reconnected.’);

  $(‘.view-profile’).
    before($p);
}

让我们简要回顾一下前面的代码。这段代码的第一部分在内存中创建了一个缓存元素,并将其存储在变量$p中。接下来的一行是最重要的,它通过检查navigator对象的onLine属性来检测在线连接。如果用户不在线,我们最终设置了缓存元素的文本,并将其附加到我们之前的代码中。

如果我们的应用程序处于离线状态,它将如下所示:

检测网络连接

检测网络连接

当然,这只是你在现实世界应用中处理网络连接的简化版本,但它展示了你可以获取网络状态并确定离线体验。这对我们来说很棒,因为它为我们打开了一个我们以前无法探索的网络开发新世界。但当然,这也需要一些复杂的预先考虑,以确定如何处理这样的体验。

回到我们定义的用例,用户想要更新他/她的个人资料信息,显然如果没有必要的资源,这将是非常困难的。幸运的是,我们有两种新技术可以用来完成类似这个用例的简单任务。因此,让我们简要地介绍一下这两种技术。

localStorage API

尽管离线状态是新的 HTML5 规范的一个特性,但它与 HTML5 的另一个特性——存储(dev.w3.org/html5/webstorage/)密切相关。存储是许多开发人员之前认为与后端系统有一对一关系的东西。这不再是真的,因为现在我们能够使用localStorageAPI 在客户端设备上保存数据。

让我们通过一个简短的例子来使用localStorage,看看它是如何工作的:

localStorage.setItem(‘name’, ‘John Smith’);

我们刚刚写的代码有多个部分。首先,有一个名为localStorage的全局对象。这个对象有开发人员可以交互的方法,包括setItem方法。最后,setItem方法接受两个参数,都是字符串。

要检索我们刚刚设置的项目的值,我们会这样做:

localStorage.getItem(‘name’);

很酷,对吧?唯一的缺点是,当前的实现描述了 API 只接受每个键/值对的字符串,类似于 JavaScript 对象。然而,我们可以通过以下方式克服这个限制:

localStorage.setItem(‘name’, JSON.stringify({ ‘first’: ‘John’, ‘last’: ‘Smith’ }));

这里的区别在于,我们还访问了内置的JSON对象,将对象转换为字符串,以便localStorageAPI 可以高效地将其存储为纯字符串。否则,你会存储[object Object],这只是对象的类型。

要访问这些信息,我们需要做以下操作:

JSON.parse(localStorage.getItem(‘name’));

当你在控制台中运行这段代码时,你应该会看到一个对象。JSON功能所做的是将“字符串化”的对象转换为传统的 JavaScript 对象。这使得访问我们存储的数据变得简单和高效。

正如你开始了解的那样,我们有能力在客户端存储信息,这是过去无法做到的,这使得我们能够在应用离线时暂时允许用户与网站的某些方面进行交互。结合 HTML5 的存储和离线功能,使我们能够为我们的应用带来更深入的交互,同时满足客户和用户的期望。

然而,localStorage的限制在于可以存储的信息量。因此,另一种技术被称为IndexedDB。尽管它的支持在各个浏览器中并不一致,而且技术仍处于实验阶段,但它值得一看。不幸的是,由于在 iOS Safari 中缺乏支持,我们在本书中不会涉及这项技术,但它仍然值得一些审查(developer.mozilla.org/en-US/docs/IndexedDB)。

总结

在本章中,我们介绍了应用程序缓存的基础知识,包括其实施示例。我们的回顾指出了使用这项新技术的好处,但也讨论了缺点,比如不一致的支持,主要是在旧版浏览器中,以及在测试时面临的问题。我们学会了如何处理离线交互,以及localStorageIndexedDB如何允许我们在客户端临时存储信息作为解决方案。在下一章中,我们将讨论性能优化,并看看这在本书中开发的应用程序中是如何发挥作用的。

第九章:清洁和优化代码的原则

在整本书中,我们都强调了从应用程序开发的最开始就进行优化的重要性。尽管我们已经从 JavaScript 中缓存元素到模块化我们的样式等主题,但在本章中,我们想总结一下书中使用的技术。毕竟,性能对于我们的移动应用程序来说是非常重要的。在本章中,我们将涵盖优化我们的样式、脚本和媒体。除了涵盖优化技术之外,我们还将讨论增强代码可维护性的良好编码标准,同时也提高性能。我们将从讨论样式表开始。

在本章中,我们将涵盖以下主题:

  • 验证 CSS

  • 分析 CSS

  • CSS 最佳实践

  • 验证 JavaScript

  • 分析 JavaScript

  • JavaScript 最佳实践

优化样式表

传统上,样式被“随意”地添加到 Web 应用程序中而没有任何预见性。通常,我们只是为我们的页面添加样式,而没有考虑模块化、可重用性和可维护性。然而,由于今天 Web 应用程序的广泛性质,这种做法已不再可接受。

在本书中,我们努力遵守了一些行业标准,比如模块化。然而,现在我们有工具可以帮助我们验证和分析我们的样式。从分析样本 CSS 文件开始,我们可以优化这些样式;这就是我们在本章节中的目标。

验证我们的 CSS

为了优化我们的样式表,我们需要首先验证我们的 CSS 是否有效,并符合当今的标准。我们可以使用各种工具来验证我们的样式,包括 W3C CSS 验证器和一个名为CSS Lint的工具。这些工具都会检查您的样式表,并为您总结出错的地方、为什么出错以及您应该怎么做。

W3C CSS 验证器

要访问 W3C CSS 验证器,您可以访问以下 URL:

jigsaw.w3.org/css-validator/

以下屏幕截图显示了 W3C 验证器的默认视图,允许您输入包含样式的页面的 URI。它将根据 W3C 规范自动获取您的样式表并对其进行验证。然而,我们不仅仅局限于在现场或生产就绪的网站上让我们的页面可爬行。我们还可以选择上传我们的样式表,或直接将它们放入这个应用程序中。

W3C CSS 验证器

W3C CSS 验证器-URI 视图

在以下视图中,您可以看到我们可以通过文件上传过程验证样式。这将简单地通过后端处理器运行这些样式表,以检查样式是否有效;一旦这个过程完成,我们就会得到结果。

W3C CSS 验证器

W3C CSS 验证器-文件上传视图

最后,我们有直接将我们的样式插入到工具中的选项,这可能是根据项目和团队或个人的需求最快速、最简单的解决方案。我们不必担心样式被剥离或以任何方式修改;文本字段将正确处理您的所有输入。与其他视图类似,一旦单击检查按钮,输入将通过处理器运行并向您呈现结果。

W3C CSS 验证器

W3C CSS 验证器-直接输入视图

可定制选项

与任何优质保证工具一样,我们需要有能力定制此工具的选项以适应我们的需求。在这种情况下,我们有各种可用的选项,包括:

  • 配置文件:此选项指定验证样式时要使用的配置文件,例如 CSS Level 1、CSS Level 2、CSS Level 3 等。

  • 警告:此选项指定报告中要呈现的警告,例如 Normal、Most Important、No Warnings 等。

  • 介质:此选项指定了样式表应该表示的介质,例如屏幕、打印、手持设备等。

  • 供应商扩展:此选项指定了供应商扩展(-webkit--moz--o-)在报告中的处理方式,例如警告或错误。

验证一个成功的例子

让我们看一个成功的验证例子。首先,让我们使用我们在之前章节中创建的一些样式来查看 CSS 是否通过验证;特别是,让我们使用singlepage.css文件的内容,并将其粘贴到 W3C 验证器的直接输入视图中,并使用默认选项运行它。

当我们运行验证器时,我们的结果应该是这样的:

验证一个成功的例子

W3C CSS 验证器 - 成功验证

如您所见,输出是成功的,并通过了 CSS Level 3 规范。令人惊讶的是,我们甚至可以从验证器那里得到徽章放在我们的网站上!但不要这样做;尽管您应该对自己的工作感到满意,但这是我们大多数人不太喜欢在我们的网站上看到的东西。现在让我们看一个不成功的例子。

验证一个不成功的例子

编程中经常出现错误,因此在我们的样式、脚本和标记中遇到验证错误是很自然的。因此,让我们看一下 W3C CSS 验证器中验证错误的示例是什么样子。在这个例子中,我们将使用我们在第二章中创建的video.css文件的变体,集成 HTML5 视频。为了这个例子,我引入了一些错误,包括以下样式:

video {
  display: block;
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  font-size: 2px;
  font-style: italics;
}

.video-container {
  width: 100;
}

.video-controls {
  margin: 12px auto;
  width: 100%;
  text-align:;
}

.video-controls .vc-state,
.video-controls .vc-track,
.video-controls .vc-volume,
.video-controls .vc-fullscreen, {
  display: inline-block;
  margin-right: 10px;
}

.video-controls .vc-fullscreen {
  margin-right: 0;
}

.video-controls .vc-state-pause,
.video-controls .vc-volume-unmute {
  display: none;

当我们通过 W3C CSS 验证器传递前面的样式时,我们得到以下结果:

验证一个不成功的例子

W3C CSS 验证器 - 不成功的验证

在上面的例子中,我们得到了一些值、属性和解析错误,所有这些都可以通过此不成功的验证示例中给出的参考轻松解决。这样做的好处是,不用试图弄清楚可能破坏布局的原因,屏幕截图中显示的错误可能解决所有问题。

从某种意义上说,这基本上是您需要确保您的 CSS 在多个浏览器中有效和符合规范的所有内容。但是,如果您能够防止这些错误发生呢?好吧,有一个工具可以做到,那就是 CSS Lint。

CSS Lint

在大多数情况下,我们在编码时希望尽量避免出现错误,并且使用某种工具及早捕捉这些错误将会很有帮助。CSS Lint 就是这样一个工具,实际上可以直接在您选择的文本编辑器或 IDE 中使用。CSS Lint 不仅检查您的样式是否符合 CSS 的某些原则(如盒模型),还进行了大量的语法检查,帮助您有效地调试样式。

CSS Lint 指出了 CSS 代码的问题。它进行基本的语法检查,并应用一组规则来查找问题模式或低效迹象。这些规则都是可插拔的,因此您可以轻松编写自己的规则或省略您不想要的规则。

有关 CSS Lint 的详细信息可以在github.com/stubbornella/csslint/wiki/About找到。

与 W3C CSS 验证器类似,CSS Lint 有自己的网站,您可以将样式复制粘贴到文本区域中,让处理器检查您的样式。我们与之交互的页面如下所示:

CSS Lint

CSS Lint

可定制选项

CSS Lint 还带有可定制的选项,这些选项非常广泛,您可以根据自己或团队的需要进行定制。可定制选项有六个部分,包括错误可维护性和重复性兼容性可访问性性能OOCSS面向对象的 CSS)。

可定制的选项位于Lint!按钮的正下方:

可定制的选项

CSS Lint 选项

检查适当的选项使引擎能够根据这些属性进行验证。通常这些选项在项目之间会有所不同;例如,您可能正在开发一个需要在某些元素上设置填充和宽度的应用程序,因此,取消选中注意破碎的框尺寸选项可能更适合您,这样您就不会看到多个错误。

使用 CSS Lint 验证成功的示例

当我们自定义选项并通过 CSS Lint 传递页面时,如果样式表符合标准,同时也满足团队的需求,我们应该收到一个成功的验证,如下面的截图所示:

使用 CSS Lint 验证成功示例

CSS Lint – 成功的验证

在上述情况下,我们的 CSS 样式通过了,不需要额外的信息。但是,当我们的 CSS 未通过验证时会发生什么呢?

使用 CSS Lint 验证不成功的示例

如果我们将在上一节中为 W3C CSS 验证器创建的备用视频样式通过 CSS Lint,我们会得到以下结果:

使用 CSS Lint 验证不成功的示例

CSS Lint – 未成功的验证

然而,仅仅因为我们收到了四个错误和两个警告并不意味着我们无助。事实上,当我们向下滚动页面时,我们会看到需要处理的项目列表;它还包括问题类型,描述以及错误发生的行:

使用 CSS Lint 验证不成功的示例

CSS Lint – 未成功的验证列表

集成 CSS Lint

尽管我们有一个可以用来验证我们的样式的图形用户界面GUI),但如果我们能够简化我们的个人开发工作流程,那将会更容易。例如,如果我们可以在文本编辑器或集成开发环境IDE)中保存样式表时验证我们的 CSS,那将会很好。CSS Lint 非常灵活,允许我们实现这些集成的工作流程。

一些集成开发环境和文本编辑器供应商已经实现了 CSS Lint,包括 Sublime Text,Cloud 9,Microsoft Visual Studio 和 Eclipse Orion。虽然将 CSS Lint 安装和设置到您喜欢的工具中超出了本书的范围,但您可以在这里查找所有所需的信息:

github.com/stubbornella/csslint/wiki/IDE-integration

分析我们的 CSS

以前很难对 CSS 进行分析,事实上可能是不可能的。但是随着浏览器调试工具的进步,我们现在能够在一定程度上对样式表进行分析。在本节中,我们将回顾如何对我们的样式进行分析,并阅读 Safari 浏览器在 Mac 上向我们呈现的信息。

在接下来的屏幕中,我们将简要介绍如何使用分析来分析样式以及 Safari 浏览器如何向我们呈现这些信息。我们只会查看我们的样式的布局和渲染。使用我们之前构建的单页面应用程序,我们将查看我们的样式的有效性,并查看我们的样式在与应用程序的呈现层相关的方面的弱点和优势。

让我们从查看我们的单页面应用程序的仪表板视图开始,Safari 调试工具已打开,并处于配置文件选项卡(时钟符号)上。

分析我们的 CSS

Safari 分析工具

当我们首次加载我们的单页面应用程序并查看页面加载的分析时,我们会看到三个不同的时间轴,包括网络请求布局和渲染JavaScript 和事件。对于我们的目的,让我们看看布局和渲染

当我们查看布局和渲染时间轴时,我们可以看到重绘和重排计算是在页面加载时进行的。调试器还让我们知道运行了什么类型的进程,何时运行以及更改了哪些属性,包括其开始时间和持续时间。在寻找页面性能泄漏时,这些都非常有帮助。但是,运行时分析呢?嗯,调试器也有这个功能。

实际上,在我们的左侧边栏上有一个圆圈,与Profiles选项卡在同一行,它允许我们对 JavaScript 或 CSS 进行分析。这很棒,因为当我们启用它时,我们将开始对应用程序进行运行时分析。因此,假设我们启用了对 CSS 的分析,然后在应用程序中点击Profile选项卡以切换页面视图;我们肯定会执行一些更改,使我们的样式发生变化。当我们这样做并停止我们的 CSS 分析时,我们会得到以下结果:

分析我们的 CSS

Safari 分析工具-运行时分析

当我们分析我们的分析时,我们可以看到正在使用的选择器,它们渲染的总时间,页面上的匹配数以及它们的来源。这是对发生了什么样的处理进行了很好的分解,并让我们对每个选择器查找和渲染所花费的时间有了一个很好的概念,从而让我们知道可以改进什么。鉴于我们为本书的应用程序很小,但如果你正在开发一个包括复杂动画或渲染数千行数据的应用程序,这将在调试您的移动应用程序时非常有用。

一旦我们对我们的应用程序的瓶颈有了一个很好的想法,我们就需要采取一些行动。拥有这些信息为我们提供了关于应用程序性能的关键信息以及我们应该关注的内容。优化阶段是基于每个团队或个人面临的问题和项目需求的,因此在下一节中,我们将讨论一些用于更快渲染和匹配我们样式的优化技术。

优化我们的 CSS

在这一部分,我们简要介绍了一些行业标准,这些标准通过提供高效、可维护和精心制作的模块化样式来帮助我们优化应用程序的渲染时间。这些标准已经被业内知名的个人和组织广泛讨论,并最终被各种框架采纳。当然,这里讨论的标准可能随着时间的推移和浏览器实现更好的处理方法而发生变化,使新技术更快、更高效,但这应该是任何希望创建符合当今需求的样式表的人的良好起点指南。

避免通用规则

不要在规则中使用*选择器。这会选择 DOM 中的每个元素,因此它们的遍历方法是低效的。

例如,以下是极其低效的:

header > *

前面的代码之所以效率低,是因为它使用了通用选择器。因为 CSS 是从右到左读取的,引擎会说“让我们选择所有元素,然后看它们是否与标题元素直接相关”。因为我们需要遍历整个 DOM,所以这个选择器的渲染比像这样的东西要慢得多:

header > h1

不要限定 ID 或类规则

限定 ID 或类涉及直接将标签名称与适当的选择器相结合,但出于与前一条规则相同的原因,这是极其低效的。

例如,以下所有选择器都是不好的:

input#name-text-field

.text-field#name-text-field

input.text-field

.text-field.address-text-field

尽管其中一些可能看起来很诱人,但它们是不必要和低效的。但是,这里有一个例外;如果我们想通过向元素添加类来更改样式,那么限定类可能是必要的。无论如何,我们可以通过以下方式来纠正前面的限定 ID 或类。

#name-text-field

.text-field

.text-field.address-text-field

正如前一段提到的,最后一个选择器在通过 JavaScript 基于用户操作更改元素样式时可能更有用。

永远不要使用!important

这条规则相当不言自明。使用它来覆盖样式肯定很诱人,但不要这样做;随着您的应用程序变得更加复杂,这只会带来麻烦。因此,请查看下一条规则。

模块化样式

创建通用于 Web 应用程序或网站的样式非常容易;然而,如果我们开始以模块化的方式思考,我们就会开始创建专门用于该应用程序部分的样式。例如,考虑一个表单及其输入,假设我们希望网站上的所有表单都包含具有棕色边框的文本字段。我们可以这样做:

form .text-field { border: 1px solid brown; }

现在我们已经将所有包含在form元素内的类为.text-field的字段保留为这种样式。因此,如果任何类为.text-field的输入字段在此选择器之外,我们可以按照自己的方式进行样式设置。或者,我们也可以这样覆盖样式:

form .personal-information .text-field { border: 1px solid blue; }

现在,如果我们在原始样式之后包含这个样式,它将优先使用,因为我们实际上使用了使我们的样式更高效和更易管理的级联原则。

提示

请记住,后代选择器是最昂贵的选择器。然而,它们非常灵活,因此我们不应该为了高效的 CSS 而牺牲可维护性或语义。

在大多数情况下,这些规则应该足够了,但您很可能会发现实施一些其他行业中已经写过的最佳实践是有用的。当然,您应该使用您所使用的框架采用的最佳实践,或者更好的是适合您的团队。我发现这些对我非常有帮助,是一个很好的起点,我鼓励您根据需要进行研究和实验。现在,让我们看看如何优化我们的应用的 JavaScript。

优化 JavaScript

现在我们已经涵盖了样式表的优化,让我们看看我们的脚本。JavaScript 也曾经被毫无考虑或计划地放在页面上,总的来说给这门语言带来了不好的声誉。但是,由于 Web 应用程序的复杂性,开源社区已经帮助塑造了这门语言。

在整本书中,我们采用了几个行业标准,包括命名空间、闭包、缓存变量等。然而,验证和分析我们的脚本也是必不可少的,以便进行优化。在本节中,我们将介绍这一点,并希望涵盖制作高性能移动应用所需的主要要点。

使用 JSLint 验证 JavaScript

近年来,出现了各种工具来帮助我们验证 JavaScript。诸如 JSLint 和 JSHint 之类的工具已经被创建,以帮助我们编码,类似于 CSS Lint。但为什么我们应该使用这些工具,特别是对于 JavaScript 呢?JSLint 的网站(www.jslint.com/lint.html)提到了工具背后的原因:

JavaScript 是一种年轻但成熟的语言。最初,它是用来在网页中执行一些小任务的,这些任务对于 Java 来说太笨重、太笨拙了。但 JavaScript 是一种令人惊讶的功能强大的语言,现在它也被用于更大的项目中。许多旨在使语言易于使用的功能在项目变得复杂时会带来麻烦。JavaScript 需要一个语法检查器和验证器:JSLint。

JSLint 的网站还提到了以下内容:

JavaScript 是一种松散的语言,但在其中有一种更优雅、更好的语言。JSLint 可以帮助您使用更好的语言进行编程,并避免大部分松散。JSLint 会拒绝浏览器会接受的程序,因为 JSLint 关心您的代码质量,而浏览器不关心。您应该接受 JSLint 的所有建议。

要测试我们的 JavaScript,我们可以轻松访问 JSLint 的网站(www.jslint.com/):

使用 JSLint 验证 JavaScript

JSLint 网站

正如您所看到的,JSLint 与 CSS Lint 非常相似,您只需要将 JavaScript 输入到页面上,结果就会显示出来。让我们看看成功和失败的输出会是什么样子。

使用 JSLint 验证成功的例子

在我们的例子中,我们将利用我们的App.js JavaScript 来测试 JSLint 实用程序。当我们运行这个文件时,成功的输出将详细列出闭包中使用的方法、变量和属性。让我们看看以下截图:

使用 JSLint 验证成功的例子

使用 JSLint 进行成功验证 - 方法和变量

前面的例子是使用 JSLint 进行成功验证的顶视图。验证器将返回一个以所有全局对象列表开头的列表。然后它将继续列出方法、变量以及每个的一些细节。例如,initVideo返回thisApp的一个实例等等。

使用 JSLint 验证成功的例子

使用 JSLint 进行成功验证 - 属性

验证失败的例子

如果不修改 JSLint 选项,使用与前一个相同的例子将产生多个错误。这些错误主要是空格、间距和处理器不知道的全局对象。

验证失败的例子

JSLint - 验证失败

根据前面的输出,错误以红色列出,包括描述、示例代码和错误发生的行号,让您轻松调试应用程序。现在,假设我们不希望空格或间距实际影响验证结果;那么我们可以定制 JSLint 的选项。

可定制选项

与本章讨论的大多数工具一样,JSLint 也提供了可以根据我们的需求定制的选项。让我们简要回顾一下网站上提供给我们的一些选项。

可定制选项

JSLint - 选项屏幕

对我们可用的选项非常广泛,从空格格式到对我们所有放在 JavaScript 中的TODO注释的正确性的容忍。当然,其中一些选项可能在测试时不符合我们的需求,但总的来说,它们非常有助于保持一致的编码标准,提供跨平台有效的脚本。

集成 JSLint

与 CSS Lint 类似,JSLint 可以在您喜欢的 IDE 或文本编辑器中使用。许多供应商已经创建了插件或扩展工具,使您可以在输入或保存代码时轻松进行代码检查。例如,Sublime Text 有一个SublimeLinter包,其中包括 CSS Lint、JSLint 以及其他一些可以帮助您更高效编码的工具。这是如何可能的?

JSLint 可以在任何可以运行 JavaScript(或 Java)的地方运行。例如 github.com/douglascrockford/JSLint/wiki/JSLINT

有关更多详细信息,请参考以下内容:

github.com/douglascrockford/JSLint

JSLint 本质上是一个 JavaScript 方法,可以传入代码,然后由 JavaScript 本身进行评估,使其非常高效地处理您的代码并集成到其他环境中。因此,如果您的文本编辑器或 IDE 中还没有它,您可以轻松创建一个扩展,帮助您使用 JSLint 编写高质量的代码。

对我们的 JavaScript 进行分析

与 CSS 性能分析一样,在 Web 的旧时代,测试 JavaScript 的性能是非常困难的。然而,这些天我们不需要太担心这个问题,因为几乎每个浏览器调试器都实现了一种对脚本进行性能分析的方法。使用 Safari 内置的调试工具,我们将了解如何调试我们应用程序的脚本性能。

在以下示例中,我们将仅仅讨论我们之前构建的单页面应用程序中 JavaScript 的性能分析,类似于我们在上一节中对样式进行性能分析的做法。

对我们的 JavaScript 进行性能分析

Safari 性能分析工具- JavaScript

前面的截图是页面加载时脚本的回顾。当我们查看“JavaScript & Events”时间轴时,我们可以得到每个脚本的类型、细节、位置、开始时间和持续时间的详细信息,这些都对脚本时间轴的结果有所贡献。虽然开始时间是我们肯定想要知道的,以便查看可能阻塞脚本(其他脚本),但持续时间可能更重要,因为如果每个脚本不是异步加载的话,它们可能会阻塞页面渲染的过程。

除了查看脚本对页面加载的影响之外,我们还可以对脚本执行的功能进行性能分析。例如,假设我们想要检测当我们在应用程序中点击“Profile”按钮时我们的方法的执行情况。这可以很容易地通过与对 CSS 进行性能分析相同的技术来实现,点击“Profile”选项卡中的圆圈并启用 JavaScript 的性能分析;我们将能够看到所有调用的方法及其性能。

对我们的 JavaScript 进行性能分析

Safari 性能分析工具- JavaScript 运行时

根据我们之前的用例,我们可以很容易地详细了解我们应用程序的性能。从这个例子中我们可以得知,我们的onProfileClick事件大约需要 8.40 毫秒来执行,并且只调用了一次。然而,更重要的是,我们可以看到所有被调用的方法以及执行顺序,这是非常有用的信息,可以帮助我们检测内存泄漏和性能优化,这对我们的应用程序是必要的。

从这些非常基本的示例中,我们可以看到调试我们的应用程序性能比以往任何时候都更容易。我们可以对 JavaScript 进行性能分析,了解我们的应用程序的运行情况和代码的效率。但是既然我们有了这些信息,我们可以做些什么来改进我们的代码库呢?这就是我们在下一节要解决的问题,一些通用的优化技巧,我们都可以使用这些技巧来提高我们的应用程序的性能,而不会牺牲代码质量。

优化我们的 JavaScript

JavaScript 是高度可扩展的,允许我们几乎做任何我们想做的事情-这很棒,但也可能非常有害。例如,你可以很容易地忘记在变量前使用关键字var。然而,我们不希望这样做,因为这会使我们的变量在全局范围内可用,这可能会与其他脚本发生冲突,这些脚本可能使用完全相同的变量名。我们也可以很容易地将我们的 JavaScript 包装在try...catch语句中,这并不是最佳实践,因为我们并没有找出问题所在。或者,如果我们想的话,我们可以很容易地使用eval来评估一串 JavaScript,而不进行任何错误检查。

因此,该行业已经采用了多种经过验证的最佳实践,这些最佳实践由最常用的开源库实施,包括 jQuery、Backbone 和 Underscore。在本节中,我们简要介绍了我所基于的书籍的最佳实践,以及我认为对任何应用程序的成功至关重要的最佳实践。

避免全局变量

在全局范围内或在我们应用程序中创建的闭包之外创建所有变量和函数非常容易且诱人。但不要这样做;这是一个糟糕的想法,并且因为几个原因而受到社区的鄙视。例如,如果一个变量保留在全局范围内,它必须在整个应用程序的执行过程中进行维护,从而降低应用程序的性能。

所以,而不是这样做:

Var Modal = function(){};

你应该这样做:

(function(){ function Modal(){} }());

前面的技术与我们一直在做的非常相似。实际上,这就是我们所谓的闭包或立即调用的函数表达式IIFE)。当我们将一个方法包装在括号内,然后使用()调用它时,我们立即调用该方法并创建一个新的包含范围,因此括号内的任何内容在全局范围内不可用,使我们的代码更易管理。

不要触及 DOM

嗯,我们可能不会这样做,但我们肯定应该尽量减少。访问 DOM 是昂贵的,并且在应用程序性能方面存在问题。因此,让我们来看一个用例,比如更新信息列表。

避免这样做:

var $list = $('ul');

for (var i; i < 100; i++) {
  var li = '<li>' + i + '</li>';

  $list.append(li);
}

相反,你应该这样做:

var $list = $('ul'),
  liArray = [];

for (var i; i < 100; i++) {
  liArray.push('<li>' + i + '</li>');
}

$list.append(liArray.join(''));

两者之间的区别在于前者每次创建列表项时都会触及 DOM,而后者会将每个项目推到一个数组中,当涉及到追加时,将数组与空字符串连接,只触及 DOM 一次。

使用文字

这可以在我们整本书的代码库中看到。这更有效,因为我们不使用new关键字。例如,我们可以使用Array文字,而不是通过新关键字声明一个新变量,就像这样:

var arr = []; // not new Array();
var str = ''; // not new String('');

模块化功能

为了保持代码的模块化,您需要确保每个函数或类都有特定的功能集合。大多数情况下,每个函数可能应该是大约 10 到 15 行代码,实现特定的目标。

例如,您可以编写以下功能:

function init() {
  var $list = $('ul'),
    liArray = [];

  for (var i; i < 100; i++) {
    liArray.push('<li>' + i + '</li>');
  }

  $list.append(liArray.join(''));	

  $list.on('click', function() {
    // do something
  });
}

而不是编写前面的代码,我们可以这样做:

function populateLists() {
  var $list = $('ul'),
    liArray = [];

  for (var i; i < 100; i++) {
    liArray.push('<li>' + i + '</li>');
  }

  $list.append(liArray.join(''));	

  return $list;
}

function attachListsEvents($list) {
  $list.on('click', doSomething);
}

function doSomething(e) {
  // do something
}

function init() {
  var $list = populateLists();

  attachListsEvents($list);
}

正如您所看到的,代码已经模块化,以执行特定的功能集,使我们能够创建运行特定指令集的方法,每个方法都以名称描述。这对于维护我们的代码库并提供有效的功能非常有用。

总结

在这一章中,我们考虑了优化应用程序各个部分的性能,包括样式、脚本和媒体。我们讨论了验证、优化和分析我们的样式和脚本。此外,我们简要介绍了如何优化我们的媒体,包括图像、音频和视频。现在我们对本书中用于优化应用程序的技术有了坚实的理解,下一章中,我们将看看可以帮助我们使用 HTML5、CSS3 和 JavaScript 交付原生应用程序的框架。

第十章:创建本机 iPhone Web 应用程序

在本章中,我们将研究如何使用 PhoneGap 框架将我们的 iOS Safari 本机应用程序转移到本机环境。我们将深入设置我们的开发环境,包括设置 Xcode IDE 和使用 iOS 模拟器。我们将构建一个HelloWorld示例,以演示快速入门的简单性,然后转移我们在第七章构建的单页应用程序,单页应用程序。一旦我们在本机应用程序开发上有了坚实的基础,我们将通过使用 PhoneGap 的联系人 API 来绑定本机功能,从而增强单页应用程序,以引入我们的联系人并显示其中一些信息。

我们的目标是帮助您使用单一代码库实现本机应用程序的一致外观和感觉。这里的目标是让您开始使用您已经喜爱和理解的 Web 编程语言进行本机应用程序开发。考虑到这一点,让我们从设置我们的开发环境开始。

在本章中,我们将涵盖:

  • Xcode 安装

  • 使用 iOS 模拟器

  • 实施 PhoneGap

  • 创建HelloWorld示例

  • 转移当前应用程序,包括 CSS、JavaScript、HTML 和资产

  • 使用 PhoneGap 在 iOS 中绑定本机功能的联系人 API

设置开发环境

与创建软件的任何工作流程一样,我们的开发环境至关重要。因此,让我们花些时间设置许多工程师喜欢的环境,以创建本机应用程序。在本节中,我们将介绍 Xcode 的安装和集成开发环境(IDE)的概述。我们将继续设置 PhoneGap 框架,最后看看 iOS 模拟器如何在测试我们的应用程序中发挥关键作用。作为一个额外的奖励,我们将在本章中看看如何配置我们的应用程序以满足我们的需求。所以让我们开始吧。

开始使用 Xcode

Xcode 是 iOS 操作系统上本机应用程序开发的首选 IDE,因为它得到了苹果的积极支持,并专门针对 OS X 和 iOS 操作系统进行了定制。这个 IDE 由苹果提供,可以用来创建 Mac OS X、iPhone 和 iPad 应用程序。虽然它也可以用于其他各种类型的开发,但这三个平台最常与 Xcode 相关。默认情况下,您的 Mac 没有预装 Xcode,所以我们需要安装它。

安装 Xcode

幸运的是,Xcode 非常容易安装。我们可以通过 Mac App Store(itunes.apple.com/us/app/xcode/id497799835?ls=1&mt=12)安装这个 IDE。安装完成后,我们的计算机上将安装各种软件,包括 Instruments 分析工具、iOS 模拟器和最新的Mac OS X 和 iOS 软件开发工具包SDK)。

Xcode IDE 概述-基础知识

默认情况下,Xcode IDE 安装在应用程序目录中;双击显示的图标启动它。图标是一个对角放置在蓝色技术图纸上的锤子,上面有一个形成字母 A 的铅笔、刷子和尺子。应用程序启动时,我们将看到欢迎屏幕。

Xcode IDE 概述-基础知识

欢迎屏幕

这是 Xcode 的欢迎屏幕,列出了最近的项目和创建新项目、连接到存储库、了解 Xcode 或查看苹果开发者门户网站的能力。在您的屏幕上,您很可能不会看到前面截图中列出的HelloWorld项目,这是我们将要构建的项目,如果这是您第一次,它应该是空的。

提示

因为这一部分是让我们熟悉 Xcode 本身,不要担心接下来的几个屏幕。接下来的屏幕是我们要构建的,但只是为了帮助我们识别 Xcode 应用程序的某些部分,以便更容易使用。

Xcode 工作区

现在,让我们了解 Xcode 的用户界面,以了解如何利用这个强大的工具。首先,正如我们已经知道的,当我们打开应用程序时,会看到欢迎屏幕。您可以选择通过在欢迎屏幕上取消选中Xcode 启动时显示此窗口复选框来禁用此功能。但是当我们打开一个已创建的项目时,它看起来是这样的:

Xcode 工作区

项目显示

看起来很简单对吧?这很好,因为这被称为工作区,这很关键,因为 Xcode 旨在使所有开发工作都集中在 IDE 的一个中心窗口中,帮助我们整合和加快开发过程。但要认识到这个工作区的两个关键方面:左侧的导航器区域,其中包含我们所有的文件,以及我们可以编辑所在项目的编辑器区域。

Xcode 工作区

导航器和编辑器区域

前面的截图有助于演示 Xcode 在开发应用程序时的两个最关键的方面。请记住,根据所选的文件,您的编辑器区域会发生变化。例如,在前面的截图中,我们有一个 GUI,可以让我们设置项目的属性。

Xcode 工具栏

Xcode 工具栏具有各种功能,我们在开发原生应用程序时会经常使用。例如,在下面的截图中,有RunStopBreakpoints按钮,以及Scheme选择器。在调试应用程序时,这些操作非常重要。Run按钮会运行您的应用程序。另一方面,Stop按钮将停止运行应用程序的所有活动。Breakpoints按钮将在编辑器区域显示我们的断点。

Xcode 工具栏

显示运行、Scheme 和断点的工具栏

Scheme选择器允许您选择要测试的应用程序以及要测试的环境。在我们的示例应用程序中,HelloWorld将使用 iPhone 6.0 模拟器进行测试,但我们有各种选项可供选择。从下面的截图中可以看到,如果安装了,我们可以使用 iPad 模拟器和各个版本以及 iPhone 模拟器来测试我们的应用程序。

Xcode 工具栏

工具栏 Scheme 选择器

工具栏还有各种操作,位于 IDE 右侧,包括编辑器视图、常规视图和组织者。默认的编辑器视图是文本编辑器组件,允许我们对源文件进行基本编辑。中间的编辑器视图是助理编辑器,我们不会涉及。最后的编辑器视图是版本编辑器。

Xcode 工具栏

工具栏项目显示选项

版本编辑器对于我们作为开发人员来说非常有用,可以让我们立即编辑文件并查看版本变化。例如,在下面的截图中,我们可以看到添加了注释,并且原始版本文件通知用户更改发生的位置,让我们可以看到同一文件的实时编辑。

Xcode 工具栏

项目版本显示

继续讨论View工具栏部分,我们有三个按钮。每个按钮根据情况显示对我们有用的编辑器的某个部分。第一个按钮默认选中,因为它显示了左侧的导航器区域。中间的按钮显示了调试区域,如下面的截图所示:

Xcode 工具栏

项目调试显示

这很好,因为我们现在可以在应用程序运行时调试应用程序并查看日志。记得我们在应用程序中使用的所有日志吗?它们会显示在这里;如果我们的浏览器中没有非常有用的开发者控制台,它们非常有用。工具栏中的最后一个按钮控制工具。这些工具帮助我们控制当前文件的各种设置;从名称到源代码控制,我们可以定制文件的各种细节。

Xcode 工具栏

项目文件配置显示

好的,我们知道了 Xcode 的基本功能,还有很多可以探索的地方,而且作为开发者,它对我们来说既伟大又有益。我们可以继续介绍 Xcode 的所有非常有用的功能,但为了我们的利益,让我们转向 PhoneGap,毕竟我们更感兴趣的是学习如何构建原生应用程序。工具总是可以根据我们的需求使用和定制。

设置 PhoneGap

Xcode 在应用程序开发环境中非常好用。然而,PhoneGap 才是魔法发生的地方。它是一个框架,使我们能够基于我们已经用 HTML、CSS 和 JavaScript 编写的代码创建原生应用程序。因此,让我们回顾一下如何安装它,创建一个项目,并简要介绍它的支持和许可,以便为我们自己的应用程序利用其能力做好准备。

安装 PhoneGap

PhoneGap 非常容易上手;首先让我们从 PhoneGap 的网站安装它,网址是:phonegap.com/download/。当 ZIP 文件完全下载完成后,我们需要提取其内容。现在当您开始检查提取的内容时,您会注意到有很多内容,特别是在lib目录中列出了多个操作系统。这很好,因为 PhoneGap 支持多个平台,但我们想要的是特别针对 iOS 的。我们的重点应该放在以下内容上:

安装 PhoneGap

PhoneGap 目录结构

请注意,在 iOS 目录中,我们有多个文件,所有这些文件对于创建我们的第一个 PhoneGap 项目至关重要。在接下来的部分中,我们将使用这个经过简化的 PhoneGap 框架创建我们的第一个 PhoneGap 项目。

创建一个 PhoneGap 项目

现在我们已经下载并简化了 PhoneGap 框架以满足我们的需求,我们想要使用这个框架创建我们的第一个项目。为了做到这一点,我们需要我们值得信赖的命令行界面CLI)的帮助。默认情况下,所有 Mac 操作系统都带有终端,但我们也可以使用 iTerm(免费)。无论哪种方式,启动该应用程序,它位于/Applications/Utilities/

当您打开终端时,我们需要导航到 PhoneGap 文件所在的目录。默认情况下,这应该在我们的Downloads目录中,具体取决于您的浏览器设置。在这种情况下,我会使用cd命令导航到/Users/acresp/Downloads,如下所示:

cd /Users/acresp/Downloads

一旦我们进入 PhoneGap 解压到的目录,我们需要导航到phonegap文件夹内iOS文件夹内的bin目录以查看。为此,我们可以输入以下内容:

cd phonegap-2.5.0/lib/ios/bin/

现在我们可以使用bin文件夹内的create shell 脚本构建我们的 PhoneGap 应用程序。该脚本的文档如下:

#
# create a Cordova/iOS project
#
# USAGE
#   ./create <path_to_new_project> <package_name> <project_name>
#
# EXAMPLE
#  ./create ~/Desktop/radness org.apache.cordova.radness Radness
#

这对我们来说非常好,因为我们知道可以轻松创建我们的应用程序。但在这之前,让我们确保我们的应用程序目录已经在我们的项目中创建了。在本章中,我创建了一个cordova250目录,其中包含我们的HelloWorld应用程序,可能还包含其他 PhoneGap 项目。

现在我们已经确保我们的应用程序目录存在,我们可以运行以下命令来确保我们的应用程序被创建:

./create ~/Sites/HTML5-iPhone-Web-App/cordova250/HelloWorld .org.apache.cordova.HelloWorld HelloWorld

这将在cordova250文件夹内产生一个名为HelloWorld的目录,其中包含我们启动所需的所有必要文件。我们现在已经创建了我们的第一个 PhoneGap 项目。目前还没有太多的事情发生,但让我们继续;我们很快就会开始开发原生应用程序。首先,让我们回顾一下这个框架的支持以及支持它的许可证。

PhoneGap 许可证

您可能会对 PhoneGap 许可证感到好奇,特别是因为我们在创建应用程序的过程中使用了许多开源项目。PhoneGap 基于 Apache 许可证(phonegap.com/about/license/)。对我们来说更好的是,Apache 基金会为我们提供了清晰简明的关于允许、禁止和要求的信息。直接来自常见问题部分的这意味着什么?部分(可在www.apache.org/foundation/license-faq.html#WhatDoesItMEAN找到),我们得到了所有我们需要的细节:

它允许您:

自由下载和使用 Apache 软件,全部或部分,用于个人、公司内部或商业目的;

在您创建的软件包或分发中使用 Apache 软件。

它禁止你:

在没有适当归属的情况下重新分发任何 Apache 来源的软件;

以任何方式使用 Apache 软件基金会拥有的标记,可能会声明或暗示基金会支持您的分发;

以任何方式使用 Apache 软件基金会拥有的标记,可能会声明或暗示您创建了相关的 Apache 软件。

它要求你:

在任何包含 Apache 软件的重新分发中包含许可证的副本;

为包含 Apache 软件的任何分发提供清晰的归属于 Apache 软件基金会。

它不要求你:

在任何包含 Apache 软件的重新分发中,包括 Apache 软件本身的源代码,或者您对其进行的任何修改;

提交您对软件所做的更改给 Apache 软件基金会(尽管鼓励这样的反馈)。

基于这些参数,我们可以继续使用 PhoneGap 创建开源软件,只要我们在每次重新分发时包含许可证的副本,同时清晰地归属于 Apache 软件基金会。如果您有任何与 PhoneGap 许可证或 Apache 2.0 许可证相关的其他问题,可以在上述链接和 PhoneGap 许可证页面(phonegap.com/about/license/)上找到更多信息。

配置我们的项目

我们的项目可以配置以满足我们的需求,同时也满足我们的用户需求。这个过程非常简单,并且在 PhoneGap API 文档网站(docs.phonegap.com/en/2.5.0/guide_project-settings_index.md.html#Project%20Settings)上有很好的文档。大多数这些设置都可以通过我们项目目录/cordovar250/HelloWorld/HelloWorld/config.xml中的config.xml文件轻松修改。

以下是可以定制的当前列表:

首选项 描述
UIWebViewBounce(布尔值,默认为true 这设置了橡皮筋类型的交互/弹跳动画的属性。
TopActivityIndicator(字符串,默认为gray 这设置了状态/电池栏中旋转的指示器的颜色,有效值为whiteLargewhitegray
EnableLocation (布尔值,默认为false) 这确定是否在启动时初始化地理位置插件,使您的位置在启动时更准确。
EnableViewportScale (布尔值,默认为false) 这启用/禁用视口缩放。
AutoHideSplashScreen (布尔值,默认为true) 这控制着是否通过 JavaScript API 隐藏启动画面。
FadeSplashScreen (布尔值,默认为true) 这使启动画面淡入淡出。
FadeSplashScreenDuration (浮点数,默认为2) 这表示启动画面的淡入淡出持续时间(以秒为单位)。
ShowSplashScreenSpinner (布尔值,默认为true) 这显示或隐藏启动画面的加载旋转器。
MediaPlaybackRequiresUserAction (布尔值,默认为false) 这允许 HTML5 自动播放。
AllowInlineMediaPlayback (布尔值,默认为false) 这控制内联 HTML5 媒体播放。HTML 文档中的video元素还必须包括webkit-playsinline属性。
BackupWebStorage (字符串,默认为cloud) 如果设置为cloud,存储数据将备份到 iCloud。如果设置为local,只会进行本地备份。如果设置为none,则不会发生任何备份。
KeyboardDisplayRequiresUserAction (布尔值,默认为true) 如果设置为false,当通过 JavaScript 的focus()调用form元素时,键盘将打开。
SuppressesIncrementalRendering (布尔值,默认为false) 这允许在渲染之前接收内容。

转移网络应用

此时,我们已经使用 PhoneGap 和 Xcode 创建了一个名为HelloWorld的示例应用程序。现在,我们将通过回顾从第七章单页应用程序转移我们的单页应用程序。在本节中,我们将介绍如何转移我们的资产,包括我们的标记、样式和脚本,然后学习如何调试我们的应用程序。最后,我们将通过使用 PhoneGap 允许我们利用已经编写的代码来扩展我们的单页应用程序,使用本机功能来扩展我们的单页应用程序。

转移我们的资产

让我们开始转移我们的资产。本节将简要介绍如何以最小的努力转移我们所写的内容。这里的目标基本上是拥有与本地运行的相同应用程序。我们暂时不会使用 PhoneGap 的内置功能,但我们将很快拥有一个正在运行的应用程序。

包括我们的标记

我们要做的第一件事是打开之前使用 PhoneGap 生成的 Xcode 项目。为此,我们首先在 Finder 中找到我们的项目,在我的情况下是~/Sites/HTML5-iPhone-Web-App/cordova250/HelloWorld/。一旦找到我们的项目,双击HelloWorld.xcodeproj文件;这将在 Xcode 中启动项目。

一旦 Xcode 启动了我们的项目,我们将看到它索引我们的文件。在索引过程中,它不会阻止您与项目进行交互,因此您可以开始编辑文件。因此,让我们继续查看位于www目录中的index.html文件。

包括我们的标记

我们项目的初步 HelloWorld 标记

正如您所看到的,我们已经为我们设置了一个基本模板。让我们运行这个HelloWorld标记,看看结果。您应该首先看到的是一个带有默认 PhoneGap 图像的启动画面,紧接着是设备准备好的介绍。以下是显示结果的屏幕截图:

包括我们的标记

启动画面和设备准备好画面

现在我们知道我们的应用程序正在使用默认的标记和样式运行,我们应该能够相当快地移动。因此,首要任务是从第七章 单页应用中看到的单页应用程序屏幕中带有完成标记的导入。我们不会在这里回顾为该章节编写的代码,但这是模板:

<!DOCTYPE html>
<html class="no-js">
<head>
    <meta charset="utf-8">
    <title></title>

    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="format-detection" content="telephone=no" />
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />

    <link rel="stylesheet" href="css/normalize.css">
    <link rel="stylesheet" href="css/main.css">
    <link rel="stylesheet" href="css/singlepage.css">
    <script src="img/modernizr-2.6.1.min.js"></script>
</head>
    <body>
        <div class="app">
            <div id="deviceready" class="blink">
                <p class="event listening">Connecting to Device</p>
                <div class="event received site-wrapper">
                    <header>
                        <hgroup>
                            <h1>iPhone Web Application Development</h1>
                            <h2>Single Page Applications</h2>
                        </hgroup>
                    </header>
                    <div class="content"></div>
                    <footer>
                        <p>iPhone Web Application Development &copy; 2013</p>
                    </footer>
                </div>
            </div>
        </div>
    </body>
</html>

请记住,我们已经进行了一些修改,以适应这个目录结构。例如,我们不再使用../css/somefile.css来引用我们的 CSS 文件,而是使用css/somefile.css,其他资产也是如此。您还会注意到,前面的代码模板不包括我们从第七章 单页应用中创建的模板;这是为了使前面的模板在如何导入资产到您自己的 PhoneGap 项目方面保持简短和简单。

在这一点上,我们不会测试我们的应用程序,因为我们还没有导入我们的资产,包括样式和脚本,但我们现在应该还不错。我们想要在这里得到的是,导入现有的静态 Web 应用程序就像复制和粘贴一样简单,但不要被这个愚弄;大多数应用程序并不像这样简单,这个例子只是为了演示开始的简单。现在让我们继续导入我们的样式。

整合我们的样式

我们现在在我们的项目index.html文件中设置了标记。这很容易;这部分也将很容易。我们需要做的就是包含用于此项目的 CSS 文件。为了简化,我只是将我们以前的所有样式表都包含到 Xcode 项目的 CSS 目录中。您的项目现在应该是这样的:

整合我们的样式

我们导入的样式表

现在我们已经将我们的样式表导入到 Xcode 项目中,我们已经完成了一半。在这一点上,我们需要导入我们的脚本。同样,在这里不要测试您的应用程序,因为它可能不起作用。这最后一点将使我们达到我们需要的地方,所以让我们开始导入我们的脚本。

插入我们的脚本

好的,我们已经导入了我们的标记和样式表,这很棒。但还有最后一部分,我们的 JavaScript。这最后一部分对于使我们的应用程序运行至关重要。因此,让我们开始做与我们的样式相同的事情;只需将所有脚本导入 Xcode 项目的js目录中。当您这样做时,结果将如下所示:

插入我们的脚本

我们导入的脚本

我们的脚本在 Xcode 项目中。但我们仍然需要进行一些配置,包括在index.html文件中正确引用我们的脚本,并确保我们的应用程序将按预期启动。让我们首先在index.html文件中正确引用我们的脚本。

还记得两节前我们转移过的标记,展示了一个默认模板吗?我们要退一步再次看看那个模板,除了我们只会看标记底部在body标签关闭之前。这是我们的应用程序以前包含 JavaScript 的地方;所以这里没有什么新的,我们只是想确保文件被正确引用。只需确保在您的index.html文件中,您的脚本看起来像这样:

        <!-- BEGIN: LIBRARIES / UTILITIES-->
        <script src="img/cordova-2.5.0.js"></script>
        <script src="img/zepto.min.js"></script>
        <script src="img/underscore-1.4.3.js"></script>
        <script src="img/backbone-0.9.10.js"></script>
        <script src="img/helper.js"></script>
        <!-- END: LIBRARIES / UTILITIES-->
        <!-- BEGIN: FRAMEWORK -->
        <script src="img/App.js"></script>
        <script src="img/App.Nav.js"></script>
        <script src="img/BaseView.js"></script>
        <!-- END: FRAMEWORK -->
        <!-- BEGIN: MUSIC PLAYLIST APPLICATION -->
        <script src="img/Music.js"></script>
        <script src="img/SongModel.js"></script>
        <script src="img/SongCollection.js"></script>
        <script src="img/SongView.js"></script>
        <script src="img/PlayListView.js"></script>
        <script src="img/AudioPlayerView.js"></script>
        <!-- END: MUSIC PLAYLIST APPLICATION -->
        <!-- BEGIN: USER APPLICATION -->
        <script src="img/User.js"></script>
        <script src="img/UserModel.js"></script>
        <script src="img/DashboardView.js"></script>
        <script src="img/ProfileView.js"></script>
  <!-- END: USER APPLICATION -->
        <script src="img/main.js"></script>
        <!-- END: BACKBONE APPLICATION -->
    </body>
</html>

注意这里发生的一些事情。首先,我们在最顶部包含了 PhoneGap 提供的cordova库;当我们尝试检测deviceready事件时,这将是至关重要的。接下来,我们将所有 JavaScript 源文件引用到 Xcode 项目中的js目录,而不是../js。现在,我们需要做的最后一件事是确保我们的代码在设备准备就绪时运行,这意味着我们需要修改我们的单页应用程序的启动方式。

为了确保我们的应用程序启动,我们需要监听 PhoneGap 事件提供的deviceready事件(docs.phonegap.com/en/2.5.0/cordova_events_events.md.html#deviceready)。一旦 Cordova 完全加载,就会触发此事件。这是至关重要的,因为在本地代码加载时 DOM 没有加载,并且启动画面被显示。因此,当 DOM 加载之前需要 Cordova 函数时,我们可能会遇到问题。因此,为了我们的目的,我们将监听deviceready事件,然后启动我们的应用程序。可以使用以下代码完成:

<script>
    (function(){
     document.addEventListener('deviceready', onDeviceReady, false);

     function onDeviceReady(){
        console.log("onDeviceReady");
        var parentElement,
            listeningElement,
            receivedElement;

        parentElement = document.getElementById('deviceready');
        listeningElement = parentElement.querySelector('.listening');
        receivedElement = parentElement.querySelector('.received');

        listeningElement.setAttribute('style', 'display:none;');
        receivedElement.setAttribute('style', 'display:block;');

        // Start our application
        Backbone.history.start();
     }
    }());
</script>

让我们逐行检查这段代码。首先,我们创建一个立即执行的闭包。在这个范围内,我们监听deviceready事件并分配onDeviceReady回调函数。然后,我们定义了onDeviceReady回调,显示和隐藏我们的应用程序。这个方法创建了三个变量,parentElementlisteningElementreceivedElement。我们缓存了deviceready DOM 元素并将其分配给parentElement,我们对listeningElementreceivedElement也做了同样的事情。接下来,我们在适当的元素上设置style属性,显示应用程序并隐藏启动画面。最后,我们启动基于 Backbone 的单页应用程序。

让我们将前面的脚本放在index.html文件中所有脚本之后。现在,我们应该能够成功运行我们的应用程序并导航到仪表板、个人资料和播放列表视图。如果之前讨论的一切都正确地完成了,您应该能够像这样本地使用您的单页应用程序:

插入我们的脚本

本地单页应用程序

注意

请注意,在前面的屏幕截图中,我们有一个联系人导航项。这尚未构建,将成为本章最后一部分的一部分。

到目前为止,我们已经创建了一个本地应用程序,展示了使用 PhoneGap 转移当前 Web 应用程序的简单性。是的,我们没有充分利用 PhoneGap 或 Xcode,但我们现在明白了启动流程是相当容易的。我们将暂时绕过来理解调试我们的应用程序,并最终使用 PhoneGap 的 API 构建本地组件到我们的应用程序中。

调试我们的应用程序

调试应用程序对于任何工作流程或流程都是至关重要的;因此,我们需要知道调试基于 Web 技术构建的本地应用程序是什么样的。这并不像你想象的那样复杂或容易。但它仍然是可行的,并且在撰写本文时,这是调试应用程序的最佳方式之一。所以让我们开始吧。

记录我们的代码

我们都熟悉通过 JavaScript 可用的控制台对象。这对我们仍然可用,但在创建本地应用程序时,尝试找到日志输出的位置时会有些困惑。传统上,我们在模拟器或实际设备上有一个可用于调试错误的控制台工具;然而,现在不再是这样。

首先,让我们看看 Xcode 中的日志记录是如何进行的。还记得本章前面讨论过的调试视图吗?好吧,这就是我们想要使用它的地方。所以首先,让我们启用调试视图。现在,让我们运行我们目前拥有的应用程序。

当我们运行您的应用程序时,我们应该在调试器区域看到以下内容:

2013-03-16 14:24:43.732 HelloWorld[2322:c07] Multi-tasking -> Device: YES, App: YES
2013-03-16 14:24:44.624 HelloWorld[2322:c07] Resetting plugins due to page load.
2013-03-16 14:24:45.196 HelloWorld[2322:c07] Finished load of: file:///Users/acresp/Library/Application%20Support/iPhone%20Simulator/6.0/Applications/DEEABC2E-C2D6-40F3-A19E-43E4F7F5EB47/HelloWorld.app/www/index.html
2013-03-16 14:24:45.243 HelloWorld[2322:c07] [LOG] onDeviceReady

我们应该关注最后一行,即[LOG]发生的地方。这是使用console.log()生成的输出,目前在我们的onDeviceReady回调中。这对我们来说很好,因为我们可以积极地看到我们创建的日志。这样做的负面影响是,我们没有在其他浏览器中找到的典型开发人员工具。但是最近的发展使我们现在可以使用 Safari 内置的开发人员工具来调试在模拟器中运行的 iOS 应用程序。

使用 Safari 开发人员工具

正如我之前提到的,我们现在能够使用 Safari 的开发者工具调试基于 PhoneGap 构建的 Web 应用程序。所以让我们快速尝试一下,打开我们电脑上的 Safari。如果您还没有启用开发者工具,请进入 Safari 的偏好设置,并在高级选项卡下选择在菜单栏中显示开发菜单复选框。

使用 Safari 开发者工具

Safari 偏好设置的高级选项卡

一旦我们启用了开发者工具,我们可以从 Safari 的开发菜单中访问它们。如果我们的应用程序在 iOS 模拟器中运行,那么我们应该能够通过从 iPhone 模拟器子菜单中选择index.html来调试我们的应用程序。然后这将在 Safari 中启动本机开发者工具。

使用 Safari 开发者工具

调试环境

现在我们能够使用 Safari 开发者工具完全调试本机应用程序。拥有一个完全集成的开发环境,模拟和调试都是这个过程的一部分,这真的很容易。虽然我们可以进一步详细讨论调试,但这超出了本书的范围。然而,让我们继续本书的最后一部分,我们将学习如何利用 PhoneGap 的内置 API 来连接到我们单页应用程序的本机功能。

扩展我们的应用程序与本机功能

恭喜!我们已经能够使用我们已经创建的 HTML5、CSS 和 JavaScript 创建我们的第一个本机应用程序。这是令人兴奋的事情,但我们还没有完成。现在让我们利用 PhoneGap 的 API 之一来利用本机功能。

从更高的层次上,我们希望我们的应用程序显示我们手机上的联系人。当我们点击应用程序导航中的联系人按钮时,我们希望能够访问这些信息。在这个例子中,我们只想显示我们联系人的全名。为了实现这些目标,我们将使用 PhoneGap 的 Contacts API (docs.phonegap.com/en/2.5.0/cordova_contacts_contacts.md.html#Contacts)。为此,我们将确保在我们的应用程序中进行了配置,然后编写适当的代码来处理这个问题,已经存在的应用程序框架中。让我们从配置开始。

配置我们的应用程序

我们已经在之前讨论了配置我们的应用程序的基础知识,但让我们再次看一下以确保完全理解。首先,让我们打开位于项目顶部的config.xml文件。然后通过将其值设置为CDVContacts来启用 Contacts API。完成后,您的config.xml文件应包含以下内容:

配置我们的应用程序

项目配置

设置我们的联系人功能

在本章的这一部分,我们将看看如何连接到我们的联系人信息以在我们的本机应用程序中显示。首先我们将创建视图,然后模板,最后是随 PhoneGap 提供的实际 API。完成后,我们应该对如何利用这些 API 来为 iOS 创建本机 Web 应用程序有一个很好的想法。

创建 ContactsView 类

一旦我们为这个应用程序设置了配置,我们需要设置其他一切以使联系人视图正常工作。首先,让我们创建一个联系人视图,添加到我们的用户目录中。我们稍后会扩展其功能,但现在这是我们将使用的模板:

(function(window, document, $, Backbone, _){

  var ContactsView = App.BaseView.extend({
    'template': _.template($('#tmpl-user-contacts').html()),

    'initialize': function() {

      this.render();
    },

    'render': function() {

      return this;
    }
  });

  window.User.ContactsView = ContactsView;

}(window, document, Zepto, Backbone, _));

上述代码并没有什么新东西。我们基本上正在创建一个遵循我们之前设置的约定的ContactsView类,没有任何额外的功能。请注意,我们已经为此视图设置了一个尚不存在的模板。让我们在index.html中包含此文件,并将其添加到最后一个被包含的视图中。现在,让我们创建与此视图相关联的模板。

实现 ContactsView 模板

使用我们已经为播放列表构建的内容,我们将简单地复制播放列表视图的模板并更改其标题。与此同时,我们还将将无序列表的类更改为contacts-list。完成后,我们的模板将如下所示:

<script type="tmpl/User" id="tmpl-user-contacts">
    <section class="view-contacts">
    <header>
    <h1><%= name + "'s" %> Contacts</h1>
    <% print(_.template($('#tmpl-user-nav').html(), {})); %>
    </header>
    <ul class="contacts-list"></ul>
    </section>
</script>

在我们创建的其他模板之后包含此模板。此时,我们应该已经完成了 50%。现在,您可能会遇到一些样式问题,但请确保将contacts-list类添加到与播放列表使用的相同样式中。我们不会在这里详细介绍,因为这相当简短;因此,我们将继续编写联系人实现。

集成联系人 API

查找用户的联系人使用 PhoneGap API 非常简单。实际上,我们的示例将基于文档中的Navigator对象contacts。但首先,我们需要创建一个ContactFindOptions的新实例(docs.phonegap.com/en/2.5.0/cordova_contacts_contacts.md.html#ContactFindOptions),它将在查找联系人时保存我们的过滤选项。

'initialize': function() {

  // Filter options
  this.contactOptions = new ContactFindOptions();
  this.contactOptions.filter = "";
  this.contactOptions.multiple = true;

  this.render();
},

上述代码在ContactFindOptions的实例上设置了filtermultiple属性。默认情况下,filter为空,表示没有限制,multiple设置为true,允许多个联系人通过。接下来,当我们获取联系人时,我们希望找到两个字段,它们的displayNamename。这些字段将在一个数组中,我们很快会用到。

'initialize': function() {

  // Filter options
  this.contactOptions = new ContactFindOptions();
  this.contactOptions.filter = "";
  this.contactOptions.multiple = true;

  // Fileds we want back from query
  this.contactFields = ['displayName', 'name'];

  this.render();
},

接下来,我们希望在视图渲染时找到联系人。因此,在我们的渲染视图中,我们希望传入前面的选项。

'render': function() {
    // Find user contacts
    navigator.contacts.find(this.contactFields, this.onContactsSuccess, this.onContactsError, this.contactOptions);

    this.$template = $(this.template(this.model.attributes));

    this.$el.prepend(this.$template);
  }

  return this;
},

请注意,我们尚未创建我们的onContactsErroronContactsSuccess方法。此外,您将看到我们创建模板并将其附加到 DOM 的方式与我们为所有其他视图所做的方式相同。这个方法没有太多要做的事情,所以让我们看看我们的回调,从onContactSuccess开始。

onContactSuccess回调是我们所有魔法发生的地方。我们将在内存中创建一个div元素,然后循环遍历结果,将每个元素作为列表项添加到div中。一旦完成所有操作,我们将获取div元素的内容并将其添加到我们的contacts-list无序列表中。

'onContactsSuccess': function(contacts) {
  console.log('onContactsSuccess');
  // Temporary Div
  var $div = $('<div />');
  if (contacts.length !== 0) {
    console.log('contacts length greater than 0');
    _.each(contacts, function(contact){
      console.log(contact.name);
      $div.append($('<li>' + contact.name.formatted + '</li>'));
    });
  } else {
    alert("No contacts found!");
  }

  $('.contacts-list').append($div.html());
},

正如您在这里看到的,我们使用underscore方法each来循环遍历结果。正如我们之前提到的,我们创建一个包含用户姓名的列表项作为其文本内容。这里的行为非常简单,没有太复杂的东西。现在,让我们来看看我们的onContactsError回调:

'onContactsError': function(contactsError) {
  alert('onContactsError!');
}

在这个回调中,我们只是警告发生了错误。当然,在我们的真实应用程序中,我们会创建更全面的内容,但对于我们的目的来说,这已经足够了。如果我们现在运行我们的应用程序,我们应该会得到以下结果:

集成联系人 API

联系人 API 实现

给自己一个鼓励!您已经到达本节的末尾,现在已成功集成了 PhoneGap API,并利用了本地功能。非常酷,不是吗?

注意

请注意,本书提供的源代码进行了一些检查,确保用户每次访问联系人视图时不会添加相同的联系人。这样做是为了节省一些时间,真正专注于解决方案的核心。

摘要

在本章中,我们介绍了使用与我们用于 Web 应用程序相同的编程语言进行本机应用程序开发。使用流行的开源 PhoneGap 框架,我们实现了创建单页面应用程序的能力,在第七章中构建的单页应用程序,作为 iOS 的本机应用程序。我们通过使用 PhoneGap 中的联系人 API 来扩展单页面应用程序,将其与本机功能联系起来,列出我们的联系人和一些信息。现在我们应该有一个创建本机应用程序的基础,使我们能够使用 Web 技术来分发 iOS Safari 和 iOS 操作系统的 Web 应用程序。

posted @ 2024-05-24 11:06  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报