PHP 编程高级教程(六)
十五、JSON 和 Ajax
近年来,网页模仿桌面应用的功能不断发展。尽管增加了复杂性,但用户体验已经大大改善;发达的网站感觉更具响应性和吸引力。通过更快的反馈、弹出提示、自动完成和更少的必要整页重新加载,浏览是一种更丰富、更直观和愉快的体验。
使这一切成为可能的是从浏览器向服务器发出异步请求并接收返回响应的技术。这些请求是异步的,因为它们是在独立的线程中完成的,不会阻塞主脚本的执行。信息以 JSON (JavaScript 对象表示法)、XML(可扩展标记语言)或纯文本的形式来回传递。浏览器和服务器之间的异步通信不需要重新加载整个页面,这就是 Ajax。
注意【Ajax 这个术语是 Jesse James Garrett 在 2005 年创造的,最初代表异步 JavaScript 和 XML。从那以后,其他脚本语言和数据格式,如 VBScript 和 JSON,使得异步通信成为可能。出于这个原因,取决于你问谁,Ajax 也可以仅仅意味着异步 web 通信技术。
Ajax 不是一种技术,而是由几个相互关联的工具组成。这些组件是:
- 表示层:HTML(超文本标记语言)或 XHTML(可扩展 HTML)和 CSS(级联样式表)和 DOM(文档对象模型)
- 数据交换:XML、JSON、HTML 或纯文本
- 异步通信:JavaScript
XMLHttpRequest对象
XML 和 DOM 在第十四章中介绍。假设读者熟悉 HTML、CSS 和 JavaScript。
在这一章中,我们将首先看看 JSON 格式,并在 PHP 中使用它。我们将讨论 JavaScript XMLHttpRequest对象以及如何使用它。我们将展示如何向一个 URL 发送一个 Ajax 请求,并用数据进行响应。我们将展示更高级的 JavaScript API jQuery 如何使 Ajax 请求变得更加容易。
在本章快结束时,我们将构建一个包含我们所学的所有组件的演示示例。演示将是一个基于表格的绘图网格,我们可以修改,编辑,保存和加载。我们将使用 jQuery 来更改单元格的背景颜色,并使用 Ajax 请求和 PHP 将图像数据保存到一个文件中,并在我们再次访问页面时加载它。
上
像 XML 一样,JSON 只是表示数据的另一种方式,XML 将在第十四章详细介绍。JSON 有七种数据类型:strings、objects、arrays、numbers、true、false和null。Strings必须用双引号括起来,可以包含转义字符,如\n、\t和\"。JSON objects用大括号括起来,包含用逗号分隔的键/值对。在 JSON 中,键总是字符串,而值可以是七种数据类型中的任何一种,包括对象和数组。
JSON 对象的一个例子如下所示:
{"name":"Brian", "age":29}
这里,关键字"name"对应于字符串值"Brian",关键字"age"对应于数值 29。
JSON 数组用括号括起来,包含用逗号分隔的值。JSON 数组的一个例子如下:
["Brian", 29]
JSON 对象和数组也可以嵌套。下面是一个表示图像的 JSON 对象:
{ "dimensions": { "width":800, "height":600 }, "format":"jpg", "alpha_channel": false, "filename":"clouds.jpg" }
键"dimensions"有另一个对象作为它的值。这个嵌套对象有代表对象宽度和高度的键/值对。
这里显示了嵌套在一个 JSON 数组中的多个 JSON 对象:
`[ { "dimensions": { "width":800, "height":600 }, "format":"jpg", "alpha_channel": false, "filename":"clouds.jpg" },
{ "dimensions": { "width":40, "height":40 },`
"format":" png", "alpha_channel":true, "filename":"icon.jpg" } ]
此处显示了一个 JSON 对象,它包含表示一些颜色数据的独立红、绿、蓝(RGB)通道的数组:
{ "red": [128,128,255,255,255,128,128,0,0], "green": [0, 0, 0, 0, 0, 0, 0,0,0], "blue": [128,128,255,255,255,128,128,0,0] }
下面是相同的颜色数据,表示为 RGB 数组三元组的嵌套数组:
[ [128, 0, 128], [128,0,128], [255, 0, 255], [255, 0,255], [255, 0, 255], [128,0,128], [128, 0, 128], [0, 0, 0], [0,0,0] ]
PHP 和 JSON
幸运的是,PHP 数组非常类似于 JSON 对象,PHP 有内置的函数来编码和解码 JSON。这些功能分别是json_encode和json_decode,。
注意一种不能被编码成 JSON 的 PHP 数据类型是资源,就像数据库或文件句柄。与 PHP 不同,您不能在 JSON 中指定整数和浮点数之间的区别。两者都表示为相同的数字类型。
json_encode和json_decode都只处理UTF-8编码的数据。json_decode、$assoc,的第二个可选参数取布尔值,默认为FALSE。当$assoc设置为TRUE时,JSON 对象被解码成关联数组。在对json_decode进行故障排除时,重要的是要知道*“如果 json 不能被解码或者如果编码的数据比递归限制更深,则返回 NULL。”*这是根据在[www.php.net/manual/en/function.json-decode.php](http://www.php.net/manual/en/function.json-decode.php)找到的手册
第三个 PHP 函数json_last_error,返回一个表示错误代码的整数值。返回的错误代码是以下之一:
JSON_ERROR_NONE No error has occurred JSON_ERROR_DEPTH The maximum stack depth has been exceeded JSON_ERROR_CTRL_CHAR Control character error, possibly incorrectly encoded JSON_ERROR_STATE_MISMATCH Invalid or malformed JSON JSON_ERROR_SYNTAX Syntax error JSON_ERROR_UTF8 Malformed UTF-8 characters, possibly incorrectly encoded
清单 15-1 是一个将 PHP 数据类型的代表编码成 JSON 表示,然后再转换回 PHP 数据类型的例子。
***清单 15-1。*将 PHP 数据类型编码成 JSON 并解码回 PHP 数据类型
`<?php //we will leave out the PHP resource type $php_data_types = array(4.1, 3, NULL, true, false, "hello", new StdClass(), array());
php_data_types); json); ?>
JSON Representation:
PHP Representation:
`
运行清单 15-1 会产生以下输出:
`JSON Representation string(37) "[4.1,3,null,true,false,"hello",{},[]]"
PHP Representation: array(8) { [0]=> float(4.1) [1]=> int(3) [2]=> NULL [3]=> bool(true) [4]=> bool(false) [5]=> string(5) "hello" [6]=> object(stdClass)#2 (0) { } [7]=> array(0) { } }`
清单 15-2 将 PHP 嵌套的书籍数组编码成 JSON,然后将 JSON 解码回 PHP。正如您将看到的,JSON 将编码表示为一个对象数组。
***清单 15-2。*一个 PHP 嵌套数组首先被编码成 JSON,然后解码回 PHP
`<?php
$books = array( array("author" => "Lewis Carroll", "title" => "Alice's Adventures in Wonderland", "year" => 1865), array("author" => "Yann Martel", "title" => "Life of Pi", "year" => 2001), array("author" =>"Junot Diaz", "title" => "The Brief Wondrous Life of Oscar Wao", "year" => 2007), array("author" => "Joseph Heller", "title" => "Catch-22", "year" => 1961), array("author" => "Timothy Findley", "title" => "Pilgrim", "year" => 1999), array("author" => "Fyodor Dostoyevsky", "title" => "Brothers Karamazov", "year" => 1880), );
books); json_books); ?>
`
清单 15-2 首先输出 PHP 嵌套数组的 JSON 表示,它是对象数组的形式。实际输出是一个连续的字符串。添加了换行符以提高可读性:
string(415) "[ {"author":"Lewis Carroll","title":"Alice's Adventures in Wonderland","year":1865}, {"author":"Yann Martel","title":"Life of Pi","year":2001}, {"author":"Junot Diaz","title":"The Brief Wondrous Life of Oscar Wao","year":2007}, {"author":"Joseph Heller ","title":"Catch-22","year":1961}, {"author":"Timothy Findley","title":"Pilgrim","year":1999}, {"author":"Fyodor Dostoyevsky","title":"Brothers Karamazov","year":1880} ]"
清单 15-2 然后输出 PHP 编码,再次表示为一个对象数组。
array(6) { [0]=> object(stdClass)#1 (3) { ["author"]=> string(13) "Lewis Carroll" ["title"]=> string(32) "Alice's Adventures in Wonderland" ["year"]=> int(1865) } [1]=> object(stdClass)#2 (3) { ["author"]=> string(11) "Yann Martel" ["title"]=> string(10) "Life of Pi" ["year"]=> int(2001) } [2]=> object(stdClass)#3 (3) { ["author"]=> string(10) "Junot Diaz" ["title"]=> string(36) "The Brief Wondrous Life of Oscar Wao" ["year"]=> int(2007) } [3]=> object(stdClass)#4 (3) { ["author"]=> string(14) "Joseph Heller " ["title"]=> string(8) "Catch-22" ["year"]=> int(1961) } [4]=> object(stdClass)#5 (3) { ["author"]=> string(15) "Timothy Findley" ["title"]=> string(7) "Pilgrim" ["year"]=> int(1999) }
[5]=> object(stdClass)#6 (3) { ["author"]=> string(18) "Fyodor Dostoyevsky" ["title"]=> string(18) "Brothers Karamazov" ["year"]=> int(1880) } }
值得注意的是,JSON 忽略了单个 book 数组的数字键。但是,一旦我们将一个键设置为关联键,所有的键,包括数字键,都存储在 JSON 对象中。修改清单 15-2 中的开头
$books = array( array("author" => "Lewis Carroll", "title" => "Alice's Adventures in Wonderland", "year" => 1865),
到
$books = array( **"sample_book"** => array("author" => "Lewis Carroll", "title" => "Alice's Adventures in Wonderland", "year" => 1865),
因此它包含一个关联键,将产生一个编码的 JSON 和解码的 PHP 表示中的对象的对象:
`string(449) "{ "sample_book": {"author":"Lewis Carroll","title":"Alice's Adventures in Wonderland","year":1865}, "0":{"author":"Yann Martel","title":"Life of Pi","year":2001}, "1":{"author":"Junot Diaz","title":"The Brief Wondrous Life of Oscar Wao","year":2007}, "2":{"author":"Joseph Heller ","title":"Catch-22","year":1961}, "3":{"author":"Timothy Findley","title":"Pilgrim","year":1999}, "4":{"author":"Fyodor Dostoyevsky","title":"Brothers Karamazov","year":1880} }"
object(stdClass)#1 (6) { ["sample_book"]=> object(stdClass)#2 (3) { ["author"]=> string(13) "Lewis Carroll" ["title"]=> string(32) "Alice's Adventures in Wonderland" ["year"]=> int(1865) }`
["0"]=> object(stdClass)#3 (3) { ["author"]=> string(11) "Yann Martel" ["title"]=> string(10) "Life of Pi" ["year"]=> int(2001) } ["1"]=> object(stdClass)#4 (3) { ["author"]=> string(10) "Junot Diaz" ["title"]=> string(36) "The Brief Wondrous Life of Oscar Wao" ["year"]=> int(2007) } ["2"]=> object(stdClass)#5 (3) { ["author"]=> string(14) "Joseph Heller " ["title"]=> string(8) "Catch-22" ["year"]=> int(1961) } ["3"]=> object(stdClass)#6 (3) { ["author"]=> string(15) "Timothy Findley" ["title"]=> string(7) "Pilgrim" ["year"]=> int(1999) } ["4"]=> object(stdClass)#7 (3) { ["author"]=> string(18) "Fyodor Dostoyevsky" ["title"]=> string(18) "Brothers Karamazov" ["year"]=> int(1880) } }
Ajax
Ajax 允许部分重新加载和操作呈现的内容,而不需要重新加载整个页面。Ajax 调用可以是同步的,但通常是异步的后台调用。这是为了在不干扰主程序流程的情况下发送和检索数据。如前所述,Ajax 不是一种单一的技术,而是协同工作的几个部分。
Ajax 的一些缺点是:
- 浏览器的后退按钮和书签不会跟踪 Ajax 的状态。
- 搜索引擎很难索引动态生成的内容。
- 非 JavaScript 用户需要适度降级,这需要额外的工作。
- 屏幕阅读器的可访问性问题。
然而,Ajax 的响应性和动态性通常会超过它的负面影响。像 Gmail、Google Docs 和脸书这样的应用展示了 Ajax 的能力。
传统的网络模式
在经典 web 模型的简化视图中(见图 15-1 ,客户端浏览器向 web 服务器发送 HTTP 请求并接收响应。任何时候,如果浏览器希望更新显示,即使是单个<div>元素或<img />元素发生了变化,或者验证输入,都需要向服务器发出完整的请求。对于每个请求,浏览器都在等待来自服务器的反馈。
***图 15-1。*传统网络模式
20 年前,当网络首次被广泛使用时,等待 30 秒或更长时间来提交表格是可以接受的。互联网连接要慢得多,网络仍然是一项令人惊叹的新技术,比亲自寄信或提交纸质表格要快得多。随着人类已经习惯于更快的连接和及时的反馈,对缓慢响应时间的容忍度已经稳步下降。需要一种不中断用户体验的方式与服务器通信。
Ajax 网络模型
在 Ajax web 模型中(如图 15-2 和图 15-3 所示),有一个中介——Ajax 引擎,它位于客户端和服务器之间。有了这个模型,客户端现在可以将其事件发送给 Ajax 引擎。根据事件的类型,Ajax 引擎要么操纵客户机的表示层(HTML 和 CSS ),要么向服务器发送异步事件。在后一种情况下,服务器响应 Ajax 引擎,Ajax 引擎反过来更新客户端。不需要直接的客户端到服务器的请求允许通信,而不需要中断用户思路的全页面刷新。
使用 Ajax web 模型,更新显示和表单验证等事件可以在不联系服务器的情况下发生。当我们需要保存和加载数据时,就会联系服务器。
图 15-2。 Ajax web 模型——一个简单的事件,只有客户端和 Ajax 引擎交互
你可以在图 15-2 中看到,一些浏览器客户端事件,比如显示变化,并不需要从服务器请求或接收数据。
图 15-3。 Ajax web 模型——需要客户端、Ajax 引擎和服务器交互的更复杂事件
其他事件确实需要 HTTP 请求和对服务器的响应,如图图 15-3 所示。
异步与同步事件
假设我们有三个 HTTP 请求事件:A、B 和 C。在同步模式下,我们需要等到收到事件 A 的服务器响应后才能发送请求 B。然后,我们必须等到收到事件 B 的响应后才能发送请求 C。事件是连续的,因此事件 A 会阻塞事件 B 和事件 C,直到它完成。类似地,下一个事件 B 阻塞事件 C,直到它完成。参见图 15-4 。
***图 15-4。*顺序同步 HTTP 事件
对于异步事件,请求从不等待。它们是单独并行执行的。即使 HTTP 事件 A 仍在等待服务器响应,新事件 B 和 C 也可以立即开始它们的 HTTP 请求。通过比较图 15-4 和图 15-5 可以看出,异步事件加快了整体事件处理时间。
***图 15-5。*并行、异步 HTTP 事件
XMLHttpRequest 对象
对象,通常缩写为 XHR,由微软在 2000 年创建。它是一个 API,通常用 JavaScript 实现,支持从客户端向服务器发送请求并接收响应,而无需重新加载页面。物体的名称不能照字面理解。其组成部分只是象征性的,例如:
- XML :实际上可以是 XML,JSON,HTML,或者纯文本文档。
- Http :可能是 Http 或者 HTTPS。
- 请求:请求或响应。
有些浏览器不支持XMLHttpRequest对象,但支持XDomainRequest对象或window.createRequest()方法。在本章中,我们不会担心支持过时的或非标准的浏览器。
创建一个新的XMLHttpRequest对象需要一行代码,如清单 15-3 中的所示。
***清单 15-3。*在 JavaScript 中创建an XMLHttpRequest对象
<script type="text/javascript"> var xhr = new XMLHttpRequest(); </script>
为了设置请求的参数,我们使用了open()函数。该函数采用以下参数:
- 请求方式:
{"GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS"}之一。 - URL:请求 URL。可以是 PHP、JavaScript、HTML、纯文本或其他文件类型。
- 异步(可选):默认值为 true,表示非阻塞调用。
- Username(可选):如果在请求服务器上使用身份验证,则为 Username。
- Password(可选):在请求服务器上使用身份验证时的密码。
异步调用有一个监听器回调函数onreadystatechange,它允许主脚本继续运行。同步调用没有侦听器,因此需要阻塞主脚本,直到收到响应。如果我们发送一个异步调用,那么onreadystatechange回调将设置请求对象的readyState属性。
为了设置对象属性,但不发送请求,我们将使用如下代码:
<script type="text/javascript"> var xhr = new XMLHttpRequest(); xhr.open("GET", "animals.xml"); </script>
默认情况下,与请求一起发出的头是"application/xml;charset=*_charset*",其中 _charset 是实际使用的编码,例如UTF-8.,如果我们需要覆盖这些值,我们将使用函数setRequestHeader(String header_name, String header_value)。如果使用代理,请求对象将自动设置和发送代理授权头。
在发送请求之前,我们需要定义回调函数。我们将使用一个匿名(未命名)函数,并检查 readyState 是否为 4,这表示请求已完成:
xhr.onreadystatechange=function(){ if (xhr.readyState == 4){ //ready state 4 is completion if (xhr.status==200){ //success
可能的就绪状态有:
0 未初始化-尚未调用 open()。
1 尚未调用 Loading - send()。
2 Loaded - send()已被调用,标题和状态可用。
3 交互式下载,responseText 保存部分数据。
4 完成-完成所有操作。
不同浏览器的状态 0-3 不一致。我们主要对状态 4 感兴趣。
既然我们已经初始化了请求对象并定义了回调,我们就可以发出请求了:
xhr.send("our content");
发送一个带有XMLHttpRequest的 Ajax 请求看起来像清单 15-4 中的。
***清单 15-4。*一个基本XMLHttpRequest
<script type="text/javascript"> var xhr = new XMLHttpRequest(); xhr.open("GET", "animals.xml"); xhr.onreadystatechange=function(){ if (xhr.readyState == 4){ //ready state 4 is completion if (xhr.status==200){ alert("success"); } else{ alert("error"); } } } xhr.send("our content"); </script>
使用 XMLHttpRequest
在我们使用XMLHttpRequest对象的第一个例子(清单 15-5 )中,我们将替换<p>标签的内容。
清单 15-5*。**用XMLHttpRequest、listing_15_5.html、*修改页面元素
`
Original content
//assign the request attributes xhr.open("GET", window.location.pathname, true);
//define the callback function xhr.onreadystatechange=function(){ if (xhr.readyState == 4){ //ready state 4 is completion var message = ""; if (xhr.status==200){ //success message = "Ajax loaded content"; } else{ //error message = "An error has occured making the request"; } document.getElementsByTagName("p")[0].innerHTML = message; } }
//send the actual request xhr.send(null);
`我们在清单 15-5 中的open()方法中使用的 URL 是当前页面,可以在 JavaScript 变量window.location.pathname中访问。我们在 Ajax 调用xhr.send(null)中没有发送任何数据。
JavaScript 放在我们操作的 HTML 元素之后。这是因为我们需要加载所有的 DOM 树,以便 JavaScript 能够找到并操作其中的元素。像 jQuery 这样的高级框架有测试文档是否准备好的功能,通过这样做,可以将 JavaScript 放在页面的任何地方。
根据你的计算机的响应时间,你可能会看到元素从初始值"Original content"变为"Ajax loaded content"
在页面加载后,清单 15-6 将获取一个外部 XML 文件的纯文本内容,并将其放入我们的文档中。对于纯文本,我们的意思是只检索 XML 元素值。元素名称和属性被丢弃。
***清单 15-6。*用一个XMLHttpRequest抓取一个 XML 文件的内容,并将其显示为纯文本
` XHR Example #generated_content{ border: 1px solid black; width: 300px; background-color: #dddddd; }
Ajax grabbed plain text:
//assign the request attributes xhr.open("GET", "animals.xml", true);
//define the callback function xhr.onreadystatechange=function(){ if (xhr.readyState == 4){ //ready state 4 is completion var message = ""; if (xhr.status==200){ //success //retrieve result as plain text message = "
" + xhr.responseText + ""; } else{ //error message = "An error has occured making the request"; } document.getElementById("generated_content").innerHTML = message; } }
//send the actual request xhr.send(null);
`***清单 15-7。*所包含的 XML 文件,animals.xml
<?xml version="1.0" encoding="UTF-8" ?> <animals> <dogs> <dog> <name>snoopy</name> brown <breed>beagle cross</breed> </dog> <dog> <name>jade</name> black <breed>lab cross</breed> </dog> </dogs> <cats> <cat> <name>teddy</name> brown <breed>tabby</breed> </cat> </cats> </animals>
清单 15-6 的输出如图图 15-6 所示
***图 15-6。*运行清单 15-6 的输出,它使用 Ajax 读取 XML 文件的纯文本
在清单 15-6 中需要注意的关键一行是,我们为我们的成功响应指定了纯文本输出:
if (xhr.status==200){ //success //retrieve result as plain text **message = "<pre>" + xhr.responseText + "</pre>";** }
并将其作为 id 等于generated_content的<div>的innerHTML:
document.getElementById("generated_content").innerHTML = message;
为了只获取动物名称(清单 15-8 ),我们检索 XML 格式的输出,并解析所有的名称元素值。
***清单 15-8。*用XMLHttpRequest抓取 XML 并解析特定值
` XHR Example - XML #generated_content{ border: 1px solid black; width: 300px; background-color: #dddddd; padding: 20px; }
Ajax grabbed specific XML below:
//assign the request attributes xhr.open("GET", "animals.xml", true);
//define the callback function
xhr.onreadystatechange=function(){
if (xhr.readyState == 4){ //ready state 4 is completion
var message = "";
if (xhr.status==200){ //success
var xml_data = xhr.responseXML
//retrieve result as an XML object
var names = xml_data.getElementsByTagName("name");
for(i=0; i<names.length; ++i){
message += names[i].firstChild.nodeValue + "
\n";
//ex) "Snoopy\n"
}
}
else{ //error
message = "An error has occured making the request";
}
document.getElementById("generated_content").innerHTML = message;
}
}
//send the actual request xhr.send(null);
`我们在清单 15-8 中使用 JavaScript 来获取使用xhr.responseXML的 Ajax 调用返回的 XML 数据,并解析它以获取<name>元素值。输出如图图 15-7 所示。
***图 15-7。*运行清单 15-8 的输出,它使用 Ajax 解析 XML 数据
如果我们请求一个用 HTML 编写的文件,那么使用responseText保留 HTML 结构,如清单 15-9 所示。输出如图 15-8 中的所示
***清单 15-9。*用XMLHttpRequest 抓取 HTML
` XHR Example - Plain Text Containing HTML #generated_content{ border: 1px solid black; width: 300px; background-color: #dddddd; }
Ajax grabbed plain text containing html:
//assign the request attributes xhr.open("GET", "sample_table.html", true);
//define the callback function xhr.onreadystatechange=function(){ if (xhr.readyState == 4){ //ready state 4 is completion var message = ""; if (xhr.status==200){ //success message = xhr.responseText //retrieve result as plain text } else{ //error message = "An error has occured making the request"; } document.getElementById("generated_content").innerHTML = message; } }`
` //send the actual request xhr.send(null);
`其中sample_table.html包含
`
| foo | bar |
|---|---|
| a | 1 |
| b | 2 |
| c | 3 |
***图 15-8。*运行清单 15-9 的输出,它使用 Ajax 包含 HTML
高级 JavaScript APIs
jQuery、Prototype 和 YUI 之类的高级 JavaScript APIs 之所以广受欢迎,部分原因是它们抽象出了细节,使得使用复杂对象(如XMLHttpRequest)变得更加容易。这意味着库的用户不需要直接知道XMLHttpRequest对象的内部工作原理。然而,对XMLHttpRequest对象的理解有助于理解“引擎盖下”发生了什么这些库的其他优点是它们使得跨浏览器支持和 DOM 操作更加容易。
有几个库可供选择。Danchilla 是 jQuery 的倡导者,jQuery 是目前最流行的 JavaScript 库。它被 Google、Amazon、Twitter、Microsoft Visual Studio、IBM、Drupal CMS(内容管理系统)以及许多其他网站和框架使用:如果您不喜欢它,请参见[docs.jquery.com/Sites_Using_jQuery](http://docs.jquery.com/Sites_Using_jQuery).其他选择包括 Dojo、YUI、Prototype、MooTools 和 script.aculo.us。然而,丹希拉会解释我们使用的任何功能。
jQuery 示例
清单 15-10 是清单 15-5 的的 jQuery 等价物,它在页面加载后替换了一个<p>元素的内容。
***清单 15-10。*用 jQuery 加载页面后修改<p>元素
` First jQuery Example <script type="text/javascript" src="ajax.googleapis.com/ajax/libs/j…" > .ajax( { type: "get", url: window.location.pathname, dataType: "text", success: function(data) { ("p").html("Ajax loaded content");** **},** **failure: function(){** **("p").html("An error has occurred making the request"); } }); });
Original content
`在清单 15-10 中,该行
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" > </script>
从 Google CDN(内容交付网络)加载 jQuery 库。或者,您可以提供一个本地下载的库副本。在生产环境中,cdn 通常更快、更可靠。大多数浏览器对可以从一个域中同时下载的文件数量有限制。使用外部 CDN 会从网页的加载队列中删除一个文件。结果是更高的吞吐量和更快的页面加载。注意文件名是jquery.min.js。这是该库的打包、混淆版本。文件较小,您可能希望在生产中使用此版本。在开发中,您可能会调试您的输出,最好包括人类可读的版本,jquery.js。
$(document).ready函数调用是 jQuery 脚本的标准。$(document)代表完整的 DOM 文档,稍后在脚本中简称为$()。一旦 DOM 文档完全加载完毕,对.ready的调用就会执行脚本。这允许我们将脚本放在我们在 HTML 文档中操作的元素之前。
Ajax 参数在一个函数调用$.ajax()中初始化和设置。该函数将请求类型——GET 或 POST、URL 和响应数据类型作为参数。它还定义了成功和失败的回调。
最后我们原剧本的document.getElementsByTagName("p")[0].innerHTML行换成了$("p").html("*some data*")。该行的第一部分通过使用 CSS 选择器找到相关的<p>元素。第二部分设置元素数据。
注意从技术上讲,
$("p")匹配文档中所有的<p>标签。如果我们想显式匹配第一次出现的内容,如清单 15-5 中的,我们可以链接内置函数$("p").first()。或者,我们可以使用 CSS 选择器,如$("p:first")或$("p:eq(0)")。
我们脚本的这个 jQuery 版本比使用XMLHttpRequest对象的原始版本要短。随着我们的脚本变得更加复杂,像 jQuery 这样的高级 API 的价值变得更加明显。
清单 15-11 是清单 15-6 中的 jQuery 等价物,它从 XML 文件中加载纯文本。
***清单 15-11。*使用 jQuery 从 XML 文件加载纯文本
<html> <head> <title>Loading Plain Text with jQuery</title> <style type="text/css"> #generated_content{ border: 1px solid black; width: 300px; background-color: #dddddd; } </style> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" > </script> <script type="text/javascript"> $(document).ready(function() { $.ajax( { type: "get", **url: "animals.xml",** **dataType: "text",** success: function(data) { $("#generated_content").html("<pre>" + data + "</pre>"); }, failure: function(){ $("#generated_content").html( "An error has occured making the request"); } } ); }); </script>
`
Ajax grabbed plain text:
如果我们不关心可能的错误值,我们可以将清单 15-11 中的脚本重写为清单 15-12 中所示的更简洁的代码..
***清单 15-12。*使用 jQuery 加载文件的更简洁版本。load()函数
` Loading Plain Text with jQuery #generated_content{ border: 1px solid black; width: 300px; background-color: #dddddd; } <script type="text/javascript" src="ajax.googleapis.com/ajax/libs/j…" > ("#generated_content").load("animals.xml");** $("#generated_content").wrap("
"); });Ajax grabbed plain text:
`清单 15-12 中的 jQuery
load函数执行一个 GET 请求,并使用“智能猜测”返回纯文本。然后,它将文本插入到选定的元素中。jQuerywrap函数在元素内容周围放置标记。这允许我们将加载的数据包含在上面的<pre>..</pre>标签中。除了
$.ajax函数,jQuery 还有用于 GET 和 POST 请求的$.get和$.post函数。通过这些函数,jQuery 试图猜测期望的输出。如果猜测错误,我们可以显式指定返回类型。有关更深入的内容,请参考位于[api.jquery.com/jQuery.get/](http://api.jquery.com/jQuery.get/)的 jQuery 文档。参见清单 15-13 。***清单 15-13。*使用
jQuery $.get并请求 XML 数据类型。` Loading XML with jQuery #generated_content{ border: 1px solid black; width: 300px; background-color: #dddddd; } <script type="text/javascript" src="ajax.googleapis.com/ajax/libs/j…" > .get("animals.xml" , function(data){ var message = ""; var names = data.getElementsByTagName("name"); for(i=0; i < names.length; ++i){ message += names[i].firstChild.nodeValue + "
\n"; } $("#generated_ content").html(message); }, "xml"); });Ajax parsed XML:
`
在清单 15-13 中,
$.get函数有三个参数。第一个是请求文件,第二个是函数回调,在这里我们操作响应数据,第三个是预期的数据类型。如果没有指定"xml",jQuery 会选择纯文本。到目前为止,我们已经展示了如何使用
XMLHttpRequest对象,以及像 jQuery 这样的高级 API 包装器如何隐藏一些细节,让生活变得更简单。现在让我们尝试一个 JSON 例子(见清单 15-14 )***清单 15-14。*从 PHP 数组输出 JSON 数据,
json_example.php`<?php
$animals = array( "africa" => array("gorilla", "giraffe", "elephant"), "asia" => array("panda"), "north america" => array("grizzly bear", "lynx", "orca"), );
print json_encode($animals); ?>`
清单 15-15 使用 jQuery 通过 Ajax 请求从 PHP 文件中获取 JSON 值(清单 15-14 )。
***清单 15-15。*使用
$.getJSON和$.each` Loading JSON with jQuery <script type="text/javascript" src="ajax.googleapis.com/ajax/libs/j…" > .getJSON("json_example.php" , function(data){ .each(data, function(continent, animals){ var message = "<strong>" + continent + "</strong><br/>"; for(j=0;j<animals.length;++j){ message += animals[j] + ", "; } //remove last comma and space message = message.trim(); message = message.substring(0, message.length - 1); ("#generated_content").append("
" + message + "
"); }); }); });Ajax parsed JSON:
`清单 15-15 的输出是:
`Ajax parsed JSON:
africa: gorilla, giraffe, elephant
asia: panda
north america: grizzly bear, lynx, orca`
在清单 15-15 中,我们使用了
$.getJSON速记函数。我们也可以使用$.get和json作为第三个参数。我们还使用 jQuery 函数$.each遍历返回的 JSON 对象。为了分配continent和animals的键/值数据变量名,我们将回调函数定义为:
$.each(data, function(continent, animals){)通过 Ajax 向 PHP 脚本发送数据
在这个例子中(清单 15-16 ,我们将在浏览器页面上有两个按钮,标记为“捕食者”和“猎物”。当按下任何一个按钮时,一个 Ajax 请求被发送到一个 PHP 脚本,带有查询参数
?type=predator或?type=prey。当 PHP 脚本收到请求时,它使用查询值选择并返回一个适当的编码为 JSON 的动物条目。清单 15-16。 PHP 文件,选择一个捕食者或猎物,并以 JSON 格式输出,
predator_prey.php`<?php error_reporting(E_ALL); prey = array( "salmon", "seal", "gazelle", "rabbit", "cow", "moose", "elk", "turkey" );
if (isset(_GET['type'])) { switch (_GET['type']) { case "predator": print json_encode(predators)]); break; case "prey": print json_encode(prey)]); break;`
default: print json_encode("n/a"); break; } } ?>在清单 15-17 的中,我们通过发送一个 Ajax
.load请求来处理两个按钮的.click事件。predator_prey.php文件接收这个请求,以及一个type参数,并发回一个字符串响应,我们将它加载到我们的文档中。我们已经使用array_rand为我们选择的数组生成了一个随机索引,然后使用json_encode将其以 JSON 格式输出。***清单 15-17。*加载 Ajax 请求响应的 HTML 文件
` Predator/Prey Example <script type="text/javascript" src="ajax.googleapis.com/ajax/libs/j…" > ("#predator").click(function(){ $("#response").load("predator_prey.php?type=predator"); });
("#prey").click(function(){ ("#response").load("predator_prey.php?type=prey"); }); }); Predator Prey
Ajax response from PHP:
`清单 15-17 的输出如图图 15-9 所示。
***图 15-9。**清单 15-17 的样本输出清单 *
简单的图形程序
在清单 15-18 中,我们将使用调色板、HTML 表格单元格网格和 jQuery 构建一个简单的绘图应用。一旦成功,我们将添加使用 PHP 和 Ajax 保存和加载图像的功能。
***清单 15-18。*图形应用操纵表格单元格的背景颜色
` Drawing Grid Example #grid, #palette{ padding: 0px; margin: 0px; border-collapse: collapse; }
#palette td, #grid td{ width: 20px; height: 20px; }
#grid td{ border: 1px solid #cccccc; } <script type="text/javascript" src="ajax.googleapis.com/ajax/libs/j…" > ("#grid").append( "" + " " + " " + " " + " " + " " +
" " + " " + " " + " " + " " + "" ); }`
` var active_color = "rgb(0, 0, 0)"; ("#palette td").each( function( index ){ //bind the onClick event ( this ).bind ( "click", function(){ active_color = ("#debug_palette_color").html("active palette color is: " + "" + active_color + ""); }
); });
("#grid td").each( function( index ){ //bind the onClick event ( this ).bind ( "click", function(){ $(this).css("background-color", active_color); } );
}); });
Palette
Draw!
Debug console:
`在清单 15-18 的的 CSS 中,我们为表格网格设置了margin-collapse: collapse,这样内部边界和边缘边界的厚度相同。我们创建了一个调色板供选择。即使我们指定了尺寸, (不间断空格)字符有助于确保浏览器绘制单元格边框。没有 DOM 操作,我们的网格是空的。在我们的 jQuery .ready函数中,我们使用一个循环和 jQuery append函数向网格添加 10 个表格行,每个行有 10 个单元格。
接下来,我们用以下内容定义调色板单元格的单击动作:
$("#palette td").each( function( index ){ //bind the onClick event $( this ).bind ( "click", function(){
在函数细节中,我们更改了active_color,并在我们的调试区域显示了它的内容:
function(){ **active_color = $(this).css("background-color");** $("#debug_palette_color").html("active palette color is: " + "<span style='width: 20px; height: 20px; background-color:" + active_color + ";'>" + active_color + "</span>"); }
我们将网格单元绑定到一个点击事件,这样在任何点击之后,background-color都会变成我们的active_color:
$("#grid td").each( function( index ){ //bind the onClick event $( this ).bind ( "click", function(){ **$(this).css("background-color", active_color);** } );
输出如图 15-10 所示。我们的计划成功了。然而,我们不能挽救我们的形象。所以当我们从页面浏览出去再回来的时候,我们总会有一个空白的画布。我们接下来将讨论这个问题。
注意 jQuery 背景颜色是更新的
rgb(255, 0, 0)形式,而不是像#ff0000.这样的十六进制值。新的颜色格式是 CSS 3 规范的一部分,它也包括一个 alpha 版本 rgba。Alpha 值允许简单的不透明度设置,并且将很快全面支持跨浏览器。
***图 15-10。*我们绘制的网格来自清单 15-18
维持状态
为了保存 Ajax 的修改,我们可以使用 PHP,将数据写入数据库、$_SESSION或文件中。当我们以后重新加载页面时,我们可以从保存的数据中填充图像网格。对于我们的例子,我们将使用一个物理文件。您可以扩展该示例以保存每个唯一会话或用户名的数据,但我们将只存储一组结果。
我们不希望在每次像素改变后都保存结果,因为那样会非常慢并且耗费资源。相反,我们将添加一个保存按钮,以便用户可以显式地请求保存。您还可以跟踪每次保存之间的更改次数。然后,我们可以在大约每 100 次更改后进行一次后台自动保存。这有助于保护用户数据,而无需中断用户备份工作。
我们还可以添加一个“Clear”按钮,将网格重置为透明的、未修改的状态,并截断保存的数据文件。参见清单 15-19 。
***清单 15-19。*显示绘图网格并对我们的 PHP 脚本进行 Ajax 调用的 HTML】
` Drawing Grid Example #grid, #palette{ padding: 0px; margin: 0px; border-collapse: collapse; }
#palette td, #grid td{ width: 20px; height: 20px; }
#grid td{ border: 1px solid #cccccc; } <script type="text/javascript" src="ajax.googleapis.com/ajax/libs/j…" > ("#grid").append( "" + " " + " " + " " + " " + " " +
" " + " " + " " + " " + " " + "" ); }
.getJSON("load_drawing.php", function(data){** **("#grid td").each(function(index){ $(this).css("background-color", data[index]); }); });`
` var active_color = "rgb(0, 0, 0)"; ("#palette td").each( function( index ){ //bind the onClick event ( this ).bind ( "click", function(){ active_color = ("#debug_palette_color").html("active palette color is: " + "" + active_color + ""); }
); });
("#grid td").each( function( index ){ //bind the onClick event ( this ).bind ( "click", function(){ $(this).css("background-color", active_color); } );
});
("#clear").click(function(){** **("#grid td").css("background-color", "transparent"); });
("#save").click(function(){** **var colorsAsJson = new Object();** **var i=0;** **("#grid td").each(function() { colorsAsJson[i] = $(this).css("background-color"); ++i; });
.ajax(** **{** **type: "post",** **url: "save_drawing.php",** **dataType: "text",** **data: colorsAsJson,** **success: function(data) {** **("#debug_message").html("saved image"); },`
` failure: function(){ $("#debug_message").html( "An error has occured trying to save the image"); } }); }); });
Palette
Save
Draw!
Debug console:
..`***清单 15-20。*将传入的$_POST变量数据保存为 JSON 格式的 PHP 脚本,save_drawing.php
<?php error_reporting(E_ALL); file_put_contents("image.x", json_encode($_POST)); ?>
***清单 15-21。*PHP 脚本加载保存的文件数据,load_drawing.php
<?php $filename = "image.x"; if (file_exists($filename)) { print file_get_contents($filename); } ?>
我们新的 jQuery 现在有一个在点击“保存”按钮时保存数据的功能。为此,创建了一个新的 JavaScript 对象。然后将每个单元格的 CSS 背景色属性添加到对象中。一旦完成,我们向我们的save_drawing.php文件发送一个 Ajax POST 请求。我们需要使它成为 POST 请求,因为我们发送的数据太长,不能包含在 GET 查询字符串中。在 PHP 脚本中,我们将$_POST值编码成 JSON 并保存在一个文件中。参见清单 15-22 。
***清单 15-22。*我们整个程序的保存功能(清单 15-19 )
` ("#save").click(function(){ var colorsAsJson = new Object(); var i=0; ("#grid td").each(function() { colorsAsJson[i] = $(this).css("background-color"); ++i; });
.ajax( { type: "post", url: "save_drawing.php", dataType: "text", data: colorsAsJson, success: function(data) { ("#debug_message").html("saved image"); }, failure: function(){ $("#debug_message").html( "An error has occured trying to save the image"); } }); });`
现在我们已经保存了图像数据,我们可以在再次访问页面时加载数据。为此,我们向load_colors.php发送一个$.getJSON请求。这将返回我们保存的 JSON 格式文件的内容。在我们的 jQuery 中,我们遍历网格的每个单元格,并分配相应的背景颜色。参见清单 15-23 。输出如图图 15-11 所示。
***清单 15-23。*我们完整程序的加载功能(清单 15-19 )
$.getJSON("load_drawing.php", function(data){ $("#grid td").each(function(index){ $(this).css("background-color", data[index]); }); });
***图 15-11。*我们的 Ajax 绘图程序,带有初始加载的数据、保存按钮和 Firebug 输出
使用 Ajax 时,使用开发人员工具进行调试很有帮助。Firefox 扩展 Firebug 是目前最好的工具之一。在 Firebug 中,Ajax 数据可以在Net > XHR部分找到。Chrome 开发者工具也非常有用。
总结
在这一章中,我们已经解释了异步 web 请求是如何实现更丰富、更灵敏、更有趣的网站的。这是通过在客户机和服务器端点之间注入一个中介 Ajax 引擎来实现的。这导致更新浏览器显示的服务器请求减少,并且当我们在服务器和客户机之间传输数据时,调用不会阻塞。
用来发送 Ajax 请求的最流行的脚本语言是 JavaScript。与直接处理XMLHttpRequest对象相比,使用 jQuery 这样的高级 API 可以使使用 Ajax 变得更加容易和愉快。
可以用 Ajax 交流的数据格式包括 XML(在第十四章中讨论过的)、JSON(我们在本章中讨论过的)、HTML 和纯文本。
Ajax 在现代 web 开发中的使用是一把双刃剑。一方面,Ajax 实现了传统 web 模型无法实现的响应性和幕后数据传输。另一方面,用户期待丰富的浏览体验。因此,为了满足这些期望,web 应用需要做大量的工作。为了用 Ajax 创造良好的用户体验(UX ),开发人员需要精通几种技术,其中最重要的是 JavaScript、DOM 选择器、JSON 和 XML。
最后,Ajax 是一个新兴领域,其他技术如反向 Ajax 也在探索中。反向 Ajax 涉及长期 HTTP 连接和服务器将数据推送到客户端。未来有望让 Ajax 成为 web 开发的核心。
十六、结论
我们希望你喜欢阅读这本书,并且很好地利用了每一章的资源和代码。我们尽了一切努力使这本书对你有很大的价值,我们希望它是你的高级 PHP“入门”书籍;把它放在你电脑旁边的桌子上,而不是和你库里的其他编程书籍一起放在书架上。
当然,我们都明白,IT 行业以光速前进,这本书的许多内容在几个月内就会过时。所以我们增加了这一章,以帮助你找到还没有被问过的网络开发问题的答案。这是我们所知道的一些最好的互联网资源的总结,并且完全赞同作为你在 web 开发和 PHP 编程方面继续教育的额外材料。
资源
首先,我们想向您介绍大量的网络资源。本节总结了您可以在这些网站上找到的内容。
www.php.net
在这里您可以找到最新版本的 PHP 供下载。此外,您还会发现完整的在线参考资料,其中包含大量示例代码以及用户/读者添加的注释和说明。在线文档也可以很容易地搜索到,如果你没有找到正确的措辞,一些合理的替代建议。除了基本的有价值的材料之外,您将会发现一个即将到来的 PHP 事件的列表,比如会议和用户组会议,当然还有 PHP 开发人员认为可能会增加价值的最新相关链接。参见图 16-1 。
注意当使用 php.net 进行代码帮助时,可以通过在 URL 的末尾添加函数名来尝试直接 URL 查找技术,如下所示:
php.net/date
例如,如果您想查看 PHP 日期函数的文档,您可以这样做。
***图 16-1。*www.php.net
www.zend.com
下一个你最有可能花费大量时间的网站是 Zend 公司的网站。这就是自称的“PHP 公司”。在这里,您将能够看到 Zend 为帮助 PHP 开发和 PHP 服务器交付而开发的许多附加产品。如果你查看它的完整产品列表,你还会看到该公司在很大程度上参与了 PHP 本身的开发。除了所有的商业产品之外,他们全年还会举办许多有价值的网络研讨会。参见图 16-2 。
图 16-2。【www.zend.com 号
devzone.zend.com
这是 Zend 主页的姐妹网站。在这里你会发现一个伟大的开发者社区,他们都希望在 PHP 开发和 Zend 产品线的使用上互相帮助。这里的其他内容包括书评和会议报告。如果你真的遇到了 PHP 的问题,这里是和专家交流的地方。每个主题领域都有特定的论坛,还有播客、教程、文章,甚至是 PHP 手册的另一种途径。参见图 16-3 。
图 16-3。【devzone.zend.com 号
建筑师杂志:www.phparch.com
我们极力推荐的另一个资源是伟大的虚拟杂志 PHP | Architect 。你可以在上面的网址访问它的网页,看一期免费的,然后如果你足够喜欢它,你可以以合理的价格订阅 PDF 版本的杂志。这本杂志的内容质量是它如此有价值的原因。涵盖的主题通常是 PHP 技术的前沿,并且通常是中级到高级的信息。
会议
对于继续你的 PHP 教育来说,没有什么比参加一个会议更好的了。它让你有机会摆脱日常压力,真正专注于与 PHP 程序员同事会面和交流,讨论生活、宇宙和一切。这些事件的社会方面同样有价值。信不信由你,一天结束时,即使你正在酒店的休息室里喝饮料,技术会议上也会传递大量的知识。自然,在正式的演讲中通常会传播大量的信息,这也是参加会议的主要原因,但是附带的好处是无法估量的。
如果你有任何类型的高级经验,为什么不提交一个会议主题呢?如果你被选为演讲者,你将进入会议生活的另一个领域,这个领域没有多少人有机会体验——与行业名人接触,并与许多非常聪明的人建立宝贵的联系。当然,随着你被提升到会议发言人和主题专家的境界,你现在也会被认为是他们中的一员了!
如果可能的话,以下是推荐参加的会议列表(如果你资金有限,按优先顺序降序排列):
- 主要的 PHP 编程世界大会通常在每年的 11 月举行。你一定会在这里看到一些 PHP 大佬的名字,并且结交一些很棒的人脉。此外,主要的行业公告和产品发布通常会在 ZendCon 上宣布;因此,如果你想成为第一个看到和听到重大公告的人,那就试着参加这个会议。
- OSCON: O'Reilly 的 web 开发和开源会议。这个会议比 PHP 更广泛,但是 PHP 肯定是其中的一部分。OSCON 通常在仲夏的七月举行。
- ConFoo :以前被称为 PHP 魁北克,这是一个伟大的加拿大会议,它的内容扩展到 PHP 之外,尽管它的根源肯定是基于 PHP web 开发领域。
- 国际 PHP 大会:每年在德国举行两次,分别在春季和秋季。春季会议通常在柏林举行,而秋季会议——通常在 10 月份——似乎在德国的不同城镇举行。
- **开源印度:**这个为期三天的会议在每年的秋季举行。这是一个开源会议,所以像 OSCON 一样,它涵盖的不仅仅是 PHP 相关的主题。这是全亚洲最大的开源商业会议之一,所以如果你想在这个地区建立商业联系,你应该试着参加。不过,提醒一句:如果你想参加这次会议,除了护照之外,你还需要一份旅行签证。
注意一定要查看 php.net 的会议版块,因为新的会议总是不断出现。
PHP 认证
本章讨论的最后一个主题是获得 PHP 认证的价值(或感知价值),以及准备和参加考试的必要条件。关于这个主题有很多讨论,因为这个认证从 PHP 4.0 版(2004 年 7 月)开始就已经存在了,所以已经有足够的时间对它的价值进行合理的讨论了。有趣的是,这本书的四位作者中有三位是经过认证的 PHP 开发人员;这应该会让你对认证的价值有所了解。因此,让我们来看看它需要什么。
认证考试由 Zend 公司管理。这件事的巧妙之处在于,就像 PHP 本身一样,测试的准备工作类似于开源方法。Zend 邀请世界各地的 PHP 专家加入一个委员会,为考试准备问题和答案。这至少有以下两个好处:
- 这项测试是由不止一个团体或公司准备的,所以它对什么是并且应该被认为是可测试的有一个非常广阔的视角。
- 因为问题来自 PHP 专家的广泛基础,所以你知道测试并不局限于 PHP 语言的一两个特定领域。
目前的考试水平是基于 PHP 5.3 版本,被认为比 4.0 认证稍微难通过一点。考试的及格水平没有透露,但据猜测,考生必须取得 60 分或更高的分数才能通过考试。
注意这种测试有时会在某些会议上免费提供,ZendCon 就是其中之一。在同一会议上还提供了一个考试准备速成班。这是一个免费尝试测试的好机会。
测试由 70 个随机选择的问题组成,你有 90 分钟的时间来回答这些问题。有 12 个不同的主题领域,问题将从所有这些领域中抽取,为了尝试测试,您应该非常了解这些领域。此外,建议您在一年半到两年的时间里每天都使用 PHP,以获得测试所要求的基本实践经验。如果你真的通过了考试,你将被授予 Zend 认证工程师 ZCE 的称号,你将获得一个适合装裱的签名证书,你可以把 ZCE 添加到你的名片或网站上。
在 Zend.com 和 phparch.com 的网站上有学习指南和测试样题,所以如果你想得到额外的学习资料,可以去看看那些资源。
以下是测试主题领域:
- PHP 基础
- 功能
- 数组
- 面向对象编程
- 字符串和正则表达式
- 设计和理论
- 网页功能
- PHP 4 和 PHP 5 版本的区别
- 文件、流、网络
- XML 和 web 服务
- 数据库
- 安全
那么,认证值吗?绝对的!你会看到许多工作列表要求这一名称,因为它证明了 PHP 语言的专家能力的一定水平,它有助于你获得更高的工资。你会有一个新的自信水平,你会更好地准备下一次与你的老板讨论你的表现和薪酬。
总结
在这一章中,我们看了本书范围之外的资源和材料。我们还讨论了获得 PHP 语言 Zend 认证的价值。这本书的所有作者都希望你继续提高和扩展你对这种奇妙而强大的开源 web 开发语言的知识。
十七、附录:正则表达式
本附录以快速实用的方式介绍了正则表达式。然而,这个介绍仅仅是一个开始,因为这个主题是如此广泛,以至于已经有整本书都是关于它的。一个很好的例子是 Nathan A. Good (Apress,2004)的正则表达式食谱。正则表达式是一种精确匹配符合特定选择标准的字符串的方法。它们是理论计算机科学中的一个老概念,基于有限状态机。
正则表达式有很多种,它们在很多方面都是不同的。两个最常见的正则表达式引擎是 Posix 正则表达式和 Perl 兼容正则表达式(PCRE)。PHP 使用的是后者。实际上,两者都可以使用,但是 Posix 变体在 PHP 5.3 和更高版本中被否决了。以下是实现 PCRE 正则表达式引擎的主要 PHP 函数:
preg_matchpreg_replacepreg_splitpreg_grep
正则表达式机制中还有其他函数,但这四个函数是最常用的。每个中的“preg”代表“Perl 正则表达式”,与 Posix 正则表达式或“扩展正则表达式”相对。是的,曾经有过“ereg”版本的正则表达式函数,但是从 PHP 5.3.0 开始它们被弃用了。
本附录有两部分:第一部分解释 PCRE 正则表达式语法;第二个展示了在 PHP 脚本中使用它们的例子。
正则表达式语法
正则表达式的基本元素是元字符。当元字符被反斜杠字符(“\”)转义时,它们就失去了特殊的意义。表 A-1 是元字符的列表。
除了元字符,还有一些特殊的字符类,在表 A-2 中列出。
正则表达式.*将匹配任何字符。正则表达式^.*3将匹配从该行开始直到该行最后一个数字“3”的字符。这种行为是可以改变的;我们将在后面关于贪婪的章节中讨论这一点。现在,让我们看看更多正则表达式的例子。
正则表达式示例
首先,让我们看看日期。今天是 2011 年 4 月 30 日,星期六。以这种格式匹配日期的第一个模式如下所示:
/[A-Z][a-z]{2,},\s[A-Z][a-z]{2,}\s\d{1,2},\s\d{4}/
它的意思是“一个大写字母,后面是至少两个小写字母和一个逗号,后面是空格,大写字母,至少两个小写字母,空格,1 或 2 位数字,逗号,空格,最后,精确地是一年的 4 位数字。”列出 A-1 是测试正则表达式的一小段 PHP 代码:
***列举 A-1。*测试正则表达式
<?php $expr = '/[A-Z][a-z]{2,},\s[A-Z][a-z]{2,}\s\d{1,2},\s\d{4}/'; $item = 'Saturday, April 30, 2011.'; if (preg_match($expr, $item)) { print "Matches\n"; } else { print "Doesn't match.\n"; } ?>
注意,清单 A-2 中的$item变量末尾有一个点,而我们的正则表达式以年份的\d{4}结尾,与末尾的点不匹配。如果我们不希望这样,我们可以将正则表达式“锚定”到行尾,编写成这样:/[A-Z][a-z]{2,},\s[A-Z][a-z]{2,}\s\d{1,2},\s\d{4}$/。表达式末尾新添加的美元符号表示“行尾”,这意味着如果在年份之后的行中有任何尾随字符,正则表达式将不匹配。类似地,我们可以通过使用元字符“^”将正则表达式锚定到行首。匹配整行内容的正则表达式是/^.*$/。
现在,我们来看一种不同的日期格式,YYYY-MM-DD。任务是解析日期并提取组件。
注意这可以很容易地用日期函数来完成,它很好地说明了一些 PHP 函数的内部工作原理。
我们不仅需要验证该行包含有效日期;我们还需要提取年、月和日。为此,我们需要匹配分组或子表达式。匹配分组可以被认为是按序列号排序的子表达式。使我们能够执行手头任务的正则表达式如下所示:
/(\d{4})-(\d{2})-(\d{2})/
括号用于匹配分组。这些分组是子表达式,可以看作是独立的变量。清单 A-2 展示了如何使用内置的 preg_match 函数来实现这一点。
***清单 A-2。*用内置的In preg_match函数匹配分组
<?php $expr = '/(\d{4})-(\d{2})-(\d{2})/'; $item = 'Event date: 2011-05-01'; $matches=array(); if (preg_match($expr, $item,$matches)) { foreach(range(0,count($matches)-1) as $i) { printf("%d:-->%s\n",$i,$matches[$i]); } list($year,$month,$day)=array_splice($matches,1,3); print "Year:$year Month:$month Day:$day\n"; } else { print "Doesn't match.\n"; } ?>
在这个脚本中,函数preg_match接受第三个参数,数组$matches。以下是输出:
./regex2.php 0:-->2011-05-01 1:-->2011 2:-->05 3:-->01 Year:2011 Month:05 Day:01
数组$matches的第 0 个元素是匹配整个表达式的字符串。这与整个输入字符串不同。之后,每个连续的分组被表示为数组的一个元素。让我们看另一个更复杂的例子。让我们解析一个 URL。通常,URL 的形式如下:
http://hostname:port/loc?arg=value
当然,表达式的任何部分都可能缺失。解析上述形式的 URL 的表达式如下所示:
/^https?:\/\/[^:\/]+:?\d*\/[^?]*.*/
这个表达中有几个值得注意的新元素。首先是^http[s]?:中的s?部分。匹配字符串开头的http:或https:。脱字符^将表达式锚定在字符串的开头。那个?表示“前一个表达式出现 0 次或 1 次”前面的表达式是字母 s,翻译成“字母 s 出现 0 次或 1 次”。此外,斜线字符/以反斜杠字符\为前缀,以消除它们的特殊含义。
PHP 对正则表达式分隔符非常宽容;它允许将其更改为任何其他分隔符。PHP 会识别括号或竖线字符|,所以如果写成[^https?://[^:/]+:?\d*/[^?]*.*],或者甚至使用竖线字符作为分隔符:|^https?://[^:/]:?\d*/[^?]*.*|,表达式也同样有效。从特殊字符中去除特殊含义的一般方法是在它们前面加上一个反斜杠字符。该过程也称为“转义特殊字符”正则表达式很聪明,可以在给定的上下文中猜出字符的含义。在[^?]*中省略问号是不必要的,因为从上下文可以清楚地看出,它表示与问号不同的字符类别。这不适用于分隔符,如/;我们必须逃离这些。还有表达式的[^:\/]+部分,它代表“一个或多个不同于冒号或斜杠的字符”这个正则表达式甚至可以帮助我们处理稍微复杂一点的 URL 形式。参见列表 A-3 。
***清单 A-3。*复杂 URL 格式的正则表达式
<?php $expr = '[^https*://[^:/]+:?\d*/[^?]*.*]'; $item = 'https://myaccount.nytimes.com/auth/login?URI=http://'; if (preg_match($expr, $item)) { print "Matches\n"; } else { print "Doesn't match.\n"; } ?>
这是纽约时报的登录表单。现在让我们使用分组提取主机、端口、目录和参数字符串,就像我们在清单 A-2 中所做的那样(参见清单 A-4 )。
***列举 A-4。*提取主机、端口、目录和参数字符串
<?php $expr = '[^https*://([^:/]+):?(\d*)/([^?]*)\??(.*)]'; $item = 'https://myaccount.nytimes.com/auth/login?URI=http://'; $matches = array(); if (preg_match($expr, $item, $matches)) { list($host, $port, $dir, $args) = array_splice($matches, 1, 4); print "Host=>$host\n"; print "Port=>$port\n"; print "Dir=>$dir\n"; print "Arguments=>$args\n"; } else { print "Doesn't match.\n"; } ?>
执行时,该脚本将产生以下结果:
./regex4.php Host=>myaccount.nytimes.com Port=> Dir=>auth/login Arguments=>URI=http://
内部选项
URL 中未指定端口的值,因此没有可提取的内容。其他的东西都提取得很好。现在,如果 URL 是用大写字母写的,会发生什么,就像这样:
HTTPS://myaccount.nytimes.com/auth/login?URI=http://
这不匹配,因为我们当前的正则表达式指定了小写字符,然而它是一个完全有效的 URL,可以被任何浏览器正确识别。如果我们想考虑到这种可能性,我们需要在正则表达式中忽略大小写。这可以通过在正则表达式中设置“忽略大小写”选项来实现。正则表达式现在看起来像这样:
[(?i)^https?://([^:/]+):?(\d*)/([^?]*)\??(.*)]
对于(?i)之后的任何匹配,将忽略大小写。正则表达式Mladen ( ?i)g将匹配两个字符串Mladen G and Mladen g,但不匹配MLADEN G。
另一个经常使用的选项是“多行”的m通常,当遇到换行符“\n”时,正则表达式解析停止。可以通过设置(?m)选项来改变这种行为。在这种情况下,解析不会停止,直到遇到输入的结尾。美元符号字符也将匹配换行符,除非设置了“D”选项。“D”选项意味着元字符“$”将只匹配输入的结尾,而不匹配字符串中的换行符。
选项可以分组。在表达式的开头使用(?imD)将设置所有三个选项:忽略大小写、多行和“美元仅匹配结尾”
还有一种更传统的全局选项表示法,允许在最后一个正则表达式分隔符之后指定全局修饰符。使用这种符号,我们的正则表达式将如下所示:
[^https?://([^:/]+):?(\d*)/([^?]*)\??(.*)]i
新符号的优点是它可以在表达式中的任何地方指定,并且只会影响修饰符之后的表达式部分,而在最后一个表达式分隔符之后指定它将不可避免地影响整个表达式。
注意全局模式修改器的完整文档可从这里获得:
[www.php.net/manual/en/reference.pcre.pattern.modifiers.php](http://www.php.net/manual/en/reference.pcre.pattern.modifiers.php)
贪婪
正常情况下,正则表达式是贪婪的。这意味着解析器将尝试匹配尽可能多的输入字符串。如果正则表达式'(123)+'被用于输入字符串'123123123123123A',那么字母 A 之前的所有内容都将被匹配。下面的小脚本测试了这个概念。这个想法是从 HTML 行中只提取img标签,而不提取任何其他标签。该脚本的第一次迭代不能正常工作,看起来像是列出了 A-5 。
***清单 A-5。*在此插入列表标题。
<?php $expr = '/<img.*>/'; $item = '<a><img src="file">text</a>"'; $matches=array(); if (preg_match($expr, $item,$matches)) { printf( "Match:%s\n",$matches[0]); } else { print "Doesn't match.\n"; } ?>
执行时,结果如下所示:
./regex5.php Match:<img src="file">text</a>
注意一些浏览器,最著名的是谷歌 Chrome,会试图修复不良标记,因此贪婪和非贪婪输出都会排除错误的
</a>。
我们匹配了比我们想要的更多的字符,因为模式“.*>”匹配了尽可能多的字符,直到它到达最后一个“>”,它是</a>标签的一部分,而不是lt;img>标签的一部分。使用问号会使“*”和“+”量词不贪心;它们将匹配最小数量的字符,而不是最大数量的字符。通过将正则表达式修改为'<img.*?>',模式匹配将继续进行,直到遇到第一个“>”字符,产生期望的结果:
Match:<img src="file">
解析 HTML 或 XML 是使用非贪婪修饰符的典型情况,正是因为需要匹配标签边界。
PHP 正则表达式函数
到目前为止,我们所做的只是检查给定的字符串是否与规范匹配,规范是以复杂的 PCRE 正则表达式形式编写的,并根据正则表达式从字符串中提取元素。使用正则表达式还可以做其他事情,比如替换字符串或将字符串拆分成数组。除了大家已经熟悉的preg_match函数之外,这一节专门介绍实现正则表达式机制的其他 PHP 函数。这其中最值得注意的就是preg_replace。
替换字符串:preg_replace
preg_replace函数使用以下语法:
$result = preg_replace($pattern,$replacement,$input,$limit,$count);
参数$pattern, $replacement和$input是不言自明的。$limit参数限制替换次数,-1 表示没有限制;默认值为-1。最后一个参数$count,如果指定的话,将在替换完成后填充实际执行的替换次数。这看起来很简单,但是还有更多的分歧。首先,模式和替换可以是数组,如清单 A-6 中所示。
***清单 A-6。*在此插入列表标题。
`<?php $cookie = <<<'EOT' Now what starts with the letter C? Cookie starts with C Let's think of other things that starts with C Uh ahh who cares about the other things
C is for cookie that's good enough for me C is for cookie that's good enough for me C is for cookie that's good enough for me
Ohh cookie cookie cookie starts with C EOT; replacement = array("donut", "D"); expression, cookie); print "$donut\n"; ?>`
当执行时,这个小脚本产生的结果可能不会吸引来自芝麻街的饼干怪兽:
`./regex6.php Now what starts with the letter D? donut starts with D Let's think of other things that starts with D Uh ahh who cares about the other things
D is for donut that's good enough for me D is for donut that's good enough for me D is for donut that's good enough for me
Ohh donut donut donut starts with D`
需要注意的重要一点是,模式和替换都是数组。模式和替换数组应该具有相同数量的元素。如果替换比模式少,那么丢失的替换将被空字符串替换,从而有效地破坏模式数组中指定的剩余字符串的匹配。
正则表达式的全部力量可以在列出 A-7 的清单中看到。该脚本将从提供的表名列表中产生 SQL 就绪的 truncate table 命令。这是一项相当常见的任务。
为了简洁起见,表的列表已经是一个数组,尽管它通常是从文件中读取的。
***列举 A-7。*在此插入列表标题。
<?php $tables = array("emp", "dept", "bonus", "salgrade"); foreach ($tables as $t) { $trunc = preg_replace("/^(\w+)/", "truncate table $1;", $t); print "$trunc\n"; }
执行时,结果如下所示:
./regex7.php truncate table emp; truncate table dept; truncate table bonus; truncate table salgrade;
preg_replace的使用说明了几件事。首先,正则表达式中有一个分组(\w+)。当从清单 A-2 的中的字符串中提取日期元素时,我们在上一节中看到了分组。这种分组也出现在替换论元中,如“$1”。每个子表达式的值在变量$n中被捕获,其中n的范围从 0 到 99。当然,与preg_match, $o一样,它包含整个匹配的表达式,后续变量包含子表达式的值,从左到右编号。另外,请注意这里的双引号。不存在将变量$1与其他变量混淆的危险,因为形式$n, 0<=n<=99的变量是保留的,不能在脚本的其他地方使用。PHP 变量名必须以字母或下划线开头,这是语言规范的一部分。
其他正则表达式函数
还有两个正则表达式函数要讨论:preg_split和preg_grep。这两个函数中的第一个,preg_split,是相对于explode函数更强大的。explode函数将根据提供的分隔符字符串将输入字符串分割成元素数组。换句话说,如果输入字符串是$a="A,B,C,D",那么explode函数使用字符串“,”作为分隔符,将生成包含元素“A”、“B”、“C”和“D”的数组。问题是,如果分隔符不是固定的格式,字符串看起来像$a='A, B,C .D'?这样,我们如何拆分字符串。在这里,我们在分隔逗号的前后都有空格字符,我们还用一个点作为分隔符,这使得仅使用 explode 函数是不可能的。preg_split没有任何问题。通过使用下面的正则表达式,该字符串将被完美地拆分成各个部分:
$result=preg_split('/\s*[,.]\s*/',$a);
正则表达式的含义是"0或多个空格,后跟一个字符,该字符可以是点或逗号,后跟 0 个或多个空格当然,添加正则表达式处理比仅仅比较字符串更昂贵,所以如果常规的explode函数足够了,就不应该使用preg_split,但是工具箱中有它真的很好。增加的成本来自于正则表达式是相当复杂的事实。
注意正则表达式不是魔法。它们需要谨慎和测试。如果不小心,也可能得到意想不到的或不好的结果。使用正则表达式函数而不是更熟悉的内置函数本身并不能保证得到想要的结果。
所有知道如何使用名为grep的命令行实用程序的人都应该熟悉preg_grep函数。函数就是以此命名的。preg_grep函数的语法如下所示:
$results=preg_grep($pattern,$input);
函数preg_grep将为输入数组$input的每个元素计算正则表达式$pattern,并将匹配的输出存储在结果数组结果中。结果是一个关联数组,其原始数组的偏移量作为键提供。清单 A-8 展示了一个基于文件系统grep实用程序的例子:
***清单 A-8。*在此插入列表标题。
<?php $input = glob('/usr/share/pear/*'); $pattern = '/\.php$/'; $results = preg_grep($pattern, $input); printf("Total files:%d PHP files:%d\n", count($input), count($results)); foreach ($results as $key => $val) { printf("%d ==> %s\n", $key, $val); } ?>
中的点。php 扩展名被一个反斜杠字符转义,因为 dot 是一个元字符,所以为了去掉它的特殊含义,它必须加上前缀“\”。用于编写本附录的系统的结果如下所示:
./regex8.php Total files:35 PHP files:12 4 ==> /usr/share/pear/DB.php 6 ==> /usr/share/pear/Date.php 8 ==> /usr/share/pear/File.php 12 ==> /usr/share/pear/Log.php 14 ==> /usr/share/pear/MDB2.php 16 ==> /usr/share/pear/Mail.php 19 ==> /usr/share/pear/OLE.php 22 ==> /usr/share/pear/PEAR.php 23 ==> /usr/share/pear/PEAR5.php 27 ==> /usr/share/pear/System.php 29 ==> /usr/share/pear/Var_Dump.php 32 ==> /usr/share/pear/pearcmd.php
在安装了不同 PEAR 模块的不同系统上,结果可能会有所不同。
preg_grep函数可以让我们不用在循环中检查正则表达式,非常有用。
还有其他几个正则表达式函数,它们的使用频率远低于本附录中描述的函数。这些函数都有很好的文档记录,感兴趣的读者可以在[www.php.net](http://www.php.net.).的在线文档中查找它们