深入理解Ajax结合JSONP实现跨域请求

时间:2022-08-29 08:50:17

一个众所周知的问题,Ajax直接请求普通文件存在跨域无权限访问的问题,甭管你请求的是什么东西,静态html,动态web资源,只要是跨域请求,一律不准;

写一个跨域无权限访问的DEMO:

本地js客户端代码:

//生成XMLHttpRequest
var xhr = new XMLHttpRequest();
//注册监听
xhr.onreadystatechange=function(){
if(xhr.readyState==4){
if(xhr.status==200||xhr.status==304){
//获取服务器的响应
var data = xhr.responseText;
alert(data);
}
}
};
//客户端与服务端建立连接
xhr.open("post","http://localhost:8080/HadoopClass/mainServlet",true);
//发生post设置请求头
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
//发送请求参数
xhr.send("username=虛竹&age=21");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

远程服务短代码:

@WebServlet("/mainServlet")
public class MainServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

/**
* @see HttpServlet#HttpServlet()
*/

public MainServlet() {
super();
// TODO Auto-generated constructor stub
}

/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
* response)
*/

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}

/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
* response)
*/

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String age = request.getParameter("age");
username = new String(username.getBytes("ISO-8859-1"),"utf8");
System.out.println(username);
System.out.println(age);
response.setContentType("text/html;charset=utf8");
response.setCharacterEncoding("utf8");
//向客户端生成一个响应
PrintWriter pw = response.getWriter();
pw.println("请求成功");
}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

如上就是一个再简单不过的Ajax请求了,如果服务端的代码和Ajax客户端在同一工程下则Ajax客户端接受到服务器的响应数据没问题。但当两者不在同一域名下,则Ajax客户端不能接受到服务端的响应。xhr.status永远是0,不会出现HTTP协议中的状态码,如200,304等。

现在我们来看什么是JSONP

JSON(JavaScript Object Notation)和JSONP(JSON with Padding)虽然只有一个字母的差别,但其实他们根本不是一回事儿:JSON是一种数据交换格式,而JSONP是一种依靠开发人员的聪明才智创造出的一种非官方跨域数据交互协议。

我们发现,Web页面上调用js文件时则不受是否跨域的影响(不仅如此,我们还发现凡是拥有”src”这个属性的标签都拥有跨域的能力,比如<script>、<img>、<iframe>);

于是可以判断,当前阶段如果想通过纯web端跨域访问数据就只有一种可能,那就是在远程服务器上设法把数据装进js格式的文件里,供客户端调用和进一步处理;

这样子解决方案就呼之欲出了,web客户端通过与调用脚本一模一样的方式,来调用跨域服务器上动态生成的js格式文件(一般以JSON为后缀),显而易见,服务器之所以要动态生成JSON文件,目的就在于把客户端需要的数据装入进去。

客户端在对JSON文件调用成功之后,也就获得了自己所需的数据,剩下的就是按照自己需求进行处理和展现了,这种获取远程数据的方式看起来非常像AJAX,但其实并不一样。

为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作JSONP,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

如上的执行过程听起来很模糊:我们写一个如上的实例的DEMO来完成。

远程服务端的代码:

@WebServlet("/mainServlet")
public class MainServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

/**
* @see HttpServlet#HttpServlet()
*/

public MainServlet() {
super();
// TODO Auto-generated constructor stub
}

/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
* response)
*/

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}

/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
* response)
*/

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 响应类型
response.setHeader("Access-Control-Allow-Methods","GET,POST");
// 响应头设置
response.setHeader("Access-Control-Allow-Headers","x-requested-with,content-type");*/
PrintWriter pw = response.getWriter();
pw.println("cb({result:6666666})");
}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

本地客户端的代码:

  <script type="text/javascript">
var cb = function(data){
alert('我是本地函数,可以被跨域的文件调用,远程js带来的数据是:' + data.result);
};
</script>
<script type="text/javascript" src="http://localhost:8080/HadoopClass/mainServlet"></script>
</head>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

运行之后发现,我们成功的获取到了远程服务器的响应。

跨域远程获取数据的目的基本实现了,但是又一个问题出现了,我怎么让远程服务器知道它应该调用的本地函数叫什么名字呢?毕竟是jsonp的服务者都要面对很多服务对象,而这些服务对象各自的本地函数都不相同啊?解决方法是我们要本地函数的名字发送给远程服务器,然后让远程服务器根据名字动态的构建JSON。

