创建-jQueryMobile-移动应用-全-

创建 jQueryMobile 移动应用(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

我们能建造它吗?是的,我们能!

移动技术是存在的最快增长的技术领域。这是一股改变的浪潮,颠覆了所有分析师的预期。你可以选择驾驭这股浪潮,也可以选择被淹没。在 使用 jQuery Mobile 创建移动应用 中,我们将带领您完成一系列逐渐复杂的项目,涉及各种行业。与此同时,我们将解决一些移动可用性和体验问题,这些问题对所有移动实现都是共通的,不仅仅是 jQuery Mobile。

到最后,你将拥有使用 jQuery Mobile 和许多其他技术和技巧创建真正独特产品所需的所有技能。这将是有趣的。它将是具有挑战性的,到最后,你将引用《建筑工人鲍勃》中的话:“我们能建造它吗?是的,我们能!”

本书内容

第一章, 使用 jQuery Mobile 原型设计,在开始编码之前利用快速原型设计的力量。与客户更快、更好、更共享地达成共识。

第二章, 一个小型移动网站,实现了第一章中的原型。设计独特,开始建立基本的服务器端模板。

第三章, 分析、长表单和前端验证,将 第二章 的随意实现与 Google Analytics、jQuery Validate 框架以及处理长表单的技术相结合。

第四章, QR 码、地理定位、Google 地图 API 和 HTML5 视频,将让您为一个电影院连锁店实现一个网站。

第五章, 客户端模板化、JSON API 和 HTML5 Web 存储,创建了一个社交新闻中心,利用 Twitter、Flickr 和 Google Feeds API 的 API 功能。

第六章, HTML5 音频,利用 HTML5 音频和渐进增强,将一个非常基本的网页音频播放器页面转变为音乐艺术家的展示页面。

第七章, 完全响应式摄影,探讨了使用 jQuery Mobile 作为移动优先的、响应式网页设计 (RWD) 平台。我们还简要介绍了排版与 RWD 的关系。

第八章, 将 jQuery Mobile 集成到现有网站中,探讨了为想要将其页面移动化但没有内容管理系统 (CMS) 的客户构建 jQuery Mobile 网站的方法。我们还深入探讨了包括客户端、服务器端以及两者结合在内的移动检测方法。

第九章,内容管理系统和 jQM,教我们如何将 jQM 集成到 WordPress 和 Drupal 中。

第十章,将一切放在一起 - Flood.FM,借鉴了前几章的知识,并进行了一些增加,考虑使用 PhoneGap Build 进行编译。

你需要为这本书做好什么准备

你真的只需要一些东西来读这本书。

  • 文本编辑器

    你的代码只需要一个基本的文本编辑器;在 Windows 上 Notepad++ 非常好用。我真的很喜欢 Sublime Text 2。Eclipse 也可以,虽然有点笨重。Dreamweaver 也不错,但价格昂贵。其实没太大关系;你可以选择任何让你开心的文本编辑器。

  • 一个 Web 服务器

    你可以使用像 HostGator、Godaddy、1&1 等托管解决方案,或者在本地使用像 XAMPP、WAMP、MAMP 或 LAMP 这样的东西来进行所有的测试。

  • JavaScript 库

    在章节中,我们会介绍一些 JS 库。在每种情况下,我会告诉你它们是什么,以及在哪里找到它们。

  • 开发者的幽默感

    我们都想到了,我们都说了。你会在这里找到一两个怒斥。根据它们的价值去看待它们,但不要太认真。

本书适合的读者群体

如果你已经相当擅长 web 开发(HTML、CSS、JavaScript 和 jQuery),那对我来说已经足够了。你可以在本书中学习并掌握 jQM,我想你会没问题的。

我们将覆盖的内容

  • 构思和原型制作技术

  • 集成自定义字体和图标集

  • 使用 jQuery Validate 集成客户端表单验证

  • Google Analytics、Maps 和 Feeds API

  • 地理位置

  • 嵌入 HTML5 视频和音频

  • 使用客户端模板和 JSON

  • 消化 RSS 订阅

  • 集成 PhotoSwipe

  • 媒体查询

  • 移动检测技术

  • 与 Wordpress 和 Drupal 集成

  • 与现有网站集成

为什么选择 jQuery Mobile

在移动领域,国王的崛起和衰落如此之快,几乎不可能预测谁会取胜。只需问问 RIM(黑莓设备制造商),他们从完全统治下降到了世界市场份额的 6%。在这种变化的程度和速度下,你怎么能知道你选择的平台是否适合你的项目?

  • 一个保险的选择

    核心 jQuery 库被应用在超过 57% 的现存网站上,增长率没有显示出减缓的迹象 (trends.builtwith.com/javascript/jQuery)。它是目前为止,在开源 JavaScript 库中最值得信赖的名称。现在他们已经加入到移动领域,你可以打赌 jQuery Mobile 是一个相当安全的选择,可以用最小的努力达到最多的人。

    还值得注意的是,你可能会在一段时间后放弃大部分项目。使用 jQM 将增加后来者已经具备继续你工作的技能集的可能性。

  • 最广泛的设备支持

    jQuery Mobile 具有最广泛的设备支持范围。通过对渐进增强PE)的出色遵循,这一直是他们使命的一部分。当电梯坏了,它并不会变得完全无用。它只是变成了楼梯。同样,对于那些拥有智能手机的人,jQuery Mobile 为他们提供了一些非常棒的功能。但其他人呢?他们将看到一个没有所有花哨功能的标准网页。在一天结束时,一个精心制作的 jQM 页面可以适用于所有人。

  • 首先是移动端,但不仅限于移动端

    jQM 从头开始就是为移动端设计的,但通过一些合理使用响应式网页设计RWD),一个 jQM 项目可以服务于移动设备、平板甚至桌面电脑。

  • 声明式,而非程序式

    在 jQM 中,大部分想要做的事情都可以在不写一行代码的情况下完成。这使得它成为即使是最新的新手也能够涉足移动领域并入门的理想工具。即使是没有真正编程经验的设计师也能轻松将他们的构想转化为具有外观的工作原型。对于我们这些会编程的人来说,这意味着我们需要做的编码要少得多,这总是件好事。jQM 完美地符合 jQuery 核心的座右铭:“写得少,做得多。”

  • jQM 与其他框架比较

    如果你想使用移动框架,有很多选择供你考虑。查看 www.markus-falk.com/mobile-frameworks-comparison-chart/ 来比较所有选项的工具。底线是:如果你想要支持所有人并且轻松实现,jQuery Mobile 是框架的正确选择。

  • jQM 与响应式网页设计比较

    最近关于 RWD 的讨论很多。我全力支持。一个统一的网站是每个开发者的梦想。然而,这通常要求网站从头开始就以 RWD 为基础构建。这也意味着网站的每一页都值得为移动受众提供服务。如果你有这样的增长机会,好好享受吧。

    令人沮丧的事实是,我们大多数人没有奢侈的条件从头开始建立一个全新的网站,也没有时间和三倍的预算来做好工作。而且,如果我们很诚实的话...很多网站有很多无用的页面,这些页面在移动网络中并没有存在的必要。你知道的。我知道的。一个完全符合用户需求和环境的定制解决方案通常是更好的选择。

  • jQM 与自行开发比较

    你当然可以选择从头开始创建自己的手机网站,但那就相当于用斧头砍树,然后用木板建造自己的房子。使用预制组件来制作你的杰作并不会使你不是一位手艺人。移动框架的存在是有原因的,它们所需的开发时间和跨设备测试将为你节省更多的时间和头痛。

    值得一提的是,Kasina 报告中突出的三大行业领导者中,资产管理人和保险公司的移动领导力www.kasina.com/Page.asp?ID=1415)有两家都是使用了 jQuery Mobile。富兰克林坦普顿、美国世纪投资和万得理财被强调。前两者是使用 jQM 实现的。

    全面披露:我曾是美国世纪投资公司的手机网站团队成员,所以我对这份报告感到相当自豪。

渐进增强和优雅降级

抵抗是徒劳的。这种情况将发生在你身上。每年都会在 Black Hat 大会(www.blackhat.com/)上宣布新的漏洞利用。就像钟表一样,公司会关闭 JavaScript 直到提供补丁。你的移动受众中会有一个或多个受到影响。

虽然这种情况几乎和早期版的 Internet Explorer 一样恼人,但由于 jQuery Mobile 对渐进增强的精湛运用,它可以帮助。如果你按照框架的设计编码你的页面,那么你将不必害怕失去 JavaScript。网站仍然可以工作。可能没有那么漂亮,但对于从最智能的智能手机到最愚蠢的“傻手机”的所有人,它都能正常运行。

作为我们的责任(尽管可能让人不快),我们需要关闭 JavaScript 测试我们的产品,以确保人们始终可以访问。关掉手机的设置只需一会儿,看看会发生什么并不难。通常来说,很容易修复出问题的部分。

都说了这么多,但在本书中,我们将无情地打破这个规则,因为我们要超越框架的基础知识。在可能的情况下,我们将努力牢记这个原则并提供替代方案,但有些我们要尝试的东西如果没有 JavaScript 就做不到。欢迎来到 21 世纪!

辅助功能

智能手机是残障人士的优秀工具。jQuery Mobile 团队已尽一切努力支持 W3C 的 WAI-ARIA 辅助功能标准。至少你应该使用手机的语音助手技术测试你的成品。你会震惊于你的网站在多大程度上可以表现出色。你的需要帮助的客户将会感到高兴。

惯例

在本书中,您将找到几种不同信息类型的文本样式。以下是这些样式的一些例子,并解释它们的含义。

文本中的代码单词显示如下:"要使用清单文件,您的网络服务器或.htaccess必须配置为返回text/cache-manifest类型。"

代码块设置如下:

<link rel="apple-touch-icon-precomposed" sizes="144x144" href="images/album144.png">     
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="images/album114.png">     
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="images/album72.png">     
<link rel="apple-touch-icon-precomposed" href="images/album57.png"> 

新术语重要单词以粗体显示。在屏幕上看到的单词,例如菜单或对话框中的单词,会在文本中以这种方式出现:"从那里,您可以下载最新的WURFL API 包并解压缩它。"

注意

警告或重要提示显示在这种框中。

提示

提示和技巧以这种方式出现。

第一章:jQuery Mobile 的原型设计

2011 年 11 月 22 日,我在RoughlyBrilliant.com上开始了我的博客,以分享我对 jQuery Mobile 和移动用户体验(UX)的一切了解。我完全不知道它会变成什么样子,会引起怎样的共鸣。由于这是一个面向开发者的博客,我对我提到的首先远离键盘,先草绘设计的评论能够引起最积极的回应感到有些惊讶。我坚信,开始你的 jQuery Mobile 项目的最佳方式是在一叠便利贴上。

这一章可能会感觉是最费力的,也感觉最陌生的。但最终,我相信这可能是让你成长最多的章节。开发者坐下来开始编码是很正常的,但是现在是时候超越这一点了。是时候远离键盘了!

在本章中,我们涵盖:

  • 移动领域的变化

  • 移动设备的使用模式

  • 纸上原型

  • 小型企业移动网站的关键组件

  • 绘制 jQuery Mobile UI

  • 其他原型设计方法

游戏已经改变了

不久之前,开发者可以制作出产品,无论它有多糟糕,人们都会使用。它通常会因其存在而取得一定程度的成功。现在,我们生活在一个竞争更加激烈的时代。现在,借助像 jQuery Mobile 这样的工具,任何人都可以在几小时内迅速制作出看起来令人印象深刻的移动网站。

那么,我们如何与竞争对手区分开来呢?我们当然可以竞争价格。人们喜欢物有所值。但有一件事似乎一直超越价格,那就是用户的体验。用户体验UX)是世界上大多数成功品牌的区别所在。

哪家电脑公司不仅保持盈利,而且绝对是成功的?苹果公司。这可能部分是因为他们的产品价格是应有之义的三倍。最终,我相信这是因为他们一直站在以用户为中心设计的前沿。

亚马逊通过帮助您快速找到所需的东西提供了很好的体验。他们为您的购买决策提供了很好的评价和建议。他们的一键购买功能非常方便,以至于他们实际上曾因此在法庭上进行了争斗,以保护它作为竞争的点(en.wikipedia.org/wiki/1-Click)。

谷歌本可以走雅虎、AOL、MSN 等许多其他公司的路线。他们本可以在首页上推广任何他们想要的内容。相反,他们几乎保持了他们开始时的干净。他们的名字、一个搜索框和出色的结果。最多,有些可爱的徽标渲染。他们给用户他们想要的,并且基本上保持低调。

这很难!我们喜欢认为我们如何制作程序或网页至关重要。我们喜欢认为,通过减少 10%的代码,我们正在做出重大改变。但你有试过向朋友解释你当前项目的细节,只看着他们的眼睛开始发直吗?除了我们之外没有人关心。他们只听到更快、更小、更容易、更简单等等。他们只关心那些直接影响他们生活和用户体验的事情。

作为开发人员,我们可以写出最优雅的代码,创建最高效的系统,在不到 1K 的 JavaScript 中完成小奇迹,但如果我们在可用性方面失败……我们将彻底失败。

移动使用模式

jQuery Mobile 并非一种灵丹妙药。它不会立即创造对我们产品的吸引力。如果我们未能意识到用户的环境和使用模式,技术和库也无法拯救我们。

想一下:你上次在手机上花超过三分钟连续的时间在一个不是游戏的网站或应用上是什么时候?我们都知道《愤怒的小鸟》可以有多吸引人,但除此之外,我们往往匆匆忙忙就离开了。移动使用的特点是短暂的高效活动。这是因为我们的智能手机是完美的时间回收设备。我们随时随地都可以拿出来利用可以节省的任何时间,包括:

  • 在家里(菜谱,发短信,无聊)

  • 在排队或候诊时(无聊)

  • 购物(女性:寻找优惠,男性:无聊)

  • 工作期间(会议,厕所-我们都做过)

  • 观看电视(每个广告间歇)

  • 通勤(乘坐公共交通或困在交通拥堵中)

我们可以很容易地从自己的日常生活中看到这种微爆发活动。这就是我们希望成功的产品所必须适应的环境。最重要的是,这将要求我们专注。当用户在排队等候时,他们来找我们做什么?他们在一个广告间歇内能完成什么任务?在他们的第二优先事项中,他们会认为什么任务是最重要的?

HTML 原型与绘制

不要从代码开始。作为一名开发人员,这真的很难说。jQuery Mobile 非常快速且易用。重构也很快速。然而,当你直接进行 HTML 原型设计时会发生一些事情。

不懂代码的人会认为我们距离完整的产品要比实际情况更接近。这在 jQuery Mobile 中尤其如此,因为即使是对项目最原始的尝试也会看起来经过精心打磨和完成。

人们会开始专注于像间距、边距、颜色、标志大小等细枝末节。

由于我们在当前设计中投入的时间成本,我们不太可能对最初编码的内容进行重大更改,因为重构比重做更容易。

相反,拿起笔和纸。等等,什么?这不是一本网页开发者的书吗?放松,你不必是一位艺术家。相信这个过程。后面会有很多机会来编码。现在,我们要画出我们的第一个 jQuery Mobile 站点。

用纸质的构思开始的伟大之处在于:

  • 我们更愿意简单地放弃一个不到 30 秒就可以创建的图纸。

  • 实际上,通过手工素描使用了大脑的不同部分,并且解锁了我们的创造中心。

  • 我们可以在创建一个 HTML 页面的时间内提出三种完全不同的设计

  • 即使不擅长平面设计或编码,每个人都可以贡献他们最好的想法

  • 我们自然会从首要的事情开始画起

  • 我们更多地关注能够使我们的网站正常运行的想法和流程,而不是无数的细节,很少有人会注意到

  • 我们最终可能会得到一个更加以用户为中心的设计,因为我们正在绘制我们实际想要的东西

理想情况下,3x5 英寸的便笺是完美的,因为我们可以轻松地将它们摆放在墙上或桌子上,以模拟网站结构或流程。我们甚至可以用它们进行可用性测试。稍后,我们将布置我们的绘图供业主参考,看整个流程如何工作。

让我们的手弄脏一些小生意

根据凯瑟琳·科比在 archive.sba.gov/advo/research/rs299tot.pdf 上所述:

“小企业继续在美国经济中发挥着重要作用。在 1998 年至 2004 年期间,小企业产生了一半的私人非农 GDP。”

www.msnbc.msn.com/id/16872553/上的一篇文章称:

“尽管大约有三分之二的小企业能够度过两年的时间,但根据劳工统计局的最新数据,只有 44%的企业能够度过四年的时间。”

即使在大企业的土地上,这对我们的手艺也是有利的;小企业的数量和变动如此之大。这意味着几乎无穷无尽的小商店在竞争。这就是我们介入的地方。

Nicky's Pizza 最近开业了。和许多其他企业一样,业主意识到他在开业之前应该有一个网站。他的朋友做了网站,而且实际上相当不错。只是还不是移动版。

披萨很棒,当我们坐在那里享受时,我们拿出笔,拿起一张餐巾纸。我们就要在这里,现在制作一个移动网站,赢得一些生意。让我们开始吧。

对于任何小型本地企业来说,都应该首先放在他们的移动网站上的是某些基本内容:

  • 位置

  • 联系信息

  • 提供的服务/商品

由于这是一家餐厅,服务将是菜单。他们还足够聪明地创建了一个 Facebook 页面。因此,我们将链接到那里并带来一些推荐。

由于我们正在绘制而不是使用工具,您可以选择尽可能详细。以下两个图示是绘制相同页面的两个示例。任何一个都能传达核心思想。

当与我们自己的团队合作时,第一个可能已经足够了,因为我们都知道 jQuery Mobile 能做什么。我们知道框架会填充哪些细节,可以绘制足够的细节来告诉彼此我们在想什么。然而,当为客户(或者你知道更注重视觉和细节的人)绘制时,最好多花几秒钟添加更精细的细节,如阴影、渐变色彩和特别是标志。企业所有者对他们的“宝贝”非常自豪,而你为其添加的努力将立即赋予你的绘图一点额外的重量感。

亲手动手小型企业

第一张图肯定足够好,可以拿起来,放在手里,假装它是一个智能手机屏幕。在第二张图中,我们可以看到实际绘制标志会产生多大的不同,以及添加较硬的边缘和阴影会给人一种深度感。稍微擦亮一下,效果就大不同。

有几种方法可以为您的绘画添加投影阴影。最艺术的方式是使用铅笔,但使用铅笔绘图的问题在于会导致污渍,并且会过分关注细节。这些图纸应该是粗略的。如果你稍微搞砸了,没关系。毕竟,你可能每张图只花了不到一分钟,这就是重点。目标是快速实现共享的视觉理解。

这里有四种不同的方式来绘制相同的按钮:铅笔、钢笔、Sharpie 和标记笔。我个人偏爱使用细尖的 Sharpie。

亲手动手小型企业

这里还有一些其他 jQuery Mobile 元素和绘制方法:

列表视图亲手动手小型企业 对话框亲手动手小型企业
导航栏亲手动手小型企业 按钮亲手动手小型企业
可折叠亲手动手小型企业 分组按钮亲手动手小型企业
输入亲手动手小型企业 搜索亲手动手小型企业
翻转开关亲手动手小型企业 滑块亲手动手小型企业
复选框集亲手动手小型企业 单选按钮集亲手动手小型企业
选择菜单忙碌的小企业 多选忙碌的小企业
分割列表视图忙碌的小企业 气泡计数列表视图忙碌的小企业

网站的其余部分

地图定位按钮将引导用户到这个页面,在这里我们将列出地址并显示静态谷歌地图。点击地址或地图上的任何一个都将链接到完整的谷歌地图位置。

在 Android 和 iOS 5 系统上,链接到谷歌地图会导致本机系统在本机界面上打开指定位置,从而实现逐步导航。iOS 6 中发生了变化,但我们以后会讨论这个问题。

作为额外的奖励,以防用户不想去实际位置,让我们在标有电话订餐按钮上添加一个电话链接。

注意线条的不同粗细。还有一点颜色和我们典型的投影效果。添加这些小细节并不特别困难,但可以产生很大的影响。

网站的其余部分

整个网站上的所有呼叫按钮都将启动本机呼叫界面。下一张图是 iOS 版本的呼叫对话框。Android 版本基本相似。

注意背景按钮上闪亮的线条,表明它被点击了。还要注意,我们如何将背景(铅笔作品)遮蔽,以表明它的模态状态。

网站的其余部分

现在,让我们考虑菜单以及将作为全局标头的内容。您放入全局标头的前两个链接将转换为按钮。有一个设置可以在当前主页按钮位置自动插入返回按钮。只需将data-add-back-btn="true"添加到 jQuery Mobile 页面中即可。不过,我通常不会使用这个功能。我协助进行的可用性测试表明,大多数人只是按下他们设备的原生返回按钮。因此,让我们将第一个链接设为主页,第二个链接设为呼叫

网站的其余部分

这里我们看到沙拉的详细视图。它基本上和以前一样,但我们在列表视图中进行了一些格式化。我们将在下一章中看到实际的代码。

网站的其余部分

当然,我们可以使用白板和标记笔来完成所有这些工作。我们可以协作地在白板上画出我们的想法,并使用我们打算针对的智能手机拍摄快照。我的建议是使用我们忠实的便利贴,简单地贴在白板上,使用标记笔来指示屏幕流程。下图显示了我在规划项目后的白板情况:

网站的其余部分

如果我们需要重新映射我们的应用流程,我们所要做的就是重新排列笔记并重新绘制我们的线条。这比在白板上再将一切都重新绘制一遍要少得多。

需求

考虑到我们到目前为止所做的事情。考虑到我们绘制的屏幕以及业主能够查看并签字确认这就是他想要的东西,还有多少问题需要问?我们真的需要一个列出需求或一个 30 页的功能设计规格(FDS)文档来准确告诉你一切应该是什么样子并且应该做什么吗?这样就够了吗?真的需要用 Photoshop 做吗,然后做成幻灯片展示吗?

还要考虑到到目前为止我们所做的事情总共花了五张便签纸、一个马克笔、一支铅笔和 20 分钟。我相信在大多数情况下,这就是你所需要的,你自己就可以做到。

替代纸上原型

如果纸上原型的速度和简洁还不足以说服你远离键盘,那么考虑另外两种快速原型设计的选项:

我个人推荐 Balsamiq Mockups。它产生的原型具有统一但手绘的外观。这将达到与纸上原型相同的效果,但输出更一致,更容易在分布式团队之间进行协作。这两种工具都可以产生完全交互式的模型,用户实际上可以通过原型点击。最终,纸上原型仍然更快,任何人都可以贡献。

摘要

对于我们中的一些人来说,从未将纸上原型视为一门严肃的学科,这一开始可能会感到非常奇怪。有幸的是,这里学到的经验扩展了你的思维,并给了你对打造良好用户体验的新热情。如果你想深入探讨构思技术,我最推荐的一本书是 Gamestorming,作者是 Dave Gary (www.goodreads.com/book/show/9364936-gamestorming)。

现在,你应该能够有效地为你的同事和客户勾勒出一个 jQuery Mobile 接口。在下一章中,我们将把这里绘制的内容翻译成一个真正的 jQuery Mobile 实现,超越了普通的 jQuery Mobile 外观和感觉。只要记住,用户体验和可用性是首要的。追求快速、集中的直觉式生产力。

第二章:一家小型移动网站

前一章教会了我们一些有关纸质原型的宝贵经验,并为我们开始开发奠定了坚实的基础。现在,我们将把这些图纸变成一个真正的 jQuery Mobile (jQM) 网站,它具有响应式的功能并且看起来独特。

在本章中,我们涵盖:

  • 一个新的 jQuery Mobile 样板

  • 对完整网站链接的一种新思考

  • 将样板分解为可配置的服务器端 PHP 模板

  • 使用备用图标集

  • 自定义字体

  • 仅使用 CSS 实现页面翻页效果

  • 性能优化技巧

  • 移动设备检测和重定向技术

一个新的 jQuery Mobile 样板

jQuery Mobile 文档中有很多隐藏的宝藏。它们是一个很好的起点,但实际上有几种方法可以创建你的基础模板。有单页面模板、多页面模板、带有全局配置的模板以及动态生成的页面。

