使用 Web 服务和 Ajax 编写 Web 应用程序

作者:Mike Diehl

如果您最近做过任何 Web 开发,您无疑听说过关于 Web 服务和 Ajax 的热议。行业炒作如此普遍,以至于您几乎会认为人们在谈论下一个 Microsoft 操作系统。幸运的是,他们不是。Web 服务和 Ajax 是两种 Web 技术,它们使开发人员能够创建更有趣的 Web 应用程序,并使开发更轻松且更不易出错。

既然我已经增加了炒作,那么让我花一些时间来概述一下我们所说的“Web 服务和 Ajax”是什么意思。

Web 服务是一个可以通过 Internet 访问的程序,并提供特定的服务,例如搜索图书馆的馆藏或从 eBay 获取竞标历史记录。我们不是在谈论成熟的应用程序,而是在谈论基于 Web 的应用程序编程接口 (API),给定程序可以通过 Internet 调用该接口来执行所需的功能。通常,对给定 Web 服务的调用的结果以 XML 格式返回,调用程序可以轻松地对其进行操作。

当人们讨论 Web 服务时,他们经常提到诸如 JSON、REST、SOAP 或 XML-RPC 之类的东西。这些只是用于调用 Web 服务的一些可用协议。熟悉这些协议使您能够使用 Amazon、Google 和 eBay 等公司提供的一些非常强大的 Web 服务。但是,对于我的个人 Web 开发而言,我发现这些协议有点重。

Ajax 是一种机制,允许 Web 页面在不刷新页面或使用隐藏框架的情况下回调服务器。例如,如果用户更改了表单字段的值,则 Web 页面可以告诉服务器更改数据库,而无需像标准 CGI 脚本那样刷新 Web 页面。从用户的角度来看,更新只是发生了。

在本文中,我概述了一组非常原始的 Web 服务,这些服务对数据库执行特定功能。对 Web 服务的调用将通过 Ajax 完成。本质上,我们将构建一个简单的联系人管理程序,该程序存储人的名字、姓氏和电话号码。我们将能够在数据库中上下移动,进行添加和更正以及删除记录。最棒的是,一旦页面最初加载,即使我们进行更改,我们也不必再次刷新它。

但在我们开始之前,我们需要在数据库中创建一个表来存储信息。我碰巧使用 PostgreSQL 作为我首选的 DBMS。对于我们简单的应用程序,我们只需要一个表(清单 1)。

清单 1. 为项目准备 PostgreSQL 序列和表

create sequence contacts_id_seq;

create table contacts (
   id      integer default nextval('contacts_id_seq') not null,
   first   varchar(20),
   last    varchar(20),
   phone   varchar(20)
);

清单 1 中的 SQL 代码片段创建了一个序列和一个表。对于我们简单的应用程序,表结构非常简单明了。唯一值得一提的是 id 字段。默认情况下,当记录插入到我们的 contacts 表中时,id 字段的值设置为 contacts_id_seq 序列中的下一个数字。最终结果是我们的每个联系人记录都有一个唯一的整数,可以用来定位它。

现在我们已经定义了数据库表,我们可以开始充实实际的应用程序了。清单 2 显示了我们简单应用程序的 HTML,图 1 显示了应用程序在 Web 浏览器中的外观。

清单 2. 应用程序的基本 HTML

<html>
<head>
   <title>Contact Application</title>
   <script src=http://contacts.js></script>
</head>
<body>
<form method=POST name=main>
<input type=button name=new value="New"
    onclick="insert_record();">
<input type=button name=delete value="Delete"
    onclick="delete_record(main.id.value);">
<p>
Record Number: <input id=id name=id>
<p>
First: <input id=first name=first
   onChange="update_record(main.id.value,
   'first', main.first.value);">
<br>
Last: <input id=last name=last
   onChange="update_record(main.id.value,
   'last', main.last.value);">
