在 Forge - Prototype

作者:Reuven M. Lerner

在过去的几个月中,我们研究了如何使用 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 的使用更加容易,为一些最常见的用途提供了许多快捷方式。

获取和使用 Prototype

如果您为您的 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 做更多事情

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 一起提供。

Ajax

最近对 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 阅读他的博客。

加载 Disqus 评论