3.1.4 表单增量验证实例
如上所述,提交节流模式可以通过不同的用户交互来实现。当使用表单时,增量上传用户输入的数据有时是很用的。最常见的使用场景是对用户在表单上填写的数据进行实时验证,而非等到最后再来检查所有的错误。在这种情况下,你最有可能使用表单中每个元素的onchange事件处理函数来决定什么时候来上传其数据。
当<select/>元素选择了另一个选项,其他控件的值改变并失去焦点时,都会启动change事件。例如,如果你在一个文本框中输入一些字符,然后点击屏幕的其他地方(使得文本框失去了焦点),那么就将启动change事件,同时将调用onchange事件处理函数。如果你再次点击这个文本框,然后再点其他地方(或按下Tab键),那么该文本框会失去焦点但不会启动change事件,因为其内容没有变化。对提交节流模式而言,使用这个事件处理函数可以避免产生无关的请求。
通常,表单的验证要优先于提交。一开始表单的提交按钮是禁用的(disable),只有填入表单的所有字段已经通过服务器验证,才将其启用(enable)。例如,假设你访问的网站中有一个需要注册才能够使用的功能时。这可能是一个购物网站,需要注册后才能购买商品,或者是一个只允许注册用户访问消息公告(message board)的网站。当创建一个新账号时,可能要求提供以下信息:
◎ 不重复的用户名;
◎ 有效的电子邮件地址;
◎ 填写的生日必须是有效的日期。
当然,不同的使用场景需要的数据类型是不同的,但这些内容为大部分应用程序提供了一个良好的开始。
创建这种交互的第一步是定义一个用来收集这些信息的HTML表单。该表单可以独立使用,即使在不支持Ajax调用的情况下也一样可以使用:
<form method="post" action="Success.php">
<table>
<tr>
<td><label for="txtFirstName">First Name</label></td>
<td><input type="text" id="txtFirstName" name="txtFirstName" /></td>
</tr>
<tr>
<td><label for="txtLastName">Last Name</label></td>
<td><input type="text" id="txtLastName" name="txtLastName" /></td>
</tr>
<tr>
<td><label for="txtEmail">E-mail</label></td>
<td><input type="text" id="txtEmail" name="txtEmail" /><img
src="error.gif" alt="Error" id="imgEmailError" style="display:none" /></td>
</tr>
<tr>
<td><label for="txtUsername">Username</label></td>
<td><input type="text" id="txtUsername" name="txtUsername" /><img
src="error.gif" alt="Error" id="imgUsernameError" style="display:none" /></td>
</tr>
<tr>
<td><label for="txtBirthday">Birthday</label></td>
<td><input type="text" id="txtBirthday" name="txtBirthday" /><img
src="error.gif" alt="Error" id="imgBirthdayError" style="display:none" />
(m/d/yyyy)</td>
</tr>
<tr>
<td><label for="selGender">Gender</label></td>
<td><select id="selGender"
name="selGender"><option>Male</option></option>Female</option></select></td>
</tr>
</table>
<input type="submit" id="btnSignUp" value="Sign Up!" />
</form>
在这个表单中需要注意一些事件。首先,并非所有的字段都需要使用Ajax调用进行验证。例如名字、姓和性别字段(以组合框形式表示)都不需要验证,而其他字段,包括电子邮件、用户名和生日字段都需要使用Ajax来进行验证。其次,你会发生所有的这些字段对应的文本框后面都包含一个隐藏的图像,该图像仅在验证失败时使用。该图像在开始时是隐藏的,如果浏览器不支持Ajax功能则永远都看不到它们。在该表单中完全没有JavaScript,所有的函数和事件处理函数都定义在独立的文件中。
我们将通过一个名为validateField()的函数来验证每个字段。由于每个字段都使用相同的验证技术(向服务器发出调用并等待响应),因此是可行的。唯一的区别是什么样类型的数据需要验证,以及在验证失效时显示哪一个图像。
服务器端程序存放在一个名为ValidateForm.php的文件中。该文件预设为通过查询字符串来获取名字—值数据对,其中名字是要验证其值的控件名字,而值则是该控件的值。根据控件的名称,该页面将对值进行相应的验证。然后,以下述格式返回一个简单的字符串:
<true|false>||<error message>
该字符串的第一部分用来表示该值是否有效(true表示有效,false表示无效)。在两个管道符(| |)之后的第二部分则是一个错误消息,只当值无效时才提供。以下是一些可能返回的字符串的实例:
true||
false||Invalid date.
第一行表示这是一个有效值;第二行则表示这是一个无效值。
这里使用的是纯文本格式的消息,而在本书的后面部分将会讲述使用其他数据格式来实现该功能的方法,诸如XML和JSON。
用来实现验证功能的代码如下所示:
<?php
$valid = "false";
$message = "An unknown error occurred.";
if (isset($_GET["txtUsername"])) {
//载入用户名数组
$usernames = array();
$usernames[] = "SuperBlue";
$usernames[] = "Ninja123";
$usernames[] = "Daisy1724";
$usernames[] = "NatPack";
//检查用户名
if (in_array($_GET["txtUsername"], $usernames)) {
$message = "This username already exists. Please choose another.";
} else if (strlen($_GET["txtUsername"]) < 8) {
$message = "Username must be at least 8 characters long.";
} else {
$valid = "true";
$message = "";
}
} else if (isset($_GET["txtBirthday"])) {
$date = strtotime($_GET["txtBirthday"]);
if ($date < 0) {
$message = "This is not a valid date.";
} else {
$valid = "true";
$message = "";
}
} else if (isset($_GET["txtEmail"])) {
if(!eregi(
"^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$",
$_GET["txtEmail"])) {
$message = "This e-mail address is not valid";
} else {
$valid = "true";
$message = "";
}
}
echo "$valid||$message"; ?>
在这个文件中,第一步是确定哪些字段要验证。这可过使用isset()函数检查$_GET数组是否有值来完成。如果特定字段有值,那么就开始验证。对于用户名而言,则先检查其值是否已经存储于用户名数组中,然后再检查它是否满足至少8个字符的要求。生日则直接传给PHP内建的strtotime()函数,它将把任何U.S.格式的日期字符串转成UNIX时戳(即从1970年1月1日到该日期的总秒数)。如果存在错误,则该函数将返回-1,说明传入的字符串不一个有效的日期。电子邮件地址则通过正则表达式来检查,确保其格式正确,这个正则表达式是由John Coggeshall设计的(参见他的文章“基于PHP4验证电子邮件”,可以在www.zend.com/zend/ spotlight/ ev12apr.php中获得)。
注意在本例中,用户名是存储在一个简单的数组中,并以硬性编码的方式写在页面中。而在实际的实现中,用户名应该存储在一个数据库中,我们可以通过查询数据库来决定该用户名是否存在。
$valid和$message变量分别初始化为false和An unknown error occurred,这样就可以确保如果该文件产生错误(例如传入了一个未认可的字段名),仍然能够返回表示否定的验证结果。但是如果验证通过,则需要将这两个变量设置为相应的值(即将$valid赋值为true,$message赋值为空字符串)。而如果验证失效,则只需要设置$message变量的值,因为$valid的值已经是false了。在该页面中的最后一步是以前面提到的格式输出这个字符串。
接下来,必须创建执行该验证的JavaScript。只要知道哪个字段需要验证,就可以只通过一个validateField()函数来验证每个字段。为了消除跨浏览器兼容问题,还需要一些简单的处理:
function validateField(oEvent) {
oEvent = oEvent || window.event;
var txtField = oEvent.target || oEvent.srcElement;
//更多代码
}
在这个函数中的前两行代码用来处理IE和DOM兼容浏览器(诸如Mozilla Firfox、Opera和Safari)之间事件模型的不同。对于DOM兼容(DOM-compliant)浏览器,是向每个事件处理函数传递一个event对象,引发这个事件的控件存储于该event对象的target属性中。而在IE中,event对象是window对象的一个属性,因此,该函数中的第一行则用来为oEvent变量赋予正确的值。当对一个对象和一个null对象进行逻辑或(| |)运算时,将返回一个非空值。如果你使用IE,那么oEvent将为null;因此将把window.event的值赋给oEvent。如果你使用的是DOM兼容的浏览器,则oEvent将把自身的值再赋给自己。第二行代码对引发该事件的控件做相同操作,因为在IE中该控件存放在srcElement属性中。在这两行代码执行后,引发该事件的控件将存储在txtField变量中。下一步就该使用XMLHttp来创建这个HTTP请求了:
function validateField(oEvent) {
oEvent = oEvent || window.event;
var txtField = oEvent.target || oEvent.srcElement;
var oXmlHttp = zXmlHttp.createRequest();
oXmlHttp.open("get", "ValidateForm.php?" + txtField.name + "="
+ encodeURIComponent(txtField.value), true);
oXmlHttp.onreadystatechange = function () {
//更多代码
};
oXmlHttp.send(null);
}
与第2章一样,你可以使用zXml库来获得跨浏览器兼容的XMLHttp支持。XMLHttp对象将被创建并存储在oXmlHttp对象中。接下来,使用open()来启动一个GET请求的连接。注意,ValidateForm.php的查询字符串是由字段名称、等号以及字段的值组合而成的(并且还将使用encodeURIComponent()对URL进行编码)。另外还要注意这是一个异步请求。对于这个应用而言,这是十分重要的,因为你并不想在对某个字段进行验证时对用户输入表单中其他信息的操作造成影响。紧记,使用XMLHttp对象发出同步请求将在其执行过程中冻结用户界面(包括输入和点击操作)。该函数的最后一部分是处理来自服务器的响应:
function validateField(oEvent) {
oEvent = oEvent || window.event;
var txtField = oEvent.target || oEvent.srcElement;
var oXmlHttp = zXmlHttp.createRequest();
oXmlHttp.open("get", "ValidateForm.php?" + txtField.name + "="
+ encodeURIComponent(txtField.value), true);
oXmlHttp.onreadystatechange = function () {
if (oXmlHttp.readyState == 4) {
if (oXmlHttp.status == 200) {
var arrInfo = oXmlHttp.responseText.split("||");
var imgError = document.getElementById("img"
+ txtField.id.substring(3) + "Error");
var btnSignUp = document.getElementById("btnSignUp");
if (!eval(arrInfo[0])) {
imgError.title = arrInfo[1];
imgError.style.display = "";
txtField.valid = false;
} else {
imgError .style.display = "none";
txtField.valid = true;
}
btnSignUp.disabled = !isFormValid();
} else {
alert("An error occurred while trying to contact the server.");
}
}
};
oXmlHttp.send(null);
}
当确认readyState和status的值都是正确的后,则用JavaScript的split()方法将responseText的值分解到一个字符串数组(arrInfo)中。arrInfo的第一组值将是PHP变量$valid的值;第二组值则是PHP变量$message的值。同时也引用相应的错误图像并且返回“Sign Up”(注册)按钮。错误图像可以通过分析字段名获得,移除最前面的“txt”(使用substring()),在前面加上“img”,后面加上“Error”(因此对于字段“txtBirthday”而言,其错误图像名将构造为“imgBirthdayError”)。
arrInfo[0]的值必须传给eval()函数,以获得真正的布尔值。(紧记,这时它只是一个字符串:true或false。)如果该值是false,那么错误图像的title属性将赋值为arrInfo[1]中存放的错误消息,并且将显示出错误图像,而为文本框定制的valid属性将设置为false(这在稍后将用到)。如果一个值是无效的,那么将显示错误图像,当用户把鼠标移到图像上时,将显示出错误消息(参见图3-3)。但如果该值是有效的,则图像仍然是隐藏的,而定制的valid属性也将设置为true。
图 3-3
你也会发现在该函数中使用了“Sign Up”按钮。如果该表单中仍然存在无效数据,那么“Sign Up”按钮将会是禁用的。要实现这一功能,需要调用一个名为isFormValid()函数。如果该函数返回false,那么将把“Sign Up”按钮的disable属性设置为true,从而禁用它。isFormValid()函数只是简单遍历表单中的每个字段,检查其valid属性。
function isFormValid() {
var frmMain = document.forms[0];
var blnValid = true;
for (var i=0; i < frmMain.elements.length; i++) {
if (typeof frmMain.elements[i].valid == "boolean") {
blnValid = blnValid && frmMain.elements[i].valid;
}
}
return blnValid;
}
对于表单中的每个元素,首先将检查valid属性是否存在,这可以使用typeof操作符来实现。如果该属性存在将会返回一个布尔值。由于有些字段是不需要验证的(因此也不需设置定制的valid属性),因此只检查认为需要验证的字段。
该脚本的最后一部分是为文本框设置事件处理函数。这将在表单载入完成后,并且只有当具备XMLHttp支持时(由于这里执行的是基于Ajax的验证)才进行:
//如果Ajax可用,则禁用提交按钮,并指定事件处理函数
window.onload = function () {
if (zXmlHttp.isSupported()) {
var btnSignUp = document.getElementById("btnSignUp");
var txtUsername = document.getElementById("txtUsername");
var txtBirthday = document.getElementById("txtBirthday");
var txtEmail = document.getElementById("txtEmail");
btnSignUp.disabled = true;
txtUsername.onchange = validateField;
txtBirthday.onchange = validateField;
txtEmail.onchange = validateField;
txtUsername.valid = false;
txtBirthday.valid = false;
txtEmail.valid = false;
}
};
这个onload事件处理函数将为每个文本框指派一个onchange事件处理函数,同时将定制的valid属性初始化为false。另外,将“Sign Up”按钮禁用可以避免提交无效的数据。但要注意,只有当具有XMLHttp支持时才能禁用该按钮;否则它将成为一个普通的Web表单,而当整个表单要提交时都无法完成验证。
当你调入该例子页面时,在三个要验证的文本字段中修改了值并移到其他字段时,将会向服务器发出一个验证请求。使用提交节流模式的用户体验是很流畅,而且即使禁用了JavaScript或不支持XMLHttp,该表单的功能仍然是可用的。
即使使用了这种类型的验证,但实际上在整个表单提交时还需要对所有数据再进行一次验证。紧记,如果用户禁用了JavaScript,在对这些数据操作前还要确保它们是有效的。