因此,让我们从基于原始单页面模板的新 jQM 单页面样板开始 ([view.jquerymobile.com/1.3.0/docs/widgets/pages/](http:// http://view.jquerymobile.com/1.3.0/docs/widgets/pages/))。随着我们进入其他章节,我们将逐步完善它,使其成为一个全面的模板。以下是我们将为本章创建的基本目录结构和我们将使用的文件:

一个新的 jQuery Mobile 样板

现在,这是基础 HTML。让我们把它存储在 template.html 中:

提示

下载示例代码

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

<!DOCTYPE html> 
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css" />
    <link rel="stylesheet" href="css/custom.css" />
    <script src="img/jquery-1.7.1.min.js"></script>
    <script src="img/custom-scripting.js"></script>
    <script src="img/jquery.mobile-1.1.0.min.js"></script>
    <title>Boilerplate</title> 
</head> 
<body>
    <div data-role="page">
        <div data-role="header">
            <h1>Boilerplate</h1>
        </div>
        <div data-role="content"> 
            <p>Page Body content</p>
        </div>
        <div data-role="footer">
            <h4>Footer content</h4>
        </div>
        <a href="{dynamic location}" class="fullSiteLink">View Full Site</a>
    </div>
</body>
</html>

meta viewport 的不同之处

meta viewport 标签是真正使移动设备成为移动设备的关键!没有它,移动浏览器会假设它是一个桌面站点,一切都会变得很小,需要捏合缩放:

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">

这个 meta viewport 标签与众不同,因为它实际上阻止了所有的捏合缩放操作。为什么?因为现在智能手机不仅仅掌握在了了解这些事情的技术精英手中。我个人见过人们在试图点击链接时不小心放大了页面。他们不知道他们做了什么或如何退出。无论如何,如果你使用 jQuery Mobile,你的用户不需要缩放:

<linkrel="stylesheet" href="css/custom.css" />

我们将需要自定义样式。没有别的办法。即使我们使用了 jQuery Mobile ThemeRoller (jquerymobile.com/themeroller/),总会有一些需要覆盖的内容。这就是你放置的地方:

<script src="img/custom-scripting.js"></script>

最初在有关全局配置的部分提到(jquerymobile.com/demos/1.1.0/docs/api/globalconfig.html),这是您放置全局覆盖的地方,以及您可能想要运行或普遍可用的任何脚本:

<a href="{dynamic location}" class="fullSiteLinmk">View Full Site</a>

大多数移动网站遵循“最佳实践”,包括一个指向完整网站的链接。它通常位于页脚,并且通常链接到完整网站的主页。好的,很好。工作完成了对吗?错!最佳实践更应该被标记为“行业标准”,因为有更好的方法。

超越行业标准的完整网站链接

简单包括一个完整网站链接的行业标准未能支持用户的心理状态。当用户在移动网站上导航时,他们清楚地表明了他们想要查看的内容。支持用户从移动到完整网站的心理模型转换是更多工作,但打造良好的用户体验始终是如此。

想象一下。萨莉正在我们的移动网站上四处浏览,因为她想要从我们这里购买商品。她实际上花了时间向下浏览或搜索她想要查看的产品。然而,由于移动设备的限制,我们做出了一些有意识的选择,不在那里放置所有信息。我们只包括市场研究显示人们真正关心的重点。此时,她可能有点沮丧,因为她点按完整网站链接以获取更多信息。完整网站链接是以传统(懒惰)方式编码的,将她带到完整网站的根目录,现在她必须再次找到产品。现在她必须使用捏和缩放来做到这一点,这只会增加她的烦恼。除非萨莉非常感兴趣,否则她在经历了如此糟糕的体验后,继续在移动设备上查找的机会有多大,她会在桌面浏览器上回来的机会有多大?

现在,相反地,想象一下同样的移动产品页面经过深思熟虑地制作,将完整网站链接指向产品页面的桌面视图。这正是我们在我的工作地方所做的。每个可能的移动页面都明确映射到其桌面等效页面。这种无缝的过渡已经通过实际客户的用户测试,并获得了 50%的冷漠和 50%的喜悦的混合反应。用户方面肯定会有惊喜,因为它违反了他们的期望,但没有一个负面的反应。如果这不成功地论证了重新考虑传统方式处理完整网站链接的情况,我不知道还有什么。

当然,你可能会有用户体验专业人员,他们会使用像“一致性”、“最佳实践”、“行业标准”和“违背用户期望”这样的流行词汇。如果用户测试的证据无法说服他们,给他们一些以下哲学的剂量:

  • 一致性:这种方法在自身内部是一致的。每个完整站点链接都映射到完整站点的那个页面。

  • 最佳实践:实践只有在新的、更好的实践出现之前才是最佳的。如果他们宁愿坚持的最佳实践,那么也许他们应该卖掉他们的汽车,换一匹马和马车。

  • 行业标准:行业标准是全世界试图跟随创新者的支撑物。好往往是伟大的敌人。不要满足于它。

  • 违背用户期望:如果我们告诉用户我们将发送给他们一个免费的 MP3 播放器,然后我们发送给他们一台 128 GB 的 iPad 4,我们违背了他们的期望吗?是的!他们会介意吗?有些期望值是值得违背的。

让我们考虑另一面。如果用户确实想要转到完整站点的起始页面呢?嗯,他们只需一步之遥,因为现在他们只需点击主页按钮。因此,很有可能,我们已经为用户节省了几个导航步骤,而且最坏的情况下,只增加了一个步骤回到起点。

从好到伟大,细节决定成败。这确实是一个小细节,但我向你挑战,每页额外花 30 秒去做好这部分工作。

全局 JavaScript

由于 jQuery Mobile 中的 Ajax 导航和渐进增强,有很多不同和额外的事件。让我们考虑我发现最有用的三个独特的 jQuery Mobile 事件。我们不会立即使用它们,只是了解它们,并确保阅读注释。最终,我们将创建 /js/global.js 来存放我们需要的脚本。目前,只需阅读以下脚本:

// JavaScript Document  

$('div[data-role="page"]').live( 'pageinit', 
function(event){          
    /* Triggered on the page being initialized, after
     initialization occurs. We recommend binding to this 
     event instead of DOM ready() because this will work
     regardless of whether the page is loaded directly or 
     if the content is pulled into another page as part of 
     the Ajax navigation system. */ 
});  

$('div[data-role="page"]').live('pagebeforeshow', function(event){   
    /* Triggered on the "toPage" we are transitioning to, 
     before the actual transition animation is kicked off. 
     Callbacks for this event will receive a data object as 
     their 2nd arg. This data object has the following  
     properties on it: */ 
});  

$('div[data-role="page"]').live( 'pageshow', 
function(event){    
    /* Triggered on the "toPage" after the transitionanimation has completed. Callbacks for this event will 
    receive a data object as their 2nd arg. This data 
    object has the following properties on it: */ 
});

.live.on

你可能已经注意到,我们在这里使用了 .live 方法来捕获事件。该方法自 jQuery 1.7 版本起已被弃用。截至撰写本文时,我们使用的是 jQuery 1.9 版本。然而,即使你查看文档中事件处理程序的示例,它们仍然在多个地方使用 .live

.live 函数的作用是检查到达文档级别的每个事件,并查看它是否与选择器匹配。如果匹配,则执行该函数。.live 如此有用的原因在于,它非常适用于处理变动和动态注入的元素。毕竟,绑定尚不存在的东西很困难。但你总是可以依靠 .live 来捕获事件。由于它被过度使用且效率一般,它已被弃用,改用 .on。因此,下面是我们如何使用以下新方法完成相同任务的方式:

$('div[data-role="page"]').live( 'pageinit', function(event){
  var $page = $(this);
});

将变为

$(document).on('pageinit', function(event){
  var $page = $(event.target);
});

如果你想要针对每个页面进行处理,这样做非常合适。现在让我们考虑一个代码片段,可以单独针对单个页面的初始化:

$('#someRandomPage').live( 'pageinit', function(event){
  var $page = $(this);
});

将变成

$(document).on('pageinit', '#someRandomPage', function(event){
  var $page = $(event.target);
});

差异微妙,最终对于我们来说,从性能的角度来看并不会产生任何差异,因为我们处理的是一个围绕让页面事件冒泡到文档级别的框架。在 jQuery Mobile 实现中,使用.on.live不会带来性能提升。但是,当你不得不更新时,可能会遇到升级头疼,因为它们最终摒弃了.live

全局 CSS

如果这是你第一次接触响应式网页设计,大多数情况下,你的自定义样式将在默认部分。其他部分是用来覆盖默认样式,以适应其他设备的宽度和分辨率。Horizontal Tweaks部分是用来覆盖横向方向的样式。iPad部分适用于 768px 和 1024px 之间的平板分辨率。在HD and Retina Tweaks部分,你很可能只需要覆盖背景图样式以替换更高分辨率的图形。我们很快将看到这些实例,并将我们使用的内容放入/css/custom.css。与此同时,只需要看看这些结构。

/* CSS Document */  
/* Default Styles   -------------*/  

/* Horizontal Tweaks   ----------*/ 
@media all and (min-width: 480px){   

}  

/* HD and Retina Tweaks ---------*/ 
@media only screen and (-webkit-min-device-pixel-ratio: 1.2),        
only screen and (min--moz-device-pixel-ratio: 1.2),       
only screen and (min-resolution: 240dpi) {   

}   

/* iPad ----------------*/ 
@media only screen and (min-device-width: 768px)
and (max-device-width: 1024px) {      

}

将 HTML 分解为服务器端模板

通常情况下,我是一个 Java 程序员,但由于 LAMP (Linux, Apache, MySql, PHP) 平台的普及,我选择了 PHP。其实我们在这里真正做的就是使用变量和服务器端包含来使我们的模板具有一致性和灵活性。

这并不是真正的生产代码。这只是将初始 HTML 拆分成漂亮的 PHP 样板。如果你现在想将其保存到文件中,我建议使用/boilerplate.php

<?php   
    /* the document title in the <head> */  
    $documentTitle = "jQuery Mobile PHP Boilerplate";       

    /* Left link of the header bar       
     *   
     * NOTE: If you set the $headerLeftLinkText = 'Back'     
     * then it will become a back button, in which case,     
     * no other field for $headerLeft need to be defined.    
     */     
    $headerLeftHref = "/";  
    $headerLeftLinkText = "Home";   
    $headerLeftIcon = "home";       

    /* The text to show up in the header bar */ 
    $headerTitle = "Boilerplate";   

    /* Right link of the heaer bar */   
    $headerRightHref = "tel:8165557438";    
    $headerRightLinkText = "Call";  
    $headerRightIcon = "grid";      

    /* The href to the full-site link */    
    $fullSiteLinkHref = "/";     
?>  
<!DOCTYPE html>  
<html> 
  <head>    
    <?php include "includes/meta.php" ?> 
  </head>  
  <body>
    <div data-role="page">

      <?php include "includes/header.php" ?>

      <div data-role="content">              
        <p>Page Body content</p>         
      </div>      

      <?php include "includes/footer.php" ?>                    
    </div> 
  </body> 
</html> 

现在我们将提取大部分的头部内容,并将其放入/includes/meta.php中:

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<linkrel="stylesheet" href="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.css" />
<linkrel="stylesheet" href="css/custom.css" />
<scriptsrc="img/jquery-1.8.2.min.js"></script>
<!-- from https://raw.github.com/carhartl/jquery-cookie/master/jquery.cookie.js-->
<scriptsrc="img/jquery.cookie.js"></script>
<scriptsrc="img/global.js"></script>
<scriptsrc="img/jquery.mobile-1.2.0.min.js"></script>src="img/jquery.mobile-1.1.0.min.js"></script>

<title><?=$documentTitle?></title>

注意

注意js/jquery.cookie.js中的 cookies 插件。你需要从github.com/carhartl/jquery-cookie下载它。我们稍后将在移动设备检测中使用它。

现在,让我们将页面头部变为动态内容,并将其放入/includes/header.php中:

<div data-role="header">	
<?PHP if(strtoupper ($headerLeftLinkText) == "BACK"){?>	<a data-icon="arrow-l" href="javascript://"                 
data-rel="back"><?=$headerLeftLinkText?></a>		
<?PHP } else if($headerLeftHref != ""){ ?>
<a<?PHP if($headerLeftIcon != ""){ ?>	
data-icon="<?=$headerLeftIcon ?>" 			
<?PHP } ?>href="<?=$headerLeftHref?>"><?=$headerLeftLinkText?></a>
<?PHP } ?>

<h1><?=$headerTitle ?></h1>

<?PHP if($headerRightHref != ""){ ?>
<a<?PHP if($headerRightIcon != ""){ ?>	
data-icon="<?=$headerRightIcon ?>" 
data-iconpos="right" 			
<? } ?>
href="<?=$headerRightHref?>"><?=$headerRightLinkText?></a>
<?PHP } ?>	
</div><!-- /header -->

接下来,让我们将页脚内容提取到/includes/footer.php中:

<div data-role="footer">		
<insert 2 spaces>
<h4>Footer content</h4>	
</div><!-- /footer -->
<p class="fullSite">
<a class="fullSiteLink" href="<?=$fullSiteLinkHref?>">View Full Site</a>
</p>
<p class="copyright">&copy; 2012</p>

头部和底部的 PHP 文件是设置并忘记的文件。我们只需要在主页和meta.phpheader.phpfooter.php上填写一些变量,剩下的就交给它们来处理。headers.php被编码成当您的$headerLeftLinkText设置为单词Back(不区分大小写),它就会将头部的左侧按钮变成返回按钮。

我们需要创建我们的网站的内容

我们已经有了一个可行的样板文件。我们有了一个客户。让我们开始工作,并编写我们在第一章中绘制的内容,jQuery Mobile 原型。在本章中,我们将只专注于第一个屏幕,因为这是我们教授技能所需的全部内容。

我们需要创建我们网站的内容

这是我们需要考虑的内容:

  • 标志:我们将简单地包含桌面视图中的标志。

  • 按钮:我们可以通过几种方式来完成这些按钮。乍一看,我们可能会考虑使用标准的data-role="button"链接。我们可以利用ui-grid (jquerymobile.com/demos/1.2.0/docs/content/content-grids.html) 来添加格式。如果我们只打算针对垂直持有的手机进行优化,那将是一个很好的方法。然而,我们要在这里跳出框架,创建一个在不同分辨率下反应良好的响应式菜单。

  • 图标:这些不是标准的 jQuery Mobile 图标。在线有无数的图标集可供我们使用,但我们选择Glyp****hish (glyphish.com/)。它们制作了包含多个尺寸、视网膜显示优化和原始 Adobe Illustrator 文件的高质量图标,以防您想要调整它们。这是一个非常优秀的选择。

  • 客户见证:这看起来非常适合使用带有图像的列表视图。我们将从他们的 Facebook 页面上提取这些内容。

获取 Glyphish 并定义自定义图标

Glyphish 有一个许可证,允许在署名下免费使用。免费套装 (www.glyphish.com/download/) 只有一个尺寸和 200 个图标,"专业"套装有多个尺寸、400 个图标和无限许可证。仅需 25 美元,这是一个不费吹灰之力的选择。

创建一个带有图标的按钮非常简单。你所需要做的就是使用data-icon属性。像下面的代码一样,将产生一个按钮,如下图所示:

<a href="index.html" data-role="button" 
data-icon="delete">Delete</a>

获取 Glyphish 并定义自定义图标

你可能还没有意识到 jQuery Mobile 实际上是这样做的。无论你将data-icon的值写成什么样,它都将成为按钮上的一个类名。如果你有一个data-icon="directions"的属性,那么 jQM 应用的类就是ui-icon-directions。当然,你需要像这样在你自己的自定义 CSS 文件中制作这个。我们将把这个以及其他类似的内容放入css/custom.css中。

.ui-icon-directions{   
    background-image: 
    url(../icons/icons-gray/113-navigation.png);   
    height:28px;    
    width:28px;   
    background-size:28px 28px;   
    margin-left: -14px !important;  
}

另一件你需要做的事情是去掉典型图标周围的彩色圆盘。我们还需要删除边框半径,否则我们的图标将被裁剪以适应ui-icon样式中定义的圆形半径的形状。为此,我们将为每个要以这种方式自定义的链接添加glyphishIcon类。我们还需要将此定义添加到我们的custom.css

.glyphishIcon .ui-icon{   
    -moz-border-radius: 0px;   
    -webkit-border-radius: 0px;   
border-radius: 0px;    
background-color:transparent; 
}

最后,我们在首页上的四个按钮的代码将如下所示:

<div class="homeMenu">
<a class="glyphishIcon" href=" https://maps.google.com/maps?q=9771+N+Cedar+Ave,+Kansas+City,+MO+64157&hl=en&sll=39.20525,-94.526954&sspn=0.014499,0.033002&hnear=9771+N+Cedar+Ave,+Kansas+City,+Missouri+64157&t=m&z=17&iwloc=A" data-role="button" data-icon="directions" data-inline="true" data-iconpos="top">Map it</a>
<a class="glyphishIcon" href="tel:+18167816500" data-role="button" data-inline="true" data-icon="iphone" data-iconpos="top">Call Us</a>
<a class="glyphishIcon" href="https://touch.facebook.com/nickyspizzanickyspizza" data-role="button" data-icon="facebook" data-iconpos="top" data-inline="true">Like Us</a>
<a class="glyphishIcon" href="menu.php" data-role="button" data-inline="true" rel="external" data-icon="utensils" data-iconpos="top">Menu</a>
</div>

它会在屏幕上呈现如下的截图所示:

获取 Glyphish 并定义自定义图标

链接到电话、电子邮件和地图

移动浏览器具有独特的可用性优势。如果我们想要链接到一个电子邮件地址,本机电子邮件客户端将立即弹出。以下代码是一个示例:

<a href="mailto:shane@roughlybrilliant.com" >email me</a>

我们也可以对电话号码采取相同的方式,每个设备都会立即弹出一个选项,让用户拨打那个号码。这是桌面无法匹敌的功能,因为大多数桌面设备都没有电话功能。这是来自前述代码的href元素:

href="tel:+18167816500"

地图是移动设备的另一个特色,因为几乎所有智能手机都内置了 GPS 软件。以下是地图链接的href元素。它只是一个到谷歌地图的标准链接:

href="https://maps.google.com/maps?q=9771+N+Cedar+Ave,+Kansas+City,+MO+64157"

对于 iOS 5 和 Android,操作系统将拦截该点击,并在本机地图应用程序中显示位置。iOS 6 更改了这种模式,但我们仍然可以链接到谷歌地图链接,用户将会看到网页视图,并提示他们在 iOS 中打开谷歌地图,如下图所示:

链接到电话、电子邮件和地图

对于除了 iOS 和 Android 之外的平台,用户将直接转到谷歌地图网站。这很好,因为谷歌在使该网站可用于任何设备,包括非智能手机方面做得非常出色。

当然,我们可以就此结束,并且说它已经足够好了,但我们可以做更多的工作,通过发送用户到内置的苹果地图应用程序,为苹果用户提供更好的体验。这段代码将创建一个具有可配置属性的对象,用于配置和未来的适应。它通过版本嗅探来查看操作系统的主要版本是否大于 5。如果是,它将吸收谷歌地图链接。

这些链接可以通过两种方式进行转换。首先,它会查找超链接上的data-appleMapsUrl属性并使用它。如果链接上没有这个属性,它将检查forceAppleMapsConversionIfNoAlt配置选项,看看您是否已经配置了转换器对象来直接转换谷歌地图链接。

一旦系统意识到这部手机需要切换,它就会将这个事实存储到localStorage中,这样它就不必再次进行版本检查的工作。它只会检查localStorage中的值是否为true

以下是位于/js/global.js的代码:

var conditionalAppleMapsSwitcher = {
  appleMapsAltAttribute:"data-appleMapsUrl",
  forceAppleMapsConversionIfNoAlt:true,
  iPhoneAgent:"iPhone OS ",
  iPadAgent:"iPad; CPU OS ",
  process: function(){
    try{
      var agent = navigator.userAgent;
      if(window.localStorage && localStorage.getItem("replaceWithAppleMaps")){
        if(localStorage.getItem("replaceWithAppleMaps") == "true"){
          this.assimilateMapLinks();
        }
      }else{
        var iOSAgent = null;
        if(agent.indexOf(this.iPhoneAgent) > 0){
          iOSAgent = this.iPhoneAgent
        }
        else if(agent.indexOf(this.iPadAgent) > 0){  
          iOSAgent = this.iPadAgent
        }
        if(iOSAgent){
          var endOfAgentStringIndex = (agent.indexOf(iOSAgent)+iOSAgent.length);
          var version = agent.substr(endOfAgentStringIndex, agent.indexOf(" " , endOfAgentStringIndex));
          var majorVersion = Number(version.substr(0, version.indexOf("_")));
          if(majorVersion > 5){
            localStorage.setItem("replaceWithAppleMaps", "true");
            this.assimilateMapLinks();
          }
        }
      }
    }catch(e){}
  },
  assimilateMapLinks:function(){
    try{
      var switcher = this;
      $("a[href^='http://maps.google.com']").each(function(index, element) {
        var $link = $(element);
        if($link.attr(switcher.appleMapsAltAttribute)){
          $link.attr("href", $link.attr(switcher.appleMapsAltAttribute));
        }else if(switcher.forceAppleMapsConversionIfNoAlt){
          $link.attr("href", $link.attr("href").replace(/maps\.google\.com\/maps/,"maps.apple.com/"));
        }
      });
    }catch(e){}
  }

使用这段代码,现在很容易在我们的/js/global.js中的pageinit上调用它:

$(document).on("pageinit", function(){        conditionalAppleMapsSwitcher.process();        
});

这种方法对用户来说是完全无缝的。无论他们使用的是什么系统,他们都会在尝试访问您客户的业务时获得最无摩擦的体验。

自定义字体

自定义字体出现在他们的完整网站上(因此也是他们品牌的一部分)。这些字体在移动端同样适用。像 iOS、Android 和最新的 BlackBerry 完全支持 @font-face CSS。旧版 BlackBerry 和 Windows Phone 可能会根据用户的型号支持或不支持 @font-face。对于任何不支持 @font-face 的人,他们将只看到您在 font-family 规则中指定的标准网络字体。有许多不同的网络字体提供商:

对于我们的项目,我们将使用 Google Web Fonts。我们需要在每个我们想要使用它们的页面的<head>中包含这些行。因为我们可能会在任何地方使用它们,所以让我们把这些行直接包含在我们的文件/includes/meta.php中。

<link href='http://fonts.googleapis.com/css?family=Marvel' rel='stylesheet' type='text/css'>

一旦我们在<head>中链接了我们的字体,我们将需要在/css/custom.css文件中使用font-family规则来指定它们的使用方式,如下所示:

h1,h2,h3,.cardo{font-family: Marvel, sans-serif;}

现在,对于任何(大多数情况下)支持它的浏览器,他们将看到如下内容:

自定义字体

注意

注意:网络字体并不轻量级。Marvel 的体积为 20 KB。不算大,但也不小。你不会想包含太多这样的字体的。

列表项的页面翻页阴影效果

我们将使用无序列表来布置客户的推荐。我们可以使用图像列表视图,但我们也想要在每个项目之间有一些间距以及一个页面翻页效果。所以,让我们只是给一个普通的无序列表加样式。尽可能避免覆盖标准的 jQuery Mobile 代码。那只是在找麻烦。每当你覆盖一个被设计成一个框架的东西(比如 jQuery Mobile)时,你都会面临下一个版本完全破坏你所做的覆盖和自定义适应的风险。

此定制的代码稍后将在本章显示并标记为最终的 CSS。重点是,我们将使用 CSS3 来完成这个。大多数移动浏览器完全支持 CSS3,包括转换、过渡、动画、阴影、渐变和圆角。古老的平台,如 Windows Phone 7 和 BlackBerry 5,是基于 Internet Explorer 7 或更早版本的,并且不完全支持 CSS3。在这些情况下,他们不会看到花哨的页面翻页效果,而只会看到一个包含图像和文本的白色框。虽然不是理想的情况,但这是一个完全合理的后备方案。

优化:为什么你应该首先考虑它

我相信优化是如此重要,以至于你需要在一开始就知道并且意识到它。你将做一些了不起的工作,我不希望你或你的利益相关者认为它不够了不起,或者慢,或者其他任何东西,因为你不知道如何挤压系统性能的技巧。从你的创作中获得最佳性能的窍门永远不嫌早。移动环境是一个非常苛刻的环境,本节中的一些技巧将产生比任何“最佳编码实践”更大的影响。

从性能的角度来看,绝对没有比 HTTP 请求更糟糕的事情了。这就是为什么 CSS 精灵是个好主意。我们发出的每一个请求都会减慢我们的速度,因为 TCP/IP 协议假定每个请求的可用带宽从几乎零开始。因此,我们不仅需要通信的延迟时间来开始从服务器拉取资产,而且还需要一段时间才能将该资产以最大可能的速度传输。4G 也无法拯救我们脱离这些事实。当然,它们一旦开始传输,传输速率是很快的,但是实际开始传输的延迟时间才是我们的致命问题。我们还必须考虑到用户在多久或没有接收到信号的情况下发现自己。这在建筑物中尤其如此。因此,以下是一些优化移动站点的技巧:

  • 通过尽可能合并尽可能多的资产来减少 HTTP 请求。SPDY 协议 (www.chromium.org/spdy/spdy-whitepaper/) 最终获得进展时,它将解决我们的问题,但是,目前和可预见的未来,这是最让我们变慢的原因。这也是为什么我不会建议用户使用像 Require.js (requirejs.org/) 这样的工具来动态加载页面中所需的内容。不要偷懒。了解你的页面需要什么,并尽可能合并。

  • 在服务器上启用 gzip 压缩。 任何给定服务器都很有可能启用了 gzip 压缩,但是你应该检查一下。这将使你的基于文本的资产(HTML、CSS、JS)在传输时缩小多达 70%。这实际上比缩小代码更有影响。想要了解更多,请查看developers.google.com/speed/articles/gzip

  • 缩小文件。 缩小是这样一个过程,一个完全可读的代码被剥夺了所有有用的空格、格式和注释。推送到浏览器的只是代码。有些人甚至会将变量和函数名称改为一个或两个字母的替换。这对于长期稳定的代码确实是一个好主意。具有倾向于在一开始就比较大的库,如 jQuery,肯定会受益。然而,对于你自己的代码,最好保持其可读性,这样如果必要的话,你就可以进行调试。就尽量让你的 HTML 页面保持在 25 KB(未压缩)以下,你的 JS 和 CSS 文件在 1 MB(同样未压缩)以下。雅虎进行的一项研究表明,在所有平台上,这似乎是设备在访问之间允许被缓存的最低公共分母(www.yuiblog.com/blog/2010/07/12/mobile-browser-cache-limits-revisited/)。

  • 缓存和微缓存。如果你使用的是大多数其他网站上的 Apache(news.netcraft.com/archives/2012/01/03/january-2012-web-server-survey.html),你可以很容易地使用htaccess文件设置缓存。如果你为某种类型的资源指定了一个月的缓存时间,那么浏览器将尝试在一个月内将这些资源保存在缓存中,甚至都不会检查服务器上是否有新的内容。在这里要小心。你不希望对任何可能需要迅速更改的东西设置长时间的缓存时间。然而,那些不会改变的 JavaScript 库和图像等内容肯定可以被缓存而不会产生任何不良影响。

    为了保护自己免受流量洪泛的影响,你可以使用htaccess缓存规则,使页面保持时间尽可能短,例如一分钟,使用以下代码:

    # 1 MIN 
    <filesMatch "\.(html|htm|php)$">
      Header set Cache-Control "max-age=60, private, proxy-revalidate" 
    </filesMatch>
    

    你可以在www.askapache.com/htaccess/speed-up-sites-with-htaccess-caching.html上了解更多关于 htaccess 缓存的内容。

  • 不要使用图片,如果可以用 CSS3 实现。CSS3 标准始于 1999 年。W3C 在 2009 年开始起草 CSS4 推荐的第一稿。现在是让网络向前发展,让旧版本的浏览器归于历史的时候了。如果有人使用不支持 CSS 渐变的浏览器,让他们退回到他们丰富应得的纯色背景。如果他们的浏览器不支持 CSS 中的圆角,那么他们只能用方角了。

    如果潜在客户希望您超越网络标准来支持古老的技术,或者坚持像素完美的设计,那么辞退客户,或者收取足够多的额外费用以使其值得您的时间。像素完美的设计在桌面上已经很困难了。移动设备是一个无序之地,每个人都在实现自己的解决方案,只是稍有不同,以至于您永远不可能实现像素完美的解决方案。 (dowebsitesneedtolookexactlythesameineverybrowser.com/)

    在可能的情况下,使用 CSS3 代替图像以节省重量和 HTTP 请求。现在大多数现代智能手机都支持它(iOS、Android、BlackBerry 6+、Windows Phone 8+)。到 2013 年和 2014 年,几乎所有早期的智能手机都将被替换。

最终产品

现在我们已经具备了制作第一页所需的所有要求、知识和资产。我们将把这段代码作为第一页,并将其命名为 index.php。所有示例的图像都提供在源文件夹中。

以下是 index.php 的最终代码:

<?php 
 $documentTitle = "Nicky's Pizza";

 $headerLeftHref = "/";
 $headerLeftLinkText = "Home";
 $headerLeftIcon = "home";

 $headerTitle = "Boilerplate";

 $headerRightHref = "tel:8165077438";
 $headerRightLinkText = "Call";
 $headerRightIcon = "grid";

 $fullSiteLinkHref = "/";

?>
<!DOCTYPE html>
<html>
<head>
 <?php include("includes/meta.php"); ?> 
</head>

<body>
<div data-role="page">
    <div data-role="content">

     <div class="logoContainer"><img src="img/LogoMobile.png" alt="Logo" width="290" style="margin:0" /></div>

        <div class="homeMenu">
            <a class="glyphishIcon" href="http://maps.google.com/maps?q=9771+N+Cedar+Ave,+Kansas+City,+MO+64157&hl=en&sll=39.20525,-94.526954&sspn=0.014499,0.033002&hnear=9771+N+Cedar+Ave,+Kansas+City,+Missouri+64157&t=m&z=17&iwloc=A" data-role="button" data-icon="directions" data-inline="true" data-iconpos="top">Map it</a>
            <a class="glyphishIcon" href="tel:+18167816500" data-role="button" data-inline="true" data-icon="iphone" data-iconpos="top">Call Us</a>
            <a class="glyphishIcon" href="https://touch.facebook.com/nickyspizzanickyspizza" data-role="button" data-icon="facebook" data-iconpos="top" data-inline="true">Like Us</a>
            <a class="glyphishIcon" href="menu.php" data-role="button" data-inline="true" rel="external" data-icon="utensils" data-iconpos="top">Menu</a>
        </div>

        <h3>What customers are saying:</h3>
        <div class="testimonials">
            <ul class="curl">
                <li><img class="facebook" src="img/fb2.jpg" alt="facebook photo" width="60" height="60" align="left" />I recommend the Italian Sausage Sandwich. Awesome!! Will be back soon!</li>
                <li><img class="facebook" src="img/fb0.jpg" alt="facebook photo" width="60" height="60" align="left" />LOVED your veggie pizza friday night and the kids devoured the cheese with jalapenos!!! salad was fresh and yummy with your house dressing!!</li>
                <li><img class="facebook" src="img/fb1.jpg" alt="facebook photo" width="60" height="60" align="left" />The Clarkes love Nicky's pizza! So happy you are here in liberty.</li>
            </ul>
        </div>

    </div>

    <?php include("includes/footer.php"); ?>
</div>

</body>
</html>

自定义 CSS

这段代码位于 /css/custom.css 中,包含了我们所做的所有自定义外观。其中包括自定义图标、页面翻页效果和自定义字体的定义。任何引用的图像都是客户提供的,并且在最终源文件中提供。

特别注意这里的评论,因为我已经详细说明了每个部分的目的以及它如何融入响应式网页设计

@charset "UTF-8";   

/*************************************************/
/* define the places we'll use custom fonts */
/*************************************************/

h1,h2,h3,.cardo{font-family: Marvel, sans-serif;} 
.logoContainer{
    font-family: Marvel, sans-serif; 
    text-align:center;margin:auto;
} 
.makersMark{
    margin:1.5em auto;
    font-family: Marvel, sans-serif; 
    text-align:center;
} 
.testimonials{margin:0 auto;} 

/*************************************************/
/*  define the background for the site */
/*************************************************/

.ui-content{ 
    background-image:url(../images/cropfade.jpg);
    background-repeat:no-repeat; 
    background-size: 100%;
}

/*************************************************/
/*  override the listview descriptions to allow them */
/*  to wrap instead of simply cutting off with an */
/*  ellipsis */
/*************************************************/

.ui-li-desc{white-space:normal;} 

/*************************************************/
/*  define our custom menu on the front page  */
/*************************************************/

.homeMenu{ text-align:center;} 
.homeMenu .ui-btn{ min-width:120px;  margin:.5em;}  
.glyphishIcon .ui-icon{
    -moz-border-radius: 0px;
    -webkit-border-radius: 0px;
    border-radius: 0px;
    background-color:transparent; 
}
/*************************************************/
/* define custom icons for our four menu buttons  */
/*************************************************/

.ui-icon-directions{
    background-image: url(../icons/icons-gray/113-navigation.png);  
    height:28px;
    width:28px;
    background-size:28px 28px;
    margin-left: -14px !important;
  }
.ui-icon-iphone{
    background-image: url(../icons/icons-gray/32-iphone.png);
    height:28px;
    width:16px;
    background-size:16px 28px;
    margin-left: -8px !important;
  }
.ui-icon-facebook{
    background-image: url(../icons/icons-gray/208-facebook.png);
    height:28px;
    width:22px;
    background-size:22px 22px;
    margin-left: -11px !important;
}
.ui-icon-utensils{
    background-image: url(../icons/icons-gray/48-fork-and-knife.png);
    height:28px;
    width:18px;
    background-size:18px 26px;
    margin-left: -9px !important;  
}  

/*************************************************/
/* define how to show people's Facebook images
/*************************************************/

li img.facebook{padding:0 10px 10px 0;} 

/*************************************************/
/* define the look of the footer content */
/*************************************************/
.fullSite{text-align:center;} 
.copyright{
    text-align:center;font-family: Marvel, sans-serif; 
    marign-top:2em;
} 		

/*************************************************/
/* define how the layout and images will change for */
/* phones in landscape mode.  RESPONSIVE WEB DESGIN */
/*************************************************/

/* Horizontal ----------*/ 
@media all and (min-width: 480px){

  /*************************************************/
  /* reflow the main menu buttons to display as */
  /* four in a row and give some appropriate margin */
  /*************************************************/
.homeMenu .ui-btn{ min-width:100px;  margin:.2em;} 
}  

/*************************************************/
/* define how we'll override the image URLs for */
/* devices with high resolutions. */
/* RESPONSIVE WEB DESIGN */
/*************************************************/
@media only screen and (-webkit-min-device-pixel-ratio: 1.5),    
only screen and (min--moz-device-pixel-ratio: 1.5),    
only screen and (min-resolution: 240dpi) { 	
.ui-icon-directions{ 
   background-image: url(../icons/icons-gray/113-navigation@2x.png);
  }
.ui-icon-iphone{
   background-image: url(../icons/icons-gray/32-iphone@2x.png);
  }
.ui-icon-facebook{
   background-image: url(../icons/icons-gray/208-facebook@2x.png);
  }
.ui-icon-utensils{
   background-image: url(../icons/icons-gray/48-fork-and-knife@2x.png);
  } 
}  

/*************************************************/
/* define the reflow, sizes, spacing for the menu */
/* buttons for iPad.  RESPONSIVE WEB DESIGN
/*************************************************/
/* iPad size -----------*/ 
@media only screen and (min-device-width: 768px) 
and (max-device-width: 1024px) {     
    .homeMenu .ui-btn{ min-width:120px;  margin:.7em; } 
}

/*************************************************/
/* begin page curl CSS */   
/*************************************************/
ul.curl {
    position: relative;
    z-index: 1;  
    list-style: none;   
    margin: 0;
    padding: 0;
  }
ul.curl li {   
    position: relative;
    float: left;
    padding: 10px;
    border: 1px solid #efefef;
    margin: 10px 0;
    background: #fff;   
    -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.27), 0 0 40px rgba(0, 0, 0, 0.06) inset;   
    -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.27), 0 0 40px rgba(0, 0, 0, 0.06) inset;    
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.27), 0 0 40px rgba(0, 0, 0, 0.06) inset;    text-align:left;   
}
ul.curlli:before,
ul.curlli:after {   
    content: '';   
    z-index: -1;   
    position: absolute;   
    left: 10px; 	
    bottom: 10px;   
    width: 70%; 	
    max-width: 300px;  
    max-height: 100px;   
    height: 55%;   
    -webkit-box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);   
    -moz-box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);   
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);   
    -webkit-transform: skew(-15deg) rotate(-6deg); 	
    -moz-transform: skew(-15deg) rotate(-6deg); 	
    -ms-transform: skew(-15deg) rotate(-6deg);   
    -o-transform: skew(-15deg) rotate(-6deg);   
    transform: skew(-15deg) rotate(-6deg); 
}  
ul.curlli:after {   
    left: auto;
    right: 10px;
    -webkit-transform: skew(15deg) rotate(6deg);
    -moz-transform: skew(15deg) rotate(6deg);
    -ms-transform: skew(15deg) rotate(6deg);
    -o-transform: skew(15deg) rotate(6deg);   
    transform: skew(15deg) rotate(6deg); } 
/*************************************************/
/* end page curl CSS */ 
/*************************************************/

第一页的结果

让我们来看看我们工作的最终产品。在左侧,我们有纵向视图中的呈现页面,右侧是横向视图:

第一页的结果

在两个方向上测试设计非常重要。当有人稍后过来,只是简单地转动手机就破坏了您的工作,这可能会让人感到相当尴尬。

第一页的结果

这是在 iPad 上的效果。行业内存在一些关于 iPad 是否算作移动设备的辩论,因为它具有足够的分辨率和足够大的屏幕来查看正常的桌面站点,特别是在横向模式下查看。主张桌面视图的人忽略了一个非常重要的事实。iPad 和所有其他平板电脑,如 Kindle Fire、Nook Color 和 Google Nexus 设备,仍然是触摸界面。虽然全站点仍然可以完美阅读,但交互点可能仍然是小目标。如果是触摸界面,您的客户将更好地通过 jQuery Mobile 服务。

引导用户访问我们的移动站点

现在我们已经有了一个很好的移动站点,用户如何到达那里呢?yourdomain.mobim.yourdomain.com?事实上,用户不会直接访问移动站点。他们通常会执行以下两种操作之一:在 Google 中搜索该站点,或者在地址栏中输入主域名,这与他们在桌面站点上的行为相同。因此,我们有责任正确检测移动用户并为他们提供适当的界面。

行业内对此如何完成存在很多争议。大多数专家似乎都同意,你不希望涉足检测特定平台的业务,这被称为用户代理嗅探。起初,这似乎不是一个坏主意。毕竟,实际上只有四个主要平台:iOS、Android、Windows Phone 和 BlackBerry。即使如此,随着未来新平台的开发或主导地位的出现,这种方法很快就会变成一场噩梦。这里真正的问题是,我们为什么要关心他们使用的平台?我们真正关心的是设备的功能。

使用 JavaScript 进行检测和重定向

自然地,这不会涵盖移动市场的所有人。即使在美国,智能手机的普及率也仅为 50%。(blog.nielsen.com/nielsenwire/online_mobile/smartphones-account-for-half-of-all-mobile-phones-dominate-new-phone-purchases-in-the-us/) 但这有关系吗? 。

如果这种方法最多只能覆盖市场的 50%,那么它真的是一个合适的解决方案吗?是的,但是怎么可能呢?以下两个原因最能解释这个问题:

  • 没有智能手机的人通常没有数据计划。在网上冲浪变得经济上不可行。大多数没有智能手机和数据计划的人不会接触到你。

  • 拥有旧款智能手机(如 BlackBerry 5 或更早版本)的人可能有数据计划。然而,这些设备的浏览器几乎不值得一提,他们的用户也知道这一点。他们可能会访问你的网站,但可能性不大,并且它们的存在正在迅速减少。

在大多数情况下,有可能会使用智能手机访问你的站点的人会有很好的响应。例外情况不值一提。

如果设备支持媒体查询并具有触摸界面,那么它非常适合我们的移动站点。当然,唯一的例外是 Windows Phone 7 上的 Internet Explorer。因此,我们将对他们稍作让步。首先,我们需要为 jQuery 下载 cookie 插件。如果你还没有,请从 github.com/carhartl/jquery-cookie 获取,并将其放入 /js/ 文件夹中。此代码将放置在你想要进行移动重定向的任何文件夹中。

<script type="text/javascript">	
  //First, we check the cookies to see if the user has
  //clicked on the full site link.  If so, we don't want
//to send them to mobile again.  After that, we check for     
  //capabilities or if it's IE Mobile
if("true" != $.cookie("fullSiteClicked") &&
      ('querySelector' in document &&
       'localStorage' in window&&
'addEventListener' in window &&
('ontouchstart' in window || 
window.DocumentTouch && document instanceOf DocumentTouch
)
)     
|| navigator.userAgent.indexOf('IEMobile') > 0
)
{                
location.replace(YOUR MOBILE URL);   
}  
</script>

我们还可以根据每个页面的需求定制移动端目标页面。将这种技术与之前创建的动态完整站点链接配对,可以在用户想要切换时实现无缝的移动端和桌面端视图转换。我们现在只有一个问题。我们需要设置一个 Cookie,这样,如果他们点击完整站点链接,就不会被立即重定向回移动端。让我们把这个放到 /js/global.js 中:

$("[data-role='page']").live('pageinit', function (event, ui) { 
    $("a.fullSiteLink").click(function(){     
    $.cookie("fullSiteClicked","true", {path: "/", expires: 3600});   
    }); 
}); 

对于为移动设备编写的任何 cookie,设置过期时间是个好主意。在台式电脑上,人们倾向于关闭他们的浏览器。在移动设备上,人们点击主页按钮,这可能实际上并未关闭该浏览器的会话。在 Android 上,除非用户明确关闭,否则浏览器永远不会被关闭。

在服务器端进行检测

如果你必须将所有移动用户都引导到你的移动站点,你需要在服务器端进行检测,使用类似 WURFLwurfl.sourceforge.net/)这样的工具。这是一个由社区维护的无线设备描述符的终极数据库。本质上,这是用户代理嗅探,但是数据库由社区良好维护。该工具将能够告诉你访问你的每个设备的各种有用信息。链接 www.scientiamobile.com/wurflCapability/tree 将为您提供 WURFL 的所有功能的完整列表。我们将在后面的章节中深入了解这个工具的具体原理。

总结

在本章中,我们涵盖了很多内容,现在我们具备了所有技能和工具,可以将原本看起来相当普通的移动站点变成独特的东西。我们知道如何使其看起来独特,如何托管它,如何引导用户到达那里,以及如何给他们一个更加功能强大的“降落伞”,以防他们不满意。已经,我们领先于那些刚刚入门的普通开发者数步,而这仅仅是第二章。在下一章中,我们将开始探讨更深入的话题,这些话题通常是大型企业关心的,比如验证、分析等等。

第三章:分析、长表单和前端验证

是时候发展了。业务正在增长,没有什么比大型表单、指标和定制体验更能体现出大企业的风范了。

在本章中,我们将涵盖:

  • 谷歌静态地图

  • 谷歌分析

  • 长型和多页表单

  • 集成 jQuery 验证

谷歌静态地图

在上一章中,我们完全沉浸在如何动态地直接链接到 iOS 和 Android 的本机 GPS 系统中。现在,让我们考虑另一种方法。客户希望有机会向用户显示街道地址、地图,并给他们另一次打电话的机会。在这种情况下,简单地链接到本机 GPS 系统是不够的。如果用户点击地址或地图,我们仍然可以触发它,但作为中间步骤,我们可以从谷歌注入一个静态地图(developers.google.com/maps/documentation/staticmaps/)。

它是否像直接启动应用程序开始逐步转向方向一样惊艳?没有,但它要快得多,也许这就是用户所需要的。他们可能会立即识别出位置,并决定,是的,实际上,他们更愿意打电话。记住,始终从用户的角度来看待事物。并不总是要做我们能做到的最酷的事情。

让我们来看一下客户批准的绘图:

谷歌静态地图

让我们来看一下将放在/map.php中的此页面的代码:

<?php 
  $documentTitle = "Map | Nicky's Pizza";

  $fullSiteLinkHref = "/";

  $mapsAddress = "https://maps.google.com/maps?q=9771+N+Cedar+Ave,+Kansas+City,+MO+64157&hl=en&sll=39.20525,-94.526954&sspn=0.014499,0.033002&hnear=9771+N+Cedar+Ave,+Kansas+City,+Missouri+64157&t=m&z=17&iwloc=A";
  $staticMapUrl = "https://maps.googleapis.com/maps/api/staticmap?center=39.269109,-94.45281&amp;zoom=15&amp;size=288x200&amp;markers=color:0xd64044%7Clabel:N%7C39.269109,-94.45281&amp;sensor=true;"
?>
<!DOCTYPE html>
<html>
<head>
  <?php include("includes/meta.php"); ?>
</head>

<body>
<div data-role="page">
  <div data-role="content">
    <div class="logoContainer"><img src="img/LogoMobile.png" alt="Logo" width="290" style="margin:0" /></div>
    <p>
      <a href="<?=$mapsAddress ?>">
        <address class="vcard">
          <div class="adr">
            <div class="street-address">9771 N Cedar Ave</div>
            <span class="locality">Kansas City</span>, 
            <span class="region">MO</span>, 
            <span class="postal-code">64157</span> 
            <div class="country-name">U.S.A.</div>
          </div>
        </address>
      </a>
    </p>
    <p><a href="<?= $mapsAddress ?>"><img src="img/<?=$staticMapUrl ?>" width="288" height="200" /></a></p>
    <p><a href="tel:+18167816500" data-role="button">Call for delivery</a></p>
  </div>
  <?php include("includes/footer.php"); ?>
</div>
</body>
</html>

注意使用微格式 (microformats.org/) 来标记地址。虽然这不是必需的,但自 2007 年以来已经成为相当标准的做法,这是赋予您的信息更多语义价值的好方法。这意味着不仅人们可以读懂它,甚至计算机也可以读懂并理解它。如果您想了解更多关于微格式的信息,可以阅读 Smashing Magazine 的这篇文章:coding.smashingmagazine.com/2007/05/04/microformats-what-they-are-and-how-to-use-them/

添加 Google Analytics

每个网站都应该有分析功能。如果没有,很难说有多少人访问了您的网站,我们是否通过转化漏斗吸引了人们,或者是哪些页面导致了人们离开我们的网站。

让我们增强全局 JavaScript (/js/global.js) 文件,以自动记录每个显示的页面。这是一个非常重要的区别。在桌面世界中,每个分析命中都基于文档就绪事件。这对于jQuery MobilejQM)不起作用,因为基于 Ajax 导航系统的第一个页面是唯一触发页面加载事件的页面。在 jQM 中,我们需要使用以下代码在pageshow事件上触发这个动作:

/**********************************************/
/* Declare the analytics variables as global */
/**********************************************/
var _gaq = _gaq || [];

/**********************************************/
/* Initialize tracking when the page is loaded*/
/**********************************************/
$(document).ready(function(e) { 
(function() { 
var ga = document.createElement('script'); 
ga.type = 'text/javascript'; 

//Call in the Google Analytics scripts asynchronously.
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 
'https://ssl' :
'http://www') 
+'.google-analytics.com/ga.js'; 
var s = document.getElementsByTagName('script')[0]; 
s.parentNode.insertBefore(ga, s); })(); 
});

/**********************************************/
/* On every pageshow, register each page view in GA */
/**********************************************/
$("[data-role='page']").live('pageshow', function (event, ui)
{

//wrap 3rd party code you don't control in try/catch
try {
_gaq.push(['_setAccount', 'YOUR ANALYTICS ID']);
if ($.mobile.activePage.attr("data-url")) { 
_gaq.push(['_trackPageview', 
//Pull the page to track from the data-url attribute 
//of the active page.
$.mobile.activePage.attr("data-url")]);
} else { 
_gaq.push(['_trackPageview']); 
} 
} 
 //if there is an error, let's dump it to the console
catch(err) {console.log(err);}
}); 

通过使用异步调用来拉取 Google Analytics,我们允许用户继续操作,即使跟踪功能不起作用或加载需要一些时间。通常,对 JavaScript 文件的调用会暂停所有进一步的资产加载和 JavaScript 执行,直到所请求的脚本完全加载和执行为止。我们真的不希望因为一些广告网络或分析跟踪需要一段时间才能响应而导致我们精心设计的、速度快且功能完善的页面受阻。

我们从当前页面的data-url属性中提取要跟踪的位置,因为你不能可靠地使用document.location函数来进行页面跟踪。jQM 的基于 Ajax 的导航会导致跟踪中出现一些非常奇怪的 URL。jQM 团队正在解决这个问题,但需要一段时间才能在所有设备上提供所需的技术。相反,只需从 jQM 页面的data-url属性中提取要跟踪的 URL。如果你动态创建页面,这也是你会为跟踪目的放置自定义页面名称的地方。如果你使用多页面模板,每个页面的 ID 将被跟踪为页面视图。

我们确实还没有做太多的分析工作,但让我们看一些我们已经开始收集的见解。这里只是一小部分技术细分的样本:

添加 Google Analytics

以下图片显示了同一视图的完整报告,稍微细分以显示哪些设备最受欢迎:

添加 Google Analytics

在前一张图片中,特别关注每个平台整体的跳出率列。如果其中一个显着高于另一个,这可能表明我们需要更仔细地查看该设备上的网站。

添加 Google Analytics

制作移动网站远不止于在移动浏览器上美化外观。一个量度良好的移动网站的最佳指标是人们能够快速进入并找到他们需要的内容。这使得“热门内容”报告成为我们的新朋友。

毫不奇怪,大多数访问网站的人都在点击菜单,如前一张报告中所示。然而,菜单只是一个起点而已。他们在菜单中最感兴趣的是什么?特色披萨。正是这种洞察力可以引导你成功地进行首次重新设计。也许,我们应该考虑在首页上展示特色产品,为用户节省时间。

底线是,没有良好的分析,你就不知道自己是否在构建正确的东西。当前设计的网站,让他们要点击两次才能看到最关心的内容,对吗?

到目前为止,我们只跟踪了页面浏览。但在移动世界中,这还不是全部的图片。那些拨打电话号码但不触发页面浏览的链接呢?那些转移到 Facebook 或到地图软件(如 Google 地图)的链接呢?这些当然也算是进一步的互动,但希望也能对所有这些点击行为进行统计。我们已经以不同的方式跟踪页面浏览,让我们继续下去。

自然地,我们希望跟踪自定义事件而不必为每个要跟踪的事件编写 JavaScript。让我们把我们的链接做成这样:

<a href="tel:+18167816500" data-pageview="call">Call Us</a>

然后,让我们在 pageinit 处理程序中添加一些代码:

$(document).on('pageinit', function (event, ui) { 
$page = $(event.target);

$page.find("[data-pageview]").click(function(){ 
var $eventTarget = $(this); 
if($eventTarget.attr("data-pageview") == "href"){ 
_gaq.push(['_trackPageview', 
$eventTarget.attr("href")]); 
}else{
_gaq.push(['_trackPageview', 
$eventTarget.attr("data-pageview")]); 
} 
});

还有很多可以进行的分析跟踪,比如自定义事件跟踪,电子商务活动跟踪,目标跟踪等等。既然你已经知道如何将 Google Analytics 与 jQuery Mobile 结合起来的基本知识,你可以继续探索更多的跟踪方式,可以查看这里:developers.google.com/analytics/devguides/collection/gajs/

长表单和多页面表单

在桌面上,长表单是很正常的。我们都见过注册页面和电子商务订单流程。表单越长,就越倾向于将它们分成更小、更合乎逻辑的片段。这通常是通过以下几种方式来实现的:

  • 保持它作为一个完整的页面,但注入足够的空白和分组,使其看起来不那么令人生畏

  • 要么物理上将表单分成多个页面,要么使用显示/隐藏技术来完成同样的事情

这两种方法在任务完成方面并没有太大的区别。无论哪种方式,都不是移动限制条件下特别不利的策略。增加成功的最佳方法是:

  • 完全去除所有可选字段

  • 尽量减少必填字段的数量(对此要尽快着手)

  • 预先填写合理默认值的元素

  • 立即验证字段,而不是等到最后

  • 提前告知用户任务可能需要多长时间

即使这样做了,有时表单还是会很长。如果你遇到这种情况,下面是使用 jQuery Mobile 将一个长表单分成多个页面的一个有用方法。以下是来自 ordercheckout.php 的代码:

<body>
 <form action="/m/processOrder.php" method="post">
  <div data-role="page" id="delivery">
    <?php $headerTitle = "Deliver To"; ?>
    <?php include("includes/header.php"); ?>
    <div data-role="content">
    <h2>Where will we be delivering?</h2>

      <!—-form elements go here -->   

      <p>
        <div class="ui-grid-a">
          <div class="ui-block-a"><a data-role="button" href="index.php">Cancel</a></div>
          <div class="ui-block-b"><a data-role="button" href="#payment">Continue</a></div>
        </div>
      </p>

    </div>
    <?php include("includes/footer.php"); ?>
  </div>

  <div data-role="page" id="payment">
    <?php $headerTitle = "Payment"; ?>
    <?php include("includes/header.php"); ?>
    <div data-role="content">
      <h2>Please enter payment information</h2>

        <!-—form elements go here -->              

      <p>
        <div class="ui-grid-a">
          <div class="ui-block-a"><a data-role="button" data-theme="d" href="index.php">Cancel</a></div>
          <div class="ui-block-b"><input type="submit"data-theme="b" value="Submit"/></div>
        </div>
      </p>

    </div>
      <?php include("includes/footer.php"); ?>
  </div>

 </form>
<body>

这里要注意的第一件事是 body 和 form 标签都在所有 jQuery Mobile 页面之外。记住,所有这些只是一个大的文档对象模型(DOM)。所有疯狂的渐进增强和 UI 中的页面切换都没有改变这一点。这个页面,在根本上,是一个我们将用来提交整个订单流程的巨大表单。

集成 jQuery 验证

在客户端尽可能多地验证始终对用户体验很重要。HTML5 通过提供更多的输入类型控制大大推动了这一目标。尽管 HTML5 输入类型很好,但我们需要更多。进入 Query Validate。 (bassistance.de/jquery-plugins/jquery-plugin-validation/)

Validate 插件是 jQuery 社区的一个基石,但有一些东西可以帮助我们的移动实现。让我们从自动将验证添加到任何具有 validateMe 类表单的页面开始。

$("form.validateMe").each(function(index, element) { 
var $form = $(this); 
var v = $form.validate({
errorPlacement: function(error, element) {
vardataErrorAt = element.attr("data-error-at");
    if (dataErrorAt) 
        $(dataErrorAt).html(error); 
    else
      error.insertBefore(element); 
    } 
  }); 
});

由于页面可能包含多个表单,让我们现在就处理它,通过将其挂钩到每个请求验证的表单中,使用以下命令:

$("form.validateMe").each

默认情况下,ValidateMe 在无效字段后放置错误信息。但在移动设备上,这样做不太好,因为错误信息会显示在表单元素的下方。在 BlackBerry 和某些 Android 系统上,表单元素不一定会垂直居中于键盘和字段本身之间的空间内。如果用户输入有误,反馈不会是即时和明显的。这就是为什么我们要对错误放置进行两个更改,使用以下代码行:

errorPlacement:

在任何给定的元素上,我们都可以使用标准的 jQuery 选择器指定我们想要放置错误的位置,就像以下代码行所示的那样。也许我们永远不会使用它,但拥有它是方便的。

element.attr("data-error-at");

如果在元素级别未指定错误放置位置,我们将在元素本身之前插入错误,就像以下代码行所示的那样。错误语言将显示在标签文本和表单元素之间。这样,键盘永远不会遮挡反馈。

error.insertBefore(element);

在单表单、多页面的环境中,我们希望能够在继续到下一页之前逐个验证一个 jQM 页面。我们需要做的第一件事情是给出一个替代方式来处理 required 函数,因为我们显然不是一次性验证整个表单。

这可以在我们的全局脚本中在任何函数外部声明:

$.validator.addMethod("pageRequired", function(value, element) {  	
var $element = $(element);
  if ($element.closest("."+$.mobile.subPageUrlKey).hasClass($.mobile.activePageClass)){  
    return !this.optional(element);
}
  return "dependency-mismatch";
}, $.validator.messages.required);

像这样添加额外的 validator 方法非常方便。我们可以为几乎任何事情声明自己的验证方法。

供您快速参考,以下是其他验证选项:

  • required

  • remote

  • email

  • url

  • date

  • dateISO

  • number

  • digits

  • creditcard

  • equalTo

  • accept

  • maxlength

  • minlength

  • rangelength

  • range

  • max

  • min

要查看更多启发人心的演示,请访问 bassistance.de/jquery-plugins/jquery-plugin-validation/ 并考虑向该项目捐赠。它让我们所有人的生活变得更美好。

集成 jQuery Validate

现在我们已经将 jQuery Validate 正确集成到我们的多页表单中,我们需要使我们的错误看起来像是真正的错误。我们可以选择一些非常简单的东西,比如文本上的红色,但我更喜欢保持与 jQuery Mobile 的样式一致。他们的默认主题集有一个 data-theme="e",非常适合用于错误状态。将我们的错误类添加到他们的 ui-bar-e 的定义上似乎是个好主意,但不要这样做。在写这本书的过程中,jQuery Mobile 被修补了三次,如果我们采取这种方法,将会导致每次升级都有摩擦。相反,让我们将 ui-bar-e 的定义直接复制到我们的自定义样式表中,如下所示:

label.error,input.error{
border:1px solid #f7c942;
background:#fadb4e;
color:#333;
text-shadow:0 1px 0 #fff;
background-image:-webkit-gradient(linear,lefttop,leftbottom,from(#fceda7),to(#fbef7e));
background-image:-webkit-linear-gradient(#fceda7,#fbef7e);
background-image:-moz-linear-gradient(#fceda7,#fbef7e);
background-image:-ms-linear-gradient(#fceda7,#fbef7e);
background-image:-o-linear-gradient(#fceda7,#fbef7e);
background-image:linear-gradient(#fceda7,#fbef7e)} 

我们几乎已经准备好使用我们的精美表单了。现在我们只需要能够在转移到下一页之前对其进行验证即可。我们不必担心提交链接,因为自然会触发验证,但让我们使用以下代码为继续链接添加一个类:

<a data-role="button" data-theme="b" href="#payment"class="validateContinue">Continue</a>

然后,在我们的全局脚本中,让我们使用以下代码将这个函数添加到我们的 pageinit 处理程序中:

$page.find(".validateContinue").click(function(){ 
  if($(this).closest("form").data("validator").form()){ 
    return true; 
  }else{
    event.stopPropagation();
    event.preventDefault();
    return false; 
  } 
}); 

如果用户在此过程中刷新会发生什么?字段将为空,但我们已经进入到下一页了。页面底部的一个小脚本,如下面的代码所示,应该可以处理这个问题:

//page refresh mitigation 
$(document).on("pagebeforeshow", function(){ 
  if(document.location.hash != ""){
    var $firstRequiredInput = 
$("input.pageRequired").first(); 
    if($firstRequiredInput.val() == ""){
      var redirectPage = 
$firstRequiredInput.closest("[data-role='page']"); 
      $.mobile.changePage(redirectPage);
    }
  }
});

现在我们已经掌握了基本概念,并克服了一些小问题,让我们看看ordercheckout.php 文件的最终代码:

<!DOCTYPE html>
<html>
<?php 
  $documentTitle = "Check Out | Nicky's Pizza";

  $headerLeftHref = "";
  $headerLeftLinkText = "Back";
  $headerLeftIcon = "";

  $headerRightHref = "tel:8165077438";
  $headerRightLinkText = "Call";
  $headerRightIcon = "grid";

  $fullSiteLinkHref = "/";

?>
<head>
  <?php include("includes/meta.php"); ?>
  <style type="text/css">
    #ordernameContainer{display:none;}
  </style>
</head>

<body>
  <form action="thankyou.php" method="post" class="validateMe">

这是我们多页表单的第一页。请记住,这些页面将一次性全部提交。在用户转移到下一页之前,我们将使用以下代码验证每一页:

div data-role="page" id="delivery">
  <?php $headerTitle = "Deliver To"; ?>
  <?php include("includes/header.php"); ?>
  <div data-role="content">
    <h2>Where will we be delivering?</h2>

    <p>
      <label for="streetAddress">Street Address</label>
      <input type="text" name="streetAddress" id="streetAddress" class="pageRequired" />
    </p>

    <p>
      <label for="streetAddress2">Address Line 2 | Apt#</label>
      <input type="text" name="streetAddress2" id="streetAddress2" />
    </p>

    <p>
      <label for="zip">Zip Code</label>
      <input type="number" name="zip" id="zip" maxlength="5" class="pageRequired zip" />
    </p>

    <p>
      <label for="phone">Phone Number</label>
      <input type="tel" name="phone" id="phone" maxlength="10" class="number pageRequired" />
    </p>

    <p>
      <div class="ui-grid-a">
        <div class="ui-block-a"><a data-role="button" data-icon="delete" data-iconpos="left" data-theme="d" href="javascript://">Cancel</a></div>
        <div class="ui-block-b"><a data-role="button" data-icon="arrow-r" data-iconpos="right" data-theme="b" href="#payment" class="validateContinue">Continue</a></div>
      </div>
    </p>

  </div>
  <?php include("includes/footer.php"); ?>
</div>

这是用于收集付款信息的表单的第二页。请注意信用卡的验证。我们只需添加类 "creditcard" 即可使框架检查卡号是否符合 Luhn 算法(en.wikipedia.org/wiki/Luhn_algorithm)。

<div data-role="page" id="payment">
  <?php $headerTitle = "Payment"; ?>
  <?php include("includes/header.php"); ?>
  <div data-role="content">
    <h2>Please enter payment information</h2>

    <p>
      <label for="nameOnCard">Name on card</label>
      <input type="text" name="nameOnCard" id="nameOnCard" class="pageRequired" />
    </p>

    <p>
      <label for="cardNumber">Card Number</label>
      <input type="tel" name="cardNumber" id="cardNumber" class="pageRequired creditcard" />
    </p>

    <p>
      <label for="expiration">Expiration</label>
      <input class="pageRequired number" type="tel" name="expiration" id="expiration" maxlength="4" size="4" placeholder="MMYY" />
    </p>

    <p>
      <label for="cvv">CVV2 (on the back of your card)</label>
      <input class="pageRequired number" type="number" name="cvv" id="cvv" minlength="3" maxlength="4" />
    </p>

    <p>
      <input type="checkbox" value="true" name="savePayment" id="savePayment" /><label for="savePayment">Save payment info for easier ordering?</label>
      <input type="checkbox" value="true" name="saveOrder" id="saveOrder" onchange="showHideOrderNameContainer()" /><label for="saveOrder">Save this order to your favorites?</label>
    </p>

    <p id="ordernameContainer">
      <label for="ordername">Give your order a name</label>
      <input type="text" name="ordername" id="ordername" placeholder="example: the usual" />
    </p>

    <p>
      <div class="ui-grid-a">
        <div class="ui-block-a"><a data-role="button" data-icon="delete" data-iconpos="left" data-theme="d" href="javascript://">Cancel</a></div>
        <div class="ui-block-b"><input type="submit" data-icon="arrow-r" data-iconpos="right" data-theme="b" value="Submit" /></div>
      </div>
    </p>

  </div>
  <?php include("includes/footer.php"); ?>
</div>

</form>

这些是我们在本章早些时候提到的脚本:

 <script type="text/javascript">
  function showHideOrderNameContainer(){
   if($("#saveOrder").attr("checked")){
    $("#ordernameContainer").show();
   }else{
    $("#ordernameContainer").hide();
   }
  }

  //page refresh mitigation
  $("[data-role='page']").live("pagebeforeshow", function(){
   if(document.location.hash != ""){
    var $firstRequiredInput = $("input.pageRequired").first();
    if($firstRequiredInput.val() == ""){
     var redirectPage = $firstRequiredInput.closest("[data-role='page']");
     $.mobile.changePage(redirectPage);
    }
   }

  });
 </script>
</body>
</html>

这是自从集成 jQuery Validate 以来的 meta.php 文件:

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link href='http://fonts.googleapis.com/css?family=Marvel'rel='stylesheet' type='text/css'>
<linkrel="stylesheet" href="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.css"/>
<link rel="stylesheet" href="css/custom.css" />
<script src="img/jquery-1.8.2.min.js"></script>
<script src="img/jquery.cookie.js"></script>
<script src="img/jquery.validate.min.js"></script>
<script src="img/global.js"></script>
<script src="img/jquery.mobile-1.2.0.min.js"></script>
<title><?=$documentTitle?></title>

经过三章,以下可能可以称为主 JavaScript 文件(global.js)。基本上是我在每个项目中使用的文件,只有轻微的变化:

var _gaq = _gaq || []; 
var GAID = 'UA-XXXXXXXX-X'; 

/*******************************************************/
/* Load Google Analytics only once the page is fully loaded.
/*******************************************************/
$(document).ready(function(e) { 
(function() { 
var ga = document.createElement('script'); 
ga.type = 'text/javascript'; 
ga.async = true; 
ga.src = ('https:' == document.location.protocol ? 
'https://ssl' : 'http://www') +'.google-analytics.com/ga.js'; 
var s = document.getElementsByTagName('script')[0]; 
s.parentNode.insertBefore(ga, s); 
})();
});

/*******************************************************/
/* Upon jQM page initialization, place hooks on links with 
/* data-pageview attributes to track more with GA.
/* Also, hook onto the full-site links to make them cookie
/* the user upon click. 
/*******************************************************/
$(document).on('pageinit', function (event, ui) { 
$page = $(event.target); 

$page.find("[data-pageview]").click(function(){ 
var $eventTarget = $(this); 
if($eventTarget.attr("data-pageview") == "href"){ 
_gaq.push(['_trackPageview', 
$eventTarget.attr("href")]); 
}else{ 
_gaq.push(['_trackPageview', 
$eventTarget.attr("data-pageview")]); 
} 
}); 

$page.find("a.fullSiteLink").click(function(){ 
$.cookie("fullSiteClicked","true", {
path: "/", 
expires:3600
}); 
}); 

/*******************************************************/
/* Find any form with the class of validateMe and hook in
/* jQuery Validate.  Also, override the error placement.
/*******************************************************/
//Any form that might need validation 
$("form.validateMe").each(function(index, element) { 
var $form = $(this);
var v = $form.validate({
errorPlacement: function(error, element) { 
var dataErrorAt = element.attr("data-error-at"); if (dataErrorAt) 
$(dataErrorAt).html(error); 
else
error.insertBefore(element);
      }
});     
});  

/*******************************************************/
/* Hook in the validateContinue buttons.
/*******************************************************/
$page.find(".validateContinue").click(function(){ 
if($(this).closest("form").data("validator").form()){ return true;
}else{
event.stopPropagation(); 
event.preventDefault();
return false;
}
}); 
});   

/*******************************************************/
/* Every time a page shows, register it in GA.
/*******************************************************/

$(document).on('pageshow', function (event, ui) { 
try {
_gaq.push(['_setAccount', GAID]);
if ($.mobile.activePage.attr("data-url")) { 
_gaq.push(['_trackPageview', 
$.mobile.activePage.attr("data-url")]);
} else {
_gaq.push(['_trackPageview']);
    }
} catch(err) {}  
});  

/*******************************************************/
/*  Add the custom validator class to allow for validation 
/*  on multi-page forms.
/*******************************************************/
$.validator.addMethod("pageRequired", function(value, element) {
var $element = $(element);
if( $element.closest("."+$.mobile.subPageUrlKey)
.hasClass($.mobile.activePageClass)) 
{  
return !this.optional(element);  
} 
return "dependency-mismatch";  
}, $.validator.messages.required); 

使用 Google Analytics 进行电子商务跟踪

到目前为止,我们所跟踪的只是页面浏览量。确保非常有用,但大多数经理和业主都喜欢他们的报告。在感谢页面上,我们应该包含一些简单的电子商务跟踪。同样,由于 jQuery Mobile 的基于 Ajax 的导航系统,我们需要微调默认示例,以使其完全符合 jQM 的工作原理。

这是感谢页面(thankyou.php)的完整代码,其中的电子商务跟踪设置为只有在页面显示后才运行:

<!DOCTYPE html>
<html>
<?php 
  $documentTitle = "Menu | Nicky's Pizza";

  $headerLeftHref = "index.php";
  $headerLeftLinkText = "Home";
  $headerLeftIcon = "home";

  $headerRightHref = "tel:8165077438";
  $headerRightLinkText = "Call";
  $headerRightIcon = "grid";

  $fullSiteLinkHref = "/index.php";
?>
<head>
  <?php include("includes/meta.php"); ?>
</head>

<body>
<div data-role="page" id="orderthankyou">
  <?php 
    $headerTitle = "Thank you"; 
    include("includes/header.php"); 
  ?>
  <div data-role="content" >
    <h2>Thank you for your order. </h2>
    <p>In a few minutes, you should receive an email confirming your order with an estimated delivery time.</p>

    <script type="text/javascript">
      $("#orderthankyou").live('pageshow', function(){
        _gaq.push(['_addTrans',
          '1234',                      // order ID - required
          'Mobile Checkout',  // affiliation or store name
          '21.99',                    // total - required
          '1.29',                     // tax
          ' ',                          // shipping
          'Kansas City',       // city
          'MO',              // state or province
          'USA'              // country
          ]);
        _gaq.push(['_trackTrans']); //submits transaction to the Analytics servers
      });
    </script>
  </div>
  <?php include("includes/footer.php"); ?>
</div>

</body>
</html>

摘要

表单并不是什么新鲜事物。自从互联网问世以来,我们就一直在使用它们。它们并不起眼,但可以是优雅、有效和响应灵敏的。jQuery Mobile 让您在基于触摸的界面中更有效地创建表单。现在,您可以通过多页面表单和客户端验证进一步完善它。不要低估这两种技术配合使用时的威力。当客户几乎可以在不必返回服务器的情况下完成所需的一切时,体验会自动得到提升。混合使用观察用户在您的网站上是如何浏览、他们喜爱的内容以及他们的流失点的能力,将帮助您打造更具吸引力的体验。只需记住,在思考分析数据时,重要的不是绝对数字,而是趋势;在完成这些基础工作之后,让我们着手研究一些更有趣的技术吧。在下一章中,我们将开始研究地理定位等内容。

第四章:QR 码、地理定位、Google 地图 API 和 HTML5 视频

我们已经讨论了许多小型和大型企业的核心关注点。现在让我们把目光转向其他可能会让媒体公司感兴趣的概念。在本章中,我们将看一下一个电影院连锁,但实际上,这些概念可以应用于任何具有多个实体位置的企业。

在本章中,我们将涵盖:

  • QR 码

  • 基本地理定位

  • 整合 Google 地图 API

  • 链接和嵌入视频

QR 码

我们热爱我们的智能手机。我们喜欢展示我们的智能手机可以做什么。所以,当那些充满神秘感的方块开始在各个地方出现并迷惑着大众时,智能手机用户迅速行动起来,并以同样过度热情的方式向人们展示这是怎么一回事,就像我们掏出它们来回答甚至是路过听到的最琐碎的问题一样。而且,由于看起来 NFC 不会很快普及,我们最好熟悉 QR 码以及如何利用它们。

QR 码

数据显示,根据调查,QR 码的知识和使用率非常高:(researchaccess.com/2012/01/new-data-on-qr-code-adoption/)

  • 超过三分之二的智能手机用户扫描过码

  • 超过 70%的用户表示他们会再次这样做(尤其是为了折扣)

等等,这和 jQuery Mobile 有什么关系?流量。大量成功的流量。如果只有百分之二的人点击横幅广告,那么这被认为是成功的 (en.wikipedia.org/wiki/Clickthrough_rate)。QR 码的点击率超过 66%!我会说这是吸引人们注意我们创造物的一个相当好的方式,因此应该引起关注。但 QR 码不仅仅用于 URL。在下面的 QR 码中,我们有一个 URL、一个文本块、一个电话号码和一个短信:

QR 码

提示

有许多生成 QR 码的方法 (www.the-qrcode-generator.com/www.qrstuff.com/)。实际上,只需在 Google 上搜索QR Code Generator,你就会有很多选择。

让我们考虑一个当地的电影院连锁。Dickinson Theatres (dtmovies.com) 自 1920 年代起就存在,并考虑加入移动领域。也许他们会投资于移动网站,并在公交车站和其他户外场所放置海报和广告。自然地,人们会开始扫描,这对我们很有价值,因为他们会告诉我们哪些位置是有效的。这真的是广告业的首创。我们有一个媒介似乎激励人们在扫描时与设备互动,这将告诉我们他们扫描时在哪里。地理位置很重要,这可以帮助我们找到合适的位置。

地理定位

当 GPS 首次出现在手机上时,除了紧急情况下的警察跟踪之外,它几乎没有什么用处。今天,它使我们手中的设备比我们的个人电脑更加个性化。目前,我们可以非常可靠地获得纬度、经度和时间戳。W3C 的地理位置 API 规范可以在dev.w3.org/geo/api/spec-source.html找到。目前,我们假装有一张海报,提示用户扫描 QR 码以找到最近的影院和放映时间。它会带用户到这样的页面:

地理定位

由于没有比共进晚餐和看电影更好的初次约会,看电影的人群倾向于偏年轻一些。不幸的是,这群人通常没有很多钱。他们可能比较倾向于使用功能手机而不是智能手机。有些人可能只有非常基本的浏览器。也许他们有 JavaScript,但我们不能指望它。如果有的话,他们可能会有地理位置信息。无论如何,考虑到受众,渐进增强将是关键。

我们要做的第一件事是创建一个基本级别的页面,其中包含一个简单的表单,该表单将向服务器提交一个邮政编码。由于我们使用了之前的模板,我们将为表单添加验证,供那些使用validateMe类的 JavaScript 的人使用。如果他们有 JavaScript 和地理位置,我们将用一条消息替换表单,说我们正在尝试找到他们的位置。目前,不要担心创建这个文件。此时源代码不完整。此页面将不断发展,最终版本将在文件qrresponse.php中的本章源包中,如以下代码所示:

<?php  
  $documentTitle = "Dickinson Theatres";  
  $headerLeftHref = "/"; 
  $headerLeftLinkText = "Home"; 
  $headerLeftIcon = "home";  

  $headerTitle = "";  	
  $headerRightHref = "tel:8165555555"; 
  $headerRightLinkText = "Call"; 
  $headerRightIcon = "grid";  

  $fullSiteLinkHref = "/";  
?> 
<!DOCTYPE html>
<html>
<head> 
  <?php include("includes/meta.php"); ?>
</head>
<body>
<div id="qrfindclosest" data-role="page">
  <div class="logoContainer ui-shadow"></div>
  <div data-role="content">
    <div id="latLong>
      <form id="findTheaterForm" action="fullshowtimes.php"method="get" class="validateMe">             
        <p>
          <label for="zip">Enter Zip Code</label>
          <input type="tel" name="zip" id="zip"class="required number"/>
        </p>
        <p><input type="submit" value="Go"></p>             
      </form>
    </div>         
    <p>         
      <ul id="showing" data-role="listview" class="movieListings"data-dividertheme="g">              
      </ul>         
    </p>
  </div>
  <?php include("includes/footer.php"); ?>
</div>
<script type="text/javascript">
 //We'll put our page specific code here soon
</script>
</body>
</html>

对于没有 JavaScript 的任何人来说,这就是他们会看到的,没有什么特别的。我们可以用一点 CSS 来装饰它,但有什么意义呢?如果他们使用的是没有 JavaScript 的浏览器,那么他们的浏览器很可能也不擅长呈现 CSS。这其实没关系。毕竟,渐进增强并不一定意味着让它对每个人都很美好,它只意味着确保它对每个人都有效。大多数人永远不会看到这个,但如果他们看到了,它会正常工作。

地理定位

对于其他人,我们需要开始用 JavaScript 来以可以编程消化的格式获取我们的剧院数据。JSON 对于这个任务非常合适。如果你已经熟悉 JSON 的概念,现在就跳到下一段。如果你对它不熟悉,基本上,它是一种在互联网上传输数据的另一种方法。它就像 XML 但更有用。它不那么冗长,并且可以直接使用 JavaScript 进行交互和操作,因为它实际上是用 JavaScript 写的。JSON 是 JavaScript 对象表示法的首字母缩略词。特别感谢道格拉斯·克罗克福德(JSON 之父)。XML 在服务器上还有它的位置。如果你可以得到 JSON,它在浏览器中作为一种数据格式是没有理由存在的。这是一个如此普遍的观点,以至于在我参加的最后一次开发者大会上,有一个演讲者在问道时发出笑声,“还有谁在真正使用 XML 吗?”

本章的示例代码列有完整的剧院清单,但这应该足够让我们开始了。对于这个示例,我们将把 JSON 数据存储在/js/theaters.js中。

{ 
  "theaters":[ 
    {
      "id":161,
      "name":"Chenal 9 IMAX Theatre", 
      "address":"17825 Chenal Parkway",
      "city":"Little Rock",
      "state":"AR",
      "zip":"72223",
      "distance":9999,
      "geo":{"lat":34.7684775,"long":-92.4599322}, 
      "phone":"501-821-2616"
    },
    {
      "id":158,
      "name":"Gateway 12 IMAX Theatre", 
      "address":"1935 S. Signal Butte", 
      "city":"Mesa",
      "state":"AZ",
      "zip":"85209",
      "distance":9999,
      "geo":{"lat":33.3788674,"long":-111.6016081}, 
      "phone":"480-354-8030"
    },
    {
      "id":135,
      "name":"Northglen 14 Theatre",
      "address":"4900 N.E. 80th Street",
      "city":"Kansas City",
      "state":"MO",
      "zip":"64119",
      "distance":9999,
      "geo":{"lat":39.240027,"long":-94.5226432}, 
      "phone":"816-468-1100"
    }   
  ]
}

现在我们有了要处理的数据,我们可以准备在页面中准备好脚本。让我们把以下的 JavaScript 代码片段放在 HTML 底部的脚本标签中,就在我们的注释处:我们很快就会把我们的页面特定代码放在这里

//declare our global variables
var theaterData = null; 
var timestamp = null; 	
var latitude = null; 
var longitude = null; 	
var closestTheater = null; 

//Once the page is initialized, hide the manual zip code form
//and place a message saying that we're attempting to find 
//their location.
$(document).on("pageinit", "#qrfindclosest", function(){
  if(navigator.geolocation){   
     $("#findTheaterForm").hide(); 
     $("#latLong").append("<p id='finding'>Finding your location...</p>"); 
  } 
});

//Once the page is showing, go grab the theater data and find out which one is closest.  
$(document).on("pageshow", "#qrfindclosest", function(){ 
 theaterData = $.getJSON("js/theaters.js", 
 function(data){ 
      theaterData = data;
      selectClosestTheater();
    });
}); 

function selectClosestTheater(){ 
 navigator.geolocation.getCurrentPosition(
   function(position) { //success 
  latitude = position.coords.latitude; 
  longitude = position.coords.longitude; 
  timestamp = position.timestamp; 
  for(var x = 0; x < theaterData.theaters.length; x++){  var theater = theaterData.theaters[x]; 
    var distance = getDistance(latitude, longitude,theater.geo.lat, theater.geo.long); 
    theaterData.theaters[x].distance = distance; 
  }} 
  theaterData.theaters.sort(compareDistances); 
  closestTheater = theaterData.theaters[0]; 	
 _gaq.push(['_trackEvent', "qr", "ad_scan",(""+latitude+","+longitude) ]); 
  var dt = new Date(); 
  dt.setTime(timestamp); 
  $("#latLong").html("<div class='theaterName'>"
    +closestTheater.name+"</div><strong>"
    +closestTheater.distance.toFixed(2)
    +"miles</strong><br/>"
    +closestTheater.address+"<br/>"
    +closestTheater.city+", "+closestTheater.state+" "
    +closestTheater.zip+"<br/><a href='tel:"
    +closestTheater.phone+"'>"
    +closestTheater.phone+"</a>"); 
  $("#showing").load("showtimes.php", function(){ 
    $("#showing").listview('refresh'); 
  });
}, 
function(error){ //error  
  switch(error.code)  	
  { 
    case error.TIMEOUT: 
      $("#latLong").prepend("<div class='ui-bar-e'>Unable to get your position: Timeout</div>"); 
      break; 
    case error.POSITION_UNAVAILABLE: 
      $("#latLong").prepend("<div class='ui-bar-e'>Unable to get your position: Position unavailable</div>"); 
      break; 
    case error.PERMISSION_DENIED: 
      $("#latLong").prepend("<div class='ui-bar-e'>Unable to get your position: Permission denied.You may want to check your settings.</div>"); 
      break; 
    case error.UNKNOWN_ERROR:  
      $("#latLong").prepend("<div class='ui-bar-e'>Unknown error while trying to access your position.</div>"); 
      break; 
   }
   $("#finding").hide();   
   $("#findTheaterForm").show(); 
},
{maximumAge:600000}); //nothing too stale
}

这里的关键是geolocation.getCurrentPosition函数,它将提示用户允许我们访问他们的位置数据,就像在 iPhone 上所示的那样。

地理位置

如果有人是隐私倡导者,他们可能已经关闭了所有的位置服务。在这种情况下,我们需要告知用户他们的选择已经影响了我们帮助他们的能力。这就是错误函数的作用。在这种情况下,我们将显示一个错误消息,并再次显示标准表单。

一旦我们有了用户的位置和剧院列表,就该按距离对剧院进行排序并显示最近的一个。以下是一个相当通用的代码,我们可能希望在多个页面上使用。因此,我们会把它放到我们的global.js文件中:

function getDistance(lat1, lon1, lat2, lon2){ 
  //great-circle distances between the two points
  //because the earth isn't flat 
  var R = 6371; // km 	
  var dLat = (lat2-lat1).toRad(); 
  var dLon = (lon2-lon1).toRad(); 
  var lat1 = lat1.toRad(); 
  var lat2 = lat2.toRad();  
  var a = Math.sin(dLat/2) * Math.sin(dLat/2) +  
    Math.sin(dLon/2) * Math.cos(lat1) * 
    Math.cos(lat2);  
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));  
  var d = R * c; //distance in km 
  var m = d * 0.621371;  //distance in miles 
  return m; 
} 
if (typeof(Number.prototype.toRad) === "undefined") {   
  Number.prototype.toRad = function() { 
    return this * Math.PI / 180;   
  } 
}  

function compareDistances(a,b) {   
  if (a.distance<b.distance) return -1;   
  if (a.distance>b.distance) return 1;   
  return 0; 
} 

有了所有这些组件,现在就足够简单获取用户的位置并找到最近的剧院。它将成为数组中的第一个,并且直接存储在全局变量closestTheater中。如果他们关闭了 JavaScript,我们将不得不使用一些服务器端的算法或 API 来找出最近的剧院(这超出了本书的范围)。无论如何,我们都会将每个剧院的放映时间作为一个平面文件的列表项集合起来(showtimes.php)。在现实世界情况下,这将是由数据库驱动的,并且我们将调用带有正确剧院 ID 的 URL 的页面。现在,以下的代码就够了:

<li data-role="list-divider">Opening This Week</li>     
<li>         
  <a href="movie.php?id=193818">             
    <img src="img/darkknightrises.jpeg">             
    <h3>Dark Knight Rises</h3>             
    <p>PG-13 - 2h 20m<br/>
      <strong>Showtimes:</strong> 
      12:00 - 12:30 - 1:00 - 1:30 - 3:30 - 4:00 - 4:30 – 
      7:00 - 7:15 - 7:30 - 7:45 - 8:00 - 10:30 - 10:45
    </p>         
  </a>     
</li>     
<li>         
  <a href="moviedetails.php?id=193812">
    <img src="img/iceagecontinentaldrift.jpeg">             
    <h3>Ice Age 4: Continental Drift</h3>
    <p>PG - 1h 56m<br/>
      <strong>Showtimes:</strong> 10:20 AM - 10:50 AM – 
      12:40 - 1:15 - 3:00 - 7:00 - 7:30 - 9:30
    </p>         
  </a>     
</li>     
<li data-role="list-divider">Also in Theaters</li>
<li>
  <a href="moviedetails.php?id=194103">
    <img src="img/savages.jpeg">             
    <h3>Savages</h3>
    <p>R - 7/6/2012<br/><strong>Showtimes:</strong> 
      10:05 AM - 1:05 - 4:05 - 7:05 - 10:15
    </p>         
  </a>     
</li>     
<li>
  <a href="moviedetails.php?id=194226">
    <img src="img/katyperrypartofme.jpeg">             
    <h3>Katy Perry: Part of Me</h3>
    <p>PG - 7/5/2012<br/>
      <strong>Showtimes:</strong> 10:05 AM - 1:05 – 
      4:05 - 7:05 - 10:15
    </p>         
  </a>     
</li>     
<li>         
  <a href="moviedetails.php?id=193807">
    <img src="img/amazingspiderman.jpeg">             
    <h3>Amazing Spider-Man</h3>
    <p>PG-13 - 7/5/2012<br/>
      <strong>Showtimes:</strong> 10:00 AM - 1:00 – 
      4:00 - 7:00 - 10:00
    </p>         
  </a>     
</li> 

我们使用以下的页面片段来引入这个页面片段的:

$("#showing").load("showtimes.php", function(){ 
    $("#showing").listview('refresh'); 
});

在这种情况下,我们有包含仅列出视图项的 showtimes.php 文件,并且我们直接将它们注入到视图列表中,然后刷新。实现同样效果的另一种方法是拥有另一个文件,比如 fullshowtimes.php,它是一个完全渲染的页面,带有标题、页脚和其他一切。这在 JavaScript 或地理位置信息不可用且我们必须返回标准页面提交的情况下是完美的。

<?php  
  $documentTitle = "Showtimes | Northglen 16 Theatre";  
  $headerLeftHref = "/"; 
  $headerLeftLinkText = "Home"; 
  $headerLeftIcon = "home";  
  $headerTitle = "";  	
  $headerRightHref = "tel:8165555555"; 
  $headerRightLinkText = "Call"; 
  $headerRightIcon = "grid";  
  $fullSiteLinkHref = "/";  
?> 
<!DOCTYPE html> 
<html> 
<head> 
  <?php include("includes/meta.php"); ?>  
</head>  
<body> 
  <div id="qrfindclosest" data-role="page">     
    <div class="logoContainer ui-shadow"></div>     
    <div data-role="content">
      <h3>Northglen 14 Theatre</h3>

      <p><a href="https://maps.google.com/maps?q=Northglen+14+Theatre,+Northeast+80th+Street,+Kansas+City,+MO&hl=en&sll=38.304661,-92.437099&sspn=7.971484,8.470459&oq=northglen+&t=h&hq=Northglen+14+Theatre,&hnear=NE+80th+St,+Kansas+City,+Clay,+Missouri&z=15">4900 N.E. 80th Street<br>         
        Kansas City, MO 64119</a>
      </p>

      <p><a href="tel:8164681100">816-468-1100</a></p>                  
      <p>
        <ul id="showing" data-role="listview"class="movieListings" data-dividertheme="g">             
          <?php include("includes/showtimes.php"); ?>             
        </ul>
      </p>
    </div>     
    <?php include("includes/footer.php");?> 
  </div> 
</body> 
</html>

然后,我们不再仅仅使用页面调用加载函数,而是加载整个页面,然后使用以下代码选择我们要注入的页面元素:

$("#showing").load("fullshowtimes.php #showing li", function(){ 
  $("#showing").listview('refresh'); 
});

当然,这种做法效率较低,但值得注意的是,这样的事情是可以做到的。在未来,这几乎肯定会派上用场。

集成谷歌地图 API

到目前为止,我们已经很好地完成了自己的工作。我们可以告诉哪个影院最近,以及直线距离。不幸的是,尽管它有很多优点,但 21 世纪并没有让我们所有人都拥有私人喷气式背包。因此,最好不要显示那个距离。最有可能的是,他们会开车、乘坐公交车、骑自行车或步行。

让我们利用谷歌地图 API (developers.google.com/maps/documentation/javascript/)。如果您的网站要使用大量 API,请准备付费购买商业定价。对于我们来说,当我们处于开发阶段时,没有必要付费。

这是我们即将构建的样子:

集成谷歌地图 API

首先,我们需要另一页来显示地图和方向,以及将实际从谷歌地图 API 加载地图的脚本,使用以下代码:

<div id="directions" data-role="page"> 
  <div data-role="header">         
    <h3>Directions</h3>     
  </div>     
  <div data-role="footer">         
    <div data-role="navbar" class="directionsBar">             
      <ul>                 
        <li>
          <a href="#" id="drivingButton"onClick="showDirections('DRIVING')">
            <div class="icon driving"></div>
          </a>
        </li>                 
        <li>
          <a href="#" id="transitButton"onClick="showDirections('TRANSIT')">
            <div class="icon transit"></div>
          </a>
        </li>
        <li>
          <a href="#" id="bicycleButton"onClick="showDirections('BICYCLING')">
            <div class="icon bicycle"></div>
          </a>
        </li>                 
        <li>
          <a href="#" id="walkingButton"onClick="showDirections('WALKING')">
            <div class="icon walking"></div>
          </a>
        </li>
      </ul>
    </div> 
  </div>     
  <div id="map_canvas"></div>     
  <div data-role="content" id="directions-panel">
  </div> 
</div> 
<scriptsrc="img/js?sensor=true"></script>

我们页面有几个重要部分。首先是 footer 属性中的 navbar 属性,用于指向剧院的方向。您可能没有意识到的是,页脚实际上不一定要位于页面底部。当您在 footer 属性中使用 navbar 属性时,您单击的链接将保持其活动状态。如果没有周围的页脚,链接将仅闪烁一次活动状态,然后恢复正常。map_canvasdirections-panel 属性将由谷歌地图 API 填充。

现在,我们需要更新额外图标和地图约束的 CSS 代码。和以前一样,我们将它们保存在 /css/custom.css 的位置。

.directionsBar .icon{ 	  
  height:28px;   
  width:34px;   
  margin:auto;   
  background-repeat:no-repeat;   
  background-position:center center; 
} 

.directionsBar .driving{ 
  background-image:url(../icons/xtras-white/16-car.png); 
  background-size:34px 19px; 
} 
.directionsBar .transit{ 
  background-image:url(../icons/xtras-white/15-bus.png); 
  background-size:22px 28px; 
} 
.directionsBar .bicycle{ 	
  background-image:url(../icons/xtras-white/13-bicycle.png); 
  background-size:34px 21px; 
} 
.directionsBar .walking{ 
  background-image:url(../icons/icons-white/102-walk.png); 
  background-size:14px 27px; 
} 
.theaterAddress{ 
  padding-left:35px; 
  background-image:url(../icons/icons-gray/193-location-arrow.png); 
  background-size:24px 24px; 
  background-repeat:no-repeat;  
} 
.theaterPhone{ 
  padding-left:35px; 
  background-image:url(../icons/icons-gray/75-phone.png); 
  background-size:24px 24px; 
  background-repeat:no-repeat; 
  height: 24px;  
} 

#map_canvas { height: 150px; }  

@media only screen and (-webkit-min-device-pixel-ratio: 1.5),    
  only screen and (min--moz-device-pixel-ratio: 1.5),    
  only screen and (min-resolution: 240dpi) { 
    .directionsBar .driving{ 
      background-image:url(../icons/xtras-white/16-car@2x.png); 
    }
    .directionsBar .transit{ 
      background-image:url(../icons/xtras-white/15-bus@2x.png); 
    } 
    .directionsBar .bicycle{ 
      background-image:url(../icons/xtras-white/13bicycle@2x.png); 
    } 
    .directionsBar .walking{ 
      background-image:url(../icons/icons-white/102-walk@2x.png); 
    } 
    .theaterAddress{ 
      background-image:url(../icons/icons-gray/193-location-arrow@2x.png); 
    } 
    .theaterPhone{ 
      background-image:url(../icons/icons-gray/75-phone@2x.png); 
    } 
  }  

接下来,我们将在当前页面脚本中添加一些全局变量和函数。

var directionData = null; 
var directionDisplay; 	
var directionsService = new google.maps.DirectionsService(); 
var map; 

function showDirections(travelMode){ 
  var request = { 
    origin:latitude+","+longitude, 
    destination:closestTheater.geo.lat+","
      +closestTheater.geo.long, 
    travelMode: travelMode 
}; 

  directionsService.route(request, 
    function(response, status){ 
      if (status == google.maps.DirectionsStatus.OK){
        directionsDisplay.setDirections(response); 
      } 
    }); 

  $("#directions").live("pageshow", 
    function(){ 
      directionsDisplay = new google.maps.DirectionsRenderer(); 
      var userLocation = new google.maps.LatLng(latitude, longitude); 
      var mapOptions = {
        zoom:14, 
        mapTypeId: google.maps.MapTypeId.ROADMAP, 
        center: userLocation 
      } 
      map = new google.maps.Map(   
        document.getElementById('map_canvas'), mapOptions);
        directionsDisplay.setMap(map);   
        directionsDisplay.setPanel(
        document.getElementById('directions-panel')
      ); 
      showDirections(
      google.maps.DirectionsTravelMode.DRIVING
  ); 
  $("#drivingButton").click(); 
});

在这里,我们看到了用于保存谷歌对象的全局变量。showDirections 方法被设计为接受一个表示四种不同出行方式的字符串:'DRIVING''TRANSIT''BICYCLING''WALKING'

我们可以在弄清最近的影院的同时填充地图和方向。这实际上会为用户带来很好的体验。然而,没有分析数据显示大多数人确实需要方向,那么产生这些成本就没有意义了。最终,这是一个商业决策,但是任何规模的客户群体都可能受到 API 成本的打击。目前来看,最好在用户转到directions页面时触发地图和方向的加载。

极客时刻—GPS 监控

那么,让我们来极客一分钟。我们所做的对于大多数情况可能已经足够了。我们展示了一张地图和逐步转向指南。让我们再进一步。地理位置 API 不仅仅确定您当前的位置。它包括一个时间戳(没什么大不了的)并且可以允许您使用方法navigator.geolocation.watchPositiondev.w3.org/geo/api/spec-source.html#watch-position)连续监视用户的位置。这意味着,只需要一点点努力,我们就可以将我们之前的方向页面变成一个持续更新的方向页面。在示例代码中,所有这些都包含在文件qrresponse2.php中。

再次更新太频繁可能会变得很昂贵。因此,我们应该真正限制地图和方向的重新绘制频率。对于每种交通模式,更新之间需要的有意义时间量是不同的。趁热打铁,让我们重新设计按钮以包含这些选项。这是整个页面的代码:

<?php  
  $documentTitle = "Dickinson Theatres";  

  $headerLeftHref = "/"; 
  $headerLeftLinkText = "Home"; 
  $headerLeftIcon = "home";  

  $headerTitle = "";  	

  $headerRightHref = "tel:8165555555"; 
  $headerRightLinkText = "Call"; 
  $headerRightIcon = "grid";  

  $fullSiteLinkHref = "/";  
?> 
<!DOCTYPE html> 
<html> 
<head> 
  <?php include("includes/meta.php"); ?> 
  <style type="text/css"> 
    .logoContainer{ 
      display:block; 
      height:84px; 
      background-image:url(images/header.png);  
      background-position:top center;   
      background-size:885px 84px;
      background-repeat:no-repeat;
    }  
  </style>     
  <script type="text/javascript"src="img/js?key=asdfafefaewfacaevaeaceebvaewaewbk&sensor=true"></script> 
</head>  
<body> 
  <div id="qrfindclosest" data-role="page">
    <div class="logoContainer ui-shadow"></div>
    <div data-role="content">
      <div id="latLong">
        <form id="findTheaterForm" action="fullshowtimes.php"method="get" class="validateMe">
          <p>
            <label for="zip">Enter Zip Code</label>
            <input type="tel" name="zip" id="zip"class="required number"/>
          </p>
          <p><input type="submit" value="Go"></p>              
        </form>
      </div>
      <p>         
        <ul id="showing" data-role="listview"class="movieListings" data-dividertheme="g">
        </ul>         
      </p>     
    </div>          
    <?php include("includes/footer.php"); ?> 
  </div>  

  <div id="directions" data-role="page">
    <div data-role="header">
      <h3>Directions</h3>
    </div>
    <div data-role="footer">
      <div data-role="navbar" class="directionsBar">             
        <ul>
          <li>
            <a href="#" id="drivingButton"data-transMode="DRIVING" data-interval="10000"
>
              <div class="icon driving"></div>
            </a>
          </li>
          <li>
            <a href="#" id="transitButton"data-transMode="TRANSIT" data-interval="10000">
              <div class="icon transit"></div>
            </a>
          </li>
          <li>
            <a href="#
" id="bicycleButton"data-transMode="BICYCLING" data-interval="30000">
              <div class="icon bicycle"></div>
            </a>
          </li>
          <li>
            <a href="#" id="walkingButton
"data-transMode="WALKING" data-interval="60000">
              <div class="icon walking"></div>
            </a>
          </li>
        </ul>
      </div>
    </div>
    <div id="map_canvas"></div>
    <div data-role="content" id="directions-panel"></div> 
  </div> 

那么,现在让我们看一下此 GPS 监控版本的页面脚本:

  <script type="text/javascript"> 
    //declare our global variables 
    var theaterData = null; 
    var timestamp = null; 
    var latitude = null; 
    var longitude = null; 
    var closestTheater = null; 
    var directionData = null; 
    var directionDisplay; 
    var directionsService = new 
      google.maps.DirectionsService(); 
    var map; 
    var positionUpdateInterval = null; 
    var transporationMethod = null;   

    //Once the page is initialized, hide the manual zip form 
    //and place a message saying that we're attempting to find their location. 
    $(document).on("pageinit", "#qrfindclosest", function(){ 
      if(navigator.geolocation){ 
        $("#findTheaterForm").hide(); 
        $("#latLong").append("<p id='finding'>Finding your 
           location...</p>");
      } 
    }); 

    $(document).on("pageshow", "#qrfindclosest", function(){ 
      theaterData = $.getJSON("js/theaters.js", 
        function(data){ 
          theaterData = data; 
          selectClosestTheater(); 
    }); 

 $("div.directionsBar a").click(function(){
 if(positionUpdateInterval != null){ 
 clearInterval(positionUpdateInterval);
 } 
 var $link = $(this);
      transporationMethod = $link.attr("data-transMode"); 
 showDirections(); 
 setInterval(function(){
 showDirections(); 
        },Number($link.attr("data-interval"))); 
 }); 

    function showDirections(){
      var request = {
        origin:latitude+","+longitude,   
          destination:closestTheater.geo.lat+","
          +closestTheater.geo.long,
        travelMode: transportationMethod
      }

      directionsService.route(request, 
        function(response, status) { 
          if (status == google.maps.DirectionsStatus.OK){       directionsDisplay.setDirections(response);
          }
      }); 
    }  

    $(document).on("pageshow", "#directions", function(){  
      directionsDisplay = new google.maps.DirectionsRenderer();
      var userLocation = new google.maps.LatLng(latitude, longitude);
      var mapOptions = {
        zoom:14,
        mapTypeId: google.maps.MapTypeId.ROADMAP, 
        center: userLocation
      }
      map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions); 
      directionsDisplay.setMap(map);   
      directionsDisplay.setPanel(
        document.getElementById('directions-panel')); 
 if(positionUpdateInterval == null) { 
        transportationMethod = "DRIVING"; 
 positionUpdateInterval = setInterval(function(){
 showDirections(); 
 },(10000)); 
      } 
      $("#drivingButton").click();
  });

  function selectClosestTheater(){ 
 var watchId=navigator.geolocation.watchPosition(
        function(position){ //success 
        latitude = position.coords.latitude;
        longitude = position.coords.longitude; 
        timestamp = position.timestamp;
        var dt = new Date();
        dt.setTime(timestamp);

        for(var x = 0; x < theaterData.theaters.length; x++){ 
          var theater = theaterData.theaters[x]; 
          var distance = getDistance(latitude, longitude, 
            theater.geo.lat, theater.geo.long); 
          theaterData.theaters[x].distance = distance;      } 

        theaterData.theaters.sort(compareDistances);  
        closestTheater = theaterData.theaters[0]; 

        $("#latLong").html("<div class='theaterName'>"
          +closestTheater.name
          +"</div><p class='theaterAddress'><a href='#directions'>"         
          +closestTheater.address+"<br/>"
          +closestTheater.city+", "
          +closestTheater.state
          +" "+closestTheater.zip
          +"</a></p><p class='theaterPhone'><a href='tel:"
          +closestTheater.phone+"'>"
          +closestTheater.phone+"</a></p>"
        );

        $("#showing").load("fullshowtimes.php #showing li", 
          function(){ 	
            $("#showing").listview('refresh'); 
        });
      }
    }, 
    function(error){ //error    
     $("#findTheaterForm").show();   
     $("#finding").hide();
     switch(error.code) { 
       case error.TIMEOUT: 
         $("#latLong").prepend("<div class='ui-bar-e'>Unable to get your position: Timeout</div>"); 
         break;
       case error.POSITION_UNAVAILABLE: 
         $("#latLong").prepend("<div class='ui-bar-e'>Unable to get your position: Position unavailable</div>");
         break;
     case error.PERMISSION_DENIED: 
       $("#latLong").prepend("<div class='ui-bar-e'>Unable to get your position: Permission denied.You may want to check your settings.</div>"); 
         break;
       case error.UNKNOWN_ERROR: 
         $("#latLong").prepend("<div class='ui-bar-e'>Unknown error while trying to access your position.</div>"); 
         break; 
     }
  }); 
}  
</script>   
</body> 
</html> 

链接和嵌入视频

预览是电影行业的一个主打。我们可以像许多人一样直接链接到 YouTube 上的预览。这里是一个简单的做法:

<p><a data-role="button"href="http://www.youtube.com/watch?v=J9DlV9qwtF0">Watch Preview</a></p> 

链接和嵌入视频

那样会起作用,但问题是它会把用户带离您的网站。尽管从用户的角度来看这可能并不是世界末日,但这是一个大忌。

为了改善用户体验并将用户留在我们自己的网站上,让我们直接嵌入 HTML5 视频,并像我们在这里描述的那样使用通用图像作为电影预览。

尽管看起来它将在页面的一个极小的部分中播放,但在智能手机上,视频将以全屏横向模式播放。在 iPad 上的情况有些不同,它将在内嵌侧边以内联方式播放。

最终,我们希望使用以下代码将适合用户设备的合适尺寸的视频返回给用户。没有高分辨率显示屏的智能手机不会真的受益于 720p 视频。

<video id="preview" width="100%" controlsposter="images/preview.gif"> 

  <source src="img/batmanTrailer-2_720.mp4" type="video/mp4"  media="only screen and (-webkit-min-device-pixel-ratio: 1.5),only screen and (min--moz-device-pixel-ratio: 1.5),only screen and (min-resolution: 240dpi)"/>                 

  <source src="img/batmanTrailer-1_480.mov"type="video/mov" />                 

  <a data-role="button"href="http://www.youtube.com/watch?v=J9DlV9qwtF0">Watch Preview</a> 

</video>  

如果浏览器识别 HTML5 视频标签,播放器将从顶部开始查找每个源标签,直到找到一个它知道如何播放并符合正确的媒体查询(如果已指定媒体查询)。如果浏览器不支持 HTML5 视频,它将不知道如何处理视频和源标签,并且简单地将它们视为有效的 XML 元素。它们将被当作多余的 div 标签,并显示链接按钮。

正如你所见,我们在这里添加了媒体查询到不同的资源。如果是高分辨率屏幕,我们将加载更漂亮的视频。你可以真正地通过添加许多不同的来源来深入研究:为普通智能手机添加一个 480p 的视频,为 iPhone 和早期的 iPad 添加一个 720p 的视频,为第三代 iPad 添加一个 1080p 的视频。这里唯一需要注意的是,即使苹果视网膜显示屏能够显示更美丽的视频,但它仍然必须通过同样的管道传输。加载一个较小的视频可能仍然更好,因为它会更快播放,并为客户节省带宽成本。

让我们为这张图片添加一点 CSS。我们将图片宽度保留在容器的 100%。在智能手机上,随着宽度的增加,图片比例将正确缩放。iPad 则不太一样。因此,让我们使用媒体查询来检测其屏幕分辨率,并为其指定一个显式高度,以更好地利用屏幕空间。

 /* iPad ----------------*/ @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) { 
  #preview{ height:380px;} 
}

总结

我们已经在智能手机上探索了现代媒体的边界。现在你可以思考一下 QR 码的用途,并利用它,找出用户所在位置,监视用户的位置,从谷歌获取方向和地图,并向用户提供响应式视频。

想想你刚刚学到的所有内容。创建一个社交连接的网站,允许用户获取彼此位置的地图,并随着彼此的接近或远离而持续更新,这有多难?如果包装和营销得当,那是很有价值的。

在下一章中,我们将利用 GPS 来提取你地理区域内的 Twitter 动态。我们还将研究从其他几个来源提取动态的方法,比如 reddit、RSS 动态等等。这将非常有趣。这是我最喜欢写的章节之一。

第五章:客户端模板化、JSON API 和 HTML5 Web 存储

我们已经走了很长一段路,为业务准备了一些相当庞大的默认模板和样板。在这一章中,我们将简化并专注于其他事项。我们将创建一个基于社交媒体的新闻聚合网站。到目前为止,我们一直非常重视渐进式增强。在本章中,我们将放弃这一点。这将需要 JavaScript。

在这一章中,您将学到以下内容:

  • 客户端模板化选项

  • JsRender

  • 联接到 JSON API(Twitter)

  • 以编程方式更改页面

  • 生成的页面和 DOM 权重管理

  • 利用 RSS 订阅(本地化)

  • HTML5 Web 存储

  • 利用 Google Feeds API

客户端模板化

(以一个脾气暴躁的老人的声音)在我那个年代,我们在服务器上渲染所有页面,我们喜欢这样!哈哈!时代正在变化,我们看到客户端模板化框架的巨大潮流。它们的核心都差不多,即它们接收 JSON 数据并应用在一个包含在 script 标签中的基于 HTML 的模板上。

如果你知道JSON是什么,跳过这一段。上一章我花了一点时间讨论了这个问题,但是万一你跳过了并且不知道,JSON 是用 JavaScript 编写的,以便可以用作数据交换格式。它比 XML 更高效,并且以面向对象的方式立即被浏览器解释。JSON 甚至可以使用 JSONP 跨域请求数据。有关 JSON 的更多信息,请阅读en.wikipedia.org/wiki/JSON。有关 JSONP 的更多信息,请阅读en.wikipedia.org/wiki/JSONP

所有这些客户端库都有一些标记,显示数据的去向,并提供实现循环和条件语句的方法。有些是“无逻辑”的,并且根据尽可能少的逻辑的理念运行。如果你赞同这种美妙的学术方法,那太棒了。

老实说,从纯粹实用的角度来看,我认为模板是代码的完美容器。越灵活越好。JSON 保存数据,而模板用于转换数据。打个比方,XML 是数据格式,XSL 模板用于转换数据。没有人在 XSL 中抱怨逻辑;所以,我不明白为什么在 JS 模板中会成为问题。但是,所有这些讨论都是纯学术性的。最终,它们几乎都能做你想做的事情。如果你更多的是设计师而不是编码者,你可能会更多地关注无逻辑的模板。

以下是一个相当详尽的客户端模板化框架列表。我可能会漏掉一些,而且到这本书出版时可能会有更多,但这是一个开始。

  • doT

  • dust.js

  • Eco

  • EJS

  • Google Closure Templates

  • handlebars

  • haml-js

  • kite

  • Jade

  • jQote2

  • jQuery 模板(已停止)

  • jsRender / jsView

  • Parrot

  • node-asyncEJS

  • Nun

  • Mu

  • mustache

  • montage

  • Stencil

  • underscore.js

现在,虽然我是一个粉丝,但是,如果它是官方的 jQuery,我喜欢它。因此,我尝试的第一件事是 jQuery Templates。遗憾的是,在我刚学会喜欢它不久之后,jQuery 团队放弃了这个项目,并指向 JsRender 作为项目的延续。未来是否会持续沿着这个方向是另一个问题,但是,目前,JsRender 的功能和强大性使其成为一个引人注目的选择,并且是本章其余部分模板工作的基础。更不用说,它只有经过精简的 14k 并且速度快如闪电。您可以从 github.com/BorisMoore/jsrender 下载最新版本。

如果您正在寻找帮助以决定适合您的正确模板框架,那么在本章节审阅过程中,Andy Matthews 很友好地提供了以下链接:garann.github.com/template-chooser/。它讨论了几个框架的优点,帮助您做出明智的选择。谢谢,Andy!

连接至 JSON API(Twitter)

观看 Twitter 上的热门话题总是很有趣。就像许多其他受欢迎的在线目的地一样,它具有 JSON API。让我们来玩一下。这是我们要构建的内容。您可以在左侧看到列表视图,在右侧看到搜索视图。

连接到 JSON API(Twitter)

在这一点上,我将放弃从 HTML 中分离出 CSS 和 JS 的学术正确做法。除了库之外,所有特定于页面的代码(HTML、CSS 和 JS)将位于单个页面内。以下代码是我们起始的基本页面。在本章的代码包中,它是twitter.html

<!DOCTYPE html>  
<html>  
  <head>   
    <meta charset="utf-8">   
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">   
    <title>Chapter 5 - News</title>       
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.css" />   
    <script src="img/jquery-1.8.2.min.js"></script>  
    <script src="img/jsrender.min.js" type="text/javascript"></script>
  <script src="img/jquery.mobile-1.3.0.min.js"></script>

下面的样式将帮助我们的 Twitter 结果看起来更像 Twitter:

    <style type="text/css">     
      .twitterItem .ui-li-has-thumb .ui-btn-inner a.ui-link-inherit, #results .ui-li-static.ui-li-has-thumb{       
         min-height: 50px;       
         padding-left: 70px;     
      } 
      .twitterItem .ui-li-thumb, #results .ui-listview .ui-li-icon, #results .ui-li-content{       
         margin-top: 10px;
         margin-left: 10px;     
      }     
      .twitterItem .ui-li-desc{       
         white-space:normal;       
         margin-left:-25px;       
      }     
      .twitterItem .handle{       
        font-size:80%;       
        font-weight:normal;         
        color:#aaa;     
      }     
      .twitterItem .ui-li-heading{       
        margin: 0 0 .6em -25px;     
      }   
    </style> 
  </head>   
  <body>  

这个页面基本上只是一个占位符,一旦从 Twitter API 获取到结果,它将被填充:

  <div id="home_page" data-role="page"> 	
    <div data-role="header"><h1>Now Trending</h1></div>   
    <div data-role="content">
      <ul id="results" data-role="listview" data-dividertheme="b">
      </ul>
    </div>
  </div>  

下面的脚本是页面的处理核心。

  <script type="text/javascript"> 
    $(document).on("pagebeforeshow", "#home_page",  function(){ 	

     //before we show the page, go get the trending topics
     //from twitter
    $.ajax({       
      url:"https://api.twitter.com/1/trends/daily.json",
        dataType:"jsonp",       
        success: function(data) {       
          var keys = Object.keys(data.trends);       

          //Invoke jsRender on the template and pass in
          //the data to be used in the rendering.
          var content = $("#twitterTendingTemplate")
           .render(data.trends[keys[0]]);

          //Inject the rendered content into the results area 
          //and refresh the listview
          $("#results").html( content ).listview("refresh"); 
        }	
      })
      .error(function(jqXHR, textStatus, errorThrown){                  
        alert(textStatus+" - "+errorThrown);     
      });
    });    

    $(document).on('click', 'a.twitterSearch', function(){     
      var searchTerm = $(this).attr("data-search");     

      //take the search term from the clicked element and 
      //do a search with the Twitter API
      $.ajax({        
        url:"http://search.twitter.com/search.json?q="+escape(searchTerm),        
        dataType:"jsonp",       
        success: function(data){

          //create a unique page ID based on the search term
          data.pageId = searchTerm.replace(/[# ]*/g,"");             
          //add the search term to the data object
          data.searchTerm = searchTerm; 

          //render the template with JsRender and the data    
          var content = $("#twitterSearchPageTemplate").render(data);  

          //The rendered content is a full jQuery Mobile 
          //page with a unique ID.  Append it directly to the 
          //body element
          $(document.body).append(content); 	

          //switch to the newly injected page
          $.mobile.changePage("#"+data.pageId);       
        }     
      })
      .error(function(jqXHR, textStatus, errorThrown){                  
        alert(textStatus+" - "+errorThrown);     
      });   
    });     
  </script>  

以下是两个 JsRender 模板:

  <script id="twitterTendingTemplate" type="text/x-jsrender"> 
    <li class="trendingItem">     
      <a href="javascript://" class="twitterSearch" data-search="{{>name}}">       
        <h3>{{>name}}</h3>     
      </a>   
    </li> 
  </script>  

  <script id="twitterSearchPageTemplate" type="text/x-jsrender">   
    <div id="{{>pageId}}" data-role="page" data-add-back-btn="true">     
      <div data-role="header">
        <h1>{{>searchTerm}}</h1>
      </div>     
      <div data-role="content">
        <ul id="results" data-role="listview" data-dividertheme="b">
          {{for results}}           
            <li class="twitterItem">             
            <a href="http://twitter.com/{{>from_user}}">   
              <img src="img/{{>profile_image_url}}" alt="{{>from_user_name}}" class="ui-shadow ui-corner-all" /> 
              <h3>{{>from_user_name}} 
                <span class="handle">
                  (@{{>from_user}})<br/>
                  {{>location}} 
                  {{if geo}}
                    {{>geo}}
                  {{/if}}
                </span>
              </h3>               
              <p>{{>text}}</p>             
            </a>           
          </li>         
        {{/for}} 	      
      </ul>     
    </div>   
  </div> 
</script>  
</body> 
</html>

好吧,一次把那么多代码给你可能有点多,但大部分代码在这一点上看起来应该相当熟悉。让我们开始解释一些最新的东西。

通常,要将数据加载到网页中,即使您正在获取 JSON 格式的数据,也会受到同源策略的限制。然而,如果数据来自另一个域,您将需要绕过同源策略。为了绕过同源策略,您可以使用某种服务器端代理,例如 PHP 的 cURLphp.net/manual/en/book.curl.php)或 Java 世界中的 Apache HTTP Core Componentshc.apache.org/)。

让我们保持简单,使用JSONP(也称为JSON with Padding)。JSONP 不使用常规的 Ajax 请求来获取信息。尽管配置选项是为$.ajax命令,但在幕后,它将以独立的脚本标签执行数据调用,如下所示:

 <script type="text/javascript" src="img/daily.json?callback=jQuery172003156238095834851_1345608708562&_=1345608708657"></script>

值得注意的是,通过 JSONP 使用 GET 请求。这意味着你不能用它传递敏感数据,因为它会通过网络流量扫描或简单查看浏览器的请求历史立即可见。所以,请不要通过 JSONP 登录或传递任何敏感信息。明白了吗?

在实际请求发出之前,jQuery 会创建一个半随机的函数名称,一旦从服务器收到响应,该函数将被执行。通过在 URL 中附加该函数名称作为回调,我们告诉 Twitter 用这个函数调用包裹他们发给我们的响应。因此,我们不会收到类似 {"trends": …}, 这样的 JSON 脚本,而是在我们页面上编写的脚本如下所示:

jQuery172003156238095834851_1345608708562({"trends": …}). 

这能够运行的原因是同域策略对于脚本并不存在。方便,对吧?在脚本加载完并且回调处理完成之后,我们将以 JSON 格式获得数据。最终,在底层执行上有着截然不同,但结果与你通过自己域上的常规getJSON请求获得的结果是一样的。

以下是从 Twitter 返回的响应片段:

jQuery1720026425381423905492_1345774796764({
  "as_of": 1345774741,
  "trends": {
    "2012-08-23 05:20": [
       {
         "events": null,
         "name": "#ThingsISayTooMuch",
         "query": "#ThingsISayTooMuch",
         "promoted_content": null
       },
       {
         "events": null,
         "name": "#QuieroUnBesoDe",
         "query": "#QuieroUnBesoDe",
         "promoted_content": null
       },
       {
          "events": null,
          "name": "#ASongIKnowAllTheLyricsTo",
          "query": "#ASongIKnowAllTheLyricsTo",
          "promoted_content": null
       },

接下来,我们将响应精简到我们想要的部分(最新一组热门话题),并将该数组传递给 JsRender 进行渲染。也许直接循环遍历 JSON 并使用字符串连接来构建输出可能更简单,但看看下面的模板,告诉我这不会更清晰易维护:

<script id="twitterTendingTemplate" type="text/x-jsrender"> 
  <li class="trendingItem">     
    <a href="javascript://" class="twitterSearch" data-search="{{>name}}">       
      <h3>{{>name}}</h3>     
    </a>   
  </li> 
</script>  

脚本上的text/x-jsrender类型将确保页面不会尝试解析内部内容为 JavaScript。由于我们向 JsRender 传入了一个数组,模板将为数组中的每个对象编写。这样就简单了!尽管我们只从数据对象中提取了名称,但你明白这是如何工作的。

让我们来看看下一个重要的 JavaScript 代码块:

$(document).on('click', "a.twitterSearch", function(){     
  //grab the search term off the link     
  var searchTerm = $(this).attr("data-search");          

  //do a Twitter search based on that term     
  $.ajax({       url:"http://search.twitter.com/search.json?q="+escape(searchTerm),        
   dataType:"jsonp",       
   success: function(data){         
     //create the pageID by stripping 
     //all non-alphanumeric data         
     var pageId = searchTerm.replace(/[^a-zA-Z0-9]+/g,"");                  
     //throw the pageId and original search term 
     //into the data that we'll be sending to JSRenderdata.pageId = pageId;
     data.searchTerm = searchTerm;          	      

     //render the page and append it to the document body         $(document.body).append($("#twitterSearchPageTemplate")
       .render(data));                  

     //set the page to remove itself once left          
     $("#"+pageId).attr( "data-" + $.mobile.ns 
       + "external-page", true )
       .one( 'pagecreate', $.mobile._bindPageRemove );                  
     //switch to the new page          
     $.mobile.changePage("#"+data.pageId);   
    }
  })
  .error(function(jqXHR, textStatus, errorThrown){
    //If anything goes wrong, at least we'll know.           
    alert(textStatus+" - "+errorThrown);     
  });    
});

首先,我们从链接本身的属性中提取搜索词。搜索词本身作为用于动态渲染页面的id属性有些不合适,因此,我们将去除任何空格和非字母数字内容。然后,我们将pageIdsearchTerm属性附加到我们从 Twitter 那里收到的 JSON 对象上。以下是从这个调用返回的数据样本:

jQuery1720026425381423905492_1345774796765({
    "completed_in": 0.02,
    "max_id": 238829616129777665,
    "max_id_str": "238829616129777665",
    "next_page": "?page=2&max_id=238829616129777665&q=%23ThingsISayTooMuch",
    "page": 1,
    "query": "%23ThingsISayToMuch",
    "refresh_url": "?since_id=238829616129777665&q=%23ThingsISay
TooMuch",
    "results": [
        {
            "created_at": "Fri, 24 Aug 2012 02:46:24 +0000",
            "from_user": "MichelleEspra",
            "from_user_id": 183194730,
            "from_user_id_str": "183194730",
            "from_user_name": "Michelle Espranita",
            "geo": null,
            "id": 238829583808483328,
            "id_str": "238829583808483328",
            "iso_language_code": "en",
            "metadata": {
                "result_type": "recent"
            },
            "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/2315127236\/Photo_20on_202012-03-03_20at_2001.39_20_232_normal.jpg",
            "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/2315127236\/Photo_20on_202012-03-03_20at_2001.39_20_232_normal.jpg",
            "source": "&lt;a href=&quot;http:\/\/twitter.com\/&quot;&gt;web&lt;\/a&gt;",
            "text": "RT @MuchOfficial: @MichelleEspra I'd be the aforementioned Much! #ThingsISayTooMuch",
            "to_user": null,
            "to_user_id": 0,
            "to_user_id_str": "0",
            "to_user_name": null,
            "in_reply_to_status_id": 238518389595840512,
            "in_reply_to_status_id_str": "238518389595840512"
        }

}

因此,我们将获取到的响应传递给渲染器,以便根据twitterSearchPageTemplate进行转换:

<script id="twitterSearchPageTemplate" type="text/x-jsrender"> 
    <div id="{{>pageId}}" data-role="page" data-add-back-btn="true">     
      <div data-role="header">
        <h1>{{>searchTerm}}</h1>
      </div>     
      <div data-role="content">
        <ul id="results" data-role="listview" data-dividertheme="b">
          {{for results}}           
            <li class="twitterItem">             
            <a href="http://twitter.com/{{>from_user}}">   
              <img src="img/{{>profile_image_url}}" alt="{{>from_user_name}}" class="ui-shadow ui-corner-all" /> 
              <h3>{{>from_user_name}} 
                <span class="handle">
                  (@{{>from_user}})<br/>
                  {{>location}} 
                    {{if geo}}
                      {{>geo}}
                    {{/if}}
                </span>
              </h3>               
              <p>{{>text}}</p>             
            </a>           
          </li>         
        {{/for}}       
      </ul>     
    </div>   
  </div> 
</script> 

这些是简单的实现。 GitHub 上的示例展示了许多值得探索的选项。查看borismoore.github.com/jsrender/demos/以获取有关创建更复杂模板的详细信息。这是一个变化迅速的库(大多数客户端模板库都是如此)。因此,如果你在阅读本文时,发现有更多选项和略有变化的语法,不要感到惊讶。

一旦我们获得了转换的结果,我们就可以将新页面的源附加到文档的主体,然后以编程方式切换到这个新页面。

以编程方式更改页面

有两种方法可以在 jQuery Mobile 中以编程方式更改页面,它们之间的区别很微妙:

  • 调用$.mobile.changePage并传递一个选择器到你想要跳转到的页面的 ID。这与 URL 的工作方式相同。无论哪种方式都会产生与用户点击链接相同的结果。该页面将被插入浏览器的历史记录中,正如人们所期望的那样。以下是示例代码:

    $.mobile.changePage("#"+data.pageId);
    
  • 首先通过选择要更改的页面来创建一个 jQuery 对象。然后,将该 jQuery 对象传递到$.mobile.changePage函数中。结果是页面被显示,但 URL 永远不会更新,因此它不会存在于浏览器的历史记录中。这在用户刷新页面时重新开始第一个屏幕的过程时可能会有用。它防止了通过书签进行深度链接到多页布局中的其他页面。以下是一个示例:

    var $newPage = $("#"+data.pageId);     
    $.mobile.changePage($newPage);
    

生成的页面和 DOM 负载管理

在正常情况下,当在传统移动站点上浏览时,jQuery Mobile 将每个页面标记为external-page,这将导致用户导航离开该页面后从 DOM 中移除该页面。这样做的理念是,它将管理 DOM 的负载,因为“预算”(糟糕的)设备可能没有足够的内存来专用于其浏览器。外部页面很可能仍然在设备缓存中以便快速召回。因此,重新加载它们应该是极快的。如果你想了解 jQuery Mobile 如何处理此行为,请查看jquerymobile.com/demos/1.3.0/docs/pages/page-cache.html

jQuery Mobile 通过正常手段很好地管理 DOM 的负载。然而,当我们动态创建页面时,它们不会在退出时自动从 DOM 中删除。如果有很多这样的页面,这可能会变得非常压倒性。我们很容易就会压垮愚蠢手机上的可怜浏览器,甚至一些早期型号或预算智能手机也是如此。如果动态创建的页面可能在会话中再次查看,则将其留在 DOM 中可能是值得的。然而,由于我们开始时是在浏览器中生成它,所以将页面重新呈现可能更安全更快速。

在页面呈现完成但在页面初始化之前,你可以使用这行代码标记一页删除:

$("#"+pageId).attr( "data-" + $.mobile.ns + "external-page", true ).one( 'pagecreate', $.mobile._bindPageRemove );

注意

警告:这行代码基本上是直接从库代码中来的。这就是它们在幕后是如何做的。请注意,$.mobile._bindPageRemove以一个下划线开头。这里我们没有处理一个公共方法。

这段代码是 API 的一个未记录和非官方部分,这意味着它可能在任何发布的版本中被更改。这对于框架的核心部分来说,我怀疑它们会更改;但是,任何时候当你开始引入依赖于非公开 API 的代码时,你都面临着升级可能在发布说明中没有任何警告的情况下破坏你的代码的风险。可以自由使用,但是务必对每个库的升级进行彻底测试。

利用 RSS 源

我能说什么呢?正是我的编辑让我这么做的。最初我并没有计划围绕 RSS 构建任何东西。我很高兴他们这样做,因为经过调查,发现被 RSS 提供的信息要比 JSON 信息源要多得多。我觉得数字世界比它实际上发展得更多。所以,Usha,谢谢你让我包括这个。

首先,如果我们不使用服务器端代理,我们将立即遇到同一源策略的严苛限制。示例包括 PHP 系统中的 cURL,Java 中的 Apache HTTP Core 组件,或者是.NET 平台上的 HttpWebRequest 等。

以下是我在 PHP 中创建的页面,利用 cURL 抓取 Ars Technica 的信息流。这个文件的源代码在本章的代码包中的ars.php中。

<?PHP 

//based on original example from…
//http://www.jonasjohn.de/snippets/php/curl-example.htm

//is cURL installed yet? 
if (!function_exists('curl_init')){     
  die('Sorry cURL is not installed!'); 
}  

// OK cool. Then, let's create a new cURL resource handle 
$ch = curl_init();  

// Now set some options (most are optional)  
// Set URL to download 
curl_setopt($ch, CURLOPT_URL, "http://feeds.arstechnica.com/arstechnica/index?format=xml");  

// Set a referer 
curl_setopt($ch, CURLOPT_REFERER, "http://bookexample/chapter5");  

// User agent 
curl_setopt($ch, CURLOPT_USERAGENT, "BookExampleCurl/1.0");  

// Include header in result? (0 = yes, 1 = no) 
curl_setopt($ch, CURLOPT_HEADER, 0);  

// Should cURL return or print out the data? 
// (true = return, false = print) 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  

// Timeout in seconds 
curl_setopt($ch, CURLOPT_TIMEOUT, 10);  

// Download the given URL, and return output 
$output = curl_exec($ch);  

// Close the cURL resource, and free system resources curl_close($ch);  

echo $output; 
?>

注意

警告:cURL 和其他服务器端代理库非常强大,因此也非常危险。不要把你想要访问本页的 URL 参数化。硬编码 URL。如果你必须从调用 URL 获取参数来构建你的目标地址,那么你必须转义所有的参数。如果不这样做,可以肯定的是总有一天,黑客会利用你的网站进行跨站脚本(XSS)攻击(www.owasp.org/index.php/Cross-site_Scripting_(XSS))。

接下来,让我们在顶部添加一些按钮。一个是我们的 Twitter 源,一个是 Ars Technica 的。下一部分的最终来源将在本章的代码包中的index.html文件中:

<div data-role="header">
  <h1>News</h1>
</div>     
<div data-role="footer"> 
 <div data-role="navbar"> 
 <ul> 
 <li><a id="twitter" href="#" class="ui-btn-active">Twitter</a></li> 
 <li><a id="ars" href="#">Feed</a></li> 
 </ul> 
 </div> 
</div>
<div data-role="content">	         
  <ul id="results" data-role="listview" data-dividertheme="b"></ul>   
</div> 

接下来,让我们添加脚本来加载信息流:

function loadArs(){
  //scroll back up to the top     
  $.mobile.silentScroll(0);          

  //Go get the Ars Technica feed content     
  $.ajax({       
    url:"ars.php",        
    dataType:"xml",       
    success: function(data, textStatus, jqXHR) {         

      //Store the response for later use           
      localStorage.setItem("ars", jqXHR.responseText);            
      //prepare the content for use         
      var $feed = $(data);                  

      //prepare a list divider with the title of the feed.	var listView = "<li data-role='list-divider'>"+$feed.find("channel>title").text()+"</li>";                  
     //loop through every feed item and 
     //create a listview element.          
      $feed.find("channel>item").each(function(index){             var $item = $(this);           
        listView += "<li><a href='javascript://' "
          +"data-storyIndex='"+index
          +"' class='arsFeed'><h3>"
          +$item.find("title").text()
          +"</h3><p>"+$item.find("pubDate").text()
          +"</p></a></li>";         
      });                  

      //put the new listview in the main display          
      $("#results").html(listView); 

      //refresh the listview so it looks right         
      $("#results").listview("refresh");   

     //place hooks on the newly created links         
      //so they trigger the display of the         
      //story when clicked         
      $("#results a.arsFeed").click(function(){         

        //get the feed content back out of storage                var arsData = localStorage.getItem("ars");                 
        //figure out which story was clicked and       
        //pull that story's content from the item             var storyIndex = $(this).attr("data-storyIndex");
        var $item =   
          $(arsData).find("channel>item:eq("+storyIndex+")");                     
        //create a new page with the story content                var storyPage = "<div id='ars"+storyIndex+"' "
          +"data-role='page' data-add-back-btn='true'>"
          +"<div data-role='header'><h1>Ars Technica</h1>"
          +"</div><div data-role='content'><h2>"
          +$item.find('title').text()+"</h2>"
          +$item.find('content\\:encoded').html()
          +"</div></div>";                      

        //append the story page to the body 	        
        $("body").append(storyPage);                   
        //find all the images in the newly  	        
        //created page.          
        $("#ars"+storyIndex+" img").each(function(index, element) {                         
          var $img = $(element);                         
          //figure out its currentWidth             
          var currentWidth = Number($img.attr("width"));                          
          //if it has a width and it's large             
          if(!isNaN(currentWidth) && currentWidth > 300){              
            //remove the explicit width and height                  $img.removeAttr("width").removeAttr("height");               
            //make the image scale to the width                     //of it's container but never to be                      //larger than its original size                          
            $img.css({"max-width":currentWidth
              +"px","width":"100%"});             
          }
        });

        //switch to the new page             
        $.mobile.changePage("#ars"+storyIndex);        
      });
    }
  });   
}

$("#ars").click(loadArs); 

这是我们的新的 RSS 阅读器的样子!

利用 RSS 源

强制响应式图片

当你从一个页面导入内容,而你无法控制内容中嵌入的图片时,你可能需要调整它们以在移动设备上显示正确。就像上一个例子一样,我发现最好是移除图片本身的显式宽度和高度,并使用 CSS 使其填充当前容器的 100%。然后,使用 CSS 的max-width属性来确保图像不会被放大超出其原始意图的尺寸。

虽然在加载适合分辨率的不同尺寸的图像方面并没有真正性能响应,但我们已经使用我们有限的资源达到了相同的可见效果,对于这样的情况。

HTML5 Web 存储

如果你还没有尝试过 HTML5 Web 存储,它其实可以相当简单。如果已经尝试过了,可以跳到下一段。实际上只有两种 web 存储形式:localStorage,和 sessionStoragelocalStorage 将永久地保存信息。sessionStorage 只在单个会话的周期内保存。这是一个简单的键值配对系统。所有东西都是基于字符串的。因此,一旦你从存储中提取出来,你需要根据需要将这些值转换为其他格式。查看 www.w3schools.com/html5/html5_webstorage.asp 获取更多详细信息。

现在,关于会话的定义就变得有趣了。不要混淆你服务器上的会话和浏览器会话。你服务器上的用户会话可能在大概 20 分钟内就会过期。然而,只是因为你服务器上的会话已经过期,并不意味着你的浏览器知道这一点。HTML5 会话存储会一直持续到浏览器实际关闭。

这在移动浏览器上就特别棘手了。在安卓和 iOS 系统中,当你切换任务或按下主屏幕按钮时,浏览器并不会真正关闭。在这两种情况下,你必须使用任务关闭功能来完全关闭浏览器。这是最终用户可能并不会自己去做的事情。

但是,关于 web 存储有何不同之处呢?为什么不只是用 cookie 在客户端上存储信息呢?毕竟,这会适用于每个人,对吧?是的,cookie 会适用于每个人。然而,它们从来就不是用来存储大量数据的,就像我们在这个例子中使用的那样,并且每个域名可以存储的 cookie 数量也有软上限(根据浏览器的不同,从 20 到 50 不等)。试图使用 cookie 在客户端存储的最糟糕的一面是,它们随着每个资源的请求一起发送回服务器。这意味着每个 CSS、JS、图像以及页面/Ajax 请求都会携带着所有 cookie 以其有效荷载。你可以看到这样会很快降低你的性能。添加一个 cookie 可能导致数据被传输多次,仅用来渲染一个页面。

基于浏览器的数据库(进展中)

基于浏览器的数据库目前处于极端波动状态。实际上,目前有两种不同的标准。第一种是 Web SQL Database (www.w3.org/TR/webdatabase/)。你可以使用它,但根据 W3C 的说法,这个规范已经不再活跃。许多浏览器已经实现了 Web SQL Database,但它能够存活多久呢?

W3C 已经声明,浏览器上数据库的方向将是Indexed Databasewww.w3.org/TR/IndexedDB/)。工作草案的编辑来自微软、谷歌和 Mozilla;因此,我们可以期待未来有广泛的支持。问题是,工作草案于 2012 年 5 月 24 日发布。截至我写这一章时,只有 Firefox、Chrome 和 Internet Explorer 10 支持 IndexedDB(en.wikipedia.org/wiki/Indexed_Database_API)。

JSON 拯救了我们

目前,我们发现自己处于一个极其糟糕的境地,要么使用一个丑恶的数据库,要么等待所有人都跟上新规范。在不久的将来,Web 存储看起来是唯一的安全选择。那么,我们如何最好地利用它呢?当然是用 JSON!所有主要浏览器都原生支持 JSON。

想想我们过去一直处理关系数据库的方式。作为面向对象的程序员,我们总是进行查询,然后将结果数据转换成内存中的对象。我们几乎可以通过使用JSON.stringify方法将 JSON 直接存储到 Web 存储中,以几乎相同的方式来做完全相同的事情。

这里有一个例子,用来测试你的系统是否原生支持 JSON。源文件在本章的代码包中的jsonTest.html

<!DOCTYPE html>  
<html>  
<head>   
  <title>JSON Test</title>  
</head>    
<body>    
<script type="text/javascript">   

  var myFeedList = {     
    "lastUpdated":"whenever",     
    "feeds":[        
    {         
       "name":"ars",         
    "url":"http://feeds.arstechnica.com/arstechnica/index?format=xml" 	    
    },       
    {       
      "name":"rbds",            
      "url":"http://roughlybrilliant.com/rss.xml"       
    }     
    ]   
  }     

myFeedList.lastUpdated = new Date(); 

localStorage.feedList = JSON.stringify(myFeedList);      

var myFeedListRetrieved = JSON.parse(localStorage.feedList);      
alert(myFeedListRetrieved.lastUpdated); 
</script>  
</body> 
</html>

如果一切正常,你将看到一个包含时间戳的警报。

如果出于某种原因,你发现自己不幸地必须支持一些过时的系统(Windows Phone 7 和 BlackBerry 5 或 6,我正在看着你),请从github.com/douglascrockford/JSON-js获取json2.js并将其包含在其他脚本中。然后,你将能够 stringify 和 parse JSON。

利用 Google Feeds API

所以,我们已经看到了如何原生地拉取一个普通的 RSS 订阅,解析,并使用正常而乏味的字符串拼接构建页面。现在,让我们考虑一种替代方案,我在开始写这一章时甚至不知道它的存在。感谢雷蒙德·卡姆登和安迪·马修斯在他们的书《jQuery 移动 Web 开发要点》中指出这一点。你需要在 Twitter 上关注他们两个,@cfjedimaster@commadelimited

Google Feeds API 可以提供多种选择,但它的核心是指定 RSS 或 ATOM 订阅并返回 JSON 表示。当然,这在这一章节中开启了更多有趣的可能性。如果我们现在可以获取不同类型的多个订阅,而无需任何服务器端代理,我们可以极大地简化我们的生活。客户端模板再次出现!不再需要字符串拼接!因为它们都是统一格式(包括发布日期),我们可以把它们全部整合到一个主视图中,所有订阅故事按日期排序。

按其属性对对象进行排序实际上相当简单。你只需要传递一个比较函数。以下代码是我们将用于日期的:

function compareDates(a,b) {     
  var aPubDate = Date.parse(a.publishedDate);     
  var bPubDate = Date.parse(b.publishedDate);     
  if ( aPubDate < bPubDate) return 1;     
  if (aPubDate > bPubDate)  return -1;     
  return 0;   
}

现在,让我们指定一个 JSON 对象来存储我们想要使用的 feeds:

var allFeeds = {   

  //all the feeds we want to pull in 	
  "sources":[       
"http://feeds.arstechnica.com/arstechnica/index?format=xml", 
"http://rss.slashdot.org/Slashdot/slashdot",       
"http://www.theregister.co.uk/headlines.atom"     
],   

  //How many of the feeds have responded?  Once all have 
  //responded, we'll finish our processing.  
  "sourcesReporting":0,   

  //This is where we will store the returned stories.	
  "entries":[]   
}; 

接下来,我们将使用我们的处理函数来处理传入的故事:

function assimilateFeed(data){   

  //Mark another feed as having reported back  
  allFeeds.sourcesReporting++; 

  //Grab the title of this feed    
  var feedTitle = data.responseData.feed.title; 

  //Loop through every entry returned and add the feed title
  //as the source for the story		
  for(x = 0; x < data.responseData.feed.entries.length; 
    data.responseData.feed.entries[x++].source=feedTitle); 

  //Join this field's entries with whatever entries might have 
  //already been loaded
  allFeeds.entries = allFeeds.entries.concat(data.responseData.feed.entries); 

  //If all the feeds have reported back, it's time to process
  if(allFeeds.sourcesReporting == allFeeds.sources.length){ 

    //Sort all the stories by date
    allFeeds.entries.sort(compareDates);   

   //Take the results that have now all been combined and
    //sorted by date and use jsRender 
    $("#results").html($("#googleFeedTemplate")
      .render(allFeeds)).listview("refresh");         
  }   
} 

这是我们的 JsRender 模板:

<script type="text/x-jsrender" id="googleFeedTemplate">   
  {{for entries}}     
    <li>       
      <a href="{{:link}}" target="_blank">         
        <h3>{{:title}}</h3>          
        <p><strong>{{:source}}</strong> - {{:publishedDate}}
          <br/>{{:contentSnippet}}
        </p>
      </a>
    </li>   
  {{/for}} 
</script>

最后,这是将启动整个过程的函数:

$("#feeds").click( function() {  

  //Reset the number of received feeds
  allFeeds.sourcesReporting = 0;     

  //Get back to the top of the page
  $.mobile.silentScroll(0);     

  //Loop through all the feeds
  for(var x = 0; x < allFeeds.sources.length; x++){       
    $.ajax({   

//Call to Google's Feed API with the URL encoded      
url:"https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&output=json&q="+escape(allFeeds.sources[x]),          
      dataType:"jsonp",         
      success:assimilateFeed       
    });
  }   
});

我已将此包含在我的 challenge.html 文件的功能示例中,但源代码要比这深得多。challenge.html 的源代码还有几个隐藏的宝藏供你发现。我还添加了 Reddit、Flickr 和本地搜索 Twitter。

总结

你已经被呈现了一个非常广泛的客户端模板选择。在这一点上,你现在知道如何利用 JSON 和 JSONP 并有效地将它们结合起来动态创建页面。RSS 对你来说也不会是真正的挑战,因为你可以本地或使用 Google Feeds 来处理。

在下一章中,我们将结合一些这些技术,继续构建我们的技术工具箱,并将目光转向 HTML5 音频。

第六章:HTML5 音频

让我们把迄今为止学到的东西转向音乐领域。我们将把 jQuery Mobile 界面转化为一个媒体播放器、艺术家展示和信息中心,并可以保存到人们的主屏幕上。

在这一章中,我们将涵盖:

  • HTML5 音频(渐进增强方式)

  • 固定位置,持久工具栏(真的!?)

  • HTML5 音频的自定义 JavaScript 控件

  • iOS 中的 HTML5 音频及其区别

  • 全能解决方案(多页面实用)

  • 使用 HTML5 清单将内容保存到主屏幕

HTML5 音频

与琳赛·施特林问好。琳赛在美国达人秀第五季上首次登场。你看过小提琴手摇滚表演吗?自她在全国舞台上的表现以来,她在 YouTube 上的视频每个都有数百万次观看。2012 年 9 月 18 日,她发布了她的首张同名专辑。这一章将是对她的音乐和数字存在的粉丝致敬。如果你想要完整的体验,就去她的 YouTube 频道youtube.com/lindseystomp。她的 200 万订阅者不会错!

HTML5 音频

现在,回到正题。正如我们迄今所见,jQuery Mobile 使一切变得容易。你几乎必须要尝试才能把事情搞复杂。HTML5 音频可以像你希望它那样复杂,我们会到那一步的。现在,让我们看看把音频带入你的 jQuery Mobile 页面有多么简单。考虑下面的代码片段:

<audio id="audio" controls>                     
  <source src="img/electricdaisy.mp3" type="audio/mpeg" />
  <source src="img/electricdaisy.ogg" type="audio/ogg" />
   Your browser is so old that you can't hear the music.
</audio>

就是这样。这就是在上一张图片中得到音乐控制条所需的全部内容。我们来稍微分解一下。

就像在第四章的视频中一样,二维码,地理定位,谷歌地图 API 和 HTML5 视频音频标签可以支持多个来源,浏览器将简单地选择它知道如何处理的第一个。老旧的浏览器将毫无头绪,只会简单地解析这个像 XML,这意味着唯一显示的是文本,“你的浏览器太老了,无法播放音乐。”

每个浏览器都提供自己的本机界面来控制音频。有些像 iOS 版本那样又小又亮,而有些则完全丑陋但更可用,比如 Android。无论如何,它们都有所不足,所以让我们把 jQuery Mobile 变成一个媒体播放器。

这是我们的基本起始页面。你可以在代码文件中的electricdaisy_basic.html中找到其源代码:

<!DOCTYPE html>  
<html>  
  <head> 
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">     
    <link href='chapter6.css' rel='stylesheet' type='text/css'> 
    <title>Lindsey Sterling</title>
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.css" />
    <script src="img/jquery-1.8.2.min.js"></script>
    <script type="text/javascript" src="img/global.js"></script> 
    <script src="img/jquery.mobile-1.3.0.min.js"></script> 
    <link rel="stylesheet" href="chapter6.css" /> 
  </head>    
<body>      
<div id="electricdaisy" class="songPage" data-role="page" >
  <div data-role="header">
    <a href="basic.html" data-transition="slidedown" data-theme="c" data-icon="home" data-iconpos="notext">Home</a> 
    <h2>Lindsey Sterling</h2>             
    <a class="ui-btn-right" data-transition="slidedown" data-theme="c" href="tracklist.html" data-icon="note" data-iconpos="notext" >Music</a>         
  </div>         
  <div data-role="content">
    <img alt="cover art" src="img/electricdaisy.jpg" width="100%" />             
    <p>                 
      <audio id="audio" controls>
        <source src="img/electricdaisy.mp3" type="audio/mpeg" />
        <source src="img/electricdaisy.ogg" type="audio/ogg" />
        Your browser is very old so that you can't hear the music.
      </audio>             
    </p>         
  </div>     
</div> 
</body> 
</html>

这个构建良好的 jQuery Mobile 页面除了美化之外无需任何 JavaScript。你可以关闭 JS,整个页面仍能正常工作,还能播放音乐。对于所有的渐进增强粉丝来说,我们正从正确的角度开始。毕竟,每个人都是音乐的粉丝,不仅仅是智能手机用户。

现在让我们看看如何使用 JavaScript 和固定位置工具栏来创建更好的控制界面。

固定位置的持续工具栏(真的!?)

我要诚实地说;我对移动空间中的固定位置工具栏的看法普遍很低。从可用性的角度来看,它们是一场灾难。移动屏幕本来可用空间就很少。在没有为用户提供强大的好处的情况下浪费更多的屏幕空间是不可想象的。此外,由于涉及到的 CSS,古老版本的 Android(低于版本 2.3)将不支持固定位置工具栏。

然而,我们经常看到这种情况,不是吗?公司把他们的标志贴在永远不会消失的顶部工具栏上。他们加上一点全局导航,并称之为对用户的一个好处,而实际上这完全是为了加强他们的品牌形象。你可以从工具栏上唯一的可交互部分——一个菜单按钮和可能的一个搜索按钮上看出来(好像我们不能再次在顶部找到它们一样)。有许多更好的技术来提供全局导航。

固定位置的持续工具栏(真的!?)

今天,我们有一个合理的用途来使用这些工具栏。我们将在其中放置音乐控制,这些音乐控制将随着我们切换曲目而持续存在。如果我们做得对,这个音乐网站将更像一个应用程序,并让用户始终控制设备发出的声音。

如果你已经玩过 jQM UI 的这一部分,请立即跳到下一段。

使工具栏固定(滚动时不移动)和持续(在更改页面时不移动)其实很简单。你所要做的就是添加 data-position="fixed" 来使其固定,然后在你想要页脚在页面转换时保持不动的页面上添加 data-id="whatever" 给页脚。这个功能也适用于头部。

这是我们持续页脚的基础:

<div class="jsShow playcontrols" data-role="footer" data-id="playcontrols" data-position="fixed">         
  <div class="progressContainer">
    <input  data-theme="b" data-track-theme="c" class="progressBar" type="range" name="slider-1"  value="0" min="0" max="227" data-mini="true"/></div>         
  <div data-role="navbar" class="playcontrols">             
    <ul>                 
      <li><a data-theme="c" title="skip back" class="skipback" href="#crystallize" data-direction="reverse"><img src="img/sg_skipback2x.png" alt="Skip Back" height="14"/></a></li>                     
      <li><a data-theme="c" title="seek back" class="seekback" href="javascript://"><img src="img/sg_rw@2x.png" alt="Seek Back" height="14"/></a></li>                     
      <li><a data-theme="c" title="play/pause" class="play" href="javascript://"><img src="img/49-play@2x.png" alt="Play/Pause" height="14"/></a></li>                     
      <li><a data-theme="c" title="seek forward" class="seek" href="javascript://"><img src="img/sg_ff@2x.png" alt="Seek Forward" height="14"/></a></li>                     
      <li><a data-theme="c" title="skip forward" class="skip" href="#shadows"><img src="img/sg_skip@2x.png" alt="Skip Forward" height="14"/></a></li>
      </li>             
    </ul>         
  </div>     
</div> 

见到页脚顶部的那个类(jsShow)了吗?让我们在围绕audio标签的段落中添加另一个类(jsHide):

<p class="jsHide">                 
  <audio id="audio" controls>                     
…            
</p>

在 CSS 中,让我们添加以下规则:

.js .jsHide{display:none} 
.no-js .jsShow{display:none;}

然后我们将在我们的 global.js 文件中添加一行代码来将整个内容组合在一起:

$("html").removeClass("no-js").addClass("js");

这是 HTML5 模板 (html5boilerplate.com/) 和 Modernizer (modernizr.com/) 使用的一种技术。如果你还没有看过这两个奇迹,那值得你的时间。简单来说,我们现在有了一种方便、轻量级的处理渐进增强的方法。对于那些需要帮助的人,语音辅助也非常完美。

现在,我们离一个好用的通用媒体播放器 UI 很近了,但是如果你一直在输入代码,你可能已经注意到输入type="range"正在显示一个文本框。单独看这可能不算太糟糕,但 HTML5 音频以秒为单位跟踪其当前位置,这使得它作为显示元素相当无用。所以,让我们隐藏它,并通过一些简单的 CSS 扩展一下进度条:

input.progressBar{display:none} 
div.ui-slider{width:90%;}  

现在,我们看起来不错了,让我们将它们连接起来使其工作。

用 JavaScript 控制 HTML5 音频

好了,现在我们开始用 JavaScript 变得有点复杂了。

首先,让我们设置一个间隔来更新进度条。它将有两个功能,显示当前时间和更改时间。我们将首先添加对这些对象的引用,并为我们可能想要附加到的每一个音频事件放置事件挂钩。注释描述了何时触发哪些事件:

//for every song page 
$(document).on("pagecreate", ".songPage", function(){ 
  var $page = $(this);	
  var $currentAudio = $page.find("audio");

  //set references to the playing status, progress bar, and 
  //progress interval on the audio object itself 
  $currentAudio.data("playing",false) 
    .data("progressBar", $page.find("input.progressBar")).data("progressThread",null); 

  //loadstart and progress occur with autoload
  $currentAudio[0].addEventListener('loadstart', function(){ 
    //Fires when the browser starts looking 
    //for the audio/video
  }, false);

  $currentAudio[0].addEventListener('progress', function(){ 
    //Fires when the browser is downloading the audio/video
    //This will fire multiple times until the source 
    //is fully loaded.
  }, false); 

  //durationchange, loadedmetadata, loadeddata, canplay, 
  //canplaythrough are kicked off upon pressing play 
  $currentAudio[0].addEventListener('durationchange', 
  function(){ 
    //Fires when the duration of the audio/video is changed 

  }, false); 

  $currentAudio[0].addEventListener('loadedmetadata', 
  function(){
    //Fires when the browser has loaded meta data 
    //for the audio/video 

  }, false); 

  $currentAudio[0].addEventListener('loadeddata', function(){ 
    //Fires when the browser has loaded the current 
    //frame of the audio/video 

  }, false);

  $currentAudio[0].addEventListener('canplay', function(){  
    //Fires when the browser can start playing 
    //the audio/video 	

  }, false); 

  $currentAudio[0].addEventListener('canplaythrough', 
  function(){ 
    //Fires when the browser can play through the audio/video 
    //without stopping for buffering 

  }, false); 

  $currentAudio[0].addEventListener('ended', function(){ 
    //Fires when the current playlist is ended 

  }, false); 

  $currentAudio[0].addEventListener('error', function(){ 
    //Fires when an error occurred during the loading 
    //of an audio/video 

  }, true);  

}); 

现在,让我们创建运行间隔的函数:

function scrubberUpdateInterval(){ 

  //Grab the current page 
  var $page = $.mobile.activePage; 

  //Grab the audio element 
  var $audio = $page.find("audio"); 
  var currentAudio = $audio[0]; 

  //Grab the progress monitor and the handle 
  currentAudioProgress = $page.find("input.progressBar"); 
  scrubberHandle = currentAudioProgress
    .closest(".progressContainer")
    .find("a.ui-slider-handle"); 

  //Is the user currently touching the bar? 	
  if(scrubberHandle.hasClass("ui-focus")){ 
    //Pause it if it's not paused already 
    if(!currentAudio.paused){  
      currentAudio.pause(); 
    } 

    //Find the last scrubber's last position 
    var lastScrubPosition = currentAudioProgress
      .data("lastScrubPosition"); 
    if(lastScrubPosition == null) lastScrubPosition = 0; 
    //Are we in the same place as we were last? 
    if(Math.floor(lastScrubPosition) == 
    Math.floor(currentAudio.currentTime)){ 
      var lastScrubUnchangedCount = currentAudioProgress
       .data("lastScrubUnchangedCount");
      //If the user held still for 3 or more cycles of the 
      //interval, resume playing  
      if(++lastScrubUnchangedCount >= 2){ 
        scrubberHandle.removeClass("ui-focus"); 
        currentAudioProgress 
          .data("lastScrubUnchangedCount", 0); 
        currentAudio.play(); 
      }else{ 
        //increment the unchanged counter 
        currentAudioProgress.data("lastScrubUnchangedCount", 
        lastScrubUnchangedCount); 
      } 
    }else{ 
      //set the unchanged counter to 0 since we're not in the 
      //same place 
      currentAudioProgress
        .data("lastScrubUnchangedCount", 0); 
    } 

    //set the last scrubbed position on the scrubber 
    currentAudioProgress.data("lastScrubPosition", 
      Number(currentAudioProgress.val())); 
    //set the current time of the audio 
    currentAudio.currentTime = currentAudioProgress.val(); 
  }else{ 
    //The user is not touching the scrubber, just update the 
    //position of the handle 
    currentAudioProgress
      .val(currentAudio.currentTime)
      .slider('refresh');  
  } 
}  

当点击播放按钮时,我们将启动间隔并执行其他必要的操作。和往常一样,所有内容都有很好的注释:

$(document).on('vclick', "a.play", function(){ 
  try{ 
    var $page = $.mobile.activePage; 
    var $audio = $page.find("audio"); 

    //toggle playing 
    $audio.data("playing",!$audio.data("playing")); 
    //if we should now be playing 
    if($audio.data("playing")) { 

      //play the audio 
      $audio[0].play(); 

      //switch the playing image for pause 
      $page.find("img.playPauseImage")
        .attr("src","images/xtras-gray/48-pause@2x.png"); 
      //kick off the progress interval 
      $audio.data("progressThread",  
        setInterval(scrubberUpdateInterval, 750)); 
    }else{
      //pause the audio 
      $audio[0].pause(); 

      //switch the pause image for the playing audio 
$page.find("img.playPauseImage")
        .attr("src","images/xtras-gray/49-play@2x.png");
      //stop the progress interval
      clearInterval($audio.data("progressThread")); 				
    } 
  }catch(e){alert(e)}; 
});

设置搜索控件:

$(document).on('click', "a.seekback", function(){
  $.mobile.activePage.find("audio")[0].currentTime -= 5.0; 
}); 

$(document).on('vclick', "a.seek", function(){
  $.mobile.activePage.find("audio")[0].currentTime += 5.0; 
}); 

现在,让我们创建一个 JSON 对象来跟踪我们的当前状态和跟踪列表:

var media = { 
  "currentTrack":0, 
  "random":false, 
  "tracklist":[ 
    "electricdaisy.html", 
    "comewithus.html", 
    "crystallize.html",
    "shadows.html", 
    "skyrim.html" 
  ] 
}

接下来,是跳过后退和前进按钮。我们可以设置随机按钮,但现在我们会跳过:

$(document).on('vclick', "a.skipback", function(event){ 
  //grab the current audio 
  var currentAudio = $.mobile.activePage.find("audio")[0]; 
  //if we're more than 5 seconds into the song, skip back to 
  //the beginning 
  if(currentAudio.currentTime > 5){ 
    currentAudio.currentTime = 0; 
  }else{ 
    //otherwise, change to the previous track 
    media.currentTrack--; 
    if(media.currentTrack < 0) media.currentTrack = 
      (media.tracklist.length - 1); 
    $.mobile.changePage("#"+media.tracklist[currentTrack]);
  } 
}); 

$(document).on("vclick", "a.skip", function(event){ 
  //grab the current audio and switch to the next track 
  var currentAudio = $.mobile.activePage.find("audio")[0]; 
  media.currentTrack++; 
  if(media.currentTrack >= media.tracklist.length) 
  media.currentTrack = 0; 
  $.mobile.changePage("#"+media.tracklist[currentTrack]); 
}); 

提示

性能注解

注意我已经不再使用click事件,而是现在使用vclick事件。vclick事件是 jQuery Mobile 中的自定义事件,旨在弥合 click(桌面事件)和 tap/touchstart(触摸事件)之间的性能差距。两者之间通常存在约 300 毫秒的差距,而支持什么样的浏览器是一件难以确定的事情。通过使用vclick,您仍然可以支持桌面和触摸设备,但您可以希望获得轻微的性能提升。有关更多信息,请参阅 jQuery Mobile 贡献者之一 John Bender 在 coderwall.com/p/bdxjzg 的博客文章。

iOS 中的 HTML5 音频不同

理解 HTML5 音频的事件循环对于使其正常工作至关重要。当您开始混合 jQuery Mobile 的奇怪事件循环时,情况可能会变得特别混乱。再加上一系列因设备而异的资源限制,您就真的会变得很困惑。

作为测试移动站点的快速简便方法,你通常只需打开 Google Chrome(因为它是 WebKit)或 IE9(用于 Windows Phone)并将其缩小到移动尺寸。当然,这不能替代真正的测试。始终要在真实设备上检查你的作品。话虽如此,缩小的浏览器方法通常可以让你达到 97.5% 的目标。好吧... HTML5 音频彻底改变了这种操作模式。

在 iOS 上,即使你已经标记了audio标签以预加载和自动播放,它也不会。不会抛出错误;也没有任何迹象表明你的编码请求被完全忽视了。如果你查看本章中包含的代码,你会看到在basicPlayer.js脚本中我放了多少 try/catch 和 debug 语句来尝试让它起作用,并找出出了什么问题。

从技术上讲,pageinit是文档中说等同于document.ready的事件,但这并不意味着页面实际上已经可见。导致页面显示的事件链的最后是pageshow事件。所以,不管怎样,那应该是结束,并且应该为你可能想做的任何事情做好准备。在这个时候,你应该(理论上)能够使用 JavaScript 告诉歌曲播放(.play())。然而,事实并非如此。你可以使用完全相同的函数来触发音频播放,甚至延迟一段时间再启动它,但仍然没有任何效果。这不是一个时间问题。iOS 需要直接用户交互才能首次启动音频。直接将其绑定到点击事件,否则不起作用。

全能解决方案(多页面实用化)

现在我们有了一个完整的播放器,具有统一的界面,可以用来管理播放列表。我们目前唯一真正的问题是网络延迟。即使在这个新的 4G 和 LTE 时代,蜂窝网络的延迟也可能变得荒谬。如果你像我一样在一个像斯巴达的方阵一样阻挡信号的建筑物工作,这一点尤为真实。所以,为了给用户带来更好的体验,我们将放弃这种逐页的方式。

顺便说一句,让我们把我们在之前章节中所做的一些工作也整合进来,比如引入林赛最新的推文和她博客的内容。我们将使用之前的 CSS,但其他方面会有所改变。

全能解决方案(多页面实用化)

对于那些对服务器端和面向对象类型的人来说,最令人烦恼的事情之一就是你经常不得不重复一段代码。如果有一个全局头部或页脚,这就成为了一个真正的问题。所以,让我们创建一个div标签来容纳通用页脚内容,并创建一个脚本在适当的时候将其引入:

<div id="universalPlayerControls" style="display:none">     
  <div class="progressContainer">
    <input  data-theme="b" data-track-theme="c" class="progressBar" type="range" name="slider-1"  value="0" min="0" max="227" data-mini="true"/>
  </div>     
  <div data-role="navbar" class="playcontrols">         
    <ul>             
      <li><a data-theme="c" title="skip back" class="skipback" href="javascript://" data-direction="reverse"><img src="img/sg_skipback2x.png" alt="Skip Back" height="14"/></a></li>             
      <li><a data-theme="c" title="seek back" class="seekback" href="javascript://"><img src="img/sg_rw@2x.png" alt="Seek Back" height="14"/></a></li>             
      <li><a data-theme="c" title="play/pause" class="play" href="javascript://"><img class="playPauseImage" src="img/49-play@2x.png" alt="Play/Pause" height="14"/></a></li>             
      <li><a data-theme="c" title="seek forward" class="seek" href="javascript://"><img src="img/sg_ff@2x.png" alt="Seek Forward" height="14"/></a></li>             
      <li><a data-theme="c" title="skip forward" class="skip" href="javascript://"><img src="img/sg_skip@2x.png" alt="Skip Forward" height="14"/></a></li>         
    </ul>     
  </div> 
</div>

现在,对于任何想要在页脚中具有这些控件的页面加载,我们将在 jQM 标记页面之前将这些内容直接复制到页脚中:

$(document).on("pagebeforecreate", function(){ 
  $(this).find("div[data-id='playcontrols']")
    .html($("#universalPlayerControls").html());
});

最后,是时候使每个歌曲页面都变得动态了。我们移除了单独的音频元素,简单地在“页面”的数据属性中链接到它们。页脚消失了,取而代之的是一个空的页脚,准备好注入控件:

<div id="electricdaisy" class="songPage" data-role="page" data-mp3="audio/electricdaisy.mp3" data-ogg="audio/electricdaisy.ogg"> 
  <div data-role="header">
    <a href="#home" data-theme="c" data-icon="home" data-iconpos="notext">Home</a>
    <h2>Electric Daisy</h2>
    <a class="ui-btn-right" data-theme="c" href="#tracklist" data-icon="note" data-iconpos="notext" >Music</a>
  </div>         
  <div data-role="content">         
    <img src="img/electricdaisy.jpg" width="100%" />
  </div>          
  <div data-role="footer" data-id="playcontrols" data-position="fixed"></div> 
</div>

所有这些都将要求我们重新调整我们的 JavaScript。一些部分将保持不变,但由于我们只剩下一个音频元素,代码可以简化。以下是在 Packt Publishing 网站提供的代码捆绑包的 index.html 文件中的所有合并版本的最终源代码:

<!DOCTYPE html>  
<html>  
<head> 
  <meta charset="utf-8"> 
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">     
  <link href='http://fonts.googleapis.com/css?family=Playball' rel='stylesheet' type='text/css'> 
  <title>Lindsey Stirling</title>  
  <link rel="stylesheet" href="jquery.mobile-1.2.0-rc.1.min.css" /> 	
  <script src="img/jquery-1.7.2.min.js"></script>     
  <script type="text/javascript"> 
    $(document).bind("mobileinit", function(){ 
      $.mobile.defaultPageTransition = "slide"; 
    }); 
  </script> 
  <script src="img/jquery.mobile-1.2.0-rc.1.min.js"></script>     
  <script type="text/javascript"
src="img/jsrender.min.js"></script>     
  <link rel="stylesheet" href="chapter6.css" /> 
</head>    
<body id="body">

在完成所有常规工作之后,这是体验的第一个“页面”:

  <div id="home" data-role="page" 
    data-mp3="audio/electricdaisy.mp3" 
    data-ogg="audio/electricdaisy.ogg"> 	

    <div data-role="header">
      <h1>Lindsey Stirling</h1>
      <a class="ui-btn-right" data-theme="c" href="#tracklist" data-icon="note" data-iconpos="notext" >Music</a>
    </div>     

    <div data-role="content"> 
      <ul id="homemenu" data-role="listview" data-inset="true"> 
        <li><a href="#news">News</a></li>
        <li><a href="#tour">Tour</a></li>
        <li><a href="#comewithus">Music</a></li>  
      </ul>
      <div id="twitterFeed">
        <ul class="curl"></ul>
      </div>     
    </div>     

    <div data-role="footer" data-id="playcontrols" data-position="fixed">
    </div> 

  </div>  

  <div data-role="page" id="news"> 
    <div data-role="header">
      <a href="#home" data-theme="c" data-icon="home" data-iconpos="notext">Home</a>
      <h2>News/Blog</h2>
    </div>      

    <div data-role="content"></div> 
  </div>  

以下页面列出了所有可预览的曲目:

  <div id="tracklist" data-role="page">  
    <div data-role="header">
      <a href="#home" data-theme="c" data-icon="home" data-iconpos="notext">Home</a>
      <h2>Track List</h2>
    </div>        

    <img src="img/lindsey-header-new1.jpeg"  width="100%" alt="signature banner" /> 

    <div data-role="content"> 
       <ul data-role="listview"> 
         <li><a class="trackListLink" href="#electricdaisy">Electric Daisy</a></li> 
         <li><a class="trackListLink" href="#shadows">Shadows</a></li>
         <li><a class="trackListLink" href="#comewithus">Come With Us feat. CSWS</a></li>
         <li><a class="trackListLink" href="#skyrim">Skyrim</a></li>
         <li><a class="trackListLink" href="#crystallize">Crystallize</a></li>
      </ul>     
    </div> 
  </div>  

以下是各个歌曲页面。我没有包含每个歌曲页面,因为那只是页面的浪费。你会明白这是如何工作的。请注意,每个页面都有相同的 data-id 属性的页脚。以下允许在歌曲之间转换时保持页脚不变:

  <div id="shadows" class="songPage" data-role="page" 
    data-mp3="audio/shadows.mp3" 
    data-ogg="audio/shadows.ogg" >  
    <div data-role="header">
      <a href="#home" data-theme="c" data-icon="home" data-iconpos="notext">Home</a>
      <h2>Shadows</h2>
      <a class="ui-btn-right" data-theme="c" href="#tracklist" data-icon="note" data-iconpos="notext" >Music</a>
    </div>         

    <div data-role="content">         
      <img src="img/shadows.jpg" width="100%" alt="cover art" />     
    </div>          

    <div data-role="footer" data-id="playcontrols" data-position="fixed"></div> 
  </div>  

  <div id="crystallize" class="songPage" data-role="page" 
    data-mp3="audio/crystallize.mp3" 
    data-ogg="audio/crystallize.ogg">  
    <div data-role="header">
      <a href="#home" data-theme="c" data-icon="home" data-iconpos="notext">Home</a>
      <h2>Crystallize</h2>
      <a class="ui-btn-right" data-theme="c" href="#tracklist" data-icon="note" data-iconpos="notext" >Music</a>
    </div>         

    <div data-role="content">         
      <img src="img/crystallize.jpg" width="100%" alt="cover art" /> 
    </div>          

    <div data-role="footer" data-id="playcontrols" data-position="fixed"></div> 
  </div>  

  <div id="electricdaisy" class="songPage" data-role="page" 
    data-mp3="audio/electricdaisy.mp3" 
    data-ogg="audio/electricdaisy.ogg">  
    <div data-role="header">
      <a href="#home" data-theme="c" data-icon="home" data-iconpos="notext">Home</a>
      <h2>Electric Daisy</h2>
      <a class="ui-btn-right" data-theme="c" href="#tracklist" data-icon="note" data-iconpos="notext" >Music</a>
    </div>

    <div data-role="content">
      <img src="img/electricdaisy.jpg" width="100%" alt="cover art" /> 
    </div>          

    <div data-role="footer" data-id="playcontrols" data-position="fixed"></div> 
  </div>  

这部分不是页面。这是将被导入到播放歌曲的每个页面中的隐藏式主控制器:

  <div id="universalPlayerControls" style="display:none">     
    <div class="progressContainer">
      <input  data-theme="b" data-track-theme="c" class="progressBar" type="range" name="slider-1"  value="0" min="0" max="227" data-mini="true"/>
    </div>     
    <div data-role="navbar" class="playcontrols">         
      <ul>             
        <li><a data-theme="c" title="skip back" class="skipback" href="javascript://" data-direction="reverse"><img src="img/sg_skipback2x.png" alt="Skip Back" height="14"/></a></li>             
        <li><a data-theme="c" title="seek back" class="seekback" href="javascript://"><img src="img/sg_rw@2x.png" alt="Seek Back" height="14"/></a></li>
        <li><a data-theme="c" title="play/pause" class="play" href="javascript://"><img class="playPauseImage" src="img/49-play@2x.png" alt="Play/Pause" height="14"/></a></li>
        <li><a data-theme="c" title="seek forward" class="seek" href="javascript://"><img src="img/sg_ff@2x.png" alt="Seek Forward" height="14"/></a></li>
        <li><a data-theme="c" title="skip forward" class="skip" href="javascript://"><img src="img/sg_skip@2x.png" alt="Skip Forward" height="14"/></a></li>
      </ul>     
    </div> 
  </div>  

  <div style="display:none;">     
    <audio id="audio" controls></audio>     
  </div>  

以下代码是呈现导入的博客内容的模板:

  <script type="text/x-jsrender" id="googleFeedTemplate"> 
    <ul class="curl"> 
      {{for entries}} 	
        <li> 
          <h3 class="ul-li-heading">{{:title}}</h3> 
          <p>{{:publishedDate}}<br>{{:content}}</p> 
        </li> 
      {{/for}} 
    </ul> 
  </script> 

以下代码是呈现 Twitter 动态的模板:

  <script type="text/x-jsrender" id="twitterTemplate"> 
    <li class="twitterItem"> 
      <img src="img/{{:user.profile_image_url}}" alt="profile image" class="ui-shadow ui-corner-all" />
      <p>{{:text}}</p> 
    </li> 
  </script> 

  <script type="text/javascript"> 
    var media = { 
      "playing":false, 
      "debug":true,
      "currentTrack":0, 
      "random":false,
      "tracklist":[
        "#electricdaisy",
        "#comewithus",
        "#crystallize",
        "#shadows",
        "#skyrim"
      ] 
    } 

    //a handy little debug function
    var lastDebugTS = (new Date).getTime(); 	
    function debug(str){  
    try{ 
        if(media.debug){ 
          $.mobile.activePage.find("div[data-role='content']")
            .append(""+((new Date()).getTime()-lastDebugTS)+": "+str+"<br/>"); 
          lastDebugTS = (new Date).getTime();} 
      }catch(e){} 
    }   

    //grab the audio and control elements with global 
    //variables since everything is going to use them 
    var currentAudio = $("#audio")[0]; 
    var currentAudioProgress = null; 
    var scrubberHandle = null; 
    var scrubberUpdateSpeed = 750; 
    var progressThread = null; 

    //The ended and durationchange are the only events we 
    //really care about  
    currentAudio.addEventListener('ended', 
      function(){
        $.mobile.activePage.find(".skip").click()
      }, false); currentAudio.addEventListener('durationchange', 
     function(){   
       currentAudioProgress.attr('max',currentAudio.duration)
        .slider("refresh"); 
     }); 

   //On the home page 	
   $("#home").live('pagebeforeshow', function(){ 
     var $page = $(this); 

     //bring in the latest tweet 
$.ajax({url:"http://api.twitter.com/1/statuses/user_timeline.json?screen_name="+escape("LindseyStirling"),  
       dataType:"jsonp", 
       success: function(data) { 
         try{ 
           //parse out any hyperlinks and twitter IDs and turn 
           //them into links 
           var words = data[0].text.split(" "); 
           var newMessage = ""; 
           for(var x = 0; x < words.length; x++){
           var word = words[x]; 
             if(word.indexOf("http") == 0){ 	
               newMessage += "<a href='"+word+"' target='_blank'>"+word+"</a>"; 
             }else if(word.match(/@[a-zA-Z0-9_]*/)){ 
       newMessage += "<a href='http://twitter.com/"+word.substring(1)+"' target='_blank'>"+word+"</a> "; 
             }else{
               newMessage += word+" "; 
             } 
           } 
           data[0].text = newMessage;  
         }catch(e){} 

         //use jsRender to display the message 
        $("#twitterFeed ul")
          .html($("#twitterTemplate")
          .render(data[0])); 
      } 
    }); 

    //if we're not currently playing anything, preload audio 
    //as specified by the page's data- attributes 
    if(!media.playing) { 

      //load MP3 by default   
      if(currentAudio.canPlayType("audio/mpeg")){
         currentAudio.src = $page.attr("data-mp3");
      } 

      //load Ogg for all those purists out there 
      else{ currentAudio.src = $page.attr("data-ogg");} 
      //make it load 
      currentAudio.load();

      //set the progres bar
      currentAudioProgress = $page.find("input.progressBar"); 
      //set the scrubber handle 
      scrubberHandle = currentAudioProgress
        .closest(".progressContainer")
        .find("a.ui-slider-handle"); 
    } 
  });  

  //on the news page 
  $("#news").live('pageshow', function(){ 
    //This import can take a while, show the loading message 
  $.mobile.loading( 'show', {           
      text: "Loading Blog Content",           
      textVisible: true         
    });

    //load the actual content 
    $.ajax({ 
  url:"https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&output=json&q="+escape("http://lindseystirlingviolin.com/feed"),  
      dataType:"jsonp", 
      success: function(data) { 
        //use a jsRender template to format the blog 
        $("#news .ui-content")
          .html($("#googleFeedTemplate")
          .render(data.responseData.feed)); 	   
        //for every image in the news feed, make its width 
        //dynamic with a max width or its original size
        $("#news img").each(function(index, element) { 
         var $img = $(element); 

          //figure out its currentWidth 
          var currentWidth = Number($img.attr("width")); 
          //if it has a width and it's large 
          if(!isNaN(currentWidth) && currentWidth > 300){ 
            //remove the explicit width and height 
     $img.removeAttr("width").removeAttr("height"); 
            //make the image scale to the width 
         //of its container but never to be  
         //larger than its original size 
            $img.css({"max-width":currentWidth+"px","width":"100%"}); 
          } 
        });

        //hide the loading   
        $.mobile.loading("hide");
      }
    });
  }); 

  function setCurrentMediaSources(){ 
    var $page = $.mobile.activePage; 

    //set the audio to whatever is playable 	
    var playableSource = $page.attr("data-mp3"); 
    if(!currentAudio.canPlayType("audio/mpeg")){
      playableSource = $page.attr("data-ogg");
    }
    //set the progress bar and scrubber handles 
    currentAudioProgress = $page.find("input.progressBar"); 
  scrubberHandle = currentAudioProgress
      .closest(".progressContainer")
      .find("a.ui-slider-handle"); 

    //change the source and load it.  
    currentAudio.src = playableSource; 
    currentAudio.load(); 

    //if we're currently play, continue playing 
    if(media.playing){ 
      currentAudio.play(); 
      progressThread = setInterval(scrubberUpdateThread, scrubberUpdateSpeed); 	
    } 
  } 

  $(".songPage").live("pageshow", setCurrentMediaSources); 

  $("[data-role='page']").live("pagebeforecreate", 
  function(){ 
    $(this).find("div[data-id='playcontrols']")
      .html($("#universalPlayerControls").html());
  }); 

  function scrubberUpdateThread(){ 
    //if the scrubber has focus, the scrubber becomes 
    //input instead of status display 
    if(scrubberHandle.hasClass("ui-focus")){ 

    //pause the music for now 
    if(!currentAudio.paused){  
      currentAudio.pause(); 
    } 

    //grab the last position to see if we've moved 
    var lastScrubPosition = 
      currentAudioProgress.data("lastScrubPosition"); 
    if(lastScrubPosition == null) lastScrubPosition = 0; 
    //if the user hasn't scrubbed  
    if(Math.floor(lastScrubPosition) == Math.floor(currentAudio.currentTime)){ 
      var lastScrubUnchangedCount = 
      currentAudioProgress.data("lastScrubUnchangedCount"); 
      if(++lastScrubUnchangedCount >= 2){ 
  //since it's been 3 cycles that we haven't moved, 
        //remove the focus and play
        scrubberHandle.removeClass("ui-focus"); 
        currentAudioProgress.data("lastScrubUnchangedCount", 0); 
        currentAudio.play(); 
      }else{ 

        //store the the current position counter 
        currentAudioProgress.data("lastScrubUnchangedCount", lastScrubUnchangedCount); 
      } 
    }else{ 
      //reset the current position counter 
      currentAudioProgress.data("lastScrubUnchangedCount", 0); 
    } 

    //set the position of the scrubber and the currentTime 
    //position of the song itself  
    currentAudioProgress.data("lastScrubPosition", 
      Number(currentAudioProgress.val())); 
    currentAudio.currentTime = currentAudioProgress.val(); 
  }else{ 
    //update the progress scrubber  
    currentAudioProgress.val(currentAudio.currentTime)
     .slider('refresh');  
  } 
} 

//play button controls
$("a.play").live('click',function(){ 
  try{ 
    //toggle the playing status 
    media.playing = !media.playing; 

    //if we're supposed to playing.. 
    if(media.playing) { 

      //do it and set the interval to watch 	
      currentAudio.play(); 
      progressThread = setInterval(scrubberUpdateThread, scrubberUpdateSpeed); 	

      //switch the playing image for pause 
      $("img.playPauseImage").attr("src","images/xtras-gray/48-pause@2x.png"); 
    }else{ 

      //pause the audio and clear the interval 
      currentAudio.pause(); 

      //switch the pause image for the playing audio 
     $("img.playPauseImage").attr("src","images/xtras-gray/49-play@2x.png"); 

      //kill the progress interval  
      clearInterval(progressThread); 
    } 
  }catch(e){alert(e)}; 
}); 

$("a.seekback").live('click',function(){ 
  //back 5 seconds 
  currentAudio.currentTime -= 5.0; 
}); 

$("a.seek").live('click',function(){ 
  //forward 5 seconds 	
  currentAudio.currentTime += 5.0; 
}); 

$("a.skipback").live('click',function(event){
  //if we're more than 5 seconds into the song, skip 
  //back to the beginning 
  if(currentAudio.currentTime > 5){ 
    currentAudio.currentTime = 0; 
  }else{ 
    //othewise, change to the previous track 
    media.currentTrack--; 
    if(media.currentTrack < 0) media.currentTrack = (media.tracklist.length - 1); 

    $.mobile.changePage(media.tracklist[media.currentTrack],
    {
       transition: "slide", 
       reverse: true 
    }); 
  } 
}); 

$("a.skip").live('click',function(event){ 
  //pause the audio and reset the time to 0 	
  currentAudio.currentTime = 0; 

  //change to the next track 
  media.currentTrack++; 
  if(media.currentTrack >= media.tracklist.length) media.currentTrack = 0; 

  $.mobile.changePage(media.tracklist[media.currentTrack]); 
}); 
</script> 
</body> 
</html>

将所有内容构建到一个像这样的巨大的多页应用程序中,你将感受到界面的丝般顺滑。我们在这个文件中使用的 CSS 与独立歌曲文件中使用的完全相同。

使用 HTML5 清单保存到主屏幕

伴随着巨大的力量而来的是巨大的责任。这是一个强大的功能。如果你充分利用 HTML5 清单和其他一些元标签,你的应用程序将成为一个全屏、无浏览器边框的应用程序。

使用 HTML5 清单保存到主屏幕

要使你的应用程序在保存并启动时作为全屏应用程序,你需要为你的主屏幕准备图标。它们将是大小为 144、114、72 和 57 像素的正方形。像这样链接到它们:

<link rel="apple-touch-icon-precomposed" sizes="144x144" href="images/album144.png">     
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="images/album114.png">     
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="images/album72.png">     
<link rel="apple-touch-icon-precomposed" href="images/album57.png">     
<link rel="shortcut icon" href="img/images/album144.png">  

用户的导航按钮可以在 iOS 上隐藏。请注意,如果你选择这样做,你需要在你的应用程序中提供完整的导航。这意味着你可能想要添加返回按钮。如果你想让应用程序全屏,使用以下标签:

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

要使该内容在离线模式下可用,我们将使用清单。清单使用应用程序缓存来存储资产。你可以存储的内容有限。这因设备而异,但可能少于 25 MB。列出你想要按优先级保存的所有内容。要了解清单的所有功能,可以查看 www.html5rocks.com/en/tutorials/appcache/beginner/

这是我们清单的内容。它保存在 app.manifest 下:

CACHE MANIFEST
# 2012-09-21:v1
js/jquery-1.7.2.min.js
js/jquery.mobile-1.2.0-rc.1.min.js
js/global.js
js/jsrender.min.js

audio/shadows.mp3
audio/comewithus.mp3
audio/skyrim.mp3
audio/electricdaisy.mp3
audio/crystallize.mp3

jquery.mobile-1.2.0-rc.1.min.css
chapter6.css

images/xtras-gray/sg_skip.png
images/xtras-gray/sg_skip@2x.png
images/xtras-gray/sg_skipback.png
images/xtras-gray/sg_skipback@2x.png
images/xtras-gray/sg_ff.png
images/xtras-gray/sg_ff@2x.png
images/xtras-gray/sg_rw.png
images/xtras-gray/sg_rw@2x.png
images/xtras-gray/48-pause.png
images/xtras-gray/48-pause@2x.png
images/xtras-gray/49-play.png
images/xtras-gray/49-play@2x.png
images/ajax-loader.gif
images/comewithus.jpg
images/crystallize.jpg
images/electricdaisy.jpg
images/shadows.jpg
images/skyrim.jpg
images/wallpaper.jpg
images/cork.jpeg
images/icons-18-black.png
images/icons-18-white.png
images/icons-36-black.png
images/icons-36-white.png
images/note18.png
images/note36.png

要使用清单文件,你的网络服务器或 .htaccess 将需要配置为返回 text/cache-manifest 类型。在 HTML 文件中,你只需将它作为 html 标签本身的属性添加即可,像这样:

<html manifest="app.manifest">

如果你想清除缓存,你可以随时通过浏览器设置来执行。你也可以通过 JavaScript 控制缓存。我之前提供的链接提供了丰富的细节,如果你真的想深入了解的话。

摘要

这是一个内容丰富的章节,尽管开始很简单。但是,你现在基本上已经了解了如何将 HTML5 音频与 jQuery Mobile 结合使用的所有知识。你可以创建出精彩的学术页面,并且甚至可以制作复杂的应用程序以保存到设备中。如果这一章没有吓到你,你确实可以开始为媒体机构和场馆制作一些强大的移动站点。这一章唯一真正缺少的是为艺术家和场馆提供的图片画廊。但是,别担心;在下一章中,我们将创建一个为摄影师展示作品的平台。

第七章:完全响应式摄影

我们的手机迅速成为我们的照片相册。摄影师代表着移动网页开发中一种尚未充分开发的市场。但如果你仔细想想,这个市场应该是第一个适应移动世界的。随着发达国家智能手机的普及,智能手机上的电子邮件打开率正在迅速接近 40%,当你阅读这篇文章时,可能已经达到了这个水平 (www.emailmonday.com/mobile-email-usage-statistics)。

当你收到摄影师的电子邮件,告诉你你的照片已经准备好查看时,你是不是很兴奋,立即尝试查看?然而,有很多精通自己行业的摄影师没有准备好满足新的移动需求的网站:

完全响应式摄影

因此,这一章我们将涵盖以下内容:

  • 使用 PhotoSwipe 创建基本画廊

  • 支持完整的设备尺寸范围 - 响应式网页设计

  • 响应式设计中的文本可读性

  • 仅发送所需内容 - RESS

使用 PhotoSwipe 创建基本画廊

如果你正在寻找创建照片画廊的最快方法,那么你不会找到比 PhotoSwipe (www.photoswipe.com/) 更快的解决方案。它的大小为 82 K,并不算轻,但它几乎可以在 jQuery Mobile 支持的任何 A 或 B 级别上使用。他们的网站称它可以在任何基于 WebKit 的浏览器上使用。这基本上意味着 iOS、Android 和 BlackBerry。这三个大平台都被覆盖了。但是 Windows Phone 呢?好消息!它在那里也表现得非常好。即使 JavaScript 被关闭,PhotoSwipe 也会优雅地退化为合理的按页浏览体验。我们可以从头开始制作一个纯粹的 jQuery Mobile 体验,但实际上... 为什么呢?

再次我将放弃严格地将 JavaScript 和 CSS 完全分离到它们自己的文件中的学术上正确的行为,而是简单地将所有定制的 JavaScript 构建到页面本身。对于本书的目的来说,这样做更容易。我假设如果你在阅读这本书,你已经知道如何正确地分离事物以及原因。

让我们从基础知识开始。大部分来自于他们网站的样板,但我们将从摄影师的角度开始:

使用 PhotoSwipe 创建基本画廊

让我们从 <head> 标签的关键部分开始:

<link rel="stylesheet" href="http://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.css" />
<link rel="stylesheet" href="mullinax.min.css" />
<link rel="stylesheet" href="photoswipe.css" />
<link rel="stylesheet" href="jquery-mobile.css" />

<script src="img/klass.min.js"></script>
<script src="img/jquery-1.8.2.min.js"></script>	
<script src="img/jquery.mobile-1.3.0.min.js"></script>
<script src="img/code.photoswipe.jquery-3.0.5.min.js"></script>
<script src="img/code.photoswipe.galleryinit.js"></script>

注意

请注意,我们现在正在使用一个用 ThemeRoller 构建的自定义主题 (jquerymobile.com/themeroller/)。因此,我们只使用 jquery.mobile.structure-1.2.0.min.css 而不是完整的 jQM CSS。mullinax.min.css 文件是由 ThemeRoller 生成的,除了结构 CSS 外还包含其他所有必需的内容。

文件photoswipe.cssjquery-mobile.cssklass.min.jscode.photoswipe.jquery-3.0.5.min.js都是 PhotoSwipe 样板的一部分。文件名jquery-mobile.css有点误导。它实际上更像是一个适配器样式表,使 PhotoSwipe 在 jQuery Mobile 中工作和显示正确。没有它,您的画廊的无序列表看起来就不对了。最初,里面没有太多内容:

.gallery { 
list-style: none; 
padding: 0; 
margin: 0; 
} 
.gallery:after { 
clear: both; 
content: "."; 
display: block; 
height: 0; 
visibility: hidden; 
} 
.gallery li { 
float: left; 
width: 33.33333333%;
} 
.gallery li a { 
display: block; 
margin: 5px; 
border: 1px solid #3c3c3c; 
} 
.gallery li img { 
display: block; 
width: 100%; 
height: auto; 
} 
#Gallery1 .ui-content, #Gallery2 .ui-content { 
overflow: hidden; 
}

这个设置在 iPhone 或 Android 手机上是可以的,但是如果您在任何类型的平板电脑或桌面大小的浏览器上查看它,画廊的缩略图可能会变得令人讨厌地太大。让我们看看我们能用媒体查询做些什么来使其具有更具响应性的设计。

支持全范围设备尺寸 - 响应式网页设计

响应式网页设计RWD)是指使单个页面适应每个设备大小的概念。这意味着,我们不仅仅是在谈论具有 3.5 英寸屏幕的手机。那只是个开始。我们将支持各种尺寸的平板电脑,甚至是桌面分辨率。有关 RWD 概念的更多信息,请参阅zh.wikipedia.org/wiki/响应式网页设计

为了使 RWD 起作用,让我们根据常见设备和分辨率断点设置一些断点。我将从重新定义默认画廊项大小为 50%开始。为什么?在我使用智能手机以纵向模式浏览时,它只是让我感觉更舒适。所以,以下是断点。让我们将它们放入chapter7.css中:

.gallery li { 
float: left; width: 50%; }

/* iPhone Horizontal -------------------*/ 
@media all and (min-width: 480px){ 
.gallery li { width: 33.33333333%; } 
} 

/* iPad Vertical -----------------------*/ 
@media only screen and (min-width: 768px) {
.gallery li { width: 20%; } 
}  

/* iPad Horizontal ---------------------*/ 
@media only screen and (min-width: 1024px) {     
.gallery li { width: 16.66666666%; } 
}  

/* Nexus 7 Horizontal ------------------*/ 
@media only screen and (min-width: 1280px) {     
.gallery li { width: 14.285714%; } 
}  

/* Laptop 1440 -------------------------*/ 
@media only screen and (min-width: 1440px) {     
.gallery li { width: 12.5%; } 
}  

/* Monitor 1600 ------------------------*/ 
@media only screen and (min-width: 1600px) {
.gallery li { width: 11.111111%; } 
}  

/* Monitor 1920 ------------------------*/ 
@media only screen and (min-width: 1920px) {     
.gallery li { width: 10%; } 
}  

在测试这个设置时,我仔细考虑了我与所观看屏幕之间的平均观看距离。这些分解导致了缩略图在视野中看起来理想的大致相同的百分比。显然,我的一个人的焦点小组在科学角度上毫无意义,所以可以随心所欲地进行调整。

可能会问,为什么不只是使每个图像具有固定大小?为什么不同的分辨率断点?真的很简单,它保持了事物的均匀间距,而不是因为某些显示器或浏览器的调整大小刚好有足够的空间强制换行,而不占用空白。它还有一个额外的好处,对于这本书来说,它展示了将通用样式表分解为使用媒体查询将 jQuery Mobile 站点转换为通用站点的好方法。我们想要进行的任何其他基于分辨率的调整都可以直接放入chapter7.css中的适当位置。

脚本code.photoswipe.galleryinit.js存在于可下载示例内部的 PhotoSwipe 画廊页面上。我认为它永远不需要根据每个页面进行编辑或自定义,所以我将该脚本块提取到了code.photoswipe.galleryinit.js中。以下是代码。不要再想它,因为它现在已经成为自己的小文件,再也不会被看到或听到了:

(function(window, $, PhotoSwipe){ 
$(document).ready(function(){ 
  $(document) 
    .on('pageshow', 'div.gallery-page', function(e){ 
       var  currentPage = $(e.target), 
       options = {}, 
       photoSwipeInstance = $("ul.gallery a", e.target)
      .photoSwipe(options,  currentPage.attr('id')); 
       return true; 
    })  
   .on('pagehide', 'div.gallery-page', function(e){ 
      var currentPage = $(e.target), 
      photoSwipeInstance = 
      PhotoSwipe.getInstance(currentPage.attr('id'));
      if (typeof photoSwipeInstance != "undefined" 
      && photoSwipeInstance != null) { 
        PhotoSwipe.detatch(photoSwipeInstance); 
      } 
     return true; 
   }); 
}); 
}(window, window.jQuery, window.Code.PhotoSwipe));

现在,让我们考虑一下这些“页面”本身。我们将把这段代码放在index.html文件中,并随着进展逐步完善它:

<div id="gallery" data-role="page">
  <div class="logoContainer">
    <img class="logo" src="img/logo.png" alt="Mullinax Photography" />
  </div>
  <div data-role="content">
    <div class="artisticNav">
      <ul data-role="listview" data-inset="true">
        <li><a href="#babies">Babies</a></li>
        <li><a href="#babies">Bellies</a></li>
        <li><a href="#babies">Kiddos</a></li>
        <li><a href="#babies">Families</a></li>
        <li><a href="#babies">Senior</a></li>
        <li><a href="#babies">Other</a></li>
      </ul>
    </div>
  </div><!-- /content -->
</div><!-- /page -->

图库屏幕的设计概念如下:

  • 全屏照片背景

  • 在小屏幕上居中的标志,占屏幕宽度不超过 90%,并且不会超过其原始大小

  • 导航仍然应该明显,但不会妨碍艺术本身

以下是我们还将放入chapter7.css中的相关 CSS:

.logoContainer{text-align:center;} 
.logoContainer img{width:90%; max-width:438px;} 

#gallery{
background-image:url(backgroundSmall.jpg); 
background-repeat:no-repeat; 
background-position: top center;
} 

.portrait #gallery{ 
background-size:auto 100% !important;
}

.landscape #gallery{
background-size:100% auto !important;
} 

#gallery .ui-btn-up-c { 
background: rgba(255,255,255,.1); 
text-shadow: 1px 1px 0 white; 
background-image: -webkit-gradient(linear,left top,left bottom,from( rgba(255,255,255,.5) ),to( rgba(255,255,255,.7) )); 
background-image: -webkit-linear-gradient( rgba(255,255,255,.5),rgba(255,255,255,.7) ); 
background-image: -moz-linear-gradient( rgba(255,255,255,.5),rgba(255,255,255,.7) ); 
background-image: -ms-linear-gradient( rgba(255,255,255,.5),rgba(255,255,255,.7) ); 
background-image: -o-linear-gradient( rgba(255,255,255,.5),rgba(255,255,255,.7) ); 
background-image: linear-gradient( rgba(255,255,255,.5),rgba(255,255,255,.7) ); 
} 

#galleryNav{ position:absolute; bottom:10px; right:10px; }

现在我们只需要一点 JavaScript 来将所有这些联系在一起。当方向改变时,我们希望改变哪个方向占据 100%的背景宽度:

/*Whenever the orientation changes*/
$(window).on("orientationchange", function(event){
  $("body").removeClass("portrait")
    .removeClass("landscape")
    .addClass(event.orientation); 
}); 

/*Prime the body with the orientation on document.ready*/
$(document).ready(function(e) { 
  if($(window).width() > $(window).height()) 
    $("body").addClass("landscape") 
  else 
    $("body").addClass("portrait") 
});

这对我们的图库入口页面已经足够了,现在让我们为婴儿照片准备一个示例图库。本章的代码中有许多图库条目。但为了简洁起见,我在这里缩短了代码。同样,这将在代码文件的最终版本index.html中。

<div data-role="page" data-add-back-btn="true" id="babies" class="gallery-page">
  <div data-role="header">
    <h1>Babies</h1>
  </div>
  <div data-role="content">
    <ul class="gallery">
      <li><a href="images/full/babies1.jpg" rel="external"><img src="img/babies1.jpg" alt="001" /></a></li>
      <li><a href="images/full/babies2.jpg" rel="external"><img src="img/babies2.jpg" alt="002" /></a></li>
      <li><a href="images/full/babies3.jpg" rel="external"><img src="img/babies3.jpg" alt="003" /></a></li>
      <li><a href="images/full/babies26.jpg" rel="external"><img src="img/babies26.jpg" alt="026" /></a></li>
    </ul>
  </div>
</div>

注意

如果您没有在每个指向图像的链接上放置rel="external",它将无法正常工作。PhotoSwipe 文档已经很清楚地说明了这一点。如果您还不熟悉rel="external",它是告诉 jQuery Mobile 不要使用其通常的基于 AJAX 的导航跟随链接的一种方法。因此,它将强制全页加载到您要链接到的任何内容。

现在,只是为了好玩,将其在桌面浏览器中以全宽打开,然后将其缩小到移动设备尺寸,并观察其自适应。尝试使用图库首页、婴儿缩略图库和 PhotoSwipe 提供的幻灯片功能。

PhotoSwipe 的一个很酷的部分是,即使您在移动站点上使用 meta-viewport 标签禁用了缩放,用户仍然可以在全尺寸照片周围捏放和缩放。在平板电脑上非常方便。他们只需双击图像即可返回导航,图像将缩放到原始大小并显示导航。虽然这不是最明显的功能,但返回按钮也可以使用。

自然地,正如名称所暗示的,您可以简单地从一张照片滑动到另一张,并在到达集合末尾时循环回到集合开头。还有一个幻灯片功能,可以无限循环播放。在这两种情况下,如果用户按下返回按钮,他们将被带回缩略图页面。

我们目前唯一真正的问题是我们有一个可以很好缩放的站点,但是背景图像和全尺寸照片可能比严格必要的要大。背景图片实际上不是问题,因为我们可以根据媒体查询来确定发送哪种尺寸的图像。我们只需要创建两到三个背景图像尺寸,并覆盖jquery- le.css文件中使用的图像。在本章的最终版本代码中,我已将jquery-mobile.css重命名为chapter7.css,以避免与实际的 jQuery Mobile 库 CSS 文件混淆。

文本可读性和响应式设计

研究表明,每行理想的字符限制是有的。理想情况下,您应该选择 35、55、75 或 95 CPL(每行字符数)。人们倾向于更短或更长的行。由于我们真的想在这里展示摄影作品,所以让我们选择较短的 CPL。如果您想阅读完整的报告,可以在 psychology.wichita.edu/surl/usabilitynews/72/LineLength.asp 找到它。

在很大程度上,我们的文本列宽将受到设备本身的限制。在较小的设备上,我们确实别无选择,只能使用100%的宽度。一旦我们到了横向模式的平板电脑,我们就有了创造性地处理文本的空间。对于较大的宽度,我们可以将我们的每行字符数(CPL)增加到 55,效果会很好。我们也可以考虑使用更大的图片。无论我们做什么,都要确保设定了一组强有力的媒体查询断点是关键。

让我们使用这项研究作为指导,将一些有关会话的段落文字更具响应性:

<div id="sessions" data-role="page">
  <div class="logoContainer">
    <a href="#home"><img class="logo" src="img/logo.png" alt="Mullinax Photography" border="0" /></a>
  </div>
<div data-role="content">
  <div class="textContainer ui-shadow">
    <h3>For Your Session</h3>

    <p>Portrait sessions may be held at our Western Shawnee Studio, in the comfort of your home, or a location of your choice. I love capturing little ones in their natural environment. At home, children often feel more comfortable and are more likely to play and have fun. It's the perfect environment for capturing those sweet little smiles and laughs that you as a parent adore!!</p>

     <p>I strive to make each portrait session relaxed, fun, and beautiful. Like each child, each session will be unique to fit your family's needs. As a mother, I understand firsthand the challenges that come with photographing little ones. The perfect portrait can take time. Being the perfect model is hard work and often breaks are needed.  That is why each of my sessions is held without time constraints. A one-of-a-kind portrait cannot be rushed!! While I don't want to overstay my welcome, I do want to stay long enough that you and I are both satisfied with the portraits that were captured.</p>

    <h3>After Your Session</h3>

    <p>Approximately two weeks after your session, I will post an online gallery for you to view your proofs as well as share with friends and family. Your proof gallery will stay online for 10 days. At this time you have the option of placing your order through the website using our shopping cart or you can schedule an in-person appointment.</p>

  </div>
</div><!-- /content -->
<div data-role="footer">
  <div data-role="navbar" data-position="fixed">
    <ul>
      <li><a href="#home">Home</a></li>
      <li><a href="#about">About</a></li>
      <li><a href="#contact">Contact</a></li>
    </ul>
  </div><!-- /navbar -->
</div>
</div><!-- /page -->

接下来,让我们制定一些关于其在页面上放置的规则:

#sessions{ 
  background-color:#888; 
  background-repeat:no-repeat; 
  background-position: 
  center center; 
}  

#sessions h3{
  font-family: 'Euphoria Script', Helvetica, sans-serif; 
  font-size:200%; 
  font-weight:bold; 
  margin:0;
}

.textContainer{ 
  background-color:#EEE;
  margin:-5px;
} 

/* iPhone Portrait --*/ 
@media all and (min-width: 320px){ 
  .textContainer{ 
    padding:120px 10px 10px 10px;
  } 
  #sessions{ 
    background-image:none; 
  }
} 

/* iPad Verticle --*/ 
@media only screen and (min-width: 768px) {     
.textContainer{ padding:160px 10px 10px 10px;} 
}

/* iPad Horizontal --*/ 
@media only screen and (min-width: 1024px) {     
  .textContainer{
    float:right; 
    width:35em; 
    padding:2em 2em 2em 2em; 
    height:550px; 
    overflow:scroll;
  } 
  #sessions{ 
    background-image:url(images/Colleen.jpeg)
  }
}

