在 Forge - Ajax 入门

作者:Reuven M. Lerner

许多程序员,包括我自己,长期以来一直将 JavaScript 视为一种动态更改 HTML 页面外观或执行相对次要任务(例如检查表单的有效性)的方法。然而,在过去一年中,JavaScript 已经成为应用程序开发人员的主要力量,为所谓的 Ajax 应用程序提供了基础设施。

在 JavaScript 之前,用户操作与 HTML 页面的显示之间存在一一对应的关系。如果用户点击链接,当前显示的页面将消失并被另一个 HTML 页面替换。如果用户提交 HTML 表单,该表单的内容将被提交到 Web 服务器上的程序,服务器响应的内容随后将显示在浏览器中,替换其前身。在传统的 Web 应用程序中,服务器端程序处理大部分用户输入,并构建用户可能看到的任何动态生成的 Web 页面。

Ajax 应用程序重新分配了负载,更加强调客户端 JavaScript。在 Ajax 应用程序中,许多服务器端程序确实生成完整的 HTML 页面,然后在 Web 浏览器中完整显示。但许多其他服务器端程序生成 XML 格式的小数据片段。客户端 JavaScript 请求并使用这些数据来修改和更新当前的 HTML 页面,而无需刷新或替换它。通过使用 Web 标准,例如 DOM(文档对象模型)和 CSS(层叠样式表),Ajax 应用程序可以接近人们对桌面应用程序的可用性、友好性和即时反馈的期望。

本月,我们将继续探索客户端 JavaScript 和 Ajax,这是我们在过去几个月开始的。上个月的专栏介绍了一个用于网站的用户注册应用程序。尽管实际注册发生在服务器端程序中,但我们研究了如何为想要已被占用的用户名的注册用户提供 Ajax 风格的警告。当然,我们可以让服务器端注册程序检查用户名是否已被占用,但这需要刷新页面,这也需要延迟。

从用户的角度来看,我们上个月实施的解决方案还不错(特别是如果用户在设计方面口味有些朴素),但它以一种非常非 Ajax 的方式解决了问题——通过在 JavaScript 数组中硬编码用户名,然后在该数组中查找所需的新用户名。这种方法存在许多重大问题,首先是任何人查看 HTML 源代码都可以获得完整的用户名列表,其次是该数组会随着时间的推移变得笨拙和麻烦,下载和搜索所需的时间越来越长,随着注册用户数量的增长。

我们可以通过使用 Ajax 风格的解决方案来避免这些问题。与其在 JavaScript 中硬编码用户名列表,并且与其让服务器端程序生成完整的用户名列表,也许我们可以简单地向服务器发送请求,检查请求的用户名是否已被占用。这将带来相对快速的下载和响应时间,更简洁的应用程序设计以及可扩展的应用程序。

本月,我们深入研究 Ajax,修改了我们上个月编写的服务器端和客户端程序,以通过来自服务器的异步请求检索用户名。在制作此应用程序时,我们将看到创建 Ajax 应用程序或将 Ajax 功能集成到传统 Web 应用程序中是多么直接。在本文结束时,您应该了解如何创建 Ajax 应用程序的客户端和服务器端。

发起 Ajax 调用

使 Ajax 成为可能的许多技术是 JavaScript 的 XMLHttpRequest 对象。使用此对象,JavaScript 函数可以向服务器发出 HTTP 请求并对结果执行操作。(出于安全原因,XMLHttpRequest 发出的 HTTP 请求必须发送到加载当前 Web 页面的服务器。)HTTP 请求可以使用 GET 或 POST 方法,后者允许我们向服务器发送任意长度的复杂内容。

最有趣,并且是许多 Ajax 范例的核心,是 XMLHttpRequest 可以同步(强制浏览器等待直到完全收到响应)或异步(允许用户在下载其他信息时继续使用浏览器窗口)发出其 HTTP 请求。Ajax 应用程序通常使用异步调用。这允许 Web 页面的不同部分独立于彼此更新和修改,可能同时响应多个用户输入。

理想情况下,我们应该能够使用以下 JavaScript 代码创建 XMLHttpRequest 的实例

var xhr = new XMLHttpRequest();

不幸的是,生活并非如此简单。这是因为许多人使用 Internet Explorer 作为他们的主要浏览器。IE 没有原生的 XMLHttpRequest 对象,因此无法以这种方式实例化它。相反,它必须实例化为

var xhr = new ActiveXObject("Msxml2.XMLHTTP");

但是等等!还有一些 IE 版本需要稍微不同的语法

