精通-jQuery-全-

精通 jQuery(全)

原文:zh.annas-archive.org/md5/0EE28037989D2E7006D982EBB8295FFE

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

假设你是一个中级开发者,对编写代码相当熟悉,但觉得开发 jQuery 不应该只是在文本编辑器中敲击键盘那么简单。

你说得对;任何人都能写代码。为了成为更全面的开发者,我们必须思考更广泛。那些需要理解和调试的长串代码的时代已经过去了,取而代之的是帮助我们更明智地使用 jQuery 并更有效地利用我们忙碌生活中时间的决策。

作为作者,我坚持认为简单的解决方案通常比复杂的解决方案更有效;在本书中,我们将涉及各种主题,帮助提升你的技能,让你考虑所有选项,并理解编写 jQuery 代码的更多要点。

这将是一次很棒的旅程,比侦探小说的情节更扑朔迷离;问题是,“你准备好了吗?” 如果答案是肯定的,让我们开始吧…

本书内容概述

第一章, 安装 jQuery,开启了我们掌握 jQuery 世界的旅程,你将会了解到下载和安装 jQuery 不仅仅是使用 CDN 或本地链接那么简单。我们将看看如何使用包管理器安装 jQuery,如何定制我们下载的元素,以及如何添加源映射等等,以帮助调整库的副本。

第二章, 自定义 jQuery,进一步深入;你可能会发现 jQuery 的元素并不完全符合你的要求。在本章中,我们将看看如何创建和分发补丁,以便临时应用于扩展或更改 jQuery 核心功能。

第三章, 组织你的代码,探讨了 jQuery 设计模式的使用,这是一个在维护良好组织的代码并使开发和调试更容易的有用概念。我们将看看一些模式的示例以及它们与 jQuery 的结合方式。

第四章, 处理表单,介绍了表单功能的权威 - 对表单响应进行验证。我们将探讨如何更有效地进行表单验证,然后在一个使用 AJAX 的联系表单中将其发挥到极致,并开发一个文件上传表单。

第五章, 整合 AJAX,探讨了如何通过使用回调来提高静态站点上数据加载的速度,以及如何使用 jQuery 的 Deferreds 和 Promises 功能来更好地管理这些请求。我们将探讨 AJAX 的最佳实践,并探索如何通过 jQuery 的 Deferreds 和 Promises 功能来更好地管理这些请求。

第六章, jQuery 中的动画,带我们进入发现如何更加聪明地管理 jQuery 中的动画,并探索如何最好地管理 jQuery 队列以防止动画积压的旅程。我们还将学习如何实现自定义动画,以及为什么 jQuery 并不总是移动页面元素的正确工具。

第七章, 高级事件处理,探讨了许多开发者可能仅使用 .on() 或 .off() 来处理事件,但您将看到,如果您真的想充分利用 jQuery,这些方法使用起来更为复杂。在我们探索如何更好地管理这些事件处理程序在我们的代码中何时被调用之前,我们将创建一些自定义事件。

第八章, 使用 jQuery 效果,继续我们的旅程,通过快速回顾在 jQuery 中使用效果,我们将探讨如何使用回调创建自定义效果,并学习如何更好地管理形成 jQuery 中使用效果的基础的队列。

第九章, 使用 Web 性能 API,开启了本书的第二部分,我们将在其中探讨在使用 jQuery 时可用的一些更有趣的选项。在本章中,我们将了解如何使用 Page Visibility API 与 jQuery,并了解如何使用它来提供更平滑的外观,减少资源,并仍然在我们的页面上保持复杂的动画。感兴趣吗?当您访问这一章时,您会的!

第十章, 操作图像,演示了如何通过使用 jQuery 和一些相对简单的数学知识,我们可以对图像应用各种效果。我们可以执行诸如模糊图像之类的简单操作,也可以创建自定义效果。然后,我们将使用其中一些技术来创建一个简单的签名页面,该页面可以导出图像,并对从您自己的网络摄像头提取的图像应用各种效果。

第十一章, 编写高级插件,涵盖了使用 jQuery 的一个关键主题:创建和分发插件。随着越来越多的功能被移到使用插件,我们将介绍一些创建自己插件背后的技巧和窍门;您会发现,这不仅仅是编写代码那么简单!

第十二章,使用 jQuery 与 Node-WebKit 项目,探索了一个有趣的库,它将 Node、JavaScript/jQuery、CSS 和纯 HTML 的最佳元素结合起来,形成了一种模糊了桌面和在线世界界限的东西。我们将通过一些现有的在线代码,并将其转换为桌面应用程序的使用方式,然后将其打包并在网上提供下载。

第十三章,增强 jQuery 的性能,带您了解一些优化和增强代码性能的考虑因素、技巧和窍门。您将看到如何轻松地从 DOM 检查器(例如 Firebug)获取基础知识,直到使用 Grunt 自动化您的测试,并最终制定一种监视代码性能的策略。

第十四章,测试 jQuery,是我们在 jQuery 掌握世界之旅中的结束篇章,我们将探讨使用 QUnit 测试我们的代码以及如何利用 Grunt 自动化开发中一个否则常规但重要的任务。

您需要本书的什么

您需要工作通过本书中大多数示例的只是一个简单的文本或代码编辑器,一个 jQuery 库的副本,互联网访问和一个浏览器。我建议您安装 Sublime Text——无论是版本 2 还是 3;它与 Node 和 Grunt 很好地配合,我们将在本书的各个阶段使用它们。

一些示例使用了额外的软件,比如 Node 或 Grunt——在适当的章节中包含了相关细节,以及从其源中下载应用程序的链接。

本书适合谁

本书适合希望不仅仅是编写代码的前端开发人员,而是想要探索可用于扩展他们在 jQuery 开发中技能的提示和技巧的人。要充分利用本书,您应该具备良好的 HTML、CSS 和 JavaScript 知识,并且最好在 jQuery 方面处于中级水平。

惯例

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

文本中的代码词语如下所示:“我们将从本书的代码下载中提取相关文件;对于这个演示,我们需要clicktoggle.cssjquery.min.jsclicktoggle.html。”

代码块设置如下:

$(this).on("click", function() {
  if (clicked) {
    clicked = false;
      return b.apply(this, arguments);
    }
    clicked = true;
    return a.apply(this, arguments);
  });
});

当我们希望引起您对代码块特定部分的注意时,相关行或项目将以粗体显示:

$('#section').hide(2000, 'swing', function() {
 $(this).html("Animation Completed");
});

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

npm install jquery

新术语和重要单词都用粗体显示。你在屏幕上看到的字词,例如菜单或对话框中的字词,都会出现在文字中,就像这样:“当我们查看页面并选择图像标签后,经过短暂的延迟,我们应该看到六张新的图片。”

注意

警告或重要备注将出现在这样的一个框内。

贴士

贴士和技巧会以这种方式出现。

第一章:安装 jQuery

本地还是 CDN,我在想……?使用哪个版本……?要支持旧版 IE 吗……?

安装 jQuery 是一项无功的任务,任何开发者都不得不重复进行无数次——可以想象那个人问这个章节开头的一些问题。可以想象为什么大多数人选择使用 内容传送网络CDN)链接,但安装 jQuery 不只是走捷径那么简单!

还有更多选项可供选择,我们可以非常具体地选择我们需要使用的内容——在本章中,我们将探讨一些可用的选项,以帮助进一步发展你的技能。我们将涵盖许多主题,其中包括:

  • 下载并安装 jQuery

  • 自定义 jQuery 下载

  • 从 Git 构建

  • 使用其他来源安装 jQuery

  • 添加源映射支持

  • 使用 Modernizr 作为备用方案

有兴趣吗?让我们开始吧。

下载并安装 jQuery

就像所有需要使用 jQuery 的项目一样,我们必须从某个地方开始——毫无疑问,你已经下载并安装了 jQuery 成千上万次了;让我们快速回顾一下,以使自己跟上进度。

如果我们浏览到www.jquery.com/download,我们可以通过两种方法下载 jQuery:下载压缩的生产版本或未压缩的开发版本。如果我们不需要支持旧版 IE(IE6、7 和 8),那么我们可以选择 2.x 分支。但是,如果你仍然有一些死忠粉丝无法(或不想)升级,那么必须使用 1.x 分支。

要包含 jQuery,我们只需要将这个链接添加到我们的页面中:

<script src="img/jquery-X.X.X.js"></script>

提示

下载示例代码

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

在这里,X.X.X表示页面中正在使用的 jQuery 或 Migrate 插件的版本号。

传统智慧认为 jQuery 插件(包括 Migrate 插件在内)应该添加到 <head> 标签中,尽管也有有效的理由将其添加到闭合的 <body> 标签之前的最后一个语句;将其放在这里可能有助于加快网站的加载速度。

这个论点并不是铁板一块;在某些情况下,将其放在 <head> 标签中可能是必要的,这个选择应该根据开发者的需求来决定。我个人偏好将其放在 <head> 标签中,因为它能够将脚本(以及 CSS)代码与页面主体中的主要标记分离开来,特别是在较轻的网站上。

我甚至看到一些开发人员争论说,如果在顶部而不是底部添加 jQuery,几乎没有察觉到的差异;一些系统,比如 WordPress,在<head>部分也包含 jQuery,所以两种方式都可以。关键在于,如果你感觉到速度变慢,那么将你的脚本移到<body>标签之前是一个更好的做法。

在开发中使用 jQuery

在这个阶段需要注意的一个有用的点是,最佳实践建议不要在开发阶段使用 CDN 链接;而应该下载未压缩的文件并在本地引用。一旦网站完成并准备上传,那么就可以使用 CDN 链接。

添加 jQuery Migrate 插件

如果你使用的是 jQuery 1.9 之前的任何版本,那么值得将 jQuery Migrate 插件添加到你的页面中。从这个版本开始,jQuery 核心团队对 jQuery 进行了一些重大更改;Migrate 插件将临时恢复功能,直到旧代码可以更新或替换为止。

该插件向 jQuery 对象添加了三个属性和一个方法,我们可以使用它们来控制其行为:

属性或方法 评论
jQuery.migrateWarnings 这是一个包含由页面上的代码生成的字符串警告消息的数组,按照生成的顺序排列。即使条件发生多次,消息也只会出现在数组中一次,除非调用了jQuery.migrateReset()
jQuery.migrateMute 将此属性设置为true以防止在调试版本中生成控制台警告。如果设置了此属性,jQuery.migrateWarnings数组仍然会被维护,这允许在没有控制台输出的情况下进行程序化检查。
jQuery.migrateTrace 如果你想要警告但不想在控制台上显示跟踪信息,请将此属性设置为false
jQuery.migrateReset() 此方法清除jQuery.migrateWarnings数组并“忘记”已经看到的消息列表。

添加插件同样很简单 —— 你只需要添加类似这样的链接,其中X表示所使用的插件版本号:

<script src="img/jquery-migrate-X.X.X.js"></script>

如果你想了解更多关于插件并获取源代码,那么可以从github.com/jquery/jquery-migrate进行下载。

使用 CDN

我们同样可以使用 CDN 链接提供我们的 jQuery 库 —— jQuery 团队的主要链接由MaxCDN提供,当前版本可在code.jquery.com找到。当然,如果喜欢的话,我们也可以使用一些其他来源的 CDN 链接 —— 这些的提醒如下:

但是不要忘记,如果需要的话,我们始终可以将 CDN 提供的文件保存在本地,并引用它。jQuery CDN 总是会有最新版本,尽管可能需要几天时间才能通过其他链接更新。

使用其他来源安装 jQuery

好的。好的,让我们继续编写一些代码吧!“接下来做什么?”我听到你在问。

啊哈!如果你认为从主要站点下载并安装 jQuery 是唯一的方法,那么你就错了!毕竟,这本书是关于精通 jQuery 的,所以你不会认为我只会谈论你已经熟悉的内容,对吧?

是的,我们有更多可供选择的选项来安装 jQuery,而不仅仅是使用 CDN 或主要下载页面。让我们开始看看如何使用 Node。

注意

每个演示都是基于 Windows 的,因为这是作者首选的平台;在可能的情况下,为其他平台提供了替代方案。

使用 NodeJS 安装 jQuery

到目前为止,我们已经看到了如何下载和引用 jQuery,即使用主要的 jQuery 站点下载或通过 CDN 使用。这种方法的缺点是需要手动更新我们的 jQuery 版本!相反,我们可以使用包管理器来帮助管理我们的资产。Node.js 就是这样一个系统。让我们来看一下安装 jQuery 需要执行的步骤:

  1. 我们首先需要安装 Node.js —— 前往 www.nodejs.org 以下载适用于你选择平台的软件包;在通过向导时接受所有默认设置(对于 Mac 和 PC)。

  2. 接下来,打开一个 Node 命令提示符,然后切换到你的项目文件夹。

  3. 在提示符中,输入以下命令:

    npm install jquery
    
    
  4. Node 将会获取并安装 jQuery —— 当安装完成时,它会显示一条确认消息:使用 NodeJS 安装 jQuery

  5. 然后,你可以通过这个链接引用 jQuery:

    <name of drive>:\website\node_modules\jquery\dist\jquery.min.js.
    

Node 现在已经安装并且准备就绪 —— 虽然我们将其安装在本地文件夹中,但实际上,我们很可能会将其安装在本地 Web 服务器的子文件夹中。例如,如果我们正在运行 WampServer,我们可以安装它,然后将它复制到 /wamp/www/js 文件夹中,并使用 http://localhost/js/jquery.min.js 引用它。

注意

如果你想查看 jQuery Node 包管理器 (NPM) 包的源代码,那么请查看 www.npmjs.org/package/jquery

使用 Node 安装 jQuery 使我们的工作更简单,但代价是高昂的。Node.js(及其包管理器 NPM)主要用于安装和管理 JavaScript 组件,并期望包遵循CommonJS标准。这样做的缺点是,没有范围来管理通常在网站中使用的任何其他资产,例如字体、图像、CSS 文件甚至 HTML 页面。

“为什么会成为一个问题呢?”我听到你问。简单,当我们可以自动管理所有这些资产并仍然使用 Node 时,为什么要让生活变得困难呢?

使用 Bower 安装 jQuery

图书馆的一个相对较新的增加是支持使用 Bower 进行安装——基于 Node,它是一个包管理器,负责从互联网上获取和安装包。它设计得更加灵活,可以管理多种类型的资产(如图像、字体和 CSS 文件),并且不会干扰这些组件在页面中的使用方式(不像 Node)。

为了演示目的,我假设您已经从前一节安装了它;如果没有,请在继续以下步骤之前重新查看它:

  1. 打开 Node 命令提示符,切换到您想要安装 jQuery 的驱动器,并输入此命令:

    bower install jquery
    
    

这将下载并安装脚本,在完成时显示已安装版本的确认,如下面的屏幕截图所示:

使用 Bower 安装 jQuery

该库安装在您 PC 上的bower_components文件夹中。它看起来类似于这个例子,我已经导航到了jquery子文件夹下面:

使用 Bower 安装 jQuery

默认情况下,Bower 将 jQuery 安装在其bower_components文件夹中。在bower_components/jquery/dist/中,我们会找到一个未压缩的版本、压缩的发布版本和源映射文件。然后,我们可以使用以下行引用 jQuery 在我们的脚本中:

<script src="img/jquery.js"></script>

不过,我们可以进一步进行。如果我们不想安装 Bower 默认情况下附带的额外文件,我们可以在命令提示符中简单地输入以下内容,仅安装 jQuery 的压缩版本 2.1:

bower install http://code.jquery.com/jquery-2.1.0.min.js

现在,我们可以在这一点上真正聪明了;因为 Bower 使用 Node 的 JSON 文件来控制应安装的内容,我们可以利用这一点进行选择,并设置 Bower 同时安装其他组件。让我们来看看这将如何工作——在以下示例中,我们将使用 Bower 来安装 jQuery 2.1 和 1.10(后者为 IE6-8 提供支持):

  1. 在 Node 命令提示符中,输入以下命令:

    bower init
    
    

    这将提示您回答一系列问题,此时您可以填写信息或按Enter接受默认值。

  2. 查看项目文件夹;您应该在其中找到一个bower.json文件。在您喜欢的文本编辑器中打开它,然后按照此处显示的代码进行更改:

    {
      "ignore": [ "**/.*", "node_modules", "bower_components", "test", "tests" ] ,
     "dependencies": {
     "jquery-legacy": "jquery#1.11.1",
     "jquery-modern": "jquery#2.10"
     }
    }
    

此时,您有一个准备好供使用的 bower.json 文件。Bower 建立在 Git 之上,所以为了使用您的文件安装 jQuery,通常需要将其发布到 Bower 存储库中。

相反,您可以安装一个额外的 Bower 包,这样您就可以安装您的自定义包而无需将其发布到 Bower 存储库中:

  1. 在 Node 命令提示符窗口中,在提示符处输入以下内容

    npm install -g bower-installer
    
    
  2. 安装完成后,切换到你的项目文件夹,然后输入以下命令行:

    bower-installer
    
    
  3. bower-installer 命令现在将下载并安装 jQuery 的两个版本,如下所示:使用 Bower 安装 jQuery

此时,您已经使用 Bower 安装了 jQuery。在未来的某个时候,您可以自由升级或移除 jQuery,使用正常的 Bower 过程。

注意

如果您想了解更多关于如何使用 Bower 的信息,可以在网上找到大量参考资料;www.openshift.com/blogs/day-1-bower-manage-your-client-side-dependencies 是一个帮助您熟悉使用 Bower 的好例子。此外,还有一篇有用的文章讨论了 Bower 和 Node,可以在 tech.pro/tutorial/1190/package-managers-an-introductory-guide-for-the-uninitiated-front-end-developer 找到。

不过,安装 jQuery 的方式不止有 Bower 一种——例如,我们可以使用它来安装多个版本的 jQuery,但仍然受限于安装整个 jQuery 库。

我们可以通过仅引用库中所需的元素来改进这一点。感谢 jQuery 核心团队进行的大量工作,我们可以使用异步模块定义AMD)方法来仅引用我们网站或在线应用程序中所需的模块。

使用 AMD 方法加载 jQuery。

在大多数情况下,当使用 jQuery 时,开发人员可能只会在其代码中包含对主要库的引用。这本身没有错,但它加载了很多我们不需要的额外代码。

一种更高效的方法,尽管需要一些时间来适应,是使用 AMD 方法。简而言之,jQuery 团队使库更加模块化;这使您可以使用诸如 require.js 的加载器在需要时加载单个模块。

对于每种方法并不都适用,特别是如果您是库的不同部分的重度用户。但是,对于您仅需要有限数量的模块的情况,则是一个完美的选择。让我们通过一个简单的示例来看看实际情况。

注意

在我们开始之前,我们需要一个额外的项目-代码使用 Fira Sans 常规自定义字体,该字体可以从 Font Squirrel 获取www.fontsquirrel.com/fonts/fira-sans

让我们从以下步骤开始:

  1. Fira Sans 字体默认不带网络格式,因此我们需要将字体转换为网络字体格式。请上传FiraSans-Regular.otf文件到 Font Squirrel 的网络字体生成器www.fontsquirrel.com/tools/webfont-generator。当提示时,将转换后的文件保存到项目文件夹中的名为fonts的子文件夹中。

  2. 我们需要将 jQuery 和 RequireJS 安装到我们的项目文件夹中,所以打开一个 Node.js 命令提示符并切换到项目文件夹。

  3. 接下来,逐一输入以下命令,并在每个命令后按Enter

    bower install jquery
    bower install requirejs
    
    
  4. 我们需要从附带本书的代码下载链接中提取amd.htmlamd.css文件的副本-它包含一些简单的标记以及一个到require.js的链接;amd.css文件包含我们将在演示中使用的一些基本样式。

  5. 现在,我们需要立即在require.js的链接下添加这个代码块-这处理了对 jQuery 和 RequireJS 的调用,我们同时调用了 jQuery 和 Sizzle,jQuery 的选择器引擎:

      <script>
        require.config({
          paths: {
            "jquery": "bower_components/jquery/src",
            "sizzle": "bower_components/jquery/src/sizzle/dist/sizzle"
          }
        });
        require(["js/app"]);
      </script>
    
  6. 现在 jQuery 已被定义,我们需要调用相关模块。在一个新文件中,继续添加以下代码,并将其保存为app.js,保存到我们项目文件夹内的一个名为js的子文件夹中:

    define(["jquery/core/init", "jquery/attributes/classes"], function($) {
      $("div").addClass("decoration");
    });
    

    注意

    我们使用app.js作为文件名,以与代码中的require(["js/app"]);引用相匹配。

  7. 如果一切顺利,在浏览器中预览我们工作的结果时,我们将看到此消息:使用 AMD 方法加载 jQuery

尽管我们只在这里使用了一个简单的例子,但已足以演示仅调用我们代码中需要使用的模块比调用整个 jQuery 库要容易。的确,我们仍然必须提供一个指向库的链接,但这只是告诉我们的代码在哪里找到它;我们的模块代码重达 29 KB(在 gzip 后为 10 KB),而完整库的未压缩版本为 242 KB!

注意

我们的代码已在附带本书的代码下载链接中提供了完整版本的代码,找到并运行amd-finished.html文件以查看结果。

现在,可能会有一些情况下,仅使用这种方法引用模块并不是正确的方法;如果您需要经常引用许多不同的模块,这可能适用。

更好的选择是构建 jQuery 库的自定义版本,该版本仅包含我们需要使用的模块,其余模块在构建过程中被删除。这需要更多的工作,但是值得努力-让我们看看涉及的过程。

定制从 Git 下载 jQuery

如果我们有兴趣,我们可以真正地大展拳脚,使用 JavaScript 任务运行器 Grunt 构建一个自定义版本的 jQuery。这个过程相对直接,但涉及一些步骤;如果你之前对 Git 有一些了解,那肯定会有所帮助!

注意

该演示假定您已经安装了 Node.js——如果尚未安装,请在继续练习之前先执行此操作。

好的,让我们开始执行以下步骤:

  1. 如果系统中尚未安装 Grunt,首先需要安装 Grunt——打开 Node.js 命令提示符并输入以下命令:

    npm install -g grunt-cli
    
    
  2. 接下来,安装 Git——为此,请浏览msysgit.github.io/以下载该软件包。

  3. 双击安装文件启动向导,接受所有默认设置就足够满足我们的需求。

    注意

    如果你想了解更多关于如何安装 Git 的信息,请前往github.com/msysgit/msysgit/wiki/InstallMSysGit了解更多详情。

  4. 安装了 Git 后,从命令提示符中切换到 jquery 文件夹,并输入以下命令下载并安装构建 jQuery 所需的依赖项:

    npm install
    
    
  5. 构建过程的最后阶段是将库构建到我们所熟悉和喜爱的文件中;从同一个命令提示符中,输入以下命令:

    grunt
    
    
  6. 浏览至 jquery 文件夹——其中将有一个名为 dist 的文件夹,其中包含我们的自定义 jQuery 构建版本,准备就绪,如下面的截图所示:从 Git 定制 jQuery 的下载

移除冗余模块

如果库中有我们不需要的模块,我们可以运行自定义构建。我们可以设置 Grunt 任务,在构建库时移除这些模块,保留我们项目中需要的模块。

注意

要查看我们可以排除的所有模块的完整列表,请参阅github.com/jquery/jquery#modules

例如,要从我们的构建中移除 AJAX 支持,我们可以在第 5 步运行以下命令,如前所示:

grunt custom:-ajax

这将导致文件在原始未经处理的版本上节省 30 KB,如下图所示:

移除冗余模块

JavaScript 和映射文件现在可以像往常一样并入我们的项目中。

注意

要详细了解构建过程,请阅读 Dan Wellman 撰写的这篇文章(www.packtpub.com/books/content/building-custom-version-jquery)。

使用 GUI 作为替代方案

有一个在线 GUI 可用,执行几乎相同的任务,而无需安装 Git 或 Grunt。它可在projects.jga.me/jquery-builder/找到,尽管值得注意的是,它已经有一段时间没有更新了!

好的,所以我们已经安装了 jQuery;让我们再看看另一个有用的函数,它将在调试代码中出现错误时帮助我们。自 jQuery 1.9 版本以来,已经提供了对源映射的支持。让我们看看它们是如何工作的,并看一个简单的示例。

添加源映射支持

请想象一种情景,假如你创建了一个非常棒的网站,在运行良好,直到你开始收到关于网站上某些基于 jQuery 的功能出现问题的投诉。听起来耳熟吗?

在生产网站上使用未压缩版本的 jQuery 是不可取的选择;相反,我们可以使用源映射。简单来说,这些映射了 jQuery 的压缩版本与原始源代码中的相关行。

从历史上看,当实现源映射时,开发人员曾经遇到过很多麻烦,以至于 jQuery 团队不得不禁用自动使用映射!

提示

为了达到最佳效果,建议您使用本地 Web 服务器,例如 WAMP(PC)或 MAMP(Mac),查看此演示,并使用 Chrome 作为您的浏览器。

实现源映射并不困难;让我们来看看如何实现它们:

  1. 从本书附带的代码下载链接中提取一个 sourcemap 文件夹的副本,并将其保存到本地项目区域。

  2. 按下 Ctrl + Shift + I 在 Chrome 中打开 开发者工具

  3. 点击 Sources,然后双击 sourcemap.html 文件—在代码窗口中,最后点击 17,如下面的截图所示:添加源映射支持

  4. 现在,在 Chrome 中运行演示—我们将看到它暂停;回到开发者工具栏,其中第 17 行被突出显示。屏幕右侧显示了对 jQuery 库的相关调用:添加源映射支持

  5. 如果我们在右侧双击 n.event.dispatch 条目,Chrome 将刷新工具栏并显示来自 jQuery 库的原始源代码行(突出显示),如下所示:添加源映射支持

投入时间去了解源映射是非常值得的—所有最新版本的浏览器都支持它,包括 IE11。尽管我们在这里只使用了一个简单的示例,但原则上完全相同,无论网站使用了多少代码。

注意

对于一个更深入的涵盖所有浏览器的教程,值得访问 blogs.msdn.com/b/davrous/archive/2014/08/22/enhance-your-javascript-debugging-life-thanks-to-the-source-map-support-available-in-ie11-chrome-opera-amp-firefox.aspx—值得一读!

添加源映射支持

在前一节中,我们刚刚预览了源映射,源映射支持已经添加到了库中。值得注意的是,当前版本的 jQuery 默认情况下不包含源映射。如果你需要下载更新版本或者首次添加支持,请按照以下步骤操作:

  1. 源映射可以从主站点使用 http://code.jquery.com/jquery-X.X.X.min.map 下载,其中 X 表示正在使用的 jQuery 版本号。

  2. 打开压缩版本的库的副本,然后在文件末尾添加这一行:

    //# sourceMappingURL=jquery.min.map
    
  3. 保存并将其存储在项目的 JavaScript 文件夹中。确保在同一个文件夹中有压缩和未压缩版本的库的副本。

让我们继续并看看加载 jQuery 的另一个关键部分:如果由于某种未知原因,jQuery 完全不可用,那么我们可以为我们的站点添加一个后备位置,允许优雅地降级。这是任何站点的一个小但至关重要的部分,并且比你的站点简单崩溃提供了更好的用户体验!

使用 Modernizr 作为后备方案

在使用 jQuery 时的最佳实践是确保为库提供后备,以防主要版本不可用。(是的,当它发生时很烦人,但它确实会发生!)

通常情况下,我们可能会在最佳实践建议中使用一些 JavaScript,例如以下示例。这个方法完全有效,但不提供优雅的后备方案。相反,我们可以使用 Modernizr 来执行检查并在所有失败时提供优雅的降级。

注意

Modernizr 是一个用于 HTML5/CSS3 的特性检测库,可以在功能不可用的情况下提供标准化的后备机制。你可以在 www.modernizr.com 了解更多信息。

举个例子,在我们网站页面的末尾,代码可能看起来像这样。我们首先尝试使用 CDN 链接加载 jQuery,如果没有成功,再退回到本地副本或者其他备用方案:

<body>
  <script src="img/modernizr.js"></script>
  <script type="text/javascript">
    Modernizr.load([{
      load: 'http://code.jquery.com/jquery-2.1.1.min.js',
      complete: function () {
        // Confirm if jQuery was loaded using CDN link
        // if not, fall back to local version
        if ( !window.jQuery ) {
          Modernizr.load('js/jquery-latest.min.js');
        }
      }
    },
      // This script would wait until fallback is loaded, before loading
      { load: 'jquery-example.js' }
    ]);
  </script>
</body>

通过这种方式,我们可以确保 jQuery 要么从本地加载,要么从 CDN 链接加载 —— 如果一切都失败了,那么我们至少可以优雅地退出。

加载 jQuery 的最佳实践

到目前为止,我们已经探讨了几种加载 jQuery 到我们页面的方式,除了通常的本地下载库或者在代码中使用 CDN 链接。现在我们已经安装了它,这是一个很好的机会来介绍一些在加载 jQuery 时应该尽量纳入我们页面的最佳实践:

  • 始终尝试使用 CDN 在生产站点上包含 jQuery。我们可以利用 CDN 服务提供的高可用性和低延迟;该库可能已经预缓存,避免了再次下载的需要。

  • 尝试在本地托管相同版本的库上实现一个备用。如果 CDN 链接不可用(它们不是 100%无懈可击的),那么本地版本将自动启用,直到 CDN 链接再次可用:

    <script type="text/javascript" src="img/"></script>
    <script>window.jQuery || document.write('<script src="img/jquery-1.11.1.min.js"><\/script>')</script>
    
  • 请注意,虽然这同样适用于使用 Modernizr,但如果 jQuery 的版本都不可用,它不提供优雅的备用。虽然人们希望永远不会出现这种情况,但至少我们可以使用 CSS 来提供优雅的退出!

  • 使用协议相对/协议独立的 URL;浏览器将自动确定使用哪种协议。如果 HTTPS 不可用,它将回退到 HTTP。如果你仔细观察上一点的代码,它展示了协议独立 URL 的完美例子,通过从主 jQuery Core 网站调用 jQuery。

  • 如果可能的话,将所有的 JavaScript 和 jQuery 引用放在页面底部——脚本会阻塞页面的其余部分渲染,直到它们完全渲染完成。

  • 使用 jQuery 2.x 分支,除非你需要支持 IE6-8;在这种情况下,使用 jQuery 1.x——不要加载多个 jQuery 版本。

  • 如果你使用 CDN 链接加载 jQuery,始终指定你想要加载的完整版本号,比如jquery-1.11.1.min.js

  • 如果你正在使用其他库,比如 Prototype、MooTools、Zepto 等,它们也使用$符号,那么尽量不要用$来调用 jQuery 函数,而是简单地使用 jQuery。你可以通过调用$.noConflict()函数将$的控制权交还给其他库。

  • 对于高级浏览器功能检测,使用 Modernizr。

值得注意的是,可能有一些情况并不总是能够遵循最佳实践;情况可能需要我们对需求做出让步,不能使用最佳实践。然而,尽量将这种情况降至最低,其中一个论点是,如果大部分代码都不遵循最佳实践,那么我们的设计可能存在缺陷!

摘要

如果你以为只有手动下载或使用 CDN 链接是包含 jQuery 的唯一方法,那么希望本章打开了你的眼界,让我们花点时间回顾一下我们学到了什么。

我们开始时习惯性地看了大多数开发者可能在快速移动到其他来源之前快速地加载 jQuery 的方式。

我们从如何使用 Node 开始,然后转向使用 Bower 包管理器。接下来,我们看了我们如何可以使用 AMD 方法引用 jQuery 中的个别模块。然后,我们转移到并把注意力转向使用 Git 创建库的自定义版本。然后我们涵盖了如何使用源映射调试我们的代码,看看如何在 Google 的 Chrome 浏览器中启用对它们的支持。

在完成加载 jQuery 的旅程时,我们看到如果完全无法加载 jQuery 会发生什么,并且如何通过使用 Modernizr 来优雅地处理这个问题。然后,我们在章节结束时介绍了一些在引用 jQuery 时可以遵循的最佳实践。

在下一章中,我们将通过了解如何定制 jQuery 来加速进展。这可以通过在运行时替换或修改函数,或者应用补丁来实现;你准备好开始了吗?

第二章:自定义 jQuery

好的,我们已经下载了一个版本的 jQuery……接下来该怎么做呢,我在想?

这是一个非常好的问题——让我来揭开所有的秘密!

多年来,jQuery 已经成为一个技艺精湛的库,在世界各地的数百万个网站中被使用。虽然我们通常可以找到一种方法来使用该库来满足需求,但可能会有一些情况需要我们提供自己的补丁或修改,以满足我们的需求。

我们可以使用插件,但是这在一段时间后会变得很烦人——很快就会出现“这个插件,那个插件”综合症,我们变得过于依赖插件。相反,我们可以看一下 jQuery 本身的覆盖功能;是的,它有一些风险,但正如我们将看到的那样,它绝对值得。在本章中,我们将介绍覆盖 jQuery 的基础知识,一些这样做的利与弊,并通过一些替换功能的示例来逐步展示。我们将涵盖以下主题:

  • 介绍鸭子打孔

  • 替换或修改现有行为

  • 创建一个基本的猴子补丁

  • 考虑猴子补丁的利与弊

  • 分发或应用补丁

准备开始你的冒险了吗……?让我们开始吧!

准备工作

在这一点上,我建议你在你的电脑上的某个地方创建一个项目文件夹——为了演示的目的,我假设它被称为project并位于你的主硬盘或C:驱动器的根目录下。

在文件夹中,继续创建几个子文件夹;这些文件夹需要被命名为fontscssjsimg

在运行时修补库

多年来,数百名开发人员花费了无数个小时为 jQuery 创建补丁,以修复某种描述的错误或在库中提供新功能。

通常的做法是针对核心 jQuery 库提交一个拉取请求供同行考虑。只要补丁按预期工作且不会在库的其他地方引起问题,那么它就会被提交到核心。

这种方法的缺点意味着我们受到 jQuery 的发布时间表的约束;虽然开发人员做得很出色,但在提交到核心之前可能需要一些时间。

介绍猴子补丁

该怎么办?我们是否等待,希望我们的补丁会被提交?

对于一些人来说,这可能不是问题——但对于其他人来说,耐心可能不是他们最强的美德,等待可能是他们最不想做的事情!幸运的是,我们可以通过使用一种称为猴子补丁的方法来解决这个问题。

现在——在你问之前——让我告诉你,我不主张任何形式的动物虐待!猴子补丁,或者另一种称为鸭子打孔的方式,是一种有效的技术,可以在运行时暂时覆盖 jQuery 核心库中现有的功能。猴子补丁也有其风险:主要的风险是冲突,如果更新在库中引入了同名的方法或函数。

注意

本章稍后,我们将研究一些需要考虑的风险。

话虽如此,如果小心和深思熟虑地使用猴子补丁,它可以被用来更新功能,直到一个更持久的修复方案被应用。我想,现在是时候进行演示了——我们将看看如何改进 jQuery 中的动画支持,但首先让我们看看如何在运行时替换或修改 jQuery 核心的基础知识。

替换或修改现有行为

那么,我们如何在 jQuery 的核心功能中进行(临时)更改?

一切都始于使用立即调用的函数表达式IIFE);然后我们简单地保存原始函数的一个版本,然后用我们的新函数覆盖它。

注意

你可能听过使用自执行匿名函数这个术语;这是一个误导性的短语,尽管它的含义与 IIFE 相同,但后者是一个更准确的描述。

让我们看看基本框架在实际中是什么样子的:

(function($){
  // store original reference to the method
  var _old = $.fn.method;
  $.fn.method = function(arg1,arg2){
    if ( ... condition ... ) {
      return ....
    } 
    else { // do the default
      return _old.apply(this,arguments);
    }
  };
})(jQuery);

如果你期望有更复杂的东西,那么我很抱歉让你失望了;对于基本的猴子补丁,不需要太多的复杂性!补丁中需要加入的内容实际上取决于你试图修复或修改现有代码中的内容。

为了证明这确实是所需的全部内容,让我们看一个(虽然过于简化的)例子。在这个例子中,我们将使用一个标准的点击处理程序来展示狗对主人的反应……只是我们的狗似乎出现了个性问题。

创建一个基本的猴子补丁

“个性变化?”我听到你问。是的,没错;我们的狗似乎喜欢喵喵叫……(我想不出任何原因;我不知道有哪些原因!)

在我们的例子中,我们将使用一个简单的点击处理程序来证明(在某些情况下)我们的狗可以喵喵叫;然后我们将逐步了解如何说服它做它应该做的事情。

  1. 让我们首先打开我们选择的文本编辑器,然后添加以下标记作为我们补丁的基础:

    <!DOCTYPE html>
    <head>
      <title>Demo: Basic Duck Punching</title>
      <meta charset="utf-8">
      <script src="img/jquery.min.js"></script>
      <script src="img/duck.js"></script>
    </head>
    <body>
      <div>Hello World</div>
      <button>Make it a dog!</button>
    </body>
    </html>
    
  2. 将其保存为duck.html文件。在另一个文件中,我们需要为我们的按钮添加动画效果,因此让我们首先添加一个简单的事件处理程序:

    $(document).ready(function() {
      jQuery.fn.toBark = function() {
        this.text("The dog says: Miaow!")
        };
        $('button').on('click', function() {
          $('div').toBark();
        });
    })
    

    此时,如果我们在浏览器中运行演示,然后点击让它成为狗!,我们确实可以看到我们可怜的宠物有些问题,如下截图所示:

    创建一个基本的猴子补丁

    我们明显需要让它看到自己行为的错误,所以现在让我们来修复它。

  3. 要解决问题,我们需要覆盖原来的toBark()函数。使用我们新的修复替代品;这将采用猴子补丁的形式。将以下代码插入到.on()点击处理程序的下方,留出一行空白以提高清晰度:

    (function($) {
      var orig = $.fn.toBark;
      $.fn.toBark = function() {
        orig.apply(this,arguments);
        if (this.text() === 'The dog says: Miaow!') {
          this.append(" *Punch* Miaow! *Punch* *Punch* 
          Woof?... *Gives Dog a chew*");
        }
      };
    }(jQuery));
    
  4. 如果一切顺利,我们现在至少应该看到我们的狗已经恢复了理智,虽然是逐渐地,如下截图所示:创建一个基本的猴子补丁

尽管这个小练习虽然极为简化,但它阐明了一些关键点——花点时间更详细地研究一下这一点是值得的,所以现在让我们来做一下。

解析我们的猴子补丁

对核心库进行补丁的过程应该谨慎和慎重;技术过程可能很简单,但首先需要回答一些问题。我们将在本章后面讨论其中的一些问题,但现在,让我们假设我们需要应用一个补丁。

基本补丁采用了 IIFE 的格式——我们将所有功能都包含在一个单独的模块中;其范围受到其所放置环境的保护。

要了解 IIFE 的更详细解释,请参考en.wikipedia.org/wiki/Immediately-invoked_function_expression

在我们的例子中,我们首先将原始函数存储为一个对象orig的副本。然后我们启动我们的新替换.toBark()函数,在其中我们首先调用.toBark()函数,但紧随其后进行替换:

(function($) {
  var orig = $.fn.toBark;
  $.fn.toBark = function() {
    orig.apply(this,arguments);
    if (this.text() === 'The dog says: Miaow!') {
      this.append(" *Punch* Miaow! *Punch* *Punch* 
      Woof?... *Gives Dog a chew*");
    }
  };
}(jQuery));

我们补丁的一个关键部分是使用.apply()函数——这将调用一个函数,并将上下文设置为应用函数的对象。在这种情况下,在函数内部引用this关键字将指向该对象。

在我们的演示中使用的 IIFE 的格式有许多优点,如下:

  • 我们可以减少作用域的查找——IIFE 允许您将常用的对象传递给匿名函数,因此它们可以在 IIFE 中被引用为本地作用域的对象

    由于 JavaScript 首先在本地范围内查找属性,这消除了全局查找的需要,提供更快的查找速度和性能。使用 IIFE 可以防止局部变量被全局变量覆盖。

  • IIFE 可通过压缩来优化代码——我们可以将对象作为本地值传递给 IIFE;压缩器可以将每个全局对象的名称缩减为单个字母,前提是没有一个变量已经具有相同的名称

使用 IIFE 的缺点是可读性;如果我们的 IIFE 包含大量代码,那么必须滚动到顶部才能弄清楚正在传递哪些对象。在更复杂的示例中,我们可以考虑使用 Greg Franko 开发的模式以解决这个问题:

(function (library) {
  // Call the second IIFE and locally pass in the global jQuery, 
  window, and document objects
  library(window, document, window.jQuery);
}
// Locally scoped parameters
(function (window, document, $) {
  // Library code goes here
}));

需要注意的是,这种模式是将变量分为两个部分,以便我们可以避免过度上下滚动页面的需要;它仍会产生相同的最终结果。

我们将更深入地讨论在 jQuery 中使用模式,第三章中的整理你的代码。现在我们已经看到了一个补丁的作用,让我们继续并花点时间考虑一下我们可以从使用猴子补丁过程中获得的一些好处。

考虑猴子补丁的好处

好了,所以我们已经看到了一个典型的补丁是什么样子;然而,问题是,为什么我们要使用这种方法来打补丁核心库功能呢?

这是一个非常好的问题-这是一个有风险的方法(正如我们将在本章稍后在考虑 Monkey Patching 的缺陷部分看到的)。使用这种方法的关键是要有所考虑的方法;考虑到这一点,让我们花一点时间考虑一下插入 jQuery 的好处:

  • 我们可以在运行时替换方法、属性或函数,其中它们缺乏功能或包含需要修复而不能等待官方补丁的 bug。

  • Duck punching jQuery 允许你修改或扩展 jQuery 的现有行为,而无需维护源代码的私有副本。

  • 我们有一个安全网,可以将补丁应用于运行在内存中的对象,而不是源代码;换句话说,如果完全出错,我们可以简单地从站点中撤回补丁,并保持原始源代码不变。

  • Monkey patching 是一种很好的方法,用于分发与原始源代码并存的安全或行为修复;如果对补丁的弹性有任何疑问,我们可以在提交到源代码之前进行压力测试。

言归正传,让我们开始编写一些演示代码!我们将逐步介绍一些示例补丁,这些补丁同样适用于 jQuery,从动画开始。

更新 jQuery 中的动画支持

如果你花了任何时间用 jQuery 开发,你很可能创建了一些形式的动画,其中包括以固定频率管理更改-这听起来熟悉吗?

当然,我们可以使用setInterval()函数来实现这一点,但它-像setTimeOut()函数一样-并不理想。这两个函数在启动之前都有一个延迟,这个延迟因浏览器而异;它们都同样占用资源!

相反,我们可以使用requestAnimationFramerAF)API,这个 API 现在由大多数现代浏览器支持,根据来自caniuse.com的这个图表,绿色标签显示了哪些浏览器版本支持requestAnimationFrame

更新 jQuery 中的动画支持

requestAnimationFrame(rAF)API的伟大之处在于它占用的资源较少,不会影响页面上的其他元素,并且在失去焦点时被禁用(非常适合减少功耗!)。因此,你可能会认为默认情况下在 jQuery 中实现它是有道理的,对吗?

探索 requestAnimationFrame API 的过去

具有讽刺意味的是,jQuery 在 1.6.2 版本中使用了 rAF;在 1.6.3 中被取消了,主要是因为当窗口重新获得焦点时,动画会堆积起来。部分原因可以归因于 rAF 的(错误)使用方式,以及为了纠正这些问题需要进行重大更改。

注意

要查看一些与时序相关的问题,请访问xlo.co/requestanimationframe——该站点上有一些演示,完美地说明了为什么时序如此关键!

今天使用 requestAnimationFrame 方法

幸运的是,今天我们仍然可以使用 requestAnimationFrame 与 jQuery;jQuery 的开发者之一 Corey Frang 编写了一个插件,可以钩入并重写核心库中的setInterval()方法。

注意

该插件的原始版本可从 GitHub 下载,网址为github.com/gnarf/jquery-requestAnimationFrame/blob/master/src/jquery.requestAnimationFrame.js

当我们使用 jQuery 时,这可能是我们可以做出的最简单的更改之一——在练习结束时,我们将探讨这个问题以及更多其他问题。现在,让我们继续编写一些代码吧!

创建我们的演示

对于我们的下一个演示,我们将使用开发者 Matt West 创建的 CodePen 示例的更新版本——原始演示可从codepen.io/matt-west/pen/bGdEC/获取;我更新了外观并移除了 Corey 插件的供应商前缀元素,因为它们已不再需要。

为了让你对我们即将实现的内容有所了解,我们将重写主setInterval方法;尽管它可能看起来像是调用了 jQuery 方法,但实际上setInterval是一个纯 JavaScript 函数,如下所示:

jQuery.fx.start = function() {
  if ( !timerId ) {
    timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
  }
};

我还更改了字体——为了这个演示,我使用了 Noto Sans 字体,可以从www.fontsquirrel.com/fonts/noto-sans下载;如果您想使用其他字体,请随意相应地更改代码。

准备好了吗?让我们开始执行以下步骤:

  1. 从随书附带的代码下载链接中提取raf.cssraf.jsraf.html文件,并将它们保存到项目文件夹中。

  2. 在一个新文件中,添加以下代码——这是我们的猴子补丁或 Corey 原始插件的修改版本。我们首先初始化了一些变量,如下所示:

    (function( jQuery ) {
      var animating,
          requestAnimationFrame = window.requestAnimationFrame,
          cancelAnimationFrame = window.cancelAnimationFrame;
    
          requestAnimationFrame = window["RequestAnimationFrame"];
          cancelAnimationFrame = window["CancelAnimationFrame"];
    
  3. 接下来是动画函数,它从主requestAnimationFrame方法中调用:

      function raf() {
        if ( animating ) {
          requestAnimationFrame( raf );
          jQuery.fx.tick();
        }
      }
    
  4. 现在我们需要我们的主requestAnimationFrame方法;继续在raf()事件处理程序的下方直接添加以下代码行:

    if ( requestAnimationFrame ) {
      // use rAF
      window.requestAnimationFrame = requestAnimationFrame;
      window.cancelAnimationFrame = cancelAnimationFrame;
      jQuery.fx.timer = function( timer ) {
        if ( timer() && jQuery.timers.push( timer ) && !animating ) {
          animating = true;
          raf();
        }
      };
      jQuery.fx.stop = function() {
        animating = false;
      };
    } ( jQuery ));
    
  5. 将文件保存为jquery.requestAnimationFrame.js,放在主项目文件夹下名为js的子文件夹中。

  6. 如果在浏览器中运行演示,当你按下开始动画时,你会看到进度条移动,如下图所示:创建我们的演示

  7. 为了证明插件正在被使用,我们可以使用谷歌浏览器的Developer Tools中的Timeline选项——点击红色的Record图标,然后运行演示,然后停止它产生以下内容:创建我们的演示

    提示

    确保在Timeline下勾选了JS Profiler复选框——将会显示详细信息;可能需要向下滚动查看Event条目。

这可能是我们能在 jQuery 中最容易进行的覆盖功能的改变之一,但也可能是最具争议性的之一——后者是由我们如何使用它决定的。然而,关键点是,我们可以使用多种格式覆盖功能。

最安全的方法是使用插件;在我们的例子中,我们使用了一个修改过的插件——原始插件是从 jQuery 1.8 引入的,所以这里做的改动只是将它带入了现代化。我们当然也可以完全走向相反的方向,创建一个覆盖现有功能的函数——这更有风险,但如果小心操作,是值得的!让我们看一个简单的例子,通过覆盖.hasClass()来在适当时切换到 WebP 格式的图片。

添加 WebP 支持到 jQuery

在这一点上,我有一个小小的坦白:为 jQuery 添加全面的 WebP 支持可能会超出本书的范围,更别提填满大部分页面了!

注意

WebP 是谷歌创建的一种比标准 PNG 文件压缩更好的相对较新的图像格式,您可以在developers.google.com/speed/webp/上了解更多。目前,Chrome 和 Opera 原生支持这种格式;其他浏览器在添加支持后也将显示 WebP 图像。

下一个演示实际上是关于我们如何根据浏览器是否支持新格式来在屏幕上显示内容的两种不同方式之间进行切换的。一个很好的例子是,我们可以尽可能使用 CSS3 动画,并在那些不原生支持 CSS3 动画的浏览器上回退到使用 jQuery。

在我们的下一个演示中,我们将使用类似的原理创建一个 monkey patch,以覆盖.hasClass()方法,以便在支持的情况下自动切换到 WebP 格式的图片。

注意

如果你想了解更多,可以在blog.teamtreehouse.com/getting-started-webp-image-format上找到一个有用的讨论,介绍了如何开始使用这种格式。

入门

为了展示这个演示,我们需要使用两种不同格式的图片;我先假设 JPEG 被用作我们的基本格式。另一张图片,当然,需要是 WebP 格式的!

如果您还没有准备好将图像转换为 WebP 格式的方法,那么您可以使用谷歌提供的工具来进行转换,这些工具可在developers.google.com/speed/webp/download下载。这里提供了 Windows、Linux 和 Mac OS 的下载版本——在本练习中,我将假设您正在使用 Windows:

  1. 在下载页面上,单击downloads.webmproject.org/releases/webp/index.html,然后查找libwebp-0.4.2-windows-x64.zip(如果您仍在使用 32 位 Windows 平台,请选择x86版本)。

  2. 下载后,将libwebp-0.4.2-windows-x64文件夹解压缩到项目文件夹中的一个安全文件夹中,然后导航到其中的bin文件夹。

  3. 打开第二个资源管理器视图,然后导航到您存储图像的位置,并将其复制到bin文件夹中。

  4. 打开命令提示符,然后导航到C:\libwebp-0.4.2-windows-x64\bin

  5. 在提示符下,输入此命令,将两个名称分别替换为您的 JPEG 和 WebP 图像的名称:

    cwebp <name of JPG image> -o <name of WebP image>
    
    
  6. 如果一切顺利,我们将会得到一个类似于以下屏幕截图的屏幕,并且我们的 WebP 格式图像将会出现在bin文件夹中:开始

  7. 最后一步是将图像复制到我们的项目文件夹中,以便在我们的演示的下一阶段中使用。

创建我们的补丁

现在我们准备好了图片,可以开始设置我们演示的标记了:

  1. 现在,将以下代码复制到一个新文件中,并将其保存为replacewebp.html

    <!DOCTYPE html>
    <head>
      <title>Demo: supporting WebP images</title>
      <script src="img/jquery.js"></script>
      <script src="img/jquery.replacewebp.js"></script>
    </head>
    <body>
      <img class="webp" src="img/phalaenopsis.jpg" />
    </body>
    </html>
    
  2. 接下来,我们需要添加我们的猴子补丁——在一个新文件中,添加以下代码并将其保存为jquery.replacewebp.js。这涉及到一些更复杂的内容,所以我们将分块介绍,从标准声明开始:

    (function($){
      var hasClass = $.fn.hasClass;
      $.fn.hasClass = function(value) {
        var orig = hasClass.apply(this, arguments);
        var supported, callback;
    
  3. 接下来是执行测试以查看我们的浏览器是否支持使用 WebP 图像格式的函数;将以下代码立即添加到变量分配的下方:

    function testWebP(callback) {
      var webP = new Image();
      webP.src = "data:image/webp;   base64,UklGRi4AAABX"
      + "RUJQVlA4TCEAAAAvAUAAEB8wAiMw"
      + "AgSSNtse/cXjxyCCmrYNWPwmHRH9jwMA";
      webP.onload = webP.onerror = function () {
        callback(webP.height == 2);
      };
    };
    
  4. 接下来,我们使用testWebP函数来确定我们的浏览器是否支持 WebP 图像格式——如果支持,我们将更改所使用的文件扩展名为.webp,如下所示:

    window.onload = function() {
      testWebP(function(supported) {
        console.log("WebP 0.2.0 " + (supported ? "supported!" : "not 
        supported."));
        $('.webp').each(function() {
          if (supported) {
            src = $(this).attr('src');
            $(this).attr('src', src.substr(0, src.length-3) + 'webp');
            console.log("Image switched to WebP format");
          }
      })
    });
    }
    
  5. 我们通过执行函数的原始版本来完成我们的函数,然后用与 IIFE 通常关联的关闭括号终止它:

       return orig;
      };
    })(:jQuery);
    
  6. 然后,我们需要再添加一个函数——这用于启动对.hasClass()的调用;继续添加以下代码行到猴子补丁函数的下方:

    $(document).ready(function(){
      if ($("img").hasClass("webp")) {
        $("img").css("width", "80%");
      }
    });
    
  7. 如果一切顺利,当我们运行我们的演示时,我们将看到一幅蝴蝶兰或蛾蝶兰的图像,如下面的屏幕截图所示:创建我们的补丁

这一点并没有什么特别之处;实际上,你可能在想我们到底产生了什么,对吧?

啊哈!如果你使用 DOM 检查器(如 Firebug)检查源代码,就会看到这个问题的答案,如下所示:

创建我们的补丁

注意它正在显示 JPEG 格式的图像?那是因为 Firefox 在出厂时不支持这种格式;只有 Google Chrome 支持:

创建我们的补丁

如果您切换到使用 Google Chrome,则可以通过按下Ctrl + Shift + I来查看源代码。您可以清楚地看到所使用格式的变化。如果您仍然怀疑,甚至可以查看 Google Chrome 的控制台选项卡。在这里,它清楚地显示了引用了补丁,因为它显示了您期望看到的两条消息:

创建我们的补丁

我们已经创建了我们的补丁,它似乎运行正常——这就是我们需要做的全部了,对吗?错,还有更多步骤我们应该考虑,其中一些甚至可能会阻止我们将补丁发布给更广泛的受众,至少暂时是这样。

有一些我们需要考虑的要点和可能需要采取的行动;让我们暂停一下,考虑一下我们需要从这里走向何方,就开发而言。

进一步的事情

在这个示例中,我们重写了一个现有方法来说明“鸭子打”——实际上,在发布之前,我们需要再花一些时间来完善我们的补丁!

这样做的主要原因是下载比我们实际需要的内容多;为了证明这一点,看一下在 Google Chrome 中运行演示时的资源选项卡:

进一步的事情

就像我们需要进一步确认一样,时间轴选项卡中的这段摘录也确认了 JPEG 和 WebP 图像的存在以及对下载时间的影响:

进一步的事情

我们在这里创建了一个补丁,以说明可以做什么;实际上,我们很可能会包含代码来执行不同的操作在我们的内容上。首先,我们可以做以下操作:

  • 包括支持更多的图像格式——这可以包括 JPEG,GIF 或 SVG。

  • 将代码硬编码为接受一个图像格式;通过使其更通用,我们可以扩展我们补丁的可用性。

  • jQuery 正朝着基于插件的架构发展;我们真的应该考虑修补核心代码吗?在代码中创建一个钩子可能会更有用,这样可以使用新插件扩展现有功能。

  • 我们使用了.hasClass()作为覆盖现有方法的基础;这真的是最合适的做法吗?虽然乍一看可能很有用,但实际上,其他人可能不同意我们覆盖.hasClass的选择,并认为其他方法更有用。

有很多问题可能会被提出并需要回答;只有经过仔细考虑,我们才能最大程度地使我们的补丁成功,并有可能考虑提交到核心中。

让我们改变方法,转而考虑猴子补丁的一个关键部分。这个过程有其风险,所以让我们花点时间考虑一下这些风险以及这些风险可能对我们的工作产生的影响。

考虑猴子补丁的缺陷

现在我们已经看到一些示例在实际中的应用,值得花点时间考虑一下对库进行猴子补丁的一些风险,比如 jQuery:

  • 主要风险,也是最容易造成麻烦的风险是冲突。想象一下,您已经创建了一个包含一些函数的补丁——我们将这些函数称为 123。添加另一个补丁,重要的是我们不要使用相同的函数名称;否则,很难确定 12 或甚至 3 哪一个先执行?

  • 另一个风险是安全性。如果 jQuery 等库可以进行猴子补丁,那么有什么能阻止任何人引入破坏现有代码的恶意构造?可以认为此风险在客户端脚本中始终存在;与标准插件相比,当您覆盖核心 jQuery 功能时,风险更大。

  • 总会有一个风险,即对核心库的升级可能会引入更改,不仅会破坏您的补丁,而且会删除或修改否则可以为您的补丁提供基础的功能。这将阻止使用 jQuery 的网站升级,并最终使其容易受到攻击。

  • 添加过多没有经过仔细考虑的补丁将使您的 API 庞大且缓慢;这将降低响应速度并使其更难管理,因为我们必须花更多时间剖析代码,然后才能找到问题的关键所在。

  • 任何猴子补丁都应该真正保留在您的网站内部;它们将基于直接修改 jQuery 的代码,而不是使用标准 jQuery 插件提供的预定义机制。作者可能没有像他们为插件所做的那样广泛地测试他们的猴子补丁;如果您使用别人的补丁,这会带来更大的风险。

  • 如果一个补丁包含大量函数,则更改核心功能的影响范围更广更大;进行这些更改可能会破坏其他人的补丁或插件。

哎呀!这里有严重的问题!如果我们面临这些风险,那么为什么还要使用这个过程呢?

这是一个很好的问题;适当使用时,猴子补丁是一种有用的技术,可以提供额外的功能或纠正问题。它甚至可以作为提交之前对代码进行压力测试的手段。还有一个论点认为功能应该包含在插件中,有充分的理由:

  • 插件可以发布供他人使用;如果插件可用,他们可以通过 GitHub 等网站贡献修复或更新。

  • 插件可能与 jQuery 的更多版本兼容,而不仅仅是简单的补丁;后者可能专门用于修复特定问题。

  • 制作一个覆盖多个修复的补丁可能会导致文件大小较大或核心功能的许多更改;这在插件框架内更好地管理,可以包括其他功能,如国际化。

  • jQuery Core 正在朝着更精简、更快速的架构发展;添加大量补丁将增加冗余功能的水平,并使其对其他开发人员的使用不太吸引人。

猴子补丁的关键不是滥用它;这是一个有效的工具,但只有在耗尽所有其他可能的解决方案后才真正有效。如果您急需修复问题并且不能等待官方更新,那么考虑猴子补丁 jQuery—只是要小心如何操作!

分发或应用补丁。

一旦我们的补丁完成,我们需要分发它;诱人的是简单地更新 jQuery 版本并与我们的插件一起发布,或在我们的网站中使用它。然而,使用这种方法有一些缺点:

  • 我们无法利用浏览器的缓存功能;如果我们使用缓存版本的 jQuery,则要么不包含我们的修补代码,要么从服务器拉取一个新副本。

  • 对 jQuery 的副本进行补丁意味着我们被锁定在那个版本的 jQuery 上。这会阻止最终用户能够使用他们自己的 jQuery 版本、CDN 链接,甚至是更新的 jQuery 版本(假设补丁仍然有效!)。

  • 允许补丁在运行时独立运行意味着它只会修补源代码中的对象;如果出现严重错误,那么我们可以放弃该补丁,仍然保留干净(未补丁)的 jQuery 版本。对源代码进行更改并不会给我们带来这种便利。

相反,我们可以使用一些替代方法来应用补丁:

  • 我们可以简单地在插件或网站内的单独文件中包含我们的补丁—这样可以保持核心 jQuery 库的清洁,尽管这意味着从服务器请求补丁文件的轻微开销。用户然后可以简单地链接到运行时文件的副本,并在情况发生变化时丢弃。

  • 补丁也可以作为 Gist 分发—这使其独立于我们的网站或插件,并允许其他人评论或提出建议,这些建议可以纳入我们的代码中。

    注意

    例如,我为replacewebp.js补丁创建了以下 Gist—这可以在 gist.github.com/alibby251/89765d464e03ed6e0bc1 上找到,并且可以链接到项目中作为分发代码的手段:

    <script src="img/89765d464e03ed6e0bc1.js"></script>
    
  • 如果补丁在 GitHub 存储库中可用—作为现有项目的一部分或独立存在。GitHub 将允许用户提交拉取请求以帮助改进现有补丁,然后再考虑提交到核心。

  • 我们还有另一种选择:补丁可以通过前端包管理器(如 Bower (www.bower.io) 或 Jam (www.jamjs.org))进行打包和交付。

    注意

    有关通过 Bower 下载包装内容的更多信息,请参阅 bower.io/docs/creating-packages/

这些是我们可以使用的分发补丁的一些选项;使用其中一些选项意味着我们可以让我们的补丁面向最广泛的受众,并希望从他们的测试和反馈中受益!

概要

在过去的几页中,我们涵盖了大量内容,其中一些可能会让你头晕,所以让我们喘口气,思考一下我们所学到的内容。

我们从介绍库的打补丁开始,比如 jQuery,并介绍了“鸭子补丁”(或者叫做猴子补丁)。我们看了如何使用这种方法替换或修改 jQuery 的现有行为,然后开始创建一个基本的猴子补丁,并详细解释了其在代码中的应用。

接下来,我们将看一下使用猴子补丁可以获得的一些好处;我们谈到了涉及的风险以及在创建和应用补丁时需要考虑的一些缺陷。

然后我们转而逐步完成了一些演示,探讨了一些我们可以暂时修改代码的方式,最后看了一下如何将我们的补丁用于生产环境。

开发任何形式的补丁或插件都需要保持良好的代码才能成功。在下一章中,我们将看到如何通过学习设计模式来改善我们在这个领域的技能,以更好地组织我们的代码。

第三章:组织你的代码

是否组织代码,这是问题…

在我们迄今的旅程中,我们已经涵盖了下载 jQuery 的各种方式,以及如何用自定义代码覆盖核心功能,但是——误引用那位著名的侦探:我们应该如何组织我们的代码?

好吧,你可能会认为我在这里搞疯了,但请跟我坚持下去;精通 jQuery 这样的语言不仅仅是制作复杂的代码,而是编写结构良好、简洁、易于阅读的代码。

在这一章中,我们将回到基础,介绍 jQuery 中可用的一些设计模式。我们将看到本章讨论的一些技术如何帮助改善您的代码格式,并使您成为更好的编码人员。在本章中,我们将涵盖以下主题:

  • 介绍设计模式以及为什么我们应该使用它们

  • 分解设计模式的结构

  • 探索不同设计模式的一些示例以及使用它们的好处

  • 探索 jQuery 库中的模式使用

准备好开始了吗?让我们开始…

介绍设计模式

有多少次你看网站时对美丽的设计感到惊叹,只是发现代码看起来像传说中的一团糟?一个常见的误解是外观是设计师的领域;这并不正确,因为代码的设计同样重要。

我们如何绕过这个问题?很简单,我们可以使用设计模式或一组构造,帮助提供解决方案,并让我们更专注于项目中想要提供的功能。

最初是由建筑师克里斯托弗·亚历山大于 1977 年创立的,工程师们自那时以来一直使用这些早期原则,并发展成我们现在知道的设计模式。这项工作后来在 1995 年由四人组(GoF)在他们标志性的书籍设计模式:可复用的面向对象软件元素中进一步推广。

它们不仅推动了设计模式的应用范围,还提供了一些设计技巧和陷阱;它们还对提供今天经常使用的二十三种核心模式起到了重要作用(其中,我们将涵盖在 jQuery 开发中常用的模式)。我们将看看今天正在使用的一些模式,但首先,让我们回答一个简单的问题。设计模式真正意味着什么,它们如何帮助我们编写清晰简洁的代码,从而减少不必要的重复?

定义设计模式

在基本层面上,设计模式采用预定义模板或一组可重用原则的格式,帮助对不同方法进行分类,作为支持良好设计的一部分。

为什么使用它们?很简单,将设计模式纳入我们的项目有三大好处:

  • 设计模式是经过验证的解决方案:它们基于解决软件开发中问题的坚实方法,并基于帮助创建所使用模式的开发人员的经验

  • 模式可重用:尽管它们通常代表一个现成的解决方案,但它们可以根据我们的需求轻松适应。

  • 模式是表达性的:它们包含一组结构和词汇,帮助您清晰而优雅地表达大型解决方案

此时,您可能会原谅以为模式必须是一门精确的科学,我们受到所使用模式框架的限制。事实并非如此;它们并不是一个确切的解决方案,而仅仅是一个帮助提供解决方案的方案。

更进一步地,我们应考虑在工作中使用设计模式的其他几个原因:

  • 我们可以有效地编写或预防可能在开发过程中稍后造成重大问题的小问题——使用经过验证的技术消除了对我们代码结构的担忧,并使我们能够专注于我们解决方案的质量。

  • 模式旨在提供通用解决方案,不将其限制在特定问题上,而是可应用于改善我们代码的结构。

  • 一些模式,如果明智地选择,可以帮助减少代码量,避免重复;它们鼓励我们仔细查看我们的代码,减少重复,并坚持使用不要重复自己DRY)原则,这是 jQuery 的基本原则之一。

  • 模式不是一次性、一时半刻的解决方案;我们的工作可能有助于改善现有设计,甚至提供创造新模式的范围!这种持续改进有助于确保模式随着时间的推移变得更加健壮。

不论其目的如何,设计模式的一个关键原则是,除非它们经过模式社区的严格测试,否则它们并不总被视为设计模式。许多可能看起来像是模式;实际上,它们更可能是原型模式或者一个已经被创建但尚未被充分测试以被视为真正模式的模式。

任何设计模式的核心原则都基于亚历山大的信念,即它们应始终代表一个过程和一个产出。后一术语被故意地设定得模糊一些;它应该代表一些可视的东西,但具体的视觉输出内容将取决于所选模式的上下文。

所以,既然我们已经看到了设计模式是什么,让我们讨论一下它们是什么样子的。它们由特定的元素或结构组成吗?在我们查看一些示例之前,让我们首先考虑一下设计模式的组成和我们如何有效使用它。

解剖设计模式的结构

如果您仔细查看任何设计模式,您会发现它由一个规则组成,该规则建立了以下内容之间的关系:

  • 一种上下文

  • 在该上下文中产生的一系列力量

  • 允许这些力量在上下文中自行解决的配置

这三个关键方面可以进一步分解为许多不同的元素,除了模式名称和描述之外:

元素 目的或功能
上下文概述 模式有效的上下文,以响应用户的需求。
问题陈述 解决的问题陈述,以便我们了解模式的意图。
解决方案 描述用户问题如何在易于理解的步骤和感知列表中解决。
设计 模式设计的描述,特别是用户与其交互时的行为。
实现 模式将如何实现的指南。
插图 模式中类的可视化表示,如 UML 图表。
示例 模式的最小形式实现。
先决条件 用于支持描述的模式使用的其他模式是什么?
关系 这种模式是否类似(或模仿)任何现有模式?
已知用法 这种模式是否已经在实际应用中使用?如果是,是在哪里以及如何使用?
讨论 团队或作者关于使用模式的好处的想法。

使用模式的美妙之处在于,虽然在规划和文档编制阶段可能需要一定的努力,但它们是有用的工具,有助于使团队中的所有开发人员保持一致。

在创建新模式之前,先看看现有模式是值得的——可能已经有人在使用,这样就减少了从头设计并经过漫长的测试过程的必要性,才能被其他开发人员接受。

模式分类

现在我们已经了解了典型设计模式的结构,让我们花点时间考虑一下可用的模式类型。模式通常分为以下三类,这是最重要的类别之一:

  • 创建型模式:这些模式关注我们如何创建对象或类。虽然这听起来可能很简单(在某些方面,比如常识),但它们在需要控制对象创建过程的大型应用程序中可能非常有效。创建型模式的示例包括抽象、单例或建造者。

  • 结构设计模式:这些模式关注如何管理对象之间的关系,以使您的应用程序以可扩展的方式构建架构。结构模式的一个关键方面是确保应用程序的一个部分的更改不会影响到所有其他部分。该组涵盖了诸如代理、适配器或外观等模式。

  • 行为模式:这些模式关注对象之间的通信,包括观察者、迭代器和策略模式。

有了这个想法,让我们花点时间探索一些常用的设计,从组合模式开始。

组合模式

如果你花时间用 jQuery 开发,你有多频繁地编写类似于这样的代码:

// Single elements
$("#mnuFile").addClass("active");
$("#btnSubmit").addClass("active");

// Collections of elements
$("div").addClass("active");

没有意识到的是,我们正在使用组合模式的两个实例——它是结构模式组中的一个成员;它允许您以相同的方式对单个对象或一组对象应用相同的处理,而不管我们要定位多少个项目。

简而言之,当我们对一个元素或一组元素应用方法时,会应用一个 jQuery 对象;这意味着我们可以以统一的方式处理任何一个集合。

那么,这意味着什么?让我们看看另外一些例子:

// defining event handlers
$("#tablelist tbody tr").on("click", function(event) {
  alert($(this).text());
});
$('#btnDelete').on("click", function(event) {
  alert("This item was deleted.");
});

使用组合模式的优点在于,我们可以在每个实例中使用相同的方法,但对每个元素应用不同的值;它为最终用户提供了一个统一的界面,同时在后台无缝应用更改。

组合模式的优缺点

使用组合模式可以简单也可以复杂;使用这种模式有优点和缺点,我们应该考虑:

  • 我们可以对顶级对象调用一个函数,并将其应用于结构中的任何一个或所有节点,产生相同的结果。

  • 组合设计中的所有对象都是松散耦合的,因为它们都遵循相同的接口。

  • 组合设计为对象提供了一个良好的结构,而不需要将它们保存在数组中或作为单独的变量。

使用组合模式也有一些缺点;以下是需要考虑的主要问题:

  • 我们并不总是能够确定我们正在处理单个项还是多个项;API 对单个项和多个项使用相同的模式。

  • 如果组合模式超出一定大小,您的站点的速度和性能将受到影响。

让我们继续看看更多的模式;接下来是适配器模式

适配器模式

我们可以使用 jQuery 来切换分配给选择器的类;但在某些情况下,这可能会过于复杂,或者给选择器分配类可能会出现我们需要避免的问题。幸运的是,我们可以使用 .css() 函数直接将样式应用于我们的元素——这是在 jQuery 中使用适配器模式的一个很好的例子。

基于结构设计模式的一种模式,适配器模式将 jQuery 中元素的接口转换为与特定系统兼容的接口。在这种情况下,我们可以使用 .css() 形式的适配器为我们选择的元素分配 CSS 样式:

// Setting opacity
$(".container").css({ opacity: 0.7 });

// Getting opacity
var currentOpacity = $(".container").css('opacity');

这样做的美妙之处在于一旦样式设置好了,我们可以使用相同的命令获取样式值。

适配器模式的优缺点

使用适配器设计模式有几个关键优点;其中一个关键优点是它能够链接两个不兼容的接口,否则这两个接口将必须保持独立。

另外,值得注意以下额外的好处:

  • 适配器模式可用于在不影响其核心功能的情况下创建一个外壳,比如一个类,围绕现有的代码块。

  • 这种模式有助于使代码可重用;如果情况需要,我们可以调整外壳以包含额外的功能或修改现有代码。

使用适配器模式会带来一些缺点,如果我们不小心的话:

  • 使用关键字如 .css() 存在性能成本 —— 我们真的需要使用它们吗?或者,我们可以应用一个样式类或选择器,并将 CSS 样式移到样式表中吗?

  • 使用关键字,比如 .css(),来操作 DOM,如果我们没有简化选择器,并且如果我们使用了像这样的东西,会导致性能受损:

    $(".container input#elem").css("color", "red");
    

    这在小型站点或仅轻度使用此类操作的地方可能不明显;但在较大的站点上,它将是显而易见的!

  • 适配器模式允许您链接 jQuery 命令;尽管这将有助于减少需要编写的代码量,但这将以可读性为代价。链式命令会使得在以后的日期更难调试代码,特别是如果涉及到开发人员的变更;保持代码简单和清晰是有意义的,即使只是为了保持理智!

让我们继续并再次看看另一种模式,即外观模式

外观模式

起源于法语,"façade" 翻译为 正面面貌 ——这是对下一个模式的完美描述;它的外观可能非常具有迷惑性,就隐藏的代码量而言!

外观模式,结构模式组的另一个成员,为更大、更复杂的代码提供了一个简单的接口;在某种意义上,它抽象了一些复杂性,留下了我们可以随意操纵的简单定义。外观模式的显著示例包括 DOM 操作、动画,当然还有那个经典的 AJAX!

例如,简单的 AJAX 方法,比如 $.get$.post 都调用相同的参数:

$.get( url, data, callback, dataType );
$.post( url, data, callback, dataType );

这些在本身就是另外两个函数的外观:

// $.get()
$.ajax({ 
  url: url,
  data: data,
  dataType: dataType
}).done( callback );

// $.post
$.ajax({
  type: "POST",
  url: url,
  data: data,
  dataType: dataType
}).done( callback );

这反过来是对大量复杂代码的伪装!这种情况下的复杂性源自于需要解决 XHR 的跨浏览器差异,并且使得使用 jQuery 中的 getpostdeferredpromises 等操作变得轻而易举。

创建一个简单的动画

在非常简单的层面上,$.fn.animate 函数是 jQuery 中的一个外观函数的例子,因为它使用多个内部函数来实现所需的结果。因此,这是一个使用动画代码的简单演示:

$(document).ready(function() {
  $("#go1").click(function() {
    $("#block1")
      .animate({width: "85%"}, {queue: false, duration: 3000})
      .animate({fontSize: "24px"}, 1500)
      .animate({borderRightWidth: "15px"}, 1500);
  });

  $("#go2").click(function() {
    $("#block2")
      .animate({ width: "85%" }, 1000 )
      .animate({ fontSize: "24px" }, 1000 )
      .animate({ borderLeftWidth: "15px" }, 1000 );
  });

  $("#go3").click(function() {
    $("#go1").add( "#go2").click();
  });

  $("#go4").click(function() {
    $("div").css({width: "", fontSize: "", borderWidth: ""});
  });
})

上述代码将产生以下动画效果:

创建一个简单的动画

我们可以在核心库中使用下图中显示的函数:

创建一个简单的动画

注意

本节演示的代码在本书附带的代码下载链接中作为 animation.html 文件提供;要使此演示工作正常,您需要解压整个代码文件夹。

现在您已经看到了外观模式的应用,请考虑一下在我们的代码中使用它的好处。

外观模式的优缺点

使用外观模式隐藏复杂代码是一种非常有用的技术;除了易于实现之外,使用这种模式还有其他优点,如下所示:

  • 增强您的 Web 应用程序的安全性

  • 与其他模式结合使用效果很好

  • 使内部代码容易修补

  • 为最终用户提供简单的公共接口

与其他模式相比,在使用这种模式时没有真正显著的缺点;它为我们作为最终用户提供了统一的接口集,因此我们不必做出任何妥协。 值得注意的是,在抽象代码时,实施中会有成本—在使用外观模式时,这是我们在心中始终要记住的事情。

观察者模式

由于它是行为模式组的一部分,我们将已经熟悉下一个模式——如果您花时间创建自定义事件,那么您已经在使用观察者模式

使用 jQuery 开发的一个关键部分是使用其经过验证的发布/订阅系统来创建自定义事件—通过使用 .trigger(), .on(), 或 .off() 可以访问这些事件。 我们可以将观察者模式定义为当特定对象订阅其他对象并在特定事件发生时被通知时的模式:

观察者模式

试想一下,我们有以下 HTML:

<div id="div1">This is div 1</div>
<div id="div2">This is div 2</div>

我们希望内部的 <div> 元素触发一个名为 customEvent 的事件;当它们被点击时,就会发生这种情况:

$('div').on('click', function(e) {
  $(this).trigger('customEvent');
});

现在,让我们使文档元素订阅 customEvent

$(document).on('custom', function(e) {
  console.log('document is handling custom event triggered by ' + 
  e.target.id);
});

当一个 div 元素之一触发自定义事件时,观察者/订阅者会收到通知,并且消息将记录到控制台中。

注:

对于纯粹主义者来说,你们可能更喜欢使用典型的发布/订阅模型——在 gist.github.com/cowboy/661855 中提供了一个示例。

让我们考虑一下使用这种模式的好处以及在代码中可能需要做些让步来避免陷入与使用这种设计模式相关的一些陷阱。

观察者模式的优缺点

使用观察者模式强迫我们考虑应用程序各个部分之间的关系,远远超过我们可能习惯于考虑的水平。 它还非常擅长以下几点:

  • 促进 jQuery 中的松耦合,其中每个组件都知道自己负责什么,不关心其他模块——这鼓励可重用代码。

  • 让您遵循关注点分离原则;如果代码块是自包含的,它们可以在新项目中轻松重用。然后,我们可以订阅单个事件,而不必担心每个块中会发生什么。

  • 帮助我们准确定位项目中的依赖关系所在,作为确定这些依赖关系是否可以通过一点努力来减少或完全消除的潜在依据。

使用观察者模式也有缺点;主要缺点是将一个订阅者从一个发布者转换到另一个发布者可能在代码方面代价高昂,并且难以维护我们代码的完整性。

为了说明这一点,让我们简要地看一下一个简单的例子,我们可以看到至少一个我们不得不为发布者的切换做出额外让步的实例。

创建一个基本示例

弄清楚观察者模式的工作原理至关重要;它是更深入的模式之一,并提供了比简单的协议集更多的机会,比如外观设计模式。考虑到这一点,让我们运行一个快速的演示来说明它的工作原理,如下所示:

  • 让我们首先下载并提取本章的代码副本——我们需要observer.html文件,以及cssjs文件夹。

  • 如果您运行演示,您应该会看到两个标签变为红色,您可以单击它们;如果您尝试单击它们,您将看到计数增加,如此屏幕截图所示:

创建一个基本示例

此时,让我们考虑一下代码——关键功能在observer.js文件中,我在此处完整复制了它:

$(document).ready(function() {
  var clickCallbacks = $.Callbacks();
  clickCallbacks.add(function() {
    var count = parseInt(this.text(), 10);
    this.text(count + 1);
});
clickCallbacks.add(function(id) {
  $('span', '#last').text(id);
});
$('.click').click(function() {
  var $element = $(this).next('div') .find('[id^="clickCount"]');
  clickCallbacks.fireWith($element, [this.id]);
  });
});

注意一下,对于.click类,有一个单独的事件处理程序。我们在这里使用了一个回调函数,以允许 jQuery 执行下一个点击,即使它可能尚未完成前一个执行。在这种情况下,这不会是太大的问题,但如果我们必须更新多个不同的语句或应用更多的更改(通过使用额外的函数),那么回调将防止我们的代码产生错误。

在这里,我们订阅可观察对象,这在这种情况下是两个点击我语句;.click事件处理程序允许我们更新点击计数和最后点击的元素语句,而不会引发错误。

注意

要了解更多关于在 jQuery 中使用回调的复杂性,您可能需要浏览 API 文档,可以在api.jquery.com/jquery.callbacks/查看。

与此同时,让我们改变焦点,看看不同的模式。我们都知道 jQuery 以其 DOM 操作能力而闻名;接下来是迭代器模式,它基于 jQuery 的这一特定功能。

迭代器模式

现在,你听过或读过多少次 jQuery 以其 DOM 操作而闻名?我敢打赌,有相当多次,而且.each()关键字在这些示例中某个时候被使用过。

jQuery 中的 DOM 操作使用了行为组模式中的特殊变体——这就是它的用途;我们可以使用这种模式来遍历(或迭代)集合的所有元素,让 jQuery 处理内部工作。这种模式的一个简单示例可能是这样的:

$.each(["richard","kieran","dave","alex"], function (index, value) {
  console.log(index + ": "" + value);
});

$("li a").each(function (index) {
  console.log(index + ": " + $(this).text());
});

在这两种情况下,我们都使用了.each函数来遍历数组或li选择器的每个实例;无需担心迭代器的内部工作原理。

我们的示例代码包含最少的代码,以便遍历页面中的每个选择器或类;值得一提的是,查看核心库中jQuery.fn.each()函数的代码量:

迭代器模式

这反过来调用了jQuery.each()函数——第一个函数仅供内部使用,如下图所示:

迭代器模式

然后,这又得到了一个特殊的快速情况,即对.each()函数最常见用法的补充:

迭代器模式

迭代器模式的优缺点

在 DOM 中遍历元素的能力是 jQuery 的关键要素之一——作为迭代器模式的关键部分;使用这种模式有以下一些好处:

  • 迭代器模式隐藏了遍历集合所需的大部分功能,而无需理解提供此功能的代码的内部工作原理

  • 我们可以使用相同的一致模式来遍历任何对象或一组值

  • 使用迭代器过程还可以帮助减少或消除我们代码中典型的for循环语法,使代码更易读

与其他模式不同,使用这种模式几乎没有什么缺点。它是 jQuery 的一个关键方面,所以只要不被迭代过多的对象滥用,这个简单的模式将证明非常有用!

惰性初始化模式

呵呵,这听起来像是我周日早晨可能会遵循的东西!好吧,我知道那是一个糟糕的笑话,但是开玩笑归开玩笑,这种基于创建的模式允许您推迟昂贵的过程,直到它们被需要。

在其最简单的层面上,我们可能会使用多个不同选项来配置插件,比如要显示的图像数量,是否应该显示叠加层,或者每个图像如何显示。听起来很简单,对吧?那么,惰性初始化在哪里呢?啊哈!这比你想象的简单。以以下代码为例:

$(document).ready(function(){
  $("#wowslider-container1").wowSlider();
});

我们的示例使用了 WOW Slider 的初始化命令(可在 www.wowslider.com 上获取)—使用这种模式的关键在于初始化过程;直到第一次在我们的页面上需要时才触发它。

懒惰初始化模式的一个更复杂的示例是回调;这些不会在 DOM 准备好之前进行处理:

$(document).ready( function () {
  var jqxhr = $.ajax({
    url: "http://domain.com/api/",
    data: "display=latest&order=ascending"
  })
  .done( function( data ) ){
  $(".status").html( "content loaded" );
  console.log( "Data output:" + data );
  });
});

我们可能会直接在我们的代码中使用这个示例;更可能的是我们会在懒加载插件中使用它,比如 Mika Tuupola 在 www.appelsiini.net/projects/lazyload 上的版本。

懒惰初始化模式的优缺点

使用这种设计模式的关键优势很简单:延迟加载昂贵资源的加载;这有助于加快对站点的访问速度,并减少带宽使用(至少最初是如此)。

但是,使用这种方法还存在一些缺点,包括以下:

  • 它需要通过设置标志来进行仔细管理,以测试所召唤对象是否准备好使用;如果不是,那么在多线程代码中可以生成竞争条件

  • 任何懒变量或对象的先前使用都将绕过首次访问时的初始化原则,并意味着我们失去了不加载这些大对象或变量的任何好处。

  • 这种方法需要使用映射来存储实例,以便在下次以前使用相同参数向存储实例请求时得到相同的实例

  • 使用这种模式涉及时间成本,如果需要加载大型对象;如果最初不加载这些对象,并且有很大的可能性它们不会被使用,那么这种模式才真正起作用

最终,使用这种模式需要一些考虑和仔细规划;只要我们选择了不加载正确的对象,它就能很好地工作!说到策略,让我们继续,看看帮助我们确定在对象或变量状态改变时会发生什么的另一个模式,即策略模式

策略模式

回想一下几年前,使用 Flash 在网站上做动画内容是最新的设计潮流;有些设计得非常好,尽管经常情况下网站速度慢,并且并不总是像它们应该的那样有效!而现在,CSS 动画更受欢迎—它们不需要浏览器插件来运行,可以存储在样式表中,比 Flash 更节约资源。

"为什么我们在讨论动画?",我听到你问道,当这一章是关于设计模式时。这是一个很好的问题;答案很简单:尽管有些人可能没有意识到,但动画是我们接下来的设计模式的一个完美示例。在基本层面上,动画都是关于从一个状态变化到另一个状态—这构成了行为模式组中的策略模式的基础。

也被称为策略或状态模式,策略模式允许您在运行时选择适当的行为。简而言之,这就是模式的作用:

  • 定义一个用于确定运行时应发生什么的算法(或函数)族

  • 将每个算法(或函数)封装到其自包含的单元中,并使每个算法在该族内可互换

策略模式可以应用的一个很好的例子是在表单条目的验证中——我们需要一些规则来确定什么是有效或无效的内容;在输入内容之前,我们显然不会知道结果会是什么!

关键点在于验证规则可以封装在自己的块中(可能作为自己的对象);一旦我们知道用户想要我们验证什么,我们就可以拉入相关的块(或规则)。

在更基本的层面上,有一个策略模式的更简单的例子;它采用动画内容的形式,比如使用.toggle(),我们在不同状态之间或者回到原状态之间进行切换:

$('div').toggle(function(){}, function(){});

每个生成的状态都可以设置为自己的类;一旦我们知道请求的操作应该是什么,它们将在适当的时间被调用。为了帮助设置上下文,让我们创建一个简单的演示来看看它的运作方式。

构建一个简单的切换效果

好吧,虽然这是 jQuery 101,但当它完美地展示了我们所需要的内容时,为什么要把事情复杂化呢?

在此演示中,我们执行一个简单的切换动作来显示或隐藏两个<p>语句——关键点在于在按下按钮之前我们不知道接下来会发生什么。

要查看此操作,请下载本章的code文件夹的副本;运行strategy.html演示,然后单击切换它们以查看<p>语句的显示或隐藏,如下所示:

构建一个简单的切换效果

魔术发生在这个函数中;它是使用.toggle()命令来根据需要切换每个<p>语句的可见性的简单用法:

$(document).ready(function(){
  $("button").click(function() {
    $("p").toggle("slow");
  });
});

但是,我们可以轻松地将点击事件处理程序中包含的函数抽象为一个单独的 IIFE,然后在我们的代码中简单地调用该函数,如下所示:

$(document).ready(function(){
  var hideParagraphs = function() {
    $("p").toggle("slow");
  };

  $("button").click(hideParagraphs);
});

代码已经更容易阅读——我们已经将大部分原始操作从事件处理程序中移除;这消除了以后需要更改代码时编辑事件处理程序的需要。

注意

如果你对立即调用的函数表达式(IIFEs)感兴趣,那么你可能想要查看维基百科的条目以获取更多细节,该条目位于en.wikipedia.org/wiki/Immediately-invoked_function_expression

在不同动作之间切换

尽管我们在示例中集中讨论了动画,但我们中敏锐的人可能想知道同样的技术是否也适用于诸如 switch() 等命令。答案是肯定的;我们没有在这里讨论它,因为它是一个纯粹的 JavaScript 命令,但是你可以将相同的原则应用作为它的替代方案。

策略模式的优缺点

定义一个明智的策略是成功编码的关键;通过使用策略模式,我们可以获得一些好处:

  • 代码更易于阅读;如果我们将函数抽象成它们自己的类,我们可以将它们从决策过程中分离出来,要么作为同一文件中的独立代码块,要么甚至作为自己文件中的独立文件

  • 代码更易于维护;我们只需要进入类来更改或重构代码,而且我们只需要对核心代码进行最小的更改,以便为新的类或对象事件处理程序添加链接

  • 我们可以保持关注点的分离——我们抽象的每个独立类或对象都不会意识到其他组件,但是当提供了每个策略对象的责任和相同的接口时,它们可以与其他对象通信

  • 使用策略模式可以让你利用开放/封闭原则;每个抽象类或对象的行为可以通过启动现有行为的新类或对象实例来改变

提示

关于开放/封闭原则的更多细节,请参阅 en.wikipedia.org/wiki/Open/closed_principle

这些是我们需要注意的一些缺点:

  • 使用策略模式可以让你遵守开放/封闭原则,但同时,你可能会启动一个包含许多不必要函数或动作的新类或对象的代码,从而使你的代码变得更加繁琐

  • 有些情况下使用策略模式可能不适合你的目的;如果你的代码只包含少量函数,那么将它们抽象化所需的工作量可能会超过所带来的好处

  • 使用策略模式将增加代码中的对象数量,使其变得更加复杂,并且可能需要更多资源来管理

策略讨论够多了;让我们继续看一个不同的协议,以代理设计模式的形式

代理模式

在使用 jQuery 时,可能会有这样的情况,你可能想编写一个通用事件处理程序来管理某些元素上的样式——一个很好的例子可能是从活动状态切换到禁用状态,或者甚至是选定状态;然后我们可以使用普通的 CSS 来为这些样式编写样式。

使用这种方法,一个通用的事件处理程序可能如下所示:

$(".myCheckbox").on( "click", function () {
  // Within this function, "this" refers to the clicked element 
  $(this).addClass("active");
});

乍一看,这样做完全可以正常运行,但是如果我们在更改样式类之前引入延迟会怎么样?我们通常会使用 setTimeOut() 函数来实现这一点:

$(".myCheckbox").on( "click", function () {
  setTimeout(function () {
    // "this" doesn't refer to our element, but to the window!
    $(this).addClass("selected");
    });
});

有人发现这里有一个小但相当关键的问题吗?将任何函数传递给setTimeout都会给出错误的值—它将引用窗口对象,而不是传递的对象!

解决此问题的一种方法是使用 jQuery 的proxy()函数;我们可以使用此函数来实现代理模式或中间层,以确保正确的值通过到.addClass()方法的正确上下文中。我们可以调整我们之前的示例,如下代码片段所示:

$(".myCheckbox").on( "click", function () {
  setTimeout( $.proxy( function () {
    // "this" now refers to our element as we wanted
    $( this ).addClass( "active" );
    }, this), 500);
});

我们传递的最后一个this参数告诉$.proxy()我们的 DOM 元素是我们想要this引用的值—在这种情况下,它是复选框,而不是窗口。

代理模式的优缺点

代理模式是来自结构组的有用设计,可以帮助优化和维护快速站点;在其核心,该模式基于不加载昂贵元素直到绝对必要的原则。(最好根本不加载,如果可以的话!)

使用这种设计模式可以获得一些好处,如下所示:

  • 我们可以使用代理模式为尚未加载或可能永远不会加载的更昂贵的对象提供占位符;这包括可能从应用程序外部加载的对象

  • 使用代理可以充当包装器,为真实对象提供委托,同时保护它免受不必要的复杂性

  • 将代理模式纳入我们的页面中可以帮助减少代码繁重站点的感知速度慢或响应不足。

使用这种模式的缺点包括以下内容:

  • 代理模式存在一个风险,即代理模式可能会将易变资源的生命周期和状态隐藏在其客户端之外;这意味着代码必须等待正确的资源再次可用,或者产生错误。它需要知道它正在与原始资源交互,而不是与可能与原始资源类似的另一个资源交互。

  • 如果我们正在使用代理模式来表示远程资源,则这将掩盖两者之间的通信使用;与本地资源的通信应与远程资源的通信区别对待。

经过谨慎的使用,代理模式可以证明非常有用,只要我们对我们决定加载或不加载到我们的页面中的内容保持理性。让我们改变方向,看看另一种设计模式;这个模式基于我们可能需要动态构建一个或多个元素的方式;这个概念是生成器模式的核心。

生成器模式

在任何项目的开发过程中,都可能会出现需要动态创建新元素的情况;这可以是构建单个<div>元素,也可以是各种元素的复杂组合。

我们可能希望在代码中直接定义最终的标记,这可能会变得混乱,或者我们可以将元素分离成一个独立的机制,允许我们简单地构建这些元素,以便稍后在代码中使用。

后者,或者称为建造者模式的技术名称,更可取; 它更容易阅读,并允许您清晰区分变量和代码的其他部分。 此特定模式属于创建模式组,并且是您将看到此类模式的少数常见示例之一。

注意

您可能会在网上或书籍中看到对抽象模式的引用——它与建造者模式非常相似。

我们可以使用 jQuery 的美元符号来构建我们的对象; 我们可以传递完整的元素标记,部分标记和内容,或者简单地使用 jQuery 进行构造,如下所示:

$('<div class="foo">bar</div>');

$('<p id="newText">foo <b>bar</b></p>').appendTo("body");

var newPara = $("<p />").text("Hello world");

$("<input />")
      .attr({ "type": "text", "id":"sample"})
      .appendTo("#container");

创建后,我们可以使用变量缓存这些对象,并减少对服务器的请求次数。

值得注意的是,设计模式不仅限于脚本代码; 它们可以应用于使用类似原则的插件。 我们将在第十一章 中介绍更多适用于 jQuery 插件的设计模式,编写高级 jQuery 插件

建造者模式的优缺点

使用建造者模式并不适用于所有情况;值得注意的是,通过使用它可以获得的好处,以便查看这些是否符合您的要求。 这些好处包括:

  • 我们可以在 jQuery 内动态构建创建对象所需的标记,而无需显式创建每个对象

  • 我们可以缓存标记,然后将其与主要功能分离,这样可以更轻松地阅读代码并减少对服务器的请求

  • 核心标记将保持不可变,但我们可以对其应用不同的功能以改变值或外观

  • 我们可以进一步将我们的建造者模式转换为状态机或公开方法或事件的机制,同时仍保留私有构造函数或析构函数方法

使用建造者模式有一些缺点; 关键缺点是滥用链接的使用,但我们还应考虑以下方面:

  • 有可能定义无法轻松重用的标记; 这意味着我们可能需要创建一些包含标记的变量,所有这些变量都将占用应该用于其他用途的资源。

  • 以下是一个代码片段的示例:

    var input = new TagBuilder("button")
      .Attribute("name", "property.name")
      .Attribute("id", "property_id")
      .Class("btn btn-primary")
      .Html("Click me!");
    

    使用建造者模式允许链接操作,提供一致的 API,并遵循建造者模式。 但是,这种模式的主要缺点是使代码更难阅读,因此更难调试。

我们已经在概念层面上探讨了许多不同的设计模式类型; 对于一些人来说,将其与我们所知的 jQuery 核心联系起来可能仍然会很困难。

然而,美妙之处在于 jQuery 在整个代码库中都使用这些模式——为了帮助您将所学的一些知识付诸实践,让我们花一点时间来检查核心库并看看这些模式是如何在内部使用的一些示例。

探索 jQuery 库中模式的使用

现在,你可能会想:我仍然不确定这些模式与我的工作有何关联。是吗?

我想是的。在本章中,我们花时间研究了一些常用的模式,作为回归基础的一种方式;毕竟,提高自己的秘诀不仅仅是通过编写代码!

关键点在于,如果你花时间用 jQuery 开发,那么你已经在使用设计模式;为了加强你所学的内容,让我们来看看 jQuery 库本身的一些示例:

注意

为了演示目的,我使用了 jQuery 2.1.1;如果你使用不同版本,那么你可能会发现一些行号已经改变了。

  1. 首先,在你选择的文本编辑器中打开jquery.js的一个副本——我们将从经典的document.ready()函数开始,它使用 Façade 模式,并且在大约3375行附近运行此函数,如下面的屏幕截图所示:探索 jQuery 库中模式的使用

  2. 你有多少次切换页面中元素的状态?我猜可能会有很多次;toggle命令是策略设计模式的一个典型例子,我们在这里决定一个元素的状态,如下所示:探索 jQuery 库中模式的使用

  3. 现在,我确定你已经点击过无数个元素或使用过click事件处理程序,对吗?我希望是的,因为这是我们在学习 jQuery 时可能开始使用的第一个事件处理程序。它也是观察者模式的一个很好的例子。以下是 jQuery 中相关的代码,大约在7453行附近:探索 jQuery 库中模式的使用

jQuery 核心库中有更多设计模式的使用示例;希望这能展示它们在你自己的代码中的好处,并且它们不应该局限于 jQuery 本身的源代码!

概要

哎呀!我们确实涵盖了很多关于设计模式的理论;让我们稍事休息,回顾一下你在本章学到的东西。

我们从介绍设计模式是什么以及它们是如何产生的开始;然后我们继续探讨了使用它们的好处以及为什么我们应该考虑在我们的项目中使用它们。

接下来,我们来看一下设计模式的结构,我们将一个典型的设计拆分成其不同的元素,并看到每个元素在设计方案中扮演的角色。我们还看到了如何将设计模式分类为不同类型,即创建型、结构型和行为型。

我们接着来看一些常见的设计模式,我们将了解每种类型的作用,并且检视一些我们将如何使用它们的示例。然后,我们将审视本章涵盖的每个设计模式的优缺点,最后看看其中一些模式在 jQuery 库内是如何实际运用的,而不仅仅是在我们自己的代码中。

我认为现在的理论已经足够了;让我们继续并且实践起来。在下一章中,我们将学习如何通过一些技巧来精通表单开发,将你的表单开发技能提升一个档次。

第四章:与表单一起工作

你有多少次在网上购买产品,比如亚马逊之类的?我打赌多年来你已经做了相当多的次数——毕竟,你不能在深夜去书店,浏览书籍,然后选择一本,而不担心商店的关门时间或者不知道你是否会找到一本特定的书。

在线网站构建表单可能是您可能使用 jQuery 的关键领域之一;其成功的关键在于确保它正确验证,作为提供成功用户体验的一部分。

在本章节中,我们将回到基础知识,并深入探讨一些我们可以使用的技术,使用一些 HTML 和 jQuery 验证技巧来验证表单。您还将看到,创建成功的表单并不需要很多复杂的代码,而是同时确保我们考虑了表单的功能要求。

在接下来的几页中,我们将涵盖以下几个主题:

  • 探讨验证的必要性

  • 使用正则表达式添加表单验证

  • 开发一个验证的插件架构

  • 使用 jQuery/AJAX 创建一个高级联系表单

  • 使用 jQuery 开发高级文件上传表单

准备好开始了吗?让我们开始吧……在我们开始之前,我建议你创建一个项目文件夹。为了本章的目的,我假设你已经这样做了,并且它被称为forms

探讨表单验证的必要性

有不同的方法可以改进表单的可用性,但验证无疑是我们应该考虑的最重要的方面之一。你有多少次访问一个网站并填写你的详细信息,只是被告知出现了问题?听起来很熟悉,对吧?

验证表单对于维护信息的一致性至关重要;表单将处理已输入的信息,以确保其正确性。举个例子,以下是一些情况:

  • 如果输入了电子邮件地址,让我们确保它具有有效的格式。电子邮件地址应包含一个句点,并在地址中的某个地方包含一个@符号。

  • 打电话给某人?他们在哪个国家?如果我们已经设置表单以显示已选择国家的字段的特定格式,让我们确保电话号码遵循正确的格式。

我想你已经明白了。现在,这可能听起来好像我们在这里说的是显而易见的事情(不,我没有变疯!),但往往情况是,表单验证被留到了项目的最后阶段。最常见的错误通常是由以下原因造成的:

  • 格式化:这是最终用户在字段中输入非法字符的地方,比如在电子邮件地址中输入空格。

  • 缺少必填字段:你有多少次填写表单,然后发现你没有在必填字段中输入信息?

  • 匹配错误:当两个字段需要匹配但却不匹配时,就会出现这种情况;一个经典的例子是密码或电子邮箱字段。

在这个阶段,你可能会认为我们将被大量的 jQuery 困住,以产生一个全方位的解决方案,对吧?

错了!很抱歉让你失望,但我一直坚持的口头禅是KISS 原则,或者保持简单,蠢货!这并不是对任何人的一种反映,而是为了让我们的设计生活变得更容易一些。正如我在前面的章节中提到的,我相信掌握 jQuery 这样的技术并不总是关于我们产生的代码!

这些是表单验证的关键元素:

  • 告诉用户他们在表单上有问题

  • 向用户显示问题所在的地方

  • 向他们展示一个你期望看到的例子(比如一个电子邮箱地址)

在接下来的几页中,我们将看看如何向表单添加验证以及如何减少(或消除)最常见的错误。我们还将使用颜色和接近性来帮助加强我们的消息。然而,在我们能够进行验证之前,我们需要一些东西来验证,所以让我们快速创建一个表单作为我们练习的基础。

创建一个基本表单

与所有项目一样,我们需要从某个地方开始;在这种情况下,我们需要一个可以作为在本章节中给出的各种示例中添加验证的基础的表单。

在本书的代码下载中,查找并提取basicform.htmlbasicform.css文件到您的项目文件夹;当您运行basicform.html时,它将看起来类似于这个屏幕截图:

Creating a basic form

如果我们看一下使用的标记,我们会发现这并不是什么新鲜事;它包含了我们在创建联系表单时将使用的标准 HTML5 字段,比如文本字段或文本区域:

<form class="contact_form" action="" method="post" name="contact_form">
  <ul>
  <li>
  <label for="name">Name:</label>
  <input type="text" name="username" required>
  </li>
  <li>
  <label for="name">Email:</label>
  <input type="email" name="email" required>
  </li>

  </ul>
  <button class="submit" type="submit">Submit Form</button>
</form>

这里的关键是,我们的例子没有包含任何形式的验证——它让我们完全暴露在垃圾之中,用户可以输入任何东西,我们收到的提交表单会—嗯—是垃圾!在这种情况下,当您点击提交时,您将只会看到这个屏幕截图:

Creating a basic form

不是很好,是吗?大多数桌面浏览器将在使用必填标签时接受任何内容而不进行一些验证,只要有一些东西,表单就会被提交。这个规则的例外是 Safari,它不会显示我们屏幕截图中显示的弹出通知。

我相信我们能做得更好,但可能不是以你期待的方式…感到好奇吗?

从简单的 HTML5 验证开始

表单验证的好处在于它可以很容易或很复杂地修复—这完全取决于我们解决问题的路线。

这里的关键是我们可以使用 jQuery 来提供表单验证;这是一个完全足够的解决方案,可以正常工作。但是,对于字段的简单验证,比如姓名或电子邮件地址,有一种替代方案:HTML5 验证,它使用 HTML5 约束验证 API。

注意

约束验证 API 使用 HTML5 属性,如minsteppatternrequired;这些在大多数浏览器中都可以工作,除了 Safari。

在我解释这个疯狂背后的逻辑之前,让我们快速看看如何修改我们的演示,以使用这种形式的验证:

  1. 在你常用的文本编辑器中打开basicform.html文件的一个副本,然后查找这一行:

    <li>
    <label for="name">Name:</label>
    <input type="text" name="username" required>
    </li>
    
  2. 我们需要添加将用作验证检查的模式,所以继续按照指示修改代码:

    <li>
      <label for="name">Name:</label>
      <input id="name" name="username" value="" required="required" 
      pattern="[A-Za-z]+\s[A-Za-z]+" title="firstnamelastname">
    </li>
    
  3. 我们可以对email字段进行类似的更改,以引入 HTML5 验证;首先,查找这些行:

    <li>
      <label for="email">Email:</label>
      <input type="email" name="email" id="email" required= 
      "required">
    </li>
    
  4. 按照指示修改代码,为email添加 HTML 验证:

    <li>
      <label for="email">Email:</label>
      <input type="email" name="email" id="email" 
      required="required" pattern="[^ @]*@[^ @]*\.[a-zA-Z]{2,}" 
      title="test@test.com">
    </li>
    
  5. 将文件保存为basicvalidation.html;如果你在浏览器中预览结果,你会立即看到一个变化:从简单的 HTML5 验证开始

这已经是一个进步了;虽然文本不太用户友好,但至少你可以看到表单期望看到名字 姓氏的格式,而不仅仅是名字,如所示。类似的变化也将出现在电子邮件中,当你按下提交按钮验证表单时。

提示

如果你仔细查看代码,你可能会注意到我已经开始使用required="required"标签,代替required。任何格式都可以正常工作——如果在你的浏览器中只使用required时出现了任何不一致,你可能会发现使用前一种标签是必要的。

使用 HTML5 而不是 jQuery

现在我们有一个使用 HTML 验证nameemail字段的表单,是时候兑现我的承诺并解释我疯狂背后的逻辑了。

在某些情况下,通常诱人的做法是简单地回归到使用 jQuery 来处理一切。毕竟,如果我们已经在使用 jQuery,为什么要引用另一个 JavaScript 库呢?

如果不是这两个小问题,这似乎是一个合理的方法:

  • 使用 jQuery 会增加网站的负担;对于简单的验证,这可以被视为一种过度投入,收益甚微。

  • 如果 JavaScript 被关闭,那么可能会导致验证无法操作或在屏幕上显示错误或在控制台日志中显示错误。这会影响用户体验,因为访问者将很难提交一个经过验证的表单,或者更糟的是,简单地离开网站,这可能会导致销售额损失。

更好的方法是考虑使用 HTML5 验证来验证标准文本字段,并将 jQuery 的使用保留给更复杂的验证,正如我们将在本章后面看到的那样。这种方法的好处在于,我们将能够完成一些有限的验证,减少对标准字段的 jQuery 依赖,并以更渐进的增强方式使用它。

考虑到这一点,让我们继续,开始查看使用 jQuery 来进一步增强我们的表单,并提供更复杂的验证检查。

使用 jQuery 验证我们的表单

在某些情况下,如果浏览器不支持所使用的输入类型,则使用 HTML5 验证将失败;这是我们需要回到使用 JavaScript 或在本例中使用 jQuery 的时候。例如,日期作为输入类型在 IE11 中不受支持,如下所示:

<input type="date" name="dob"/>

这就是上述代码将如何呈现的方式:

<input type="text" name="dob"/>

麻烦的是,由于类型回退为文本,浏览器将无法正确验证字段。为了解决这个问题,我们可以使用 jQuery 实现一个检查——然后我们可以开始使用 jQuery 添加一些基本的验证,这些验证将覆盖浏览器中进行的现有本地 HTML 检查。

让我们来看看如何在实践中实现其中一些,通过一个简单的演示,如下所示:

  1. 打开本书附带的代码下载中的basicform.html的副本。

  2. <head>部分,添加一个指向 jQuery 的链接以及一个指向您的验证脚本的链接:

    <script src="img/jquery.js"></script>
    <script src="img/basicvalidation.js"></script>
    
  3. 将文件保存为basicvalidation.html。在一个新文件中,添加以下代码——这将执行一个检查,以确保您只验证了email字段:

    $(document).ready(function () {
       var emailField = $("#email");
        if (emailField.is("input") && emailField.prop("type") === "email") {
      }
    });
    
  4. 在关闭}之前的位置,让我们加入两个函数中的第一个;第一个函数将添加一个 CSS 钩子,允许您在成功或失败的情况下进行样式设置:

    emailField.on("change", function(e) {
      emailField[0].checkValidity();
        if (!e.target.validity.valid) {
          $(this).removeClass("success").addClass("error")
        } else {
          $(this).removeClass("error").addClass("success")
        }
    });
    
  5. 你们中间敏锐的人会注意到添加了两个 CSS 样式类;我们需要在样式表中允许这个,所以继续添加这些代码行:

    .error { color: #f00; }
    .success { color: #060; }
    
  6. 现在我们可以添加部分函数,该函数将更改浏览器显示的默认消息以显示自定义文本:

    emailField.on("invalid", function(e) {
      e.target.setCustomValidity("");
      if (!e.target.validity.valid) {
      e.target.setCustomValidity("I need to see an email address 
      here, not what you've typed!");
    }
    else {
      e.target.setCustomValidity("");
    }
    });
    
  7. 将文件保存为basicvalidation.js。如果您现在在浏览器中运行演示,您将看到当您添加一个有效的电子邮件地址时,文本会变为绿色,如下图所示:使用 jQuery 验证我们的表单

  8. 如果您刷新浏览器会话,并且这次不添加电子邮件地址,您将收到一个定制的电子邮件地址错误,而不是浏览器提供的标准错误,如下面的屏幕截图所示:使用 jQuery 验证我们的表单

在这种情况下使用一点 jQuery 让我们能够自定义显示的消息——这是一个使用更友好的东西的好机会。请注意,默认消息与标准 HTML5 验证一起给出的消息可以很容易地……改进!

现在你已经看到了我们如何改变显示的消息,让我们专注于改进表单所进行的检查。标准的 HTML5 验证检查对于所有情况都不够;我们可以通过在我们的代码中引入正则表达式检查来改进它们。

使用正则表达式语句验证表单

到目前为止,你已经看到了可以使用 jQuery 来验证表单的一些命令,以及如何将你的检查限制在特定的字段类型(如电子邮件地址)上,或者覆盖屏幕上显示的错误消息。

但是,如果我们没有一种可以用来检查的验证模板,代码将失败——你们中的敏锐的人可能已经在我们的basicvalidation.html演示中注意到了这一点:

pattern = "[^ @]*@[^ @]*\.[a-zA-Z]{2,}";

pattern变量用于定义正则表达式或regex语句。简单地说,这些是单行语句,指示我们应该如何验证表单中的任何条目。这些并不是专门用于查询的;它们同样适用于任何脚本语言,比如 PHP 或纯 JavaScript。让我们花一点时间看一些示例,以了解这个是如何工作的:

  • [^ @]*: 这个语句匹配任意数量的不是@符号或空格的字符。

  • @: 这是一个字面值

  • \.: 这是一个字面值

  • [a-zA-Z]: 这个语句表示任意字母,无论是大写还是小写

  • [a-zA-Z]{2,}: 这个语句表示两个或更多字母的任意组合。

如果我们把这些放在一起,模式正则表达式转换为一个电子邮件,其中包含任意一组字符,除了一个@符号,紧接着是一个@符号,然后是任意一组字符,除了一个@符号,一个句点,最后至少两个字母。

好了,理论已经够了;让我们开始编码吧!我们将通过一些示例进行工作,首先修改电子邮件验证,然后开发代码以覆盖网站地址的验证。

创建一个用于电子邮件的正则表达式验证函数

我们已经使用了一个正则表达式来验证我们的email地址字段;虽然这样做效果很好,但代码可以改进。我不喜欢在事件处理程序中包含验证检查;我更喜欢将其分离到一个独立的函数中。

幸运的是,这很容易纠正;让我们现在执行以下步骤来解决这个问题:

  1. 我们将首先打开basicvalidation.js文件,并在emailField.on()事件处理程序之前立即添加一个辅助函数:

    function checkEmail(email) {
      pattern = new RegExp("[^ @]*@[^ @]*\.[a-zA-Z]{2,}");
      return pattern.test(email);
    }
    
  2. 此函数处理电子邮件地址的验证;为了使用它,我们需要修改emailField.on()处理程序,如下所示:

    emailField.on("invalid", function(e) {
      e.target.setCustomValidity("");
     email = emailField.val();
     checkEmail(emailField);
      if (!e.target.validity.patternMismatch) {
        e.target.setCustomValidity("I need to see an email address 
        here, not what you've typed!");
    }
    

如果我们保存我们的工作然后在浏览器中预览它,我们应该在验证过程中看不到任何差异;我们可以放心地说,验证检查过程现在已经被分离为一个独立的函数。

进一步进行 URL 验证

使用与前面示例中使用的相同原理,我们可以为urlField字段开发一个类似的验证检查。只需复制两个emailField.on()事件处理程序和checkEmail函数,就可以产生类似下图所示的东西:

深入了解 URL 验证

使用我们已经生成的代码,看看你是否能够创建一个使用这个正则表达式验证网站 URL 输入的东西:

/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/

如果你的代码工作,它应该产生一个类似于这个截图中显示的错误消息:

深入了解 URL 验证

希望你已经成功使用了我们迄今为止产生的代码——如果你遇到了困难,在附带本书的代码下载中有一个可工作的示例。

因此,假设我们有一些可以工作的东西,有人发现我们的代码有问题吗?我们肯定有一些问题需要解决;让我们现在来看看它们:

  • 注意,反馈并非百分之百动态?为了使我们的代码能够识别从错误到成功输入的更改,我们需要刷新我们的浏览器窗口——这一点根本不理想!

  • 我们在 jQuery 文件中重复了很多代码——从架构上看,这是不好的做法,我们肯定可以改进已经编写的内容。

与其复制代码,不如彻底重写我们的 jQuery 为一个快速插件;从架构上来说,这将消除一些不必要的重复,并使我们能够以最小的更改扩展功能。它不会完美——这是我们将在本章稍后纠正的东西——但它会产生比我们现有代码更有效的结果。

构建一个简单的验证插件

到目前为止,我们的示例都是基于个别字段的,比如电子邮件地址或网站 URL。代码大量重复,这导致了冗余且效率低下的解决方案。

相反,让我们完全改变我们的方法,将我们的代码转换成一个通用插件。我们将使用相同的核心流程来验证我们的代码,这取决于插件中设置的正则表达式。

对于下一个练习,我们将使用 Cedric Ruiz 制作的一个插件。虽然它已经有几年了,但它说明了我们如何创建一个单一的核心验证过程,该过程使用一些过滤器来验证我们表单中输入的内容。让我们从执行以下步骤开始:

  1. 从附带本书的代码下载中提取quickvalidate.htmlinfo.pngquickvalidate.css文件的副本,并将它们保存在你的项目文件夹中。

  2. 接下来,我们需要创建一个插件。在一个新文件中,添加以下代码,并将其保存为jquery.quickvalidate.js,保存在项目区域的js子文件夹中:

    $.fn.quickValidate = function() {
      return this;
    };
    
  3. 你需要开始给你的插件添加功能,从缓存表单和输入字段开始;在你的插件的return this语句之前立即添加这个功能:

    var $form = this, $inputs = $form.find('input:text, input:password');
    
  4. 接下来是规定每个字段应如何验证以及在验证失败时应显示的错误消息的过滤器,如下所示:

    var filters = {
      required: {
        regex: /.+/,
        error: 'This field is required'
      },
      name: {
        regex: /^[A-Za-z]{3,}$/,
        error: 'Must be at least 3 characters long, and must only contain letters.'
      },
      pass: {
        regex: /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,}/,
        error: 'Must be at least 6 characters long, and contain at least one number, one uppercase and one lowercase letter.'
      },
      email: {
        regex: /^[\w\-\.\+]+\@[a-zA-Z0-9\.\-]+\.[a-zA-z0-9]{2,4}$/,
        error: 'Must be a valid e-mail address (user@gmail.com)'
      },
      phone: {
        regex: /^[2-9]\d{2}-\d{3}-\d{4}$/,
        error: 'Must be a valid US phone number (999-999-9999)'
      }
    };
    
  5. 现在我们来到验证过程,这是魔术发生的地方。继续添加以下代码,立即在过滤器下方:

    var validate = function(klass, value) {
    
      var isValid = true, f, error = '';
      for (f in filters) {
        var regex = new RegExp(f);
        if (regex.test(klass)) {
          if (!filters[f].regex.test(value)) {
            error = filters[f].error;
            isValid = false;
            break;
          }
        }
      }
    return { isValid: isValid, error: error }
    };
    
  6. 如果您的代码正确识别出错误,您需要通知用户;否则,他们将不知道为什么表单似乎未正确提交。现在让我们通过添加一个函数来确定如果验证测试失败会发生什么,如下所示:

    var printError = function($input) {
      var klass = $input.attr('class'),
      value = $input.val(),
      test = validate(klass, value),
      $error = $('<span class="error">' + test.error + '</span>'),
      $icon = $('<i class="error-icon"></i>');
      $input.removeClass('invalid').siblings('.error, .erroricon').remove();
      if (!test.isValid) {
        $input.addClass('invalid');
        $error.add($icon).insertAfter($input);
        $icon.hover(function() {
          $(this).siblings('.error').toggle();
        });
      }
    };
    
  7. 我们已经确定了当验证过程失败时会发生什么,但尚未采取任何措施调用函数。现在让我们通过根据字段是否标记为必填来添加适当的调用来解决此问题,如下所示:

    $inputs.each(function() {
      if ($(this).is('.required')) {
        printError($(this));
      }
    });
    
  8. 如果我们字段中的内容发生更改,我们需要确定它是有效还是无效;这需要在输入文本时进行,所以现在让我们做这个,使用keyup事件处理程序:

    $inputs.keyup(function() {
    printError($(this));
    });
    
  9. 最后,如果在我们的表单中发现错误,我们需要阻止提交:

    $form.submit(function(e) {
      if ($form.find('input.invalid').length) {
        e.preventDefault();
        alert('There are errors on this form – please check...');
      }
    return false;
    });
    
  10. 保存您的工作;如果一切正常,当在浏览器中预览工作结果时,您应该会看到表单验证:构建一个简单的验证插件

在这个阶段,我们有一个工作正常的插件,我们已经将核心验证代码重构为一组单一的流程,可以应用于每种字段类型(使用适当的过滤器)。

然而,我们可以做得更好;以下是一些问题,我们可以解决以进一步完善我们的代码:

  • 虽然我们已经将代码重构为一组单一的核心验证流程,但过滤器仍然是核心代码的一部分。尽管可以轻松扩展不同类型的过滤器,但我们仍然仅限于文本或密码字段类型。添加任何标准的 HTML5 字段类型,例如urlemail,都会导致错误,因为伪类型不受 jQuery 支持。

  • 从架构的角度来看,最好将验证器过滤器保留在核心插件之外;这有助于保持验证器的简洁,并且不包含我们的目的不需要的代码。

  • 我们的代码不允许任何功能,比如本地化、设置最大长度或验证表单对象,比如复选框。

我们可以花费大量时间开发我们的插件,使其采用更模块化的方法,但这值得吗?目前有数十个可供使用的表单验证插件;更明智的做法将是使用其中一个插件:

  • 核心验证流程经过了反复测试,消除了担心我们的字段是否会正确验证的需要。开发任何形式的验证器插件,使其适用于超过几个字段,都是非常棘手的,要做到完全正确;毕竟,我们要验证还是不验证?不同的语言?例如邮政编码或邮政编码的不同格式?

  • 大多数插件都会有某种架构,允许添加自定义验证器,这些验证器补充了作为标准包含的验证器,例如使用语言、特定的数字格式或奇偶数。在本章的后面,我们将充分利用这一点,以向我们的演示添加一些自定义验证器。

  • 使用现有的插件可以让您专注于提供特定于您的环境的功能,并在您可以添加最大价值的地方添加功能——毕竟,尝试在其他人已经为我们完成工作的地方添加有效性是没有意义的,对吧?

有了这个想法,让我们继续看看如何使用现有的插件。现在大多数插件都具有某种模块化架构,允许您轻松定制它并添加额外的自定义验证器;毕竟,为什么要浪费时间重新发明轮子呢,对吧?

开发验证插件架构

在本章中,我们使用了各种 HTML5 和 jQuery 技术来验证我们的表单。总的来说,它们效果很好,但它们的简单性意味着我们会很快超越它们的有用性。

要充分利用表单验证的所有可能性,最好不要仅仅尝试验证字段,而是使用现有的插件来处理基本的验证过程,并允许您集中精力进行定制,并确保为您的表单提供正确的功能。

输入 jQuery Form Validator。这个插件是由 Victor Jonsson 创建的,已经存在多年,所以经过了测试;它还包含了我们需要定制的模块化架构,以在我们的表单中提供我们将提供的检查。让我们看看验证器的运作方式。

注意

原始插件及相关文档可在 formvalidator.net/ 上找到。

创建我们的基本表单

在我们开始向代码添加自定义验证器插件之前,我们需要一个要验证的基本表单。为此,我们将基于本章早期部分中创建的 basicvalidation.html 中的修改版本的表单的标记。

让我们先让我们的基本表单工作起来,确保标准验证生效。要做到这一点,请执行以下步骤:

  1. 我们将从提取伴随本书的代码下载中的 formvalidator.htmlformvalidator.css 文件的副本开始。将 HTML 文件保存在项目文件夹的根目录中,将 CSS 文件保存在 css 子文件夹中。

  2. 在新文件中,添加以下代码行,并将其保存为 formvalidator.js,保存在项目区域的 js 子文件夹中:

    $(document).ready(function() {
      $.validate();
    });
    
  3. 这就是开始使用 Form Validator 插件所需的全部内容;如果您在浏览器中预览表单,您应该会看到以下截图——如果您输入了一个有效的名称和电子邮件地址但省略了网站 URL:创建我们的基本表单

现在我们的表单已经准备好了,让我们真正开始开发表单内部使用的一些验证器,首先是name字段的新验证器。

创建自定义验证器

到目前为止,我们的表单依赖于使用标准的 HTML5 技术进行验证;这对大多数要求都适用,但它的能力是有限的。接下来是 jQuery;我们可以利用 FormValidator 的功能创建我们自己的自定义验证器,以满足我们自己的要求。

创建自定义验证器的关键部分是 $.formutils.addValidator 配置对象;FormValidator 处理了基本的插件架构,这样你就可以通过设计表单的正确检查来添加值。

在接下来的几页中,我们将通过两个基本示例进行工作:

  1. 我们将从创建自定义验证器开始;在你选择的文本编辑器中,添加以下代码:

    $.formUtils.addValidator({
      name : 'user_name',
      validatorFunction : function(value, $el, config, language, 
      $form) {
        return (value.indexOf(" ") !== -1)
      },
      errorMessage : 'Please enter your full name',
    });
    
  2. 将文件保存为user_name.js,放在项目区域的js子文件夹内。打开之前创建的formvalidator.js文件,并根据下面的示例进行修改:

    $(document).ready(function() {
     $.formUtils.loadModules('user_name');
      $.validate({
     modules: 'user_name'
      });
    });
    
  3. 虽然你已经将验证规则添加到验证器中,但你需要在 HTML 标记内部激活它,如下所示:

    <div class="form-group">
      <label class="control-name" for="name">Name: <span 
      class="asterisk">*</span></label>
      <input name="username" id="username" datavalidation="user_name">
    </div>
    
  4. 如果一切正常,当你在浏览器中预览表单并点击 提交 按钮时,就会看到使用自定义验证器的效果,如下面的截图所示:

创建自定义验证器

在这个阶段,你可以简单地将它留在这个自定义验证器的位置,但我认为有更多的空间——比如电子邮件地址?

标准的 HTML5 验证将会检查电子邮件地址是否符合合适的格式,例如确保它包含@符号,域名后有一个小数点,以及域名后缀是有效的。然而,它不能阻止用户提交具有某些类型地址的表单,比如www.hotmail.com(或现在的www.outlook.com)。

此时值得注意的是使用正则表达式进行电子邮件验证可能会引发一系列问题,所以要谨慎行事并进行彻底的测试——比如如何对mail+tag@hotmail.com进行验证?这是一个完全有效的地址,但大多数正则表达式都会失败……

注意

关于为什么使用正则表达式实际上可能带来更多问题的讨论可以在davidcel.is/blog/2012/09/06/stop-validating-email-addresses-with-regex/上找到。

在我们的示例中,我们将添加一个简单的检查来防止使用 Hotmail、Gmail 或 Yahoo! 的电子邮件地址;让我们看看我们如何做到这一点:

  1. 在文本编辑器中,将以下代码添加到一个新文件中,并将其保存为 free_email.js,放在 js 子文件夹内:

    $.formUtils.addValidator({
      name : 'free_email',
      validatorFunction : function(value, $el, config, language, 
      $form) {
        varemailName = /^([\w-\.]+@(?!gmail.com)(?!yahoo.com)(?!hotmail.com)([\w-]+\.)+[\w- 
        ]{2,4})?$/;
        return (emailName.test(value))
      },
      errorMessage : 'Sorry - we do not accept free email accounts 
      such as Hotmail.com'
    });
    
  2. 现在你的 free_email 验证器已经就位,当验证表单时,你需要调用它;为此,请返回到你在前一个练习中打开的 formvalidator.js 文件,并按照以下示例修改代码:

    $(document).ready(function() {
     $.formUtils.loadModules('free_email');
      $.validate({ modules: 'free_email'});
    });
    
  3. 这个练习的最后一步是从 HTML 标记中激活自定义验证器——还记得我们在上一个练习中是如何改变它的吗?同样的原理在这里也适用:

    <div class="form-group">
      <label class="control-name" for="email">Email: <span 
      class="asterisk">*</span></label>
      <input type="text" name="email" id="email" datavalidation="free_email">
    </div>
    
  4. 保存 formvalidator.jsformvalidator.html 文件;如果你预览你的工作结果,你会清楚地看到,如果你输入了一个无效的电子邮件地址,你的自定义消息会出现,如下面的截图所示:Creating custom validators

现在,你们中间敏锐的人可能会发现我们一次只加载一个验证器;我相信你们肯定想知道如何同时加载多个验证器,对吧?

没问题,我们已经就位了验证器文件,所以我们需要做的就是修改我们的验证器对象,使其加载这两个模块。让我们快速看看如何修改我们的验证器对象:

  1. 打开 formvalidator.js 文件的副本,并按照这里显示的代码进行修改:

    $.formUtils.loadModules('free_email, user_name');
    $.validate({
      modules: 'free_email, user_name',
    });
    

这就是你需要做的一切。如果你保存文件并在浏览器中预览结果,你会发现它会验证 nameemail 字段,就像前两个练习中所示的那样。

这为我们打开了一扇机会之门;在我们的两个示例中,我们创建了相当简单的验证器,但原则是相同的,无论我们的验证器有多么复杂或简单。

注意

如果你想了解更多关于如何创建自定义验证器的信息,那么阅读formvalidator.net/index.html#custom-validators中的文档是值得的。然后我们可以结合创建模块的基本原理和正则表达式示例,例如www.sitepoint.com/jquery-basic-regex-selector-examples/中显示的那些,来创建一些有用的验证器检查。

让我们继续看看 FormValidator 插件的另一个有用部分——我们毕竟不都说同样的语言,是吗?如果我们都说同样的语言,生活会很无聊;相反,你应该考虑本地化你的验证消息,这样国际访问者就可以理解何时出现验证问题以及如何解决它。

本地化我们的内容

在这个现代化的在线工作时代,可能会有一些情况需要以不同的语言显示消息——例如,如果你的大多数访问者说荷兰语,那么用荷兰语的等价物覆盖标准消息将是有价值的。

尽管需要一些仔细的思考和规划,但添加语言支持仍然非常容易;让我们看看如何操作:

  1. 对于这个练习,你需要修改验证器对象。在 formvalidator.js 文件中,在 document.ready() 语句之后立即添加以下代码:

    varmyLanguage = {
      badUrl: "De ingangswaarde is geencorrecte URL"
    };
    
  2. 我们需要引用语言的变化,因此,请继续将此配置行添加到验证器对象中:

    $.validate({
      modules: 'free_email, user_name',
     language: myLanguage
    });
    
  3. 保存文件。如果在浏览器中预览结果,你可以看到错误消息现在以荷兰语显示,如下所示:本地化我们的内容

  4. 我们不限于荷兰语;这里是相同的代码,但是错误消息是法语:

    varmyLanguage = { badUrl: "La valeur d'entrée n'est pas une URL correcte" };
    

这是一个快速简单的方法,可以确保访问您网站的访客了解为什么您的表单未经验证以及他们如何修复它。值得注意的是,无论您的 PC 或移动设备的区域设置如何,设置的消息都会显示;建议您在更改表单消息中使用的语言之前检查任何分析日志以确认您的访问者来自哪个地区或国家。

集中我们的错误消息

在我们结束表单开发之前,还有一个功能可以添加到我们的表单中。

到目前为止,显示的任何验证错误消息都是针对每个单独字段的。这样做可以工作,但意味着我们没有一种立即知道哪些字段可能未通过验证的方法。当然,我们可以滚动浏览表单,但我懒得做这个;如果我们可以修改我们的代码以在顶部显示错误,那为什么还要滚动长表单呢?

绝对,使用 FormValidator 这样做非常简单;现在让我们来看看需要做什么:

  1. 打开 formvalidator.js 文件的副本,并按照此处所示更改验证器对象;我们将 errMessagePosition 属性设置为 top,将 validatorOnBlur 属性设置为 false,以便在表单顶部显示消息:

    $.validate({ modules: 'user_name, free_email', validateOnBlur: false, errorMessagePosition : 'top', language: myLanguage});
    
  2. 如果现在运行表单,设置的任何错误消息都会显示在顶部,但它们看起来不太好看。现在,让我们通过对样式表进行一些微小的更改来修复这个问题:

    div.form-error { 
      font-size: 14px; 
      background: none repeat scroll 0 0 #ffe5ed;
      border-radius: 4px; color: #8b0000;
      margin-bottom: 22px; padding: 6px 12px;
      width: 88%; margin-left: 0px; margin: 10px;
    }
    
  3. 现在,让我们在浏览器中运行表单;如果一切顺利,你将看到顶部的错误已经正确格式化的表单。以下截图显示了如果你不填写网站 URL 会出现什么;请注意,我们的代码仍然显示了前一个示例中的荷兰语消息:集中我们的错误消息

到目前为止,我们已经涵盖了一些与使用 jQuery 进行验证相关的主题。我们将继续并查看一下几个示例表单的操作。在我们这样做之前,我们需要完成一些最后的调整作为开发的结束部分。

结束开发

在预览最后一个练习时,更加细心的人会注意到一些样式似乎缺失了。这有一个很好的理由;让我解释一下。

作为最低要求,我们可以提供指示成功或失败的消息。这样做是有效的,但不是很好;更好的选择是提供一些额外的样式来真正突出我们的验证:

结束开发

这很容易做到,所以让我们从执行以下步骤开始:

  1. 打开formvalidator.css文件并添加以下代码行:

    .has-error, .error { color: #f00; }
    .has-success, .valid { color: #060; }
    .error { background-image: url(../img/invalid.png); background-position: 98%; background-repeat: no-repeat; background-color: #ff9a9a; }
    .valid { background-image: url(../img/valid.png); background-position: 98%; background-repeat: no-repeat; background-color: #9aff9a; }
    
  2. 我们需要将两个图标添加到项目区域的img子文件夹中——为此,我使用了位于www.iconfinder.com/icons/32520/accept_check_good_green_ok_success_tick_valid_validation_vote_yes_icon的红叉和绿勾图标。如果您想使用不同的图标,则可能需要相应调整样式规则。

  3. 保存formvalidator.css。如果在浏览器中预览结果并在表单中输入详细信息,则在单击提交表单时,您应该看到与本练习开始时显示的屏幕截图类似的结果。

希望您会同意这看起来好多了!在伴随本书的代码下载中有一个formvalidator.css的副本;它包含了在表单中为其赋予非常精致外观的一些额外样式,正如我们在这个练习中所见。

提示

如果您想看到包含自定义内容的工作示例,请从代码下载中提取formvalidatior-fullexampleJavaScript、CSS 和 HTML 文件,并将它们分别重命名为formvalidator.jsformvalidator.cssformvalidator.html

注意最佳实践的使用

在我们的每个示例中,我们都将表单设置为一次显示所有字段——考虑到用户的目标和期望是一个关键点。他们试图实现什么?我们真的需要一次显示几十个字段吗?或者,我们可以使表单更简单吗?

尽管本书的重点自然是掌握 jQuery,但仅仅集中于编写代码是愚蠢的;我们还必须在构建表单及其相关验证时考虑一些外观和功能方面的问题。

举一个小例子,值得考虑的是,当字段可用时,我们是否可以使用 CSS 来模糊或聚焦字段。我们可以使用少量 CSS 来模糊或聚焦这些字段,类似于以下代码:

input[type=email], input[type=text] { filter: blur(3px); opacity: .4; transition: all .4s; }

input[type=email]:focus, input[type=text]:focus { filter: none; opacity: 1; }

这里的想法是淡化我们已输入内容的字段,并将注意力集中在我们尚未完成或即将完成的字段上,如下面的屏幕截图所示:

注意最佳实践的使用

一个小警告:如果我们在使用此样式时不小心,可能会导致字段失效,这将破坏练习的整个意义!现在让我们改变焦点,转向表单设计的一个关键部分:如果某些浏览器不支持我们在本章中使用的 CSS 样式会发生什么?

提供后备支持

在本章中,我们在设计大多数现代浏览器中可用的表单方面进行了尝试。尽管如此,在某些情况下,这可能不起作用;如果我们仍然必须迎合不支持的浏览器(例如 iOS7),那么我们需要提供某种形式的后备方案。

幸运的是,如果我们使用类似 Modernizr 这样的工具,在html元素上应用formvalidation类,以提供一种优雅的降级,那么这不会成为太大的问题。然后,我们可以利用这一点,在浏览器不支持伪选择器(例如:valid:invalid)的情况下提供优雅的后备方案。

小贴士

如果你想使用自定义版本的 Modernizr,该版本将测试表单验证支持,请访问modernizr.com/download/#-shiv-cssclasses-teststyles-testprop-testallprops-prefixes-domprefixes-forms_validation-load

足够的理论了,让我们来点乐子吧!在接下来的几页中,我们将通过两个练习来看一个更复杂的例子。它将基于一个简单的联系表单,我们将在其中添加表单上传功能——尽管请注意,这里会有一个陷阱……!

使用 AJAX 创建一个高级联系表单

在我们复杂示例的第一部分中,我们将开发一个表单,允许我们提交一些基本详细信息,并且允许首先出现在表单消息面板上,然后通过电子邮件进行确认。

对于本练习,我们将需要使用一些工具,如下所示:

小贴士

另一个可能的选项,如果你更喜欢走跨浏览器的路线,是 XAMPP (www.apachefriends.org/index.html);这包括 Mercury Mail Transport 选项,因此如果你在 Windows 上工作,则不需要 Test Mail Server 工具。

好的,工具就位了,让我们开始执行以下步骤:

  1. 我们将从打开此书附带的代码下载的副本并提取 ajaxform 文件夹开始;这包含了我们演示的标记、样式和各种文件。我们需要将该文件夹保存到 Web 服务器的 WWW 文件夹中,对于 PC 来说,通常是 C:\wamp\www

  2. 标记相对简单,与本章中已经见过的内容非常相似。

  3. 我们需要对 mailer.php 文件进行一个小修改;用你选择的文本编辑器打开它,然后找到这一行:

            $recipient = "<ENTER EMAIL HERE>";
    
  4. <ENTER EMAIL HERE> 更改为你可以使用的有效电子邮件地址,以便检查之后是否出现了电子邮件。

  5. 这个演示的魔法发生在 ajax.js 文件中,所以现在让我们看看这个文件,并开始设置一些变量:

    $(function() {
      var form = $('#ajaxform');
      var formMessages = $('#messages');
    
  6. 当提交按钮被按下时,我们开始真正的魔法;首先我们阻止表单提交(因为这是默认操作),然后将表单数据序列化为字符串以便提交:

    $(form).submit(function(e) {
      e.preventDefault();
      var formData = $(form).serialize();
    
  7. 接下来是这个表单的 AJAX 操作的核心;这个函数设置请求类型,发送内容的位置以及要发送的数据:

    $.ajax({ 
      type: 'POST',
      url: $(form).attr('action'),
      data: formData
    })
    
  8. 然后我们添加两个函数来确定应该发生什么;第一个函数处理我们表单的成功提交:

    .done(function(response) {
      $(formMessages).removeClass('error');
      $(formMessages).addClass('success');
      $(formMessages).text(response);
    
      $('#name').val('');
      $('#email').val('');
      $('#message').val('');
    })
    
  9. 失败提交表单后的处理函数如下:

    .fail(function(data) {
          $(formMessages).removeClass('success');
          $(formMessages).addClass('error');
    
          if (data.responseText !== '') {
            $(formMessages).text(data.responseText);
          } else {
            $(formMessages).text('Oops! An error occurred and your message could not be sent.');
           }
         });
       });
     });
    
  10. 启动电子邮件工具。如果你在浏览器中预览表单并填写一些有效的细节,当你提交它时,你应该会看到这个截图:使用 AJAX 创建高级联系表单

我们的表单现在已经就位,并且能够提交,在几秒钟内通过电子邮件确认。我们将在下一章中更深入地重新讨论在 jQuery 中使用 AJAX 的用法;现在,让我们继续开发我们的表单。

使用 jQuery 开发高级文件上传表单

正如一位善良的人不久前所说的那样,"前进和上升!",现在是时候添加我们表单功能的第二部分了,以文件上传选项的形式。

不考虑这可能带来的风险(如上传病毒),添加文件上传功能相对简单;它需要客户端和服务器端组件才能正常工作。

在我们的示例中,我们将更多地关注客户端功能;为了演示目的,我们将文件上传到项目区域内的一个虚拟文件夹。为了让你了解我们将构建的内容,这是一个完成示例的截图:

使用 jQuery 开发高级文件上传表单

有了这个想法,让我们开始执行以下步骤:

  1. ajaxform.html文件的副本中,我们需要向各种 JavaScript 和 CSS 文件添加一些额外的链接;所有这些添加都在随书附带的代码下载中可用,如下所示:

    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/styles.css">
    <link rel="stylesheet" href="css/fileupload.css">
    <script src="img/jquery.min.js"></script>
    <script src="img/ajax.js"></script>
    <script src="img/jquery.ui.widget.js"></script>
    <script src="img/jquery.iframe-transport.js"></script>
    <script src="img/jquery.fileupload.js"></script>
    <script src="img/uploadfiles.js"></script>
    
    
  2. 接下来,我们需要在index.html中添加一些标记;所以,在ajaxform.html中,首先按照这里显示的更改标题:

    <div id="formtitle">
      <h2>File Upload Demo</h1>
    </div>
    <div id="form-messages"></div>
    
  3. 现在我们需要添加文件上传代码;所以,在消息字段的结束</div>标签之后,立即添加以下代码:

    <div class="container">
      Click the button to select files to send:
      <span class="btnbtn-success fileinput-button">
      <span>Select files...</span>
      <input id="fileupload" type="file" name="files[]" multiple>
      </span>
      <p>Upload progress</p>
      <div id="progress" class="progress progress-success 
      progress-striped">
      <div class="bar"></div>
      </div>
      <p>Files uploaded:</p>
      <ul id="files"></ul>
    </div>
    
  4. 我们需要对我们的一个 jQuery 文件进行一点小小的修改;在uploadfiles.js中,找到以下开头的行:

    $('#files').append('<li><img src="img/div>Now, amend the highlighted part to match the location of where you are hosting the files within your local web server. The `files` and `thumbnail` folders are created automatically by the scripts.
    
  5. 要完成我们的演示,我们需要两个额外的文件:一个是files.php,另一个是UploadHandler.php;这些文件将处理文件的后端上传和电子邮件的发送。这些文件的副本都在随书附带的代码下载中。

  6. 保存所有文件。如果使用本地 Web 服务器预览结果,则应该看到一个可用的表单,类似于此练习的第一部分;现在它还将显示您的上传表单。

注意

在随书附带的代码下载中的combined文件夹中有一个演示的工作版本。

此时,我们应该有一个可用的表单。但是等等……有没有人注意到什么?希望在这一点上,你应该已经注意到我们的表单中几乎没有验证了……!“嗯?”我听到你问。你完全正确,但是像所有好事一样,这也是有原因的。

我在这里故意没有添加任何验证,因为我想首先集中精力让表单功能正常运行,并最终从验证中小休一下,考虑到我们在过去几页中已经涵盖了很多内容。

然而,这确实为你提供了一个绝佳的机会(是的,这里有个陷阱)——要不要检查一下你是否能为示例添加验证?我在代码下载中没有提供答案——毕竟,并不存在绝对正确或错误的答案;验证取决于你表单的需求。不过,在本章中应该有足够的内容让你开始。我强烈建议你查看主网站formvalidator.net上的文档,因为它会提供更多答案!

摘要

哦!我们终于到达了本章的末尾,所以让我们花点时间回顾一下我们到目前为止所涵盖的内容。

我们从为什么表单验证很重要以及关键点是保持任何解决方案简单以确保表单成功开始。我们先看了简单的 HTML5 验证,然后讨论了何时使用 jQuery 代替标准 HTML5 验证的优点。

然后,我们开始了解使用 jQuery 进行简单验证,然后扩展到覆盖正则表达式语句的使用。接下来,我们看了一下开发一个快速而肮脏的验证插件,然后审查了保持更模块化架构以帮助通过使用自定义验证器来实现可扩展性的需求。

我们转而使用现有的插件来添加验证功能,因为这样可以让我们花更多时间确保我们满足我们的需求,而不仅仅是能够验证任何内容的简单优点。我们还研究了如何本地化我们的内容,并集中显示错误消息,然后在开发结束前进行了一些额外的样式调整。

然后,我们在章节结尾部分提出了最佳实践和提供备用支持的说明,然后以开发复杂表单的基础作为结束,文件上传功能作为使用本章提供的一些技术进行未来个人发展的基础。

在下一章中,我们将扩展一个我们在表单开发中简要介绍过的主题;是时候来看看那个经得起考验的技术,叫做 AJAX…

第五章:集成 AJAX

一个问题 - DeferredsPromises和/ˈeɪdʒæks/有什么共同点?

答案很简单 - 至少其中两个是工作与 AJAX 的实用程序;第三个实际上是 AJAX 的国际音标拼写。

在互联网的历史上,我们作为最终用户被迫忍受页面刷新 - 您必须强制刷新页面才能显示新内容。现在不一样了 - 我们可以在不需要清除页面的情况下刷新内容,利用了 AJAX 的力量,但并非没有问题。进入延迟和承诺 - 不适合初学者,但一旦了解其工作原理就是有用的工具!

在接下来的几页中,我们将研究与使用 jQuery 进行 AJAX 相关的一些主题。这些主题包括:

  • 详述 AJAX 最佳实践

  • 通过静态站点提高数据加载速度

  • 使用回调处理多个 AJAX 请求

  • 使用 jQuery Deferreds 和 Promises 增强您的代码

  • 看 Deferreds 和 Promises 的实际效果

让我们开始吧...!

重新审视 AJAX

有没有人还记得点击链接或按钮会强制刷新页面的日子,无论我们在哪个网站上?那些日子,当您知道要订购披萨外卖或从在线书店订购书籍时,意味着要点击许多页面...真无聊!

幸运的是,AJAX 在 2006 年作为一种标准的出现已经结束了这种需要。今天,我们不再受到完全刷新页面的限制;我们可以轻松地异步更新页面的内容。好吧 - 所以我们不需要刷新页面,但是...AJAX?那不是上个世纪的某种老技术,早就消亡了吗?

答案是否定的 - 尽管 jQuery 的魔力使我们能够异步更新页面,但 AJAX 仍然起着重要的作用。互联网的惊人崛起意味着我们会有需要从另一个站点获取页面的情况。众所周知,大多数浏览器中的安全设置将阻止对内容的访问,如果违反了跨域资源共享CORS)策略,如下图所示:

重新审视 AJAX

为了规避与 CORS 相关的安全控制,我们可以使用 AJAX。在我们涉足更复杂的用法之前,让我们花点时间回顾一下 AJAX 是什么,以及我们如何在实践中使用它。

注意

要看到这种效果的实际情况,您可以运行本书附带的代码下载中提供的ajaxblocked演示。

定义 AJAX

如果您花费了一些时间使用 AJAX,毫无疑问您会遇到一种或多种变体,例如 AHAH、AJAH 或 AJAJ,它们使用相似的原理。但数据的交换有所不同;例如,AJAH 使用 HTML,AJAJ 使用 JSON,而 AHAH 是基于 HTML 的。

无论使用哪种变体,这组相互关联的技术可能已经成熟,但它仍然在向最终用户呈现数据方面发挥着重要作用。在本章的整个过程中,我们将与可以说是它们中的鼻祖的 AJAX 一起工作。作为提醒,它由以下各个技术组成:

  • 用于演示的 HTML(或 XHTML)和 CSS

  • 用于动态显示和与数据交互的文档对象模型DOM

  • 用于数据的交换和操作的 XML 和可扩展样式表语言转换XSLT

  • 用于异步通信的 XMLHttpRequest 对象

  • JavaScript 将这些技术结合起来

当在 jQuery 中使用时,我们可以使用 $.ajax 对象轻松配置 AJAX。有许多可用的选项,但我们可能经常使用的一些包括:

配置选项 目的
url 请求的内容的 URL。
data 要发送到服务器的数据。
error 在请求失败时调用此函数 - 函数将传递三个参数:jqXHR 对象,描述错误的字符串以及(如果生成了一个)可选异常对象。
dataType 这描述了您期望从服务器返回的数据类型。默认情况下,jQuery 将尝试自动解决此问题,但它可能是以下之一:XML、JSON、脚本或 HTML。
Success 如果请求成功,则调用的函数。
type 发送的请求类型,例如,'POST','GET' 或 'PUT' - 默认为 'GET'。

提示

还有许多其他选项可用。作为提醒,值得浏览 api.jquery.com/jQuery.ajax/ 以获取更多详细信息。

足够的理论 - 至少暂时够了!让我们继续并看看如何使用 AJAX 和 jQuery 开发一个示例。

使用 AJAX 创建一个简单的示例

在我们开始编写代码并推动我们所能做的边界之前,让我们花一点时间了解典型的 AJAX 代码在实际中是什么样子的。

在依赖导入内容的典型应用程序中,我们可能会遇到类似以下摘录的内容:

var jqxhr = $.ajax({
  url: url,
  type: "GET",
  cache: true,
  data: {},
  dataType: "json",
  jsonp: "callback",
  statusCode: {
    404: handler404,
    500: handler500
  }
});
jqxhr.done(successHandler);
jqxhr.fail(failureHandler);

这是一个用于 AJAX 启用代码的标准配置对象。让我们更详细地看看其中一些配置选项:

选项 注释
url URL 的
type 默认为 GET,但如果需要,可以使用其他动词代替
cache 默认为 true,但对于 'script''jsonp' 数据类型为 false,因此必须根据具体情况进行设置
data 任何请求参数都应设置在 data 对象中
datatype 应将 datatype 设置为将来参考
jsonp 只需指定此项以匹配你的 API 期望的 JSONP 请求的回调参数的名称,这些请求是对托管在不同域中的服务器发起的
statusCode 如果您想处理特定的错误代码,请使用状态代码映射设置

提示

jQuery Core 站点上有大量文档 - 值得一读!一个好的起点是主要的 ajax() 对象,位于 api.jquery.com/jQuery.ajax/

我们可以将其用于生成一个简单的演示,比如从 XML 文件或者甚至是纯 HTML 中显示信息,如下一张截图所示:

使用 AJAX 创建一个简单示例

让我们更详细地看看这个演示:

  1. 从随书附带的代码下载中,提取 basicajax.htmlcontent.htmlbasicajax.css 文件的副本 - 将 HTML 文件放入我们项目文件夹的根目录,样式表放入 css 子文件夹。

  2. 接下来,将以下代码添加到一个新文件中,将其保存为 basicajax.js,并将其放入我们项目区域的 js 子文件夹中:

    $(document).ready(function(){
    });
    
  3. 在声明的 $description 变量的下方,添加以下辅助函数来控制我们在屏幕上提取的文本的呈现:

       var displaytext = function(data) {
         var $response = $(data), $info = $("#info");
         var $title = $('<h1>').text($response.find(".title") .text());
         $info.append($title);
         $response.find(".description").each(function(){
           $(this).appendTo($info);
         });
       };
    
  4. 接下来是我们的 jQuery 代码的核心部分 - 对 $.ajax 的调用。立即在辅助函数下面添加以下事件处理程序:

    $('#action-button').click(function() {
      $.ajax({
        url: 'content.html',
        data: { format: 'html' },
        error: function() {
            $('#info').html('<p>An error has occurred</p>');
        },
        dataType: 'html',
        success: displaytext,
        type: 'GET'
      });
    });
    
  5. 如果我们在浏览器中预览结果,我们可以看到点击按钮时内容出现,就像在这个演示开始时所示的截图中一样。

在这个实例中,我们创建了一个简单的演示。它首先引用了 content.html 文件,使用 HTML 格式将其导入到我们的页面中。我们的 jQuery 代码然后提取内容并将其分配给 $response,然后首先提取标题,然后每个段落,并将它们附加到 #info div 中。

此时值得注意的是,我们可以使用类似以下语句引用每个提取的段落:

var $description1 = $('<p>').text($response.find(".description:eq(0)").text());

然而,这是提取文本的一种低效方式 - 我们必须多次运行代码来引用后续的值,这会给我们的服务器带来不必要的负载。

提高静态站点加载数据的速度

现在我们已经看到了一个 AJAX 示例的实际操作,也许让你惊讶的是,即使在我们在屏幕上显示的少量文本中,使用的代码并不 技术上 是尽可能高效的。

嗯?我听到你在问 - 我们真的不能改进这样一个简单的演示吗?好吧,尽管可能有些奇怪,但我们确实可以做出改进。让我们看看我们可以使用哪些技巧来减少代码中的任何缓慢 - 并不是所有的技巧都只是简单地改变我们的代码:

  • 减少 AJAX 请求的数量 - 不,我没有疯掉;改善我们的代码并不总是意味着改变代码本身!如果我们考虑每次 AJAX 请求何时发出,可能会有机会减少数量,如果重新排序意味着我们可以达到相同的结果。例如,如果我们有一个基于定时器发出的 AJAX 请求,我们可以设置一个标志来指示仅在进行更改时才执行 AJAX 请求。

  • 如果我们需要获取内容,那么简单地使用 GET 往往更有效,而不是 POST - 前者只是检索内容,而后者会导致服务器反应,例如更新数据库记录。如果我们不需要执行操作,则使用 GET 完全足够了。

  • 当更新页面内容时,请确保只更新少量内容;如果我们的页面设置为更新大量内容而不是定义的部分,则会影响 AJAX 性能。

  • 减少要传输的数据量 - 记得我说过我们的代码可以做出改变吗?这就是我们可以做出改变的地方 - 虽然我们不需要限制我们检索的内容,但我们可以从使用 HTML 格式更改为纯文本。这允许我们删除标记标签,从而减少我们的内容。当然,我们也可以完全反向,转而使用 XML,但这不会不增加数据大小!

  • 我们还应该检查我们的服务器是否已正确配置 - 要检查的两个关键领域是使用 ETags(或实体标签),以及服务器是否设置为发送正确的过期或 Cache-Control 头用于提供的内容,如下例所示:通过静态网站改善数据加载速度

  • 简而言之,如果服务器检测到某个 URL 的 ETags 没有更改,它将不会发送任何响应。

    小贴士

    如果您想了解更多关于 ETags 及其在浏览器中的工作方式,请访问 en.wikipedia.org/wiki/HTTP_ETag

  • 我们可以通过在正确的时间仅创建和销毁 XMLHttpRequest 来进一步限制 AJAX 请求的影响 - 如果它们只在某些情况下需要,那么这将对 AJAX 性能产生显著影响。例如,如果我们的代码没有活动类,我们可能只会启动 AJAX 请求:

    if (!($this).hasClass("active")) {
    ...perform our ajax request here...
    }
    
  • 确保您的回调函数设置正确 - 如果我们的代码已更新,那么我们需要告诉我们的用户,并且不要让他们等待;毕竟,我们最不想做的事情就是陷入回调地狱的陷阱!(本章后面我们将更详细地介绍这个问题。)

我们甚至可以进一步!我们可以通过缓存内容来减少对服务器的不必要调用。但是,在你说“我知道”的之前,我还没说在哪里呢!

是的——在这种情况下,where 是关键,而 where 是 - localStorage。这是内置在每个浏览器中的,可以用来消除不断击中服务器的需要。虽然您可以存储的量因浏览器而异(通常为 5 MB,但最高可达 20 MB),但对于每个浏览器,它使用相同的原则工作——内容必须存储为文本,但可以包括图像和文本(在合理范围内!)。

有兴趣吗?使用一个简单的插件并对代码进行一些小修改,我们可以快速实现一个可行的解决方案——让我们重新审视一下我们之前的基本 AJAX 演示,并立即进行这些更改。

使用 localStorage 缓存 AJAX 内容

使用 AJAX 需要仔细考虑——重要的是在适当的点获取正确数量的内容,而不是对服务器进行太多不必要的请求,这一点很重要。

我们已经看到了一些可以帮助减少 AJAX 请求影响的技巧。其中一种更为冒险的方式是将内容存储在每个浏览器的 localStorage 区域中——我们可以使用 AJAX 预过滤器来实现这一点。开发者 Paul Irish 已经将完成此操作所需的代码封装在一个插件中,该插件可在 github.com/paulirish/jquery-ajax-localstorage-cache 获取。

我们将使用它来修改我们之前的 basicajax 演示。让我们看看我们将如何做到这一点:

  1. 让我们从随附本书代码下载中提取 basicajax 演示文件夹的副本,并将其保存到我们的项目区域。

  2. 接下来,我们需要下载插件——这可以在 github.com/paulirish/jquery-ajax-localstorage-cache/archive/master.zip 获取。从 zip 文件中提取 jquery-ajax-localstorage-cache.js,并将其保存到 basicajax 中的 js 子文件夹中。

  3. 我们需要对 JavaScript 和 HTML 标记进行一些更改。让我们首先更改 JavaScript。在 basicajax.js 中,按如下所示添加以下两行:

     localCache: true,
       error: function() {
     cacheTTL: 1,
    
    
  4. basicajax.html 中,我们需要引用新的插件,因此继续修改脚本调用,如下所示:

      <script src="img/basicajax.js"></script>
      <script src="img/jquery-ajax-localstorage-cache.js"></script>
    </head>
    
  5. 如果我们重新运行演示并点击按钮加载内容,从视觉上不应该看到任何不同;如果我们启动 Firebug,切换到 Net 选项卡,然后点击 JavaScript,则更改将显而易见:使用 localStorage 缓存 AJAX 内容

  6. 如果我们进一步探索,现在我们可以看到我们的 AJAX 内容被存储在浏览器的 localStorage 区域中的迹象:使用 localStorage 缓存 AJAX 内容

    提示

    如果您想要查看所有 localStorage 设置,请尝试下载并安装 FireStorage Plus! 插件,该插件可以从 addons.mozilla.org/en-US/firefox/addon/firestorage-plus/ 获取。

现在我们可以使用 jQuery 和localStorage.getItemlocalStorage.clearItem方法来操纵该区域中的所有内容。如果你想了解更多,可以参考我的书HTML5 Local Storage How-to,该书可在 Packt Publishing 上获得。

注意

本书附带的代码下载中有这段代码的可运行版本,位于basicajax-localstorage文件夹中。

也许有时候你会发现你想将缓存 TTL 值减少到几分钟(甚至几秒钟?)。你可以通过修改jquery-ajax-localstorage-cache.js中的第 70 到 72 行,并删除一个乘数来实现,留下以下内容:

if ( ! ttl || ttl === 'expired' ) {
  localStorage.setItem( cacheKey  + 'cachettl', +new Date() + 1000 * 60 * hourstl );
}

让我们改变方向。我们先前提到当处理 AJAX 时,提高性能的一种方法是确保我们尽量减少请求的数量。如果我们的代码包含多个请求,将对性能产生不利影响,特别是如果我们必须等待每个请求完成后才能开始下一个请求。

我们可能会使用 localStorage 来减少影响,通过在浏览器内请求内容,而不是从服务器请求;这样做是可行的,但可能不适用于每种类型的请求。相反,正如我们将在后面看到的,有更好的替代方法可以轻松处理多个请求。让我们更详细地探讨一下这个问题,首先从使用回调来处理多个请求的影响开始。

使用回调处理多个 AJAX 请求

当使用 AJAX 时,我们可以使用$.Callbacks对象来管理回调列表—可以使用callbacks.add()方法添加回调,使用.fire()触发,使用.remove()方法移除。

如果我们决定在需要的时候才出现内容,而不是一直存在的情况下,通常我们会启动一个单一的 AJAX 请求。这样做没有错—这是一种完全有效的工作方式,可以减少页面刷新的需求。

但是,如果我们决定必须同时执行多个请求,并且需要每个请求都完成后才能继续,那么情况将变得混乱。

// Get the HTML, then get the CSS and JavaScript
$.get("/feature/", function(html) {
  $.get("/assets/feature.css", function(css) {
    $.getScript("/assets/feature.js", function() {

      // All is ready now, so...add CSS and HTML to the page
      $("<style />").html(css).appendTo("head");
      $("body").append(html);
    });
  });
});

我们可能需要等一会儿!

这里的问题是当处理多个请求时响应速度很慢,尤其是如果所有请求都必须在我们继续之前完成。我个人肯定不想等待一个响应速度慢的页面完成!

为了避免许多人喜欢称之为回调地狱的情况,我们可以使用另一种方法——jQuery 的 Deferreds 和 Promises。这可以被视为一种特殊形式的 AJAX。在接下来的几页中,我们将深入探讨这项技术的奥秘,并通过一个简单的示例来演示,你可以将其作为将来开发自己想法的基础。

提示

甚至有一个专门讨论回调地狱的网站—你可以在callbackhell.com/上查看它—绝对值得一读!

让我们看看 Deferreds 和 Promises 如何在 jQuery 中工作,以及如何使用它来增强我们的代码。

用 jQuery Deferreds 和 Promises 增强你的代码

尽管 Deferreds 和 Promises 听起来像是一种相对新的技术,但它们早在 1976 年就已经可用了。简而言之:

  • Deferred 代表了一个尚未完成的任务

  • Promise 是一个尚未知晓的值

如果我们必须使用标准 AJAX,那么我们可能必须等待每个请求完成才能继续。这在使用 Deferreds / Promises 时是不必要的。当使用 Deferreds / Promises 时,我们不必等待每个请求被处理。我们可以通过jQuery.Deferred()对象排队多个请求以同时触发它们,并单独或一起管理它们,即使每个请求可能需要不同的时间来完成。

如果您的应用程序使用了 AJAX 启用的请求,或者可能受益于使用它们,那么花时间熟悉 Deferreds 和 Promises 是值得的。

在使用标准 AJAX 时,一个关键的缺陷是缺乏来自任何 AJAX 调用的 标准 反馈 - 很难判断何时完成了某事。jQuery AJAX 现在创建并返回一个 Promise 对象,该对象将在与之绑定的所有操作都完成时返回一个 promise。使用 jQuery,我们会使用when()then()fail()方法来实现以下方式:

$.when($.get("content.txt"))
  .then(function(resp) {
    console.log("third code block, then() call");
    console.log(resp);
  })
  .fail(function(resp) { console.log(resp); });

我们可以使用以下图表来表示使用 Deferreds 和 Promises 的工作原理:

用 jQuery Deferreds 和 Promises 增强你的代码

使用 Deferreds 的关键好处是我们可以开始链接多个函数,而不仅仅是一次调用一个函数(这是标准 AJAX 的情况)。然后,我们可以从jQuery.Deferred列表内部.resolve().reject()单个 Deferreds,并使用.success().fail()error() 事件处理程序提供一致的机制来确定如果 Deferreds 成功或失败应该发生什么。

最后,我们可以调用.done()事件处理程序来确定在与我们的 Promise 绑定的操作完成后应该发生什么。

注意

如果您想了解更多关于 Deferreds 和 Promises 的内部工作原理的信息,请查阅 github.com/promises-aplus/promises-spec,尽管它可能会让人感觉有些枯燥(无意冒犯!)。

现在我们已经了解了 Deferreds 和 Promises 的基础知识,让我们改变一下方向,看看如何在实际中使用这两者,并概述为什么值得花时间了解它们背后的概念。

使用 Deferreds 和 Promises

切换到使用 Deferreds 和 Promises 需要一些时间,但值得花费精力去理解它们的工作原理。为了感受使用 Deferreds 和 Promises 的好处,让我们来看看将它们融入我们的代码中的一些优势:

  • 更清晰的方法签名和统一的返回值:我们可以将决定任何请求结果如何处理的代码分离出来,这样读起来更清晰,如果需要,还可以进行链式调用,如下所示:

      $.ajax(url, settings);
      settings.success(data, status, xhr);
      settings.error(data, status, errorThrown);
      settings.always(xhr, status)
    
  • 容易组合在一起:我们不需要在每个请求中加入复杂的函数来管理处理;这意味着启动每个请求所需的核心代码大大简化,如下面的示例所示:

      function getEmail(userEmail, onSuccess, onError) {
         $.ajax("/email?" + userEmail, {
           success: onSuccess,
           error: onError
         });
       }
    
  • 容易链式调用语句:Deferred / Promise 的架构允许我们将多个事件处理程序链接在一起,这样我们就可以通过单个操作触发多个方法,如下所示:

      $("#button").clickDeferred()
         .then(promptUserforEmail)
         .then(emailValidate)
    
  • Promises 总是异步执行:它们甚至可以在我们不知道哪些回调函数将使用 Promises 生成的值之前就被触发,而不管任务是否完成。Promises 将存储生成的值,我们可以从现有的回调函数中调用该值,或者在生成 Promise 后添加任何回调函数时调用。

  • 异常式错误传递:通常在 AJAX 中,我们需要使用一系列的 if…then…else 语句,这样做会使工作方式复杂(有时还会脆弱)。使用 Promises,我们只需链式连接一个或多个 .then() 语句来处理任何结果,如下所示:

    getUser("Alex")
      .then(getFriend, ui.error)
      .then(ui.showFriend, ui.error)
    

    提示

    关于 Promises,有很多内容我们无法在这里详细介绍。关于将 Promises 与标准 AJAX 请求进行比较的有用讨论,请查看此处的讨论

还记得我们之前审查的关于使用回调函数处理多个 AJAX 请求的代码吗?使用多个回调函数的主要缺点是结果混乱(最终影响了我们网站的性能)——显然我们需要一个更好的替代方案!

Deferreds 和 Promises 的美妙之处在于它允许我们重构代码,使其更易于阅读。这不仅包括我们需要作为请求的一部分运行的命令,还包括成功或失败时发生的情况。让我们重新审视一下之前的代码片段,并看看当重写为使用 Deferreds / Promises 时会是什么样子:

$.when(
  // Get the HTML, CSS and JS
  $.get("/feature/", function(html) {
    globalStore.html = html;
  }),
  $.get("/assets/feature.css", function(css) {
    globalStore.css = css;
  }),
  $.getScript("/assets/feature.js")
).then(function() {
  // All is ready now, so...add the CSS and HTML to the page
  $("<style />").html(globalStore.css).appendTo("head");
  $("body").append(globalStore.html);
});

希望您会同意,它看起来明显更清晰了,现在我们可以从单个过程中运行多个请求,而不必等待每个请求完成后再进行下一个请求!

现在是编写一些代码的时候了,我认为——让我们利用 Deferreds 和 Promises,并构建一个使用 AJAX 的演示。我们将看到如何使用它来响应表单提交,而无需刷新页面。

修改我们的高级联系表单

在我们的实际示例的第一部分中,我们将重新使用并开发本章前面创建的基本 AJAX 表单,并从第四章中的 使用 jQuery 开发高级文件上传表单 演示中获取,与表单一起工作。我们将调整它以使用 AJAX 显示提交确认,并且确认也会显示为电子邮件。

对于这个练习,我们需要准备一些工具:

  • 使用默认设置安装本地 Web 服务器 - 选项包括 WAMP(适用于 PC - www.wampserver.dewww.wampserver.com/en/),或者 MAMP(适用于 Mac,www.mamp.info/en/)。Linux 用户可能已经作为其发行版的一部分拥有可用的工具。您需要确保您的 PHP 版本为 5.4 或更高,因为代码依赖于如果使用较旧版本将会中断的功能。您也可以尝试跨平台解决方案 XAMPP,可从 www.apachefriends.org/index.html 获取(请注意,如果使用此选项则不需要测试邮件工具 - 电子邮件支持已包含在 XAMPP 中)。

  • 免费的测试邮件服务器工具(仅限 Windows),可从 www.toolheap.com/test-mail-server-tool/ 获取。从本地 Web 服务器发送电子邮件可能很难设置,因此这个出色的工具监视端口 25 并提供本地发送电子邮件的功能。对于 Mac,您可以尝试discussions.apple.com/docs/DOC-4161中的说明;Linux 用户可以尝试遵循cnedelcu.blogspot.co.uk/2014/01/how-to-set-up-simple-mail-server-debian-linux.html中概述的步骤。

  • 访问正在使用的个人电脑或笔记本电脑的电子邮件包 - 这是接收使用测试邮件服务器工具发送的电子邮件所必需的。

好的 - 工具已经准备就绪,让我们开始吧:

  1. 我们将从打开附带本书的代码下载的副本开始,并提取ajaxform文件夹;这个文件夹包含我们演示的标记、样式和各种文件。我们需要将该文件夹保存到 Web 服务器的WWW文件夹中,对于 PC(通常为C:\wamp\www)。

  2. 标记相对简单,并且与本章中我们已经看到的非常相似。

  3. 我们需要对mailer.php文件进行一个小小的更改 - 在您选择的文本编辑器中打开它,然后查找以下行:

            $recipient = "<ENTER EMAIL HERE>";
    

    <ENTER EMAIL HERE>更改为您可以用来检查邮件是否出现的有效电子邮件地址。

  4. 这个演示的魔法发生在ajax.js中,所以现在让我们来看一下,首先设置一些变量:

      $(function() {
         var form = $('#ajaxform');
         var formMessages = $('#messages');
    
  5. 当按下发送按钮时,我们开始了真正的魔术。我们首先阻止表单提交(因为它的默认操作),然后将表单数据序列化为一个字符串以进行提交:

    $(form).submit(function(e) {
      e.preventDefault();
      var formData = $(form).serialize();
    
  6. 接下来是此表单的 AJAX 操作的核心。此函数设置要执行的请求类型,内容将被发送到哪里,以及要发送的数据:

    $.ajax({
      type: 'POST',
      url: $(form).attr('action'),
      data: formData
    })
    
  7. 然后我们添加了两个函数来确定接下来该发生什么 - 第一个处理表单成功提交的情况:

    .done(function(response) {
      $(formMessages).removeClass('error');
      $(formMessages).addClass('success');
      $(formMessages).text(response);
      $('#name').val('');
      $('#email').val('');
      $('#message').val('');
    })
    
  8. 接下来是处理表单提交失败结果的函数:

    .fail(function(data) {
      $(formMessages).removeClass('success');
      $(formMessages).addClass('error');
      if (data.responseText !== '') {
        $(formMessages).text(data.responseText);
      } 
      else {
        $(formMessages).text('Oops! An error occured and your 
        message could not be sent.');
      }
      });
      });
    });
    
  9. 双击启动电子邮件测试服务器工具。如果我们在浏览器中预览表单,并填写一些有效的详细信息,当提交时我们应该会看到以下图片:修改我们的高级联系表单

我们的表单现在已经就位,并且能够提交,确认邮件将在几分钟内出现。在下一章中,我们将更深入地讨论 jQuery 中的 AJAX 使用;现在让我们继续开发我们的表单。

使用 AJAX 添加文件上传功能

添加文件上传功能相对简单;它需要客户端和服务器端组件才能正常运行。

在我们的示例中,我们将更专注于客户端功能。为了演示目的,我们将上传文件到项目区域内的一个虚拟文件夹。以下是我们将构建的内容示例的屏幕截图:

使用 AJAX 添加文件上传功能

为了帮助我们进行这个演示,我们将使用 BlueImp 文件上传插件;它超过 1300 行代码,是一个非常全面的插件!这个插件与 BlueImp 的基于 PHP 的文件处理插件以及一些额外的 jQuery UI 一起,将有助于创建一个可用的文件上传设施。

注意

插件文件的副本可在伴随本书的代码下载中找到,也可以在github.com/blueimp/jQuery-File-Upload上找到。

让我们开始吧:

  1. 我们将首先提取伴随本书的代码下载中ajaxform-files文件夹的副本 - 这个文件夹中包含了 BlueImp 文件上传插件,以及一些额外的自定义 CSS 和 JavaScript 文件。

  2. ajaxform-files文件夹中的文件添加到存储在 web 服务器文件夹内的ajaxform文件夹中;JavaScript 文件应放在js文件夹中,CSS 样式表应放在css文件夹中,而 2 个 PHP 文件可以放在我们的ajaxform文件夹的根目录中。

  3. 接下来,我们需要打开上一个练习中的ajaxform.html文件的副本 - 我们首先需要添加一个链接到fileupload.css,其中包含了我们上传表单的一些额外样式:

      <link rel="stylesheet" href="css/bootstrap.min.css">
      <link rel="stylesheet" href="css/styles.css">
     <link rel="stylesheet" href="css/fileupload.css">
    
    
  4. 我们同样需要引用我们刚刚下载的额外 JavaScript 文件 - 将以下突出显示的链接添加到ajax.js的引用下面,如下所示:

    <script src="img/ajax.js"></script>
    <script src="img/jquery.ui.widget.js"></script>
    <script src="img/jquery.iframe-transport.js"></script>
    <script src="img/jquery.fileupload.js"></script>
    <script src="img/uploadfiles.js"></script>
    
    
  5. 接下来是对index.html的一些标记更改。因此,在ajaxform.html中,首先按照下面的步骤更改标题:

    <div id="formtitle"><h2>AJAX File Upload Demo</h1></div>
    <div id="form-messages"></div>
    
  6. 现在我们需要添加文件上传代码,所以在消息字段的</div>标签关闭后立即添加以下代码:

    <div class="container">
      Click the button to select files to send:
      <span class="btn btn-success fileinput-button">
        <span>Select files...</span>
        <input id="fileupload" type="file" name="files[]" multiple>
      </span> 
      <p>Upload progress</p>
      <div id="progress" class="progress progress-success progress-striped">
       <div class="bar"></div>
      </div>
      <p>Files uploaded:</p>
      <ul id="files"></ul>
    </div>
    
  7. 保存所有文件。—如果我们使用本地 Web 服务器预览结果,那么我们应该期望看到更新后的表单,现在在表单底部显示一个文件上传区域。

提示

如果您想看到已经进行了更改的版本,那么在随附本书的代码下载中的ajaxform-completed文件夹中有一个完成的代码版本。

检查演示中使用 Promises 和 Deferreds

尽管我们在此演示的第二部分中所做的更改相对简单,但它们隐藏了丰富的功能。要了解 AJAX 的使用方式,有必要详细查看jquery.fileupload.js插件的源代码。

如果我们打开ajax.js的副本,我们可以清楚地看到 jQuery 的 Deferred 对象的使用,形式为.done(),如下所示的摘录:

.done(function(response) {
  $(formMessages).removeClass('error');
  ....
})

然而,如果我们的 AJAX 代码失败了,jQuery 将执行.fail()事件处理程序中概述的方法或函数:

.fail(function(data) {
  $(formMessages).removeClass('success');
  ....
});

如果我们转而查看uploadfiles.js中的代码,我们可能会认为它根本没有使用 AJAX。相反,AJAX 被使用了,但是在jquery.fileupload.js插件中。

如果我们在文本编辑器中打开插件文件,我们可以看到很多 Deferreds 和 Promises 的实例。让我们看一些摘录作为示例:

  • upload方法 - 第 762-766 行:

      jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || that._getXHRPromise(false, o.context))
         .done(function (result, textStatus, jqXHR) {
    
  • 在同一方法中,但这次是从第 794-804 行:

          .fail(function (jqXHR, textStatus, errorThrown) {
            o.jqXHR = jqXHR;
            o.textStatus = textStatus;
    
  • 这次,从私有的_onSend方法,在第 900-904 行:

      ).done(function (result, textStatus, jqXHR) {
         that._onDone(result, textStatus, jqXHR, options);
       }).fail(function (jqXHR, textStatus, errorThrown) {
         that._onFail(jqXHR, textStatus, errorThrown, options);
       }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
    

这些只是一些示例,说明了我们如何使用 Deferreds 和 Promises 来增强我们的代码。希望这给您留下了一些可能性的味道,以及我们如何显着改善代码的可读性以及项目的性能。

详细介绍 AJAX 最佳实践

在本章中,我们重新访问了基础知识,并探讨了一些可以用来提升 AJAX 知识的技巧 - 关键不仅仅是编码,还有一些提示和技巧,可以帮助我们成为更全面的开发人员。

在“使用 Deferreds 和 Promises”部分,我们探讨了使用 jQuery 的 Deferreds 和 Promises 的基础知识,以及在使用它们时架构的变化如何导致性能显著提高。在我们总结本章之前,有一些额外的最佳实践,我们应该尽可能遵循。以下列表解释了它们:

  1. 没有必要直接调用.getJson().get()。这些在使用$.ajax()对象时默认调用。

  2. 调用请求时不要混合协议。最好尽可能使用无模式请求。

  3. 如果您只是进行 GET 请求,请尽量避免将请求参数放在 URL 中 - 而是使用 data 对象设置来发送它们,如下所示:

    // Less readable
    $.ajax({
        url: "something.php?param1=test1&param2=test2",
        ....
    });
    
    // More readable
    $.ajax({
        url: "something.php",
     data: { param1: test1, param2: test2 }
    });
    
  4. 尝试指定 dataType 设置,以便更容易知道您正在处理的数据类型。例如,请参考本章前一节中的 使用 AJAX 创建简单示例

  5. 使用委托事件处理程序将事件附加到使用 AJAX 加载的内容。委托事件可以处理稍后添加到文档中的后代元素的事件:

    $("#parent-container").on("click", "a", delegatedClickHandler);
    

提示

要了解更多,请参阅api.jquery.com/on/#direct-and-delegated-events

摘要

AJAX 作为一种技术已经存在多年。可以说它是一个改变游戏规则的技术,在这里,JavaScript 的使用消除了在浏览器中不断刷新页面内容的需求。jQuery 已经帮助增强了这一系列技术。在本章中,我们重新审视了一些基础知识,然后探讨了如何更好地提升我们的开发技能。让我们回顾一下我们学到的内容:

我们首先简要回顾了 AJAX 是什么,并提醒自己构建 jQuery 中 AJAX 请求的基础知识。

接下来,我们看了一些可以用来提高静态站点加载速度的技巧;我们学到了一项额外的技巧,即使用 localStorage 缓存内容。然后,我们讨论了如何实现回调可能会使代码混乱和缓慢,然后转而看到 Deferreds 和 Promises 如何改进我们的代码,最终改进了我们的站点的性能。

最后,我们看了一个演示,在这个演示中,我们借用了第四章中的一个表单,与表单一起工作,并通过首先添加基于 AJAX 的通知,然后利用 BlueImp 插件来扩展它,以实现一个文件上传功能,该功能利用了 Deferreds 和 Promises。

在下一章中,我们将深入研究我个人最喜欢的一个主题。是时候加入动画了,我们将看看如何使用 jQuery 为我们的网站元素赋予生命。

第六章 在 jQuery 中进行动画

举手喜欢静态网站的人?想不到吧,为网站添加动画效果能赋予其生命;但过度使用可能会带来灾难性的后果!

我们经常用来为网站注入生命力的两个常见效果是 AJAX 和动画;我们在前一章节详细介绍了前者。在本章中,我们将探讨何时使用 jQuery 而不是 CSS(或反之),如何更好地管理队列,以及如何实现一些漂亮的自定义动画效果。您还将看到如何轻松创建一些有用的自定义缓动效果,作为将来转换为 CSS 等效的基础。在本章中,我们将涵盖以下主题:

  • 何时使用 CSS 而不是 jQuery

  • 管理或避免 jQuery 动画队列

  • 设计自定义动画

  • 实现一些自定义动画

  • 在响应式网站中进行动画

准备好开始了吗?让我们开始吧……

选择 CSS 还是 jQuery

让我们从一个问题开始这个话题。

看一看 Dia do Baralho 网站,托管在 www.diadobaralho.com.br - 你们中有多少人认为你在那里看到的动画是仅使用 jQuery 创建的?

如果您认为是的话,那么很抱歉让您失望了;实际上答案是不是!如果您仔细查看源代码,您会发现有些地方同时使用了 CSS3 动画和 jQuery。现在,您可能会想:为什么我们在谈论掌握 jQuery 的书籍时要讨论 CSS3 动画呢?

这是有道理的;还记得我之前在书中提到过的,拥有正确技能的任何个体都可以编写 jQuery 吗?普通编码人员和优秀开发人员的区别在于:为什么我会使用 jQuery?现在,这听起来可能让人觉得我疯了,但我没有。让我解释一下我的意思,如下所示:

  • CSS3 动画不依赖于外部库;考虑到 jQuery 仍然有一定的大小,少一个资源请求总是一件好事!

  • 对于简单轻量的动画,如果 CSS3 动画足够,引用 jQuery 是没有好处的。尽管需要提供相同语句的供应商前缀版本(并且不使用 jQuery),但所需的代码量可能比使用 jQuery 少。

使用 jQuery 会有性能影响,这使得使用 CSS 动画更加诱人,原因有几个:

  • 这个库从来没有被设计成一个高性能的动画引擎;它的代码库必须服务于许多目的,这可能导致布局抖动。

  • jQuery 的内存消耗通常意味着我们需要进行垃圾收集,这可能会导致动画短暂冻结

  • jQuery 使用 setInterval 而不是 requestAnimationFrame 来管理动画(尽管这是由于即将推出的 jQuery 版本的更改)

同样有许多理由支持我们使用 jQuery;尽管它作为一个库有其局限性,但在某些情况下,我们可能需要在原生 CSS3 动画的位置使用 jQuery,如下所述:

  • CSS 动画对 GPU 负荷较大,在浏览器负载较高时可能会导致卡顿和色带效应——这在移动设备上尤为普遍。

    注意

    有关硬件加速和 CSS3 的影响的讨论,请访问css-tricks.com/myth-busting-css-animations-vs-javascript/

  • 大多数浏览器都支持 CSS3 动画,但 IE9 或更低版本除外;对于这种情况,必须使用 jQuery。

  • CSS3 动画目前(还)不及 jQuery 的灵活——它们一直在不断发展,因此总会有一天两者变得非常相似。例如,在使用 CSS3 时,我们不能在关键帧中使用不同的缓动方式;整个关键帧必须应用相同的缓动方式。

这里的关键点是,我们有自由选择的权利;事实上,正如开发者 David Walsh 所指出的,当我们只需要简单的状态更改时,使用 CSS3 动画更为明智。他的论点是基于能够在样式表中保留动画逻辑,并从多个 JavaScript 库中减少页面的臃肿。

但要注意,如果您的需求更复杂,则 jQuery 是前进的道路;开发者 Julian Shapiro 认为,使用动画库可以保持每个动画的性能,并使我们的工作流程易于管理。

注意

要查看使用 JavaScript 或 CSS 对多个对象进行动画效果的效果,请转到css3.bradshawenterprises.com/blog/jquery-vs-css3-transitions/,该网站展示了一个非常启发性的演示!

只要我们在使用 CSS 方面小心谨慎,对于简单的、自包含的状态动画,更明智的做法是使用原生 CSS3,而不总是依赖 jQuery 来解决我们所有的需求。

顺便提一句,值得注意的是,有一个相对较新的 API 正在考虑中:Web Animations API。该 API 旨在使用 JavaScript 创建效率与原生 CSS3 动画相同的动画。鉴于我们在使用 jQuery 时存在的固有问题,这值得关注;截至撰写本文时,该 API 的支持仅限于 Chrome 和 Opera。

提示

要了解 Web Animations API 的支持细节,请查看 Can I use 网站updates.html5rocks.com/2014/05/Web-Animations---element-animate-is-now-in-Chrome-36 上也发布了一篇有用的教程——不过这只适用于 Chrome!

足够的理论,让我们进行一些编码!假设我们需要使用 jQuery 来进行我们的动画项目,那么一个主要的问题很可能会困扰开发者:在任何使用动画的功能中设置了可以设置的排队动画的快速循环。让我们深入了解一下这意味着什么,以及我们可以采取什么措施来减少或消除这个问题。

控制 jQuery 动画队列

如果你花费了一些时间使用 jQuery 进行开发,毫无疑问,当你在处理动画时,你会遇到一个关键问题:当你切换到另一个浏览器窗口然后再切换回来时,你看到浏览器循环执行多个排队的动画多少次?

我敢打赌,答案是相当多次;这个问题的关键在于 jQuery 排队执行所有被要求执行的动画。如果发生了太多的初始化,那么 jQuery 的动画队列就会混乱,因此它似乎会变得疯狂!在解决问题之前,让我们先看看问题是如何出现的:

  1. 从附带本书的代码下载中提取 blockedqueue.htmlblockedqueue.css 文件,它们将提供一些简单的标记以说明我们的排队问题。

  2. 在文本编辑器中,将以下内容添加到一个新文件中,并将其保存为我们项目区域的 js 子文件夹中的 blockedqueue.js

    $(document).ready(function() {
      $(".nostop li").hover(
        function () { $(this).animate({width:"100px"},500); },
        function () { $(this).animate({width:"80px"},500); } 
      );
    });
    
  3. 如果我们现在运行我们的演示,那么当我们重复将鼠标移动到每个条形图上时,我们可以看到它们全部快速地增加或减少,下一个条形图在前一个动画完成之前就会改变,如下图所示:控制 jQuery 动画队列

显然,这种行为是不希望出现的;如果这个演示被自动化并与 requestAnimationFrame 结合使用(我们稍后会在第九章中介绍,使用 Web 性能 API),那么当我们切换到一个标签并返回到原来的标签时,我们会看到一系列动画被完成。

解决问题

我们如何解决这个问题?很简单;我们只需要在语句链中添加 .stop() 方法;这将在开始下一个动画之前清除前面的动画。让我们看看这在实践中意味着什么,通过执行以下步骤:

  1. blockedqueue.html 文件的副本中,按照以下方式修改 <head> 部分:

    <title>Demo: Clearing the animation queue</title>
      <link rel="stylesheet" href="css/blockedqueue.css">
      <script src="img/jquery.min.js"></script>
     <script src="img/unblockqueue.js"></script>
    </head>
    
  2. 我们需要在演示的主体中稍微更改标记,所以按照以下代码进行修改:

    <div id="container">
     <ul class="stop">
        <li></li>
    
  3. 将其保存为 unblockqueue.html。在一个新文件中,添加以下代码,然后将其保存为我们项目区域的 js 子文件夹中的 unblockedqueue.js。这包含了修改后的标记,以及添加了 .stop()

    $(document).ready(function() {
      $(".stop li").hover(
        function () {
          $(this).stop().animate({width:"100px"},500);
        },
        function () {
          $(this).stop().animate({width:"80px"},500);
        }
      );
    });
    
  4. 如果我们现在运行演示,然后快速地依次移动到每个条形图上,我们应该看到条形图会依次增加和减少,但是下一个条形图在前一个条形图返回到原始大小之前不会改变,如下所示:解决问题

希望您同意添加.stop()已经显著改善了我们的代码——添加.stop()将终止前一个动画但排队下一个动画,准备就绪。

让过渡更加顺畅

我们还可以进一步。仔细查看.stop()可用的属性,我们可以使用clearQueuejumpToEnd在匹配的元素上停止运行动画,从而产生更干净的过渡,如下图所示:

让过渡更加顺畅

提示

有关使用.stop()的更多信息,请参阅主要的 jQuery 文档 api.jquery.com/stop/

让我们修改我们的 jQuery 代码,看看这在实践中意味着什么,通过执行以下步骤:

  1. 返回到unblockedqueue.js文件,然后按如下所示修改代码:

      function () {
        $(this).stop(true, false).animate({width:"100px"},500);
      },
      function () {
        $(this).stop(true, false).animate({width:"80px"},500);
      }
    
  2. 保存您的工作,然后在浏览器中预览演示的结果。如果一切顺利,您应该看不到条形图本身的任何变化,但当您将鼠标悬停在每个条形图上时,动画效果会更加顺畅。

在这个阶段,我们应该有一个仍然工作但过渡更加顺畅的动画——值得注意的是,这个技巧只适用于动画。如果您的项目使用其他函数队列,则需要使用.clearQueue()清除这些队列。

注意

为了比较使用.stop()的不同方式,值得看看 Chris Coyier 的演示,网址为 css-tricks.com/examples/jQueryStop/——这产生了一些有趣的效果!类似的解释也可以在 www.2meter3.de/code/hoverFlow/ 找到。

使用纯 CSS 解决方案

好的,所以我们在 jQuery 中有我们的动画;对于一个简单的动画,如果我们改用纯 CSS 会是什么样子呢?虽然我们无法复制.stop()的相同效果,但我们可以非常接近。让我们看看这在实践中意味着什么,以unblockedqueue.html作为我们演示的基础:

  1. 首先移除两个 JavaScript 链接,一个指向unblockqueue.js,另一个指向 jQuery 本身。

  2. blockedqueue.css底部添加以下内容——这包含了我们演示所需的动画样式规则:

    li { width: 50%; transition: width 1s ease-in, padding-left 1s ease-in, padding-right 1s ease-in; }
    li:hover { width: 100%; transition: width 1s ease-out, padding-left 1s ease-out, padding-right 1s ease-out; }
    

此时,如果我们在浏览器中预览结果,我们应该看不到动画列表元素的 可见 差异;真正的变化可以在使用 Google Chrome 的开发者工具栏监视时间线时看到。让我们看看这种变化是什么样子的。

  1. 启动 Google Chrome。按 Shift + Ctrl + I 召唤开发者工具栏(或 Option + Cmd + I 适用于苹果 Mac)。

  2. 单击 Timeline 标签,然后单击放大镜下方的灰色圆圈——圆圈将变为红色。

  3. 依次将鼠标悬停在列表项上;Chrome 将监视并收集执行的操作的详细信息。

  4. 几分钟后,单击红色圆圈以停止生成配置文件;您将得到类似这样的结果:使用纯 CSS 解决方案

我们清楚地看到,仅 CSS 解决方案几乎不会影响浏览器的性能。相比之下,当我们运行 unblockedqueue.html 演示时,看一下相同的时间轴:

使用纯 CSS 解决方案

注意到区别了吗?虽然这只是一个快速的非科学性测试,但当我们查看详细数字时,我们可以清楚地看到差异。

在大约 3 秒的时间内,Google Chrome 在运行仅 CSS 解决方案时花费了 33 毫秒进行渲染和 48 毫秒进行绘制。运行 unblockedqueue.html 显示数字几乎翻了一番:脚本 107 毫秒,渲染 78 毫秒,绘制 76 毫秒!这绝对是需要考虑的事情...

改进 jQuery 动画

从前面的部分,我们可以很容易地看出,在浏览器中渲染 CSS 时具有明显的优势——尽管演示中使用了某种程度上不科学的方法!

但关键点在于,尽管在使用 jQuery 时我们在灵活性和全面的浏览器支持方面获得了一些好处,但我们在速度上失去了——jQuery 从未被设计为在渲染动画时性能良好。

为了帮助提高性能,您可以探索几个插件选项:

  • Velocity.js:这个插件对 $.animate() 进行了重新设计以提供更快的性能,并且可以与或无需 jQuery 一同使用;这包括 IE8。该插件可以从 julian.com/research/velocity/ 下载。这还包含了一些预先注册的效果——我们将在本章稍后介绍更多关于创建自定义缓动效果的内容。

  • jQuery-animate-enhanced:这个插件会自动检测和重新设计动画,以使用原生的 CSS 过渡效果,适用于 WebKit、Mozilla 和 IE10 或更高版本。可以从这里下载 playground.benbarnett.net/jquery-animate-enhanced/

我们还可以进一步探讨使用 jQuery 在动画完成时通知我们的方法,使用 transitionend 事件。虽然这可能无法解决动画队列积压的原始问题,但使用 jQuery 将允许您将动画效果与您的 jQuery 逻辑分开。

注意

对于一个有趣的文章和演示,关于使用 transitionend(及其供应商前缀版本),请查看 Treehouse 网站上的一篇文章,链接在这里 blog.teamtreehouse.com/using-jquery-to-detect-when-css3-animations-and-transitions-end

现在我们已经看到了如何使我们的动画更流畅,让我们继续看看如何生成定制动画;理论是我们可以利用一些知识来创建更复杂和有趣的动画,同时减少我们在运行队列时看到的一些问题。

但是,在这样做之前,我想给你留下两个有用的建议,可以帮助您改善您的动画:

让我们开始设计这些定制动画,首先看一下如何使用缓动函数。

介绍缓动函数

在页面上动画化任何对象或元素时,我们可以简单地将其上下滑动或从一个地方移动到另一个地方。这些都是完全有效的效果,但缺乏实际感,例如在打开抽屉时可能得到的效果。

动画并不总是以恒定的速度运动;相反,如果我们弹跳一个球或者打开抽屉时会有一些反弹,或者会有一些减速。为了实现这种效果,我们需要使用缓动函数,它们控制变化的速率。在互联网上有很多例子——一个很好的起点是www.easings.net——或者我们可以观看一些网站上的效果,比如matthewlein.com/ceaser/。在接下来的几页中,我们将更详细地探讨这些,并且看一些技巧,可以帮助我们将动画技能推向新的高度。

设计定制动画

如果您花费过任何时间开发 jQuery 代码来动画页面上的对象或元素,无疑您曾使用过 jQuery UI 或者可能是插件,比如由 George Smith 创建的 jQuery Easing(gsgd.co.uk/sandbox/jquery/easing/)。

这两种方法都是在页面上动画化对象的绝佳方法,使用缓动方法如easeIn()easeOutShine()。问题在于两者都需要使用插件,这为我们的代码增添了不必要的负担;它们也是实现我们需要的效果的一种非常安全的方法。如果我说我们两者都不需要,只需使用 jQuery 本身就可以产生相同的效果呢?

在我介绍如何做到这一点之前,让我们看一个展示这一点的工作演示:

  1. 让我们开始吧,从附带本书的代码下载中提取相关文件——对于这个演示,我们将需要以下文件的副本:

    • customanimate.html:将此文件保存在项目文件夹的根区域

    • customanimate.css:将此文件保存在项目文件夹的css子文件夹中

    • customanimate.js:将此文件保存在项目文件夹的js子文件夹中

    打开 Sans 字体;将其保存在项目文件夹的font文件夹中;或者,该字体可以在www.fontsquirrel.com/fonts/open-sans获取。

  2. 如果你在浏览器中预览customanimate.html文件,然后运行演示,你应该会看到类似于这个屏幕截图的东西,其中<div>标签正在运行动画的中途:设计自定义动画

那么,这里发生了什么?嗯,我们所用的不过是一个标准的.animate(),用来增加<div>标签的大小并将其移动到新位置。

这里没有什么新鲜的,对吧?错了,这里的“新鲜”之处实际上在于我们如何构建缓动!如果你看一下customanimate.js,你会找到这段代码:

$(document).ready(function() {
  $.extend(jQuery.easing, {
    easeInBackCustom: function(x,t,b,c,d) {
      var s;
      if (s == undefined) s = 2.70158;
      return c*(t/=d)*t*((s+1)*t - s) + b;
    }
  })

我们所做的就是取得实现相同效果所需的数学,并将其包装在一个扩展了$.easing的 jQuery 对象中。然后我们可以在我们的代码中引用新的缓动方法,如下所示:

  $("#go").click(function() {
    $("#block").animate({
     ...
    }, 1500, 'easeInBackCustom');
  });
})

这打开了很多可能性;然后我们可以用我们自己的创意替换自定义的缓动函数。在互联网上搜罗了很多可能性,比如这两个例子:

$.easing.easeOutBack = function(t) {
  return 1 - (1 - t) * (1 - t) * (1 - 3*t);
};

$.easing.speedInOut = function(x, t, b, c, d) {
  return (sinh((x - 0.5) * 5) + sinh(-(x - 0.5)) + (sinh(2.5) + Math.sin(-2.5))) / (sinh(2.5) * 1.82);
};

要真正深入了解缓动函数是如何工作的,超出了本书的范围——如果你对其背后的数学感兴趣,那么互联网上有几个网站可以更详细地解释这个问题。

注意

两个使用缓动函数的示例包括upshots.org/actionscript/jsas-understanding-easingwww.brianwald.com/journal/creating-custom-jquery-easing-animations—注意,它们看起来确实有点枯燥!

简而言之,获得缓动函数的最佳来源是 jQuery 的源代码,我们可以查看每个所需的计算,并将其用作创建自己的缓动效果的基础。

这一切都很好;这是一个很好的方式来实现良好的动画,而不会产生难以理解或调试的复杂代码。但是……你知道的,我认为我们仍然可以做得更好。怎么做?很简单,如果我们能够在 jQuery 中复制一些我们可能在 CSS 过渡中看到的缓动效果呢?

转换为与 jQuery 一起使用

在这一点上,你可能认为我现在真的疯了;CSS 过渡使用贝塞尔曲线,当与 jQuery 的animate()方法一起使用时不受支持。那么,我们如何实现相同的效果呢?

答案总是在一个插件之中—尽管这与我们在以前的演示中所讨论的有所不同!然而,存在一个区别:这个插件在压缩后的大小为 0.8 KB;这明显比使用 jQuery UI 或 Easing 插件要小得多。

我们打算使用的插件是 Robert Grey 开发的 Bez 插件,网址为github.com/rdallasgray/bez;这将使我们能够使用三次贝塞尔的值,比如0.23, 1, 0.32, 1,这相当于easeOutQuint。让我们看看它的效果:

  1. 我们首先需要下载并安装 Bez 插件—可以从 GitHub 上下载,网址为github.com/rdallasgray/bez;然后将其引用于customanimate.html,就在对 jQuery 的引用下面。

  2. 接下来,打开一份customanimate.js的副本;然后按照下面显示的方式更改这一行,替换我们之前使用的easeInBackCustom动作:

      }, 1500, $.bez([0.23, 1, 0.32, 1]));
    

保存这两个文件;如果您在浏览器中预览结果,您将看到与前面示例中所见不同的操作。

那么,我们是如何做到这一点的呢?这背后的诀窍是结合插件和 easings.net 网站。以easeOutQuint作为我们的示例缓动效果,如果我们首先访问easings.net/#easeOutQuint,我们可以看到产生我们效果所需的三次贝塞尔值:0.86, 0, 0.07, 1。我们所需要做的就是将它们插入到对 Bez 插件的调用中,就完成了:

}, 1500, $.bez([0.86, 0, 0.07, 1]));

如果我们想要创建自己的三次贝塞尔效果,我们可以使用cubic-bezier.com来创建我们的效果;这将给我们提供需要使用的数值,如下面的截图所示:

转换为 jQuery 使用

然后,我们可以像在上一个例子中那样将这些值插入到我们的对象调用中。使用这种方法的美妙之处在于,以后如果我们决定减少对 jQuery 的使用,我们可以轻松地将动画转换为 CSS3 的等效动画。

注意

要了解更多有关贝塞尔曲线背后理论的知识,请参阅维基百科上的文章,网址为en.wikipedia.org/wiki/B%C3%A9zier_curve

好的,所以我们已经学会了如何创建自己的动画缓动函数;如果我们想要使用现有库中可用的效果该怎么办?没问题,互联网上有一些很好的示例,包括以下内容:

  • daneden.github.io/animate.css/:这是 Animate.css 库的主页;我们可以使用github.com/jQueryKeyframes/jQuery.Keyframes上提供的jQuery.Keyframes插件来复制此库中的效果。

  • github.com/yckart/jquery-custom-animations:这个库包含了许多不同的效果,以类似于 jQuery UI 的风格创建;可以轻松使用并以类似于本章前面 设计自定义动画 演示中的方式引用效果。

  • github.com/ThrivingKings/animo.js:Animo.JS 采用了不同的方法;它不使用 jQuery 的 animate() 函数,而是使用自己的 animo() 方法来对对象进行动画处理。它使用了 Animate.css 库中的效果,该库由 Dan Eden 创建——虽然有人可能会争论是否值得额外开销,但它仍然值得一看,可能是你项目中动画的可能来源之一。

  • lvivski.com/anima/:值得仔细查看这个库;源代码中包含了 easings.js 源文件中的许多三次贝塞尔值。如果需要的话,这些可以轻松地移植到你自己的代码项目中,或者可以为你自己的示例提供灵感。

是时候将我们所学到的动画概念投入到实践中了;让我们继续,看看如何在我们自己的项目中使用动画的一些例子。

实现一些自定义动画

在本章中,我们探讨了使用 jQuery 对对象进行动画处理,并看到了这与基于 CSS 的动画的比较;我们还看到了如何创建一些自定义的缓动模式,以控制元素在屏幕上的移动方式。

够了解理论,让我们深入一些实际应用吧!在接下来的几页中,我们将看一些元素动画的例子;我们将包括一些响应式网站的示例,因为这是一个热门话题,随着移动设备用于访问互联网内容的增加。

让我们开始吧,看看如何对一个简单的元素进行动画处理,以按钮的形式——注意演示结束时的转折!

对按钮进行动画处理

谦逊的按钮必须是任何网站上最重要的元素之一;按钮有各种形状和大小,并且可以通过标准的 <button> HTML 元素或使用 <input> 字段创建。

在这个演示中,我们将使用 jQuery 不仅来滑入和滑出按钮图标,还将同时旋转它们。但是等等——我们都知道 jQuery 不支持元素的旋转,对吗?

我们可以使用插件,例如 QTransform(github.com/puppybits/QTransform),甚至是 jQuery Animate Enhanced(playground.benbarnett.net/jquery-animate-enhanced/),但这样会增加负担,我们采用不同的方式。相反,我们将使用 Monkey Patch 直接改造支持;为了证明它有效,我们将更新Codrops演示,该演示在其网站上有原始版本的滚动按钮,现在将使用 jQuery 2.1。

注意

此演示的原始版本可在tympanus.net/codrops/2010/04/30/rocking-and-rolling-rounded-menu-with-jquery/找到。

让我们看看演示:

  1. 从本书附带的代码下载中提取相关文件;对于此演示,我们需要以下文件:

    • rollingbuttons.html:将此文件保存在项目区域的根子文件夹中

    • style.css:将此文件保存在项目区域的css子文件夹中

    • jquery-animate-css-rotate-scale.js:将此文件保存在项目区域的js子文件夹中

    • rollingbuttons.js:将此文件保存在项目区域的js子文件夹中

    • img:将此文件夹复制到项目区域

    注意

    此 Monkey Patch 的原始版本可在www.zachstronaut.com/posts/2009/08/07/jquery-animate-css-rotate-scale.html找到;它是为 jQuery 1.3.1+开发的,但是当我将其与 jQuery 2.1 一起使用时,我没有看到任何不良影响。

  2. 在浏览器中运行演示,然后尝试悬停在一个或多个按钮上。如果一切运行正常,我们将看到绿色图标图像开始向左旋转,而灰色背景扩展形成一个长的药片,其中包含链接,如下图所示:Animating rollover buttons

更详细地探索代码

此演示产生了一个巧妙的效果,同时还节省了空间;访问者只有在需要查看信息时才会暴露,而在其他时间都是隐藏的。

但是,如果我们更仔细地查看代码,就会发现一个有趣的概念:jQuery 在使用.animate()时不提供旋转元素的本地支持,就像在此演示开始时提到的那样。

那么,我们怎样才能解决这个问题呢?我们可以使用插件,但相反,我们使用一个 Monkey Patch(由开发者 Zachary Johnson 创建)来给 jQuery 添加支持。值得注意的是,使用补丁总是有风险的(如第二章所述,“自定义 jQuery”),但在这种情况下,尽管更新到了 jQuery 2.1,似乎没有明显的不良影响。

如果你想看到在使用补丁时的差异,请在运行演示之前激活 DOM 检查器,如 Firebug。将鼠标悬停在其中一个图标上,你应该会看到类似于这张截图的东西:

更详细地探索代码

如果你想深入了解matrix()的工作原理,请访问 Mozilla 的笔记,网址为developer.mozilla.org/en-US/docs/Web/CSS/transform

让我们继续并查看我们的下一个动画示例。我相信你以某种形式使用过覆盖层,但我们将看一个采用全新方法且摒弃了大多数覆盖层中典型的灰色遮罩的覆盖层。

动画覆盖效果

如果你在互联网上浏览网站花费了一些时间,你肯定会遇到一些使用某种形式覆盖层的网站,对吧?

你知道这套流程:它们首先用半透明的覆盖层将屏幕变黑,然后开始显示图像或视频的放大版本。这是在全球数千个网站上找到的典型效果,如果使用得当,可以非常有效。

然而,你比这更了解我;我喜欢将事情推向更深层次!如果我们打破传统,做一个不显示图像但显示全屏展示的覆盖层,会怎么样?感兴趣吗?让我们看看我指的是什么:

动画覆盖效果

对于这个演示,我们将运行覆盖效果的一个版本,显示在tympanus.net/Tutorials/ExpandingOverlayEffect/

  1. 让我们从与本书附带的代码下载中提取以下文件开始;将它们保存在项目区域内的相应文件夹中:

    • jquery.min.js: 将此文件保存在项目区域的js子文件夹中

    • fittext.js:将此文件保存在项目区域的js子文件夹中

    • boxgrid.js:将此文件保存在项目区域的js子文件夹中

    • default.csscomponent.cssclimacons.css:将这些文件保存在项目区域的css子文件夹中

    • overlayeffect.html:将此文件保存在项目区域的根目录中

  2. 运行overlayeffect.html然后尝试点击其中一个彩色框。

注意发生了什么?它显示了一个覆盖效果,就像你期望的那样,但这个效果覆盖了整个浏览器窗口,并且没有常见的传统覆盖效果中经常显示的遮罩效果。

在这个演示中,我们使用了一些 HTML 来生成我们的初始网格;fittext.js插件用于帮助确保文本(因此是覆盖层)被调整大小以填充屏幕;覆盖效果是使用我们的代码内的boxgrid.js插件产生的。

魔法发生在boxgrid.js中——这包含了由 Louis Remi 开发的jquery.debouncedresize.js插件;尽管这已经有 2 年历史了;但在现代浏览器中仍然完美运行。您可以从github.com/louisremi/jquery-smartresize/blob/master/jquery.debouncedresize.js下载原始插件。

让我们改变焦点,继续看一看如何将 jQuery 动画应用于响应式网站。在两个演示中的第一个中,您将看到如何结合使用 CSS3、jQuery 和history.pushState来创建一些令人愉悦的转换效果,这些效果可以将一个多页面站点转变为一个看似是单页面应用程序。

在响应式网站中进行动画处理

您有多少次访问了一个站点,结果发现您必须在每个页面加载之间等待很长时间?听起来熟悉吗?

过去几年来,我们对页面转换的期望已经发生了变化——页面上元素重新排列的笨拙副作用已经不够了;我们对网站有了更多期望。基于 JavaScript 的单页面应用程序SPA)框架通常被视为答案,但是要以使用冗长的代码为代价。

我们可以做得比这更好。我们可以介绍 smoothState.js,这是由 Miguel Ángel Pérez 创建的一个有用的插件,它允许我们添加转换效果,使整个体验对访问者更加平滑和愉快。在这个示例中,我们将使用插件作者提供的演示的修改版本;一些代码已经从原始代码中重新组织和清理。

让我们看看插件的实际效果,并看看它如何使体验更加流畅。要做到这一点,请执行以下步骤:

  1. 从附带本书的代码下载中,提取以下文件的副本:

    • smoothstate.htmlsmoothstate.css:将这些文件分别保存在您项目文件夹的根目录和css子文件夹中。

    • jquery.smoothstate.js:将其保存在项目区域的js子文件夹中;最新版本可从github.com/miguel-perez/jquery.smoothState.js下载。

    • jquery.min.js:将其保存在项目区域的js子文件夹中。

    • animate.css:将其保存在项目区域的css子文件夹中;最新版本可在daneden.github.io/animate.css/下载。

    • Roboto 字体:使用的两种字体的副本在附带本书的代码下载中。或者,它们可以从 Font Squirrel 网站下载,网址为www.fontsquirrel.com/fonts/roboto。我们只需要选择 WOFF 字体;我们将在演示中使用字体的轻和常规版本。

  2. 在浏览器中运行smoothstate.html文件;尝试点击三个链接中的中间链接,看看会发生什么。注意它如何显示下一个页面,即transitions.html。smoothState.js 将网站视为单页面应用程序,而不是加载新页面时经常出现的暂停。您应该看到一个非常简单的页面显示,如下图所示:在响应式网站中进行动画处理

传统上,面对这个问题时,许多人可能会诉诸于 SPA 框架,以解决问题并改善过渡外观。采用这种方法确实有效,但会以使用不显眼代码获得的好处为代价。

相反,我们可以使用 jQuery、CSS3、history.pushState()和渐进增强的混合来实现相同的效果,从而为我们的最终用户带来更好的体验。

注意

值得一提的是,可以查看网站文档,位于weblinc.github.io/jquery.smoothState.js/index.html。在 CSS-Tricks 网站上有一个有用的教程,位于css-tricks.com/add-page-transitions-css-smoothstate-js/

维护良好的用户体验应始终是任何开发人员心中的首要任务——在处理响应式网站时,这更为重要。其中一个关键部分应该是监控我们动画的性能,以确保我们在用户体验和对服务器的需求之间取得良好的平衡。

在处理基于 jQuery 的动画在响应式网站上使用时,我们可以使用一些技巧来帮助提高性能。让我们来看看其中的一些问题以及我们如何缓解或解决它们。

考虑响应式网站上的动画性能

在这个可以从任何设备访问互联网的现代时代,对用户体验的重视比以往任何时候都更为关键——使用 jQuery 并不会帮助这一点。作为最低公共分母,它有助于简化处理内容(特别是复杂动画),但并不为其使用进行了优化。

在使用 jQuery 进行内容动画时,我们会遇到一些问题——我们在本章早些时候已经讨论过其中一些,在选择 CSS 还是 jQuery中介绍过;它们同样适用于响应式网站。此外,还有其他一些需要我们注意的问题,包括以下内容:

  • 使用 jQuery 的动画将消耗大量资源;再加上可能不适合移动环境的内容(由于其量),将导致桌面体验缓慢。在笔记本电脑和移动设备上情况会更糟!

  • 移动设备上的最终用户通常只对获取所需信息感兴趣;动画可能使网站看起来很好,但通常不针对移动设备进行优化,并且可能会减慢访问速度并导致浏览器崩溃。

  • jQuery 的垃圾收集进程经常会导致问题;它使用setInterval()而不是requestAnimationFrame()会导致高帧率,使得体验可能会出现卡顿和高帧丢失率。

    提示

    在撰写本文时,有计划使用requestAnimationFrame(及clearAnimationFrame)替换 jQuery 中的setInterval(及clearInterval)。

  • 如果我们使用动画——无论是 jQuery 还是纯 CSS——在一些平台上,我们经常需要启用硬件加速。虽然这可以提高移动设备的性能,但也可能导致闪烁,如果硬件加速的元素与不是硬件加速的其他元素重叠。在本章稍后的改善动画外观部分,我们将介绍如何启用 3D 渲染。

  • jQuery 的.animate在每帧动画都会增加元素的style属性;这会迫使浏览器重新计算布局,导致持续刷新。这在响应式网站上尤为严重,每个元素需要在屏幕调整大小时重新绘制;这会对服务器资源产生额外的需求并影响性能。如果需要的话,可以使用 jQuery Timer Tools (github.com/lolmaus/jquery.timer-tools)等插件来限制或延迟操作,这样它们只会在必要时执行,或者多次重复调用能够被有效地合并成一次执行。

  • 如果改变元素的显示状态(使用display...或display: none),那么这会导致向 DOM 添加或移除元素。如果您的 DOM 中有大量的元素,则这可能会对性能产生影响。

  • 使用 jQuery 会在 DOM 中留下特异性很高的内联样式,这样会覆盖我们精心维护的 CSS。如果视口被调整大小并触发了不同的断点,这是一个大问题。

    注意

    CSS 特异性是浏览器决定哪些属性值对元素最相关并作为结果应用的地方——查看css-tricks.com/specifics-on-css-specificity/了解更多细节。

  • 顺便说一句,在编写 JavaScript 文件时,我们丢失了关注点的分离(或者为我们的代码定义独立的部分)。

有可能减少或解决这些问题吗?有可能,但这可能需要一些牺牲;这将取决于您的需求是什么以及需要支持的目标设备。让我们花点时间考虑我们可以做出哪些改变:

  • 在实际情况下,考虑在移动网站中使用 CSS 来取代 jQuery;大多数浏览器(除了 Opera Mini)支持 CSS 关键字,如translatetransform。由于它们是浏览器的本机支持,这消除了对引用额外代码的依赖,从而节省了资源和带宽的使用。

  • 如果使用 jQuery 无法实现动画效果,或者所需的工作量超过了所获得的收益,则考虑使用插件,如 Velocity.js(可从github.com/julianshapiro/velocity获取),因为该插件已经优化了对内容进行动画处理。

    注意

    值得注意的是,正在讨论将 Velocity.js 集成到 jQuery 中——有关更多详细信息,请参阅github.com/jquery/jquery/issues/2053。也有一篇帖子值得阅读,详细讨论了 Velocity 的使用情况,请参见www.smashingmagazine.com/2014/09/04/animating-without-jquery/

  • 更好的选择是使用 jQuery.Animate-Enhanced 插件或 jQuery++ 中的 animate 助手;两者都会默认将动画转换为使用 CSS3 等效果,在支持的情况下。

那么,在使用 jQuery 处理响应式网站上的动画请求时,我们该如何做呢?有几种方法可以做到这一点;让我们更详细地探讨这个关键问题。

处理响应式网站上的动画请求

在使用 jQuery 处理响应式网站内的内容动画时,最好的方法可能似乎有点反常:除非绝对必要,否则不要使用 jQuery!此时,您可能认为我完全疯了,但以下是一些很好的理由:

  • jQuery 不是为动画效果进行优化的;样式表、HTML 和 JavaScript 之间的分界线将开始模糊,这意味着我们失去了对内容样式的控制。

  • 在移动设备上,使用 jQuery 进行动画效果不佳;为了提高性能,必须使用额外的 CSS 样式。

  • 由于 CSS 的特异性,我们失去了对特定元素应用哪些规则的控制——将样式保持在 CSS 样式表中意味着我们可以保留控制。

  • 默认情况下,jQuery 动画会消耗资源。在简单的网站上,这将产生很小的影响,但在较大的网站上,影响将显著更大。

  • 使用纯 CSS 方法的一个额外好处是它允许您使用 CSS 预处理器,如SASS或 Less,来处理媒体查询。这种缩写 CSS 可以让您更有效地编写样式,同时仍保持最终期望的输出。

有了这个想法,让我们来看看我们可以用来处理响应式网站上动画请求的一些指针:

  • 首先考虑移动端。如果你正在使用 CSS,那么首先基于最小的屏幕进行布局,然后添加额外的媒体查询来处理在越来越大的设备上查看时布局的变化。考虑使用 CSS 媒体查询样板,比如由开发者 Paul Lund 在www.paulund.co.uk/boilerplate-css-media-queries创建的样板;然后我们可以在适当的断点内插入动画规则。

  • 避免在你的 jQuery 代码中使用.css语句,而是使用.addClass().removeClass()方法—这样可以保持内容和表现层之间的清晰分隔。如何使用这个的一个很好的例子(对于那些不确定的人)可以在 Justin Aguilar 的 Animation Cheat Sheet 网站上找到,网址为www.justinaguilar.com/animations/。这会产生各种不同的动画,所有这些都可以使用.addClass()添加。

  • 基于在代码中使用无前缀版本的属性,并使用自动添加任何供应商前缀的自动添加器。当使用 Grunt 和插件(例如 grunt-autoprefixer)时,这变得非常简单。

  • 考虑尽可能使用 jQuery.Animate-Enhanced 插件(可在github.com/benbarnett/jQuery-Animate-Enhanced获取)。虽然它有几年历史了,但仍然可以与当前版本的 jQuery 一起使用;它将$.animate()扩展为检测转换并用 CSS 等效项替换它们。

    提示

    值得一看的另一个插件是 Animsition,可在git.blivesta.com/animsition获取。

  • 关键在于不要将其视为网站的永久部分,而是将其视为用 CSS 等效样式替换现有 jQuery 动画的工具。你能够转向使用 CSS 的越多,对页面的影响就越小,因为对服务器资源的需求将会减少。

  • 时刻关注www.caniuse.com。虽然浏览器对 CSS3 转换和过渡的支持非常好,但仍然有一些情况需要使用 WebKit 前缀,即适用于 Safari 和 iOS Safari(移动端)。

  • 尽可能在你的动画中使用requestAnimationFrame(和clearAnimationFrame)。这将有助于在动画不可见时保护资源。这将需要使用 jQuery,但由于我们应该将其保留用于最复杂的动画,因此使用库的影响将会减小。

  • 看一看诸如 cssanimate.com/ 这样的网站 —— 这些网站可以生成复杂的基于关键帧的动画,可以直接嵌入到您现有的代码中。如果您担心现有内容无法进行动画处理,那么这个网站可能会帮助您消除一些疑虑。

  • 问问自己这个问题:“如果我的动画真的很复杂,它是否会有效?”如果动画做得好,它们可以视觉上令人惊叹,但这并不意味着它们必须复杂。通常,简单而经过深思熟虑的动画效果比它们的复杂、资源消耗大的等效效果更好。

这里需要考虑的重要一点是,使用 jQuery 来执行动画不应完全被排除在外;随着浏览器对 CSS 动画的支持不断发展,这更支持了以后大多数动画的基础是使用 CSS。

jQuery 团队意识到 jQuery 从未被设计用于高效地对内容进行动画处理。在撰写本书时,关于引入 Velocity.js 版本的讨论仍在进行中;原则上,这可能会改善使用 jQuery 来对内容进行动画处理的效果,但这距离成为现实还有很长的路要走!

与此同时,我们应该仔细考虑所使用的 jQuery 与 CSS 动画之间的平衡,并且如果可以使用 CSS 动画的话,就应该避免使用 jQuery 动画。

注意

为了证明一点,Chris Coyier 制作了一个 CodePen 示例,展示了一个相当简单的网站如何实现响应式并包含基于 CSS 的动画效果,您可以在 codepen.io/chriscoyier/pen/tynas 上查看。

好的,让我们继续。我们将继续讨论动画主题,但这次我们将看看如何在移动设备上实现动画效果。我们需要注意一些问题;让我们更详细地看看这些。

为移动设备创建动画内容

到目前为止,我们已经考虑了使用 jQuery 在响应式网站上对内容进行动画处理,但是移动平台呢?越来越多的非台式设备(如笔记本电脑和智能手机)用于查看内容。这带来了一些额外的考虑因素,我们需要考虑如何在移动设备上获得最佳性能。

在移动平台上进行动画处理与编写代码关系不大,更多的是关于决定使用哪些技术;在大多数情况下,简单地编写 jQuery 代码就可以工作,但效果可能不如预期的那么好。

获得最佳体验的秘诀在于使用智能手机的GPU图形处理单元;为此,我们可以通过启用 3D 渲染来卸载标准的 jQuery 动画(速度较慢)。

提示

虽然此浏览器应该在所有台式机和移动设备上都能正常工作,但在基于 WebKit 的浏览器(例如 Google Chrome)中,您将获得最佳效果。

让我们通过一个启用了 3D 渲染的简单示例来更详细地探讨一下:

  1. 对于这个演示,我们需要三个文件。从代码下载中提取mobileanimate.htmlmobileanimate.cssjquery.min.js,并将它们保存在项目区域的相应文件夹中。

  2. 在一个新文件中,添加以下代码。它处理我们的下拉框的动画。我们将逐步详细介绍它,首先是为我们的代码分配所需变量的数量:

    var thisBody = document.body || document.documentElement,
        thisStyle = thisBody.style,
        transitionEndEvent = 'webkitTransitionEnd transitionend',
        cssTransitionsSupported = thisStyle.transition !== undefined,
        has3D = ('WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix());
    
  3. 接下来是初始检查,如果浏览器支持 CSS3 变换,则在ul对象中添加accordion_css3_support类:

    // Switch to CSS3 Transform 3D if supported & accordion element exist
    if(cssTransitionsSupported && has3D ) {
      if($('.children').length > 0) { 
        $('.children').addClass("accordion_css3_support");
      }
    }
    
  4. 这个事件处理程序发生了神奇的事情。如果不支持 CSS3 过渡效果,则下拉框将使用slideToggle方法来打开或关闭;否则,它将使用 CSS3 变换:

    $('.parent a').on('touchstart click', function(e) {
      e.preventDefault();
      // If transitions or 3D transforms are not supported
      if(!cssTransitionsSupported || !has3D ) {
        $(this).siblings('.children').slideToggle(500);
      }
      else {
        $(this).siblings('.children').toggleClass("animated");
      }
    });
    
  5. 将文件保存为mobileanimate.js。如果一切顺利,你将看到一个样式化的下拉框准备好打开,如下所示:为移动设备动画内容

尝试点击下拉箭头。乍看之下,我们的下拉框似乎与其他任何下拉框没有区别;它以与任何其他下拉框相同的方式展开和收缩。实际上,我们的代码使用了两个重要的技巧来帮助管理动画;让我们花点时间来了解在使用 jQuery 时两者的重要性。

改善动画外观

如果我们仔细看代码,我们对两个地方感兴趣;第一个在 jQuery 代码中:

if(!cssTransitionsSupported || !has3D ) {
  $(this).siblings('.children').slideToggle(500);
}
else {
  $(this).siblings('.children').toggleClass("animated");
}

第二个在 CSS 样式表中两个地方显示:

.accordion_css3_support { display: block; max-height: 0;
  overflow: hidden; transform: translate3d(0,0,0);
  transition: all 0.5s linear; -webkit-backface-visibility: hidden; -webkit-perspective: 1000; }
.children.animated { max-height: 1000px; transform: translate3d(0,0,0); }

“为什么这些重要呢?”,我听到你问。答案很简单。在大多数情况下,我们可能会使用slideToggle()事件处理程序。这没有什么不对,除了动画不是硬件加速的(还需要你将其转换为 CSS),因此不会充分利用平台的功能。此外,它模糊了代码和样式之间的界线;如果我们既在代码中又在样式表中拥有它们,那么在调试样式时就更难了。

更好的选择是先弄清楚浏览器是否支持 CSS3 变换(或类似的功能),然后应用一个新的类,我们可以在样式表中进行样式设计。如果浏览器不支持变换,则我们简单地回退到在 jQuery 中使用slideToggle()方法。前者的好处是 CSS 样式将减少运行动画所需的资源,并有助于节省资源。

提示

如果还必须使用 jQuery,则值得测试设置给jQuery.fx.interval的值——尝试大约 12fps 左右,看看是否有助于提高性能;更多细节请参阅主文档api.jquery.com/jquery.fx.interval/

第二个值得关注的点可能显得不太明显;如果我们对包含动画的任何 CSS 规则应用变换translate3d(0,0,0),那么这足以启用 3D 渲染,并允许浏览器通过将动画卸载到 GPU 上来提供流畅的体验。在某些浏览器(如 Google Chrome)中,我们可能会遇到闪烁的情况;我们可能需要添加以下代码来消除不需要的闪烁:

-webkit-backface-visibility: hidden; -webkit-perspective: 1000;

也有可能translate3d(x, y, z)不能为某些平台(如 iOS 6)启用硬件加速;我们可以改用–webkit-transform: translate (0)

最终,虽然可能有些情况下我们需要(或更喜欢)使用 jQuery 来动画内容,但应考虑是否它真的是正确的工具,以及是否可以使用 CSS 动画来替代它。

一个很好的例子是在 JSFiddle(jsfiddle.net/ezanker/Ry6rb/1/)上展示的,它使用了来自 Dan Eden 的 Animate.css 库来处理动画,将 jQuery 作为 jQuery Mobile 的依赖项留给了演示中使用的版本。诚然,jQuery 的版本有点旧,但原理仍然非常合理!

小贴士

Treehouse 团队发布了一篇探讨动画和过渡如何影响性能的好博文,值得一读;你可以在blog.teamtreehouse.com/create-smoother-animations-transitions-browser找到它。

让我们转移焦点,继续前进吧。有多少人访问过具有视差滚动效果的网站?视差…滚动…不确定这到底是什么?没问题,在接下来的几页中,我们将看看这是如何成为网页设计中最热门的技术之一的,但如果在我们的项目中没有正确实现它,同样也可能适得其反。

实现响应式视差滚动

视差滚动到底是什么?简单来说,它涉及在向下滚动页面时,以比前景更慢的速度移动背景,以创建三维效果。

最初由 Ian Coyle 于 2011 年为耐克创建,视差滚动是一种流行的技术。它可以提供微妙的深度元素,但如果不正确使用,同样也可能会让人感到不知所措!

想要了解可能性的话,可以看看 Creative Bloq 网站上的文章,链接为www.creativebloq.com/web-design/parallax-scrolling-1131762

目前已经有数十款视差滚动插件可用,比如来自 PixelCog 的 parallax.js 插件(位于pixelcog.github.io/parallax.js/)或 Mark Dalgleish 的 Stellar.js 可以在markdalgleish.com/projects/stellar.js/找到。可以说,最著名的插件是 Skrollr,可以从github.com/Prinzhorn/skrollr下载——这将构成我们下一个演示的基础。

构建一个视差滚动页面

如果你在网上花时间做一些研究,毫无疑问你会看到很多关于如何给网站添加视差滚动效果的教程。在接下来的几页中,我们将以澳大利亚前端开发者 Petr Tichy 的教程为基础,进行我们的下一个练习。毕竟,试图重复造轮子是没有意义的,对吧?

注意

这个原始教程可以查看:ihatetomatoes.net/how-to-create-a-parallax-scrolling-website/

我们的下一个演示将使用广为人知的 Skrollr 库(位于github.com/Prinzhorn/skrollr)来构建一个简单的页面,其中可以滚动查看五张图片,同时还将使用一些效果来控制图片在页面上的滚动方式:

构建视差滚动页面

现在我们已经看到我们的演示将会产生的效果,接下来让我们按照以下步骤来实施:

  1. 我们将从本书附带的代码下载中提取parallax文件夹,并将整个文件夹保存到你的项目区域。

  2. 我们的演示需要一些额外的插件才能工作,所以去下载以下插件:

    把全部这些插件都保存在 parallax 文件夹中的 js 子文件夹中。

  3. 在一个新文件中,添加以下代码;这个代码处理 Skrollr 插件的初始化。让我们详细地介绍一下,从设置一系列变量以及使用 ImagesLoaded 插件预加载图像开始,然后调整它们的大小并在每个部分淡入:

    $(document).ready(function($) {
      // Setup variables
      $window = $(window);
      $slide = $('.homeSlide');
      $slideTall = $('.homeSlideTall');
      $slideTall2 = $('.homeSlideTall2');
      $body = $('body');
    
      //FadeIn all sections
      $body.imagesLoaded( function() {
        setTimeout(function() {
          // Resize sections
          adjustWindow();
    
      // Fade in sections
      $body.removeClass('loading').addClass('loaded');
      }, 800);
    });
    
  4. 在 DOM 函数下面、闭合括号前,添加以下代码。这个代码处理每个幻灯片的调整大小,使其适应窗口高度或至少550px的最小高度,以确保更佳的显示:

    function adjustWindow(){
      var s = skrollr.init();  // Init Skrollr
      winH = $window.height(); // Get window size
    
      // Keep minimum height 550
      if(winH <= 550) { winH = 550; } 
    
      // Resize our slides
      $slide.height(winH);
      $slideTall.height(winH*2);
      $slideTall2.height(winH*3);
    
      // Refresh Skrollr after resizing our sections
      s.refresh($('.homeSlide'));
    }
    
  5. 如果一切顺利,当您预览结果时,图像将在我们向上或向下滚动时从一个图像交叉到另一个图像,如此屏幕截图所示:构建视差滚动页面

视差滚动作为一种技术,当使用得当时可以产生一些真正令人惊叹的效果。一些很好的例子,请参阅 Costa Coffee 的网站,网址为www.costa.co.uk,或 Sony 的 Be Moved 网站,网址为www.sony.com/be-moved/。很难相信这样原创的设计是基于视差滚动的!

提示

查看彼得的一个关于如何使视差滚动响应式的教程,网址为ihatetomatoes.net/make-parallax-website-responsive/

考虑视差滚动的影响

尽管很难相信使用视差滚动可以创建如此漂亮的网站,但必须提出警告:这种技术并不是没有问题的。当然,大多数(如果不是全部)问题都可以通过一些注意和关注来解决;然而,如果在设计和实施过程中不注意,这些问题可能会使任何设计师遇到困难。让我们更详细地探讨一些这些问题:

  • 最大的问题是,视差滚动默认情况下不友好于 SEO。有一些可用的技术可以解决这个问题,例如 jQuery 或多个页面,但它们会影响分析或服务器资源。数字营销策略师卡拉·道森撰写了一篇关于这些解决方案的优点的优秀文章,可在moz.com/blog/parallax-scrolling-websites-and-seo-a-collection-of-solutions-and-examples找到——值得一读!

  • 视差滚动(自然地)需要访客滚动;关键在于确保我们不要创建滚动时间过长的单个页面。这可能会影响移动用户的性能并使访客失去兴趣。

  • 使用 jQuery 来创建基于这种技术的效果本身就可能是一个缺点;jQuery 会影响页面加载时间,因为必须计算页面上每个元素的位置。我们可以通过使用我们在第一章中介绍的技术来自定义我们的 jQuery 的副本来在一定程度上减轻这种影响,但在使用库时性能总会有所降低。

  • 视差滚动可能会揭示许多可用性问题。如果视觉吸引力与内容和易访问性的平衡不均匀,布局对最终用户可能显得杂乱无章。在某些情况下,视差滚动将是合适的选择,例如你可能希望访客仅浏览您的网站一次,或者为公司展示他们的能力。但在你为产品或业务做推介的情况下,这可能会产生负面影响。

  • 在许多情况下,您会发现视差滚动在移动设备上无效;这主要是由于动画在最后执行时会破坏视差滚动。已经尝试解决此问题,但成功的程度各不相同。以下是一些成功尝试的例子:

视差滚动的关键信息是不要仓促行事;的确有一些站点成功地创建了一些令人惊叹的视差滚动示例,但在构建示例时必须经过深思熟虑的规划和规划,以便性能好,满足 SEO 的要求,并为访问者呈现可用的体验。

总结

如果做得好,项目中的内容动画会非常令人满意;这不仅取决于我们使用正确的代码,还要决定 jQuery 是否是正确的工具,或者 CSS 动画是否更适合我们的需求。在过去的几页中,我们涵盖了很多内容,让我们花一点时间来回顾一下我们学到了什么。

我们以讨论使用 jQuery 或 CSS 的优点开始,并讨论了在何时使用其中一种而不是另一种以及使用 CSS 的好处,情况可能决定使用 jQuery。

然后,我们开始讨论了困扰 jQuery 开发人员的经典问题,即控制动画队列;我们看到了如何实施一个快速而简单的修复方法,并随后改进以减少或消除这个问题。

接下来讨论了使用缓动函数的问题;我们看到不仅可以依赖于诸如 jQuery UI 之类的经过验证的源,还可以开发扩展核心 jQuery 的简单动作。我们看了一下如何构建我们自己的自定义缓动函数,然后将我们可能在 CSS 中看到的函数转换为 jQuery 等效函数。

接着,我们通过一些动画示例来结束本章,例如对按钮进行动画处理,实现带有特效的覆盖效果以及在响应式网站上对内容进行动画处理。

在下一章中,我们将深入研究高级事件处理。在大多数情况下,人们使用.on().off(),但正如我们将看到的,这只是 jQuery 可能性的冰山一角。

第七章:高级事件处理

你有多少次访问网站执行一个操作?它可能是在线银行业务,或者从亚马逊购买东西;在这两种情况下,网站将检测到正在发生的动作,并作出相应的反应。

使用 jQuery 的一部分是知道何时以及如何响应不同类型的事件。在大多数情况下,人们可能会使用.on().off()事件处理程序来处理它们。虽然这样做完全没问题,但它只是触及了事件处理的表面。在本章中,我们将探讨一些可以帮助我们扩展事件处理技能的技巧和窍门。我们将涵盖以下主题:

  • 事件委托

  • 使用$.proxy函数

  • 创建和解耦自定义事件类型

  • 事件命名空间

有兴趣吗?那我们就开始吧!

介绍事件处理

一个问题 - 你多久上线执行一项任务?我敢打赌这是每周都会发生的事情;它可能是任何事情,从在线银行业务到点击亚马逊购买最新的 DVD(DVD - 谁会下载它们,我想知道?)

话虽如此,我们无法避免必须点击链接或按钮来通过流程。在大多数情况下,事件背后的代码可能是普遍存在的点击处理程序,或者甚至可能是.change().hover()。所有这些都是.on()(甚至.off())事件处理程序的简写形式,并且当然与以下内容的功能等效:

$('a').on('click', function(){
  $(this).css('background-color','#f00');
});

这将使所选元素变成一个漂亮的红色。然而,事件处理不仅仅是在已知元素上定义一个操作。在接下来的几页中,我们将(引用一个航海术语)冒险一试,并查看一些可以帮助我们进一步发展技能的提示和技巧。我们将从事件委托开始。

事件委托

有人曾经说过,成为一名优秀的经理的艺术就是知道什么时候委派任务。我希望这不是他们把一个可怕的工作推卸给下属的借口,尽管愤世嫉俗的人可能会说另外一种看法!

撇开风险,事件委托遵循着 jQuery 中相同的原则。如果我们需要创建一个需要将某种形式的事件处理程序绑定到大量相同类型元素的应用程序,那么我们可以考虑编写事件处理程序来覆盖每个元素。

它在某种程度上是有效的,但非常浪费资源。如果列表很大,那么事件将绑定到其中所有的元素,这会比所需的内存更多。我们可以通过使用事件委托来解决这个问题,我们可以将一个事件处理程序绑定到一个祖先元素,该元素为多个后代服务,或者为新创建的元素启用事件处理。

有一些技巧可以帮助我们更好地使用委托来管理事件。在我们看看它们之前,让我们快速回顾一下事件委托的基本原理。

重新审视事件委托的基础

一个问题——你在使用 jQuery 编写事件处理程序时有多少次使用过.on(),甚至.off()?我敢打赌答案可能是无数次。如果你之前没有使用过事件委托,那么你已经无意中使用了一半!

事件委托依赖于使用事件传播,有时也称为事件冒泡。这是理解委托工作原理的关键。让我们通过一个快速的示例来说明。

想象一下,我们正在使用以下 HTML 代码作为列表的基础:

<div id="container">
  <ul id="list">
    <li><a href="http://domain1.com">Item #1</a></li>
    <li><a href="/local/path/1">Item #2</a></li>
    <li><a href="/local/path/2">Item #3</a></li>
    <li><a href="http://domain4.com">Item #4</a></li>
  </ul>
</div>

这里没有什么特别的——这是一个简单的示例。每当我们的锚点标签之一被点击时,都会触发一个点击事件。事件在三个阶段之一中分派:捕获目标冒泡

它将被捕获到文档根,在命中目标(li标签)之前向下工作,然后冒泡回文档根,如下所示:

  • 文档根

  • <html>

  • <body>

  • <div #container>

  • <ul #list>

  • <li>

  • <a>

哎呀!这意味着每次点击链接时,实际上都在点击整个文档!不好!这会消耗资源,即使我们使用如下代码添加了额外的列表项:

$("#list").append("<li><a href='http://newdomain.com'>Item #5</a></li>");

我们会发现上述的点击处理程序不适用于这些项目。

提示

这里使用的冒泡示例有些简化,并没有显示所有的各种阶段。要进行有用的讨论,请前往 Stack Overflow 上发布的评论 stackoverflow.com/questions/4616694/what-is-event-bubbling-and-capturing

重新调整我们的代码

我们可以利用事件传播来重新调整我们的处理程序,监听后代锚点,而不是仅绑定到现有锚点标签。这可以在以下代码中看到:

$("#list").on("click", "a", function(event) {
  event.preventDefault();
  console.log($(this).text());
});

代码中唯一的区别是我们将a选择器移动到.on()方法的第二个参数位置。这将创建一个针对#list的单个事件处理程序,事件从a向上冒泡到#list。事件委托消除了创建多个事件处理程序的需要,这是一种浪费资源的做法——代码将同样适用于#list内现有的锚点标签,以及将来添加的任何锚点标签。

提示

如果您想了解更多关于事件委托的信息,建议查看 jQuery API 文档,网址是 learn.jquery.com/events/event-delegation/。jQuery 文档还有一个关于在委托事件中使用.on()的有用部分,网址是 api.jquery.com/on/

支持旧版浏览器

一个小提示 - 如果你需要重构旧代码,那么你可能会看到.bind().live().delegate()作为事件处理程序。在 jQuery 1.7 之前,所有这些都用于委托事件,但现在应该替换为.on()。事实上,第一个.bind是一个调用.on(及其伴侣.off())的单行函数:

支持旧版浏览器

同样适用于.delegate()及其配对事件处理器.undelegate()

支持旧版浏览器

应该注意.on()模仿了使用.bind().delegate()时发现的行为。前者非常耗资源,因为它会附加到每个可以匹配的元素;后者仍然需要确定要调用哪个事件处理程序。然而,与使用.bind()方法相比,这种情况的范围应该较小。

现在我们已经深入了解了.on()的内部工作原理,让我们将其付诸实践,并创建一个简单的演示来提醒自己 jQuery 中事件委托的工作方式。

探索一个简单的演示

现在是行动的时候了,让我们快速回顾一下在使用 jQuery 时事件委托是如何工作的:

  1. 让我们从伴随本书的代码下载中提取我们需要的文件。对于这个演示,我们需要simpledelegation.htmlsimpledelegation.cssjquery-ui.min.css文件。

  2. 将 CSS 文件保存在项目区域的css子文件夹中。HTML 标记需要存储在项目文件夹的根目录中。

  3. 在一个新文件中,添加以下代码 - 将文件保存为simpledelegation.js,并将其存储在项目区域的js子文件夹中:

    $(document).ready(function(event){
      var removeParent = function(event) {
        $('#list').parent('li').remove();
      }
    
      var removelistItem = function(event) {
        $(event.target).parent().remove();
      }
    
      $('li.ui-widget-content').children().on("click", removeParent);
    
      $('ul').on("click", "li", removelistItem);
    });
    
  4. 如果一切正常,当在浏览器中预览结果时,我们应该看到以下项目列表:探索一个简单的演示

  5. 尝试点击一些链接 - 如果你点击任何一个移除链接,那么列表项将被移除;点击其中一个列表项将会移除列表中的所有项。

这个演示的关键在于以下一行:

$('ul').on("click", "li", removelistItem);

尽管列表中有多个项目,但我们创建了一个单一的委托事件处理程序。它冒泡到我们点击的<li>项的父级,然后将其移除。在这种情况下,我们将触发事件时调用的函数分开了出来;这很容易可以合并到处理程序中。

现在我们重新审视了事件委托的基础知识,让我们看看事件委托为何可以在处理许多相似元素时提高性能。

探索使用事件委托的影响

在直接等效物代替委托事件的关键好处是减少内存使用量,避免代码中存在多个事件处理程序时的内存泄漏。通常,我们需要为需要发生某些事情的每个实例实现一个事件处理程序。

注意

使用事件委托的真正影响在于内部数据结构中存储事件处理程序定义所带来的内存使用量节约。

相反,减少事件处理程序的数量意味着我们可以减少内存泄漏,改善性能(通过减少需要解析的代码量)。只要我们小心绑定事件处理程序的位置,就有潜力显著减少对 DOM 的影响和由此产生的内存使用量,特别是在更大的应用程序中。好处是,如果已实施事件委托,它将同样适用于已定义的现有元素,以及尚未创建的元素。直接应用的事件处理程序将不起作用;它们只能应用于在代码中调用事件处理程序之前已经存在的元素。

处理已存在和尚未发生的事件的能力听起来是件好事。毕竟,如果一个事件处理程序可以处理多个事件,那么为什么要重复自己呢?绝对可以 - 只要我们小心管理!如果我们在特定元素上触发事件,例如一个锚点标签,那么这将被允许首先处理事件。事件会冒泡直到达到文档级别,或者更低级别的事件处理程序决定停止事件传播。这最后一部分很关键 - 没有控制,我们可能会得到意外的结果,其中事件处理程序的响应与预期相反,可能也没有被触发。

提示

要看详细的解释可以发生什么,可以看看css-tricks.com/capturing-all-events/。它包含了指向 CodePen 上很好地说明了这个问题的示例的链接。

为了帮助减少事件冒泡引起事件处理程序无序触发的影响,我们使用诸如event.stopPropagation()之类的方法。这不是我们唯一能使用的技巧,因此让我们花点时间来探讨在使用事件委托时可用的一些选项。

控制委托

利用事件冒泡增加了我们在代码中实现的事件处理程序数量的减少的范围;缺点在于出现意外行为的实例,其中事件处理程序可能在期望的点上没有被触发。

要控制哪些元素可能触发委托事件处理程序,我们可以使用以下两种技巧之一:event.stopPropagation(),或者捕获事件目标并确定它是否符合给定的一组条件(例如特定类或data-名称)。

让我们首先来看看这第二个选项 - 一个代码块的例子可能如下所示:

$("ul.my-list").click(function(event) {
  if ( $( event.target).hasClass("my-item") ) {
    handleListItemAction(event.target);
  }
else if ( $( event.target).hasClass("my-button") ) {
    handleButtonClickedAction(evt.target);
  }
});

那是一种笨拙的做事方式!相反,我们可以简单地对类名进行检查,使用下面显示的委托事件处理程序的变体:

$("ul.my-list").on("click",".my-item", function(evt) {
  //do stuff
});

这是一个非常简单的技巧,我们可以使用它 - 它非常简单,可能不算是一个技巧!为了看到改变有多容易,让我们现在快速进行一个演示:

  1. 从代码下载中,我们需要提取propagation-css.htmlpropagation.html文件。这些文件包含了一些简单的标记和样式,用于我们的基本列表。将 CSS 文件保存在我们项目区域的css子文件夹中,将 HTML 标记保存在同一区域的根目录中。

  2. 接下来,我们需要创建事件处理程序,当条件匹配时将触发。继续添加以下内容到一个新文件中,并将其保存为propagation-css.js,保存在我们项目区域的js子文件夹中:

        $(document).ready(function() {
          $('#list').on('click', '.yes', function eventHandler(e) {
            console.log(e.target);
          });
        });
    

此时,如果我们在浏览器中预览结果,我们将得到一个简单的列表,其中列表项在我们悬停在特定项上时会变暗。这没什么特别的 - 它只是从 jQuery UI 中借用了一些样式。

但是,如果我们启动一个 DOM 检查器,比如 Firebug,然后悬停在每个项目上,我们可以看到每次悬停在一个类为.yes的项目上时都会添加控制台输出:

控制委托

因此,我们不像探索一个简单的演示中那样提供选择器,我们简单地使用了一个类名;只有当事件处理程序函数与指定的类名匹配时,事件处理程序函数才会被触发。

提示

我们甚至可以使用data-标签作为我们的检查:

$(document).on('keypress', '[data-validation="email"]', function(e) {
  console.log('Keypress detected inside the element');
})

作为替代方法使用 stopPropagation()方法

作为替代方法,我们可以使用一个全 jQuery 的解决方案,形式为stopPropagation()。这可以防止事件冒泡到 DOM 树,并阻止任何父处理程序被通知事件。

这一行代码的实现非常简单,尽管使用它的关键在于确保我们将其添加到我们代码的正确位置。如果你以前没有使用过它,那么它需要放在事件处理程序内,在处理程序的最后一个命令之后立即添加(如下面的片段中所突出显示的那样):

document.ready(function ($) {
  $('div'). on("click", function (event) {
    console.log('You clicked the outer div');
  });
  $('span').on("click", function (event) {
    console.log('You clicked a span inside of a div element');
    event.stopPropagation();
  });
})

作为快速检查,尝试从附带本书的代码下载中提取propagation-js文件。将它们保存在我们项目区域内的相关文件夹中。如果我们在浏览器中预览它们,我们会看到一个简单的span包含在div内。参考下面的图片:

作为替代方法使用 stopPropagation()方法

这个演示的关键在于 DOM 检查器。尝试点击灰褐色的外环,或其中的 span,我们将看到我们选择的结果出现在控制台日志中,如下所示:

作为替代方法使用 stopPropagation()方法

如果你在代码中注释掉event.stopPropagation()行,那么附加到div的点击事件也将被调用。

提示

除非必要,否则不应该停止事件传播。有一篇有用的文章位于css-tricks.com/dangers-stopping-event-propagation/,其中讨论了如果停止传播可能遇到的问题。

好的,让我们改变焦点,转到事件处理程序中的另一个关键概念。是时候看看使用 $.proxy 函数了,以及为什么需要它,如果事件委托不能满足我们的需要。

使用 $.proxy 函数

到目前为止,我们已经介绍了如何利用事件冒泡可以帮助我们减少大量事件处理程序的需求;只要我们仔细管理冒泡,委托就可以成为使用 jQuery 进行开发的非常有用的工具。

另一面的情况是,在某些情况下,我们可能需要帮助 jQuery;当它的传播不足够高!起初这可能不合理,所以让我解释一下我的意思。

假设我们有一个作为对象创建的事件处理程序,并且当点击链接时,我们想调用它:

var evntHandlers = {
  myName : 'Homer Simpson',

  clickHandler : function(){
    console.log('Hello, ' + this.myName);
  }
};

$("a").on('click',evntHandlers.clickHandler);

如果我们在浏览器中运行这个代码,您期望在控制台日志区域看到什么?

提示

要找出答案,请尝试从随本书附带的代码下载中提取 proxy-before.html 文件。确保您安装了 DOM 检查器!

如果您期望看到 你好,霍默·辛普森,那我将让您失望;答案不会是您期望的,而是 你好,未定义,如下图所示:

使用  函数

好的,这是怎么回事?

原因是所使用的上下文位于 clickHandler 事件中,而不是 evntHandler 对象中;我们在 clickHandler 事件中没有 myName 属性。

幸运的是,这有一个简单的解决方法。我们可以使用 $.proxy 强制更改上下文,如下所示:

var evntHandlers = {
  myName : 'Homer Simpson',
  clickHandler : function(){
    console.log('Hello, ' + this.myName);
  }
};

$("a").on('click',$.proxy(evntHandlers.clickHandler,evntHandlers));

要看到这种情况的效果,请从随本书附带的代码下载中提取 proxy-before.htmlproxy-after.html 文件。如果在浏览器中运行它们,你将看到与下图中显示的相同的结果:

使用  函数

这是一个简单的改变,但它打开了各种可能性。这是一种设定闭包上下文的简易方法。当然,我们也可以使用纯 JavaScript 的.bind()方法。但是,使用 $.proxy 确保传入的函数是实际上是一个函数,并且向该函数传递了唯一的 ID。如果我们给我们的事件添加命名空间,我们可以确保解绑正确的事件。$.proxy 函数被视为 jQuery 中的单个函数,即使它被用来绑定不同的事件。使用命名空间而不是特定的代理函数将避免在代码中解绑错误的处理程序。

注意

如果您想了解更多关于使用 $.proxy 的内容,那么值得阅读主要 jQuery 网站上的文档,可以在api.jquery.com/jquery.proxy/上找到。

为了让我们真正了解可能性,考虑一下这个问题:你有多少次最终以三到四层深度嵌套的函数结束了?考虑以下代码:

MyClass = function () {
  this.setupEvents = function () {
    $('a').click(function (event) {
      console.log($(event.target));
    });
  }
}

而不是使用上述代码,我们可以通过使用$.proxy来重构代码以增加可读性,如下所示:

MyClass = function () {
  this.setupEvents = function () {
    $('a').click( $.proxy(this, 'clickFunction'));
  }

  this.clickFunction = function (event) {
    console.log($(event.target));
  }
}

我认为你会同意这样更容易阅读,对吧?

好的 - 让我们继续吧。我相信我们都很熟悉在 jQuery 中创建事件处理程序。但是,很可能你正在使用标准事件处理程序。这些可以很好地工作,但我们仍然受限于我们所能做的事情。

那么,让我们改变这一点。使用 jQuery,我们可以创建打破我们知道可能性的常规的自定义事件,并且允许我们创建各种事件处理程序。让我们看看我们如何在实际操作中做到这一点。

创建和解耦自定义事件类型

如果你花了一些时间开发 jQuery,那么我相信你对我们可以使用的标准事件类型非常熟悉,比如.click().hover().change()等。

这些都有用途,但都有一个共同点 - 我们在使用它们时受到了一些限制!我们的代码将受到这些处理程序可以做到的程度的限制。如果我们能打破这种限制,创建任何类型的自定义事件处理程序会怎样呢?

当然,我们始终可以将多个事件组合在一起,由同一个函数来处理:

$('input[type="text"]').on('focus blur', function() {
  console.log( 'The user focused or blurred the input' );
});

但这仍然局限于那些现成的事件处理程序。我们需要的是打破常规,在设计我们自己的处理程序时发挥创意。

没问题 - 我们可以使用 jQuery 的特殊事件功能构建几乎任何类型的事件以满足我们的需求。这打开了一个真正的可能性世界,可能需要一本专门的书来介绍。在接下来的几页中,我们将介绍一些概念,以帮助您开始创建事件。

小贴士

要深入了解创建自定义事件,请参阅 learn jQuery 网站上的一篇有用的文章,网址为learn.jquery.com/events/introduction-to-custom-events/

事件的好处是它们的行为就像它们的标准版本一样,包括在 DOM 中冒泡:

$('p').bind('turnGreen', function() { 
  $(this).css('color', '#00ff00');
});

$('p:first').trigger('turnGreen');

那么,特殊事件的组成是什么?特殊事件通常采用插件的形式;格式可能类似,但我们经常会看到几个fixHooks中的任何一个,我们用它来控制 jQuery 中事件处理的行为。

注意

jQuery 特殊事件钩子是一组按事件名称分组的函数和属性,允许代码控制 jQuery 内部事件处理的行为。

让我们花一点时间来看一下特殊事件插件的典型组成,然后再深入介绍一个这样的插件的示例。

创建自定义事件

fixHooks 接口提供了规范或扩展将覆盖本机浏览器事件对象的路径。我们通常会看到类似以下格式的事件插件被使用:

jQuery.event.special.myevent = {
  noBubble: false;
  bindType: "otherEventType",
  delegateType: "otherEventType",
  handle: function ($event, data { 
    // code
  },
  setup: function( data, namespaces, eventHandle ) {
    // code
  },
  teardown: function( namespaces ) {
    // code
  },
  add: function( handleObj ) {
    // code
  },
  remove: function( handleObj ) {
    // code
  },
  _default: function( event ) {
    // code
  }
};

值得注意的是,在创建特殊事件类型时,我们经常会使用两种方法 - .on(),用于绑定事件,以及.trigger(),用于在需要时手动触发特定事件。此外,特殊事件插件将公开许多有用的关键方法,值得学习。让我们来探索一下:

方法/属性名称 目的
noBubble: false 布尔类型,默认值为false。指示如果调用.trigger()方法,是否应用冒泡到此事件类型。
bindType 当定义时,这些字符串属性指定应该如何处理特殊事件类型,直到事件被传递。对于直接附加的事件,请使用bindType,对于已委托的事件,请使用delegateType。在这两种情况下,这些都应该是标准的 DOM 类型,例如.click()
handle: function(event: jQuery.Event, data: Object) 当事件发生时调用处理程序钩子,jQuery 通常会调用用户通过.on()或其他事件绑定方法指定的事件处理程序。
setup: function(data: Object, namespaces, eventHandle: function) 第一次将特定类型的事件附加到元素时调用。这提供了一个机会,可以处理将应用于该元素上此类型的所有事件的处理。
teardown: function() 当特定类型的最终事件从元素中移除时调用。
add: function(handleObj)``remove: function(handleObj) 当通过.on()等 API 向元素添加事件处理程序时调用,或者在使用.off()时移除时调用。
_default: function(event: jQuery.Event, data: Object) 当从代码中使用.trigger().triggerHandler()方法触发特殊类型事件时调用,而不是由浏览器内部发起的事件。

如果您在开发中使用 jQuery Mobile,了解这些方法非常重要。移动端依赖特殊事件来产生事件,如tapscrollstartscrollstopswipeorientationchange

提示

要了解每种方法的更多细节,请查看gist.github.com/cowboy/4674426上的 Ben Alman 的 Gist。

如果您正在使用特殊事件来覆盖标准事件行为,那么就需要更深入地了解这些特殊事件。如果想要更多地了解内部工作原理,值得阅读 jQuery Learning Site 上的这篇文章:learn.jquery.com/events/event-extensions/。请注意 - 这可能会变得非常复杂!

现在我们已经看到了一个特殊事件插件的一些内部工作原理,现在是时候投入使用并看到它在实际中的效果了。为此,我们将使用由 James Greene 制作的 jQuery Multiclick 插件,以展示捕获例如三次点击并将其用于执行操作的简单性。

使用多次点击事件插件

创建自定义事件可以简单也可以复杂。对于此演示,我们将使用 James Greene 的 jQuery Multiclick 事件插件。该插件可从jamesmgreene.github.io/jquery.multiclick/获取。我们将使用它在屏幕上发布一些消息,并且在每三次点击时更改消息。参考以下图片:

使用多次点击事件插件

让我们看一下涉及的内容:

  1. 让我们从附带本书的代码下载中提取以下文件。对于此演示,我们将需要jquery.multiclick.jsjquery.min.jsmulticlick.cssmulticlick.html文件。将每个文件存储在项目区域内的相应子文件夹中。

  2. 在一个新文件中,添加以下代码,并保存为multiclick.js

    $(document).ready(function() {
      var addText = "Click!<br>";
      var addBoom = "Boom...!<br>";
    
      $("button").on("click", function($event) {
        $("p").append(addText);
      });
    
      $("button").on("multiclick", { clicks: 3 }, function($event) {
        $("p").append(addBoom);
      });
    });
    
  3. 这是配置多次点击插件所必需的,并在鼠标点击时触发相应的响应。

  4. 尝试在浏览器中运行演示。如果一切正常,一旦我们点击Click me!按钮几次,我们应该会看到与练习开始时显示的屏幕截图类似的东西。

尽管可能必须说这并不完全代表一个真实世界的例子,但所涉及的技术仍然是相同的。插件绑定到标准的点击处理程序,并且如果达到的点击次数是插件配置选项中指定值的倍数,则会触发。

事件命名空间

到目前为止,我们已经看到了如何委托事件并创建可以接受自定义触发器的处理程序。如果我们有一个单击事件处理程序,那么这些方法就非常完美,但是如果我们需要有多个点击处理程序呢?

好在,这里有一个简单的解决方案:给事件添加一个命名空间!而不是讨论它的工作原理,让我们快速看一下以下示例:

$("#element")
  .on("click", doSomething)
  .on("click", doSomethingElse);

这段代码是完全可接受的 - 没有任何问题!当然,这可能不像一些人所希望的那样可读,但我们现在并不担心这一点!

这里的关键点是如果我们调用:

$("#element").off("click");

然后我们不仅会丢失第一个点击处理程序,还会丢失第二个点击处理程序。这不是理想的。我们可以通过添加命名空间或标识符来修复此问题,如下所示:

$("#element")
  .on("click.firsthandler", doSomething)
  .on("click.secondhandler", doSomethingElse);

如果我们现在运行相同的.off命令,显然两个事件处理程序都不会被移除。但是 - 假设我们做出以下更改:

$("#element").off("click.firsthandler");

现在我们可以安全地移除第一个事件处理程序,而不会移除第二个。

注意

如果我们写的是 $("#element").off(".firsthandler"),那么它将删除所有拥有该命名空间的事件处理程序。这在开发插件时非常有用。

理解这是如何工作的最好方法,就是看它在实际中的表现。现在让我们来看下面这个简单的例子吧:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Demo: Adding namespaces to jQuery events</title>
  <script src="img/jquery.min.js"></script>
</head>
<body>
  <button>display event.namespace</button>
  <p></p>
  <script>
    $("p").on("test.something", function (event) {
      $("p").append("The event namespace used was: <b>" +
event.namespace + "</b>");
    });
    $("button").click(function(event) {
      $("p").trigger("test.something");
    });
  </script>
</body>
</html>

注意

此演示的代码可在随书附带的代码下载中找到,名为 namespacing.html 文件。您需要提取它以及 jQuery 的副本才能运行演示。

在这里,我们分配了两个调整大小的函数。然后我们使用命名空间删除第二个,这将完全不影响第一个,如下图所示:

事件命名空间

如果我们使用 DOM Inspector 来检查代码,我们可以清楚地看到分配的命名空间;要做到这一点,设置一个断点在第 12 行,然后展开右侧的列表,如下图所示:

事件命名空间

起初,这可能看起来像是一个非常简单的改变,但我非常相信 KISS 这个短语 - 你懂的!

提示

使用的命名空间的深度或数量没有限制;例如,resize.layout.headerFooterContent。命名空间也可以与标准事件或自定义事件处理程序一样使用。

添加命名空间标识符是我们可以应用于任何事件处理程序的一个非常快速简便的方法。它使我们对任何事件处理程序都有完美的控制,特别是在我们的代码中为多个实例分配函数到相同事件类型时。

注意

如果您经常创建复杂的事件处理程序,则可能值得查看 Mark Dalgleish 的 Eventralize 库,该库可从 markdalgleish.com/projects/eventralize/ 获取。注意,它已经有 2-3 年没有更新了,但测试一下看它是否有助于整合和简化您的事件可能是值得的。

摘要

事件处理对于任何网站或在线应用的成功至关重要。如果处理正确,可以打造一个引人入胜的用户体验;而处理不当则可能导致一些意想不到的结果!在过去的几页中,我们已经研究了一些概念,以帮助我们发展事件处理技能;让我们花点时间回顾一下我们学到的东西。

我们先快速介绍了事件处理,然后迅速转移到探讨事件委托作为我们能够从中受益的工具之一。我们首先看了事件委派的基础知识,然后检查了使用它的影响,并学习了如何在我们的代码中控制它。

接下来是查看 $.proxy,在那里我们看到 jQuery 有时需要帮助,以确保在我们的代码意味着事件没有在足够高的位置传播时,事件在正确的上下文中被触发。

然后,我们将注意力转向简要介绍创建自定义事件类型和处理程序,然后探讨这些事件处理程序是如何构建的。然后,我们以 jQuery Multiclick 插件作为示例,展示了如何创建这些自定义事件处理程序,最后以使用命名空间来确保我们能够在代码中绑定或解除绑定正确的事件处理程序来结束本章。

在下一章中,我们将看一些视觉方式如何增强我们的网站 - 我们将看到如何应用效果,并管理结果效果队列如何帮助我们的网站的成功与否。

第八章:使用 jQuery 效果

在任何网站上添加事件处理程序是一个必要的步骤;毕竟,我们需要一些方式来响应我们代码中的合法事件。

另一方面,添加效果的反面是,如果做得好,它们可以带来巨大的回报,尽管其中一些新奇感可能会消失,特别是如果你已经过度使用了所有的核心效果!通过新的自定义效果使你的网站焕发活力 - 我们将在本章中看到如何做到这一点,以及如何管理生成的队列。在接下来的几页中,我们将涵盖以下主题:

  • 重温基本效果

  • 添加回调

  • 构建自定义效果

  • 创建和管理效果队列

感兴趣吗?让我们开始吧...

重温效果

一个问题 - 你多少次访问网站,看到内容平稳地向上滑动,或逐渐淡化至无?

我相信你当然会认出这些代码中提供的效果;这些可以是从简单的向上滑动到内容逐渐从一幅图像或元素淡入另一幅图像或元素的任何内容。

创建效果是任何网站的重要考虑因素。我们在本书的早些时候已经涉及了一些方法,在第六章中,使用 jQuery 进行动画。我相信我们都很熟悉淡入淡出或切换元素的基本代码。毫无疑问,在开发网站时,你会无数次地使用诸如$("blockquote").fadeToggle(400);$("div.hidden").show(1250);这样的代码。

看起来很熟悉?在接下来的几页中,我们将介绍一些额外的技巧,可以帮助我们在添加效果时取得更好的效果,并考虑使用 jQuery 提供这些效果的一些影响。在此之前,有一个重要的问题需要澄清,那就是探索简单动画和向元素添加效果之间的关键区别。

探索动画和效果之间的差异

也许有些人会认为我们在第六章中讨论动画时已经涵盖了效果的提供,这是正确的,它们之间确实有一些重叠;快速查看 jQuery 效果的 API 列表将显示.animate()作为有效的效果方法。

然而,有一个重要的区别 - 我们已经讨论过的内容是关于移动元素的,而提供效果将专注于控制内容的可见性。不过,很棒的是,我们可以将这两者联系在一起。.animate()方法可以用来在代码中实现移动和效果。

现在这个小区别已经澄清了,让我们开始行动吧。我们将首先看一下如何向我们的效果添加自定义缓动函数。

创建自定义效果

如果你使用过.animate()或其快捷方法,比如.fadeIn().show().slideUp()对动画元素应用效果,那么你很可能都使用过它们。所有这些方法都遵循类似的格式,至少需要提供一个持续时间、缓动类型,还有可能需要提供一个回调函数,在动画完成时执行一个任务,或者在控制台中记录一些内容。

然而,我们在决定时往往会坚持使用标准值,比如slowfast,或者可能是一个数值,比如500

$("button").click(function() {
  $("p").slideToggle("slow");
});

使用这种方法绝对没错,只是非常无聊,而且只能发挥出了很小一部分可能性。

在接下来的几页中,我们将探索一些技巧,用来拓宽我们在应用效果时的知识,了解到我们并不总是必须坚持已经验证过的方法。在我们探索这些技巧之前,不妨先了解一下这些效果在 Core jQuery 库中是如何处理的。

探索animate()方法作为效果的基础

如果你被要求使用预配置的效果,比如hide()slideToggle(),那么你可能期望在 jQuery 内部使用一个命名函数。

注意

注意,本节中给出的行号适用于未压缩版本的 jQuery 2.1.3,可从code.jquery.com/jquery-2.1.3.js获取。

好吧,这是对的,但只是部分正确:jQuery 内部预配置的函数都是指向animate()的简写指针,就像在68296840行附近所示的那样。它们经过了两个阶段的处理:

  • 第一阶段是向genFX()方法传递三个值,即showhidetoggle

  • 这之后传递给animate()方法来产生最终效果,在67086725行。

代码中快速查看每个可用的值以及它们如何传递给.animate()

探索方法作为效果的基础

我们在第六章 使用 jQuery 进行动画 中详细介绍了animate()的用法。以下是关于在我们的代码中使用animate()的几个关键要点:

  • 只能支持取数值的属性,虽然有一些例外情况。一些值,比如backgroundColor,没有插件的情况下是无法进行动画的(jQuery Color – github.com/jquery/jquery-color, 或 jQuery UI – www.jqueryui.com),还有一些属性可以取多个值,比如background-position

  • 可以通过使用适用的任何标准 CSS 单位来对 CSS 属性进行动画 – 完整列表可在www.w3schools.com/cssref/css_units.asp中查看。

  • 元素可以使用相对值移动,这些相对值在属性值前加上+=-=。如果设置了持续时间为0,则动画将立即将元素设置为它们的最终状态。

  • 作为快捷方式,如果传递了toggle的值,动画将简单地从当前位置反转,并动画到目标位置。

  • 通过单个animate()方法设置的所有 CSS 属性将同时进行动画处理。

现在我们已经看到了库中如何处理自定义效果,让我们探索创建一些新的效果,这些效果结合了库中已经可用的效果。

将自定义效果付诸实践

如果我们花费时间开发代码,限制在使用 jQuery 中可用的默认效果,我们很快就会超出它所能做的限制。

为了防止这种情况发生,值得花时间去研究我们真正想使用的效果,并看看我们是否可以从 jQuery 内部构建一些东西来复制它们。为了证明这一点,我们将深入一些示例;我们的第一个示例是基于点击选定元素产生一个切换效果。

创建一个 clickToggle 处理程序

我们三个示例中的第一个的灵感不来自在线评论,而是来自 jQuery 本身。核心库有一个可用的切换函数(如api.jquery.com/toggle-event/所示),在版本 1.8 中已弃用,在 1.9 中已移除。

我们将探讨如何添加类似的功能,使用一个小型插件,想法是根据插件中设置的值的状态运行两个函数中的一个:

创建 clickToggle 处理程序

让我们看看需要什么:

  1. 我们将从本书的代码下载中提取相关文件开始。对于此演示,我们需要clicktoggle.cssjquery.min.jsclicktoggle.html文件。将 CSS 文件放在css子文件夹中,jQuery 库放在js子文件夹中,并将标记文件放在项目区域的根目录下。

  2. 在一个新文件中,我们需要创建我们的clicktoggle()事件处理程序,所以继续并添加以下代码,并将其保存为clicktoggle.js

    $.fn.clicktoggle = function(a, b) {
      return this.each(function() {
        var clicked = false;
        $(this).on("click", function() {
          if (clicked) {
            clicked = false;
            return b.apply(this, arguments);
          }
          clicked = true;
          return a.apply(this, arguments);
        });
      });
    };
    

    注意

    apply()函数用于调用函数的上下文 - 更多细节,请参阅api.jquery.com/Types/#Context.2C_Call_and_Apply

  3. clicktoggle事件处理程序的下方立即添加以下函数:

    function odd() {
      $("#mydiv").append("Your last number was: odd<br>");
    }
    
    function even() {
      $("#mydiv").append("Your last number was: even<br>");
    }
    
    $(document).ready(function() {
      $("#mydiv").clicktoggle(even, odd);
    });  
    The first two look after adding the appropriate response on screen, with the third firing off the event handler when text has been clicked.
    
  4. 如果一切顺利,我们应该看到与练习开始时显示的屏幕截图类似的东西,在那里我们可以看到文本已经被点击了几次。

    注意

    许多人已经产生了类似版本的代码 - 请参阅gist.github.com/gerbenvandijk/7542958作为一个例子;这个版本使用了data-标签并将处理函数合并到一个调用中。

好的,让我们继续,看看另一个示例:在这个示例中,我们将创建一个滑动淡入淡出切换效果。这将使用与前面示例相似的原理,我们将检查元素的状态。这次,我们将使用 :visible 伪选择器来确认应该在屏幕上呈现哪个回调消息。

注意

作为一个想法,为什么不尝试将这个插件与 Toggles 插件结合使用呢?这可以用来制作一些漂亮的开关按钮。我们然后可以触发由本例中创建的 clickToggle 插件处理的事件。

使用滑动淡入淡出切换内容

在我们之前的示例中,我们的效果在屏幕上出现得非常突然 - 要么是一个声明,要么是另一个声明,但没有中间状态!

从视觉效果来看,这并不总是理想的;如果我们能让过渡更平滑,那会给人留下更柔和的印象。这就是滑动淡入淡出切换插件的作用。让我们看看如何创建它:

  1. 我们将像往常一样,从附带本书的代码下载中提取我们需要的相关文件。对于这个演示,我们将需要常见的 jquery.min.js,以及 slidefade.cssslidefade.html。JavaScript 文件需要放在 js 子文件夹中,样式表需要放在 css 子文件夹中,HTML 标记文件需要放在我们项目区域的根目录中。

  2. 在一个新文件中,让我们继续创建 slideFadeToggle 效果。将以下行添加到文件中,将其保存为 slidefade.js,并将其放在 js 子文件夹中:

    jQuery.fn.slideFadeToggle = function(speed, easing, callback) {
      return this.animate({opacity: 'toggle', height: 'toggle'}, speed, easing, callback);
    };
    
    $(document).ready(function() {
      $("#sfbutton").on("click", function() {
        $(this).next().slideFadeToggle('slow', function() {
          var $this = $(this);
          if ($this.is(':visible')) {
            $this.text('Successfully opened.');
          } else {
            $this.text('Successfully closed.');
          }
        });
      });
    });
    
  3. 如果一切顺利,那么当我们在浏览器中预览结果时,我们应该看到黑灰色的方块在我们单击按钮后淡出,这在以下图片中显示:滑动淡入淡出切换内容

代码创建了一个漂亮的警告效果 - 它可以用来在您的网站内向访问者显示适当的消息,因为它滑入视图。我们的插件是基于在两个状态之间切换。如果您的首选是仅仅使用 fadeIn()fadeOut() 状态的等效值,那么我们可以根据需要轻松地使用其中任何一个函数:

$.fn.slideFadeIn  = function(speed, easing, callback) {
  return this.animate({opacity: 'show', height: 'show'}, speed, easing, callback);
};

$.fn.slideFadeOut  = function(speed, easing, callback) {
  return this.animate({opacity: 'hide', height: 'hide'}, speed, easing, callback);
};

好的,让我们继续。我们已经创建了一些自定义效果,但感觉还是缺了点什么。啊,是的 - 我知道了:从一个状态到另一个状态的缓动怎么样?(是的,绝对是双关语!)

我们可以添加一个缓动功能,不仅可以简单地设置慢速、快速、正常速度甚至是数字值来控制效果的持续时间,还可以增加一个缓动功能,使效果更具动感。让我们深入了解一下涉及到的内容。

对效果应用自定义缓动函数

如果有人对你提到 "缓动" 这个词,我敢打赌会发生两件事中的一件:

  • 你很可能会认为你需要使用 jQuery UI,这可能会向页面添加相当大的代码块

  • 你可能会逃跑,一想到要解决一些可怕的数学问题!

不过,这里的讽刺是,对于两者的答案可能是肯定的也可能是否定的(至少对于第二个评论的前半部分而言)。等等,怎么回事?

之所以如此,是因为你绝对不需要 jQuery UI 提供特殊的缓动函数。当然,如果你已经在使用它,那么使用其中包含的效果是有道理的。虽然你可能需要计算一些数学问题,但这只有在你真正想要深入研究复杂的公式时才是必要的。感兴趣吗?让我解释一下。

将缓动添加到代码中不必超过一个简单的函数,该函数使用下表中的五个不同值之一,如下表所示:

目的
x null请注意,虽然始终包含 x,但几乎总是设置为 null 值。
t 经过的时间。
b 初始值
c 变化的量
d 持续时间

在正确的组合中,它们可以用来产生一个缓动效果,例如 jQuery UI 中可用的 easeOutCirc 效果:

$.easing.easeOutCirc= function (x, t, b, c, d) {
  return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
}

进一步说,我们总是可以计算出自己的自定义缓动函数。一个很好的例子在 tumblr.ximi.io/post/9587655506/custom-easing-function-in-jquery 中概述,其中包括使其在 jQuery 中运行所需的评论。作为替代方案,你也可以尝试 gizma.com/easing/,其中列出了几个类似效果的例子。

我认为现在是我们实践的时候了。让我们深入其中,利用这些值来创建自己的缓动函数。我们将从为我们之前的示例添加一个预定义的缓动开始,然后将其剥离并替换为自定义创建。

添加自定义缓动到我们的效果中

当然,我们可以使用像 Easing 插件这样的工具,可以从 gsgd.co.uk/sandbox/jquery/easing/ 下载,或者甚至使用 jQuery UI 本身。但其实并不需要。添加基本的缓动效果只需要几行代码。

尽管涉及的数学可能不容易,但添加特定的缓动值却很容易。让我们看一些例子:

  1. 对于这个演示,我们将从本书附带的代码下载中提取相关文件。我们需要 slidefade.htmlslidefade.jsjquery.min.jsslidefade.css 文件。这些文件需要保存到我们项目区域的相应文件夹中。

  2. slidefade.js 的副本中,我们需要添加我们的缓动效果。在 slideFadeToggle() 函数之前,立即在文件开始处添加以下代码:

    $.easing.easeOutCirc= function (x, t, b, c, d) {
      return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
    }
    
  3. 尽管我们已经添加了缓动效果,但我们仍然需要告诉我们的事件处理程序去使用它。为此,我们需要修改代码如下所示:

    $(document).ready(function() {
      $("#sfbutton").on("click", function() {
        $(this).next().slideFadeToggle(1000, 'easeOutCirc');
      });
    });
    
  4. 将文件保存为slidefadeeasing.htmlslidefadeeasing.cssslidefadeeasing.js,然后在浏览器中预览结果。如果一切正常,我们应该注意到<div>元素在收缩和渐隐过程中有所不同。

在这个阶段,我们已经有了创建自定义缓动函数的完美基础。为了测试这一点,请尝试以下操作:

  1. 浏览到位于www.madeinflex.com/img/entries/2007/05/customeasingexplorer.html的自定义缓动函数资源管理器网站,然后使用滑块设置以下值:

    • Offset: 420

    • P1: 900

    • P2: -144

    • P3: 660

    • P4: 686

    • P5: 868

  2. 这将生成以下方程函数:

    function(t:Number, b:Number, c:Number, d:Number):Number {
      var ts:Number=(t/=d)*t;
      var tc:Number=ts*t;
      return b+c*(21.33482142857142*tc*ts +  - 66.94196428571428*ts*ts + 75.26785714285714*tc +  - 34.01785714285714*ts + 5.357142857142857*t);
    }
    
  3. 就目前而言,我们的方程在代码中使用时不会起作用;我们需要编辑它。删除所有:Number的实例,然后在参数中的t之前添加一个x。当编辑后,代码将如下所示 – 我给它赋了一个缓动名称:

    $.easing.alexCustom = function(x, t, b, c, d) {
      var ts=(t/=d)*t;
      var tc=ts*t;
      return b+c*(21.33482142857142*tc*ts +  - 66.94196428571428*ts*ts + 75.26785714285714*tc +  - 34.01785714285714*ts + 5.357142857142857*t);
    }
    
  4. 将其放入slidefade.js,然后修改document.ready()块中使用的缓动名称,并运行代码。如果一切正常,我们将在动画<div>元素时使用新的自定义缓动。

这开启了许多可能性。手动编写我们刚生成的函数是可行的,但需要大量的努力。最好的结果是使用缓动函数生成器为我们生成结果。

现在,我们可以继续使用像我们在这里检查的两个函数一样的函数,但这似乎是一个很难解决的难题,每次我们想要为动画元素提供一些变化时!我们也可以懒惰一些,简单地从 jQuery UI 中导入效果,但这也会带来很多不必要的负担;jQuery 应该是提供轻量级方法的!

相反,我们可以使用一种更简单的选项。虽然许多人最初可能会害怕使用贝塞尔曲线,但有些善良的人已经为我们完成了大部分繁重的工作,这使得在创建效果时使用起来非常轻松。

在效果中使用贝塞尔曲线

一个问题 – 请举手,如果您能猜出雷诺和雪铁龙除了是两个竞争对手汽车制造商之外,还有什么共同之处?答案是我们下一个话题的主题 – 贝塞尔曲线!

是的,也许很难相信,但贝塞尔曲线曾在 1962 年用于雷诺的汽车设计中,尽管在此之前雪铁龙就已经使用了,早在 1959 年。

但是,我岔开了话题 – 我们在这里是来看如何在 jQuery 中使用贝塞尔曲线的,例如下一个示例:

在效果中使用贝塞尔曲线

提示

您可以在cubic-bezier.com/#.25,.99,.73,.44查看此示例。

这些默认不受支持;尝试过将其支持整合进去,但没有成功。相反,包含它们最简单的方法是使用 Bez 插件,可从github.com/rdallasgray/bez获取。为了看到它的易用性,让我们看看它的实际效果。

添加贝塞尔曲线支持

有许多在线网站展示了缓动函数的示例;我的个人喜爱是easings.net/www.cubic-bezier.com

前者是 Andrey Sitnik 创建的,我们在第六章使用 jQuery 进行动画中介绍过。其中提供了 jQuery 可用的所有缓动函数的工作示例。如果我们点击其中一个,可以看到它们可以被创造或在 jQuery 中以不同方式使用的各种方法。

提供支持的最简单方法是使用前面提到的 Bez 插件。现在是进行一个简短演示的时候了:

  1. 对于这个演示,我们将从随本书附带的代码下载中提取相关文件。我们需要blindtoggle.htmljquery.min.cssblindtoggle.cssjquery.bez.min.js文件。这些文件需要存储在项目区域的相应子文件夹中。

  2. 在一个新文件中,让我们继续创建 jQuery 效果。在这种情况下,将以下内容添加到一个新文件中,并将其保存为blindtoggle.js,放置在项目区域的js子文件夹中:

    jQuery.fn.blindToggle = function(speed, easing, callback) {
      var h = this.height() + parseInt(this.css('paddingTop')) +
        parseInt(this.css('paddingBottom'));
      return this.animate({
        marginTop: parseInt(this.css('marginTop')) <0 ? 0 : -h}, 
        speed, easing, callback
      );
    };
    
    $(document).ready(function() {
      var $box = $('#box').wrap('<div id="box-outer"></div>');
      $('#blind').click(function() {
        $box.blindToggle('slow', $.bez([.25,.99,.73,.44]));
      });
    });
    
  3. 如果我们在浏览器中预览结果,可以看到文本首先向上滚动,然后很快出现棕色背景,如下图所示:添加贝塞尔曲线支持

这似乎是相当多的代码,但这个演示的真正关键在于以下一行:

$box.blindToggle('slow', $.bez([.25,.99,.73,.44]));

我们正在使用$.bez插件从 cubic-bezier 值创建我们的缓动函数。这样做的主要原因是避免同时提供基于 CSS3 和基于 jQuery 的 cubic-bezier 函数的需求;这两者不是相互兼容的。插件通过允许我们提供缓动函数作为 cubic-bezier 值来解决这个问题,以匹配可以在样式表中使用的值。

在我们的代码中添加 cubic-bezier 支持打开了无限的可能性。以下是一些启发的链接,让你开始:

值得花时间熟悉使用 cubic-bezier 值。提供它们非常简单,所以现在轮到你创建一些真正酷炫的效果了!

使用纯 CSS 作为替代方案

在使用 jQuery 进行开发时,很容易陷入认为效果必须由 jQuery 提供的陷阱中。这是一个完全可以理解的错误。

成为一个更全面的开发者的关键是了解使用 jQuery 提供这样一个效果的影响。

在旧版浏览器上,我们可能没有选择。然而,在新版浏览器上,我们有选择。不要仅仅使用诸如slideDown()这样的效果,考虑是否可以使用 CSS 来实现相同(或非常相似)的效果。例如,作为slideDown()的替代方案,可以尝试以下操作:

.slider {transition: height 2s linear; height: 100px;
background: red;( )}
.slider.down { height: 500px; }

然后,我们可以将重点放在简单地更改分配的 CSS 类上,如下所示:

$('.toggler').click(function(){ 
  $('.slider').toggleClass('down');
});

啊,但是——这是一本关于精通 jQuery 的书,对吧?我们为什么要避免使用 jQuery 代码呢?嗯——引用莎士比亚《哈姆雷特》中的波洛尼斯——“……虽然这有点疯狂,但其中确有方法。”或者,换句话说,遵循这一原则有一个非常明智的理由。

jQuery 是一个本质上较重的库,对于默认的版本 2.1.3 的最小化副本来说,它的体积为 82 KB。当然,正在做一些工作来移除冗余功能,是的,我们总是可以移除我们不需要的元素。

但是,jQuery 资源消耗大,这给你的站点增加了不必要的负担。相反,更明智的做法是使用诸如toggleClass()这样的功能,就像我们在这里做的一样,来切换类。然后我们可以通过将 CSS 类存储在样式表中来保持分离。

这一切都取决于你的需求。例如,如果你只需要产生一些效果,那么将 jQuery 引入此任务中就没有太多意义。相反,我们可以使用 CSS 来创建这些效果,并将 jQuery 留给在站点本身提供大部分价值的地方。

注意

为了证明一点,在伴随本书的代码下载中查看replacejquery.html演示。你还需要提取replacejquery.css文件,以使其正常工作。这段代码创建了一个非常基本但功能齐全的滑块效果。仔细观察,你应该看不到任何 jQuery 的影子……!

现在,别误会。可能有一些情况下必须使用 jQuery(例如支持旧版本浏览器),或者情况要求使用该库能提供一个更整洁的选择(我们不能在纯 CSS 中进行链式操作)。在这些情况下,我们必须接受额外的负担。

为了证明这应该是例外而不是规则,以下是一些吸引你的例子:

这里的关键信息是并不总是需要使用 jQuery —— 成为更好的开发者的一部分是要弄清楚何时应该以及何时不应该使用大锤来解决问题!

好了,该继续了(抱歉,开了个玩笑)。让我们快速看看如何添加回调,以及如何改变思维方式,用一个更好的替代方案来替换它,使其更容易在 jQuery 中使用。

在我们的效果中添加回调

好的,我们已经创建了我们的效果,并设置了运行方式。如果我们希望在完成时或者失败时得到提醒呢?很简单!只要我们提供一个回调函数(带参数或不带参数都可以)。然后我们可以要求 jQuery 在效果完成后执行一个动作,就像下面的例子所示:

  $(document).ready(function() {
    $("#myButton").on("click", function () {
      $('#section').hide(2000, 'swing', function() {
 $(this).html("Animation Completed");
 });
    });
  });

这是一个完全可行的通知方式,而且实现起来非常轻松。但它并不是没有缺点。其中两个主要缺点是无法控制回调何时以及如何执行,以及只能运行一个回调。

庆幸的是,我们不必使用标准的回调函数,因为 jQuery 的 Deferreds 来拯救我们了。我们在第五章 集成 AJAX中曾提及过它的使用。Deferreds 和 Promises 的美妙之处在于它们可以应用于任何 jQuery 功能;事件特别适用于此目的。让我们看看在效果的上下文中我们如何利用这个功能。

用 jQuery 的 Promises 控制内容

Promises,promises - 我想我已经听到这个短语多少次了。

与现实生活不同,承诺经常被违背,我们可以保证 jQuery 中的 Promises 最终会得到满足。当然,答案可能并不总是积极的,但是,是的,至少会有对 Promise 的响应。

不过,我听到你在问一个问题 - 如果大多数事件已经内置了回调选项,那么为什么我们需要使用 jQuery 的 .promises()

简单的答案是,我们可以更好地控制构建和读取 Promises。例如,我们可以设置一个单一的回调,可以应用于多个 Promises;我们甚至可以设置一个 Promise 只在需要时触发一次!但美妙之处在于使用 Promises 更容易阅读代码,并链接多个方法在一起:

var myEvent = function(){
  return $(selector).fadeIn('fast').promise();
};
$.when( myEvent()).done( function(){
  console.log( 'Task completed.' );
});

我们甚至可以将主要效果分离到一个单独的函数中,然后将该函数链接到 Promise 中,以确定在我们的代码中如何处理它。

为了看到如何简单地结合两者,让我们花点时间考虑下面的简单示例,它使用 jQuery 中的 slideUp() 效果:

用 jQuery 的 Promises 控制内容

  1. 我们将从 promises.htmlpromises.cssjquery.min.js 文件中提取出来。继续将它们存储在我们项目区域的相关文件夹中。

  2. 在一个新文件中,添加以下代码—这包含了一个点击处理程序,用于我们标记文件中的按钮,当完成时首先滑动 <li> 项,然后在屏幕上显示通知。

    $(document).ready(function() {
      $('#btn').on("click", function() {
        $.when($('li').slideUp(500)).then(function() {
          $("p").text("Finished!");
        });
      });
    });
    
  3. 尝试在浏览器中运行演示。如果一切顺利,当点击屏幕上的按钮时,我们应该看到三个列表项被卷起,就像本节开头的截图中所示。

这个简单的演示完美地说明了我们如何使用 Promises 使我们的代码更易读。如果你期望更多的话,很抱歉让你失望了!但这里的关键并不一定是提供回调的 技术能力,而是使用 Promises 带来的 灵活性可读性

注意

值得注意的是,在这个示例中,我们使用了 jQuery 对象的 promise() 方法 - 在这种情况下,我们最好使用不同的对象作为 Promise 的基础。

要真正了解 Promises 如何使用,请查看 jsfiddle.net/6sKRC/,该链接显示了在 JSFiddle 中的一个工作示例。一旦动画完成,此示例将扩展slideUp()方法以完全删除元素。

应该注意,虽然这显示了扩展此效果的一个很好的方法,但代码本身可以从一些调整中受益,以使其更易读。例如,this. slideUp(duration).promise()可以轻松地分解成一个变量,这将使该行更短,更易读!

注意

如果您想了解更多关于使用 jQuery 的 Promises 和 Deferreds 的信息,则在线上有许多关于这两个主题的文章。两篇可能感兴趣的文章可以在 code.tutsplus.com/tutorials/wrangle-async-tasks-with-jquery-promises--net-24135tutorials.jenkov.com/jquery/deferred-objects.html 找到。如果您以前没有使用过promises(),那么花点时间来理解这个主题肯定是值得的!

我们接近本章的结束,但在总结之前,还有一个重要的主题要讨论。我们已经考虑了以某种形式使用 CSS 而不只是依赖 jQuery 的好处。如果情况要求必须使用后者,那么我们至少应该考虑管理队列以最大程度地从使用效果中获益。让我们花点时间更详细地探讨一下这个问题。

创建和管理效果队列

排队,排队 - 谁喜欢排队呢,我想知道?

尽管我们中并不是所有人都喜欢排队,比如排队买午餐或去银行,但排队对于成功运行动画至关重要。无论我们使用.slideUp().animate()甚至.hide(),都无关紧要 - 如果我们链接太多动画,就会达到动画无法运行的点。

要释放动画,我们需要明确调用.dequeue(),因为方法是成对出现的。请考虑一下来自cdmckay.org/blog/2010/06/22/how-to-use-custom-jquery-animation-queues/的以下示例:

想象一下,你正在制作一款游戏,你希望一个对象从top:100px开始,然后在 2000 毫秒内向上浮动。此外,你希望该对象在完全透明 1000 毫秒之前保持完全不透明,在剩余的 1000 毫秒内逐渐变得完全透明:

时间(毫秒) 顶部 不透明度
0 100px 1.0
500 90px 1.0
1000 80px 1.0
1500 70px 0.5
2000 60px 0.0

乍一看,似乎animate命令可以处理这个问题,如下面的代码所示:

$("#object").animate({opacity: 0, top: "-=40"}, {duration: 2000});

不幸的是,这段代码将使对象在 2000 毫秒内淡出,而不是等待 1000 毫秒,然后在剩余的 1000 毫秒内淡出。延迟也无济于事,因为它也会延迟上升浮动。此时,我们可以要么纠结于超时,要么,你猜对了,使用队列。

考虑到这一点,下面是修改后使用.queue().dequeue()的代码:

$("#object")
  .delay(1000, "fader")
  .queue("fader", function(next) {
    $(this).animate({opacity: 0},
      {duration: 1000, queue: false});
      next();
  })
 .dequeue("fader")
 .animate({top: "-=40"}, {duration: 2000})

在这个例子中,我们有两个队列:fx队列和fader队列。首先,我们设置了fader队列。由于我们想要在淡化前等待1000毫秒,我们使用了带有1000毫秒延迟命令。

接下来,我们排队进行一个动画,在1000毫秒内将对象淡出。请特别注意我们在动画命令中设置的queue: false选项。这是为了确保动画不使用默认的fx队列。最后,我们使用dequeue释放队列,并立即使用animate调用在顶部对象上移40像素的常规fx队列。

我们甚至可以将对.queue().dequeue()的使用转化为插件。鉴于两者都需要使用,将其转化为在代码中更易于阅读的形式是有意义的。考虑下一个例子:

$.fn.pause = function( delay ) {
  return this.queue(function() {
    var elem = this;
    setTimeout(function() {
      return $( elem ).dequeue();
    }, delay );
  });
};
$(".box").animate({height: 20}, "slow" ).pause( 1000 ).slideUp();

在上一个例子中,我们首先对.box的高度进行动画变化,然后暂停,然后上滑.box元素。

需要注意的关键点是,queue()dequeue()都是基于 jQuery 中的fx对象的。由于这已经在默认情况下设置,因此在我们的插件中没有必要指定它。

提示

如果您对queue()dequeue()的用途感到不确定,那么不妨看一看learn.jquery.com/effects/uses-of-queue-and-dequeue/,其中概述了一些有用的案例示例。

使用.queue()及其对应的.dequeue()提供了一种优雅的动画控制方式。它的使用可能更适合于多个、复杂的动画,特别是需要实现动画时间轴的情况。但如果我们只是使用了少量的简单动画,那么附加插件的重量可能就是不必要的。相反,我们可以简单地增加.stop()来提供类似的效果。参考以下内容:

$(selector).stop(true,true).animate({...}, function(){...});

使用.stop()可能不太优雅,但确实改善了动画的外观!

总结

哇,我们在过去的几页中涵盖了很多内容。肯定是紧张的!让我们来喘口气,回顾一下我们学到的内容。

我们首先回顾了 jQuery 的基本效果,以回顾我们可以在 jQuery 中使用的内容,然后探讨了标准动画和特效之间的关键区别。接着我们转向创建自定义效果,了解了所有效果的基础,然后在代码中创建了两个自定义效果的例子。

然后,我们把焦点转向了添加自定义缓动效果,并探讨了我们在本书前面看到的那些效果如何同样适用于 jQuery 效果。我们通过一个例子来说明,即添加基于贝塞尔曲线的缓动支持,然后探讨如何仅使用 CSS 就可以实现类似的效果。我们随后简要介绍了向我们的效果添加回调,然后探讨了如何通过使用 jQuery 的 Deferreds / Promises 选项来更好地控制回调,作为标准回调的替代方案。

我们接着以管理效果队列的方式结束了本章。这是一个很好的机会来探讨仔细管理队列的好处,这样我们在使用 jQuery 时就可以避免任何混乱或意外的结果。

然后,我们迅速进入了一些真正有趣的内容!在接下来的几章中,我们将探讨两个你可能不会立即与 jQuery 关联起来的主题;我们将从探索页面可见性 API 开始,你会发现编写大量复杂代码并不一定是件好事。

第九章:使用 Web 性能 API

您有多少次打开了带有多个选项卡的浏览器会话?作为开发人员,我希望那几乎是正常情况,对吗?

现在,如果当您切换选项卡时,内容仍然在原始选项卡上播放会怎样?真的很烦人,对吧?当然,我们可以停止它,但是嘿,我们是忙碌的人,有更重要的事情要做...!

幸运的是,这不再是问题 - 在移动时代,资源的保护变得更加重要,我们可以采用一些技巧来帮助节省资源的使用。本章将介绍如何使用页面可见性 API,并向您展示如何通过一些简单的更改,可以显著减少您的站点使用的资源。接下来的几页中,我们将涵盖以下主题:

  • 介绍页面可见性和 requestAnimationFrame API

  • 检测并添加支持,使用 jQuery

  • 使用 API 控制活动

  • 将支持合并到实际应用中

准备好开始了吗?很好!让我们开始吧...

页面可见性 API 简介

请暂时考虑一下这种情景:

您正在 iPad 上浏览一个内容丰富的网站,该网站已设置为预渲染内容。这开始大量使用设备上的资源,导致电池电量迅速耗尽。你能做些什么?好吧,在那个站点上,可能不行 - 但如果是您拥有的站点,那么是的。欢迎使用页面可见性 API

页面可见性 API 是一个巧妙的小 API,用于检测浏览器选项卡中的内容是否可见(即正在查看)或隐藏。为什么这很重要?简单 - 如果浏览器选项卡隐藏,那么在站点上播放媒体或频繁轮询服务就没有意义了,对吧?

使用此 API 的净影响旨在减少资源使用(因此)节省电力。毕竟,如果您的访问者因访问媒体密集型站点而耗尽电池电量,他们是不会感谢您的!

在接下来的几页中,我们将详细了解这个库,并看看如何与 jQuery 结合使用它。让我们从查看 API 的浏览器支持开始。

支持 API

与其他 API 不同,所有主要浏览器对此库的支持非常好。与许多 API 一样,页面可见性经历了通常的过程,需要供应商前缀,然后在 2013 年 10 月底达到推荐阶段。目前,最新的浏览器(IE8 之后)都不需要供应商前缀才能运行。

使用纯 JavaScript 时,使用页面可见性 API 的典型代码片段如下所示:

var hidden, state, visibilityChange;
if (typeof document.hidden !== "undefined") {
  hidden = "hidden", 
  visibilityChange = "visibilitychange",
  state = "visibilityState";
}

我们稍后会研究在本章中使用 jQuery。

在代码中实现它是微不足道的,所以没有理由不这样做。为了证明这一点,让我们看看演示的效果。

实现页面可见性 API

到目前为止,我们已经介绍了页面可见性 API,并介绍了在内容不可见时使用它暂停内容的好处。 值得花一点时间看看我们如何在代码中实现它,以及这样一个简单的改变如何带来巨大的好处。

我们将先从普通 JavaScript 开始,然后在本章稍后再看看如何使用 jQuery:

  1. 让我们从附带本书的代码下载中提取我们需要的标记文件。 对于此演示,我们将需要 basicuse.htmlbasicuse.css。 将文件分别保存到我们项目区域的根目录和 css 子文件夹中。

  2. 接下来,在一个新文件中添加以下代码:

    function log(msg){
      var output = document.getElementById("output");
      output.innerHTML += "<li>" + msg + "</li>";
    }
    
    window.onload = function() {
      var hidden, visibilityState, visibilityChange;
      if (typeof document.hidden !== "undefined") {
        visibilityChange = "visibilitychange";
      }
      document.addEventListener(visibilityChange, function() {
        log(document.visibilityState]);
      });
    };
    
  3. 这是我们演示的关键,使用页面可见性 API 来确定选项卡是可见还是隐藏的。 将其保存在我们项目区域的 js 子文件夹中,命名为 basicuse.js

  4. 如果一切顺利,那么当我们在浏览器中预览结果时,我们应该看到类似于以下截图的内容 - 这显示了切换到新选项卡然后再切换回来后的结果:实现页面可见性 API

拆解 API

快速查看前一个演示中的代码应该会发现两个值得注意的属性 - 它们是 document.visibilityStatedocument.hidden

这些构成了页面可见性 API。 如果我们首先更详细地查看 document.visibilityState,它可以返回以下四个不同的值之一:

  • hidden:页面在任何屏幕上都不可见

  • prerender:页面已加载到屏幕外,准备供访问者查看

  • visible:页面可见

  • unloaded:页面即将卸载(用户正在从当前页面导航离开)

我们还使用 document.hidden 属性 - 它是一个简单的布尔属性,如果页面可见,则设置为 false,如果页面隐藏,则设置为 true

结合 visibilitychange 事件,我们可以很容易地在页面可见性更改时收到通知。 我们将使用类似于以下代码的内容:

document.addEventListener('visibilitychange', function(event) {
  if (!document.hidden) {
    // The page is visible.
  } else {
   // The page is hidden.
  }
});

这将适用于大多数浏览器,但并非所有。 尽管它只是少数,但我们仍然必须允许它。 要了解我的意思,请尝试在 IE8 或更低版本中运行演示 - 它不会显示任何内容。 显示空白不是一个选项; 相反,我们可以提供一个优雅降级的路径。 因此,让我们看看如何避免代码崩溃成一堆。

检测对页面可见性 API 的支持

尽管 API 在大多数现代浏览器中都可以很好地工作,但在有限的几个浏览器中会失败; IE8 就是一个很好的例子。 为了解决这个问题,我们需要提供一个根本性的方式来优雅地降级,或者使用一个回退机制; 这个过程的第一步是首先弄清楚我们的浏览器是否支持该 API。

这样做的方法有很多种。我们可以使用 Modernizr 的Modernizr.addTest选项(来自www.modernizr.com)。相反,我们将使用 Matthias Bynens 的一个插件,其中包含了对旧浏览器的支持检查。原始版本可以从github.com/mathiasbynens/jquery-visibility获取。代码下载中包含的版本是一个简化版,删除了对旧浏览器的支持。

注意

代码下载中包含使用 Modernizr 的此演示版本。提取并运行usemodernizr.html文件,查看其运行方式。

现在我们已经看到了页面可见性如何被整合到我们的代码中,我们将切换到使用 jQuery 进行这个演示。

让我们开始:

  1. 我们需要从附带本书的代码下载中下载标记和样式文件。继续并提取以下副本:usingjquery.htmlusingjquery.cssjquery.min.jsjquery-visibility.js。将 CSS 文件保存到css子文件夹,将 JS 文件保存到js子文件夹,将 HTML 文件保存到我们项目文件夹的根目录。

  2. 在一个新文件中,添加以下代码 - 这包含了检查可见性和确认浏览器是否支持 API 所需的代码:

    $(document).ready(function() {
      var $pre = $('pre');
      var $p = $('p')
      var supported = 'The Page Visibility API is natively 
      supported in this browser.'
      var notsupported = 'The Page Visibility API is not 
      natively supported in this browser.'
      $('p').first().html(
        $.support.pageVisibility ? log($p, supported) : log($p, 
        notsupported)
      );
      function log(obj, text) { obj.append(text + '<br>'); }
      $(document).on({
        'show.visibility': function() {
        log($pre, 'The page gained visibility; the 
        <code>show</code> event was triggered.');
      },
      'hide.visibility': function() {
        log($pre, 'The page lost visibility; the 
        <code>hide</code> event was triggered.');
      }
      });
    });
    
  3. 将文件保存为usingjquery.js,放在项目区域的js子文件夹中。如果我们在 IE9 或更高版本上运行演示,我们将看到它在我们在标签之间切换时呈现出变化。参考以下图片:检测页面可见性 API 的支持

  4. 尝试将浏览器更改为 IE8 - 可以使用 IE 开发者工具栏,或切换到浏览器的本地副本。我们还需要更改所使用的 jQuery 版本,因为我们的演示是面向更新的浏览器的。将 jQuery 链接更改为以下内容:

    <script src="img/jquery-1.11.2.min.js"> </script>
    
  5. 现在尝试刷新浏览器窗口。它会显示它不支持页面可见性 API,但也不会因意外错误而崩溃。参考下一张图片:检测页面可见性 API 的支持

有了备用选项,我们现在有两个选择:

  • 当浏览器不被支持时,我们可以提供一条优雅降级的路径。这是完全可以接受的,但首先应该考虑一下。

  • 我们还可以提供备用支持,以允许旧浏览器仍然可以使用。

假设我们使用后一种方式。我们可以使用许多插件中的任何一种; 我们将使用 Andrey Sitnik 创建的visibility.js插件,为此目的。

提供备用支持

为任何应用程序提供备用支持是任何开发者的噩梦。我失去了对我想开发一些打破新领域的东西而不断出错的次数的计数。我必须为那些简单无法兼容新技术的旧浏览器提供支持!

幸运的是,这不是 Page Visibility API 的问题 – 浏览器覆盖非常好,尽管少数浏览器版本仍需要一些回退支持。有许多插件可用于此目的 – 也许最著名的是 Mathias Bynens 制作的,可在 github.com/mathiasbynens/jquery-visibility 获取。我们在上一个演示中看到了如何使用定制版本。

对于此演示,我们将使用 Andrey Sitnik 的类似插件,可从 github.com/ai/visibilityjs 获取。这包含额外的功能,包括一个定时器,用于显示页面可见的时间;我们将在以下演示中使用它。

安装 visibility.js

在我们开始演示之前,值得注意的是 visibility.js 插件可以通过几种方式引用:

为了这个演示的目的,我假设您正在使用 CDN 版本(其中包含额外的定时器功能),但保存为本地副本。

注意

注意 – 如果您不使用此方法,则需要下载所有四个可见性 JavaScript 文件,网址为 github.com/ai/visibilityjstree/master/lib,因为这些文件提供了压缩 CDN 版本中可用的回退和定时器功能。

构建演示

好的,现在我们已经安装了插件,接下来是我们将演示的屏幕截图:

构建演示

让我们开始吧:

  1. 从附带本书的代码下载中提取相关的标记文件。在这个练习中,我们需要 fallback.htmlfallback.css 文件。将它们存储在项目区域的根目录和 css 文件夹中。

  2. 我们还需要 visibility.min.js 插件文件 – 它们都在代码下载文件中。将其提取并保存到我们项目区域的 js 子文件夹中。

  3. 接下来,将以下内容添加到一个新文件中,将其保存为项目区域的 js 子文件夹中的 fallback.js

    $(document).ready(function() {
      if ( Visibility.isSupported() ) {
        $("#APIsupported").html('is supported');
      } else {
        $("#APIsupport").html('isn't supported');
      }
    
      document.title = Visibility.state();
      Visibility.change(function (e, state) {
        document.title = state;
      });
    
      var sec = 0;
      Visibility.every(1000, function () {
        $("#APIcounter").html(sec++);
      });
    });
    
  4. 此代码包含我们演示所需的魔法。

  5. 保存文件。如果我们在浏览器中预览结果,可以期望看到类似练习开始时的屏幕截图。如果我们切换到不同的选项卡,如下一个屏幕截图所示,则计时器计数将暂停,并相应地更新原始选项卡的标题:构建演示

那么,发生了什么?这是一个非常简单的演示,但我们首先启动了一个检查,以确保我们的浏览器能够支持这个 API。在大多数情况下,这不是问题,除了 IE8 或更低版本。

我们随后在窗口的标题区域显示了窗口的初始状态;每次从演示切换到不同的标签页然后再切换回来时,都会更新这一状态。作为额外奖励,我们利用了主插件附带的visibility.timer.js插件,显示了窗口可见时间的计数。当然,每次我们切换到不同的浏览器窗口然后再切换回来时,这个计时就会停止!

不过,与之前的演示不同的是,这个插件即使我们使用 IE8 或更低版本仍然可以工作;我们可能需要修改演示中的代码以确保其样式正确,但这只是一个小问题。

让我们继续。既然我们已经了解了如何使用页面可见性 API 的基础知识,我相信你一定会问:我们如何在实际场景中使用它?没问题 - 让我们看一些可能的用例。

在实际场景中使用 API

这个 API 可以在各种不同的上下文中使用。经典的用法通常是帮助控制视频或音频的播放,尽管它也可以与其他 API 一起使用,比如电池 API,以防止在电量过低时显示内容。

让我们花点时间深入研究一些实际示例,这样我们就可以看到实现 API 有多么简单。

暂停视频或音频

API 最常见的用途之一是控制音频或视频等媒体的播放。在我们的第一个示例中,我们将使用 API 在切换标签时播放或暂停视频。让我们深入了解一下。

对于这个演示,我们将使用一些额外的内容 - 动态网站图标库,可以从softwareas.com/dynamic-favicons/获取。虽然这个库已经有几年了,但仍然可以与当前版本的 jQuery 正常工作。视频来自大黄蜂项目网站,网址为peach.blender.org

注意

这个演示的视频来自 Blender 基金会,版权为 2008 年,Blender 基金会/ www.bigbuckbunny.org

好了!让我们开始吧:

  1. 像往常一样,我们需要从某个地方开始。对于这个演示,继续从本书附带的代码下载中提取pausevideo演示文件夹。

  2. 打开pausevideo.js文件。其中包含使用jquery-visibility插件播放或暂停视频的代码。参考以下代码:

    var $video = $('#videoElement');
    
    $(document).on('show.visibility', function() {
      console.log('Page visible');
      favicon.change("img/playing.png");
      $video[0].play();
    });
    
    $(document).on('hide.visibility', function() {
      console.log('Page hidden');
      favicon.change("img/paused.png");
      $video[0].pause();
    });
    
  3. 这个插件非常简单。它公开了两种方法,即show.visibilityhide.visibility。现在试运行演示。如果一切正常,我们应该看到大黄蜂视频播放;当我们切换标签时,它会暂停。以下是视频的截图:暂停视频或音频

  4. 另外,使用favicon.js库更新窗口标题。当我们切换选项卡时,它显示一个暂停符号,如下图所示:暂停视频或音频

那很容易,对吧?这就是 API 的美妙之处。它非常简单,但可以与各种不同的工具一起使用。让我们通过将 API 的支持纳入内容管理系统CMS)(如 WordPress)来证明这一点。

为 CMS 添加支持

到目前为止,我们已经看到了在静态页面站点中支持标准是多么容易 - 那么对于 CMS 系统,例如 WordPress,我听到你在问什么?

好了,API 也可以在这里轻松使用。与其谈论它,不如看看我们如何添加它。对于这个演示,我将使用 WordPress,尽管原理同样适用于其他 CMS 系统,如 Joomla。我将使用的插件是我自己创建的。

应该注意,您应该有一个可用的 WordPress 安装,可以是在线的,也可以是自托管版本,并且您对安装插件有一些了解。

注意

请注意 - jquery-pva.php插件仅用于开发目的;在将其用于生产环境之前,还需要进一步的工作。

好的,让我们开始:

  1. 我们需要对主题中的functions.php文件进行更改。为此,我将假设您正在使用 Twenty Fourteen 主题。打开functions.php,然后添加以下代码:

    function pausevideos() {
      wp_register_script('pausevideo', plugins_url( '/jquery- pva/pausevideo.js'), array('jquery'),'1.1', true);
      wp_enqueue_script('pausevideo');
    }
    
    add_action( 'wp_enqueue_scripts', 'pausevideos' );
    
  2. 从附带本书的代码下载中,找到并提取jquery-pva文件夹,然后将其复制到您的 WordPress 安装中;它需要放在plugins文件夹中。返回您的 WordPress 安装,然后以通常的方式激活插件。

  3. 接下来,登录您的 WordPress 管理区域,然后点击Settings | PVA Options,输入您想使用的 jQuery 版本号。我将假设已选择 2.1.3。点击Save Changes以生效。参考以下图片:为 CMS 添加支持

此时,我们可以开始使用库了。如果我们上传一个视频并将其添加到帖子中,当我们开始播放时,它将显示已经过的时间;当我们切换选项卡时,它将暂停:

为 CMS 添加支持

要确认它是否工作正常,值得查看源代码,使用 DOM 检查器。如果一切正常,我们应该会看到以下链接。第一个链接将确认引用了 Page Visibility 库,如下所示:

为 CMS 添加支持

第二个链接将确认我们的脚本被调用了,如下图所示:

为 CMS 添加支持

如我们所见,API 确实有其用处!在本章中,我尽量使代码相对简单,以便容易理解。现在轮到你来实验和进一步探索了 - 也许我可以给你一些启发?

探索示例的想法

页面可见性 API 的基本原理很容易实现,因此我们所能做到的复杂程度仅受想象力的限制。在我的研究中,我找到了一些灵感的想法——希望以下内容能让你了解可能的一些情况:

好的,我认为现在是改变的时候了。让我们继续并看看另一个与页面可见性 API 大约在同一时间创建的 API,它使用类似的原理来帮助减少资源需求。

我当然是指 requestAnimationFrame API。让我们深入探讨一下,看看它是什么,是什么原因让它运行起来,以及为什么这样一个简单的 API 对我们开发者来说会是一个真正的福音。

介绍 requestAnimationFrame API

过去几年转向在线工作导致了对性能浏览器的巨大需求增加,同时减少了资源消耗和电池功耗。

有了这个想法,浏览器厂商和微软联合创建了三个新的 API。我们已经探讨了其中一个,以页面可见性 API 的形式;我们要看的另一个是requestAnimationFrame。三者(第三个是setImmediate)都是为了提高性能和增加功耗效率而设计的。

探索概念

那么,requestAnimationFrame 是什么?简单——如果你花了一些时间使用 jQuery 创建动画,你肯定使用过setInterval方法(甚至是clearInterval),对吧?requestAnimationFrame(和 clearAnimationFrame)分别被设计为替代它们。

我们为什么要使用它?在下一节中,我们将探讨使用 requestAnimationFrame 的好处,但首先让我们了解它的本质。

大多数动画在绘制动画时都使用小于 16.7 毫秒的基于 JavaScript 的定时器,即使显示器只能以 16.7 毫秒(或 60Hz 频率)显示,如下图所示:

探索概念

这为什么重要?关键在于,典型的setIntervalsetTimeout频率通常约为 10 毫秒。这意味着每第三次监视器的绘制不会被观看者看到,因为在显示刷新之前会发生另一次绘制。请参考下一个图表:

探索概念

这会导致显示不连贯,因为会丢帧。电池寿命可能会降低高达 25%,这是一个显著的损失!

浏览器厂商意识到了这一点,因此提出了 requestAnimationFrame API。这告诉应用程序浏览器何时需要更新屏幕,以及浏览器何时需要刷新。这导致资源使用减少,丢帧较少,因为帧速率与代码相比更一致。

注意

开发者 Paul Irish 在他的博客www.paulirish.com/2011/requestanimationframe-for-smart-animating/中对此做了完美的总结,他指出这使得浏览器能够“将并行动画优化到单个回流和重绘周期中,从而实现更高保真度的动画。”

查看 API 的实际效果

几乎总是这样,看到实际效果要比阅读有用得多。对我来说,移动演示有助于深化概念!

为了帮助理解,本书附带的代码下载中有两个演示——requestAnimationFrame.htmlcancelAnimationFrame.html文件。它们包含了两个 API 的简单示例。我们将在本章末尾探讨 API 的更多实际用途。

使用 requestAnimationFrame API

虽然从上一节末尾引用的简单演示中可能并不立即明显,但使用 requestAnimationFrame 有一些明显的好处,下面列出了这些值得注意的好处:

  • requestAnimationFrame 与浏览器合作,在重绘转换期间将动画组合到单个重绘中,使用屏幕刷新率来决定何时应该发生这些操作。

  • 如果浏览器标签处于非活动或隐藏状态,动画会被暂停,这会减少刷新屏幕的请求,从而降低移动设备的内存消耗和电池使用。

  • 浏览器通过优化动画,而不是通过代码 - 较低的帧刷新率会导致更平滑、更一致的外观,因为较少的帧将被丢弃。

  • 该 API 在大多数移动设备上都受支持。目前唯一不支持的平台是 Opera Mini 8.0。CanIUse 网站(www.caniuse.com)显示全球使用率仅为 3%,因此这不太可能造成太大问题。

值得注意的是,cancelAnimationFrame(作为 requestAnimationFrame 的姐妹 API)可用于暂停动画。如果电池电量太低,我们可以潜在地将其与诸如 Battery API 等东西一起使用,以阻止动画(或媒体,如视频)的启动。

提示

要查看 requestAnimationFrame 与 setTimeout 之间的区别,请访问 jsfiddle.net/calpo/H7EEE/。尽管演示非常简单,但您可以清楚地看到两者之间的区别!

需要注意的一个关键点是,有些情况下,requestAnimationFrame 并不总是比使用 jQuery 更好。David Bushell 在 dbushell.com/2013/01/15/re-jquery-animation-vs-css/ 上有一篇有用的文章,概述了这个问题,并指出 requestAnimationFrame 最适合用于基于 <canvas> 的动画。

基于 requestAnimationFrame(以及 cancelAnimationFrame)创建动画非常简单。开发者 Matt West 在 CodePen 上创建了一个 JavaScript/jQuery 示例,可在 codepen.io/matt-west/full/bGdEC 查看。他还编写了一篇配套教程,可在 Team Treehouse 的博客上查看,链接为 blog.teamtreehouse.com/efficient-animations-with-requestanimationframe

这让我们顺利过渡到下一个主题。现在我们已经看到如何使用 JavaScript 操纵 API,让我们看看如何使用类似技术的 jQuery。

向 jQuery 进行的更改改造

到目前为止,我们已经介绍了如何使用 requestAnimationFrame 及其姐妹 API cancelAnimationFrame 的基础知识;我们已经看到如何使用纯 JavaScript 实现它。

但值得注意的是,jQuery 目前不包含原生支持。在 1.8 版本之前尝试将其添加到 jQuery 中,但由于主要浏览器供应商的支持问题而将其删除。

幸运的是,供应商支持现在比以前好得多;并且计划在 jQuery 2.2 或 1.12 中添加requestAnimationFrame支持。您可以按如下方式查看需要进行的更改,以及历史记录:

作为临时措施(如果您仍然需要支持 jQuery 的早期版本),您可以尝试使用 Corey Frang 的插件,github.com/gnarf/jquery-requestAnimationFrame,该插件为 1.8 版本后的 jQuery 版本添加了支持。

但是,如果您感到更有冒险精神,那么直接将requestAnimationFrame支持添加到使用它的库中就很容易。让我们花点时间看看涉及转换的内容。

更新现有代码

进行更改相对直接。关键在于使更改模块化,这样一旦 jQuery 支持requestAnimationFrame,就可以轻松地将其替换回来。

如果您使用的库中存在对setIntervalclearInterval的代码引用,可以进行更改。例如,考虑以下代码摘录:

var interval = setInterval(doSomething, 10)
var progress = 0
function doSomething() {
  if (progress != 100){
  // do something here
  }
  else {
  clearInterval(interval)
  }
}

它将被更新为以下代码摘录,将对setInterval的引用替换为requestAnimationFrame(并添加clearInterval的等效替换):

var requestAnimationFrame = window.requestAnimationFrame;
var cancelAnimationFrame = window.cancelAnimationFrame;

// your code here

var progress = 0;

function doSomething() {
  if (progress != 100) {
    // do something here
 var myAnimation = requestAnimationFrame(doSomething);
  } else {
    cancelAnimationFrame(myAnimation);
  }
}

在前面的代码示例中,粗体突出显示的代码指示了更新代码所需的更改类型。我们将在本章稍后使用此技术,为现有库添加支持。这将是我们将探讨的两个演示之一,它们使用requestAnimationFrame

一些使用requestAnimationFrame的示例

到目前为止,我们已经了解了使用requestAnimationFrame的理论,并覆盖了我们可能需要对现有代码进行的典型更改。

这是一个很好的起点,但不一定易于理解概念;在实际操作中更容易理解!考虑到这一点,我们将查看一些利用 API 的演示。第一个将为现有支持添加支持,而第二个已经在代码中包含了支持。

创建可滚动效果

对于我们的第一个演示,我们将来看看更新经典的可滚动 UI 元素的示例。我们将使用来自github.com/StarPlugins/thumbelina的 Thumbelina 插件。虽然已经有几年了,但它仍然可以完美运行,即使使用最新版本的 jQuery!

在这个演示中,我们将在插件中替换setInterval调用,改用requestAnimationFrame。让我们开始:

  1. 让我们从这本书附带的代码下载中提取thumbelina演示文件夹的副本。如果我们运行scrollable.html文件,我们应该会看到一个具有兰花图片的可滚动内容,如下图所示:创建可滚动效果

  2. Thumbelina 插件目前使用setInterval来管理动画之间的时间间隔。我们将修改它,改为使用新的requestAnimationFrame

  3. 打开 thumbelina.js,然后在$.fn.Thumbelina = function(settings) {下面立即添加以下代码,该行在第 16 行处:

      var start = new Date().getTime(),
      handle = new Object();
      function loop() {
        var current = new Date().getTime(),
        delta = current - start;
        if(delta >= delay) {
          fn.call();
          start = new Date().getTime();
        }
        handle.value = 
        window.requestAnimationFrame(loop);
      };
      handle.value = window.requestAnimationFrame(loop);
      return handle;
    }
    
  4. 向下滚动到以下行,该行将在第 121 行左右:

    setInterval(function(){
    
  5. 按照下面的修改,使它使用我们刚刚添加的新的requestInterval()函数:

    requestInterval(function() {
      animate();
      },1000/60);
    };
    
  6. 保存该文件。如果我们运行演示,应该不会看到任何视觉上的差异;真正的差异发生在后台。

提示

尝试在 Google Chrome 中运行演示,然后在时间轴中查看结果。如果你进行前后对比,应该会看到显著的差异!如果你不确定如何对演示进行分析,那就前往developer.chrome.com/devtools/docs/timeline获取完整详情。

动画化谷歌地图标记

本章的最终演示将使用众所周知的 Google Maps 服务,以动画化地图上指示特定位置的标记:

动画化谷歌地图标记

在这个例子中,我们将使用 Robert Gerlach 创建的演示,该演示可从robsite.net/google-maps-animated-marker-move/ 获取。我已经调整了他的markerAnimate.js插件文件中的代码,以去掉厂商前缀,因为这些不再需要。

他创造了一个漂亮的效果,为看似十分枯燥的内容增添了些活力。尽管如此,它仍然需要相当数量的代码!由于空间限制,我们无法在印刷品中探索所有内容,但我们可以探讨一些更重要的概念:

  1. 让我们从这本书附带的代码下载中提取googlemap演示文件夹。这包含了我们演示的样式、JavaScript 库和标记。

  2. 在浏览器中运行googlemap.html。如果一切正常,我们应该会看到指针位于英国伯明翰,那里是 Packt Publishing 的英国办公室所在地。

尝试在地图的其他位置单击 - 注意它是如何移动的?它利用了 jQuery Easing 插件中提供的一些缓动效果,我们在 第六章 中使用过的,jQuery 动画

我们可以通过简单地更改右下角下拉框中显示的值来选择要使用的缓动效果。这甚至可以包括我们自己制作的自定义动画,以 第六章 中给出的示例为基础。只要将自定义动画函数包含在我们的代码中,并在下拉框中添加适当的名称,我们就可以使用它。

实际上要注意的重点在于 markeranimate.js 文件。如果我们打开它并滚动到第 64 - 71 行,我们可以看到 requestAnimationFrame 的使用方法。如果浏览器支持该 API,则使用它,否则使用 setTimeout,如下面的截图所示:

Google 地图标记的动画效果

结合使用缓动效果和调用 requestAnimationFrame 会产生一个很酷的效果,同时也减少了对资源的需求 - 如果您的网站有很多动画效果,那就很棒!

提示

要更容易地替换setIntervalclearInterval(以及setTimeout / clearTimeout),请使用 Joe Lambert 的替换函数,可在 gist.github.com/joelambert/1002116 获取。

探索灵感来源

在过去的几页中我们已经涵盖了很多内容 - 完全理解 requestAnimationFrame(以及其姊妹函数 clearAnimationFrame)的工作原理可能需要一些时间,但随着 jQuery 的即将更改,值得花时间熟悉这些 API 及其为我们开发带来的好处。

在我们结束本章之前,下面列出了一些可能对你有用的灵感来源:

还有很多其他资源可用 - 接下来就看你的想象力带你去哪里!

总结

探索新的 API 总是很有趣的。即使它们可能在本质上很简单(例如,查看振动 API),它们也可以证明是任何人工具箱中真正有用的补充。在本章节中,我们详细探讨了其中的两个。让我们花点时间回顾一下我们所涵盖的内容。

我们以介绍页面可见性 API 开始。我们查看了 API 的浏览器支持情况,然后实施了一个基本示例。我们进一步讨论了如何检测并提供备用支持,然后查看了一些实际示例。

接下来我们来看一下 requestAnimationFrame API,我们了解了一些与页面可见性 API 的相似之处。我们探讨了它的基本原理,然后看了一些实际用途以及如何添加支持到 jQuery 本身。然后我们用两个例子总结了这一章节;一个是基于使用 API 进行转换的,而另一个则是从零开始构建的。

进入下一章节,我们将探讨网站的另一个关键元素,即图片。我们将探讨如何使用 jQuery 操纵图片,以产生一些非常有趣的效果。

第十章:操纵图像

常常有人说图像胜过千言万语 – 网站也不例外。

我们使用图像来说明一个过程,帮助强化信息,或者为原本可能被视为非常普通的内容应用一些视觉身份。图像在任何网站中都起着关键作用;图像的质量会决定一个站点的成败。

使用 jQuery 操纵图像的一小部分是我们如何应用滤镜,或者操纵图像中的颜色。在本章中,我们将探讨如何使用 jQuery 操纵图像,然后探索几个以捕获图像作为进一步操纵基础的真实世界示例。在本章中,我们将涵盖以下主题:

  • 使用 CSS 和 jQuery 应用滤镜

  • 使用插件编辑图像

  • 使用 jQuery 和 canvas 创建一个简单的签名板

  • 捕获和操纵网络摄像头图像

让我们开始吧…!

操纵图像中的颜色

一个问题 – 你多久以为操纵图像的唯一方式是使用像 Photoshop 或者 GIMP 这样的软件?我打赌不止一次 – 如果我说这些广为人知的重量级应用程序在某些情况下是多余的,而你只需要一个文本编辑器和一点点 jQuery 呢?

此时,你可能想知道我们可以用 jQuery 如何操纵图像。别担心!我们有几个绝招。在接下来的几页中,我们将逐个看看,发现虽然我们可以使用可能是开发人员最常用的 JavaScript 库之一,但并不总是正确的做法。

为了理解我的意思,让我们快速回顾一下我们可以使用的方法,它们是:

  • 使用 CSS3 滤镜,并使用 jQuery 切换它们的使用与否

  • 使用 HTML5 <canvas> 元素,jQuery 和 getImageData 方法处理程序的混合方式来操作每个图像的颜色元素,然后将其重新绘制到画布上。

在本章中,我们将依次看看每一个,并探讨为什么即使我们可以使用 jQuery 创建复杂的滤镜,它也并不总是正确的答案。希望通过我们的一些绝招,能让我们成为更好的开发人员。让我们从简单的 CSS3 滤镜开始,看看我们如何轻松地将它们应用到我们的 jQuery 代码中。

使用 CSS3 添加滤镜

至少在主流桌面浏览器中,滤镜支持已经有一段时间了,尽管我们仍然需要使用 -webkit- 厂商前缀支持,因为我们还不完全是无前缀的:

使用 CSS3 添加滤镜

注意

关于前面图像的信息来自 CanIUse 网站,网址为 caniuse.com/#feat=css-filters

使用这些方法的美妙之处在于它们非常简单易行;如果客户决定改变主意,我们不必花费数小时重新制作图像!我们可以轻松地使用 jQuery 应用和移除样式,这有助于将样式与我们的标记分开。

操纵图像可能变得非常复杂 – 实际上,要涵盖所涉及的数学,我们可能需要写一本专门的书!相反,我们将从简单回顾使用 CSS3 滤镜开始,然后转向创建更复杂的滤镜,并以帮助从两个不太可能的源捕获图像的几个演示结束。

感兴趣吗?在本章末尾一切都会变得清晰起来,但我们首先将从一个简单的练习开始,重新熟悉应用 CSS3 滤镜。

准备工作

在开始练习之前,我强烈建议您在这些演示中使用 Firefox 或 IE;如果您使用 Chrome,那么在本地运行时,某些演示将显示跨源错误。

一个很好的例子是跨平台应用程序 XAMPP(可从www.apachefriends.org获取),或者您可以尝试 WAMPServer(适用于 PC,从www.wampserver.com/en获取),或者 MAMP(适用于 Mac,从www.mamp.info获取)。我将假设您是从 Web 服务器中运行演示。

创建我们的基页

在本章的第一个演示中,我们将从简单回顾使用 addClass 方法开始,将特定的滤镜应用到页面上的图像。我们将使用加拿大开发者 Nick La 开发的拍立得效果,并且可以从webdesignerwall.com/demo/decorative-gallery-2/获取。.addClass() 方法是您几乎肯定以前使用过无数次的方法;我们在这里使用它是为了引入本章后面更复杂效果的介绍。让我们开始:

  1. 让我们从从伴随本书的代码下载中下载并提取以下文件开始:

    • cssfilters.html

    • cssfilters.css

    • jquery.min.js

    • cssfilters.js

  2. 将 HTML 标记文件放入项目区域的根目录,将 JavaScript 和 CSS 文件放入项目区域的相关子文件夹中。

  3. 在一个新文件中,添加以下简单的代码块 – 这是按钮的事件处理程序,我们将用它来更改滤镜状态:

    $(document).ready(function(){
      $("input").on("click", function(){
        $("img").toggleClass("change-filter");
      })
    });
    
  4. 在这个阶段,尝试在浏览器中预览结果。如果一切正常,我们应该看到一张蓝色花朵的图片,设置在拍立得效果的背景中。参考以下图片:创建我们的基页

  5. cssfilters.css – 屏幕底部附近仔细查看。我们应该看到以下内容:

    .change-filter {
      filter: blur(5px);
      -webkit-filter: blur(5px); 
    }
    

    紧接着这个区块:

    img { -webkit-transition: all 0.7s ease-in-out; transition: all 0.7s ease-in-out; }
    
  6. 现在点击使用 CSS 更改滤镜按钮。如果一切正常,我们的图像应该逐渐变模糊,如下图所示:创建我们的基页

一个简单的演示 - 在目前阶段没有太多困难,考虑到我们在本书中已经涵盖了一些更复杂的主题!

小贴士

小贴士 - 如果你发现在某些版本的 Firefox 中滤镜显示不出来,那么请检查about:config中的layout.css.filters.enabled属性。在 34 版或更早的版本中,默认情况下未启用;这一点是从 35 版开始改变的:

创建我们的基本页面

当然,这个演示的关键是使用.addClass()方法处理程序。当点击按钮时,我们只是将一个新的,预设的类应用于图像。但在这里美妙的是,我们可以访问许多快速、简单的滤镜,可以减少(甚至消除)对 PhotoShop 或 GIMP 的使用。为了看到切换有多容易,让我们现在做出这个改变,切换到使用亮度滤镜。

更改亮度级别

下一个演示是对我们刚刚工作过的cssfilters.css文件进行快速简单的更改。以下是我们将要制作的屏幕截图:

更改亮度级别

在继续下面列出的步骤之前,请确保你有这个文件可用:

  1. cssfilters.css中,查找并修改.change-filter规则如下所示:

    .change-filter { filter: brightness(170%); -webkit-filter: brightness(170%); }
    
  2. 现在点击使用 CSS 更改滤镜。如果一切正常,我们应该会发现图像变得更加明亮。

同样 - 在这里没有太多困难;希望在本书中我们所涵盖的一些内容后,这是一个放松的时刻!我们可以使用一些 CSS3 滤镜;由于空间限制,我们不能在这里涵盖它们所有,但至少我们可以再看一种滤镜。在接下来的练习后面,我们将介绍可供使用的其他滤镜。

向我们的图像添加深褐色滤镜

与以前一样,我们需要恢复更改cssfilters.css,所以确保你已经准备好了这个文件。让我们看看我们需要做什么:

  1. 恢复到cssfilters.css,然后按如下所示修改这一行:

    .change-filter { filter: sepia(100%); -webkit-filter: sepia(100%); }
    
  2. 现在点击使用 CSS 更改滤镜。如果一切正常,我们应该会发现图像现在应用了一种深褐色滤镜,如此屏幕截图所示:向我们的图像添加深褐色滤镜

这就是我喜欢使用 CSS3 滤镜的地方 - 尽管有些纯粹主义者可能会说的,但并不总是必要回到使用图形软件包;在 CSS 中简单更改一个值就足够了。

如果需要,我们可以手动更改该值,但是现在我们也可以以编程方式灵活地进行更改,对性能几乎没有影响。这一点非常重要,因为我们将在本章后面看到。使用 jQuery 创建复杂滤镜来操纵图像是一个资源消耗大的过程,因此不宜频繁进行。

探索其他滤镜

在我们继续探讨不同的图像处理方法之前,下表展示了不同滤镜的风格;所有这些滤镜都可以使用 jQuery 进行设置,就像我们在之前的练习中所概述的那样:

滤镜名称 使用示例
contrast()
.change-filter { filter: contrast(170%); -webkit-filter: contrast(170%); }

|

hue-rotate()
.change-filter { filter: hue-rotate(50deg); -webkit-filter: hue-rotate(50deg); }

|

grayscale()
.change-filter { filter: grayscale(100%); -webkit-filter: grayscale(100%); }

|

invert()
.change-filter { filter: invert(100%); -webkit-filter: invert(100%); }

|

Saturate()
.change-filter { filter: saturate(50%); -webkit-filter: saturate(50%);}

|

要查看这些滤镜的实际示例,值得上网查看一下——有很多示例可供参考。作为一个起点,可以查看约翰尼·辛普森在www.inserthtml.com/2012/06/css-filters/上的文章;虽然这篇文章已经有几年了,而且有些设置已经进行了调整,但仍然可以对 CSS3 滤镜的可能性提供有用的了解。

让我们换个方式来思考——虽然我们可以使用简单的 CSS3 滤镜来调整对比度和亮度等方面,但我们也可以使用另一种方法:背景混合。

使用 CSS3 合并图像

在某些情况下,我们可能更喜欢不直接处理图像,而是改变背景图像。在 PhotoShop 中可以很容易地在静态图像中实现类似的效果,但在互联网上较少见。

幸运的是,我们可以在 CSS 中使用background-blend模式来实现相同的效果——这样可以让我们将两张图像合并在一起。使用background-blend模式(在桌面浏览器中的浏览器支持良好)可以避免手动编辑每张照片的需求,因此如果任何照片更改了,同样的效果可以轻松应用到它们的替代品上。

与我们已经检查过的那些滤镜一样,我们会在 CSS 中应用这些滤镜。然后我们可以随心所欲地使用 jQuery 打开或关闭它们。我不会重新介绍所需的 jQuery 代码,因为我们已经在本章的早些时候见过了;简单地说,我们会应用background-blend模式,使用以下示例:

  <style>
    .blend { width: 389px; height: 259px; background:#de6e3d url("img/flowers.jpg") no-repeat center center; }
    .blend.overlay { background-blend-mode: overlay; }
  </style>
</head>

在这个例子中,我们使用了overlay滤镜。这个复杂的滤镜会根据背景色值来乘以颜色。它的净效果是让浅色变得更浅,让深色变得更深,如下一个截图所示:

使用 CSS3 合并图像

提示

代码下载包中有两个示例,其中包括overlay.htmlmultiply.html文件中的这种混合模式的示例。

有很多滤镜选项可供选择,比如乘法、变亮、避免和颜色燃烧——这些都旨在产生类似于 PhotoShop 中使用的效果,但不需要昂贵的应用程序。所有滤镜都遵循类似的格式。在谷歌上搜索滤镜的示例很值得,比如在www.webdesignerdepot.com/2014/07/15-css-blend-modes-that-will-supercharge-your-images/上展示的那些。

注意

如果您想了解更多信息,请访问 Mozilla 的开发者网站developer.mozilla.org/en-US/docs/Web/CSS/background-blend-mode。要获取此滤镜的真正有用的示例(以及与 jQuery 结合的灵感来源),请查看 2016 年美国总统候选人演示codepen.io/bennettfeely/pen/rxoAc

好了,是时候真正投入一些 jQuery 的时间了!让我们转向使用插件,并看看我们可以使用什么可用的东西来实现一些效果。我们将从使用 CamanJS 作为示例开始,然后深入探讨手动创建滤镜,并看看为什么这并不总是实现所需效果的最佳方式!

使用 CamanJS 应用滤镜

到目前为止,我们已经使用 CSS3 应用了滤镜。这对于轻量级解决方案来说是完美的,但在某些情况下,我们可能需要做更多,而 CSS3 则不够。

进入 jQuery!在接下来的几页中,我们将简要介绍如何使用 CamanJS 作为我们示例 jQuery 插件来应用滤镜。然后,我们将继续看看如何轻松(或复杂)地手动创建相同的效果,而不需要依赖第三方插件。

介绍 CamanJS 作为插件

CamanJS 是为 jQuery 提供的几个插件之一,它允许我们应用任意数量的滤镜;我们可以从库中提供的预设滤镜中选择,或者创建我们自己的组合。

该插件可以从camanjs.com/获得,并可以从 GitHub 下载github.com/meltingice/CamanJS。另外,我们可以使用 NodeJS 或 Bower 来安装该库。该插件还可以通过 CDN 在www.cdnjs.com获得 - 搜索 CamanJS 以获取在您的项目中使用的最新 URL。

值得注意的是,可以使用两种方法之一来应用滤镜 - 第一种是作为 HTML 数据属性:

<img data-caman="saturation(-10) brightness(20) vignette('10%')" src="img/image.jpg">

第二种方法是使用 jQuery,正如我们将在下一个演示中看到的;我们将在我们的示例中一直使用这种方法。有了这个想法,让我们开始动手,并看看如何使用 CamanJS 来应用滤镜,就像我们下一个演示中展示的那样。

构建一个简单的演示

在这个演示中,我们将使用 CamanJS 库来对我们在本章节中一直在使用的花朵图像应用三个滤镜中的任何一个。

注意

记住 - 如果您使用 Chrome,请在本地 Web 服务器内运行此演示,如“准备就绪”部分所建议的那样。

让我们开始:

  1. 首先,从附带本书的代码下载中提取以下文件。对于这个演示,我们需要以下文件:caman.htmlflowers.jpgusecaman.jsjquery.min.jsusecaman.css。将 JavaScript 文件存储在js子文件夹中,将 CSS 文件存储在css子文件夹中,将图像存储在img子文件夹中,并将 HTML 标记存储在项目文件夹的根目录中。

  2. 运行caman.html演示文件。如果一切顺利,我们应该看到以下图片出现:构建一个简单的演示

  3. 让我们探索操作演示所需的 jQuery。如果我们查看usecaman.js,我们会看到以下代码。这用于获取我们标记中<canvas>元素的句柄,然后在其上绘制flowers.jpg图像。

      var canvas = $('#canvas');
      var ctx = canvas[0].getContext("2d");
      var img = new Image();
      img.src = "img/flowers.jpg";
      ctx.drawImage(img, 0, 0);
    
  4. 深入挖掘一下,我们应该看到以下方法——这个方法处理了<canvas>元素恢复到其原始状态的重置;请注意使用的drawImage()方法,这是使用不同滤镜操作图像的关键:

      $reset.on('click', function(e){
        e.preventDefault();
        var img = new Image();
        img.src = "img/flowers.jpg";
        ctx.save();
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, canvas[0].width, canvas[0].height);
        ctx.restore();
        ctx.drawImage(img, 0, 0);
        Caman('#maincanvas', 'img/flowers.jpg', function(){
          this.revert(false).render();
        });
      });
    
  5. 然后,我们再加上三个不同的事件处理程序——这些应用了相应的 CamanJS 滤镜:

    $noise.on('click', function(e) {
      e.preventDefault();
      Caman('#maincanvas', 'img/flowers.jpg', function() {
        this.noise(10).render();
      });
    });
    

我们的简单演示只是展示了使用 CamanJS 可能性的冰山一角。详细查看该网站,了解使用该库可以实现的效果是非常值得的。作为灵感的来源,请查看 Carter Rabasa 的文章,他使用该库创建了一个基于著名的 Instagram 网站的 Phonestagram 应用程序;该文章位于www.twilio.com/blog/2014/11/phonestagram-fun-with-photo-filters-using-node-hapi-and-camanjs.html

注意

值得注意的是,CamanJS 能够轻松处理 HiDPI 图像——我们只需在代码中设置data-caman-hidpi属性。如果检测到设备支持高分辨率图像,Caman 将自动切换到使用高分辨率版本。但要注意,由于使用了额外的像素,渲染时间会更长。

Getting really creative

回想一下本章开头提到的地方,我提到 CSS3 滤镜提供了一个方便且轻量级的手段来操作图像。它们的使用意味着我们可以减少编辑图像所需的工作量,并且如果图像的大小或内容发生变化,更新它们会更容易。

然而,使用 CSS3 滤镜只能做到这一点——这就是 jQuery 接管的地方。要了解原因,请让我们通过另一个演示来进行工作。这一次,我们将使用 CamanJS 附带的更高级的预设滤镜之一,如果仅使用 CSS3 滤镜就很难实现。

记住——如果您使用的是 Chrome,请从本地 Web 服务器中运行此演示,如“准备就绪”部分所建议的那样。让我们开始:

  1. 对于这个演示,我们需要从本书配套的代码下载中获取一些文件。它们是:caman-advanced.csscaman-advanced.htmlcaman.full.jsjquery.min.jsflowers.jpg。将每个文件放在相关的子文件夹中,而将 HTML 标记文件放在项目区的根目录。

  2. 在一个新文件中,添加以下代码以配置 CamanJS 对象以使用库提供的针孔滤镜;将其保存为caman-advanced.js,放在js子文件夹中。

    $(document).ready(function() {
      $("input").on("click", function() {
        Caman("#caman-image", function () {
          this.pinhole().render();
        });
      })
    });
    
  3. 如果我们预览演示,可以看到点击更改滤镜按钮时,图像现在显示为针孔相机效果。参考下面的图片:Getting really creative

在 CamanJS 网站上有许多更不寻常滤镜的示例。前往camanjs.com/examples/查看使用该库可能出现的情况。

尽管我们集中在使用 CamanJS 作为示例(部分是因为这个库的广泛可能性),但是还有其他可用的库,提供类似的滤镜功能,但并非所有库都能达到 CamanJS 的水平。以下是一些可供探索的例子,让你开始:

对于那些不喜欢使用开源软件的人,一个你可能喜欢探索的例子是 JSManipulation 库,这个库可以在 CodeCanyon 网站上以出售的方式获得,网址为:codecanyon.net/item/jsmanipulate-jquery-image-manipulation-plugin/428234

好的,让我们继续,并且真正投入到一些事情中去。目前为止,我们已经使用了大多数情况下适用的插件。但是在一些情况下,我们可能会发现需要手动创建自己的滤镜,因为现有的滤镜不适用于我们的需求。让我们看看一些例子,以了解涉及到的内容。

小贴士

要了解使用 Caman 时可能出现的情况,请查看 Martin Angelov 在tutorialzine.com/2013/02/instagram-filter-app/的这篇文章。他通过使用 jQuery,CamanJS 和 jQuery Mousewheel 来构建一个 Instagram 滤镜应用程序。

手动创建简单的滤镜

创造我们自己的滤镜的关键(也与许多预构建的插件一样)是使用<canvas>元素,并熟悉getImageData方法。我们可以使用后者来操纵每个图像中的颜色通道,以产生所需的效果。

我们可以花时间详细讨论如何使用此方法,但我认为亲自尝试会更好。所以让我们深入了解并使用它手动创建一些滤镜,首先是将图像转换为灰度。

将图像转换为灰度

对于三个演示中的第一个演示,我们将对我们在本章中一直使用的flowers.jpg图像的颜色进行去饱和处理。这将使其呈现出灰度外观。

注意

如果在本地运行此演示,您可能会遇到跨域错误。我建议按照准备工作部分的建议在本地 Web 服务器上运行它。

让我们看看我们需要做什么:

  1. 让我们从附带本书代码下载中提取flowers.jpg的副本、jquery.min.jsmanual-grayscale.htmlmanual-grayscale.css。将图像存储在img子文件夹中,JavaScript 文件存储在js子文件夹中,样式表存储在css子文件夹中;HTML 标记需要存储在我们项目文件夹的根目录下。

  2. 在一个新文件中,继续添加以下代码,并将其保存为manual-grayscale.js - 这将查找每个设置了图片类名为 picture 的图像集,然后调用grayscale函数执行魔术:

    $(window).load(function(){
      $('.picture').each(function(){
        this.src = grayscale(this.src);
      });
    });
    
  3. 将以下函数添加到$(window).load方法的下方 - 这将用等效的灰度重写图像:

    function grayscale(src){
      var i, avg;
      var canvas = document.createElement('canvas');
      var ctx = canvas.getContext('2d');
      var imgObj = new Image();
      imgObj.src = src;
      canvas.width = imgObj.width;
      canvas.height = imgObj.height;
      ctx.drawImage(imgObj, 0, 0);
      var imgPixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
      for(var y = 0; y < imgPixels.height; y++){
        for(var x = 0; x < imgPixels.width; x++){
          i = (y * 4) * imgPixels.width + x * 4;
          avg = (imgPixels.data[i] + imgPixels.data[i + 1] + imgPixels.data[i + 2]) / 3;
          imgPixels.data[i] = avg;
          imgPixels.data[i + 1] = avg;
          imgPixels.data[i + 2] = avg;
        }
      }
      ctx.putImageData(imgPixels, 0, 0, 0, 0, imgPixels.width, imgPixels.height);
      return canvas.toDataURL();
    }
    
  4. 如果我们此时运行演示,我们应该会看到一张带有极化效果边框的图像的副本,但这次,它已经被转换成了灰度等效图像,接着是截图本身:将图像转换为灰度

在我们继续进行下一个演示之前,有一些关键点需要注意,与我们刚刚使用的代码相关。所以让我们花点时间详细介绍一下这些:

  • 我们所做的大部分工作都使用了<canvas>元素 - 这使我们能够以比使用普通的 JPG 或 PNG 格式图像更细致的细节来操作图像。

  • 在这种情况下,我们使用纯 JavaScript 使用语句 document.createElement('canvas') 创建了 canvas 元素。有些人可能会认为将纯 JavaScript 与 jQuery 混合使用是不好的做法。在这种情况下,我个人认为它提供了更清洁的解决方案,因为使用 jQuery 动态创建的<canvas>元素不会自动添加上下文。

  • 作为一种方法,getImageData()是使用此路由操作任何图像的关键。然后,我们可以处理每个颜色通道,即红色、绿色和蓝色,以产生所需的效果。

我们可以使用这个过程来生成任意数量的不同滤镜 - 比如说一个棕褐色调的滤镜?让我们看看我们如何手动创建这样一个滤镜。在这种情况下,我们将进一步将其转换为一个小插件,以便以后重复使用。

添加棕褐色调

我们已经看到了从头开始制作一个彩色滤镜是多么简单 – 那么创建不同类型的滤镜呢?我们可以使用类似的技术来制作其他滤镜,所以让我们继续创建一个基于棕褐色的滤镜,以补充本章早些时候使用的 CSS3 版本。

注意

记住 – 如果您使用的是 Chrome,请从本地 Web 服务器中运行此演示,如“准备就绪”部分所建议的那样。

让我们开始吧:

  1. 我们将像往常一样从随书代码下载中提取相关文件。对于这一个,我们需要以下文件:jquery.min.jsflowers.jpgmanual-sepia.cssmanual-sepia.html。将它们存储在我们项目文件夹的相应子文件夹中。

  2. 在一个新文件中,我们需要创建我们的棕褐色插件,所以继续添加以下代码,从设置调用开始,以找到所有类名为.sepia的图像:

    jQuery.fn.sepia = function () {
      $(window).load(function () {
        $('.sepia').each(function () {
          var curImg = $(this).wrap('<span />');
          curImg.attr("src", grayImage(this));
        });
      });
    
  3. 下一个非常重要的函数是grayImage函数,它接收图像,将其绘制到画布上,然后操纵图像中的每个颜色通道,最后将其渲染回屏幕:

      function grayImage(image) {
        var canvas = document.createElement("canvas");
        var ctx = canvas.getContext("2d");
        canvas.width = image.width;
        canvas.height = image.height;
        ctx.drawImage(image, 0, 0);
        var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    
        for (var y = 0; y < imgData.height; y++) {
          for (var x = 0; x < imgData.width; x++) {
            var pos = (y * 4) * imgData.width + (x * 4);
            var mono = imgData.data[pos] * 0.32 + imgData.data[pos + 1] * 0.5 + imgData.data[pos + 2] * 0.18;
            imgData.data[pos] = mono + 50;
            imgData.data[pos + 1] = mono;
            imgData.data[pos + 2] = mono - 50;
          }
        }
        ctx.putImageData(imgData, 0, 0, 0, 0, imgData.width, imgData.height);
        return canvas.toDataURL();
      }
    };
    $.fn.sepia();
    
  4. 让我们在浏览器中预览结果。如果一切顺利,我们应该会看到我们的图像具有漂亮的棕褐色调,如下图所示:添加棕褐色调

这个滤镜版本在我们使用的代码方面可能看起来略有不同,但其中大部分是由于将其重新配置为插件以及一些变量名称的更改。如果我们仔细观察,就会发现两个示例中都使用了相同的原理,但产生了两个不同版本的同一图像。

提示

如果您想要了解更多关于使用getImageData()方法的信息,请查看 W3School 的教程,可在www.w3schools.com/tags/canvas_getimagedata.asp上找到。

图像混合

对于我们的第三个和最后一个演示,并且为了证明getImageData()的多功能性,我们将在本章中一直使用的同一张花朵图像上添加一种色调。

这个演示相对而言比较简单。我们已经有了一个框架,以插件的形式存在;我们只需要将嵌套的for…块替换为我们的新版本即可。让我们开始吧:

  1. manual-sepia.js的副本中,查找大约在第17行左右的以下行:

    for (var y = 0; y < imgData.height; y++) {
    
  2. 将高亮显示的内容删除直到第25行。用以下代码替换它:

        var r_weight = 0.44;
        var g_weight = 0.5;
        var b_weight = 0.16;
        var r_intensity = 255;
        var g_intensity = 1;
        var b_intensity = 1;
    
        var data = imgData.data;
        for(var i = 0; i < data.length; i += 4) {
          var brightness = r_weight * data[i] + g_weight * data[i + 1] + b_weight * data[i + 2];
          data[i] = r_intensity * brightness; // red
          data[i + 1] = g_intensity * brightness; // green
          data[i + 2] = b_intensity * brightness; // blue
        }
        ctx.putImageData(imgData, 0, 0);
    
  3. 现在,将文件保存为manual-sepia.js,然后在浏览器中预览manual-sepia.html。如果一切正常,我们应该会看到图像出现,但这次有了红色色调,如下图所示:图像混合

这个演示中使用的数学看起来很简单,但可能需要一点解释。这是一个两阶段过程,我们首先使用_weight变量来计算亮度级别,然后使用_intensity变量来计算相关的强度级别,然后再将其重新应用于适当的颜色通道。

掌握使用这种方法构建滤镜所需的数学可能需要一些时间(这超出了本书的范围),但一旦你理解了数学,它就会带来一些真正的可能性!

注意

为了方便起见,我在这个演示中重新使用了相同的文件,以证明我们可以应用特定的颜色色调。在实践中,我们需要重新命名插件名称,以更好地反映正在使用的颜色(而且在这种情况下,不会是 sepias!)。

当然,我们还可以更进一步。这样做可能需要一些强大的数学,因此不适合胆怯的人!如果你喜欢挑战,那么一个很好的起点是学习使用卷积掩模,它看起来类似于以下内容(这个是用于图像模糊的):

    [.1, .1, .1],
    [.1, .2, .1],
    [.1, .1, .1],

这将使我们能够创建一些非常复杂的滤镜,比如 Sobel 滤镜(en.wikipedia.org/wiki/Sobel_operator),甚至是 Laplace 滤镜(en.wikipedia.org/wiki/Discrete_Laplace_operator#Implementation_in_Image_Processing)- 警告:这数学真的很强大!为了将它变得简单一点,请看看 Google。以下是一些有用的起点:

让我们换个方式!我们已经使用不同的方法对我们的图像应用了一些滤镜,但有没有人注意到效果有多突然?一个更令人愉悦的路线是动画过渡过程。让我们看看如何使用cssAnimate库实现这一点。

使用滤镜来实现图像动画

好的,我们已经讨论了许多不同的方法来应用滤镜来操作图像的外观。在我们继续并查看一些实际示例之前,让我们停顿一下。

有没有人注意到,当使用 jQuery 时,我们失去了逐渐从一个状态过渡到另一个状态的能力?过渡只是提供状态变化的一种方式之一 - 毕竟,逐渐改变状态比看到突然切换更容易接受!

我们可以花时间从头开始使用 jQuery 创造一个解决方案。然而,更明智的解决方案是使用一个专门用于此目的的插件。

引入 cssAnimate

进入 cssAnimate!这个小宝石由 Clemens Damke 制作,它生成了必要的 CSS3 样式来动画地更改状态,但如果不支持,则退回到使用 jQuery 的 animate() 方法处理程序。该插件可从 cortys.de/cssAnimate/ 下载。尽管该网站指出了 jQuery 1.4.3 或更高版本的最低要求,但在与 jQuery 2.1 一起使用时,它可以无明显问题地运行。

让我们看一下我们即将产生的截图:

引入 cssAnimate

让我们开始吧:

  1. 我们将从随本书附带的代码下载中提取以下文件开始:cssanimate.htmlcssanimate.cssflowers.jpgjquery.min.jsjquery.cssanimate.min.js

  2. 将 JavaScript 文件保存到 js 子文件夹中,将图像保存到 img 文件夹中,将 CSS 文件保存到 css 子文件夹中,并将 HTML 标记保存到我们项目区域的根文件夹中。

  3. 在一个单独的文件中,添加以下代码,该代码将动画更改为 hue-rotate 滤镜:

    $(document).ready(function(){
      $("input[name='css']").on("click", function(){
        $("img").cssAnimate({filter: hue-rotate(50deg), -webkit- filter: hue-rotate(50deg)}, 500, "cubic-bezier(1,.55,0,.74)");
      })
    });
    
  4. 如果一切顺利,当点击使用 CSS 更改滤镜按钮时,我们应该看到花朵似乎变成深粉色,就像我们练习开始时所示。

乍一看,我们唯一能看到的变化是图像变为深粉色。然而,真正的变化将在我们使用 DOM 检查器(例如 Firebug)检查代码时显示出来:

引入 cssAnimate

这个库的美妙之处在于,尽管它已经有几年了,但似乎仍然可以很好地与现代版本的 jQuery 配合使用。这为我们开辟了一些真正可以探索的途径,就我们可以使用的过渡动画而言。

注意

过渡支持在主要浏览器中几乎达到了 100%,除了 Opera Mini。要获取最新的情况,值得查看 Can I Use 网站 caniuse.com/#feat=css-transitions

尽管 cssAnimate 中内置的动画数量有限,但至少它包括对 cubic-bezier 值的支持。Matthew Lein 制作了一个文件,其中包含一些著名缓动效果的 cubic-bezier 等效值;这可以从 github.com/matthewlein/Ceaser/blob/master/developer/ceaser-easings.js 获取。我们可以使用这个来提供可以放入我们动画中以产生期望效果的值。或者,我们可以使用像 cubic-bezier.com 这样的网站设计自己的 cubic-bezier 缓动效果 - 这提供了可以用于我们动画的类似值。

注意

顺便说一下 - 当我为这本书进行研究时,我发现了这个简洁的演示:codepen.io/dudleystorey/pen/pKoqa。我想知道我们是否可以使用 cssAnimate 来产生类似的效果?

好了 - 目前足够使用滤镜了!让我们转换焦点,深入一些更实际的内容。你们有多少人曾经在线签署过某物,使用电子签名?如果情况需要,这是一个很棒的效果。我们将看看如何实现,但是扩展它,以便我们可以保存图像供以后使用。

创建签名板并导出图像

现在我们已经看到了如何操作图像,让我们把注意力转向更基础的事情;捕捉绘制在画布元素上的图像。

随着我们越来越多地进入数字化世界,会有一些场合需要我们用电脑电子签署文件。这并不意味着我们不应该在狂欢一晚之后的早晨签署任何文件,但更糟糕的事情可能会发生...!考虑到这一点,让我们看看在文档签署后如何捕捉图像。

对于此演示,我们将使用 Thomas Bradley 的 jQuery Signature Pad 插件。该插件可从thomasjbradley.ca/lab/signature-pad获取。我们将进一步进行 - 不仅仅是签署我们的名字,而且还将提供一个选项,使用canvas.toDataURL()方法将输出保存为 PNG 文件。

注意

记住 - 如果你使用 Chrome,请从本地网络服务器中运行此演示,正如 准备就绪 部分建议的那样。

让我们开始:

  1. 我们将从附带本书的代码下载中下载所需的 CSS 和 HTML 标记文件,开始这个演示。继续并提取签名板文件夹并将其保存到项目区域。

  2. 接下来,将以下代码添加到一个新文件中 - 将其保存为signaturepad.js,放在我们演示文件夹的js子文件夹中:

    $(document).ready(function() {
      $('.sigPad').signaturePad();
      var canvas = $('#canvas')[0], ctx = canvas.getContext('2d');
    
      $('#download').on('click', function() {
        saveImage();
        downloadCanvas(this, 'canvas', 'signature.png');
      });
    
      function saveImage() {
        var api = $('.sigPad').signaturePad();
        var apitext = api.getSignatureImage();
        var imageObj = new Image();
        imageObj.src = apitext;
        imageObj.onload = function() {
          ctx.drawImage(imageObj, 0, 0);
        };
      }
    
      function downloadCanvas(link, canvasId, filename) {
        link.href = $(canvasId)[0].toDataURL();
        link.download = filename;
      }
    });
    

    注意

    代码下载中已经有这个文件的一个版本;提取并将signaturepad-completed.js重命名为signaturepad.js,然后按照本演示中所述的方法将其存储在相同的js文件夹中。

  3. 如果我们在浏览器中预览结果,应该会看到一个签名板显示,如下面的屏幕截图所示:创建签名板并导出图像

在此屏幕截图中,我已经添加了我的名字。尝试点击绘制然后画出你的名字 - 小心,需要手脚稳!接下来,点击链接。如果一切正常,我们将被提示打开或保存名为signature.png的文件。在适当的图形软件中打开它确认签名已正确保存。参考以下图像:

创建签名板并导出图像

尽管这只是一个相对简单的演示,但它开启了一些真正的可能性。除了我们使用的签名插件之外,这个演示的关键在于两个方面:使用<canvas>元素来捕获绘制的签名,以及使用.toDataURL()方法将画布元素的内容转换为数据 URI,其中包含以 PNG 格式表示的图像(默认情况下)。

我们首先获取一个句柄,然后将图像绘制到一个 canvas 元素上。一旦下载事件处理程序被触发,它就会将图像转换为数据 URI 表示形式,然后将其呈现为我们可以保存以供以后使用的格式。

注意

如果您想了解更多关于toDataURL()方法的信息,那么 Mozilla 的开发者实验室有一篇很好的文章,可以在developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement.toDataURL找到。

让我们将这个技术应用到实践,并将其与本章开头涵盖的摄像头和图像操作技术相结合。这使我们可以变得非常疯狂;想要玩一些捕捉和更改网络摄像头图像的有趣内容吗?

捕获和操作网络摄像头图像

在本章的第二个也是最后一个演示中,我们将通过网络摄像头玩一些有趣的东西 - 我们可以从笔记本电脑或独立摄像头中获取和操作图像的方式之一。

这个演示的关键在于使用getUserMedia,它允许我们控制音频或视频源。这是一个相对年轻的 API,需要使用供应商前缀来确保完全支持。与其他 API 一样,它们的需求会随着时间的推移而消失,因此定期检查caniuse.com/#search=getusermedia是值得的,以查看是否已更新支持并删除了前缀的需求。

这个演示将汇集我们探讨过的一些概念,比如应用过滤器、将画布图像保存到文件以及控制网络摄像头。为了正确运行这个演示,我们将需要从 HTTP 协议地址而不是file://.运行它。为此,您将需要一些可用的网络空间,或者使用像 WAMP(适用于 PC - www.wampserver.com/en)或 MAMP(适用于 Mac,现在也适用于 PC,来自www.mamp.info/en/)。

好的,假设这一切都就绪,让我们开始吧:

  1. 我们将从与本书附带的代码下载中提取webcam demo文件夹开始。它包含了为本演示所需的样式、标记和 jQuery 库的副本。

  2. 一旦提取出来,将整个文件夹上传到您的网络空间。我将假设您正在使用 WAMPServer,所以这将是/www文件夹;如果您使用的是其他内容,请相应地进行更改。

  3. 我们需要添加使此演示工作所需的 jQuery 魔法。在一个新文件中,继续添加以下代码;我们将逐节介绍它,从分配变量和过滤器数组开始:

    $(document).ready(function() {
      var idx = 0;
      var filters = ['grayscale', 'sepia', 'blur', 'saturate', ''];
    
      var canvas = $("canvas")[0], context = canvas.getContext("2d"),
      video = $("video")[0], localStream, videoObj = { "video": true }, errBack = function(error) {
          console.log("Video capture error: ", error.code);
        };
    
  4. 第一个函数处理通过过滤器的分页。我们循环遍历存储在过滤器数组中的过滤器名称。如果样式表中有相应的样式规则,则将以下内容应用于画布图像:

      function changeFilter(e) {
        var el = e.target;
        el.className = '';
        var effect = filters[idx++ % filters.length];
        if (effect) {
          el.classList.add(effect);
        }
      }
    
  5. 接下来,我们需要获取getUserMedia的实例,我们将用它来控制网络摄像头。由于这仍然是一个相对年轻的 API,我们必须使用供应商前缀:

      navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
    
  6. 第一个事件处理程序中,#startplay按钮是最重要的。在这里,我们捕获网络摄像头源,然后将其分配给视频对象,并生成引用我们内容的 URL。一旦分配完成,我们开始播放视频,这样我们就可以在屏幕上查看内容:

      $("#startplay").on("click", function(e) {
        if (navigator.getUserMedia) {
          navigator.getUserMedia(videoObj, function(stream) {
            video.src = window.URL.createObjectURL(stream);
            localStream = stream;
            video.play();
          }, errBack);
        }
      });
    
  7. 然后,我们需要分配一些事件处理程序。按顺序,以下处理程序处理请求以拍摄图像快照,停止视频,更改过滤器,并下载快照图像的副本:

      $("#snap").on("click", function() {
        context.drawImage(video, 0, 0, 320, 240);
      });
    
      $("#stopplay").on("click", function(e, stream) {
        localStream.stop();
      });
    
      $('#canvas').on('click', changeFilter);
    
      $("#download").on('click', function (e) {
        var dataURL = canvas.toDataURL('image/png');
        $("#download").prop("href", dataURL);
      });
    });
    
  8. 将文件保存为webcam.js,并放在我们之前在此演示中上传的webcam demo文件夹的js子文件夹中。

  9. 此时,我们可以尝试在浏览器中运行演示。如果一切正常,我们将首先收到一个请求,询问浏览器是否可以访问网络摄像头(出于安全原因),如下图所示:捕捉和操作网络摄像头图像

  10. 然后是摄像头的初始化。它以一个占位符图像开始,如下图所示;几秒钟后,这将显示实时视频:捕捉和操作网络摄像头图像

在这一点上,我们可以玩得很开心。尝试点击拍照以拍摄自己的照片快照;这将显示在实时视频的右侧。如果单击此图像,它将在样式表中设置的几个过滤器之间进行循环,并在webcam.js中引用:

  var filters = ['grayscale', 'sepia', 'blur', 'saturate', ''];

等一下 - 有人注意到单击下载照片按钮后得到的图像有什么不同吗?你们中敏锐的人很快就会注意到,这是原始图像的副本,在应用过滤器之前。

原因是这些过滤器在 CSS 中设置 - 当在浏览器窗口中显示时,它们才会产生任何效果!为了解决这个问题,我们需要修改我们的下载事件处理程序。我们可以使用我们之前探索过的 CamanJS 库来应用一些基本的过滤器,例如库中提供的日出效果。

为此,请修改#download事件处理程序以显示以下代码:

  $("#download").on('click', function (e) {
    e.preventDefault();
    Caman('#canvas', function(){
      this.sunrise();
      this.render(function() { this.save("webcam-photo.png"); });
    });
  });

现在尝试保存截图的副本。虽然它不会强制下载到您的桌面,但它仍然会在浏览器中显示一个图像,显示应用了日出滤镜的图像。

当使用getUserMedia时,我们只是触及了可能性的表面——在线学习更多是非常值得的。一个很好的起点是 Mozilla 开发者网络上的文章,可在developer.mozilla.org/en-US/docs/NavigatorUserMedia.getUserMedia找到。注意——getUserMedia不支持 IE11 或更低版本,因此您需要使用像 Addy Osmani 的getUserMedia.js这样的 polyfill 库,可在github.com/addyosmani/getUserMedia.js下载。

注意

顺便说一句,我曾考虑在这本书中加入使用 reveal.js 库来控制一个简单图像画廊的手势控制内容展示,就像在www.chromeexperiments.com/detail/gesture-based-revealjs/中展示的那样。不幸的是,代码不够稳固,而且已经有一段时间没有更新了。我很想听听你的想法。这是展示一种流畅呈现内容的好方法,但需要更多的工作!

收尾工作

在我们总结这一章之前,值得暂停一下,考虑一下我们在本章中涵盖的一些技术带来的影响。

纯粹主义者可能会质疑使用 jQuery 应用过滤器的必要性,特别是如果我们所需要做的只是使用诸如.addClass()或者甚至.toggleClass()这样的方法来应用或移除特定的过滤器。另一方面,这本书当然是关于 jQuery 的,我们应该集中精力使用它,即使这意味着显示我们使用的一些过滤效果的明显延迟。

这个问题的简短答案取决于你——任何人都可以在某种程度上编写 jQuery 代码,但一般和优秀开发者的区别不仅仅在于编写代码。

真正的区别部分在于做出正确选择。jQuery 经常被视为简单的选择,特别是因为它提供了最广泛的支持范围。我们可以创建任何类型的过滤器来满足我们的需求,但这总是以处理能力为代价——我们无法摆脱操作画布元素需要大量资源的事实,因此完成速度很慢。如果使用高清图像(正如我们在使用 CamanJS 应用过滤器部分中注意到的那样)——事实上,速度会更慢,因为需要处理更多像素!

结论是,我们需要仔细考虑我们需要应用哪些过滤器,以及我们是否可以简单地使用 CSS3 过滤器来满足我们的需求。没错,这些可能无法解决我们所有的需求,但是支持正在变化。我们应该真正考虑在延迟不是问题的情况下使用 jQuery 过滤器,并且应用程序不会在移动平台上使用(由于处理每个像素所需的资源!)。

总结

操纵图像是 jQuery 中的一个悖论——我们可以使用 CSS3 滤镜轻松产生简洁的效果,但受到 CSS3 滤镜能够提供的限制;或者我们可以产生任何我们想要的滤镜,但以像素级别操作图像所需的处理资源为代价!在本章中,我们涵盖了大量信息,让我们花一点时间回顾我们学到的东西。

我们以添加 CSS3 滤镜开始,看到了将这些应用到图像上是多么容易。然后,我们转而研究了使用 CSS3 混合图像的不同技术,然后将注意力转向了检查 jQuery 图像插件。

我们花了一些时间探索一些应用滤镜的基本选项,然后创建了我们自己的基于 jQuery 的滤镜。然后我们转而研究如何通过动画过渡到使用滤镜,以帮助提供更流畅的过渡,最后看一下使用签名板和网络摄像头创建基本演示的方法,作为使用 jQuery 捕获图像的手段。

然后,我们总结了本章关于何时应该使用 CSS3 滤镜或 jQuery 的一些最终想法,强调任何人都可以编写代码,但好的开发人员知道在开发过程中何时使用正确的工具。

在下一章中,我们将扩展插件的使用,并探讨将插件开发技能提升到下一个水平。

第十一章:编写高级插件

在整本书中,一个共同的主题是使用插件——现在是创建一个插件的时候了!

可供使用的插件数量之多简直令人难以置信,从只有几行代码的插件到数百行的插件不等。我非常相信“有志者,事竟成”这句话——可以说插件满足了这种意愿,并为用户提供了解决需求或问题的途径。

在接下来的几页中,我们将从头到尾看一下如何开发一个高级插件。我们不仅关注构建本身,还将探讨一些技巧和窍门,以帮助我们在使用插件时进一步提高开发技能。我们将涵盖最佳实践,并查看一些可以提高当前编码技能的领域。在接下来的几页中,我们将涵盖以下主题:

  • 最佳实践和原则

  • 检测插件开发不佳的迹象

  • 为 jQuery 插件创建设计模式

  • 设计一个高级插件并使其可供使用

准备好了吗?

检测插件开发不佳的迹象

想象一下场景,如果你愿意——你花几周时间开发一个复杂的插件,它几乎包含了所有功能,让所有看到的人都惊叹不已。

听起来像是完美的理想境界,不是吗?你把它发布到 GitHub 上,创建一个很棒的网站,等待用户踊跃下载你的最新作品。你等待着……等待着……但最后一位用户也没有。好吧……怎么回事?

正如我经常说的,任何人都可以编写代码。成为更好的 jQuery 插件开发者的关键是理解什么是好的插件,以及如何将其付诸实践。为了帮助理解,让我们花点时间看一下一些指标,可以用来判断一个插件是否可能失败:

  • 你没有在做一个插件!通行的做法是使用少数几种插件模式之一。如果你没有使用其中一种模式(如下所示的模式),那么你的插件被接受的可能性很低。

    (function($, window, undefined) {
      $.fn.myPlugin = function(opts) {
        var defaults = {
        // setting your default values for options
      }
      // extend the options from defaults with user's options
      var options = $.extend(defaults, opts || {});
      return this.each(function(){ // jQuery chainability
        // do plugin stuff
      });
    })(jQuery, window);
    
  • 虽然我们在参数中定义了undefined,但我们只在自调用函数中使用了 $window。这可以防止恶意传递undefined的值到插件中,因为它在插件内部将保持为undefined

  • 你花时间编写代码,但忽略了其中一个关键元素——准备文档!我一次又一次地看到插件的文档非常少或根本不存在。这使得理解插件的构成和如何充分利用它变得困难。关于文档编写没有硬性规定,但普遍认为,文档越多越好,而且应该是内联和外部的(以 readme 或 wiki 的形式)。

  • 在缺乏合适文档主题上继续进行,开发人员会因为插件具有硬编码的样式或者过于不灵活而感到不满。我们应该考虑所有可能的需求,但要确定我们是否会为特定需求提供解决方案。应用于插件的任何样式都应该通过插件选项提供,或者作为样式表中的特定类或选择器 ID – 将其放在行内被认为是不良实践。

  • 如果你的插件需要太多配置,那么这很可能会让人们失去兴趣。虽然一个更大、更复杂的插件应该明确地为最终用户提供更多的选项,但提供的内容也是有限度的。相反,每个插件至少应该设置一个不带参数的默认行为;用户不会喜欢为了使插件工作而设置多个值!

  • 对最终用户来说,插件不提供任何形式的示例是很让人失望的。至少应该提供一个基本的“hello world”类型的示例,其中定义了最小配置。提供更多涉及的示例,甚至与其他插件一起工作的示例,可能会吸引更多的人。

  • 一些插件失败的原因很基础。这些包括:没有提供变更日志或使用版本控制,不能在多个浏览器中工作,使用过时的 jQuery 版本或在实际上不需要时包含它(依赖性太低),或者没有提供插件的缩小版本。使用 Grunt 就没有借口了!我们可以自动化大部分开发人员所期望的基本管理任务,如测试、缩小插件或维护版本控制。

  • 最后,插件可能因为两个简单的原因而失败:要么它们太聪明,试图实现太多(使得调试困难),要么太简单,jQuery 作为库的依赖性不足以保证包含它。

很多事情需要考虑!虽然我们无法预测插件是否会成功,或者使用情况会不会低,但我们至少可以尝试通过将这些提示中的一些(或全部)纳入我们的代码和开发工作流程中来最小化失败的风险。

从更实际的角度来看,我们可以选择遵循许多设计模式中的任何一种,以帮助我们的插件给予结构和一致性。我们在第三章中提到过这一点,组织您的代码。美妙之处在于我们可以自由地在 jQuery 插件中使用类似的原则!让我们在使用其中一个来开发一个简单插件之前,花一点时间考虑一些可能的例子。

介绍设计模式

如果你在 jQuery 中开发代码花费了任何时间,那么很可能你创建了一个或多个插件;从技术上讲,这些插件可以只有几行代码,也可以更加实质性。

随着时间的推移,修改插件中的代码可能会导致内容变得笨重且难以调试。解决这个问题的一种方法是使用设计模式。我们在第三章中介绍了这一点,组织您的代码。许多相同的原则同样适用于插件,尽管模式本身当然会有所不同。让我们考虑一些例子。

最基本的模式是轻量级起步,适合那些以前开发过插件但对遵循特定模式的概念尚不熟悉的人。这种特定模式基于常见的最佳实践,例如在调用函数之前使用分号;它会传递标准参数,如windowdocumentundefined。它包含一个基本的默认对象,我们可以扩展它,并在构造函数周围添加一个包装器以防止多个安装引起的问题。

相反,我们也可以尝试使用完整小部件工厂。尽管它被用作 jQuery UI 的基础,但它也可以用来创建标准的 jQuery 插件。这种模式非常适合创建复杂的、基于状态的插件。它包含了所有使用的方法的注释,以确保逻辑符合你的插件。

我们还介绍了命名空间的概念,即添加特定名称以避免与全局命名空间中的其他对象或变量发生冲突。虽然我们可能在代码中使用命名空间,但我们也可以同样将其应用于插件。这种模式的好处在于我们可以检查其现有实例;如果名称不存在,则我们可以自由添加它,否则我们可以使用相同命名空间扩展现有插件。

这些是可供使用的三种插件模式之一;但我确信会有一个问题,那就是使用哪一个?和许多事情一样,没有对错答案;这将取决于具体情况。

注意

最常见的插件设计模式列表可在github.com/jquery-boilerplate/jquery-patterns找到。

创建或使用模式

如果你是第一次使用插件设计模式,那么轻量级起步是开始的最佳位置。使用任何插件模式或设计自己的插件模式有三个关键方面:

  • 架构:这定义了组件之间应如何交互的规则。

  • 可维护性:任何编写的代码都应易于扩展和改进。它不应从一开始就被锁定。

  • 可重用性:你现有的代码可以多频繁地重用?它可以多频繁地重用,节省的时间就越多,维护起来也会更容易。

使用模式的重要之处在于没有一个单一的正确答案。关键在于哪种模式最符合你的需求。最好的方法是尝试它们。随着时间的推移,经验将为您提供一个明确的指示,哪种模式对于特定情景效果最佳。

提示

关于使用特定插件模式的利弊的讨论,请移步到 Smashing Magazine 的文章www.smashingmagazine.com/2011/10/11/essential-jquery-plugin-patterns/。虽然已经有几年了,但其中许多观点仍然具有价值。

无论如何,让我们回到现在吧! 没有比现在更好的时间来获得经验了,所以让我们看一看 jQuery 轻量级样板模式。这实现了 Singleton/Module 设计模式。它帮助开发人员编写封装代码,可以远离污染全局命名空间。

在接下来的几页中,我们将开发一个提示框插件。我们将从一个不使用任何模式的典型构建开始,然后修改它以使用轻量级样板格式。然后我们将深入探讨一些小贴士和技巧,这将帮助我们考虑更大的画面,并希望使我们成为更好的开发者。

设计一个高级插件

好了——不要再闲聊了! 让我们深入研究一些代码吧! 在接下来的几页中,我们将花一些时间开发一个在页面上显示简单提示框的插件。

好吧,在你们都喊叫说“不要再一个提示框插件了……!”之前,选择这个功能有一个很好的理由。一旦我们开发了插件的第一个版本,一切都会变得清晰。让我们开始吧——我们将从简要介绍创建我们的插件开始:

  1. 对于这个演示,我们将需要这本书附带的代码下载中这一章的整个代码文件夹。继续并提取它,保存到我们的项目区域。

  2. 在文件夹中,运行tooltipv1.html文件,其中包含一个由六幅图像组成的网格,以及一些虚拟文本。依次将鼠标悬停在图像上。如果一切正常,它会显示一个提示框:设计一个高级插件

此刻你可能在想所有代码是如何串联在一起的。这是一个合理的问题……但我们将打破传统,不去审视它。相反,我想专注于重新设计代码,使用样板格式,这将有助于使其更易于阅读、调试和扩展。让我们考虑一下这对我们的插件意味着什么。

使用样板重建我们的插件

如果您对样板编制还不熟悉,请举手?有可能您已经遇到了一些例子,如 Bootstrap(www.getbootstrap.com),或者甚至 HTML5 Boilerplate(html5boilerplate.com/)。为了帮助您熟悉这个术语,它基于一个简单的想法:使用模板来帮助构建代码结构。这并不意味着它会为我们编写代码(可惜——我们可以因此无所作为而赚取数百万,哈哈!),但它通过重用框架来快速开发代码,比如完整的网站或者甚至 jQuery 插件,有助于节省时间。

对于我们的下一个演示,我们将使用来自github.com/jquery-boilerplate/jquery-patterns的 jQuery Boilerplate 模板重新设计我们的插件。与互联网一样,某种善良的灵魂已经创建了一个使用这种技术的良好的工具提示示例,因此我们将根据我们的需要进行调整。

提示

如果您对学习更多关于 jQuery Boilerplate 插件模式的内容感兴趣,您可能会喜欢查看 Jonathan Fielding 的《Instant jQuery Boilerplate for Plugins》,该书由 Packt Publishing 出版。

我们将使用的插件示例是由法国网页开发者 Julien G 提供的。原始版本可通过 JSFiddle 在jsfiddle.net/molokoloco/DzYdE/上找到。

  1. 让我们开始(像往常一样),从代码下载中提取本章的代码文件夹的副本。如果您已经从上一个练习中拥有它,那么我们可以使用它。

  2. 导航至version 2文件夹,然后在浏览器中预览tooltipv2.html。如果一切顺利,我们应该看到与前一个示例中相同的一组图像,并且工具提示应用了相同的样式。

乍看之下,似乎什么也没有改变——这本身实际上是成功的一个很好的指标!真正的变化在于tooltipv2.js中,在version 2文件夹下的js子文件夹中。让我们逐步进行,从声明变量开始:

  1. 我们首先声明了 jQuery、documentwindowundefined的属性。你可能会问为什么我们要传入undefined——这是一个很好的问题:这个属性是可变的(意味着它可以被更改)。虽然在 ECMAScript 5 中它被设置为不可写,但在我们的代码中不使用它意味着它可以保持未定义并防止恶意代码的尝试。传递剩下的三个属性可以使我们在代码中更快地引用它们:

    (function($, window, document, undefined) {
      var pluginName = 'tooltip', debug = false;
    
  2. 下一步是内部方法。我们将它们创建为internal对象中的方法;第一个负责将工具提示定位在屏幕上,而showhide控制工具提示的可见性:

    var internal = {
      reposition: function(event) {
        var mousex = event.pageX, mousey = event.pageY;
    
        $(this)
        .data(pluginName)['tooltip']
        .css({top: mousey + 'px', left: mousex + 'px'});
      },
    
      show: function(event) {
        if (debug) console.log(pluginName + '.show()');
        var $this  = $(this), data = $this.data(pluginName);
    
        data['tooltip'].stop(true, true).fadeIn(600);
        $this.on('mousemove.' + pluginName, internal.reposition);
      },
    
      hide: function(event) {
        if (debug) console.log(pluginName + '.hide()');
        var $this = $(this), data  = $this.data(pluginName);
        $this.off('mousemove.' + pluginName, internal.reposition);
        data['tooltip'].stop(true, true).fadeOut(400);
      }
    };
    
  3. 我们继续外部方法。首先在external对象内部,init函数首先出现,用于初始化我们的插件并在屏幕上呈现它。然后在移动到带有.tooltip类实例的元素时,我们调用internal.showinternal.hide内部方法:

    var external = {
      init: function(options) {
        if (debug) console.log(pluginName + '.init()');
    
        options = $.extend(
          true, {},
          $.fn[pluginName].defaults,
          typeof options == 'object' &&  options
        );
    
        return this.each(function() {
          var $this = $(this), data = $this.data(pluginName);
          if (data) return;
    
          var title = $this.attr('title');
          if (!title) return;
          var $tooltip = $('<div />', {
            class: options.class,
            text:  title
          }).appendTo('body').hide();
    
          var data = {
            tooltip:   $tooltip,
            options:   options,
            title:     title
          };
    
          $this.data(pluginName, data)
            .attr('title', '')
            .on('mouseenter.' + pluginName, internal.show)
            .on('mouseleave.' + pluginName, internal.hide);
          });
        },
    
  4. 第二个外部方法处理了更新提示文本,使用.data()方法:

        update: function(content) {
          if (debug) console.log(pluginName + '.update(content)', content);
          return this.each(function() {
            var $this = $(this), data  = $this.data(pluginName);
            if (!data) return;
            data['tooltip'].html(content);
          });
        },
    
  5. 我们将我们的插件中可用的方法圆满地结束了,包括destroy()处理程序。这样可以阻止所选提示显示,并将元素从代码中删除:

        destroy: function() {
          if (debug) console.log(pluginName + '.destroy()');
    
          return this.each(function() {
            var $this = $(this), data  = $this.data(pluginName);
            if (!data) return;
    
            $this.attr('title', data['title']).off('.' + pluginName)
              .removeData(pluginName);
              data['tooltip'].remove();
          });
        }
      };
    
  6. 最后,但同样重要的是我们的插件启动器。这个函数简单地将方法名映射到我们插件中的有效函数,或者在它们不存在时进行优雅降级:

    $.fn[pluginName] = function(method) {
      if (external[method]) return external[method]
      apply(this, Array.prototype.slice.call(arguments, 1));
      else if ($.type(method) === 'object' || !method) 
      return external.init.apply(this, arguments);
      else $.error('Method ' + method + ' does not exist on
      jQuery.' + pluginName + '.js');
    };
      $.fn[pluginName].defaults = {
      class: pluginName + 'Element'
      };
    })(window.jQuery);
    

不过,从这个演示中最重要的要点不是我们可以使用的具体功能,而是用于生成我们的插件的格式。

任何人都可以写代码,但使用像我们在这里使用的样板模式将有助于提高可读性,使调试更容易,并在以后的扩展或升级功能时增加机会。记住,如果你编写了一个插件,并且在一段时间内没有回顾它(比如说 6 个月);那么酸测试是你能从良好结构化的代码中解决多少问题,而不需要大量文档。如果你做不到这一点,那么你需要重新审视你的编码!

让我们继续。还记得我提到选择使用提示插件作为我们例子基础的一个很好的原因吗?现在是时候揭示为什么了...

自动将动画转换为使用 CSS3

我们建立了一个提示插件,它在悬停在标记有.tooltip类的元素上时使用一点动画淡入淡出。那没错 - 代码完全正常运行,是一种可以接受的显示内容的方式...对吗?错!正如你现在应该知道的,我们绝对可以做得更好。这就是为什么我选择了提示作为我们的例子的原因。

还记得我在第六章中提到过的,在 jQuery 中进行动画,我们应该考虑使用 CSS3 样式来控制我们的动画吗?好吧,这里有一个完美的例子:我们可以轻松地改变我们的代码,强制 jQuery 尽可能使用 CSS3,或者在旧版本的浏览器中回退到使用库。

其中的诀窍在于一行代码:

<script src="img/jquery.animate-enhanced.min.js"></script>

要看看有多简单,请按照以下步骤操作:

  1. tooltipv2.html的副本中,按照提示添加这行:

      <script src="img/jquery.min.js"></script>
     <script src="img/jquery.animate-enhanced.min.js"></script>
      <script src="img/jquery-ui.min.js"></script>
      <script src="img/jquery.quicktipv2.js"></script>
      <script src="img/tooltipv2.js"></script>
    
  2. 在浏览器中预览结果。如果一切顺利,我们应该看到提示反应方式稍有改变。然而,当在像 Firebug 这样的 DOM 检查器中查看提示代码时,真正的改变就显而易见了:自动将动画转换为使用 CSS3

如果我们在 Firebug 的计算样式一半查看,我们可以看到样式被分配给提示元素:

自动将动画转换为使用 CSS3

这是一个简单的变化,但希望我们能看到在性能上有显著改进。在这种情况下,我们使用一个插件来强制 jQuery 使用 CSS3 样式代替标准的 jQuery 基础动画。

但在这里的关键信息是,作为开发人员,我们不应感到受限于使用 jQuery 来提供我们的动画。尽管对于管理复杂动作可能是一种必要之恶,但我们仍应考虑在那些不太华丽的情况下使用它。

使用基于 CSS 的动画

嗯 - 此时脑海中浮现一个问题:如果我们使用现代浏览器,为什么还需要依赖基于 jQuery 的动画呢?

答案很简单 - 简而言之,这取决于具体情况。但长话短说,对于现代浏览器,我们不需要依赖使用 jQuery 来提供我们的动画。只有在我们被迫为旧版浏览器版本(如 IE6)提供支持时,我们才需要使用 jQuery。

但是可能性应该很低。如果有必要的话,我们真的应该问自己我们是否在做正确的事情,或者是否应该逐渐降低支持,使用类似 Modernizr 这样的工具。

尽管如此 - 让我们通过以下步骤来理解我们需要做什么才能使用 CSS3 代替基于 jQuery 的动画:

  1. tooltipv2.css 的副本中,在文件底部添加以下 CSS 样式 - 这将是我们的工具提示的过渡效果:

    div.arrow_box { transition-property: all; transition- duration: 2s; transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
    
  2. 打开 jquery.quicktipv2.js 的副本,然后首先注释掉以下行:

    data['tooltip'].stop(true, true).fadeIn(600);
    

    在原位添加以下行:

    data['tooltip'].css('display', 'block');
    
  3. 重复相同的过程,但这次是针对以下行:

    data['tooltip'].stop(true, true).fadeOut(400);
    

    将下一行作为替换添加:

    data['tooltip'].css('display', 'none');
    
  4. 保存文件。如果我们在浏览器中预览变化的结果,应该看到工具提示滑动并悬停在所选图像上。

效果看起来非常流畅。虽然它不会淡入淡出,但仍然为工具提示的出现方式带来了有趣的变化。这确实引发了一个有趣的问题 - 我们应该使用什么效果?让我们暂停一下,考虑一下进行这种变化的影响。

考虑变化的影响

在我们的示例中使用 CSS3 样式提出了一个重要的问题 - 哪种效果效果最好?我们可以选择经典的线性或摆动效果,但这些已经被用得厌了。我们可以轻松地用更原创的东西替换它。在我们的示例中,我们使用了 cubic-bezier(0.23, 1, 0.32, 1),这是 easeOutQuint 函数的 CSS3 等效函数。

解决这些效果可能会耗费时间。相反,我们可以使用 Lea Verou 创建的一个很棒的工具,它可以在 www.cubic-bezier.com 上使用。

考虑变化的影响

要查看我们选择的效果在实际中的样子,前往 cubic-bezier.com/#.23,1,.32,1。 该网站有一个我们可以运行的示例,以查看效果如何工作。 该网站的好处在于我们可以使用图表来微调我们的效果,这会自动转换为我们可以转移到我们的代码中的相关值。

这打开了进一步的可能性——我们提到了来自 github.com/rdallasgray/bez 的 Bez 插件的使用;这很容易在这里代替标准的 .css() 方法,来提供我们的动画。

提示

对于众所周知的缓动函数(如 easeInQuint),其 CSS 等效函数都列在 gist.github.com/tzachyrm/cf83adf77246ec938d1b 上;我们可以在 www.easings.net 上看到它们的效果。

不过,重要的是,当在 DOM Inspector 中查看 CSS 时,我们可以看到的变化是:

考虑到变化的影响

与其内联应用(如 自动将动画转换为使用 CSS3 部分所示),我们可以保持关注点分离原则,将 CSS 样式保留在样式表中,将 HTML 用于组织我们的网页内容。

回退到 jQuery 动画

到目前为止,我们使用 CSS 样式来创建我们的动画效果。 这引发了一个问题,即我们是否应该将此技术用于我们所有的动画需求,还是应该使用 jQuery 效果。

一切都归结为两个关键点——动画有多复杂,以及你是否需要支持旧版浏览器? 如果答案是肯定的(或两者都是肯定的),那么 jQuery 很可能会胜出。 但是,如果你只有一个简单的动画,或者你不需要支持旧版浏览器,那么使用 CSS 应该值得认真考虑。

到目前为止,我们使用的动画的一个很棒的地方是,我们可以使用两种方法提供相同的效果——即 CSS 和 jQuery。 jQuery 中缓动函数的一个很好的来源是 github.com/gdsmith/jquery.easing - 这里列出了所有标准的、在诸如 jQuery UI 等库中可用的众所周知的缓动函数。 为了证明我们可以实现相同的效果,让我们继续对我们的代码进行快速更改,使用已经使用过的动画的 jQuery 等效方法。 按照以下步骤进行:

  1. 我们首先要编辑 quickTip 插件文件的副本。 继续找到 jquery.quicktipv2.js 的副本,然后在变量声明之后立即添加以下代码块:

    $.extend($.easing, {
      easeInQuint: function (x, t, b, c, d) {
        return c*(t/=d)*t*t*t*t + b;
      },
    
      easeOutQuint: function (x, t, b, c, d) {
        return c*((t=t/d-1)*t*t*t*t + 1) + b;
      }
    });
    
  2. 现在我们需要调整我们的动画以利用缓动函数,所以继续修改 fadeIn 方法,如下所示的代码行:

    data['tooltip'].stop(true, true).fadeIn(600, 
      'easeInQuint');
      $this.on('mousemove.' + pluginName, internal.reposition);
    },
    
  3. 没有 fadeIn 就不能有其姐妹 fadeOut(),因此我们也需要更改这个调用,如下所示:

    $this.off('mousemove.' + pluginName, internal.reposition);
      data['tooltip'].stop(true, true).fadeOut(400, 
      'easeInQuint');
    }
    
  4. 将文件保存为jquery.quicktipv2.easing.js。不要忘记在tooltipv2.html中修改原始插件引用!我们还需要在tooltipv2.css文件中取消div.arrow_box的过渡样式,因此请继续并注释掉这段代码。

在这一点上,我们已经使用 jQuery 实现了一个可行的解决方案。如果我们在浏览器中预览结果,工具提示将显示为应该显示的样子。不过,缺点是我们失去了所使用的样式的可见性,并且(如果 JavaScript 在浏览器中被禁用的话)动画就不会播放了。

还有一个重要的观点是 jQuery 动画已经消耗了更多资源,我们在第六章中也提到过,在 jQuery 中进行动画。那么,在这些情况下,为什么我们要求在这里使用 jQuery 动画,而不是 CSS?再一次,这是成为更好的开发者的一部分 - 容易诉诸于使用 jQuery; 在考虑所有替代方案之前考虑这一点是正确的!

提示

如果您设计了自定义缓动,并希望使用 CSS 等效-添加我们之前使用的 jQuery 动画增强插件的链接。这将使用贝塞尔曲线值提供 CSS 等效。然后我们可以使用之前的 Bez 插件,或者甚至使用来自github.com/gre/bezier-easing的 bezier-easing 将其添加回作为基于贝塞尔曲线的动画。

现在让我们转移重点并继续前进。到目前为止,我们的插件中提供了有限的选项;如果我们想要扩展它怎么办?我们可以尝试深入代码并进行调整;尽管在某些情况下,这可能对我们的需求来说有些过度。一个更好的选择可能是将其简单地封装为一个新插件的实例。让我们来看看涉及到什么。

扩展我们的插件

使用插件时常见的问题是找到完全符合我们要求的插件;发生这种情况的可能性可能比中彩票还要小!

为了解决这个问题,我们可以扩展我们的插件,以在不影响现有方法的情况下加入额外的功能。这样做的好处是,我们可以覆盖现有的方法或合并额外的功能,帮助使插件更接近我们的需求。要了解这在实际应用中是如何工作的,我们将向我们现有的插件添加一个方法和额外的变量。有许多方法可以实现这一点,但我使用的方法也很有效。让我们按照以下步骤进行:

  1. 我们将从编辑tooltipv2.js的副本开始。在#getValue点击处理程序的下面,继续添加以下代码:

      (function($) {
        var extensionMethods = {
          fadeInValue: 600,
          showDebug: function(event) {
            console.log("This is a test");
          }
        }
        $.extend($.fn.quicktip, extensionMethods);
      })(jQuery);
    
  2. 保存文件。如果我们在浏览器中预览tooltipsv2.html,然后通过 DOM 检查器深入渲染的代码,我们应该会看到类似于以下截图的内容:扩展我们的插件

在这个例子中,我们添加了一个并不真正执行很多功能的方法;关键不是它做了什么,而是我们如何添加它。在这里,我们将其作为现有对象的附加方法提供。将以下内容添加到 tooltipsv2.js 的末尾:

  $('#img-list li a.tooltips').on("mouseover", function() {
    $('#img-list li a.tooltips').quicktip.showDebug();
  })

如果现在刷新浏览器会话,我们可以在浏览器的 控制台 区域看到它的运行情况,如下一个截图所示:

扩展我们的插件

我们可以做更多的事情,值得花时间在线研究。扩展的关键是确保您了解 $.fn.extend$.extend 之间的区别。它们看起来可能相同,但请相信我 - 它们的作用是不同的!

使用 Bower 打包我们的插件

好了 - 在这个说明下,我们现在有一个可工作的插件,它已准备好使用。

此时,我们可以直接发布它,但明智的选择是将其打包以供像 Bower 或 NPM 这样的管理器使用。这样做的优点是下载并安装所有所需的包,而无需浏览到各个站点并手动下载每个版本。

提示

我们甚至可以进一步自动化我们的开发工作流程,使用诸如 Gulp 和 Grunt 这样的构建工具 - 有关如何的示例,请访问 www.codementor.io/bower/tutorial/beginner-tutorial-getting-started-bower-package-manager

现在,让我们快速看一下自动创建 Bower 包的步骤:

  1. 为了进行这个演示,我们需要安装 NodeJS。所以请访问 nodejs.org/,下载适当的二进制或包,并安装,接受所有默认设置。

  2. 接下来,我们需要安装 Bower。启动已安装的 NodeJS 命令提示符,并在命令行输入以下内容:

    npm install –g bower
    
    
  3. Bower 将通过一系列问题提示我们有关插件的信息,然后显示它将为我们创建的 bower.json 文件。在这个例子中,我使用了工具提示插件作为我们示例的基础。对于您创建并想要使用 Bower 分发的任何插件,相同的问题都将适用,如下图所示:使用 Bower 打包我们的插件

  4. 最后一步,确认我们对所创建的 bower.json 文件没有问题,就是在 Bower 中注册插件。在命令提示符下,运行以下命令:

    bower register <name of plugin>
    
    
  5. Bower 将在最终确认插件可通过 Bower 使用之前经历多个阶段。

此时,我们将可提供插件供任何人下载。因为它必须链接到有效的 GitHub 帐户,我们现在可以将插件上传到这样的帐户,并通过 Bower 使其对任何人都可下载。作为奖励,我们现在可以利用 NodeJS 和 Grunt 来帮助自动化整个过程。不妨看看 grunt-bump(github.com/vojtajina/grunt-bump),作为一个起点?

小贴士

Bower 还有很多我们无法在这里覆盖到的功能。为了获得灵感,不妨阅读bower.io/上的文档。

自动化文档的提供

发展我们的插件技能的最后阶段是提供文档。任何编码人员都可以生成文档,但更好的开发人员的标志是可以产生高质量的文档,而不必花费大量时间。

进入 JSDoc!它可从github.com/jsdoc3/jsdoc获取。如果您尚未熟悉它,这是创建不仅外观良好而且可以轻松使用 Node 自动化的文档的好方法。让我们花点时间安装它,并看看它如何工作。需要执行以下步骤:

  1. 这次我们将从使用 NodeJS 安装 JSDoc 开始。为此,我们需要打开一个 NodeJS 命令提示符;如果您使用 Windows 8,则可以在程序菜单中找到其图标,或者从开始页面找到。

  2. 在命令提示符下,将位置更改到您的项目文件夹,然后输入以下命令:

    npm install –g jsdoc
    
    
  3. 在确认完成该过程之前,Node 将运行安装过程。

要生成文档,需要在我们的代码中输入注释,例如:

自动化文档的提供

添加后,可以通过在插件文件夹中运行以下命令来编译文档:

jsdoc <name of plugin>

我们会看到一个名为 out 的文件夹出现;里面包含了我们可以逐步建立的文档。如果我们对内联注释进行了更改,我们需要重新运行编译过程。这可以通过 Node 的grunt-contrib-watch插件来自动化。如果我们在 out 文件夹中看一下,就会看到文档出现。它会看起来类似于以下截图提取:

自动化文档的提供

还有很多内容可以进行覆盖,以了解可以用来指导文档如何呈现的某些参数的感觉,不妨阅读usejsdoc.org/about-getting-started.html上的广泛文档。有很多可能性可供选择!

从我们的插件返回值

创建插件的关键部分是 - 我们能从插件中得到什么信息?有时我们无法从中获取信息,但这可能仅是我们试图通过插件实现的目标的局限性。在我们的情况下,我们应该能够获取内容。让我们看看如何使用我们的快速提示插件来实现这一点。

在深入代码之前,我们先来看一下我们要创建的东西:

从我们的插件返回值

  1. 我们需要从某个地方开始,所以最好的地方就是标记。在tooltipv2.html的副本中,在关闭<div>标签之前添加以下突出显示的代码:

     <input type="submit" id="getValue" value="Get text from first tooltip" />
     <div id="dialog" title="Basic dialog">
      </div>
    
  2. tooltipv2.js的副本中,我们需要暴露我们在标记中实现的data-标签。继续添加tiptag的配置选项,如下所示:

    $(document).ready(function() {
      $('#img-list li a.tooltips').quicktip({
        class: 'arrow_box', tiptag: "title"
      });
    });
    
  3. 这部分的最后一步是修改我们的标记。我们将使用data-标签替换标准的title=""标签,以提供更多的灵活性。在tooltipv2.html的副本中,搜索所有title的实例,然后用data-title替换它们。

  4. 接下来,我们需要添加一个链接到 jQuery UI CSS 样式表中。这纯粹是为了创建一个对话框,显示我们从其中一个工具提示中获取文本的结果:

    <link rel="stylesheet" type="text/css" 
    href="http://code.jquery.com/ui/1.10.4/themes/humanity/jquery-ui.css">
    <link rel="stylesheet" type="text/css" href="css/tooltipv2.css">
    
  5. 要使 jQuery UI CSS 起作用,我们需要添加对 jQuery UI 库的引用。所以继续添加一个。为了方便起见,我们将使用 CDN 链接,但是在生产环境中,我们将考虑生成一个定制的缩小版本:

    <script src="img/jquery-ui.js"> </script>
    <script src="img/jquery.quicktipv2.data.js"></script>
    
  6. tooltip.js的副本中,删除其中的所有代码,并用以下代码替换:

    $(document).ready(function() {
      $('#img-list li a.tooltips').quicktip({ 
        class: 'arrow_box', 
        tiptag: "title"
      });
    
      $('#getValue').on("click", function(event){
        var tipText = $('a.tooltips').eq(0).data().title;
        $("#dialog").text(tipText);
        $("#dialog").dialog({
          title: "Text from the first tooltip",
          modal: true,
          buttons: {
            Ok: function() { $(this).dialog("close"); }
          }
        });
      });
    });
    
  7. 保存所有文件。如果我们切换到像 Firebug 这样的 DOM 检查器,我们可以看到通过输入第 6 步中突出显示的代码行返回的文本:从我们的插件返回值

  8. 在同一个浏览器会话中,单击从第一个工具提示获取文本按钮。如果一切正常,我们应该看到一个温和的覆盖效果出现,然后是一个对话框,显示在本练习开始时显示的文本。

诚然,我们的示例有点牵强,我们应该尽量不将获取文本的依赖硬编码在内,而是通过选择我们想要的任何工具提示中的文本来实现。关键在于,我们可以同样轻松地自定义用于文本的标签,并使用.data()方法检索该内容。

探索最佳实践和原则

在过去的几页中,我们已经涵盖了一些概念和技巧,可以帮助我们进一步发展插件技能。还有一些值得考虑的额外因素,我们还没有涵盖。值得花几分钟来探索这些因素:

  • 质量和代码风格:你有考虑通过 JSHint (www.jshint.com) 或 JSLint (www.jslint.com) 对插件代码进行 linting 吗?遵循写 jQuery 最佳实践是确保成功的一种方式,比如遵循一致的代码风格或者在 contribute.jquery.org/style-guide/js/ 上发布的指南?如果没有,那你的代码有多清理和可读?

  • 兼容性:你的插件与哪个版本的 jQuery 兼容?这个库多年来已经进行了重大更改。你是打算提供对旧浏览器的支持(需要使用 1.x 分支的 jQuery),还是保持与更现代的浏览器兼容(使用库的 2.x 版本)?

  • 可靠性:你应该考虑提供一组单元测试。这些测试可以帮助证明插件的工作情况,并且很容易产生。如果你想了解如何在 QUnit 中执行这些测试,可以看看 Dmitry Sheiko 编著的 Instant Testing with QUnit,这本书由 Packt Publishing 出版。

  • 性能:一个运行速度慢的插件会让潜在用户望而却步。考虑使用 JSPerf.com (www.jsperf.com) 作为测试段的基准,评估插件的工作情况以及是否需要进一步优化任何部分。

  • 文档:给你的插件文档是必须的。文档的程度通常会决定插件的成功与失败。插件是否包含开发者需要了解的任何怪癖?它支持哪些方法和选项?如果代码有内联注释,那也会有帮助,虽然最好为生产使用提供一个压缩版本。如果开发人员可以很好地导航你的代码库来使用或改进它,那么你已经完成了一份不错的工作。

  • 维护:如果我们发布一个东西到公众面前,就必须考虑支持机制。我们需要提供多少时间进行维护和支持?提前清楚地说明对问题的回答、解决问题和持续改进代码的期望是至关重要的。

哎呀 – 需要考虑的事情还真不少!创建一个插件可能会是一次有益的经历。希望这些建议能帮助你提高技能,使你成为更全面的开发者。记住,任何人都可以编写代码,就像我经常说的那样。成为更好的开发者的关键在于理解什么是一个好的插件,并知道如何付诸实践。

注意

Learn jQuery 网站有一些额外的提示值得探索,地址是 learn.jquery.com/plugins/advanced-plugin-concepts/

摘要

如果有人问你学习 jQuery 的一个关键主题的名称,很可能插件会在答案中占据重要地位!为了帮助写作,我们在本章中涵盖了许多技巧和窍门。让我们花五分钟回顾一下我们学到的内容。

我们的起点是讨论如何检测开发不良的插件的迹象,作为学习如何通过使用插件模式来改进我们的开发的先导。然后,我们开始设计和构建一个高级插件,首先创建基本版本,然后重新排序以使用样板模板。

接下来我们详细研究了转换到使用 CSS3 动画,在书中早些时候我们讨论的一些论点,考虑到使用 CSS3 来更好地管理动画,而不是诉诸于 jQuery。

然后,我们开始研究如何在我们的插件中扩展功能,然后学习如何通过 Bower 打包它以便通过 GitHub 使用。然后我们涵盖了自动提供文档的功能,以及如何从我们的插件中返回值,最后总结了一些我们可以在开发中采用的最佳实践和原则。

好的 - 我们继续前进!在下一章中,我们将混合使用 jQuery(包括一些插件),HTML5 标记和 CSS,并制作一个网站。好的,没有什么特别的 - 那是非常正常的。但是,这里有一个转折:怎么样在离线状态下运行整个网站?是的,你没听错……离线……而且,看不到 USB 键或 DVD……

第十二章:使用 jQuery 与 Node-WebKit 项目

在这个现代化的时代,响应式设计是最新的热门词汇,使用 jQuery 构建的网站可以在任何设备或平台上正确工作。尽管如此,这需要一个互联网连接——如果我们可以开发一个同样的应用的离线版本呢?

进入 Node-WebKit(或现在称为 NW.js)。在本章中,我们将暂停探索 jQuery 并探索使用该库的较少知名的方式之一。你将看到如何利用 jQuery、HTML5 和桌面的强大功能,将它们混合在一起,以在任何桌面或笔记本环境中离线运行您站点的副本。我们将使用它来通过使用 jQuery 开发一个简单的文件大小查看器来进行一些有趣的开发,这可以轻松地开发成可以根据需要在线或离线运行的更复杂的内容。

在本章中,我们将涵盖以下主题:

  • 介绍 Node-WebKit

  • 构建一个简单的站点

  • 打包和部署您的应用程序

  • 深入了解

准备好探索 Node-WebKit 的世界了吗?让我们开始吧...

注意

你可能会在网上看到对 NW.js 的引用——这是自 2015 年 1 月以来 Node-WebKit 的新名称;在本章中,你可能会看到两个名称都被使用。

设置情景

想象一下情景,如果你愿意,客户要求你制作一个基于网络的应用程序;他们概述了一组特定的要求,如下所示:

  • 它必须具有简单的 GUI

  • 不应该有重复的内容——必须是一个适用于所有平台的版本

  • 解决方案必须易于安装和运行

  • 它需要是可移动的,以便在更换计算机时可以传输

如果你认为一个网站就足够了,请举手?现在,如果你没有仔细阅读需求,请举手...!

在这种情况下,一个网站是不够的;一个桌面应用程序将处理重复的要求,但可能不易使用,并且肯定不会跨平台。那么,我们从这里该怎么办呢?

介绍 Node-WebKit

Node-WebKit(或现在称为 NW.js)最初由英特尔创建,但在 2011 年开源,并可在 nwjs.io/ 获取;该项目试图将 SPA 开发的优势与离线环境结合起来(在那里托管 Web 服务器并不实际)。

Node-WebKit 基于 Chromium,一个基于 WebKit 的浏览器进行了扩展,以便让你控制通常对 Web 开发人员不可用的用户界面元素。安全模型已经放宽(基于我们运行的代码是受信任的)并且它集成了 NodeJS;这打开了一系列的可能性,超出了 HTML5 API 的正常范围。

起初,这可能看起来像是一种复杂的混合。然而,请不要害怕,因为大多数最终解决方案仅由普通的 HTML、CSS 和 JavaScript 构建,最后加上一些图像来完成。

正如我们在本章中将要看到的,基本原理是生成一个普通的站点,然后将 HTML、CSS 和所有相关资源文件压缩成一个 ZIP 文件。我们只需将其重新命名为.nw扩展名,然后运行主要的nw.exe应用程序。只要我们已经设置了一个必需的package.json文件,它就会自动获取我们的应用程序并在屏幕上显示出来,如下所示:

介绍 Node-WebKit

不过,这本书是关于 jQuery 的,对吗?是的,绝对是;这里就是最棒的部分:Node-WebKit 允许你运行标准的 JavaScript 和 jQuery,以及任何 Node 的第三方模块!这打开了各种机会;我们可以使用主要库或任何大量基于 jQuery 的附加库,比如 Three.js、AngularJS 或 Ember。

注意

我们真正需要记住的唯一关键部分是,使用 NW.js 有一些怪癖,比如使用文件夹对话框浏览和选择本地文件夹;我们稍后将在本章中更详细地介绍这一点。

此时,我相信你一定会问自己一个问题:为什么我要使用 nw.js(或 Node-WebKit)?这是一个非常合理的问题;我们以桌面应用程序的形式运行基于 Web 的站点可能看起来很不合逻辑!在这种明显的疯狂中,有一些合理的原因让我们这样做,所以让我们现在看一下它们,看看为什么将站点作为桌面应用程序运行是有意义的。

在桌面上运行 HTML 应用程序

作为开发人员,我们面临的最大头疼之一是确保用户在访问我们的站点时在所有需要支持的浏览器上拥有相同的体验。现在,我应该明确一点:在同样的体验方面,可能存在一些情况,这根本不可能实现,所以我们至少必须为那些不支持特定功能的浏览器提供一个优雅的退出路径。

幸运的是,这个问题正在逐渐减少。Node-WebKit 的好处在于,我们只需要支持 Chrome(因为 Node-WebKit 就是基于 Chrome 的)。

在大多数情况下,我们可以简单地重用为 Chrome 创建的代码;这使我们能够轻松地使用我们已经了解或使用的前端框架(包括 jQuery!)和 Node 模块推出跨平台应用程序。除此之外,还有几个原因可以让你使用 Node-WebKit 来帮助制作跨平台应用程序,如下所示:

  • 访问 Blink 中提供的最新 Web 技术,Blink 是 Google Chrome 后面的渲染引擎。

  • NW.js 支持 一次构建,到处运行 的概念——这可能不适用于所有应用程序,但许多应用程序可以从在桌面、Web 和移动环境之间共享代码中受益。

  • 如果你想让你的应用程序以特定大小运行或者在弹出窗口中做一些更高级的事情,你可以在桌面上获得这种控制。大多数解决方案还提供了一种访问文件系统并允许其他更高级控件的方式,这些是常规 Web 应用程序所不能提供的。

不想显得消极,但有一些需要注意的事项;主要关注的是可执行文件的大小。

使用原生 UI 库(如 jQuery)创建的站点或应用程序可能只有几千字节大小。使用 Node-WebKit 构建的等效版本会显著更大,因为它包含了一个精简版的 Node 和 Chromium。因此,你需要注意文件大小——你可以使用 第二章 Customizing jQuery 中的一些技巧来减小 jQuery 的大小。还有一些其他需要注意的问题;它们包括以下内容:

  • 与原生应用程序相比,桌面 Web 应用程序通常需要更大量的 RAM 和 CPU 力量来运行和渲染。

  • 在外观方面,如果你想要让你的应用程序在你计划部署的平台上看起来好看,那么你需要使用 CSS 重新创建常见的 UI 元素,或者创建一个全新的 UI,包括为每个操作系统提供的 UI 元素(如标题栏、菜单栏和上下文菜单)创建新的设计。

  • 虽然 Node-WebKit 放宽了一些在使用浏览器应用程序时发现的安全问题(如同源策略),但你仍然只能访问 Node-WebKit 上下文;而且在某些情况下,你必须使用 WebKit 特定的标签,比如在创建选择目录对话框时使用 nwdirectory。最终效果是代码增加,如果你想要创建一个同时支持 Web 和桌面环境的文件。你可以缓解这个问题的影响:videlais.com/2014/08/23/lessons-learned-from-detecting-node-webkit/ 提供了一个有用的技巧来确定你所处的环境,并允许你引用该环境所需的适当文件。

注意

有关一些安全考虑的更多信息,请查看 NW.js Wiki 上的安全页面,网址为 github.com/nwjs/nw.js/wiki/Security

现在我们已经相互介绍了,让我们深入探讨并开始安装 Node,在我们开始构建基于 jQuery 的应用程序之前。值得注意的是,本章的重点将主要基于 Windows,因为这是作者使用的平台;对于使用 Linux 或 Mac 平台的人来说,需要进行一些更改。

准备我们的开发环境

在接下来的几页中,我们将构建一个简单的应用程序,该应用程序在主窗口中显示任何拖放的文件的文件大小,或者通过文件对话框选择。实际上,我们不会单独使用该应用程序,而是作为上传图像进行处理的基础,或者可能作为压缩应用程序的离线版本。我们有很多方法可以进一步开发它——我们将在本章后面的 深入探讨 部分中涉及一些想法。

与此同时,让我们开始安装 NW.js。在这之前,我们需要利用以下工具:

  • 需要一个压缩程序;在 Windows 平台上,您可以使用内置功能或类似于 7-Zip (www.7-zip.org) 的东西,如果更喜欢的话。

  • 我们需要一个文本编辑器;在本章的过程中,我们将使用 Sublime 2 或 3,但如果您已经有个人偏爱,任何好的文本编辑器都应该足够。Sublime Text 可以从 www.sublimetext.com 下载,适用于 Mac、Linux 和 Windows 平台。

  • 我们将利用 Node 和 Grunt 来安装额外的包。Node 可以在 www.nodejs.org 上获得,所以请继续安装适合您平台的版本。安装完成后,请从 NodeJS 命令提示符中运行以下命令以安装 Grunt:

    npm install -g grunt-cli
    
    
  • 最后,但绝不是最不重要的,我们需要 Node-WebKit 库(当然),所以请访问 nwjs.io/ 并下载适合您平台的版本。如果您展开文件夹,您应该会看到类似于此截图所示的内容:准备我们的开发环境

顺便说一下,Node-WebKit 可以很容易地集成到现有的 Grunt 文件中,这意味着我们可以利用诸如cssmin之类的包来缩小我们为应用程序创建的 CSS 样式表。随着您对 Node-WebKit 的了解越来越深入,这绝对值得探索。

废话少说;是时候开始开发了!与其他事物一样,我们需要从某个地方开始。在我们看如何使用 jQuery 之前,让我们试试创建一个简单的 "Hello World" 示例。

安装和构建我们的第一个应用程序

我在想:你有多少次读过关于编程语言的书籍或在线文章,它们对无处不在的 "Hello World" 示例都提供了自己的见解?我敢打赌,这些年来肯定有不少次……是的,在你问之前,我们也不打算打破传统!在提供 "Hello World" 示例的任何人的脚步之后,这是我们自己的见解。

安装和构建我们的第一个应用程序

要构建这个,我们需要做以下事情:

  1. 浏览到nwjs.io/并下载适用于您平台的软件包;我们暂时假设使用 Windows,但也有 Mac 和 Linux 平台的软件包可用。

  2. 提取node-webkit-vX.XX.XX-win-x64文件夹(其中XX是版本号),将其重命名为nodewebkit,并将其复制到主 PC 驱动器——Linux 或 Mac 用户可以将此文件夹复制到他们的用户区域。完成后,在nodewebkit文件夹中创建一个名为development的新文件夹。

  3. 接下来,我们需要安装 NodeJS。为此,请前往nodejs.org/download/以下载并安装适合您平台的版本,接受所有默认值。

Node-WebKit 可以使用任何可用的标准 Node 软件包。作为示例,我们将安装markdown包,该包将合适标记的纯文本转换为有效的 HTML。让我们继续安装它并看看它是如何工作的:

  1. 在 NodeJS 命令提示符中,切换到helloworld文件夹,然后输入以下代码并按Enter

    npm install markdown
    
    

    安装和构建我们的第一个应用程序

  2. 关闭窗口,因为你不需要它。接下来,从附带本书的代码下载中的helloWorld文件夹中提取index.htmlpackage.json文件的副本;将它们保存在项目区域中的helloWorld文件夹中。

  3. 创建一个名为helloWorld.zip的新 ZIP 文件夹,然后将这两个文件添加到其中;将helloWorld.zip重命名为helloWorld.nw

现在我们可以运行我们的应用程序了;有三种方式可以使用 Node-WebKit 来执行此操作:

  • 在 NodeJS 命令提示符中,切换到nodewebkit文件夹,然后运行以下命令:

    nw C:\nodewebkit\development\helloWorld.nw
    
    
  • 双击nw.exe应用程序;这将拾取package.json文件并自动运行helloworld.nw文件

  • helloworld.nw文件拖放到nw.exe上即可运行该应用程序

无论您喜欢使用哪种方式,运行它都会显示在本练习开始时显示的Hello World窗口。这是一个简单的、不带花哨的 Node-WebKit 使用示例——尽管它不会赢得任何奖项,但它展示了如何从现有 HTML 页面创建一个功能性应用程序是多么简单。

解析package.json文件

我们的应用程序的核心是package.json文件。这个清单文件告诉 Node-WebKit 如何打开应用程序,并控制浏览器的行为方式:

解析文件

值得详细了解这个文件;它包含了项目的所有元数据,并遵循所有基于 Node 的软件包的标准格式。如果您不熟悉清单文件,您可以在browsenpm.org/package.json看到一个详细的示例,其中包含每个部分的交互式解释;Node-WebKit 的版本使用方式类似。

注意

关于 Node-WebKit 清单文件及其组成部分的更深入详细信息,请访问主 NW.js 站点上的文档 (github.com/nwjs/nw.js/wiki/manifest-format)。

好的,现在是时候开始构建我们的示例应用程序了!

构建我们的简单应用程序

在接下来的几页中,我们将构建一个简单的应用程序,允许我们将文件拖放到拖放区域以渲染文件大小。它基于 Martin Angelov 的教程,可在 tutorialzine.com/2013/05/mini-ajax-file-upload-form/ 上获得;我们将专注于前端 UI 界面,并不考虑后端上传功能,以供我们的演示使用:

构建我们的简单应用程序

即使只是在前端用户界面上工作,仍然需要相当数量的代码;我们的重点将主要放在 jQuery 代码上,因此在更详细地探索之前,让我们先看一下演示的实际操作。要做到这一点,请执行以下步骤:

  1. 在我们的演示中,我们使用了一小部分 PHP 代码,因此我们首先需要设置 Web 服务器空间,如 WAMP(适用于 PC—www.wampserver.de/en)或 XAMPP(或 MAMP 适用于 Mac—www.mamp.info/en)。Linux 用户将在其发行版中获得某种可用内容。我们将在此演示中使用 WAMP—如果您的情况不同,请相应调整位置;在安装时使用默认设置。如果您喜欢跨浏览器解决方案,则 XAMPP 是一个不错的选择—它可在 www.apachefriends.org/index.html 上获得。

  2. 接下来,我们需要从附带本书的代码下载中提取一个FileSizeView文件夹的副本。这包含了我们应用程序所需的标记。将文件夹保存在C:\wamp\www中。

  3. 我们需要一个 Node-WebKit 的副本来运行我们的应用程序,所以请复制代码下载中的nwjs文件夹的内容到FileSizeView文件夹中。如果一切正常,您应该看到如下所示的文件:构建我们的简单应用程序

  4. 在此阶段,如果我们双击nw.exe,我们应该可以看到我们的应用程序运行。另外,您将看到在本练习开始时显示的窗口。

好的,它显示了窗口;“它是如何工作的”,我听到你在问?嗯,从这个练习中有一些关键点需要注意,所以让我们花些时间更详细地讨论一下。

进一步探索我们的演示

如果您仔细查看FileSizeView文件夹,您会发现大部分内容围绕着index.htmlupload.php文件展开,还有为使演示工作所需的相关 CSS、图像和 JavaScript 文件。此外,我们还有一些来自 Node-WebKit 文件夹的文件,这些文件提供了 Node 和 Chromium 的精简版本,用于托管我们的文件:

  • nw.exenw.pak:这是主要的 Node-WebKit 可执行文件和 JavaScript 库文件,分别运行我们的代码。

  • package.json:这是一个清单文件,在本章早些时候的安装和构建我们的第一个应用程序部分中就使用过;它向 Node-WebKit 提供了如何显示我们应用程序的指示。

  • ffmpegsumo.dll:用于提供视频和音频支持;对于我们的演示来说并不是必需的,但可以用于将来使用。

  • filesizeview.nw:这是我们打包的应用程序;这是 Node-WebKit 在检查package.json以验证应如何显示后运行的文件。

  • gruntfile.js:这是用于grunt-node-webkit-builder的 Grunt 文件,我们稍后会在自动化流程中使用它将我们的文件编译成一个应用程序。

  • icudtl.dll:这是 Node-WebKit 所需的网络库。

  • libEGL.dlllibGLESv2.dll:这些文件用于Web 图形库WebGL)和 GPU 加速。

在一些可在线使用的 Node-WebKit 应用程序中,您可能会看到D3DCompiler_43.dlld3dx9_43.dll文件。这些来自 DirectX 可再发行包,用于提供增强的 WebGL 支持。

解剖我们的内容文件

好的,那么我们有我们的主要 Node-WebKit 文件;我们还使用了什么呢?除了标准的 HTML 标记、图像和样式外,我们还使用了许多基于 jQuery 的插件和一些自定义的 jQuery 代码进行连接。

使用的主要插件文件是 jQuery、jQuery UI、jQuery Knob 和 BlueImp 文件上传插件。我们还使用一些自定义代码将它们组合在一起——它们位于window.jsscript.js中。让我们更详细地查看这些,从window.js开始。

探究window.js

window.js中,我们首先调用nw.gui,这是 Node-WebKit 的本机 UI 库,使用了require();这是调用任何模块(如内部模块或外部第三方模块)的标准格式。然后我们将这分配给gui变量,然后使用它来获取我们应用程序窗口的句柄:

var gui = require('nw.gui'), win = gui.Window.get();

需要注意的是,由于我们只能访问 Node-WebKit 上下文,我们必须使用专用库;我们无法通过标准的 JavaScript 调用访问窗口。

提示

要获取有关访问模块的更多信息,请查看位于github.com/nwjs/nw.js/wiki/Using-Node-modules上的文档。

接下来,我们设置了两个委托文档处理程序,一个用于处理窗口的最小化,另一个用于完全关闭它:

$(document).on('click', '#minimize', function () {
  win.minimize();
});

$(document).on('click', '#close', function () {
  win.close();
});

这只是我们可以做的一小部分;还有很多。前往github.com/nwjs/nw.js/wiki/Window了解我们可以实现的可能性。

解析 BlueImp 插件配置

我们站点的主要功能是在script.js中托管的。它包含 BlueImp 文件上传插件的主配置对象以及一些额外的辅助函数。让我们更详细地看一下。

我们从常规的文档准备调用开始,然后将#upload li列表项的引用分配给一个变量,如下所示:

$(function(){
  var ul = $('#upload ul');

  $('#drop a').click(function(){
    // Simulate a click on the file input button
    // to show the file browser dialog
    $(this).parent().find('input').click();
  });

接下来,我们配置文件上传插件。首先,我们将初始拖放区域设置为#drop选择器:

  // Initialize the jQuery File Upload plugin
  $('#upload').fileupload({

    // This element will accept file drag/drop uploading
    dropZone: $('#drop'),

然后,我们设置add回调函数。这个函数负责显示已添加到列表中的每个列表项,无论是通过拖放还是通过浏览文件。我们首先创建一个模板,然后将其缓存在tpl变量中:

  add: function (e, data) {
    var tpl = $('<li class="working"><input type="text" value="0" data-width="48" data-height="48"'+ ' data-fgColor="#0788a5" data-readOnly="1" data- bgColor="#3e4043"/><p></p><span></span></li>');

我们接着找到刚刚添加的文件名,然后计算并附加filesize函数到列表中:

    tpl.find('p').text(data.files[0].name).append('<i>' + formatFileSize(data.files[0].size) + '</i>');

    // Add the HTML to the UL element
    data.context = tpl.appendTo(ul);

接下来,我们初始化 jQuery Knob 插件。虽然现在它还没有运行,但它将产生一个良好的圆形状态表,显示上传任何文件到远程位置的进度:

    // Initialize the knob plugin
    tpl.find('input').knob();

目前,我们没有使用取消图标。这将是我们需要使用的事件处理程序,以确定是否在某个项目正在进行时取消上传:

    tpl.find('span').click(function(){

      if(tpl.hasClass('working')){
        jqXHR.abort();
      }

      tpl.fadeOut(function(){
        tpl.remove();
      });
    });

    // Automatically upload the file once it is added to the queue
    var jqXHR = data.submit();
  },

这是fileupload对象内的关键方法处理程序。它负责在触发更改更新 jQuery Knob 插件之前,计算文件上传进度的百分比值,如下所示:

  progress: function(e, data){
    var progress = parseInt(data.loaded / data.total * 100, 10);
    data.context.find('input').val(progress).change();
    if(progress == 100){
      data.context.removeClass('working');
    }
  },

如果文件上传失败,我们将设置一个.error类,这在附带的样式表中有适当的样式:

  fail:function(e, data){
    // Something has gone wrong!
    data.context.addClass('error');
  }
});

除了主要的fileupload配置对象之外,我们还设置了一些辅助函数。第一个辅助函数阻止了正常操作,如果我们拖动任何内容到文档对象上,将尝试在浏览器窗口中显示它:

$(document).on('drop dragover', function (e) {
  e.preventDefault();
});

第二个辅助函数处理文件大小从字节值转换为其对应的千字节、兆字节或千兆字节,然后返回用于在屏幕上渲染的值:

function formatFileSize(bytes) {
  if (typeof bytes !== 'number') {
    return '';
  }

  if (bytes >= 1000000000) {
    return (bytes / 1000000000).toFixed(2) + ' GB';
  }

  if (bytes >= 1000000) {
    return (bytes / 1000000).toFixed(2) + ' MB';
  }

  return (bytes / 1000).toFixed(2) + ' KB';
  }
});

目前,我们的项目肯定有改进的空间:它可以在普通浏览器窗口中正常工作,但需要修改以使其在 Node-WebKit 环境中 100%正常运行。我们稍后将在进一步探讨部分讨论一些改进代码的想法,但现在,在我们考虑调试应用程序之前,有一个重要的提示需要说明。

自动创建我们的项目

我在本书中试图保持的一个关键主题是我们如何更聪明地做事;任何人都可以编写代码,但更聪明的开发者知道何时是时候自动化一些更乏味的任务,并将他们的时间用在能够带来更多价值的任务上。

我们可以改进创建和构建项目的方法之一是自动化生成我们的骨架项目。幸运的是,我们可以使用 Yeoman generator for node-webkit 应用程序(可在 github.com/Dica-Developer/generator-node-webkit 找到),我们可以使用以下命令安装:

npm install -yeoman

前面的命令后面是这样的:

npm install –g generator-node-webkit

这显示了以下屏幕截图,显示了为测试项目输入详细信息的过程:

自动化创建我们的项目

如果一切顺利,你应该看到预定义的文件夹结构已经就位,可以供你使用,如下图所示:

自动化创建我们的项目

这样做会更容易创建所需的文件夹结构并在项目中保持一致性。

调试你的应用程序

此时,你应该有一个可以部署的工作应用程序。虽然必须说我们的应用程序在发布前还需要更多的工作,但是部署背后的原理是相同的,不论应用程序如何!在我们看部署之前,有一件小事我想讲一下。

还记得我在本章中提到 Sublime Text 将被广泛使用吗?这是有充分理由的:它非常适合构建应用程序,以至于我们可以运行和调试应用程序。为此,我们需要为 Sublime Text 创建一个新的构建系统文件(例如以下所述的 Windows):

{
  "cmd": ["nw.exe", "--enable-logging", "${project_path:${file_path}}"],
  "working_dir": "${project_path:${file_path}}",
  "path": "C:/Tools/nwjs/",
  "shell": true
}

为 Sublime 添加新的构建文件的过程很快—具体细节,请访问 github.com/nwjs/nw.js/wiki/Debugging-with-Sublime-Text-2-and-3。在开发应用程序时使用这个技巧是很有用的,因为手动构建过程可能会变得非常乏味!

打包和部署你的应用程序

好了,我们有一个可以打包和部署的工作应用程序;我们如何将其转化为可以提供下载的内容?

打包 Node-WebKit 应用程序出奇的简单。有一些注意事项,但主要过程是将所有的 Node-WebKit 可分发文件和我们的内容一起放入一个文件夹中,然后将其作为重命名的压缩文件发布。

有几种不同的方法可以打包我们的文件,这取决于所使用的平台。让我们看看在 Windows 平台上使用一些选项的情况,首先是手动编译。

注意

对于那些使用苹果 Mac 或 Linux 的人,有关如何打包应用程序的详细信息,请访问github.com/rogerwang/node-webkit/wiki/How-to-package-and-distribute-your-apps

手动创建软件包

假设我们已经准备好部署我们的应用程序,这些是手动创建软件包时要遵循的基本步骤——对于此示例,我们将使用构建我们的简单应用程序部分中早期创建的文件:

  1. 创建一个新的空 ZIP 文件,并添加package.jsonffmpegsumo.dllicudtl.datlibEGL.dlllibGLESv2.dllnw.pak文件——这些文件是在 Chromium 和 Node 的精简版本中托管站点所需的。

  2. cssimgjs文件夹以及index.html添加到 ZIP 文件中。

  3. 将 ZIP 文件重命名为.nw文件,然后运行nw.exe——这将使用package.json文件来确定应该运行什么。

注意

请注意,Node-WebKit 软件包不保护、混淆、数字签名或使软件包安全;这意味着,将您的软件包开源是一个更好的选择,即使只是为了避免任何与许可证相关的问题!

自动化过程

等等,创建一个软件包是一个手动过程,如果我们要添加很多更改,那么这个过程会变得很乏味,对吗?

绝对,智能的前进方式是自动化这个过程;然后,我们可以将其与 Grunt 软件包结合起来,例如grunt-contrib-watch(来自github.com/gruntjs/grunt-contrib-watch),以便在进行任何更改后立即构建我们的软件包。有几种自动化的方法——我个人最喜欢使用grunt-node-webkit-builder,来自github.com/mllrsohn/grunt-node-webkit-builder

注意

node-webkit-builder 插件由与 grunt-node-webkit-builder 背后的开发人员相同的开发人员创建;唯一的区别是,后者对与 Grunt 一起使用的额外支持。如果您想切换到使用 Grunt,您可以安装一个补充包,grunt-node-webkit-builder-for-nw-updater,可在www.npmjs.com/package/grunt-node-webkit-builder-for-nw-updater上找到。

让我们看看插件的运行情况——在继续演示之前,假设您已经安装了 NodeJS:

  1. 在项目文件夹中的一个新文件中,添加以下代码并将其保存为gruntfile.js

    module.exports = function(grunt) {
    
      grunt.initConfig({
        nodewebkit: {
          options: {
            platforms: ['win'],
            buildDir: './builds',
            winIco: './img/filesize.ico'
          },
          src: ['./css/*.css', './img/*.*', './js/*.js', '*.html', '*.php', '*.json', '*.ico']
        }
      })
    
      grunt.loadNpmTasks('grunt-node-webkit-builder');
      grunt.registerTask('default', ['nodewebkit']);
    };
    
  2. 接下来,我们需要安装 grunt-node-webkit-builder;因此,请启动 NodeJS 命令提示符的一个实例,然后导航到我们之前在构建我们的简单应用程序部分中使用过的项目文件夹。

  3. 输入此命令,然后按Enter,等待其完成:

    Npm install grunt-node-webkit-builder --save-dev
    
    
  4. package.json文件中,您会看到已添加了以下行,如下所示:

      "icon": "img/filesize.png"
      },
     "devDependencies": {
     "grunt": "~0.4.5",
     "grunt-node-webkit-builder": "~1.0.2"
     }
    }
    

    提示

    如果您需要查看 package.json 将会是什么样子,请转到 github.com/3dd13/sample-nw。有一个样本文件位于 github.com/3dd13/sample-nw/blob/master/package.json,显示了我们刚刚输入到我们自己版本文件中的代码的内容。

  5. 在此阶段,我们现在准备构建我们的包。在提示符处,键入 grunt,然后等待它完成;您应该看到它构建了包,如下面的截图所示:自动化过程

  6. 如果您回到我们的文件存储的文件夹,现在应该可以看到一个名为 builds 的文件夹已经出现了;在其中导航将会显示类似于此截图的内容,在其中显示了 win64 构建文件夹的内容:自动化过程

    在此阶段,我们可以双击 FileSizeView.exe 应用程序来启动该程序。这将以所有荣耀展示我们的应用程序,准备好使用。完美!我们现在可以部署文件了,对吧?

部署您的应用程序

嗯…慢点;你现在应该知道,我们总是可以做得更好!

绝对可以;在这种情况下,更好的方式是创建一个设置安装程序,这样我们只需要分发一个单个文件。这样更容易处理!它还有额外的好处,可以进一步压缩文件;在我们的示例中,使用开源的 Inno Setup 软件包,结果从大约 80 MB 降低到约 30 MB。让我们看看为在 Windows 平台上生成安装文件所需的内容:

  1. 我们首先需要下载并安装 Inno Setup。前往 www.jrsoftware.org/isinfo.php,然后点击 下载 Inno Setupsetup.exe 文件可以从页面中部的表格中下载。

  2. 双击 setup.exe 文件并完成流程,接受所有默认设置。

  3. 在我们的项目文件夹中,我们需要创建一个名为 setup 的新文件夹。这将存储用于 Inno Setup 的源脚本和最终构建的文件。

  4. 从代码下载中,继续提取 filesizeview-1.0.iss 并将其存储在 setup 文件夹中。

  5. 双击文件以启动它,然后点击下面的高亮图标,如下面的截图所示,编译构建文件:部署您的应用程序

  6. 完成后,Inno Setup 将自动启动新创建的安装程序,如此处所示:部署您的应用程序

现在我们可以跟随安装过程直至完成,在愤怒中使用该应用程序之前。 Inno Setup 还通过包含一个 unins000.exe 文件来处理卸载过程,如果我们需要从系统中移除应用程序,我们可以使用它。

对于那些使用 Mac 的人,可能会有类似的软件包可用。作为起点,请尝试www.codepool.biz/tech-frontier/mac/make-pkg-installer-on-mac-os-x.html中列出的说明。您还可以尝试在 Linux 上使用 Wine 使用 Inno Setup,说明列在derekstavis.github.io/posts/creating-a-installer-using-inno-setup-on-linux-and-mac-os-x/,尽管它们不适合初学者!

接下来的事情

哎呀!在过去的几页中,我们确实覆盖了很多内容!

但是,在生活的大计划中,我们只是触及了表面。我们可以在我们的应用程序中做更多的事情,甚至可以探索它来帮助我们在使用 Node-WebKit 和 jQuery 时提高技能。为了让你开始,这里有一些想法:

  • 该应用程序是调整图像大小甚至压缩它们的理想基础;我们可以在线完成这些操作,但有一些影响,主要是关于保密性和图像大小的问题。

  • 上传功能只有部分可用。我们使用 BlueImp 文件上传插件,但它实际上并没有做任何事情。在我们的应用程序中让它正常工作怎么样?

  • 如何显示文件类型的图标,甚至是如果我们上传的是图像的小缩略图?

  • 没有办法在不重新启动应用程序的情况下清除列表——修复这个问题应该很容易……

  • 我们故意没有包括任何错误检查,以保持事情简单;现在加入一些如何?

  • 我认为界面在某种程度上有些受限制:如果我们上传一个文件名非常长的文件,那么它就会被截断;截断有点混乱!

  • 我们还没有添加任何菜单控件。虽然 Node-WebKit 非常适合速度不是问题的应用程序,但一旦我们添加了更多功能,能够进行导航仍然是很好的。要了解如何添加这样的菜单的示例,请参阅www.4elements.com/blog/2013/12

希望在这里你能找到一些启发你进一步学习的想法。一旦掌握了基础知识,并允许我们必须使用 Node 特定标签的场合,就没有限制了!相当多的人已经制作了各种复杂性的应用程序并将它们发布在网上——一定要在网上进行一些研究,看看有什么可用的。以下是一些想法:

现在网上的内容越来越多。图书馆最近进行了一些名称更改(如前所述),所以如果你想了解更多关于使用 Node-WebKit 的信息,那么值得搜索 Node-WebKit 和 NW.js 两个词以确保全面覆盖。

总结

近年来,在线和离线应用之间的分界线变得模糊,许多人使用移动设备访问互联网来代替正常的桌面浏览器。随着 Node-WebKit 的出现,这进一步扩大了许多融合这些界限的机会——让我们回顾一下我们在过去几页学到的东西。

我们以似乎是一个典型简单请求开始,大多数开发人员将自动考虑设计一个网站。然而,随着 Node-WebKit 的引入,我们可以探索创建我们的应用程序或网站的离线版本。我们稍微探讨了一下库的工作原理,以及从桌面运行这种应用程序的利弊。

接着,我们开始准备开发环境,然后简要介绍了安装 Node-WebKit 并使用它创建我们的第一个应用程序。我们深入研究了 package.json 文件,这对于运行我们的应用程序至关重要,然后开始构建我们的文件大小查看器应用程序。接下来更深入地看了应用程序背后使用的代码;我们还介绍了如何使用 Yeoman Node-WebKit 生成器创建应用程序的基本框架。

接着,我们看了一个快速调试 Node-WebKit 应用程序的小窍门,然后继续研究如何手动打包和部署应用程序,或者使用 Grunt 自动化部署它们。我们旅程的最后阶段涵盖了应用程序的部署。我们研究了使用 Inno Setup 生成 setup.exe 文件以供部署使用。然后我们在章节结束时看了一些在使用 Node-WebKit 开发时可以进一步发展的想法。

哎呀!我们确实涵盖了很多内容,但正如他们常说的那样,恶人永无休息之日。在下一章中,我们将重点研究 jQuery 使用中最重要的部分之一:优化和提高项目性能。

第十三章:第 13 章:在 jQuery 中增强性能

到目前为止,我们在书中涵盖了一系列不同的主题:从定制 jQuery 到使用动画,甚至是在 Node-WebKit 中使用 jQuery。

但是,有一个关键的主题我们还没有涉及。虽然使用 jQuery 可能非常令人满意,但我们必须注意在实际情况下优化我们的代码,以确保用户体验良好。许多开发人员可能只是目测代码,但这是耗时的。在本章中,我们将探讨优化 jQuery 代码的方法,介绍可以补充现有工作流程并帮助实际反馈你的更改的工具。我们将在本章中涵盖一些主题,其中将包括:

  • 了解为什么性能很重要

  • 添加元素时监视性能

  • 监视 jQuery 的速度

  • 自动化性能监控

  • 使用 Node 自动清理我们的代码

  • 实施增强性能的最佳实践

  • 考虑使用 jQuery 的情况

准备好开始了吗?

注意

在本章中,我们将集中讨论使用 jQuery——你会发现很多给出的建议也可以应用于纯 JavaScript,在你的代码中更多地使用它(正如我们稍后在本章中将讨论的那样)。

了解为什么性能至关重要

想象一下情景——你的团队使用最新技术创建了一个杀手级的基于 Web 的应用程序,可以做任何事情,你准备坐下来享受成功的荣誉。除了一个小但相当关键的事情……

没有人购买。你的应用程序没有一个副本被售出——原因是什么?简单——它非常缓慢,而且没有经过适当的优化。在这个移动设备时代,一个慢速的应用程序将会让用户失去兴趣,无论你怎么推销。

我们应该关注应用程序的性能吗?绝对是!有很多理由要对我们应用程序的性能持批评态度;让我们来看几个:

  • 移动设备的出现以及与之相关的上网成本意味着我们的内容必须经过优化,以确保网站在连接超时之前快速显示

  • 将注意力集中在开发上而不是解决跨浏览器问题是非常容易的——每个怪癖本身可能并不多,但累积起来的影响很快就会显现出来。

  • 一旦你开始编写经过深思熟虑的代码,那么它很快就会成为第二天性

当然,必须说的是,存在过早优化的风险,即我们花费大量时间为小收益优化代码,甚至在以后删除后可能会给自己带来问题的代码!

好吧,假设我们有优化代码的余地,我们应该怎么做呢?嗯,我们可以使用一些技巧;虽然我们可能渴望不辞劳苦地优化我们的代码,但并不总是值得这样做。更明智的方法是始终考虑整体情况,确保通过糟糕的样式表或大图像而失去了优化脚本的好处,例如!

让我们花一点时间考虑一下我们可用的一些选项——它们包括:

  • 构建自定义版本的 jQuery

  • 对我们的脚本进行最小化处理

  • 调整选择器的使用

  • 在事件冒泡中谨慎行事

  • 持续使用适当的工具来检查我们的代码

  • 最小化 DOM 操作

这些是我们可用的一些选项。不过,我们首先要做的是对我们的代码进行基准测试,以了解在进行任何更改之前它的性能如何。这个过程的第一步是对我们的脚本运行性能检查。让我们花一点时间来看看涉及到什么以及这是如何运作的。

使用 Firebug 监控 jQuery 速度

我们可以大谈性能的重要性,但没有什么比亲眼看到它并弄清楚我们如何改进代码以获得额外优势更好。手动确定何处进行更改是耗时且低效的。相反,我们可以利用一些工具来更清晰地指示我们代码中的问题所在。

有数十种工具可用于帮助我们对页面性能进行基准测试,其中包括与 jQuery 或基于 jQuery 的脚本和插件的交互。在接下来的几页中,我们将介绍一系列不同的方法。让我们从一个简单的可视检查开始,使用 Firebug,从 www.getfirebug.com。安装完成后,单击 Net | JavaScript,然后加载您的页面以获取有关加载到页面上的每个插件或脚本的统计信息。

在下面的图像中,我们可以看到来自 Packt Publishing 网站的结果:

使用 Firebug 监控 jQuery 速度

相比之下,以下是从 www.jquery.com 显示的结果的图像:

使用 Firebug 监控 jQuery 速度

提示

在加载页面之前,清除缓存以避免结果出现偏差。

从 Firebug 返回的统计信息为我们提供了一个良好的开端,但要更好地了解瓶颈在哪里,我们需要对我们的代码进行分析。幸运的是,使用控制台来优化代码非常简单。让我们看看如何使用控制台来优化代码,使用我们在第十一章中创建的 tooltipv2.html 演示的副本为例,Authoring Advanced Plugins。为了这个小演示的目的,我们将从本地 Web 服务器(如 WAMP)运行它:

  1. 从代码下载中提取一个 tooltip 演示文件夹的副本,并将其存储在 WAMP 的 www 文件夹中。

  2. tooltipv2.js中,按照下面的示例修改前几行代码 - 这样可以添加调用以分析我们的代码:

    $(document).ready(function() {
      console.profile();
      $('#img-list li a.tooltips').quicktip({
    
  3. 我们需要告诉浏览器何时停止分析,所以请继续按照下面的代码进行修改:

      })
      console.profileEnd();
    });
    
  4. 在浏览器中加载tooltipv2.html,然后打开 Firebug。如果一切顺利,我们应该会看到类似以下屏幕截图的内容,在其中我们看到了简要的配置文件报告的前几行:使用 Firebug 监控 jQuery 速度

使用诸如 Firebug 这样的工具分析我们的网站可能会非常有启发性。想象一下,如果我们添加了更多的选择器会怎样,其中一些数字可能会比现在高得多。

提示

如果你只想关注花费的时间,与其使用console .profile(),不如改用console.time()console.timeEnd()

有许多工具可用于分析我们的网站。并非所有工具都专用于 jQuery,但它们仍然可以用于了解我们的脚本执行情况。以下是一些你可以尝试的示例,除了经典网站如JSPerf.com (www.jsperf.com)之外:

绝对有大量选择可供使用 - 并非所有选择都适合每个人的需求;关键是要了解你正在测试什么,并学会如何解释它。

jQuery 核心团队成员 Dave Methin 写了一篇精彩的文章,概述了在没有正确解释来自诸如 JSPerf 使用结果的情况下盲目尝试优化代码的危险。开发者 Fionn Kelleher 在他指出,你的代码应该是一种艺术品 - 没有必要为了做而优化所有东西;更重要的是代码应该是可读的并且运行良好。

好吧 - 是时候继续了。我们已经介绍了监控的基础知识,但这是以手动方式为代价的。一个更好的选择是自动化。我们可以使用许多工具来实现这一点,与我们的老朋友 Grunt 一起,所以让我们深入了解一下,看看自动化监控涉及哪些内容。

自动化性能监控

举手之劳,作为一名开发者,有多少人使用过 YSlow?很好——相当多;不过,你有没有考虑过自动化这些检查呢?

没错!我们总是可以手动检查以了解性能瓶颈出现的位置;然而,更聪明的方法是使用我们的好朋友 Grunt 自动化这些检查。开发者 Andy Shora 创建了一个模块,专门用于此目的;我们可以从github.com/andyshora/grunt-yslow获取它的源代码。让我们花点时间来让它运行起来,看看它是如何工作的:

  1. 让我们开始创建一个用于存放文件的项目文件夹。为了本练习的目的,我假设它叫做chapter13(是的,我知道——非常原创);如果你的名称不同,请更改。

  2. 对于本练习,我们需要使用 NodeJS。我假设你已经从之前的练习中安装了它;如果没有,请访问www.nodejs.org下载并安装适合你平台的版本。

  3. 接下来,将以下内容添加到一个空文件中,并将其保存为我们项目文件夹内的gruntfile.js——你会注意到我们的测试将针对 jQuery 的网站进行(如下所示):

    'use strict';
    
    module.exports = function (grunt) {
      grunt.initConfig({
        yslow: {
          pages: {
            files: [{
              src: 'http://www.jquery.com',
            }],
            options: {
              thresholds: {
                weight: 500,
                speed: 5000,
                score: 90,
                requests: 15
              }
            }
          }
        }
      });
    
      grunt.loadNpmTasks('grunt-yslow');
      grunt.registerTask('default', ['yslow']);
    };
    
  4. 在 NodeJS 命令提示符窗口中,输入以下命令以安装grunt-yslow包:

    npm install –g grunt-yslow
    
    
  5. Node 将进行安装。完成后,在命令提示符处输入以下命令以执行测试:

    grunt yslow
    
    
  6. 如果一切正常,Node 将显示类似于以下截图的内容,其中显示了一个失败:自动化性能监控

命令提示符窗口中显示的结果有点基本。为了更好地了解问题所在,我们可以安装 YSlow 插件。现在让我们来做这个:

提示

在撰写本文时,使用 Firefox 运行 YSlow 存在持续问题;请改用 Chrome 查看结果。如果你是 Mac 用户,那么你可以尝试从yslow.org/safari/获取 YSlow 插件。

  1. 浏览至www.yslow.org,然后在可用性下点击 Chrome,然后点击 添加 将插件添加到 Chrome:自动化性能监控

  2. 安装完成后,我们可以在 YSlow 中运行报告。如果我们对主要的 jQuery 网站进行测试,那么我们将得到类似于以下截图中所见的结果:自动化性能监控

    如果我们浏览各个给定的等级,我们可以清楚地看到还有改进的空间。专注于脚本,检查将显示至少有五个脚本应移至页面底部,因为在这些脚本完成之前,浏览器无法开始任何其他下载。

  3. 若要查看这将产生什么影响,请在 Firebug 中查看相同页面。单击 Net | JavaScript,然后刷新页面以查看从页面调用的所有脚本。将鼠标悬停在 jQuery 链接上 - 这证明了文件越大,加载时间越长:自动化性能监控

在前面的截图中,我们可以清楚地看到许多脚本,所有这些脚本都显示出长时间。在这种情况下,缩小那些尚未压缩的脚本将改善这些时间。

我们可以花时间尝试优化 jQuery,但这应该放在更大的背景下来考虑;如果我们的代码中仍然加载大型脚本,那么我们清楚地失去了优化 jQuery 的任何好处。

注意

值得注意的是,在 gruntfile.js 中的阈值已设置得比平常要高。在移动设备时代,确保页面内容可以快速下载非常重要;在这两个示例中,我们都会看到肯定有改进的空间!

让我们看看第二个示例,看看它与之前的有什么区别。在这种情况下,我们将使用 Packt Publishing 网站,网址为 www.packtpub.com

  1. 让我们回到我们在本节开头创建的 gruntfile.js 文件。我们需要修改以下行:

    files: [{
      src: 'http://www.packtpub.com',
    }],
    options: {
    
  2. 保存文件,然后切换到 NodeJS 命令提示符,并输入以下命令:

    grunt yslow
    
    
  3. 如果一切顺利,Node 将显示我们对 http://www.packtpub.com 评估的结果,我们会看到另一个失败,如下面的截图所示:自动化性能监控

如果我们像之前那样使用 YSlow 来查看,那么我们可以看到提出了一些建议,这些建议将改善性能。对我们来说,关键的建议是将六个脚本压缩为更少的文件(并对其进行缩小)。参考以下截图:

自动化性能监控

在之前的截图中,我们看到 YSlow 提到了类似的问题,尽管数字没有在 jQuery 网站上那么高。当我们检查由主页面调用的脚本的加载时间时,真正的问题就出现了:

自动化性能监控

尽管我们发出的请求较少,这是好事,但只有一个脚本被缩小了。这将抵消缩小的好处。我们可以通过缩小代码在一定程度上纠正这个问题。我们将在本章稍后的部分看看如何自动化此过程,在 使用 NodeJS 缩小代码 中会详细介绍。

使用 Google PageSpeed 获取见解

到目前为止,我们已经看到了如何监控页面,但是在非常技术性的层面上。我们的检查集中在从我们的页面调用的脚本的大小和返回时间上。

更好的选择是运行一个测试,比如 Google PageSpeed,使用 Grunt 包,可以从github.com/jrcryer/grunt-pagespeed获取;我们可以在这个截图中看到结果:

使用 Google PageSpeed 获取洞察力

它不会查看页面上的特定脚本或元素,但会给出我认为更真实的页面性能视图。

注意

这个演示需要使用 Node 和 Grunt,所以在继续之前确保你已经安装了它们两个。

现在让我们看看它在 Packt Publishing 网站上的实际应用:

  1. 我们将首先启动一个 NodeJS 命令提示符,然后切换到我们的项目文件夹区域。

  2. 输入以下内容以安装grunt-pagespeed包:

    npm install grunt-pagespeed --save-dev
    
    
  3. 在一个新文件中,添加以下内容,并将其保存为gruntfile.js,保存在相同的文件夹中 - 在代码下载中有一个此文件的副本;提取并将gruntfile-pagespeed.js重命名为gruntfile.js

    Gruntfile.js:
    'use strict';
    
    module.exports = function (grunt) {
      grunt.initConfig({
        pagespeed: {
          options: {
            nokey: true,
            url: "https://www.packtpub.com"
          },
          prod: {
            options: {
              url: "https://www.packtpub.com/books/content/blogs",
              locale: "en_GB",
              strategy: "desktop",
              threshold: 80
            }
          }
        }
      });
    
      grunt.loadNpmTasks('grunt-pagespeed');
      grunt.registerTask('default', 'pagespeed');
    };
    
  4. 在 NodeJS 命令提示符下,输入以下命令以生成报告:

    grunt-pagespeed
    
    
  5. 如果一切正常,我们应该会看到一个类似于我们练习开始时显示的报告。

grunt-pagespeed插件只是使用 Grunt 运行的几个示例中的一个。还有其他可用的基准任务,我们可以集成到持续监视我们网站的过程中。这些包括以下内容:

  • grunt-topcoat-telemetry: 从遥测中获取流畅性、加载时间和其他统计数据作为 CI 的一部分。这可以帮助您设置一个性能基准仪表板,类似于 Topcoat 使用的仪表板(bench.topcoat.io)。

  • grunt-wpt: 用于测量 WebPageTest 分数的 Grunt 插件。

  • grunt-phantomas: 请求的响应时间,响应的响应时间,首个image/CSS/JS的时间,在DOM Ready上等等。

注意

如果你更喜欢使用 Gulp,那么之前的 Grunt 插件可以使用gulp-grunt来运行,可以从npmjs.org/package/gulp-grunt获取。

现在我们知道了我们的基准,是时候探索如何优化我们的代码了;大多数开发者要么手动查看代码,要么可能使用网站如www.jshint.com(甚至jslint.com)。这种方法没有错。但是,这不是最好的方法,因为这是对我们时间的低效使用,有可能错过改进我们代码的机会。

对代码进行代码检查的更聪明的方法是自动化这个过程 - 虽然它可能不会警告你需要进行重大更改,但它至少会确保我们的代码不会由于错误而无法优化。当然,它还会为我们提供一个坚实的基础,以便我们可以进一步进行优化。我们将在本章后面更多地介绍这个。

是时候进行演示了!让我们花一点时间设置使用 NodeJS 进行自动检查。

自动对 jQuery 代码进行代码检查

对代码进行清理,或者检查它是否存在错误,是 jQuery 开发的一个重要部分。它不仅有助于消除错误,还有助于识别脚本中未使用的代码。

不要忘记 - 优化不仅仅是调整选择器,甚至用 CSS 等效替换 jQuery 代码(正如我们在第六章中看到的那样,用 jQuery 进行动画)。我们首先需要确保有一个坚实的基础来工作 - 我们始终可以手动完成这个过程,但更明智的选择是使用像 Grunt 这样的任务运行器来自动化该过程。

让我们花点时间看看这是如何运作的 - 请注意,这假设你之前的练习中仍然安装了 NodeJS。这一次,我们将用它来安装grunt-contrib-jshint包,可从github.com/gruntjs/grunt-contrib-jshint获取:

  1. 设置自动检查非常容易。首先,我们需要下载并安装grunt-contrib-jshint。打开 NodeJS 命令提示符,并在项目文件夹区域内输入以下内容:

    npm install grunt-contrib-watch
    
    
  2. 安装完成后,继续在新文件中添加以下内容,并将其保存为gruntfile.js,保存在项目文件夹中:

    'use strict';
    
    module.exports = function (grunt) {
      // load jshint plugin
      grunt.loadNpmTasks('grunt-contrib-jshint');
    
      grunt.initConfig({
        jshint: {
          options: { jshintrc: '.jshintrc' },
          all: [ 'js/script.js' ]
        }
      });
    
      grunt.loadNpmTasks('grunt-contrib-jshint');
      grunt.registerTask('default', ['jshint']);
    };
    
  3. 从代码下载中,我们需要提取我们的目标 JavaScript 文件。继续并在我们的项目区域的js子文件夹中保存script.js的副本。

  4. 回到 NodeJS 命令提示符,并输入以下命令,对我们的代码运行jshint检查:

    grunt jshint
    
    
  5. 如果一切顺利,我们应该会看到它弹出三个需要修复的错误,如下一张截图所示:自动检查 jQuery 代码

注意

你们中注意力集中的人可能会注意到,这是我们在第十一章中创建的快速提示插件的代码,编写高级插件

我们可以更进一步!我们可以让 Grunt 在代码更新时自动运行检查,而不是手动运行检查。为实现这一点,我们需要安装grunt-contrib-watch包,并相应地更改 Grunt 文件。现在就来做吧:

  1. 打开gruntfile.js的副本,然后在grunt.initConfig对象的结束});之前立即添加以下代码:

        },
        watch: {
          scripts: {
            files: ['js/script.js'],
            tasks: ['jshint'],
            options: { spawn: false }
          }
        }
    
  2. 在文件末尾添加以下行,以注册额外的任务:

    grunt.loadNpmTasks('grunt-contrib-jshint');
    
  3. 我们需要修改registerTask调用以使 Grunt 意识到我们的新任务。继续按照下面的修改:

    grunt.registerTask('default', ['watch', 'hint']);
    
  4. 切换回命令提示符窗口,然后在命令行中输入以下内容:

    grunt watch
    
    
  5. 切换回script.js,并在代码中的某处进行更改。如果一切顺利,Node 将启动并重新检查我们的代码。自动检查 jQuery 代码

运行代码清楚地显示我们有一些问题需要解决。在这个阶段,我们会花时间来解决它们。一旦更改完成,Node 将启动并显示更新后的错误列表(或通过!)。

假设我们的代码适合用途,我们可以真正开始优化它。一个简单的方法是压缩代码,以帮助保持文件大小的低水平。当然,我们可以手动压缩它,但那是老掉牙的做法;是时候再次挖掘 Node 了!

使用 NodeJS 压缩代码

开发者工作流程中的关键部分应该是一个用于压缩站点中使用的脚本的过程。这样做有助于减少页面下载内容的大小。

当然,我们也可以手动执行此操作,但这是一个耗时的过程,几乎没有什么好处;一个更聪明的方法是让 NodeJS 为我们处理这个问题。这样做的美妙之处在于,我们可以配置 Node 以运行一个诸如grunt-contrib-watch之类的包;我们所做的任何更改都将自动被压缩。甚至可能会有一些情况,我们决定不生成一个压缩文件;如果我们不确定我们正在编写的代码是否会起作用。在这种时候,我们可以从我们的文本编辑器中启动 Grunt,如果我们正在使用 Sublime Text 等包。

提示

如果你想在 Sublime Text 中实现这种级别的控制,请查看sublime-grunt,可以从github.com/tvooo/sublime-grunt获取。

好的,让我们开始设置我们的压缩过程。为此,我们将使用著名的包,UglifyJS(来自github.com/mishoo/UglifyJS2),并让 Node 自动检查:

  1. 对于这个演示,我们将使用 NodeJS,所以如果你还没有这样做,可以从www.nodejs.org下载适合你平台的相应版本,接受所有默认值。

  2. 对于这个演示,我们需要安装两个包。UglifyJS 提供了对源映射的支持,所以我们首先需要安装它。从 NodeJS 命令提示符,切换到项目文件夹,输入以下命令,然后按Enter

    npm install source-map
    
    
  3. 接下来,输入以下命令,然后按Enter

    npm install uglify-js
    
    
  4. 安装完成后,我们可以运行 UglifyJS。在命令提示符处,小心输入以下命令:

    uglifyjs js/jquery.quicktipv2.js --output js/jquery.quicktipv2.min.js --compress dead_code=true,conditionals=true,booleans=true,unused=true,if_return=true,join_vars=true,drop_console=true --mangle --source-map js/jquery.quicktipv2.map
    
    
  5. 如果一切正常,Node 将按照这个下一个屏幕截图所示的过程进行:使用 NodeJS 压缩代码

  6. 最后,在我们的项目区域中应该有三个文件,如下面的屏幕截图所示:使用 NodeJS 压缩代码

我们现在可以在生产环境中自由使用我们代码的压缩版本。虽然在这个例子中我们没有节省太多,但你可以想象如果我们扩大这些数字来覆盖更大的脚本时的结果!

探索一些值得注意的点

压缩脚本的过程应该成为任何开发人员工作流程的一部分。NodeJS 使添加它变得容易,尽管有一些提示可以帮助使压缩文件更容易和更高效:

  • UglifyJS 的默认配置将只生成显示很少压缩的文件。要获得更好的结果,需要仔细阅读所有可用的选项,了解哪个选项可能适合您的需求,并且可能会产生最佳结果。

  • 我们在压缩过程中包含了源映射选项。我们可以使用它来将出现的问题与原始源代码关联起来。启用源映射支持在不同的浏览器中会有所不同(对于支持它的浏览器);例如,在 Firefox 中,按下 F12 键显示开发者工具栏,然后点击齿轮图标并选择 显示原始源代码探索一些值得注意的点

  • 值得检查一下,您的项目中是否已经存在所使用文件的最小化版本。例如,您的项目是否使用了提供了最小化版本的插件?如果是这样的话,我们所需要做的就是将它们连接到一个文件中;再次对它们进行最小化可能会导致问题,并且破坏文件中的功能。

对文件进行最小化并不是一门黑魔法,但也不是一门精确的科学。在压缩文件之前,很难知道在文件大小方面会得到多大的改进。你可能会得到一些意想不到的结果。现在就值得探索一个这样的例子。

通过一个真实的示例来理解

在为这本书研究材料时,我尝试将 Packt Publishing 网站上使用的 Drupal 文件之一进行最小化,作为一个测试。原始文件大小为 590 KB;使用与我们演示中相同配置选项的压缩版本,生成了一个文件大小为 492 KB 的文件。

这告诉我们什么?嗯,有几点需要注意:

  • 保持合理的期望是很重要的。压缩文件是我们使用的一种有用的技巧,但它并不总能产生我们需要的结果。

  • 我们使用了 UglifyJS(版本 2)。这个工具非常易于使用,但在原始压缩能力方面存在一些折衷。在某些情况下,它可能不适合我们的需求,但这并不意味着它有缺陷。目前有几十种压缩工具可供选择;我们只需选择另一种替代方案即可!

  • 要真正实现显著的大小减小,可能需要使用gzip来压缩文件,并配置服务器以动态解压缩。这将增加处理页面的开销,需要将其纳入我们的优化工作中。

相反,逐个检查每个脚本以确定哪些正在使用、哪些可以安全地删除可能是更好的选择。当然,我们可以手动执行此操作,但是嘿——您现在已经认识我了:为什么自己做当您可以将其推迟到其他事情去做呢(错误引用一个短语)?进入 Node!让我们来看看unusedjs,它可以帮助我们了解我们的脚本中有多少额外的代码。

提示

我们已经集中在压缩一个文件上,但是通过使用通配符条目,自动压缩任何文件的配置变得非常简单。

找出未使用的 JavaScript

到目前为止,我们已经看到了如何轻松地压缩代码而不需要任何努力——但是如果仅仅压缩还不够,我们需要删除多余的代码怎么办呢?

好吧,我们可以手动检查代码——这没什么错。这是一种完全可以接受的工作方式,但关键是这是一个手动过程,需要大量时间和精力——更不用说频繁尝试查找可以删除的代码而不会破坏其他代码了!

更明智的做法是设置 Node 为我们解决哪些代码正在使用以及哪些可以安全丢弃。网页性能专家 Gaël Métais 创建了 unused JS 来帮助解决此问题。它与 Node 一起工作,并且可以在 www.npmjs.com/package/unusedjs 上获得。这是一个正在进行中的工作,但只要它被用作指南,它就可以为我们提供一个有用的基础,让我们知道在哪里可以进行更改。

让我们花点时间深入了解它是如何工作的。对于此演示,我们将使用我们在 第十二章 中创建的 Tooltip 插件演示,使用 jQuery 与 Node-WebKit 项目

找出未使用的 JavaScript

在使用此功能时,有几件事情需要记住:

  • 在撰写时,此插件的状态仍然处于非常初期阶段——使用 alpha 版软件的常见风险仍然存在!它并不完美;它应该只作为指南使用,并且需自担风险。它不能很好地处理长脚本(比如 jQuery UI 库),但可以处理大约 2,500-3000 行。

  • 您需要清除浏览历史记录,因此不要在对浏览历史记录至关重要的浏览器中使用它。

  • 该插件使用 Node。如果您没有安装它,请转到 Node 网站 www.nodejs.org 下载并安装适合您平台的版本。

  • 我们还需要使用本地 Web 服务器,如 WAMP(用于 PC - www.wampserver.com/dewww.wampserver.com/en/),或 MAMP(用于 Mac - www.mamp.info)进行演示。确保您已经设置和配置了某些内容以供使用。

假设我们已经安装并配置了 Node 和本地 Web 服务器供使用,让我们从设置 unusedjs 脚本开始。我们将使用 Firefox 运行演示,如果你更喜欢使用其他浏览器,请相应调整。让我们开始:

  1. 我们需要从某个地方开始。第一步是安装 unusedjs.,在 NodeJS 提示符下运行以下命令:

    npm install unusedjs -g
    
    
  2. 通过在控制台中输入以下内容启动服务器:

    unused-js-proxy
    
    
  3. 点击三条杠图标,然后点击 选项,以显示选项对话框。确保以下条目设置如下图所示:清除未使用的 JavaScript

  4. 确保 No Proxy 字段为空。然后点击 OK 确认设置。

  5. 接下来,我们需要清除浏览器会话中的缓存。这一点至关重要,如果不清除缓存,我们可能会得到扭曲的结果。

  6. 在这个阶段,从随书代码下载中打开 tooltipv2.html 的副本,并等待页面完全加载。

  7. 按下 F12 显示 Firefox 的控制台,并在提示符处输入以下内容:

    _unusedjs.report()
    
    
  8. 如果一切正常,当查看控制台结果时,我们应该看到类似以下截图的内容:清除未使用的 JavaScript

尝试在控制台中输入 _unusedjs.file(2)。这个函数会显示代码的副本,并用红色突出显示未使用的部分,如下截图所示:

清除未使用的 JavaScript

现在我们可以集中精力在突出显示的部分上,从我们自己的脚本中删除冗余代码。当然,这将取决于我们自己的要求,以及冗余代码是否将作为即将到来的工作的一部分而被使用。

提示

毫无疑问,我们不能简单地从诸如 jQuery 这样的库中删除代码。我们需要构建 jQuery 的自定义版本——我们在第一章中详细讨论了这一点,安装 jQuery

现在我们已经建立了基准,并确定了我们的脚本中是否包含未使用的代码,现在是时候来优化它了。让我们看看我们的代码中可以使用的一些技巧和窍门;作为将最佳实践嵌入到我们的正常开发工作流程的基础。

实施最佳实践

想象一下情景——我们已经编写了我们的代码,并检查了以确保所有文件在可能的情况下都被最小化,而且我们没有包含大量冗余代码。此时,有些人可能会认为我们已经准备好发布代码,并将我们的努力提供给公众使用,对吗?

错误!在这个阶段发布代码而不检查我们的代码的速度和效率,是不负责任的。谷歌的联合创始人兼首席执行官拉里·佩奇在他说到这一点时表达得很完美:

"作为产品经理,你应该知道速度是产品功能的第一要素。"
--Google 联合创始人兼首席执行官拉里·佩奇

速度绝对是王道!我们已经在满足 Larry 的评论方面有了一些进展,但我们可以做得更多。到目前为止,我们已经看过了将我们的代码最小化和生成自定义版本的 jQuery。我们可以通过评估我们编写的代码来进一步确保它被有效执行。每个人的需求自然会有所不同,所以我们需要使用一些技巧的组合来确保有效的执行。让我们看看其中的一些:

  1. 毫无疑问,我们应该只在绝对必要时针对 DOM 执行任务。对 DOM 的每次命中可能对资源是昂贵的,使您的应用程序变慢。例如,考虑以下代码:

    <script src="img/jquery-2.1.3.min.js"></script>
    <script type="text/javascript">
      $("document").ready(function() {
        console.log("READY EVENT (B) => " + (newDate().
    getTime() - performance.timing.navigationStart) + 'ms');
      });
      console.log("END OF HEAD TAG (A) => " + (new Date()
    .getTime() - performance.timing.navigationStart) + 'ms');
    </script>
    
  2. 在空的<body>标签上,加载 jQuery 库并使其可用所花费的时间相对较少;一旦我们在页面上添加元素,这个值就会增加。为了看到差异,我使用了这段代码运行了一个小演示。在下面的图像中,加载空的<body>标签上的 jQuery 的结果在左侧,而在之前演示中使用tooltipv2.html的结果在右侧:实施最佳实践

  3. 如果使用的是 jQuery 的 1.11 版本,那么由于包含了支持旧版浏览器的代码,其效果甚至更加明显。要亲自看到效果,请尝试运行test loading jquery.html,然后切换到您的浏览器的开发者工具栏中的Console来查看测试结果。将 jQuery 版本更改为1.11以真正看到差异!

    为了保持性能,DOM 元素应该被缓存在变量中,然后在被操作后才添加:

    // append() is called 100 times
    for (var i = 0; i < 100; i++) {
      $("#list").append(i + ", ");
    };
    
    // append() is called once
    var html = "";
    for (var i = 0; i < 100; i++) {
      html += i + ", ";
    }
    $("#list").append(html);
    

    提示

    您可以通过在 JSPerf 上运行测试来查看结果的实际情况,网址为jsperf.com/append-on-loop/2

  4. 另一方面,如果我们需要修改与单个元素相关的几个属性,则使用对象会使操作变得更容易,但同时也会撤销我们的所有努力!

  5. 检查选择器非常重要。jQuery 是从右向左读取它们的。尽可能使用 ID,因为它们比标准类选择器更快。还要确保您没有使用诸如.myClass1 #container之类的规则,其中 ID 跟在类选择器后面。这是低效的 - 我们失去了指定只能是单个值的好处,因为不断地迭代代码以确保我们已经涵盖了我们在代码中使用的类的所有实例。

    毫无疑问,所使用的任何选择器都应该被缓存。当引用多级深度的选择器时,最佳实践规定我们应该在左侧尽可能具体(即.data),在右侧尽可能不太具体:

    // Unoptimized:
    $( ".data .gonzalez" );
    
    // Optimized:
    $( "div.data td.gonzalez" );
    
  6. 最重要的是,避免使用*或类型等形式的通用选择器,例如:radio,除非您使您的选择器引用尽可能明确 - 这两者都非常慢!

  7. 尽管这本书是关于 jQuery 的,但在性能不够时,可能需要使用经典 JavaScript 方法。例如,for循环比 jQuery 的.each()方法更有效,使用querySelector API 比使用 jQuery 选择器更好。

  8. 如果您正在加载多个脚本,请考虑在页面末尾加载它们,一旦在页面上方(或在页面向下滚动之前显示的内容)加载了所有内容。jQuery 应始终用于逐步增强页面,而不是运行一段代码,如果禁用 jQuery,则会破坏页面。感知力可能起很大作用 - 您的页面可能没有做很多事情,但仍然被感知为慢。重新排序脚本(和内容)可以帮助改变这种感知。

  9. 有些开发者可能仍然使用 jQuery 的 AJAX 对象来处理异步 HTTP 请求。虽然它能够运行,但不是处理此类请求的最干净的方式:

    $.ajax({
      url: "/firstAction",
      success: function() {
        //whatever
        secondAction();
        return false;
      },
      error: error()
    });
    
    var secondAction = function() {
      $.ajax({
        url: "/secondAction",
        success: function() {
          // whatever
        },
        error: error()
      });
    };
    

    更明智的选择是使用 jQuery promises(),在那里我们可以将代码延迟到更容易阅读和调试的函数中。代码存储在何处并不重要;promises()将允许我们在代码的适当位置调用它:

    $.when($.ajax( { url: "/firstAction" } ))
    
    // do second thing
    .then(
      // success callback
      function( data, textStatus, jqXHR ) {},
      // fail callback
      function(jqXHR, textStatus, errorThrown) {}
    )
    
    // do last thing
    .then(function() {});
    
  10. 如果我们正在调用整个脚本,则探索使用条件加载器是有意义的,比如 RequireJS(使用纯 JavaScript),或grunt-requirejs(如果我们更喜欢使用 Node)。

    提示

    毫无疑问,懒加载我们的代码的同样原则也适用于页面上的元素,例如图像;jquery-lazy是一个完美的 Node 模块示例,可以帮助实现这一点。它可在www.npmjs.com/package/jquery-lazy找到。

  11. 先前提到使用promises()的提示展示了一个完美的例子,说明我们仍然可以做出改进。有些开发者赞美代码链的优点,这样做似乎可以缩短代码。然而,这使得代码更难阅读,因此也更难调试;由此产生的代码混乱将导致错误和浪费时间,最终需要部分或完全重构代码。先前提示中的例子还突显了需要确保使用良好的命名约定,因为当链式命令时,我们无法具体指定回调函数的名称。

  12. 这个下一个提示可能看起来有点矛盾,因为我们在本书中始终在谈论 jQuery,但是要使用更少的 JavaScript - 可以转移到 HTML 或 CSS 的任何内容都会对我们的性能产生积极影响。虽然使用 jQuery 很有趣,但它是基于 JavaScript 的,而 JavaScript 是 Web 堆栈中最脆弱的层,会影响性能。这的经典例子是创建动画。查看css-tricks.com/myth-busting-css-animations-vs-javascript/,了解为什么在不必要时使用 jQuery(或 JavaScript)来驱动我们的动画是愚蠢的。

  13. 考虑削减芥末,或者为能力较弱的浏览器放弃功能。当在能力较弱或移动浏览器上使用基于 jQuery 的站点时,这将带来更好的体验。在一些具有许多 polyfill 运行以支持功能(例如 CSS3 样式)的站点上,放弃这些 polyfill 的影响可能很大!

  14. 要了解加载和解析 jQuery 所需时间的差异,请查看开发人员 Tim Kadlec 在timkadlec.com/2014/09/js-parse-and-execution-time/进行的测试。

我们可以在我们的代码中使用许多更多的技巧和诀窍。有关更多灵感来源,请参考以下链接作为起点:

通过所有这些的关键点是,性能优化永远不应被视为一次性的活动;我们必须将其视为代码生命周期中的持续过程。为了帮助实现这一点,我们可以设计一个策略来掌握优化。让我们以这些提示作为我们需要考虑的策略的基础。

设计一个性能策略

到目前为止,我们集中在可以用来改善性能的技巧和窍门上。采取一种反应式的方法是可行的,但需要额外的时间来投入,而我们可以在编写代码时就将这些技巧和窍门融入其中。

考虑到这一点,制定一项策略来鼓励这种思维方式将是有益的。让我们来看看可以形成这样一项策略基础的几个关键点:

  • 始终使用最新版本的 jQuery - 您将从代码改进、速度和已知问题的错误修复中受益。

  • 在可能的情况下,合并和压缩脚本,以减少带宽使用。

  • 使用原生函数而不是 jQuery 的等效函数 - 一个完美的例子是使用for()而不是.each()

  • 使用 ID 而不是类 - ID 只能分配一次,而 jQuery 将多次命中 DOM 以查找每个类,即使只有一个实例存在也是如此。

  • 给选择器一个上下文。参考以下代码,简单指定一个类:

    $('.class').css ('color' '#123456');
    

    相反,更明智的做法是使用上下文化的选择器,形式为$(expression, context),从而产生:

    $('.class', '#class-container').css ('color', '#123456');
    

    第二个选项运行速度更快,因为它只需要遍历#class-container 元素而不是整个 DOM。

  • 在可能的情况下,缓存值,以避免直接操作 DOM。

  • 使用join()而不是concat()来连接较长的字符串。

  • 在使用#作为源链接的链接的点击事件上始终添加return false,或者使用e.preventDefault() - 如果不添加,将会跳转到页面顶部,在长页面上会很烦人。

  • 在页面重量、请求和渲染时间方面为自己设定预算 - 请参阅timkadlec.com/2014/11/performance-budget-metrics/。这给优化提供了目的,并鼓励更长期的性能监控精神。

  • 使用诸如 SpeedCurve 之类的性能监控服务来帮助监视您的网站,并在出现问题时提醒您。

  • 在办公室展示性能 - 这有助于鼓励团队精神。如果团队中的某人提出的更改对性能产生了积极影响,则表彰他们并让其余团队成员知晓;这将有助于激励团队之间的健康竞争意识。

  • 但是,如果一个改变破坏了性能,那么不要惩罚罪犯;这将使他们不愿参与。相反,试着培养解决问题的文化,然后学会如何防止它再次发生。如何运行像 PhantomJS 这样的测试来帮助检查和减少问题出现的风险呢?

  • 自动化一切。有服务可以压缩图像或缩小脚本,但投入时间开发类似的内部流程可以节省时间和金钱。关键在于,手动执行诸如优化图像或压缩脚本的任务是没有意义的;你需要自己找出最适合你需求的方法。

  • 是否决定使用 Grunt 或 Gulp 是一个关键考虑因素——它们是否提供了有用的附加功能,还是只是一个可以通过谨慎使用 NPM 来减少或消除的额外负担?开发者 Keith Cirkel 在blog.keithcirkel.co.uk/why-we-should-stop-using-grunt/提出了一个有力的理由,认为只使用 NPM 就足够了;这是一个发人深省的观点!

  • 花时间影响你的同事和那些更高层的人——他们通常可能不知道你在某个问题上可能遇到的困难,但实际上可能有能力帮助你解决问题!

  • 花时间学习。我们往往花太多时间在客户工作上,而没有为自我发展留出足够的时间;花一些时间来纠正这一点。如果这意味着需要调整价格来弥补由于未花时间在客户工作上而导致的收入损失,那么这是需要考虑的事情。这一切都关乎于建立工作/娱乐/学习的平衡,这将在长期内得到回报。

这里有很多值得深思的东西——并不是每个技巧都适用。在某些情况下,一个或多个技巧的融合会产生你需要的结果。然而,花费时间在这上面是值得的,因为长期来看会得到丰厚的回报,并且有望在你的团队现有的工作文化中获得融入。

让我们继续。我们快要完成本章了,但在我们结束之前,让我们来看看如何为 jQuery 进行测试,我想问一个简单的问题:我们真的需要使用 jQuery 吗,如果需要,为什么?

关于使用 jQuery

在这一点上,如果你认为我完全失去了理智,那是可以理解的,特别是当我们刚刚在考虑优化代码的方法时,却建议完全将其从我们的代码中删除。你可能会问,为什么我会考虑放弃 jQuery 呢?

嗯,有几个充分的理由。任何人都可以编写 jQuery 代码,但聪明的开发者应该始终考虑他们是否应该使用 jQuery 来解决问题:

  • jQuery 是一个抽象库,它需要 JavaScript,并且是在开发当时的浏览器可能是一个真正的挑战的时候构建的。需要抽象掉浏览器的不一致性正在变得越来越少。重要的是要记住,我们应该使用 jQuery 来逐步增强普通的 JavaScript;jQuery 首先是为了让编写 JavaScript 更容易而设计的,并不是一种独立的语言。

  • 浏览器在功能上比以往任何时候都更接近。随着 Firefox 放弃了大部分厂商前缀,库几乎不再需要消除不一致性。如果某个功能在 IE10 或最新版本的 Firefox 中可行,那么很可能在 Google Chrome 或 Opera 中也适用。当然,会有一些差异,但这实际上仅适用于一些尚未被广泛使用的更为深奥的 CSS3 样式。那么 - 如果浏览器如此接近,为什么还要使用 jQuery?

  • 使用纯 JavaScript 总是比使用 jQuery 快,无论我们如何努力 - 这还有一个额外的好处,即 JavaScript 代码将产生比等效 JavaScript 代码(不包括库本身)更小的文件。如果我们只使用少量 JavaScript 代码,那么为什么要引用一个完整的库呢?当然,我们总是可以尝试构建 jQuery 的自定义版本,就像我们在 第一章 中看到的那样,安装 jQuery - 但无论我们如何尝试修剪库中的不必要代码,我们仍然会引入比我们需要的更多的代码!当然,我们当然可以使用 gzip 进一步压缩 jQuery 代码,但它仍然会比纯 JavaScript 多。

  • 编写 jQuery 实在太容易了 - 它拥有庞大的社区,学习曲线很低。这为编写大量低质量的代码创造了完美的条件,我们只使用了 jQuery 中提供的一小部分功能。从长远来看,学习如何有效地使用纯 JavaScript,然后使用 jQuery 为其提供象征性的锦上添花将会更好。

但这里的关键点是我们不应完全放弃 jQuery - 现在是时候真正考虑是否需要使用它了。

当然 - 如果我们使用了大量在纯 JavaScript 中要么很难实现,要么根本无法实现的功能,那么就必须使用 jQuery。然而,我给你留下一个挑战,用拍照作为类比。像平常一样构图。然后停下来,闭上眼睛十秒钟,深吸几口气。现在问问自己,你是否还准备拍同样的照片。很可能你会改变主意。同样的道理也适用于使用 jQuery。如果你停下来认真考虑了自己的代码,我想你们中有多少人仍然决定继续使用它呢?

就个人而言,我认为 jQuery 仍将发挥作用,但我们已经到了一个不应该盲目或出于习惯使用它的时候,而是要在何时何地使用它代替纯 JavaScript 做出有意识的决定的时候。

要了解如何从 jQuery 切换到 JavaScript 来满足简单的需求,请参阅 Todd Motto 的文章 toddmotto.com/is-it-time-to-drop-jquery-essentials-to-learning-javascript-from-a-jquery-background/

摘要

维护高性能的网站是开发的关键部分。除了优化代码之外,还有更多内容,让我们花点时间回顾一下本章学到的东西。

我们首先了解了为何理解性能的重要性,并通过各种监视性能的方式,从在 Firebug 中查看统计数据到使用 Grunt 自动化我们的检查,进行了深入研究。

然后,我们继续了解如何自动地对我们的代码进行代码检查,作为优化代码的众多方式之一,然后对其进行缩小以供生产使用。然后,我们深入研究了如何判断我们的代码是否包含任何未使用的代码,这些代码可以安全地删除,以简化我们的代码。

然后,我们通过实施最佳实践来结束本章。这里的重点不是提供具体的例子,而是分享一些可应用于任何网站的技巧和窍门。然后,我们以此为基础设计了一种策略,帮助维护高性能的网站。

我们即将完成精通 jQuery 的旅程,但在结束之前,我们需要快速看一下如何测试我们的代码。开发者可能会使用 QUnit,因为它是 jQuery 家族项目的一部分;我们将在下一章中进一步探讨如何更深入地使用它。

第十四章:测试 jQuery

要测试还是不要测试,这是个问题…

为了用这位世界著名的侦探的话来说,对于这个问题的答案应该是很简单的!

如果你花了一些时间学习 jQuery,你肯定知道它的单元测试的重要性,而且最流行的方法就是使用它的测试库 QUnit。在本章中,我们将回顾如何使用它,然后看一些我们应该使用的最佳实践,以及探讨如何真正减少我们的工作流程,通过自动化我们对代码的测试。

在本章中,我们将涵盖以下主题:

  • 重新审视 QUnit

  • 使用 NodeJS 和 RequireJS 进行自动化测试

  • 使用 QUnit 时的最佳实践

准备好了吗?让我们开始吧…

重新审视 QUnit

对任何代码进行测试对于成功构建任何在线应用程序或站点至关重要;毕竟,我们不希望在最终结果中出现错误,对吧?

测试可以手动进行,但存在人为因素的增加风险,我们无法始终确定测试是否完全相同。为了减少(甚至消除)这种风险,我们可以使用 jQuery 的单元测试套件 QUnit 来自动化测试。当然,我们可以手动运行 QUnit 测试,但 QUnit 的美妙之处在于它可以完全自动化,我们将在本章后面看到。

现在,让我们花一点时间回顾一下如何安装 QUnit 并运行一些基本测试的基础知识。

安装 QUnit

安装 QUnit 有三种方式。我们可以简单地在代码中包含两个链接,使用 qunitjs.com 提供的 JavaScript 和 CSS 文件。这些文件可以直接引用,因为它们是托管在由 MaxCDN 提供的 QUnit 的 CDN 链接上。

另一种方法是使用 NodeJS。为此,我们可以浏览到 NodeJS 网站 www.nodejs.org,下载适合我们平台的版本,然后在 NodeJS 命令提示符上运行以下命令:

npm install --save-dev qunitjs

我们甚至可以使用 Bower 来安装 QUnit;要做到这一点,我们首先需要安装 NodeJS,然后运行以下命令来安装 Bower:

npm install -g bower

一旦安装了 Bower,就可以用这个命令安装 QUnit:

bower install --save-dev qunit

在这个阶段,我们准备开始用 QUnit 创建我们的自动化测试。

注意

如果你想要真正地深入了解,你可以测试最新提交的 QUnit 版本——链接在 code.jquery.com/qunit/;需要注意的是这不适用于生产环境!

创建一个简单的演示

现在我们已经安装了 QUnit,我们准备运行一个简单的测试。为了证明它的工作原理,我们将修改一个简单的演示,以便测试文本框中的字母数,并指示它是否超过或低于给定的限制,如下所示:

  1. 我们将首先从附带本书的代码下载中提取我们演示所需的代码副本;请提取 qunit.html 文件以及 cssjs 文件夹,并将它们存储在项目区域中:创建一个简单的演示

    提示

    不必担心 node_modules 文件夹的存在;在后面的章节中,当安装 Node 时,我们将创建它。

  2. 现在,我们需要修改我们的测试标记,所以请打开 qunit.html,然后按照指示进行修改:

    <!DOCTYPE html>
    <html>
      <head>
        <title>Testing jQuery With QUnit</title>
        <meta charset="utf-8">
        <link rel="stylesheet" href="css/qunit.css" />
     <link rel="stylesheet" href="css/qunittest.css" />
     <script src="img/jquery.min.js"></script>
     <script src="img/qunit.js"></script>
     <script src="img/qunittest.js"></script>
      </head>
      <body>
        <form id="form1">
          <input type="text" id="textLength">
          <span id="results"></span>
     <div id="qunit"></div>
     <div id="qunit-fixture"></div>
        </form>
      </body>
    </html>
    
  3. 接下来,打开你喜欢的文本编辑器,添加以下代码,并将其保存为 js 文件夹中的 qunittest.js。第一个代码块对文本字段的长度进行检查,并显示计数;如果超过了规定的八个字符的长度,则将该计数的背景设置为红色:

    $(document).ready(function() {
      var txt = $("input[id$=textLength]");
      var span = $("#results");
      $(txt).keyup(function() {
        var length = $(txt).val().length;
        $(span).text(length + " characters long");
        $(span).css("background-color", length >= 8 ? "#FF0000" : "#00FF00");
      });
    
  4. 在上一个代码块的下方立即添加以下代码行;这将调用 QUnit 来测试我们的文本字段的长度,并在字母计数下方显示结果:

      $(txt).val("Hello World!");
      QUnit.test("Number of characters in text field is 8 or more", function(assert) {
        $(txt).trigger("keyup");
        assert.ok($(txt).val().length >= 8, "There are " + $(txt).val().length + " characters.");
      });
    });
    
  5. 文件已就绪,我们可以运行测试了;请在浏览器中运行 qunit.html。如果一切顺利,我们应该看到我们测试的结果,这种情况下将显示一个通过:创建一个简单的演示

  6. 在现实生活中,我们进行的并非每次测试都会成功;如果我们没有提供正确的数值或执行了给出意外结果的计算,就会出现测试失败的情况。要查看在 QUnit 中的效果,请按照以下步骤将这些代码添加到 qunittest.js 文件中,如下所示:

      assert.ok($(txt).val().length >= 8, "There are " + $(txt).val().length + " characters.");
    });
    
     $(txt).val("Hello World!");
     QUnit.test("Number of characters in text field is 8 or less", function(assert) {
     $(txt).trigger("keyup");
     assert.ok($(txt).val().length <= 8, "There are " + $(txt).val().length + " characters.");
      });
    
  7. 现在,刷新你的浏览器窗口;这一次,你应该看到测试已完成,但有一个失败,如下图所示:创建一个简单的演示

注意

在代码下载的 completed version 文件夹中有这个示例的已完成版本,它探讨了测试的结果。

尽管这被设计为一个简单的演示,但它仍然突出了创建简单测试并给出适当响应的简易性;让我们暂停片刻,考虑一下我们在这个练习中涵盖了什么。

每个测试的关键在于使用 assert.ok() 函数—它执行一个简单的布尔检查。在我们的例子中,我们检查文本长度是否为 8 个字符或更少,或者为 8 个字符或更多,并根据结果显示通过或失败。此外,我们可以要求 QUnit 显示标准文本,或者用个性化消息进行覆盖。这种方法应该足以开始对代码进行单元测试;随着时间的推移,如果需要,我们总是可以进一步开发测试。

这个库的美妙之处在于我们可以用它来使用 jQuery 或 JavaScript;本章中的示例基于使用前者,但 QUnit 足够灵活,可以用于后者,如果我们决定将来不再使用 jQuery。QUnit 是 jQuery 产品系列的一部分;与其他简单的测试库(如Junit)类似。

当我们利用 QUnit 的力量时,我们可以做大量事情——我们在这里看到的只是可能实现的表面一角。

注意

如果您想了解更多关于 QUnit 基础知识,那么我建议您参考Dmitry Sheiko的《Instant Testing with QUnit》,该书由 Packt Publishing 出版。也有很多在线教程可供参考;您可以从这个链接开始:code.tutsplus.com/tutorials/how-to-test-your-javascript-code-with-QUnit--net-9077

作为可能性的一个示例,我们将专注于一个特定功能,这将帮助您进一步提高您的 jQuery 开发技能:不要每次都手动运行测试,而是完全自动化它们,让它们自动运行。

使用 QUnit 进行自动化测试

慢着,QUnit 不是已经为我们自动运行了这些测试吗?

答案是肯定和否定。QUnit 自动化了测试,但只到一定程度;我们每次都需要手动运行一组测试。虽然这很有用,但你知道吗?我有点懒,也没有时间或意愿一直手动运行测试,我相信您也是如此。我们可以做得更好;可以使用 NodeJS/Grunt 和 PhantomJS 自动化我们的测试。

当然,设置需要一些努力,但当任何已识别的内容发生变化时,自动运行测试的节省时间是值得的。

使用 QUnit 进行自动化测试

让我们来看看自动化我们测试所涉及的内容:

  1. 我们将从安装 NodeJS 开始。要做到这一点,请浏览至nodejs.org/,并下载适合您系统的二进制文件;它适用于 Windows、Mac OS 和 Linux。

  2. 安装完成后,打开 NodeJS 命令提示符,然后切换到我们在创建一个简单演示中创建的qunit文件夹。

  3. 在命令提示符下,输入以下命令:

    npm install –g grunt-cli
    
    

    NodeJS 需要创建两个文件才能正确运行;它们是package.jsongruntfile.js。让我们现在就去创建它们。

  4. 切换到您选择的普通文本编辑器,然后在一个新文件中添加以下代码,将其保存为package.json

    {
      "name": "projectName",
      "version": "1.0.0",
      "devDependencies": {
        "grunt": "~0.4.1",
        "grunt-contrib-QUnit": ">=0.2.1",
        "grunt-contrib-watch": ">=0.3.1"
      }
    }
    
  5. 切换到 NodeJS 命令提示符,然后输入以下内容:

    npm install
    
    
  6. 在一个单独的文件中,添加以下代码并将其保存为gruntfile.js

    module.exports = function(grunt) {
      grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
    
        QUnit: {
          all: ['tests/*.html']
        },
        watch: {
          files: ['tests/js/*.js', 'tests/*.html'],
          tasks: ['QUnit']
        }
      });
    
      grunt.loadNpmTasks('grunt-contrib-watch');
      grunt.loadNpmTasks('grunt-contrib-QUnit');
      grunt.registerTask('default', ['QUnit, watch']);
    };
    
  7. 再次切换到 NodeJS 命令提示符,然后输入以下内容:

    npm install –g phantomjs
    
    
  8. 如果一切顺利,我们应该看到类似以下截图的内容出现:使用 QUnit 自动化测试

  9. 现在让我们启动 Grunt 并设置它监视代码的任何更改;要做到这一点,请在 NodeJS 命令提示符中运行以下命令:

    grunt watch
    
    
  10. 打开我们在本章前面创建的 qunittest.js 的副本,然后保存文件 —— 我知道这听起来有点疯狂,但这是触发 Grunt 进程所必需的。

  11. 如果一切顺利,我们应该在 NodeJS 窗口中看到这个结果出现:使用 QUnit 自动化测试

  12. 回滚到 qunittest.js,然后按照这里所示更改此行:

    assert.ok($(txt).val().length <= 8, "There are " + $(txt).val().length + " characters.");
    
  13. 保存文件,然后观察 Grunt 窗口,现在应该指示测试失败:使用 QUnit 自动化测试

让我们转变方向,继续前进;尽管我们没有深入讨论过如何使用 QUnit,但在使用 QUnit 时,尽可能遵循最佳实践仍然很重要。让我们花一点时间考虑一些这些最佳实践,以便了解它们如何提高我们的编码技能。

探索使用 QUnit 时的最佳实践

任何开发者的目标应该是在可能的情况下遵循最佳实践;但实际情况并非总是如此,因此在必要时学会在什么情况下妥协是很重要的。假设这种情况不会经常发生,那么在使用 QUnit 时,我们可以尝试遵循一些最佳实践指南:

  • 使每个测试相互独立:我们运行的每个测试都应该只测试一个特定的行为;如果我们在多个测试中测试相同的行为,那么如果行为需要更改,我们就必须更改所有的测试。

  • 不要做不必要的断言:问问自己这个问题,“我们要测试什么行为?” 单元测试应该是对某个行为应该如何工作的设计草图,而不是详细描述代码发生的每件事情。尽可能地在每个测试中保持一个断言;如果我们的代码中已经在其他地方测试过某个断言,那么运行测试就没有意义了。

  • 一次只测试一个代码单元:您的代码的架构设计必须支持独立测试单元(即类或非常小的类组),而不是将它们链接在一起。如果不这样做,您就会面临大量重叠的风险,这将导致代码的其他地方发生故障。如果您的应用程序或站点的设计不允许这样做,那么您的代码质量将会受到影响;可能需要使用控制反转IoC)来测试您的工作。

    通常的做法是让自定义代码调用通用的、可重用的库(例如 QUnit);IoC 将这个过程反转,以便在这种情况下,测试由 QUnit 调用我们的自定义代码执行。

  • 模拟所有外部服务和状态数据: 单元测试的关键部分是尽可能减少外部服务对你的代码的影响——这些服务的行为可能会与你的测试重叠,并影响结果。

  • 避免模拟太多的对象或状态数据: 如果你有任何控制应用程序或站点状态的数据,请尝试将任何模拟数据保持在低于 5%的水平;任何更高的数值都会使你的测试不太可靠。在运行连续测试之前,将它们重置回一个已知的值也是明智的,因为不同的测试可能会影响其他测试的这些值。如果你发现你必须按特定顺序运行测试,或者你对活动数据库或网络连接有依赖,那么你的设计或代码就不正确,你应该重新审视两者,理解为什么以及如何去除这些依赖关系。

  • 避免不必要的前提条件: 避免在许多不相关的测试的开头运行常见的设置代码。这会使你的测试变得混乱,因为不清楚你的测试依赖于哪些假设,并且表明你并不只是在测试单个单元。关键是创造正确的条件,即使这可能很困难——诀窍在于尽可能简单地保持它们。

  • 不要对配置设置进行单元测试: 在运行单元测试时检查你的配置设置没有任何好处;这可能会导致重复的代码,这是没有必要的。

  • 不要指定你的实现方式 - 而是指定结果: 单元测试旨在专注于结果,而不是实现方式——你的函数是否产生了你期望的结果?以以下代码片段为例:

    test("adds user in memory", function()  {
      var userMgr — makeUserMgr();
      userMgr.addUser("user", 'pass");
      equal (userMgr. —internalUsersCØ) . name , "user")
      equal (userMgr. —internalUsersCØ) . pass , "pass")
    });
    

    这看起来完全合理,对吧?如果不是因为它专注于代码是如何实现的,而不是结果,那就是完全有效的。

    测试代码的更好方式是使用这种方法:

    test( "adds user in memory", function() var userMgr = makeUserMgr(); userMgr.addUser("user", "pass"); ok(userMgr. loginUser("user" , "pass"));
    });
    

    在这个例子中,我们不关注获得结果的路线,而是关注结果本身;它是否产生了我们需要看到的内容?

  • 清晰且一致地命名你的单元测试: 一个成功的单元测试将清晰地表明其目的;一个有用的命名测试的方法是使用我称之为SSR原则,即主题、场景和结果。这意味着我们可以确定正在测试什么,测试应该何时运行,以及预期结果是什么。如果我们仅仅用主题来命名它,那么如果我们不知道我们试图维护什么,它就会变得难以维护!

这些提示只是浅尝辄止,作为良好实践应该遵循的内容;要进行更深入的讨论,值得阅读亚当·科拉瓦关于应用单元测试的文章,该文章位于www.parasoft.com/wp-content/uploads/pdf/unittestting.pdf。但要记住的关键点是要保持简单、逻辑,并且不要试图过度复杂化你的测试,否则它们将变得毫无意义!

总结

我们现在已经到了本章的结尾;虽然篇幅不长,但涵盖了一些关于单元测试实践的有用观点,以及我们如何通过自动化测试来节省时间和精力。让我们快速回顾一下我们学到的内容。

我们开始时简要回顾了 QUnit 的原则以及如何安装它;我们简要介绍了最流行的方法,但也介绍了如何使用 CDN 和 Bower 在我们的代码中使用库。

接下来我们来看一些基本的测试示例;虽然这些示例非常简单,但它们突显了我们在单元测试中应该遵循的原则。这些原则在使用 QUnit 进行单元测试时进一步探讨。

我们现在已经到了本书的结尾。希望你喜欢我们在掌握 jQuery中的旅程,并且发现这不仅仅是关于编写代码,还涉及一些更加软性的话题,这些话题将有助于提高你作为 jQuery 开发者的技能。

posted @ 2024-05-19 20:13  绝不原创的飞龙  阅读(104)  评论(0)    收藏  举报