<br>
Phone: <input id=phone name=phone
   onChange="update_record(main.id.value,
   'phone', main.phone.value);">
<p>
<input type=button name=previous value="Previous"
   onClick="select_record(main.id.value-1);">
<input type=button name=next value="Next"
   onClick="select_record(Number(main.id.value) + 1);">
</form>
</body>
</html>
Writing Web Applications with Web Services and Ajax

图 1. 此示例应用程序的简陋 Web 页面

如您所见,我们简单的应用程序确实很简单。我已经将其剥离到最基本的需求,以便我们的讨论更容易。

图 1 显示了我们的应用程序如何允许我们通过按顶部的按钮来插入新的联系人记录或删除当前记录。在应用程序的底部,我们可以移动到数据库中的上一个或下一个记录。当然,我们有输入字段来保存名字和姓氏以及电话号码。我们还有一个表单字段来显示记录 id 号。在实际应用程序中,我可能会将其设为隐藏字段,但为了教学目的,我将其保留为可见。

回顾清单 1,您可以看到该页面非常简单明了。除了导入 contacts.js JavaScript 之外,页面的第一部分是标准的样板代码。当我们到达表单按钮和字段时,事情变得有趣起来。

让我们看一下“新建”按钮

<input type=button name=new value="New"
    onclick="insert_record();">

此按钮只是在用户按下按钮时调用名为 insert_record() 的 JavaScript 函数。“删除”、“上一个”和“下一个”按钮的工作方式都类似。魔力在于 JavaScript。让我们首先看一下 JavaScript(清单 3)。

清单 3. JavaScript 代码处理数据库操作和数据处理。

var req;

function insert_record () {
   send_transaction("/cgi-bin/insert.pl");
   return 1;
}

function select_record (i) {
   send_transaction("/cgi-bin/select.pl?id=" + i);
   return 1;
}

function delete_record (i) {
   send_transaction("/cgi-bin/delete.pl?id=" + i);

   var id = document.getElementById("id");
   select_record(id);
   return;
}

function update_record (i, field, value) {
   send_transaction("/cgi-bin/update.pl?id=" + i +
   "&field=" + field + "&value=" + value);
   return 1;
}

function send_transaction (i) {

   if (window.XMLHttpRequest) {
      req = new XMLHttpRequest();
   } else if (window.ActiveXObject) {
      req = new ActiveXObject("Microsoft.XMLHTTP");
   }

   if (req) {
      req.onreadystatechange = process_results;
      req.open("GET", i, true);
      req.send(null);
   }
}

function process_results () {
   var name = "";
   var value = "";
   var fields;
   var i;
   var length;

   if (req.readyState < 4) { return 1; } // transaction
                                         // not done, yet

   var xml = req.responseXML;
   var result = xml.getElementsByTagName("result").item(0);

   fields = result.getElementsByTagName("field");
   length = fields.length;

   for (i=0; i<length; i++) {
      var field = fields[i];

      name = field.getAttribute("name");
      value = field.getAttribute("value");


      var form_field = document.getElementById(name);
      form_field.value = value;
   }

   return 1;
}

insert_record() JavaScript 函数在用户按下“新建”按钮时调用,它是最简单的 JavaScript 函数之一。insert_record() 所做的只是使用 send_transaction() 函数来调用 insert.pl Web 服务。实际上,insert_record()、delete_record()、select_record() 和 update_record() 函数都是 send_transaction() 的包装器。

send_transaction() 函数是 Ajax 进入我们应用程序的地方。此函数接受需要调用的服务的 URL 以及需要通过 HTTP 的 GET 方法传递给服务的任何参数。然后,该函数创建一个对象,允许调用该服务。我们必须跳过一个小障碍,因为 Microsoft 选择将此对象称为 ActiveXObject,而几乎所有其他浏览器都将其称为 XMLHttpRequest。一旦对象被创建,无论名称是什么,我们都会告诉对象调用我们的 Web 服务,然后在调用返回其结果时调用我们的 process_results() 函数。这是通过将函数名称分配给对象的 onreadystatechange 属性的行来完成的。