/* Laptop 1440 --*/ 
@media only screen and (min-width: 1440px) { 
  #sessions{ 
    background-image:url(images/Gliser.jpg) 
  }   
}

与以前一样,在较小的宽度上设置的规则将延伸到更宽的宽度,除非指定了一个值来覆盖。您可以看到我是如何在 iPad 横向视图和 1440 分辨率上切换用于会话的图像的。在那之前,每个分辨率都继承了 background-image:none 形式和 320px 的规则。

现在让我们来看看我们的结果。

智能手机尺寸设备

在这里,我们看到了小屏幕上的会话内容,无论是纵向还是横向,都非常易读,但是都不是真正适合显示除文本以外的任何内容的理想方式。如果我们试图塞入任何形式的艺术作品,它都不会显示得好。我们会违反刚刚谈到的良好文本可读性。你或者摄影师可能会认为,将其中一张图片淡入背景看起来不错,但不要这样做!将大部分阅读文本保持为黑底白字、标准字体大小和标准字体。

智能手机尺寸设备

平板设备尺寸

这里我们看到相同的内容在平板上渲染。在纵向方向上,如果我们将文本保持在100%的宽度,仍然非常适合阅读。我们完全符合良好可读性的指南。然而,当用户切换到横向时,情况就不同了。在横向模式下,平板终于有足够的空间来展示一些摄影作品和文本:

平板设备

桌面尺寸设备

这仍然是一个 jQuery Mobile 页面,但我们看起来更像是一个桌面站点。现在我们可以展示不止一个面孔,所以我们不妨换一些不同的照片来展示艺术家的能力:

桌面尺寸设备

是的,这是我和我的家人。是的,为他们感到非常自豪。而且我对于每一个分辨率断点上的文本处理方式都非常满意,并且它是在一个页面上完成的。

循环背景图像

那么,当我们使用的图像依赖于我们当前的分辨率和方向时,我们如何循环背景图像呢?这几乎排除了循环一个单一图像的可能性。相反,我们将不得不交换整个样式表。下面是代码:

<link rel="stylesheet" href="rotating0.css" id="rotatingBackgrounds" />

它开始时是一个非常简单的样式表,但你可以将它制作得像你想要的那样复杂。我们暂不考虑高清显示和标清显示。iPhone 4 具有视网膜显示屏(326 ppi)在 2010 年 6 月发布。自那以后,趋势已经转向高清屏幕,所以我只是假设大多数人在过去两年内已经更新了他们的智能手机,或者他们很快就会更新。同样要记住,我们正处于 LTE(第四代移动宽带)普及的边缘。这意味着很快,移动速度将比大多数家庭宽带速度更快。