本地客户端代码:

<script type="text/javascript">
var showMessage = function(data){
alert('我是本地函数,可以被跨域的文件调用,远程js带来的数据是:' + data.result);
};
</script>
<script type="text/javascript" src="http://localhost:8080/HadoopClass/mainServlet?callback=showMessage">
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

远程服务端:

@WebServlet("/mainServlet")
public class MainServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

/**
* @see HttpServlet#HttpServlet()
*/

public MainServlet() {
super();
// TODO Auto-generated constructor stub
}

/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
* response)
*/

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}

/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
* response)
*/

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String callback = request.getParameter("callback");
PrintWriter pw = response.getWriter();
pw.println(callback+"({result:abc})");
}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

此时我们也不难看出,在使用JSONP来完成跨域请求时,GET比POST更方便 
如何你的请求是在监听某个操作后发生的,此时动态拼接一个<script>即可:

 // 提供jsonp服务的url地址(不管是什么类型的地址,最终生成的返回值都是一段javascript代码)
var url = "http://localhost:8080/HadoopClass/mainServlet?callback=showMessage";
// 创建script标签,设置其属性
var script = document.createElement('script');
script.setAttribute('src', url);
// 把script标签加入head,此时调用开始
document.getElementsByTagName('head')[0].appendChild(script);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

到这里为止的话,相信你已经能够理解jsonp的客户端实现原理了吧?剩下的就是如何把代码封装一下,以便于与用户界面交互,从而实现多次和重复调用。

_由于在开发中我们经常使用jQuery,而jQuery对JSONP进行了一步封装.使用起来更加简单

<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title>Untitled Page</title>
<script type="text/javascript" src=jquery.min.js"></script>
<script type="text/javascript">
$.ajax({
type:"get",
url:"http://localhost:8080/HadoopClass/mainServlet",/*url写异域的请求地址*/
dataType:"jsonp",/*加上datatype*/
jsonpCallback:"showMessage",/*设置一个回调函数,名字随便取,和下面的函数里的名字相同就行*/
success:function(data,textStatus){
alert(data.result);
alert(textStatus);
}
});
</script>
</head>
<body>
</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

这次我们没有写showMessage这个函数呢?而且竟然也运行成功了。这就是jQuery的功劳了,jquery在处理jsonp类型的ajax时,自动帮你生成回调函数并把数据取出来供success属性方法来调用。

最后铭记:JSONP不支持POST请求仅仅执行GET请求

通过如上的联系,相信你已经很容易理解JSONP的使用方法了。但JSONP不是解决跨域请求的唯一方法,最简单粗暴的方法就是让服务器支持跨域:

// 指定允许其他域名访问 
response.setHeader("Access-Control-Allow-Origin","*");
// 响应类型
response.setHeader("Access-Control-Allow-Methods","GET,POST");
// 响应头设置
response.setHeader("Access-Control-Allow-Headers","x-requested-with,content-type");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Access-Control-Allow-Origin:* 表示允许任何域名跨域访问

如果需要指定某域名才允许跨域访问,只需把Access-Control-Allow-Origin:*改为Access-Control-Allow-Origin:允许的域名

例如:header(‘Access-Control-Allow-Origin:http://www.client.com‘);

这样服务器就支持跨域请求了,不需要利用JSONP了。 
最后总结:

  • 1.ajax和jsonp这两种技术在调用方式上“看起来”很像,目的也一样,都是请求一个url,然后把服务器返回的数据进行处理,因此jquery都把jsonp作为ajax的一种形式进行了封装;
  • 但ajax和jsonp其实本质上是不同的东西。ajax的核心是通过XmlHttpRequest获取非本页内容,而jsonp的核心则是动态添加<script>标签来调用服务器提供的js脚本。
  • 所以说,其实ajax与jsonp的区别不在于是否跨域,ajax通过服务端代理一样可以实现跨域,jsonp本身也不排斥同域的数据的获取。
  • 还有就是,jsonp是一种方式或者说非强制性协议,如同ajax一样,它也不一定非要用json格式来传递数据,如果你愿意,字符串都行,只不过这样不利于用jsonp提供公开服务。

 总而言之,jsonp不是ajax的一个特例,哪怕jquery等巨头把jsonp封装进了ajax,也不能改变这一点!