好吧,我撒了一点谎。事实证明,浏览器将在服务请求期间的各个阶段调用我们的 process_results() 函数最多四次。每次调用该函数时,readyState 属性的值都会更改以反映正在发生的事务阶段。不幸的是,似乎对于何时调用该函数没有太多共识。所有浏览器似乎都同意的唯一一点是,当事务完成时,readyState 属性设置为 4。检查此值是我们的 process_results() 函数所做的第一件事。如果事务未完成,我们只需安静地返回。

一旦事务完成,我们可以从请求对象的 responseXML 属性中恢复生成的 XML。获得 XML 后,我们循环遍历每个字段元素,记录字段名称和值。然后,我们在 HTML 文档中找到相应的字段,并将新值分配给它。因此,通过发送适当的 XML,Web 服务可以安排更新任何或所有 Web 表单字段。

如果您认为 JavaScript 很容易理解,请等到您看到实现 Web 服务的 Perl 脚本;它们甚至更容易理解和调试。insert.pl 程序如清单 4 所示。

清单 4. 服务器端 Perl 脚本处理数据库插入操作。

#!/usr/bin/perl

use DBI;

$dbh = DBI->connect("dbi:Pg:dbname=database",
 ↪"postgres", "password");

$dbh->do("insert into contacts (first,last,phone) values
(NULL,NULL,NULL)");

$sth = $dbh->prepare("select last_value from
 ↪contacts_id_seq");
$sth->execute();
($index) = $sth->fetchrow_array();

print "Content-type: text/xml\n\n\n";

print "<result>\n";
print "<field name=\"id\" value=\"$index\"></field>\n";
print "</result>\n";

此程序所做的只是连接到数据库,将空记录插入到 contacts 表中,检索新创建记录的 id,并以 text/xml MIME 类型返回 XML 块中的结果。生成的 XML 类似于清单 5 中所示。

清单 5. 生成的 XML

<result>
<field name="id" value="25"></field>
</result>

select.pl、delete.pl 和 update.pl 服务非常相似,分别如清单 6、7 和 8 所示。

清单 6. 用于选择数据的 Perl 脚本

#!/usr/bin/perl

use CGI;
use DBI;

$dbh = DBI->connect("dbi:Pg:dbname=database",
 ↪"postgres", "password");

print "Content-type: text/xml\n\n\n";
print "<result>\n";

$cgi = new CGI;

$id = $cgi->param("id");

$sth = $dbh->prepare("select * from contacts where id=$id");
$sth->execute();

$a = $sth->fetchrow_hashref();

foreach $key (keys %$a) {
   print "<field name=\"$key\" value=\"$a->{$key}\"></field>\n";
}

print "</result>\n";

清单 6 中显示的 select.pl 服务采用单个参数——要检索的记录的 id 号。结果是一个 XML 文件,其中包含记录中的所有字段和适当的值。这允许我们使用记录 id 调用该函数并检索该记录的所有字段以供稍后操作。

清单 7. 用于删除记录的 Perl 脚本

#!/usr/bin/perl

use CGI;
use DBI;

$dbh = DBI->connect("dbi:Pg:dbname=database",
 ↪"postgres", "password");

$cgi = new CGI;

$id = $cgi->param("id");

$dbh->do("delete from contacts where id=$id");

$sth = $dbh->prepare("select max(id) from contacts where id<$id");
$sth->execute();
($index) = $sth->fetchrow_array();

print "Content-type: text/xml\n\n\n";

print "<result>\n";
print "<field name=\"id\" value=\"$index\"></field>\n";
print "</result>\n";

清单 7 中显示的 delete.pl 服务采用记录 id 并删除具有该 id 的记录。然后,程序找到下一个最低的记录号并返回该记录 id。

清单 8. 用于更新记录的 Perl 脚本

#!/usr/bin/perl

use CGI;
use DBI;

$dbh = DBI->connect("dbi:Pg:dbname=database",
 ↪"postgres", "password");

$cgi = new CGI;

$id = $cgi->param("id");
$field = $cgi->param("field");
$value = $cgi->param("value");

$dbh->do("update contacts set $field=\'$value\' where id=$id");

print "Content-type: text/xml\n\n\n";
print "<result>\n";
print "<field name=\"$field\" value=\"$value\"></field>\n";
print "</result>\n";

最后,清单 8 中显示的 update.pl 服务采用记录 id、字段名称和新值作为参数。然后,程序使用新值更新所选记录的给定字段。然后通过 XML 返回新的字段值。

诚然,我们的小应用程序相当简单,但它确实执行了需要在数据库上执行的所有基本功能:插入、删除、更新和搜索。更重要的是,此应用程序的任何单个元素都不难编写、调试或理解。实际上,通过我对接下来概述的一些改进,Web 服务脚本和大部分 JavaScript 可以重用于更大应用程序的其他部分,甚至许多不同的应用程序。Web 服务只是成为用 JavaScript 粘合在一起以构建应用程序的砖块,这就是使用 Web 服务成为如此优雅的 Web 开发方法的原因。

从用户的角度来看,使用 Ajax 执行数据库功能是一大胜利。如前所述,一旦应用程序加载,用户永远不必承担重新下载它并让浏览器重新渲染它的成本。在更复杂的页面上,这可能会造成明显的延迟。此外,由于给定操作的结果以小段 XML 返回,因此带宽要求非常低。可以说,用户不仅会认为这种类型的应用程序更快,而且它还会降低提供应用程序的服务器和网络基础设施的需求。

但是,向我们的应用程序添加一个新字段(可能是一个电子邮件地址)有多难呢?好吧,我们必须向我们的数据库表方案添加一个适当的字段。然后,我们必须将具有相同名称的字段添加到我们的 HTML 文档中。当然,我们可以使用其他表单字段作为模板。而且,这应该就足够了。

那么,我们如何改进我们的代码呢?首先,我们需要处理一些明显的安全问题。我们的 Web 服务应该使用某种形式的身份验证,以确保只有授权用户才能执行数据库功能。但更微妙的是,Web 服务需要对它们接收的参数执行一些基本验证。delete.pl 服务以 id=25 的形式接受记录号作为参数。如果有人想恶意,而是向我们的服务发送 id=25 或 1=1 怎么办?好吧,我们的数据库将不复存在,因为 1=1 始终为真,并且我们的程序将删除 所有 记录。因此,我们必须在在野外使用这些服务之前处理此类问题。

您可能已经注意到,我们数据库中的所有字段都是 varchar(20) 类型。这不是很灵活或高效。为了真正有用,我们的服务需要能够查询数据库以确定给定字段的数据类型,并采取适当的措施。例如,字符和 varchar 需要用引号引起来,但整数和布尔值则不需要。该服务应该能够确定如何处理这些情况。

最后,通过简单地将表名作为参数之一发送,我们可以构建一个 Web 服务,该服务可以修改除我们的 contacts 表之外的其他数据库表。我们将能够使用相同的服务来更新购物清单、库存或日历。像这样概括我们的 Web 服务将使我们简单的联系人应用程序以及我们选择使用它们的任何其他应用程序都易于编写。

因此,通过将 Ajax 与我们自己的 Web 服务品牌相结合,我们能够编写对用户输入响应更快、对服务器基础设施要求更低且更易于编写和维护的应用程序。

Mike Diehl 是新墨西哥州阿尔伯克基市 Sandia 国家实验室的承包商,他在那里编写网络管理软件。Mike 与他的妻子和两个年幼的儿子住在一起,可以通过电子邮件 mdiehl@diehlnet.com 联系他。

加载 Disqus 评论