现在,这真的是懒惰的借口,不去制作更小的版本以充分利用性能吗?不,很可能,一些讨厌者和学者甚至会对上一段提出异议。我会说,性能确实很重要。这是一个可计费的功能。但想想你想循环播放多少图像,然后乘以你想要花时间准备和测试多少分辨率和尺寸变体。再次强调,这都是可计费的,除非你是免费做的。

一直进行这样细微的优化,到底还需要多长时间才能让其真正没有明显的差别?如果你是在 2014 年或之后阅读此内容,你可能对必须在任何实际意义上担心带宽的想法感到嗤之以鼻(取决于你所在的市场)。这只是一些思考。

下面是用于旋转的一个 CSS 文件:

@charset "UTF-8"; 
/* CSS Document */ 

#gallery{background-image:url(images/homebg.jpg);}   

/* iPhone Portrait --*/ 
@media all and (min-width: 320px){ 
#home{
background-image:url(images/backgroundSmartphone.jpg);
} 
#sessions{ background-image:none; }  
}  

/* iPhone Horizontal / Some Droids --*/ 
@media all and (min-width: 480px){  } 

/* iPad Verticle --*/ 
@media only screen and (min-width: 768px) { 	
#home{background-image:url(images/backgroundSmall.jpg);} 
}  

/* iPad Horizontal --*/ 
@media only screen and (min-width: 1024px) { 
#sessions{ background-image:url(images/Colleen.jpeg) }  
}  

/* Nexus 7 Horizontal --*/ 
@media only screen and (min-width: 1280px) {  }  

/* Laptop 1440 --*/ 
@media only screen and (min-width: 1440px) { 
#sessions{ background-image:url(images/Gliser.jpg) }   
}  

/* Monitor 1600 --*/ 
@media only screen and (min-width: 1600px) {  }  

/* Monitor 1920 --*/ 
@media only screen and (min-width: 1920px) {  } 

现在我们需要决定如何循环它们。我们可以使用setInterval JavaScript 来定时交换样式表。说实话,即使对于一个摄影网站,我认为这有点乐观。我们可能不希望每五秒钟就交换一次。想想:移动设备的使用模式涉及快速、短暂的工作或游戏。大多数人不会在任何给定的移动屏幕上停留超过 5 秒,除非它要么是文字密集的,比如一篇文章,要么制作得如此糟糕以至于用户无法导航。所以可以很肯定地说,setInterval选项不可行。

好吧,也许最好在pagebeforeshow事件上随机选择一个样式表?考虑以下代码:

$(document).on("pagebeforeshow", "[data-role='page']", function(){ 
  $("#rotatingBackgrounds").attr("href", "rotating" + 
Math.floor(Math.random()*4) + ".css");
});

但是当我们尝试这样做时会发生什么?我们会得到奇怪、丑陋的图像闪烁。使用淡入淡出转换或幻灯片,真的无关紧要。使用pageshow事件也没有任何区别。看起来很糟糕。不要这样做。我知道很诱人,但这样做一点也不好看。因此,经过这一切,我建议保留单一、每次会话随机分配的样式表。考虑下面的代码片段:

<link rel="stylesheet" href="" id="rotatingBackgrounds" />
<script type="text/javascript">
$("#rotatingBackgrounds")
  .attr("href","rotating"+Math.floor(Math.random()*4)+".css")
</script>

请注意,我并没有简单地使用document.write()

注意

专业提示

永远不要在 jQuery Mobile 环境中使用document.write()。它会对你的 DOM 造成严重影响,你会摸不着头脑想知道出了什么问题。我以前看到过它折磨过人们。我的朋友的头发已经很少了,这个问题使他抓狂。相信我,要避免使用document.write()

另一种响应式方法 - RESS

响应式设计 + 服务器端组件RESS)是一个非常合理的想法。其概念是使用服务器端的移动设备检测方法,比如WURFLwurfl.sourceforge.net/)。然后,你会发送不同版本的页面组件、不同大小的图片等等。然后我们可以像任何自制的标记一样轻松地改变页面内容和导航的包装以使用 jQuery Mobile。这种方法的美妙之处在于每个人都能得到适合他们的内容,而不会像典型的响应式设计那样臃肿,而且始终在相同的 URL 上。

我第一次看到这个想法被提出是在 2011 年 9 月的一篇文章中,作者是 Luke Wroblewski(twitter.com/lukew),文章链接为www.lukew.com/ff/entry.asp?1392。在文章中,他概述了我们现在面临的与图像相关的性能问题。Luke 认为这是一种在没有任何移动框架的情况下进行纯粹响应式网页设计的方法。

WURFL 可以告诉你所服务的设备的屏幕尺寸,你可以(实时)调整你的摄影师原始的 3 MB 图像大小,缩小到 150 KB、70 KB 等,具体取决于设备分辨率。你仍然希望确保它比你所服务的屏幕尺寸大约两倍,否则用户在 PhotoSwipe 视图中尝试放大照片时将只会看到模糊的混乱。

虽然在某些方面很方便,但 RESS 永远不会是一个完美的解决方案,因为它依赖于浏览器嗅探来完成其工作。那么,这是不好的吗?不,不是真的。没有一个解决方案是完美的,但设备数据库是由社区驱动的,并且快速更新,所以这有所帮助。这将是一个非常可行的解决方案,我们将在下一章更深入地讨论它。

最终代码