var xhr = new ActiveXObject("Microsoft.XMLHTTP");

我们将如何处理这三种不同的实例化 XMLHttpObject 的方法?一种方法是使用服务器端浏览器检测。也可以使用客户端浏览器检测。但我迄今为止见过的最优雅的方法来自 Ajax Design Patterns,这是 Michael Mahemoff 的一本新书(由 O'Reilly Media 出版)。Mahemoff 使用 JavaScript 的异常处理系统依次尝试这些方法,直到它起作用为止。通过将我们的三种不同的实例化方法包装在一个函数中,然后将我们的 xhr 变量的值分配给该函数返回的任何内容,我们可以为我们的应用程序提供跨平台兼容性

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();

执行上述代码后,我们可以确定 xhr 要么为空(表示所有实例化 XMLHttpRequest 的尝试都失败了),要么包含 XMLHttpRequest 的有效实例。一旦实例化,XMLHttpRequest 就跨浏览器和平台兼容。因此,相同的方法将适用于所有系统。

在 xhr 上调用的最常见的方法是 open,它告诉对象向原始服务器上的特定 URL 发送 HTTP 请求。对 xhr.open 的调用如下所示

xhr.open("GET", "foo.html", true);

第一个参数 (GET) 告诉 xhr.open 我们要使用 HTTP GET 方法。第二个参数命名我们要检索的 URL;请注意,由于我们必须连接到原始服务器,因此 URL 的初始协议和主机名部分缺失。第三个参数指示调用是异步 (true) 还是同步 (false)。几乎所有 Ajax 应用程序都传递 true,因为这意味着浏览器在等待 HTTP 响应时不会冻结。这种发出异步 HTTP 请求的能力是 Ajax 魔力的核心。由于 HTTP 请求不影响用户界面并在后台处理,因此 Web 应用程序感觉更像桌面应用程序。

对 xhr.open() 的调用实际上并未发送 HTTP 请求。相反,它设置对象,以便在发送请求时,它使用指定的请求方法和参数。要将请求发送到服务器,我们使用

xhr.send(null);

XMLHttpRequest 不会返回调用 xhr.send() 的 HTTP 响应。这是因为我们 异步 使用 XMLHttpRequest,正如在 xhr.open() 中使用 true 值指定的那样。我们无法预测我们是否会在半秒、五秒、一分钟或十分钟内获得结果。

相反,我们告诉 JavaScript 在收到 HTTP 响应时调用一个函数。此函数将负责读取和解析响应,然后采取适当的操作。我称之为 parseHttpResponse 的函数的简单版本如下

function parseHttpResponse() {
    alert("entered parseHttpResponse");
    if (xhr.readyState == 4) {
        alert("readystate == 4");
        if (xhr.status == 200) {
            alert(xhr.responseText);
        }
        else
        {
            alert("xhr.status == " + xhr.status);
        }
    }
}

当我们的 Ajax 请求的 HTTP 响应到达时,将调用 parseHttpResponse。但是,我们必须确保响应内容已完全到达,我们通过监视 xhr.readyState 来做到这一点。当它等于 4 时,我们知道 xhr 已收到完整响应。我们的下一步是检查响应是否具有 HTTP “OK”(200)代码。毕竟,我们始终可能从服务器收到 404(“文件丢失”)错误,或者我们根本无法连接到服务器。

要告诉 JavaScript 当我们的 HTTP 请求返回时我们要调用 parseHttpResponse,我们在 XMLHttpRequest 对象中设置 onreadystatechange 属性

xhr.onreadystatechange = parseHttpResponse;

最后,在我们确信我们已收到响应并且一切正常之后,我们可以使用 xhr.responseText 方法获取响应的文本。我们的 XMLHttpRequest 可以将其响应作为文本字符串(如此处)或作为 XML 文档返回。在后一种情况下,我们可以使用 DOM 导航它,就像我们对 Web 页面所做的那样。

当然,实际的 Ajax 应用程序不会在其执行的每个步骤中发出警报,并且可能会做一些更有用的事情——也许更改一些文本,从文档树中添加或删除一些节点,或更改文档的样式表的一部分。尽管如此,您可以在列表 1 (ajax-test.html) 中看到此代码的实际效果。

列表 1. ajax-test.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>Ajax test</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 parseHttpResponse() {
        alert("entered parseHttpResponse");
        if (xhr.readyState == 4) {
            alert("readystate == 4");
            if (xhr.status == 200) {
                alert(xhr.responseText);
            }
            else
            {
                alert("xhr.status == " + xhr.status);
            }
        }
    }

    var xhr = getXMLHttpRequest();
    alert("xhr = " + xhr);
    xhr.open("GET", "atf.html", true);
    xhr.onreadystatechange = parseHttpResponse;
    xhr.send(null);

    </script>
  </head>
  <body>
    <h2>Headline</h2>
    <p>Paragraph</p>
  </body>
</html>

请注意,ajax-test.html 虽然简单,但却是完全可用的 Ajax 程序。为了使其工作,您需要在 Web 站点的 DocumentRoot 目录中有一个名为 atf.html 的文件。(否则,您将获得 HTTP 响应代码 404。)如果您曾经想知道执行 Ajax 调用有多难,那么您现在可以看到它相对简单。

向注册添加 Ajax

现在我们已经了解了 Ajax 程序的工作原理,让我们使用这些知识来修改我们上个月构建的注册程序。我们旧的注册页面在 JavaScript 中定义了用户名列表。如果用户请求的用户名是该列表的成员,我们会警告用户错误并禁止用户实际注册。

我不会描述这种方法的所有问题,因为有很多。作为一种简单的替代方案,如果我们使用 Ajax 来检索用户名列表会怎样?这样,我们可以确保列表是最新的。

如果,我们不是对数组内容进行硬编码,而是从服务器上的 Web 页面下载它们会怎样?(诚然,这不如获得对特定用户名的“是”或“否”答案那么复杂;我们将在下个月的专栏中介绍该功能。)如果 Ajax 检索的用户名列表是动态生成的,我们可以让它从数据库中抓取适当的数据,然后返回一个可以轻松转换为数组的 XML 文档。为了使本月的专栏中的示例更简单,我们不使用动态页面,而是使用静态页面。但是,如果您过去做过任何服务器端 Web 编程,您可能会理解如何获取我们的文件 usernames.txt(列表 2)并将其转换为动态页面。

列表 2. usernames.txt

abc
def
ghi
jkl
mno
pqr
stu
vwx
yzz

列表 3 中显示了遵循此原则的注册页面。该文件 ajax-register.html 与我们上个月创建的注册表单类似。在上个月的非 Ajax 版本中,我们定义了一个数组 (usernames)。然后我们定义了一个 checkUsername 函数,该函数由用户名文本字段的 onchange 处理程序调用。这具有在用户完成用户名时调用 checkUsername 的效果。如果请求的用户名在用户名数组中,则会向用户发出警告,并且禁用提交按钮。否则,用户可以向服务器端注册程序提交表单,大概是参与该站点的第一步。

列表 3. 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 parseUsernames() {

            // Set up empty array of usernames
            var usernames = [ ];

            // Wait for the HTTP response
        if (xhr.readyState == 4) {
        if (xhr.status == 200) {
            usernames = xhr.responseText.split("\n");
        }
        else
        {
            alert("problem: xhr.status = " + xhr.status);
        }
        }

            // Get the username that the person wants
            var new_username = document.forms[0].username.value;
            var found = false;
            var warning = document.getElementById("warning");
            var submit_button = document.getElementById("submit-button");

        // Is this new username already taken? Iterate over
        // the list of usernames to be sure.
            for (i=0 ; i<usernames.length; i++)
            {
                if (usernames[i] == new_username)
                {
                    found = true;
                }
            }

        // If we find the username, issue a warning and stop
        // the user from submitting the form.
            if (found)
            {
                setText(warning, "Warning: username '" + new_username
                +"' was taken!");
                submit_button.disabled = true;
            }

            else
            {
                removeText(warning);
                submit_button.disabled = false;
            }

        }

        function checkUsername() {
            // Send the HTTP request
        xhr.open("GET", "usernames.txt", true);
        xhr.onreadystatechange = parseUsernames;
        xhr.send(null);
        }

    </script>
  </head>
  <body>
    <h2>Register</h2>
    <p id="warning"></p>
    <form action="/cgi-bin/register.pl" method="post">
        <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>

要将上个月的注册页面转换为 Ajax 风格的页面,我们修改了 checkUsername 函数,该函数在用户完成输入其请求的用户名时调用。我们没有定义用户名数组,而是让 checkUsername 向服务器发出 Ajax 请求。与上个月的非 Ajax 版本不同,这就是 checkUsername 将要做的全部工作。更新后的函数如下所示

function checkUsername() {
xhr.open("GET", "usernames.txt", true);
xhr.onreadystatechange = parseUsernames;
xhr.send(null);
}

如您所见,我们的函数正在从服务器请求文件 usernames.txt。当 xhr 的状态更改时,我们要求调用 parseUsernames 函数。正是在此函数中,我们放入了严肃的逻辑,首先将检索到的文件内容转换为数组

var usernames = [ ];

if (xhr.readyState == 4) {
if (xhr.status == 200) {
    usernames = xhr.responseText.split("\n");
}
}

在这里,我们看到了前一个示例中重复的标准 Ajax 模式:等待 xhr.readyState 为 4,然后检查 xhr.status(HTTP 响应状态代码)是否为 200。此时,我们知道我们已收到 usernames.txt 的内容,如您从列表 2 中看到的那样,其中包含现有用户名,每行一个用户名。我们使用 JavaScript 的 split 函数将其转换为数组,我们将其分配给用户名。

从那时起,我们可以重用上个月非 Ajax 版本的逻辑,首先使用 DOM 方法从页面中抓取各种节点 ID

var new_username = document.forms[0].username.value;
var found = false;
var warning = document.getElementById("warning");
var submit_button = document.getElementById("submit-button");

然后,我们检查请求的用户名是否在我们的数组中

for (i=0 ; i<usernames.length; i++)
{
    if (usernames[i] == new_username)
    {
        found = true;
    }
}

如果在列表中找到用户名,我们会在页面顶部发出警告。否则,我们会清除可能存在的任何警告

if (found)
{
    setText(warning, "Warning: username '" + new_username +"' was taken!");
    submit_button.disabled = true;
}

else
{
    removeText(warning);
    submit_button.disabled = false;
}
}

