PHP-和-jQuery-高级教程-全-

PHP 和 jQuery 高级教程(全)

原文:Pro PHP and jQuery

协议:CC BY-NC-SA 4.0

一、jQuery 简介

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​1) contains supplementary material, which is available to authorized users.

为了全面理解 jQuery 及其在现代 web 编程中的应用,有必要花点时间回顾一下 jQuery 的起源,它的构建是为了满足什么需求,以及在 jQuery 出现之前 JavaScript 编程是什么样子的。

在这一章中,您将了解 JavaScript 库和它们寻求满足的需求,以及为什么 jQuery 是大多数 web 开发人员的首选库。您还将学习 jQuery 的基础知识,包括如何在您的应用中使用这个库,以及 jQuery 的核心——它强大的选择器引擎——是如何工作的。

选择 jQuery 而不是 JavaScript

JavaScript 语言在软件开发社区中有着非常复杂的名声。许多语法在表面上类似于我们熟悉的语言,如 C 或 Java。但是 JavaScript 代码的语义可能非常不同,这往往会让外行感到沮丧。(著名的软件架构师道格拉斯·克洛克福特写了很多关于这方面的文章,如果你想更深入地了解,当然值得在网上查找他的资料。)

浏览器给 web 开发过程增加了另一层甚至更严重的复杂性。不同的浏览器提供了不同的 JavaScript 解释器实现。而且,您实际上无法控制最终用户将运行的浏览器,也无法控制它对您所依赖的功能的支持。但是情况并不像看上去的那么糟糕;web 开发社区已经站出来提供帮助。

了解 JavaScript 库

与 JavaScript 相关的陡峭的学习曲线和浏览器支持问题多年来一直是开发人员的痛处,随着挫折的增加,一些雄心勃勃的开发人员开始构建 JavaScript 库,也称为 JavaScript 框架。

这些库旨在简化 JavaScript 的使用,并通过创建易于使用的控制函数来消除日常 JavaScript 任务中的一些繁重工作,使新老开发人员更容易使用 JavaScript 的强大功能。库在 AJAX 领域特别有用(这个术语最初源于异步 JavaScript 和 XML)。正如您将在后面看到的,AJAX 是通过异步执行对服务器的请求(通常用户甚至不会注意到)来提高 web 应用响应能力的关键。

JavaScript 库为常见任务提供了更简单的语法,这为开发人员带来了更快的工作流程,为初学者带来了更轻松的学习曲线。它们还通过在其内置方法中为您进行所有兼容性检查,消除了编写跨浏览器 JavaScript 代码时的一些麻烦,这在编写代码时可以节省大量时间。

Note

使用 jQuery 的 AJAX 工具和直接的 JavaScript 方法之间的区别将在第二章中探讨。

有很多 JavaScript 库可用。目前使用的几种最流行的是 Prototype ( www.prototypejs.org )、MooTools ( http://mootools.net )、Yahoo!UI 库( http://developer.yahoo.com/yui )、AngularJS ( https://angularjs.org/ )、Dojo ( https://dojotoolkit.org/ ),等等。它们中的许多为各种目的提供了非常不同的功能,但是我们将关注最流行的库:jQuery,它专门用于促进与 web 浏览器最常见的交互。

了解 jQuery 的好处

每个 JavaScript 框架都有自己的好处。jQuery 也不例外,它提供了以下好处:

  • 小文件大小(从版本 2.1.4 开始大约 80KB)
  • 极其简单的语法
  • 可链接方法
  • 用于扩展框架的简单插件架构
  • 一个巨大的在线社区
  • http://api.jquery.com 大文档
  • 用于增加功能的可选 jQuery 扩展,如 jQueryUI

了解 jQuery 的历史

jQuery 是开发者 John Resig 的智慧结晶,于 2006 年初在纽约的 BarCamp 上首次发布(关于 BarCamp 的更多信息,请参见 http://barcamp.org )。Resig 在他的网站上提到,他创建 jQuery 是因为他对当前可用的库不满意,并认为可以通过减少“语法错误”并为常见操作添加特定控件( http://ejohn.org/blog/selectors-in-javascript/ )来极大地改善这些库。

jQuery 在开发社区大受欢迎,并迅速获得了发展势头。其他开发人员开始帮助改进这个库,最终在 2006 年 8 月 26 日发布了第一个稳定的 jQuery 版本 1.0。

从那以后,jQuery 已经发展到了 2.1.4 版本(在撰写本文时),并且已经看到了来自开发社区的大量插件的涌入。插件是 jQuery 的扩展,不是核心库的一部分。在第十章中,你会学到更多关于(和构建)jQuery 插件的知识。

设置测试环境

因为理解一门新语言没有比动手更好的方法,所以您需要一个测试环境来尝试一些 jQuery 入门练习。幸运的是,设置这个测试环境是一个简单的两步过程:安装 Firefox,然后安装 Firebug。

在本书中,所有的练习都将假设您正在使用 Firefox 浏览器和 Firebug 插件,因为它有出色的 JavaScript 测试控制台。

安装 Firefox

要让 Firefox 在你的电脑上运行,导航到 http://firefox.com 并下载最新版本的 Firefox(在撰写本文时为 42.0 版本),可从 www.mozilla.org/en-US/firefox/products/ .下载

安装萤火虫

要安装 Firebug,使用 Firefox 导航到 http://getfirebug.com/downloads ,点击最新版本的下载链接(撰写本文时为 2.0.13)。这将带您进入 Firebug 的 Firefox 附加组件目录条目。在那里,点击“添加到 Firefox”按钮,这将在浏览器中弹出安装对话框(见图 1-1 )。单击“安装”按钮,等待附件安装。

A978-1-4842-1230-1_1_Fig1_HTML.jpg

图 1-1。

The installation dialog for Firebug

安装程序完成后,状态栏中会出现一个类似闪电的图标。单击该图标将调出 Firebug 控件,从控制台开始(参见图 1-2 )。

A978-1-4842-1230-1_1_Fig2_HTML.jpg

图 1-2。

The Firebug add-on opens to the console panel Note

Firebug 的用处远不止于 JavaScript 调试。对于任何 web 开发者来说,这都是一个无价之宝。欲了解更多信息,请访问 http://getfirebug.com

Setting up a local testing environment

尽管本书中介绍的练习并不要求设置本地测试环境,但是这样做是一个很好的开发实践。本地测试允许更快、更安全的开发,并且通常比尝试在远程服务器上开发更容易。

安装 XAMPP

要快速方便地在您的计算机上设置本地开发环境,请按照以下步骤下载并安装 XAMPP。

Visit www.apachefriends.org/en/xampp.html , and download the latest version of XAMPP for your operating system (7.0.1 as of this writing). Throughout this book, PHP version 7.x will be assumed; this will become important starting with Chapter 3.   Open the downloaded file. For a PC, run the EXE file, select a directory, and install. For a Mac, mount the DMG, and drag the XAMPP folder into your Applications folder.   Open the XAMPP Control Panel in the XAMPP folder, and start Apache.   Navigate to http://localhost/ to ensure than XAMPP is working. If so, the XAMPP home page will let you know.

除了 XAMPP 的 Windows 和 Mac 版本,还有 Linux 和 Solaris 的发行版。每个操作系统在安装 XAMPP 时都有不同之处,所以请参考帮助部分,以获得在您的机器上运行本地测试环境的更多信息。

在网页中包含 jQuery

要在项目中使用 jQuery,需要将该库加载到 HTML 文档中,以便脚本可以访问该库的方法。如果没有首先加载这个库,任何使用 jQuery 语法的脚本都可能导致 JavaScript 错误。幸运的是,加载 jQuery 非常简单,开发人员可以通过几个选项来完成。

包括 jQuery 库的下载副本

将 jQuery 包含在项目中的第一个选项是在项目的文件结构中保存该库的副本,并像包含任何其他 JavaScript 文件一样包含它:

<script src="js/jquery-2.1.4.min.js"></script>

包括 jQuery 库的远程托管副本

第二种选择是包含 Google 托管的 jQuery 库的副本。这样做是希望您的网站的访问者将拥有一个已经从另一个网站缓存的包含相同文件的库的副本,这减少了网站用户的加载时间。

远程副本就像下载的副本一样包含在内:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js

</script>

设置测试文件

既然您的测试环境已经设置好了,那么在您的 XAMPP 安装中的htdocs文件夹中创建一个名为testing的新文件夹,并在其中创建一个名为index.html的新文件。在您选择的编辑器中,插入以下 HTML 标记:

<!DOCTYPE html>

<html>

<head>

<title>Testing jQuery</title>

</head>

<body>

<p>Hello World!</p>

<p class="foo">Another paragraph, but this one has a class.</p>

<p><span>This is a span inside a paragraph.</span></p>

<p id="bar">Paragraph with an id.

<span class="foo">And this sentence is in a span.</span>

</p>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js

</script>

</body>

</html>

Note

在结束 body 标记(</body>)之前加载 JavaScript 是为了防止脚本阻止其他页面元素(比如图像)的加载。这样做还可以防止 JavaScript 在元素完全加载到页面之前运行,这可能会导致意外行为或 JavaScript 错误。

保存该文件并在 Firefox 中导航至http://localhost/testing/(参见图 1-3 )。

A978-1-4842-1230-1_1_Fig3_HTML.jpg

图 1-3。

The test file loaded in Firefox

您将使用这个文件来熟悉 jQuery 的基本操作。

jQuery 函数简介

jQuery 的核心是 jQuery 函数。这个函数是 jQuery 的核心和灵魂,在每个实现 jQuery 的实例中都要用到。在 jQuery 的大多数实现中,使用快捷方式$()而不是jQuery()来保持代码简洁。

我们不会深入研究这个函数的编程理论,但基本上它创建了一个 jQuery 对象,并计算作为其参数传递的表达式。然后,它决定应该如何响应,并相应地修改自己。

Caution

某些其他 JavaScript 库也使用$()函数,因此当试图同时使用多个库时可能会发生冲突。jQuery 通过jQuery.noConflict()为这种情况提供了一个解决方案。详见 http://docs.jquery.com/Core/jQuery.noConflict

使用 CSS 语法选择 DOM 元素

jQuery 中的一切都围绕着它极其强大的选择器引擎。本章的其余部分将教您使用 jQuery 从文档对象模型(DOM)中选择元素的不同方法。

Note

DOM 是组成 HTML、XHTML 和 XML 文档的对象和节点的集合。它独立于平台和语言;这实质上意味着开发人员可以使用多种编程语言(比如 JavaScript)在多个平台(比如 web 浏览器)上访问和修改 DOM 信息,而不会出现兼容性问题。

jQuery 最强大和最吸引人的特性之一是开发人员能够轻松地在 DOM 中选择元素。伪 CSS 选择器 1 的使用为 jQuery 增添了令人难以置信的强大功能。伪 CSS 允许开发者在他的 HTML 中瞄准特定的元素实例。由于几乎相同的语法,这对任何以前有 CSS 经验的人都特别有帮助。本质上,使用与设置样式规则相同的 CSS 语法,您可以通过以下方式选择元素:

  • 基本选择器
  • 层次选择器
  • 过滤
    • 基本过滤器
    • 内容过滤器
    • 可见性过滤器
    • 属性过滤器
    • 子过滤器
  • 表单过滤器

基本选择器

基本选择器允许开发人员通过标记类型、类名、ID 或它们的任意组合来选择元素。在查看http://localhost/testing/的同时,启动 Firebug 对话框,点击控制台选项卡(参见图 1-4 )。如果控制台面板被禁用,请单击控制台选项卡,然后选择启用。本章中的所有示例都将使用该控制台。

A978-1-4842-1230-1_1_Fig4_HTML.jpg

图 1-4。

The Firebug console after executing a command Note

如果您熟悉 CSS,您将能够浏览这一部分,因为选择器的行为与它们的 CSS 对应物相同。

按标记类型选择元素

要通过标记类型选择元素,只需使用标记的名称(如pdivspan)作为选择器:

element

要选择测试文档中的所有段落(<p>)标签,请在控制台底部输入以下代码片段:

$("p");

按回车键,代码将执行。以下结果将显示在控制台上(参见图 1-4 ):

> $("p");

Object[``p, p.foo, p, p#bar

第一行显示执行的命令,第二行显示代码返回的内容。您的测试文档中有四个段落标记:两个没有 class 或 ID 属性,一个有 class foo,一个有 ID bar(您将在接下来的小节中学习这个语法)。当您将标记名传递给 jQuery 函数时,所有实例都会被找到并添加到 jQuery 对象中。

按类名选择标签

正如您可以按标记类型选择一样,您也可以按元素的指定类来选择元素。其语法是在类名前加一个句点(.):

.class

通过在控制台中执行以下代码片段,选择具有类foo的所有元素:

$(".foo");

执行后,控制台中将显示以下内容:

> $(".foo");

Object[``p.foo, span.foo

paragraph 标签和 span 都被返回,因为它们都有类foo

按 ID 选择元素

要通过 ID 属性选择元素,可以使用前面带有散列符号(#)的 ID 的 CSS 语法:

#id

将 ID 为bar的所有元素与以下内容进行匹配:

$("#bar");

您的文档中只有一个段落的 ID 为“bar ”,您可以在结果中看到:

> $("#bar");

Object[``p#bar

组合选择器以实现更精确的选择

在某些情况下,可能需要只隔离对应于某个类的某些标签,这很容易通过在选择器中组合标签类型和类来实现。

在控制台中输入以下内容,仅选择类别为foo的段落标记:

$("p.foo");

控制台中的结果确认 span 被忽略,即使它有类foo:

> $("p.foo");

Object[p.foo]

使用多个选择器

如果需要访问多个元素,可以使用多个选择器一次访问所有这些元素。例如,如果您想要选择类别为foo的任何段落标签或 ID 为bar的任何元素,您可以使用以下代码:

$("p.foo,#bar");

这将返回至少与字符串中指定的一个选择器匹配的元素:

> $("p.foo,#bar");

Object[``p.foo, p#bar

层次选择器

有时候,仅仅通过元素、类或 ID 进行选择是不够的。有些时候,您需要访问包含在另一个元素中、旁边或后面的元素,比如从除了刚才单击的菜单项之外的所有菜单项中删除一个活动类,从选定的无序列表中取出所有列表项,或者在选择一个表单项时更改包装元素的属性。

选择后代元素

选择后代元素(包含在其他元素中的元素)是使用祖先选择器(后跟一个空格)和后代选择器来完成的,如下所示:

ancestor descendent

要在测试文档中选择后代跨度,请在 Firebug 控制台中执行以下命令:

$("body span");

这将查找包含在文档正文标签(<body>)内的所有范围,即使这些范围也在段落标签内:

> $("body span");

Object[``span, span.foo

选择子元素

子元素是后代选择器的一种更具体的样式。只有下一级元素才被考虑进行匹配。要选择子元素,请使用父元素后跟一个大于号(>),再后跟要匹配的子元素:

parent>child

在您的测试文件中,通过在控制台中输入以下命令,尝试选择 body 元素的子元素:

$("body>span");

因为 body 元素中没有直接包含的跨度,所以控制台将输出以下内容:

> $("body>span");

Object[ ]

接下来,过滤作为段落元素的直接子元素的所有 span 元素:

$("p>span");

结果输出如下所示:

> $("p>span");

Object[``span, span.foo

选择下一个元素

有时候在脚本中,你需要选择 DOM 中的下一个元素。这是通过为开始元素提供一个标识符来实现的(这里任何选择器模式都适用),后跟一个加号(+),再跟一个匹配下一个实例的选择器,如下所示:

start+next

通过键入以下命令,在控制台中尝试这样做:

$(".foo+p");

只有一个类为foo的元素,所以只返回一个段落元素:

> $('.foo+p');

Object[``p

接下来,使用更一般的查询,并在任何段落元素之后选择下一个段落元素:

$('p+p');

标记中有四个段落,除了最后一个以外,所有段落都有一个next段落,因此控制台将在结果中显示三个元素:

> $('p+p');

Object[``p.foo, p, p#bar

这个结果集是 HTML 标记中的第二、第三和第四段。

选择同级元素

兄弟元素是同一元素中包含的任何元素。选择同级元素的工作方式类似于选择下一个元素,只是同级选择器将匹配起始元素之后的所有同级元素,而不仅仅是下一个元素。

要选择同级元素,请使用起始元素选择器,后跟一个等价符号(),以及匹配同级元素的选择器,如下所示:

start∼siblings

要将段落之后的所有同级与类foo匹配,请在控制台中执行以下命令:

$(".foo∼p");

结果集将如下所示:

> $(".foo∼p");

Object[``p, p#bar

基本过滤器

过滤器是访问 DOM 中元素的另一种非常强大的方法。您可以根据元素的位置、当前状态或其他变量来查找元素,而不是依赖于元素类型、类或 id。

过滤器的基本语法是冒号(:)后跟过滤器名称:

:filter

在某些过滤器中,参数可以用括号传递:

:filter(parameter)

最常见和最有用的过滤器将在接下来的几节中介绍。

Note

为了快速进入实际开发,这里没有涵盖所有可用的过滤器。有关可用过滤器的完整列表,请参见 jQuery 文档。

选择第一个或最后一个元素

过滤器最常见的用途之一是确定一个元素是集合中的第一个还是最后一个元素。使用过滤器,找到第一个或最后一个元素非常简单。您只需将过滤器:first:last附加到任何选择器上,就像这样:

$("p:last");

在控制台中执行时,将返回以下内容:

> $("p:last");

Object[``p#bar

选择与选择器不匹配的元素

如果您需要找到所有不匹配选择器的元素,:not()过滤器是最简单的方法。将这个过滤器和一个选择器作为它的参数添加到您的选择器中,结果集将返回匹配原始选择器的任何元素,但不包括作为参数传递给:not()的选择器。

例如,

$("p:not(.foo)");

将返回以下结果集:

> $("p:not(.foo)");

Object[``p, p, p#bar

选择偶数或奇数元素

:first:last类似,:even:odd过滤器在语法上很简单,并且分别返回您可能期望的结果集的偶数或奇数元素:

$("p:odd");

在控制台中执行前面一行将产生以下输出:

> $("p:odd");

Object [``p.foo, p#bar

按索引选择元素

如果您需要通过索引获取一个特定的元素,:eq()过滤器允许您通过传递一个索引作为过滤器的参数来指定需要哪个元素:

$("p:eq(3)");

这将输出以下内容:

> $("p:eq(3)");¸

Object[``p#bar

Note

一个元素的索引是指它在集合中其他元素中的位置。编程中的计数从零(0)开始,因此第一个元素在索引0处,第二个元素在索引1处,依此类推。

内容过滤器

过滤器也可用于根据内容选择元素。这些范围可以从包含某些文本到包围给定的元素。

选择包含特定文本的元素

要仅选择包含特定文本的元素,使用:contains()过滤器,其中要匹配的文本作为参数传递给过滤器:

$("p:contains(Another)");

在控制台中执行时,前面的行将返回以下内容:

> $("p:contains(Another)");

Object[``p.foo

Note

:contains()过滤器区分大小写,这意味着匹配文本时大小写很重要。一个不区分大小写的过滤器版本已经被开发社区的成员添加到 API 文档的:contains()条目的注释中。关于这个过滤器的更多信息,请参见 http://api.jquery.com/contains-selector

选择包含特定元素的元素

如果您只需要选择包含另一个元素的元素,您可以使用:has()过滤器。这类似于:contains(),除了它接受一个元素名而不是一串文本:

$("p:has(span)");

在控制台中执行时,会输出以下内容:

> $("p:has(span)");

Object[``p, p#bar

仅返回包含 span 元素的段落。

选择作为父元素的元素

:empty:parent相反,它只匹配包含子元素的元素,子元素可以是其他元素、文本或者两者都是。

使用以下选项选择所有父段落:

$("p:parent");

因为示例 HTML 文档中的所有段落都包含文本(在某些情况下还包含其他元素),所以所有段落都在结果集中返回:

> $("p:parent");

Object[``p, p.foo, p, p#bar

可见性过滤器

可见性过滤器:hidden:visible将分别选择隐藏和可见的元素。选择所有可见段落,如下所示:

$("p:visible");

因为 HTML 示例中的元素当前都没有隐藏,所以这将返回以下结果集:

> $("p:visible");

Object[``p, p.foo, p, p#bar

属性过滤器

元素属性也是选择元素的好方法。属性是元素中进一步定义它的任何东西(包括 class、href、ID 或 title 属性)。对于下面的例子,您将访问 class 属性。

Note

请记住,在生产脚本中尽可能使用 ID (#id和 class ( .class)选择器会更快(也更好);下面的例子只是为了演示过滤器的功能。

选择与属性和值匹配的元素

要匹配具有给定属性和值的元素,请用方括号([])将属性-值对括起来:

[attribute=value]

要选择 class 属性为foo的所有元素,请在控制台中执行以下命令:

$("[class=foo]");

这将返回以下内容:

> $("[class=foo]");

Object[``p.foo, span.foo

选择没有属性或与属性值不匹配的元素

相反,要选择不匹配属性-值对的元素,请在属性和值之间的等号前插入感叹号(!):

[attribute!=value]

通过运行以下命令,选择所有不包含类别foo的段落:

$("p[class!=foo]");

这将导致以下结果:

> $("p[class!=foo]");

Object``p, p, p#bar

子过滤器

子过滤器为:even:odd:eq()的使用增加了一种选择。主要区别在于这组过滤器在1而不是0开始分度(像:eq()一样)。

通过索引或等式选择偶数或奇数参数或参数

作为一个更加通用的过滤器,:nth-child()提供了四个不同的选项作为选择元素时的参数:偶数、奇数、索引或等式。

像其他子过滤器一样,这个过滤器在1而不是0开始索引,所以第一个元素在索引1,第二个元素在2等等。

使用:odd,结果集包含类别为foo且 ID 为foo的段落;使用:nth-child()选择奇数段落,通过执行以下命令查看过滤器处理方式的差异:

$("p:nth-child(odd)");

控制台中显示的结果如下:

> $("p:nth-child(odd)");

Object[``p, p

虽然这个输出看起来很奇怪,但是不匹配的结果是元素索引方式不同的结果。

选择第一个或最后一个子元素

虽然与:first:last非常相似,:first-childlast-child的不同之处在于返回的元素集可以包含多个匹配。例如,要查找段落元素的最后一个子元素,可以使用

$("p span:last");

这将在控制台中返回以下内容:

> $("p span:last");

Object[``span.foo

但是,如果您需要找到段落元素的最后一个子元素,您可以使用:last-child来代替:

$("p span:last-child");

这使用每个父对象作为引用,而不是整个 DOM,所以结果是不同的:

> $("p span:last-child");

Object[``span, span.foo

表单过滤器

如今,表单是网站的重要组成部分,它们的主要作用激发了一组专门针对表单的过滤器。

因为您的 HTML 示例中没有任何表单元素,所以您需要为下面的示例添加一些新的标记。在index.html中,在最后一个段落标记和第一个脚本标记之间添加以下 HTML:

<form action="#" method="post">

<fieldset>

<legend>Sign Up Form</legend>

<label for="name">Name</label><br />

<input name="name" id="name" type="text" /><br />

<label for="password">Password</label><br />

<input name="password" id="password"

type="password" /><br /><br />

<label>

<input type="radio" name="loc" />

I’m on my computer

</label><br />

<label>

<input type="radio" name="loc" checked="checked" />

I’m on a shared computer

</label><br /><br />

<input type="submit" value="Log In" /><br />

<label>

<input type="checkbox" name="notify"

disabled="true" />

Keep me signed in on this computer

</label><br />

</fieldset>

</form>

保存后,在http://localhost/testing/在浏览器中重新加载页面以查看测试表单(见图 [1-5 )。

A978-1-4842-1230-1_1_Fig5_HTML.jpg

图 1-5。

The form as it appears after editing index.html Note

由于该页面包含一个密码字段,并且您正在普通(不安全)模式下运行,您可能会看到如图 1-5 所示的安全警告。将 apache 设置配置为安全运行会让您走得太远,但是生产应用当然应该在安全(https)模式下运行。更多信息见 https://httpd.apache.org/docs/2.4/ssl/ 的文档。

按表单元素类型匹配

最常见的特定于表单的过滤器只是匹配表单元素类型。可用的过滤器有:button:checkbox:file:image:input:password:radio:submit:text

要选择所有无线电输入,请使用以下代码:

$("input:radio");

这将在控制台中输出以下内容:

> $("input:radio");

Object[ input property value = "on" attribute value = "null", input property value = "on" attribute value = "null" ]

这些过滤器特别有用,因为所有提供的类型都是输入元素,所以如果没有这些过滤器,只匹配某些类型的输入会有点困难。

仅选择启用或禁用的表单元素

此外,使用:enabled:disabled可以使用过滤器来选择启用或禁用的表单元素。要选择所有禁用的表单元素,请使用以下代码:

$(":disabled");

这将在控制台中输出以下内容:

> $(":disabled");

Object[ input property value = "on" attribute value = "null" ]

:disabled过滤器禁用并返回“让我在这台计算机上保持登录”复选框。

选择选中或选定的表单元素

单选和复选框输入有一个checked状态,选择输入有一个selected状态。提供过滤器来分别使用:checked:selected检索处于任一状态的表单元素。

要在 HTML 示例中选择当前选中的单选按钮,请在控制台中执行以下代码:

$(":checked");

这将返回控制台中当前选择的无线电输入:

> $(":checked");

Object[ input property value = "on" attribute value = "null" ]

摘要

在这一章中,你学习了什么是 jQuery,为什么要创建它,以及它的基本工作原理。您还使用 XAMPP、Firefox 和 Firebug 插件设置了开发环境。

现在,您应该可以使用 jQuery 强大的选择器引擎轻松地从 DOM 中选择元素了。这一章有点枯燥,但是在开始编写更重的代码之前,充分理解 jQuery 是非常重要的。

在下一章中,您将学习如何使用 jQuery 的内置方法来遍历、访问和操作 DOM。

Footnotes 1

www.w3schools.com/CSS/css_pseudo_classes.asp

二、常见的 jQuery 动作和方法

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​2) contains supplementary material, which is available to authorized users.

现在您已经理解了元素选择的工作原理,您可以开始学习 jQuery 如何简化与 web 页面的交互的基础知识了。在本章中,您将接触到 jQuery 最常见和最有用的方面。

这一章读起来更像是一个参考,有时可能有点枯燥,但是阅读其中的例子绝对对你有好处。对这些方法如何工作以及它们做什么有一个基本的了解将会证明是无价的,因为你将在本书的后面开始构建示例项目。

理解 jQuery 脚本的基本行为

jQuery 最方便的特性之一是几乎所有的方法都是可链接的,这意味着方法可以一个接一个地执行。这就产生了清晰、简洁、易于理解的代码,比如

$('p')

.addClass('new-class')

.text("I’m a paragraph!")

.appendTo('body');

可链接的方法是可能的,因为每个方法在修改后都返回 jQuery 对象本身。起初,这个概念似乎很难理解,但是当你通过本章中的例子时,它会变得更加清晰。

理解 jQuery 方法

jQuery 试图简化一些常见的编程任务。一眼看去,它通过提供以下强大的工具简化了 JavaScript 开发:

  • 使用 CSS 语法的 DOM 元素选择(你在第一章中学到的)
  • DOM 的简单遍历和修改
  • 处理浏览器事件(如点击和鼠标悬停)的简单语法
  • 访问元素的所有属性,包括 CSS 和样式属性,以及修改它们的能力
  • 动画和其他效果
  • 简单的 AJAX 控件

Note

前面的列表只是 jQuery 特性和功能的一部分。随着您继续完成本书中的项目,将会探索其他有用的特性。如需完整的参考资料,请访问 http://api.jquery.com 的文档。

遍历 DOM 元素

jQuery 中的遍历是从一个 DOM 元素移动到另一个 DOM 元素的行为;遍历本质上是在初始选择完成后执行的另一种形式的过滤。这很有用,因为它允许开发人员完成一个动作,然后移动到 DOM 的另一部分,而不需要通过选择器执行另一次搜索。

它还帮助开发人员影响正被脚本操纵或利用的元素周围的元素。这包括向父元素添加一个类来指示活动,禁用所有不活动的表单元素,以及许多其他有用的任务。

Note

对于本章中的例子,您也将使用第一章中的 HTML 测试文件。如果您使用 XAMPP 进行本地测试,请将浏览器指向http://localhost/testing/来加载该文件。确保 Firebug 控制台打开并处于活动状态(参见第一章复习使用 Firebug 控制台)。

。eq()

如果一组元素需要缩小到只有一个由索引标识的元素,那么可以使用.eq()方法。该方法接受一个参数:所需元素的索引。对于.eq(),索引从0开始。

$("p").eq(1);

在 Firebug 控制台中执行时,会返回以下内容:

> $("p").eq(1);

Object[``p.foo

此外,可以向.eq()提供一个负数,以便从选择集的末尾向后计数(例如,传递-2将从该集中返回倒数第二个元素)。

若要通过从结果集的末尾向后计数来选择与前面的示例相同的段落,请使用以下代码:

$("p").eq(-3);

这将在控制台中返回相同的段落:

> $("p").eq(-3);

Object[``p.foo

。过滤器( )和。不是( )

要在一组元素中使用一个全新的选择器,.filter()方法很方便。它接受任何可以在 jQuery 函数中使用的选择器,但是它只适用于 jQuery 对象中包含的元素子集。

例如,要选择所有段落,然后过滤掉除类别为foo的段落之外的所有段落,请使用以下代码:

$("p").filter(".foo");

控制台中的结果如下所示:

> $("p").filter(".foo");

Object[``p.foo

.find()的逆运算是.not(),它将返回结果集中与给定选择器不匹配的所有元素。例如,要选择所有段落,然后将选择范围限制为不包含类别foo的段落,请使用以下代码:

$("p").not(".foo");

这将导致以下结果:

> $("p").not(".foo");

Object[``p, p, p#bar

。首先( )和。最后一个( )

.first().last()方法的工作方式分别与.eq(0).eq(-1)相同。要从页面上的一组所有段落中选择最后一个段落,请使用以下内容:

$("p").last();

这将导致以下结果:

> $("p").last();

Object[``p#bar

。有( )

要选择包含匹配特定模式的元素,可以使用.has()方法。例如,使用以下代码选择所有段落,并将结果过滤为仅包含 span 元素的段落:

$("p").has("span");

这将输出以下内容:

> $("p").has("span");

Object[``p, p#bar

。是( )

.is()方法与其他方法略有不同,因为它不返回 jQuery 对象。它评估结果集而不修改它,这使它非常适合用于回调函数或在成功执行函数或方法后执行的函数。

在本书后面的例子中,你会学到更多关于.is()的实际用法;现在,选择测试文档中的所有段落,然后检查是否有一个段落具有类foo:

$("p").is(".foo");

结果是一个布尔(truefalse)答案:

> $("p").is(".foo");

true

。切片( )

为了根据索引选择元素的子集,需要使用.slice()方法。它接受两个参数:第一个是生成子集的起始索引,第二个是可选的结束点。如果没有提供第二个参数,子集将继续,直到到达选择的结尾。

Note

第二个参数中传递的索引将不包括在结果集中。因此,如果你需要一个集合中的第二到第四个元素(索引13,你的参数需要是14

此外,与.eq()一样,可以使用负索引。这可以应用于起点和/或终点。

若要选择所有段落,然后将选择范围限制在第二和第三个段落,请使用以下代码:

$("p").slice(1,3);

控制台中的结果如下所示:

> $("p").slice(1,3);

Object[``p.foo, p

要从段落集中选择最后两个元素,请使用以下内容:

$("p").slice(-2);

这会产生以下结果:

> $("p").slice(-2);

Object[``p, p#bar

。儿童( )

经常需要在结果集中向下钻取以找到子元素。这是使用.children()方法完成的,该方法接受一个可选参数:一个匹配子元素的选择器。

要选择所有段落,然后更改选择以匹配段落的所有子元素,请执行以下代码:

$("p").children();

这将输出以下内容:

> $("p").children();

Object[``span, span.foo

如果您需要一组更具体的子元素,您可以向.children()方法传递一个可选的选择器。要选择所有段落,然后查找类别为foo的所有子段落,请使用以下命令:

$("p").children(".foo");

这会产生以下结果:

> $("p").children(".foo");

Object[``span.foo

。最近的( )

.closest()方法是一种在 DOM 树中查找元素的简单方法,DOM 树是元素的嵌套顺序(本例中的 DOM 树关系是 body 元素中段落内的跨度)。

例如,要查找与类foo的跨度最近的段落,请在控制台中运行以下代码片段:

$("span.foo").closest("p");

这将输出以下内容:

> $("span.foo").closest("p");

Object[``p#bar

。查找( )

类似于.children()方法,.find()方法匹配当前集合中元素的后代。.find().children()的主要区别在于.children()只检查 DOM 树中的下一层,而.find()不关心匹配的元素有多深。

为了进行演示,请选择 body 标记,然后使用以下代码查找任何包含的 span 元素:

$("body").find("span");

这导致返回两个跨度:

> $("body").find("span");

Object[``span, span.foo

但是,如果您使用.children()做同样的事情,将返回一个空的结果集:

> $("body").children("span");

Object[ ]

。下一个( ),。nextAll()和。nextUntil()

.next().nextAll().nextUntil()中提供了三种在集合中寻找下一个兄弟元素的有用方法。

.next()方法将为原始结果集中的每个元素在集合中查找下一个兄弟元素。要选择具有类foo的段落,然后遍历到下一个同级元素,请在控制台中执行以下代码:

$("p.foo").next();

这将生成以下输出:

> $("p.foo").next();

Object[``p

选择器也可以传递给.next(),这允许开发人员确定下一个兄弟元素的类型应该匹配:

$("p.foo").next("#bar");

这将返回一个空的结果集,因为下一个元素没有 IDbar:

> $("p.foo").next("#bar");

Object[ ]

因为.next()只返回一个元素,所以创建了一个伴生方法来返回所有下一个兄弟元素.nextAll()。要选择类别为foo的段落之后的所有段落,请使用以下代码:

$(".foo").nextAll("p");

这将返回以下结果:

> $(".foo").nextAll("p");

Object[``p, p#bar

Note

选择器在.nextAll()中是可选的,如同在.next()中一样。

选择下一个兄弟元素的第三种方法是.nextUntil()方法。顾名思义,该方法将返回所有后续元素,直到匹配一个选择器。需要注意的是,选择器匹配的元素不会包含在结果集中。

为了演示这一点,选择带有类别foo的段落,并使用带有选择器"#bar".nextUntil():

$(".foo").nextUntil("#bar");

结果集中只返回一个段落,不包括 ID 为bar的段落:

> $(".foo").nextUntil("#bar");

Object[``p

要包含 ID 为bar的段落,您需要查看紧随其后的元素,在本例中是 form 元素。使用更新后的代码再次尝试选择器:

$(".foo").nextUntil("form");

现在返回以下两段:

> $(".foo").nextUntil("form");

Object[``p, p#bar

。prev(),。prevAll()和。prevUntil()

.prev().prevAll().prevUntil()函数的工作方式与.next().nextAll().nextUntil()完全一样,只是它们查看的是前一个同级元素,而不是下一个同级元素:

> $("#bar").prev();

Object[``p

> $("#bar").prevAll();

Object[``p, p.foo, p

> $("#bar").prevUntil(".foo");

Object[``p

。兄弟姐妹( )

要选择元素两侧的同级元素,请使用.siblings()方法。它接受一个选择器作为参数来限制返回什么类型的元素。要将所有同级段落元素与 ID 为bar的段落匹配,请执行以下代码:

$("#bar").siblings("p");

结果将如下所示:

> $("#bar").siblings("p");

Object[``p, p.foo, p

。父级( )

方法返回当前选择的一组直接父元素。例如,要选择具有类foo的任何元素的所有父元素,请使用以下代码:

$(".foo").parent();

这将返回以下内容:

> $(".foo").parent();

Object[``body, p#bar

要仅匹配是类foo元素的父元素的段落元素,请将代码修改如下:

$(".foo").parent("p");

这缩小了结果集的范围:

> $(".foo").parent("p");

Object[``p#bar

。父母( )和。parentsUntil()

.parent()不同,.parents()将返回所有父元素,并传递一个可选的选择器来过滤结果。

若要在示例页的表单中选择复选框的所有父元素,请使用以下代码:

$(":checkbox").parents();

这将查找每个父元素,一直到html元素:

> $(":checkbox").parents();

Object[``label, fieldset, form #, body, html

要筛选结果以便只返回父表单元素,请按如下方式修改代码:

$(":checkbox").parents("form");

这仅返回父表单元素:

> $(":checkbox").parents("form");

Object[``form #

最后,要选择一系列父节点,直到一个选择器匹配,类似于.nextUntil().prevUntil(),使用.parentsUntil():

$(":checkbox").parentsUntil("form");

这将返回所有父元素,直到遇到 form 元素:

> $(":checkbox").parentsUntil("form");

Object[``label, fieldset

。添加( )

.add()方法是通用的,因此有点复杂。本质上,它允许您使用选择器或 HTML 字符串向现有的 jQuery 对象添加额外的元素。

要选择所有段落,然后将带有类别foo的范围添加到对象,请使用以下命令:

$("p").add("span.foo");

这将输出以下内容:

> $("p").add("span.foo");

Object[``p, p.foo, p, p#bar, span.foo

.add()方法还允许您动态创建元素,如下所示:

$("p").add('<span id="bat">This is a new span</span>');

执行前面的代码将输出以下内容:

> $("p").add('<span id="bat">This is a new span</span>');

Object[ p, p.foo, p, p#bar, span#bat ]

Note

请注意,控制台输出中的元素span#bat消失了。发生这种情况是因为,虽然该元素存在于 jQuery 对象中,但它没有被附加到 DOM 中,因此不会显示在页面上。在下一节“创建和插入 DOM 元素”中,您将了解如何向 DOM 添加新元素。

。andSelf()

如果您使用遍历方法,您可能还想保留原始的匹配元素集。c通过允许调用原始集合并将其附加到新集合来提供这种能力。

例如,要匹配所有段落元素,然后查找子跨度,请使用以下代码:

$("p").find("span");

这将返回文档中的范围,但是您已经丢失了段落:

> $("p").find("span");

Object``span, span.foo

为了保留段落并匹配跨度,在代码末尾添加一个对.andSelf()的调用:

$("p").find("span").andSelf();

这导致了期望的输出:

> $("p").find("span").andSelf();

Object[``p, p.foo, p, span, p#bar, span.foo

。内容()“

除了.contents()也返回文本节点之外,.contents()方法的工作方式与.children()方法类似,文本节点只是元素中包含的字符数据(元素显示的实际文本)。 [1

要查找类为foo的 span 的所有内容,请使用以下代码:

$("span.foo").contents();

这会产生以下输出:

> $("span.foo").contents();

Object``<TextNode textContent="And this sentence is in a span.">

。结束( )

在 jQuery 脚本中,有时您会发现有必要备份到 jQuery 对象中存储的最后一组元素。.end()方法正是这样做的:它将 jQuery 对象恢复到当前 jQuery 对象链中最后一个过滤动作之前的状态。

要选择所有段落,然后查找所有范围,原始段落集不再可用:

> $("p").find("span");

Object[``span, span.foo

要恢复到段落集,请将.end()添加到链中:

> $("p").find("span").end();

Object[``p, p.foo, p, p#bar

创建和插入 DOM 元素

您将学到的第一件事是如何创建新元素并将它们插入 DOM,而不是简单地从中选择元素。幸运的是,在 jQuery 中,这非常简单。

本书的这一部分开始使用更复杂的代码片段,因此需要对 Firebug 控制台进行一些小的调整。控制台右下方是一个圆形按钮,箭头向上(见图 [2-1 )。

A978-1-4842-1230-1_2_Fig1_HTML.jpg

图 2-1。

The button to activate the multiline console test area

点击此按钮激活多行测试区域,在这里您可以跨多行输入命令,使它们更容易阅读,并允许更高级的示例(见图 2-2 )。

A978-1-4842-1230-1_2_Fig2_HTML.jpg

图 2-2。

The multiline testing area (shown at the right-hand side of the console)

对于多行测试区域,您现在需要单击底部的 Run 按钮来执行代码。与单行测试控制台一样,按 Enter 键将会换行。

创建新的 DOM 元素

要创建新的 DOM 元素,jQuery 只需要创建标签。例如,要创建新的段落元素,请使用以下代码:

$("<p>");

要向该元素添加属性和文本,只需将它写成普通的 HTML:

$('<p class="bat">This is a new paragraph!</p>');

Note

前面的示例使用单引号而不是双引号将 HTML 字符串括起来。这对 jQuery 函数没有影响;它只是消除了对 class 属性中使用的双引号进行转义的需要(例如,class=\"bat\")。

您还可以通过将第二个参数作为 JavaScript Object Notation(JSON)2传递来为这个新元素添加属性:

$("<p>", {

"class":"bat",

"text":"This is a new paragraph!"

});

该代码会导致以下结果:

> $("<p>", { "class":"bat", "text":"This is a new paragraph!" });

Object``p.bat

因为这只是创建元素,它还没有附加到 DOM,因此在浏览器窗口中不可见。您将在下一节“向 DOM 插入新元素”中学习插入新元素

Note

最简单地说,JSON 是一个键-值对,其中键和值都用引号括起来,所有的键-值对都用逗号分隔,并用大括号({})括起来。JSON 数据的一个例子是{ "key":"value" }{ "key1":"value1", "key2":"value2" }

向 DOM 中插入新元素

现在您已经对如何创建新元素有了基本的了解,您可以开始学习如何将它们插入到 DOM 中。jQuery 提供了几种处理这种情况的方法,您将在本节中探讨这些方法。

这里需要注意的一点是,对 DOM 的修改是暂时的,这意味着一旦页面被刷新,所做的任何更改都将被重置回原来的 HTML 文档。这是因为 JavaScript 是一种客户端语言,这意味着它不是从服务器修改实际的文件,而是浏览器对文件的单独解释。

用 JavaScript 做的修改可以通过使用 AJAX 保存在服务器上(这一点你将在本章后面了解到),它允许 JavaScript 与服务器端语言如 PHP 接口。

Note

在执行完以下各节中的示例后,刷新页面,以便每个新示例都从示例 HTML 文件的新副本开始。

。append()和。前置( )

.append().prepend()函数将把作为参数传递的元素附加到它们所链接的 jQuery 对象上。唯一的区别是.append()在末尾附加元素,而.prepend()在开头附加元素。

内容将被追加或前置到匹配的元素中,这意味着如果您匹配页面上的所有段落并追加一个新句子“这是由 jQuery 添加的”,它将被追加到结束段落标记(</p>)中。

通过在控制台中输入以下代码来尝试一下:

$("p").append(" This was added by jQuery.");

执行代码会将这句话添加到结束段落标记内的每个段落的末尾。这一点很明显,因为文本不会跳到下一行,如果它在结束标记之外,就会跳到下一行。

Inspecting HTML using the element inspector in Firebug

使用 Firebug 提供的元素检查工具也可以看到这一点。在控制台的左上方附近,有一个看起来像鼠标光标的按钮(见图 [2-3 )。点按它以激活元素检查器。

A978-1-4842-1230-1_2_Fig3_HTML.jpg

图 2-3。

The button to activate the element inspector

检查器激活后,您可以将鼠标悬停在浏览器中的不同元素上,它们会以蓝色轮廓高亮显示。将鼠标悬停在您刚刚添加了文本的段落上,然后单击它。这将打开 Firebug 的 HTML 面板,当前元素被折叠并高亮显示,还有一个选项卡用于展开该元素(参见图 2-4 )。

A978-1-4842-1230-1_2_Fig4_HTML.jpg

图 2-4。

The collapsed element as displayed after hovering over and clicking it

单击选项卡展开元素,您可以看到包含在段落元素中的内容,包括附加的文本(参见图 2-5 )。

A978-1-4842-1230-1_2_Fig5_HTML.jpg

图 2-5。

The expanded element, including the dynamically added text

您可以在本书的其余练习中使用这种技术来查看内容和元素被添加到 DOM 中的位置。

使用.append().prepend(),还可以向 DOM 添加新元素。例如,要在浏览器页面顶部添加一个新段落,请使用以下代码在正文前添加一个新元素:

var para = $("<p>", {

"text":"I’m a new paragraph!",

"css":{"background":"yellow"}

});

$("body").prepend(para);

Note

此示例在将新元素添加到正文之前使用一个变量来存储新元素。这样做是为了增加脚本的可读性。在本书中,你会经常用到这种技巧。

在您的控制台中执行上述代码后,一个带有黄色背景的新段落会出现在浏览器窗口的顶部(参见图 2-6 )。

A978-1-4842-1230-1_2_Fig6_HTML.jpg

图 2-6。

The new paragraph as it appears after prepending it to the body element

。appendTo()和。前置到( )

在最后一个例子中,您必须创建一个元素,存储它,然后选择它所附加到的元素。这可能是一种迂回的方法,但幸运的是,jQuery 提供了.appendTo().prependTo(),它们链接到要追加的对象,并接受您希望追加的元素的选择器。

以最后一个例子为起点,使用.prependTo()将相同的段落元素添加到正文中,您的代码将如此简化:

$("<p>", {

"text":"I’m a new paragraph!",

"css":{"background":"yellow"}

})

.prependTo("body");

这产生了相同的结果,但代码片段更简洁。

。在( )和之后。之前( )

.after().before()方法类似于.append().prepend(),除了它们在元素之前或之后添加元素外部的内容,而不是在元素的开头或结尾添加内容。

要在具有类foo的段落之后添加新段落,请使用以下代码片段:

$("p.foo").after("<p>A new paragraph.</p>");

执行该代码会在类别为foo的段落下方插入一个新段落(见图 2-7 )。

A978-1-4842-1230-1_2_Fig7_HTML.jpg

图 2-7。

A new paragraph inserted after the paragraph with class foo

。insertAfter()和。insertBefore()

.appendTo().prependTo()允许更简洁地向 DOM 添加新元素一样,.insertAfter().insertBefore().after().before()提供了相同的选择。

要使用.insertAfter()重复上一节中的示例,将代码修改如下:

$("<p>", {

"text":"A new paragraph."

})

.insertAfter("p.foo");

这复制了之前的结果(参见图 2-7 )。

。换行( )

.wrap()方法允许开发人员快速、轻松地用一个或多个新元素封装现有元素。

.wrap()接受的参数可以是一个或多个标记的集合,用来包装所选的元素,也可以是一个回调函数来生成标记。

首先,使用下面的代码用一个strong标签包装示例文档中的所有跨度:

$("span").wrap("<strong />");

这导致两个跨度的文本变为粗体(见图 2-8 )。

A978-1-4842-1230-1_2_Fig8_HTML.jpg

图 2-8。

The spans appear bold after wrapping them with strong tags

包装元素使用的语法相对宽松,图 2-8 中显示的输出可以使用"<strong />""<strong>""<strong></strong>"来完成。

此外,通过将一组嵌套的标记传递给.wrap()方法,可以将多个标记包装在元素周围:

$("span").wrap("<strong><em></em></strong>");

执行前一行后,范围中的文本将显示为粗体和斜体(参见图 2-9 )。

A978-1-4842-1230-1_2_Fig9_HTML.jpg

图 2-9。

Span text appears bold and italicized after wrapping it with strong and em tags

要使用回调函数生成包装元素所需的 HTML 标记,必须从回调函数返回一个标记。例如,要在strong标签中用类foo包装所有跨度,在em标签中包装所有其他跨度,请执行以下代码:

$("span").wrap(function(){

return $(this).is(".foo") ? "<strong>" : "<em>";

});

执行这个代码片段后,浏览器用斜体显示一个 span,用粗体显示另一个 span(带有类foo的 span)(参见图 2-10 )。

A978-1-4842-1230-1_2_Fig10_HTML.jpg

图 2-10。

Use a callback function to conditionally wrap certain elements

。展开( )

.wrap().unwrap()的逆操作将删除给定元素周围的标签。它不接受任何参数;它只是找到直接的父元素并删除它。

要打开示例文件中的 span 元素,请执行以下代码:

$("span").unwrap();

这将删除父元素(但保持文本节点不变),从而改变布局(见图 2-11 )。

A978-1-4842-1230-1_2_Fig11_HTML.jpg

图 2-11。

After unwrapping the span elements, the document layout changes

。wrapAll()

如果整个元素集需要包装在一个新的标签中,那么使用.wrapAll()。它不是用新标签单独包装每个选定的元素,而是将所有选定的元素分组,并在整个组周围创建一个包装器。

要用黄色背景将div元素环绕在页面上的所有段落周围,请使用以下代码:

var div = $("<div>", {

"css":{"background-color":"yellow"}

});

$("p").wrapAll(div);

执行这段代码后,新的div就就位了,所有段落都出现在它的黄色背景中(见图 2-12 )。

A978-1-4842-1230-1_2_Fig12_HTML.jpg

图 2-12。

The yellow background shows the div successfully wrapped all paragraphs

关于.wrapAll()有一个重要的注意事项:它将移动 DOM 中的元素来对它们进行分组。为了演示这一点,使用.wrapAll()在文档中的所有跨度周围添加一个strong标签:

$("span").wrapAll("<strong />");

执行该命令后,注意文档中的第二个区域被移动到了第一个区域的旁边,因此它们可以被包含在同一个标签中(参见图 2-13 )。

A978-1-4842-1230-1_2_Fig13_HTML.jpg

图 2-13。

The spans are relocated to be next to one another so both can be wrapped

。wrapInner()

在某些情况下,最好包装元素的内容,而不是标签本身。一个很好的例子是将整个段落加粗:将strong标签放在段落周围不是有效的 HTML,因此不是一个理想的解决方案。幸运的是,jQuery 提供了.wrapInner(),它将元素中包含的所有内容包装在一个新标签中。

要将测试页面上段落中的所有文本变为斜体,请使用以下代码:

$("p").wrapInner("<em />");

执行后,页面上的所有文本都以斜体显示,标记被有效嵌套(见图 2-14 )。

A978-1-4842-1230-1_2_Fig14_HTML.jpg

图 2-14。

All text is italicized, and the em tags are inside the paragraph tags

。移除( )和。分离( )

为了从 DOM 中完全删除一个元素,使用了.remove().detach()方法。这两种方法都从 DOM 中删除选定的元素,但是.detach()方法保持元素的 jQuery 数据不变,这使得它非常适合元素将在某个时候重新附加到 DOM 的情况。

.remove().detach()都接受一个可选的选择器来过滤被删除的元素。在您的示例中,使用以下代码删除所有带有类别foo的段落:

$("p").remove(".foo");

当代码运行时,带有类foo的段落从视图中移除,不再是 DOM 的一部分。

为了演示.remove().detach()之间的区别,您必须向前跳一点,使用一种叫做.data()的方法,它允许开发人员在不添加额外标签或属性的情况下将信息附加到元素上。注意.data()将在下一节更全面地介绍。

首先,向 DOM 中的第一段添加一些数据。然后,添加数据后,使用.detach()从 DOM 中移除元素,重新附加它,并尝试读取数据:

$("p:first").data("test","This is some data.");

var p = $("p:first").detach();

console.log("Data stored: "+p.data("test"));

Note

您正在使用一个特定于 Firebug 的对象console及其.log()方法向 Firebug 控制台输出特定的信息。这对于调试特别有用,但是在项目上线之前需要删除它,以避免在没有安装 Firebug 的计算机上出现 JavaScript 错误。

运行这段代码后,.data()方法将一些信息附加到第一段,然后从 DOM 中移除并存储在一个变量中;然后脚本试图输出用.data()存储的信息的值。控制台将输出以下内容:

> console.log("Data stored: "+p.data("test"));

Data stored: This is some data.

现在,运行相同的测试,但是使用.remove()而不是.detach():

$("p:first").data("test","This is some data.");

var p = $("p:first").remove();

console.log("Data stored: "+p.data("test"));

输出显示删除元素时数据丢失:

> console.log("Data stored: "+p.data("test"));

Data stored: undefined

访问和修改 CSS 和属性

以前,当您创建 DOM 元素时,您可以定义诸如 CSS 样式、其中包含的文本等属性。为了访问和修改现有元素的信息,jQuery 有一组内置方法。

。属性( )

对于大多数元素属性,使用的是.attr()方法。这个方法有两个目的。第一种是读取给定的属性,这是通过将所需属性的名称作为第一个参数提供给方法来完成的,没有其他参数。第二种方法是通过传递要设置的属性的名称作为第一个参数和要设置的值作为第二个参数来设置属性。

首先,使用以下代码检索最后一段的 ID:

$("p:eq(3)").attr("id");

在控制台中,这会产生以下输出:

> $("p:eq(3)").attr("id");

"bar"

接下来,使用以下代码将最后一段的 ID 属性更改为"bat":

$("#bar").attr("id", "bat");

执行后,控制台中将显示以下内容:

> $("#bar").attr("id", "bat");

Object[``p#bat

现在,如果您试图选择 ID 为bar的元素,将返回一个空的结果集:

> $("#bar");

Object[ ]

但是,现在您可以选择一个 ID 为bat的段落元素:

> $("#bat");

Object``p#bat

此外,可以使用 JSON 格式设置多个属性:

$("p:eq(3)").attr({

"id":"baz",

"title":"A captivating paragraph, isn’t it?"

});

执行这段代码后,Firebug 的 HTML 面板显示该段落的标记已被更改:

<p id="baz" title="A captivating paragraph, isn’t it?">

。移除属性( )

要删除一个属性,只需在想要删除属性的元素上调用.removeAttr(),并传递属性的名称。

通过删除禁用的属性来启用示例表单中的复选框:

$(":checkbox").removeAttr("disabled");

执行这段代码后,现在可以随意选中和取消选中复选框。

。css()

除了应用于样式规则之外,.css()方法的工作方式与.attr()相似。若要返回值,请将该值的名称作为唯一参数传递给该方法;要设置值,请为其传递属性名和新值。像.attr()一样,可以使用 JSON 格式设置多个值。

要将类foo的所有元素更改为红色文本和黄色背景,请使用以下代码:

$(".foo").css({

"color":"red",

"background-color":"yellow"

});

该代码一旦被执行,就会向所选元素添加新的样式规则(参见图 [2-15 )。

A978-1-4842-1230-1_2_Fig15_HTML.jpg

图 2-15。

The document after adding CSS styling to elements with class foo

在重新加载页面之前,使用以下代码从类为foo的元素中检索背景值:

$(".foo").css("background-color");

这将返回以下内容:

> $(".foo").css("background-color");

"rgb(255, 255, 0)"

Tip

返回的值是 CSS 速记属性。jQuery 的一个额外的好处是能够使用 CSS 简写来设置 CSS 属性,这在使用基本的 JavaScript 时是不起作用的。

。文本( )和。html()

当处理一个元素的内容时,使用了.text().html()方法。两者的区别在于,.html()将允许你读取新的 HTML 标签并将其插入到一个元素中,而.text()仅用于读取和写入文本。

如果在没有参数的元素集上调用这些方法中的任何一个,则返回元素的内容。当一个值被传递给该方法时,现有的值将被覆盖,新的值将被替换。

要从 ID 为bar的段落中读出文本,请在控制台中运行以下代码:

$("#bar").text();

这将捕获所有文本(包括空白),但忽略 span 标签。以下是输出:

> $("#bar").text();

"Paragraph with an id.

And this sentence is in a span.

"

要读取段落中的所有内容,包括 span 标记,请使用以下代码:

$("#bar").html();

这将导致以下结果:

> $("#bar").html();

"Paragraph with an id.

<span class="foo">And this sentence is in a span.</span>

"

现在,通过向.text()方法传递一个值来更改文本:

$("#bar").text("This is new text.");

删除该段落以前的内容,并插入新文本。请注意,span 标记也被删除了;使用.text().html()时,元素的所有内容都被替换。

要在段落中插入 HTML,请用以下代码片段再次替换其内容:

$("#bar").html("This is some <strong>HTML</strong> text.");

执行后,新文本出现在段落中,单词“HTML”以粗体显示(见图 2-16 )。

A978-1-4842-1230-1_2_Fig16_HTML.jpg

图 2-16。

The browser after inserting text and HTML tags

。瓦尔( )

访问和修改表单元素的内容是通过.val()方法完成的。此方法返回输入的值,或者如果提供了值,则设置输入的值。

使用以下命令检索测试表单中 submit 按钮的值

$(":submit").val();

哪些输出

> $(":submit").val();

"Log In"

现在,使用以下代码将提交输入的值更新为"Sign In":

$(":submit").val("Sign In");

运行前面的代码片段后,提交按钮被称为登录。

。数据( )

之前,您使用了.data()方法来为.remove().detach()的测试存储信息。.data()方法正是这样做的:它允许您以一种安全、简单的方式存储 jQuery 对象中的元素信息。

为了给测试文档中的前两段起绰号,使用.data()存储信息,然后将它记录在控制台中:

$("p:first")

.data("nickname", "Pookie")

.next("p")

.data("nickname", "Shnookums");

console.log("My nickname: "+$("p:first").data("nickname"));

console.log("My nickname: "+$("p:eq(1)").data("nickname"));

执行此脚本后,控制台中将记录以下内容:

> $("p:first") .data("nick…name: "+$("p:eq(1)").data("nickname"));

My nickname: Pookie

My nickname: Shnookums

数据也可以以 JSON 格式一起添加到元素中:

$("p.foo").data({

"nickname":"Wubby",

"favorite":{

"movie":"Pretty Woman",

"music":"Sade",

"color":"pink"

}

});

console.log("Nickname: "+$("p.foo").data("nickname"));

console.log("Favorite Movie: "+$("p.foo").data("favorite").movie);

上述代码在执行时将产生以下输出:

> $("p.foo").data({ "nickname":"Wubby",....data("favorite").movie);

Nickname: Wubby

Favorite Movie: Pretty Woman

这也可以通过在变量中缓存数据来简化,如下所示:

$("p.foo").data({

"nickname":"Wubby",

"favorite":{

"movie":"Pretty Woman",

"music":"Sade",

"color":"pink"

}

});

var info = $("p.foo").data(); // cache the data object in a variable

console.log("Nickname: "+info.nickname);

console.log("Favorite Movie: "+info.favorite.movie);

这产生了与前一个例子相同的结果,但是执行得更好,也更容易阅读。

。addClass(),。removeClass()、和。toggleclass()

三个快捷方法是为处理类而写的,因为它们的使用是现代网页设计不可或缺的一部分。前两种方法.addClass().removeClass()分别简单地添加或删除一个类属性:

$("p:first").addClass("bat");

console.log("Text: "+$(".bat").text());

$("p:first").removeClass("bat");

console.log("Text: "+$(".bat").text());

上述代码片段在控制台中输出以下内容:

> $("p:first").addClass("bat"…le.log("Text: "+$(".bat").text());

Text: Hello World!

Text:

第三种方法是.toggleClass(),接受一个或多个类名,如果元素不存在这个类,就添加它,如果这个类已经存在,就删除它。

使用以下代码在示例页面的第二段中添加类baz并删除类foo:

$("p.foo").toggleClass("foo baz");

在执行时,该段落被修改并出现,旧类被删除,新类被添加(见图 2-17 )。

A978-1-4842-1230-1_2_Fig17_HTML.jpg

图 2-17。

The foo class is removed, and the baz class is added

要恢复到foo的原始类别并删除baz,请选择段落,并再次应用.toggleClass():

$("p.baz").toggleClass("foo baz");

这导致该段落返回到只有一个类:foo

。hasClass()

.hasClass()方法的工作方式类似于.is()方法,它确定一个类是否存在于一个被选择的元素上,然后返回truefalse。这使得它非常适合回调函数。

检查第一段是否有类foo,并使用以下命令有条件地输出一条消息:

var msg = $("p:eq(1)").hasClass("foo") ? "Found!" : "Nope!";

console.log("Class? "+msg);

。高度( )和。宽度( )

要获得元素的高度或宽度,.height().width()方法很方便。两者都返回不带单位的值,这意味着返回的值是一个整数(如果元素是 68 像素高,.height()将返回68)。这不同于.css(),它也将返回度量单位。

通过运行以下代码获取窗体的高度:

console.log("Form height: "+$("form").height()+"px");

这将在控制台中输出以下内容:

> console.log("Form height: "+$("form").height()+"px");

Form height: 252px

Note

根据您使用的操作系统,返回的实际高度可能会因浏览器而异。

通过向.height().width()传递一个值,设置一个新值。使用以下代码,使页面上的所有段落高度为 100 像素,背景为黄色:

$("p").height(100).css("background-color","yellow");

执行时,所有段落高度都会改变,其背景变成黄色(参见图 2-18 )。

A978-1-4842-1230-1_2_Fig18_HTML.jpg

图 2-18。

The modified height and backgrounds of all document paragraphs

.innerHeight( ), .innerWidth( ), .outerHeight( ) 和 .outerWidth( )

元素的内部高度和宽度是不包括边框或边距的宽度或高度。您可以使用.innerHeight().innerWidth()方法来访问这些信息。

如果您希望在元素的高度或宽度中包含边框,请使用.outerHeight().outerWidth()。要包含边距,请使用.outerHeight(true).outerWidth(true)

使用类foo为段落添加边距和边框,然后记录其不同的宽度和高度:

var el = $("p.foo");

el.css({

"margin":"20px",

"border":"2px solid black"

});

console.log("Inner width: "+el.innerWidth()+"px");

console.log("Inner height: "+el.innerHeight()+"px");

console.log("Outer width: "+el.outerWidth()+"px");

console.log("Outer height: "+el.outerHeight()+"px");

console.log("Outer width with margins: "+el.outerWidth(true)+"px");

console.log("Outer height with margins: "+el.outerHeight(true)+"px");

这将在控制台中输出以下内容:

> var el = $("p.foo"); el.c…rgins: "+el.outerHeight(true)+"px");

Inner width: 840px

Inner height: 20px

Outer width: 844px

Outer height: 24px

Outer width with margins: 884px

Outer height with margins: 64px

Note

同样,根据您使用的操作系统,您的结果可能会有所不同。

影响结果集

要处理一组元素,您需要一组允许您影响集合中每个元素的方法。

。map()和。每个( )

.map().each()方法允许开发人员使用回调函数将函数分别应用于集合中的每个元素,回调函数有两个参数:当前元素索引和当前 DOM 元素。

两者的区别在于,.map()返回一个包含回调返回值的新对象,而.each()将返回包含回调执行的更改的原始对象。这意味着.each()是可链接的,而.map()不是。

要使用类foo遍历每个段落和元素,并追加标记名和元素索引,请使用以下代码:

$("p,.foo").map(function(index, ele){

$(this).append(" "+ele.tagName+" #"+index);

});

这将元素的标签名和索引号添加到每个匹配元素的末尾(见图 2-19 )。

A978-1-4842-1230-1_2_Fig19_HTML.jpg

图 2-19。

The test page after mapping a callback function to display names and indexes for each element

要用.each()完成同样的事情,只需交换对.map()的调用:

$("p,.foo").each(function(index, ele){

$(this).append(" "+ele.tagName+" #"+index);

});

这产生了相同的结果。

如果您需要在调用.map().each()之后执行进一步的处理,这种差异就开始起作用了。例如,如果您想要将标签名称和索引附加到每个段落和带有类别foo的范围,如前所述,然后过滤到带有类别foo的范围,并更改其背景和文本颜色,您可以尝试以下方法:

$("p,.foo").map(function(index, ele){

$(this).append(" "+ele.tagName+" #"+index);

})

.find("span.foo")

.css({

"color":"red",

"background":"yellow"

});

执行后,标签名和索引被追加,但是 span 没有应用任何样式更改。这是因为从.map()返回的对象不再引用这些元素。

为了让前面的代码片段按预期执行,您必须将对.map()的调用替换为对.each()的调用:

$("p,.foo").each(function(index, ele){

$(this).append(" "+ele.tagName+" #"+index);

})

.find("span.foo")

.css({

"color":"red",

"background":"yellow"

});

现在运行代码产生了期望的结果(见图 2-20 )。

A978-1-4842-1230-1_2_Fig20_HTML.jpg

图 2-20。

Using .each(), the expected results are produced

使用动画和其他效果

jQuery 最令人兴奋的特性之一是它的方法库,允许动画和特殊效果,这些在普通的 JavaScript 中都是可能的,但是使用 jQuery 非常容易。传统的 JavaScript 方法很棘手,也更复杂。

Note

因为很难将动画显示为静态图像,所以您需要依靠您的浏览器来演示这些示例应该是什么样子。对于不同动画效果的现场演示,请访问位于 http://api.jquery.com 的 jQuery API,并查找您希望看到演示的个别方法。

。显示( )和。隐藏( )

最基本的特效功能是.show().hide()。当在没有参数的情况下触发时,它们只是在元素的样式属性中添加或移除display:none;

使用以下代码隐藏 ID 为bar的段落:

$("#bar").hide();

该段落会从浏览器窗口中消失,但在使用元素检查器的 DOM 中仍然可见。要将它带回到视图中,请调用.show():

$("#bar").show();

元素会恢复到原来的样子。

为了使元素的隐藏和显示具有动画效果,可以传递持续时间(以毫秒为单位),以及在动画完成后触发的可选回调。为了进行演示,向 ID 为bar的段落添加背景和边框,然后用 2 秒的时间隐藏它,并使用回调函数在控制台中记录一条消息:

$("#bar")

.css({

"background":"yellow",

"border":"1px solid black"

})

.hide(2000,function(){

console.log("Animation complete!");

});

执行时,CSS 样式被添加到元素中,并且触发.hide()方法。这将导致元素水平和垂直收缩,并淡化其不透明度。两秒钟后,它将消失,回调函数将"Animation complete!"消息记录在控制台中。

Note

回调函数将为动画集中的每个元素触发。

。fadeIn(),。fadeOut()和。fadeTo()

要淡入或淡出一个元素(使用不透明度),使用.fadeIn().fadeOut()。当被调用时,这些方法调整元素的不透明度,从.fadeIn()中的01,或者从.fadeOut()中的10。当一个元素淡出时,display:none;也应用于该元素。淡入时,display:none;会从元素中移除(如果它存在的话)。

这两种方法都接受动画持续时间的可选参数(默认为 400 毫秒)和动画完成时触发的回调。持续时间有两个快捷字符串,"fast""slow",分别转换为 200 和 600 毫秒。

要淡出表单、记录一条消息、淡入并记录另一条消息,请使用以下命令:

$("form")

.fadeOut(1000, function(){

console.log("Faded out!");

})

.fadeIn(1000, function(){

console.log("Faded in!");

});

或者,.fadeTo()允许您指定元素应该淡化到的不透明度。这个方法需要两个参数:持续时间和元素淡入淡出的不透明度(一个在01之间的数字)。可选的回调也可以作为第三个参数传递。

将表格的透明度降低到 50 %,并使用以下内容记录一条消息:

$("form")

.fadeTo(1000, 0.5, function(){

console.log("Faded to 50%!");

});

。slideUp(),。slideDown()和。slideToggle()

通过将元素的高度降低到0.slideUp()来隐藏元素是一种快捷的方法。它动画显示元素高度的减少,直到它到达0,然后设置display:none;以确保布局不再受元素影响。为了扭转这种情况,.slideDown()方法删除了display:none;,并把从0到元素原始高度的高度制作成动画。

就像.fadeIn().fadeOut()一样,接受两个可选参数:持续时间和一个回调函数。

向上滑动带有类别foo的段落,记录一条消息,向下滑动,并记录另一条消息:

$("p.foo")

.slideUp(1000, function(){

console.log("Hidden!");

})

.slideDown(1000, function(){

console.log("Shown!");

});

.slideToggle()方法与.slideUp().slideDown()做同样的事情,但是它足够聪明,可以知道一个元素是否被隐藏或显示,并使用该信息来决定采取什么行动。

要为类别为foo的段落设置显示切换,请使用以下命令:

$("p.foo")

.slideToggle("slow", function(){

console.log("Toggled!");

});

多次运行此代码,以交替方式上下滑动段落。

。动画( )

前面讨论的动画方法都是调用.animate()方法的快捷方式。此方法将动画显示元素的大多数可视 CSS 属性,并支持缓动,缓动是改变动画操作方式的任意数量的数学公式之一。默认情况下,"linear""swing"宽松是受支持的,但是 jQuery 有一个易于包含的宽松插件(您将在本书后面了解插件)。

.animate()方法接受两种格式的几个参数。在第一种格式中,向该方法传递一组 JSON 格式的 CSS 属性作为第一个参数,可选的持续时间(毫秒)作为第二个参数,可选的缓动公式作为第三个参数,可选的回调作为第四个参数。第二种格式传递一组 JSON 格式的 CSS 属性作为第一个参数,传递一组 JSON 格式的选项作为第二个参数。

在设置了背景和边框样式之后,要使用"swing"缓动类型在 5 秒内动画显示 ID 为bar的段落元素的高度和宽度,并在完成时记录一条消息,请使用以下第一种格式:

$("#bar")

.css({

"background":"yellow",

"border":"1px solid black"

})

.animate({

"width":"500px",

"height":"100px"

},

5000,

"swing",

function(){

console.log("Animation complete!");

});

完成后,该段落为黄色,带有黑色边框,并已更改其大小以匹配所传递的参数(参见图 2-21 )。

A978-1-4842-1230-1_2_Fig21_HTML.jpg

图 2-21。

The paragraph after animating its height and width

使用第二种格式,代码将更改如下:

$("#bar")

.css({

"background":"yellow",

"border":"1px solid black"

})

.animate({

"width":"500px",

"height":"100px"

},

{

"duration":5000,

"easing":"swing",

"complete":function(){

console.log("Animation complete!");

}

});

这产生了相同的结果。.animate()的第二种格式也提供了额外的选项。要使用所有可用选项完成相同的操作,您的代码可能如下所示:

$("#bar")

.css({

"background":"yellow",

"border":"1px solid black"

})

.animate({

"width":"500px",

"height":"100px"

},

{

"duration":5000,

"easing":"swing",

"complete":function(){

console.log("Animation complete!");

},

"step":function(){

console.log("Step completed!");

},

"queue":true,

"specialEasing":{

"width":"linear"

}

});

step选项允许开发人员创建一个回调函数,在动画的每一步之后触发。这是每次属性被调整的时候,所以前面的例子最终输出了相当多的"Step completed!"的日志消息。

queue选项告诉动画是否应该被添加到当前队列,即动画被调用的顺序。如果多个动画被调用并排队,第一个动画将在第二个开始之前完成,第二个将在第三个开始之前完成,依此类推。

specialEasing选项允许开发人员将不同的缓动样式附加到每个被动画化的 CSS 属性上。

。延迟( )

.delay()方法本质上允许开发人员在给定的毫秒数内暂停脚本的执行。它提供了运行一个动画并在开始下一个动画之前等待一段时间的能力。

要向上滑动 ID 为bar的段落,等待 3 秒钟,然后向下滑动,请使用以下代码:

$("#bar")

.css({

"background":"yellow",

"border":"1px solid black"

})

.slideUp(1000, function(){

console.log("Animation completed!");

})

.delay(3000)

.slideDown(1000, function(){

console.log("Animation completed!");

});

。停止( )

要停止动画,使用.stop()方法。该方法接受两个布尔参数:一个用于确定是否应该清除队列,另一个用于确定动画是否应该跳到结尾。两个值都默认为false

要启动动画,停止动画,清除队列,并在 200 步后跳到结尾,请使用以下命令:

var count = 0; // Keep track of the current step count

$("#bar")

.css({

"background":"yellow",

"border":"1px solid black"

})

.animate({

"width":"500px"

},

{

"duration":6000,

"step":function(){

if(count++==200)

{

$(this).stop(true, true);

}

}

});

处理事件

在许多脚本中,当某些事件或浏览器动作发生时,希望发生某些动作。jQuery 内置了对处理浏览器事件的支持,您将在本节中了解到这一点。

浏览器事件

当浏览器本身遇到更改或错误时,就会发生浏览器事件。

。错误()

如果发生浏览器错误,将触发此事件。浏览器错误的一个常见实例是图像标签试图加载不存在的图像。.error()方法允许开发人员将一个处理程序(即事件发生时要触发的函数)绑定到事件。

创建一个试图显示一个不存在的图像的图像标签,并将一个错误处理程序附加到向控制台输出消息的error事件:

$("<img />", {

"src":"not/an/image.png",

"alt":"This image does not exist"

})

.error(function(){

console.log("The image cannot be loaded!");

})

.appendTo("body");

执行这段代码后,控制台将显示类似如下的内容:

> $("<img />", { "src":"not/an/image.png", …ot be loaded!"); }) .appendTo("body");

Object[ img not/an/image.png ]

"NetworkError: 404 Not Found -``http://localhost/testing/not/an/image.png

image.png

The image cannot be loaded!

。滚动( )

如果文档被滚动,则触发scroll事件。要将处理程序绑定到该事件,请使用.scroll()方法:

$(window)

.scroll(function(){

console.log("The window was scrolled!");

});

执行此代码后,滚动浏览器窗口将导致在控制台中记录一条消息。

此外,不带任何参数调用.scroll()方法将触发scroll事件。将前面的处理程序绑定到窗口后,通过运行以下命令来触发事件:

$(window).scroll();

执行这段代码将在控制台中记录scroll事件处理程序的消息。

处理文档加载事件

通常,JavaScript 需要等到文档准备好之后才能执行任何脚本。此外,当用户退出一个页面时,有时需要启动一个函数来确保他们想离开这个页面。

。就绪( )

几乎每个 jQuery 脚本中都使用了.ready()方法,以防止脚本过早执行,从而无法正常运行。该方法在触发其处理程序之前等待 DOM 准备好进行操作。

通常的做法是让整个脚本成为由.ready()处理程序触发的回调函数:

$(document).ready(function(){

// All jQuery functionality here

});

此外,.ready()方法接受一个参数作为 jQuery 函数的别名。这允许您编写故障保护 jQuery 脚本,即使使用jQuery.noConflict()$别名返回给另一个库,这些脚本也能按预期工作(这允许多个使用$别名的 JavaScript 库在同一个项目中使用,不会出现问题)。

您可以使用以下代码保证$别名正常工作:

jQuery.ready(function($){

// All jQuery functionality here

$("p").fadeOut();

});

从技术上讲,任何别名都可以在这里传递:

jQuery(document).ready(function(xTest){

xTest("#bar").click(function(){console.log("Clicked!");});

});

这按预期执行,没有错误。在很多情况下,这种检查是不必要的,但是它说明了别名如何与.ready()方法一起工作。

最后,jQuery 函数本身可以用作.ready()的别名:

jQuery(function($){

// actions to perform after the DOM is ready

});

。卸载( )

每当用户通过单击链接、重新加载页面、使用前进或后退按钮或完全关闭窗口退出页面时,就会触发unload事件。然而,卸载的处理并不是在所有的浏览器中都是一致的,因此在用于生产脚本之前,应该在多个浏览器中进行测试。要创建一个指向 Google 的链接并向 unload 事件附加一个警报,请使用以下代码:

$("<a>", {

"href":"http://google.com

"text":"Go to Google!"

})

.appendTo("#bar");

$(window).unload(function(){

alert("Bye! Google something neat!");

});

执行这段代码,然后单击新链接。警报触发,您被重定向到 Google 主页。

处理事件附件

用户触发了大量的浏览器事件,jQuery 提供了几种方法来轻松处理它们。可用的事件有:blurfocusfocusinfocusoutresizescrollunloadclickdblclickmousedownmouseupmousemovemouseovermouseoutmouseentermouseleavechangeselectsubmitkeydownkeypresskeyuperror

。绑定( )和。解除绑定( )

为了将事件处理程序绑定到元素,使用了.bind()方法。它接受一个事件作为第一个参数,接受一个处理函数作为第二个参数。

使用空格分隔的事件列表作为第一个参数,可以绑定多个事件。为了将不同的处理程序绑定到不同的事件,JSON 格式的对象也可以传递给.bind()

要将控制台消息日志绑定到click事件,请使用以下命令:

$("p")

.bind("click", function(){

console.log("Click happened!");

});

运行此代码后单击一个段落将导致一条消息被记录到控制台。

要将处理程序绑定到clickmouseover事件,请使用以下代码:

$("p")

.bind("click mouseover", function(){

console.log("An event happened!");

});

现在,点击或悬停在一个段落上会在控制台中记录一条消息。

如果处理程序需要向其传递数据,还有一个额外的参数可用。这是一个 JSON 格式的对象,包含函数中使用的变量。这些变量被绑定到event对象,因此这些值在给定的处理程序中保持不变。

使用以下代码为测试文档中功能相同但日志消息不同的两个段落设置一个click处理程序:

// Create a value for the notice variable

var notice = "I live in a variable!";

$("p.foo").bind("click", { n:notice }, function(event){

console.log(event.data.n);

});

// Change the value of the notice variable

var notice = "I live in a variable too!";

$("#bar").bind("click", { n:notice }, function(event){

console.log(event.data.n);

});

要将不同的处理程序绑定到clickmouseover事件,请使用以下代码:

$("p")

.bind({

"click":function(){

console.log("Click happened!");

},

"mouseover":function(){

console.log("Mouseover happened!");

}

});

执行后,当每个事件发生时,将在控制台中记录不同的消息。

要删除一个事件,只需调用.unbind()方法。如果不带参数调用,则从元素中移除所有事件绑定。要指定,要解除绑定的事件的名称可以作为第一个参数传递。为了进一步说明,要从事件中移除的函数可以作为第二个参数传递。

要解除示例段落中所有事件的绑定,请使用以下命令:

$("p").unbind();

要仅删除click事件处理程序,请使用以下代码:

$("p").unbind("click");

或者,如果一个特定的函数被绑定到一个元素,它可以像这样被解除绑定:

var func1 = function(){

console.log("An event was triggered!");

},

func2 = function(){

console.log("Another handler!");

};

$("#bar")

.bind("click", func1)

.bind("click", func2)

.trigger("click") // fire the event once

.unbind("click", func1);

前面的代码创建了两个函数(存储在func1func2变量中),将它们绑定到 ID 为bar的段落的click事件,触发该事件一次(您将在本节稍后了解到.trigger(),并解除绑定存储在func1中的函数。

运行这段代码后,点击该段落将只触发存储在func2中的函数。

。live()和。模具( )

类似于.bind().unbind() , .live().die()将分别附加和移除元素的事件处理程序。主要区别在于.live()不仅会将处理程序和 JavaScript 属性附加到现有的事件上,还会附加到添加到 DOM 中的任何新元素上,这些新元素随后也会匹配选择器。

例如,使用以下代码为任何锚元素添加一个click事件处理程序:

$("a")

.live("click", function(){

console.log("Link clicked!");

return false; // prevent the link from firing

});

当然,目前示例页面上没有任何链接。无需重新加载,使用以下代码向 ID 为bar的段落添加锚标记:

$("<a>", {

"href":"http://google.com

"text":"Go to Google!"

})

.appendTo("#bar");

新链接出现,即使事件是在 DOM 中存在任何锚标记之前绑定的,单击链接也会导致在控制台中记录一条消息,并且链接不会触发。

使用.bind()执行之前的动作不起作用。另外,.live()绑定的click事件处理程序不能用.unbind()移除;要删除事件,您必须使用.die().die()的使用与.unbind()相同。

。一个( )

.one()方法的功能和使用与.bind()相同,除了事件处理程序在事件发生一次后被解除绑定。

使用以下代码为 ID 为bar的段落添加一个新的click事件处理程序,该事件处理程序只会触发一次:

$("#bar").one("click", function(){

console.log("This will only fire once.");

});

执行后,单击 ID 为bar的段落会导致控制台记录一条消息,随后的单击没有任何效果。

。切换( )

.toggle()函数允许开发人员将两个或多个函数绑定到在交替点击时触发的点击事件。或者,该函数可用于切换元素的可见性(如切换.show().hide()——类似于.slideToggle()在被调用时如何交替执行.slideUp().slideDown()的功能)。

首先,使用以下代码将三个不同的日志消息绑定到 ID 为bar的段落的click事件:

$("#bar")

.toggle(function(){

console.log("Function 1");

},

function(){

console.log("Function 2");

},

function(){

console.log("Function 3");

});

执行后,点击 ID 为bar的段落,在随后的点击中连续记录三条消息。

接下来,用下面的代码切换 ID 为bar的段落的可见性:

$("#bar").toggle();

启动这个函数会隐藏段落。再次开火会把它带回来。通过添加持续时间作为第一个参数,该方法将显示或隐藏元素的动画效果:

$("#bar").toggle(2000);

最后,可以传递一个布尔标志来确定是否应该显示或隐藏所有元素:

$("#bar").toggle(true);  // all elements will be shown

$("#bar").toggle(false); // all elements will be hidden

。触发器( )

为了触发事件,使用了.trigger()方法。该方法接受要触发的事件和要传递给处理程序的可选参数数组。

将处理程序绑定到 ID 为bar的段落,并使用以下代码触发它:

$("#bar")

.bind("click", function(){

console.log("Clicked!");

})

.trigger("click");

要传递附加数据,请按如下方式修改代码:

// create a variable

var note = "I was triggered!";

$("#bar")

.bind("click", function(event, msg){ // allow a 2nd argument

// If no msg variable is passed, a default message

var log = msg || "I was clicked!";

console.log(log);

})

.trigger("click", [ note ]); // array passed in square brackets

这将存储在note变量中的信息输出到控制台。

快捷事件方法

每个事件都有一个快捷方法,该方法接受处理函数作为参数。如果传递时没有参数,它将调用.trigger()获取其事件类型。可用的快捷功能有.blur().focus().focusin().focusout().load().resize().scroll().unload().click().dblclick().mousedown().mouseup().mousemove().mouseover().mouseout().mouseenter().mouseleave().change().select().submit().keydown().keypress().keyup().error()

例如,下面将一个处理程序绑定到 click 事件,并触发该事件:

$("#bar").click(function(){ console.log("Clicked!"); }).click();

使用 AJAX 控件

我们将要讨论的最后一组 jQuery 方法可能是最有用的,并且很可能在 jQuery 的广泛采用中发挥了重要作用。提供 AJAX 4 功能的方法非常有用,尤其是对于以前用普通 JavaScript 构建过 AJAX 脚本的人来说,非常简单。

Note

关于 AJAX 的进一步阅读,请参见维基百科的文章 https://en.wikipedia.org/wiki/Ajax_(programming) .

对于这一部分,您需要一个外部文件来使用 AJAX 控件进行访问。在testing文件夹中创建一个名为ajax.php的新文件。在内部,插入以下代码:

<?php

echo '<p class="ajax">This paragraph was loaded with AJAX.</p>',

'<pre>GET variables: ', print_r($_GET, TRUE), '</pre>',

'<pre>POST variables: ', print_r($_POST, TRUE), '</pre>';

?>

这个文件将被 jQuery 中各种可用的 AJAX 方法调用。为了便于说明,它将向您显示传递给脚本的数据。

$.ajax()

发送 AJAX 请求的底层或最基本的函数是$.ajax()。注意,调用这个函数时没有选择器,因为它不适用于 jQuery 对象。AJAX 动作是全局函数,独立于 DOM 执行。

$.ajax()函数接受一个参数:一个包含 AJAX 调用设置的对象。如果在没有任何设置的情况下调用,该方法将加载当前页面,并且对结果不做任何处理。

$.ajax()有相当多的设置可用,这里没有全部涵盖或在本书中使用。可用设置的完整列表见 http://api.jquery.com/jQuery.ajax 。最常见如下:

  • data:这描述了发送到远程脚本的任何数据,或者作为查询字符串(key1=val1&key2=val2)或者作为JSON ({"key1":"val1","key2":"val2"})
  • 这个回调允许数据的预过滤,并且对于净化来自远程脚本的数据非常有用。
  • dataType:描述请求中预期的数据类型。如果没有定义,jQuery 会进行智能猜测。可用类型有"xml", "html", "script", "json", "jsonp","text"
  • error(XMLHttpRequest, textStatus, errorThrown):这个回调将在请求出错时执行。XMLHttpRequest对象、传达请求状态的字符串和错误代码作为参数传递。
  • success(data, textStatus, XMLHttpRequest):如果请求成功完成,这个回调将被执行。从远程脚本返回的数据、传达请求状态的字符串和XMLHttpRequest对象作为参数传递。
  • type:这是要发送的请求类型。默认为GET,但POST也可用。PUTDELETE可以使用,但可能无法在所有浏览器中正常工作。
  • url:这是请求要发送到的 URL。

要向您的示例脚本发送一个基本的POST请求,并将结果加载到 ID 为bar的段落中,请使用以下代码:

$.ajax({

"type":"POST",

"url":"ajax.php",

"data":"var1=val1&var2=val2",

"success":function(data){

$("#bar")

.css("background","yellow")

.html(data);

}

});

执行该代码后,该段落的内容被替换为加载的信息(见图 2-22 )。

A978-1-4842-1230-1_2_Fig22_HTML.jpg

图 2-22。

The loaded AJAX information from ajax.php

$.ajaxSetup()

为了设置 AJAX 调用的默认选项,使用了$.ajaxSetup()函数。例如,要指定在默认情况下,所有 AJAX 请求都应该使用POST发送到ajax.php,然后加载到 ID 为bar的段落中,请使用下面的代码:

$.ajaxSetup({

"type":"POST",

"url":"ajax.php",

"success":function(data){

$("#bar")

.css("background","yellow")

.html(data);

}

});

现在,只需传递新数据,就可以轻松地发出新的 AJAX 请求:

$.ajax({

"data":{

"newvar1":"value1",

"newvar2":"value2"

}

});

这导致该段落的内容被来自ajax.php的新内容替换(见图 2-23 )。

A978-1-4842-1230-1_2_Fig23_HTML.jpg

图 2-23。

The result of an AJAX call after setting default options

这些默认值可以在随后对$.ajax()的调用中被覆盖,只需在新调用中重新定义选项:

$.ajax({

"type":"GET",

"data":{

"newvar1":"value3",

"newvar2":"value4"

}

});

这导致使用GET发送数据(参见图 2-24 )。

A978-1-4842-1230-1_2_Fig24_HTML.jpg

图 2-24。

The result after overriding the default type option with GET

使用速记 AJAX 方法

有几个简单的一次性函数可用于执行常见的 AJAX 任务。简而言之,这些简写方法只是简单的包装函数,用一些已经设置好的参数调用$.ajax()

使用这些方法会导致轻微的性能损失,因为您实际上是在调用一个设置参数的方法,并在其内部调用$.ajax()。然而,使用速记方法的便利确实加快了许多脚本的开发。

\(.get()和\)。帖子( )

对于标准的GETPOST请求,$.get()$.post()功能易于使用。两者都有四个参数:请求发送到的 URL、发送到远程脚本的可选数据、请求成功时执行的可选回调以及可选的dataType设置。

要在不发送数据的情况下使用GET加载ajax.php的结果,请使用以下命令:

$.get("ajax.php", function(data){

$("#bar")

.css("background","yellow")

.html(data);

});

要使用POST发送带有数据的请求,可以使用以下代码:

$.post("ajax.php", {"var1":"value"}, function(data){

$("#bar")

.css("background","yellow")

.html(data);

});

$.getJSON( )

加载 JSON 数据时,$.getJSON()是一个快捷函数。它接受请求发送到的 URL、可选数据和可选回调函数。

要运行这个函数的示例,需要创建另一个测试文件:在testing文件夹中创建一个名为json.php的新文件,并在其中插入以下 JSON:

{"var1":"value1","var2":"value2"}

现在,加载json.php的内容并输出 ID 为bar的段落中的内容:

$.getJSON("json.php",

function(data){

$("#bar")

.css("background","yellow")

.html(data.var1+", "+data.var2);

});

执行后,该段落的内容将被替换为字符串"value1, value2"

$.getScript()

要加载外部 JavaScript,使用$.getScript()函数。它接受一个 URL 和一个可选的回调(通常不需要,因为脚本会在成功加载时自动执行)。

testing文件夹中创建一个名为script.php的新文件,并插入以下内容:

alert("This script was loaded by AJAX!");

现在,通过在控制台中执行以下代码来加载该脚本:

$.getScript("script.php");

执行时,将触发警报。

。负载( )

.load()方法的工作方式就像$.get()$.post(),除了它是一个方法而不是一个全局函数。它有一个隐式回调,用远程文件返回的内容替换匹配元素的 HTML。

该方法接受相同的三个参数:目标 URL、可选数据和可选回调(在元素内容被替换后触发)。

使用此代码发送一些数据后,加载 ID 为bar的段落中ajax.php的内容:

$("#bar").load("ajax.php", {"var1":"value1"});

运行该代码片段后,该段落的内容被替换为返回的结果。

摘要

这一章内容丰富,涉及的范围非常广。记得在线查看 jQuery API 文档,获取更多示例、进一步的解释以及社区中其他开发人员的讨论。要搜索一个方法,只需将其名称添加到 API 的 URL 的末尾;例如,要查找.slideup()方法,在浏览器中导航到 http://api.jquery.com/slideup

在本书的下一部分中,您将复习 PHP 技能,包括面向对象编程,然后开始从第四章开始构建活动日历的后端开发。

Footnotes 1

www.w3.org/TR/DOM-Level-3-Core/core.html#ID-1312295772

2

http://en.wikipedia.org/wiki/Json

3

www.456bereastreet.com/archive/200502/efficient_css_with_shorthand_properties/

4

https://en.wikipedia.org/wiki/Ajax_(programming)

三、面向对象编程

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​3) contains supplementary material, which is available to authorized users.

在本章中,你将学习面向对象编程(OOP)背后的概念,面向对象编程是一种编码风格,在这种风格中,相关的动作被分组到类中,以帮助创建更紧凑、更有效的代码。你将在本书中构建的项目的后端在很大程度上是基于 OOP 的,所以在你将要完成的其余练习中会经常引用到本章中的概念。

理解面向对象编程

如上所述,面向对象编程是一种编码风格,它允许开发人员将相似的任务分组到类中。这有助于保持您的代码易于维护,并符合“不要重复自己”(DRY)的租户。

Note

关于干式编程的进一步阅读,见http://en.wikipedia.org/wiki/Don’t_repeat_yourself .

干编程的主要好处之一是,如果程序中的一条信息发生变化,通常只需要一个变化就可以更新代码。对于开发人员来说,最大的噩梦之一就是维护代码,在代码中一遍又一遍地声明数据,这意味着对程序的任何更改都变成了一个极其令人沮丧的游戏,比如 Waldo 在哪里?因为他们寻找重复的数据和功能。

OOP 让许多开发人员望而生畏,因为它引入了新的语法,乍看之下,似乎比简单的过程化或内联代码复杂得多。然而,仔细观察一下,OOP 实际上是一种非常简单的编程方法。

了解对象和类

在我们深入 OOP 的细节之前,有必要对对象和类的组件有一个基本的了解。本节将介绍类的构造块、它们不同的功能以及它们的一些用途。

认识到对象和类之间的差异

很快,OOP 中出现了混乱:经验丰富的开发人员开始谈论对象和类,它们似乎是可互换的术语。然而,事实并非如此,尽管一开始你很难理解其中的区别。

一个班级就像一栋房子的蓝图。它在纸上定义了房子的形状,明确定义和规划了房子不同部分之间的关系,即使房子并不存在。

那么,一个物体就像是根据蓝图建造的真正的房子。存储在对象中的数据就像组成房子的木材、电线和混凝土:如果没有根据蓝图进行组装,它只是一堆东西。然而,当所有这些都聚集在一起时,它就变成了一个有组织的、有用的房子。

类构成了数据和动作的结构,并使用这些信息来构建对象。可以同时从同一个类中构建多个对象,每个对象都相互独立。继续我们的建筑类比,这类似于从相同的蓝图建造整个小区的方式:150 个不同的房子,它们看起来都一样,但内部有不同的家庭和装饰。

构建类

创建类的语法非常简单:使用class关键字声明一个类,后跟类名和一组花括号({}):

<?php

declare(strict_types=1);

class MyClass

{

// Class properties and methods go here

}

?>

Note

可选声明strict_types=1是 PHP 7 的新增功能,它强制对所有函数调用和返回语句中出现的标量类型声明进行严格的类型检查。请参阅附录,了解更多关于这个特性和您将使用的其他 PHP 7 特性的信息。

创建类后,可以使用new关键字实例化一个新类并存储在一个变量中:

$obj = new MyClass;

要查看类的内容,使用var_dump():

var_dump($obj);

通过将所有前面的代码放在testing文件夹中一个名为test.php的新文件中来尝试这个过程:

<?php

declare(strict_types=1);

class MyClass

{

// Class properties and methods go here

}

$obj = new MyClass;

var_dump($obj);

?>

http://localhost/testing/test.php将页面加载到浏览器中,应该会显示以下内容:

object(MyClass)#1 (0) { }

最简单的形式是,您已经完成了第一个 OOP 脚本。

定义类属性

要向类中添加数据,需要使用属性或特定于类的变量。这些变量的工作方式与常规变量完全一样,只是它们被绑定到对象上,因此只能使用对象来访问。

要将属性添加到MyClass,请将以下粗体代码添加到脚本中:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

}

$obj = new MyClass;

var_dump($obj);

?>

关键字public决定了属性的可见性,这一点你将在本章稍后了解。接下来,使用标准变量语法命名属性,并赋值(尽管类属性不需要初始值)。

若要读取此属性并将其输出到浏览器,请引用要从中读取的对象和要读取的属性:

echo $obj->prop1;

因为一个类可以存在多个实例,所以如果没有引用单个对象,脚本就无法确定要读取哪个对象。箭头(->)的使用是一个 OOP 结构,它访问给定对象包含的属性和方法。

修改test.php中的脚本以读出属性,而不是转储整个类,方法是修改粗体的行:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

}

$obj = new MyClass;

echo $obj->prop1;

?>

重新加载浏览器现在会输出以下内容:

I’m a class property!

定义类方法

方法是特定于类的函数。对象能够执行的单个操作在类中被定义为方法。

例如,要创建设置和获取类属性$prop1的值的方法,请在代码中添加以下粗体行:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

$obj = new MyClass;

echo $obj->prop1;

?>

Note

OOP 允许对象使用$this引用自己。当在一个方法中工作时,使用$this,就像在类外使用对象名一样。

要使用这些方法,就像调用常规函数一样调用它们,但是首先要引用它们所属的对象。从MyClass中读取属性,更改其值,并通过修改以粗体显示的内容再次读取:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

$obj = new MyClass;

echo $obj->getProperty(); // Get the property value

$obj->setProperty("I’m a new property value!"); // Set a new one

echo $obj->getProperty(); // Read it out again to show the change

?>

重新加载您的浏览器,您将看到以下内容:

I’m a class property!

I’m a new property value!

当您使用同一个类的多个实例时,OOP 的强大就变得显而易见了。向混合中添加一个额外的实例MyClass,并开始设置和获取属性:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

// Create two objects

$obj = new MyClass;

$obj2 = new MyClass;

// Get the value of $prop1 from both objects

echo $obj->getProperty();

echo $obj2->getProperty();

// Set new values for both objects

$obj->setProperty("I’m a new property value!");

$obj2->setProperty("I belong to the second instance!");

// Output both objects' $prop1 value

echo $obj->getProperty();

echo $obj2->getProperty();

?>

当您在浏览器中加载结果时,结果如下所示:

I’m a class property!

I’m a class property!

I’m a new property value!

I belong to the second instance!

正如你所看到的,OOP 将对象作为独立的实体,这使得将不同的代码分成小的、相关的包变得容易。

为了使对象的使用更容易,PHP 还提供了许多神奇的方法,或者当对象中发生某些常见动作时调用的特殊方法。这允许开发人员相对容易地执行许多有用的任务。

使用构造函数和析构函数

当一个对象被实例化时,通常需要立即设置一些东西。为了处理这个问题,PHP 提供了神奇的方法__construct(),每当创建一个新对象时,都会自动调用这个方法。

为了说明构造函数的概念,向MyClass添加一个构造函数,每当创建一个新的类实例时,它都会输出一条消息:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

// Create a new object

$obj = new MyClass;

// Get the value of $prop1

echo $obj->getProperty();

// Output a message at the end of the file

echo "End of file.<br />";

?>

Note

__CLASS__是所谓的魔法常数,在这种情况下,它返回调用它的类的名称。有几个可用的魔术常数;你可以在 http://us3.php.net/manual/en/language.constants.predefined.php 的 PHP 手册中读到更多关于它们的内容。

在浏览器中重新加载该文件将产生以下结果:

The class "MyClass" was initiated!

I’m a class property!

End of file.

要在对象被销毁时调用函数,可以使用__destruct()魔法方法。这对于类清理很有用(例如,关闭数据库连接)。

通过在MyClass中定义魔法方法__destruct(),当物体被破坏时输出一条信息:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function __destruct()

{

echo 'The class "', __CLASS__, '" was destroyed.<br />';

}

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

// Create a new object

$obj = new MyClass;

// Get the value of $prop1

echo $obj->getProperty();

// Output a message at the end of the file

echo "End of file.<br />";

?>

定义了析构函数后,重新加载测试文件会产生以下输出:

The class "MyClass" was initiated!

I’m a class property!

End of file.

The class "MyClass" was destroyed.

当到达一个文件的末尾时,PHP 自动释放文件中使用的所有资源以保持内存可用。这触发了MyClass对象的析构函数。

要显式触发析构函数,可以使用函数unset()销毁对象:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function __destruct()

{

echo 'The class "', __CLASS__, '" was destroyed.<br />';

}

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

// Create a new object

$obj = new MyClass;

// Get the value of $prop1

echo $obj->getProperty();

// Destroy the object

unset($obj);

// Output a message at the end of the file

echo "End of file.<br />";

?>

现在,在浏览器中加载时,结果会更改如下:

The class "MyClass" was initiated!

I’m a class property!

The class "MyClass" was destroyed.

End of file.

转换为字符串

为了避免脚本试图将MyClass输出为字符串时出现错误,使用了一种称为__toString()的神奇方法。

如果没有__toString(),试图将对象输出为字符串会导致致命错误。尝试使用echo输出对象,而不使用魔法方法,如下所示:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function __destruct()

{

echo 'The class "', __CLASS__, '" was destroyed.<br />';

}

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

// Create a new object

$obj = new MyClass;

// Output the object as a string

echo $obj;

// Destroy the object

unset($obj);

// Output a message at the end of the file

echo "End of file.<br />";

?>

这将导致以下结果:

The class "MyClass" was initiated!

Catchable fatal error``: Object of class MyClass could not be converted to string

in``C:\wamp\www\book\testing\tst01.php``on line

为了避免这个错误,添加一个__toString()方法:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function __destruct()

{

echo 'The class "', __CLASS__, '" was destroyed.<br />';

}

public function __toString()

{

echo "Using the toString method: ";

return $this->getProperty();

}

public function setProperty($newval)

{

$this->prop1 = $newval;

}

public function getProperty()

{

return $this->prop1 . "<br />";

}

}

// Create a new object

$obj = new MyClass;

// Output the object as a string

echo $obj;

// Destroy the object

unset($obj);

// Output a message at the end of the file

echo "End of file.<br />";

?>

在这种情况下,试图将对象转换为字符串会导致调用getProperty()方法。在您的浏览器中加载测试脚本以查看结果:

The class "MyClass" was initiated!

Using the toString method: I’m a class property!

The class "MyClass" was destroyed.

End of file.

Tip

除了本节讨论的神奇方法之外,还有其他几种方法可用。关于魔术方法的完整列表,请参见 PHP 手册页的 http://us2.php.net/manual/en/language.oop5.magic.php

使用类继承

使用extends关键字,类可以继承另一个类的方法和属性。例如,要创建扩展MyClass并添加一个方法的第二个类,您可以将以下内容添加到测试文件中:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function __destruct()

{

echo 'The class "', __CLASS__, '" was destroyed.<br />';

}

public function __toString()

{

echo "Using the toString method: ";

return $this->getProperty();

}

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

class MyOtherClass extends MyClass

{

public function newMethod(): string

{

return "From a new method in " . __CLASS__ . ".<br />";

}

}

// Create a new object

$newobj = new MyOtherClass;

// Output the object as a string

echo $newobj->newMethod();

// Use a method from the parent class

echo $newobj->getProperty();

?>

在浏览器中重新加载测试文件时,会输出以下内容:

The class "MyClass" was initiated!

From a new method in MyOtherClass.

I’m a class property!

The class "MyClass" was destroyed.

覆盖继承的属性和方法

要更改新类中现有属性或方法的行为,只需通过在新类中再次声明来覆盖它:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function __destruct()

{

echo 'The class "', __CLASS__, '" was destroyed.<br />';

}

public function __toString()

{

echo "Using the toString method: ";

return $this->getProperty();

}

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

class MyOtherClass extends MyClass

{

public function __construct()

{

echo "A new constructor in " . __CLASS__ . ".<br />";

}

public function newMethod(): string

{

return "From a new method in " . __CLASS__ . ".<br />";

}

}

// Create a new object

$newobj = new MyOtherClass;

// Output the object as a string

echo $newobj->newMethod();

// Use a method from the parent class

echo $newobj->getProperty();

?>

这会将浏览器中的输出更改为

A new constructor in MyOtherClass.

From a new method in MyOtherClass.

I’m a class property!

The class "MyClass" was destroyed.

覆盖方法时保留原始方法功能

要向继承的方法添加新功能,同时保持原始方法不变,请使用带有范围解析操作符(::)的parent关键字:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function __destruct()

{

echo 'The class "', __CLASS__, '" was destroyed.<br />';

}

public function __toString()

{

echo "Using the toString method: ";

return $this->getProperty();

}

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

class MyOtherClass extends MyClass

{

public function __construct()

{

parent::__construct(); // Call the parent class’s constructor

echo "A new constructor in " . __CLASS__ . ".<br />";

}

public function newMethod(): string

{

return "From a new method in " . __CLASS__ . ".<br />";

}

}

// Create a new object

$newobj = new MyOtherClass;

// Output the object as a string

echo $newobj->newMethod();

// Use a method from the parent class

echo $newobj->getProperty();

?>

这将输出父构造函数和新类的构造函数的结果:

The class "MyClass" was initiated!

A new constructor in MyOtherClass.

From a new method in MyOtherClass.

I’m a class property!

The class "MyClass" was destroyed.

分配属性和方法的可见性

为了增加对对象的控制,方法和属性被赋予可见性。这控制了如何以及从哪里访问属性和方法。可见性关键字有三个:publicprotectedprivate。除了它的可见性,一个方法或属性可以被声明为static,这允许它们在没有类的实例化的情况下被访问。

公共属性和方法

到目前为止,您使用的所有方法和属性都是public。这意味着可以在任何地方访问它们,包括在类内部和外部。

受保护的属性和方法

当一个属性或方法被声明为protected时,它只能在类本身或在子类(扩展包含受保护方法的类的类)中被访问。

MyClass中将getProperty()方法声明为protected,并尝试从类外部直接访问它,如下所示:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function __destruct()

{

echo 'The class "', __CLASS__, '" was destroyed.<br />';

}

public function __toString()

{

echo "Using the toString method: ";

return $this->getProperty();

}

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

protected function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

class MyOtherClass extends MyClass

{

public function __construct()

{

parent::__construct();

echo "A new constructor in " . __CLASS__ . ".<br />";

}

public function newMethod(): string

{

return "From a new method in " . __CLASS__ . ".<br />";

}

}

// Create a new object

$newobj = new MyOtherClass;

// Attempt to call a protected method

echo $newobj->getProperty();

?>

尝试运行该脚本时,会出现以下错误:

The class "MyClass" was initiated!

A new constructor in MyOtherClass.

Fatal error``: Uncaught Error: Call to protected method MyClass::getProperty() from

context '' in C:\wamp\www\book\testing\test.php:54 Stack trace: #0 {main} thrown A978-1-4842-1230-1_3_Figa_HTML.jpg

in``C:\wamp\www\book\testing\test.php``on line

现在,在MyOtherClass中创建一个新方法来调用getProperty()方法:

<?php

declare(strict_types=1);

class MyClass

{

public $prop1 = "I’m a class property!";

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function __destruct()

{

echo 'The class "', __CLASS__, '" was destroyed.<br />';

}

public function __toString()

{

echo "Using the toString method: ";

return $this->getProperty();

}

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

protected function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

class MyOtherClass extends MyClass

{

public function __construct()

{

parent::__construct();

echo "A new constructor in " . __CLASS__ . ".<br />";

}

public function newMethod(): string

{

return "From a new method in " . __CLASS__ . ".<br />";

}

public function callProtected(): string

{

return $this->getProperty();

}

}

// Create a new object

$newobj = new MyOtherClass;

// Call the protected method from within a public method

echo $newobj->callProtected();

?>

这将产生预期的结果:

The class "MyClass" was initiated!

A new constructor in MyOtherClass.

I’m a class property!

The class "MyClass" was destroyed.

私有属性和方法

声明为private的属性或方法只能从定义它的类中访问。该类之外的任何代码都不能直接访问该属性或方法。

为了演示这一点,最简单的方法是回到第一个例子,简单地在MyClass中将$prop1声明为private,然后尝试运行修改后的代码,如下所示:

<?php

declare(strict_types=1);

class MyClass

{

private $prop1 = "I’m a class property!";

}

$obj = new MyClass;

echo $obj->prop1;

?>

重新加载浏览器,出现以下错误:

Fatal error``: Uncaught Error: Cannot access private property MyClass::$prop1

in C:\wamp\www\book\testing\test.php:13 Stack trace: #0 {main} thrown A978-1-4842-1230-1_3_Figa_HTML.jpg

in``C:\wamp\www\book\testing\test.php``on line

通过保持成员属性private来限制对它们的访问通常被认为是良好的 OOP 实践。管理类属性的通常方法是提供public方法来设置和获取它们的值,正如您在上面看到的那样。

这里的区别在于,通过保留类属性private,您只通过方法强制访问。这种类属性的封装使您的代码更健壮,更易于维护。通过向客户端代码提供方法,您可以更好地控制类属性的完整性,并且可以在不破坏客户端代码的情况下自由地重新设计类的内部结构。

下面是恢复了访问属性的公共方法的示例:

<?php

declare(strict_types=1);

class MyClass

{

private $prop1 = "I’m a class property!";

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

public function getProperty(): string

{

return $this->prop1 . "<br />";

}

}

$obj = new MyClass;

echo $obj->getProperty();

$obj->setProperty("Now I am different!");

echo $obj->getProperty();

?>

这将生成以下输出:

I’m a class property!

Now I am different!

这段代码的工作方式和以前非常相似,但是增加了完全封装类属性的好处。类方法也可以被声明为private,就像我们偶尔对“helper”方法所做的那样,它的访问被限制在同一个类中的其他方法。但是private可见性更常用于类属性,如本例所示。我们通常会遵循这种模式前进。

静态属性和方法

声明为static的方法或属性无需首先实例化该类即可访问;您只需提供类名、范围解析操作符以及属性或方法名。

使用静态属性的一个主要好处是,它们在脚本运行期间保持其存储的值。这意味着,如果您修改了一个静态属性,并在稍后的脚本中访问它,修改后的值仍将被存储。

为了演示这一点,回到完整的示例,向MyClass添加一个名为$count的私有静态属性、一个名为getCount()的公共访问器和一个名为plusOne()的静态方法。然后设置一个do...while循环,只要值小于10就输出$count的递增值,像这样:

<?php

declare(strict_types=1);

class MyClass

{

private $prop1 = "I’m a class property!";

private static $count = 0;

public function __construct()

{

echo 'The class "', __CLASS__, '" was initiated!<br />';

}

public function __destruct()

{

echo 'The class "', __CLASS__, '" was destroyed.<br />';

}

public function __toString()

{

echo "Using the toString method: ";

return $this->getProperty();

}

public function setProperty(string $newval)

{

$this->prop1 = $newval;

}

protected function getProperty(): string

{

return $this->prop1 . "<br />";

}

public static function getCount(): int

{

return self::$count;

}

public static function plusOne()

{

echo "The count is " . ++self::$count . ".<br />";

}

}

class MyOtherClass extends MyClass

{

public function __construct()

{

parent::__construct();

echo "A new constructor in " . __CLASS__ . ".<br />";

}

public function newMethod(): string

{

return "From a new method in " . __CLASS__ . ".<br />";

}

public function callProtected()

{

return $this->getProperty();

}

}

do

{

// Call plusOne without instantiating MyClass

MyClass::plusOne();

} while ( MyClass::getCount() < 10 );

?>

Note

当访问静态属性时,美元符号($)跟在范围解析运算符之后。

当您在浏览器中加载此脚本时,会输出以下内容:

The count is 1.

The count is 2.

The count is 3.

The count is 4.

The count is 5.

The count is 6.

The count is 7.

The count is 8.

The count is 9.

The count is 10.

用文档块注释

虽然不是 OOP 的正式部分,但文档块注释风格是一种被广泛接受的记录类的方法。除了为开发人员提供一个编写代码时使用的标准,它还被许多最流行的 SDK(软件开发工具包)所采用,如 Eclipse(在 http://eclipse.org 可用)和 NetBeans(在 http://netbeans.org 可用),并将用于生成代码提示。

使用以附加星号开头的块注释来定义 DocBlock:

/**

* This is a very basic DocBlock

*/

DocBlocks 的真正强大之处在于它能够使用标记,标记以一个 at 符号(@)开始,紧接着是标记名和标记值。这些允许开发人员定义文件的作者、类的许可、属性或方法信息以及其他有用的信息。

最常见的标签如下:

  • @author:当前元素的作者(可能是一个类、文件、方法或任何一段代码)使用这个标签列出。如果不止一个作者被认可,则在同一个文档块中可以使用多个作者标签。作者姓名的格式为John Doe <john.doe@email.com>
  • @copyright:这表示当前元素的版权年份和版权所有者的名字。格式为2010 Copyright Holder
  • @license:链接到当前元素的许可。许可证信息的格式为http://www.example.com/path/to/license.txt License Name
  • @var:保存变量或类属性的类型和描述。格式为type element description
  • @param:该标签显示函数或方法参数的类型和描述。格式为type $element_name element description
  • @return:该标签提供了函数或方法返回值的类型和描述。格式为type return element description

用 DocBlocks 注释的示例类可能如下所示:

<?php

declare(strict_types=1);

/**

* A simple class

*

* This is the long description for this class,

* which can span as many lines as needed. It is

* not required, whereas the short description is

* necessary.

*

* It can also span multiple paragraphs if the

* description merits that much verbiage.

*

* @author Jason Lengstorf <jason.lengstorf@ennuidesign.com>

* @copyright 2010 Ennui Design

* @licensehttp://www.php.net/license/3_01.txt

*/

class SimpleClass

{

/**

* A private variable

*

* @var string stores data for the class

*/

private $foo;

/**

* Sets $foo to a new value upon class instantiation

*

* @param string $val a value required for the class

* @return void

*/

public function __construct(string $val)

{

$this->foo = $val;

}

/**

* Multiplies two integers

*

* Accepts a pair of integers and returns the

* product of the two.

*

* @param int $bat a number to be multiplied

* @param int $baz a number to be multiplied

* @return int the product of the two parameters

*/

public function bar(int $bat, int $baz): int

{

return $bat * $baz;

}

}

?>

一旦浏览了前面的类,DocBlock 的好处就显而易见了:所有的东西都被清晰地定义了,这样下一个开发人员就可以拿起代码,而不必担心一段代码是做什么的,或者它应该包含什么。

Note

有关文档块的更多信息,请参见 http://en.wikipedia.org/wiki/PHPDoc

比较面向对象代码和过程代码

写代码没有正确和错误的方式。也就是说,本节概述了在软件开发中采用面向对象方法的有力论据,尤其是在大型应用中。

易于实施

虽然一开始可能会令人望而生畏,但 OOP 实际上提供了一种更简单的处理数据的方法。因为对象可以在内部存储数据,所以变量不需要在函数之间传递才能正常工作。

此外,因为同一个类的多个实例可以同时存在,所以处理大型数据集要容易得多。例如,假设您正在处理一个文件中两个人的信息。他们需要姓名、职业和年龄。

程序方法

下面是这个例子的程序方法:

<?php

declare(strict_types=1);

function changeJob(array $person, string $newjob): array

{

$person['job'] = $newjob; // Change the person’s job

return $person;

}

function happyBirthday(array $person): array

{

++$person['age']; // Add 1 to the person’s age

return $person;

}

$person1 = array(

'name' => 'Tom',

'job' => 'Button-Pusher',

'age' => 34

);

$person2 = array(

'name' => 'John',

'job' => 'Lever-Puller',

'age' => 41

);

// Output the starting values for the people

echo "<pre>Person 1: ", print_r($person1, TRUE), "</pre>";

echo "<pre>Person 2: ", print_r($person2, TRUE), "</pre>";

// Tom got a promotion and had a birthday

$person1 = changeJob($person1, 'Box-Mover');

$person1 = happyBirthday($person1);

// John just had a birthday

$person2 = happyBirthday($person2);

// Output the new values for the people

echo "<pre>Person 1: ", print_r($person1, TRUE), "</pre>";

echo "<pre>Person 2: ", print_r($person2, TRUE), "</pre>";

?>

执行时,该代码输出以下内容:

Person 1: Array

(

[name] => Tom

[job] => Button-Pusher

[age] => 34

)

Person 2: Array

(

[name] => John

[job] => Lever-Puller

[age] => 41

)

Person 1: Array

(

[name] => Tom

[job] => Box-Mover

[age] => 35

)

Person 2: Array

(

[name] => John

[job] => Lever-Puller

[age] => 42

)

虽然这段代码不一定不好,但是在编码的时候有很多需要记住的地方。受影响人员的属性数组必须从每个函数调用中传递和返回,这就为错误留下了余地。

为了简化这个例子,最好是留给开发人员尽可能少的事情。只有当前操作绝对必要的信息才需要传递给函数。

这就是 OOP 介入并帮助你清理的地方。

面向对象的方法

下面是这个例子的 OOP 方法:

<?php

declare(strict_types=1);

class Person

{

private $_name;

private $_job;

private $_age;

public function __construct(string $name, string $job, int $age)

{

$this->_name = $name;

$this->_job = $job;

$this->_age = $age;

}

public function changeJob(string $newjob)

{

$this->_job = $newjob;

}

public function happyBirthday()

{

++$this->_age;

}

}

// Create two new people

$person1 = new Person("Tom", "Button-Pusher", 34);

$person2 = new Person("John", "Lever Puller", 41);

// Output their starting point

echo "<pre>Person 1: ", print_r($person1, TRUE), "</pre>";

echo "<pre>Person 2: ", print_r($person2, TRUE), "</pre>";

// Give Tom a promotion and a birthday

$person1->changeJob("Box-Mover");

$person1->happyBirthday();

// John just gets a year older

$person2->happyBirthday();

// Output the ending values

echo "<pre>Person 1: ", print_r($person1, TRUE), "</pre>";

echo "<pre>Person 2: ", print_r($person2, TRUE), "</pre>";

?>

这将在浏览器中输出以下内容:

Person 1: Person Object

(

[_name:Person:private] => Tom

[_job:Person:private] => Button-Pusher

[_age:Person:private] => 34

)

Person 2: Person Object

(

[_name:Person:private] => John

[_job:Person:private] => Lever Puller

[_age:Person:private] => 41

)

Person 1: Person Object

(

[_name:Person:private] => Tom

[_job:Person:private] => Box-Mover

[_age:Person:private] => 35

)

Person 2: Person Object

(

[_name:Person:private] => John

[_job:Person:private] => Lever Puller

[_age:Person:private] => 42

)

要使这种方法面向对象,还需要一点点的设置,但是在定义了类之后,创建和修改人是轻而易举的事情;一个人的信息不需要从方法中传递或返回,只有绝对必要的信息才会传递给每个方法。

在小范围内,这种差异可能看起来不太大,但是随着您的应用规模的增长,如果实现得当,OOP 将显著减少您的工作量。

Tip

并非所有东西都需要面向对象。在应用内部的一个地方处理一些小事情的快速函数不一定需要包装在一个类中。当在面向对象和过程方法之间做出决定时,使用你的最佳判断。

更好的组织

OOP 的另一个好处是它非常容易打包和编目。每个类通常可以保存在自己单独的文件中,如果使用统一的命名约定,访问这些类就非常简单。

假设您有一个包含 150 个类的应用,这些类是通过应用文件系统根目录下的控制器文件动态调用的。所有 150 个类都遵循命名约定class. classname .inc.php,并驻留在应用的inc文件夹中。

控制器可以实现 PHP 的__autoload()函数,在调用时只动态地引入它需要的类,而不是将所有 150 个类都包含在控制器文件中以防万一,或者想出一些聪明的方法将这些文件包含在您自己的代码中:

<?php

function __autoload($class_name)

{

include_once 'inc/class.' . $class_name . '.inc.php';

}

?>

将每个类放在一个单独的文件中也使得代码更容易移植,更容易在新的应用中重用,而不需要大量的复制和粘贴。

更容易维护

由于 OOP 在正确执行时更紧凑的特性,代码中的变化通常比冗长的、杂乱无章的过程化实现更容易发现和做出。

如果一个特定的信息数组获得了一个新的属性,一个程序性的软件可能需要(在最坏的情况下)将新的属性添加到使用该数组的每个函数中。

一个 OOP 应用可以很容易地通过添加新的属性,然后添加处理该属性的方法来更新。

本节中提到的许多好处都是 OOP 与干编程实践相结合的产物。创建易于维护且不会引起噩梦的过程化代码是绝对可能的,创建糟糕的面向对象代码也是同样可能的。这本书将试图展示良好的编码习惯与 OOP 的结合,以生成易于阅读和维护的干净代码。

摘要

至此,您应该对面向对象的编程风格感到满意了。事件日历后端的整个核心将基于 OOP,所以任何目前看起来不清楚的概念都将被更彻底地检查,因为本章的概念将被放入一个实际的、真实世界的例子中。

在下一章中,您将开始构建事件日历的后端。

四、创建活动日历

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​4) contains supplementary material, which is available to authorized users.

既然您已经熟悉了面向对象编程的概念,那么您可以开始从事本书的核心项目:事件日历。一切都从这里开始,随着本书的进展,您将使用 PHP 和 jQuery 添加越来越多的功能。

规划日历

因为您完全是从零开始,所以您需要花一分钟来规划应用。这个应用将是数据库驱动的(使用 MySQL),因此规划将分两个阶段进行:首先是数据库结构,然后是将访问和修改数据库的应用的基本图。

定义数据库结构

为了使构建应用变得更加容易,首先应该计划如何存储数据。这塑造了应用中的一切。

对于基本事件日历,您需要存储的所有信息如下:

  • event_id:自动递增的整数,唯一标识每个事件
  • event_title:事件的标题
  • 事件的完整描述
  • event_start:事件的开始时间(格式YYYY-MM-DD HH:MM:SS)
  • event_end:事件的结束时间(格式YYYY-MM-DD HH:MM:SS)

创建类别映射

下一步是布置主类,它将处理应用将执行的与日历事件相关的所有操作。这个类将被称为Calendar;这些方法和属性将如下所示:

  • 构建构造函数。
  • 请确保数据库连接存在或创建一个。
  • 设置以下基本属性:
    • 数据库对象
    • 要使用的日期
    • 正在查看的月份
    • 要查看的年份
    • 一个月中的天数
    • 一个月开始的工作日
  • 生成 HTML 来构建事件表单。
    • 检查事件是否正在被编辑或创建。
    • 如果需要编辑,将事件信息加载到表单中。
  • 在数据库中保存新事件并整理输入。
  • 从数据库中删除事件并确认删除。
  • 加载事件信息。
    • 从数据库加载事件数据。
    • 将每个事件作为一个数组存储在该月的适当日期。
  • 输出带有日历信息的 HTML。使用 events 数组,遍历该月的每一天,并在适用的地方附加事件标题和时间。
  • 将事件信息显示为 HTML。接受事件 ID 并加载事件的描述和详细信息

规划应用的文件夹结构

这个应用在完成后会变得有些复杂,所以值得花几分钟来考虑如何组织文件。

为了安全起见,所有可能的东西都将放在 web 根目录或公共文件夹之外:这包括数据库凭证、应用的核心以及运行它的类。由于 web 根目录中没有任何内容,恶意用户将无法在不在服务器上的情况下查看您的文件夹结构,这是一个很好的安全做法。

首先,您有两个文件夹:public包含应用用户可以直接访问的所有文件,比如 CSS、索引文件和 JavaScript 文件,而sys包含非公共文件,比如数据库凭证、应用的类和核心 PHP 文件。

公共文件

public文件夹将作为网络根目录。当用户访问您的应用的 URL 时,这是服务器将首先查找的文件夹。在根级别,它包含用户用来查看和操作存储在数据库中的数据的文件:

  • index.php:这是主文件,以日历格式显示月份,事件标题显示在事件发生当天的方框中。
  • view.php:如果用户点击一个事件标题,他们会被带到这个页面,在这里会显示事件的详细数据。
  • admin.php:创建或修改新事件,使用本页显示的表格。
  • confirmdelete.php:要删除一个事件,用户必须首先通过在此页面提交确认表单来确认该选择。

public文件夹还将有一个名为assets的子文件夹,其中将包含该站点的附加文件。这些文件将根据它们的用途进行分组,在本节中分为四类:通用文件、CSS 文件、JavaScript 文件和表单处理文件。

在资产中创建四个文件夹,分别名为commoncssincjscommon文件夹将存储将在所有可公开访问的页面上使用的文件(即应用的页眉和页脚);css文件夹将存储站点样式表;inc文件夹将存储文件以处理表单提交的输入;而js文件夹将存储站点 JavaScript 文件。

非公共应用文件

sys文件夹将被分成三个子文件夹:class,它将存储应用的所有类文件(如Calendar类);config,存储数据库凭证等应用配置信息;和core,它保存初始化应用的文件。

当一切都组织好了,所有文件都创建好了,文件结构就组织好了,以后就容易伸缩了(见图 4-1 )。

A978-1-4842-1230-1_4_Fig1_HTML.jpg

图 4-1。

The folder structure and files as they appear in Eclipse 4.5 (Mars) on Windows Public and Nonpublic Folders—Why Bother?

你现在可能会问自己,“为什么要花额外的精力来创建公共和非公共文件夹呢?有什么好处?”

要回答这个问题,您需要了解一点 web 服务器的工作原理。服务器本质上是存储文件并使用网络标识符(IP 地址或映射到 IP 地址的 URL)向网络(如万维网)提供选定文件的计算机。一台服务器上可以托管数百个网站或其他应用,每个网站或应用都有自己的文件夹。

服务器允许外部用户访问这些公共文件夹,这意味着可以从服务器所连接的网络访问文件夹中的所有文件。对于包含敏感信息的文件,这并不总是可取的。

幸运的是,公用文件夹中的文件仍然可以访问公用文件夹之外的文件,尽管网络上的用户不能。这允许您对外界隐藏您的敏感数据,但保持您的应用可以访问它。

还有其他方法来隐藏这些信息,但简单地保持敏感数据不公开是最直接、最可靠的方法。

修改开发环境

因为您在这个应用中使用了公共和非公共文件夹,所以有必要对您的开发环境进行快速修改:您需要将服务器指向您的公共文件夹,而不是包含两者的文件夹。在本节中,您将学习如何将您的服务器指向public文件夹。

Note

您可以跳过这一部分,将sys文件夹保留在 public 文件夹中,而不会丢失应用中的任何功能(请记住,文件路径将与本书练习中使用的路径不同)。但是,您将使应用面临潜在的安全风险。强烈建议您花一分钟时间来遵循这些步骤。

地方发展

要更改本地安装中的文档根目录(public文件夹),需要修改服务器的配置文件。本书假设 Apache 被用作 XAMPP 堆栈中的服务器,所以您需要定位httpd.conf文件(在 Mac 上位于/xamppfiles/etc/httpd.conf,在 Linux 上位于/opt/lampp/etc/httpd.conf,在 Windows 上位于C:\wamp\bin\apache\apache2.4.x\conf\httpd.conf)。

httpd.conf中,搜索DocumentRoot指令。这是你设置你的public文件夹路径的地方。该文件应该如下所示:

#

# DocumentRoot: The directory out of which you will serve your

# documents. By default, all requests are taken from this directory, but

# symbolic links and aliases may be used to point to other locations.

#

DocumentRoot "c:\wamp\www\"

此外,在您的httpd.conf文件中搜索引用文档根目录的一行来设置权限。它看起来会像这样:

<Directory "c:/wamp/www/">

找到并修改了上面的路径后,使用 XAMPP 控制面板重新启动 Apache。现在,访问的默认文件夹是应用的public文件夹。为了测试这一点,创建文件index.php并添加以下代码片段:

<?php echo "I’m the new document root!"; ?>

在浏览器中导航到开发环境的文档根目录(默认为localhost)以确保重新配置有效(参见图 4-2 )。

A978-1-4842-1230-1_4_Fig2_HTML.jpg

图 4-2。

The public folder’s index file is displayed after reconfiguring Apache

远程开发

因为远程开发通常发生在托管公司的服务器上,将您的域指向应用的public文件夹的步骤会因托管提供商而异,因此不会在本书中讨论。

但是,在许多情况下,主机会允许您将域指向您的主机帐户中的文件夹。如果是这种情况,只需将域指向public文件夹,一切都应该正常工作。

有些主机不允许文档根目录以外的访问。如果你的主机提供商就是这种情况,只需将sys文件夹放在public文件夹中,并相应地改变文件路径。

制作日历

文件夹结构准备好了,开发环境也设置好了,是时候真正开始开发了。我们将逐步介绍三个事件视图(主视图、单个事件视图和管理视图),从主日历视图开始。

创建数据库

与应用规划过程一样,开发应用的第一步是创建数据库。在您的本地开发环境中,打开 phpMyAdmin ( http://localhost/phpmyadmin在 XAMPP),并打开 SQL 选项卡(如果您没有使用 phpMyAdmin,也可以在 PHP 脚本中执行这些命令)。使用以下 SQL 创建数据库、存储事件数据的表events和一些虚拟条目:

CREATE DATABASE IF NOT EXISTS php-jquery_example``

DEFAULT CHARACTER SET utf8

COLLATE utf8_unicode_ci;

CREATE TABLE IF NOT EXISTS php-jquery_example.events (

``event_id INT(11) NOT NULL AUTO_INCREMENT,

``event_title VARCHAR(80) DEFAULT NULL,

``event_desc TEXT,

``event_start TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',

``event_end TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',

PRIMARY KEY (event_id),

INDEX (event_start)

) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_unicode_ci;

INSERT INTO php-jquery_example.events``

(event_title, event_desc, event_start, event_end) VALUES

('New Year’s Day', 'Happy New Year!',

'2016-01-01 00:00:00', '2016-01-01 23:59:59'),

('Last Day of January', 'Last day of the month! Yay!',

'2016-01-31 00:00:00', '2016-01-31 23:59:59');

Note

前面所有的命令都是 MySQL 特有的。由于这本书主要关注 jQuery 和 PHP,所以我们不会在这里详细介绍 MySQL。有关 MySQL 的更多信息,请查看 Jason Gilmore 的《PHP 和 MySQL 入门》。

在您执行了前面的命令之后,一个名为php-jquery_example的新数据库将出现在左边的列中。点击数据库名称以显示表格,然后点击events表格以查看您创建的条目(参见图 4-3 )。

A978-1-4842-1230-1_4_Fig3_HTML.jpg

图 4-3。

The database, table, and entries after they’re created

使用类连接到数据库

因为您将在这个应用中创建多个需要数据库访问的类,所以创建一个打开并存储该数据库对象的对象是有意义的。这个对象将被称为DB_Connect,它将驻留在名为class.db_connect.inc.php ( /sys/class/class.db_connect.inc.php)的类文件夹中。

这个类将有一个属性和一个方法,两者都受到保护。该属性将被称为$db,并将存储一个数据库对象。该方法将是一个构造函数;这将接受一个可选的数据库对象存储在$db中,或者如果没有传递数据库对象,它将创建一个新的 PDO 对象。

将以下代码插入class.db_connect.inc.php:

<?php

declare(strict_types=1);

/**

* Database actions (DB access, validation, etc.)

*

* PHP version 7

*

* LICENSE: This source file is subject to the MIT License, available

* athttp://www.opensource.org/licenses/mit-license.html

*

* @author     Jason Lengstorf <jason.lengstorf@ennuidesign.com>

* @copyright  2009 Ennui Design

* @licensehttp://www.opensource.org/licenses/mit-license.html

*/

class DB_Connect {

/**

* Stores a database object

*

* @var object A database object

*/

protected $db;

/**

* Checks for a DB object or creates one if one isn’t found

*

* @param object $db A database object

*/

protected function __construct($db=NULL)

{

if ( is_object($db) )

{

$this->db = $db;

}

else

{

// Constants are defined in /sys/config/db-cred.inc.php

$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME;

try

{

$this->db = new PDO($dsn, DB_USER, DB_PASS);

}

catch ( Exception $e )

{

// If the DB connection fails, output the error

die ( $e->getMessage() );

}

}

}

}

?>

Note

前面的函数使用了尚未定义的常量。在下一节中,您将创建文件来定义这些常量。

创建类包装

为了构建应用本身,首先在非公共的sys文件夹(/sys/class/class.calendar.inc.php)中的class文件夹中创建文件class.calendar.inc.php。这个类将扩展DB_Connect类,以便访问数据库对象。在您选择的编辑器中打开该文件,并使用以下代码创建Calendar类:

<?php

declare(strict_types=1);

/**

* Builds and manipulates an events calendar

*

* PHP version 7

*

* LICENSE: This source file is subject to the MIT License, available

* athttp://www.opensource.org/licenses/mit-license.html

*

* @author     Jason Lengstorf <jason.lengstorf@ennuidesign.com>

* @copyright  2009 Ennui Design

* @licensehttp://www.opensource.org/licenses/mit-license.html

*/

class Calendar extends DB_Connect

{

// Methods and properties go here

}

?>

创建了类之后,就可以开始向该类添加属性和方法了。

添加类属性

Calendar类不需要任何公共属性,并且您不会在本书包含的示例中扩展它,所以所有的类属性都是私有的。

按照规划一节中的定义,为Calendar类创建属性:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

/**

* The date from which the calendar should be built

*

* Stored in YYYY-MM-DD HH:MM:SS format

*

* @var string the date to use for the calendar

*/

private $_useDate;

/**

* The month for which the calendar is being built

*

* @var int the month being used

*/

private $_m;

/**

* The year from which the month’s start day is selected

*

* @var int the year being used

*/

private $_y;

/**

* The number of days in the month being used

*

* @var int the number of days in the month

*/

private $_daysInMonth;

/**

* The index of the day of the week the month starts on (0-6)

*

* @var int the day of the week the month starts on

*/

private $_startDay;

// Methods go here

}

?>

Note

为了简洁起见,重复的代码片段中将不包含文档块。

根据原规划,班级性质如下:

  • $_useDate:以YYYY-MM-DD HH:MM:SS格式创建日历时使用的日期
  • $_m:建立日历时使用的月份
  • $_y:建立日历时使用的年份
  • 当前月份有多少天
  • $_startDay:从 0 到 6 的索引,代表一个月从星期几开始

构建构造函数

接下来,您可以构建类构造函数。首先声明它:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

/**

* Creates a database object and stores relevant data

*

* Upon instantiation, this class accepts a database object

* that, if not null, is stored in the object’s private $_db

* property. If null, a new PDO object is created and stored

* instead.

*

* Additional info is gathered and stored in this method,

* including the month from which the calendar is to be built,

* how many days are in said month, what day the month starts

* on, and what day it is currently.

*

* @param object $dbo a database object

* @param string $useDate the date to use to build the calendar

* @return void

*/

public function __construct($dbo=NULL, $useDate=NULL)

{

}

}

?>

构造函数将接受两个可选参数:第一个是数据库对象,第二个是构建日历显示的日期。

检查数据库连接

为了正常运行,该类需要一个数据库连接。构造函数将从DB_Connect调用父构造函数来检查现有的数据库对象,并在可用时使用它,或者如果没有提供对象,它将创建一个新对象。

使用粗体显示的代码设置调用以进行此检查:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL)

{

/*

* Call the parent constructor to check for

* a database object

*/

parent::__construct($dbo);

}

}

?>

Note

Calendar类构造函数接受一个可选的$dbo参数,该参数被依次传递给DB_Connect构造函数。这允许您创建一个数据库对象,并轻松地传递它以便在类中使用。

创建文件来存储数据库凭据

为了将数据库凭证与应用的其余部分分开,以便于维护,您需要使用一个配置文件。在config文件夹(/sys/config/db-cred.inc.php)中创建一个名为db-cred.inc.php的新文件。在内部,创建一个名为$C(用于常量)的数组,并将每条数据存储为一个新的键值对:

<?php

declare(strict_types=1);

/*

* Create an empty array to store constants

*/

$C = array();

/*

* The database host URL

*/

$C['DB_HOST'] = 'localhost';

/*

* The database username

*/

$C['DB_USER'] = 'root';

/*

* The database password

*/

$C['DB_PASS'] = '';

/*

* The name of the database to work with

*/

$C['DB_NAME'] = 'php-jquery_example';

?>

Note

$C初始化为一个空数组是防止任何被污染的数据被存储在$C中并被定义为常量的一种保护措施。这是一个好习惯,尤其是在处理敏感数据的时候。

保存此文件。如果您没有使用 XAMPP 或者修改了默认的数据库凭证,那么您需要在代码中替换您自己的主机、用户名、密码和数据库名称。

创建初始化文件

此时,您的数据库凭证仍然没有存储为常量。您将使用一个初始化文件来处理这个问题。

初始化文件为应用收集数据、加载文件和组织信息。在这个例子中,它将加载并定义所有必要的常量,创建一个数据库对象,并为类设置一个自动加载函数。其他功能将在以后必要时添加。

创建一个名为init.inc.php的文件,并将它放在core文件夹(/sys/core/init.inc.php)中。在内部,添加以下内容:

<?php

declare(strict_types=1);

/*

* Include the necessary configuration info

*/

include_once '../sys/config/db-cred.inc.php';

/*

* Define constants for configuration info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a PDO object

*/

$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME;

$dbo = new PDO($dsn, DB_USER, DB_PASS);

/*

* Define the auto-load function for classes

*/

function __autoload($class)

{

$filename = "../sys/class/class." . $class . ".inc.php";

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

当一个脚本试图实例化一个还没有被加载的类时,一个自动加载函数被调用。这是按需将类轻松加载到脚本中的一种便捷方式。有关自动加载的更多信息,请访问 http://php.net/autoload

创建一个索引文件来把所有的内容放在一起

要查看所有活动,请修改public文件夹中的index.php。在内部,只需包含初始化文件并实例化Calendar类。接下来,检查类是否正确加载,如果正确,则输出对象的结构:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar for January

*/

$cal = new Calendar($dbo, "2016-01-01 12:00:00");

if ( is_object ($cal) )

{

echo "<pre>", var_dump($cal), "</pre>";

}

?>

导航到http://localhost/后,会输出以下消息:

object(Calendar)#2 (6) {

["_useDate":"Calendar":private]=>

NULL

["_m":"Calendar":private]=>

NULL

["_y":"Calendar":private]=>

NULL

["_daysInMonth":"Calendar":private]=>

NULL

["_startDay":"Calendar":private]=>

NULL

["db":protected]=>

object(PDO)#1 (0) {

}

}

设置基本属性

有了所有的基础设施,您就可以继续完成Calendar类的构造函数了。

检查完数据库对象后,构造函数需要存储几条关于构建日历的月份的数据。

首先,它检查是否向构造函数传递了日期;如果是,则存储在$_useDate属性中;否则,将使用当前日期。

接下来,日期被转换成 UNIX 时间戳(从 Unix epoch 开始的秒数;在 http://en.wikipedia.org/wiki/Unix_time 阅读更多相关内容)之前,月和年被分别提取并存储在$_m$_y中。

最后,$_m$_y用于确定一个月中有多少天被使用,以及该月从星期几开始。

下面的粗体代码将此功能添加到构造函数中:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL)

{

/*

* Call the parent constructor to check for

* a database object

*/

parent::__construct($dbo);

/*

* Gather and store data relevant to the month

*/

if ( isset($useDate) )

{

$this->_useDate = $useDate;

}

else

{

$this->_useDate = date('Y-m-d H:i:s');

}

/*

* Convert to a timestamp, then determine the month

* and year to use when building the calendar

*/

$ts = strtotime($this->_useDate);

$this->_m = (int)date('m', $ts);

$this->_y = (int)date('Y', $ts);

/*

* Determine how many days are in the month

*/

$this->_daysInMonth = cal_days_in_month(

CAL_GREGORIAN,

$this->_m,

$this->_y

);

/*

* Determine what weekday the month starts on

*/

$ts = mktime(0, 0, 0, $this->_m, 1, $this->_y);

$this->_startDay = (int)date('w', $ts);

}

}

?>

现在,当您重新加载http://localhost/时,所有先前为NULL的属性都将具有值:

object(Calendar)#2 (6) {

["_useDate":"Calendar":private]=>

string(19) "2016-01-01 12:00:00"

["_m":"Calendar":private]=>

int(1)

["_y":"Calendar":private]=>

int(2016)

["_daysInMonth":"Calendar":private]=>

int(31)

["_startDay":"Calendar":private]=>

int(5)

["db":protected]=>

object(PDO)#1 (0) {

}

}

加载事件数据

要加载关于事件的数据,您需要创建一个新的方法来访问数据库并检索它们。因为可以通过两种方式访问事件数据(第二种方式将在本章后面讨论),所以加载数据的动作将保持通用,以便于重用。

这个方法是私有的,命名为_loadEventData()。它接受一个可选参数,即事件的 ID,并按照以下步骤加载事件:

  • 创建一个基本的SELECT查询,从 events 表中加载可用字段。
  • 检查是否传递了一个 ID,如果是,向查询中添加一个WHERE子句以只返回一个事件。
  • 否则,请执行以下两项操作:
    • 找出该月第一天的午夜和该月最后一天的晚上 11:59:59。
    • 添加一个WHERE...BETWEEN子句,只加载当前月份内的日期。
  • 执行查询。
  • 返回结果的关联数组。

综合起来,这个方法看起来是这样的:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

/**

* Loads event(s) info into an array

*

* @param int $id an optional event ID to filter results

* @return array an array of events from the database

*/

private function _loadEventData($id=NULL)

{

$sql = "SELECT

``event_id, event_title, event_desc,

event_start`, `event_end

FROM events";

/*

* If an event ID is supplied, add a WHERE clause

* so only that event is returned

*/

if ( !empty($id) )

{

$sql .= "WHERE event_id=:id LIMIT 1";

}

/*

* Otherwise, load all events for the month in use

*/

else

{

/*

* Find the first and last days of the month

*/

$start_ts = mktime(0, 0, 0, $this->_m, 1, $this->_y);

$end_ts = mktime(23, 59, 59, $this->_m+1, 0, $this->_y);

$start_date = date('Y-m-d H:i:s', $start_ts);

$end_date = date('Y-m-d H:i:s', $end_ts);

/*

* Filter events to only those happening in the

* currently selected month

*/

$sql .= "WHERE event_start``

BETWEEN '$start_date'

AND '$end_date'

ORDER BY event_start";

}

try

{

$stmt = $this->db->prepare($sql);

/*

* Bind the parameter if an ID was passed

*/

if ( !empty($id) )

{

$stmt->bindParam(":id", $id, PDO::PARAM_INT);

}

$stmt->execute();

$results = $stmt->fetchAll(PDO::FETCH_ASSOC);

$stmt->closeCursor();

return $results;

}

catch ( Exception $e )

{

die ( $e->getMessage() );

}

}

}

?>

Note

为了简洁起见,不引用的方法是折叠的。

该方法返回一个数组,当使用您先前输入到数据库中的测试条目时,该数组如下所示:

Array

(

[0] => Array

(

[event_id] => 1

[event_title] => New Year’s Day

[event_desc] => Happy New Year!

[event_start] => 2016-01-01 00:00:00

[event_end] => 2016-01-01 23:59:59

)

[1] => Array

(

[event_id] => 2

[event_title] => Last Day of January

[event_desc] => Last day of the month! Yay!

[event_start] => 2016-01-31 00:00:00

[event_end] => 2016-01-31 23:59:59

)

)

创建在日历中使用的事件对象数组

_loadEventData()的原始输出不能立即在日历中使用。因为事件需要在正确的日期显示,所以从_loadEventData()检索的事件需要按照事件发生的日期进行分组。为了便于参考,事件字段也将被简化。

最终目标是一个事件数组,该数组使用一个月中的某一天作为索引,将每个事件作为一个对象。当新方法完成时,数据库中的两个测试条目最终应该像这样存储:

Array

(

[1] => Array

(

[0] => Event Object

(

[id] => 1

[title] => New Year’s Day

[description] => Happy New Year!

[start] => 2016-01-01 00:00:00

[end] => 2016-01-01 23:59:59

)

)

[31] => Array

(

[0] => Event Object

(

[id] => 2

[title] => Last Day of January

[description] => Last day of the month! Yay!

[start] => 2016-01-31 00:00:00

[end] => 2016-01-31 23:59:59

)

)

)

创建事件类

为此,您必须首先在类文件夹(/sys/class/class.event.inc.php)中创建一个名为Event的新类。它将有五个公共属性($id$title$description$start$end)和一个构造函数,该构造函数将使用数据库查询返回的关联数组来设置这些属性。创建文件,并在其中插入以下代码:

<?php

declare(strict_types=1);

/**

* Stores event information

*

* PHP version 7

*

* LICENSE: This source file is subject to the MIT License, available

* athttp://www.opensource.org/licenses/mit-license.html

*

* @author     Jason Lengstorf <jason.lengstorf@ennuidesign.com>

* @copyright  2010 Ennui Design

* @licensehttp://www.opensource.org/licenses/mit-license.html

*/

class Event

{

/**

* The event ID

*

* @var int

*/

public $id;

/**

* The event title

*

* @var string

*/

public $title;

/**

* The event description

*

* @var string

*/

public $description;

/**

* The event start time

*

* @var string

*/

public $start;

/**

* The event end time

*

* @var string

*/

public $end;

/**

* Accepts an array of event data and stores it

*

* @param array $event Associative array of event data

* @return void

*/

public function __construct($event)

{

if ( is_array($event) )

{

$this->id = $event['event_id'];

$this->title = $event['event_title'];

$this->description = $event['event_desc'];

$this->start = $event['event_start'];

$this->end = $event['event_end'];

}

else

{

throw new Exception("No event data was supplied.");

}

}

}

?>

创建在数组中存储事件对象的方法

既然每个事件都可以存储为一个对象,那么您可以创建一个方法,该方法将循环遍历可用的事件,并将它们存储在一个与事件发生日期相对应的数组中。首先,使用_loadEventData()从数据库加载事件数据。接下来,从每个事件的开始日期中提取一个月中的某一天,并在该天的索引处向数组中添加一个新值。在Calendar类中,创建一个名为_createEventObj()的新方法,并将其设置为私有。从数据库加载事件,并使用以下粗体代码创建新数组:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

private function _loadEventData($id=NULL) {...}

/**

* Loads all events for the month into an array

*

* @return array events info

*/

private function _createEventObj()

{

/*

* Load the events array

*/

$arr = $this->_loadEventData();

/*

* Create a new array, then organize the events

* by the day of the month on which they occur

*/

$events = array();

foreach ( $arr as $event )

{

$day = date('j', strtotime($event['event_start']));

try

{

$events[$day][] = new Event($event);

}

catch ( Exception $e )

{

die ( $e->getMessage() );

}

}

return $events;

}

}

?>

现在可以加载和组织事件,这样输出实际日历的方法 HTML 可以很容易地将日期放在适当的位置。

输出 HTML 以显示日历和事件

至此,您已经建立了数据库,存储了测试事件,并准备好了将事件数据加载和组织到一个易于使用的数组中的方法。您已经准备好将这些碎片放在一起并创建一个日历了!

日历将由一个名为buildCalendar()的公共方法构建。这将生成具有以下属性的日历:

  • 显示月份和年份的标题
  • 工作日缩写,使日历看起来像日历
  • 包含给定日期存在的事件的编号框

首先,在Calendar类中声明buildCalendar()方法,并在H2元素中创建标题。此外,创建一个工作日缩写数组,并遍历它们以生成一个无序列表。为此,添加以下粗体代码:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

/**

* Returns HTML markup to display the calendar and events

*

* Using the information stored in class properties, the

* events for the given month are loaded, the calendar is

* generated, and the whole thing is returned as valid markup.

*

* @return string the calendar HTML markup

*/

public function buildCalendar()

{

/*

* Determine the calendar month and create an array of

* weekday abbreviations to label the calendar columns

*/

$cal_month = date('F Y', strtotime($this->_useDate));

define('WEEKDAYS', array('Sun', 'Mon', 'Tue',

'Wed', 'Thu', 'Fri', 'Sat'));

/*

* Add a header to the calendar markup

*/

$html = "\n\t<h2>$cal_month</h2>";

for ( $d=0, $labels=NULL; $d<7; ++$d )

{

$labels .= "\n\t\t<li>" . WEEKDAYS[$d] . "</li>";

}

$html .= "\n\t<ul class=\"weekdays\">"

. $labels . "\n\t</ul>";

/*

* Return the markup for output

*/

return $html;

}

}

?>

修改索引文件

要查看buildCalendar()方法的输出,需要修改public文件夹中的index.php来调用该方法。用粗体显示的代码更新文件:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar for January

*/

$cal = new Calendar($dbo, "2016-01-01 12:00:00");

/*

* Display the calendar HTML

*/

echo $cal->buildCalendar();

?>

在浏览器中调出该文件,查看目前的结果(图 4-4 )。

A978-1-4842-1230-1_4_Fig4_HTML.jpg

图 4-4。

The heading and weekday abbreviations

制作日历

下一步是构建实际的日历日。要解决这个问题,需要完成几个步骤。

Create a new unordered list.   Set up a loop (with an iteration counter, a calendar date counter, today’s date, and the month and year stored as variables) that runs as long as the calendar date counter is less than the number of days in the month.   Add a fill class to the days of the week that occur before the first.   Add a today class if the current date is contained within the same month and year and matches the date being generated.   Create an opening and closing list item tag for each day.   Check if the current calendar box falls within the current month, and add the date if so.   Check if the current calendar box is a Saturday, and close the list and open a new one if so.   Assemble the pieces of the list item and append them to the markup.   After the loop, run another loop to add filler days until the calendar week is completed.   Close the final unordered list and return the markup.

首先,通过向buildCalendar()方法添加以下粗体代码来完成步骤 1 和 2:

public function buildCalendar()

{

/*

* Determine the calendar month and create an array of

* weekday abbreviations to label the calendar columns

*/

$cal_month = date('F Y', strtotime($this->_useDate));

define('WEEKDAYS', array('Sun', 'Mon', 'Tue',

'Wed', 'Thu', 'Fri', 'Sat'));

/*

* Add a header to the calendar markup

*/

$html = "\n\t<h2>$cal_month</h2>";

for ( $d=0, $labels=NULL; $d<7; ++$d )

{

$labels .= "\n\t\t<li>" . WEEKDAYS[$d] . "</li>";

}

$html .= "\n\t<ul class=\"weekdays\">"

. $labels . "\n\t</ul>";

/*

* Create the calendar markup

*/

$html .= "\n\t<ul>"; // Start a new unordered list

for ( $i=1, $c=1, $t=date('j'), $m=date('m'), $y=date('Y');

$c<=$this->_daysInMonth; ++$i )

{

// More steps go here

}

/*

* Return the markup for output

*/

return $html;

}

接下来,添加下面的粗体代码来完成步骤 3–5:

public function buildCalendar()

{

/*

* Determine the calendar month and create an array of

* weekday abbreviations to label the calendar columns

*/

$cal_month = date('F Y', strtotime($this->_useDate));

define('WEEKDAYS', array('Sun', 'Mon', 'Tue',

'Wed', 'Thu', 'Fri', 'Sat'));

/*

* Add a header to the calendar markup

*/

$html = "\n\t<h2>$cal_month</h2>";

for ( $d=0, $labels=NULL; $d<7; ++$d )

{

$labels .= "\n\t\t<li>" . WEEKDAYS[$d] . "</li>";

}

$html .= "\n\t<ul class=\"weekdays\">"

. $labels . "\n\t</ul>";

/*

* Create the calendar markup

*/

$html .= "\n\t<ul>"; // Start a new unordered list

for ( $i=1, $c=1, $t=date('j'), $m=date('m'), $y=date('Y');

$c<=$this->_daysInMonth; ++$i )

{

/*

* Apply a "fill" class to the boxes occurring before

* the first of the month

*/

$class = $i<=$this->_startDay ? "fill" : NULL;

/*

* Add a "today" class if the current date matches

* the current date

*/

if ( $c==$t``&&``$m==$this->_m``&&

{

$class = "today";

}

/*

* Build the opening and closing list item tags

*/

$ls = sprintf("\n\t\t<li class=\"%s\">", $class);

$le = "\n\t\t</li>";

// More steps go here

}

/*

* Return the markup for output

*/

return $html;

}

要完成步骤 6-10(实际构建日期,检查该周是否需要换行,组装日期标记,用 filler 完成最后一周,并返回标记),请添加以下粗体代码:

public function buildCalendar()

{

/*

* Determine the calendar month and create an array of

* weekday abbreviations to label the calendar columns

*/

$cal_month = date('F Y', strtotime($this->_useDate));

define('WEEKDAYS', array('Sun', 'Mon', 'Tue',

'Wed', 'Thu', 'Fri', 'Sat'));

/*

* Add a header to the calendar markup

*/

$html = "\n\t<h2>$cal_month</h2>";

for ( $d=0, $labels=NULL; $d<7; ++$d )

{

$labels .= "\n\t\t<li>" . WEEKDAYS[$d] . "</li>";

}

$html .= "\n\t<ul class=\"weekdays\">"

. $labels . "\n\t</ul>";

/*

* Create the calendar markup

*/

$html .= "\n\t<ul>"; // Start a new unordered list

for ( $i=1, $c=1, $t=date('j'), $m=date('m'), $y=date('Y');

$c<=$this->_daysInMonth; ++$i )

{

/*

* Apply a "fill" class to the boxes occurring before

* the first of the month

*/

$class = $i<=$this->_startDay ? "fill" : NULL;

/*

* Add a "today" class if the current date matches

* the current date

*/

if ( $c+1==$t && $m==$this->_m && $y==$this->_y )

{

$class = "today";

}

/*

* Build the opening and closing list item tags

*/

$ls = sprintf("\n\t\t<li class=\"%s\">", $class);

$le = "\n\t\t</li>";

/*

* Add the day of the month to identify the calendar box

*/

if ( $this->_startDay<$i``&&

{

$date = sprintf("\n\t\t\t<strong>%02d</strong>",$c++);

}

else { $date="``&

/*

* If the current day is a Saturday, wrap to the next row

*/

$wrap = $i!=0``&&

/*

* Assemble the pieces into a finished item

*/

$html .= $ls . $date . $le . $wrap;

}

/*

* Add filler to finish out the last week

*/

while ( $i%7!=1 )

{

$html .= "\n\t\t<li class=\"fill\">``&

++$i;

}

/*

* Close the final unordered list

*/

$html .= "\n\t</ul>\n\n";

/*

* Return the markup for output

*/

return $html;

}

测试现在的功能。图 4-5 显示了浏览器中的无序列表。

A978-1-4842-1230-1_4_Fig5_HTML.jpg

图 4-5。

The markup as generated by buildCalendar()

在日历中显示事件

将事件添加到日历显示中非常简单,只需从_createEventObj()加载events数组,并遍历索引中存储的与当前日期匹配的事件(如果存在的话)。使用以下粗体代码将事件数据添加到日历标记中:

public function buildCalendar()

{

/*

* Determine the calendar month and create an array of

* weekday abbreviations to label the calendar columns

*/

$cal_month = date('F Y', strtotime($this->_useDate));

define('WEEKDAYS', array('Sun', 'Mon', 'Tue',

'Wed', 'Thu', 'Fri', 'Sat'));

/*

* Add a header to the calendar markup

*/

$html = "\n\t<h2>$cal_month</h2>";

for ( $d=0, $labels=NULL; $d<7; ++$d )

{

$labels .= "\n\t\t<li>" . WEEKDAYS[$d] . "</li>";

}

$html .= "\n\t<ul class=\"weekdays\">"

. $labels . "\n\t</ul>";

/*

* Load events data

*/

$events = $this->_createEventObj();

/*

* Create the calendar markup

*/

$html .= "\n\t<ul>"; // Start a new unordered list

for ( $i=1, $c=1, $t=date('j'), $m=date('m'), $y=date('Y');

$c<=$this->_daysInMonth; ++$i )

{

/*

* Apply a "fill" class to the boxes occurring before

* the first of the month

*/

$class = $i<=$this->_startDay ? "fill" : NULL;

/*

* Add a "today" class if the current date matches

* the current date

*/

if ( $c+1==$t && $m==$this->_m && $y==$this->_y )

{

$class = "today";

}

/*

* Build the opening and closing list item tags

*/

$ls = sprintf("\n\t\t<li class=\"%s\">", $class);

$le = "\n\t\t</li>";

/*

* Add the day of the month to identify the calendar box

*/

if ( $this->_startDay<$i && $this->_daysInMonth>=$c)

{

/*

* Format events data

*/

$event_info = NULL; // clear the variable

if ( isset($events[$c]) )

{

foreach ( $events[$c] as $event )

{

$link = '<a href="view.php?event_id='

. $event->id . '">' . $event->title

. '</a>';

$event_info .= "\n\t\t\t$link";

}

}

$date = sprintf("\n\t\t\t<strong>%02d</strong>",$c++);

}

else { $date=" "; }

/*

* If the current day is a Saturday, wrap to the next row

*/

$wrap = $i!=0 && $i%7==0 ? "\n\t</ul>\n\t<ul>" : NULL;

/*

* Assemble the pieces into a finished item

*/

$html .= $ls . $date . $event_info . $le . $wrap;

}

/*

* Add filler to finish out the last week

*/

while ( $i%7!=1 )

{

$html .= "\n\t\t<li class=\"fill\"> </li>";

++$i;

}

/*

* Close the final unordered list

*/

$html .= "\n\t</ul>\n\n";

/*

* Return the markup for output

*/

return $html;

}

Caution

不要忘记将新的$event_info变量添加到循环底部的标记中!

当数据库事件加载到日历显示中时,标题显示在相应日期的旁边(参见图 4-6 )。

A978-1-4842-1230-1_4_Fig6_HTML.jpg

图 4-6。

An event title displayed next to the appropriate date Note

链接的事件标题指向一个尚不存在的名为view.php的文件。该文件将在本章后面的“输出 HTML 以显示完整的事件描述”一节中进行构建和解释。

使日历看起来像日历

此时,您的标记是正确的,您的事件也在那里,但是生成的代码看起来一点也不像日历。

为了纠正这一点,花点时间完成 HTML 标记,并使用 CSS 样式化页面。

Note

因为这本书不是讲 CSS 的,所以用的规则就不详细解释了。有关 CSS 的更多信息,请参阅 David Powers 的《CSS3 入门》。

简而言之,CSS 文件将执行以下操作:

  • 将每个列表项向左浮动。
  • 调整边距和边框,使日期看起来像传统日历。
  • 添加悬停效果,这样鼠标悬停的日子将被突出显示。
  • 事件标题样式。
  • 也为事件标题添加悬停效果。
  • 添加一些 CSS3 风格,包括圆角和阴影,以获得乐趣。

Tip

有关 CSS3 的更多信息,请访问 http://css3.info/

css文件夹(/public/assets/css/style.css)中创建一个名为style.css的新文件,并添加以下规则:

body {

background-color: #789;

font-family: georgia, serif;

font-size: 13px;

}

#content {

display: block;

width: 812px;

margin: 40px auto 10px;

padding: 10px;

background-color: #FFF;

-moz-border-radius: 6px;

-webkit-border-radius: 6px;

border-radius: 6px;

border:2px solid black;

-moz-box-shadow: 0 0 14px #123;

-webkit-box-shadow: 0 0 14px #123;

box-shadow: 0 0 14px #123;

}

h2,p {

margin: 0 auto 14px;

text-align: center;

}

ul {

display: block;

clear: left;

height: 82px;

width: 812px;

margin: 0 auto;

padding: 0;

list-style: none;

background-color: #FFF;

text-align: center;

border: 1px solid black;

border-top: 0;

border-bottom: 2px solid black;

}

li {

position: relative;

float: left;

margin: 0;

padding: 20px 2px 2px;

border-left: 1px solid black;

border-right: 1px solid black;

width: 110px;

height: 60px;

overflow: hidden;

background-color: white;

}

li:hover {

background-color: #FCB;

z-index: 1;

-moz-box-shadow: 0 0 10px #789;

-webkit-box-shadow: 0 0 10px #789;

box-shadow: 0 0 10px #789;

}

.weekdays {

height: 20px;

border-top: 2px solid black;

}

.weekdays li {

height: 16px;

padding: 2px 2px;

background-color: #BCF;

}

.fill {

background-color: #BCD;

}

.weekdays li:hover,li.fill:hover {

background-color: #BCD;

-moz-box-shadow: none;

-webkit-box-shadow: none;

box-shadow: none;

}

.weekdays li:hover,.today {

background-color: #BCF;

}

li strong {

position: absolute;

top: 2px;

right: 2px;

}

li a {

position: relative;

display: block;

border: 1px dotted black;

margin: 2px;

padding: 2px;

font-size: 11px;

background-color: #DEF;

text-align: left;

-moz-border-radius: 6px;

-webkit-border-radius: 6px;

border-radius: 6px;

z-index: 1;

text-decoration: none;

color: black;

font-weight: bold;

font-style: italic;

}

li a:hover {

background-color: #BCF;

z-index: 2;

-moz-box-shadow: 0 0 6px #789;

-webkit-box-shadow: 0 0 6px #789;

box-shadow: 0 0 6px #789;

}

保存样式表,并关闭它;在本章中,您不需要再次修改它。在下一节中,您将创建公共文件,这些文件将在页面中包含这些样式。

创建通用文件—页眉和页脚

这个应用将有多个供用户查看的页面,它们都需要一组通用的 HTML 元素、样式表等等。为了尽可能简化维护,您将使用两个文件header.inc.phpfooter.inc.php来包含这些公共元素。

首先,在common文件夹(/public/assets/common/header.inc.php)中创建一个名为header.inc.php的文件。这个文件将保存 HTML 的DOCTYPE声明,并创建一个head部分,其中包含一个Content-Type meta 标签、文档标题和到文档所需的任何 CSS 文件的链接。

因为文档标题会因页面而异,所以您将设置一个变量$page_title来存储每个页面的标题。

此外,因为一个页面可能需要多个 CSS 文件,所以一个 CSS 文件名数组将被传入一个名为$css_files的变量,并循环生成正确的标记。

在该文件中,放置以下代码:

<!DOCTYPE html

PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd

<html xmlns="http://www.w3.org/1999/xhtml

<head>

<meta http-equiv="Content-Type"

content="text/html;charset=utf-8" />

<title><?php echo $page_title; ?></title>

<?php foreach ( $css_files as $css ): ?>

<link rel="stylesheet" type="text/css" media="screen,projection"

href="assets/css/<?php echo $css; ?>" />

<?php endforeach; ?>

</head>

<body>

接下来,在common文件夹(/public/assets/common/footer.inc.php)中创建一个名为footer.inc.php的文件来包含标记的结束部分。

目前,这个文件不需要做太多事情:它只是关闭了在header.inc.php中打开的bodyhtml标签。随着您继续开发这个应用,这里会添加更多的内容。

将以下内容插入footer.inc.php:

</body>

</html>

将文件添加到索引

要将这些新的片段组合在一起,您需要修改索引文件。首先,将值添加到$page_title$css_files变量中,然后包含头文件。

此外,为了包装页面内容,添加一个新的 ID 为contentdiv,包装对buildCalendar()的调用。

最后,添加对页脚文件的调用来完成页面。完成后,将用粗体显示的代码修改索引文件:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo, "2016-01-01 12:00:00");

/*

* Set up the page title and CSS files

*/

$page_title = "Events Calendar";

$css_files = array('style.css');

/*

* Include the header

*/

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<?php

/*

* Display the calendar HTML

*/

echo $cal->buildCalendar();

?>

</div>``<!--``end #content

<?php

/*

* Include the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存更改后,重新加载浏览器以查看结果,如图 4-7 所示。

A978-1-4842-1230-1_4_Fig7_HTML.jpg

图 4-7。

The calendar with the header, footer, and CSS styles applied

输出 HTML 以显示完整的事件描述

接下来,您需要允许用户查看事件的细节。这将分三步完成。

Create a method to format an array of a single event’s data when loaded by ID.   Create a method to generate markup containing the data as loaded by the first method.   Create a new file to display the markup generated by the second method.

创建格式化单个事件数据的方法

_createEventObj()类似,这个方法的目的是从_loadEventData()返回的结果集中生成一个Event对象。

因为当只使用一个事件时,标记生成相当简单,所以这个方法所做的就是使用_loadEventData()通过 ID 加载所需的事件,然后从该方法返回第一个(也是唯一一个,因为有了LIMIT 1子句)结果。

将以下方法添加到Calendar类中:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

/**

* Returns a single event object

*

* @param int $id an event ID

* @return object the event object

*/

private function _loadEventById($id)

{

/*

* If no ID is passed, return NULL

*/

if ( empty($id) )

{

return NULL;

}

/*

* Load the events info array

*/

$event = $this->_loadEventData($id);

/*

* Return an event object

*/

if ( isset($event[0]) )

{

return new Event($event[0]);

}

else

{

return NULL;

}

}

}

?>

当被调用时,该方法将返回一个对象(对于 ID 为1)如下所示:

Event Object

(

[id] => 1

[title] => New Year’s Day

[description] => Happy New Year!

[start] => 2016-01-01 00:00:00

[end] => 2016-01-01 23:59:59

)

创建生成标记的方法

既然单个事件的数据数组已经可用,您可以构建一个新的公共方法来将事件数据格式化为 HTML 标记。这个方法将被称为displayEvent();它将接受一个事件的 ID,并使用以下步骤生成 HTML 标记。

Load the event data using _loadEventById().   Use the start and end dates to generate strings to describe the event.   Return the HTML markup to display the event.

通过将粗体代码添加到Calendar类来创建displayEvent()方法:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

/**

* Displays a given event’s information

*

* @param int $id the event ID

* @return string basic markup to display the event info

*/

public function displayEvent($id)

{

/*

* Make sure an ID was passed

*/

if ( empty($id) ) { return NULL; }

/*

* Make sure the ID is an integer

*/

$id = preg_replace('/[⁰-9]/', '', $id);

/*

* Load the event data from the DB

*/

$event = $this->_loadEventById($id);

/*

* Generate strings for the date, start, and end time

*/

$ts = strtotime($event->start);

$date = date('F d, Y', $ts);

$start = date('g:ia', $ts);

$end = date('g:ia', strtotime($event->end));

/*

* Generate and return the markup

*/

return "<h2>$event->title</h2>"

. "\n\t<p class=\"dates\">$date, $start``&

. "\n\t<p>$event->description</p>";

}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

}

?>

创建新文件以显示全部事件

为了显示displayEvent()的输出,您将创建一个新文件。这个文件将被称为view.php,它将驻留在公共文件夹(/public/view.php)中。

将使用包含要显示的事件 ID 的查询字符串调用该文件。如果没有提供 ID,用户将返回到日历的主视图。

view.php的顶部,检查一个事件 ID,然后加载初始化文件;页面标题和 CSS 文件在变量中设置,头文件被调用。之后,创建了一个Calendar类的新实例。

接下来,建立一个新的 ID 为contentdiv,并调用displayEvent()方法。添加一个返回主日历页面的链接,关闭div,并包含页脚。

考虑到所有因素,文件最终应该是这样的:

<?php

declare(strict_types=1);

/*

* Make sure the event ID was passed

*/

if ( isset($_GET['event_id']) )

{

/*

* Make sure the ID is an integer

*/

$id = preg_replace('/[⁰-9]/', '', $_GET['event_id']);

/*

* If the ID isn’t valid, send the user to the main page

*/

if ( empty($id) )

{

header("Location: ./");

exit;

}

}

else

{

/*

* Send the user to the main page if no ID is supplied

*/

header("Location: ./");

exit;

}

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Output the header

*/

$page_title = "View Event";

$css_files = array("style.css");

include_once 'assets/common/header.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

?>

<div id="content">

<?php echo $cal->displayEvent($id) ?>

<a href="./">``&

</div>``<!--``end #content

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

通过返回主日历并单击事件标题来测试该文件。view.php文件以匹配日历的格式加载并显示事件信息(见图 4-8 )。

A978-1-4842-1230-1_4_Fig8_HTML.jpg

图 4-8。

The event information displayed after clicking an event title

摘要

现在您有了一个功能完整的事件日历,它是使用面向对象的 PHP 和 MySQL 创建的。在这个过程中,您学习了如何处理日期,如何将条目组织到对象中以便于访问,以及如何输出标记和样式表以类似于传统的日历。在下一章中,您将构建添加、编辑和创建事件的控件。

五、添加控件来创建、编辑和删除事件

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​5) contains supplementary material, which is available to authorized users.

现在可以查看日历了,您需要添加允许管理员创建、编辑和删除事件的控件。

生成表单以创建或编辑事件

要编辑事件或向日历中添加新事件,您需要使用一个表单。通过向Calendar类添加一个名为displayForm()的方法来实现这一点,该方法生成一个用于编辑和创建事件的表单。

这个简单的方法可以完成以下任务:

  • 检查作为事件 ID 传递的整数。
  • 为用于描述事件的不同字段实例化空变量。
  • 如果传递了事件 ID,则加载事件数据。
  • 将事件数据存储在先前实例化的变量中(如果存在)。
  • 输出表单。

Note

通过显式清理在$_POST超全局中传递的事件 ID,您可以确保该 ID 可以安全使用,因为任何非整数值都将被转换为0

通过向Calendar类添加以下粗体代码来构建displayForm()方法:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct(``$db=NULL

public function buildCalendar() {...}

public function displayEvent($id) {...}

/**

* Generates a form to edit or create events

*

* @return string the HTML markup for the editing form

*/

public function displayForm()

{

/*

* Check if an ID was passed

*/

if ( isset($_POST['event_id']) )

{

$id = (int) $_POST['event_id'];

// Force integer type to sanitize data

}

else

{

$id = NULL;

}

/*

* Instantiate the headline/submit button text

*/

$submit = "Create a New Event";

/*

* If no ID is passed, start with an empty event object.

*/

$event = new Event();

/*

* Otherwise load the associated event

*/

if ( !empty($id) )

{

$event = $this->_loadEventById($id);

/*

* If no object is returned, return NULL

*/

if ( !is_object($event) ) { return NULL; }

$submit = "Edit This Event";

}

/*

* Build the markup

*/

return <<<FORM_MARKUP

<form action="assets/inc/process.inc.php" method="post">

<fieldset>

<legend>$submit</legend>

<label for="event_title">Event Title</label>

<input type="text" name="event_title"

id="event_title" value="$event->title" />

<label for="event_start">Start Time</label>

<input type="text" name="event_start"

id="event_start" value="$event->start" />

<label for="event_end">End Time</label>

<input type="text" name="event_end"

id="event_end" value="$event->end" />

<label for="event_description">Event Description</label>

<textarea name="event_description"

id="event_description">$event->description</textarea>

<input type="hidden" name="event_id" value="$event->id" />

<input type="hidden" name="token" value="$_SESSION[token]" />

<input type="hidden" name="action" value="event_edit" />

<input type="submit" name="event_submit" value="$submit" />

or <a href="./">cancel</a>

</fieldset>

</form>

FORM_MARKUP;

}

private function _loadEventData($id=NULL) {...}

private function _``createEventObj

private function _loadEventById($id) {...}

}

?>

向表单添加令牌

如果您查看前面的表单,有一个名为token的隐藏输入,它保存一个会话值,也称为token。这是一种防止跨站点请求伪造(CSRF)的安全措施,跨站点请求伪造是通过从表单本身以外的其他地方将表单提交到应用的处理文件来伪造的表单提交。这是垃圾邮件发送者发送多个伪造的条目提交的常用策略,这是令人讨厌的、潜在有害的,并且肯定是不受欢迎的。

这个令牌是通过生成一个随机散列并将其存储在会话中,然后将令牌与表单数据一起提交来创建的。如果$_POST超全局中的令牌与$_SESSION超全局中的令牌匹配,那么可以相当肯定地断定提交是合法的。

通过用粗体显示的代码修改初始化文件,可以将反 CSRF 令牌添加到应用中:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Generate an anti-CSRF token if one doesn’t exist

*/

if ( !isset($_SESSION['token']) )

{

$_SESSION['token'] = sha1(uniqid((string)mt_rand(), TRUE));

}

/*

* Include the necessary configuration info

*/

include_once '../sys/config/db-cred.inc.php'; // DB info

/*

* Define constants for configuration info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a PDO object

*/

$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME;

$dbo = new PDO($dsn, DB_USER, DB_PASS);

/*

* Define the auto-load function for classes

*/

function __autoload($class)

{

$filename = "../sys/class/class." . $class . ".inc.php";

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

Caution

您可能希望包括令牌的时间限制,以进一步提高安全性。例如,确保令牌不超过 20 分钟,有助于防止用户离开无人看管的计算机,并防止恶意用户稍后开始四处窥探。欲了解更多关于代币和预防 CSRF 的信息,请访问 Chris Shiflett 的博客,并在 http://shiflett.org/csrf 阅读他关于该主题的文章。

创建显示表单的文件

现在已经有了显示表单的方法,您需要创建一个调用该方法的文件。这个文件将被命名为admin.php,它将驻留在public文件夹(/public/admin.php)的根级别中。

view.php类似,该文件完成以下任务:

  • 加载初始化文件。
  • 设置页面标题和 CSS 文件数组。
  • 包括标题。
  • 创建Calendar类的新实例。
  • 调用displayForm()方法。
  • 包括页脚。

接下来,在新的admin.php文件中添加以下内容:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Output the header

*/

$page_title = "Add/Edit Event";

$css_files = array("style.css");

include_once 'assets/common/header.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

?>

<div id="content">

<?php echo $cal->displayForm(); ?>

</div>``<!--``end #content

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存该代码后,导航到http://localhost/admin.php查看结果表单,如图 5-1 所示。

A978-1-4842-1230-1_5_Fig1_HTML.jpg

图 5-1。

The form before adding any CSS styles

为管理功能添加新样式表

显然,前面的表单需要一些视觉上的增强,以使它更有用。然而,这个表单最终只能由管理员访问(因为您不希望任何人对您的日历进行更改),所以 CSS 规则将被分离到一个名为admin.css的单独样式表中。你可以在css文件夹(/public/assets/css/)中找到这个文件。

还是那句话,既然这本书不是讲 CSS 的,规则就不解释了。本质上,下面的 CSS 使表单元素看起来更像用户期望的表单;它还为即将创建的元素添加了一些规则。

现在将以下代码添加到admin.css中:

fieldset {

border: 0;

}

legend {

font-size: 24px;

font-weight: bold;

}

input[type=text],input[type=password],label {

display: block;

width: 70%;

font-weight: bold;

}

textarea {

width: 99%;

height: 200px;

}

input[type=text],input[type=password],textarea {

border: 1px solid #123;

-moz-border-radius: 6px;

-webkit-border-radius: 6px;

border-radius: 6px;

-moz-box-shadow: inset 1px 2px 4px #789;

-webkit-box-shadow: inset 1px 2px 4px #789;

box-shadow: inset 1px 2px 4px #789;

padding: 4px;

margin: 0 0 4px;

font-size: 16px;

font-family: georgia, serif;

}

input[type=submit] {

margin: 4px 0;

padding: 4px;

border: 1px solid #123;

-moz-border-radius: 6px;

-webkit-border-radius: 6px;

border-radius: 6px;

-moz-box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

-webkit-box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

background-color: #789;

font-family: georgia, serif;

text-transform: uppercase;

font-weight: bold;

font-size: 14px;

text-shadow: 0px 0px 1px #fff;

}

.admin-options {

text-align: center;

}

.admin-options form,.admin-options p {

display: inline;

}

a.admin {

display: inline-block;

margin: 4px 0;

padding: 4px;

border: 1px solid #123;

-moz-border-radius: 6px;

-webkit-border-radius: 6px;

border-radius: 6px;

-moz-box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

-webkit-box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

background-color: #789;

color: black;

text-decoration: none;

font-family: georgia, serif;

text-transform: uppercase;

font-weight: bold;

font-size: 14px;

text-shadow: 0px 0px 1px #fff;

}

保存该文件,然后通过进行粗体显示的更改将admin.css添加到admin.php中的$css_files数组:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Output the header

*/

$page_title = "Add/Edit Event";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

?>

<div id="content">

<?php echo $cal->displayForm(); ?>

</div><!-- end #content -->

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存前面的代码后,重新加载http://localhost/admin.php以查看样式化的表单(见图 5-2 )。

A978-1-4842-1230-1_5_Fig2_HTML.jpg

图 5-2。

The form to add or edit events after applying CSS styles

在数据库中保存新事件

为了保存表单中输入的事件,您在Calendar类中创建了一个名为processForm()的新方法,它完成了以下任务:

  • 清理通过POST从表单传递的数据。
  • 确定事件是正在编辑还是正在创建。
  • 如果没有正在编辑的事件,则生成一条INSERT语句;如果发布了事件 ID,则生成一条UPDATE语句。
  • 创建预准备语句并绑定参数。
  • 执行查询并在失败时返回TRUE或错误消息。

以下代码在Calendar类中创建了processForm()方法:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayEvent($id) {...}

public function displayForm() {...}

/**

* Validates the form and saves/edits the event

*

* @return mixed TRUE on success, an error message on failure

*/

public function processForm()

{

/*

* Exit if the action isn’t set properly

*/

if ( $_POST['action']!='event_edit' )

{

return "The method processForm was accessed incorrectly";

}

/*

* Escape data from the form

*/

$title = htmlentities($_POST['event_title'], ENT_QUOTES);

$desc = htmlentities($_POST['event_description'], ENT_QUOTES);

$start = htmlentities($_POST['event_start'], ENT_QUOTES);

$end = htmlentities($_POST['event_end'], ENT_QUOTES);

/*

* If no event ID passed, create a new event

*/

if ( empty($_POST['event_id']) )

{

$sql = "INSERT INTO events``

(event_title, event_desc, event_start,

``event_end)

VALUES

(:title, :description, :start, :end)";

}

/*

* Update the event if it’s being edited

*/

else

{

/*

* Cast the event ID as an integer for security

*/

$id = (int) $_POST['event_id'];

$sql = "UPDATE events``

SET

``event_title=:title,

``event_desc=:description,

``event_start=:start,

``event_end=:end

WHERE event_id=$id";

}

/*

* Execute the create or edit query after binding the data

*/

try

{

$stmt = $this->db->prepare($sql);

$stmt->bindParam(":title", $title, PDO::PARAM_STR);

$stmt->bindParam(":description", $desc, PDO::PARAM_STR);

$stmt->bindParam(":start", $start, PDO::PARAM_STR);

$stmt->bindParam(":end", $end, PDO::PARAM_STR);

$stmt->execute();

$stmt->closeCursor();

return TRUE;

}

catch ( Exception $e )

{

return $e->getMessage();

}

}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

}

?>

添加处理文件以调用处理方法

添加和编辑事件的表单被提交到一个名为process.inc.php的文件中,该文件位于inc文件夹(/public/assets/inc/process.inc.php)中。该文件检查提交的表单数据,并通过执行以下步骤保存或更新条目:

Enables the session.   Includes the database credentials and the Calendar class.   Defines constants (as occurs in the initialization file).   Creates an array that stores information about each action.   Verifies that the token was submitted and is correct, and that the submitted action exists in the lookup array. If so, go to Step 6. If not, go to Step 7.   Creates a new instance of the Calendar class.

  • 调用processForm()方法。
  • 将用户返回到主视图,或者在失败时输出错误。

Sends the user back out to the main view with no action if the token doesn’t match.

在步骤 4 中创建的数组允许您避免一长串重复的if...elseif块来测试每个单独的动作。使用 action 作为数组键,并将对象、方法名和用户应该重定向到的页面存储为数组值,这意味着您可以使用数组中的变量编写单个逻辑块。

将以下代码插入process.inc.php以完成刚刚描述的步骤:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define('ACTIONS', array(

'event_edit' => array(

'object' => 'Calendar',

'method' => 'processForm',

'header' => 'Location: ../../'

)

)

);

/*

* Need a PDO object.

*/

$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME;

$dbo = new PDO($dsn, DB_USER, DB_PASS);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( $_POST['token']==$_SESSION['token']

&& isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

if ( TRUE === $msg=$obj->$method() )

{

header($use_array['header']);

exit;

}

else

{

// If an error occured, output it and end execution

die ( $msg );

}

}

else

{

// Redirect to the main index if the token/action is invalid

header("Location: ../../");

exit;

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

保存该文件,然后导航至http://localhost/admin.php并用以下信息创建一个新事件:

  • 活动名称:晚宴
  • 开始时间:2016-01-22 17:00:00
  • 结束时间:2016-01-22 19:00:00
  • 描述:在约翰家吃五道菜并搭配葡萄酒

点击“创建新事件”按钮后,日历会更新为新事件,如图 5-3 所示。

A978-1-4842-1230-1_5_Fig3_HTML.jpg

图 5-3。

The new event as it appears when hovered over

向主视图添加按钮以创建新事件

为了让授权用户更容易创建新事件,在日历中添加一个按钮,将用户带到admin.php中的表单。通过在Calendar类中创建一个名为_adminGeneralOptions()的新私有方法来实现这一点:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayEvent($id) {...}

public function displayForm() {...}

public function processForm() {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

/**

* Generates markup to display administrative links

*

* @return string markup to display the administrative links

*/

private function _adminGeneralOptions()

{

/*

* Display admin controls

*/

return <<<ADMIN_OPTIONS

<a href="admin.php" class="admin">+ Add a New Event</a>

ADMIN_OPTIONS;

}

}

?>

Note

确保该按钮仅显示给授权用户的检查将添加到第六章。

接下来,通过插入以下粗体代码,修改buildCalendar()方法以调用新的_adminGeneralOptions()方法:

public function buildCalendar()

{

// To save space, the bulk of this method has been omitted

/*

* Close the final unordered list

*/

$html .= "\n\t</ul>\n\n";

/*

* If logged in, display the admin options

*/

$admin = $this->_adminGeneralOptions();

/*

* Return the markup for output

*/

return $html . $admin;

}

最后,使用以下粗体代码将管理样式表(admin.css)添加到index.php,以确保链接正确显示:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo, "2016-01-01 12:00:00");

/*

* Set up the page title and CSS files

*/

$page_title = "Events Calendar";

$css_files = array('style.css', 'admin.css');

/*

* Include the header

*/

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<?php

/*

* Display the calendar HTML

*/

echo $cal->buildCalendar();

?>

</div><!-- end #content -->

<?php

/*

* Include the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存文件并重新加载http://localhost/以查看按钮(见图 5-4 )。

A978-1-4842-1230-1_5_Fig4_HTML.jpg

图 5-4。

The Admin button appears in the bottom left of the calendar

将编辑控制添加到完整事件视图

接下来,您需要让授权用户能够编辑事件。您将通过在view.php中向事件的完整视图添加一个按钮来实现这一点。

然而,与用于创建新选项的简单链接不同,编辑按钮需要实际的表单提交。为了保持这段代码的可管理性,您将在Calendar类中创建一个名为_adminEntryOptions()的新私有方法,该方法将为表单生成标记。

现在,这个表单将简单地返回表单标记来显示编辑按钮。随着您继续完成本书中的练习,更多内容将添加到表格中。

您可以通过向Calendar类添加以下粗体代码来创建该方法:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayEvent($id) {...}

public function displayForm() {...}

public function processForm() {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

private function _adminGeneralOptions() {...}

/**

* Generates edit and delete options for a given event ID

*

* @param int $id the event ID to generate options for

* @return string the markup for the edit/delete options

*/

private function _adminEntryOptions($id)

{

return <<<ADMIN_OPTIONS

<div class="admin-options">

<form action="admin.php" method="post">

<p>

<input type="submit" name="edit_event"

value="Edit This Event" />

<input type="hidden" name="event_id"

value="$id" />

</p>

</form>

</div>``<!--``end .admin-options

ADMIN_OPTIONS;

}

}

?>

修改完整事件显示方法以显示管理控件

在显示编辑按钮之前,需要从displayEvent()方法中调用_adminEntryOptions()方法。这很简单,只需将_adminEntryOptions()的返回值存储在变量$admin中,然后将该变量与其余的条目标记一起输出。

将以下粗体修改添加到Calendar类中的displayEvent():

/**

* Displays a given event’s information

*

* @param int $id the event ID

* @return string basic markup to display the event info

*/

public function displayEvent($id)

{

/*

* Make sure an ID was passed

*/

if ( empty($id) ) { return NULL; }

/*

* Make sure the ID is an integer

*/

$id = preg_replace('/[⁰-9]/', '', $id);

/*

* Load the event data from the DB

*/

$event = $this->_loadEventById($id);

/*

* Generate strings for the date, start, and end time

*/

$ts = strtotime($event->start);

$date = date('F d, Y', $ts);

$start = date('g:ia', $ts);

$end = date('g:ia', strtotime($event->end));

/*

* Load admin options if the user is logged in

*/

$admin = $this->_adminEntryOptions($id);

/*

* Generate and return the markup

*/

return "<h2>$event->title</h2>"

. "\n\t<p class=\"dates\">$date, $start—$end</p>"

. "\n\t<p>$event->description</p>$admin";

}

Note

确保在return字符串的末尾包含了$admin变量。

与“创建新条目”按钮一样,稍后将添加检查,以确保只有授权用户才能看到编辑控件。

将管理样式表添加到完整事件视图页面

编辑按钮准备就绪之前的最后一步是将admin.css样式表包含在view.php$css_files变量中:

<?php

declare(strict_types=1);

/*

* Make sure the event ID was passed

*/

if ( isset($_GET['event_id']) )

{

/*

* Collect the event ID from the URL string

*/

$id = htmlentities($_GET['event_id'], ENT_QUOTES);

}

else

{

/*

* Send the user to the main page if no ID is supplied

*/

header("Location: ./");

exit;

}

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Output the header

*/

$page_title = "View Event";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

?>

<div id="content">

<?php echo $cal->displayEvent($id) ?>

<a href="./">« Back to the calendar</a>

</div><!-- end #content -->

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存该文件,然后点击一个事件以查看编辑按钮(参见图 5-5 )。

A978-1-4842-1230-1_5_Fig5_HTML.jpg

图 5-5。

The Edit button as it appears when viewing a full event description

点击编辑按钮将调出admin.php上的表格,表格中加载了所有事件的数据(见图 5-6 )。

A978-1-4842-1230-1_5_Fig6_HTML.jpg

图 5-6。

The admin form when an event is being edited

删除事件

创建Calendar类的最后一步是允许授权用户删除事件。事件删除不同于创建或编辑事件,因为您希望在删除事件之前确认用户的意图。否则,意外的点击会给用户带来挫败感和不便。

这意味着您必须分两个阶段实现删除按钮。

The Delete button is clicked, and the user is taken to a confirmation page.   The confirmation button is clicked, and the event is removed from the database.

生成删除按钮

首先,通过用粗体显示的代码修改Calendar类中的_adminEntryOptions(),向全视图编辑控件添加一个删除按钮:

/**

* Generates edit and delete options for a given event ID

*

* @param int $id the event ID to generate options for

* @return string the markup for the edit/delete options

*/

private function _adminEntryOptions($id)

{

return <<<ADMIN_OPTIONS

<div class="admin-options">

<form action="admin.php" method="post">

<p>

<input type="submit" name="edit_event"

value="Edit This Event" />

<input type="hidden" name="event_id"

value="$id" />

</p>

</form>

<form action="confirmdelete.php" method="post">

<p>

<input type="submit" name="delete_event"

value="Delete This Event" />

<input type="hidden" name="event_id"

value="$id" />

</p>

</form>

</div><!-- end .admin-options -->

ADMIN_OPTIONS;

}

这将添加一个按钮,将用户发送到一个尚未创建的名为confirmdelete.php的确认页面,您将在本节稍后构建该页面。保存前面的更改后,在查看完整的事件描述时,您将看到编辑和删除选项(参见图 5-7 )。

A978-1-4842-1230-1_5_Fig7_HTML.jpg

图 5-7。

The Delete button as it appears on the full event view

创建需要确认的方法

当用户单击 Delete 按钮时,她会被发送到一个确认页面,该页面包含一个确认她确实想要删除该事件的表单。该表单将由名为confirmDelete()Calendar类中的新公共方法生成。

此方法通过执行以下操作来确认应删除事件:

Checks if the confirmation form was submitted and a valid token was passed. If so, go to Step 2. If not, go to Step 3.   Checks whether the button clicked was the Confirmation button.

  • 如果是,它将删除该事件。
  • 如果没有,它会将用户送回主日历视图。

It loads the event data and displays the confirmation form.

通过将粗体显示的新方法添加到Calendar类中,可以完成前面的步骤:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayEvent($id) {...}

public function displayForm() {...}

public function processForm() {...}

/**

* Confirms that an event should be deleted and does so

*

* Upon clicking the button to delete an event, this

* generates a confirmation box. If the user confirms,

* this deletes the event from the database and sends the

* user back out to the main calendar view. If the user

* decides not to delete the event, they’re sent back to

* the main calendar view without deleting anything.

*

* @param int $id the event ID

* @return mixed the form if confirming, void or error if deleting

*/

public function confirmDelete($id)

{

/*

* Make sure an ID was passed

*/

if ( empty($id) ) { return NULL; }

/*

* Make sure the ID is an integer

*/

$id = preg_replace('/[⁰-9]/', '', $id);

/*

* If the confirmation form was submitted and the form

* has a valid token, check the form submission

*/

if ( isset($_POST['confirm_delete'])

&& $_POST['token']==$_SESSION['token'] )

{

/*

* If the deletion is confirmed, remove the event

* from the database

*/

if ( $_POST['confirm_delete']=="Yes, Delete It" )

{

$sql = "DELETE FROM events``

WHERE event_id=:id

LIMIT 1";

try

{

$stmt = $this->db->prepare($sql);

$stmt->bindParam(

":id",

$id,

PDO::PARAM_INT

);

$stmt->execute();

$stmt->closeCursor();

header("Location: ./");

return;

}

catch ( Exception $e )

{

return $e->getMessage();

}

}

/*

* If not confirmed, sends the user to the main view

*/

else

{

header("Location: ./");

return;

}

}

/*

* If the confirmation form hasn’t been submitted, display it

*/

$event = $this->_loadEventById($id);

/*

* If no object is returned, return to the main view

*/

if ( !is_object($event) ) { header("Location: ./"); }

return <<<CONFIRM_DELETE

<form action="confirmdelete.php" method="post">

<h2>

Are you sure you want to delete "$event->title"?

</h2>

<p>There is <strong>no undo</strong> if you continue.</p>

<p>

<input type="submit" name="confirm_delete"

value="Yes, Delete It" />

<input type="submit" name="confirm_delete"

value="Nope! Just Kidding!" />

<input type="hidden" name="event_id"

value="$event->id" />

<input type="hidden" name="token"

value="$_SESSION[token]" />

</p>

</form>

CONFIRM_DELETE;

}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

private function _adminGeneralOptions() {...}

private function _adminEntryOptions($id) {...}

}

?>

创建文件以显示确认表单

为了调用confirmDelete()方法,需要创建文件confirmdelete.php。这个文件将驻留在public文件夹(/public/confirmdelete.php)的根目录下,它将非常类似于index.php。该文件完成以下任务:

  • 确保事件 ID 被传递并存储在$id变量中;否则将用户发送到主视图。
  • 加载初始化文件。
  • 创建Calendar对象的新实例。
  • confirmDelete()的返回值加载到变量$markup中。
  • 定义$page_title$css_files变量,包括标题。
  • 输出存储在$markup中的数据。
  • 输出页脚。

Note

在包含头部之前将confirmDelete()的输出加载到一个变量中的原因是,该方法有时会使用header()将用户发送到应用中的其他地方;如果在调用confirmDelete()之前包含了头文件,脚本在某些情况下会失败,因为在调用header()之前没有数据可以输出到浏览器,或者发生致命错误。关于header()功能的更多信息,请访问 http://php.net/header

现在在confirmdelete.php中添加以下代码:

<?php

declare(strict_types=1);

/*

* Make sure the event ID was passed

*/

if ( isset($_POST['event_id']) )

{

/*

* Collect the event ID from the URL string

*/

$id = (int) $_POST['event_id'];

}

else

{

/*

* Send the user to the main page if no ID is supplied

*/

header("Location: ./");

exit;

}

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

$markup = $cal->confirmDelete($id);

/*

* Output the header

*/

$page_title = "View Event";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<?php echo $markup; ?>

</div>``<!--``end #content

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存这个文件,然后通过删除“晚宴”条目来测试系统。在向您展示完整的事件描述后,日历会带您进入确认表单(参见图 5-8 )。

A978-1-4842-1230-1_5_Fig8_HTML.jpg

图 5-8。

The confirmation form a user sees after clicking the Delete button

点击“是,删除”按钮后,该事件将从日历中删除(参见图 5-9 )。

A978-1-4842-1230-1_5_Fig9_HTML.jpg

图 5-9。

After the user confirms the deletion, the event is removed from the calendar

摘要

至此,您已经有了一个功能齐全的活动日历。您已经学习了如何创建表单来创建、编辑、保存和删除事件,包括如何确认事件删除。但是,管理控件目前对访问该站点的任何人都可用。

在下一章中,您将构建一个类来授予授权用户对站点管理控件的访问权限。

六、通过密码保护敏感动作和区域

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​6) contains supplementary material, which is available to authorized users.

现在您的应用可以添加、编辑和移除事件,您需要通过要求用户登录才能进行任何更改来保护这些操作。要做到这一点,你需要在数据库中创建一个新表,在应用中创建一个新类;您还需要对现有文件进行一些修改。

在数据库中构建管理表

要存储被授权修改事件的用户的信息,您需要创建一个新的数据库表。这个表将被称为users,它将存储每个用户的四条信息:ID、姓名、密码散列和电子邮件地址。

要创建该表,导航到http://localhost/phpmyadmin并选择 SQL 选项卡执行以下命令:

CREATE TABLE IF NOT EXISTS php-jquery_example.users (

``user_id INT(11) NOT NULL AUTO_INCREMENT,

``user_name VARCHAR(80) DEFAULT NULL,

``user_pass VARCHAR(47) DEFAULT NULL,

``user_email VARCHAR(80) DEFAULT NULL,

PRIMARY KEY (user_id),

UNIQUE (user_name)

) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_unicode_ci;

这段代码执行后,从左栏选择php-jquery_example数据库,点击users表查看新表(见图 6-1 )。

A978-1-4842-1230-1_6_Fig1_HTML.jpg

图 6-1。

The users table as it appears in phpMyAdmin

构建文件以显示登录表单

为了登录,用户需要访问登录表单。这将显示在名为login.php的页面上,该页面存储在公共文件夹(/public/login.php)中。这个文件将类似于admin.php,除了它将简单地输出表单,因为它的信息没有一个是可变的。

该表单将接受用户名和密码,还将传递会话令牌和一个动作user_login。将以下代码插入到login.php中以创建该表单:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Output the header

*/

$page_title = "Please Log In";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<form action="assets/inc/process.inc.php" method="post">

<fieldset>

<legend>Please Log In</legend>

<label for="uname">Username</label>

<input type="text" name="uname"

id="uname" value="" />

<label for="pword">Password</label>

<input type="password" name="pword"

id="pword" value="" />

<input type="hidden" name="token"

value="<?php echo $_SESSION['token']; ?>" />

<input type="hidden" name="action"

value="user_login" />

<input type="submit" name="login_submit"

value="Log In" />

or <a href="./">cancel</a>

</fieldset>

</form>

</div>``<!--``end #content

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存此代码,并在浏览器中导航至http://localhost/login.php以查看生成的登录表单(参见图 6-2 )。

A978-1-4842-1230-1_6_Fig2_HTML.jpg

图 6-2。

The login form

创建管理类

有了您的表,现在您可以开始构建将与之交互的类了。在class文件夹中,创建一个名为class.admin.inc.php ( /sys/class/class.admin.inc.php)的新文件。这个类将包含允许用户登录和注销的方法。

定义类别

首先,定义一个类,它将扩展DB_Connect来访问数据库。这个类将有一个私有属性,$_saltLength,您将在本节稍后了解到。

构造函数将调用父构造函数以确保数据库对象存在,然后它将检查是否有一个整数作为构造函数的第二个参数传递。如果是,则该整数被用作$_saltLength的值。

现在将以下代码插入到class.admin.inc.php中,以定义类、属性和构造函数:

<?php

declare(strict_types=1);

/**

* Manages administrative actions

*

* PHP version 7

*

* LICENSE: This source file is subject to the MIT License, available

* athttp://www.opensource.org/licenses/mit-license.html

*

* @author     Jason Lengstorf <jason.lengstorf@ennuidesign.com>

* @copyright  2010 Ennui Design

* @licensehttp://www.opensource.org/licenses/mit-license.html

*/

class Admin extends DB_Connect

{

/**

* Determines the length of the salt to use in hashed passwords

*

* @var int the length of the password salt to use

*/

private $_saltLength = 7;

/**

* Stores or creates a DB object and sets the salt length

*

* @param object $db a database object

* @param int $saltLength length for the password hash

*/

public function __construct($db=NULL, $saltLength=NULL)

{

parent::__construct($db);

/*

* If an int was passed, set the length of the salt

*/

if ( is_int($saltLength) )

{

$this->_saltLength = $saltLength;

}

}

}

?>

构建检查登录凭证的方法

需要对来自login.php的数据进行验证,以确认用户有权对events表进行更改。您可以按照这些步骤来完成。

Verify that the form was submitted using the proper action.   Sanitize the user input with htmlentities().   Retrieve user data that has a matching username from the database.   Store the user information in a variable, $user, and make sure it isn’t empty.   Generate a salted hash from the user-supplied password and the password stored in the database.   Make sure the hashes match.   Store user data in the current session using an array and return TRUE.   Note

加盐散列将在下一节“构建创建加盐散列的方法”中讨论

首先在Admin类中定义方法,并使用下面的粗体代码完成前面的步骤 1 和 2:

<?php

declare(strict_types=1);

class Admin extends DB_Connect

{

private $_saltLength = 7;

public function __construct($db=NULL, $saltLength=NULL) {...}

/**

* Checks login credentials for a valid user

*

* @return mixed TRUE on success, message on error

*/

public function processLoginForm()

{

/*

* Fails if the proper action was not submitted

*/

if ( $_POST['action']!='user_login' )

{

return "Invalid action supplied for processLoginForm.";

}

/*

* Escapes the user input for security

*/

$uname = htmlentities($_POST['uname'], ENT_QUOTES);

$pword = htmlentities($_POST['pword'], ENT_QUOTES);

// finish processing...

}

}

?>

接下来,通过添加以粗体显示的以下代码来完成步骤 3 和 4:

public function processLoginForm()

{

/*

* Fails if the proper action was not submitted

*/

if ( $_POST['action']!='user_login' )

{

return "Invalid action supplied for processLoginForm.";

}

/*

* Escapes the user input for security

*/

$uname = htmlentities($_POST['uname'], ENT_QUOTES);

$pword = htmlentities($_POST['pword'], ENT_QUOTES);

/*

* Retrieves the matching info from the DB if it exists

*/

$sql = "SELECT

user_id`, `user_name`, `user_email`, `user_pass

FROM users``

WHERE

``user_name = :uname

LIMIT 1";

try

{

$stmt = $this->db->prepare($sql);

$stmt->bindParam(':uname', $uname, PDO::PARAM_STR);

$stmt->execute();

$user = array_shift($stmt->fetchAll());

$stmt->closeCursor();

}

catch ( Exception $e )

{

die ( $e->getMessage() );

}

/*

* Fails if username doesn’t match a DB entry

*/

if ( !isset($user) )

{

return "Your username or password is invalid.";

}

// finish processing.. .

}

现在,用户的数据存储在变量$user中(或者该方法失败,因为在users表中没有找到与提供的用户名匹配的内容)。

完成步骤 5-7 完成该方法;通过添加以下粗体代码来实现这一点:

public function processLoginForm()

{

/*

* Fails if the proper action was not submitted

*/

if ( $_POST['action']!='user_login' )

{

return "Invalid action supplied for processLoginForm.";

}

/*

* Escapes the user input for security

*/

$uname = htmlentities($_POST['uname'], ENT_QUOTES);

$pword = htmlentities($_POST['pword'], ENT_QUOTES);

/*

* Retrieves the matching info from the DB if it exists

*/

$sql = "SELECT

user_id`, `user_name`, `user_email`, `user_pass

FROM users``

WHERE

``user_name = :uname..

LIMIT 1";

try

{

$stmt = $this->db->prepare($sql);

$stmt->bindParam(':uname', $uname, PDO::PARAM_STR);

$stmt->execute();

$user = array_shift($stmt->fetchAll());

$stmt->closeCursor();

}

catch ( Exception $e )

{

die ( $e->getMessage() );

}

/*

* Fails if username doesn’t match a DB entry

*/

if ( !isset($user) )

{

return "No user found with that ID.";

}

/*

* Get the hash of the user-supplied password

*/

$hash = $this->_getSaltedHash($pword, $user['user_pass']);

/*

* Checks if the hashed password matches the stored hash

*/

if ( $user['user_pass']==$hash ) ..

{

/*

* Stores user info in the session as an array

*/

$_SESSION['user'] = array(

'id' => $user['user_id'],

'name' => $user['user_name'],

'email' => $user['user_email']

);

return TRUE;

}

/*

* Fails if the passwords don’t match

*/

else

{

return "Your username or password is invalid.";

}

}

该方法现在将验证登录表单提交。然而,它还没有完全发挥作用;首先,您需要构建_getSaltedHash()方法。

构建创建加盐散列的方法

为了验证存储在数据库中的用户密码哈希,您需要一个函数来从用户提供的密码生成加盐哈希(哈希是由 MD5 或 SHA1 等安全算法生成的加密字符串)。

Note

有关密码哈希和安全算法的更多信息,请访问 http://en.wikipedia.org/wiki/Cryptographic_hash_function

Increasing Security with Salted Passwords

即使 PHP 提供了散列或加密字符串的功能,您也应该使用额外的安全措施来确保您的信息完全安全。提高安全性的最简单、最有效的方法之一是使用 saltss,salt 是散列密码时使用的附加字符串。

使用彩虹表和普通加密算法

常见的加密算法,如 SHA1 和 MD5,已经使用彩虹表、 1 进行了完全映射,彩虹表是密码哈希的反向查找表。简而言之,彩虹表允许攻击者在一个大表中搜索由给定加密算法产生的散列,该大表包含每个可能的散列和将产生该散列的值。

彩虹表是为 MD5 和 SHA1 生成的,所以如果不采取额外的安全措施,攻击者可以相对容易地破解用户的密码。

使用加盐散列提高安全性

虽然不是刀枪不入,但在哈希算法中加入盐会让攻击者破解用户密码变得更加麻烦。salt 是一个预定义的或随机的字符串,在散列时除了用户输入之外还使用它。

如果不使用 salt,密码可能会被哈希如下:

$hash = sha1($password);

要将随机 salt 添加到前面的散列中,您可以对其应用以下代码:

$salt = substr(md5(time()), 0, 7); // create a random salt

$hash = $salt . sha1($salt . $password);

前面的代码生成一个随机的七位数 salt。在散列之前,salt 被添加到密码字符串的前面;这意味着,即使两个用户拥有相同的密码,他们各自的密码哈希也会不同。

然而,为了再现该散列,盐需要可用。由于这个原因,salt 也以未加密的形式添加到哈希中。这样,当用户登录时,您就能够从数据库检索到的散列中提取 salt,并使用它来重新创建用户密码的 salt 散列:

$salt = substr($dbhash, 0, 7); // extract salt from stored hash

$hash = $salt . sha1($salt . $_POST['password']);

if ( $dbhash==$hash )

{

echo "Match!";

}

else

{

echo "No match.";

}

结合咸哈希和彩虹表

加入盐后,彩虹桌就没用了。需要生成一个新的表,将 salt 考虑在内,以便破解用户密码;虽然这并非不可能,但对攻击者来说非常耗时,而且会给你的应用增加一层额外的安全性。

在大多数应用中(尤其是那些不存储太多敏感个人信息的应用,如信用卡信息),加盐密码足以阻止潜在的攻击者。

作为一种额外的对策,还建议添加对重复失败登录尝试的检查。这样,攻击者在被锁定在系统之外之前有有限次数的尝试来破解密码。这还可以防止拒绝服务攻击(发送大量请求试图使网站过载并使其离线的攻击)。

创建这个函数相对简单,只需要几个步骤。

Check whether a salt was supplied; if not, generate a new salt by hashing the current UNIX timestamp, and then take a substring of the returned value at the length specified in $_saltLength and store it as $salt.   Otherwise, take a substring of the supplied salted hash from the database at the length specified in $_saltLength and store it as $salt.   Prepend the salt to the hash of the salt and the password, and return the new string.

通过将以下方法插入到Admin类中来完成所有三个步骤:

<?php

declare(strict_types=1);

class Admin extends DB_Connect

{

private $_saltLength = 7;

public function __construct($db=NULL, $saltLength=NULL) {...}

public function processLoginForm() {...}

/**

* Generates a salted hash of a supplied string

*

* @param string $string to be hashed

* @param string $salt extract the hash from here

* @return string the salted hash

*/

private function _getSaltedHash($string, $salt=NULL)

{

/*

* Generate a salt if no salt is passed

*/

if ( $salt==NULL )

{

$salt = substr(md5((string)time()), 0, $this->_saltLength);

}

/*

* Extract the salt from the string if one is passed

*/

else

{

$salt = substr($salt, 0, $this->_saltLength);

}

/*

* Add the salt to the hash and return it

*/

return $salt . sha1($salt . $string);

}

}

?>

为加盐哈希创建测试方法

为了了解加盐散列是如何工作的,为_getSaltedHash()创建一个名为testSaltedHash()的快速测试方法。这将是一个调用并输出值的公共函数,使您能够看到脚本是如何运行的。

Admin类中,定义testSaltedHash()方法:

<?php

declare(strict_types=1);

class Admin extends DB_Connect

{

private $_saltLength = 7;

public function __construct($db=NULL, $saltLength=NULL) {...}

public function processLoginForm() {...}

private function _getSaltedHash($string, $salt=NULL) {...}

public function testSaltedHash($string, $salt=NULL)

{

return $this->_getSaltedHash($string, $salt);

}

}

?>

接下来,添加一个名为test.php的新文件来使用这个函数,并将它放在public文件夹(/public/test.php)中。在这个函数内部,调用初始化文件,创建一个新的Admin类,输出这个单词的三个哈希:test。创建第一个不加盐的 hash,然后休眠一秒钟获得新的时间戳。创建第二个不加盐的 hash,然后再睡一秒。最后,使用第二个散列中的 salt 创建第三个散列。插入以下代码来完成该测试:

<?php

declare(strict_types=1);

// Include necessary files

include_once '../sys/core/init.inc.php';

// Load the admin object

$obj = new Admin($dbo);

// Load a hash of the word test and output it

$hash1 = $obj->testSaltedHash("test");

echo "Hash 1 without a salt:<br />", $hash1, "<br /><br />";

// Pause execution for a second to get a different timestamp

sleep(1);

// Load a second hash of the word test

$hash2 = $obj->testSaltedHash("test");

echo "Hash 2 without a salt:<br />", $hash2, "<br /><br />";

// Pause execution for a second to get a different timestamp

sleep(1);

// Rehash the word test with the existing salt

$hash3 = $obj->testSaltedHash("test", $hash2);

echo "Hash 3 with the salt from hash 2:<br />", $hash3;

?>

Note

sleep()函数将脚本的执行延迟给定的秒数,作为唯一的参数传递。您可以在 http://php.net/sleep 了解该功能的更多信息。

您的结果不会完全相同,因为用于 salt 的时间戳哈希会有所不同;但是,您的结果应该看起来像这样:

Hash 1 without a salt:

fa260349138b9541c4b2895aeb0f0effe490194f4ef6c30

Hash 2 without a salt:

8d130612280d9e54c5fa4558cf80fac18a0514456dc11dc

Hash 3 with the salt from hash 2:

8d130612280d9e54c5fa4558cf80fac18a0514456dc11dc

如您所见,单词 test 的散列在单独通过时并不匹配;但是,如果您提供 test 的现有 salted 散列,则会生成相同的散列。这样,即使两个用户有相同的密码,他们存储的哈希值也会不同,这使得潜在的攻击者更难破解密码。

Note

请记住,没有任何算法是 100%有效的。然而,使用像加盐散列这样的技术可以显著降低攻击的可能性。

创建用户以测试管理访问权限

为了测试管理功能,您需要在您的users表中存在一个用户名/密码对。为简单起见,用户名为testuser,密码为admin,电子邮件地址为admin@example.com。请记住,这不是一个安全密码;它仅用于说明目的,在您将其用于任何生产脚本之前,应该对其进行更改。

首先生成密码的散列值admin,使用测试方法testSaltedHash()test.php很容易做到这一点。将以下粗体代码添加到test.php中,以生成测试用户密码的加盐散列:

<?php

declare(strict_types=1);

// Include necessary files

include_once '../sys/core/init.inc.php';

// Load the admin object

$obj = new Admin($dbo);

// Generate a salted hash of "admin"

$pass = $obj->testSaltedHash("admin");

echo 'Hash of "admin":<br />', $pass, "<br /><br />";

?>

导航到http://localhost/test.php,您将看到类似如下的输出:

Hash of "admin":

0c6c835a48f6c5577e322f49c96ce0c719a8c272d4d8609

复制散列,导航到http://localhost/phpmyadmin,然后单击 SQL 选项卡。执行以下查询,将测试用户插入表中:

INSERT INTO php-jquery_example.users``

(user_name, user_pass, user_email)

VALUES

(

'testuser',

'a1645e41f29c45c46539192fe29627751e1838f7311eeb4',

'admin@example.com'

);

执行上述代码后,单击php-jquery_example数据库,然后单击users表。选择浏览选项卡查看表格中的用户信息(参见图 6-3 )。

A978-1-4842-1230-1_6_Fig3_HTML.jpg

图 6-3。

The test user data after inserting it into the database

现在用户已经存在于用户数据库中,并且存储了一个 salted hash,您可以从Admin类中删除testSaltedHash()方法和整个test.php文件。

修改应用以处理登录表单提交

至此,您差不多已经准备好测试用户登录了。然而,在它工作之前,您需要修改process.inc.php来处理来自登录表单的表单提交。

由于文件设置的方式,这种改变就像向ACTIONS数组添加一个新元素一样简单。打开process.inc.php并插入以下粗体代码:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define(ACTIONS, array(

'event_edit' => array(

'object' => 'Calendar',

'method' => 'processForm',

'header' => 'Location: ../../'

),

'user_login' => array(

'object' => 'Admin',

'method' => 'processLoginForm',

'header' => 'Location: ../../'

)

)

);

/*

* Need a PDO object.

*/

$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME;

$dbo = new PDO($dsn, DB_USER, DB_PASS);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( $_POST['token']==$_SESSION['token']

&& isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

if ( TRUE === $msg=$obj->$method() )

{

header($use_array['header']);

exit;

}

else

{

// If an error occured, output it and end execution

die ( $msg );

}

}

else

{

// Redirect to the main index if the token/action is invalid

header("Location: ../../");

exit;

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

现在您可以正式测试登录了。因为还没有对登录进行检查,所以只需在文件中插入以下粗体行,在index.php中添加一个条件注释来显示登录或注销状态:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo, "2016-01-01 12:00:00");

/*

* Set up the page title and CSS files

*/

$page_title = "Events Calendar";

$css_files = array('style.css', 'admin.css');

/*

* Include the header

*/

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<?php

/*

* Display the calendar HTML

*/

echo $cal->buildCalendar();

?>

</div><!-- end #content -->

<p>

<?php

echo isset($_SESSION['user']) ? "Logged In!" : "Logged Out!";

?>

</p>

<?php

/*

* Include the footer

*/

include_once 'assets/common/footer.inc.php';

?>

现在保存这个文件并导航到http://localhost/以查看“注销!”日历下方的消息(见图 6-4 )。

A978-1-4842-1230-1_6_Fig4_HTML.jpg

图 6-4。

Before the user logs in, the “Logged Out!” message appears below the calendar

接下来,导航到http://localhost/login.php,输入用户名testuser和密码admin(见图 6-5 )。

A978-1-4842-1230-1_6_Fig5_HTML.jpg

图 6-5。

The login form with the username and password information entered

单击登录按钮后,您将被重定向回日历;然而,现在日历下面的消息将显示“已登录!”(参见图 6-6 )。

A978-1-4842-1230-1_6_Fig6_HTML.jpg

图 6-6。

After the user logs in, the “Logged In!” message appears below the calendar

允许用户注销

接下来,您需要添加一个允许用户注销的方法。您将使用一个表单向process.inc.php提交信息。Calendar类中的方法_adminGeneralOptions()生成表单。

将注销按钮添加到日历

要添加允许用户注销的按钮,请修改Calendar类中的_adminGeneralOptions()。除了提供添加新事件的按钮之外,这个方法现在还将输出一个表单,该表单将站点令牌和一个动作值user_logout提交给process.inc.php。打开Calendar类,用下面的粗体代码修改_adminGeneralOptions():

private function _adminGeneralOptions()

{

/*

* Display admin controls

*/

return <<<ADMIN_OPTIONS

<a href="admin.php" class="admin">+ Add a New Event</a>

<form action="assets/inc/process.inc.php" method="post">

<div>

<input type="submit" value="Log Out" class="logout" />

<input type="hidden" name="token"

value="$_SESSION[token]" />

<input type="hidden" name="action"

value="user_logout" />

</div>

</form>

ADMIN_OPTIONS;

}

现在保存更改并刷新浏览器中的http://localhost/以查看新按钮(参见图 6-7 )。

A978-1-4842-1230-1_6_Fig7_HTML.jpg

图 6-7。

The Log Out button as it appears after you modify the Calendar class

创建处理注销的方法

为了处理注销,需要将一个名为processLogout()的新公共方法添加到Admin类中。该方法进行快速检查以确保提供了正确的操作user_logout,然后使用session_destroy()通过完全销毁当前会话来删除用户数据数组。

通过插入以下粗体代码,将该方法添加到Admin类中:

<?php

declare(strict_types=1);

class Admin extends DB_Connect

{

private $_saltLength = 7;

public function __construct($db=NULL, $saltLength=NULL) {...}

public function processLoginForm() {...}

/**

* Logs out the user

*

* @return mixed TRUE on success or messsage on failure

*/

public function processLogout()

{

/*

* Fails if the proper action was not submitted

*/

if ( $_POST['action']!='user_logout' )

{

return "Invalid action supplied for processLogout.";

}

/*

* Removes the user array from the current session

*/

session_destroy();

return TRUE;

}

private function _getSaltedHash($string, $salt=NULL) {...}

}

?>

修改应用以处理用户注销

在用户可以成功注销之前,您需要做的最后一步是向process.inc.php中的ACTIONS数组添加另一个数组元素。将以下粗体代码插入process.inc.php以完成注销过程:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define(ACTIONS, array(

'event_edit' => array(

'object' => 'Calendar',

'method' => 'processForm',

'header' => 'Location: ../../'

),

'user_login' => array(

'object' => 'Admin',

'method' => 'processLoginForm',

'header' => 'Location: ../../'

),

'user_logout' => array(

'object' => 'Admin',

'method' => 'processLogout',

'header' => 'Location: ../../'

)

)

);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( $_POST['token']==$_SESSION['token']

&& isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

if ( TRUE === $msg=$obj->$method() )

{

header($use_array['header']);

exit;

}

else

{

// If an error occured, output it and end execution

die ( $msg );

}

}

else

{

// Redirect to the main index if the token/action is invalid

header("Location: ../../");

exit;

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

保存该文件,然后导航至http://localhost/,并点击日历底部的注销按钮。点按此按钮会导致日历下方的消息显示为“已注销!”(参见图 6-8 )。

A978-1-4842-1230-1_6_Fig8_HTML.jpg

图 6-8。

Clicking the Log Out button removes the user data from the session Note

即使我们已经确定登录正在运行,我们仍然会继续显示登录!/已注销!在我们工作的过程中。但是如果您愿意,您可以从index.php中删除消息逻辑和包含它的段落标记。

仅向管理员显示管理工具

您的用户可以登录和注销;您需要采取的最后一个步骤是确保所有需要管理访问权限的操作和选项只显示给已登录的用户。

向管理员显示管理选项

除非用户登录,否则不应显示用于添加和编辑事件的按钮。为了执行这个检查,您需要修改Calendar类中的_adminGeneralOptions()_adminEntryOptions()方法。

修改常规管理选项方法

现在让我们看看日历的常规选项。如果用户已经登录,您希望向她显示创建新条目和注销的选项。

但是,如果用户被注销,她应该会看到一个登录链接。通过对Calendar类中的_adminGeneralOptions()方法进行粗体显示的修改来执行该检查:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($db=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayForm() {...}

public function processForm() {...}

public function confirmDelete($id) {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

private function _adminGeneralOptions()

{

/*

* If the user is logged in, display admin controls

*/

if ( isset($_SESSION['user']) )

{

return <<<ADMIN_OPTIONS

<a href="admin.php" class="admin">+ Add a New Event</a>

<form action="assets/inc/process.inc.php" method="post">

<div>

<input type="submit" value="Log Out" class="logout" />

<input type="hidden" name="token"

value="$_SESSION[token]" />

<input type="hidden" name="action"

value="user_logout" />

</div>

</form>

ADMIN_OPTIONS;

}

else

{

return <<<ADMIN_OPTIONS

<a href="login.php">Log In</a>

ADMIN_OPTIONS;

}

}

private function _adminEntryOptions($id) {...}

}

?>

保存更改后,注销时重新加载http://localhost/以查看管理选项被一个简单的Log In链接取代(见图 6-9 )。

A978-1-4842-1230-1_6_Fig9_HTML.jpg

图 6-9。

While a user is logged out, a Log In link is displayed in the lower left corner

修改事件选项方法

接下来,您希望添加代码来防止未经授权的用户编辑和删除事件;您可以通过用以下粗体代码修改Calendar类中的_adminEventOptions()来实现这一点:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($db=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayForm() {...}

public function processForm() {...}

public function confirmDelete($id) {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

private function _adminGeneralOptions() {...}

private function _adminEntryOptions($id)

{

if ( isset($_SESSION['user']) )

{

return <<<ADMIN_OPTIONS

<div class="admin-options">

<form action="admin.php" method="post">

<p>

<input type="submit" name="edit_event"

value="Edit This Event" />

<input type="hidden" name="event_id"

value="$id" />

</p>

</form>

<form action="confirmdelete.php" method="post">

<p>

<input type="submit" name="delete_event"

value="Delete This Event" />

<input type="hidden" name="event_id"

value="$id" />

</p>

</form>

</div><!-- end .admin-options -->

ADMIN_OPTIONS;

}

else

{

return NULL;

}

}

}

?>

插入这些更改后,在注销时导航至http://localhost/,并点击一个事件以调出其完整视图;将不显示管理选项(参见图 6-10 )。

A978-1-4842-1230-1_6_Fig10_HTML.jpg

图 6-10。

The full event view while logged out

限制对管理页面的访问

作为额外的安全预防措施,您应该确保只有授权用户才能访问的任何页面,如事件创建/编辑表单,在执行之前检查是否有适当的授权。

不允许在未登录的情况下访问事件创建表单

通过执行添加到文件中的简单检查,可以防止恶意用户在注销时找到事件创建表单。如果用户没有登录,在脚本有机会执行之前,他将被发送到主日历视图。

要实现这一更改,打开admin.php并插入粗体显示的代码:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* If the user is not logged in, send them to the main file

*/

if ( !isset($_SESSION['user']) )

{

header("Location: ./");

exit;

}

/*

* Output the header

*/

$page_title = "Add/Edit Event";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

?>

<div id="content">

<?php echo $cal->displayForm(); ?>

</div><!-- end #content -->

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存该文件后,尝试在注销时导航至http://localhost/admin.php。你会被自动发送到http://localhost/

确保只有登录的用户才能删除事件

此外,为了防止未经授权的用户删除事件,在confirmdelete.php文件中插入一个有效用户会话检查:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Make sure an event ID was passed and the user is logged in

*/

if ( isset($_POST['event_id'])``&&

{

/*

* Collect the event ID from the URL string

*/

$id = (int) $_POST['event_id'];

}

else

{

/*

* Send the user to the main page if no ID is supplied

* or the user is not logged in

*/

header("Location: ./");

exit;

}

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

$markup = $cal->confirmDelete($id);

/*

* Output the header

*/

$page_title = "View Event";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<?php echo $markup; ?>

</div><!-- end #content -->

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

现在保存这段代码,并尝试在注销时直接访问http://localhost/confirmdelete.php。不出所料,您将被重定向到http://localhost/

摘要

在本章中,您学习了如何将用户授权添加到您的日历应用,这意味着现在只有授权用户才能修改日历。您了解了如何创建Admin类、检查登录凭证、仅向管理员显示管理工具以及限制对管理页面的访问。

在下一章中,您将开始将 jQuery 集成到应用中,以逐步增强用户体验。

Footnotes 1

http://en.wikipedia.org/wiki/Rainbow_table

七、使用 jQuery 增强用户界面

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​7) contains supplementary material, which is available to authorized users.

目前的应用功能齐全。可以查看事件,具有管理权限的用户可以登录以创建、编辑或删除事件。

下一步是向应用添加一层润色,以创建最终的外观和感觉,这将通过使用一种称为渐进增强的技术向应用添加 AJAX 功能来完成。

使用 jQuery 添加渐进式增强

渐进式改进是一个术语,最初由 Steven Champeon 1 在 2003 年创造,用于描述一种 web 开发技术,其中应用被设计为可访问任何互联网连接和浏览器,使用语义 HTML 和其他分层应用的技术(如 CSS 文件和 JavaScript 代码)。

对于遵循渐进增强原则的应用,它必须遵循以下准则:

  • 所有浏览器都可以使用最简单、最具语义的 HTML 标记来访问基本内容。
  • 该应用的所有基本功能都适用于所有浏览器。
  • 尊重用户的偏好;这意味着 web 应用不会覆盖浏览器设置(如窗口大小)。
  • 外部链接的 CSS 处理文档的样式和表示。
  • 外部链接的 JavaScript 增强了用户体验,但它并不引人注目,或者对应用的操作并不重要。

您的应用已经满足了前四条准则(虽然不太好看,但是应用可以在禁用样式的情况下工作)。因此,只要您的 JavaScript 没有创建任何在禁用 JavaScript 的情况下无法访问的新功能,您就已经成功地创建了一个逐步增强的 web 应用。

设定渐进增强目标

使用渐进式增强的原则,您将添加无需在模式窗口中刷新页面即可查看事件信息的能力,模式窗口是位于现有标记之上的内容区域,用于显示附加信息。这种窗口通常是由 JavaScript 触发的,它们被用在许多当今最流行的网站上。

在您的日历应用中,您将使用一个模态窗口在用户单击事件标题后显示事件的详细信息。这不需要使用 AJAX 刷新页面就可以完成。

在日历应用中包含 jQuery

正如您在本书前面的 jQuery 介绍中所了解到的,使用 jQuery 语法要求您首先包含 jQuery 库。如果可能的话,最好将<script>标签放在 HTML 标记的底部附近,就在结束 body 标签(</body>)之前。这样,HTML 可以先加载,用户不必在看到任何页面之前等待(通常较慢的)脚本文件加载。此外,它还可以防止代码与未完全加载的页面元素交互时出现 JavaScript 错误。

为了便于始终如一地遵循这一实践,您将在footer.inc.php ( /public/assets/common/footer.inc.php)中包含 jQuery 库和所有后续文件。首先在你的应用中包含最新版本的 jQuery 您可以通过在footer.inc.php中添加以下粗体行来实现这一点:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js

</script>

</body>

</html>

保存这段代码,然后在浏览器中加载http://localhost/。打开 Firebug 控制台并执行以下命令,以确保 jQuery 加载到您的应用中:

$("h2").text();

运行此命令后,控制台将显示以下输出:

>>> $("h2").text();

"January 2016

Note

因为您使用的是 Google 托管的 jQuery 库,所以除了 Apache 服务器之外,您还需要有一个可用的 Internet 连接。如果您无法访问互联网连接或不喜欢使用互联网连接,请从 http://jquery.com/ 下载最新版本的 jQuery 并包含它。

创建 JavaScript 初始化文件

您的应用遵循渐进增强准则,因此所有脚本都将保存在一个名为init.js的外部文件中。它将驻留在公共的js文件夹(/public/assets/js/init.js)中,并将包含您的应用的所有定制 jQuery 代码。

包括应用中的初始化文件

在您的应用可以使用任何脚本之前,您需要在应用中包含初始化文件。您的应用将使用 jQuery 语法,因此初始化文件需要包含在footer.inc.php中加载 jQuery 库的脚本之后。

通过在footer.inc.php中插入以下粗体代码,您可以将该文件包含在您的应用中:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js

</script>

<script src="assets/js/init.js"></script>

</body>

</html>

确保文档在脚本执行前准备就绪

在创建了init.js之后,使用 jQuery 中的document.ready快捷方式来确保在文档真正准备好被操作之前没有脚本执行。将以下代码插入init.js:

"use strict";  // enforce variable declarations – safer coding

// Makes sure the document is ready before executing scripts

jQuery(function($){

// A quick check to make sure the script loaded properly

console.log("init.js was loaded successfully.");

});

保存这个文件,并在 Firebug 控制台打开的情况下在浏览器中加载http://localhost/。文件加载后,您应该会在控制台中看到以下结果:

init.js was loaded successfully.

为 jQuery 创建的元素创建新的样式表

为了确保用 jQuery 创建的元素在开始构建时看起来是正确的,我们将向前跳一步,创建一个新的 CSS 文件来存储将要用 jQuery 脚本创建的元素的样式信息。

这个文件将被命名为ajax.css,它将驻留在css文件夹(/public/assets/css/ajax.css)中。创建后,在其中放置以下样式规则:

.modal-overlay {

position: fixed;

top: 0;

left: 0;

bottom: 0;

width: 100%;

height: 100%;

background-color: rgba(0,0,0,.5);

z-index: 4;

}

.modal-window {

position: absolute;

top: 140px;

left: 50%;

width: 300px;

height: auto;

margin-left: -150px;

padding: 20px;

border: 2px solid #000;

background-color: #FFF;

-moz-border-radius: 6px;

-webkit-border-radius: 6px;

border-radius: 6px;

-moz-box-shadow: 0 0 14px #123;

-webkit-box-shadow: 0 0 14px #123;

box-shadow: 0 0 14px #123;

z-index: 5;

}

.modal-close-btn {

position: absolute;

top: 0;

right: 4px;

margin: 0;

padding: 0;

text-decoration: none;

color: black;

font-size: 16px;

}

.modal-close-btn:before {

position: relative;

top: -1px;

content: "Close";

text-transform: uppercase;

font-size: 10px;

}

在索引文件中包含样式表

接下来,打开index.php,通过添加粗体行将新样式表包含在$css_files数组中:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo, "2016-01-01 12:00:00");

/*

* Set up the page title and CSS files

*/

$page_title = "Events Calendar";

$css_files = array('style.css', 'admin.css', 'ajax.css');

/*

* Include the header

*/

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<?php

/*

* Display the calendar HTML

*/

echo $cal->buildCalendar();

?>

</div><!-- end #content -->

<?php

/*

* Include the footer

*/

include_once 'assets/common/footer.inc.php';

?>

为事件数据创建模式窗口

您将为此应用创建的模式窗口将非常简单;创建它的脚本将遵循以下步骤。

Prevent the default action (opening the detailed event view in view.php).   Add an active class to the event link in the calendar.   Extract the query string from the event link’s href attribute.   Create a button that will close the modal window when clicked.   Create the modal window itself and put the Close button inside it.   Use AJAX to retrieve the information from the database and display it in the modal window.

当针对事件标题链接触发 click 事件时,将执行前面的所有步骤。

将函数绑定到标题链接的点击事件

首先向init.js添加一个新的选择器,它选择列表项(li>a)的直接后代的所有锚元素,并使用.on()方法将一个处理程序绑定到 click 事件(jQuery 版本来会使用.live()方法,但现在已经不推荐使用了)。

因为您需要绑定到许多动态创建的元素,所以您将始终使用三参数形式的.on() : $( selector ).on( event, childSelector, function ),其中 selector 是父容器(非动态),childSelector 表示(通常)动态创建的元素。为了获得最大的灵活性,您将使用“body”作为父容器。

将以下粗体代码插入init.js:

"use strict";  // enforce variable declarations – safer coding

// Makes sure the document is ready before executing scripts

jQuery(function($){

// Pulls up events in a modal window

$("body").on("click", " li>a", function(event){

// Event handler scripts go here

});

});

防止默认操作并添加活动类

接下来,您需要使用. preventDefault()来阻止默认动作,然后使用. addClass()向被点击的元素添加一个active类。

这是通过添加以下粗体代码来实现的:

"use strict";  // enforce variable declarations – safer coding

// Makes sure the document is ready before executing scripts

jQuery(function($){

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){

// Stops the link from loading view.php

event.preventDefault();

// Adds an "active" class to the link

$(this).addClass("active");

// Proves the event handler worked by logging the link text

console.log( $(this).text() );

});

});

保存此代码后,在浏览器中重新加载http://localhost/,并点击任何事件标题。不是在view.php上查看事件详情,而是在控制台中输出事件标题。例如,如果您单击New Year’s Day事件,您将在控制台中看到以下输出:

New Year’s Day

用正则表达式提取查询字符串

创建模式窗口是为了显示事件信息,所以您需要某种方法来知道应该显示哪个事件。无需添加任何额外的标记,您实际上可以使用正则表达式从href属性中提取事件 ID。

为此,您需要从链接中提取查询字符串。(如果href属性值为http://localhost/view.php?event_id=1,查询字符串为event_id=1。)

您将使用两项来提取查询字符串:.replace(),一个接受字符串或正则表达式模式进行匹配的原生 JavaScript 函数,匹配的字符串或模式应该替换为。

使用惰性方法:基于字符串的替换

乍一看,显而易见的解决方案似乎如下:

var data = string.replace("``http://localhost/view.php

是的,这确实有效,产生了输出"event_id=1"(如果你假设$string的初始值是http://localhost/view.php?event_id=1)。不幸的是,这种方法不够灵活;例如,如果应用被移动到另一个域名,该怎么办?或者,文件名改成event.php怎么办?这两种更改都会破坏前面的逻辑,需要更新脚本。

采用更好的解决方案:正则表达式

然而,有一个更好的解决方案:正则表达式。在大多数现代编程语言中,正则表达式是一种强大的模式匹配工具。

为了提取查询字符串,您将使用一种模式来查找字符串中的第一个问号(?),然后返回其后的所有内容。该模式将如下所示:

/.*?\?(.*)$/

JavaScript 中的正则表达式由表达式两端的正斜杠(/)分隔。在这个表达式中,模式寻找零个或更多的任意字符(从左到右),直到第一次到达一个问号;然后,它将问号之后直到字符串末尾的所有字符存储为一个命名组,以便在替换中使用。

Note

在第九章中,你会学到更多关于正则表达式及其工作原理的知识。

将正则表达式合并到脚本中

您想要提取被点击链接的href值,所以您将使用this关键字。为了使用 jQuery 方法,您必须首先将this作为选择器传递给 jQuery 函数。现在用.attr()方法访问href值,调用.replace(),并提取查询字符串。

. replace()中使用正则表达式时,不使用引号将模式括起来。使用刚才描述的正则表达式,修改init.js,将来自单击链接的查询字符串存储在一个名为data的变量中;通过添加粗体显示的代码来实现这一点:

"use strict";  // enforce variable declarations – safer coding

// Makes sure the document is ready before executing scripts

jQuery(function($){

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){

// Stops the link from loading view.php

event.preventDefault();

// Adds an "active" class to the link

$(this).addClass("active");

// Gets the query string from the link href

var data = $(this)

.attr("href")

.replace(/.+?\?(.*)$/, "$1");

// Logs the query string

console.log( data );

});

});

保存这段代码,重新加载http://localhost/,然后点击一个链接。您应该会在控制台中看到类似以下内容的内容:

event_id=1

创建模式窗口

下一步是生成实际创建模式窗口和覆盖图的 HTML 标记。这个标记非常简单:它基本上由一个包装在其他内容周围的div元素组成。例如,New Year’s Day事件模式窗口标记将如下所示:

<div class="modal-window">

<h2>New Year’s Day</h2>

<p class="dates">January 01, 2016, 12:00am—11:59pm</p>

<p>Happy New Year!</p>

</div>

您还将为其他特性使用这个相同的模式窗口(比如为事件显示编辑表单),因此模式窗口的实际创建将被抽象在一个单独的函数中,以便于重用。因为您将重用多个函数,所以您将通过将所有实用函数放在一个对象文字中来组织您的脚本,该对象文字是一个逗号分隔的名称-值对列表(有关更多信息,请参见侧栏“使用实用函数的对象文字”)。

创建实用函数来检查模式窗口

init.js的顶部,声明一个名为fx的新对象文字来存储您的实用函数:

"use strict";  // enforce variable declarations – safer coding

// Makes sure the document is ready before executing scripts

jQuery(function($){

// Functions to manipulate the modal window

var fx = {};

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){

// Stops the link from loading view.php

event.preventDefault();

// Adds an "active" class to the link

$(this).addClass("active");

// Gets the query string from the link href

var data = $(this)

.attr("href")

.replace(/.+?\?(.*)$/, "$1");

// Logs the query string

console.log( data );

});

});

存储在fx中的第一个函数将被调用initModal,它将检查一个模态窗口是否已经存在。如果是,该函数将选择它;否则,它将创建一个新的并将其附加到 body 标签中。

要查看某个元素是否已经存在,请在使用该元素的选择器执行 jQuery 函数后使用length属性。如果length属性返回0,则该元素当前不存在于文档对象模型(DOM)中。

通过在init.js内的fx中插入以下粗体代码,执行检查并返回一个模态窗口:

// Functions to manipulate the modal window

var fx = {

// Checks for a modal window and returns it, or

// else creates a new one and returns that

"initModal" : function() {

// If no elements are matched, the length

// property will return 0

if ( $(".modal-window").length==0 )

{

// Creates a div, adds a class, and

// appends it to the body tag

return $("<div>")

.addClass("modal-window")

.appendTo("body");

}

else

{

// Returns the modal window if one

// already exists in the DOM

return $(".modal-window");

}

}

};

从事件处理程序调用实用函数

接下来,通过在init.js中添加以下粗体代码,修改 click 事件处理程序,将fx.initModal的结果加载到一个变量中,以便在脚本中使用:

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){

// Stops the link from loading view.php

event.preventDefault();

// Adds an "active" class to the link

$(this).addClass("active");

// Gets the query string from the link href

var data = $(this)

.attr("href")

.replace(/.+?\?(.*)$/, "$1"),

// Checks if the modal window exists and

// selects it, or creates a new one

modal = fx.initModal();

});

Note

在本例中,data变量后的分号已被逗号替换。

保存并重新加载http://localhost/,然后点击其中一个事件标题,使一个模态窗口出现在屏幕上(见图 7-1 )。

A978-1-4842-1230-1_7_Fig1_HTML.jpg

图 7-1。

Clicking an event title causes a modal window to appear Using an Object Literal for Utility Functions

在编写应用时,效用函数经常发挥作用。应用越复杂,就越有可能存在大量的实用功能,也越难将这些功能组织起来。

保持效用函数有组织的一个选择是使用对象文字。这使得开发人员可以将功能放在一个地方,甚至可以根据它们的用途对功能进行分组。

理解对象文字

最简单地说,对象文字是 JavaScript 中的一个变量,它是一组空的花括号,表示一个空的对象文字:

var obj = {};

您可以使用逗号分隔的名称-值对向对象文字添加任意数量的值:

var obj = {

"name" : "Jason Lengstorf",

"age" : "25"

};

要访问一个值,只需添加一个点(.)和您希望访问的属性的名称:

alert(obj.name); // alerts "Jason Lengstorf"

对象文字如此有用是因为您还可以在其中存储函数:

var obj = {

"func" : function() { alert("Object literals rule!"); }

};

若要调用存储在对象文字中的函数,请使用与访问值相同的语法;但是,您还必须在末尾包含括号。否则,JavaScript 假设您试图将该函数存储在另一个变量中,并简单地返回它:

obj.func(); // alerts "Object literals rule!"

对象文字中的函数也可以接受参数:

var obj = {

"func" : function(text){ alert(text); }

};

obj.func("I’m a parameter!"); // alerts "I’m a parameter!"

对象文字与过程编程

将函数组织在一个对象文字中可以使代码更易读,如果开发人员努力使函数足够抽象,可以减少将来维护代码所花费的时间,因为所有的东西都是分门别类的,很容易找到。

也就是说,对象文字并不总是最好的解决方案。在处理多个对象的情况下,使用完全面向对象的方法会更好。如果几乎不需要任何脚本,一个对象文字可能是多余的。

最终,由作为开发人员的您来决定哪种方法最适合您的项目。最终,这是一个品味和舒适的问题;你需要决定什么使你的开发过程最容易。

用 AJAX 检索和显示事件信息

既然模式窗口已经加载,现在是时候加载事件信息并显示它了。为此,您将使用$.ajax()方法。

使用$.ajax()方法,您将使用POST方法将数据发送到一个处理文件(您将在下一节构建该文件),然后将响应插入到模态窗口中。

创建一个文件来处理 AJAX 请求

在调用$.ajax()之前,了解数据应该发送到哪里以及如何发送会有所帮助。在inc文件夹中,创建一个名为ajax.inc.php ( /public/assets/inc/ajax.inc.php)的新文件。这个文件将非常类似于process.inc.php,除了它将专门处理 AJAX 调用。因为从 PHP 函数返回的值不能被 JavaScript 读取,除非该值被实际输出(使用echo或其类似物),process.inc.php在应用的这个方面将不能正常工作。

本质上,ajax.inc.php将使用一个查找数组来确定需要使用哪些对象和方法,然后使用echo输出返回值,以便与 AJAX 一起使用。

首先启用会话,加载必要的配置信息,定义一个常数,并组装一个自动加载功能。现在将以下内容添加到ajax.inc.php:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

接下来,使用用于加载事件数据的信息定义查找数组,然后编写将实例化对象的代码,调用方法,并使用下面的粗体代码输出返回值:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define(ACTIONS, array(

'event_view' => array(

'object' => 'Calendar',

'method' => 'displayEvent'

)

)

);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

/*

* Check for an ID and sanitize it if found

*/

if ( isset($_POST['event_id']) )

{

$id = (int) $_POST['event_id'];

}

else { $id = NULL; }

echo $obj->$method($id);

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

与前面代码中的process.inc.php唯一真正的区别是在查找数组中缺少一个头键,并且使用了echo来输出被调用方法的返回值。

使用 AJAX 加载事件数据

回到init.js,您现在可以将呼叫添加到$.ajax()。在您的应用中,最终会有几次对$.ajax()的调用,所以将处理文件的位置存储在一个变量中,以便在文件位置或名称可能改变时易于维护。通过插入粗体显示的代码,将该变量添加到init.js的顶部:

"use strict";  // enforce variable declarations – safer coding

// Makes sure the document is ready before executing scripts

jQuery(function($){

// File to which AJAX requests should be sent

var processFile = "assets/inc/ajax.inc.php",

// Functions to manipulate the modal window

fx = {

// Checks for a modal window and returns it, or

// else creates a new one and returns that

"initModal" : function() {

// If no elements are matched, the length

// property will be 0

if ( $(".modal-window").length==0 )

{

// Creates a div, adds a class, and

// appends it to the body tag

return $("<div>")

.addClass("modal-window")

.appendTo("body");

}

else

{

// Returns the modal window if one

// already exists in the DOM

return $(".modal-window");

}

}

};

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){

// Stops the link from loading view. php

event.preventDefault();

// Adds an "active" class to the link

$(this).addClass("active");

// Gets the query string from the link href

var data = $(this)

.attr("href")

.replace(/.+?\?(.*)$/, "$1"),

// Checks if the modal window exists and

// selects it, or creates a new one

modal = fx.initModal();

});

});

接下来,在事件处理程序中设置对$.ajax()的调用。它将使用POST方法,指向processFile,并发送适当的数据。因为从链接中提取的查询字符串不包含 action 字段,所以在这里手动插入一个。最后,如果调用成功,使用.append()将返回的标记插入模式窗口,如果调用失败,则显示一条错误消息。

通过在init.js中插入以下粗体行来完成:

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){

// Stops the link from loading view.php

event.preventDefault();

// Adds an "active" class to the link

$(this).addClass("active");

// Gets the query string from the link href

var data = $(this)

.attr("href")

.replace(/.+?\?(.*)$/, "$1"),

// Checks if the modal window exists and

// selects it, or creates a new one

modal = fx.initModal();

// Loads the event data from the DB

$.ajax({

type: "POST",

url: processFile,

data: "action=event_view``&

success: function(data){

// Alert event data for now

modal.append(data);

},

error: function(msg) {

modal.append(msg);

}

});

});

保存您的更改,重新加载http://localhost/,点击事件标题,查看加载到模态窗口中的事件信息(见图 7-2 )。

A978-1-4842-1230-1_7_Fig2_HTML.jpg

图 7-2。

The event information loaded into the modal window

添加关闭按钮

就目前的情况而言,在单击事件标题后摆脱模式窗口的唯一方法是重新加载页面。当然,这还不够好,所以您需要添加一个关闭按钮。

为此,您需要创建一个新的链接,并绑定一个 click 事件处理程序,从 DOM 中移除模式窗口。为了给它一个传统的关闭按钮的感觉,使用乘法符号作为它的内容(并且ajax.css中的 CSS 在它前面添加了“关闭”这个词)。另外,添加一个href属性来确保鼠标悬停在链接上时,鼠标的行为就像按钮是可点击的一样。

接下来,通过在init.js中插入以下粗体代码来添加一个关闭按钮:

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){

// Stops the link from loading view.php

event.preventDefault();

// Adds an "active" class to the link

$(this).addClass("active");

// Gets the query string from the link href

var data = $(this)

.attr("href")

.replace(/.+?\?(.*)$/, "$1"),

// Checks if the modal window exists and

// selects it, or creates a new one

modal = fx.initModal();

// Creates a button to close the window

$("<a>")

.attr("href", "#")

.addClass("modal-close-btn")

.html("``&

.click(function(event){

// Prevent the default action

event.preventDefault();

// Removes modal window

$(".modal-window")

.remove();

})

.appendTo(modal);

// Loads the event data from the DB

$.ajax({

type: "POST",

url: processFile,

data: "action=event_view&" + data,

success: function(data){

// Alert event data for now

modal.append(data);

},

error: function(msg) {

modal.append(msg);

}

});

});

保存前面的代码后,加载http://localhost/并点击事件标题以查看新的关闭按钮(参见图 7-3 )。单击关闭按钮移除模式窗口。

A978-1-4842-1230-1_7_Fig3_HTML.jpg

图 7-3。

The Close button is now visible in the modal window

向模式窗口的创建和销毁添加效果

为了给模式窗口增加更多的风格和修饰,您将添加一些效果,使盒子在创建时淡入,在移除时淡出。此外,为了在模式窗口处于活动状态时帮助将焦点吸引到它,您将向站点添加一个覆盖图,该覆盖图将使除模式窗口之外的所有内容变暗。

淡出模式窗口

首先,您需要添加淡出模式窗口的效果。这个函数会以几种方式触发,其中一些也会触发事件;要处理这个问题,您需要创建一个条件语句来检查事件是否被触发,如果是这种情况,就阻止默认操作。

接下来,从所有链接中移除类active,因为当模式窗口不可见时,它们都不被使用。

最后,使用. fadeOut()选择并淡出模态窗口。在.fadeOut()的回调函数中,模态窗口将被完全从 DOM 中移除。

您可以通过在fx对象文本中插入以下粗体代码来添加该函数:

// Functions to manipulate the modal window

fx = {

// Checks for a modal window and returns it, or

// else creates a new one and returns that

"initModal" : function() {

// If no elements are matched, the length

// property will be 0

if ( $(".modal-window").length==0 )

{

// Creates a div, adds a class, and

// appends it to the body tag

return $("<div>")

.addClass("modal-window")

.appendTo("body");

}

else

{

// Returns the modal window if one

// already exists in the DOM

return $(".modal-window");

}

},

// Fades out the window and removes it from the DOM

"boxout" : function(event) {

// If an event was triggered by the element

// that called this function, prevents the

// default action from firing

if ( event!=undefined )

{

event.preventDefault();

}

// Removes the active class from all links

$("a").removeClass("active");

// Fades out the modal window, then removes

// it from the DOM entirely

$(".modal-window")

.fadeOut("slow", function() {

$(this).remove();

}

);

}

};

要将这个新函数合并到脚本中,请使用以下粗体代码修改 Close 按钮的 click 事件处理程序:

// Creates a button to close the window

$("<a>")

.attr("href", "#")

.addClass("modal-close-btn")

.html("×")

.click(function(event){

// Removes modal window

fx.boxout(event);

})

.appendTo(modal);

保存init.js并在浏览器中重新加载http://localhost/。单击事件标题创建一个新的模态窗口,然后单击关闭按钮观看模态窗口淡出(参见图 7-4 )。

A978-1-4842-1230-1_7_Fig4_HTML.jpg

图 7-4。

The modal window mid-fade after the user clicks the Close button

在模式窗口中添加覆盖和淡入淡出

要在模式窗口中添加覆盖和淡入,需要向fx对象文字添加另一个函数。它将被调用boxin,在事件标题点击处理程序中$.ajax()的成功回调中被调用。该函数将接受两个参数:由ajax.inc.php ( data)返回的数据和模态窗口对象(modal)。

首先,该函数将创建一个新的div,其类为modal-overlay;接下来,它将隐藏div并将其追加到 body 元素中。为了提高可用性,这个覆盖图还会附带一个 click 处理程序,当通过调用fx.boxout()被点击时,它会移除模态窗口。

接下来,该函数将隐藏模态窗口,并将存储在data中的信息追加到其中。最后,它将使用.fadeIn()淡入两个元素。

通过插入粗体显示的代码,将该函数添加到fx对象文字中:

// Functions to manipulate the modal window

fx = {

// Checks for a modal window and returns it, or

// else creates a new one and returns that

"initModal" : function() {

// If no elements are matched, the length

// property will be 0

if ( $(".modal-window").length==0 )

{

// Creates a div, adds a class, and

// appends it to the body tag

return $("<div>")

.addClass("modal-window")

.appendTo("body");

}

else

{

// Returns the modal window if one

// already exists in the DOM

return $(".modal-window");

}

},

// Adds the window to the markup and fades it in

"boxin" : function(data, modal) {

// Creates an overlay for the site, adds

// a class and a click event handler, then

// appends it to the body element

$("<div>")

.hide()

.addClass("modal-overlay")

.click(function(event){

// Removes event

fx.boxout(event);

})

.appendTo("body");

// Loads data into the modal window and

// appends it to the body element

modal

.hide()

.append(data)

.appendTo("body");

// Fades in the modal window and overlay

$(".modal-window,.modal-overlay")

.fadeIn("slow");

},

// Fades out the window and removes it from the DOM

"boxout" : function(event) {

// If an event was triggered by the element

// that called this function, prevents the

// default action from firing

if ( event!=undefined )

{

event.preventDefault();

}

// Removes the active class from all links

$("a").removeClass("active");

// Fades out the modal window, then removes

// it from the DOM entirely

$(".modal-window")

.fadeOut("slow", function() {

$(this).remove();

}

);

}

};

接下来,您需要修改在单击事件标题调用fx.boxin时成功执行$.ajax()时触发的回调函数;您可以通过添加以下粗体代码行来实现这一点:

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){

// Stops the link from loading view.php

event.preventDefault();

// Adds an "active" class to the link

$(this).addClass("active");

// Gets the query string from the link href

var data = $(this)

.attr("href")

.replace(/.+?\?(.*)$/, "$1"),

// Checks if the modal window exists and

// selects it, or creates a new one

modal = fx.initModal();

// Creates a button to close the window

$("<a>")

.attr("href", "#")

.addClass("modal-close-btn")

.html("×")

.click(function(event){

// Removes modal window

fx.boxout(event);

})

.appendTo(modal);

// Loads the event data from the DB

$.ajax({

type: "POST",

url: processFile,

data: "action=event_view&" + data,

success: function(data){

fx.boxin(data, modal);

},

error: function(msg) {

modal.append(msg);

}

});

});

保存这段代码,重新加载http://localhost/,点击一个事件标题,查看模态叠加和模态窗口淡入(见图 7-5 )。

A978-1-4842-1230-1_7_Fig5_HTML.jpg

图 7-5。

The modal window with an overlay to help draw the focus

你可能已经注意到模式窗口在打开时会闪烁。发生这种情况是因为fx.initModal()将模态窗口附加到 body 元素,而没有隐藏它。要纠正这一点,使用下面的粗体代码添加对fx.initModal()中的.hide()的调用:

// Functions to manipulate the modal window

fx = {

// Checks for a modal window and returns it, or

// else creates a new one and returns that

"initModal" : function() {

// If no elements are matched, the length

// property will be 0

if ( $(".modal-window").length==0 )

{

// Creates a div, adds a class, and

// appends it to the body tag

return $("<div>")

.hide()

.addClass("modal-window")

.appendTo("body");

}

else

{

// Returns the modal window if one

// already exists in the DOM

return $(".modal-window");

}

},

// Adds the window to the markup and fades it in

"boxin" : function(data, modal) {

// Code omitted for brevity

},

// Fades out the window and removes it from the DOM

"boxout" : function(event) {

// Code omitted for brevity

}

};

最后,单击关闭按钮不会删除覆盖。要淡出并移除覆盖图,只需修改fx.boxout()中的选择器以包含类别modal-overlay:

// Functions to manipulate the modal window

fx = {

// Checks for a modal window and returns it, or

// else creates a new one and returns that

"initModal" : function() {

// Code omitted for brevity

},

// Adds the window to the markup and fades it in

"boxin" : function(data, modal) {

// Code omitted for brevity

},

// Fades out the window and removes it from the DOM

"boxout" : function(event) {

// If an event was triggered by the element

// that called this function, prevents the

// default action from firing

if ( event!=undefined )

{

event.preventDefault();

}

// Removes the active class from all links

$("a").removeClass("active");

// Fades out the modal window and overlay,

// then removes both from the DOM entirely

$(".modal-window,.modal-overlay")

.fadeOut("slow", function() {

$(this).remove();

}

);

}

};

完成更改后,重新加载http://localhost/并点击事件标题。模式窗口和覆盖将淡入,单击关闭按钮或覆盖将导致模式窗口和覆盖淡出。

摘要

在本章中,您学习了如何使用渐进式增强技术在 jQuery 中动态加载事件数据。您还了解了事件处理、基本效果,甚至还了解了一点正则表达式。

在下一章中,您将继续添加 AJAX 功能,通过 AJAX 使编辑控件工作。

Footnotes 1

www.hesketh.com/about-us/leadership-team/

八、使用 AJAX 和 jQuery 编辑日历

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​8) contains supplementary material, which is available to authorized users.

现在您的应用可以显示事件数据而无需刷新页面,您可以看到 AJAX 在 web 应用中提供的额外便利。从历史上看,使用 web 应用的最大缺陷之一是,每个动作,无论多小,通常都需要等待页面刷新,同时保存设置。当用户需要在共享计算机上访问他的信息时,Web 应用很方便,但缓慢的工作流程通常足以让用户尽可能倾向于桌面应用。

然而,随着 AJAX 的主流接受和使用,用户现在可以快速地进行更改,而不必一直等待页面重新加载。这让网络应用感觉更像桌面应用,也让它们对用户更有吸引力。

在本章中,您将学习添加脚本,使管理员的编辑控件运行顺畅,而不需要为每个操作刷新页面。唯一需要页面刷新的操作是登录,因为这需要对会话进行更改。

Note

开始本章中的练习之前,请登录日历应用。默认登录依赖于用户名testuser和密码admin

打开事件创建表单

首先,您将修改脚本,让管理员在不刷新页面的情况下添加新事件。打开init.js,选择按钮,按其类别添加新事件(admin)。添加一个 click 事件处理程序来阻止默认操作,并(目前)记录一条消息来确认它被正确触发:

"use strict";

jQuery(function($){

var processFile = "assets/inc/ajax.inc.php",

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...}

}

// Pulls up events in a modal window

$("body").on("click", “li>a”, function(event){...});

// Displays the edit form as a modal window

$("body").on("click", ".admin", function(event){

// Prevents the form from submitting

event.preventDefault();

// Logs a message to prove the handler was triggered

console.log( "Add a New Event button clicked!" );

});

});

Note

为了简洁起见,所有未更改的函数都被缩写,并且本章的代码示例中省略了注释。你可以在这本书的首页找到代码, http://www.apress.com/9781484212318

保存此代码并刷新http://localhost/。单击“添加新事件”按钮,您将看到以下结果记录到控制台中:

Add a New Event button clicked!

添加 AJAX 调用来加载表单

接下来,创建一个变量来存储将被发送到处理文件的动作。您正在加载编辑和创建表单,因此将动作设置为event_edit

现在可以调用$.ajax()函数了。该函数类似于将事件数据加载到模式窗口的脚本;事实上,唯一的区别在于提交的数据和处理返回值的方式。

在一次成功的加载中,您隐藏表单并在变量form中存储对它的引用。接下来,使用fx.initModal()检查模态窗口,并使用第一个参数为空的fx.boxin()淡入。最后,将表单添加到模式窗口,淡入,并为其分配类edit-form,以便以后选择。

将以下粗体代码添加到init.js以执行这些步骤:

"use strict";

jQuery(function($){

var processFile = "assets/inc/ajax.inc.php",

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...}

}

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){...});

// Displays the edit form as a modal window

$("body").on("click", ".admin", function(event){

// Prevents the form from submitting

event.preventDefault();

// Loads the action for the processing file

var action = "edit_event";

// Loads the editing form and displays it

$.ajax({

type: "POST",

url: processFile,

data: "action="+action,

success: function(data){

// Hides the form

var form = $(data).hide(),

// Make sure the modal window exists

modal = fx.initModal();

// Call the boxin function to create

// the modal overlay and fade it in

fx.boxin(null, modal);

// Load the form into the window,

// Fades in the content, and adds

// a class to the form

form

.appendTo(modal)

.addClass("edit-form")

.fadeIn("slow");

},

error: function(msg){

alert(msg);

}

});

});

});

但是您还没有准备好运行这段代码。

修改 AJAX 处理文件以加载表单

在前面的 AJAX 调用工作之前,您需要修改ajax.inc.php查找数组。添加一个新的数组元素,告诉脚本创建一个新的Calendar对象,然后用粗体显示的代码调用displayForm()方法:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define(ACTIONS, array(

'event_view' => array(

'object' => 'Calendar',

'method' => 'displayEvent'

),

'edit_event' => array(

'object' => 'Calendar',

'method' => 'displayForm'

)

)

);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

/*

* Check for an ID and sanitize it if found

*/

if ( isset($_POST['event_id']) )

{

$id = (int) $_POST['event_id'];

}

else { $id = NULL; }

echo $obj->$method($id);

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

现在保存文件,加载http://localhost/,点击“添加新事件”按钮。一个新的模态窗口将淡入,编辑表单在里面(见图 8-1 )。

A978-1-4842-1230-1_8_Fig1_HTML.jpg

图 8-1。

The event creation form loaded in a modal window

使取消按钮的行为像关闭按钮一样

您可能已经注意到,当显示窗体时,模式窗口不包含关闭按钮。但是,模式窗口包含一个取消按钮,单击该按钮将刷新页面。不需要在窗口中添加更多的按钮,只需要让 Cancel 按钮调用fx.boxout()方法来关闭窗口。

为了实现这一点,使用.on()将一个 click 事件处理程序绑定到带有edit-form类的表单中包含单词 cancel 的任何链接。

"use strict";

jQuery(function($){

var processFile = "assets/inc/ajax.inc.php",

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...}

}

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){...});

// Displays the edit form as a modal window

$("body").on("click", ".admin", function(event){

// Make the cancel button on editing forms behave like the

// close button and fade out modal windows and overlays

$("body").on("click", ".edit-form a:contains(cancel)", function(event){

fx.boxout(event);

});

});

保存文件,重新加载http://localhost/,点击“添加新事件”按钮。模式窗口加载后,点击表单中的Cancel链接。模式窗口和覆盖将淡出,就像单击关闭按钮时一样。

在数据库中保存新事件

要使表单正常工作,现在必须向表单上的提交按钮添加一个 click 事件处理程序。这个处理程序将阻止默认的表单提交,然后它将使用.serialize()从表单输入中创建一个查询字符串。然后,它将使用序列化数据通过POSTajax.inc.php提交表单。

首先,向存在于带有edit-form类的表单中的submit类型的任何输入添加一个新的点击处理程序。使用.on()确保动态创建的输入仍然是处理程序的目标。您可以使用event.preventDefault()阻止默认动作。

通过将粗体代码插入init.js来完成:

"use strict";

jQuery(function($){

var processFile = "assets/inc/ajax.inc.php",

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...}

}

// Pulls up events in a modal window

$("body").on("click", "li>a", function(event){...});

// Displays the edit form as a modal window

$("body").on("click", ".admin", function(event){

// Make the cancel button on editing forms behave like the

// close button and fade out modal windows and overlays

$("body").on("click", ".edit-form a:contains(cancel)", function(event){

fx.boxout(event);

});

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function(event){

// Prevents the default form action from executing

event.preventDefault();

// Logs a message to indicate the script is working

console.log( "Form submission triggered!" );

});

});

接下来,在浏览器中保存并重新加载日历。单击“添加新事件”按钮打开模式窗口,然后单击“创建新事件”按钮提交表单。这会将以下结果输出到控制台:

Form submission triggered!

序列化表单数据

要将事件数据发送到处理文件,您需要将数据转换为查询字符串。幸运的是,jQuery 有一个名为.serialize()的内置方法来做这件事。它将把表单输入转换成一串名称-值对,用一个&符号(&)隔开。

修改init.js通过选择点击输入的父表单来序列化表单数据,然后序列化其数据。接下来,暂时将输出记录到控制台:

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function(event){

// Prevents the default form action from executing

event.preventDefault();

// Serializes the form data for use with $.ajax()

var formData = $(this).parents("form").serialize();

// Logs a message to indicate the script is working

console.log( formData );

});

保存前面的代码,并在浏览器中打开事件创建表单。现在输入以下测试数据:

  • 事件标题:测试事件
  • 活动开始时间:2016-01-04 08:00:00
  • 活动结束时间:2016-01-04 10:00:00
  • 事件描述:这是一个测试描述。

单击“创建新事件”按钮提交表单,并将以下内容输出到您的控制台(令牌值会有所不同):

event_title=Test+Event&event_start=2016-01-04+08%3A00%3A00&event_end=2016 A978-1-4842-1230-1_8_Figa_HTML.jpg

-01-04+10%3A00%3A00&event_description=This+is+a+test+description.&event_id=&token= A978-1-4842-1230-1_8_Figa_HTML.jpg

861e58daa0cfcf2c215f71d6f2bda1661e81c4c0&action=event_edit

将序列化的表单数据提交到处理文件

既然表单数据已经序列化,您就可以使用$.ajax()将数据发送到处理文件。

使用POST方法将序列化数据提交给ajax.inc.php,然后淡出模态窗口,并在成功提交时使用fx.boxout()进行覆盖。此外,在 Firebug 控制台中记录一条确认消息,并将以下粗体代码附加到init.js:

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function(event){

// Prevents the default form action from executing

event.preventDefault();

// Serializes the form data for use with $.ajax()

var formData = $(this).parents("form").serialize();

// Sends the data to the processing file

$.ajax({

type: "POST",

url: processFile,

data: formData,

success: function(data) {

// Fades out the modal window

fx.boxout();

// Logs a message to the console

console.log( "Event saved!" );

},

error: function(msg) {

alert(msg);

}

});

});

此时,脚本已经准备好保存新事件。但是您还没有准备好运行这段代码。首先,需要修改ajax.inc.php来接受这个数据。

修改 AJAX 处理文件以处理新的提交

ajax.inc.php准备好接受来自事件编辑表单的提交就像向查找数组添加一个新元素一样简单:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define(ACTIONS, array(

'event_view' => array(

'object' => 'Calendar',

'method' => 'displayEvent'

),

'edit_event' => array(

'object' => 'Calendar',

'method' => 'displayForm'

),

'event_edit' => array(

'object' => 'Calendar',

'method' => 'processForm'

)

)

);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

/*

* Check for an ID and sanitize it if found

*/

if ( isset($_POST['event_id']) )

{

$id = (int) $_POST['event_id'];

}

else { $id = NULL; }

echo $obj->$method($id);

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

保存该文件并重新加载http://localhost/。接下来,单击“添加新事件”按钮,在模式窗口中打开表单,然后输入一个包含以下信息的新事件:

  • 事件标题:测试事件
  • 活动开始时间:2016-01-04 08:00:00
  • 活动结束时间:2016-01-04 10:00:00
  • 事件描述:这是一个测试描述。

现在点击“创建新事件”按钮;模式窗口将淡出,并且控制台中将记录以下消息:

Event saved!

请注意,除非页面刷新,否则新事件不会出现在日历中。这可能会让用户感到困惑,因此在下一节中,您将修改应用,以便在成功保存后将新创建的事件添加到日历中。

添加事件而不刷新

将新事件添加到日历中相当复杂;保存事件后,您需要采取以下步骤。

Deserialize the form data.   Create date objects for both the currently displayed month and the new event.   Make sure the month and year match up for the new event.   Get the new event’s ID.   Determine on what day of the month the event falls.   Generate a new link with the proper event data and insert it into the corresponding calendar day.

这个功能将被包含在一个新添加的名为addeventfx对象中,它将接受从ajax.inc.php ( data)返回的数据,以及序列化的表单数据(formData)。

首先,通过插入以下粗体代码修改init.js中的fx对象文字:

"use strict";

jQuery(function($){

var processFile = "assets/inc/ajax.inc.php",

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {... },

// Adds a new event to the markup after saving

"addevent" : function(data, formData){

// Code to add the event goes here

}

};

$("body").on("click", "li>a", function(event){...});

$("body").on("click", ".admin", function(event){...});

$("body").on("click", ".edit-form a:contains(cancel)", function(event){...});

$("body").on("click", ".edit-form input[type=submit]", function(event){...});

反序列化表单数据

添加新事件的第一步是反序列化表单数据。因为这个动作是独立的,所以您将通过在接受字符串(str)的名为deserializefx对象文本中创建一个附加函数来处理这个步骤:

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...},

// Adds a new event to the markup after saving

"addevent" : function(data, formData){

// Code to add the event goes here

},

// Deserializes the query string and returns

// an event object

"deserialize" : function(str){

// Deserialize data here

}

};

正如您在本书前面所学的,序列化的字符串是一系列由等号(=)连接并由&符号(&)分隔的名称-值对。两个序列化名称-值对的示例可能如下所示:

name1=value1&name2=value2

要反序列化这些值,首先使用本地 JavaScript 函数.split()在每个&符号处拆分字符串。此函数将字符串分解为一个名称-值对数组:

Array

(

0 => "name1=value1",

1 => "name2=value2"

)

接下来,您需要遍历名称-值对数组。在这个循环中,在等号处分割数组,并将数组存储在一个名为pairs的变量中。这意味着每个名称-值对被分成一个数组,第一个索引包含名称,第二个索引包含值。该数组遵循以下格式:

Array

(

0 => "name1",

1 => "value1"

)

将这些值分别存储在名为keyval的变量中,然后作为属性存储在名为entry的新对象中。

当循环完成时,返回反序列化的data对象。

接下来,在fx.deserialize中添加以下粗体代码:

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...},

// Adds a new event to the markup after saving

"addevent" : function(data, formData){

// Code to add the event goes here

},

// Deserializes the query string and returns

// an event object

"deserialize" : function(str){

// Breaks apart each name-value pair

var data = str.split("``&

// Declares variables for use in the loop

pairs=[], entry={}, key, val;

// Loops through each name-value pair

for (var x in data )

{

// Splits each pair into an array

pairs = data[x].split("=");

// The first element is the name

key = pairs[0];

// Second element is the value

val = pairs[1];

// Stores each value as an object property

entry[key] = val;

}

return entry;

}

};

解码表单值中任何 URL 编码的字符

fx.deserialize正式投入使用之前,您必须首先修改它来解码任何 URL 编码的实体。当数据被序列化时,字符串值被编码,以便它们可以在查询字符串中传递。这意味着字符串“我正在测试&日志记录!”序列化时将转换为以下内容:

I’m+testing+%26+logging!

要扭转这种情况,使用正则表达式/\+/g将所有加号(+)替换为空格;该表达式只匹配加号。表达式结束分隔符后面的g使正则表达式进行全局搜索,因此不止一个匹配将被替换。

接下来,您需要使用本机的独立 JavaScript 函数decodeURIComponent()。在fx中创建一个名为urldecode的新函数,并插入以下代码:

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...},

// Adds a new event to the markup after saving

"addevent" : function(data, formData){

// Code to add the event goes here

},

// Deserializes the query string and returns

// an event object

"deserialize" : function(str){

// Breaks apart each name-value pair

var data = str.split("&"),

// Declares variables for use in the loop

pairs=[], entry={}, key, val;

// Loops through each name-value pair

for ( x in data )

{

// Splits each pair into an array

pairs = data[x].split("=");

// The first element is the name

key = pairs[0];

// Second element is the value

val = pairs[1];

// Stores each value as an object property

entry[key] = val;

}

return entry;

},

// Decodes a query string value

"urldecode" : function(str) {

// Converts plus signs to spaces

var converted = str.replace(/\+/g, ' ');

// Converts any encoded entities back

return decodeURIComponent(converted);

}

};

接下来,通过添加以下粗体代码来实现fx.deserialize中的fx.urldecode:

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...},

// Adds a new event to the markup after saving

"addevent" : function(data, formData){

// Code to add the event goes here

},

// Deserializes the query string and returns

// an event object

"deserialize" : function(str){

// Breaks apart each name-value pair

var data = str.split("&"),

// Declares variables for use in the loop

pairs=[], entry={}, key, val;

// Loops through each name-value pair

for ( x in data )

{

// Splits each pair into an array

pairs = data[x].split("=");

// The first element is the name

key = pairs[0];

// Second element is the value

val = pairs[1];

// Reverses the URL encoding and stores

// each value as an object property

entry[key] = fx.urldecode(val);

}

return entry;

},

"urldecode" : function(str) {...}

};

将这一切结合在一起

有了fx.deserializefx.urldecode之后,您现在可以通过添加一个变量(entry)来存储反序列化的事件数据,从而修改fx.addevent:

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...},

// Adds a new event to the markup after saving

"addevent" : function(data, formData){

// Converts the query string to an object

var entry = fx.deserialize(formData);

},

"deserialize" : function(str){...},

"urldecode" : function(str) {...}

};

创建日期对象

因为只有为正在显示的月份创建的事件才应该添加到日历中,所以您需要确定正在显示的月份和年份,以及事件发生的月份和年份。

Note

对于这一步,您将利用 JavaScript 的内置Date对象,它提供了简化许多日期相关操作的方法。关于与Date对象相关的所有可用方法的完整解释,请访问 http://w3schools.com/jsref/jsref_obj_date.asp

使用 ID 修改日历类

要为当前显示的月份生成一个Date对象,您需要向在日历上方显示月份的h2元素添加一个 ID。为了确保跨浏览器的兼容性,用下面的粗体代码修改Calendar类中的buildCalendar()方法:

public function buildCalendar()

{

/*

* Determine the calendar month and create an array of

* weekday abbreviations to label the calendar columns

*/

$cal_month = date('F Y', strtotime($this->_useDate));

$cal_id = date('Y-m', strtotime($this->_useDate));

define('WEEKDAYS', array('Sun', 'Mon', 'Tue',

'Wed', 'Thu', 'Fri', 'Sat'));

/*

* Add a header to the calendar markup

*/

$html = "\n\t<h2 id=\"month-$cal_id\">$cal_month</h2>";

for ( $d=0, $labels=NULL; $d<7; ++$d )

{

$labels .= "\n\t\t<li>" . WEEKDAYS[$d] . "</li>";

}

$html .= "\n\t<ul class=\"weekdays\">"

. $labels . "\n\t</ul>";

// For brevity, the remainder of this method has been omitted

}

Note

对 ID 使用“month-”前缀意味着您符合 W3 标准,该标准规定元素 ID 必须以字母开头。

用 JavaScript 构建日期对象

为了确保新事件在当前月份内,创建两个空的Date对象:一个用于当前月份,一个用于新事件。

要设置当前月份的Date对象的值,使用.attr()方法从H2元素中检索 ID 属性,在连字符处将其拆分,并将其存储在cdata变量中。

对于新事件,在空格处拆分entry.event_start的值,并获取第一个数组元素(以YYYY-MM-DD的格式表示的日期)并将其存储在一个名为date的变量中。接下来,在连字符处分割信息,并将数组存储在一个名为edata的变量中。

要设置Date对象,使用来自cdataedata的数据分别设置calevent中的日期。

最后,用下面的粗体代码修改fx.addevent:

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...},

// Adds a new event to the markup after saving

"addevent" : function(data, formData){

// Converts the query string to an object

var entry = fx.deserialize(formData ),

// Makes a date object for current month

cal = new Date(NaN),

// Makes a date object for the new event

event = new Date(NaN),

// Extracts the calendar month from the H2 ID

cdata = $("h2").attr("id").split('-'),

// Extracts the event day, month, and year

date = entry.event_start.split(' ')[0],

// Splits the event data into pieces

edata = date.split('-');

// Sets the date for the calendar date object

cal.setFullYear(cdata[1], cdata[2], 1);

// Sets the date for the event date object

event.setFullYear(edata[0], edata[1], edata[2]);

},

"deserialize" : function(str){...},

"urldecode" : function(str) {...}

};

修复时区不一致

您没有向Date对象传递时间或时区,因此该对象将默认为格林威治标准时间午夜(00:00:00 GMT)。这可能会导致您的日期对于不同时区的用户表现出意外。为了解决这个问题,您需要使用两个内置的Date对象方法:.setMinutes().getTimezoneOffset()来根据时区偏移量调整日期。

. getTimezoneOffset()的返回值是 GMT 和用户所在时区的分钟数之差。例如,.getTimezoneOffset()在山地标准时间(-0700)的返回值是 420。

使用.setMinutes(),您可以将时区偏移量的值添加到Date对象中,这将把日期返回到给定日期的午夜,而不管用户处于哪个时区。

您可以使用以下粗体代码进行调整:

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...},

// Adds a new event to the markup after saving

"addevent" : function(data, formData){

// Converts the query string to an object

var entry = fx.deserialize(formData),

// Makes a date object for current month

cal = new Date(NaN),

// Makes a date object for the new event

event = new Date(NaN),

// Extracts the event day, month, and year

date = entry.event_start.split(' ')[0],

// Splits the event data into pieces

edata = date.split('-'),

// Extracts the calendar month from the H2 ID

cdata = $("h2").attr("id").split('-');

// Sets the date for the calendar date object

cal.setFullYear(cdata[1], cdata[2], 1);

// Sets the date for the event date object

event.setFullYear(edata[0], edata[1], edata[2]);

// Since the date object is created using

// GMT, then adjusted for the local time zone,

// adjust the offset to ensure a proper date

event.setMinutes(event.getTimezoneOffset());

},

"deserialize" : function(str){...},

"urldecode" : function(str) {...}

};

确保事件发生在当月

下一步是设置一个条件语句,确保只追加属于日历的事件。如果当前日历月和事件日期之间的年份和月份都匹配,您可以使用Date对象的. getDay()方法提取该月的日期。为了正确处理下一步,即向一位数日期添加前导零,您还需要将该值转换为字符串,这是通过将该值传递给String()来完成的。

一个月中的某一天需要有一个前导零来正确匹配日历。例如,如果返回的日期只有一位数字,您可以在日期前添加一个前导零。为此,请插入以下粗体代码:

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...},

// Adds a new event to the markup after saving

"addevent" : function(data, formData){

// Converts the query string to an object

var entry = fx.deserialize(formData),

// Makes a date object for current month

cal = new Date(NaN),

// Makes a date object for the new event

event = new Date(NaN),

// Extracts the event day, month, and year

date = entry.event_start.split(' ')[0],

// Splits the event data into pieces

edata = date.split('-'),

// Extracts the calendar month from the H2 ID

cdata = $("h2").attr("id").split('-');

// Sets the date for the calendar date object

cal.setFullYear(cdata[1], cdata[2], 1);

// Sets the date for the event date object

event.setFullYear(edata[0], edata[1], edata[2]);

// Since the date object is created using

// GMT, then adjusted for the local timezone,

// adjust the offset to ensure a proper date

event.setMinutes(event.getTimezoneOffset());

// If the year and month match, start the process

// of adding the new event to the calendar

if ( cal.getFullYear()==event.getFullYear()

&& cal.getMonth()==event.getMonth() )

{

// Gets the day of the month for event

var day = String(event.getDate());

// Adds a leading zero to 1-digit days

day = day.length==1 ? "0"+day : day;

}

},

"deserialize" : function(str){...},

"urldecode" : function(str) {...}

};

将事件附加到日历

您终于可以将新事件添加到日历中了。为此,创建一个新的锚元素,隐藏它,设置它的href属性,并使用事件的标题作为链接文本。

接下来,使用.delay(1000)设置一秒钟的延迟,并淡入新事件。

您可以通过添加以下粗体显示的代码来实现这一点:

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...},

// Adds a new event to the markup after saving

"addevent" : function(data, formData){

// Converts the query string to an object

var entry = fx.deserialize(formData),

// Makes a date object for current month

cal = new Date(NaN),

// Makes a date object for the new event

event = new Date(NaN),

// Extracts the event day, month, and year

date = entry.event_start.split(' ')[0],

// Splits the event data into pieces

edata = date.split('-'),

// Extracts the calendar month from the H2 ID

cdata = $("h2").attr("id").split('-');

// Sets the date for the calendar date object

cal.setFullYear(cdata[1], cdata[2], 1);

// Sets the date for the event date object

event.setFullYear(edata[0], edata[1], edata[2]);

// Since the date object is created using

// GMT, then adjusted for the local timezone,

// adjust the offset to ensure a proper date

event.setMinutes(event.getTimezoneOffset());

// If the year and month match, start the process

// of adding the new event to the calendar

if ( cal.getFullYear()==event.getFullYear()

&& cal.getMonth()==event.getMonth() )

{

// Gets the day of the month for event

var day = String(event.getDate());

// Adds a leading zero to 1-digit days

day = day.length==1 ? "0"+day : day;

// Adds the new date link

$("<a>")

.hide()

.attr("href", "view.php?event_id="+data)

.text(entry.event_title)

.insertAfter($("strong:contains("+day+")"))

.delay(1000)

.fadeIn("slow");

}

},

"deserialize" : function(str){...},

"urldecode" : function(str) {...}

}

Note

到目前为止,data变量还没有定义。您将在下一节中解决这个问题。

现在,回到 Submit 按钮的 click 事件处理程序,使用下面的粗体代码修改$.ajax()函数的成功回调以执行fx.addevent():

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function (event){

// Prevents the default form action from executing

event.preventDefault();

// Serializes the form data for use with $.ajax()

var formData = $(this).parents("form").serialize();

// Sends the data to the processing file

$.ajax({

type: "POST",

url: processFile,

data: formData,

success: function(data) {

// Fades out the modal window

fx.boxout();

// Adds the event to the calendar

fx.addevent(data, formData);

},

error: function(msg) {

alert(msg);

}

});

});

保存该文件并重新加载http://localhost/。调出事件创建表单,用以下信息创建一个新事件:

  • 事件标题:加法测试
  • 活动开始时间:2016-01-09 12:00:00
  • 活动结束时间:2016-01-09 14:00:00
  • 事件描述:这是一个向日历动态添加新事件的测试。

提交表单会导致模式窗口淡出;一秒钟后,新的事件标题将淡入日历的适当位置(见图 8-2 )。

A978-1-4842-1230-1_8_Fig2_HTML.jpg

图 8-2。

The calendar after the new event is created

获取新事件的正确 ID

目前,新事件在创建后不刷新页面是不可见的。要看到这一点,请立即尝试单击您刚刚添加的新创建的“添加测试”链接。如果你从第四章开始就一直关注事态的发展,你会得到如图 8-3 所示的相当令人惊讶的结果。(如果您没有遵循,您可能会得到不同的结果,例如一个空的对话框,甚至是一个致命的错误!)

A978-1-4842-1230-1_8_Fig3_HTML.jpg

图 8-3。

Something has gone terribly wrong!

你很可能会问,这里究竟出了什么问题。没必要惊慌。如果您打开文件/sys/class/class.calendar.inc.php并检查processForm()方法,您会注意到在成功完成后,它返回TRUE。然后TRUE被解释为事件 ID,被强制转换为整数 1,于是元旦被取出。(如果您一直关注事态的发展,这个事件的 ID 将等于 1。)

修改事件创建方法以返回新的事件 id

为了解决这个新的事件 ID 问题,您只需要在processForm()方法中做一个小的调整。在这个方法中,修改return命令,使用 PDO 的lastInsertId()方法输出最后插入的行的 ID:

public function processForm()

{

/*

* Exit if the action isn’t set properly

*/

if ( $_POST['action']!='event_edit' )

{

return "The method processForm was accessed incorrectly";

}

/*

* Escape data from the form

*/

$title = htmlentities($_POST['event_title'], ENT_QUOTES);

$desc = htmlentities($_POST['event_description'], ENT_QUOTES);

$start = htmlentities($_POST['event_start'], ENT_QUOTES);

$end = htmlentities($_POST['event_end'], ENT_QUOTES);

/*

* If no event ID passed, create a new event

*/

if ( empty($_POST['event_id']) )

{

$sql = "INSERT INTO events``

(event_title, event_desc, event_start,

``event_end)

VALUES

(:title, :description, :start, :end)";

}

/*

* Update the event if it’s being edited

*/

else

{

/*

* Cast the event ID as an integer for security

*/

$id = (int) $_POST['event_id'];

$sql = "UPDATE events``

SET

``event_title=:title,

``event_desc=:description,

``event_start=:start,

``event_end=:end

WHERE event_id=$id";

}

/*

* Execute the create or edit query after binding the data

*/

try

{

$stmt = $this->db->prepare($sql);

$stmt->bindParam(":title", $title, PDO::PARAM_STR);

$stmt->bindParam(":description", $desc, PDO::PARAM_STR);

$stmt->bindParam(":start", $start, PDO::PARAM_STR);

$stmt->bindParam(":end", $end, PDO::PARAM_STR);

$stmt->execute();

$stmt->closeCursor();

/*

* Returns the ID of the event

*/

return $this->db->lastInsertId();

}

catch ( Exception $e )

{

return $e->getMessage();

}

}

完成上述更改后,保存该文件并在浏览器中重新加载http://localhost/。接下来,使用以下信息创建一个新事件:

  • 事件标题:ID 测试
  • 活动开始时间:2016-01-06 12:00:00
  • 活动结束时间:2016-01-06 16:00:00
  • 事件描述:此事件在创建后应该可以立即查看。

现在保存活动,标题将出现在日历上。点击标题,事件将加载到一个模态窗口中(见图 8-4 )。

A978-1-4842-1230-1_8_Fig4_HTML.jpg

图 8-4。

An event loaded immediately after creation

在模式窗口中编辑事件

在目前的状态下,你的应用离允许用户从模态窗口编辑事件只有一步之遥。现有的用于加载事件创建表单的 click 事件处理程序也可以用于事件编辑,只需稍加修改。

首先,展开选择器以包含任何具有 admin 类的元素;您可以通过包含以下粗体代码来实现这一点:

// Displays the edit form as a modal window

$("body").on("click", ".admin-options form,.admin", function(event){

// Prevents the form from submitting

event.preventDefault();

// Loads the action for the processing file

var action = "edit_event";

// Loads the editing form and displays it

$.ajax({

type: "POST",

url: processFile,

data: "action="+action,

success: function(data){

// Hides the form

var form = $(data).hide(),

// Make sure the modal window exists

modal = fx.initModal();

// Call the boxin function to create

// the modal overlay and fade it in

fx.boxin(null, modal);

// Load the form into the window,

// fades in the content, and adds

// a class to the form

form

.appendTo(modal)

.addClass("edit-form")

.fadeIn("slow");

},

error: function(msg){

alert(msg);

}

});

});

确定表单操作

在为单个事件显示的编辑控件中,按钮名称描述了该按钮采取的操作(例如,edit_event表示“编辑该事件”按钮,delete_event表示“删除该事件”按钮)。这些按钮将被ajax.inc.php用作提交的动作。

因为事件创建按钮没有按钮名,所以需要保留一个默认值(edit_event)。

要访问被点击按钮的名称,可以使用名为targetevent对象的属性。此属性包含对触发事件的元素的引用。使用 jQuery 选择事件目标,并使用.attr()检索其名称。

现在,使用以下粗体代码修改事件处理程序:

// Displays the edit form as a modal window

$("body").on("click", ".admin-options form,.admin", function(event){

// Prevents the form from submitting

event.preventDefault();

// Sets the action for the form submission

var action = $(event.target).attr("name") || "edit_event";

// Loads the editing form and displays it

$.ajax({

type: "POST",

url: processFile,

data: "action="+action,

success: function(data){

// Hides the form

var form = $(data).hide(),

// Make sure the modal window exists

modal = fx.initModal();

// Call the boxin function to create

// the modal overlay and fade it in

fx.boxin(null, modal);

// Load the form into the window,

// fades in the content, and adds

// a class to the form

form

.appendTo(modal)

.addClass("edit-form")

.fadeIn("slow");

},

error: function(msg){

alert(msg);

}

});

});

存储事件 ID(如果存在)

接下来,需要提取事件 ID,假设它是可用的。要查找这个值,再次使用event.target属性,但是这次查找名为event_id的兄弟元素,然后将这个值存储在名为id的变量中。使用下面的粗体代码将它添加到事件处理程序中:

// Displays the edit form as a modal window

$("body").on("click", ".admin-options form,.admin", function(event){

// Prevents the form from submitting

event.preventDefault();

// Sets the action for the form submission

var action = $(event.target).attr("name") || "edit_event" ,

// Saves the value of the event_id input

id = $(event.target)

.siblings("input[name=event_id]")

.val();

// Loads the editing form and displays it

$.ajax({

type: "POST",

url: processFile,

data: "action="+action,

success: function(data){

// Hides the form

var form = $(data).hide(),

// Make sure the modal window exists

modal = fx.initModal();

// Call the boxin function to create

// the modal overlay and fade it in

fx.boxin(null, modal);

// Load the form into the window,

// fades in the content, and adds

// a class to the form

form

.appendTo(modal)

.addClass("edit-form")

.fadeIn("slow");

},

error: function(msg){

alert(msg);

}

});

});

将事件 ID 添加到查询字符串

有了存储在id变量中的 ID,您现在可以将该值附加到查询字符串中,以便提交给ajax.inc.php

先检查id是否未定义,然后创建一个event_id名值对。接下来,使用以下粗体代码将数据附加到查询字符串:

// Displays the edit form as a modal window

$("body").on("click", ".admin-options form,.admin", function(event){

// Prevents the form from submitting

event.preventDefault();

// Sets the action for the form submission

var action = $(event.target).attr("name") || "edit_event",

// Saves the value of the event_id input

id = $(event.target)

.siblings("input[name=event_id]")

.val();

// Creates an additional param for the ID if set

id = ( id!=undefined ) ? "``&

// Loads the editing form and displays it

$.ajax({

type: "POST",

url: processFile,

data: "action="+action+id,

success: function(data){

// Hides the form

var form = $(data).hide(),

// Make sure the modal window exists

modal = fx.initModal();

// Call the boxin function to create

// the modal overlay and fade it in

fx.boxin(null, modal);

// Load the form into the window,

// fades in the content, and adds

// a class to the form

form

.appendTo(modal)

.addClass("edit-form")

.fadeIn("slow");

},

error: function(msg){

alert(msg);

}

});

});

从模式窗口中移除事件数据

若要用编辑窗体替换模式窗口的内容,必须首先移除事件显示信息。

在成功处理程序中调用了fx.initModal()的地方,选择所有不属于关闭按钮的子按钮并删除它们。移除它们之后,调用.end()恢复到模态窗口的初始选择。(调用子元素后,jQuery 对象只引用您刚刚删除的子元素。)

您可以通过添加以下粗体代码来实现这一点:

// Displays the edit form as a modal window

$("body").on("click", ".admin-options form,.admin", function(event){

// Prevents the form from submitting

event.preventDefault();

// Sets the action for the form submission

var action = $(event.target).attr("name") || "edit_event",

// Saves the value of the event_id input

id = $(event.target)

.siblings("input[name=event_id]")

.val();

// Creates an additional param for the ID if set

id = ( id!=undefined ) ? "&event_id="+id : "";

// Loads the editing form and displays it

$.ajax({

type: "POST",

url: processFile,

data: "action="+action+id,

success: function(data){

// Hides the form

var form = $(data).hide(),

// Make sure the modal window exists

modal = fx.initModal()

.children(":not(.modal-close-btn)")

.remove()

.end();

// Call the boxin function to create

// the modal overlay and fade it in

fx.boxin(null, modal);

// Load the form into the window,

// fades in the content, and adds

// a class to the form

form

.appendTo(modal)

.addClass("edit-form")

.fadeIn("slow");

},

error: function(msg){

alert(msg);

}

});

});

保存该文件并在浏览器中重新加载http://localhost/后,点击New Year’s Day事件标题,调出事件描述。在模式窗口中,单击“编辑此事件”按钮;这导致事件描述消失,编辑表单将淡入,条目的数据加载到表单中进行编辑(参见图 8-5 )。

A978-1-4842-1230-1_8_Fig5_HTML.jpg

图 8-5。

Editing an event in a modal window

确保日历中只添加新事件

如果您对元旦活动进行编辑并保存,一个额外的活动标题将被添加到日历中(参见图 8-6 )。

A978-1-4842-1230-1_8_Fig6_HTML.jpg

图 8-6。

After you edit an event, its title is duplicated

如果刷新页面,将会消除重复。

为了防止这种重复问题,您需要向表单提交点击处理程序添加一个额外的调整。因为正在编辑的事件将把它们的 ID 加载到名为event_id的编辑表单的隐藏输入中,所以您可以检查输入值的长度。如果长度不为零,就不要调用fx.addevent()

插入以下粗体代码进行检查:

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function (event){

// Prevents the default form action from executing

event.preventDefault();

// Serializes the form data for use with $.ajax()

var formData = $(this).parents("form").serialize();

// Sends the data to the processing file

$.ajax({

type: "POST",

url: processFile,

data: formData,

success: function(data) {

// Fades out the modal window

fx.boxout();

// If this is a new event, adds it to

// the calendar

if ( $("[name=event_id]").val().length==0 )

{

fx.addevent(data, formData);

}

},

error: function(msg) {

alert(msg);

}

});

});

事件编辑功能就要完成了。但是,您可能已经注意到了一个遗留问题。如果事件标题发生了变化,那么在页面没有刷新的情况下,新标题不会出现。(如果您还没有尝试过,现在就尝试一下。)幸运的是,您可以很容易地解决这个问题。您只需要将主日历页面上显示的标题与提交时表单中包含的(可能)不同的标题进行比较。

插入以下粗体代码来实现此逻辑:

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function (event){

// Prevents the default form action from executing

event.preventDefault();

// If editing an existing event, need to pay attention to title.

if ( $(this).attr("name")=="event_submit"``&&

{

// Need to check if the event title has been changed.

// Here’s the title that’s on the main calendar page.

var oldTitle = $(".active")[0].innerHTML;

// Here we fish out the (possibly) different title from the form data.

var formArray = $(this).parents("form").serializeArray();

var titleArray = $.grep(formArray, function(elem) {

return elem.name === 'event_title';

});

var newTitle = titleArray.length > 0 ? titleArray[0].value : "";

if (newTitle !== oldTitle)

{

// The event title has been changed, so update the page.

$(".active")[0].innerHTML = newTitle;

}

}

// Serializes the form data for use with $.ajax()

var formData = $(this).parents("form").serialize();

// Sends the data to the processing file

$.ajax({

type: "POST",

url: processFile,

data: formData,

success: function(data) {

// Fades out the modal window

fx.boxout();

// If this is a new event, adds it to

// the calendar

if ( $("[name=event_id]").val().length==0 )

{

fx.addevent(data, formData);

}

},

error: function(msg) {

alert(msg);

}

});

});

有了这些更改,您的用户现在可以编辑事件,而不会在页面上看到潜在的令人困惑的重复标题或过时标题。

在模式窗口中确认删除

为了完善您的应用,您还将允许用户在不刷新页面的情况下删除条目。完成这项工作所需的大部分脚本已经准备好了,所以添加这项功能主要需要对现有代码进行调整。

显示确认对话框

要在点击“删除该事件”按钮时显示事件删除的确认对话框,您需要向ajax.inc.php中的查找数组添加一个额外的元素:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define(ACTIONS, array((

'event_view' => array(

'object' => 'Calendar',

'method' => 'displayEvent'

),

'edit_event' => array(

'object' => 'Calendar',

'method' => 'displayForm'

),

'event_edit' => array(

'object' => 'Calendar',

'method' => 'processForm'

),

'delete_event' => array(

'object' => 'Calendar',

'method' => 'confirmDelete'

)

)

);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

/*

* Check for an ID and sanitize it if found

*/

if ( isset($_POST['event_id']) )

{

$id = (int) $_POST['event_id'];

}

else { $id = NULL; }

echo $obj->$method($id);

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

此时点击模式窗口中的“删除该事件”按钮,将出现确认对话框(见图 8-7 )。

A978-1-4842-1230-1_8_Fig7_HTML.jpg

图 8-7。

The confirmation dialog to delete an event displayed in a modal window

为删除配置表单提交事件处理程序

确认事件删除需要对init.js稍加修改。为了正确执行,需要存储 Submit 按钮的值并将其传递给处理文件。这是因为表单可以用Yes, Delete ItNope! Just Kidding!作为值提交;该脚本检查哪个按钮被点击,以确定采取什么行动。

要存储按钮的值,使用this关键字作为 jQuery 选择器,然后将从.val()返回的字符串存储为一个名为submitVal的变量。接下来,检查按钮的name属性是否为confirm_delete。如果是这样,在提交之前将动作confirm_delete和按钮的值附加到查询字符串中。

插入以粗体显示的以下代码来完成此操作:

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function (event){

// Prevents the default form action from executing

event.preventDefault();

// If editing an existing event, need to pay attention to title.

if ( $(this).attr("name")=="event_submit" && $(".active").length > 0 )

{

// Need to check if the event title has been changed.

// Here’s the title that’s on the main calendar page.

var oldTitle = $(".active")[0].innerHTML;

// Here we fish out the (possibly) different title from the form data.

var formArray = $(this).parents("form").serializeArray();

var titleArray = $.grep(formArray, function(elem) {

return elem.name === 'event_title';

});

var newTitle = titleArray.length > 0 ? titleArray[0].value : "";

if (newTitle !== oldTitle)

{

// The event title has been changed, so update the page.

$(".active")[0].innerHTML = newTitle;

}

}

// Serializes the form data for use with $.ajax()

var formData = $(this).parents("form").serialize() ,

// Stores the value of the submit button

submitVal = $(this).val();

// If this is the deletion form, appends an action

if ( $(this).attr("name")=="confirm_delete" )

{

// Adds necessary info to the query string

formData += "``&

+ "``&

}

// Sends the data to the processing file

$.ajax({

type: "POST",

url: processFile,

data: formData,

success: function(data) {

// Fades out the modal window

fx.boxout();

// If this is a new event, adds it to

// the calendar

if ( $("[name=event_id]").val().length==0 )

{

fx.addevent(data, formData);

}

},

error: function(msg) {

alert(msg);

}

});

});

修改处理文件以确认删除

最后,您需要向ajax.inc.php中的查找数组添加一个额外的元素,以使删除按钮工作:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define(ACTIONS, array((

'event_view' => array(

'object' => 'Calendar',

'method' => 'displayEvent'

),

'edit_event' => array(

'object' => 'Calendar',

'method' => 'displayForm'

),

'event_edit' => array(

'object' => 'Calendar',

'method' => 'processForm'

),

'delete_event' => array(

'object' => 'Calendar',

'method' => 'confirmDelete'

),

'confirm_delete' => array(

'object' => 'Calendar',

'method' => 'confirmDelete'

)

)

);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

/*

* Check for an ID and sanitize it if found

*/

if ( isset($_POST['event_id']) )

{

$id = (int) $_POST['event_id'];

}

else { $id = NULL; }

echo $obj->$method($id);

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

您可以通过从日历中删除 ID 测试事件来测试前面的代码。在模态窗口淡出后,事件标题仍然存在并可点击;但是,如果您试图查看事件的详细信息,则它的信息不可用,并且没有意义(参见图 8-8 )。

A978-1-4842-1230-1_8_Fig8_HTML.jpg

图 8-8。

Because the event no longer exists, the event view makes no sense

删除后从日历中移除事件

您希望避免由于用户删除日历上不存在的事件而造成的混乱,因此您需要添加功能,以便在发生这种情况时从日历中删除事件。

为此,向名为removeeventfx对象文字添加一个新函数。当事件出现在模态窗口时,这个函数将使用应用于事件的active类来淡出事件并将其从 DOM 中移除。您可以使用以下粗体代码将该功能添加到fx:

fx = {

"initModal" : function() {...},

"boxin" : function(data, modal) {...},

"boxout" : function(event) {...},

"addevent" : function(data, formData){...},

// Removes an event from the markup after deletion

"removeevent" : function()

{

// Removes any event with the class "active"

$(".active")

.fadeOut("slow", function(){

$(this).remove();

});

},

"deserialize" : function(str){...},

"urldecode" : function(str) {...}

};

修改表单提交处理程序以删除已删除的事件

要在事件被删除后移除它们,需要向表单提交事件处理程序添加一个名为remove的新变量。这将存储一个布尔值,告诉脚本是否删除一个事件。默认情况下,该值将被设置为false,这意味着该事件不应被删除。

删除事件的唯一条件是在确认对话框中点击“是,删除”按钮。在 Submit 按钮中添加对该文本的检查,如果匹配,则将remove设置为true

在成功处理程序中,设置一个条件来检查remove是否为true,如果是,则触发fx.removeevent()

最后,为了防止空元素被添加到日历中,修改触发fx.addevent()的条件,以确保在执行之前removefalse

您可以通过添加以粗体显示的代码来进行这些更改:

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function (event){

// Prevents the default form action from executing

event.preventDefault();

// If editing an existing event, need to pay attention to title.

if ( $(this).attr("name")=="event_submit" && $(".active").length > 0 )

{

// Need to check if the event title has been changed.

// Here’s the title that’s on the main calendar page.

var oldTitle = $(".active")[0].innerHTML;

// Here we fish out the (possibly) different title from the form data.

var formArray = $(this).parents("form").serializeArray();

var titleArray = $.grep(formArray, function(elem) {

return elem.name === 'event_title';

});

var newTitle = titleArray.length > 0 ? titleArray[0].value : "";

if (newTitle !== oldTitle)

{

// The event title has been changed, so update the page.

$(".active")[0].innerHTML = newTitle;

}

}

// Serializes the form data for use with $.ajax()

var formData = $(this).parents("form").serialize(),

// Stores the value of the submit button

submitVal = $(this).val(),

// Determines if the event should be removed

remove = false;

// If this is the deletion form, appends an action

if ( $(this).attr("name")=="confirm_delete" )

{

// Adds necessary info to the query string

formData += "&action=confirm_delete"

+ "&confirm_delete="+submitVal;

// If the event is really being deleted, sets

// a flag to remove it from the markup

if ( submitVal=="Yes, Delete It" )

{

remove = true;

}

}

// Sends the data to the processing file

$.ajax({

type: "POST",

url: processFile,

data: formData,

success: function(data) {

// If this is a deleted event, removes

// it from the markup

if ( remove===true )

{

fx.removeevent();

}

// Fades out the modal window

fx.boxout();

// If this is a new event, adds it to

// the calendar

if ( $("[name=event_id]").val().length== 0

&& remove===false )

{

fx.addevent(data, formData);

}

},

error: function(msg) {

alert(msg);

}

});

});

保存这些更改,重新加载http://localhost/,并调出测试事件描述。删除该事件;在你点击“是,删除”按钮后,模态框和事件标题将淡出,有效地从日历中消除事件,并为你的用户消除任何潜在的混淆(见图 8-9 )。

A978-1-4842-1230-1_8_Fig9_HTML.jpg

图 8-9。

After deleting Test Event, the event title is removed from the calendar

摘要

在本章中,您实现了允许用户快速创建、编辑和删除事件而无需刷新页面的控件。这使得应用感觉更加精简和用户友好。

在下一章中,您将学习如何使用正则表达式来确认编辑表单中输入的数据是有效的,确保您的应用不允许可能破坏它的数据输入到数据库中。

九、使用正则表达式执行表单验证

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​9) contains supplementary material, which is available to authorized users.

作为开发人员,您有责任确保您的用户数据对您的应用有用,因此您需要确认关键信息在存储到数据库之前已经过验证。

在日历应用的情况下,日期格式是至关重要的:如果格式不正确,应用将在几个地方失败。为了验证数据库中只允许有效的日期,您将使用正则表达式(regexes),这是强大的模式匹配工具,与严格的字符串比较搜索相比,它允许开发人员对数据进行更多的控制。

在开始向应用添加验证之前,您需要熟悉正则表达式的使用。在本章的第一节,你将学习如何使用正则表达式的基本语法。然后,您将使用正则表达式进行服务器端和客户端验证。

熟悉正则表达式

正则表达式通常被认为是令人生畏的、困难的工具。事实上,正则表达式在程序员中的名声如此之差,以至于关于它们的讨论经常夹杂着这样一句话:

Some people think, "I know, I can use regular expressions." Now they have two problems. —— Jamie Zavinsky

这种观点并非完全没有根据,因为正则表达式具有复杂的语法,几乎没有出错的余地。然而,在克服了最初的学习曲线之后,正则表达式是一个非常强大的工具,在日常编程中有无数的应用。

理解基本正则表达式语法

在本书中,您将学习 Perl 兼容的正则表达式(PCRE)语法。这种语法与 PHP 和 JavaScript 以及大多数其他编程语言兼容。

Note

您可以在 http://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions 了解更多关于 PCRE 的信息。

设置测试文件

要了解如何使用正则表达式,您需要一个用于测试的文件。在public文件夹中,创建一个名为regex.php的新文件,并将以下代码放入其中:

<!DOCTYPE html

PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd

<html xmlns="http://www.w3.org/1999/xhtml

<head>

<meta http-equiv="Content-Type"

content="text/html;charset=utf-8" />

<title>Regular Expression Demo</title>

<style type="text/css">

em {

background-color: #FF0;

border-top: 1px solid #000;

border-bottom: 1px solid #000;

}

</style>

</head>

<body>

<?php

/*

* Store the sample set of text to use for the examples of regex

*/

$string = <<<TEST_DATA

<h2>Regular Expression Testing</h2>

<p>

In this document, there is a lot of text that can be matched

using regex. The benefit of using a regular expression is much

more flexible, albeit complex, syntax for text

pattern matching.

</p>

<p>

After you get the hang of regular expressions, also called

regexes, they will become a powerful tool for pattern matching.

</p>

<hr />

TEST_DATA;

/*

* Start by simply outputting the data

*/

echo $string;

?>

</body>

</html>

保存该文件,然后在浏览器中加载http://localhost/regex.php以查看示例脚本(参见图 9-1 )。

A978-1-4842-1230-1_9_Fig1_HTML.jpg

图 9-1。

The sample file for testing regular expressions

用正则表达式替换文本

为了测试正则表达式,您将使用<em>标签包装匹配的模式,这些标签在测试文档中被设计为具有顶部和底部边框,以及黄色背景。

用正则表达式实现这一点类似于在 PHP 中使用str_replace()preg_replace()函数。传递一个要匹配的模式,后跟一个用于替换匹配模式的字符串(或模式)。最后,传递要在其中执行搜索的字符串:

preg_replace($pattern, $replacement, $string);

Note

preg_replace()中的 p 表示使用了 PCRE。PHP 也有ereg_replace(),使用略有不同的 POSIX 正则表达式语法;然而,从 PHP 5.3.0 开始,ereg函数族就被弃用了。

在基本层面上,str_replace()preg_replace()之间唯一的区别是传递给模式的preg_replace()的元素必须使用分隔符,这让函数知道正则表达式的哪一部分是模式,哪一部分由修饰符或影响模式匹配的标志组成。在本节的稍后部分,您将了解更多关于修改器的内容。

preg_replace()中 regex 模式的分隔符可以是位于模式开头和结尾的任何非字母数字、非反斜杠和非空白字符。最常见的是使用正斜杠(/)或散列符号(#)。例如,如果您想在一个字符串中搜索字母 cat,那么模式应该是/cat/(或者#cat#%cat%@cat@等等)。

选择正则表达式与常规字符串替换

为了探究str_replace()preg_replace()之间的区别,尝试使用这两个函数用<em>标签包装任何出现的单词 regular。对regex.php进行如下修改:

<!DOCTYPE html

PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd

<html xmlns="http://www.w3.org/1999/xhtml

<head>

<meta http-equiv="Content-Type"

content="text/html;charset=utf-8" />

<title>Regular Expression Demo</title>

<style type="text/css">

em {

background-color: #FF0;

border-top: 1px solid #000;

border-bottom: 1px solid #000;

}

</style>

</head>

<body>

<?php

/*

* Store the sample set of text to use for the examples of regex

*/

$string = <<<TEST_DATA

<h2>Regular Expression Testing</h2>

<p>

In this document, there is a lot of text that can be matched

using regex. The benefit of using a regular expression is much

more flexible, albeit complex, syntax for text

pattern matching.

</p>

<p>

After you get the hang of regular expressions, also called

regexes, they will become a powerful tool for pattern matching.

</p>

<hr />

TEST_DATA;

/*

* Use str_replace() to highlight any occurrence of the word

* "regular"

*/

echo str_replace("regular", "<em>regular</em>", $string);

/*

* Use preg_replace() to highlight any occurrence of the word

* "regular"

*/

echo preg_replace("/regular/", "<em>regular</em>", $string);

?>

</body>

</html>

在您的浏览器中执行这个脚本输出测试信息两次,结果相同(见图 9-2 )。

A978-1-4842-1230-1_9_Fig2_HTML.jpg

图 9-2。

The word regular highlighted with both regexes and regular string replacement

深入了解模式修改器的基础知识

你可能注意到了,题目中的正则这个词并没有突出显示。这是因为前面的例子是区分大小写的。

要用简单的字符串替换来解决这个问题,您可以选择使用str_ireplace()函数,除了不区分大小写之外,它几乎与str_replace()相同。

对于正则表达式,您仍然使用preg_replace(),但是您需要一个修饰符来表示不区分大小写。修饰符是模式定界符后面的一个字母,为正则表达式提供关于它应该如何处理模式的附加信息。对于不区分大小写,应该应用修饰符i

通过进行粗体显示的修改,修改regex.php以使用不区分大小写的替换功能:

<!DOCTYPE html

PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd

<html xmlns="http://www.w3.org/1999/xhtml

<head>

<meta http-equiv="Content-Type"

content="text/html;charset=utf-8" />

<title>Regular Expression Demo</title>

<style type="text/css">

em {

background-color: #FF0;

border-top: 1px solid #000;

border-bottom: 1px solid #000;

}

</style>

</head>

<body>

<?php

/*

* Store the sample set of text to use for the examples of regex

*/

$string = <<<TEST_DATA

<h2>Regular Expression Testing</h2>

<p>

In this document, there is a lot of text that can be matched

using regex. The benefit of using a regular expression is much

more flexible, albeit complex, syntax for text

pattern matching.

</p>

<p>

After you get the hang of regular expressions, also called

regexes, they will become a powerful tool for pattern matching.

</p>

<hr />

TEST_DATA;

/*

* Use str_ireplace() to highlight any occurrence of the word

* "regular"

*/

echo str_ireplace("regular", "<em>regular</em>", $string);

/*

* Use preg_replace() to highlight any occurrence of the word

* "regular"

*/

echo preg_replace("/regular/i", "<em>regular</em>", $string);

?>

</body>

</html>

现在,在浏览器中加载文件将突出显示所有出现的单词 regular,无论大小写如何(参见图 9-3 )。

A978-1-4842-1230-1_9_Fig3_HTML.jpg

图 9-3。

A case-insensitive search of the sample data

正如你所看到的,这种方法有一个缺点:标题中大写的 regular 在被替换时被改为小写。在下一节中,您将学习如何通过在正则表达式中使用组来避免这个问题。

用反向引用变得有趣

当您应用正则表达式最有用的特性之一:分组和反向引用时,它的威力就开始显现了。组是用括号括起来的模式的任何一部分。一个组可以在替换字符串中(或者在模式的后面)使用反向引用,即一个命名组的编号引用。

这听起来很混乱,但实际上很简单。正则表达式中从左到右的每组括号都存储有一个数字反向引用,可以使用反斜杠和反向引用的编号(\1)或使用美元符号和反向引用的编号($1)来访问。

这样做的好处是,它让正则表达式能够在替换中使用匹配的值,而不是像str_replace()及其类似物中那样使用预先确定的值。

为了将前面例子中的替换内容保留在适当的情况下,您需要使用两次str_replace();然而,只需一次函数调用,就可以通过使用preg_replace()中的反向引用达到同样的效果。

regex.php做如下修改,看看正则表达式中反向引用的威力:

<!DOCTYPE html

PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd

<html xmlns="http://www.w3.org/1999/xhtml

<head>

<meta http-equiv="Content-Type"

content="text/html;charset=utf-8" />

<title>Regular Expression Demo</title>

<style type="text/css">

em {

background-color: #FF0;

border-top: 1px solid #000;

border-bottom: 1px solid #000;

}

</style>

</head>

<body>

<?php

/*

* Store the sample set of text to use for the examples of regex

*/

$string = <<<TEST_DATA

<h2>Regular Expression Testing</h2>

<p>

In this document, there is a lot of text that can be matched

using regex. The benefit of using a regular expression is much

more flexible, albeit complex, syntax for text

pattern matching.

</p>

<p>

After you get the hang of regular expressions, also called

regexes, they will become a powerful tool for pattern matching.

</p>

<hr />

TEST_DATA;

/*

* Use str_replace() to highlight any occurrence of the word

* "regular"

*/

$check1 = str_replace("regular", "<em>regular</em>", $string);

/*

* Use str_replace() again to highlight any capitalized occurrence

* of the word "Regular"

*/

echo str_replace("Regular", "<em>Regular</em>", $check1);

/*

* Use preg_replace() to highlight any occurrence of the word

* "regular", case-insensitive

*/

echo preg_replace("/(regular)/i", "<em>$1</em>", $string);

?>

</body>

</html>

如前面的代码所示,使用str_replace()进行任何复杂的字符串匹配已经变得很麻烦。然而,在保存前面的更改并重新加载浏览器后,您可以使用正则表达式和标准字符串替换来获得想要的结果(参见图 9-4 )。

A978-1-4842-1230-1_9_Fig4_HTML.jpg

图 9-4。

A more complex replacement Note

本节中的其余示例将只使用正则表达式。

匹配字符类

在某些情况下,希望匹配的不仅仅是一个单词。例如,有时您希望验证是否只使用了特定范围的字符(即,确保只为电话号码提供了数字,或者用户名字段中没有使用特殊字符)。

正则表达式允许您指定一个字符类,它是一组用方括号括起来的字符。例如,要匹配字母 a 和字母 c 之间的任何字符,可以在模式中使用[a-c]

可以修改regex.php高亮显示 A-C 中的任意字符,另外可以将模式移入变量,在样本数据底部输出;这有助于您了解加载脚本时使用的是什么模式。添加以粗体显示的代码来实现这一点:

<!DOCTYPE html

PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd

<html xmlns="http://www.w3.org/1999/xhtml

<head>

<meta http-equiv="Content-Type"

content="text/html;charset=utf-8" />

<title>Regular Expression Demo</title>

<style type="text/css">

em {

background-color: #FF0;

border-top: 1px solid #000;

border-bottom: 1px solid #000;

}

</style>

</head>

<body>

<?php

/*

* Store the sample set of text to use for the examples of regex

*/

$string = <<<TEST_DATA

<h2>Regular Expression Testing</h2>

<p>

In this document, there is a lot of text that can be matched

using regex. The benefit of using a regular expression is much

more flexible, albeit complex, syntax for text

pattern matching.

</p>

<p>

After you get the hang of regular expressions, also called

regexes, they will become a powerful tool for pattern matching.

</p>

<hr />

TEST_DATA;

/*

* Use regex to highlight any occurence of the letters a-c

*/

$pattern = "/([a-c])/i";

echo preg_replace($pattern, "<em>$1</em>", $string);

/*

* Output the pattern you just used

*/

echo "\n<p>Pattern used: <strong>$pattern</strong></p>";

?>

</body>

</html>

重新加载页面后,你会看到字符高亮显示(见图 9-5 )。您可以使用[abc][bac]或任何其他字符组合获得相同的结果,因为该类将匹配该类中的任何一个字符。此外,因为您使用了不区分大小写的修饰符(i),所以不需要包含字母的大写和小写版本。如果没有修饰符,您将需要使用[A-Ca-c]来匹配三个字母的任意一种情况。

A978-1-4842-1230-1_9_Fig5_HTML.jpg

图 9-5。

Any character from A-C is highlighted

匹配除。。。

要匹配除类中的字符之外的任何字符,请在字符类前面加上一个脱字符号(^)。为了突出显示除 A-C 以外的任何字符,使用图案/([^a-c])/i(参见图 9-6 )。

A978-1-4842-1230-1_9_Fig6_HTML.jpg

图 9-6。

Highlighting all characters, except letters A-C Note

值得一提的是,前面的模式将字符类括在括号内。字符类不存储反向引用,所以以后仍必须使用括号来引用匹配的文本。

使用字符类速记

某些字符类有一个速记字符。例如,每个单词、数字或空格字符都有一个速记类:

  • Word 字符类速记(\w):匹配类似[A-Za-z0-9_]的模式
  • 数字字符类速记(\d):匹配类似[0-9]的模式
  • 空白字符类速记(\s):匹配类似[ \t\r\n]的模式

使用这三个简写类可以提高正则表达式的可读性,这在处理更复杂的模式时非常方便。

您可以通过将速记字符大写来排除特定类型的字符:

  • 非单词字符类速记(\W):匹配类似[^A-Za-z0-9_]的模式
  • 非数字字符类速记(\D):匹配类似[⁰-9]的模式
  • 非空白字符类速记(\S):匹配类似[^ \t\r\n]的模式

Note

\t\r\n是代表制表符和换行符的特殊字符;空格由常规空格字符( )表示。

查找单词边界

另一个需要注意的特殊符号是单词边界符号(\b)。通过将它放在一个模式之前和/或之后,可以确保该模式不包含在另一个单词中。例如,如果您想匹配单词 stat,而不是恒温器、统计数据或狂喜,您可以使用这个模式:/\bstat\b/

使用重复运算符

当使用字符类时,只有一个字符匹配,除非模式指定了不同的字符数。正则表达式为您提供了几种方法来指定要匹配的字符数:

  • 星号(*)匹配一个字符的零个或多个出现。
  • 加号运算符(+)匹配一个或多个出现的字符。
  • 特殊的重复运算符({min,max})允许您指定字符匹配的范围。

当使用可能包含也可能不包含某个模式片段的字符串时,匹配零个或多个字符非常有用。例如,如果您想要匹配 John 或 John Doe 的所有出现,您可以使用这个模式来匹配这两个实例:/John( Doe)*/

匹配一个或多个字符有助于验证至少输入了一个字符。例如,如果您希望验证用户在表单输入中至少输入了一个字符,并且该字符是有效的单词字符,那么您可以使用这个模式来验证输入:/\w+/

最后,匹配特定范围的字符在匹配数字范围时特别有用。例如,您可以使用这个模式来确保一个值在099之间:/\b\d{1,2}\b/

在您的示例文件中,使用这个 regex 模式查找恰好由四个字母组成的任何单词:/(\b\w{4}\b)/。您可以在图 9-7 中看到结果。

A978-1-4842-1230-1_9_Fig7_HTML.jpg

图 9-7。

Matching only words that consist of exactly four letters

检测字符串的开头或结尾

此外,您可以强制模式从字符串的开头或结尾(或两者)开始匹配。如果模式以一个插入符号(^)开始,正则表达式只有在模式以一个匹配字符开始时才匹配。如果以美元符号($)结尾,则只有在字符串以前面的匹配字符结尾时,正则表达式才会匹配。

您可以组合这些不同的符号,以确保整个字符串匹配一个模式。这在验证输入时很有用,因为您可以验证用户只提交了有效的信息。例如,您可以使用这个 regex 模式来验证用户名是否只包含字母 A-Z、数字 0-9 和下划线字符:/^\w+$/

使用替代方案

在某些情况下,最好使用一种模式或另一种模式。这被称为交替,它是使用管道字符(|)来完成的。这种方法允许您为匹配定义两种或多种可能性。例如,您可以使用这个模式来匹配regex.php : /\b(\w{3}|\w{6,7})\b/中的三个、六个或七个字母的单词。图 9-8 显示了结果。

A978-1-4842-1230-1_9_Fig8_HTML.jpg

图 9-8。

Using alternation to match only three-, six-, and seven-letter words

使用可选项目

在某些情况下,有必要允许某些项目是可选的。例如,要匹配单词 like 表达式的单复数形式,您需要使 s 可选。

为此,请在可选项目后放置一个问号(?)。如果模式的可选部分长于一个字符,则需要在一个组中捕获它(您将在下一节中使用这种技术)。

现在,使用这个模式来突出显示所有出现的单词 expression 或 expressions: /(expressions?)/i。图 9-9 显示了结果。

A978-1-4842-1230-1_9_Fig9_HTML.jpg

图 9-9。

Matching a pattern with an optional s at the end

把这一切放在一起

现在您已经对正则表达式有了大致的了解,是时候使用您的新知识来编写一个正则表达式模式,它将匹配任何出现的短语正则表达式或正则表达式,包括复数形式。

首先,查找短语 regex: /(regex)/i(参见图 9-10 )。

A978-1-4842-1230-1_9_Fig10_HTML.jpg

图 9-10。

Matching the word regex

接下来,通过在末尾插入一个可选的 es 来增加短语成为复数的能力:/(regex (es)? )/i(见图 9-11 )。

A978-1-4842-1230-1_9_Fig11_HTML.jpg

图 9-11。

Adding the optional match for the plural form of regex

接下来,添加到模式中,使其也匹配单词 regular,并在单词 regular 后面加一个空格,并使匹配可选:/(reg (ular\s)? ex(es)?)/i(参见图 9-12 )。

A978-1-4842-1230-1_9_Fig12_HTML.jpg

图 9-12。

Adding an optional check for the word regular

现在扩展模式以匹配单词 expression 作为 es: /(reg(ular\s)?ex( pression| es)?)/i(见图 9-13 )。

A978-1-4842-1230-1_9_Fig13_HTML.jpg

图 9-13。

Adding alternation to match expression

最后,在匹配表达式的末尾加一个可选的 s:/(reg(ular\s)?ex(pression``s?``|es)?)/i(见图 9-14 )。

A978-1-4842-1230-1_9_Fig14_HTML.jpg

图 9-14。

The completed regular expression Tip

本章中的例子介绍了正则表达式最常见的特性,但并没有涵盖正则表达式必须提供的所有特性。Jan Goyvaerts 在 www.regular-expressions.info/ 为学习正则表达式的所有细节以及测试它们的一些工具提供了一个极好的资源。

添加服务器端日期验证

现在您已经对正则表达式有了基本的了解,可以开始验证用户输入了。对于这个应用,您需要确保日期格式是正确的,这样应用就不会因为试图解析它无法理解的日期而崩溃。

您将从添加服务器端验证开始。这更像是一个后备方案,因为稍后您将使用 jQuery 添加验证。但是,您不应该仅仅依赖 JavaScript 来验证用户输入,因为用户可以很容易地关闭 JavaScript 支持,从而完全禁用您的 JavaScript 验证工作。

定义正则表达式模式来验证日期

实现日期验证的第一步是定义一个正则表达式模式来匹配所需的格式。日历 app 使用的格式是YYYY-MM-DD HH:MM:SS

设置测试数据

您需要用一个有效的日期格式和一些无效的格式来修改regex.php,这样您就可以测试您的模式了。首先将零个或多个数字字符与您的正则表达式模式进行匹配。为此,请进行以下以粗体显示的更改:

<!DOCTYPE html

PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd

<html xmlns="http://www.w3.org/1999/xhtml

<head>

<meta http-equiv="Content-Type"

content="text/html;charset=utf-8" />

<title>Regular Expression Demo</title>

<style type="text/css">

em {

background-color: #FF0;

border-top: 1px solid #000;

border-bottom: 1px solid #000;

}

</style>

</head>

<body>

<?php

/*

* Set up several test date strings to ensure validation is working

*/

$date[] = '2016-01-14 12:00:00';

$date[] = 'Saturday, May 14th at 7pm';

$date[] = '02/03/10 10:00pm';

$date[] = '2016-01-14 102:00:00';

/*

* Date validation pattern

*/

$pattern = "/(\d*)/";

foreach ( $date as $d )

{

echo "<p>", preg_replace($pattern, "<em>$1</em>", $d), "</p>";

}

/*

* Output the pattern you just used

*/

echo "\n<p>Pattern used: <strong>$pattern</strong></p>";

?>

</body>

</html>

保存上述代码后,在浏览器中重新加载http://localhost/regex.php以查看所有高亮显示的数字字符(参见图 9-15 )。

A978-1-4842-1230-1_9_Fig15_HTML.jpg

图 9-15。

Matching any numeric character

匹配日期格式

要匹配日期格式,首先匹配字符串开头的四位数字以验证年份:/^(\d{4})/(参见图 9-16 )。

A978-1-4842-1230-1_9_Fig16_HTML.jpg

图 9-16。

Validating the year section of the date string

接下来,您需要通过匹配连字符和另外两位数字来验证月份:/^(\d{4} (-\d{2}) )/(见图 9-17 )。

A978-1-4842-1230-1_9_Fig17_HTML.jpg

图 9-17。

Expanding the validate month section of the date string

请注意,月份和日期部分是相同的:一个连字符后跟两位数字。这意味着您可以简单地重复月份匹配模式,在组:/^(\d{4}(-\d{2}) {2} )/(参见图 9-18 )之后使用重复运算符来验证日期。

A978-1-4842-1230-1_9_Fig18_HTML.jpg

图 9-18。

Adding the day part of the date string to the pattern

现在匹配单个空格和小时段:/^(\d{4}(-\d{2}){2} (\d{2}) )/(见图 9-19 )。

A978-1-4842-1230-1_9_Fig19_HTML.jpg

图 9-19。

Validating the hour section of the date string Note

确保包含空格字符。不应该使用速记类(\s),因为在这种情况下新的行和制表符不应该匹配。

为了验证分钟,您匹配一个冒号和两个数字:/^(\d{4}(-\d{2}){2} (\d{2}) (:\d{2}) )/(参见图 9-20 )。

A978-1-4842-1230-1_9_Fig20_HTML.jpg

图 9-20。

Validating the minutes section of the date string

最后,重复分钟的模式以匹配秒,然后使用美元符号修饰符来匹配字符串的结尾:/^(\d{4}(-\d{2}){2} (\d{2})(:\d{2}) {2} ) $ /(见图 9-21 )。

A978-1-4842-1230-1_9_Fig21_HTML.jpg

图 9-21。

Validating the seconds section of the date string and completing the pattern

有了这个正则表达式模式,现在可以验证应用中的日期输入了。

向 Calendar 类添加验证方法

为了验证日期字符串,您将向名为_validDate()Calendar类添加一个新的私有方法。该方法将接受要验证的日期字符串,然后使用preg_match()将其与验证模式进行比较,后者返回在给定字符串中找到的匹配数。因为这个特定的模式只有在整个字符串都符合该模式时才会匹配,所以有效的日期将返回1,而无效的日期将返回0。如果日期有效,该方法将返回TRUE;否则将返回FALSE

通过将以下粗体代码插入到class.calendar.inc.php中,将该方法添加到Calendar类中:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayEvent($id) {...}

public function displayForm() {...}

public function processForm() {...}

public function confirmDelete($id) {...}

/**

* Validates a date string

*

* @param string $date the date string to validate

* @return bool TRUE on success, FALSE on failure

*/

private function _validDate($date)

{

/*

* Define a regex pattern to check the date format

*/

$pattern = '/^(\d{4}(-\d{2}){2} (\d{2})(:\d{2}){2})$/';

/*

* If a match is found, return TRUE. FALSE otherwise.

*/

return preg_match($pattern, $date)==1 ? TRUE : FALSE;

}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

private function _adminGeneralOptions() {...}

private function _adminEntryOptions($id) {...}

}

? >

如果日期未通过验证,则返回错误

下一步是修改processForm()方法,使它在新条目的开始和结束时间调用_validDate()方法。如果验证失败,只需返回一条错误消息。

将以下粗体代码添加到processForm()以实现验证:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayEvent($id) {...}

public function displayForm() {...}

/**

* Validates the form and saves/edits the event

*

* @return mixed TRUE on success, an error message on failure

*/

public function processForm()

{

/*

* Exit if the action isn’t set properly

*/

if ( $_POST['action']!='event_edit' )

{

return "The method processForm was accessed incorrectly";

}

/*

* Escape data from the form

*/

$title = htmlentities($_POST['event_title'], ENT_QUOTES);

$desc = htmlentities($_POST['event_description'], ENT_QUOTES);

$start = htmlentities($_POST['event_start'], ENT_QUOTES);

$end = htmlentities($_POST['event_end'], ENT_QUOTES);

/*

* If the start or end dates aren’t in a valid format, exit

* the script with an error

*/

if ( !$this->_validDate($start)

|| !$this->_validDate($end) )

{

return "Invalid date format! Use YYYY-MM-DD HH:MM:SS";

}

/*

* If no event ID passed, create a new event

*/

if ( empty($_POST['event_id']) )

{

$sql = "INSERT INTO events``

(event_title, event_desc, event_start,

``event_end)

VALUES

(:title, :description, :start, :end)";

}

/*

* Update the event if it’s being edited

*/

else

{

/*

* Cast the event ID as an integer for security

*/

$id = (int) $_POST['event_id'];

$sql = "UPDATE events``

SET

``event_title=:title,

``event_desc=:description,

``event_start=:start,

``event_end=:end

WHERE event_id=$id";

}

/*

* Execute the create or edit query after binding the data

*/

try

{

$stmt = $this->db->prepare($sql);

$stmt->bindParam(":title", $title, PDO::PARAM_STR);

$stmt->bindParam(":description", $desc, PDO::PARAM_STR);

$stmt->bindParam(":start", $start, PDO::PARAM_STR);

$stmt->bindParam(":end", $end, PDO::PARAM_STR);

$stmt->execute();

$stmt->closeCursor();

/*

* Returns the ID of the event

*/

return $this->db->lastInsertId();

}

catch ( Exception $e )

{

return $e->getMessage();

}

}

public function confirmDelete($id) {...}

private function _validDate($date) {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

private function _adminGeneralOptions() {...}

private function _adminEntryOptions($id) {...}

}

?>

您可以通过在http://localhost/admin.php向表格中输入错误条目来测试有效性(参见图 9-22 )。

A978-1-4842-1230-1_9_Fig22_HTML.jpg

图 9-22。

An entry with bad date values that should fail validation Note

您使用http://localhost/admin.php是因为您的服务器端验证被调用的唯一原因是用户禁用了 JavaScript。在这种情况下,模式窗口将不起作用,用户将被带到这个窗体。在启用 JavaScript 的情况下,服务器端充当双重检查和针对恶意用户的额外安全措施。

提交该表单后,应用将简单地输出错误信息并终止(见图 9-23 )。日历应用是为启用了 JavaScript 的用户设计的;使用这种方法可以防止应用显示错误。

A978-1-4842-1230-1_9_Fig23_HTML.jpg

图 9-23。

The error message displayed when invalid dates are supplied

添加客户端日期验证

对于大多数用户,JavaScript 将被启用。用户在表单上获得即时反馈要方便得多,因此您将添加新的 jQuery 功能来在客户端验证日期字符串。

创建新的 JavaScript 文件来验证日期字符串

因为您将在下一章继续使用这个脚本,所以您应该将它放在一个名为valid-date.jsjs文件夹中的单独文件中。这个文件将包含一个功能上等同于Calendar类中的_validDate()方法的函数。

它将接受一个日期进行验证,根据您之前使用match()编写的日期匹配正则表达式模式进行检查,如果找到匹配,则返回true,如果match()返回 null,则返回false

您可以通过将以下代码插入到valid-date.js中来构建这个函数:

"use strict";

// Checks for a valid date string (YYYY-MM-DD HH:MM:SS)

function validDate(date)

{

// Define the regex pattern to validate the format

var pattern = /^(\d{4}(-\d{2}){2} (\d{2})(:\d{2}){2})$/;

// Returns true if the date matches, false if it doesn’t

return date.match(pattern)!=null;

}

Note

正则表达式模式没有用引号括起来。如果使用引号,模式将被存储为一个字符串,并相应地进行解释。这将导致脚本寻找精确的字符匹配,而不是正确地解释正则表达式模式。

在页脚中包含新文件

要使用validDate()函数,您需要在init.js之前包含新的 JavaScript 文件,以便可以调用该函数。打开common文件夹中的footer.inc.php,插入以下粗体代码:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js

</script>

<script src="assets/js/valid-date.js"></script>

<script src="assets/js/init.js"></script>

</body>

</html>

如果验证失败,阻止表单提交

现在init.js中有了validDate(),您需要在提交表单之前添加日期验证。将开始和结束日期存储在变量中(分别是startend,然后在允许提交表单之前使用validDate()检查它们。

接下来,将 click 处理程序修改为表单上的 Submit 按钮,该按钮用于编辑或创建事件,如果任一日期输入有无效值,则触发一个带有有用错误消息的警报。您还需要防止表单被提交,这样用户就不必重新填充其他表单域。

您可以通过在init.js中插入以下粗体代码来实现这一点:

// Makes sure the document is ready before executing scripts

jQuery(function($){

var processFile = "assets/inc/ajax.inc.php",

fx = {...}

$("body").on("click", "li>a", function(event){...});

$("body").on("click", ".admin-options form,.admin", function(event){...});

$("body").on("click", ".edit-form a:contains(cancel)", function(event){...});

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function(event){

// Prevents the default form action from executing

event.preventDefault();

// Serializes the form data for use with $.ajax()

var formData = $(this).parents("form").serialize(),

// Stores the value of the submit button

submitVal = $(this).val(),

// Determines if the event should be removed

remove = false,

// Saves the start date input string

start = $(this).siblings("[name=event_start]").val(),

// Saves the end date input string

end = $(this).siblings("[name=event_end]").val();

// If this is the deletion form, appends an action

if ( $(this).attr("name")=="confirm_delete" )

{

// Adds necessary info to the query string

formData += "&action=confirm_delete"

+ "&confirm_delete="+submitVal;

// If the event is really being deleted, sets

// a flag to remove it from the markup

if ( submitVal=="Yes, Delete It" )

{

remove = true;

}

}

// If creating/editing an event, checks for valid dates

if ( $(this).siblings("[name=action]").val()=="event_edit" )

{

if ( !validDate(start) || !validDate(end) )

{

alert("Valid dates only! (YYYY-MM-DD HH:MM:SS)");

return false;

}

}

// Sends the data to the processing file

$.ajax({

type: "POST",

url: processFile,

data: formData,

success: function(data) {

// If this is a deleted event, removes

// it from the markup

if ( remove===true )

{

fx.removeevent();

}

// Fades out the modal window

fx.boxout();

// If this is a new event, adds it to

// the calendar

if ( $("[name=event_id]").val().length==0

&& remove===false )

{

fx.addevent(data, formData);

}

},

error: function(msg) {

alert(msg);

}

});

});

现在保存这些更改,在浏览器中加载http://localhost/,然后使用模态窗口表单创建一个带有坏参数的新事件(见图 9-24 )。

A978-1-4842-1230-1_9_Fig24_HTML.jpg

图 9-24。

An entry that will fail to validate

如果此时单击提交按钮,验证将失败,应用将显示一个警告框,其中包含关于日期格式的错误消息(参见图 9-25 )。

A978-1-4842-1230-1_9_Fig25_HTML.jpg

图 9-25。

The error message in an alert box after failing validation

单击警告框中的 OK 按钮后,用户将能够编辑她的条目,而不必重新填充任何字段。

摘要

在这一章中,你解决了使用正则表达式进行表单验证的问题。您学到的概念可以应用于验证任何类型的数据,它们将极大地帮助您确保表单中提供的信息可以被您的应用使用。

在下一章中,您将学习如何扩展 jQuery 对象,包括直接扩展 jQuery 核心和为 jQuery 开发定制插件。

十、扩展 jQuery

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​10) contains supplementary material, which is available to authorized users.

jQuery 易于使用的语法导致开发人员开始编写脚本来实现自定义效果和其他任务。为了使这些脚本可配置和可重用,这些开发人员将它们构建为插件,或者通过向库中添加新方法来扩展 jQuery 的脚本。在本章中,您将学习如何向 jQuery 添加自己的插件。

向 jQuery 添加函数

在某些情况下,可能需要直接向jQuery对象添加一个函数,这意味着您可以像这样调用它:

$.yourFunction();

向 jQuery 添加函数可以帮助您组织脚本,并确保函数调用遵循一致的格式。但是,需要注意的是,向 jQuery 添加一个函数并不允许您将它与一组选定的 DOM 元素链接起来;为此,你必须使用一个方法,你将在本章中学习如何做。

向 jQuery 添加日期验证函数

在您的第一个扩展 jQuery 示例中,您将把您在上一章中编写的日期验证函数添加到jQuery对象中。具体来说,您将利用valid-date.js

允许在 jQuery 插件中使用自定义别名

您应该考虑的一件事是允许在 jQuery 插件中使用自定义别名。虽然这并不是绝对必要的,但是我们强烈建议这样做,因为如果jQuery.noConflict()放弃了$快捷方式,它可以帮助您防止插件崩溃。更好的是,这个特性实现起来如此简单,以至于不包含它实际上有点愚蠢。

当构建一个新的插件时,您应该将插件代码放在一个函数中,该函数在脚本加载时立即执行。一开始,脚本应该是这样的:

(function(){

// plug-in code here...

})();

第二组括号使前面的代码作为函数立即执行,这就是自定义别名的用武之地。如果您将jQuery对象传递给第二组括号,将$快捷方式传递给内部函数,那么代码将使用$快捷方式正常工作,即使使用jQuery.noConflict()将它返回给全局名称空间:

(function($){

// plug-in code here...

})(jQuery);

您可以使用任何有效的 JavaScript 变量名来代替$,使用这种方法脚本仍然可以正确执行:

(function(custom){

// Adds a background color to any paragraph

// element using a custom alias

custom("p").css("background-color","yellow");

})(jQuery);

将函数附加到 jQuery 对象

要将该函数附加到 jQuery,请将以下代码添加到valid-date.js:

(function($){

// Extends the jQuery object to validate date strings

$.validDate = function()

{

// code here

};

})(jQuery);

使用这种格式,您现在可以像这样调用validDate()函数:

$.validDate();

允许可配置选项

就像在最初的validDate()函数中一样,一个日期字符串将被传递到函数中。但是,为了使这个函数更具可配置性,您可以传入一个包含配置选项的对象(如果需要)来修改用于匹配日期字符串的 regex 模式:

(function($){

// Extends the jQuery object to validate date strings

$.validDate = function(date, options)

{

// code here

};

})(jQuery);

options对象只有一个属性:用于验证的模式。因为您希望options对象是可选的,所以您通过插入以下粗体代码为函数中的模式定义了一个默认值:

(function($){

// Extends the jQuery object to validate date strings

$.validDate = function(date, options)

{

// Sets up default values for the method

var defaults = {

"pattern" : /^(\d{4}(-\d{2}){2} (\d{2})(:\d{2}){2})$/

};

};

})(jQuery);

使用用户提供的选项扩展默认选项

您可以使用$.extend()函数扩展default对象,这将通过组合默认选项和用户提供的选项来创建一个新对象。如果有三个可用选项,而用户传递一个只定义了其中两个的对象,使用$.extend()只会替换用户重新定义的两个属性。

插入粗体显示的代码来扩展default对象:

(function($){

// Extends the jQuery object to validate date strings

$.validDate = function(date, options)

{

// Sets up default values for the method

var defaults = {

"pattern" : /^(\d{4}(-\d{2}){2} (\d{2})(:\d{2}){2})$/

},

// Extends the defaults with user-supplied options

opts = $.extend(defaults, options);

};

})(jQuery);

执行验证并返回值

执行验证和返回值的步骤与原始函数中的步骤几乎相同,除了您在这里通过opts对象访问模式:

(function($){

// Extends the jQuery object to validate date strings

$.validDate = function(date, options)

{

// Sets up default values for the method

var defaults = {

"pattern" : /^(\d{4}(-\d{2}){2} (\d{2})(:\d{2}){2})$/

},

// Extends the defaults with user-supplied options

opts = $.extend(defaults, options);

// Returns true if a match is found, false otherwise

return date.match(opts.pattern)!=null;

};

})(jQuery);

符合 jQuery 插件文件命名约定

要将插件正式称为插件,必须使用插件文件的 jQuery 命名约定。接受的格式是jquery.[name of plug-in].js。为了满足这一准则,将valid-date.js的名称改为jquery.validDate.js

修改包含脚本

现在文件名已经更改,您需要更新footer.inc.php来包含它。进行以粗体显示的更改,以加载正确的文件:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js

</script>

<script src="assets/js/jquery.validDate.js"></script>

<script src="assets/js/init.js"></script>

</body>

</html>

修改初始化脚本

最后,调整init.js来调用您刚刚添加的新 jQuery 函数,方法是进行粗体显示的调整:

jQuery(function($){

var processFile = "assets/inc/ajax.inc.php",

fx = {...}

$("body").on("click", "li>a", function(event){...});

$("body").on("click", ".admin-options form,.admin", function(event)(event){...});

$("body").on("click", ".edit-form a:contains(cancel)", function(event){...});

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function(event){

// Prevents the default form action from executing

event.preventDefault();

// Serializes the form data for use with $.ajax()

var formData = $(this).parents("form").serialize(),

// Stores the value of the submit button

submitVal = $(this).val(),

// Determines if the event should be removed

remove = false,

// Saves the start date input string

start = $(this).siblings("[name=event_start]").val(),

// Saves the end date input string

end = $(this).siblings("[name=event_end]").val();

// If this is the deletion form, appends an action

if ( $(this).attr("name")=="confirm_delete" )

{

// Adds necessary info to the query string

formData += "&action=confirm_delete"

+ "&confirm_delete="+submitVal;

// If the event is really being deleted, sets

// a flag to remove it from the markup

if ( submitVal=="Yes, Delete It" )

{

remove = true;

}

}

// If creating/editing an event, checks for valid dates

if ( $(this).siblings("[name=action]").val()=="event_edit" )

{

if ( !$.validDate(start) || !$.validDate(end) )

{

alert("Valid dates only! (YYYY-MM-DD HH:MM:SS)");

return false;

}

}

// Sends the data to the processing file

$.ajax({

type: "POST",

url: processFile,

data: formData,

success: function(data) {

// If this is a deleted event, removes

// it from the markup

if ( remove===true )

{

fx.removeevent();

}

// Fades out the modal window

fx.boxout();

// If this is a new event, adds it to

// the calendar

if ( $("[name=event_id]").val().length==0

&& remove===false )

{

fx.addevent(data, formData);

}

},

error: function(msg) {

alert(msg);

}

});

});

});

保存前面的代码后,您可以重新加载http://localhost/并尝试提交一个带有错误日期值的新事件。该结果与使用原始validDate()功能时获得的结果相同。

向 jQuery 添加方法

要向 jQuery 对象添加可链接的方法,必须将其附加到 jQuery 的fn对象。这允许您对一组选定的元素调用方法:

$(".class").yourPlug-in();

Note

jQuery 的fn对象实际上只是 jQuery 对象的prototype对象的别名。修改对象的原型将影响该对象的所有未来实例,而不仅仅是当前实例。有关这方面的更多信息,请在 www.javascriptkit.com/javatutors/proto.shtml 查看 JavaScript 中对prototype对象的简要说明。

构建您的插件

您将在本节中构建的插件将依靠一个简单的方法,当用户将鼠标悬停在事件标题上时放大它们,然后当用户将鼠标从标题上移开时将它们恢复到原始大小。

这个插件将被称为dateZoom,它将允许用户配置用于动画的大小、速度和缓动方程式。

创建一个正确命名的插件文件

创建这个插件时,您的首要任务是给它命名。在js文件夹中创建一个名为jquery.dateZoom.js的新文件,并插入自定义别名函数:

(function($){

// plug-in code here

})(jQuery);

在该函数中,通过插入以下粗体代码将新方法附加到fn对象:

(function($){

// A plug-in that enlarges the text of an element when moused

// over, then returns it to its original size on mouse out

$.fn.dateZoom = function(options)

{

// code here

};

})(jQuery);

提供可公开访问的默认选项

在您的validDate()插件中,函数的默认选项存储在私有对象中。这可能是不可取的,尤其是在用户可能将插件方法应用于多组元素,然后想要修改所有实例的默认值的情况下。

为了使默认选项可以公开访问,您可以将它们存储在dateZoom名称空间中。对于您的dateZoom插件,创建一个可公开访问的defaults对象,它包含六个定制属性:

  • fontsize:字体将扩展到的大小。默认设置为110%
  • easing:动画使用的缓动功能。默认设置为swing
  • duration:动画应该持续的毫秒数。默认设置为600
  • selector:要绑定到的元素的 CSS 选择器。默认设置为li>a
  • match:与要绑定到的元素相匹配的属性。默认设置为href
  • callback:动画完成时触发的功能。默认设置为null

现在,通过插入以下粗体代码,将默认选项添加到dateZoom插件中:

(function($){

// A plug-in that enlarges the text of an element when moused

// over, then returns it to its original size on mouse out

$.fn.dateZoom = function(options)

{

// code here

};

// Defines default values for the plug-in

$.fn.dateZoom.defaults = {

"fontsize" : "110%",

"easing" : "swing",

"duration" : "600",

"selector" : "li>a",

"match" : "href",

"callback" : null

};

})(jQuery);

此时,用户可以使用类似下面的语法更改对dateZoom插件的所有调用的默认值:

$.fn.dateZoom.defaults.fontsize = "120%";

要覆盖默认选项,用户可以为一个或多个默认选项传递一个带有新值的对象,就像在validDate插件中一样。您可以使用$.extend()创建一个新的对象,该对象包含创建插件时当前调用的值。

以下粗体代码将该功能添加到了dateZoom插件中:

(function($){

// A plug-in that enlarges the text of an element when moused

// over, then returns it to its original size on mouse out

$.fn.dateZoom = function(options)

{

// Only overwrites values that were explicitly passed by

// the user in options

var opts = $.extend($.fn.dateZoom.defaults, options);

// more code here

};

// Defines default values for the plug-in

$.fn.dateZoom.defaults = {

"fontsize" : "110%",

"easing" : "swing",

"duration" : "600",

"selector" : "li>a",

"match" : "href",

"callback" : null

};

})(jQuery);

保持链接能力

为了保持插件方法可链接,该方法必须返回修改后的jQuery对象。幸运的是,使用 jQuery 很容易做到这一点:您所需要做的就是对this对象运行.each()方法来迭代每个选中的元素,然后返回this对象。

dateZoom插件中,您可以通过插入粗体显示的代码使您的方法可链接:

(function($){

// A plug-in that enlarges the text of an element when moused

// over, then returns it to its original size on mouse out

$.fn.dateZoom = function(options)

{

// Only overwrites values that were explicitly passed by

// the user in options

var opts = $.extend($.fn.dateZoom.defaults, options);

// Loops through each matched element and returns the

// modified jQuery object to maintain chainability

return this.each(function(){

// more code here

});

};

// Defines default values for the plug-in

$.fn.dateZoom.defaults = {

"fontsize" : "110%",

"easing" : "swing",

"duration" : "600",

"selector" : "li>a",

"match" : "href",

"callback" : null

};

})(jQuery);

创建可公开访问的帮助器方法

为了保持插件代码整洁有序,您将把元素的实际动画放在一个名为zoom的助手方法中。

这个方法和defaults对象一样,可以在dateZoom名称空间下公开访问。将该方法公开意味着用户可以在调用插件之前重新定义该方法,或者甚至在插件之外调用该方法,如果她愿意的话。

您可以通过将以下粗体代码插入到dateZoom插件中来创建zoom方法:

(function($){

// A plug-in that enlarges the text of an element when moused

// over, then returns it to its original size on mouse out

$.fn.dateZoom = function(options)

{

// Only overwrites values that were explicitly passed by

// the user in options

var opts = $.extend($.fn.dateZoom.defaults, options);

// Loops through each matched element and returns the

// modified jQuery object to maintain chainability

return this.each(function(){

// more code here

});

};

// Defines default values for the plug-in

$.fn.dateZoom.defaults = {

"fontsize" : "110%",

"easing" : "swing",

"duration" : "600",

"selector" : "li>a",

"match" : "href",

"callback" : null

};

// Defines a utility function that is available outside of the

// plug-in if a user is so inclined to use it

$.fn.dateZoom.zoom = function(element, size, opts)

{

// zoom the elements

};

})(jQuery);

此方法接受要制作动画的元素、应该制作动画的大小以及包含选项的对象。

Note

您将大小与其余选项分开,因为元素的原始字体大小将用于将元素返回到其原始状态,并且该值在options对象中不可用。

在这个方法中,您将使用.animate().dequeue().clearQueue()方法来制作对象的动画并防止动画队列堆积;添加以粗体显示的代码来实现这一点:

(function($){

// A plug-in that enlarges the text of an element when moused

// over, then returns it to its original size on mouse out

$.fn.dateZoom = function(options)

{

// Only overwrites values that were explicitly passed by

// the user in options

var opts = $.extend($.fn.dateZoom.defaults, options);

// Loops through each matched element and returns the

// modified jQuery object to maintain chainability

return this.each(function(){

// more code here

});

};

// Defines default values for the plug-in

$.fn.dateZoom.defaults = {

"fontsize" : "110%",

"easing" : "swing",

"duration" : "600",

"selector" : "li>a",

"match" : "href",

"callback" : null

};

// Defines a utility function that is available outside of the

// plug-in if a user is so inclined to use it

$.fn.dateZoom.zoom = function(element, size, opts)

{

// Limit zoom effect to currently hovered element.

if (opts.match)

{

element = $.grep($(element), function(elem) {

return elem[opts.match] === $('a:hover')[0][opts.match];

});

}

$(element).animate({

"font-size" : size

},{

"duration" : opts.duration,

"easing" : opts.easing,

"complete" : opts.callback

})

.dequeue()     // Prevents jumpy animation

.clearQueue(); // Ensures only one animation occurs

};

})(jQuery);

Note

.dequeue()方法将当前动画从动画队列中取出,防止在用.clearQueue()清空队列时动画跳到末尾。允许队列建立会导致动画元素看起来跳动或者快速连续地多次执行动画,这绝对是不希望的效果。

修改每个匹配的元素

因为.each()方法接受回调,所以您可以很容易地修改正在处理的jQuery对象中的每个匹配元素。对于dateZoom插件,您将为每个选中的元素添加hover事件处理程序。

当用户将鼠标悬停在已经应用了dateZoom的元素上时,zoom方法将会运行。这个方法依赖于defaults对象的selector属性来指定应用效果的元素的类型。

如果需要的话,defaults对象的可选属性match用于进一步缩小受影响的元素。如果指定了这个选项,请注意,它使用 jQuery $.grep函数根据与当前鼠标悬停元素的成功匹配进一步过滤最初选择的元素($('a:hover')[0][opts.match])。

属性相应地调整文本的大小。当用户停止悬停时,原来的文本大小将被传递给zoom,元素的文本将恢复到原来的大小。要存储原始大小,使用.css()方法并将原始字体大小放在私有变量中。

通过将下面的粗体代码插入到dateZoom插件中,您可以使用.hover()方法来实现这个功能:

(function($){

// A plug-in that enlarges the text of an element when moused

// over, then returns it to its original size on mouse out

$.fn.dateZoom = function(options)

{

// Only overwrites values that were explicitly passed by

// the user in options

var opts = $.extend($.fn.dateZoom.defaults, options);

// Loops through each matched element and returns the

// modified jQuery object to maintain chainability

return this.each(function(){

// Stores the original font size of the element

var originalsize = $(opts.selector).css("font-size");

// Binds functions to the hover event. The first is

// triggered when the user hovers over the element, and

// the second when the user stops hovering

$(this).hover(function(){

$.fn.dateZoom.zoom(opts.selector, opts.fontsize, opts);

},

function(){

$.fn.dateZoom.zoom(opts.selector, originalsize, opts);

});

});

};

// Defines default values for the plug-in

$.fn.dateZoom.defaults = {

"fontsize" : "110%",

"easing" : "swing",

"duration" : "600",

"selector" : "li>a",

"match" : "href",

"callback" : null

};

// Defines a utility function that is available outside of the

// plug-in if a user is so inclined to use it

$.fn.dateZoom.zoom = function(element, size, opts)

{

// Limit zoom effect to currently hovered element.

if (opts.match)

{

element = $.grep($(element), function(elem) {

return elem[opts.match] === $('a:hover')[0][opts.match];

});

}

$(element).animate({

"font-size" : size

},{

"duration" : opts.duration,

"easing" : opts.easing,

"complete" : opts.callback

})

.dequeue()     // Prevents jumpy animation

.clearQueue(); // Ensures only one animation occurs

};

})(jQuery);

实现您的插件

此时,您的插件已经准备好实现了。剩下的工作就是包含文件并选择一组元素来运行它。

包括插件文件

要包含插件文件,需要修改footer.inc.php并添加一个新的脚本标签。与validDate插件一样,dateZoom插件需要包含在init.js之前,以便该方法可以被调用:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js

</script>

<script src="assets/js/jquery.validDate.js"></script>

<script src="assets/js/jquery.dateZoom.js"></script>

<script src="assets/js/init.js"></script>

</body>

</html>

在一组元素上初始化插件

插件现在已经包含在应用中,所以您可以对一组元素调用.dateZoom()方法。下一组更改要求您修改init.js,所以现在打开那个文件。

首先将默认的fontsize值更改为13px,然后将.dateZoom()方法添加到使用selector属性选择的元素集的方法链中(在本应用中为"li>a")。如前所述,您可以通过修改init.js来实现这些更改,如下面的粗体代码所示:

jQuery(function($){

var processFile = "assets/inc/ajax.inc.php",

fx = {...}

// Set a default font-size value for dateZoom

$.fn.dateZoom.defaults.fontsize = "13px";

// Pulls up events in a modal window and attaches a zoom effect

$("body")

.dateZoom()

.on("click", "li>a", function(event){

// Stops the link from loading view.php

event.preventDefault();

// Adds an "active" class to the link

$(this).addClass("active");

// Gets the query string from the link href

var data = $(this)

.attr("href")

.replace(/.+?\?(.*)$/, "$1"),

// Checks if the modal window exists and

// selects it, or creates a new one

modal = fx.checkmodal();

// Creates a button to close the window

$("<a>")

.attr("href", "#")

.addClass("modal-close-btn")

.html("×")

.click(function(event){

// Removes event

fx.boxout(event);

})

.appendTo(modal);

// Loads the event data from the DB

$.ajax({

type: "POST",

url: processFile,

data: "action=event_view&"+data,

success: function(data){

// Displays event data

fx.boxin(data, modal);

},

error: function(msg) {

alert(msg);

}

});

});

$("body").on("click", ".admin-options form,.admin", function(event)(event){...});

$("body").on("click", ".edit-form a:contains(cancel)", function(event){...});

// Edits events without reloading

$("body").on("click", ".edit-form input[type=submit]", function(event){...});

});

保存这些更改,在浏览器中重新加载http://localhost/,然后将鼠标悬停在一个事件标题上,查看运行中的dateZoom插件(参见图 10-1 )。

A978-1-4842-1230-1_10_Fig1_HTML.jpg

图 10-1。

The event title enlarges when hovered over

通过将defaults对象的match属性设置为空字符串"",然后重新加载,从而暂时禁用该属性,这也是一个有益的尝试。如果你这样做了,你会注意到所有的selector元素(在这个应用中是"li>a")都会在鼠标经过它们时得到缩放效果。这显示了 jQuery(CSS 派生的)访问 DOM 中元素的方法的多样性和强大功能,以及可能的复杂性。

摘要

现在,您应该可以轻松地在 jQuery 中构建定制插件了,无论是作为可链接的方法还是作为函数。这一章相当短,但是这种简短证明了您可以轻松地用自己的定制脚本扩展 jQuery 库。

恭喜你!现在,您已经了解了如何结合使用 PHP 和 jQuery 来构建具有类似桌面应用感觉的定制应用。现在,您已经准备好将您所有的好想法在网络上实现了!

十一、PHP 7 简介

在本附录中,我们简要分析了 PHP 语言版本 7 中的一些变化。

大多数遗留代码将保持不变地运行(就像本书前一版本中的所有代码一样),但是这里我们来看看那些对您的目的最有用的新特性。

Note

要进一步了解 PHP 7 的所有变化,请参见 http://php.net/manual/en/migration70.php 的官方迁移指南。

严格打字

对于那些在编码实践中喜欢结构和纪律的人来说,PHP 7 中一个受欢迎的新增功能是可选的每个文件的指令strict_types,它使文件中的所有函数调用和返回语句都接受标量类型的“严格”类型检查。而且除了已经存在的callablearray类型,PHP 7 还增加了stringintfloatbool变量类型。参数类型在参数列表中指定,返回类型在参数列表后用冒号指定。

下面是一些严格类型:

<?php

declare(strict_types=1);

function doStuff(``int``$int=0,``string``$str="",``float``$flt=0.0,``bool``$b=False)``: string

{

echo $int . "<br/>";

echo $flt . "<br/>";

echo $b . "<br/>";

return $str;

}

$output = doStuff(1, "hi there", "2.34", True); // Oops! 2.34 should not have quotes!

echo $output . "<br/>";

?>

当您在浏览器中加载该脚本时,您将看到如下内容:

Fatal error``: Uncaught TypeError: Argument 3 passed to doStuff() must be of the type

float, string given, called in C:\wamp\www\book\appendix\test.php on line 13 and A978-1-4842-1230-1_11_Figa_HTML.jpg

defined in C:\wamp\www\book\appendix\test.php:5 Stack trace: #0 A978-1-4842-1230-1_11_Figa_HTML.jpg

C:\wamp\www\book\appendix\test.php(13): doStuff(1, 'hi there', '2.34', true) A978-1-4842-1230-1_11_Figa_HTML.jpg

#1 {main} thrown in``C:\wamp\www\book\appendix\test.php``on line

这其实是一件好事!通常情况下,代码尽早失败并发出噪音会更好。无声的臭虫本质上是你(或者更糟,一个用户)会引爆的地雷,通常是在相当不方便的时候。在doStuff()的定义中的参数类型声明为它的第三个参数指定了一个float,但是却传递了一个string(因为引号的原因)。

让我们通过去掉2.34周围的引号来修复参数类型错误,然后引入另一个不同的错误——返回类型错误。代码如下:

<?php

declare(strict_types=1);

function doStuff(int $int=0, string $str="", float $flt=0.0, bool $b=False): string

{

echo $int . "<br/>";

echo $flt . "<br/>";

echo $b . "<br/>";

return $flt; // Oops! Supposed to be a string (such as $str).

}

$output = doStuff(1, "hi there", 2.34, True);

echo $output . "<br/>";

?>

当您在浏览器中加载这个脚本时,您会看到

1

2.34

1

Fatal error``: Uncaught TypeError: Return value of doStuff() must be of the type

string, float returned in C:\wamp\www\book\appendix\test.php:10 Stack trace: A978-1-4842-1230-1_11_Figa_HTML.jpg

#0 C:\wamp\www\book\appendix\test.php(13): doStuff(1, 'hi there', 2.34, true) #1 A978-1-4842-1230-1_11_Figa_HTML.jpg

{main} thrown in``C:\wamp\www\book\appendix\test.php``on line

这一次,你用错误的回球类型“画了犯规”:a float而不是承诺的string

尽管这最终是个人(或团队)偏好的问题,但我们相信 PHP 7 中引入的严格类型是非常值得的,并且可能是新添加的特性中最有用的。

新例外

除了上面讨论的严格类型之外,PHP 7 还提供了相应的异常:TypeError

下面是使用这种新的异常类型封装在try块中的上述代码:

<?php

declare(strict_types=1);

function doStuff(int $int=0, string $str="", float $flt=0.0, bool $b=False): string

{

echo $int . "<br/>";

echo $flt . "<br/>";

echo $b . "<br/>";

return $flt; // Oops! Supposed to be a string (such as $str).

}

try {

$output = doStuff(1, "hi there", 2.34, True);

echo $output . "<br/>";

} catch(TypeError $e) {

echo $e;

}

?>

您可以选择如何最好地处理异常,但关键是错误不再是致命的。上面的代码转储关于异常的所有信息:

1

2.34

1

TypeError: Return value of doStuff() must be of the type string, float returned A978-1-4842-1230-1_11_Figa_HTML.jpg

in C:\wamp\www\book\appendix\test.php:10 Stack trace: #0 A978-1-4842-1230-1_11_Figa_HTML.jpg

C:\wamp\www\book\appendix\test.php(14): doStuff(1, 'hi there', 2.34, true) #1 {main}

PHP 7 中还包含了ArithmeticErrorAssertionErrorDivisionByZeroErrorParseError异常。有关更多信息,请参考上面给出的链接。

常量数组

另一个受欢迎的特性是定义常量(只读)数组的能力。这保护了查找表和其他在程序执行期间不打算改变的数据。该数组可以根据需要深度嵌套。

与普通变量不同,数组常量前面没有$符号,通常(尽管不是必需的)用大写字母来命名它们。否则,语法是熟悉的:

<?php

declare(strict_types=1);

define('ACTIONS', array(

'user_login' => array(

'object' => 'Admin',

'method' => 'processLoginForm',

'header' => 'Location: ../../'

),

'user_logout' => array(

'object' => 'Admin',

'method' => 'processLogout',

'header' => 'Location: ../../'

)

)

);

echo ACTIONS['user_logout']['method'];

?>

结果如下:

processLogout

如果试图通过添加新值或重新分配现有值来改变常量数组,如下所示:

<?php

declare(strict_types=1);

define('ACTIONS', array(

'user_login' => array(

'object' => 'Admin',

'method' => 'processLoginForm',

'header' => 'Location: ../../'

),

'user_logout' => array(

'object' => 'Admin',

'method' => 'processLogout',

'header' => 'Location: ../../'

)

)

);

ACTIONS['user_logout']['method'] = 'somethingElse'; // Not allowed!

?>

然后,令人高兴的是,当您的代码运行时,您将立即发现类似这样的消息:

Fatal error``: Cannot use temporary expression in write context in

C:\wamp\www\appendix\test.php``on line

对于那些喜欢在编码实践中增加结构和安全性的人来说,这又是一个福音。

参数解包

添加的另一个方便的特性是参数解包,其中数组变量的元素可以作为一个函数的单独参数展开,该函数需要一个标量列表而不是一个数组作为其输入。这使您可以更加灵活地调用给定的函数,而不必在其定义中重写参数列表(对于库函数,您甚至可能没有这个选项)。

这个操作符通常被称为“splat”操作符(尤其是 Ruby 社区),由三个点(句点)组成,用法如下:

<?php

declare(strict_types=1);

function add($a, $b, $c) {

return $a + $b + $c;

}

$two_more = [2, 5];

echo add(1, ...$two_more); // Unpack the elements of $two_more.

?>

你得到预期的总和:

8

注意,虽然...操作符解包了它所应用的数组中的所有元素,但函数调用仍然只使用其定义中列出的那些参数。换句话说,如果要解包的数组包含两个以上的元素(在本例中),那么所有附加的元素都会被忽略。(你可以自己试一下,看看类似于$two_more = [2, 5, 77, -13];的东西仍然给出与上面的8相同的和。)这与现有的 PHP 行为一致。

整数除法

增加了一个整数除法函数intdiv(),使整数除法变得方便明了。它的工作方式正如您所料:

<?php

declare(strict_types=1);

$num = 11;

$den = 3;

$result = intdiv($num, $den);

echo $result;

?>

返回截断的整数值:

3

句法糖

在撰写本文时,PHP 7 的最终版本仍然悬而未决,我们鼓励您查看最新的文档,但是这里我们提到了一些已经发布的好东西。

添加了一个特殊的条件操作符,称为“null coalesce”操作符,用双问号表示,它扩展了三元条件操作符的功能。null coalesce 运算符在下降到最终值之前检查非 null 值。此外,这个新操作符是可链接的。

一段代码说明了一切:

<?php

declare(strict_types=1);

$alpha = null;

$beta = null;

$gamma = "I’m not null!";

echo $alpha ?? 'this value' . "<br/>";

echo $alpha ?? $beta ?? $gamma ?? 'that value' . "<br/>"; // chainable

?>

输出是

this value

I’m not null!

我们要看的最后一个特性无疑是受 Perl 社区的启发,它与排序有关。考虑下面的例程,尽管它很笨拙,但它执行不区分大小写的字母排序:

<?php

declare(strict_types=1);

$values = ['Zebra', 'zed', 'apple', 'baseball', 'Angel'];

usort($values, function($a, $b) {

$x = strtolower($a);

$y = strtolower($b);

return ($x < $y) ? -1 : (($x > $y) ? 1 : 0);

});

var_dump($values);

?>

这段代码确实完成了工作:

array(5) { [0]=> string(5) "Angel" [1]=> string(5) "apple" [2]=> string(8) "baseball" A978-1-4842-1230-1_11_Figa_HTML.jpg

[3]=> string(5) "Zebra" [4]=> string(3) "zed" }

但是使用新的 PHP 7“飞船”操作符<=>,同样的结果可以更好地实现,如下面的代码所示:

<?php

declare(strict_types=1);

$values = ['Zebra', 'zed', 'apple', 'baseball', 'Angel'];

usort($values, function($a, $b) {

return strtolower($a) <=> strtolower($b); // spaceship!

});

var_dump($values);

?>

您可以检查输出是否与之前完全相同,但是使用了更简洁的代码。

同样,请继续在线查看最新的 PHP 7 文档,以获得对所有语言特性的详尽论述和详细的迁移指南。

第一部分:熟悉 jQuery

Abstract

在本书的第一部分,您将熟悉 jQuery 的历史和基本功能。在本节结束时,您将对 jQuery 背后的总体概念有一个大致的了解,并且——在您重温了第二部分中面向对象的 PHP 之后——您将准备好处理第三部分中的练习(在那里您实际上开始用 jQuery 和 PHP 构建一个真实世界的项目)。

Getting Comfortable with jQuery

在本书的第一部分,您将熟悉 jQuery 的历史和基本功能。在本节结束时,您将对 jQuery 背后的总体概念有一个大致的了解,并且——在您重温了第二部分中面向对象的 PHP 之后——您将准备好处理第三部分中的练习(在那里您实际上开始用 jQuery 和 PHP 构建一个真实世界的项目)。

第二部分:进入高级 PHP 编程

Abstract

此时,您将把新的 jQuery 知识暂时放在一边,专注于使用 PHP 的后端。第二部分将教您如何为事件日历规划和实现一个面向对象的后端解决方案,稍后您将使用 jQuery 的新知识对其进行增强。这本书假设你对 PHP 的基本概念(变量、函数、基本语言结构)有合理的理解;要提高你的 PHP 基础知识,请查看学习 PHP 7 (Apress,2015)。

Getting Into Advanced PHP Programming

此时,您将把新的 jQuery 知识暂时放在一边,专注于使用 PHP 的后端。第二部分将教您如何为事件日历规划和实现一个面向对象的后端解决方案,稍后您将使用 jQuery 的新知识对其进行增强。这本书假设你对 PHP 的基本概念(变量、函数、基本语言结构)有合理的理解;要提高你的 PHP 基础知识,请查看学习 PHP 7 (Apress,2015)。

第三部分:将 jQuery 与 PHP 应用相结合

Abstract

日历正常运行后,现在可以用 jQuery 增强应用,以改善用户体验。在接下来的章节中,您将创建一个 JavaScript 层,它将位于您的应用之上,以添加 AJAX 功能。

Combining jQuery with PHP Applications

日历正常运行后,现在可以用 jQuery 增强应用,以改善用户体验。在接下来的章节中,您将创建一个 JavaScript 层,它将位于您的应用之上,以添加 AJAX 功能。

第四部分:高级 jQuery 和 PHP

Abstract

至此,您已经有了一个功能性的日历应用。但是,您可以添加一些东西来改善用户体验,比如表单验证。本书的这一部分还将介绍一些高级技术,比如用正则表达式验证用户输入,以及构建定制的 jQuery 插件。

Advancing jQuery and PHP

至此,您已经有了一个功能性的日历应用。但是,您可以添加一些东西来改善用户体验,比如表单验证。本书的这一部分还将介绍一些高级技术,比如用正则表达式验证用户输入,以及构建定制的 jQuery 插件。

posted @ 2024-08-03 11:25  绝不原创的飞龙  阅读(5)  评论(0编辑  收藏  举报