本次体验的完整代码有点冗长,不太适合放入一本书中,而且我们已经探讨过相关概念了。我强烈建议你查看代码。到此时,对你来说应该没有什么令人惊讶的了。与之互动。调整它。通过交换服务来建立你的作品集,免费获取一些摄影作品。

摘要

处理响应式设计时,采用移动优先的方法,就像我们这里所做的一样,可以将一个很棒的移动站点变成一个性能非常高的桌面站点,但通常反之则不行。其中的关键在于媒体查询和先从小尺寸开始。如果它在移动设备上运行得如此出色,那么想象一下在没有任何限制的机器上会有多么惊人。在下一章中,我们将探讨 WURFL 和其他移动检测方法,尝试调整现有的网站并使其适应移动设备。

第八章:把 jQuery Mobile 整合到现有网站中

我们并非都有幸只为新网站工作。也许客户不愿意为移动优先的站点付费,或者他们喜欢他们的桌面站点,只想要一个移动站点。你的移动实施可能是未来与客户业务的入口。我们需要准备一些技术手段将 jQuery Mobile 嵌入到他们现有的站点。

我们将涵盖的内容如下:

  • 服务器端、客户端的移动检测,以及两者的结合

  • 移动化全站页面 - 比较困难的方式

  • 移动化全站页面 - 比较简单的方式

服务器端、客户端的移动检测,以及两者的结合

并非每个人都在做响应式设计,所以你很有可能需要知道如何检测移动设备。我们之前只是轻描淡写地谈到过这个话题,现在让我们认真对待它。

浏览器嗅探与特性检测

这个话题有潜力引发一场极客之战。一方面,有人赞美由社区维护的数据库在服务器端执行移动设备检测的优点。WURFL 就是一个典型的例子。使用它,我们可以获取访问我们网站的设备的大量信息。在这里列出所有内容只是浪费空间。可以去查看www.tera-wurfl.com/explore/index.php来看它的运行情况,或者查看所有功能的完整列表在www.scientiamobile.com/wurflCapability/

在辩论的另一面,有人指出服务器端的检测(即使是数据库驱动的)可能导致全新的设备在数据库中没有被识别,直到它们进入数据库,站点管理员更新他们的本地副本。这并非完全正确。所有的安卓都是这样。同样的情况也发生在 iPhone、iPad、BlackBerry 和 Microsoft 上。但是,一个更具有未来前景的(futurefriend.ly/)方法是使用特性检测。例如,设备是否支持画布或触摸事件?几乎可以肯定,如果你支持这些技术和事件,你就有了使用 jQuery Mobile 的移动体验的条件。

无论如何,在这一点上,我们要假设我们正在为一家已经拥有网站且现在也想要一个移动站点的公司工作。因此,我们需要能够检测移动设备并将它们路由到正确的站点。

WURFL – 服务器端数据库驱动的浏览器嗅探

WURFL 拥有 Java、PHP 和.NET 的 API。在wurfl.sourceforge.net/apis.php可以下载适合你的版本。由于几乎每个主机提供商都默认支持 PHP,我们将以 PHP 示例为例:

WURFL – 服务器端数据库驱动的浏览器嗅探

我只是使用了 Mac OS X 自带的服务器,但你也可以使用 MAMP (www.mamp.info/en/index.html)。你可以轻松地在任何托管平台上运行示例,比如 1&1、GoDaddy、Host Gator,你随便选。如果你想在自己的 Windows 计算机上尝试这些示例,你可以使用 XAMPP (www.apachefriends.org/en/xampp.html) 或 WAMP (www.wampserver.com/en/) 作为快捷方式。我不打算在这本书中详细介绍服务器设置和环境配置。这可能需要一本专门的书来解释。

因此,PHP… 这就是我们要做的。从 wurfl.sourceforge.net/php_index.php 开始。从那里,你可以下载最新版本的 WURFL API package 并解压缩它。把整个解压后的文件夹放在你的网站的任何位置。如果一切正常,你应该能够访问演示页面并查看有关你的浏览器和设备的详细信息。在我的 Mac 上,是 127.0.0.1/~sgliser/wurfl-php/examples/demo/index.php,但你的路径可能会有所不同。

当你运行默认示例时,你可以立即看到它有多有用,但让我们让它变得更好一些。我创建的这个版本将最有用的功能放在顶部,并在下面列出所有其他选项:

<?php 
  // Move the configuration and initialization to 
  // the tip so you can use it in the head.  

  // Include the configuration file 
  include_once './inc/wurfl_config_standard.php';  

  $wurflInfo = $wurflManager->getWURFLInfo();  

  if (isset($_GET['ua']) && trim($_GET['ua'])) { 
    $ua = $_GET['ua']; 
    $requestingDevice = $wurflManager->getDeviceForUserAgent($_GET['ua']); 
  } else { 
    $ua = $_SERVER['HTTP_USER_AGENT']; 

    //This line detects the visiting device by looking 
    //at its HTTP Request ($_SERVER) 

    $requestingDevice = $wurflManager->getDeviceForHttpRequest($_SERVER); } ?> 

<html> 
  <head> 
    <title>WURFL PHP API Example</title>     
    <?php if($requestingDevice->getCapability('mobile_browser') !== ""){ ?>     
      <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">         
      <link rel="stylesheet" href="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.css" />         
      <script src="img/jquery-1.8.2.min.js"></script>         
      <script src="img/jquery.mobile-1.2.0.min.js"></script> 
    <?php } ?> 
  </head> 
  <body> 

在这里,我们按照 jQuery Mobile 的方式创建了唯一的真实页面:

  <div data-role="page">     
    <div data-role="header">     	
      <h1>WURFL XML INFO</h1>     
    </div> 
  <div data-role="content" id="content"> 

  <h4>VERSION: <?php echo $wurflInfo->version; ?> </h4> 
  <p>User Agent: <b> <?php echo htmlspecialchars($ua); ?> </b></p> 
  <ul data-role="listview">        
    <li data-role="list-divider">
      <h2>Very Useful</h2>
    </li> 
    <li>Brand Name: <?php echo $requestingDevice->getCapability('brand_name'); ?> </li> 
    <li>Model Name: <?php echo $requestingDevice->getCapability('model_name'); ?> </li> 
    <li>Is Wireless Device: <?php echo $requestingDevice->getCapability('is_wireless_device'); ?></li>             
    <li>Mobile: 
    <?php if($requestingDevice->getCapability('mobile_browser') !== ""){ 
       echo "true"; 
     }else{ 
       echo "false"; 
     }; ?>
    </li>             
    <li>Tablet: <?php echo $requestingDevice->getCapability('is_tablet'); ?> </li>             
    <li>Pointing Method: <?php echo $requestingDevice->getCapability('pointing_method'); ?> </li> 	
    <li>Resolution Width: <?php echo $requestingDevice->getCapability('resolution_width'); ?> </li> 
    <li>Resolution Height: <?php echo $requestingDevice->getCapability('resolution_height'); ?> </li> 
    <li>Marketing Name: <?php echo $requestingDevice->getCapability('marketing_name'); ?> </li> 
    <li>Preferred Markup: <?php echo $requestingDevice->getCapability('preferred_markup'); ?> </li> 

在这里,我们通过循环遍历属性数组来列出 WURFL 中已知数据的整个集合:

    <li data-role="list-divider">
      <h2>All Capabilities</h2>
    </li>         

    <?php foreach(array_keys($requestingDevice->getAllCapabilities()) as $capabilityName){ ?> 
      <li><?php echo "<h3>" .$capabilityName."</h3><p>" .$requestingDevice->getCapability($capabilityName)."</p>"; ?>
      </li>         
    <?php } ?>         
    </ul> 

    <p><b>Query WURFL by providing the user agent:</b></p> 
    <form method="get" action="index.php"> 
      <div>User Agent: <input type="text" name="ua" size="100" value="<?php echo isset($_GET['ua'])? htmlspecialchars($_GET['ua']): ''; ?>" /> 
        <input type="submit" value="submit" />
      </div> 
    </form> 
  </div> 
</div> 
</body> 
</html>

注意

注意,我们通过使用服务器端检测来查看用户是否是移动用户,有条件地将其制作成了 jQuery Mobile 页面。只有在用户是移动用户时,我们才注入 jQM 库。

非常有用 部分下的属性可能是你在日常工作中真正需要的所有内容,但请务必至少浏览一下其他选项。最有用的功能如下:

  • is_wireless_device

  • mobile_browser

  • is_tablet

  • pointing_method

  • resolution_width

  • resolution_height

现在,需要说明的是,这并不能告诉我们有关浏览器/设备的所有信息。例如,iPhone 4S 或 5 将被识别为原始 iPhone。WURFL 也无法区分使用 WURFL 的 iPad mini。这是因为随着 Apple 设备的发展,用户代理从未更新。WURFL 无法知道设备具有高像素密度,因此应该发送更高分辨率的图像。因此,我们仍然需要使用媒体查询来确定像素比率,并相应地调整我们的图形。这里是一个简短的示例:

.logo-large{
  background-image:url(../images/logo.png);
  background-repeat:no-repeat;
  background-position:0 0;
  position:relative;
  top:0;
  left:0;
  width:290px;
  height:65px; 
  margin:0 auto; 
  border:none;
}  

/* HD / Retina ---------------------------------------------*/ @media only screen and (-webkit-min-device-pixel-ratio: 1.5),
       only screen and (min--moz-device-pixel-ratio: 1.5),
       only screen and (min-resolution: 240dpi) 
{ 
  .logo-large{
    background-image:url(../images/logoHD.png);
    background-size:290px 65px;
  }  
}

注意

使用媒体查询几乎是检测 iPad mini 的唯一方法。它具有与 iPad 2 相同的分辨率,只是格式较小。但是,正如我们从前面的代码中可以看到的那样,我们可以使用 DPI 对媒体查询进行限定。iPad 2 的 DPI 为 132。iPad mini 的 DPI 为 163。更多信息,请访问 www.mobilexweb.com/blog/ipad-mini-detection-for-html5-user-agent

到目前为止,我们几乎假定了智能手机,但请记住,jQuery Mobile 是一个同样适用于……不那么智能的手机的框架。您可能有客户在一个不那么发达并且几乎使用手机连接的市场。在那里可能没有那么多启用 JavaScript 的触摸屏手机。在这种情况下,您将无法使用基于 JavaScript 的功能检测。非常快地,WURFL 或其他服务器端检测将成为检测无线设备并为其提供有用内容的唯一合理选项。

基于 JavaScript 的浏览器嗅探

可以说,这可能是(学术上)检测移动设备的最糟糕的方法,但它确实有其优点。这个实用的例子非常有用,因为它给了您很多选择。也许我们的预算有限,因此我们只测试了某些设备。我们想确保我们只让我们知道会有良好体验的人进来。有一个例子:不会允许使用 BlackBerry 版本低于版本 6 的设备,因为我们选择使用了一些版本低于版本 5 的精美 JavaScript 模板。也许我们还没有花时间为平板电脑进行优化,但同时我们可以开始为任何智能手机提供更好的体验。无论如何,这可能会非常有用:

<script type="text/javascript">     
  var agent = navigator.userAgent;      
  var isWebkit = (agent.indexOf("AppleWebKit") > 0);      
  var isIPad = (agent.indexOf("iPad") > 0);      
  var isIOS = (agent.indexOf("iPhone") > 0 || agent.indexOf("iPod") > 0);     
  var isAndroid = (agent.indexOf("Android")  > 0);     
  var isNewBlackBerry = (agent.indexOf("AppleWebKit") > 0 && agent.indexOf("BlackBerry") > 0);     
  var isWebOS = (agent.indexOf("webOS") > 0);      
  var isWindowsMobile = (agent.indexOf("IEMobile") > 0);     
  var isSmallScreen = (screen.width < 767 || (isAndroid && screen.width < 1000));     
  var isUnknownMobile = (isWebkit && isSmallScreen);     
  var isMobile = (isIOS || isAndroid || isNewBlackBerry || isWebOS || isWindowsMobile || isUnknownMobile);     
  var isTablet = (isIPad || (isMobile && !isSmallScreen));     
if ( isMobile && isSmallScreen && document.cookie.indexOf( "mobileFullSiteClicked=") < 0 ) mobileRedirect(); 
</script>

我们在这里做了一些工作,通过创建一个未知移动设备的分类,将其视为运行 WebKit 并具有小屏幕的任何设备,来未来证明检测的有效性。有可能,任何新推出的平台都将使用 WebKit 作为其浏览器。微软是唯一一个似乎仍然认为自己有更多东西可以提供的例外,他们的平台足够容易被嗅探到。尽管这种方法灵活,但如果没有一个 WebKit 浏览器启动一个新平台,就需要直接干预。但是,这种情况并不经常发生。即使发生了,该平台也需要一段时间才能获得值得考虑的关键性质量。如果您按照 80/20 法则(成功达到 80% 并在能够时达到剩下的 20%),那么这将使您的成功率远远超过 90%。

使用 Modernizr 进行基于 JavaScript 的功能检测

有几种方法可以进行功能检测。可能最简单的方法是使用像 Modernizr(modernizr.com/)这样的工具。您可以定制下载以仅检测您关心的功能。如果您想使用 HTML5 音频/视频,知道您是否可以可能很好:

使用 Modernizr 进行基于 JavaScript 的特性检测

这个平台并不是特别轻便。仅在前面的屏幕截图中显示的选项就导致了 12 K 压缩后的 JS。但是嘿,我们可以轻易地处理那样大小的图像。至少 JavaScript 库是有用的。这仍然不会告诉你访问你的用户是否是移动设备,但这是否是正确的问题?

或许,我们只需要知道我们正在查看的设备是否支持触摸事件。其他选项对于知道您可以和不能做什么是很好的,但是如果用户界面是触摸的,即使是平板电脑或全尺寸的触摸型显示器,也应该给用户他们应得的界面。给他们 jQuery Mobile。

基于 JavaScript 的精简特征检测

这个有用的小代码片段是为检测移动设备而凑合在一起的。它是特性检测和浏览器嗅探的混合体。大多数现代智能手机都将支持我们在这里寻找的所有事件和 API。微软,总是显得有些特殊,必须进行浏览器嗅探。根据他们的 Windows Phone 开发者博客,你可以简单地检查用户代理是否为 IEMobile。好吧,这是结果:

if( 
  ('querySelector' in document 
  && 'localStorage' in window      
  && 'addEventListener' in window      
  && ('ontouchstart' in window || 
  window.DocumentTouch && document instanceof DocumentTouch)
  )      

  || navigator.userAgent.indexOf('IEMobile') > 0)
{                  
  location.replace('YOUR MOBILE SITE'); 
}

如果出于某种原因,我们决定不将平板发送到我们的 jQM 杰作,我们总是可以从上一节中加入一些其他测试。

服务器端加客户端检测

这是一个主意,当用户首次访问您的服务器时,发送一个页面,其唯一任务是运行 Modernizer,然后将结果能力返回给服务器,以便所有收集的知识都在一个地方。

这个文件在章节的代码文件包中名为 test.html

<!doctype html> 
<html> 
<head> 
  <style type="text/css"> 

    #sd{display:block;} /*standard def*/ 
    #hd{display:none;} /*high dev*/ 

    @media only screen and 
      (-webkit-min-device-pixel-ratio: 1.5),        
      only screen and (min--moz-device-pixel-ratio: 1.5),        
      only screen and (min-resolution: 240dpi) { 
        #sd{display:none;} /*standard def*/ 	
        #hd{display:block;} /*high dev*/    
      } 
  </style> 
  <script type="text/javascript" src="img/modernizr.custom.94279.js"></script> 
  <script type="text/javascript" src="img/jquery.min.js"></script> 
  <meta charset="UTF-8"> 
  <title>Loading</title> 
</head>  
<body> 
  <div id="hd"></div> 
  <div id="sd"></div> 
</body> 
<script type="text/javascript"> 
  if($("#hd").is(":visible")){ 
    $("html").addClass("hdpi"); 
  }else{ 
    $("html").addClass("sdpi"); 
  } 

  $.post("/~sgliser/wurfl-php/examples/demo/session_set.php", 
    { 
      modernizrData: $("html").attr("class") 
    } 
  ) 
  .success(function(data, textStatus, jqXHR) {  
    console.log(data); 
    location.replace("YOUR MOBILE SITE");  }) 
  .error(function(jqXHR, textStatus, errorThrown) {  
    console.log(errorThrown); 
    location.replace("SOMEWHERE ELSE");  
  }); 
</script> 
</html> 

为了使圆圈完整。这里是一些 WURFL 检测脚本的版本,它将返回 JSON 格式的值,以便我们可以将其存储到 HTML5 的 sessionStorage 中。此文件位于 /wurfl-php/examples/demo/session_set.php

<?php session_start();  

// Move the configuration and initialization 
// to the tip so you can use it in the head.  

// Include the configuration file 

include_once './inc/wurfl_config_standard.php';  

$wurflInfo = $wurflManager->getWURFLInfo();  

if (isset($_GET['ua']) && trim($_GET['ua'])) { 
  $ua = $_GET['ua']; 
  $requestingDevice = $wurflManager->getDeviceForUserAgent($_GET['ua']); 
} else { 
  $ua = $_SERVER['HTTP_USER_AGENT']; 

  // This line detects the visiting device by looking 
  // at its HTTP Request ($_SERVER) 

  $requestingDevice = $wurflManager->getDeviceForHttpRequest($_SERVER); 
}  

// store session data $_SESSION['wurflData']=$requestingDevice; 

$_SESSION['modernizrData']=$_POST['modernizrData'];  

$i = 0; 

$capabilities = $requestingDevice->getAllCapabilities(); 
$countCapabilities = count($capabilities); 
?> 
{ 
  "wurflData": <?php  

  //echo json_encode($capabilities); 
  foreach(array_keys($capabilities) as $capabilityName){  
    $capability = $requestingDevice->getCapability($capabilityName); 
    $isString = true; 	
    if($capability == "true" || 
       $capability == "false" || 
       is_numeric($capability))
    { 
      $isString = false; 
    } 

    echo "\"".$capabilityName
      ."\":".(($isString)?"\"":"")l
      .$requestingDevice->getCapability($capabilityName)
      .(($isString)?"\"":"");  

    if(($i + 1) < $countCapabilities){ 
      echo ",\n";  
    } 

    $i++; 
  }   
?> 
}

这个示例已经注释掉了 JSON 编码关联数组的简单方式。用一些 PHP 代码替换,将发送回使用真实布尔值和数值的 JSON 编码,而不是将所有内容都存储为字符串。

有了这些文件,你现在可以了解关于你的访问者在服务器端和客户端的一切都是可知的。

移动化全站页面 - 走弯路

为什么要走弯路?为什么?实际上只有一个很好的理由:为了将内容保持在同一页上,这样用户就不会有一个用于移动设备的页面和一个用于桌面的页面。当电子邮件和推特等信息飞来飞去时,用户通常不在乎他们是发送移动视图还是桌面视图,而且他们也不应该在乎。就他们而言,他们正在向某人发送内容。这是响应式设计的主要论点之一。但别担心,当我们也以简单的方式处理事情时,我们将在稍后考虑到这一点。

一般来说,很容易看出站点的哪些部分会转换为移动站点。几乎不管站点布局如何,您都会在现有标签上添加data属性来使其移动化。当页面上没有 jQuery Mobile 的库时,这些属性将保持原样,不会造成任何伤害。然后您可以使用我们的许多检测技术之一来决定何时添加 jQM 库。

了解您的角色

让我们考虑一些移动页面所需的关键data-role属性:

  • data-role="page":这包含了移动视图中将显示的所有内容。

  • data-role="header":这会将h1h2h(x)和多达两个链接包装成条形外观,并将链接转换为按钮。您可以将更多内容放入页眉中,但这是不建议的。如果您有很多内容尝试挤入页眉中,您可能最好只留一个“菜单”按钮。页眉可以固定其位置。页眉内的任何内容都将固定在顶部。

  • data-role="content":这为你的内容提供了边距。

  • data-role="button":这将链接转换为按钮。

  • data-role="navbar":这在链接列表周围包装时创建一个导航栏。

  • data-role="footer":这会在底部包装任何您想要的内容。这是次要链接、下一步导航、联系我们以及所有标志着所有有用性结束的法律内容的绝佳位置。这也可以设为固定位置。

  • data-role="none":这将防止 jQuery Mobile 对内容进行样式处理。

从理想的用户体验角度来看,页面上的内容不应该超出用户完成他们访问该页面的任务所需的内容。让我们为失去的梦想默哀一会… 在此之前,请记住,任何data-role="page"中的内容都将显示在移动视图中。因此,在大多数全站页面上,您可以做的最好的事情就是确定用户实际上想要来到该页面的页面部分,然后使用content角色标记该部分,并立即用page角色包装起来。这样做,您将自动剔除大多数网页其余部分的琐事。

第 1 步中的第 1 步 – 关注内容,市场抗议!

此时,拥有市场营销背景的任何人可能会因为这种方法削减了他们的宣传和定向广告等而哭泣。然而,值得注意的是,人们已经有能力很长时间以来能够自己做这件事。诸如 Pocket(前身为 Read it Later)、Instapaper,甚至 iOS Safari 上的简单阅读工具等具有争议的服务都能向用户提供他们想要的内容。下面是一个普通桌面站点的例子,左边是 iOS Reader 如何去除除内容本身以外的一切。

第 2 步中的第 1 步 – 关注内容,市场抗议!

我们有一个选择;提供用户想要的格式和内容,或者可能会失去与他们联系的机会,因为他们会转向这些工具。这将需要在移动端进行更有创意的市场营销活动。但不要误解,除了页面核心以外的所有内容都应该是你的第一步。

在清除了页面的除主要内容以外的所有内容之后,我们还需要清除当前位于头部的样式和脚本。如果我们可以修改页面本身,我们可以轻松地在服务器端使用 WURFL 来实现这一点。否则,我们可以始终使用 JavaScript 来删除我们不想要的样式表和脚本,然后注入我们自己的样式表和脚本。我们还可以简单地劫持第一个样式表,然后删除其余的样式表,并以同样的方式处理脚本,首先引入 jQuery,然后是 jQuery Mobile。有一千种方式可以解决这个情况,但如果您打算以这种方式移动现有页面,我真的建议使用 WURFL。否则,事情会变得一团糟。

第 2 步/2 - 选择全局导航样式并插入

所以,在这一点上,我们已经有了页面的开头,但可能仍然有一些需要移除的小东西。拥有一个移动端样式表来处理那些少数需要覆盖的样式会非常有帮助,而且比使用 JavaScript DOM 操作更快。这很简单,下一个重要的问题是,我们应该如何处理全局导航,因为我们刚刚明确地排除了它。

全局导航作为单独的页面

这可能是最简单的方法,并尽可能保持界面的清洁(在以下步骤中提到):

  1. 将全局导航包装在自己独立的pagecontent角色中,并确保它们易于选择。

  2. 在页面底部(或者在全局导航和内容完成后的任何位置)放置一个脚本,将全局导航所在的页面移动到内容下方。这一点特别重要,因为我们现在处于多页面视图中,而 DOM 中的第一个“页面”将在 jQuery Mobile 启动时显示给用户。我们希望在 jQuery Mobile 甚至知道自己应该做些什么之前就完成这个操作。如果我们不这样做,那么来到网站上期望阅读某些内容的用户首先会被全局导航所迎接。以下是基于我们之前看到的页面的一个非常简单的示例:

    $("#NavMainC").insertAfter("#ContentW");
    
  3. 在这些内部页面中添加标题,以便它们可以相互链接:

    $("#ContentW").prepend("<div data-role='header'><h3>"+$("title").text()+"</h3><a href='#NavMainC' data-icon='grid' class='ui-btn-right'>Menu</a></div>") 
    
    $("#NavMainC").prepend("<div data-role='header'><a data-rel='back' data-icon='back' href='javascript://'>Back</a><h3>Menu</h3></a>");
    

    全局导航作为单独的页面

底部的全局导航

在诸如文章之类的页面中,用户可能会一直阅读到底部,将菜单放在页面底部并不罕见。这是一种促进持续参与的方法。他们已经在那里了,对吧?也许你可以加上一两篇相关文章的链接,然后将全局菜单附加到页面底部。这样,用户就有了更多内容可供阅读,而不必滚动回页面顶部:

底部的全局导航

就我个人而言,我认为采取这种两方面的方法是最好的。顶部菜单链接到底部,底部菜单包括返回顶部的链接。这是通过$.mobile.silentScroll函数实现的。

全局导航作为面板

从 jQuery 1.3 开始,现在有一个Panel组件,可以直接嵌入到页面中,然后通过按钮单击来显示。它就像 Facebook 应用程序一样:

全局导航作为面板

这可能是全局导航的最简单方法。它还有一个好处,即不会更改页面或使界面混乱。有关新面板小部件的完整 API 和选项,请查看 view.jquerymobile.com/1.3.0/docs/widgets/panels/

困难的方式 - 最终想法

总的来说,将属性注入到完整网站页面中并调用 jQuery Mobile 的方法可能效果不错。你将遇到的最大问题是大多数页面上堆积的垃圾太多了。需要大量的清理和/或 CSS 处理。这也有一个不幸的副作用,那就是它相当脆弱。如果有人稍微修改了页面,可能会破坏你的实现。我只能在页面使用模板或内容管理系统(CMS)创建,以便网站结构的更改不会经常发生,并且发生更改时是统一的情况下,才会推荐这种方法。

移动化完整网站页面 - 简单方式

没有比创建一个独立的 jQuery Mobile 页面更容易和更清晰的了。让我们就这样做,简单地使用 AJAX 导入我们想要的页面。然后我们可以取出我们想要的部分,其余的部分就留下来。

这种方法的最大缺点主要是学术上的。渐进增强被抛弃了。对于设备上没有 JavaScript 的任何人来说,网站完全崩溃。我的观点是这可能并不重要。我不能代表每个地方,但在美国,如果你没有智能手机,你就不能用你的设备上网。就这么简单。当然也有例外只能证明规则。但是,如果你的市场不同,你可能要考虑这个选项是否适合你。因此,让我们继续。

在任何给定的页面上,我们实际上只需要一个简单的重定向,以便使用我们列出的众多方法之一的移动设备上的任何人。然后,只需使用一个简单的location.replace。这个代码示例比这个更多。它检查用户是否在移动设备上并单击了完整网站链接。如果是这样,我们将插入一个iframe标签,以允许用户手动切换回移动视图。否则,我们将只是将他们弹回到移动视图。

if (isMobile && isSmallScreen){  
  if(document.cookie.indexOf("mobileFullSiteClicked=")<0){ 
    location.replace("mobileadapter.php?p="
      +escape(location.pathname));
  }else{ 
    document.addEventListener("DOMContentLoaded", function(){ 
      try{ 
        var iframe = document.createElement("iframe");
        iframe.setAttribute("src","gomo.html"); 
        iframe.setAttribute("width","100%"); 
        iframe.setAttribute("height","80");  
        document.body.insertBefore(
          iframe,
          document.body.firstChild); 
      }catch(e){alert(e);} 
    }, false); 
  } 
}

这是一个允许完整网站链接到移动端的页面的代码。此文件是章节代码文件中的gomo.html