现在,这是处理用户名检查的好方法吗?不完全是——尽管现在我们已经有了基本的 Ajax 逻辑,我们可以稍微修改它以使其更有效和更安全。

一个问题是用户名列表在一个静态文件中。也许我们的服务器正在运行一个 cron 作业,定期创建 usernames.txt,但这似乎有点傻,因为我们可以改为使用服务器端程序动态查询数据库。因此,仅出于性能原因,从静态文件切换到动态页面似乎是个好主意。

还有安全原因。与上个月的版本一样,我们将整个用户名列表下载到用户的浏览器。这意味着潜在的恶意用户可以访问所有用户名,并且能够戳穿它们,目的是试图闯入站点或向用户发送垃圾邮件。

使用 Ajax 进行此类检查的一个潜在缺点是速度问题。正如我之前指出的,Ajax 的核心是其异步性质,这意味着我们无法知道服务器响应我们的查询需要多长时间。在我的简单测试中,从我的浏览器到我的服务器再返回的往返行程几乎是瞬间的,它立即为我提供了有用的反馈。在负载更重的服务器上,或者使用更复杂的数据库查询,或者如果用户 Internet 连接速度较慢,异步调用可能会开始感到缓慢。话虽如此,即使是最糟糕的 Ajax 函数也可能比页面刷新更快,因为涉及的开销减少了。

