http://www.ibm.com/developerworks/cn/web/wa-aj-simplejava1/
简介:很多 Web 开发人员都经常抱怨说 Java™ EE 太复杂、构建新的 Web 组件太难、定制现有的组件没有预想的那样简单,并且即便是很小的更改都需要重新启动应用程序。本系列给出了针对这些问题的解决方案,即采用代码生成器、约定、脚本语言和先进的 JavaServer Pages ™ (JSP) 特性。在本文中,您将了解如何基于 JSP 标记文件构建可重用的 Ajax 和 Java 组件,而这些 JSP 标记文件很容易开发和部署。更改之后,JSP 标记文件会由 Java EE 服务器自动重编译,而无须重启应用程序。此外,您还能完全控制所生成的代码,并能轻松地定制这些轻量级组件,因为它们使用的是 JSP 语法。
本系列含 4 部分,展示了一种基于 JSP 的技术,用以生成 JavaScript 代码、显著减少需要手动编写的代码量,本文是第 1 部分。本文的示例应用程序展示了如何生成 JavaScript 函数来发送 Ajax 请求和处理 Ajax 响应。如果想要轻松地更改 Ajax 代码,可以将这里讨论的简单技巧应用到实际的应用程序中。本文更宽泛的目标是展示如何使用 JSP 标记文件针对具体需求生成 JavaScript 代码,而非只是 Ajax 例程。
如果您很幸运地找到了一种能满足您需要的组件或框架,那么就请使用它吧。如果没有找到也没关系,因为您总是可以开发自己的解决方案,也可以定制现有的一段代码。不管是哪种情况,一种很好的做法是 “参数化” 代码并将其放入一个可重用的库,而非将参数硬编码到您的代码里。不过有时候,实现泛型并不实际,因为它会使开发变得复杂,而非简化。在将泛型代码放入可重用组件或框架时,可以考虑使用代码生成器来更有效地生成特定的代码。
假设,您需要应用程序使用 Ajax 请求站点上的某些信息,最快的(当然不是最好的)方法是找到一些如清单 1 这样的免费代码、更改 URL 并将这些代码粘贴到 Web 页面。很多开发人员都会这么做,但这种做法会导致巨大的维护问题。如果应用程序具有数百个页面,最后的结果将是出现大量像清单 1 中的getInfo()
这样的函数。不好的一面是每次需要进行添加或更改(比如实现 Ajax 请求的错误处理)时,您都必须要手动修改所有页面并重新测试它们。好的一面是您可以通过使用库、框架和代码生成器,很容易地避免这个维护问题。
function getInfo(country,city) { var request = null; if (window.ActiveXObject) request = new ActiveXObject("Microsoft.XMLHTTP"); else if (window.XMLHttpRequest) request = new XMLHttpRequest(); else return; var url = "CityInfo.jsp?country=" + escape(country) + "&city=" + escape(city); request.open("GET",url,true); function processResponse() { if (request.readyState == 4) { if (request.status == 200) { // ... } } } request.onreadystatechange = processResponse; request.send(null); } |
一种好的开发实践是将尽量多的代码移入可重用的例程、函数、类或组件,而这些例程、函数、类或组件均能划分到库或框架中。在我们的示例中,您能找到一个泛型函数,此函数能创建XMLHttpRequest
实例和调用实例的open()
和send()
方法。
假设您决定使用的函数的名称为xhr()
,它能接受 5 个参数:返回信息的页面的 URL、包含名称和请求参数的值的两个数组、HTTP 方法和用来处理 Ajax 响应的一个回调函数。现在,您的应用程序将会包含更为简单的函数,比如清单 2 中所示的getInfo2()
,而且代码也更容易维护。如果想要更改发送 Ajax 请求的代码,只能修改xhr()
函数。
function xhr(url,paramNames,paramValues,method,callback) { // send Ajax request ... } function getInfo2(country,city) { function processResponse(request) { // process Ajax response ... } xhr("CityInfo.jsp",["country","city"],[country,city],"GET",processResponse); } |
清单 2 包含的泛型函数,名为xhr()
,特定于应用程序的函数名为getInfo2()
。泛型代码应被移入单独的 JavaScript 文件以便能将可重用函数导入需要它们的任何页面。对于特定的代码,比如getInfo2()
函数,如果应用程序需要基于相同模式的很多函数,就应该考虑使用代码生成器。
代码生成器能显著提高开发和维护 Web 应用程序的效率。比如,您可以使用 JSP、Java 代码或任何其他语言来从模板生成 JavaScript 函数。得益于属性名称,用来指定生成器参数的基于 XML 的语法能让代码可读性更好,也更容易理解。此外,标记属性没有像 JavaScript 函数或 Java 方法的参数那样的固定顺序。
对比起来,使用 XML 标记的一个明显优点就是它们能为属性使用默认值,然而编程语言只提供了有限的可能性来删除方法参数。请考虑这些关于代码生成器可扩展性的诸多方面,因为在不打乱现有代码的情况下向标记增加新的属性要比更改方法签名简单得多。使用 XML 和 JSP 的这些句法方面的优点是很明显的,尤其是当代码生成器需要大量属性的时候。
JSP 是在服务器端生成 JavaScript 代码的一种很好的选择,因为:
此外,JSP 技术已经有了一种很强大的机制来将可执行代码放在定制标记后面,所以,不必为基于 JSP 语法的模板实现解析器。最后,在每次您做更改时,也不需要外部工具来重新生成代码。
清单 3 中包含取自于本文下一章节所要展示的应用程序的一个代码片段。此处使用了一个名为<da:xhr>
的定制的 JSP 标记来生成getInfo3()
函数,当用户单击一个标签为Get Info的按钮时,该函数就会在 Web 浏览器中被调用。
<%@ taglib prefix="da" tagdir="/WEB-INF/tags/dynamic/ajax" %> ... <script type="text/javascript"> <da:xhr function="getInfo3(country,city)" url="CityInfo.jsp" method="GET"> // process Ajax response ... </da:xhr> </script> ... <button ... onclick="getInfo3(...)">Get Info</button> |
所生成的 JavaScript 代码可被放入调用清单 3 中所示的生成函数的 Web 页面的<script>
元素中。如果多个 Web 页面需要同样的 JavaScript 代码,就像任何常规的 JavaScript 文件一样, 动态生成此代码的一个单独的 JSP 文件将被导入到这个应用程序的 Web 页面,在<script>
元素的src
属性中指定其 URI(参见清单 4)。
清单 4. 导入由 JSP 页面生成的 JavaScript
<script src="DynamicJavaScript.jsp" type="text/javascript"> </script> |
虽然在开发阶段为每个请求生成 JavaScript 代码并不会产生问题,但您不得不考虑在生产环境中的性能损失。解决的方法就是缓存代码,比如用 SJTL 将生成的代码存储到 JSPapplication
作用域,如清单 5 所示。然后,可以在 Web 页面中用 EL 结构(例如${applicationScope.cachedCode}
)插入所缓存的代码。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:if test="${empty cachedCode}"> <c:set var="cachedCode" scope="application"> alert("Cached Code"); </c:set> </c:if> ${applicationScope.cachedCode} |
本节描述了示例应用程序的 JSP 页面。CityForm.jsp
页面包括一个 Web 表单,其数据由 Ajax 发送到 Web 服务器。另一个名为CityInfo.jsp
的页面生成 Ajax 响应。
此示例应用程序的CityForm.jsp
页面使用了两个定制标记,名称分别为<da:xhr>
和<da:innerHTML>
,是作为 JSP 标记文件实现的。xhr.tag
文件生成能发送 Ajax 请求的一个 JavaScript 函数,而innerHTML.tag
则生成单一一行代码,它用innerHTML
属性在 HTML 元素内插入一些内容。两个标记文件的 JSP 代码将在本文的稍后部分给出。
JSP 页面(见清单 6)声明了所使用的标记库,它们是 JSTL Core(前缀为c
)和标记文件库(前缀为da
)。CityForm.jsp
还导入了两个 JavaScript 文件,名字分别为ajax.js
和encode.js
,其函数从由<da:xhr>
和<da:innerHTML>
生成的代码调用。这些定制标记用于在<script>
元素内生成名为getInfo()
的一个 JavaScript 函数的代码。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="da" tagdir="/WEB-INF/tags/dynamic/ajax" %> <html> <head> <title>Ajax Demo</title> <script src="ajax.js" type="text/javascript"> </script> <script src="encode.js" type="text/javascript"> </script> <script type="text/javascript"> <da:xhr function="getInfo(country,city)" method="GET" url="CityInfo.jsp" sync="false" json="true" cpr="true"> <da:innerHTML id="info" value="json.info" encode="true"/> </da:xhr> function getInfo2(form) { var country = form.country.options[form.country.selectedIndex].text; var city = form.city.value; getInfo(country,city); } </script> </head> <body> <form name="data"> Country: <select name="country" size="1"> <option>Canada</option> <option>UK</option> <option selected>USA</option> </select> City: <input name="city" size="20"> <button type="button" onclick="getInfo2(this.form)">Get Info</button> </form> <div id="info"></div> </body> </html> |
CityForm.jsp
页面的 Web 表单包含一个标签为Get Info的按钮、一个国家列表和一个让用户输入城市名称的输入字段。当用户单击这个按钮时,Web 浏览器将调用getInfo2()
函数,它的调用是在onclick
属性内编码的。此函数包含 Web 表单中的country
和city
字段的值,并且会将这些值传递给getInfo()
函数,该函数会将 Ajax 请求发送给服务器。Ajax 响应将包含需要插入到置于 Web 表单下的<div>
元素的信息。
<da:xhr>
的属性允许指定已生成的 JavaScript 函数的头、用来发送 Ajax 请求的 HTTP 方法以及生成 Ajax 响应的那个页面的 URL。名为sync
、json
和cpr
的属性则可以让您指定已生成代码的各种特性。
如果sync
为true
,那么信息就会被同步请求,这意味着当从服务器检索数据时,用户界面将会被阻塞。 如果sync
为false
,请求就是异步的,意味着在信息通过网络传递和由服务器处理时,用户可以进行操作。
如果json
属性为true
,那么这个代码生成器将会用eval(request.responseText)
添加一行 JavaScript 代码来评估 Ajax 响应。最后,如果<da:xhr>
标记的cpr
属性是true
,那么xhr.tag
文件将产生一些 JavaScript 代码片段,它将在发送一个新请求前关闭之前的请求。我在讨论ajax.js
和xhr.tag
文件时,将会介绍更多关于此特性的内容。
清单 7 显示了CityForm.jsp
页面已生成的getInfo(country,city)
函数。所使用的 HTTP 方法是 GET,生成 Ajax 响应的页面的 URL 是CityInfo.jsp
,sync
属性是false
,json
与cpr
属性都是true
。所生成的 JavaScript 代码使用ajax.js
文件的openRequest()
、sendRequest()
、closeRequest()
和httpError()
函数,以及encode.js
的appendParam()
与htmlEncode()
函数。
var getInfoRequest = null; function getInfo(country,city) { if (getInfoRequest) closeRequest(getInfoRequest); var url = "CityInfo.jsp"; url += '?'; url = appendParam(url,"country",country); url = appendParam(url,"city",city); var request = openRequest("GET",true); getInfoRequest = request; if (request == null) return null; function processResponse() { if (request.readyState == 4) { if (request.status == 200) { eval(request.responseText); document.getElementById("info").innerHTML = htmlEncode(json.info); } else { httpError(request); document.location = url; } } } request.onreadystatechange = processResponse; sendRequest(request,null); return request; } |
如果将 HTTP 方法改为 POST,那么所生成的代码也会相应地被修改,如清单 8 中所示。这里并没有向url
添加请求参数,相反,getInfo()
函数会将这些参数追加给一个名为body
的变量,此变量之后会被传递给ajax.js
文件的sendRequest()
函数。
清单 8. 为使用 POST 的 Ajax 请求所生成的函数
CityInfo.jsp
页面(见清单 9)使用 JavaScript Object Notation (JSON) 生成对 Ajax 请求的响应。为了保持页面无脚本,Java 代码将被移入名为noCache.tag
和jstring.tag
的两个 JSP 标记文件,它们将由<da:noCache>
和<da:jstring>
从 JSP 页面调用。将 Java 代码放入 JSP 标记文件要比开发标记处理程序类容易得多,因为 JSP 容器会在不必重启应用程序的情况下为您生成这些类并且会在代码更改后自动重新编译 Java 代码。
<%@ taglib prefix="da" tagdir="/WEB-INF/tags/dynamic/ajax" %> <da:noCache/> json = { country: <da:jstring value="${param.country}"/>,city: <da:jstring value="${param.city}"/>,info: <da:jstring>Info on ${param.city},${param.country}</da:jstring> } |
清单 10 给出了 JSON 响应。
json = { country: "UK",city: "London",info: "Info on London,UK" } |
noCache.tag
文件(参见清单 11)包含单一一行 Java 代码,可以设置 HTTP 响应的Cache-Control
头。
<% response.setHeader("Cache-Control","no-cache"); %> |
jstring.tag
文件(如清单 12 所示)编码一个 JavaScript 字符串,其值可以作为属性,也可以作为内容主体传递给标记文件。如果value
属性不指定,<jsp:doBody>
动作就会执行包括在<da:jstring>
和</da:jstring>
之间的 JSP 代码,设置page
作用域的value
变量。 在这两种情况下,Java 代码用jspContext.getAttribute()
获得字符串值并会逐个地输出字符,转义特殊和非 ASCII 字符。
<%@ attribute name="value" required="false" rtexprvalue="true" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:if test="${empty value}"> <jsp:doBody var="value"/> </c:if> <% String value = (String) jspContext.getAttribute("value"); out.write('"'); int len = value.length(); for (int i = 0; i < len; i++) { char ch = value.charAt(i); switch (ch) { case '\\': out.write("\\\\"); break; case '\n': out.write("\\n"); break; case '\r': out.write("\\r"); break; case '\t': out.write("\\t"); break; case '"': out.write("\\\""); break; default: { if (' ' <= ch && ch <= '~') out.write(ch); else { out.write("\\u"); for (int j = 3; j >= 0; j--) { int k = (((int) ch) >> (j << 2)) & 0x0f; out.write((char) (k < 10 ? k + 48 : k + 55)); } } } } } out.write('"'); %> |
本节介绍ajax.js
和encode.js
文件,其函数均从由xhr.tag
和innerHTML.tag
文件生成的 JavaScript 代码调用。
ajax.js
文件的openRequest()
函数(参见清单 13)接受 3 个参数(method
、url
和async
)并会创建一个XMLHttpRequest
实例。然后,它会调用open()
方法并返回初始化了的request
对象。如果body
参数不是null
,sendRequest()
函数就会设置Content-Type
头并调用request
对象的send()
方法。
function openRequest(method,async) { var request = null; if (window.ActiveXObject) request = new ActiveXObject("Microsoft.XMLHTTP"); else if (window.XMLHttpRequest) request = new XMLHttpRequest(); if (request) request.open(method,async); return request; } function sendRequest(request,body) { if (body) request.setRequestHeader("Content-Type","application/x-www-form-urlencoded"); request.send(body); } function closeRequest(request) { request.onreadystatechange = function() { }; request.abort(); delete request; } function httpError(request) { alert("Http Error: " + request.status); } |
closeRequest()
方法将onreadystatechange
属性设置为一个空函数,调用abort()
方法并之后使用 JavaScript 的delete
操作符释放request
对象的内存。该函数应该在处理完 Ajax 请求之后对每个XMLHttpRequest
实例调用。否则,很可能导致 Web 浏览器内的内存泄露。ajax.js
的最后一个函数是httpError()
,它在警告窗口显示请求的状态。
encode.js
文件的htmlEncode()
函数(参见清单 14)接受字符串参数并会用&
、<
和>
替换&
、<
和>
字符。attrEncode()
函数执行的是同样的操作,之后用"
替换"
字符以便所编码的字符串可用作属性的值。
JavaScript 的escape()
函数通常用来编码 Ajax 请求的请求参数。应该注意escape()
没有编码+
字符,这是个问题,因为所有的+
均会在服务器上被解码为空格字符。
上面谈及的问题可由encode.js
文件的urlEncode()
函数修复,此文件使用了 JavaScript 的escape()
来执行 URL 编码,并会在之后用%2B
替换所有的+
字符以便编码后的字符串可以在服务器端正确解码。
function htmlEncode(value) { return value ? value.replace(/&/g,"&") .replace(/</g,"<").replace(/>/g,">") : ""; } function attrEncode(value) { return htmlEncode(value).replace(/"/g,"""); } function urlEncode(str) { return str ? escape(str).replace(/\+/g,"%2B") : ""; } function isArray(a) { return a.sort ? true : false; } function appendParam(url,name,value) { if (isArray(value)) { for (var i = 0; i < value.length; i++) url = appendParam(url,value[i]); } else { if (url && url.length > 0) { if (url.charAt(url.length-1) != '?') url += "&"; } else url = ""; url += urlEncode(name) + "=" + urlEncode(value); } return url; } |
appendParam()
函数向给定 URL 添加名称/值对。如果第三个参数是一个数组,JavaScript 代码就会在其元素上迭代并会递归调用appendParam()
,以便针对数组中的每个元素将名称/值对添加至url
。
本节介绍的是xhr.tag
和innerHTML.tag
文件。前者生成能将 Ajax 请求发送给服务器的 JavaScript 函数,后者则生成能在 Web 浏览器内处理 Ajax 请求的回调函数的代码。
<da:xhr>
标记接受 6 个属性:所生成的 JavaScript 函数的头部、HTTP 方法、将响应返回给 Ajax 请求的页面的 URL、指示请求应该同步还是异步发送的一个布尔属性、指定请求是否使用 JSON 格式的另一个布尔属性以及用来启用本文称为 “关闭之前请求” 特性的属性(cpr
)。所有这些属性均通过<%@attribute%>
指令在xhr.tag
文件中声明(参见清单 15)。
接下来,标记文件使用<%@taglib%>
指令声明所使用的标记库,将method
属性的字符转变成大写,定义称为reqVarName
的 JSP 变量,该变量构建自所生成的 JavaScript 函数的名称和Request
字符串。这之后,xhr.tag
文件开始生成 JavaScript 代码。如果cpr
属性为true
,就会声明一个 JavaScript 变量并由null
对该变量进行初始化。此变量用来保存之前的请求,而该请求必须在下次调用所生成的函数时被 “关闭”。本文前一章节已经介绍过ajax.js
文件的closeRequest()
函数。
<dau:appendParams>
标记用于xhr.tag
文件以便向url
变量追加请求参数(如果method
为GET
)或向body
变量追加请求参数(如果method
为POST
)。ajax.js
文件的openRequest()
函数创建和初始化XMLHttpRequest
实例。Ajax 请求通过sendRequest()
函数发送给服务器,该函数的代码可以在同一个ajax.js
文件找到。
<%@ attribute name="function" required="true" rtexprvalue="true" %> <%@ attribute name="method" required="false" rtexprvalue="true" %> <%@ attribute name="url" required="true" rtexprvalue="true" %> <%@ attribute name="sync" required="false" rtexprvalue="true" type="java.lang.Boolean" %> <%@ attribute name="json" required="false" rtexprvalue="true" type="java.lang.Boolean" %> <%@ attribute name="cpr" required="false" rtexprvalue="true" type="java.lang.Boolean" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%@ taglib prefix="dau" tagdir="/WEB-INF/tags/dynamic/ajax/util" %> <c:set var="method" value="${empty method ? 'GET' : fn:toUpperCase(method)}"/> <c:set var="reqVarName" value="${fn:trim(fn:substringBefore(function,'('))}Request"/> <c:if test="${cpr}">var ${reqVarName} = null;</c:if> function ${function} { <c:if test="${cpr}">if (${reqVarName}) closeRequest(${reqVarName});</c:if> var url = "${url}"; <c:if test="${method == 'GET'}"> url += '?'; <dau:appendParams jsVarName="url" function="${function}"/> </c:if> <c:if test="${method == 'POST'}"> var body = ""; <dau:appendParams jsVarName="body" function="${function}"/> </c:if> var request = openRequest("${method}",${!sync}); <c:if test="${cpr}">${reqVarName} = request;</c:if> if (request == null) return null; function processResponse() { if (request.readyState == 4) { if (request.status == 200) { <c:if test="${json}">eval(request.responseText);</c:if> <jsp:doBody/> } else { httpError(request); <c:if test="${method == 'POST'}">url += '?' + body;</c:if> document.location = url; } } } request.onreadystatechange = processResponse; <c:if test="${method == 'GET'}">sendRequest(request,null);</c:if> <c:if test="${method == 'POST'}">sendRequest(request,body);</c:if> return request; } |
内部函数名为processResponse()
,是通过onreadystatechange
属性传递给request
对象的一个回调函数。此回调函数在 Ajax 请求的整个生命周期进行了多次调用,而请求的当前状态可从readyState
属性获得。当readyState
为4
时,请求完成。
如果 HTTP 状态码为200
,表示没有发生错误,并且 Ajax 请求也可以由<da:xhr>
和</da:xhr>
之间的 JSP 代码处理。此代码由<jsp:doBody/>
自xhr.tag
文件调用。如果出现 HTTP 错误,所生成的代码会调用ajax.js
文件的httpError()
函数,并且浏览器也会被重定向到生成 Ajax 响应的 URL,因此开发人员就能够看到导致 HTTP 错误的服务器端错误。例如,如果 HTTP 错误码为500
(内部错误),应该可以看到一个 Java 堆栈跟踪。
清单 16 给出了此appendParams.tag
文件,该文件在给定函数头的参数上迭代并生成一个 JavaScript 代码行,此代码行调用encode.js
文件的appendParam()
函数。本文前面介绍的清单7和8中给出了所生成的代码。
<%@ attribute name="jsVarName" required="true" rtexprvalue="true" %> <%@ attribute name="function" required="true" rtexprvalue="true" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <c:set var="paramList" value="${fn:substringBefore(fn:substringAfter(function,'('),')')}"/> <c:forEach var="paramName" items="${paramList}"> <c:set var="paramName" value="${fn:trim(paramName)}"/> ${jsVarName} = appendParam(${jsVarName},"${paramName}",${paramName}); </c:forEach> |
innerHTML.tag
文件(如清单 17 所示)中有一行 JavaScript 代码,包含 DOM 对象,用来代表具有给定id
的 HTML 元素,所使用的是document.getElementById()
。随后,该元素的内容会被更改,方式是设置innerHTML
属性,其新值可以通过<da:innerHTML>
标记的value
属性传递,也可以在标记体内传递。如果encode
属性为true
,所生成的代码就会调用encode.js
文件的htmlEncode()
函数。
<%@ attribute name="id" required="true" rtexprvalue="true" %> <%@ attribute name="value" required="false" rtexprvalue="true" %> <%@ attribute name="encode" required="false" rtexprvalue="true" type="java.lang.Boolean" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:if test="${empty value}"> <jsp:doBody var="value"/> </c:if> <c:if test="${encode}"> document.getElementById("${id}").innerHTML = htmlEncode(${value}); </c:if> <c:if test="${!encode}"> document.getElementById("${id}").innerHTML = ${value}; </c:if> |
<da:innerHTML>
标记可用在<da:xhr>
之内以处理 Ajax 响应,如本文的示例应用程序所示。您可以构建类似的标记文件以根据应用程序的具体需要执行不同的处理操作。
在本文,您接触了如何利用 JSP 标记文件生成 Ajax 例程。您可以使用相同的技术生成服务器端其他类型的 JavaScript 函数,这样一来,您就能够更容易地添加或更改特性,因为每次修改了充当模板的标记文件时,代码都会自动重生成。请继续关注本系列的下一篇文章,您将会了解如何使用数据绑定、页面导航和样式约定以最小化设置和配置。
原文链接:https://www.f2er.com/ajax/166728.html