<!doctype html> 
<html> 
<head> 
  <meta charset="UTF-8"> 
  <style type="text/css"> 
    body{ background-color:#000;} 
    p{
      font-size:60px; 
      font-family:Arial, Helvetica, sans-serif; 
      text-align:center;
    } 	
    a{color:white;}  
  </style> 
</head>  
<body> 
<script type="text/javascript"> 
  document.write("<p><a href='mobileadapter.php?p="
    +escape(window.parent.location.pathname)
    +"' target='_top'>Switch to mobile view</a>"
    +"<img src='32-iphone@2x.png'/></p>");     
</script> 
</body> 
</html> 

这两个页面都使用了不需要 jQuery 的脚本。如果每个页面都有 jQuery 就好了,但是市场上有其他竞争平台,我们不能指望我们要移动的基本页面已经为我们准备好了。原生 JavaScript 更快。我们可以直接将其放在页面顶部,而无需先引入库。

移动全站页面-简便方法

这是包含移动内容的 jQuery Mobile 页面。它也链接回全站视图并设置一个 cookie,这样用户点击全站链接时就不会直接被弹回移动页面。

如前所述,我们正在拉取下一个前 3 篇文章,并将它们放在菜单底部之前,以保持用户的参与度。在这个视图中做起来要容易得多。

该示例还利用了replaceState。对于所有支持它的浏览器,当用户来到移动页面时,地址栏和历史记录中的 URL 都将被更新,以显示原始文章的 URL。

现在,不再拖延,我们将看到如何轻松地移动全站页面的最佳示例。它足够通用,你可能只需将其应用到你正在工作的任何项目中,并只需调整做拉取和注入的代码即可:

<!DOCTYPE html>  
<html>  
<head> 
  <meta charset="utf-8"> 	
  <meta name="viewport" content="width=device-width, initial-scale=1">  
  <title class="pageTitle">Loading...</title>  
  <link rel="stylesheet" href="http://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.css" /> 
  <script src="img/jquery-1.8.2.min.js"></script> 
  <script src="img/jquery.mobile-1.3.0.min.js"></script>     
  <!-- cookie code from https://github.com/carhartl/jquery-cookie -->
  <script src="img/jquery.cookie.js"></script>
  <style type="text/css"> 
    #iscfz,.comment-bubble{display:none;} 
    #bottomMenu .byline
    {
      padding:0 0 8px 12px; 
      font-weight:normal;
    } 	
  </style> 
</head>   
<body>   
<div id="mainPage" data-role="page">

这一部分是 jQuery Mobile 1.3 中可用的新面板。它将接收全局菜单:

      <div data-role="panel" id="globalmenu" data-position="left" data-display="reveal" data-theme="a">     
      <ul data-role="listview"></ul>         
      <!-- panel content goes here -->     
   </div><!-- /panel --> 

  <div data-role="header"> 		
    <a href="#globalmenu" data-icon="bars">Menu</a>
    <h1 class="pageTitle">Loading...</h1>         

  </div><!-- /header -->  
  <div id="mainContent" data-role="content">	          
  </div><!-- /content -->     
  <div>     
    <ul id="bottomMenu" data-role="listview"></ul>     
  </div> 	
  <div data-role="footer"> 
    <h4>
      <a class="fullSiteLink" data-role="button" data-inline="true" href="<?php echo htmlspecialchars(strip_tags($_REQUEST["p"])) ?>" target="fullsite">Full Site View</a>
    </h4> 
  </div><!-- /footer --> 	 
</div><!-- /page -->  

<script type="text/javascript"> 	

  $.cookie("mobileFullSiteClicked","true", {
    path:"/",expires:0}
  );  //0 minutes - erase cookie 

我们在这里为了替换用户历史记录中的状态而采取的措施,并不是所有移动浏览器都完全支持。为了安全起见,我已经将那行代码放在了 try/catch 块中。对于那些在你的客户群体中部分支持的东西,这是一个不错的技巧。

  try{ 
    //make the URL the original URL so if the user shares 
    //it with others, they'll be sent to the appropriate URL 
    //and that will govern if they should be shown 
    //mobile view. 
    history.replaceState({},"","<?php echo htmlspecialchars(strip_tags($_REQUEST["p"])) ?>"); 
  }catch(e){ 
    //history state manipulation is not supported 
  }  

  //Global variable for the storage of the imported 
  //page content. Never know when we might need it 
  var $pageContent = null; 

  //Go get the content we're supposed to show here 
  function loadPageContent(){ 

    $.ajax({ 
       //strip_tags and htmlspecialchars are to to help 
       //prevent cross-site scripting attacks 
       url:"<?php echo htmlspecialchars(strip_tags($_REQUEST["p"])) ?>",
       beforeSend: function() { 
         //show the page loading spinner 
         $.mobile.loading( 'show' );
       }
     }) 
    .done(function(data, textStatus, jqXHR){ 

        //jQuery the returned page and thrown it into 
        //the global variable 
        $pageContent = $(data); 

        //take the pieces we want and construct the view  
        renderPage(); 	
     }) 
    .fail(function(jqXHR, textStatus, errorThrown){ 

        //let the user know that something went wrong 
        $("mainContent").html("<p class='ui-bar-e'>Aw snap! Something went wrong:<br/><pre>"+errorThrown+"</pre></p>"); 
      })
     .always(function(){ 
        //Set a timeout to hide the image, in production 
        //it was being told to hide before it had even been shown 	
        //resulting a loading gif never hiding   
        setTimeout(function(){$.mobile.loading( "hide" )}, 300); 
     });; 
  } 

这一部分负责拆分导入的页面并将其注入到正确的位置。请注意,我在开始处选择对象并在名称前加上美元符号。我们为了性能而预先选择它们。任何你要引用超过一次的东西都应该存储到一个变量中,以减少 DOM 遍历来重新选择它。美元符号的原因是它提示编码人员,他们看到的变量已经被 jQuery 处理过了:

  function renderPage(){ 
    var $importedPageMainContent = $pageContent.find("#main"); 
    var $thisPageMainContent = $("#mainContent"); 

    //pull the title and inject it. 
    var title = $importedPageMainContent.find("h1.title").text(); 	

    $(".pageTitle").text(title); 

    //set the content for the main page starting 
    //with the logo then appending the headline, 
    //byline, and main content 
    var $logo = $pageContent.find("#logo-headerC img"); 

    $thisPageMainContent.html($logo);  
    $thisPageMainContent.append(
      $importedPageMainContent.find("h1.title")
    ); 
    $thisPageMainContent.append(
      $importedPageMainContent.find("div.byline")
    ); 
    $thisPageMainContent.append(
      $importedPageMainContent.find("div.the-content")
    ); 

    var $bottomMenu = $("#bottomMenu"); 

    //Take the next 3 top stories and place them in the 
    //bottom menu to give the user something to move on to.   
$bottomMenu.html("<li data-role='list-divider'>Read On...</li>"); 	
    $bottomMenu.append(
       $pageContent.find("#alldiaries li:lt(3)")
    );  

    //Inject the main menu items into the bottom menu 

    $bottomMenu.append("<li data-role='list-divider'>Menu</li>"); 	

    var $mainMenuContent = $pageContent.find("#NavMain");  
    $bottomMenu.append($mainMenuContent.html()); 

    //After doing all this injection, refresh the listview 
    $bottomMenu.listview("refresh"); 

    //inject the main menu content into main menu page 
    var $mainMenContent = $("#mainMenuContent"); 
    $mainMenContent.find("ul").append(
      $mainMenuContent.html()
    ); 
  } 

  //once the page is initialized, go get the content. 
  $("[data-role='page']").live("pageinit", loadPageContent); 
  //if the user clicks the full site link, coolie them 
  //so they don't just bounce back.
  $("a.fullSiteLink").live("click", function(){ 
    $.cookie("mobileFullSiteClicked","true", 
      {path:"/",expires:30});  //30 minutes 
  }); 

</script> 
</body> 
</html>

注意

此处使用的 cookie 管理来自于 jQuery cookie 插件,网址为github.com/carhartl/jquery-cookie

摘要

本书前面我们深入探讨了移动检测。现在你知道了所有需要知道的内容。之前,我们从零开始创建移动站点,很少关心它们的桌面体验。现在你知道如何统一它们了。困难的部分是要知道何时从零开始设计移动体验,何时简单地将整个站点体验移动化。可惜这并没有简单的答案。但是,无论是通过在页面上使用 JavaScript 将其转换为移动端(较为困难的方式),还是通过 AJAX 加载内容并选择所需的部分(较为简单的方式),或者是通过响应式设计 + 服务器端组件(RESS),正如我们在前一章中提到的那样,现在你已经准备好处理几乎每种可能的情况了。我们还没有真正解决的唯一问题是与 CMS 集成,这将在下一章中完成。

第九章:内容管理系统和 jQM

“我是一个网页开发者。每次客户想要更改时,将微软 Word 文档剪切粘贴到网页上是对我的时间和才能的浪费” —— 到处都能听到,无数次。

如果这个说法在你心中有共鸣,那么你需要熟悉内容管理系统(CMS)。它们是将发布权交到用户手中的一种简单而强大的方式,这样你就可以专注于不那么繁琐、报酬更高的工作。你需要做的就是帮助客户设置他们的 CMS,选择并定制他们的模板,然后把内容创建和维护交给他们。CMS 通常是小型企业网站和企业网站的核心。

对于流行的平台,有许多插件和主题可供选择。宣传册网站从未如此简单。事实上,像 WordPress 和 Squarespace 这样的平台正在使这个过程变得如此简单,以至于通常一个网页开发者只需要定制外观和感觉,其他什么都不需要做。

那么,为什么还要包括这一章?因为 CMS 的普及几乎总是意味着,如果你要制作移动 Web 应用,迟早会遇到一个已经在 CMS 中拥有网站的客户,你需要知道如何集成。

在本章中,我们将涵盖:

  • 当前的 CMS 格局

  • WordPress 和 jQuery Mobile

  • Drupal 和 jQuery Mobile

  • 更新你的 WordPress 和 Drupal 主题

  • Adobe Experience Manager (AEM)

当前的 CMS 格局

WordPress 是世界上最受欢迎的 CMS,按数量计算。对于前 10,000 个网站,有 8.3%是建立在 WordPress 上的。下一个最高的是 Drupal,占 2.95%。尽管听起来似乎不多,但看看这个图表:trends.builtwith.com/cms。在所有使用 CMS 的网站中,WordPress 和 Drupal 占了近 75%。

当前的 CMS 格局

WordPress 和 jQuery Mobile

WordPress 之所以受欢迎,是因为它简单易用。你可以通过在WordPress.com上创建托管站点开始使用 WordPress,或者你可以通过访问WordPress.org下载源代码,并在任何你喜欢的机器上安装。在你进行实验时,我强烈建议采用后一种方法。本章使用的版本是 3.5。

要快速上手任何 CMS 的关键是,认识到要使用哪些插件和主题。对于 WordPress,我不建议使用 jQuery Mobile 插件。当我为本章做实验时,它破坏了管理界面,并且总体上是一次痛苦的经历。然而,有几个 jQuery Mobile 主题可以很好地为你服务。有些是免费的,有些是付费的。无论哪种方式,尽量不要重复造轮子。选择一个最接近你想要的主题,然后进行微调。到目前为止,很可能你已经足够好,可以修改现有的主题文件。以下是我找到并喜欢的一些主题链接。选择一个,解压缩它,并将其放入你的 WordPress 安装目录下的 wp-content/themes/ 中:

如果你成功安装了主题,你应该能在管理界面的 外观 | 主题 下看到它,如下一张图左侧所示。它应该在 可用主题 下列出:

WordPress 和 jQuery Mobile

接下来,我们需要一种方式来在移动设备上访问主题。这就是移动主题切换器发挥作用的地方。我们将在这里使用的切换器简单而有效,适用于大多数可能访问你的站点的人。

手动安装移动主题切换器

要手动安装移动主题,请从 wordpress.org/extend/plugins/mobile-theme-switcher/ 下载它。解压文件夹并将其放入你的 WordPress 安装目录下的 wp-content/plugins/ 中:

手动安装移动主题切换器

接下来,通过管理界面,激活名为 Mobile theme switch 的插件:

手动安装移动主题切换器

自动安装移动主题切换器

如果你喜欢的话,你可以让 WordPress 为你完成大部分工作。就我个人而言,我喜欢掌控一切。以下是通过管理界面安装的方法:

  1. 转到 插件 页面,然后在标题旁边找到 添加新 按钮,如下一张截图所示:自动安装移动主题切换器

  2. 在下一个屏幕上,搜索移动主题切换器自动安装移动主题切换器

  3. 有很多选择可供选择,我们使用的是第一个:自动安装移动主题切换器

  4. 在下一页上输入你的 FTP 凭据。

  5. 激活你新安装的插件。

配置移动主题切换器

如果你已经成功安装并激活了插件,它现在将显示在外观菜单下,如下面的屏幕截图所示。然后,选择你安装的移动主题,点击更新选项按钮:

配置移动主题切换器

插件和主题的组合是强大、简单且有效的。以下是新主题运行的屏幕截图:

配置移动主题切换器

相当简单,对吧?现在,我们只需要调整它直到客户满意。让我们继续下一个 CMS 系统。

Drupal 和 jQuery Mobile

Drupal 是一个功能更强大的 CMS。使用其中一些标准插件,你可以轻松创建完整的网络应用程序,而不仅仅是宣传册网站。想要在发布评论前让人们证明他们是人类吗?有一个插件可以做到。想要创建联系表单吗?它是内置的。想要创建一个自定义数据库表和表单来保存输入吗?从 Drupal 7 开始,这也是内置的。

Drupal 最大的缺点是,如果你想要发挥它真正的威力,它有点学习曲线。此外,在没有进行一些调整的情况下,它可能会有点慢,并且可能会让你的页面代码变得臃肿。像缓存这样的技术可以提高性能,但也可能会对动态创建的页面产生负面影响。

为 jQuery Mobile 配置 Drupal 的过程与 WordPress 的几乎相同。同样,我们将从已经存在的主题开始。制作这些主题的人知道他们正在编码的系统。不要试图重新发明轮子。我们所要做的就是使用这个主题并进行微调。我最喜欢的 Drupal jQM 主题可以在drupal.org/project/mobile_jquery找到。在该页面的底部,你将找到主题的可下载分发:

Drupal 和 jQuery Mobile

  1. 复制适合你的分发的链接。

  2. 登录到你的 Drupal 网站的管理控制台,并转到外观部分:Drupal 和 jQuery Mobile

  3. 点击安装新主题链接,并将你复制的链接粘贴到从 URL 安装字段中。点击安装按钮,让安装完成所有步骤。Drupal 和 jQuery Mobile

  4. 在这一点上,您可能无法看到已安装的主题。制作者鼓励您创建子主题,而不是使用他们的基础安装主题。这是我们将要忽略的一个建议。所以,为了使主题显示出来,您需要编辑位于 Drupal 安装目录中sites/all/themes/jquery_mobile/中的文件mobile_jquery.info,并将hidden的值从1更改为0。一旦你这样做了,你应该会在外观菜单的禁用主题部分看到主题列表,如下一个屏幕截图所示。单击启用链接,您的主题将准备好配置和使用。Drupal 和 jQuery Mobile

  5. 接下来,我们需要安装主题切换插件。让我们使用位于drupal.org/project/mobile_theme的插件。同样,选择正确的版本并复制其网址。Drupal 和 jQuery Mobile

  6. 打开管理员界面到模块部分,然后单击安装新模块链接:Drupal 和 jQuery Mobile

  7. 将网址粘贴到标记为从 URL 安装的字段中,然后单击安装按钮。让安装过程自动进行。Drupal 和 jQuery Mobile

  8. 模块部分的底部,您将找到新安装的插件:Drupal 和 jQuery Mobile

  9. 单击复选框以启用模块,然后您将能够配置它:Drupal 和 jQuery Mobile

  10. 点击配置链接将带您到一个用于配置全局设置的屏幕。在该屏幕的右侧,您会找到一个用于配置移动主题选项的部分。移动主题部分在下面的屏幕截图中已经用红色箭头标记出来了:Drupal 和 jQuery Mobile

结果不言而喻。该主题肯定需要定制,但对于初学者来说,它完全可以使用。我们知道如何做其余的事情了。

Drupal 和 jQuery Mobile

更新您的 WordPress 和 Drupal 模板

在某些时候(可能是安装后的右后),您会想要更新这些主题以使用最新版本的 jQuery Mobile 库。一些仍在使用 beta 版本。实际上,这个过程非常简单。你只需找到相关模板的头部部分,并更新对 jQuery Mobile CSS、JS 和可能的核心 jQuery 库的引用。

WordPress – Golden Apples jQM 主题

对于 Golden Apples 的 WordPress 主题(参见 github.com/goldenapples/jqm-boilerplate),您需要更改多个文件。在header.php文件中,找到并更新以下行:

<link rel="stylesheet" href="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.css" />

functions.php文件中,你需要找到并更新以下行:

wp_enqueue_script( 'jquery',"http://code.jquery.com/jquery-1.6.4.min.js" );

wp_enqueue_script( 'jquery-mobile',"http://code.jquery.com/mobile/1.0.1/jquery.mobile-1.0.1.min.js",array( 'jquery' ) );

wp_enqueue_script( 'mobile-scripts',get_stylesheet_directory_uri().'/lib/mobile-scripts.js', array( 'jquery', 'jquery-mobile' ) );

wp_localize_script( 'mobile-scripts', 'siteData', array( 'siteUrl', home_url() ) );

wp_enqueue_style( 'jquery-mobile', "http://code.jquery.com/mobile/1.0.1/jquery.mobile-1.0.1.min.css" );

Drupal – jQuery Mobile 主题

对于Drupal jQuery Mobile 主题,你最快更新主题的方法是编辑theme文件夹根目录下的template.php文件。在文件中找到以下行并更新对 jQuery Mobile 的引用:

drupal_add_css('http://code.jquery.com/mobile/1.0.1/jquery.mobile.structure-1.0.1.min.css', array_merge($css_options,array('weight' => 100)));
drupal_add_css('http://code.jquery.com/mobile/1.0.1/jquery.mobile-1.0.1.min.css', array_merge($css_options, array('weight' => 100)));

drupal_add_js('http://code.jquery.com/jquery-1.6.4.min.js', array_merge($js_options, array('weight' => 100)));

drupal_add_js(drupal_get_path('theme', 'mobile_jquery') . '/scripts/mobile_jquery.js', array_merge($js_options, array('weight' => 101)));

drupal_add_js('http://code.jquery.com/mobile/1.0.1/jquery.mobile-1.0.1.min.js', array_merge($js_options, array('weight' => 101)));

Adobe Experience Manager

Adobe 一直是网络领域的领导者。他们的首席企业 CMS 名为 Adobe Experience Manager (AEM)(参见www.adobe.com/solutions/web-experience-management.html)。我不会介绍如何安装、配置或为 AEM 编写代码。这是几本本书那么大的培训手册的主题。相信我。我只是提到这一点,以便你知道至少有一个主要的 CMS 玩家提供了完整的 jQuery Mobile 示例。

培训材料以名为 Geometrixx 的虚构站点为中心。

Adobe Experience Manager

AEM 系统的美妙之处在于它使用 Java JCR 容器(参见en.wikipedia.org/wiki/Content_repository_API_for_Java)来存储内容。这意味着你可以创建自动从桌面页面中提取内容的移动站点,只需引用桌面页面的 JCR 内容节点或允许用户直接在看起来像移动屏幕的界面中输入。

Geometrixx 的移动示例使用了 jQuery Mobile 编写;尽管 jQM 的版本有些过时,但更改模板很容易。移动内容作者界面带有模拟手机界面,以便对内容进行框架化,使其看起来大致像在真实手机或平板电脑上。你可以在作者界面中直接切换设备配置文件。虽然这并不是对这些设备的真正模拟,因为一切都发生在你正在使用的浏览器中,但它仍然非常非常方便。

Adobe Experience Manager

如果你为一家能负担得起 AEM 的公司工作,你已经非常熟悉移动实现。这个平台给内容作者带来的力量是惊人的。

概要

自从两年前我开始涉足移动开发以来,移动主题的世界已经爆炸式增长。今天,有很多 jQuery Mobile 的选择;还有一些其他响应式主题。我没费力去列出 Google 能给我们的所有东西。当这本书出版时,即使在一个月的时间里,这些也会发生变化。要记住的重要一点是,我们不必重新发明轮子,也不必让自己背负内容更新。让你的客户有能力自行进行小型更新,而你回到你的事务中去。CMS 虽然有用,但我们不会再次涉及它。下一章将回到定制开发,我们将结合到目前为止学到的一切。

第十章:将一切汇聚在一起 - Flood.FM

Flood.FM 是一个独特的想法。这是一个网站,听众将受到来自几个流派和地理区域的本地独立乐队的音乐的欢迎。构建这个网站将需要我们迄今为止开发的许多技能,并且我们将在这项新服务中使用一些新的技术。我们已经在便签上绘制了界面,并使用了 GPS 和客户端模板。我们已经处理了常规的 HTML5 音频和视频。我们甚至已经开始处理多个移动尺寸,并使用媒体查询将我们的布局重新设计为响应式设计。

所有这些都是为了完成任务并尽可能优雅地失败而简化的实现。让我们看看我们可以在这个项目上使用什么技术和技巧。

在本章中,我们将涵盖:

  • 一份 Balsamiq 的味道

  • 组织你的代码

  • Web Audio API 简介

  • 引导用户安装你的应用程序

  • 新的设备级硬件访问

  • 要做应用还是不要做应用,这是个问题

  • PhoneGap 与 Apache Cordova

一份 Balsamiq 的味道

我们通过学习一种称为纸质原型的技术来开始这本书。对于与客户一起工作,这是一个很好的工具。然而,如果你正在处理更大或分布式的团队,你可能需要更多。Balsamiq (www.balsamiq.com/) 是一个非常流行的用于快速原型设计的 UX 工具。它非常适合创建和共享交互式的模型。

一份 Balsamiq 的味道

当我说非常流行时,我是指你习惯看到的许多大公司。超过 80,000 家公司都在使用 Balsamiq Mockups 来创建他们的软件。

一份 Balsamiq 的味道

所以,让我们看看 Flood.FM 的创建者们打算做什么。这是他们绘制的第一个屏幕;到目前为止,它看起来像是一个非常标准的实现。它在底部有一个图标工具栏,在内容中有一个列表视图。实际上,将其翻译成中文非常简单。我们以前使用 Glyphish 图标和标准工具栏做过这样的事情。

一份 Balsamiq 的味道

理想情况下,我们希望保持这个特定实现纯粹的 HTML/JS/CSS。这样,我们可以在某个时候使用 PhoneGap 将其编译为本机应用程序。但是,我们希望忠于 DRY(不要重复自己)原则。这意味着我们想要在每个页面上注入这个页脚,而不使用服务器端过程。为此,让我们设置一个应用程序的隐藏部分,其中包含我们可能想要的所有全局元素:

<div id="globalComponents">
  <div data-role="navbar" class="bottomNavBar">
    <ul>
      <li><a class="glyphishIcon" data-icon="notes" href="#stations_by_region" data-transition="slideup">stations</a></li>
      <li><a class="glyphishIcon" data-icon="magnify" href="#search_by_artist" data-transition="slideup">discover</a></li>
      <li><a class="glyphishIcon" data-icon="calendar" href="#events_by_location" data-transition="slideup">events</a></li>
      <li><a class="glyphishIcon" data-icon="gears" href="#settings" data-transition="slideup">settings</a></li>
    </ul>
  </div>
</div>

我们将把这段代码放在页面底部,并在样式表中使用一个简单的 CSS 规则来隐藏它,#globalComponents{display:none;}

现在让我们来设置我们的应用程序,在创建每个页面之前将全局页脚插入其中。使用clone()方法(下一行的代码段中显示)可以确保我们不仅复制了页脚,还带上了附加的任何数据。这样,每个页面都带有完全相同的页脚,就像它在服务器端一样。当页面经过正常的初始化过程时,页脚将接收与页面其余部分相同的标记处理。

/************************
*  The App
************************/
var floodApp = {
  universalPageBeforeCreate:function(){
    var $page = $(this);
    if($page.find(".bottomNavBar").length == 0){
      $page.append($("#globalComponents .bottomNavBar").clone());
    }

  }
}

/************************
*  The Events
************************/
//Interface Events
$(document).on("pagebeforecreate", "[data-role="page"]",floodApp.universalPageBeforeCreate);

看看我们在这段 JavaScript 代码中所做的。这跟我们之前做的有点不同。我们实际上更有效地组织了我们的代码。

组织你的代码

在之前的章节中,我们的代码结构非常松散。事实上,我确信学术界的人一定会嘲笑我们敢称之为结构化的胆量。我相信编码非常务实的方法,这导致我使用更简单的结构和最少的库。不过,其中也有一些价值和经验可以借鉴。

MVC、MVVM、MV*

过去几年里,一些认真对待 JavaScript 开发的人都将后端开发结构引入到网页开发中,因为项目的规模和范围需要更加有条理的方法。对于雄心勃勃、持续时间长、纯网页端的应用来说,这种结构化方法可以提供帮助。特别是如果你在一个较大的团队中。

MVC代表"Model-View-Controller"(参见en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller),MVVM代表"Model View ViewModel"(参见en.wikipedia.org/wiki/Model_View_ViewModel),而MV*是缩写,代表"Model View Whatever",是总称,用来概括将这些结构带到前端的整个运动。

一些更流行的库包括:

更全面的比较可以在codebrief.com/2012/01/the-top-10-javascript-mvc-frameworks-reviewed/上找到,还有其他的。

如何使 Backbone 与 jQuery Mobile 良好协作的适配器和示例可以在view.jquerymobile.com/1.3.0/docs/examples/backbone-require/index.php找到。

琥珀的示例可以在github.com/LuisSala/emberjs-jqm找到。

Angular 还在进行 jQM 的适配器。github.com/tigbro/jquery-mobile-angular-adapter 上有几个示例。

MV* 和 jQuery Mobile

是的,你可以做到。你可以将任何一个 MV* 框架添加到 jQuery Mobile 中,并制作出你喜欢的复杂应用程序。在其中,我倾向于在桌面上使用 Ember 平台,在 jQuery Mobile 中使用 Angular。但是,我想提出另一种选择。

我不打算深入探讨 MVC 框架背后的概念。基本上,这一切都是关于将应用程序的关注点分离成更可管理的部分,每个部分都有特定的目的。我们不需要再添加另一个库/框架来做到这一点。以更有组织的方式编写代码就足够了。让我们创建一个类似我之前开始的结构:

//JavaScript Document

/*******************
 * The Application
 *******************/

/*******************
 * The Events
 *******************/

/*******************
 * The Model
 *******************/

应用程序

在应用程序部分下,让我们填写一些我们的应用程序代码,并给它一个命名空间。本质上,命名空间是将你的应用程序特定代码放入自己命名的对象中,这样函数和变量就不会与其他潜在的全局变量和函数冲突。它可以防止你污染全局空间,并帮助保护你的代码免受那些对你的工作无知的人的破坏。当然,这是 JavaScript,人们可以重写任何他们想要的东西。但是,这也使得像floodApp.getStarted这样的重写比简单地创建自己的名为getStarted的函数要更有意义。没有人会意外地重写一个命名空间函数。

/*******************
 * The application
 *******************/
var floodApp = {
  settings:{
    initialized:false,
    geolocation:{
      latitude:null,
      longitude:null,
    },
    regionalChoice:null,
    lastStation:null
  },
  getStarted:function(){
    location.replace("#initialize");
  },
  fireCustomEvent:function(){
    var $clicked = $(this);
    var eventValue = $clicked.attr("data-appEventValue");
    var event = new jQuery.Event($(this).attr("data-appEvent"));
    if(eventValue){ event.val = eventValue; }
    $(window).trigger(event);
  },
  otherMethodsBlahBlahBlah:function(){}
}

特别要注意fireCustomEvent函数。有了它,我们现在可以设置一个事件管理系统。其核心思想非常简单。我们希望能够简单地在可点击的对象上放置标签属性,并使其触发事件,就像所有的 MV* 系统一样。这完全符合要求。在链接或其他东西上设置一个点击事件处理程序是相当常见的。这更简单。只需在这里或那里添加一个属性,就可以连接上。HTML 代码也变得更加可读。很容易看出这使你的代码声明性的:

<a href="javascript://" data-appEvent="playStation" data-appEventValue="country">Country</a>

事件

现在,我们不再监听点击,而是监听事件。你可以有尽可能多的应用程序部分注册自己来监听事件,然后适当地执行。

随着我们的应用程序越来越完善,我们会开始收集大量事件;而不是让它们散布在多个嵌套的回调函数中,我们会将它们全部放在一个方便的地方。在大多数 JavaScript MV* 框架中,代码的这部分被称为路由器。连接到每个事件的只会是命名空间应用程序调用:

/*******************
 * The events
 *******************/

//Interface events
$(document).on("click", "[data-appEvent]",
  floodApp.fireCustomEvent);$(document).on("pagebeforeshow",
  "[data-role="page"]",floodApp.universalPageBeforeShow);
$(document).on("pagebeforecreate",
  "[data-role="page"]",floodApp.universalPageBeforeCreate);
$(document).on("pageshow", "#initialize",
  floodApp.getLocation);
$(document).on("pagebeforeshow", "#welcome",
  floodApp.initialize);

//Application events
$(window).on("getStarted",
  floodApp.getStarted);
$(window).on("setHomeLocation",
  floodApp.setHomeLocation);
$(window).on("setNotHomeLocation",
  floodApp.setNotHomeLocation);
$(window).on("playStation",
  floodApp.playStation);

注意将关注点分为界面事件和应用程序事件。我们将其用作对 jQuery Mobile 事件(界面事件)和我们抛出的事件(应用程序事件)之间的区别点。这可能是一个任意的区别,但对于后来维护你的代码的人来说,这可能会派上用场。

模型

模型部分包含了你的应用程序的数据。这通常是从后端 API 中拉取的数据类型。这里可能不是很重要,但给自己的东西加上命名空间从来都不会有坏处。在这里,我们将我们的数据标记为 modelData。我们从 API 中拉取的任何信息都可以直接放入这个对象中,就像我们在这里使用站点数据一样:

/*******************
 * The Model
 *******************/
var modelData = {
  station:{
    genres:[
       {
        display:"Seattle Grunge",
        genreId:12,
        genreParentId:1
       }
    ],
    metroIds[14,33,22,31],
    audioIds[55,43,26,23,11]
  }
}

将这种编程风格与客户端模板配对,你将看到一些高度可维护、结构良好的代码。然而,仍然有一些功能是缺失的。通常,这些框架还会为你的模板提供绑定。这意味着你只需要渲染模板一次。之后,只需更新你的模型对象,就足以导致 UI 自动更新。

这些绑定模板的问题在于它们以一种对桌面应用程序非常完美的方式更新 HTML。但请记住,jQuery Mobile 通过大量的 DOM 操作来实现这些功能。

在 jQuery Mobile 中,一个列表视图是这样开始的:

<ul data-role="listview" data-inset="true">
  <li><a href="#stations">Local Stations</a></li>
</ul>

在正常的 DOM 操作之后,你会得到这样的结果:

<ul data-role="listview" data-inset="true" data-theme="c" style="margin-top:0" class="ui-listview ui-listview-inset ui-corner-all ui-shadow">
<li data-corners="false" data-shadow="false" data-iconshadow="true" 
data-wrapperels="div" data-icon="arrow-r" data-iconpos="right" data-theme="c" class="ui-btn ui-btn-icon-right ui-li-has-arrow ui-li ui-corner-top ui-btn-up-c">
<div class="ui-btn-inner ui-li ui-corner-top">
<div class="ui-btn-text">
<a href="#stations" class="ui-link-inherit">Local Stations
</a>
</div>
<span class="ui-icon ui-icon-arrow-r ui-icon-shadow">&nbsp;</span>
</div>
</li>
</ul>

这仅仅是一个列表项。你真的不想在你的模板中包含所有这些垃圾;所以你需要做的就是,只需将你通常的项目添加到列表视图中,然后调用 .listview("refresh")。即使你使用的是 MV* 系统之一,当添加或删除某些内容时,你仍然必须找到或编写一个适配器来刷新列表视图。希望这些问题很快就会在平台级别得到解决。在那之前,使用真正的 MV* 系统与 jQM 会很痛苦。

介绍 Web Audio API

当我们在第六章中谈到 HTML 音频时,HTML5 音频,我们是从渐进增强和最大设备支持的角度来看待它的。我们拿原生音频控件的常规页面,并使用 JavaScript 构建一个新的界面来控制音频。然后我们看了一些组合它们的方法,并追求更好的体验。现在我们将再进一步。

Web Audio API 是一个相当新的开发,截至本文写作时,它只存在于 iOS 6 的移动空间中。Web Audio API 在最新版本的桌面 Chrome 上可用,因此你仍然可以在那里进行初始测试编码。

目前,这意味着没有 Android、没有 Windows Phone,也没有 Blackberry。至少,还没有。然而,只是时间问题,这将被构建到其他主要平台中。

项目的大部分代码以及 API 的完整说明都可以在developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/PlayingandSynthesizingSounds/PlayingandSynthesizingSounds.html找到。

让我们使用特性检测来分支我们的功能:

function init() {
if("webkitAudioContext" in window) {
    myAudioContext = new webkitAudioContext();
    // ananalyser is used for the spectrum
    myAudioAnalyser = myAudioContext.createAnalyser();
    myAudioAnalyser.smoothingTimeConstant = 0.85;
    myAudioAnalyser.connect(myAudioContext.destination);

    fetchNextSong();
  } else {
    //do the old stuff
  }
}

这个页面的原始代码旨在同时下载队列中的每首歌曲。对于高速连接,这可能还可以。但在移动设备上则不太适用。由于连接性和带宽有限,最好只是链接下载以确保更好的体验和更加尊重带宽的使用:

function fetchNextSong() {
var request = new XMLHttpRequest();
  var nextSong = songs.pop();
  if(nextSong){
    request = new XMLHttpRequest();
    // the underscore prefix is a common naming convention
    // to remind us that the variable is developer-supplied
    request._soundName = nextSong;
    request.open("GET", PATH + request._soundName + ".mp3", true);
    request.responseType = "arraybuffer";
    request.addEventListener("load", bufferSound, false);
    request.send();
  }
}

现在bufferSound函数只需在缓冲后调用fetchNextSong,如下面的代码片段所示:

function bufferSound(event) {
  var request = event.target;
  var buffer = myAudioContext.createBuffer(
  request.response, false);
  myBuffers.push(buffer);
  fetchNextSong();
}

我们需要从原始版本中更改的最后一件事是,告诉缓冲器按插入顺序拉取歌曲:

function playSound() {
  // create a new AudioBufferSourceNode
  var source = myAudioContext.createBufferSource();
  source.buffer = myBuffers.shift();
  source.loop = false;
  source = routeSound(source);
  // play right now (0 seconds from now)
  // can also pass myAudioContext.currentTime
  source.noteOn(0);
  mySpectrum = setInterval(drawSpectrum, 30);
  mySource = source;
}

对于 iOS 上的任何人来说,这个解决方案相当不错。对于那些想要深入了解的人来说,这个 API 还有更多内容。通过这个开箱即用的示例,你可以得到一个很好的基于画布的音频分析器,它使音频水平跟随音乐弹跳的外观非常专业。滑块控件用于更改音量、左右平衡和高通滤波器。如果你不知道什么是高通滤波器,不要担心,我认为那个滤波器的实用性已经过时了。不管怎样,玩起来很有趣。

Web Audio API 简介

Web Audio API 是一项非常严肃的业务。这个例子是从苹果网站上的例子改编的。它只播放一个声音。然而,Web Audio API 的设计理念是使其能够播放多个声音,以多种方式改变它们,甚至使用 JavaScript 动态生成声音。深入研究可能值得一本书。它还需要比我可能会拥有的更深入的音频处理知识。同时,如果您想在 jQuery Mobile 中查看这个概念验证,您可以在webaudioapi.html的示例源代码中找到它。要更深入地了解即将到来的内容,您可以查看dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html的文档。

提示用户安装您的应用

记得在第六章中,HTML5 音频,我们添加了苹果触摸图标,使林赛·斯特林网站在添加到主屏幕书签时看起来像一个应用程序? 我们甚至进一步使用清单文件来本地缓存资产,以实现更快的访问和离线使用。

提示用户安装您的应用

现在让我们看一下如何提示我们的用户将 Flood.FM app 下载到他们的主屏幕。很可能你以前见过它;它是那个小气泡,弹出来指导用户安装应用程序的步骤。

有许多不同的项目,但我见过的最好的一个是 Google 创始的一个分支。非常感谢和尊重 GitHub 上的 Mr. Okamototk(github.com/okamototk)对它的采取和改进。Okamototk 将气泡发展成包括几个 Android 版本、传统 iOS 版本,甚至还支持 BlackBerry。你可以在github.com/okamototk/jqm-mobile-bookmark-bubble找到他的原作品。但是,除非你能读日文或乐于翻译,我建议你只是从本章的示例中获取代码。

不用太担心过于打扰你的客户。使用这个版本,如果他们三次关闭了书签气泡,他们就不会再看到它。 这个计数存储在 HTML5 本地存储中;所以如果他们清除了存储,他们会再次看到气泡。幸运的是,大多数人根本不知道这是可以做到的,所以这种情况不会发生很频繁。通常只有像我们这样的极客会清理类似 LocalStorage 和 cookies 的东西,而当我们这样做时,我们知道我们在做什么。

在我的代码版本中,我已将所有 JavaScript 合并为一个单个文件,放置在你的 jQuery 和 jQuery Mobile 导入之间。顶部的第一行非注释行是:

page_popup_bubble="#welcome";

这是你将要改变成自己的第一页或你想要气泡弹出的地方。

在我的版本中,我已经将字体颜色和文本阴影属性硬编码到了气泡中。这是因为在 jQM 中,字体颜色和文本阴影颜色根据你使用的主题而变化。因此,在 jQuery Mobile 的默认“ A”主题(黑色背景上的白色文本),字体会显示为白色,阴影为黑色,出现在白色气泡上。现在,在我修改过的 jQM 版本中,它看起来总是对的。

我们只需要确保我们在头部设置了正确的链接,以及我们的图片放在了正确的位置:

<link rel="apple-touch-icon-precomposed" sizes="144x144" href="images/album144.png">
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="images/album114.png">
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="images/album72.png">
<link rel="apple-touch-icon-precomposed" href="images/album57.png">
<link rel="shortcut icon" href="img/images/album144.png">

提示用户安装您的应用程序

注意这里的 Flood.FM 标志。标志是从我们标记有 rel="apple-touch-icon-precomposed" 的链接标签中提取并注入到气泡中的。所以,实际上,你需要改变的 jqm_bookmark_bubble.js 中的唯一东西是 page_popup_bubble

新的设备级硬件访问

每年我们的移动浏览器都会有新的硬件级访问方式。下面是一些你现在可以开始做的事情以及未来的展望。并不是所有这些都适用于每个项目,但如果你有创意,你可能会找到创新的方式来使用它们。

加速计

加速计是你手机里的小装置,用来测量手机在空间中的方向。想要深入了解,请阅读 en.wikipedia.org/wiki/Accelerometer

这超出了我们之前简单的定位。这是对加速计的真正访问,而且是详细的。想象一下用户能够摇动他们的设备或者倾斜它作为与你的应用交互的一种方法。也许 Flood.FM 正在播放一些他们不喜欢的东西,我们可以给他们一个有趣的方式来对抗这首歌。比如,“摇一首歌以永远不再听到它”。这里是某人制作的一个简单的弹珠滚动游戏,作为概念验证。参见 menscher.com/teaching/woaa/examples/html5_accelerometer.html

相机

苹果的 iOS 6 和安卓的 JellyBean 都可以访问它们的文件系统中的照片以及相机。当然,这些是这两个平台的最新版本。如果你打算支持许多仍然在货架上销售的过时的 Android 设备(2.3 2.4),好像它们是全新的一样,那么你需要选择本地编译,比如 PhoneGap 或 Apache Cordova 来获取这个功能。

<input type="file" accept="image/*">
<input type="file" accept="video/*">

以下截图显示 iOS 在左边,Android 在右边:

相机

即将推出的 API

Mozilla 正在大力推动移动网络 API 的发展。以下是即将到来并且可能在不到两年内就可以使用的内容:

  • 电池电量

  • 充电状态

  • 环境光传感器

  • 接近传感器

  • 振动

  • 联系人

  • 网络信息

  • 移动连接(运营商、信号强度等)

  • Web 短信

  • Web 蓝牙

  • Web FM

  • 存档 API(打开和读取来自压缩文件的内容)

如果你想阅读更多,请查看 wiki.mozilla.org/WebAPI

选择开发应用还是不开发应用,这是个问题

是否应该将你的项目编译成原生应用?以下是一些需要考虑的事项。

下雨了(认真对待这个问题)

当你把你的第一个项目编译成一个应用时,你会感到一种特殊的激动。你做到了!你做了一个真正的应用程序!在这一点上,我们需要记住《侏罗纪公园》电影中伊恩·马尔科博士的话(去重新看一遍吧。我等你):

下雨了(认真对待这个问题)

“你站在巨人的肩膀上,尽可能地做了一些事情,甚至在你知道你拥有什么之前,你就已经对它进行了专利申请,打包了它,把它塞进了一个塑料午餐盒里,现在 [敲打桌子] 你在卖它,你想卖它。好吧……你的科学家们是如此专注于他们是否能够做到,以至于他们没有停下来思考他们是否应该。”

这些话对我们来说很接近预言性质。最后,他们自己的创造吞食了大部分客人。

根据 2012 年 8 月的这份报告www.webpronews.com/over-two-thirds-of-the-app-store-has-never-been-downloaded-2012-08(以及我以前看过的几篇类似的报告),超过三分之二的应用商店中的所有应用从未被下载过。甚至没有一次!所以,现实情况是,大多数项目在应用商店中被抛弃。

即使你的应用被发现,任何人会长时间使用它的可能性令人惊讶地小。根据《福布斯》(tech.fortune.cnn.com/2009/02/20/the-half-life-of-an-iphone-app/)中的一篇文章,大多数应用在几分钟内被放弃,再也不会被打开。付费应用的持续时间大约是之前的两倍,然后要么被遗忘,要么被删除。游戏有一些持久力,但坦率地说,jQuery Mobile 并不是一个引人入胜的游戏平台,对吧?

安卓世界的情况糟糕透顶。仍然可以购买到运行古老版本操作系统的设备,而运营商和硬件合作伙伴在提供更新方面甚至没有及时性可言。如果你想了解采用本地策略可能带来的沉重压力,可以看看这里:

developer.android.com/about/dashboards/index.html

破坏幻想(认真对待)

你可以看到安卓生态系统有多么分裂,以及你可能需要支持多少旧版本。在安卓及其商业伙伴摆脱束缚之前,安卓将继续成为本地移动世界的 IE 6。你想支持那个。

另一方面,如果你严格发布到网络,那么每当用户访问你的网站时,他们都将使用最新版本和最新 API,你永远不必担心有人使用过时的版本。你需要应用安全补丁吗?你可以在几秒钟内完成。如果你在苹果应用商店,这个补丁可能需要数天甚至数周。

编译应用的三个好理由

是的,我知道我刚刚告诉过你成功的机会渺茫,以及你将面临支持应用的火海和硫磺。然而,以下是制作真正应用的几个好理由。实际上,在我看来,它们是唯一可接受的理由。

项目本身就是产品

这是你需要将项目打包成应用的第一个也是唯一确定的迹象。我不是在说通过你的项目销售东西。我说的是项目本身。它应该制作成一个应用。愿原力与你同在。

访问本地独有的硬件功能

GPS 和摄像头在它们的最新版本中,都可靠地为两个主要平台提供支持。iOS 甚至支持加速计。不过,如果你希望得到更多,你将需要编译成应用程序以获得这些 API 的访问权限。

推送通知

你喜欢它们吗?我不知道你,但我得到的推送通知太多了;任何一个过于张扬的应用要么被删除,要么完全关闭通知。我在这方面并不孤单。然而,如果你一定要有推送通知,而且不能等待基于网页的实现,你就必须编译一个应用程序。

支持当前客户

好吧,这有一定的牵强之处,但如果你在美国企业工作,你就会听到这个。这意味着你是一家成熟的企业,你希望为客户提供移动支持。你或者你的上级已经读过一些白皮书和/或案例研究,表明有将近50%的人首先在应用商店搜索。

即使这是真的(我对此仍然没有把握),你要对一个商人说这些。他们懂得金钱、开销和增加的维护成本。一旦向他们解释了在各种平台和它们的操作系统版本中进行建设和测试的成本、复杂性和潜在的持续头疼之后,对于公司向现有客户推广支持移动端,让他们只需要在其移动设备上访问你的网站,这成为一个非常吸引人的替代方案。营销人员总是在寻找可以向客户吹嘘的理由。营销部门可能仍然倾向于在客户设备上显示公司的图标,以增强品牌忠诚度,但这只是需要教育他们,这可以在没有应用程序的情况下完成。

即使你可能无法说服所有正确的人认为应用程序对于客户支持是错误的选择。如果你自己做不到,就用一点 Jakob Nielson 的见解敲打他们的头颅。如果他们不听你的,也许他们会听他的。我敢说任何人反驳尼尔森·诺曼集团不知道他们在说什么的说法。参见 www.nngroup.com/articles/mobile-sites-vs-apps-strategy-shift/

"总结:当前移动应用程序的可用性比移动网站更好,但即将发生的变化最终会使移动网站成为更加优越的策略。"

因此,一个价值64,000美元的问题就是:我们是为现在还是为未来而生产的?如果我们是为现在而做,那么应该标志着本地策略退休的标准是什么?或者我们打算永远固守它吗?不要在没有退出战略的情况下参与那场战争。

PhoneGap 与 Apache Cordova

好吧,在所有这些之后,如果你仍然想制作一个本地应用程序,我向你致敬。我钦佩你的精神,并祝你好运。

注意

如果你搜索 "jquery mobile phonegap performance",你会找到 很多 负面文章。问题似乎是无穷无尽的。性能低下,屏幕在转换之间闪烁,等等。并不是说 Sencha Touch 或任何其他移动 Web 框架似乎做得更好。只是要意识到它可能不像在 Web 上运行时表现那样好。

PhoneGap 最初是一个将常规的 HTML、JS 和 CSS 打包成一个可在任何应用商店分发的应用程序的项目。最终,它成为了 Apache 软件基金会的一部分。在其核心,PhoneGap Apache Cordova。事实上,如果你去 Cordova 的文档站点,它实际上仍然托管在 docs.phonegap.com/

除了简单地编译你的应用程序之外,你还可以访问以下设备级别的 API:

  • 加速度计:利用设备的运动传感器。

  • 相机:使用设备的相机拍摄照片。

  • 捕获:使用设备的媒体捕获应用程序捕获媒体文件。

  • 指南针:获取设备指向的方向。

  • 连接:快速检查网络状态和蜂窝网络信息。

  • 联系人:使用设备的联系人数据库。

  • 设备:收集设备特定信息。

  • 事件:通过 JavaScript 连接到本地事件。

  • 文件:通过 JavaScript 连接到本地文件系统。

  • 地理位置:使你的应用程序具有位置感知能力。

  • 全球化:启用特定于区域设置的对象表示。

  • InAppBrowser:在另一个应用程序浏览器实例中启动 URL。

  • 媒体:记录并回放音频文件。

  • 通知:设备的视觉、听觉和触觉通知。

  • 启动画面:显示和隐藏应用程序的启动画面。

  • 存储:连接到设备的原生存储选项。

到目前为止,一切都很顺利。我们有更多的东西可以做,而且我们可以全部在 JavaScript 中完成。

接下来,我们需要真正构建我们的应用程序。你需要在你的计算机上下载 PhoneGap 或 Cordova。不要忘记下载你打算支持的每个平台的 SDK。不,等等,划掉!

PhoneGap 与 Apache Cordova 对比

现在有了 PhoneGap Build。这是一个面向 PhoneGap 的基于云的构建服务。你根本不需要安装任何 SDK。PhoneGap Build 只是把所有工作都做了。如果你想要编译 iOS 应用程序,你仍然需要提供开发者证书,但除了这一点小问题,一切都很顺利。

要开始使用,你只需用你的 Adobe ID 或 GitHub ID 登录。然后,要么粘贴 GitHub 存储库的 URL,要么上传一个小于 9.5 MB 的 zip 文件:

PhoneGap 与 Apache Cordova 对比

接下来,你需要填写关于应用程序本身的一些信息:

PhoneGap 与 Apache Cordova 对比

点击准备构建按钮。现在只需坐下来,看着漂亮的进度指示器做它们的工作。

PhoneGap 与 Apache Cordova 对比

看,他们甚至给了你一个可爱的小二维码,用于下载这个应用。在 iOS 上显示红色标志的唯一原因是,这一点上,我还没有提供给他们我的开发者证书。

总结

我不知道你怎么想,但我真的筋疲力尽了。我真的觉得在这个时候关于 jQuery Mobile 或其支持技术已经没有更多可说的了。你已经有了如何为许多行业构建东西的例子,以及通过 Web 或 PhoneGap Build 部署它的方法。在这一点上,你应该引用建筑工人鲍勃的话。“我们能建造它吗?是的,我们能!”

我希望这本书对你有所帮助和/或启发,让你去做一些了不起的事情。我希望你改变世界,并且通过这样做获得巨额财富。在你前进的过程中,我很乐意听到你的成功故事。想要告诉我你的近况,或者指出任何勘误,甚至是有一些问题要问,欢迎直接给我发邮件到<shane@roughlybrilliant.com>。现在,去做一些精彩的事吧!

posted @ 2024-05-19 20:13  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报