结论

本月,我们终于开始在应用程序中使用 Ajax。我们在这里看到如何获取一些现有的 JavaScript 代码并将其分解为两个函数:一个调用 Ajax 调用,另一个处理在调用接收到响应时解析数据。

但是,我们也看到这种方法存在安全性和效率问题。更好的技术是在 Ajax 调用中仅发送请求的用户名,并从服务器获得简单的“是”或“否”答案,指示用户名是否已被占用。下个月,我们将做到这一点,使用 Ajax POST 查询而不是本月的 GET 查询,并将 usernames.txt 替换为与我们的 Ajax 调用协同工作的服务器端程序。

延伸阅读

去年,关于 Ajax 编程的书籍和文章大量涌现,我正在慢慢阅读其中的许多书籍。我读过的最好的两本都是由 O'Reilly 出版的。Head Rush Ajax 针对初学者,并以有趣、有效的方式教授入门材料。Ajax Design Patterns,我在本文前面提到过,可能是我目前最喜欢的 Ajax 书(尽管它的设计和编辑没有达到 O'Reilly 的通常标准)。后一本书对于经验丰富的 Web 开发人员来说是该主题的良好介绍。

Ajaxian.com 网站有大量链接、教程和文章,内容涉及各种不同平台上的 Ajax 开发。如果您对 Ajax 开发感兴趣,值得将此站点保留在您的 RSS 阅读器或书签中。

Reuven M. Lerner,一位长期的 Web/数据库顾问,是伊利诺伊州埃文斯顿西北大学学习科学博士候选人。他目前与妻子和三个孩子住在伊利诺伊州斯科基。您可以在 altneuland.lerner.co.il 阅读他的博客。

加载 Disqus 评论