【jQuery】 JQ和AJAX

时间:2023-12-15 14:19:02

AJAX

  AJAX全称异步 JavaScript 和 XML(Asynchronous JavaScript and XML),是一种用于网页前端和网站后台进行数据交互的手段。关于AJAX的详细介绍在此省略,主要讲一下怎么用JQ包装好的AJAX方法来进行AJAX操作。

■  load方法  $(selector).load(URL[, data[, callback]])

  JQ有个load方法,可以指定一个URL,将URL指向文件中的内容加载到selector指定的元素中去。URL参数是必须的,同时URL还支持JQ选择器的语法,比如URL写成xxx.html #id1就是只把URL指定的那个html文件中的id是id1的元素加载进来。data参数可以指定一些一起跟随请求发送到后台的数据,通常可以是一个object类型,不是很常用的感觉。callback指定一个回调函数,当load方法完成之后调用这个函数。callback函数规定有三个参数,分别是 callback(responseTxt, statusTxt, xhr): responseTxt是调用load成功时返回的文本内容;statusTxt是调用load的状态码,成功的话值是'success',失败的话是'error';xhr是调用后返回的XMLHttpRequest对象(这个对象具体含有哪些信息可以参见XML那一篇),主要的信息有xhr.status(http状态码,200/404这种),xhr.statusText(http状态信息,成功的话就是OK),xhr.readyState(ajax状态码,4是已完成,参见XML那篇)以及xhr.responseText(这个内容和前面那个responseTxt内容是一样的,都是返回的正文文本)。

■  get方法和post方法  $(jQuery).get或$.get

  以上说到的load方法是针对某一个元素加载一些数据。如果是泛泛地要请求一些数据的话,用$.get或者$.post方法更好。事实上,这两个也是比load方法要更加常用一些。正如两者的方法名,这两个方法分别代表了在页面上发起一次get请求和post请求。

  首先来看get,即$.get方法

  $.get(URL[, callback])  URL就是指定的一个URL,callback函数也是ajax成功后的回调函数,一般常用的两个参数就是callback(data,status),data就是返回的内容,格式是字符串;而status是本次AJAX请求的状态,值可以是'success'和'error',通常不用特别写出来。

  *get方法还有一些变体,比如$.getJSON方法相当于指定datatype参数是json,而$.getScript方法相当于指定datatype参数是script,获取到脚本后还会自动执行之。

  $.post方法也是类似的

  $.post(URL, data [, callback])  和get不同的就是post多一个data参数,一般是一个object对象,用于传递本次post请求的附带数据。

■  ajax方法  $(jQuery).ajax

  其实上面说到的$.get方法和$.post方法都是$.ajax方法的一个包装。ajax方法才是比较底层的方法,比如:

$.get(url, callback)
//相当于
$.ajax({
type:'GET',
url: url,
data: data,
success: callback //请注意,callback函数接受一个参数即后端返回的数据,如果后端返回的是一个JSON字符串,那么这里这个参数直接是json对象而不是字符串,不需要我们额外eval或者JSON.parse来解析
})

  通常,ajax方法接受的就是一个object对象作为参数,这个object的key除了上述的url,data,success还有很多,介绍几个常用的(http://www.w3school.com.cn/jquery/ajax_ajax.asp):

  async  boolean值,默认true,指定本ajax请求是否为异步请求,如果设置为false那就是同步请求,在ajax执行完成前用户将不能做下一步操作。

  datatype  可以是'json','html','xml','script','text'等,这个参数指定了服务器返回的数据的预期格式。有了指定格式, 我们就可以保证在其他地方处理服务器返回的数据时不会混乱,因为当JQ检测到返回的数据不符合格式时就自动认为ajax请求失败了。其实dataType也是$.get,$.post的一个可选参数。但是上面都没有提到,这是因为,参数未被指定时JQ会对返回的数据做一个智能的判断来自动指出返回的数据是什么格式。

  error  一个function,和success类似,只不过这个函数是当ajax请求失败时调用的函数。这个函数通常有三个参数,分别是XMLHttpRequest对象,错误信息(可能是null,也可能是timeout,parseerror等具体错误信息)和捕获的异常对象

  timeout  设置请求超时时间(毫秒)。此设置将覆盖全局设置。

  global  boolean值,可以单独指出某一个ajax请求不应用全局ajax设置。至于怎么设置ajax全局设置可以参考其他一些比如ajaxStart,ajaxStop方法等

  context  指定一个对象,使之成为ajax的回调函数中this指向的对象。当context不被指定时,this指向的对象是指传给ajax本身的那个object对象参数。所以在ajax要对触发ajax请求的那个元素进行一些操作的时候通常可以设置context参数为那个元素。另一种解决方法就是在进入事件函数之后但在触发ajax请求之前,在语句中用一个变量保存下this,再到ajax回调函数中调用这个变量,这样的话就可以做到传递这个对象本身了。

  cache  boolean值,默认为true,设置是否缓存当前页面

  beforeSend  一个函数,传入当前ajax请求的XMLHttpRequest对象作为参数,用户可以对它做出修改后返回再send出。如果用户返回了false的话那么可以取消这次ajax请求。

  dataFilter  一个函数,当ajax请求成功,服务器返回了一些数据之后,如果设置了dataFilter函数,那么在success等ajax成功后的回调函数调用返回数据之前先用dataFilter对返回的数据做处理。再用dataFilter返回的数据作为success回调函数的参数执行success函数

  complete  一个函数,当请求完成之后调用这个函数,无论成功或失败。传入 XMLHttpRequest 对象,以及一个包含成功或错误代码的字符串。

  traditional  可以设置为true或者false,发现这个参数是因为在用ajax向后台传送数组参数的时候总是会导致后台接受到的参数中数组名由原来的array变成"array[]",将这个参数设置为true之后就可以避免了。(之前碰到这种问题的第一思路是用JSON.stringify把前端数组JSON化然后让后端解析。但是有时候后端是一个组件,一定要求传递参数名纯粹的数组,这时候traditional就有用了)

■  ajaxSetup方法  $(jQuery).ajaxSetup

  以上ajax方法中提到的参数,如果所有ajax都是一样的话那么一个一个都要写很麻烦,ajaxSetup方法就可以一劳永逸。这个方法的参数格式和可选项和ajax方法一样,只是它并不是真的发出一个ajax请求,而是设置一些,针对在它后面发起的ajax请求的默认参数。比如$.ajaxSetup({"url":"/test"})之后发起的所有ajax请求,都可以不写url参数而直接默认为/test这个url。

  

■  ajax事件

  上文中也提到了一些ajax方法中参数可选的一些回调函数,其实这些函数反应的就是ajax事件。另外,一个ajax请求中写的回调函数所指代的事件那肯定是只针对这个请求而言,如果想要全局地为所有ajax事件加上一些回调函数的话另有他法。

  首先先来看一下存在哪些ajax事件?:

  ajaxStart  ajax请求开始时触发的事件

  ajaxSend  ajax请求发送之前触发的事件

  ajaxSuccess  ajax请求成功后触发事件

  ajaxError  ajax请求失败后触发的事件

  ajaxStop  ajax请求成功并完成后触发的事件

  ajaxComplete  ajax请求完成,无论成功或失败触发的事件

  以上几个事件,都有同名的方法可以调用,且参数都是一个函数。另外还需要注意的一点是,这些方法的运行机制是,任何一个ajax请求触发了相应事件后,所有被绑定到事件上的函数不论是通过哪个元素绑定的都会挨个执行一遍,换句话说,这些方法前面虽然要跟一个选择器が,选择器选择了什么元素是无所谓的,只要你写出来了,那么所有的ajax请求在触发事件时都会执行你关联起来的那个函数。据说在1.9版本以后的jQuery就规定只能$(document)来绑定这些ajax事件了。如果想要让某个ajax请求不受它们的控制也很简单,就是之前说的global参数,设置成false就可以了。

  下面一个个来说

  ●  ajaxStart方法

    参数function()(没错,是无参数函数作为参数),再强调一下,无论在何时发送 Ajax 请求,jQuery 都会检查是否存在其他 Ajax 请求。如果不存在,则 jQuery 会触发该 ajaxStart 事件。在此时,由 .ajaxStart() 方法注册的任何函数都会被执行。

  ●  ajaxSend方法

    参数function([event,xhr,options])  三个参数都是可选的,event是ajax事件对象,xhr是本次ajax请求的XMLHttpRequest对象,options是本次ajax请求的参数。

  ●  ajaxSuccess方法

    相当于给每个ajax设置了success参数的回调函数,参数function规则同ajaxSend

  ●  ajaxStop方法

    参数是空参的function()。

  ●  ajaxError方法

    参数是function([event,xhr,options,exce])  前三样参数和之前是一样的,最后一个exce代表本次出错JS捕获到的异常对象。

  ●  ajaxComplete方法

    参数function([event,xhr,options])同ajaxSend。

  以上比较粗浅地讲了一下JQ中如何调用ajax的办法。如果想要进一步了解,应该得再去深挖一下XMLHttpRequest对象这玩意儿,毕竟它才是整个ajax的根基和底层。以上。

■  用ajax模仿action表单动作

  form标签的action指定之后,点击submit就会上传表单数据了。AJAX当然也可以通过POST方法来递交POST请求,但是接收完POST请求之后后台往往是返回一段HTML代码,这就使得ajax比较尴尬(涉及到了转义,单双引号等等问题)。其实在$.ajax方法的dataType字段,可以设置为html(一般都是设json的),这样jQuery就会自动将返回的内容识别为HTML,然后在success函数中write一下就好了。

■  ajax方法提交的内容类型(contentType)引起的异常

  今天碰到了这么一个坑爹的问题,在用rest_framework搭建起一个django项目的API之后,我要上传一个空数组作为一个字段的值,表示这个字段的值被清空。然而在rest_framework的调试web界面上,我传入xxx:[]就可以清空了,但是在自己的页面上调用$.ajax方法,在提交数据的时候总是找不到空数组那个字段,换言之,空数组字段并没有作为一个请求的字段传入后端。

  想了很多曲线救国的办法比如用JSON.stringify等等,都失败了。

  后来经过在调试web界面上发起的请求和自己的ajax发起的请求的对比,发现是请求头中contentType字段的值不一样的问题。在正确调用API的请求中,此字段是application/json,而在自己的ajax方法发起的请求中,这个字段的值是application/x-www-form-urlencoded。

  同时发现,POST,PATCH,DELETE等“侵害性方法”发起的请求,都会自动带上contentType的默认值是x-www-form-urlencoded。

  一开始仅仅是在ajax方法中指定了contentType: 'application/json',但是依然失败,看了请求包之后发现请求参数变成了一串和GET参数类似的请求字符串而不是一个JSON串。最终,把ajax方法的data字段的值用JSON.stringify({....})之后,终于试成功了。

  回头才发现,原来以前调用$.ajax方法时(尤其是POST时),都没注意到contentType一直都还是“表单数据”状态而非“JSON”状态。而如果是JSON状态的话自然需要JSON.stringify方法将数据转化为标准的JSON格式来供后台解析。

  花费了挺长时间才解决,这个坑记录一下。。

■  ajax的异步特性带来的一些麻烦

  之前一直默默吐槽有必要设置async这个参数吗,毕竟感觉异步比同步要高级好多。但是今天碰到了一个问题重新认识了这个参数。

  问题是这样的,在javascript某个地方,我需要发起两个ajax请求,并且对比他们的返回值,如果不一样则给出一些提示。自然,代码是这样的:

var res1 = null;
var res2 = null;
$.ajax({
url: 'xxx',
/*.....*/
success: function(data){
res1 = data;
}
});
$.ajax({
url: 'yyy',
/*.....*/
success: function(data){
res2 = data;
}
});
if (res1 != res2){alert('tigau!');}

  由于忽视了ajax的异步本性以及其默认是异步的设定,调试时出现了神奇的一幕。当不打开调试器,运行,即使两次ajax返回数据不同,也不给出提示。只要开着调试器,在上述代码任何一个地方加断点,然后继续运行就给出提示。原因不用多说了…就是ajax是异步的。代码自己运行时比较快,走到判断的时候两个ajax请求都还没有返回值,所以res1和res2两个变量的值都还是null,被判断成一样所以不给出提示。

  所以在连续的ajax请求发起,并且需要对其返回值进行ajax方法外的二次处理时,一定要注意其异步特点。修复的方法可以在ajax中添加async: false的参数来强行将ajax变为同步的方法。

  我之前尝试的一个办法是页面上写一个hidden的input,第一次ajax的值val到input中,然后第二次取到值再去和input中的值比较。虽然给出了提示,但是存在第一次的值还没写入input第二次取到null和第二次值对比才给出提示的情况,总之逻辑上说不过去,所以不对。

■  跨域AJAX请求

  上面说了那么多ajax相关的内容,其实有个隐含的大前提条件没有说明,那就是ajax只能请求和本javascript文件所在域名统一各域名下的内容。

  这里的域名不是广义上xxx.com那种意思,而是指location.protocol,location.host这些变量一致,更明白地说,就是协议一样,IP/域名一样,端口也一样的才叫同一域下。通常一个域包括的内容也就是一个服务可以给出的内容。

  在目前看来这还是理所当然的,但是有时候ajax难免会遇到需要请求其他服务的情况,此时就涉及到了ajax的跨域访问。

  ●  通过本服务做一个中转

  这个解决跨域访问的方案是最为朴素的,既然ajax无法直接请求其他服务,如果本服务所在的服务器可以请求到那个服务的话,那么只要把本服务作为一个中转,传递ajax和第三方服务之间的请求和应答就可以了。

  更进一步的,可以将这种请求第三方服务的模式统一化,把url和各个参数也参数化,从而实现一劳永逸地中转服务的包装。当然了,这个是要建立在本服务所在服务器可以访问第三方服务的情况下。如果不行就只能另寻他路。

  ●  通过script的方式进行第三方服务的访问

  其实细心一点就可以注意到,一个HTML页面并不是被禁止访问第三方资源的。比如我们使用CDN引入CSS和JS文件的时候,用的就是来自第三方的资源。很自然的,我们就会想到,可不可以在当前页面中引入一个来自第三方的JS,然后通过这个JS的桥梁,可以获取到第三方服务的数据。

  网上有很多相关的示例。由于按照这种方式来获取数据的话第三方服务本身需要经过定制,示例大多都是用java写的第三方服务后台代码,我下面就用flask来写。通过两个不同端口的服务模拟一个是本服务,一个是第三方服务,都由flask构成。前端代码也写在下面,使用到jQuery。

  首先是本服务:

@app.route('/',methods=['GET'])
def index():
return render_template('index.html') if __name__ == '__main__':
app.run(port=5001)

  然后再来看看前端(index.html)中是什么样的:

<!-- 我习惯将所有前端用到的JS都append在body的末尾,所以head里面的东西就不写了 -->
<body>
<button>GET DATA</button>
<textarea id="content"></textarea>
<script src="{{ url_for('static',filename='jquery.min.js') }}"></script>
<script>
function showData(res){
var data = JSON.stringify(res);
$('#content').val(data);
}
$(document).ready(function(){
$('button').click(function(event){
          event.preventDefeault();
$('body').append('<script src="http://localhost:5009/outspan?callback=showData"><\/script>'); // #1
});
});
</script>
</body>

  注意#1行处是整个前端代码的核心所在。它向本文档中增加了一个script标签。由于script标签可以进行跨域的引用,所以src属性指向的就是第三方服务,也就是下面我们要搭建的那个提供数据的服务。src的URL中带有GET参数callback,这个主要依赖于第三方服务的实现,下面编写第三方服务的时候具体再说。然后这一行还有一个小点值得注意的是script的后标签</script>中,斜杠要进行一个反转义。这是因为这些代码本身就是写在<script>标签中的,这里突然冒出来一个</script>,浏览器会以为外面的script标签在这里完结了…

  显然,在这段前端代码中被append到body末尾的一个新script标签,指向的是一个第三方服务。那么第三方服务长什么样呢:

import flask
import json # ... @app.route('/outspan',methods=['GET'])
def outspanData():
callbackFunc = flask.request.args.get('callback')
# 这里我们需要搞出一些数据来。假装我们从后台数据库取出了一些数据然后封装成JSON格式吧
res = json.dumps({'name':'Frank','age':12,'phone': ''})
return callbackFunc + '(' + res + ')' if __name__ == '__main__':
app.run(port=5009)

  来看下这段构建了第三方服务的代码。它是一个运行在5009端口上的服务。然后它并不是单纯地返回了一个JSON格式的字符串,而是在结果字符串外面加上了callback指定的函数名 + 括号对。也就是说他返回的这个东西如果直接让JS的解释器去解释的话就是将返回结果作为参数,调用了callback函数名指定的那个函数了。

  总体来说,当我们在本服务的index界面上点击按钮,textarea文本框中就会充实进{'name':'Frank'...}这个JSON格式的字符串。

  原理在上面讲述的过程中基本也解释清楚了,就是将远程的返回结果作为script标签内容加入HTML。此时JS解释器会试图解释新加入的这些JS内容,由于这个JS内容本身就是一个执行函数的过程,而这个函数又已经在HTML中预定义了,那么很自然地就在本服务的本页面得到了第三方服务的数据。此外,由于showData函数作为一个回调函数在本页面中进行定义,所以整体也是十分方便的。

  虽然实现了功能,但是怎么看都觉得不太对。比如这个例子中每点击一次按钮,页面HTML中就会增加一个script标签,这显然是不合理的。所以我们还有更好的办法来解决这个问题。

  ●  jsonp方式进行ajax请求

  由于上述的这样一个 添加script标签模拟请求 - 获取到第三方数据 - 回调函数处理数据  的模式比较类似统一。因此,自然就有人想到能不能将这整个过程封装起来,让调用接口的人感觉更好用。简单的,能不能把这种跨域方式也包装成和标准的ajax方法一样,让ajax这样一种操作更符合我们直觉上的感受,即跨域访问也是轻轻松松的。

  我们保持第三方服务的后端代码不变,重写本服务的前端页面:

<body>
<script src="{{ url_for('static',filename='jquery.min.js') }}"></script>
<script>
$(document).ready(function(){
$('button').click(function(event){
$.ajax({
url: 'http://localhost:5009/outspan' // 这次不用手动写callback参数了
type: 'get',
dataType: 'jsonp',
success: function(data){
$('#content').val(JSON.stringify(data)); // 原本定义在外面的函数现在直接作为success的参数传递给ajax方法
},
error: function(xml,err,exc){
alert(xmll.responseText);
}
});
});
});
</script>
</body>

  和普通的ajax方法相比,无非我指出的是dataType是jsonp,另外url是一个外部的链接,其他都一样。这段代码起的作用和未改写之前是一样的。

  顺便说下,success和之前单独定义的showData方法中都用了JSON.stringify方法主要是因为我们定义的第三方服务时,参数是一个json.dumps方法的返回值。这个返回值和callback函数名以及一对小括号合并起来之后,变成了这样一个字符串:'callback({"name":"Frank",...})',可以看到,如果按照JS的语法规则解释,括号当中参数是一个object而非字符串。因此需要stringify一下将object转化为字符串,否则被写入文本框的内容将会是[object Object]。

  至此,就是通过jsonp的方式进行跨域ajax请求的方法了。这种方法的原理其实和上面自己实现的是一样的(看下网络请求和应答的情况就知道),jquery为请求的第三方URL加上了额外的callback=xxx,xxx是jQuery自动生成的一个回调函数名。这个函数名和success参数传递进来的函数对象关联之后,那么就和自己实现的是一样一样的了。

  ajax中还可以加入jsonp参数来指定那个第三方服务要求的GET参数名,默认值当然就是'callback',同时还会有jsonpCallback手动设置回调函数名而不是默认使用success指定的函数。

  

  ●  jsonp的错误处理

  上面代码中虽然写了error参数,似乎指定了错误处理函数。对于jsonp跨域请求,错误并不会被error参数指定的函数处理。从ajax的官方文档明确指出,error参数指定的函数并不会处理jsonp等跨域请求引起的错误。这主要是因为虽然看似jsonp是个ajax请求,但是其原理和ajax完全没有关系,而是采用了script标签的跨域引用特性来实现的。那么对于第三方服务出现了错误,比如返回了400之后,jsonp是怎么表现的呢?在浏览器的调试器中我们可以看到,网络方面,本地确实收到了400的HTTP Response,控制台里还会提示加载script标签失败。但是就是没有信息输出到页面上来。

  要进行jsonp的错误处理也不麻烦,有两种方案可以选择:

  1. 为jsonp所在的ajax方法添加timeout参数

  timeout参数的原理是在一定时间后检查本请求是否200成功。虽然jsonp的400应答不会触发error,但是如果设置了timeout,在超过超时时间之后ajax会触发超时错误。这个超时错误不失为一种曲线救国的方法。

  但是timeout参数的这种方法弊端也很明显。比如一定要等到多少秒之后才会有错误的返回,而不是立刻。

  2. jQuery的JSONP插件

  https://github.com/jaubourg/jquery-jsonp

  这个项目就是一个jquery的jsonp插件。这个插件的目的在于将原先原生的$.ajax换成了$.jsonp,参数和ajax风格类似。支持对于jsonp出现错误的时候进行实时的处理。

  下面是一段使用jsonp插件的示例代码:

<body>

<script src="jquery.min.js"></script>
<script src="jquery.jsonp.js"></script>
<script>
$(document).ready(function(){
$('button').click(function(event){
event.preventDefault();
x_options = $.jsonp({
url: 'http://localhost:5009/outspan',
callbackParameter: 'callback',
success: function(data){
$('#content').val(JSON.stringify(data));
},
error: function(xOptions,statusText){
console.log(xOptions);
console.log(statusText);
alert('error');
}
});
});
});
</script>
</body>

  在.jsonp方法中,常用的参数就是这四个,与ajax相比去掉了type(默认是get),但是一定要加上callbackParameter,这个参数好比ajax中的jsonp参数,用来指定请求URL中指定回调函数的那个参数名的。

  success差别不大,error函数的参数中,statusText可能是'error'(访问出错)或者'timeout'(访问超时,jsonp方法也可以设置timeout参数),xOptions这个参数其实要回到最前面看,.jsonp方法调用之后如果调用成功,那么会返回一个xOptions对象。这个对象其实是本jsonp方法发起访问时完整的一个参数字典。它当然包含了你在jsonp方法中手动指定的那些参数值,更加包括了发起方法时一些默认的参数值。如果想要知道jsonp方法的参数大全,那么其实可以观察这个对象。当然看文档也是很吼的。(https://github.com/jaubourg/jquery-jsonp/blob/master/doc/API.md)。再回到error的参数里面来,相当于执行出错的时候,error方法里也可以对这次请求的参数做一个读取。