在 Forge - Prototype
在过去的几个月中,我们研究了如何使用 JavaScript,几乎每个现代 Web 浏览器都包含一个版本。在其生命的大部分时间里,JavaScript 一直被用于在网页上创建简单的客户端效果和操作。但在过去一两年中,JavaScript 已成为 Ajax(异步 JavaScript 和 XML)范例的核心部分。仅仅创建驻留在服务器上的 Web 应用程序已不再足够。现代 Web 应用程序必须包含 Ajax 风格的行为,这可能意味着将 JavaScript 集成到服务器端程序、HTML 和关系数据库的组合中。
正如我们在本专栏的最近几期中所见,使用 JavaScript 需要相当多的重复代码。我必须调用 document.getElementById() 多少次,才能抓取我想修改的节点?为什么我必须创建一个库来处理我将定期进行的基本 Ajax 调用?我必须创建所有我自己的小部件和图形效果吗?
对于世界各地的 Web 开发人员来说幸运的是,对 Ajax 的爆炸性兴趣也导致了在库方面的同样富有成效的工作,以回答这些问题和需求。这些库中的许多库已在开源许可下发布,因此 Web 开发人员可以将其包含在各种不同类型的站点中。
本月,我们来看看最著名的 JavaScript 库之一,称为 Prototype。Prototype 由 Sam Stephenson(Ruby on Rails 核心团队的成员)开发,在一段时间内已包含在所有 Ruby on Rails 副本中。Prototype 旨在使 JavaScript 的使用更加容易,为一些最常见的用途提供了许多快捷方式。
如果您为您的 Web 应用程序使用 Ruby on Rails,则 Prototype 已包含在其中。您可以通过在 Rails 视图模板中添加以下内容开始在您的应用程序中使用它
<%= javascript_include_tag 'prototype' %>
如果您不使用 Rails,您仍然可以使用 Prototype。只需从其站点下载它(请参阅在线资源)。然后使用
<script type="text/javascript" src="/javascript/prototype.js"></script>
当然,以上假设您已将 prototype.js 放在 Web 服务器上的 /javascript URL 中。您可能需要调整该 URL 以反映您系统的配置。
一旦您包含了 Prototype,您就可以立即开始利用其功能。例如,清单 1 显示了 simpletext.html。此文件包含一些简单的 JavaScript,当您单击提交按钮时,它会将标题更改为文本字段的内容。
清单 1. simpletext.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head><title>Title</title> <script type="text/javascript"> function removeText(node) { if (node != null) { if (node.childNodes) { for (var i=0 ; i < node.childNodes.length ; i++) { var oldTextNode = node.childNodes[i]; if (oldTextNode.nodeValue != null) { node.removeChild(oldTextNode); } } } } } function appendText(node, text) { var newTextNode = document.createTextNode(text); node.appendChild(newTextNode); } function setText(node, text) { removeText(node); appendText(node, text); } function setHeadline () { var headline = document.getElementById("headline"); var fieldContents = document.forms[0].field1.value; setText(headline, fieldContents); } </script> </head> <body> <h2 id="headline">Simple form</h2> <form id="the-form" action="/cgi-bin/foo.pl" method="post"> <p>Field1: <input type="text" id="field1" name="field1" /></p> <p><input type="button" value="Change headline" onclick="setHeadline()"/></p> </form> </body> </html>
我们通过定义一个函数 (setHeadline),然后通过设置该函数在单击按钮时被调用来做到这一点
<p><input type="button" value="Change headline" onclick="setHeadline()"/></p>
现在,setHeadline 内部发生了什么?首先,我们抓取包含标题的节点
var headline = document.getElementById("headline");
然后,我们获取文本字段的内容,我们将其称为 field1
var fieldContents = document.forms[0].field1.value;
请注意,我们必须通过遍历文档层次结构来抓取值。首先,我们从文档中获取表单数组 (document.forms),然后我们抓取第一个表单 (forms[0]),然后我们抓取文本字段 (field1),然后我们最终获取值。
现在我们可以通过将文本节点附加到 h2 节点来设置标题的值。我们使用一个名为 setText 的函数来做到这一点,该函数已包含在 simpletext.html 中;setText 又依赖于 removeText 和 appendText,这两个其他辅助函数使处理 JavaScript 中的文本节点变得容易。
所有这些都非常好,并且是我经常做的 JavaScript 编码类型的典型代表。Prototype 如何帮助我们?通过使用两个内置函数简化我们的代码。第一个函数 $(),看起来有点奇怪但却是合法的——它的全名是 $ (美元符号),它的功能与 document.getElementById 非常相似,返回 ID 与其参数匹配的节点。第二个函数 $F 返回 ID 与参数匹配的表单元素的值。
换句话说,我们可以将我们的函数重写为
function setHeadline() { var headline = $("headline"); var fieldContents = $F("field1"); setText(headline, fieldContents); }
当然,这与之前的版本效果一样好。但是,它更容易阅读(在我看来),并且它允许我们避免遍历文档层次结构,直到我们到达表单元素。
我们可以通过删除我们的 setText、updateText 和 removeText 函数来进一步改进我们的代码,所有这些函数都包含在内仅仅是因为 JavaScript 没有提供任何简单的方法来操作节点的文本。但是 Prototype 通过其 Element 类提供了这一点,允许我们将 setHeadline 重写为
function setHeadline() { Element.update($("headline"), $F("field1")); }
该代码调用 Element.update,并向其传递两个参数:我们想要修改其文本的节点和我们想要插入以替换当前文本的文本。由于 Prototype,我们刚刚用一行代码替换了我们 30 行的代码。您可以在清单 2 中看到结果。
清单 2. simpletext-prototype.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head><title>Title</title> <script type="text/javascript" src="prototype.js"></script> <script type="text/javascript"> function setHeadline() { Element.update($("headline"), $F("field1")); } </script> </head> <body> <h2 id="headline">Simple form</h2> <form id="the-form" action="/cgi-bin/foo.pl" method="post"> <p>Field1: <input type="text" id="field1" name="field1" /></p> <p><input type="button" value="Change headline" onclick="setHeadline()"/></p> </form> </body> </html>
$() 函数不仅仅是 document.getElementById() 的简洁替代品。如果我们向它传递多个 ID,它会返回一个包含这些 ID 的节点的数组。例如,我们可以添加第二个标题,然后使用以下代码同时设置它们
function setHeadline() { var headlines = $("headline", "empty-headline"); for (i=0; i<headlines.length; i++) { Element.update(headlines[i], $F("field1")); } }
虽然页面加载时标题节点中只有文本,但按下按钮会导致将标题和空标题都设置为 field1 字段的内容。
Prototype 为我们带来了比 $()、$F() 和一些便利类更多的东西。您可以将其视为一个包含不同实用函数和对象的百宝箱,它们使 JavaScript 编码更容易。
例如,在我们上面对 setHeadline 的定义中,我们有以下循环
for (i=0; i<headlines.length; i++) { Element.update(headlines[i], $F("field1")); }
对于任何使用 C、Java 或 Perl 编程的人来说,这应该看起来很熟悉。然而,现代编程语言(包括 Java)通常支持枚举器或迭代器,用于更具表现力和紧凑的循环,而无需索引变量(上述循环中的 i)。例如,这是我们在 Ruby 中循环遍历数组的方式
array_of_names = ['Atara', 'Shikma', 'Amotz'] array_of_names.each do |name| print name, "\n" end
Prototype 通过定义 Enumerator 类,然后将其功能提供给内置的 Array 对象,将 Ruby 风格的循环带到 JavaScript。因此,我们可以将我们的 setHeadline 函数重写为
function setHeadline() { var headlines = $("headline", "empty-headline"); headlines.each( function(headline) { Element.update(headline, $F("field1")); } ); }
此代码看起来可能有点奇怪,一半像 Ruby,一半像 JavaScript。此外,我们在一个循环内部定义一个函数似乎很奇怪,该循环本身在函数内部执行。然而,JavaScript 的一个优点,就像许多其他现代高级语言一样,是函数是一等公民对象,我们可以像创建和传递任何其他类型的对象一样创建和传递它们。正如您不会对在循环内部创建数组感到紧张一样,您也不应该对在循环内部定义函数感到紧张。
我还应该注意到,Prototype 的 Enumerated 对象提供的 each 方法采用一个可选的索引参数,该参数计算迭代次数。所以,我们可以说
function setHeadline() { var headlines = $("headline", "empty-headline"); headlines.each( function(headline, index) { Element.update(headline, index + " " + $F("field1")); } ); }
现在,每个标题将像以前一样显示,但在文本前面加上一个数字。清单 3 显示了生成的页面。
清单 3. simpletext-each.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head><title>Title</title> <script type="text/javascript" src="prototype.js"></script> <script type="text/javascript"> function setHeadline() { var headlines = $("headline", "empty-headline"); headlines.each( function(headline, index) { Element.update(headline, index + " " + $F("field1")); } ); } </script> </head> <body> <h2 id="headline">Simple form</h2> <h2 id="empty-headline"></h2> <form id="the-form" action="/cgi-bin/foo.pl" method="post"> <p>Field1: <input type="text" id="field1" name="field1" /></p> <p><input type="button" value="Change headline" onclick="setHeadline()"/></p> </form> </body> </html>
Prototype 为 Enumerable 对象提供了其他方法,例如 all find(用于查找函数返回 true 的对象);inject(使用函数组合项目,对数字求和很有用);min/max(查找集合中的最小值或最大值);以及 map(将函数应用于集合的每个成员)。这些方法不仅适用于数组,还适用于 Hash 和 ObjectRangle,这两个类都随 Prototype 一起提供。
最近对 JavaScript 产生兴趣的最常见原因之一是对结合了 Ajax 技术的 Web 应用程序的日益增长的兴趣。正如我们在本专栏的最近几期中所见,Ajax 无非是 1) 创建一个 XmlHttpRequest 对象,2) 编写一个使用该对象发送 HTTP 请求的函数,3) 设置事件处理程序以调用该函数,以及 4) 编写一个在 HTTP 响应返回时调用的函数。用代码处理所有这些事情并不是特别困难,但是当您可以专注于更高级别的问题时,为什么要创建 XmlHttpRequest 对象呢?
清单 4. post-ajax-register.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head><title>Register</title> <script type="text/javascript"> function getXMLHttpRequest () { try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) {}; try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) {} try { return new XMLHttpRequest(); } catch(e) {}; return null; } function removeText(node) { if (node != null) { if (node.childNodes) { for (var i=0 ; i < node.childNodes.length ; i++) { var oldTextNode = node.childNodes[i]; if (oldTextNode.nodeValue != null) { node.removeChild(oldTextNode); } } } } } function appendText(node, text) { var newTextNode = document.createTextNode(text); node.appendChild(newTextNode); } function setText(node, text) { removeText(node); appendText(node, text); } var xhr = getXMLHttpRequest(); function parseResponse() { // Get variables ready var response = ""; var new_username = document.forms[0].username.value; var warning = document.getElementById("warning"); var submit_button = document.getElementById("submit-button"); // Wait for the HTTP response if (xhr.readyState == 4) { if (xhr.status == 200) { response = xhr.responseText; switch (response) { case "yes": setText(warning, "Warning: username '" + new_username +"' was taken!"); submit_button.disabled = true; break; case "no": removeText(warning); submit_button.disabled = false; break; case "": break; default: alert("Unexpected response '" + response + "'"); } } else { alert("problem: xhr.status = " + xhr.status); } } } function checkUsername() { // Send the HTTP request xhr.open("POST", "/cgi-bin/check-name-exists.pl", true); xhr.onreadystatechange = parseResponse; var username = document.forms[0].username.value; xhr.send("username=" + escape(username)); } </script> </head> <body> <h2>Register</h2> <p id="warning"></p> <form action="/cgi-bin/register.pl" method="post" enctype="application/x-www-form-urlencoded"> <p>Username: <input type="text" name="username" onchange="checkUsername()" /></p> <p>Password: <input type="password" name="password" /></p> <p>E-mail address: <input type="text" name="email_address" /></p> <p><input type="submit" value="Register" id="submit-button" /></p> </form> </body> </html>
幸运的是,Prototype 包含使 Ajax 编程非常容易的对象和功能。例如,上个月的专栏展示了当我们为网站注册个人时,我们如何使用 Ajax 来检查用户名是否已被占用,我在清单 4 中展示了这一点。其想法是,当某人输入用户名时,我们会立即向服务器发出请求。服务器的响应将告诉我们用户名是否已被占用。我们通过设置 username 字段的 onchange 事件处理程序来调用我们的 Ajax 请求,以调用 checkUsername
function checkUsername() { // Send the HTTP request xhr.open("POST", "/cgi-bin/check-name-exists.pl", true); xhr.onreadystatechange = parseResponse; var username = document.forms[0].username.value; xhr.send("username=" + escape(username)); }
不幸的是,要达到这一点,我们需要已经将 xhr 定义为我们的 XmlHttpRequest 对象的实例,我们这样做如下
function getXMLHttpRequest () { try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) {}; try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) {} try { return new XMLHttpRequest(); } catch(e) {}; return null; } var xhr = getXMLHttpRequest();
Prototype 可以删除之前的大部分代码,这不仅可以减少我们网页中的混乱,还可以让我们在更高的抽象级别上思考。正如当我们考虑字符串而不是位和字符时,文本处理变得更容易一样,当我们不再需要担心正确实例化各种对象或跟踪它们的值时,Ajax 开发变得更容易。
我们可以利用 Prototype 将 checkUsername 重写如下
function checkUsername() { var url = "http://www.lerner.co.il/cgi-bin/check-name-exists.pl"; var myAjax = new Ajax.Request( url, { method: 'post', parameters: $F("username"), onComplete: parseResponse }); }
在上面的函数中,我们定义了两个变量。其中一个变量 url 包含我们的 Ajax 请求将提交到的服务器端程序的 URL。第二个变量是 myAjax,它是 Ajax.Request 的一个实例。当我们创建这个对象时,我们将我们的 url 变量以及 JSON(JavaScript 对象表示法)格式的对象传递给它。第二个参数告诉新的 Ajax.Request 对象要传递什么请求方法和参数,以及在成功返回时要调用什么函数。
看起来我们只是重写了原始版本的 checkUsername。但是,当您考虑我们现在可以对 parseResponse 进行的更改时,您将看到 Prototype 使我们的生活变得多么简单
function parseResponse(originalRequest) { var response = originalRequest.responseText; var new_username = $F("username"); var warning = $("warning"); var submit_button = $("submit-button"); switch (response) { case "yes": setText(warning, "Warning: username '" + new_username +"' was taken!"); submit_button.disabled = true; break; case "no": removeText(warning); submit_button.disabled = false; break; case "": break; default: alert("Unexpected response '" + response + "'"); } }
我们的程序 post-ajax-register.html 的重写版本如清单 5 ajax-register-prototype.html 所示。它使用了 Prototype 的许多功能,从简单的功能(例如 $())到 Ajax 请求。我们不再需要等待响应以完整形式到达;现在我们可以让 Prototype 完成繁重的工作。
清单 5. ajax-register-prototype.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head><title>Register</title> <script type="text/javascript" src="prototype.js"></script> <script type="text/javascript"> function parseResponse(originalRequest) { var warning = $("warning"); var submit_button = $("submit-button"); switch (originalRequest.responseText) { case "yes": Element.update(warning, "Username '" + $F("username") +"' is taken!"); submit_button.disabled = true; break; case "no": Element.update(warning, ""); submit_button.disabled = false; break; case "": break; default: alert("Unexpected response '" + originalRequest.responseText + "'"); } } function checkUsername() { var url = "http://maps.lerner.co.il/cgi-bin/check-name-exists.pl"; var myAjax = new Ajax.Request( url, { method: 'get', parameters: "username=" + $F("username"), onComplete: parseResponse } ); } </script> </head> <body> <h2>Register</h2> <p id="warning"></p> <form action="/cgi-bin/register.pl" method="post" enctype="application/x-www-form-urlencoded"> <p>Username: <input type="text" id="username" name="username" onchange="checkUsername()" /></p> <p>Password: <input type="password" name="password" /></p> <p>E-mail address: <input type="text" name="email_address" /></p> <p><input type="submit" value="Register" id="submit-button" /></p> </form> </body> </html>
几个月前,我在本专栏中评论说我不太喜欢 JavaScript。尽管该语言仍然存在一些我不喜欢的元素,但 Prototype 在改变我对该语言的态度方面做得非常出色。我不再感到陷入冗长的语法中。Prototype 为我提供了一种解放感,我能够专注于更高级别的功能,而不是遍历节点层次结构或担心跨浏览器兼容性。通过一些练习,您也可能会发现 Prototype 是消除反 JavaScript 情绪的解药。
更重要的是,Prototype 现在位于一系列不同的 JavaScript 库(例如 Scriptaculous 和 Rico)的基础之上。在未来的几个月中,我们将研究这些库可以为您的 Web 开发(包括 Ajax 开发)做些什么。然后,我们将研究 Prototype 的一些替代方案,这些替代方案也为有抱负的 Ajax 程序员提供了很多帮助。
本文资源: /article/9455。
Reuven M. Lerner 是一位资深的 Web/数据库顾问,是伊利诺伊州埃文斯顿西北大学学习科学博士候选人。他目前与妻子和三个孩子住在伊利诺伊州斯科基。您可以在 altneuland.lerner.co.il 阅读他的博客。