名称
Asynchronous Javascript And XML:异步的Javascript与XML
异步的:非同步的
问题
以登录功能为例,假设是失败的:当用户点击“登录”按钮后,会向服务器提交请求,然后,服务器端进行登录验证,并返回结果,返回的结果由于是失败的,很可能还是登录页面,只不过此次登录界面中包括错误信息,例如在页面上会显示“用户名或密码错误”之类的提示。
在这样的案例中,存在的问题:
1 登录前和登录后的是同一个界面,却由服务器返回了2次,第2次仅仅只是多了错误提示,返回的却是整个界面,从必要性、流量方向考虑,都是没有必要的!
2 用户体验较差,特别是网络传输速度较差的情况下表现得更加明显,毕竟需要处理的数据中有较大比例的是不必要传输的数据!
3 用户体验较差,表现为当用户提交后,接下来将得到新的界面,在提交后、新界面显示之前,用户无法在界面中进行操作。
基本概念
AJAX是一种提交HTTP请求、获取结果的处理方式。
AJAX提交的请求是异步的,即开启另一个线程提交请求,与当前浏览器显示的页面、用户的使用并不发生冲突。
AJAX获取的响应结果通常不会是一个完整的网页,而是某些数据,可以是普通的字符串直接表示的数据,例如错误提示信息,也可能是有规则组织起来的数据,例如使用XML来组织这些数据。
AJAX并不是一门新的语言,只是一门技术而已。
AJAX在程序中的表现是Javascript代码。
XMLHttpRequest
在AJAX中,XMLHttpRequest就是用于发送异步请求,并获取响应结果的对象!
由于历史版本问题,在IE 5和IE 6中,并没有XMLHttpRequest,而是使用的ActiveXObject,所以,在这2个版本的浏览器,如果需要使用AJAX,必须是:
XMLHttpRequest xmlhttp = new ActiveXObject("Microsoft.XMLHttp");
为了保证各种浏览器都可以获取到正确的XMLHttpRequest对象:
function getXMLHttpRequest() {
var xmlhttp = null;
if (window.XMLHttpRequest) {
// 适用于IE 7+、Chrome、Firefox、Safari、Opera……
xmlhttp = new XMLHttpRequest();
} else {
// 适用于IE 5、IE 6
xmlhttp = new ActiveXObject("Microsoft.XMLHttp");
}
return xmlhttp;
}
在XMLHttpRequest中的5个属性
– onreadystatechange:处理函数,即请求发出后,哪个函数对后续的操作进行处理,该属性的值应该是一个函数。
– readyState:状态,该属性的值为0~4之间的数值,即共5种状态:
– -- 0[不关心]:连接尚未初始化
– -- 1[不关心]:初始化连接
– -- 2[不关心]:发出请求
– -- 3[不关心]:服务器已经接收到请求
– -- 4:响应已经就绪
– status:响应码,例如200表示正确的响应
– responseText:响应的正文(字符串)
– responseXML:响应的XML,该属性和responseText选取其中一个来使用即可,取决于服务器响应的正文到底是字符串还是XML格式
发送请求
发送请求需要调用open()和send()这2个函数,例如:
xmlhttp.open("GET", "query.do", true);
xmlhttp.send();
在open()函数中,第1个参数表示请求类型,取值可以是"GET"或"POST",第2个参数表示请求资源的路径,可以是相对路径,也可以是绝对路径,路径对应的资源可以是一个动态页面,也可以是在服务器上的文本文件,或图片等任意资源,第3个参数表示此次请求是否是异步的,取值为true表示异步,取值为false表示同步。
在发出"GET"请求时,调用send()函数时,不需要添加参数,或添加null作为参数均可,如果发出的是"POST"请求,则应该把参数以"name1=value1&name2=value2&name3=value3"这样的形式组织起来,作为调用send()函数的参数。
由于"POST"请求的参数是需要配置请求头的,所以,在发出"POST"请求之前,还必须调用XMLHttpRequest对象的setRequestHeader()函数,以配置请求头:
xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
Ps:在使用open()函数时将第3个参数设置为false表示将发出同步请求,一旦发出请求,当前网页将暂时处于不可用状态,例如网页上的按钮将无法点击,页面将无法滚动等,直至此次同步请求处理完毕。基于这样的特性,通常极少使用同步请求,即使使用,也只用于处理少量数据,保证同步请求不会占用太多时间,从而不影响用户的正常体验。
案例一
目标
服务器端有一组数据:BeiJing、ShangHai、GuangZhou、ShenZhen、TianJin、NanJing、ChangSha、ChengDu……
在网页中输入例如字母B,将在网页中提示以字母B作为首字母的结果:BeiJing,如果输入S,则提示:ShangHai、ShenZhen
开发步骤
1 设计功能:
public String query(String keyword) {
String[] cities = "BeiJing,ShangHai,GuangZhou,ShenZhen,TianJin,NanJing,ChangSha,ChengDu".split(",");
StringBuffer result = new StringBuffer();
for (int i = 0; i < cities.length; i++) {
if (cities[i].startsWith(keyword)) {
if (result.length() > 0) {
result.append("、");
}
result.append(cities[i]);
}
}
return result.toString();
}
2 开发Web项目,提供功能
cn.tedu.ajax.servlet.QueryCityServlet
http://SERVER:PORT/PROJECT/query.do?keyword=S
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 获取请求参数:关键字
String keyword = req.getParameter("keyword");
// 2. 调用query()方法,获取关键字对应的结果
String result = query(keyword);
// 3. 获取输出流,向客户端输出结果
PrintWriter out = resp.getWriter();
out.println(result);
out.flush();
out.close();
}
另:需注册Servlet
3 设计前端页面:页面中需要1个输入框用于输入关键字,另需要1个标签显示结果,其它部分没有要求
4 编写Javascript程序,每当输入框中的内容改变时,获取输入框中的内容
<script type="text/javascript">
function showSuggest(keyword) {
// 测试获取输入框中的内容
// alert(keyword);
}
</script>
<input type="text"
onkeyup="showSuggest(this.value)" />
5 获取异步请求对象
var xmlhttp = new XMLHttpRequest();
6 发出请求
// 请求URL
var url = "query.do?keyword=" + keyword;
// 发出请求
xmlhttp.open("GET", url, true);
xmlhttp.send();
7 在发出请求之前,添加对结果处理的函数
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4
&& xmlhttp.status == 200) {
document.getElementById("suggestResult")
.innerHTML = xmlhttp.responseText;
}
};
案例二
目标
模拟登录,需要输入用户名和密码来登录,假设正确的用户名是tomcat,匹配的密码是tomcat7,当用户登录后,如果用户名错误,则在用户名的输入框下方提示用户名错误,如果密码错误,则在密码的输入框下方提示密码错误。
开发步骤
1 创建项目:DAY09-AJAX-02-Login
2 创建Servlet:cn.tedu.ajax.servlet.LoginServlet,并在该LoginServlet中重写doPost()方法,在方法中接收名为username和password的参数,根据参数模拟判断登录,如果用户名错误,向客户端输出"Username not exists",如果密码错误,向客户端输出"Password not match",如果都正确,则向客户端输出"OK",完成后,在web.xml中注册该LoginServlet,映射地址为:handleLogin.do
3 在webapp目录下添加index.html,在该文件中添加2个输入框,分别用于输入用户名和密码,并为这2个输入框设置id属性,以便于通过Javascript获取输入框中的值,然后添加按钮,最后,在2个输入框的下方分别添加1个标签,用于显示登录错误的信息
4 添加Javascript函数:funtion $(id) …
5 添加Javascript函数:function getXMLHttpRequest() …
6 添加Javascript函数:function doLogin(),在该函数中,先获取用户输入的数据,验证数据基本有效性(本次练习可选操作),然后获取XMLHttpRequest对象,配置onreadystatechange属性,设计响应函数,然后依次调用open()、setRequestHeader()、send()方法发出请求,在响应函数中,通过XMLHttpRequest对象的responseText获取响应结果,对结果进行判断,然后通过标签对象的innerHTML来显示错误信息,或者通过window.location重定向
在Spring MVC案例中,控制器返回值
在Spring MVC的默认情况下,控制器返回的值是用于处理后续内容的View组件名称,只需要使用@ResponseBody对方法进行注解,则表示方法返回的就只是值!注意:使用@ResponseBody时,必须在Spring的配置文件中添加<mvc:annotation-driven />
public class Controller {
@RequestMapping()
@ResponseBody
public String handleLogin() {
return "value";
}
}
在Spring MVC中使用@ResponseBody
基本使用
@ResponseBody用于注解Controller类中的方法。
一旦使用了这个注解,Controller中方法的返回值就应该是String,并且不再表示“返回View组件的名称”的意义,而是返回普通的字符串,最终,客户端得到的响应也就是这个字符串!
使用@ResponseBody时,推荐在Spring的配置文件中添加<mvc:annotation-driven />驱动注解。
乱码问题
一旦使用了@ResponseBody,在默认情况下,会通过StringHttpMessageConverter来处理结果,其默认的处理方式中,使用的是ISO-8859-1编码,并不会按照UTF-8来处理,所以,会产生中文乱码的问题!
解决方案就是在Spring的配置文件中,在mvc:annotation-driven节点下,添加mvc:message-converters节点,显式的配置StringHttpMessageConverter,并设置字符编码,在这个类中,字符编码是通过父类的supportedMediaType属性来设置的,所以,可以配置为:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=utf-8</value>
<value>application/json;charset=utf-8</value>
<value>*/*;charset=utf-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
由于StringHttpMessageConverter中supportedMediaTypes属性的类型是List集合,所以,推荐以上配置方式,当然,也可以简化为:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes"
value="text/html;charset=utf-8" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
JSON
为什么要使用JSON
在很多情况下,服务端程序作出的响应不再是显示某个HTML页面,而是一系列相关的数据,这样的话,更加利于服务端程序“提供数据服务”,即只提供数据,而不负责数据的呈现。
由服务端程序提供数据往往包含很多信息,比如自定义的响应代码(例如1表示成功,-1表示某种操作错误等),还包含提示信息、关键数据等等。
诸多的信息应该有规则的组织起来,便于发出请求的一方进行分析并且处理!早期的做法是使用XML进行处理:
<response-body>
<status>1</status>
<data>
<username>tomcat</username>
<age>18</age>
<from>BeiJing</form>
</data>
</response-body>
使用XML最大的不足在于使用的节点过多,解析过程相对麻烦,并且由于节点过多,导致传输的数据量较大!
用于取代XML这种组织数据的方式的就是JSON:
{
"status":1,
"data":{
"username":"tomcat",
"age":18,
"from":"BeiJing"
}
}
通过对比,可以发现,使用JSON表达的数据,体量更小(字节数更少),所以,最终将表现得传输更快,消耗的流量更少一些!事实上,数据解析过程也更加简单一些!
JSON的数据格式
1 JSON数据必须使用一对大括号框住后再开始编写,相当于每个XML都有且仅有1个根节点
2 JSON数据中的属性与值都是使用 “属性名”:属性值 这样的格式来组织,多对属性与值的配置之间使用逗号进行分隔,例如:
{
"name":"tomcat",
"age":18
}
3 JSON中也有数据类型的概念,如果属性的值是字符串类型的,必须使用双引号框住,如果是数值型的,直接写值即可,但是,由于JSON被多用于服务器端向客户端传输数据,所以,并不一定需要在意数据的类型,即使全部作为字符串类型来处理也是可以的:
{
"name":"tomcat",
"age":"18"
}
4 JSON中也可以表示对象,如果某个属性的值是对象,则该属性值应该使用大括号框住,在这对大括号中,继续使用 “属性名”:属性值 的格式表示对象内部的属性:
{
"status":1,
"data":{
"username":"tomcat",
"age":18,
"from":"BeiJing",
"department":{
"id":9527,
"name":"rd"
}
}
}
5 JSON中还可以表示数组或集合,对于JSON而言,严格的说只有数组,如果要表示List集合,语法上与数组是一样的,没有区别!表示数组的方式是使用 [] 将所有的数组成员框住,各成员之间使用逗号分隔:
{
"users":[
{ "username":"Alex", "age":18 },
{ "username":"Billy", "age":19 }
]
}
6 在Javascript中,也可以很方便的访问JSON数据,例如:
var jsonData = { "name":"tomcat", "age":"18" };
alert(jsonData.name);
jsonData = {
"status":1,
"data":{
"username":"tomcat",
"age":18,
"from":"BeiJing"
}
};
alert(jsonData.data.username);
jsonData = {
"users":[
{ "username":"Alex", "age":18 },
{ "username":"Billy", "age":19 }
]
};
alert(jsonData.users[1].username);
7 如果在Javascript中只能先得到JSON的字符串格式,需要转换为JSON对象,可以通过JSON.parse()函数来实现转换:
var text = '{ "status":1, "message":"ok" }';
var jsonObject = JSON.parse(text);
返回通用的JSON数据格式
为了便于前端页面及其它客户端更好的处理数据,通常会定义一个通用的响应数据类型,即无论此次是什么样的请求,或请求对应的操作成功与否,都会响应的同一种数据类型:
public class ResponseResult<T> {
int state; // 状态码:成功?失败?哪种原因的失败?
String message; // 错误描述
T data; // 数据
}
通常,在使用了通用数据格式后,每个操作都会提供相关的文档说明,例如:
请求URL:handleLogin.do
请求方式:POST
请求参数:username=tomcat&password=tomcat7
响应:
state:1,表示操作成功
state:-1,表示用户名错误,描述文字可从message属性中获取
state:-2,表示密码错误,描述文字可从message属性中获取
使用Jackson处理JSON
Jackson是一款第三方的用于处理JSON数据的库。
导入Jackson:
-- jackson-core
-- jackson-annotations
-- jackson-databind
当已经导入所需的包后,即可通过Jackson将对象转换为JSON格式:
ObjectMapper om = new ObjectMapper();
String jsonString = om.writeValueAsString(obj); // try...catch
练习一
目标
在HTML页面中存在id="time"的区域,用于显示时间,时间的值是由服务器端提供的,每间隔1秒钟更新一次时间。
开发步骤
1 创建项目:DAY10-03-AJAX-Timer,完成相对固定的操作:解决web.xml问题、添加spring-webmvc和jackson的依赖、配置Tomcat Runtime、复制Spring配置文件、配置web.xml
2 创建ResponseResult类,用于表示通用的响应结果
3 创建TimeController,添加public String showTime()方法,映射到"showTime.do"这个路径,在方法中,结合SimpleDateFormat和Date类得到时间的字符串,格式例如:2017-12-25 15:36:15,然后创建ResponseResult对象,将时间字符串封装到ResponseResult中的data中,最后,通过Jackson将ResponseResult对象转换为JSON格式,并作为showTime()方法的返回值
4 创建index.html文件,添加用于显示时间,添加按钮用于点击后显示时间,按钮绑定的Javascript函数为showTime()
5 在index.html中,添加 $(id) 和 getXMLHttpRequest() 这2个函数
6 在index.html中,添加showTime()函数,在函数中,通过XMLHttpRequest对象向服务器的showTime.do发出GET请求,并获取结果的JSON字符串,然后解析出其中的data的数据,并显示到id=timer的标签中
[问题] 在Javascript中的setTimeout和setInterval
这2个函数的本质并不相同,setTimeout用于设置多久以后执行什么任务,setInterval用于设置每间隔多久执行什么任务。
如果在setTimeout设置的函数中调用了自身,也可以实现“每间隔多久执行什么任务”的效果。
推荐使用setTimeout完成循环的任务,特别是某些执行时间可能略长的任务!
假设某个任务单次执行时间需要0.1秒,然后使用setInterval(“xxx”, 100)来循环运行,实际运行效果就是不停的在执行任务,其间并没有暂停过100毫秒!
练习二
在页面上设置一个下拉菜单( + ),表示省份,默认的值可自行添加2个以上,每当该菜单项的选中状态发生变化时,动态的显示第2个下拉菜单中的内容,第2个下拉菜单中应该显示某省份对应的城市列表。
练习 - 二级联动菜单
1 创建项目及准备工作
2 设计数据
----- 请选择 -----
1.广东省 1广州,2深圳,3珠海
2.河北省 4石家庄,5保定,6秦皇岛
3 设计服务端的功能:
URL:getCities.do
请求类型:GET
参数:provinceId
返回:ResponseResult,其中的数据部分是List<City>,City中至少封装id, name
a) 创建cn.tedu.spring.bean.ResponseResult,属性包括:int state,String message,Object data
b) 创建cn.tedu.spring.bean.City,属性包括:int id,String name
c) 创建cn.tedu.spring.controller.CityController,添加public String getCities(int provinceId)方法,映射到getCities.do路径,在方法内部,根据参数provinceId进行判断,然后得到List<City>,封装到ResponseResult中,最后转换为JSON字符串,作为方法的返回值
d) 通过浏览器测试
4 创建前端页面index.html,并添加2个下拉菜单,其中第1个下拉菜单中添加1广东省、2河北省这2个有效选项
5 设计AJAX请求
function $(id) {
return document.getElementById(id);
}
function getXMLHttpRequest() {
var xmlhttp = null;
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
} else {
xmlhttp = new ActiveXObject("Microsoft.XMLHttp");
}
return xmlhttp;
}
function getCities(provinceId) {
// 无论选中哪个选项,先清空“城市”列表
while ($("cities").firstChild) {
$("cities").removeChild(
$("cities").firstChild);
}
// 判断provinceId是否有效
if (provinceId == 0) {
return;
}
// 获取XMLHttpRequest
var xmlhttp = getXMLHttpRequest();
// 设计响应函数
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4
&& xmlhttp.status == 200) {
// 获取响应的正文文本
var text = xmlhttp.responseText;
// 将响应的正文文本转换为JSON对象
var jsonObject = JSON.parse(text);
// 判断JSON对象中的state是否正确
if (jsonObject.state == 1) {
// 遍历JSON对象中的data对应的数组
for (var i = 0; i < jsonObject.data.length; i++) {
// 将数组中的每个数据设计为一个个<option>
var opt = document.createElement("option");
// <option value="3">珠海</option>
// "data":[
// {"id":1,"name":"广州"},
// {"id":2,"name":"深圳"},
// {"id":3,"name":"珠海"}
// ]
opt.value = jsonObject.data[i].id;
opt.innerHTML = jsonObject.data[i].name;
// 将<option>添加到“城市”列表中
$("cities").appendChild(opt);
}
}
}
};
// 发出请求
var url = "getCities.do?provinceId="
+ provinceId;
xmlhttp.open("GET", url, true);
xmlhttp.send();
}
AJAX的数据缓存问题
如果反复向同一个URL发出请求,并且请求参数没有发生变化的话,可能会获取到缓存的数据,即获取到的不是最新数据!
针对这个问题的解决方案:使得每次请求参数都不同!例如为每次的请求多添加1个参数,参数名可以自定义,参数值使用随机数即可!
jQuery AJAX - load()
在使用了jQuery后,关于AJAX的应用也得到了极大的简化,通过load()函数可以快速的提交AJAX的异步请求,并且将响应的结果填充到某个标签中。
注意:这种做法适用于(但不局限于)将结果直接填充到标签中!
这种做法的语法格式是:
$(选择器).load(URL,参数);
在以上语法中,$(选择器)表示需要将结果填充到哪个标签中,URL表示请求路径,参数表示发出请求时向服务器提交的参数,参数值的格式可以是字符串格式,例如:
username=tomcat&password=123456
参数值的格式还可以是JSON对象格式:
{ "username":"tomcat", "password":"123456" }
其实,load()函数的完整格式是:
$(选择器).load(URL,参数,处理响应结果的函数);
也就是说,load()函数还可以有第3个参数,第3个参数是一个函数,专门用于处理结果,类似于传统方式中的xmlhttp.onreadystatechange属性的值。
另外,其实在jQuery中,还有另一个也叫做load()的函数,也是被某个标签对象所调用的,但是,其参数是某个函数,用于表示标签加载完成时的自定义处理,这2个load()函数,至于执行的是哪个,取决于参数的设计。
jQuery AJAX - ajax()
在使用jQuery时,通过ajax()函数可以设置AJAX请求过程中所有的可设置项!
使用ajax()的基本语法是:
$.ajax({配置});
以上语法中,“配置”及左右两侧的{ }整体表现为一个JSON对象格式。
在“配置”中,通常会设置的属性有:
url:请求的路径
例如:
$.ajax({
"url":"getCities.do",
"success":function(obj) {
}
});
还会设置的属性有:
type:请求的类型(请求方式),取值为"GET"或"POST"
data:请求的参数,参数值可以是字符串格式的,也可以是JSON对象格式的
dataType:响应结果的数据的类型,常用的取值有"text"、"json",当取值为"text"时,后续success属性对应的处理函数中的参数将是字符串类型的,当取值为"json"时,后续success属性对应的处理函数中的参数将是JSON对象
success:成功得到响应结果时的处理函数,即该属性的值是某个函数,该函数仅会在成功得到响应结果后被调用,所以,在函数内部,无须再判断状态为4或响应码为200,并且,在编写处理函数时,请为该函数添加参数,参数名称可自行定义,参数将是成功得到响应结果时的响应结果,类似于xmlhttp.responseText
在配置以上属性时,不区分先后顺序,即先配置哪个后配置哪个,并没有要求!
除此以外,ajax()函数还可以配置许多属性,如有需要,请参考相关文档。