JavaScript DOM 和 AJAX 入门指南(四)
八、Ajax 和 Node.js 的后端交互
你终于读到了我谈论 Ajax 的那一章。好消息是,您可以使用 Ajax 创建非常漂亮的界面,并且可以将 JavaScript 的应用范围扩展到浏览器和当前显示的文档之外。
不太好的消息是,Ajax 依赖于 XMLHTTPRequest 对象(或简称为 XHR)或其微软对等物,并且它被“HTTP”所覆盖。这意味着没有服务器就不能使用任何 Ajax 示例。此外,要使用 Ajax,您需要一些服务器端脚本的基础知识(除非您使用现成的软件包——在本章的“概述”一节中有更多介绍)。
这也意味着使用 Ajax 剥夺了 JavaScript 的一项优势:创建可以在计算机的文件系统上,甚至从 CD 或记忆棒上离线工作的界面的能力。然而,Ajax 的好处弥补了这一点。
本章的另一部分是对 Node.js 或 Node 的介绍。Node 使 JavaScript 开发人员能够在服务器端编写代码。与 Ruby 或 PHP 类似,Node 允许您与数据库对话,并向应用的客户端发送信息。在本章的例子中,您将完全用 JavaScript 创建一个 HTTP 服务器。
首先,您需要设置一个本地服务器,因为 Ajax 示例使用 PHP 作为它们的服务器端语言。这并不像看起来那么难,因为有很多预打包的服务器可用。
我最喜欢的是 www.apachefriends.org/可以下载的《XAMPP…
XAMPP 安装 Apache 2、MySQL、PHP 和所有你需要的附加软件,并且可以在许多平台上使用。它还配备了一个 FTP 和电子邮件服务器、一个统计软件包和许多其他选项,并且由 Apache Friends(www.apachefriends.org)的维护者不断更新。哦,是的,当然是免费的。
提示同样,为了避免在阅读本章其余部分时遇到挫折,您应该亲自尝试一下本章中的许多代码示例,看看我在说什么。与其他章节的不同之处在于,代码示例不能在文件系统的本地计算机上运行;它们需要服务器,因为 Ajax 需要 HTTP 协议才能工作。如果你不想安装服务器,但是你已经在线了,你可以去本书的主页
www.beginningjavascript.com/,在那里你可以看到所有运行中的代码示例。
当您安装 xamp 时,您可以将章节示例解压缩到服务器安装的 htdocs 目录中的一个目录中,例如,名为 jsbook 的目录,该目录可能是 c:\ xamp \ htdocs \。要查看示例,请打开浏览器并键入*localhost/jsbook/*作为位置。
提示除了在
www.apachefriends.org/en/faq-xampp.html阅读官方帮助常见问题外,Mac 用户还可以使用 MAMP,它也做同样的事情。你可以在 www.mamp.info 下载 MAMP。
家用清洁液,足球俱乐部,还是飞侠哥顿的飞船:什么是 Ajax?
Ajax 最初代表异步 JavaScript 和 XML ,这个术语是 Jesse James Garrett 在 2005 年 2 月的 Adaptive Path 上创造的(www . Adaptive Path . com/publications/essays/archives/000385 . PHP)。它描述了一种不同于传统的开发 web 应用的方法。
正如文章中所解释的,传统的 web 应用和网站是同步工作的——每当你点击一个链接或提交一个表单时,浏览器将数据发送给服务器,服务器(希望如此)做出响应,然后整个页面被刷新。
Ajax 应用异步工作*,这意味着您可以在用户代理和服务器之间来回发送数据,而无需重新加载整个页面。您只替换页面中发生变化的部分。您还可以发送几个请求,继续滚动和使用页面,而其他部分在后台加载。*
*一个很好的比喻是,Ajax 之于传统网页,就像即时消息之于电子邮件一样:即时反馈,没有很长的等待时间,有更多的交流选择。图 8-1 显示了与传统网站和网络应用相比,Ajax 应用的流程。
图 8-1 。Ajax 与传统的请求
乍一看,这似乎给整个事情增加了一层额外的复杂性。然而,真正酷的是 Ajax 引擎和浏览器之间的通信是通过 JavaScript 触发的,而不是通过页面重载。
实际上,最终用户等待页面加载和呈现的时间更少,与页面的交互也更容易,因为她可以请求数据,并且仍然可以阅读文本或查看页面上的其他内容。这使得界面更加光滑,因为你可以在不改变整个网站的情况下在登录表单上给出反馈,同时能够在服务器或数据库中测试正确的条目。
让我们看一个简单的例子。演示文件 exampleXHR.html 使用 Ajax(没有 X,因为不涉及 XML)在用户点击链接时从服务器加载并显示文件,如图 8-2 所示。
图 8-2 。通过 Ajax 加载外部文件
这一切背后的魔棒是我之前介绍的一个名为 XMLHttpRequest 的对象。这是一个非标准对象,因为它不是万维网联盟(W3C)网站上官方标准的一部分。(目前是工作草案;更多详情见www.w3.org/TR/XMLHttpRequest/。)但是,所有现代浏览器都支持它。如果你需要支持 Internet Explorer 6,你应该寻找能做同样事情的 ActiveX object:ActiveX object(“微软。XMLHTTP”)。
警告这样做的问题是,当用户在 Microsoft Internet Explorer 中启用了 JavaScript 但禁用了 ActiveX 时,他将无法体验到您的 Ajax 努力。如果您创建 Ajax 解决方案并获得用户错误报告,请记住这一点。
让我们一步一步地看这个例子,这样你就可以看到不同的部分是做什么的。HTML 包含指向文本文件的链接,并使用两个参数调用 simplexhr.doxhr 方法:文本要发送到的 HTML 元素的 ID 和文本的 URL。
exampleXHR.html(节选)
<li>
<a href="perfect_day.txt"
onclick="simplexhr.doxhr('txtcontainer1', this.href ); return false;">Perfect Day</a>
</li>
<li>
<a href="great_adventure.txt"
onclick="simplexhr.doxhr('txtcontainer1', this.href ); return false;">Great Adventure</a>
</li>
注意这些链接并非完全不引人注目,符合本书中其余代码示例的标准,但至少它们在没有 JavaScript 的情况下也能工作——当脚本不可用时,浏览器将简单地显示文本文件。创建依赖于脚本的链接是非常诱人的,尤其是在使用现成的 Ajax 库时。无论这项技术有多酷,这都不是一个好主意。
simpleXHR.js
simplexhr = {
doxhr : function( container, url ) {
if( !document.getElementById || !document.createTextNode) {
return;
}
simplexhr.outputContainer = document.getElementById( container );
if( !simplexhr.outputContainer ){ return; }
脚本首先检查 DOM,并检查您想要写入内容的元素是否可用。如果元素可用,它将被存储在名为 outputContainer 的属性中,以便脚本中的所有其他方法都可以使用它。
simpleXHR.js(续)
var request;
try{
request = new XMLHttpRequest();
} catch ( error ) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP" );
} catch ( error ) {
return true;
}
}
定义一个名为 request 的新变量,并使用 try 和 catch 构造来查看支持哪个 XHR 版本。尝试分配一个新的 XMLHttpRequest。如果不支持这种情况,则会发生错误,触发 catch 语句。(你可以在本书的附录中了解更多关于 try and catch()的知识。)这个尝试分配 Microsoft ActiveX 对象。如果这也不可用,该方法返回 true,这意味着浏览器将只跟随链接并在浏览器中显示文本。
如果赋值成功,您就拥有了一个新的 XMLHttpRequest 对象。
注意要获得 XMLHttpRequest 对象的方法、处理程序和属性的完整列表,您可以参考 W3C 站点的文档:
www.w3.org/TR/XMLHttpRequest/。你也可以参考 Mozilla 开发者网络的developer.mozilla.org/en-US/docs/DOM/XMLHttprequest或者微软网站的msdn . Microsoft . com/en-us/library/ms 53587428v = vs . 8529 . aspx。
第一步是调用 open()方法启动与服务器的连接,并检索或发送数据。open()方法有五个参数,其中三个是可选的:
request = open(requestMethod,url[,sync,[name,[password]]);
- requestMethod 参数(以及其他一些超出本章范围的选项)可以是 GET 或 POST,对应于 HTTP 方法。
- url 参数是文件在服务器上的位置。
注意 XMLHttpRequest 不允许你从其他服务器加载内容,因为那会是一个很大的安全问题。想象一下,电子邮件或网站中嵌入的 JavaScript 能够从您的计算机发送任何数据,或者从服务器检索更多代码。这个问题有解决办法。一种是通过在服务器上使用代理脚本来加载第三方内容。另一种方法是在服务器上启用跨源资源共享(CORS)。关于 CORS 的信息可以在 www.w3.org/TR/cors/的 W3C 网站上找到。
- sync 参数是可选的,它是一个布尔值,定义请求应该异步发送还是同步发送。它被硬连接为 true——这意味着请求将被异步发送。同步请求会锁定浏览器。
- name 和 password 参数是可选的,只有当您尝试调用的文件需要用户身份验证时才是必需的。
在这种情况下,您将只从服务器检索文件。为此,您使用 GET 作为请求方法,使用文件的位置作为 url 参数,省略可选参数。
simpleXHR.js(续)
request.open('get', url );
请求对象的 readyState 属性包含一个数值,它描述了请求发生了什么。它在整个请求和响应过程中递增。readyState 的不同可能值及其对应的请求状态如下:
- 0: 对象已创建,但尚未调用 open 方法。
- 1: 发送方法尚未被调用。
- 发送方法被调用,但是数据还不可用。
- 3: 服务器正在发送数据。
- 4: 连接完成—数据已发送并已检索。
每当状态改变时,XHR 触发 readystatechange 事件。您可以使用相应的 onreadystatechange 事件处理程序来调用一个函数,在该函数中,您可以根据 readyState 的可能值进行测试,并采取适当的操作。
simpleXHR.js(续)
request.onreadystatechange = function() {
if( request.readyState == 1 ) {
simplexhr.outputContainer.innerHTML = 'loading... ';
}
一旦请求被初始化(readyState 等于 1),给用户一些反馈是一个非常好的主意,即后台正在发生一些事情。在本例中,脚本显示“正在加载...”HTML 输出元素内的消息,如图图 8-3 所示。
图 8-3 。通知用户请求已经发送并且正在进行中
其他状态不能跨浏览器安全地读取,这就是为什么您跳过 2 和 3 并通过比较 readyState 和 4 来检查请求是否完成。
simpleXHR.js(续)
if( request.readyState == 4 ) {
if ( /200|304/.test( request.status ) ) {
simplexhr.retrieved(request);
} else {
simplexhr.failed(request);
}
}
当请求完成时,检查另一个名为 status 的属性,它存储请求的状态。状态是响应的标准 HTTP 响应状态代码。当无法建立连接时,在其他错误条件下,或者当请求已被取消时,它为 0,当找不到文件时,它为 404。
注关于标准 HTTP 响应状态码的完整列表,参见
www.w3.org/Protocols/rfc2616-sec10.html。
如果状态为 200(一切正常)或 304(未修改),则文件已被检索到,您可以对其进行操作。在这个演示脚本中,您调用 retrieved()方法。如果状态是任何其他值,则调用 failed()。
simpleXHR.js(续)
}
request.send( null );
return false;
},
send()方法将您的请求发送到服务器,并可以将请求参数发送到被调用的服务器端脚本。如果您没有任何要发送的参数,那么将其设置为 null 是最安全的。(Internet Explorer 接受不带任何参数的 send(),但这可能会在较旧的 Mozilla 浏览器中导致问题。)最后,将方法的返回值设置为 false 会阻止链接被跟踪。
simpleXHR.js(续)
failed : function( requester ) {
alert('The XMLHttpRequest failed. Status: ' + requester.status );
return true;
},
如果请求没有成功,failed()方法会显示一个 alert()对话框,告诉用户这个问题。(这不是很聪明也不漂亮,但目前应该可以了。)在用户单击对话框的 OK 按钮后返回 true 会导致链接被跟随。您可以通过在浏览器中本地打开文件 exampleXHR.html(没有 protocol )并点击链接来测试这一点。因为没有 HTTP 传输,任何请求都会失败,代码为 0,如图图 8-4 所示。
图 8-4 。通知用户 XMLHttpRequest 失败
但是,如果请求一切顺利,那么 retrieved()方法将接管。
simpleXHR.js(续)
retrieved : function( requester ) {
var data = requester.responseText;
data = data.replace( /\n/g, '<br>' );
simplexhr.outputContainer.innerHTML = data;
return false;
}
}
这个方法使您能够获取和使用从 XMLHttpRequest 返回的数据。数据可以根据 responseType 以几种不同的格式读出。两种基于文本的格式是 responseText 和 responseXML。两者的区别在于输出的类型——responseText 返回一个字符串,而 responseXML 返回一个 XML 对象。可以在 responseText 上使用所有常用的字符串属性和方法,比如 length、indexof()、replace()等等,也可以在 responseXML 上使用所有的 DOM 方法,比如 getElementsByTagName()、getAttribute()等等。除了返回基于文本的响应,还可以使用 returnType blob 或 arrayBuffer 接收二进制数据。
在此示例中,您仅检索文本并使用 String.replace()方法将所有换行符转换为 BR 元素。然后,您可以将更改后的字符串作为 innerHTML 写出到 outputContainer,并返回 false 以停止正常的链接行为。
在许多情况下,使用 responseText 并通过 innerHTML 写出数据就足够了。与使用 XML 和 DOM 将对象转换回 HTML 相比,对于用户的浏览器和 CPU 来说,这也要快得多,工作量也少得多。
注意Ajax 这个缩写并不真正适用于这些例子,因为这个过程缺少 XML 组件。出于这个原因,这种方法被称为异步 HTML 和 HTTP (AHAH) ,并被定义为微格式,在
microformats.org/wiki/rest/ahah有代码示例。
你呢?
通常,浏览器缓存是你的朋友。浏览器在其中存储下载的文件,这意味着用户不必一遍又一遍地下载你的脚本。然而,在 Ajax 的情况下,缓存会导致问题。
Safari 是罪魁祸首,因为它缓存了响应状态(),不再触发更改。(请记住,状态返回 HTTP 代码 200、304 或 404。)然而,避免缓存问题非常简单:在调用 send()方法之前,向请求添加另一个头。这个标题告诉浏览器测试自某个日期以来数据是否发生了变化。你设定的日期并不重要,只要它是在过去——例如,在本文写作时是这样写的:
request.setRequestHeader( ‘If-Modified-Since’, ‘Mon, 12 Jan 2013 00:00:00 GMT’ );
request.send( null );
将 X 放回 Ajax 中
如果使用 responseXML,可以使用 DOM 方法将接收到的 XML 转换成 HTML。演示 exampleXMLxhr.html 就是这样做的。作为数据源,以 XML 格式的上一章分页示例中使用的相册集合为例。
albums.xml(节选)
<?xml version="1.0" encoding="utf-8"?>
<albums>
<album>
<id>1</id>
<artist>Depeche Mode</artist>
<title>Playing the Angel</title>
<comment>They are back and finally up to speed again</comment>
</album>
<album>
<id>2</id>
<artist>Monty Python</artist>
<title>The final Rip-Off</title>
<comment>Double CD with all the songs</comment>
</album>
[... more albums snipped ...]
</albums>
您希望通过 XHR 检索这些数据,并在页面中以表格的形式显示出来。图 8-5 显示了请求的不同阶段。
图 8-5 。以表格形式检索和显示 XML 数据
剧本的主要部分不必改变。
simplexmlxmhr . js】的缩写
simplexhr = {
doxhr : function( container, url ) {
if( !document.getElementById || !document.createTextNode ){
return;
}
simplexhr.outputContainer = document.getElementById( container );
if( !simplexhr.outputContainer ) { return; }
var request;
try {
request = new XMLHttpRequest();
} catch( error ) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP" );
} catch ( error ) {
return true;
}
}
request.open('get', url,true );
request.onreadystatechange = function() {
if(request.readyState == 1) {
simplexhr.outputContainer.innerHTML = 'loading... ';
}
if(request.readyState == 4) {
if( request.status && /200|304/.test( request.status ) ) {
simplexhr.retrieved( request );
} else {
simplexhr.failed( request );
}
}
}
request.setRequestHeader('If-Modified-Since', 'Mon, 12 Jan 2013 00:00:00 GMT');
request.send( null );
return false;
},
不同之处在于 retrieved()方法,该方法通过 responseXML 读取数据,并使用 XML 作为内容源写出数据表。移除加载消息,并使用 DOM createElement()和 createTextNode()方法创建主表。
simpleXMLxhr.js(续)
retrieved : function( requester ) {
var data = requester.responseXML;
simplexhr.outputContainer.removeChild(simplexhr.outputContainer.firstChild);
var i, albumId, artist, albumTitle, comment, td, tr, th;
var table = document.createElement('table' );
var tablehead = document.createElement('thead');
table.appendChild( tablehead );
tr = document.createElement('tr');
th = document.createElement('th');
th.appendChild( document.createTextNode('ID'));
tr.appendChild( th );
th=document.createElement('th');
th.appendChild( document.createTextNode('Artist'));
tr.appendChild( th );
th = document.createElement('th');
th.appendChild( document.createTextNode('Title'));
tr.appendChild( th );
th=document.createElement('th');
th.appendChild( document.createTextNode('Comment'));
tr.appendChild( th );
tablehead.appendChild( tr );
var tablebody = document.createElement('tbody');
table.appendChild( tablebody );
请注意,当您动态创建表格时,Internet Explorer 不会显示它们,除非您将行和单元格嵌套在 TBODY 元素中。火狐不会介意的。
接下来,循环检索数据的所有相册元素。
simpleXMLxhr.js(续)
var albums = data.getElementsByTagName('album');
for( i = 0 ; i < albums.length; i++ ) {
对于每个相册,通过标记名读取 XML 节点的内容,并通过 firstChild.nodeValue 检索它们的文本内容。
simpleXMLxhr.js(续)
tr = document.createElement('tr');
albumId = data.getElementsByTagName('id')[i].firstChild.nodeValue;
artist = data.getElementsByTagName('artist')[i].firstChild.nodeValue;
albumTitle = data.getElementsByTagName('title')[i].firstChild.nodeValue;
comment = data.getElementsByTagName('comment')[i].firstChild.nodeValue;
通过 createElement()、createTextNode()和 appendChild()使用这些信息将数据单元格添加到表中。
simpleXMLxhr.js(续)
td = document.createElement('th');
td.appendChild( document.createTextNode( albumId ) );
tr.appendChild( td );
td = document.createElement('td');
td.appendChild( document.createTextNode( artist ) );
tr.appendChild( td );
td = document.createElement('td');
td.appendChild( document.createTextNode( albumTitle ) );
tr.appendChild( td );
td = document.createElement('td');
td.appendChild( document.createTextNode( comment ) );
tr.appendChild( td );
tablebody.appendChild( tr );
}
将结果表作为新的子元素添加到输出容器中,并返回 false 以阻止链接将 XML 作为新文档加载。失败的()方法保持不变。
simpleXMLxhr.js(续)
simplexhr.outputContainer.appendChild( table );
return false;
},
failed : function( requester ) {
alert('The XMLHttpRequest failed. Status: ' + requester.status );
return true;
}
}
您可以看到,通过在 DOM 脚本方面做“正确的事情”,脚本会变得相当复杂。您可以通过使用工具方法创建表行来减少代码量,但是这意味着更多的处理,因为这些方法必须在一个循环中调用。
如果像本例中那样了解 XML 结构,使用 innerHTML 和 string 方法转换数据可能会更快更容易。演示 exampleXHRxmlCheat.html 正是这样做的。大部分脚本保持不变,但是 retrieved()方法要短得多。
simplexmlxhrchet . js(节选)
retrieved : function( requester ){
var data = requester.responseText;
simplexhr.outputContainer.removeChild(simplexhr.outputContainer.firstChild);
var headrow = '<tr><th>ID</th><th>Artist</th><th>Title</th><th>Comment</th></tr>';
data = data.replace( /<\?.*\?>/g, ' ' )
data = data.replace( /<(\/*)id>/g, '<$1th>' )
data = data.replace( /<(\/*)(artist|title|comment)>/g, '<$1td>' )
data = data.replace( /<(\/*)albums>/g, '<$1table>' )
data = data.replace( /<(\/*)album>/g, '<$1tr>' );
data = data.replace( /<table>/g, '<table>' + headrow );
simplexhr.outputContainer.innerHTML = data;
return false;
},
您以 responseText 的形式检索数据,删除“加载。。."消息,然后创建一个标题表行作为字符串,并将其存储在变量 headrow 中。因为 responseText 是一个字符串,所以可以使用 String.replace()方法来更改 XML 元素。
首先通过删除任何以 and ending with ?>开头的内容来删除 XML 序言。
注意这个例子使用了正则表达式,你可能还不知道,但我们将在下一章详细讨论。只要说正则表达式用斜线分隔并匹配某种文本模式就够了。如果斜线内有括号,这些字符串将存储在以1)、后跟字符串 id 和结束字符>的所有内容。第二个参数<$ 1>,写出第<个>或第</第>个,这取决于原始 id 标签是开始标签还是结束标签。您可以执行简单的字符串替换,而不是使用正则表达式:
data = data.replace('<id>', '<th>');
data = data.replace('</id>', '</th>');
按照这个模式替换其他元素:每个相册元素变成一个表,每个相册变成一个 tr,每个 id 变成一个 th;艺术家、标题和评论各成为一个 td。将 headrow 字符串追加到中,并使用 innerHTML 将最终结果存储在 outputContainer 元素中。
用 JSON 替换 XML
尽管 XML 是一种流行的数据传输格式——它是基于文本的,您可以确保有效性,并且系统能够通过 dtd、XML Schemata 或 RELAX NG 相互通信。Ajax 爱好者已经越来越意识到将 XML 转换成 JavaScript 对象是一件非常麻烦的事情。
与其将 XML 文件作为 XML 读取并通过 DOM 解析它,或者作为文本读取并使用正则表达式,不如将数据转换成 JavaScript 可以直接使用的格式,这样会容易得多,对系统的压力也小得多。这种格式被称为JSON(【json.org/】??)。它允许数据集… exampleJSONxhr.html 使用前面例子中的 XML 作为 JSON:
<albums>
<album>
<id>1</id>
<artist>Depeche Mode</artist>
<title>Playing the Angel</title>
<comment>They are back and finally up to speed again</comment>
</album>
<album>
<id>2</id>
<artist>Monty Python</artist>
<title>The final Rip-Off</title>
<comment>Double CD with all the songs</comment>
</album>
<album>
<id>3</id>
<artist>Ms Kittin</artist>
<title>I.com</title>
<comment>Good electronica</comment>
</album>
</albums>
转换成 JSON,如下所示:
专辑。json
{
"album":
[
{
"id" : "1",
"artist" : "Depeche Mode",
"title" : "Playing the Angel",
"comment" : "They are back and finally up to speed again"
},
{
"id" : "2",
"artist" : "Monty Python",
"title" : "The final Rip-Off",
"comment" : "Double CD wiid all the songs"
},
{
"id" : "3",
"artist" : "Ms Kittin",
"title" : "I.com",
"comment" : "Good electronica"
}
]
}
好处是数据已经是 JavaScript 可以理解的格式。要将其转换为要显示的对象,只需对字符串使用 eval 方法。
examplejsonxhr . js(excerpt)
retrieved : function( requester ) {
var content = '<table><thead>';
content += '<tr><th>ID</th><th>Artist</th>';
content += '<th>Title</th><th>Comment</th>';
content += '</tr></thead><tbody>';
var data = JSON.parse(' (' + requester.responseText + ') ' );
这为您提供了作为对象的所有内容,您可以通过属性表示法或关联数组表示法(后者在 id 示例中显示,前者在所有其他示例中显示):
examplejsonxhr . js(excerpt)
var albums = data.album;
for( var i = 0; i < albums.length; i++ ) {
content += '<tr><td>' + albums[i]['id'] + '</td>';
content += '<td>' + albums[i].artist + '</td>';
content += '<td>' + albums[i].title + '</td>';
content += '<td>' + albums[i].comment + '</td></tr>';
}
Content += '</tbody></table>';
simplexhr.outputContainer.innerHTML = content;
return false;
},
对于您自己服务器上的文件,使用 JSON 而不是 XML 要快得多。(在测试中,它被证明快了十倍。)但是,如果从第三方服务器使用 JSON,使用 eval()可能会很危险,因为它会执行任何 JavaScript 代码,而不仅仅是 JSON 数据。
您可以通过使用一个解析器来避免这种危险,该解析器确保只有数据被转换成对象,而恶意代码不会被执行。www.json.org/js.html 有一个开源版本。我们会在第十一章回到 JSON。
使用服务器端脚本访问第三方内容
如前所述,出于安全原因,很难使用 XHR 从其他服务器加载内容。例如,如果您想从其他服务器检索 RSS 提要,您可以使用一个服务器端脚本来为您加载这些提要,或者连接到一个支持 CORS 的服务器。
注意这是一个关于 Ajax 的常见神话:它并不取代服务器端代码,而是由服务器端代码提供支持,并为它提供一个更光滑的接口。XHR 本身只能从同一个服务器检索数据,或者向服务器端脚本发送信息。例如,您不能用 JavaScript 访问数据库——除非您使用名为 JSONP(带填充的 JSON)的方法,数据库提供者以 JavaScript 的形式提供输出,并且您将它包含在它自己的脚本标记中。在第十一章中有一个这样的例子。
服务器端组件是一个传递或代理脚本,它获取一个 URL,加载文档内容,然后将其发送回 XHR。该脚本需要设置正确的头来告诉 XHR 它返回的数据是 XML。如果找不到该文件,脚本将返回一个 XML 错误字符串。以下示例使用 PHP,但是任何服务器端语言都可以执行相同的任务。
loadrss.php
<?php
// Set the XML header
header('Content-type: text/xml');
// Define an error message in case the feed cannot be found
$error='<?xml version="1.0"?><error>Cannot find feed</error>';
// Clear the contents
$contents = '';
// Read the url variable from the GET request
$rssurl = $_GET['url'];
// Test if the url starts with http to prevent surfers
// from calling and displaying local files
if( preg_match('/^http:/', $rssurl ) ) {
// Open the remove file, and store its contents
$handle = @fopen( $rssurl, "rb" );
if( $handle == true ){
while ( !feof($handle ) ) {
$contents .= fread( $handle, 8192 );
}
fclose( $handle );
}
}
// If the file has no channel element, delete contents
if( !preg_match('/<channel/', $contents ) ){ $contents = ''; }
// Return either the contents or the error
echo $contents == '' ? $error : $contents;
?>
演示 exampleExternalRSS.html 使用这个脚本从 Yahoo 网站上检索 RSS 格式的最新标题。
HTML 中的相关部分是调用 doxhr()方法的链接,该方法带有输出新闻的元素和作为参数的 RSS URI。
examples external RSS . html(excerpt)
<p>
<a href="http://rss.news.yahoo.com/rss/topstories"
onclick="return readrss.doxhr('newsContainer',this.href)">
Get Yahoo news
</a>
</p>
<div id="newsContainer"></div>
注意 RSS 是真正简单聚合的缩写。本质上,它是一个 XML,其中包含您想要与世界共享的内容,通常是新闻标题。RSS 的规范可以在
blogs.law.harvard.edu/tech/rss找到,你可以在维基百科上读到更多关于它的好处:en.wikipedia.org/wiki/RSS。
本例中的重要细节是,RSS 是一种标准化格式,并且您知道 XML 结构——即使您是从第三方网站获得的。每个有效的 RSS 文档都包含一个 items 元素以及嵌套的 item 元素。每一个都至少包含一个描述完整信息的标题和一个指向完整信息的链接。你可以使用这些来显示一个可点击的标题列表,将用户带到雅虎网站,在那里她可以阅读完整的新闻文章,如图 8-6 所示。
图 8-6 。检索和显示 RSS 源数据
这个剧本又是一个简单的 XHR。不同之处在于,您不是直接链接到 URL,而是将它作为 GET 参数传递给 PHP 脚本:
外部 RSS.js
readrss = {
doxhr:function( container, url ) {
[... code snipped as it is the same as in the last example ...]
request.open('get', 'loadrss.php?url=' + encodeURI( url ) );
request.setRequestHeader('If-Modified-Since', 'Mon, 12 Jan 2013 00:00:00 GMT' );
request.send( null );
return false;
},
检索到的()函数需要更改。首先,它删除了“加载。。."消息,并使用 responseXML 检索 XML 格式的数据。因为 PHP 脚本返回 XML 格式的错误消息,所以您需要测试返回的 XML 是否包含错误元素。如果是这种情况,读取第一个错误元素的第一个子元素的节点值,并将其写入由段落标记包围的 outputContainer。
externalRSS.js(续)
retrieved : function( requester ) {
readrss.outputContainer.innerHTML = '';
var data = requester.responseXML;
if( data.getElementsByTagName('error').length > 0 ) {
var error = data.getElementsByTagName(‘error’)[0].firstChild.nodeValue;
readrss.outputContainer.innerHTML = '<p>' + error + '</p>';
如果没有错误元素,则检索返回的 XML 中包含的所有 item 元素,并检查结果列表的长度。如果少于一项,则从方法返回,并允许链接在浏览器中加载 XML 文档。这是确保返回的 RSS 有效的必要步骤——因为您没有在服务器端脚本中检查这一点。
externalRSS.js(续)
} else {
var items = data.getElementsByTagName('item');
var end = items.length;
if( end < 1 ){ return; }
如果有要显示的项目,您可以定义必要的变量并遍历它们。因为有些 RSS 提要有很多条目,所以限制显示多少条目是有意义的;在这种情况下,您选择 5。您阅读了每个条目的链接和标题,并添加了一个新的列表条目,其中嵌入了一个链接,该链接分别作为它的 href 属性和文本内容。注意,这个例子只是简单地组装了一个 HTML 字符串;当然,您可以走“更干净”的路,创建元素并应用文本节点。
externalRSS.js(续)
var item, feedlink, name, description, content = '';
for( var i = 0; i < 5; i++ ) {
feedlink = items[i].getElementsByTagName('link').item(0).firstChild.nodeValue;
name = items[i].getElementsByTagName('title').item(0).firstChild.nodeValue;
item = '<li><a href="' + feedlink+'">' + name + '</a></li>';
content += item;
}
将最后的内容字符串插入到 outputContainer 的 UL 标签中,这样就有了可点击的新闻标题。
externalRSS.js(续)
readrss.outputContainer.innerHTML = '<ul>' + content + '</ul>';
return false;
}
脚本的其余部分保持不变;failed()方法仅在 XHR 不成功时显示警告。
externalRSS.js(续)
},
failed : function( requester ) {
alert('The XMLHttpRequest failed. Status: ' + requester.status );
return true;
}
}
XHR 的慢速连接
可能出现的一个问题是,XHR 的连接可能需要很长时间,用户会看到加载消息,但什么也没有发生。您可以通过使用 window.timeout()在一定时间后停止执行来避免此问题。演示 exampleXHRtimeout.html 展示了一个使用这种技术的例子。除了使用窗口对象之外,XHR 级别 2 还包括一个超时属性和一个 ontimeout 事件。目前,Chrome 和 Safari 不支持 XHR 超时,而 Opera、Firefox 和 Internet Explorer 10 支持。
该请求的默认设置是 10 毫秒,这会导致超时,如图 8-7 所示。您可以使用示例中的第二个链接将超时设置为 10 秒并重试,如果您的连接不是非常慢或者 Yahoo 没有停机,您将会获得头条新闻。
图 8-7 。允许 XHR 连接超时
该脚本的不同之处在于,您需要一个属性来定义在触发超时之前要等待多长时间,一个属性来存储 window.timeout,还有一个布尔属性来定义是否有超时。Boolean 必须在 doxhr()方法中,因为每次调用 doxhr()时都需要初始化它。
xhrtimeout . js
readrss = {
timeOutDuration : 10,
toolong : false,
doxhr : function( container, url ) {
readrss.timedout = false;
if( !document.getElementById || !document.createTextNode ){
return;
}
readrss.outputContainer = document.getElementById( container );
if( !readrss.outputContainer ){ return; }
var request;
try {
request = new XMLHttpRequest();
} catch( error ) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP");
} catch( error ) {
return true;
}
}
在 onreadystatechange 事件侦听器中,添加超时并将其分配给主对象的 toolong 属性。在超时内,定义一个匿名函数来检查 readystate 并将其与 1 进行比较。这是一个场景,当定义的时间过去了,请求仍然在第一个阶段,而不是第四个也是最后一个阶段。发生这种情况时,调用请求的 abort()方法,将 timedout 属性设置为 true,并向显示元素写出一条消息,说明请求花费的时间太长。
XHRtimeout.js(续)
request.onreadystatechange = function() {
if( request.readyState == 1) {
readrss.toolong = window.setTimeout( function(){
if( request.readyState == 1 ) {
readrss.timedout = true;
request.abort(); // Stop
readrss.outputContainer.innerHTML = 'The request took too long';
}
},
readrss.timeOutDuration
);
readrss.outputContainer.innerHTML = 'loading... ';
}
当请求成功结束并且没有任何超时(存储在 timedout 属性中)时,清除超时。
XHRtimeout.js(续)
if( request.readyState == 4 && !readrss.timedout ) {
window.clearTimeout( readrss.toolong );
if( /200|304/.test( request.status ) ) {
readrss.retrieved( request );
} else {
readrss.failed( request );
}
}
}
request.open('get', 'loadrss.php?url='+encodeURI( url ) );
request.setRequestHeader('If-Modified-Since', 'Mon, 12 Jan 2013 00:00:00 GMT' );
request.send( null );
return false;
},
脚本的其余部分保持不变。
一个更大的 Ajax 例子:连接的选择框
让我们来看一个更大的 Ajax 例子——我称它为 Ajax,尽管您不会使用 XML。连接的选择框是一个经典的例子,它展示了 JavaScript 可以如何让界面变得更快。它们的一个常见用途是航班报价网站,在这里您在选择框中选择一个机场,页面会立即在第二个选择框中显示从该机场出发的目的地机场。传统上,这是通过将所有 airport 连接数据保存在 JavaScript 数组中并操纵 select 元素的 options 数组来实现的。更改第一个机场选择框会自动将第二个机场选择框更改为可用目的地。
当你有鼠标和可用的 JavaScript 时,这是非常好的;然而,当两者中的一个缺失时,这可能是非常令人沮丧的,或者当两者都不可用时,这甚至是不可能的。这个例子将展示如何创建相互依赖的选择框,这些选择框在没有鼠标和 JavaScript 的情况下也能工作,并且在 JavaScript 可用时不会重新加载整个页面。
诀窍是让功能在服务器端工作,然后添加 JavaScript 和 XHR 技巧来阻止整个页面重新加载。因为你不知道用户是否真的能应付这个,你甚至可以让它可选而不是给定。
注意这种 Ajax 方法比原来的方法更具有可访问性。原因是你不想再一次犯 DHTML 最大的错误——使用一种技术而不考虑那些不能处理它的人。Jeremy Keith 在他的 DOM 脚本书中创造了这种方法 HIJAX ,但是到目前为止,它还没有像 Ajax 这个术语一样得到公众的广泛关注。
第一步是创建一个执行所有功能的服务器端脚本。因为这不是一本关于 PHP 的书,这里就不赘述了。只要说主文档 exampleSelectBoxes.php 包含一个更小的 PHP 脚本 selectBoxes.php 就够了。后者以数组的形式包含所有的机场数据(但也可以很容易地进入数据库来检索这些数据),并根据用户的选择和发送表单写出界面的不同状态,如图 8-8 所示。
图 8-8 。连接的选择框
主页展示了带有 DIV 的表单,DIV 的 id 可用于 XHR 输出。
exampleSelectBoxes.php(节选)
<form action="exampleSelectBoxes.php" method="post">
<div id="formOutput">
<?php include('selectBoxes.php');?>
</div>
<p class="submit"><input type="submit" name="select" id="select" value="Choose" /></p>
</form>
注意这个例子使用 POST 作为发送数据的方法。
PHP 脚本返回了一个 HTML 界面,您可以在这个过程的每个阶段进入这个界面:
- 如果还没有发送任何表单数据,它会显示一个 ID 为 airport 的选择框,列出数据集中的所有机场。
- 如果选择了一个机场并发送给服务器,脚本会将选择的机场显示在一个强元素中,并显示为一个隐藏的表单字段。它还将此选择的可能目的地机场显示为一个 ID 为 destination 的选择框。此外,它还创建了一个指向主文档的链接,用 ID 开始一个新的选择。
- 如果用户选择一个机场和一个目的地,并将它们发送回服务器,脚本只是暗示更多的功能,因为在这个例子中不需要更多的功能。但是,它提供了返回初始页面的链接。
如果 JavaScript 可用,该脚本应该执行以下操作:
- 在表单中创建一个新的复选框,允许用户打开 Ajax 功能——在本例中,只需重新加载由 selectBoxes.php 创建的表单部分。
- 如果选中了该复选框,脚本应该用事件处理程序调用的函数覆盖表单的正常提交过程。作为一个加载指示器,它应该将 Submit 按钮的文本改为“loading”
- 它还应该向返回第一阶段的链接添加一个搜索参数,以确保当用户单击该链接时,他不必再次选择复选框。
先说剧本的骨架。您需要一个复选框的标签、一个包含它的段落的类(实际上不是必需的,但是它允许样式化)、表单元素容器的 id 和返回到流程开始的链接。
作为方法,您需要一个 init()方法,带有检索和失败处理程序的主 XHR 方法,以及用于事件处理的 cancelClick()和 addEvent()。
select boxes . js(skeleton)
dynSelect = {
AJAXlabel : 'Reload only the results, not the whole page',
AJAXofferClass : 'ajax',
containerID : 'formOutput',
backlinkID : 'back',
init : function(){},
doxhr : function( e ){},
retrieved : function( requester, e ){},
failed : function( requester ){},
cancelClick : function( e ){},
addEvent : function(elm, evType, fn, useCapture ){}
}
dynSelect.addEvent( window, 'load', dynSelect.init, false );
现在开始充实骨架。
selectBoxes.js
dynSelect = {
AJAXlabel : 'Only reload the results, not the whole page',
AJAXofferClass : 'ajax',
containerID : 'formOutput',
backlinkID : 'back',
init()方法测试 W3C DOM 是否受支持,检索第一个表单,并将 ID 为 select 的 Submit 按钮存储在一个属性中——这是在最后一步中删除按钮所必需的。然后,它创建一个新段落,并为前面定义的 Ajax 触发器应用该类。
selectBoxes.js(续)
init : function(){
if( !document.getElementById || !document.createTextNode ){
return;
}
var f = document.getElementsByTagName('form')[0];
dynSelect.selectButton = document.getElementById('select');
var p = document.createElement('p');
p.className = dynSelect.AJAXofferClass;
议程上的下一步是提供打开 Ajax 选项的复选框。将复选框的名称和 ID 设置为 xhr,并确定当前 URI 是否已经具有?ajax 搜索字符串。如果有,将复选框预设为已选中。(这是必要的,以确保返回第一步的链接不会阻止 Ajax 增强的工作。)
selectBoxes.js(续)
dynSelect.cb = document.createElement('input');
dynSelect.cb.setAttribute('type', 'checkbox');
dynSelect.cb.setAttribute('name', 'xhr');
dynSelect.cb.setAttribute('id', 'xhr');
if( window.location.search != '' ) {
dynSelect.cb.setAttribute('defaultChecked', 'checked' );
dynSelect.cb.setAttribute('checked', 'checked');
}
将复选框添加到新段落中,并在其后添加一个带有适当文本的标签。新段落成为表单的第一个子节点,当表单被提交时,您应用一个触发 dohxhr()方法的事件处理程序。
selectBoxes.js(续)
p.appendChild( dynSelect.cb );
var lbl = document.createElement('label');
lbl.htmlFor = 'xhr';
lbl.appendChild( document.createTextNode( dynSelect.AJAXlabel ) );
p.appendChild( lbl );
f.insertBefore( p, f.firstChild );
dynSelect.addEvent(f, 'submit', dynSelect.doxhr, false );
},
dohxr()方法测试复选框是否被选中,如果没有,则简单地返回。如果是,您为当前机场和当前目的地定义两个变量,并将输出元素存储在一个属性中。测试输出容器是否存在,如果不存在则返回。
selectBoxes.js(续)
doxhr : function( e ) {
if( !dynSelect.cb.checked ){ return; }
var airportValue, destinationValue;
dynSelect.outputContainer = document.getElementById(dynSelect.containerID );
if( !dynSelect.outputContainer ){ return; }
下面是 XHR 代码,它定义了正确的对象并设置 onreadystatechange 事件侦听器。
selectBoxes.js(续)
var request;
try {
request = new XMLHttpRequest();
} catch( error ) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP");
} catch( error ) {
return true;
}
}
request.onreadystatechange = function() {
if( request.readyState == 1 ) {
dynSelect.selectButton.value = 'loading... ';
}
if( request.readyState == 4 ) {
if( request.status && /200|304/.test( request.status ) ) {
dynSelect.retrieved( request );
} else{
dynSelect.failed( request );
}
}
}
确定文档是否包含机场和目的地选择框;如果是,将它们的当前状态存储在变量 airportValue 和 destinationValue 中。请注意,您需要在航班选择过程的第二阶段检查 airport 字段的类型,因为它是一个隐藏字段。
selectBoxes.js(续)
var airport = document.getElementById('airport');
if( airport != undefined ) {
if( airport.nodeName.toLowerCase() == 'select' ) {
airportValue = airport.options[airport.selectedIndex].value;
} else {
airportValue = airport.value;
}
}
var destination = document.getElementById('destination');
if( destination ) {
destinationValue = destination.options[destination.selectedIndex].value;
}
因为表单是使用 POST 而不是 GET 发送的,所以您需要稍微不同地定义请求。首先,您需要将请求参数组装成一个字符串。(这是当 send 方法为 GET 时,URI 上变量的轨迹——例如,www.example.com/index.php?search+DOM&values = 20&start = 10。)
selectBoxes.js(续)
var parameters = 'airport=' + airportValue;
if( destinationValue != undefined ) {
parameters += '&destination=' + destinationValue;
}
接下来,打开请求。除了使用修改后的头防止缓存,还需要告诉服务器内容类型是 application/x-www-form-urlencoded;然后,您将所有请求参数的长度作为伴随 Content-length 的值进行传输。您还需要告诉服务器在检索完所有数据后关闭连接。与 GET 请求不同,send()在发布时需要一个参数,这是 URI 编码的参数。
selectBoxes.js(续)
request.open('POST', 'selectBoxes.php');
request.setRequestHeader('If-Modified-Since', 'Mon, 12 Jan 2013 00:00:00 GMT');
request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
request.setRequestHeader('Content-length', parameters.length );
request.setRequestHeader('Connection', 'close');
request.send( encodeURI( parameters ) );
注意如果你不知道这里发生的一切,不要自责;毕竟是服务器和 HTTP 代码,你才刚刚开始用 JavaScript。只要你这样使用它,你可能永远也不会真正理解它的含义。
如果您在最后一个页面之前,并且机场和目的地都可用,请移除提交按钮以防止出错。
注意这是这个例子的修饰步骤。一个真正的应用也应该完成下面的步骤,但是你现在不需要走那么远。
最后,调用 cancelClick()来阻止正常的表单提交。
selectBoxes.js(续)
if( airport && destination ) {
var sendButton = document.getElementById('select');
sendButton.parentNode.removeChild( sendButton );
}
dynSelect.cancelClick( e );
},
retrieved()方法与其他示例没有太大区别。在检索请求的 responseText 并用新的表单元素替换旧的表单元素之前,通过将 Submit 按钮的值改回 Select 来撤销上一步中所做的操作。补充?指向第一步的链接的 href 的 ajax,以确保激活该链接不会关闭之前选择的功能。(现在,您知道用户想要 Ajax 界面。)
selectBoxes.js(续)
retrieved : function( requester, e ) {
dynSelect.selectButton.value = 'Select';
var content = requester.responseText;
dynSelect.outputContainer.innerHTML = content;
var backlink = document.getElementById( dynSelect.backlinkID );
if( backlink ) {
var url = backlink.getAttribute('href');
backlink.setAttribute('href', url+'?ajax');
}
dynSelect.cancelClick( e );
},
脚本的其余部分由熟悉的 failed()、cancelClick()和 addEvent()实用程序方法组成。
selectBoxes.js(续)
failed : function( requester ){
alert('The XMLHttpRequest failed. Status: ' + requester.status);
return true;
},
cancelClick : function( e ){
[... code snipped ...]
},
addEvent: function( elm, evType, fn, useCapture ){
[... code snipped ...]
}
}
dynSelect.addEvent( window, 'load', dynSelect.init, false );
这个例子表明 Ajax 非常依赖于服务器代码。如果你知道你会得到什么,就很容易创建一个有用的和有吸引力的界面。
您还可以以一种不引人注目和可选的方式使用 Ajax 方法,使旧的效果更引人注目,并更好地针对那些想要它们的人。
Node.js
本书大部分时间我们都在讨论如何在客户端使用 JavaScript,以及如何用它来增强您的 web 应用。这将是一个简短的介绍,介绍如何使用您新获得的技能来开发将在服务器上运行的应用。因为这不是在您的浏览器中运行,所以首先解释一下正在发生的事情并进行设置会有所帮助。
Node.js 建立在谷歌的 Chrome JavaScript 引擎 V8 之上。正因为如此,Node 让您只需使用 JavaScript 就可以创建一个 web 服务器,而不需要安装任何其他服务器软件。
由 Ryan Dahl 创建的目标是制作一个类似 Gmail 的 web 应用,Node 是一个开源项目,可以在多种环境下运行,可以由 Heroku、亚马逊的 AWS、Nodejitsu 和 Joynet 等公司托管,Joynet 也是该项目的赞助商。最著名的是 LinkedIn 将 Node 用于其移动应用。
安装 Node.js 并开始使用
进入www.nodejs.org/download,然后在你的系统上下载并安装 Node,你就可以快速上手了。之后,你应该准备做一些快速的例子。
我们将立即投入工作,确保一切正常。首先,让我们确保节点已安装,并检查当前版本。在撰写本文时,我使用的是版本 0.10.2。您可以通过在命令行中键入 node-version 来亲自尝试一下。
此时,您应该已经安装了节点,可以开始工作了。让我们跳过“Hello World”的例子,直接进入你将在每个节点教程中看到的另一个例子:如何制作 web 服务器。
httpServer.js
var http = require('http');
var server = http.createServer(function (request, response){
response.writeHead(200,{"Content-Type": "text/plain"});
response.end("It's the information age!");
});
server.listen(8080);
从纯代码的角度来看,它看起来像普通的 JavaScript。那么这怎么能创建一个 web 服务器呢?让我们深入细节。
首先,您需要 HTTP 模块。模块为您的应用增加了功能。HTTP 模块内置在节点中;但是,有一个节点包管理器(npm)允许您安装其他模块。下一个创建的变量叫做 server,它是 http object 上调用 createServer 返回的 HttpServer *,*的一个实例。createServer 方法接受匿名函数,该函数可以接受两个参数:请求和响应。
在我们讨论这两行代码之前,让我们先讨论一下代码中的最后一行:
server.listen(8080);
这告诉服务器监听端口 8080。这意味着,如果你在浏览器中输入“localhost:8080”,它会将你导向当前正在运行的服务器。当这种情况发生时,服务器必须做出响应。每次向服务器发出请求时都会调用这个函数。它被传递一个请求对象,其中包含请求的细节,还有一个响应对象,用于在处理程序处理请求时做出响应。在这种情况下,请求处理程序无条件地用状态代码 200(“OK”状态代码,表示成功)和包含字符串的响应正文来响应每个请求。
模块
我们谈到了让您能够向基于节点的应用添加功能的模块。它们还能让你保持自己的文件井然有序。让我们把当前的文件做成一个模块。
http 服务器模块. js
var http = require('http');
function startUp(){
function onRequest(request, response){
response.writeHead(200,{"Content-Type": "text/plain"});
response.end("It's the information age!");
}
http.createServer(onRequest).listen(8080);
}
exports.startup = startUp;
现在让我们创建一个 index.js 文件来启动它。
index.js
var server = require("./httpServerModule");
server.startup();
可以看到模块大部分都是一样的。主要要注意的是最后一行:
exports.startup = startUp;
这里,您将属性 startUp 添加到对象导出中。任何使用 require()访问模块的人都可以使用该对象。模块范围内没有添加到导出中的变量对于模块来说仍然是私有的。
另一个文件 Index.js 类似于我们最初的项目。您需要加载该模块。那个。/告诉 Node 查找相对于包含当前文件的目录的模块。如果没有找到,Node 首先在核心模块中搜索,然后在其他文件夹中寻找匹配。可以在节点站点上找到完整的描述(nodejs . org/API/modules . html # loading _ from _ Node _ modules _ Folders)。第二行只是调用你的启动函数,模块完成工作。如果您愿意,也可以从命令行使用 REPL (Read-Eval-Print-Loop)来运行它。在命令行中,只需键入节点,然后就可以像在索引文件中一样运行每一行。然后检查你的浏览器。
在概念上,它类似于创建对象来保存您的功能。这样,在全局名称空间中就不会有太多的变量。
回调
在前面的例子中,您有匿名函数来处理您的请求的结果。这很好,但是如果有很多事情在进行,就很难阅读。回调让你以一种易于阅读的方式分离一些函数。下一个示例展示了如何使用 Node 向 web 服务器发出请求,并使用回调在命令行中显示结果。
在这里,您将创建一个与之前创建的模块相似的模块。这一次,在发出 HTTP 请求时,您分配了两件事情。第一个选项描述您感兴趣的主机服务器和该服务器上的路径。接下来是回调函数。该函数接收从服务器返回的响应,并为该响应分配了两个事件。
http request . js
var http = require('http');
var options = {
host: 'www.apress.com',
path: '/9781430250920'
};
callback = function(response) {
var str = ''
response.on('data', function (chunk) {
str += chunk;
});
response.on('end', function () {
console.log(str);
});
}
function client (){
var req = http.request(options, callback);
req.end();
}
exports.client = client;
这类似于常规 JavaScript 中的事件侦听器。您寻找“数据”事件并获取从服务器返回的数据。在本例中,您将它附加到 str 变量。
第二个事件 end 让您知道来自服务器的数据流已经结束。命令行的结果是一个长字符串,由您刚刚请求的网页内容组成。
因为这是一个模块,您可以从这里访问它。
服务器请求索引.js
var server = require("./httpRequest");
server.client ();
调试
Node 简介的最后一部分是谈论如何调试你的应用。运行调试器的最快方法是在命令行节点 debug filename.js。在您的代码中,您可以在想要检查代码的位置添加调试器命令。这可能不是调试应用最直观的方式。像 Eclipse、JetBrain 的 IntelliJ Idea 和 WebStorm 产品这样的 ide 都有调试选项,在这些选项中,您可以设置断点,并像使用其他服务器端语言一样使用 Node.js。你可以在 gitHub 上找到 Eclipse 的详细说明:gitHub . com/joyent/Node/wiki/Using-Eclipse-as-Node-Applications-Debugger。你可以在这里找到 JetBrain 产品的详细信息:www . jetbrains . com/idea/web help/running-and-debugging-node-js . html。
使用 Node 的另一种方法是安装节点检查器。请注意,在撰写本文时,它不再被维护。但是,如果您想尝试一下,请转到命令行并键入NPM install–g node-inspector。–g 标志将确保它是一个全局安装。
安装后,您将需要两个命令行窗口来完成这项工作。在第一个窗口中,可以输入节点***–debug filename . js或者节点*****–debug-brk filename . js***。*第二个选项告诉调试器在程序开始时使用断点。
在第二个窗口中,键入节点检查器。你会得到消息“访问 0.0.0.0:8080/debug?port=5858 。"将它复制并粘贴到 Chrome 中,您将获得带有断点的 Chrome 调试器。(参见图 8-9 。)如果您没有得到,而是得到“connect ECONNREFUSED”,请确保您没有在端口 5858 上运行任何其他程序。您可能需要关闭任何打开端口的应用。
图 8-10 显示了 JetBrain 的 IntelliJIdea 中的编辑功能。
图 8-9 。用于调试 Node.js 应用的 Chrome 调试器
图 8-10 。JetBrain 的 IntelliJIdea 具有 Node.js 编辑和调试功能
摘要
我希望这让您了解了使用 JavaScript 和 XMLHttpRequest 可以在后端和浏览器之间创建动态连接,而无需重新加载页面。另外,我希望这一章能帮助你理解如何通过 Node.js 将你的 JavaScript 技能带到服务器端。
尽管 Ajax 很酷,但有一些事情需要记住:
- 最初,Ajax 是作为开发 web 应用的方法而发明的,而不是网站。将每一个小表单和菜单“Ajax 化”可能有些过头了。
- Ajax 是客户端脚本和后端之间的连接器;它的功能取决于后端脚本或信息。
- Ajax 可以用来连接到与使用它的脚本相同的服务器上的数据源。您还可以使用服务器上的脚本来连接到不同的服务器,或者您可以拥有以 JSON 格式提供数据的第三方服务。最后,如果服务器支持 CORS,您可以连接到其他服务器上的数据。
- 创建一个看起来令人印象深刻但很突兀的 Ajax 应用是非常诱人和容易的——依赖于鼠标和可用的 JavaScript。创建一个可访问的 Ajax 界面是一项非常困难的任务。
Ajax 非常有用,许多有天赋的开发人员正在开发框架和库,它们可以帮助您快速创建 Ajax 应用,而无需了解它的所有细节——使用它们甚至可以防止您重复这些开发人员过去犯过的错误。可用的库的数量是惊人的,并且很难说哪一个是适合手头任务的。
Node.js 本身并不是一个框架,就像 Rails 对于 Ruby 一样。它确实为您提供了使用 JavaScript 开发任何类型的应用的低级权限。有一些框架可以帮助你组织你的代码和构建像 express(expressjs.com/)这样的大规模应用。
在下一章中,我们将最终仔细研究正则表达式以及如何使用它们来验证数据。您将学习如何创建一个联系人表单作为示例应用,并可能重用这里实现的一些 XHR 功能,使其比一般的联系人表单更加光滑。*
九、数据验证技术
在本章中,你将学习如何使用 JavaScript 来验证用户输入的或来自其他系统的数据。你已经在第二章中听到了很多关于这方面的内容,该章讨论了涉及数据的决策,我们将使用其中的一些知识并在这里进行扩展。
随着 HTML5 的引入,数据验证经历了一些变化。以前,JavaScript 是防止错误数据发送到数据库的第一道防线。现在,浏览器内置的新功能帮助开发人员消除了许多负担,因为浏览器自己完成了大部分验证工作。
这并不是说不应该有任何服务器端验证。有很多服务器端框架都内置了验证功能,所以在将数据提交到数据库之前,没有理由不进行第二次检查。
HTML5 引入了一个名为约束验证的概念,它涉及到给你的数据一些特定的规则或约束。这样,浏览器就可以对照您设置的规则进行检查,并确保数据是正确的。
客户端验证的利与弊
在客户端验证用户输入非常重要,原因有几个:
- 当用户输入不正确的数据时,它可以省去重新加载页面的麻烦;您可以保存变量的状态,这样用户就不必再次输入所有数据,只需输入不正确的数据。
- 它减少了服务器流量,因为在出错的情况下没有到后端的往返。
- 它使界面更加灵敏,因为它给了用户即时的反馈。
另一方面,在客户端验证有几个问题:
- 它不能作为唯一的验证手段。(JavaScript 可能不可用,甚至可能被故意关闭以规避您的验证措施。当 JavaScript 是验证数据的唯一方式时,就会出现这种情况。)
- 可能发生的情况是,用户代理没有通知用户对文档的动态更改——对于有视觉障碍的用户来说,非常旧的屏幕阅读器就是这种情况。
- 如果您不想让您的验证规则可见——比方说,为了防止垃圾邮件或出于身份验证的目的——在 JavaScript 中没有办法做到这一点。
关于用 JavaScript 保护内容的快速提示
验证是一回事,用密码保护内容或通过加密混淆内容是另一回事。如果你环顾网络,你会发现很多例子,承诺你可以用 JavaScript 密码保护网页不被访问。通常,这些脚本是这样的:
examplePassword.html
var pw = prompt('Enter Password', ' ');
if(pw != 'password123') {
alert('Wrong password');
window.location = 'boo.html' ;
} else {
window.location = 'creditCardNumbers.html';
}
解决这种保护的唯一技巧是查看页面源代码,或者——如果保护在它自己的 JavaScript 文件中——在浏览器或文本编辑器中打开它。在某些情况下,无法重定向到正确的页面,而只能重定向到错误的页面,只需关闭 JavaScript 就可以了。
似乎有更聪明的保护方法,使用密码作为文件名的一部分:
var pw = prompt('Enter Password' , ' ');
window.location = 'page' + pw + '.html';
这些可以通过找出服务器上可用的文件来破解——因为要么目录列表没有被关闭(令人惊讶的是,这种情况经常发生——为了证明,只需在“index of /mp3”上进行谷歌搜索——包括引号),要么页面可以在计数器统计数据或浏览器或谷歌的缓存中找到。
这同样适用于混淆(通过加密或替换文字使某些内容不可读)内容和脚本。只要有足够的时间和决心,任何受 JavaScript 保护的东西都可以被破解。只是不要浪费你的时间。
注意 JavaScript 是一种大部分时间在客户端计算机上执行的语言,这使得恶意攻击者很容易绕过您的保护方法。在右键单击防护脚本保护图像和文本不被复制的情况下,你很可能会疏远正常的访问者,除了真正的攻击者的干笑之外什么也得不到。
然而,用像迪安·爱德华的打包器(dean.edwards.name/packer/)这样的东西来打包 JavaScript 以使真正沉重的脚本变得更短是另一个问题,并且有时可能是一个好主意——例如,如果你想在一个高流量的网站上使用一个大的库脚本。尽管打包 JavaScript 可能会使文件变小,但是在客户端解压缩文件可能会影响性能。雅虎在 yui.github.com/yuicompress… ?? 也有压缩 JavaScript 和 CSS 的工具。
正则表达式
正则表达式帮助您将字符串与字符模式进行匹配,非常适合验证用户输入或更改文档内容。它们不局限于 JavaScript,也存在于其他语言中,如 Perl、PHP 和 UNIX 服务器脚本。它们的功能惊人地强大,如果你与 Perl 或 PHP 爱好者和服务器管理员交谈,你会惊讶地发现,它们经常可以用一个正则表达式代替你用 JavaScript 编写的 50 行的 switch / case 或 if / else 构造。许多编辑环境还具有“查找”和“搜索和替换”功能,允许使用正则表达式。
正则表达式是猫的睡衣,一旦你把头伸向它们;然而,乍一看,像/^[\w]+(.这样的建筑[\w]+)*@([\w]+\。)+[a-z]{2,7}$/i(检查一个字符串是否是有效的电子邮件语法)可能会让胆小的人感到害怕。好消息是,这并不像看上去那么难。
语法和属性
假设您想在文本中搜索字符串 cat。您可以将它定义为两种不同格式的正则表达式:
// Expression literals; notice that you must not use quotation marks!
var searchTerm = /cat/;
// Object constructor
var searchTerm = new RegExp('cat');
如果通过 match()、search()、exec()或 test()方法对字符串使用此表达式,它将返回任何包含“cat”的内容,而不管它在字符串中的位置如何,比如 cat alog 、concatenation或scat。
如果您只想将单词“cat”作为一个字符串进行匹配,而周围没有任何其他内容,那么您需要使用^来表示开始,并使用$来表示结束:
var searchTerm = /^cat$/;
var searchTerm = new RegExp('^cat$');
您也可以省略开始指示符^或结束指示符美元。这将匹配 猫 、 猫 猫或 猫 天文望远镜:
var searchTerm = /^cat/;
var searchTerm = new RegExp('^cat');
下面的代码会找到极猫 或野 猫 :
var searchTerm = /cat$/;
var searchTerm = new RegExp('cat$');
如果您想要查找“cat”而不考虑大小写,例如,为了匹配 cat 、 Catherine 或 CAT ,您需要在第二个斜杠后使用 I 属性。这导致该案例被忽略:
var searchTerm=/cat/i;
var searchTerm=new RegExp('cat', 'i');
如果您有一个字符串,其中可能多次出现单词“cat”,并且您希望将所有匹配作为一个数组,那么您需要为全局添加参数 g:
var searchTerm = /cat/g;
var searchTerm = new RegExp('cat', 'g');
默认情况下,正则表达式只匹配单行字符串中的模式。如果您想要匹配多行字符串中的模式,请使用参数 m 来表示多行。您也可以混合使用它们,顺序并不重要:
var searchTerm = /cat/gim;
var searchTerm = new RegExp('cat', 'mig');
通配符搜索、约束范围和替代项
句点字符(。)在正则表达式中扮演小丑牌的角色;它代表“任何字符”(这可能会引起混淆,因为在高级 web 搜索中或在 DOS 和 UNIX 命令行中,它是星号,*。)
var searchTerm = /c.t/gim;
var searchTerm = new RegExp('c.t', 'mig');
这搭配猫、小床、 CRT ,甚至还有 c#t 和 c 这样的废话串!T ,或者包含空格的,如 c T 或 c\tt 。(记住\t 是制表符。)
对于您的需求来说,这可能太灵活了,这就是为什么您可以使用方括号将选择范围限制在您想要提供的范围内:
var searchTerm = /c[aou]t/gim;
var searchTerm = new RegExp('c[aou]t', 'mig');
你可以用这个正则表达式匹配 cat 、 cot 或者 cut 的所有大小写版本。您还可以在括号内提供 a-z 这样的范围来匹配所有小写字母,A-Z 匹配所有大写字母,0-9 匹配数字。
注意注意正则表达式匹配的是数字的字符,而不是它们的值。带有[0-9]的正则表达式将返回 0200 作为有效的四位数。
例如,如果您想查找一个小写字母紧跟着一个大写字母,您将使用
var searchTerm = /[a-z][A-Z]/g;
var searchTerm = new RegExp(' [a-z][A-Z] ', 'g');
您可以使用括号中的^字符从搜索中排除某个选项。例如,如果您想避免“剪切”,您可以使用
var searchTerm = /c[^u]t/g;
var searchTerm = new RegExp('c[^u]t', 'g');
括号一次只能匹配一个字符,这就是为什么你不能用这个表达式匹配像 cost 、 coast 或 cast 这样的东西。如果要匹配几个选项,可以在括号内使用管道字符(|),其功能类似于逻辑 OR:
var searchTerm = /c(^u|a|o|os|oas|as)t/g;
var searchTerm = new RegExp('c(^u|a|o|os|oas|as)t', 'g');
这现在匹配猫、床、成本、滑行和投,但不匹配切(因为^u).
用量词限制字符数
在许多情况下,您希望允许一定范围的字符,如 a 到 z,但是您希望限制它们的数量。为此,可以在正则表达式中使用量词,如表 9-1 所示。
表 9-1 。正则表达式中的量词符号
| 符号 | 可能的次数 |
|---|---|
| * | 0 或 1 次 |
| + | 一次或多次 |
| ? | 0 或 1 次 |
| {n} | n 次 |
| {n,m} | n 到 m 次 |
注添加问号(?)意味着正则表达式应该尽可能少地匹配它们。
例如,如果您想匹配由两组四个字符组成的序列号的语法,每组字符用破折号分隔,您可以使用
var searchTerm = /[a-z|0-9]{4}\-[a-z|0-9]{4}/gim;
var searchTerm = new RegExp(' [a-z|0-9]{4}\-[a-z|0-9]{4}', 'mig');
注意您需要对字面上使用的字符进行转义,而不是在正则表达式模式中具有任何特殊含义的字符,比如本例中的破折号。您可以通过在字符前加一个反斜杠来做到这一点。需要转义的字符有-、+、/、(、)、[、]、*、{、}和?。比如/c.t/匹配 cat 或 cot 或 c4t ,而/c\t/仅匹配 c.t 。
单词边界、空白和其他快捷键
所有这些不同的选项都会导致非常复杂的正则表达式,这就是为什么有一些快捷符号可用的原因。你可能还记得在第二章中空格的特殊符号,比如\n 代表换行符,\t 代表制表符。正则表达式也是如此,如表 9-2 所示。
表 9-2 。正则表达式的快捷表示法
| 符号 | 等效符号 | 意为 |
|---|---|---|
| \d | [0-9] | 仅数字(整数) |
| \D | [⁰-9] | 除数字(整数)以外的所有字符 |
| \w | [a-zA-Z0-9 的声音] | 所有字母数字字符和下划线 |
| \W | 【阿扎依采夫-Z0-9】【阿扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫 | 所有非字母数字字符 |
| \b | 不适用的 | 单词边界 |
| \B | 不适用的 | 非单词边界 |
| \s | [\t\n\r\f\v] | 所有空白 |
| \S | [^\t\n\r\f\v] | 没有空白 |
例如,如果您想测试一个美国社会保险号,它是一个九位数字,第三位和第五位后面有破折号(例如,456-33-1234),您可以使用下面的正则表达式,带有可选的破折号(使用?量词),因为用户可能不会输入它们:
var searchTerm = /[0-9]{3}\-?[0-9]{2}\-?[0-9]{4}/;
var searchTerm = new RegExp('[0-9]{3}\-?[0-9]{2}\-?[0-9]{4}', ");
或者,您可以使用数字的快捷表示法:
var searchTerm = /\d{3}\-?\d{2}\-?\d{4}/;
var searchTerm = new RegExp('\\d{3}\-?\\d{2}\-?\\d{4}', ");
请注意,如果您在引号内或构造函数符号中使用快捷符号,您需要在它们前面加上双反斜杠,而不是单反斜杠,因为您需要对它们进行转义!有了这些知识,您应该能够编写自己的正则表达式。作为证明,让我们回到本节开始段落中的例子:
var validEmail = /^[\w]+(\.[\w]+)*@([\w]+\.)+[a-z]{2,7}$/i
电子邮件可以非常简单,比如 me@example.com,或者更复杂,比如 ??。这个正则表达式应该将两者都作为有效的电子邮件返回。
它测试字符串是否以一组由一个或多个单词字符组成的^[\w]+开头,后跟一组由 0 个或多个单词字符组成的句点(\。[\w]+)*,在@符号之前。在@符号之后,字符串可能有一组或多组一个或多个单词字符,后跟一个句点,([\w]+\。)+,它将以一个包含 2 到 7 个字符的字符串结束。最后一个字符串是域名,可以是短的,如 de,也可以是长的,如 name 或 museum。请注意,通过允许几个单词后跟一个句点,您还可以确保像 user@open.ac.uk 这样的电子邮件能够被识别。
使用正则表达式的方法
有几种方法将正则表达式作为参数。表达式本身——斜杠或 RegExp 构造函数中的内容——被称为模式,因为它匹配您想要检索或测试的内容。
- instance.test(string):测试字符串是否与模式匹配,并返回 true 或 false。
- instance.exec(string):匹配字符串和模式一次,并返回匹配数组或 null。
- instance.match(pattern):匹配字符串和模式,并将匹配结果作为字符串数组或 null 返回。
- instance.search(pattern):匹配字符串和模式,并返回正确匹配的位置。如果字符串不匹配任何模式,搜索返回–1。
- instance.replace(pattern,replaceString):根据模式匹配字符串,并用 replaceString 替换每个正匹配。
- instance.split(pattern,limit):将字符串与模式匹配,并将其拆分为一个数组,模式匹配周围的子字符串作为数组项。可选的 limit 参数减少了数组元素的数量。
括号分组的威力
你可能记得,要对一个表达式进行分组,需要使用圆括号,()。这不仅对模式进行分组,还将结果存储在特殊变量中,供以后使用。当您将它与 replace()方法结合使用时,这尤其方便。结果存储在名为9 的变量中,这意味着您可以在每个正则表达式中使用多达九个括号分组。如果要将某个组排除在外,可以在它前面加上?:.
例如,如果您有一个格式为姓、名的姓名列表,并且您想要将列表中的每个条目转换为名姓格式,您可以执行以下操作:
示例名称顺序.html
names=[
'Reznor, Trent',
'Eldritch, Andrew',
'Clark, Anne',
'Almond,Marc'
];
for(i = 0; i < names.length; i++) {
alert(names[i].replace(/(\w+)+,\s?(\w+)/g, '$2 $1'));
}
该模式匹配逗号前面的任何单词(后跟可选的空白字符)和后面的单词,并将两者都存储在变量中。替换字符串通过使用$变量颠倒顺序。
一个更复杂的例子是打印链接后面的内容区域的每个外部链接的 URL:
exampleShowURL.html
showURLs = function(){
var ct = document.getElementById('content');
var searchTerm = '(<a href="((?:http|https|ftp):\/\/ ';
searchTerm += '(?:[\\w]+\.)+[a-z]{2,7})">';
searchTerm += '(?:\\w|\\s|\\.)+<\/a>)';
var pattern = new RegExp(searchTerm, 'mgi');
ct.innerHTML = ct.innerHTML.replace(pattern, '$1 ($2) ');
}
您用一组括号开始这个模式,将整个结构存储在变量$1 中,并匹配一个链接< a href= "。接下来用一组括号将 href 属性中的内容括起来,href 属性是一个以 http、https 或 ftp 开头的 URL,不将这个组存储在变量中(因为这组括号的前面是?:),后面是一个冒号和两个斜杠(需要转义),后面是一个以域结尾的 URL。(这与电子邮件检查示例中使用的模式相同。)
您关闭括号,将链接的 href 属性中的所有内容存储在$2 中,并匹配链接元素中的所有内容(可以是一个或多个单词、空白或句点),但不将其存储在变量中。然后在关闭后关闭主组,使用 replace 将每一个环节都替换为模式匹配,这样实际上就把www.example.com>例< /a >变成了<a href =www.example.com>例(www.example.com)。
正则表达式资源
与任何编程语言一样,有许多方法可以达到相同的目标。我不想只给出复制和粘贴的例子,因为熟悉正则表达式会让你走得更远。
有许多在线资源根据它们的任务列出了模式:
- 正则表达式库(
regexlib.com/)有一个可搜索的模式数据库。 - 在 Regular-expressions . info(
www.regular-expressions.info/)网站上,你会找到非常广泛的关于正则表达式的教程。 - RegEx Advice(
regexadvice.com/)有一个很好的关于正则表达式的论坛和博客。
在书籍方面,有迈克尔·菲茨杰拉德(O'Reilly,2012)的介绍正则表达式、托尼·斯塔布尔宾(O'Reilly,2007)的正则表达式袖珍参考,以及杰弗里·弗里德尔(O'Reilly,2006)的非常广泛的掌握正则表达式**。
对于更倾向于 UNIX 的用户,有 Nathan A. Good (Apress,2005)的正则表达式方法:问题解决方法。
验证方法总结
在真实的脚本环境中,您不可能坚持使用前面的方法,而是混合使用它们来尽快达到您的目标。什么时候使用什么没有固定的规则,但是一些提示和好处可能值得记住:
- 正则表达式只匹配字符;您不能用它们进行计算(至少在 JavaScript 中不能;PHP 提供了 e 开关,它将匹配作为 PHP 代码进行评估)。
- 正则表达式具有独立于语言的优势——您可以在服务器端和客户端使用相同的规则。字符串和数学方法都固定在 JavaScript 中,在其他语言中可能不一样。
- 在正则表达式中匹配大范围的选项非常容易,字符串很快就会变得混乱,除非这些范围遵循简单的规则,如 A 到 Z 或 0 到 9。
- 如果你必须验证数字,大多数时候使用字符串或正则表达式验证是不值得的;坚持用数学方法测试这些值。字符串太宽容了,因为您不能用它们来比较值和进行计算。您仅有的选择是确定字符串长度和测试特殊字符。
- 使用他人开发的开箱即用的模式和方法并不可耻。其中许多已经由数十名开发人员在不同的开发环境中进行了测试。
约束验证
我们现在将讨论一些您可以在表单中使用的技术,以发现哪些字段需要验证,并告诉用户有些地方出错了。
HTML5 为表单添加了新的输入类型,并以新的方式添加了不需要 JavaScript 的验证。它还提供了一个 API(应用编程接口),允许开发人员扩展功能以更好地满足他们的需求。约束验证是让浏览器验证 web 表单内容的方法。
指定必填字段
有几种方法可以将表单元素指定为强制的并要求验证。HTML5 通过向表单字段添加必需的属性使这变得简单。Internet Explorer (IE) 10+,Safari 5,Firefox 16,Chrome 23,Opera 21.1 都支持这个。约束验证的一个好处是浏览器会为您进行验证,当字段为空时会显示一个警告。
输入标记具有所需的属性:
<input type="text" id="firstName" required title="First Name is Required! " >
当试图提交您的表单时,内置验证会根据您的浏览器向您发出类似于图 9-1 所示的警告。
图 9-1 。Chrome 在使用 validate 属性的字段上向用户显示警告的例子。Safari 5 和 6 部分支持 required 属性,因为它不会显示警告
通过添加 pattern 属性,可以将正则表达式添加到可能需要进行额外验证的字段中。
例如,您可能希望确保提供的 URL 仅适用于特定的域:
<input type="url" id="theURL" required title="URL is Required! " pattern="https?://(?:www\.)?twitter\.com/.+"/>
这个例子让用户输入她的 twitter 地址(www.twitter.com/username)。尽管该字段是必需的,但 pattern 属性为其提供了在有效之前需要遵循的附加规则。
附加验证属性
一种新的表单输入类型是数字。您可以使用此类型创建具有最小值和最大值的数字步进器。(参见图 9-2 。)如果您键入一个大于最大值的数字,然后尝试提交表单,内置验证将会发生。
图 9-2 。Chrome 在值高于 max 属性时显示警告
以下是数字输入类型的示例:
<input type="number" min="1" max="10" step="1">
placeholder 属性不做任何类型的验证,但是在指导用户如何填写表单时它很有用。结合像电子邮件这样的输入类型,它会非常有帮助。(参见图 9-3 。)
图 9-3 。Chrome 显示占位符文本和电子邮件地址无效的警告
以下是电子邮件输入类型的示例:
<input type="email" placeholder="Enter Email Address">
Novalidate 属性
如果您想要在提交时禁用节点验证,请使用以下命令:
<form novalidate>
<input type="text" required >
<input type="submit" value="Submit">
</form>
将 formnovalidate 属性添加到提交按钮将阻止表单在任何输入节点上执行任何验证:
<input type="submit" value="Submit" formnovaidate>
使用 CSS 伪类的附加用户反馈
HTML5 带来了一种一致的验证表单数据的方式,但是向用户指出错误的内置方式在不同的浏览器之间是不同的。要创建更加一致的体验,您可以使用新的级联样式表(CSS)类。
用新的伪 CSS 类可以让用户直观地知道某个字段是强制的。
exampleHTML5Required.html(节选)
<style>
input:required{
border: 1px solid #F00;
}
:optional{
background:#CCC;
}
</style>
<label>Your Email
<input required id="email" type="email" placeholder="Enter Email Address" >
</label>
<label>Message
<textarea id="message"></textarea>
</label>
<button>Submit</button>
在前面的例子中,您可以看到:required 和:optional 已经添加到 CSS 中。在第一个实例中,您要确保任何具有所需属性的输入标记都接收样式。在下一个实例中,它可以是任何不带有所需属性的字段。
当用户填写表单时,即时反馈也很有帮助。可以使用:valid 和:invalid 类让某人知道他们刚刚填写的字段的状态。在这个例子中,当用户移动到下一个字段时,您添加了交互的类。在字段“模糊”状态下,它将运行验证并显示红色或绿色边框,具体取决于信息是否有效。
exampleValidBlur.html(节选)
<style>
.interacted:invalid{
border: 1px solid red;
}
.interacted:valid {
border: 1px solid green;
}
</style>
<script>
function addInteractedClass(){
var inputs = document.getElementsByTagName("input");
for (var I = 0; i< inputs.lengh; i++){
inputs[i].addEventListener("blur", function(event){
event.target.classList.add("interacted");
},false);
}
}
document.addEventListener("DomContentLoaded", addInteractedClass, false);
</script>
<p><label for="name">Your Name</label></p>
<p><input required id="name" type="text" placeholder="Please Enter Your Name" ></p>
<p><label for="email">Your Email</label></p>
<p><input type="email" id="email" required ></p>
<p><input type="submit" id="send" valie="Send Form" ></p>
除了上两个示例中使用的 CSS 类之外,还有其他一些类可用:
- 在范围内
- 溢出
- 只读
- 读写
检测对 HTML5 表单属性的支持
并非所有的浏览器都支持每一种新的输入类型。不这样做的浏览器会忽略你给它的类型,就好像类型被设置为“文本”
要检查浏览器是否支持您正在寻找的属性,您可以编写两个函数。第一个将在 DOM 加载时运行,并执行 if 语句。这将调用第二个函数,将属性名和字段作为其参数。
第二个函数基于字段类型创建一个元素,如果属性在该元素中,则返回值 true 或 false。结果返回给第一个函数,它将完成条件语句,并知道应该使用内置浏览器函数还是 JavaScript。
function initFormCheck(){
if(testHTML5Attr("required","input")){
//use built in browser validation
}else{
//use JavaScript fall back
}
}
function testHTML5Attr(attr,elm){
return attr in document.createElement(elm);
}
document.addEventListener("DOMContentLoaded" initFormCheck, false);
约束验证 API
因为浏览器正在做大量的验证工作,所以在向服务器发送任何数据之前,检查信息输入是否正确变得很容易。约束验证 API(www . whatwg . org/specs/we b-apps/current-work/# constraint-validation)向 DOM 节点添加属性和方法,这些属性和方法可以在纯 JavaScript 解决方案中使用。
这一点也很重要,因为尽管 Safari 6 支持验证,但当所需的属性存在时,它不会禁止提交数据。
此示例使用 email 字段的 valid 属性来检查电子邮件地址的格式是否正确,并检查地址是否匹配。
exampleCheckEmail.html(节选)
<script>
function emailCheck(){
var email1 = document.getElementById("email1");
var email2 = document.getElementById("email2");
var resultDiv = document.getElementById("result");
var form = document.getElementById("emailForm");
form.addEventListener("submit", function(event){
if(email1.validity.valid && email2.validity.valid && email1.value && email2.value){
resultDiv.innerHTML = "<p>Email is valid and they match</p>";
}else{
resultDiv.innerHTML = "<p>Email is not valid or they do not match</p>";
}
event.preventDefault();
}, false);
}
document.addEventListener("DomContentLoaded", addInteractedClass, false);
</script>
显示错误字段的列表
该方法向用户显示包含错误的字段列表。(参见图 9-4 。)除了添加边框以显示字段是否有效之外,您还将遍历表单字段以生成所有无效字段的列表。
图 9-4 。显示错误字段的列表
页面加载后,向表单添加一个事件侦听器。当表单将要提交数据时,您停止表单并调用 checkInputs 函数。
checkInputs 函数首先查看一个名为 resultsDiv 的 Div,在这里向用户提供反馈并清空该 div 中的所有节点。然后,它遍历所有的输入标签,并检查每个标签的有效属性。
如果标记无效,它将获取 id,通过 reformatName 函数请求格式正确的名称,并继续创建一个段落节点,用文本表示字段的名称无效,并将这个新节点添加到 resultsDiv 中。
showInvalidFields.html(节选)
<script>
function formCheck(){
var form = document.getElementById("userForm");
form.addEventListener("submit", function(event){
checkInputs();
event.preventDefault();
}, false);
}
function checkInputs(){
var resultDiv = document.getElementById("result");
resultDiv.hidden = true;
var inputs = document.getElementsByTagName("input");
while(resultDiv.hasChildNodes()){
resultDiv.removeChild(resultDiv.firstChild);
}
for(var i = 0; i < inputs.length; i++){
inputs[i].classList.add("interacted");
if(!inputs[i].validity.valid){
var para = document.createElement("p");
var formatedName = reformatName(inputs[i].id);
var msg = document.createTextNode(formatedName + " is invalid.");
para.appendChild(msg);
resultDiv.appendChild(para);
resultDiv.hidden = false;
}
}
}
function reformatName(oldName){
switch(oldName){
case "firstName": return "First Name";
break;
case "lastName": return "Last Name";
break;
case "email": return "Email";
break;
case "phone": return "Phone";
break;
}
document.addEventListener("DomContentLoaded", formCheck, false);
</script>
其他动态验证方法
当用户更改字段时,让每个字段立即生效是非常诱人的,使用 Ajax 和适当的后端数据集或功能可以做很多事情。一个很好的例子是,在输入表单时,建议哪些数据是有效的。
谷歌是网络表单做到这一点的第一个例子。它为你提供了其他用户已经完成的搜索,有许多可能的结果,如图 9-5 所示。
图 9-5 。当你输入时,谷歌显示可能的结果
使用 HTML5,向您的站点添加自动完成功能变得很容易。首先,我将介绍它是如何工作的,然后创建一个 JavaScript 版本,它可以被更新以从服务器获取数据。
autoComplete.html(节选)
<input type="text" name="srch" id="srch" list="datalist1">
<datalist id="datalist1">
<option value="Bill Gates">
<option value="Linus Torvalds">
<option value="Douglas Coupland">
<option value="Ridley Scott">
<option value="George Lucas">
<option value="Dan Akroyd">
<option value="Sigourney Weaver">
<option value="Tim Burton">
<option value="Katie Jane Garside">
<option value="Winona Ryder">
<option value="Vince Clarke">
<option value="Martin Gore">
<option value="Kurt Harland Larson">
<option value="Paul Robb">
<option value="James Cassidy">
<option value="David Tennant">
</datalist>
在这个例子中,一个新的属性被添加到输入标签中。list 属性指向新的 datalist 元素。该元素包含预定义的选项,当用户开始在字段中输入时将显示这些选项,如图 9-6 所示。
图 9-6 。提供数据列表中的数据
既然我已经介绍了基础知识,那么创建一个动态示例只需要使用 JavaScript 为我们创建 datalist。如果您可以使用服务器端脚本来提供数据,那么可以通过使用以前课程中的一些 AJAX 技术来调整这个示例。
dynamicAutoComplete.html(节选)
<script>
function createDataList(){
var nameArray = new Array();
nameArray[0] = "Bill Gates";
nameArray[1] = "Linus Torvalds";
nameArray[2] = "Douglas Coupland";
nameArray[3] = "Ridley Scott";
nameArray[4] = "George Lucas";
nameArray[5] = "Dan Akroyd";
nameArray[6] = "Sigourney Weaver";
nameArray[7] = "Tim Burton";
nameArray[8] = "Katie Jane Garside";
nameArray[9] = "Winona Ryder";
nameArray[10] = "Martin Gore";
nameArray[11] = "Kurt Harland Larson";
nameArray[12] = "Paul Robb";
nameArray[13] = "James Cassidy";
nameArray[14] = "David Tennant";
var dataList = document.createElement("datalist");
dataList.id = "datalist1";
document.body.appendChild(dataList);
for(var i = 0; i<nameArray.length; i++){
var option = document.createElement("option");
option.value = nameArray[i];
dataList.appendChild(option);
}
}
document.addEventListener("DOMContentLoaded", createDataList, false);
</script>
不支持约束验证的浏览器
正如你所看到的,让浏览器来做繁重的工作是一件好事。这确实排除了旧的浏览器,但是有办法处理它们。
在服务器端实现验证增加了第二层保护。如果您的浏览器不支持约束验证,那也没关系,因为服务器会处理它。
Polyfills 是 JavaScript 库,允许您继续使用最新的浏览器改进,并且它们增加了对尚未本机使用它们的浏览器的支持。Paul Irish 维护的 polyfills 有一长串:github . com/Modernizr/Modernizr/wiki/html 5-Cross-Browser-poly fills。
Webshims 是 polyfills 的集合,也包括约束验证 API。从 github(【afarkas.github.com/webshim/dem… webshims 库并将 JavaScript 添加到您的页面中,这将使您能够将 HTML5 语法添加到您的页面中,并且仍然可以让旧浏览器正常响应。
下面是一个向页面添加 webshims 的示例:
<script src="js/jquery-1.8.2.js"></script>
<script src="js/modernizr-yepnope-custom.js"></script>
<script src="js-webshim/minified/polyfiller.js"></script>
<script>jQuery.webshims.polyfill('forms');</script>
<form>
<input type="text" requred>
<input type="submit" value="submit">
</form>
摘要
我希望您对编写正则表达式和使用约束验证有足够的信心。HTML5 让您不必编写复杂的脚本来验证数据。
使用约束验证 API,您可以确保数据有效,并使用新的 CSS 伪类提供可视化反馈,除了 JavaScript 之外,这些伪类还可以应用于表单元素。
这并不意味着您不应该在服务器端进行验证。许多服务器端框架都内置了验证,所以没有理由不使用它。
Polyfills 使您能够在不支持新 HTML5 方法的旧浏览器上使用这些新方法。随着人们升级他们的浏览器,polyfill 将把新的功能留给浏览器。所以你有办法为大量用户创造一致的体验。
在下一章中,我们将进行一个更大的项目,创建一个由后端驱动的动态图库,并加入 CSS、JavaScript 和 Ajax。
十、现代 JavaScript 案例研究:动态图库
在本章中,你将学习如何开发一个由 PHP 脚本支持的 JavaScript 增强的缩略图库。您将从学习与静态图库相关的技术以及如何增强它们开始。然后,您将继续学习使用 PHP 和 Ajax 从服务器动态获取图像的图库。
注你可以在
www.beginningjavascript.com下载本章演示代码或者在线查看结果。因为这一章包含了图片库,所以下载在较大的一边,但是它允许你在本地服务器上看到所有的代码——包括服务器端的 PHP。
缩略图基本知识
让我们从基础开始,计划我们的缩略图画廊。我考虑了很长时间是否应该在这本书里包含一个,因为 JavaScript 和 CSS 书籍有图库作为例子几乎已经成为陈词滥调。然而,我写这一章是为了举例说明如何用现代脚本和 CSS 来丰富一个非常普通的解决方案,比如缩略图画廊,并且独立于它们。许多例子——尤其是只有 CSS 的图库——看起来很棒,在现代浏览器中也能工作;然而,它们不能很好地降解,也不能真正提供缩略图画廊应该提供的东西。
什么是缩略图画廊,它应该做什么?
缩略图画廊的想法可以追溯到浏览器开始支持图像的时代,当时网络连接速度可以用千比特来衡量。这种图库的工作过去是,现在仍然是,通过提供图库中每个图像的较小预览,给出可用图像的概述。“更小”意味着尺寸更小,但也是最重要的是文件大小更小。这意味着只对您图库中的一张图片感兴趣的访问者无需下载所有图片,只需下载他感兴趣的那张即可,既节省了他的时间,也节省了您的服务器流量。许多纯 CSS 或 JavaScript/HTML 缩略图画廊没有做到这一点,并假设每个用户都想下载一张图片。你可以提供所有图片的下载,但这应该是一个选项,而不是一个要求。最差的缩略图画廊通过 HTML 属性或 CSS 将照片调整为缩略图,从而迫使访问者下载大图像,以看到质量差的缩略图。通过改变图像在 CSS 中的尺寸、通过 JavaScript 或使用 HTML 属性来调整图像的大小不会产生高质量的缩略图;这简直是懒惰和一个坏主意。
如果你想提供缩略图画廊在他们原来的意义上,你需要为你想显示的大图像生成较小的缩略图。您可以在上传图库之前进行批处理,也可以在服务器上通过脚本运行。
提示有很多缩略图生成和批量生成工具可用。好的——最重要的是,免费的——是谷歌的 Picasa(在 picasa.google.com/的[有售)和 IrfanView(在 www.irfanview.com/](picasa.google.com/)的[有售)。使用 PHP 和 GD 库可以很容易地在服务器上生成缩略图。我已经写了一篇关于如何做到这一点的文章,可以在](www.irfanview.com/)[`icant.co…](icant.co.uk/articles/ph…](phpthumb.sourceforge.net/)可以获得一个很棒的预… PHP 类 phpThumb()。因为这是一本关于 JavaScript 的书,所以我不会深入讨论通过 PHP 生成图像的细节,尽管它对于在线画廊来说非常方便。
静态缩略图库
传统的缩略图画廊提供表格或列表中的小缩略图。每个缩略图链接到一个带有大图像的页面,反过来,链接回缩略图画廊或提供上一个和下一个图像链接。
如果有很多图像,缩略图页面可以分页,一次显示一定数量的缩略图,并提供整个集合的向前和向后导航。对于纯静态图库,这意味着您必须生成所有缩略图页面,每张照片一个缩略图页面,这在开始时需要做大量工作,并且在图库每次更新时需要向服务器传输大量文件。
用 JavaScript 伪造动态图库
通过对所有缩略图应用事件处理程序,您可以使用 JavaScript 将静态缩略图画廊转变为看似动态的画廊。当一个缩略图被点击时,你用一个包含大图像的新元素覆盖缩略图。通过将缩略图链接到大图并简单地在浏览器中显示,保持非 JavaScript 用户可以访问图库:
exampleFakeDynamic.html(节选)
<ul id="thumbs">
<li>
<a href="galleries/animals/dog2.jpg">
<img src="galleries/animals/tn_dog2.jpg" alt="tn_dog2.jpg">
</a>
</li>
<li>
<a href="galleries/animals/dog3.jpg">
<img src="galleries/animals/tn_dog3.jpg" alt="tn_dog3.jpg">
</a>
</li>
<li>
<a href="galleries/animals/dog4.jpg">
<img src="galleries/animals/tn_dog4.jpg" alt="tn_dog4.jpg">
</a>
</li>
[... more thumbnails ...]
</ul>
提示你也可以使用表格或定义列表作为缩略图库,因为表格会降级得更好,因为即使在非 CSS 浏览器中它们仍然是多列结构,而且定义列表在语义上也是正确的。对于本章中的例子,我使用了一个简单的列表来保持简单,并允许缩略图占据屏幕上尽可能多的空间。
您可以通过打开演示 exampleFakeDynamic.html 来测试效果。让我们从脚本的框架开始,一步一步地了解功能:
fakeDynamic.js (skeleton)
fakegal = {
// IDs
thumbsListID : 'thumbs',
largeContainerID : 'photo',
// CSS classes
closeClass : 'close',
nextClass : 'next',
prevClass : 'prev',
hideClass : 'hide',
showClass : 'show',
// Labels
closeLabel : 'close',
prevContent : '<img src="last.jpg" alt="previous photo" >',
nextContent : '<img src="next.jpg" alt="next photo">',
init : function(){ },
createContainer : function(){},
showPic : function(e){ },
setPic : function(pic){ },
navPic : function(e){ }
DOMhelp.addEvent(window, 'load', fakegal.init, false);
你需要
- 包含所有缩略图的元素的 ID
- 分配给大图片容器的 ID
- CSS 类,用于移除大图片的链接
- 浏览大图的链接
- 显示和隐藏元素的类
- 告诉用户链接隐藏了大图的标签
- 下一个和上一个图片链接的标签
在方法方面,你需要
- 一种初始化功能的方法
- 最初创建图像容器的实用程序方法
- 一种显示图片的方法
- 设置要显示的图片的方法
- 导航到下一张或上一张图片的方法
设置要显示的图片的方法 setPic()是必需的,因为显示方法 showPic()和导航方法 navPic()都会更改容器中的图像。
fakeDynamic.js
fakegal = {
// IDs
thumbsListID : 'thumbs',
largeContainerID : 'photo',
// CSS classes
closeClass : 'close',
nextClass : 'next',
prevClass : 'prev',
hideClass : 'hide',
showClass : 'show',
// Labels
closeLabel : 'close',
prevContent : '<img src="last.jpg" alt="previous photo">',
nextContent : '<img src="next.jpg" alt="next photo" >',
init:function() {
if(!document.getElementById || !document.createTextNode) {
return;
}
fakegal.tlist = document.getElementById(fakegal.thumbsListID);
if(!fakegal.tlist){ return; }
var thumbsLinks = fakegal.tlist.getElementsByTagName('a');
fakegal.all = thumbsLinks.length;
for(var i = 0 ; i < thumbsLinks.length; i++) {
DOMhelp.addEvent(thumbsLinks[i], 'click', fakegal.showPic, false);
thumbsLinks[i].onclick = DOMhelp.safariClickFix;
thumbsLinks[i].i = i;
}
fakegal.createContainer();
},
init()方法测试 DOM 是否受支持,并检索包含缩略图的元素。然后,在将所有链接的数量存储在一个名为 all 的属性中之后,它遍历所有链接。(这在后面是必要的,以避免最后一个图像上的下一个链接。)它将指向 showPic()的事件处理程序应用于缩略图列表中的每个链接,并在调用 createContainer()将必要的图像容器元素添加到文档之前,将其索引号存储在名为 I 的新属性中。
fakeDynamic.js(续)
createContainer : function() {
fakegal.c = document.createElement('div');
fakegal.c.id = fakegal.largeContainerID;
通过创建一个新的 DIV 元素,将它存储在一个名为 c 的属性中,并为它分配一个大的图像容器 ID,来启动 createContainer()方法。
fakeDynamic.js(续)
var p = document.createElement('p');
var cl = DOMhelp.createLink('#', fakegal.closeLabel);
cl.className = fakegal.closeClass;
p.appendChild(cl);
DOMhelp.addEvent(cl, 'click', fakegal.setPic, false);
cl.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(p);
创建一个新段落,并在其中插入一个链接,将 closeLabel 作为文本内容。将指向 setPic()的事件处理程序分配给链接,应用 Safari 修复,并将段落添加到容器元素。
fakeDynamic.js(续)
var il = DOMhelp.createLink('#', ' ');
DOMhelp.addEvent(il, 'click', fakegal.setPic, false);
il.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(il);
现在,使用调用 setPic()的事件处理程序向容器添加另一个空链接。这个链接以后会围绕在大图的周围,使它可以点击,这样键盘用户就可以去掉它。
fakeDynamic.js(续)
fakegal.next = DOMhelp.createLink('#', ' ');
fakegal.next.innerHTML = fakegal.nextContent;
fakegal.next.className = fakegal.nextClass;
DOMhelp.addEvent(fakegal.next, 'click', fakegal.navPic, false);
fakegal.next.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(fakegal.next);
fakegal.prev = DOMhelp.createLink('#', ' ');
fakegal.prev.innerHTML = fakegal.prevContent;
fakegal.prev.className = fakegal.prevClass;
DOMhelp.addEvent(fakegal.prev, 'click', fakegal.navPic, false);
fakegal.prev.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(fakegal.prev);
还需要添加两个链接来分别显示前一个和下一个图像,两个链接的事件处理程序都指向 navPic()。
fakeDynamic.js(续)
fakegal.tlist.parentNode.appendChild(fakegal.c);
}
将新容器添加到缩略图列表的父节点,然后就可以开始展示了。
fakeDynamic.js(续)
showPic : function(e) {
var t = DOMhelp.getTarget(e);
if(t.nodeName.toLowerCase() != 'a') {
t = t.parentNode;
}
fakegal.current = t.i;
var largePic = t.getAttribute('href');
fakegal.setPic(largePic);
DOMhelp.cancelClick(e);
},
在事件侦听器方法 showPic()中,检索目标并通过测试节点名来确定它是否真的是一个链接。然后将分配给 init()方法中每个缩略图链接的 I 属性存储为主对象的新属性 current 的值,以告知所有其他方法当前显示的是哪张图片。在通过 cancelClick()停止浏览器跟踪链接之前,检索链接的 href 属性,并使用 href 作为参数调用 setPic()方法。
fakeDynamic.js(续)
setPic : function(pic) {
var a;
var picLink = fakegal.c.getElementsByTagName('a')[1];
picLink.innerHTML = ' ';
setPic()方法获取图像容器中的第二个链接(结束链接之后的链接),并通过将其 innerHTML 属性设置为空字符串来删除该链接可能包含的任何内容。这是必要的,以避免有一个以上的图片显示在同一时间。
fakeDynamic.js(续)
if(typeof pic == 'string') {
fakegal.c.className = fakegal.showClass;
var i = document.createElement('img');
i.setAttribute('src' , pic);
picLink.appendChild(i);
您将参数 pic 的类型与 string 进行比较,因为该方法可以使用 URL 作为参数来调用,也可以不使用 URL。如果有一个参数是有效的字符串,您可以将 show 类添加到容器中,以便向用户显示它,并添加一个新的图像,将 pic 参数作为它的源。
fakeDynamic.js(续)
} else {
fakegal.c.className = ' ';
}
如果没有 string 类型的参数,则从图片容器中移除任何类,从而隐藏它。
fakeDynamic.js(续)
a = fakegal.current == 0 ? 'add' : 'remove';
DOMhelp.cssjs(a, fakegal.prev, fakegal.hideClass);
a = fakegal.current == fakegal.all-1 ? 'add' : 'remove';
DOMhelp.cssjs(a, fakegal.next, fakegal.hideClass);
},
测试主对象的当前属性是否等于 0,如果是,则隐藏上一个图片链接。对下一个图片链接做同样的操作,并将当前的与所有缩略图的数量(存储在 all 中)进行比较。通过添加或删除 Hide 类来隐藏或显示每个链接。
fakeDynamic.js(续)
navPic : function(e) {
var t = DOMhelp.getTarget(e);
if(t.nodeName.toLowerCase() != 'a') {
t = t.parentNode;
}
var c = fakegal.current;
if(t == fakegal.prev) {
c -= 1;
} else {
c += 1;
}
fakegal.current = c;
var pic = fakegal.tlist.getElementsByTagName('a')[c];
fakegal.setPic(pic.getAttribute('href'));
DOMhelp.cancelClick(e);
}
}
DOMhelp.addEvent(window, 'load', fakegal.init, false);
检索对所单击链接的引用(通过 DOMhelp 的 getTarget()获取事件目标,并确保 nodeName 是 A),并通过将此节点与 prev 属性中存储的节点进行比较来确定该链接是否是上一个链接。根据激活的链路增加或减少电流。然后用新的当前链接的 href 属性调用 setPic(),通过调用 cancelClick()阻止浏览器跟随激活的链接。
剩下的就是添加一个样式表;结果可能如图 10-1 所示。
图 10-1 。使用 JavaScript 模拟服务器控制的动态图库
显示字幕
缩略图画廊是视觉构造,但考虑替代文本和图像标题仍然是一个好主意。这不仅可以让盲人用户访问你的图库,还可以通过搜索引擎搜索缩略图数据并建立索引。
许多工具,如谷歌的 Picasa,允许动态字幕和添加替代文本。您可以使用 XHR 创建类似的东西,但是因为这是一本关于 JavaScript 的书,并且如何在服务器上存储输入的数据需要一些解释,所以这不是一个相关的例子。相反,让我们修改“假”画廊,使其显示标题。
您将使用图像的标题属性作为图像的标题;这意味着静态 HTML 需要适当的替代文本和标题数据。
examplefakedyname . html(excerpt)
<ul id="thumbs">
<li>
<a href="galleries/animals/dog2.jpg">
<img src="galleries/animals/tn_dog2.jpg" title="This square is mine" alt="Dog in a shady square">
</a>
</li>
<li>
<a href="galleries/animals/dog3.jpg">
<img src="galleries/animals/tn_dog3.jpg" title="Sleepy bouncer" alt="Dog on the steps of a shop">
</a>
</li>
[... More thumbnails ...]
</ul>
剧本本身没必要改动太多;它所需要的只是在生成的图像容器中添加一个额外的段落,并修改将标题和可选文本数据发送到大图像容器的方法。
fakeddynamically . js
fakegal = {
// IDs
thumbsListID : 'thumbs',
largeContainerID : 'photo',
// CSS classes
closeClass : 'close',
nextClass : 'next',
prevClass : 'prev',
hideClass : 'hide',
closeLabel : 'close',
captionClass : 'caption',
// Labels
showClass : 'show',
prevContent : '<img src="last.jpg" alt="previous photo">',
nextContent : '<img src="next.jpg" alt="next photo">',
第一个变化是修饰性的:您添加了一个将应用于标题的新 CSS 类。
fakeDynamicAlt.js(续)
init : function() {
if(!document.getElementById || !document.createTextNode) {
return;
}
fakegal.tlist = document.getElementById(fakegal.thumbsListID);
if(!fakegal.tlist) { return; }
var thumbsLinks = fakegal.tlist.getElementsByTagName('a');
fakegal.all = thumbsLinks.length;
for(var i = 0; i < thumbsLinks.length; i++) {
DOMhelp.addEvent(thumbsLinks[i], 'click', fakegal.showPic, false);
thumbsLinks[i].onclick = DOMhelp.safariClickFix;
thumbsLinks[i].i = i;
}
fakegal.createContainer();
},
showPic : function(e) {
var t = DOMhelp.getTarget(e);
if(t.nodeName.toLowerCase() != 'a') {
t = t.parentNode;
}
fakegal.current = t.i;
var largePic = t.getAttribute('href');
var img = t.getElementsByTagName('img')[0];
var alternative = img.getAttribute('alt');
var caption = img.getAttribute('title');
fakegal.setPic(largePic, caption, alternative);
DOMhelp.cancelClick(e);
},
init()方法保持不变,但是 showPic()方法需要读取图像的可选文本和 title 属性,以及链接的 href 属性,并将这三个属性作为参数发送给 setPic()。
fakeDynamicAlt.js(续)
setPic : function(pic, caption, alternative) {
var a;
var picLink = fakegal.c.getElementsByTagName('a')[1];
picLink.innerHTML = ' ';
fakegal.caption.innerHTML = ' ';
if(typeof pic == 'string') {
fakegal.c.className = fakegal.showClass;
var i = document.createElement('img');
i.setAttribute('src', pic);
i.setAttribute('alt' ,alternative);
picLink.appendChild(I);
} else {
fakegal.c.className = ' ';
}
a = fakegal.current == 0 ? 'add' : 'remove';
DOMhelp.cssjs(a, fakegal.prev, fakegal.hideClass);
a = fakegal.current == fakegal.all-1 ? 'add' : 'remove';
DOMhelp.cssjs(a, fakegal.next, fakegal.hideClass);
if(caption != ' ') {
var ctext = document.createTextNode(caption);
fakegal.caption.appendChild(ctext);
}
},
setPic()方法现在接受三个参数,而不是一个——大图片的来源、标题和可选文本。该方法需要删除任何可能已经可见的标题,设置大图片的可选文本属性,并显示新标题。
fakeDynamicAlt.js(续)
navPic : function(e) {
var t = DOMhelp.getTarget(e);
if(t.nodeName.toLowerCase() != 'a') {
t = t.parentNode;
}
var c = fakegal.current;
if(t == fakegal.prev) {
c -= 1;
} else {
C += 1;
}
fakegal.current = c;
var pic = fakegal.tlist.getElementsByTagName('a')[c];
var img = pic.getElementsByTagName('img')[0];
var caption = img.getAttribute('title');
var alternative = img.getAttribute('alt');
fakegal.setPic(pic.getAttribute('href'), caption, alternative);
DOMhelp.cancelClick(e);
},
navPic()方法与 init()方法一样,需要检索大图片的可选文本、标题和来源,并将它们发送给 setPic()。
fakeDynamicAlt.js(续)
createContainer : function() {
fakegal.c = document.createElement('div');
fakegal.c.id = fakegal.largeContainerID;
var p = document.createElement('p');
var cl = DOMhelp.createLink('#', fakegal.closeLabel);
cl.className = fakegal.closeClass;
p.appendChild(cl);
DOMhelp.addEvent(cl, 'click', fakegal.setPic, false);
cl.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(p);
var il = DOMhelp.createLink('#', ' ');
DOMhelp.addEvent(il, 'click', fakegal.setPic, false);
il.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(il);
fakegal.next = DOMhelp.createLink('#', ' ');
fakegal.next.innerHTML = fakegal.nextContent;
fakegal.next.className = fakegal.nextClass;
DOMhelp.addEvent(fakegal.next, 'click', fakegal.navPic, false);
fakegal.next.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(fakegal.next);
fakegal.prev = DOMhelp.createLink('#', ' ');
fakegal.prev.innerHTML = fakegal.prevContent;
fakegal.prev.className = fakegal.prevClass;
DOMhelp.addEvent(fakegal.prev, 'click', fakegal.navPic, false);
fakegal.prev.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(fakegal.prev);
fakegal.caption = document.createElement('p');
fakegal.caption.className = fakegal.captionClass;
fakegal.c.appendChild(fakegal.caption);
fakegal.tlist.parentNode.appendChild(fakegal.c);
}
}
DOMhelp.addEvent(window, 'load', fakegal.init, false);
createContainer()方法只需要一个小改动,即在容器中创建一个新段落来存放标题。
如您所见,用 JavaScript 创建动态图库意味着生成大量 HTML 元素,并读写大量属性。这是由你来决定是否值得争论。当您想要提供缩略图分页时,情况会变得更糟。
不要用 JavaScript 做所有这些,你可以在后端做(例如,用 PHP 或 Ruby on Rails 或 Node.js ),为所有用户提供一个全功能的图库,并且只通过 XHR 和 JavaScript 改进它。
动态缩略图画廊
真正的动态缩略图画廊使用 URL 参数,而不是大量的静态页面,并根据这些参数创建分页和显示缩略图或大图像。
演示 examplePHPgallery.php 就是这样工作的,图 10-2 显示了它可能的样子。
图 10-2 。一个动态的 PHP 驱动的缩略图库示例,具有缩略图分页和大图片页面上的上一张和下一张图像预览
这个图库功能齐全,无需 JavaScript 即可访问,但是您可能不希望每次用户单击缩略图时都重新加载整个页面。使用 XHR,你可以两者兼得。您没有使用原始的 PHP 文档,而是使用了一个精简版本,只生成您需要的内容——在本例中是 gallerytools.php。我不会深入 PHP 脚本的细节;可以说它为您做了以下事情:
- 它读取主菜单中的链接指向的文件夹的内容,检查它是否有图像,并一次返回 10 个 HTML 列表,缩略图链接到大图像。
- 它增加了一个分页菜单,显示总共显示了多少张图片中的哪十张,并提供了上一页和下一页的链接。
- 如果单击任何缩略图,它会返回大图像的 HTML 和显示下一个和上一个缩略图的菜单。
您使用这个输出来覆盖原始 PHP 脚本的 HTML 输出,如演示 examplePHPXHRgallery.php 中所示。没有 JavaScript,它做的和 examplePHPgallery.php 一样;但是,当 JavaScript 可用时,它不会重新加载整个文档,而只会刷新图库本身。您可以通过将内容部分的链接替换为 gallerytools.php 和 XHR 电话的链接来实现这一点,而不是重新加载整个页面。
dyngal_xhr.js
dyngal = {
contentID : 'content',
originalPHP : 'examplePHPXHRgallery.php',
dynamicPHP : 'gallerytools.php',
init : function() {
if(!document.getElementById || !document.createTextNode) {
return;
}
dyngal.assignHandlers(dyngal.contentID);
},
首先定义您的属性:
- 元素的 ID,该元素包含应该用从 gallerytools.php 返回的 HTML 替换的内容
- 原始脚本的文件名
- 返回通过 XHR 调用的数据的脚本的文件名
init()方法测试 DOM 支持,并使用内容元素 ID 作为参数调用 assignHandlers()方法。
注意在这种情况下,你只替换一个内容元素;但是,因为可能会出现需要替换页面的许多部分的情况,所以为这样的任务创建单独的方法是一个好主意。
dyngal_xhr.js(续)
assignHandlers : function(o) {
if(!document.getElementById(o)){ return; }
o = document.getElementById(o);
var gLinks = o.getElementsByTagName('a');
for(var i = 0; i < gLinks.length; i++) {
DOMhelp.addEvent(gLinks[i], 'click', dyngal.load, false);
gLinks[i].onclick = DOMhelp.safariClickFix;
}
},
方法测试 ID 作为参数发送的元素是否存在,然后遍历元素中的所有链接。接下来,它添加一个指向 load 方法的事件处理程序,并应用 Safari 修复程序来阻止浏览器跟踪原始链接。(记住 cancelClick()中使用的 preventDefault()方法是 Safari 支持的,但是由于 Safari 中的一个 bug,它并不能阻止链接被关注。)
dyngal_xhr.js(续)
load : function(e) {
var t = DOMhelp.getTarget(e);
if(t.nodeName.toLowerCase() != 'a') {
t = t.parentNode;
}
var h = t.getAttribute('href');
h = h.replace(dyngal.originalPHP, dyngal.dynamicPHP);
dyngal.doxhr(h, dyngal.contentID);
DOMhelp.cancelClick(e);
},
在 load 方法中,检索事件目标并确保它是一个链接。然后读取链接的 href 属性,用只返回所需内容的动态名称替换原来的 PHP 脚本名称。使用 href 值和内容元素 ID 作为参数调用 doxhr()方法,并通过调用 cancelClick()停止链接传播。
dyngal_xhr.js(续)
doxhr : function(url, container) {
var request;
try{
request = new XMLHttpRequest();
} catch(error) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP");
} catch (error) {
return true;
}
}
request.open('get', url, true);
request.onreadystatechange = function() {
if(request.readyState == 1) {
container.innerHTML = 'Loading... ';
}
if(request.readyState == 4) {
if(request.status && /200|304/.test(request.status)) {
dyngal.retrieved(request, container);
} else {
dyngal.failed(request);
}
}
}
request.setRequestHeader('If-Modified-Since', 'Wed, 05 Apr 2013 00:00:00 GMT');
request.send(null);
return false;
},
retrieved : function(request, container) {
var data = request.responseText;
document.getElementById(container).innerHTML = data;
dyngal.assignHandlers(container);
},
failed : function(request) {
alert('The XMLHttpRequest failed. Status: ' + requester.status);
return true;
}
}
DOMhelp.addEvent(window, 'load', dyngal.init, false);
XHR 方法与你在第八章中使用的方法相同。唯一的区别是您需要再次调用 assignHandlers()方法,因为您替换了原始内容,结果丢失了链接上的事件处理程序。
注意拥有服务器端语言和 JavaScript 是一个强大的组合。一旦掌握了 JavaScript,学习 PHP 或 Node.js 之类的语言可能是个好主意,因为不了解服务器端语言 Ajax 就没什么意思了。服务器端语言可以做 JavaScript 做不到的事情,比如访问服务器上的文件并读取它们的名称和属性,甚至可以访问来自第三方服务器的内容。PHP 的语法在某些方面与 JavaScript 相似,Node.js 就是 JavaScript。
从文件夹创建图像徽章
在下一章查看一些现成的第三方代码和在线服务之前,让我们先来看看另一个使用 PHP 和 JavaScript/XHR 的小型图库示例。
这个练习将允许用户通过上一个和下一个链接浏览缩略图,并通过单击缩略图显示大照片。演示 exampleBadge.html 做到了这一点,图 10-3 显示了它在两个徽章图库中的样子。
图 10-3 。两个图像文件夹作为徽章图库
当创建这样的脚本时,尽可能使 HTML 简单是一个好主意。你对维护者的期望越低,人们就越有可能使用你的脚本。在这种情况下,维护人员要向 HTML 文档中添加徽章库,只需添加一个元素,该元素包含类徽章和一个指向包含图像的文件夹的链接:
exampleBadge.html(节选)
<p class="badge"><a href="galleries/animals/">Animals</a></p>
<p class="badge"><a href="galleries/buildings/">Buildings</a></p>
因为 JavaScript 无法检查服务器上的文件夹中的文件,所以您需要一个 PHP 脚本来完成这项工作。文件 badge.php 会这样做,并将缩略图作为列表项返回。
注以下是 PHP 脚本的快速解释。这不是 JavaScript,但是我希望您能够理解即将到来的 badge 脚本所使用的工具的工作原理。
徽章. php
<?php
$c = preg_match('/\d+/', $_GET['c']) ? $_GET['c'] : 5;
$s = preg_match('/\d+/', $_GET['s']) ? $_GET['s'] : 0;
$cd = is_dir($_GET['cd']) ? $_GET['cd'] : ' ';
你定义三个变量:s,它是所有缩略图列表中当前第一个缩略图的索引;和_GET 数组存储了 URL 的所有参数,这意味着如果 URL 是 badge.php?c = 3&s = 0&CD =动物,_GET['s']会是 0,c 和cd 确实是一个可用的文件夹。
badge.php(续)
if($cd != ' ') {
$handle = opendir($cd);
if(preg_match('/^tn_.*(jpg|jpe|jpeg)$/i', $file)) {
$images[] = $file;
}
}
closedir($handle);
如果文件夹是可用的,您开始使用 opendir()方法读出文件夹中的每个文件,并通过将文件名与模式^tn_.进行匹配来测试该文件是否是缩略图*(jpg|jpe|jpeg)$(以 tn_ 开头,以 jpg、jpe 或 jpeg 结尾)。如果文件是缩略图,将其添加到图像数组中。当文件夹中没有文件时,通过调用 closedir()方法关闭文件夹。
badge.php(续)
$imgs = array_slice($images, $s, $c);
if($s > 0) {
echo '<li class="badgeprev"> ';
echo '<a href="badge.php?c='.$c;
echo '&s=' . ($s-$c) . '&cd='.$cd. ' ">';
echo 'previous</a></li>';
} else {
echo '<li class="badgeprev"><span>previous</span></li>';
}
您使用 PHP 的 array_slice()方法将数组缩减到选定的图像(从s 是否大于 0。如果是,用 badgeprev 类写出一个列表元素,它在链接的 href 属性中有正确的参数。如果不是,在列表项中写一个 SPAN,而不是一个链接。
badge.php(续)
for($i=0; $i<sizeof($imgs); $i++) {
echo '<li><a href="'.str_replace('tn_', ' ',$cd.$imgs[$i]). ' "> '.
'<img src="' . $cd . $imgs[$i] . ' " alt="' . $imgs[$i] . ' " /></a></li>';
}
遍历图像,并在指向每个数组项的大图像的链接中显示一个 IMG 元素。通过用 str_replace()移除数组元素值的 tn_ string,可以检索到大图像的链接。
badge.php(续)
if(($c+$s) <= sizeof($images)) {
echo '<li class="badgenext">';
echo '<a href="badge.php?c=' . $c . '&s=' . ($s + $c);
echo '&cd=' . $cd . '">next</a></li>';
} else {
echo '<li class="badgenext"><span>next</span></li>';
}
}
?>
测试s 的总和是否小于文件夹中所有图像的数量,如果是,则显示一个链接,否则显示一个 SPAN。
如您所见,JavaScript 和 PHP 的编程语法和逻辑非常相似,这也是 PHP 成功的原因之一。现在让我们创建 JavaScript,它使用这个 PHP 脚本将链接转换成图像标记。
badge.js
badge = {
badgeClass : 'badge',
containerID : 'badgecontainer',
您可以定义用于指定徽章链接的 CSS 类和显示大图片的图像容器的 ID,作为主对象徽章的属性。
badge.js(续)
init : function() {
var newUL, parent, dir, loc;
if(!document.getElementById || !document.createTextNode) {
return;
}
var links = document.getElementsByTagName('a');
for(var i = 0; i < links.length; i++) {
parent = links[i].parentNode;
if(!DOMhelp.cssjs('check', parent, badge.badgeClass)) {
continue;
}
测试 DOM 支持并遍历文档中的所有链接,测试特定链接的父节点是否分配了 badge 类。如果没有,你跳过这个链接。
badge.js(续)
newUL = document.createElement('ul');
newUL.className = badge.badgeClass;
dir=links[i].getAttribute('href');
loc = window.location.toString().match(/(^.*\/)/g);
dir = dir.replace(loc, ' ');
badge.doxhr('badge.php?cd=' + dir, newUL);
parent.parentNode.insertBefore(newUL, parent);
parent.parentNode.removeChild(parent);
i--;
}
您创建了一个新的列表元素,并向其中添加了 badge 类。检索链接的 href 属性,读取窗口位置,并从 href 属性值中删除 window.location 中最后一个/之前的任何内容。
您使用正确的 URL 和新创建的列表作为参数调用 doxhr()方法,并将列表添加到当前链接的父元素之前。然后用 DOM 方法 removeChild()删除链接的父元素,并将循环计数器减 1。(你循环遍历文档的所有链接,这意味着当你移除其中一个链接时,计数器需要递减,以阻止循环跳过下一个链接。)
badge.js(续)
badge.container = document.createElement('div');
badge.container.id = badge.containerID;
document.body.appendChild(badge.container);
},
创建一个新的 DIV 作为大图像的容器,设置它的 ID,并将其添加到文档的主体。
badge.js(续)
doxhr : function(url, container) {
var request;
try {
request = new XMLHttpRequest();
} catch (error) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP");
} catch (error) {
return true;
}
}
request.open('get', url, true);
request.onreadystatechange = function() {
if(request.readyState == 1) {
}
if(request.readyState == 4) {
if(request.status && /200|304/.test(request.status)) {
badge.retrieved(request, container);
} else{
badge.failed(request);
}
}
}
request.setRequestHeader('If-Modified-Since', 'Wed, 02 Jan 2013 00:00:00 GMT');
request.send(null);
return false;
},
retrieved : function(request, container) {
var data = request.responseText;
container.innerHTML = data;
badge.assignHandlers(container);
},
failed : function(requester) {
alert('The XMLHttpRequest failed. Status: ' + requester.status);
return true;
},
Ajax/XHR 方法在很大程度上保持不变,唯一的区别是当数据被成功检索时,调用 assignHandlers()方法,并将列表项作为参数。
badge.js(续)
assignHandlers : function(o) {
var links = o.getElementsByTagName('a');
for(var i = 0; i < links.length; i++) {
links[i].parent = o;
if(/badgeprev|badgenext/.test(links[i].parentNode.className)) {
DOMhelp.addEvent(links[i], 'click', badge.load, false);
} else {
DOMhelp.addEvent(links[i], 'click', badge.show, false);
}
}
},
assignHandlers()方法遍历作为参数 o 发送的元素中的所有链接。它将该元素存储为每个名为 parent 的链接中的新属性,并测试该链接是否具有 badgeprev 或 badgenext 类,您可能还记得,这两个类是由 badge.php 添加到上一个和下一个链接中的。如果 CSS 类在那里,assignHandlers()添加一个指向 load 方法的事件处理程序;否则,它会添加一个指向 show 方法的事件处理程序,因为有些链接需要浏览缩略图,而其他链接需要显示大图像。
badge.js(续)
load : function(e) {
var t = DOMhelp.getTarget(e);
if(t.nodeName.toLowerCase() != 'a') {
t = t.parentNode;
}
var dir = t.getAttribute('href');
var loc = window.location.toString().match(/(^.*\/)/g);
dir = dir.replace(loc, ' ');
badge.doxhr('badge.php?cd=' + dir, t.parent);
DOMhelp.cancelClick(e);
},
load 方法检索事件目标并确保它是一个链接。它检索事件目标的 href 属性值,并在调用 doxhr 方法之前对其进行清理,并将链接的 parent 属性中存储的元素作为输出容器。通过调用 DOMhelp 的 cancelClick()来阻止链接被跟踪。
badge.js(续)
show : function(e) {
var t = DOMhelp.getTarget(e);
if(t.nodeName.toLowerCase() != 'a') {
t = t.parentNode;
}
var y = 0;
if(self.pageYOffset) {
y = self.pageYOffset;
} else if (document.documentElement && document.documentElement.scrollTop) {
y = document.documentElement.scrollTop;
} else if(document.body) {
y = document.body.scrollTop;
}
badge.container.style.top = y + 'px';
badge.container.style.left = 0 + 'px';
在 show 方法中,您再次检索事件目标并测试它是否是一个链接。然后将大图像容器放在屏幕上。因为您不知道徽章在文档中的位置,所以显示图像最安全的方法是读出文档的滚动位置。为了实现这一点,你需要为不同的浏览器做一些对象检测。
注意当前的垂直滚动位置是一个名为 pageYOffset 的窗口对象的属性。这在除 Internet Explorer(IE)9 版之前的所有浏览器中都受支持。如果文档中没有指定 HTML DOCTYPE,则 scrollTop 属性位于 IE、Firefox Opera、Chrome 和 Safari 中文档对象的 body 元素中。
您测试所有这些可能性,并通过设置其样式属性集合的 left 和 top 属性来相应地定位图像容器。这样,您可以始终确保大图像在用户的浏览器窗口中可见。
badge.js(续)
var source = t.getAttribute('href');
var newImg = document.createElement('img');
badge.deletePic();
newImg.setAttribute('src', source);
badge.container.appendChild(newImg);
DOMhelp.addEvent(badge.container, 'click', badge.deletePic, false);
DOMhelp.cancelClick(e);
},
您读取了链接的 href 属性并创建了一个新的 IMG 元素。通过调用 deletePic()方法删除任何可能已经显示的大图像,并将新图像的 src 属性设置为链接的 href。将新图像作为子节点添加到图像容器中,应用一个在用户单击图像时调用 deletePic()的事件处理程序,并通过调用 cancelClick()阻止链接被跟踪。
badge.js(续)
deletePic : function() {
badge.container.innerHTML = ' ';
}
}
DOMhelp.addEvent(window, 'load', badge.init, false);
deletePic 方法需要做的就是将容器元素的 innerHTML 属性设置为空字符串,从而移除大图像。
摘要
在本章中,您了解了如何使用 JavaScript 增强现有的 HTML 结构或缩略图库的动态服务器端脚本,使其变得动态,或者在用户选择另一个图像或缩略图子集时,通过不加载整个文档来使其看起来更加动态。
创建画廊总是很有趣,为他们想出新的更炫的解决方案也是令人愉快的。我希望通过学习本章介绍的一些技巧,你能自信地运用它们,并想出自己的画廊创意。