TA的每日心情 | 开心 2021-3-12 23:18 |
---|
签到天数: 2 天 [LV.1]初来乍到
|
[blockquote]
在这个包含两部分的系列文章中,作家兼 java 开发人员 Andrei Cioroianu 将教您如何使用 Ajax(Asynchronous JavaScript + XML)技术和 JSF(JavaSErver Faces)技术自动保存 Java Web 应用程序中的表单数据。您将学会如何用 Ajax 提交 Web 表单、如何使用 JSF 框架处理 Ajax 请求、如何控制 JSF 请求处理生命周期、如何在服务器端管理表单数据,以及如何识别浏览器会话间的匿名用户。此外,本系列还将探究几个经常发生的开发错误,包括错误的表单数据编码和可能导致请求失败及内存泄漏的错误的 Ajax 请求管理。
[/blockquote]
简介
很多桌面应用程序允许用户随时保存文件,还有一些产品会自动保存正在编辑的文件以最小化因程序崩溃而造成的数据损失。当用户与 Web 应用程序进行交互时,通常只有当表单提交给服务器时,用户的数据才会被保存。
大多数 Web 应用程序不允许用户保存只部分填充的表单,关闭浏览器,然后再继续此任务。此外,如果由于网络问题突然中断了连接,用户的数据将得不到保存,他们的某些工作成果也有可能会因此丢失。 Ajax 是解决这类问题的理想解决方案。当使用 Ajax 提交表单数据时,页面不必刷新,滚动条的位置也可以保持不变,就好像所处理的是一个桌面应用程序一样。若用户需要填写复杂表单或者面临数据丢失的风险,他们将会非常青睐这种自动保存功能。例如,假设用户在测试一个产品,当他们填写支持表单以便报告测试问题时,这个 Web 支持表单就应该能定期地自动保存。在测试产品时,用户的系统可能会变得不稳定,在提交完整的表单前,他们可能需要多次重启计算机。表单自动保存功能可以节省时间并且能够防止这种情况下的数据丢失。
作为包含两部分的系列文章中的第 1 部分,本期的重点将放在如何用 Ajax 发送表单数据以及如何用 JSF 处理 Ajax 请求。它将向您展示实现数据自动保存的完整数据流程,内容涵盖如何使用 JavaScript 在 Web 浏览器中获取、编码和提交表单数据。本篇文章还会涉及到 JSF 侦听程序如何在服务器端处理已提交的数据,定制 JSF 请求处理生命周期以使它能有效地处理 Ajax 请求。无论是否需要表单自动保存功能或其他类似功能,您都可以将本篇文章介绍的技术应用到基于 Ajax 和 JSF 的任何 Java Web 应用程序中。
当用户与 Web 应用程序进行交互时,通常只有当表单提交给服务器后,用户的数据才会被保存。大多数 Web 应用程序不允许用户保存只部分填充的表单、关闭浏览器,然后再继续此任务。此外,如果由于网络问题突然中断了连接,用户的数据将得不到保存,他们的某些工作成果也有可能会因此丢失。Ajax 是解决这类问题的理想解决方案。
在客户端获取表单数据 本节将给出一个 JSF 表单,其数据通过 JavaScript 和 DOM 在 Web 浏览器中获得。 您可以在自已的 Web 表单应用程序中重用这里介绍的 JavaScript 代码。本节还将解释如何正确地编码表单数据以将它提交给服务器。 构建 JSF 表单 让我们先来看一个典型的 JSF 例子。这个页面包括一些基本 HTML 的元素,比如输入框、列表、单选按钮、复选框和提交按钮。所有输入组件都将其值绑定到称为的 JavaBean 属性。这个页面的头部包括一个标记,用来导入文件。此 JavaScript 文件包括一个函数,名为,它在标记的属性内调用,以便在 Web 浏览器加载页面后激活表单的自动保存功能。 清单 1 显示了页的部分源代码。
清单 1. 包含示例 JSF 表单的 SupportForm.jsp 页
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %>
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>
<html>
<head>
<title>Support Form</title>
<script src="AutoSaveScript.js">
</script>
</head>
<body onload="setAutoSaving(10000)">
<f:view>
<h1>Support Form</h1>
<h:form id="supportForm">
<p><h:outputText value="Name: "/>
<h:message for="name"/><br>
<h:inputText id="name" value="#{supportBean.name}"
size="40" required="true">
</h:inputText>
...
<p><h:outputText value="Platform: "/>
<h:message for="platform"/><br>
<h:selectOneRadio id="platform" value="#{supportBean.platform}"
layout="lineDirection" required="true">
<f:selectItem itemValue="Windows" itemLabel="Windows"/>
<f:selectItem itemValue="Linux" itemLabel="Linux"/>
<f:selectItem itemValue="Mac" itemLabel="Mac"/>
</h:selectOneRadio>
...
<p><h:outputText value="Problem: "/>
<h:message for="problem"/><br>
<h:inputTextarea id="problem" value="#{supportBean.problem}"
rows="10" cols="40" required="true"/>
<p><h:commandButton id="submit" value="Submit"
action="#{supportBean.submit}"/>
</h:form>
</f:view>
</body>
</html>
[/code]
当用户为了打开 JSF 页面而单击一个 JSF 链接或输入一个 URL 时,Web 浏览器会构建 HTTP 请求并把此请求发送至 Web 服务器,服务器识别包含页面的应用程序并会调用(在内配置)来处理此请求。在进行了某些上下文初始化后,就会执行此页面,而且,JSF 框架还会创建组件树以镜像该 Web 页面所用的 JSF 标记。这些组件的呈现程序生成含有表单元素的 HTML 代码(见清单 2)。
清单 2. SupportForm.jsp 页面生成的 HTML 代码
<html>
<head>
<title>Support Form</title>
<script src="AutoSaveScript.js">
</script>
</head>
<body onload="setAutoSaving(10000)">
<h1>Support Form</h1>
<form id="supportForm" method="post"
action="/autosave/faces/SupportForm.jsp"
enctype="application/x-www-form-urlencoded">
<p>Name: <br>
<input id="supportForm:name" type="text"
name="supportForm:name" size="40" />
...
<p>Platform: <br>
<table id="supportForm:platform">
<tr>
<td><label><input type="radio" name="supportForm:platform"
value="Windows"> Windows</input></label></td>
<td><label><input type="radio" name="supportForm:platform"
value="Linux"> Linux</input></label></td>
<td><label><input type="radio" name="supportForm:platform"
value="Mac"> Mac</input></label></td>
</tr>
</table>
...
<p>Problem: <br>
<textarea id="supportForm:problem" name="supportForm:problem"
cols="40" rows="10">
</textarea>
<p><input id="supportForm:submit" type="submit"
name="supportForm:submit" value="Submit" />
<input type="hidden" name="com.sun.faces.VIEW"
value="H4sIAA..." />
<input type="hidden" name="supportForm" value="supportForm" />
</form>
</body>
</html>
[/code]
在这个 HTML 表单的结尾,有一些隐藏元素。如果- javax.faces.STATE_SAVING_METHOD
复制代码 参数在文件中被设为,那么 JSF 实现会内部使用这些隐藏元素以识别所提交的表单和存储组件树在请求间的状态。从浏览器的角度看,一个 JSF 表单与其他 HTML 表单无异,可以使用 JavaScript 和 DOM 在 Web 浏览器中访问表单元素。 获取及编码表单数据文件包含一个称为的 JavaScript 函数,它获取一个对象并对其元素进行迭代以构建包含名称-值对的字符串。这个字符串遵循标准的- application/x-www-form-urlencoded
复制代码 格式,用分隔参数,并在每个参数的名称和值之间使用。内部函数可以对单一参数进行编码,所使用的是 JavaScript API 提供的函数。函数用后跟被编码字符的两位 16 进制码代替了几乎所有非字母数字的 ASCII 字符。 我之所以说 “几乎所有非字母数字的 ASCII 字符”,是因为除此之外,还有字母数字字符以及无编码的其他一些字符(例如)。 例如,如果将字符串传递给,结果将是(是空格符的 16 进制码)。这是一个符合 RFC 1738 的有效 URL 编码,但如果将这个已编码的字符串提交给服务器端脚本,例如 JSP,结果将是一个空格而不是字符。这也是正确的,因为描述- application/x-www-form-urlencoded
复制代码 格式的 RFC 1866 中规定空格符的编码为,并且所有非字母数字字符都以一个后跟 16 进制码替代。当服务器对这个字符串进行解码时,任何字符都会被还原成一个空格。 总之,由执行的 URL 编码并不完全与- application/x-www-form-urlencoded
复制代码 一样,因为将字符保留,不做编码,而在- application/x-www-form-urlencoded
复制代码 中,空格则被编码为字符。解决这个问题最简单的方法是将所有字符都编码为(是的 16 进制代码)。也可以使用对的返回结果执行此操作。在本例中,可以将字符串编码为。清单 3 所示的是内部函数,它将一个编码后的名称-值对添加到的本地变量。
清单 3. 对单一请求参数进行编码
function getFormData(form) {
var dataString = "";
function addParam(name, value) {
dataString += (dataString.length > 0 ? "&" : "")
+ escape(name).replace(/+/g, "%2B") + "="
+ escape(value ? value : "").replace(/+/g, "%2B");
}
...
}
[/code]
函数获取对象的数组,并根据每个元素的类型调用。单个名称-值会针对每个文本框、密码和隐藏字段添加。只有当对应的表单元素被选中时,复选框和单选按钮的值才会被编码。如果是列表,单个名称-值对会针对每个选中项添加。之后,会返回包含表单编码数据的字符串(见清单 4)。
清单 4. 获取和编码表单数据
function getFormData(form) {
...
var elemArray = form.elements;
for (var i = 0; i < elemArray.length; i++) {
var element = elemArray;
var elemType = element.type.toUpperCase();
var elemName = element.name;
if (elemName) {
if (elemType == "TEXT"
|| elemType == "TEXTAREA"
|| elemType == "PASSWORD"
|| elemType == "HIDDEN")
addParam(elemName, element.value);
else if (elemType == "CHECKBOX" && element.checked)
addParam(elemName,
element.value ? element.value : "On");
else if (elemType == "RADIO" && element.checked)
addParam(elemName, element.value);
else if (elemType.indexOf("SELECT") != -1)
for (var j = 0; j < element.options.length; j++) {
var option = element.options[j];
if (option.selected)
addParam(elemName,
option.value ? option.value : option.text);
}
}
}
return dataString;
}
[/code]
用 Ajax 提交表单数据 本节将展示如何使用 Ajax 向 JSF 页面发送表单数据,还会涵盖与之相关的其他一些主题,例如错误处理以及对象的正确管理,这些都是防止 Web 浏览器内的内存泄漏所必须了解的。 创建和发送 Ajax 请求函数的代码可以在文件中找到(请参见 下载 部分),该函数使用 Ajax 请求对象向 Web 服务器提交编码后的数据。首先,它需要创建这个请求对象,如果是 Microsoft? Internet Explorer,就使用,如果是支持 Ajax 的其他浏览器,比如 Firefox、Netscape、Mozilla、Opera 和 Safari,就使用。清单 5 显示了创建对象所需的代码。
清单 5. 创建 Ajax 请求对象
function submitFormData(form) {
var xhr;
if (window.ActiveXObject)
xhr = new ActiveXObject("Microsoft.XMLHTTP");
else if (window.XMLHttpRequest)
xhr = new XMLHttpRequest();
else
return null;
...
}
[/code]
编码后的表单数据被提交给由表单的URL 所识别的页面,使用的是特定的 HTTP,在 JSF 表单的情况下,此方法即为。函数也可以同非 JSF 表单一起使用,这类表单有可能会使用默认的方法或。即使这个表单没有指定 action URL,此代码仍可以工作。在这种情况下,将会使用由获得的当前页面的 URL。编码后的表单数据可通过函数从对象检索到,该函数在前面已介绍过。如果此 HTTP 方法是,那么编码后的数据会追加到 URL字符后面。之后,通过方法初始化对象(见清单 6)。
清单 6. 初始化 Ajax 请求对象
function submitFormData(form) {
...
var method = form.method ? form.method.toUpperCase() : "GET";
var action = form.action ? form.action : document.URL;
var data = getFormData(form);
var url = action;
if (data && method == "GET")
url += "?" + data;
xhr.open(method, url, true);
...
}
[/code]
当从服务器收到对 Ajax 请求的响应时,会调用称为的内部函数。如果的值为,这个 Ajax 回调就会发出错误信号,请求虽完成(为),但其状态却不对(不是)。一旦发生错误,系统就会通过报告对象的和属性,而且标记也会设为以防您一次又一次地收到错误消息(因为表单保存是周期性执行的)。如果重新加载此页面,JavaScript 代码就会重新初始化,若导致 HTTP 错误的问题仍没有得到解决,您将会再次看到错误消息。这个功能非常适合开发阶段的调试之用。在实际的生产环境中,当发生错误时,与显示告警信息相比,最好是将用户重新引导到其他页面。不管何种情况,对象的属性都必须包含一个对的引用以便此回调函数在 Ajax 请求的生命周期中能被调用(见清单 7)。
清单 7. 回调函数
var autoSaveDebug = true;
function submitFormData(form) {
...
function submitCallback() {
if (autoSaveDebug && xhr.readyState == 4
&& xhr.status != 200) {
autoSaveDebug = false;
alert("Auto-Save Error: "
+ xhr.status + " " + xhr.statusText);
}
}
xhr.onreadystatechange = submitCallback;
...
}
[/code]
接下来,设定报头,它对示例应用程序是特定的,用来在服务器端识别 Ajax 请求,这将在本文后面的部分进行介绍。如果 HTTP 方法是,那么函数会设置标准报头并且会使用对象的方法将表单数据提交给 JSF 页面。如果 HTTP 方法是(当前未被 JSF 表单使用),那么表单数据应该已经追加到此 URL,并会用参数调用。在提交表单数据后,此函数返回一个对对象的引用(见清单 8)。
清单 8. 发送 Ajax 请求
function submitFormData(form) {
...
xhr.setRequestHeader("Ajax-Request", "Auto-Save");
if (method == "POST") {
xhr.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");
xhr.send(data);
} else
xhr.send(null);
return xhr;
}
[/code]
管理 XHR 实例 当需要重复发送表单数据时,您很可能会倾向于重用(XHR) 对象,但在多数情况下,这并不是一个好主意,原因很多。首先,它将使代码变得复杂,因为您将不得不进行池的管理,也不得不跟踪 XHR 实例的生命周期。请记住,这些对象的状态通常是在 HTTP 请求完成后存取的,并且在不再需要任何 XHR 时,应用程序代码必须要告知管理该池的代码。此外,一些浏览器在为多重请求而重用 XHR 对象时可能也会存在问题。 为每个 HTTP 请求创建一个新的 XHR 对象也有问题,因为只要应用程序需要这些对象,浏览器就不会从内存中删除它们。如果应用程序不使用 JavaScript操作符释放由 XHR 对象占用的内存,不断发送 Ajax 请求的 Web 页面就可能会导致 Web 浏览器中的内存泄漏。在这种情况下,如果在合理的时间段内没有得到响应,不妨采用一种有效策略:中止请求然后重新发送。服务器和客户机都必须做好获得不规范的 Ajax 请求和响应的准备,有些请求或响应甚至可能会丢失。如果这种方法行不通,可以使用发送同步请求。 应用程序可以使用 Ajax 实现自动保存,但前提是表单应提供传统的提交按钮或使用同步请求发送数据以做处理。异步请求的不可靠性对于自动保存来说还是可以接受的,因为实现自动保存的作用只是在浏览器瘫痪或网络故障时进行部分恢复。 文件包含一个函数,称为,它发送此 Web 页面所有表单的数据。这些 XHR 对象保存在一个数组内,数组为每个表单分配一个数组元素。在用发送表单数据前,函数会中止并删除前面那个自动保存表单曾使用过的请求。在成功完成了的请求上调用不起任何作用,并且仅当浏览器收到一个对旧请求的延时响应的情况下,才被设置为一个空函数。清单 9 显示了对页面表单进行迭代的代码,以将表单数据提交给服务器。
清单 9. 提交当前页面的所有表单
var autoSaveXHR = new Array(document.forms.length);
function submitAllForms() {
var formArray = document.forms;
for (var i = 0; i < formArray.length; i++) {
if (autoSaveXHR) {
var oldXHR = autoSaveXHR;
oldXHR.onreadystatechange = function() { };
oldXHR.abort();
delete oldXHR;
}
autoSaveXHR = submitFormData(formArray);
}
}
[/code]
由返回的 XHR 对象将在下一次调用时删除。文件中的另一个函数是,它能使用 JavaScript API 的函数启用表单的自动保存功能。每隔指定的毫秒数,浏览器都会调用,直到被再次使用以借助清除之前调用的影响(见清单 10)。
清单 10. 启动当前页面的表单自动保存功能
var autoSaveIntervalId = null;
function setAutoSaving(millisec) {
if (autoSaveIntervalId) {
clearInterval(autoSaveIntervalId);
autoSaveIntervalId = null;
}
if (millisec != 0)
autoSaveIntervalId = setInterval(
"submitAllForms()", millisec);
}
[/code]
使用 JSF 侦听程序处理 Ajax 请求 到目前为止,您已经学会了如何用 Ajax 将表单数据提交给 JSF 页面。现在,让我们看一下如何在服务器端处理 Ajax 请求。我们先来简单介绍 JSF 请求处理生命周期,这是理解本文所附的 示例代码 所必需的。JSF 规范包括了对请求处理生命周期的完整描述,在开发自已的 JSF 应用程序时,您会发现该规范非常有用。 理解 JSF 请求处理生命周期 JSF 框架处理一个典型的、发布表单数据的请求要经过六个阶段:
恢复视图
应用请求值
处理验证
更新模型值
调用应用程序
呈现响应
首先,框架需要恢复表单页面的组件树。根据- javax.faces.STATE_SAVING_METHOD
复制代码 配置参数的值,组件树可能会从请求参数中解除序列化,也可能会从对象获取。 然后,JSF 框架递归式地遍历组件树,更新组件状态。例如,一个实现的输入组件的属性将会设置为相应的请求参数。如果组件的属性为,那么 JSF 框架还会转换并验证所提交的值,设置组件的属性。如果属性为,那么这个转换和验证就会在 JSF 请求处理生命周期的下一阶段执行。 前三个阶段(恢复视图、应用请求值和处理验证)完成后,组件树就包含了所提交的表单数据,而且数据也已由 JSF 框架进行了解码、转换和验证。就目前而言,应用程序应保存 JSF 组件的值以便能在日后恢复 Web 表单的数据。 一个能自动保存表单数据的 Ajax 请求处理生命周期必须在验证阶段过后终止。否则,JSF 框架将进入下一阶段:更新绑定到 JSF 组件的 JavaBean 属性的模型值。在用户单击按钮发生常规表单提交的情况下,JSF 框架还将调用与这个命令按钮相关的动作方法。最后一个阶段是呈现 HTML 响应。若表单是自动保存的,请求处理生命周期的这后三个阶段(更新模型值、调用应用程序和呈现响应)是不必要的。 实现 PhaseListener 接口 表单自动保存不应妨碍应用程序的功能。这意味着当利用 Ajax 为自动保存的目的而提交表单时,不应设置任何 JavaBean 属性,也不应调用任何动作方法。此外,在自动保存后也不应生成任何 HTML 响应,因为浏览器不需要任何刷新。因此,应用程序需要控制 JSF 请求处理生命周期,这可以通过实现很轻松地完成。在本文所附的示例应用程序中,JSF 侦听程序名为并在文件中配置(见清单 11)。
清单 11. 配置 JSF 阶段侦听程序
<faces-config>
<lifecycle>
<phase-listener>autosave.AutoSaveListener</phase-listener>
</lifecycle>
...
</faces-config>
[/code]
正如前面所说明的,验证阶段过后,对自动保存请求的处理必须终止以便 JSF 框架不会更新 JavaBean 数据模型,该模型不应受到自动保存的影响。因此,只有在验证阶段以后,侦听程序才开始接收通知,其 ID 通过方法返回。如果请求被标记为报头,那么侦听程序的方法就会调用对象的方法,告知 JSF 框架,它应该停止处理请求。 (见清单 12)。
清单 12. 实现 JSF 阶段侦听程序
package autosave;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import java.util.Iterator;
import java.util.Map;
public class AutoSaveListener implements PhaseListener {
public PhaseId getPhaseId() {
return PhaseId.PROCESS_VALIDATIONS;
}
public void beforePhase(PhaseEvent e) {
}
public void afterPhase(PhaseEvent e) {
System.out.println();
System.out.print(e.getPhaseId());
System.out.print(" - ");
FacesContext ctx = e.getFacesContext();
Map headers = ctx.getExternalContext().getRequestHeaderMap();
if ("Auto-Save".equals(headers.get("Ajax-Request"))) {
System.out.println("Auto-Save");
ctx.responseComplete();
} else
System.out.println("Submit");
printTree(ctx.getViewRoot(), 0);
}
...
}
[/code]
打印组件树 除了在合适的时候停止请求处理外,侦听程序会通过称为的递归方法打印组件树。此方法会输出组件成员、呈现程序类型、惟一 ID 及每个组件验证后的值(见清单 13)。
清单 13. 打印 JSF 组件树
public class AutoSaveListener implements PhaseListener {
...
public void printTree(UIComponent comp, int level) {
if (comp == null)
return;
Object value = null;
if (comp instanceof EditableValueHolder)
value = ((EditableValueHolder) comp).getValue();
for (int i = 0; i < level; i++)
System.out.print(" ");
System.out.print(comp.getFamily());
System.out.print(" - ");
System.out.print(comp.getRendererType());
System.out.print(" - [");
System.out.print(comp.getId());
System.out.print("]");
if (value != null) {
System.out.print(" - ");
if (value instanceof Object[]) {
Object array[] = (Object[]) value;
for (int i = 0; i < array.length; i++) {
System.out.print(array);
System.out.print(" ");
}
} else
System.out.print(value);
}
System.out.println();
Iterator children = comp.getChildren().iterator();
while (children.hasNext()) {
UIComponent child = (UIComponent) children.next();
printTree(child, level + 1);
}
}
}
[/code]
清单 14 显示了所打印的组件树。
清单 14. 打印出的组件树
PROCESS_VALIDATIONS 3 - Auto-Save
javax.faces.ViewRoot - null - [null]
javax.faces.Form - javax.faces.Form - [supportForm]
javax.faces.Output - javax.faces.Text - [_id0]
javax.faces.Message - javax.faces.Message - [_id1]
javax.faces.Input - javax.faces.Text - [name] - John Smith
...
javax.faces.Output - javax.faces.Text - [_id11]
javax.faces.Message - javax.faces.Message - [_id12]
javax.faces.SelectOne - javax.faces.Radio - [platform] -
Windows
javax.faces.SelectItem - null - [_id13]
javax.faces.SelectItem - null - [_id14]
javax.faces.SelectItem - null - [_id15]
...
javax.faces.Output - javax.faces.Text - [_id26]
javax.faces.Message - javax.faces.Message - [_id27]
javax.faces.Input - javax.faces.Textarea - [problem] -
Unable to ...
javax.faces.Command - javax.faces.Button - [submit]
[/code]
通过获取能实现接口的输入组件的值,方法会从组件树实际获得已提交的表单数据。在本系列的第 2 部分,您将获得更多有关处理组件树和控制请求处理生命周期的 JSF 技巧。 结束语 本文向您展示了如何用 Ajax 编码和提交表单数据以及如何实现一个能从 JSF 组件树检索已提交数据的 JSF 侦听程序。请继续关注本系列的第 2 部分,在该部分中,您将了解如何在服务器端管理表单数据以及如何在用户关闭和重新打开浏览器后恢复 JSF 表单。第 2 部分将要给出的解决方案非常适合多用户和多表单的应用程序,同时还能识别浏览器会话间的匿名用户。
源码下载:http://file.javaxxz.com/2014/10/10/005052265.zip |
|