隐藏帧技术
Ajax通信支持许多不同的技术。每一种技术都有自己的优点和缺点,因此了解什么情况使用哪一种技术是很重要的。
隐藏帧技术
随着HTML帧的引入,隐藏帧(hidden frame)技术也应运而生了。该技术后面的基本想法是创建一个帧集,其中包含用于客户端—服务器通信的隐藏帧。可以通过将帧的宽度或高度设置为0像素来隐藏一个帧,以使其不显示。尽管一些早期的浏览器(诸如Netscape 4)不能够完全隐藏帧,经常会留下一些明显的帧边框,但该技术还是广泛地为开发人员所采用。
1. 模式
隐藏帧技术遵循一种特定的四步模式(参见图2-1)。第一步总是从一个与用户交互的Web页面中的可见帧开始的。显然,用户并不知道隐藏帧的存在(在现代浏览器中,它是不显示的),以通常的形式与网页进行交互。在某些时间,用户执行了一个需要从服务器获取额外数据的操作。当这个操作发生时,第一步就发生了:产生一个对隐藏帧的JavaScript函数调用。这个调用可以简单地将隐藏帧重定向到另一个页面,或者复杂地传送表单数据。不管这个函数有多复杂,其结果都是产生第2步:向服务器发送一个请求。
![](http://dev.yesky.com/imagelist/06/30/ui04jk1w9955.jpg)
图 2-1
该模式中的第3步是从服务器上接收一个响应。由于处理的是帧,因此该响应必然是另一个网页。该网页必须包含从服务器返回的所请求的数据,同时一些JavaScript将把这些数据传给可见的帧。通常,这是通过在返回的网页中分配一个onload事件处理函数(event handler)做到的,该网页在其全部载入之后调用可见帧中的函数(这就是第4步)。当数据位于可见帧中后,该帧就可以决定如何处理这些数据了。
2. 隐藏帧的GET请求
我们已经阐述了隐藏帧技术的基本原理,现在将更深入地研究它。对于任何一种新技术,最好的方法就是通过具体的实例来学习。在该实例中,将创建一个简单的查询页面,客户服务代表通过该页面可以查询客户的信息。由于这是本书的第一个例子,因此它十分的简单:用户输入客户ID,然后接收与该客户相关的信息。由于该功能通常需要数据库支持,因此还必须做一些服务器端的开发。该例子使用的是PHP——这是一种优秀的开源服务端语言,还将使用到MySQL(在从www.mysql.org下载)——这是一种与PHP结合得很好的开源数据库。
尽管本例确定为使用MySQL,但只需少量的修改就可以在其他数据库上运行。
首先,在实现客户资料查询之前,你必须有一个包含该信息的数据库表。可以使用以下SQL脚本来创建一个客户表:
CREATE TABLE `Customers` (
`CustomerId` int(11) NOT NULL auto_increment,
`Name` varchar(255) NOT NULL default '',
`Address` varchar(255) NOT NULL default '',
`City` varchar(255) NOT NULL default '',
`State` varchar(255) NOT NULL default '',
`Zip` varchar(255) NOT NULL default '',
`Phone` varchar(255) NOT NULL default '',
`E-mail` varchar(255) NOT NULL default '',
PRIMARY KEY (`CustomerId`)
) TYPE=MyISAM COMMENT='Sample Customer Data';
在这张数据库表中最重要的字段是CustomerId,我们将通过它来查询客户信息。
你可以在www.wrox.com下载这个脚本以及一些测试数据。
当建好数据库表后,就可以将精力转到HTML代码上了。要使用隐藏帧技术,首先必须创建一个HTML帧集,例如:
<frameset rows="100%,0" frameborder="0">
<frame name="displayFrame" src="display.htm" noresize="noresize" />
<frame name="hiddenFrame" src="about:blank" noresize="noresize" />
</frameset>
这部分代码中最重要的是<frameset/>元素的rows属性。通过将其设置为100%,0,浏览器就知道不显示名为hiddenFrame的第二个帧了。紧接着,将frameborder属性设置为0则是确保每个帧都没有可见的边框。在帧集声明中最后一个重要的步骤是为每个帧设置noresize属性,使得用户不可能在不经意间调整帧的大小而发现隐藏帧,隐藏帧的内容永远不会成为可显示的用户界面的一部分。
接下来要处理的是一个请求和显示客户信息的页面。这是一个相对简单的页面,由一个用来输入客户ID的文本框,一个执行请求的按钮,以及用来显示查询到的客户信息的<div>元素所组成:
<p>Enter customer ID number to retrieve information:</p>
<p>Customer ID: <input type="text" id="txtCustomerId" value="" /></p>
<p><input type="button" value="Get Customer Info"
onclick="requestCustomerInfo()" /></p>
<div id="divCustomerInfo"></div>
注意,按钮调用的是名为requestCustomerInfo()的函数,该函数将负责与隐藏帧交互以获取数据。它将获取文本框中的值,将其添加到getcustomerdata.php的查询字符串上,以getcustomerdata.php?id=23的格式创建一个URL。然后将这个URL指派给隐藏帧,以下就是这个函数的代码:
function requestCustomerInfo() {
var sId = document.getElementById("txtCustomerId").value;
top.frames["hiddenFrame"].location = "getcustomerdata.php?id=" + sId;
}
该函数的第一步是从文本框中获取客户标识号("txtCustomerId")。这是将文本框的ID txtCustomerId作为参数,调用document.getElementById()函数,并获取返回的value属性(value属性保存了文本框中的文本内容)来实现的。然后,将这个ID添加到字符串getcustomerdata.php?id=之后生成完整的URL。第二行代码则是创建此URL并将其赋给隐藏帧。为了获得对隐藏帧的引用,首先要使用top对象来获取浏览器的顶级窗口(topmost window)。该对象拥有一个frames数组,在其中可以找到这个隐藏帧。由于每个帧都是一个窗口对象,因此可以将其位置设置为预期的URL。
这是发出请求所需的所有信息。注意,由于这是一个GET请求(通过一个查询字符串传递信息),因此是很简单的。(很快,你将看到如何使用隐藏帧技术来执行一个POST请求。)
除了requestCustomerInfo()函数之外,还需要另一个在查询后显示客户信息的函数。当数据返回时,隐藏帧将调用这个displayCustomerInfo()函数,其唯一的参数是包含要显示的客户数据的字符串:
function displayCustomerInfo(sText) {
var divCustomerInfo = document.getElementById("divCustomerInfo");
divCustomerInfo.innerHTML = sText;
}
在这个函数中,第一行代码将查询对用于数据显示的<div/>元素的引用。第二行代码将把包含客户信息的字符串(sText)的值赋给<div/>元素的innerHTML属性。使用innerHTML属性,可以将HTML嵌入到格式化的字符串中。这将由主显示页面的代码来完成。现在我们将创建服务器端程序逻辑。
getcustomerdata.php中的基本代码是在基本的HTML页面上添加两处PHP代码:
<html>
<head>
<title>Get Customer Data</title>
<?php
//php代码
?>
</head>
<body>
<div id="divInfoToReturn"><?php echo $sInfo ?></div>
</body>
</html>
在该页面中,第一个PHP代码块将包括查询客户数据的逻辑(很快将讨论到)。而第二个PHP代码块则负责将包含客户数据的$sInfo变量的值输出到<div/>元素中。从这个<div/>元素中,你可以读取该数据并将数据传送给显示帧。为此,需要创建在页面完全载入后调用的JavaScript函数。
window.onload = function () {
var divInfoToReturn = document.getElementById("divInfoToReturn");
top.frames["displayFrame"].displayCustomerInfo(divInfoToReturn.innerHTML);
};
该函数将直接赋给window.onload事件处理函数中。它首先获取对包含客户信息的<div/>元素的引用,然后使用数组top.frames访问显示帧,并调用前面定义的display CustomerInfo()函数,将其传给<div/>元素的innerHTML属性。这就是所有与发送该信息相关的JavaScript。但首先如何获取这些信息呢?需要一些PHP代码来从数据库查询信息。
在PHP代码中的第一步是定义所有需要的数据块。在本例中,这些数据块包括用来查询的客户ID、返回信息的$sInfo变量,以及访问数据库所需要的信息(数据库服务器、数据库名、用户名、密码以及SQL查询字符串):
<?php
$sID = $_GET["id"];
$sInfo = "";
$sDBServer = "your.databaser.server";
$sDBName = "your_db_name";
$sDBUsername = "your_db_username";
$sDBPassword = "your_db_password";
$sQuery = "Select * from Customers where CustomerId=".$sID;
//更多代码
?>
这段代码首先从查询字符串中获取id参数。为了便于获取,PHP将所有的查询字符串参数组织于$-GET数组中。这个id存储在$sID中,它将用来创建存储于$sQuery中的SQL查询字符串。在此还将创建$sInfo变量,并将其设置为空字符串。在这段代码中的所有其他变量,都包含了指定特定数据库配置的信息,根据你自己的实现环境将其替换为正确的值。
获取了用户的输入,做好了连接数据库的基本准备,下一步就是创建数据库连接,执行查询,返回结果。如果存在一个指定ID的客户,$sInfo将填入包含所有数据的HTML字符串,包括对电子邮件地址创建一个链接,如果客户ID是无效的,那么$sInfo将填入错误消息,以传给显示帧:
<?php
$sID = $_GET["id"];
$sInfo = "";
$sDBServer = "your.databaser.server";
$sDBName = "your_db_name";
$sDBUsername = "your_db_username";
$sDBPassword = "your_db_password";
$sQuery = "Select * from Customers where CustomerId=".$sID;
$oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
@mysql_select_db($sDBName) or $sInfo="Unable to open database";
if($oResult = mysql_query($sQuery) and mysql_num_rows($oResult) > 0) {
$aValues = mysql_fetch_array($oResult,MYSQL_ASSOC);
$sInfo = $aValues['Name']."<br />".$aValues['Address']."<br />".
$aValues['City']."<br />".$aValues['State']."<br />".
$aValues['Zip']."<br /><br />Phone: ".$aValues['Phone']."<br />".
"<a href=\"mailto:".$aValues['E-mail']."\">".
$aValues['E-mail']."</a>";
} else {
$sInfo = "Customer with ID $sID doesn't exist.";
}
mysql_close($oLink);
?>
突出显示的头两行代码用来完成从PHP到MySQL数据库的连接。紧接着,调用mysql_ query()函数来执行SQL查询。如果函数返回结果,并且该结果至少包括一行,那么程序将获取该信息,并将其存入变量$sInfo中;否则,$sInfo将填入一个错误消息。最后两行则负责释放数据库连接。
关于更复杂的PHP和MySQL编程的阐述已超出了本文讨论的范围。
现在当$sInfo输出到<div/>元素时,它将包含正确的信息。onload事件处理函数将读取这些数据,然后将其发送到显示帧上。如果查询到客户,其相应的信息将会显示出来,如图2-2所示。
另一方面,如果客户不存在,则会在屏幕的相同位置显示错误消息。无论如何,客户服务代表都将获得一个很好的用户体验。你的第一个Ajax程序也就完成了。
![](http://dev.yesky.com/imagelist/06/30/293hai5wh7x3.jpg)
图 2-2
3. 隐藏帧的POST请求
前面的例子使用GET请求来从数据库中获取信息。由于客户ID能够以查询字符串的形式添加到URL中,因此十分简单。但如果需要发送POST请求该怎么办呢?它也可以使用隐藏帧技术,不过需要一些额外的工作。
POST请求通常是用于向服务器发送数据的场合,而与GET请求仅从服务器上获取数据不同。尽管GET请求可以通过查询字符串来向服务器发送额外的数据,但一些浏览器最多只能够处理512KB以内的查询字符串信息。对于POST请求而言,则可以发送2GB的信息,能够良好地满足绝大多数的应用。
从传统意义上说,只能够通过将表单的method属性设置为post来发送POST请求。然后,包含在表单中的数据就会通过POST请求发送到action属性中指定的URL上。更复杂的问题是当表单提交之后,将会从当前页跳转到一个新的URL上,这与Ajax的目的是背道而驰的。但万幸的是,可以通过表单中一个不太知名的target属性来简单实现。
<form/>元素的target属性的功能从某种意义上说与<a/>元素的target属性的功能类似:用来指定跳转的目的URL。通过设置表单元素的target属性,可以有效地使得在其他帧或窗口(在本例中是隐藏帧)中显示出表单的提交结果之后,表单页面仍然保持不变。
首先重新定义一个帧集。与上一个例子唯一不同的是可见帧包含了用来输入客户数据的表单:
<frameset rows="100%,0" frameborder="0">
<frame name="displayFrame" src="entry.htm" noresize="noresize" />
<frame name="hiddenFrame" src="about:blank" noresize="noresize" />
</frameset>
输入表单的内容包含在一个<form/>元素中,而且针对保存在数据库中的每个字段都有一个相应的文本框(除了自动生成的客户ID之外)。同样也有一个<div/>元素,用来显示与客户端—服务器通信相关的状态信息:
<form method="post" action="SaveCustomer.php" target="hiddenFrame">
<p>Enter customer information to be saved:</p>
<p>Customer Name: <input type="text" name="txtName" value="" /><br />
Address: <input type="text" name="txtAddress" value="" /><br />
City: <input type="text" name="txtCity" value="" /><br />
State: <input type="text" name="txtState" value="" /><br />
Zip Code: <input type="text" name="txtZipCode" value="" /><br />
Phone: <input type="text" name="txtPhone" value="" /><br />
E-mail: <input type="text" name="txtEmail" value="" /></p>
<p><input type="submit" value="Save Customer Info" /></p>
</form>
<div id="divStatus"></div>
注意,<form/>元素的target属性也设置为hiddenFrame,因此当用户点击该按钮时,提交的结果将显示在隐藏帧中。
在本例中,主页面中只需要一个JavaScript函数:savaResult()。当隐藏帧返回客户数据保存结果时,将调用该函数:
function saveResult(sMessage) {
var divStatus = document.getElementById("divStatus");
divStatus.innerHTML = "Request completed: " + sMessage;
}
隐藏帧的职责是向该函数传递一个消息,该消息将显示给用户。它可能是信息已保存的确认信息,或者是说明为什么保存失败的错误信息。
接下来处理POST请求的文件是SavaCustomer.php。与前一个例子一样,该页面也是由简单的HTML页面加上一些PHP和JavaScript代码组成的。其中PHP代码用来从请求中收集信息,然后将其保存到数据库中。由于这是一个POST请求,因此$_POST数组中包含了提交的所有信息:
<?php
$sName = $_POST["txtName"];
$sAddress = $_POST["txtAddress"];
$sCity = $_POST["txtCity"];
$sState = $_POST["txtState"];
$sZipCode = $_POST["txtZipCode"];
$sPhone = $_POST["txtPhone"];
$sEmail = $_POST["txtEmail"];
$sStatus = "";
$sDBServer = "your.database.server";
$sDBName = "your_db_name";
$sDBUsername = "your_db_username";
$sDBPassword = "your_db_password";
$sSQL = "Insert into Customers(Name,Address,City,State,Zip,Phone,`E-mail`) ".
" values ('$sName','$sAddress','$sCity','$sState', '$sZipCode'".
", '$sPhone', '$sEmail')";
//更多代码
?>
这个代码片段将获取与客户相关的所有POST信息;此外,还定义了一个状态消息($sStatus)以及所需的数据库信息(与上一个例子相同)。这里的SQL语句是一个INSERT语句,它将获取的信息添加到数据库中。
执行这个SQL语句的代码与上一个例子十分类似:
<?php
$sName = $_POST["txtName"];
$sAddress = $_POST["txtAddress"];
$sCity = $_POST["txtCity"];
$sState = $_POST["txtState"];
$sZipCode = $_POST["txtZipCode"];
$sPhone = $_POST["txtPhone"];
$sEmail = $_POST["txtEmail"];
$sStatus = "";
$sDBServer = "your.database.server";
$sDBName = "your_db_name";
$sDBUsername = "your_db_username";
$sDBPassword = "your_db_password";
$sSQL = "Insert into Customers(Name,Address,City,State,Zip,Phone,`E-mail`) ".
" values ('$sName','$sAddress','$sCity','$sState', '$sZipCode'".
", '$sPhone', '$sEmail')";
$oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
@mysql_select_db($sDBName) or $sStatus = "Unable to open database";
if($oResult = mysql_query($sSQL)) {
$sStatus = "Added customer; customer ID is ".mysql_insert_id();
} else {
$sStatus = "An error occurred while inserting; customer not saved.";
}
mysql_close($oLink);
?>
在此,mysql_query()函数的结果只是一个表示语句执行成功的指示器。如果执行成功, $sStatus变量中将填入一个消息,表明保存已经成功,并返回为该数据指定的客户ID。mysql_ insert_id()函数始终返回在最新的INSERT语句返回值的基础上自动递增的值。如果因为某些原因,该语句没有成功执行,$sStatus变量将填入一个错误消息。
$sStatus变量将输出到一个在载入窗口时运行的JavaScript函数中:
<script type="text/javascript">
window.onload = function () {
top.frames["displayFrame"].saveResult("<?php echo $sStatus ?>");
}
</script>
这段代码调用了savaResult()函数,该函数定义于显示帧中,传入的参数值是PHP变量$sStatus。由于该变量包含一个字符串,因此必须将PHP的echo语句放在引号中。当执行该函数时,假设客户数据已保存,则输入表单页面看起来如图2-3所示。
![](http://dev.yesky.com/imagelist/06/30/4faaduyn64e7.jpg)
图 2-3
当执行这段代码之后,你还可以自由地使用同样的表单向数据库中添加更多客户,因为它不再消失。
4. 隐藏iFrame
新一代的客户端—服务器通信模式幕后所采用的是iframe,它是在HTML 4.0中引入的。iframe与帧基本是相同的,唯一的区别是iframe可以放在一个未设置帧集的HTML页面中,可以使页面中的任意部分成为一个帧。iframe技术可以在未预先设置帧集的页面中使用,能够更好地适应于功能的逐渐添加。iframe甚至还可以使用JavaScript在运行时创建,为了简单起见,语义化HTML(semantic HTML)支持使浏览器将Ajax功能看作是一个有益的增强(这将在稍后讨论)。由于可以用与普通帧相同的方法使用和访问iframe,因此它们都是Ajax通信的理想选择。
发挥iframe的优势有两种方法。最简单的方法是在页面中简单地嵌入iframe,并像隐藏帧那样用来发出请求。为此,第一个例子中的显示页面将修改为:
<p>Enter customer ID number to retrieve information:</p>
<p>Customer ID: <input type="text" id="txtCustomerId" value="" /></p>
<p><input type="button" value="Get Customer Info"
onclick="requestCustomerInfo()" /></p>
<div id="divCustomerInfo"></div>
<iframe src="about:blank" name="hiddenFrame" width="0" height="0"
frameborder="0"></iframe>
注意,这个iframe中的width、height和frameborder属性都设置成了0,这可将其从视线中隐去。由于iframe的名字仍是hiddenFrame,所以这个页面的JavaScript代码可以如前一样正常工作。不过,对于GetCustomerData.php页面还需要做一些小的修改。在该页面中的JavaScript函数先前是在名为displayFrame的帧中查找displayCustomerInfo()函数。如果你使用该技术,又不存在该名字的帧,则必须修改代码,用parent来代替它:
window.onload = function () {
var divInfoToReturn = document.getElementById("divInfoToReturn");
parent.displayCustomerInfo(divInfoToReturn.innerHTML);
};
现在这个例子能够和本文中的第一例子一样正常工作了。
第二种使用隐藏iframe的方法是通过JavaScript动态地创建它们。由于并非所有浏览器实现iframe的方法都是一样的,所以需要一些技巧,使得它有助于一步步地创建隐藏的iframe。
第一步很简单,使用document.createElement()方法创建iframe并赋予必要的属性:
function createIFrame() {
var oIFrameElement = document.createElement("iframe");
oIFrameElement.width=0;
oIFrameElement.height=0;
oIFrameElement.frameBorder=0;
oIFrameElement.name = "hiddenFrame";
oIFrameElement.id = "hiddenFrame";
document.body.appendChild(oIFrameElement);
//更多代码
}
本段代码的最后一行很重要,因为它将iframe添加到document结构中;没有添加到document中的iframe是无法执行请求的。另外注意,该iframe的name和id属性都是设置为hiddenFrame。这是必要的,因为有些浏览器是通过name属性访问新的帧,而有些则是通过id属性新的帧。
紧接着定义一个全局变量,用来保存对该帧对象的引用。注意,针对iframe元素的这个帧对象并非是从createElement()函数返回的。要获得该对象,必须从帧集合中获取。以下就是即将保存在全局变量中的内容:
var oIFrame = null;
function createIFrame() {
var oIFrameElement = document.createElement("iframe");
oIFrameElement.width=0;
oIFrameElement.height=0;
oIFrameElement.frameBorder=0;
oIFrameElement.name = "hiddenFrame";
oIFrameElement.id = "hiddenFrame";
document.body.appendChild(oIFrameElement);
oIFrame = frames["hiddenFrame"];
}
如果你将这些代码放到前面的iframe例子中,那么需要对requestCustomerInfo()函数进行如下修改:
function requestCustomerInfo() {
if (!oIFrame) {
createIFrame();
setTimeout(requestCustomerInfo, 10);
return;
}
var sId = document.getElementById("txtCustomerId").value;
oIFrame.location = "GetCustomerData.php?id=" + sId;
}
基于这些修改,该函数将会检查oIFrame是否为空。如果为空,则调用createFrame(),并会为该函数的调用设置10ms的超时时间。这是很必要的,因为只有IE浏览器能够立即识别插入的iframe,大部分其他浏览器需要花几毫秒来识别它,以允许通过它发送请求。当再次执行该函数时,将执行代码的其余部分,其中最后一行已经修改为对OIFrame对象的引用。
尽管该技术能够很容易地应用于GET请求,但POST请求却完全不同。只有一部分浏览器允许你设置表单的target属性来动态创建iframe;但IE并不是其中的一种。因此,要使用隐藏iframe技术来发送POST请求还需要一些技巧。
5. 隐藏iframe的POST请求
要使用隐藏iframe来完成POST请求,其方法是在隐藏帧中载入一个包含表单的页面,用数据填充该表单,然后再提交该表单。当这个可见的表单(你实际输入数据的那个)提交时,必须取消这次提交而将信息转发给隐藏帧。为此,必须定义一个函数,用来处理iframe的创建以及隐藏表单的载入:
function checkIFrame() {
if (!oIFrame) {
createIFrame();
}
setTimeout(function () {
oIFrame.location = "ProxyForm.htm";
}, 10);
}
这个名为checkIFrame()的函数首先检查隐藏的iframe是否已经创建。如果没有,则调用createIFrame()。然后,在将iframe的地址设置为ProxyForm.htm(这是一个隐藏表单页面)之前,为其设置一个超时值。由于该函数调用需要花一些时间,而重要的是该页面每次加载时都将提交该表单。
ProxyForm.htm文件很简单,只包括很少的JavaScript,用来提示主页面已经装载完成:
<html>
<head>
<title>Proxy Form</title>
<script type="text/javascript">
window.onload = function () {
parent.formReady();
}
</script>
</head>
<body>
<form method="post"></form>
</body>
</html>
正如你所见,该页面的主体只包含一个空的表单,而标题中只包含一个onload事件处理函数。当载入该页面时,页面将通过调用parent.formReady()来使主页面知道它已经做好接收请求的准备。而formReady()函数则是包含在主页面本身中的,类似于:
function formReady() {
var oHiddenForm = oIFrame.document.forms[0];
var oForm = document.forms[0];
for (var i=0 ; i < oForm.elements.length; i++) {
var oHidden = oIFrame.document.createElement("input");
oHidden.type = "hidden";
oHidden.name = oForm.elements[i].name;
oHidden.value = oForm.elements[i].value;
oHiddenForm.appendChild(oHidden);
}
oHiddenForm.action = oForm.action;
oHiddenForm.submit();
};
在该函数中的第一步是获取对隐藏iframe中表单的引用,可以通过访问该帧的document.forms集合来获取。由于在该页面中只有一个表单,因此可以安全地从该集合中获得第一个表单(即索引值为0),并将其存储于oHiddenForm中。然后,将对主页面表单的引用存于oForm中。紧接着,一个for循环对主页面中该表单的各元素进行遍历(使用elements集合)。对于表单中的每一个元素,都将在隐藏帧(注意,必须使用oIFrame.document.createElement()而不只是document.createElement())中创建一个隐藏的输入元素。这个隐藏的输入元素拥有与该表单元素相同的名字和值,然后使用appendChild()函数将其添加到隐藏的表单中。
当所有的表单元素都添加完后,隐藏的表单还将设置与主页面表单相同的action。通过从表单中读取action来取代硬编码,就可以在任何页面中使用formReady()。该函数的最后一步是提交这个隐藏的表单。
剩下的最后一件事就是确保主页面的表单不以通常的方式提交自己。要达到这一目标,只需在onsubmit事件处理函数中调用checkIFrame()并返回false:
<form method="post" action="SaveCustomer.php"
onsubmit="checkIFrame();return false">
<p>Enter customer information to be saved:</p>
<p>Customer Name: <input type="text" name="txtName" value="" /><br />
Address: <input type="text" name="txtAddress" value="" /><br />
City: <input type="text" name="txtCity" value="" /><br />
State: <input type="text" name="txtState" value="" /><br />
Zip Code: <input type="text" name="txtZipCode" value="" /><br />
Phone: <input type="text" name="txtPhone" value="" /><br />
E-mail: <input type="text" name="txtEmail" value="" /></p>
<p><input type="submit" value="Save Customer Info" /></p>
</form>
<div id="divStatus"></div>
通过以这种方式返回flase,可以阻止表单的默认行为(将自己提交到服务器)。通过调用checkIFrame()方法来启动隐藏iframe中表单的提交进程。
当这一任务完成后,就可以像使用隐藏帧POST请求的例子一样使用本例;页面SavaCustomer.php负责处理数据,并当完成时调用主页面中的savaResult()函数。
注意,本节中的例子是为了使其聚焦于与Ajax技术相关的问题上,因而进行了简化。如果在实际的Web应用程序中使用,还需要提供更多的用户反馈,诸如在发出请求时屏蔽该表单的输入等。
6. 隐藏帧技术的优点和缺点
现在,你已经对使用隐藏帧所实现的强大功能有所了解了,我们将讨论它的实用性。正如前面所说的,该技术已经存在多年,并且仍然在许多Ajax应用中使用。
使用隐藏帧的一个最大理由之一是它可以维护浏览器的历史,使用户仍然能够使用浏览器上的后退和前进按钮。浏览器由于并不知道隐藏帧实际上被隐藏了,但对于其所发出的请求仍然是记录在案的。然而,Ajax应用程序的主页面却没有修改,在隐藏帧中的修改意味着后退和前进按钮将依据该隐藏帧的访问历史而非主页面而变化。这也是为什么Gmail和Google Maps仍然使用该技术的理由。
注意,iframe并非一直会存储浏览器的历史记录。尽管IE始终会存储iframe的历史记录,但Firefox只对使用HTML定义(也就是不包括使用JavaScript动态创建)的iframe保存历史记录。Safari从不为iframe保存历史记录,不管它们是如何包含在该页面中的。
隐藏帧技术不利的一面是,对其背后发生的事了解甚少。它完全依赖于返回的正确页面。本节的例子都存在相同的问题:如果隐藏帧的页面载入失败,并不会向用户提示出错消息;主页面将继续等待直到调用适当的JavaScript函数。必须通过设置一个较长周期(可能是5分钟)的超时时间,然后如果页面仍然没有成功载入则显示一条消息,以给用户一个安慰。但这一切都只是一个变通方法,最主要的问题是,对于后台发生的HTTP请求缺乏充足的信息。幸运的,我们还有其他选择。
隐藏帧技术
随着HTML帧的引入,隐藏帧(hidden frame)技术也应运而生了。该技术后面的基本想法是创建一个帧集,其中包含用于客户端—服务器通信的隐藏帧。可以通过将帧的宽度或高度设置为0像素来隐藏一个帧,以使其不显示。尽管一些早期的浏览器(诸如Netscape 4)不能够完全隐藏帧,经常会留下一些明显的帧边框,但该技术还是广泛地为开发人员所采用。
1. 模式
隐藏帧技术遵循一种特定的四步模式(参见图2-1)。第一步总是从一个与用户交互的Web页面中的可见帧开始的。显然,用户并不知道隐藏帧的存在(在现代浏览器中,它是不显示的),以通常的形式与网页进行交互。在某些时间,用户执行了一个需要从服务器获取额外数据的操作。当这个操作发生时,第一步就发生了:产生一个对隐藏帧的JavaScript函数调用。这个调用可以简单地将隐藏帧重定向到另一个页面,或者复杂地传送表单数据。不管这个函数有多复杂,其结果都是产生第2步:向服务器发送一个请求。
![](http://dev.yesky.com/imagelist/06/30/ui04jk1w9955.jpg)
图 2-1
该模式中的第3步是从服务器上接收一个响应。由于处理的是帧,因此该响应必然是另一个网页。该网页必须包含从服务器返回的所请求的数据,同时一些JavaScript将把这些数据传给可见的帧。通常,这是通过在返回的网页中分配一个onload事件处理函数(event handler)做到的,该网页在其全部载入之后调用可见帧中的函数(这就是第4步)。当数据位于可见帧中后,该帧就可以决定如何处理这些数据了。
2. 隐藏帧的GET请求
我们已经阐述了隐藏帧技术的基本原理,现在将更深入地研究它。对于任何一种新技术,最好的方法就是通过具体的实例来学习。在该实例中,将创建一个简单的查询页面,客户服务代表通过该页面可以查询客户的信息。由于这是本书的第一个例子,因此它十分的简单:用户输入客户ID,然后接收与该客户相关的信息。由于该功能通常需要数据库支持,因此还必须做一些服务器端的开发。该例子使用的是PHP——这是一种优秀的开源服务端语言,还将使用到MySQL(在从www.mysql.org下载)——这是一种与PHP结合得很好的开源数据库。
尽管本例确定为使用MySQL,但只需少量的修改就可以在其他数据库上运行。
首先,在实现客户资料查询之前,你必须有一个包含该信息的数据库表。可以使用以下SQL脚本来创建一个客户表:
CREATE TABLE `Customers` (
`CustomerId` int(11) NOT NULL auto_increment,
`Name` varchar(255) NOT NULL default '',
`Address` varchar(255) NOT NULL default '',
`City` varchar(255) NOT NULL default '',
`State` varchar(255) NOT NULL default '',
`Zip` varchar(255) NOT NULL default '',
`Phone` varchar(255) NOT NULL default '',
`E-mail` varchar(255) NOT NULL default '',
PRIMARY KEY (`CustomerId`)
) TYPE=MyISAM COMMENT='Sample Customer Data';
在这张数据库表中最重要的字段是CustomerId,我们将通过它来查询客户信息。
你可以在www.wrox.com下载这个脚本以及一些测试数据。
当建好数据库表后,就可以将精力转到HTML代码上了。要使用隐藏帧技术,首先必须创建一个HTML帧集,例如:
<frameset rows="100%,0" frameborder="0">
<frame name="displayFrame" src="display.htm" noresize="noresize" />
<frame name="hiddenFrame" src="about:blank" noresize="noresize" />
</frameset>
这部分代码中最重要的是<frameset/>元素的rows属性。通过将其设置为100%,0,浏览器就知道不显示名为hiddenFrame的第二个帧了。紧接着,将frameborder属性设置为0则是确保每个帧都没有可见的边框。在帧集声明中最后一个重要的步骤是为每个帧设置noresize属性,使得用户不可能在不经意间调整帧的大小而发现隐藏帧,隐藏帧的内容永远不会成为可显示的用户界面的一部分。
接下来要处理的是一个请求和显示客户信息的页面。这是一个相对简单的页面,由一个用来输入客户ID的文本框,一个执行请求的按钮,以及用来显示查询到的客户信息的<div>元素所组成:
<p>Enter customer ID number to retrieve information:</p>
<p>Customer ID: <input type="text" id="txtCustomerId" value="" /></p>
<p><input type="button" value="Get Customer Info"
onclick="requestCustomerInfo()" /></p>
<div id="divCustomerInfo"></div>
注意,按钮调用的是名为requestCustomerInfo()的函数,该函数将负责与隐藏帧交互以获取数据。它将获取文本框中的值,将其添加到getcustomerdata.php的查询字符串上,以getcustomerdata.php?id=23的格式创建一个URL。然后将这个URL指派给隐藏帧,以下就是这个函数的代码:
function requestCustomerInfo() {
var sId = document.getElementById("txtCustomerId").value;
top.frames["hiddenFrame"].location = "getcustomerdata.php?id=" + sId;
}
该函数的第一步是从文本框中获取客户标识号("txtCustomerId")。这是将文本框的ID txtCustomerId作为参数,调用document.getElementById()函数,并获取返回的value属性(value属性保存了文本框中的文本内容)来实现的。然后,将这个ID添加到字符串getcustomerdata.php?id=之后生成完整的URL。第二行代码则是创建此URL并将其赋给隐藏帧。为了获得对隐藏帧的引用,首先要使用top对象来获取浏览器的顶级窗口(topmost window)。该对象拥有一个frames数组,在其中可以找到这个隐藏帧。由于每个帧都是一个窗口对象,因此可以将其位置设置为预期的URL。
这是发出请求所需的所有信息。注意,由于这是一个GET请求(通过一个查询字符串传递信息),因此是很简单的。(很快,你将看到如何使用隐藏帧技术来执行一个POST请求。)
除了requestCustomerInfo()函数之外,还需要另一个在查询后显示客户信息的函数。当数据返回时,隐藏帧将调用这个displayCustomerInfo()函数,其唯一的参数是包含要显示的客户数据的字符串:
function displayCustomerInfo(sText) {
var divCustomerInfo = document.getElementById("divCustomerInfo");
divCustomerInfo.innerHTML = sText;
}
在这个函数中,第一行代码将查询对用于数据显示的<div/>元素的引用。第二行代码将把包含客户信息的字符串(sText)的值赋给<div/>元素的innerHTML属性。使用innerHTML属性,可以将HTML嵌入到格式化的字符串中。这将由主显示页面的代码来完成。现在我们将创建服务器端程序逻辑。
getcustomerdata.php中的基本代码是在基本的HTML页面上添加两处PHP代码:
<html>
<head>
<title>Get Customer Data</title>
<?php
//php代码
?>
</head>
<body>
<div id="divInfoToReturn"><?php echo $sInfo ?></div>
</body>
</html>
在该页面中,第一个PHP代码块将包括查询客户数据的逻辑(很快将讨论到)。而第二个PHP代码块则负责将包含客户数据的$sInfo变量的值输出到<div/>元素中。从这个<div/>元素中,你可以读取该数据并将数据传送给显示帧。为此,需要创建在页面完全载入后调用的JavaScript函数。
window.onload = function () {
var divInfoToReturn = document.getElementById("divInfoToReturn");
top.frames["displayFrame"].displayCustomerInfo(divInfoToReturn.innerHTML);
};
该函数将直接赋给window.onload事件处理函数中。它首先获取对包含客户信息的<div/>元素的引用,然后使用数组top.frames访问显示帧,并调用前面定义的display CustomerInfo()函数,将其传给<div/>元素的innerHTML属性。这就是所有与发送该信息相关的JavaScript。但首先如何获取这些信息呢?需要一些PHP代码来从数据库查询信息。
在PHP代码中的第一步是定义所有需要的数据块。在本例中,这些数据块包括用来查询的客户ID、返回信息的$sInfo变量,以及访问数据库所需要的信息(数据库服务器、数据库名、用户名、密码以及SQL查询字符串):
<?php
$sID = $_GET["id"];
$sInfo = "";
$sDBServer = "your.databaser.server";
$sDBName = "your_db_name";
$sDBUsername = "your_db_username";
$sDBPassword = "your_db_password";
$sQuery = "Select * from Customers where CustomerId=".$sID;
//更多代码
?>
这段代码首先从查询字符串中获取id参数。为了便于获取,PHP将所有的查询字符串参数组织于$-GET数组中。这个id存储在$sID中,它将用来创建存储于$sQuery中的SQL查询字符串。在此还将创建$sInfo变量,并将其设置为空字符串。在这段代码中的所有其他变量,都包含了指定特定数据库配置的信息,根据你自己的实现环境将其替换为正确的值。
获取了用户的输入,做好了连接数据库的基本准备,下一步就是创建数据库连接,执行查询,返回结果。如果存在一个指定ID的客户,$sInfo将填入包含所有数据的HTML字符串,包括对电子邮件地址创建一个链接,如果客户ID是无效的,那么$sInfo将填入错误消息,以传给显示帧:
<?php
$sID = $_GET["id"];
$sInfo = "";
$sDBServer = "your.databaser.server";
$sDBName = "your_db_name";
$sDBUsername = "your_db_username";
$sDBPassword = "your_db_password";
$sQuery = "Select * from Customers where CustomerId=".$sID;
$oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
@mysql_select_db($sDBName) or $sInfo="Unable to open database";
if($oResult = mysql_query($sQuery) and mysql_num_rows($oResult) > 0) {
$aValues = mysql_fetch_array($oResult,MYSQL_ASSOC);
$sInfo = $aValues['Name']."<br />".$aValues['Address']."<br />".
$aValues['City']."<br />".$aValues['State']."<br />".
$aValues['Zip']."<br /><br />Phone: ".$aValues['Phone']."<br />".
"<a href=\"mailto:".$aValues['E-mail']."\">".
$aValues['E-mail']."</a>";
} else {
$sInfo = "Customer with ID $sID doesn't exist.";
}
mysql_close($oLink);
?>
突出显示的头两行代码用来完成从PHP到MySQL数据库的连接。紧接着,调用mysql_ query()函数来执行SQL查询。如果函数返回结果,并且该结果至少包括一行,那么程序将获取该信息,并将其存入变量$sInfo中;否则,$sInfo将填入一个错误消息。最后两行则负责释放数据库连接。
关于更复杂的PHP和MySQL编程的阐述已超出了本文讨论的范围。
现在当$sInfo输出到<div/>元素时,它将包含正确的信息。onload事件处理函数将读取这些数据,然后将其发送到显示帧上。如果查询到客户,其相应的信息将会显示出来,如图2-2所示。
另一方面,如果客户不存在,则会在屏幕的相同位置显示错误消息。无论如何,客户服务代表都将获得一个很好的用户体验。你的第一个Ajax程序也就完成了。
![](http://dev.yesky.com/imagelist/06/30/293hai5wh7x3.jpg)
图 2-2
3. 隐藏帧的POST请求
前面的例子使用GET请求来从数据库中获取信息。由于客户ID能够以查询字符串的形式添加到URL中,因此十分简单。但如果需要发送POST请求该怎么办呢?它也可以使用隐藏帧技术,不过需要一些额外的工作。
POST请求通常是用于向服务器发送数据的场合,而与GET请求仅从服务器上获取数据不同。尽管GET请求可以通过查询字符串来向服务器发送额外的数据,但一些浏览器最多只能够处理512KB以内的查询字符串信息。对于POST请求而言,则可以发送2GB的信息,能够良好地满足绝大多数的应用。
从传统意义上说,只能够通过将表单的method属性设置为post来发送POST请求。然后,包含在表单中的数据就会通过POST请求发送到action属性中指定的URL上。更复杂的问题是当表单提交之后,将会从当前页跳转到一个新的URL上,这与Ajax的目的是背道而驰的。但万幸的是,可以通过表单中一个不太知名的target属性来简单实现。
<form/>元素的target属性的功能从某种意义上说与<a/>元素的target属性的功能类似:用来指定跳转的目的URL。通过设置表单元素的target属性,可以有效地使得在其他帧或窗口(在本例中是隐藏帧)中显示出表单的提交结果之后,表单页面仍然保持不变。
首先重新定义一个帧集。与上一个例子唯一不同的是可见帧包含了用来输入客户数据的表单:
<frameset rows="100%,0" frameborder="0">
<frame name="displayFrame" src="entry.htm" noresize="noresize" />
<frame name="hiddenFrame" src="about:blank" noresize="noresize" />
</frameset>
输入表单的内容包含在一个<form/>元素中,而且针对保存在数据库中的每个字段都有一个相应的文本框(除了自动生成的客户ID之外)。同样也有一个<div/>元素,用来显示与客户端—服务器通信相关的状态信息:
<form method="post" action="SaveCustomer.php" target="hiddenFrame">
<p>Enter customer information to be saved:</p>
<p>Customer Name: <input type="text" name="txtName" value="" /><br />
Address: <input type="text" name="txtAddress" value="" /><br />
City: <input type="text" name="txtCity" value="" /><br />
State: <input type="text" name="txtState" value="" /><br />
Zip Code: <input type="text" name="txtZipCode" value="" /><br />
Phone: <input type="text" name="txtPhone" value="" /><br />
E-mail: <input type="text" name="txtEmail" value="" /></p>
<p><input type="submit" value="Save Customer Info" /></p>
</form>
<div id="divStatus"></div>
注意,<form/>元素的target属性也设置为hiddenFrame,因此当用户点击该按钮时,提交的结果将显示在隐藏帧中。
在本例中,主页面中只需要一个JavaScript函数:savaResult()。当隐藏帧返回客户数据保存结果时,将调用该函数:
function saveResult(sMessage) {
var divStatus = document.getElementById("divStatus");
divStatus.innerHTML = "Request completed: " + sMessage;
}
隐藏帧的职责是向该函数传递一个消息,该消息将显示给用户。它可能是信息已保存的确认信息,或者是说明为什么保存失败的错误信息。
接下来处理POST请求的文件是SavaCustomer.php。与前一个例子一样,该页面也是由简单的HTML页面加上一些PHP和JavaScript代码组成的。其中PHP代码用来从请求中收集信息,然后将其保存到数据库中。由于这是一个POST请求,因此$_POST数组中包含了提交的所有信息:
<?php
$sName = $_POST["txtName"];
$sAddress = $_POST["txtAddress"];
$sCity = $_POST["txtCity"];
$sState = $_POST["txtState"];
$sZipCode = $_POST["txtZipCode"];
$sPhone = $_POST["txtPhone"];
$sEmail = $_POST["txtEmail"];
$sStatus = "";
$sDBServer = "your.database.server";
$sDBName = "your_db_name";
$sDBUsername = "your_db_username";
$sDBPassword = "your_db_password";
$sSQL = "Insert into Customers(Name,Address,City,State,Zip,Phone,`E-mail`) ".
" values ('$sName','$sAddress','$sCity','$sState', '$sZipCode'".
", '$sPhone', '$sEmail')";
//更多代码
?>
这个代码片段将获取与客户相关的所有POST信息;此外,还定义了一个状态消息($sStatus)以及所需的数据库信息(与上一个例子相同)。这里的SQL语句是一个INSERT语句,它将获取的信息添加到数据库中。
执行这个SQL语句的代码与上一个例子十分类似:
<?php
$sName = $_POST["txtName"];
$sAddress = $_POST["txtAddress"];
$sCity = $_POST["txtCity"];
$sState = $_POST["txtState"];
$sZipCode = $_POST["txtZipCode"];
$sPhone = $_POST["txtPhone"];
$sEmail = $_POST["txtEmail"];
$sStatus = "";
$sDBServer = "your.database.server";
$sDBName = "your_db_name";
$sDBUsername = "your_db_username";
$sDBPassword = "your_db_password";
$sSQL = "Insert into Customers(Name,Address,City,State,Zip,Phone,`E-mail`) ".
" values ('$sName','$sAddress','$sCity','$sState', '$sZipCode'".
", '$sPhone', '$sEmail')";
$oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
@mysql_select_db($sDBName) or $sStatus = "Unable to open database";
if($oResult = mysql_query($sSQL)) {
$sStatus = "Added customer; customer ID is ".mysql_insert_id();
} else {
$sStatus = "An error occurred while inserting; customer not saved.";
}
mysql_close($oLink);
?>
在此,mysql_query()函数的结果只是一个表示语句执行成功的指示器。如果执行成功, $sStatus变量中将填入一个消息,表明保存已经成功,并返回为该数据指定的客户ID。mysql_ insert_id()函数始终返回在最新的INSERT语句返回值的基础上自动递增的值。如果因为某些原因,该语句没有成功执行,$sStatus变量将填入一个错误消息。
$sStatus变量将输出到一个在载入窗口时运行的JavaScript函数中:
<script type="text/javascript">
window.onload = function () {
top.frames["displayFrame"].saveResult("<?php echo $sStatus ?>");
}
</script>
这段代码调用了savaResult()函数,该函数定义于显示帧中,传入的参数值是PHP变量$sStatus。由于该变量包含一个字符串,因此必须将PHP的echo语句放在引号中。当执行该函数时,假设客户数据已保存,则输入表单页面看起来如图2-3所示。
![](http://dev.yesky.com/imagelist/06/30/4faaduyn64e7.jpg)
图 2-3
当执行这段代码之后,你还可以自由地使用同样的表单向数据库中添加更多客户,因为它不再消失。
4. 隐藏iFrame
新一代的客户端—服务器通信模式幕后所采用的是iframe,它是在HTML 4.0中引入的。iframe与帧基本是相同的,唯一的区别是iframe可以放在一个未设置帧集的HTML页面中,可以使页面中的任意部分成为一个帧。iframe技术可以在未预先设置帧集的页面中使用,能够更好地适应于功能的逐渐添加。iframe甚至还可以使用JavaScript在运行时创建,为了简单起见,语义化HTML(semantic HTML)支持使浏览器将Ajax功能看作是一个有益的增强(这将在稍后讨论)。由于可以用与普通帧相同的方法使用和访问iframe,因此它们都是Ajax通信的理想选择。
发挥iframe的优势有两种方法。最简单的方法是在页面中简单地嵌入iframe,并像隐藏帧那样用来发出请求。为此,第一个例子中的显示页面将修改为:
<p>Enter customer ID number to retrieve information:</p>
<p>Customer ID: <input type="text" id="txtCustomerId" value="" /></p>
<p><input type="button" value="Get Customer Info"
onclick="requestCustomerInfo()" /></p>
<div id="divCustomerInfo"></div>
<iframe src="about:blank" name="hiddenFrame" width="0" height="0"
frameborder="0"></iframe>
注意,这个iframe中的width、height和frameborder属性都设置成了0,这可将其从视线中隐去。由于iframe的名字仍是hiddenFrame,所以这个页面的JavaScript代码可以如前一样正常工作。不过,对于GetCustomerData.php页面还需要做一些小的修改。在该页面中的JavaScript函数先前是在名为displayFrame的帧中查找displayCustomerInfo()函数。如果你使用该技术,又不存在该名字的帧,则必须修改代码,用parent来代替它:
window.onload = function () {
var divInfoToReturn = document.getElementById("divInfoToReturn");
parent.displayCustomerInfo(divInfoToReturn.innerHTML);
};
现在这个例子能够和本文中的第一例子一样正常工作了。
第二种使用隐藏iframe的方法是通过JavaScript动态地创建它们。由于并非所有浏览器实现iframe的方法都是一样的,所以需要一些技巧,使得它有助于一步步地创建隐藏的iframe。
第一步很简单,使用document.createElement()方法创建iframe并赋予必要的属性:
function createIFrame() {
var oIFrameElement = document.createElement("iframe");
oIFrameElement.width=0;
oIFrameElement.height=0;
oIFrameElement.frameBorder=0;
oIFrameElement.name = "hiddenFrame";
oIFrameElement.id = "hiddenFrame";
document.body.appendChild(oIFrameElement);
//更多代码
}
本段代码的最后一行很重要,因为它将iframe添加到document结构中;没有添加到document中的iframe是无法执行请求的。另外注意,该iframe的name和id属性都是设置为hiddenFrame。这是必要的,因为有些浏览器是通过name属性访问新的帧,而有些则是通过id属性新的帧。
紧接着定义一个全局变量,用来保存对该帧对象的引用。注意,针对iframe元素的这个帧对象并非是从createElement()函数返回的。要获得该对象,必须从帧集合中获取。以下就是即将保存在全局变量中的内容:
var oIFrame = null;
function createIFrame() {
var oIFrameElement = document.createElement("iframe");
oIFrameElement.width=0;
oIFrameElement.height=0;
oIFrameElement.frameBorder=0;
oIFrameElement.name = "hiddenFrame";
oIFrameElement.id = "hiddenFrame";
document.body.appendChild(oIFrameElement);
oIFrame = frames["hiddenFrame"];
}
如果你将这些代码放到前面的iframe例子中,那么需要对requestCustomerInfo()函数进行如下修改:
function requestCustomerInfo() {
if (!oIFrame) {
createIFrame();
setTimeout(requestCustomerInfo, 10);
return;
}
var sId = document.getElementById("txtCustomerId").value;
oIFrame.location = "GetCustomerData.php?id=" + sId;
}
基于这些修改,该函数将会检查oIFrame是否为空。如果为空,则调用createFrame(),并会为该函数的调用设置10ms的超时时间。这是很必要的,因为只有IE浏览器能够立即识别插入的iframe,大部分其他浏览器需要花几毫秒来识别它,以允许通过它发送请求。当再次执行该函数时,将执行代码的其余部分,其中最后一行已经修改为对OIFrame对象的引用。
尽管该技术能够很容易地应用于GET请求,但POST请求却完全不同。只有一部分浏览器允许你设置表单的target属性来动态创建iframe;但IE并不是其中的一种。因此,要使用隐藏iframe技术来发送POST请求还需要一些技巧。
5. 隐藏iframe的POST请求
要使用隐藏iframe来完成POST请求,其方法是在隐藏帧中载入一个包含表单的页面,用数据填充该表单,然后再提交该表单。当这个可见的表单(你实际输入数据的那个)提交时,必须取消这次提交而将信息转发给隐藏帧。为此,必须定义一个函数,用来处理iframe的创建以及隐藏表单的载入:
function checkIFrame() {
if (!oIFrame) {
createIFrame();
}
setTimeout(function () {
oIFrame.location = "ProxyForm.htm";
}, 10);
}
这个名为checkIFrame()的函数首先检查隐藏的iframe是否已经创建。如果没有,则调用createIFrame()。然后,在将iframe的地址设置为ProxyForm.htm(这是一个隐藏表单页面)之前,为其设置一个超时值。由于该函数调用需要花一些时间,而重要的是该页面每次加载时都将提交该表单。
ProxyForm.htm文件很简单,只包括很少的JavaScript,用来提示主页面已经装载完成:
<html>
<head>
<title>Proxy Form</title>
<script type="text/javascript">
window.onload = function () {
parent.formReady();
}
</script>
</head>
<body>
<form method="post"></form>
</body>
</html>
正如你所见,该页面的主体只包含一个空的表单,而标题中只包含一个onload事件处理函数。当载入该页面时,页面将通过调用parent.formReady()来使主页面知道它已经做好接收请求的准备。而formReady()函数则是包含在主页面本身中的,类似于:
function formReady() {
var oHiddenForm = oIFrame.document.forms[0];
var oForm = document.forms[0];
for (var i=0 ; i < oForm.elements.length; i++) {
var oHidden = oIFrame.document.createElement("input");
oHidden.type = "hidden";
oHidden.name = oForm.elements[i].name;
oHidden.value = oForm.elements[i].value;
oHiddenForm.appendChild(oHidden);
}
oHiddenForm.action = oForm.action;
oHiddenForm.submit();
};
在该函数中的第一步是获取对隐藏iframe中表单的引用,可以通过访问该帧的document.forms集合来获取。由于在该页面中只有一个表单,因此可以安全地从该集合中获得第一个表单(即索引值为0),并将其存储于oHiddenForm中。然后,将对主页面表单的引用存于oForm中。紧接着,一个for循环对主页面中该表单的各元素进行遍历(使用elements集合)。对于表单中的每一个元素,都将在隐藏帧(注意,必须使用oIFrame.document.createElement()而不只是document.createElement())中创建一个隐藏的输入元素。这个隐藏的输入元素拥有与该表单元素相同的名字和值,然后使用appendChild()函数将其添加到隐藏的表单中。
当所有的表单元素都添加完后,隐藏的表单还将设置与主页面表单相同的action。通过从表单中读取action来取代硬编码,就可以在任何页面中使用formReady()。该函数的最后一步是提交这个隐藏的表单。
剩下的最后一件事就是确保主页面的表单不以通常的方式提交自己。要达到这一目标,只需在onsubmit事件处理函数中调用checkIFrame()并返回false:
<form method="post" action="SaveCustomer.php"
onsubmit="checkIFrame();return false">
<p>Enter customer information to be saved:</p>
<p>Customer Name: <input type="text" name="txtName" value="" /><br />
Address: <input type="text" name="txtAddress" value="" /><br />
City: <input type="text" name="txtCity" value="" /><br />
State: <input type="text" name="txtState" value="" /><br />
Zip Code: <input type="text" name="txtZipCode" value="" /><br />
Phone: <input type="text" name="txtPhone" value="" /><br />
E-mail: <input type="text" name="txtEmail" value="" /></p>
<p><input type="submit" value="Save Customer Info" /></p>
</form>
<div id="divStatus"></div>
通过以这种方式返回flase,可以阻止表单的默认行为(将自己提交到服务器)。通过调用checkIFrame()方法来启动隐藏iframe中表单的提交进程。
当这一任务完成后,就可以像使用隐藏帧POST请求的例子一样使用本例;页面SavaCustomer.php负责处理数据,并当完成时调用主页面中的savaResult()函数。
注意,本节中的例子是为了使其聚焦于与Ajax技术相关的问题上,因而进行了简化。如果在实际的Web应用程序中使用,还需要提供更多的用户反馈,诸如在发出请求时屏蔽该表单的输入等。
6. 隐藏帧技术的优点和缺点
现在,你已经对使用隐藏帧所实现的强大功能有所了解了,我们将讨论它的实用性。正如前面所说的,该技术已经存在多年,并且仍然在许多Ajax应用中使用。
使用隐藏帧的一个最大理由之一是它可以维护浏览器的历史,使用户仍然能够使用浏览器上的后退和前进按钮。浏览器由于并不知道隐藏帧实际上被隐藏了,但对于其所发出的请求仍然是记录在案的。然而,Ajax应用程序的主页面却没有修改,在隐藏帧中的修改意味着后退和前进按钮将依据该隐藏帧的访问历史而非主页面而变化。这也是为什么Gmail和Google Maps仍然使用该技术的理由。
注意,iframe并非一直会存储浏览器的历史记录。尽管IE始终会存储iframe的历史记录,但Firefox只对使用HTML定义(也就是不包括使用JavaScript动态创建)的iframe保存历史记录。Safari从不为iframe保存历史记录,不管它们是如何包含在该页面中的。
隐藏帧技术不利的一面是,对其背后发生的事了解甚少。它完全依赖于返回的正确页面。本节的例子都存在相同的问题:如果隐藏帧的页面载入失败,并不会向用户提示出错消息;主页面将继续等待直到调用适当的JavaScript函数。必须通过设置一个较长周期(可能是5分钟)的超时时间,然后如果页面仍然没有成功载入则显示一条消息,以给用户一个安慰。但这一切都只是一个变通方法,最主要的问题是,对于后台发生的HTTP请求缺乏充足的信息。幸运的,我们还有其他选择。