JS跨域问题
处理过WEB系统前端Ajax交互同事都会碰到一个问题,跨越问题,也就是说你从一个服务器请求数据,而页面并非来自这里。浏览器认为这是一个安全问题。
比如,如下代码,是我们经常写的Ajax请求数据代码,采用了JQuery框架。
function showAmount(){
$.ajax({
type: "GET",
url: "http://10.19.125.19:8080/sz/orderForm.do",
data:{mobileNumber:'<%=mobile%>'},
success: function(data){
for(var i=0;i<data.length;i++){
var amount = data[i].amount;
$('#amount').append("<b>"+amount+"</b>");
};
},
error: function(){
$('#amount').append("<b>无数据</b>");
}
});
}
这段程序在你的本地机器Html页面上运行是不会获取到任何数据,但是http://10.19.125.19:8080/sz/orderForm.do这个请求输入到浏览器中是可以返回数据的。同样的这个URL换成域名,比如www.suning.com,结果也是一样。这是为什么呢?如果是初次接触到,肯定会产生很大的疑惑。
其实这是浏览器确实对XMLHttpRequest HTTP请求有一些安全限制,导致了这些问题。这是浏览器策略,如果从某个域提供页面本身,安全策略要求不能从另一个域获取数据。
JS可以接受的代码行为:
1首先浏览器从域名suning.com做出页面请求
2页面通过XMlHttpRequest做出对suning.com请求数据
JS代码不能够接受的行为:
1首先浏览器从域名suning.com做出页面请求
2页面通过XMLHttpRequest对另外一个域taobao.com获取数据,显然不能获取数据
我们现在知道了问题,同时也知道了为什么产生这个问题,那么接下来我们需要去寻找方法解决问题,有哪些方法呢?想到了谷歌和度娘。
通过寻找有两种解决方法,第一;将页面托管到taobao.com服务器当中,比如taobao.com/suning.html,suning.html是我们放入js代码的页面。第二;JSONP方式。第三;重构Jquery的Ajax代码。
第一种方案,显示不怎么实用,通常也不会采用此方法解决跨域问题。
接下来就是重点关注后两种方案。
什么是JSONP呢?为什么要用JSONP?怎么使用JSONP呢?
认识JSONP
JSONP 是一种基于JSON的方法,称为JSONP,全称为:JSON with Padding。JSONP是一种使用<script>标记获取JSON对象的方法,这也是一种获取数据的方法,可以避免XMLHttpRequest的同源安全问题。
当我们在HTML页面当中,嵌入<script></script>代码,例如:
<html>
<body>
<script src=”http://10.19.125.19:8080/sz/orderForm.do”></script>
</body>
</html>
这个script的src是一个Web服务的URL,这个服务将为我们提供JSON对象。浏览器遇到页面上的<script></script>元素时,会向src指向的URL,发起HTTP请求。服务器会像对待其他的请求一样,处理并响应返回JSON,JSON的返回形式是字符串。再由浏览器解析和解释,任何数据类型都会转换成真正的JavaScript对象和值,另外所有的代码被执行。
流程如下:
JSONP最重要的是这个“P”,它可以理解成一个回调,当服务器返回JSON到浏览器,浏览器都会最终转换成JavaScript对象和值来解析和解释,并执行。那么如果服务器将JSON数据包裹到一个JavaScript函数内,作为一个函数参数返回到浏览器,只要页面存在同样的方法,那么浏览器就会去执行这个获取到JSON数据的函数,在函数当中,我们可以获取该JSON数据来处理,完成需要的业务。
流程如下:
实现JSONP
JSONP的基本流程了解之后,对于JSONP的实现也就能够得心应手了,目前对于JSONP的实现方式就是一般实现方式和Ajax实现方式。
一般实现方式
前端的JS代码:
function jsonpCallback(adJsonDatas) {
loadContent=romancecpc(adJsonDatas);
var adDivObj=document.getElementById("sn_"+100001199);
if(adDivObj!=null){
adDivObj.innerHTML=loadContent;
}
};
function addScriptTag(src){
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
前端Html代码:
<div class="navBar" id="sn_100001199"></div>
<script type="text/javascript">
function show(){
loadGoods("100001199");
}
function loadGoods(pid){
var kew = document.getElementById("keyword");
var val = kew.value;
var kw=to_utf8(val);
addScriptTag("http://10.19.250.51:8999/getCpcDatas?keyword="+kw+"&positionID="+pid);
}
</script>
后端代码Lua实现:
Local tem= "{".."\"tid\":\""..temObject[1].."\",".."\"sid\":\""..temObject[2]..
"\",".."\"pid\":\""..temObject[3].."\",".."\"adSrc\":\""..temObject[4].."\","..
"\"title\":\""..temObject[5].."\",".."\"clickUrl\":\""..temObject[6].."\","..
"\"cmdCode\":\""..temObject[7].."\",".."\"adType\":\""..temObject[8].."\","..
"\"apsClickUrl\":\""..apsClickUrl.."\",".."\"cmdPrice\":\""..temObject[9].."\"}"
ngx.say("jsonpCallback("..tem..")")
后端代码Java实现:
Local tem= "{".."\"tid\":\""..temObject[1].."\",".."\"sid\":\""..temObject[2]..
"\",".."\"pid\":\""..temObject[3].."\",".."\"adSrc\":\""..temObject[4].."\","..
"\"title\":\""..temObject[5].."\",".."\"clickUrl\":\""..temObject[6].."\","..
"\"cmdCode\":\""..temObject[7].."\",".."\"adType\":\""..temObject[8].."\","..
"\"apsClickUrl\":\""..apsClickUrl.."\",".."\"cmdPrice\":\""..temObject[9].."\"}"
response.getWriter().write(“jsonpCallback(‘+ tem+’)”);
分析:
在JS代码中,我们定义了一个jsonpCallback(adJsonDatas)函数,用于回调函数。同时通过函数addScriptTaga(src)动态的创建<script src=”” ></script>。前面提过,这个<script></script>会发起后端请求,获取后端的"jsonpCallback("..tem..")"数据。这个数据到达浏览器后,会作为Js代码来执行,执行的过程其实就是执行了JS代码中定义的jsonpCallback(adJsonDatas),同时参数为JSON格式。
在前端的HTML代码里面,就包含了一个DIV id= sn_100001199,完成向里面填写数据的功能。
在笔者的代码当中省略了callback=?这个请求参数,默认在后台直接确定了jsonpCallback()这个回调函数。
如果回调函数会经常发生变化,那么可以采用添加callback=jsonpCallback请求,后端获取callback的值,直接回写。
AJAX实现方式
前台(JQuery):
$.ajax({
url: this.localUrl,
type: "GET",
data: reDatas,
dataType: "jsonp",
cache:false,
jsonp:"jsonCallback",
jsonpCallback:"jsonpCallback",
success: function(datas,textStatus) {
if(textStatus=="success"){
if(null == datas || ""==datas){
//打印错误
AdManager.showError("get datas is null || \"\":"+datas);
return;
}
//渲染页面
AdManager.romanceAd(datas);
}
},
error:function(XMLHttpRequest, textStatus, errorThrown){
}
});
}
后端代码Lua实现:
local tem= "{".."\"tid\":\""..temObject[1].."\",".."\"sid\":\""..temObject[2]..
"\",".."\"pid\":\""..temObject[3].."\",".."\"adSrc\":\""..temObject[4].."\","..
"\"title\":\""..temObject[5].."\",".."\"clickUrl\":\""..temObject[6].."\","..
"\"cmdCode\":\""..temObject[7].."\",".."\"adType\":\""..temObject[8].."\","..
"\"apsClickUrl\":\""..apsClickUrl.."\",".."\"cmdPrice\":\""..temObject[9].."\"}"
ngx.say("jsonpCallback("..tem..")")
分析:
Ajax的方式是我们比较常用的方式,因为目前的WEB系统,大量的采用了异步刷新的功能。同样的为了能够实现异步获取数据的功能,JSONP也采用Ajax的方式。红线部分是非常需要注意dataType: "jsonp", jsonp:"jsonCallback", jsonpCallback:"jsonpCallback", dataType是必须填写项,jsonp,jsonpCallback是可选的。
笔者在运行代码的时候,开始并未添加jsonp:"jsonCallback", jsonpCallback:"jsonpCallback"遇到了如下问题:
parsererror 是解析器错误,jQuery150019…. was not called 是jQuery150019...此函数没被执行。那么为什么这个函数没被调用呢?
jQuery150019…. 这个函数名的来由, 此是在跨域ajax时自动产生的 jsoncallback=? 就是加一个这个,然后后台代码里也返回这个参数的值就行了。
开始以为是JSON对象的格式有问题,但是检查了返回结果
发现返回的数据其实是正确的JSON格式。
后来将添加上了jsonp:"jsonCallback", jsonpCallback:"jsonpCallback",再运行代码显示结果就正常了。
经过了这个异常问题的修改,笔者想到了看JQuery源码,因为其实虽然解决了这个问题,但是知道为什么?
JQuery JSONP源码分析
获取JQuery源码
方式一:
下载
方式二:
https://github.com/jquery/jquery
源码地址
源码的分析其实是一件比较痛苦的体验,很容易一头扎源码堆里面去,笔者选择了直接阅读JQuery Ajax的源码.但是前提条件必须要会使用JQuery。先搞清楚需要研究的部分所处的位置。
也可以参考博客
http://nuysoft.iteye.com/blog/1178483
JQuery的架构
图1
JQuery JSONP
从图1中,我们可以看出,JSONP所处的位置在异步请求Ajax模块中。可以直接的找到这块代码阅读。
代码地址【最新的代码】:
https://github.com/jquery/jquery/blob/master/src/ajax/jsonp.js
也可以下载的老的版本阅读,笔者下载了1.7.1的版本阅读。经过阅读总结出程序流程
1,初始化默认的jsonp,jsonpCallback
// Default jsonp settings
jQuery.ajaxSetup({
jsonp: "callback",
jsonpCallback: function() {
return jQuery.expando + "_" + ( jsc++ );
}
});
jsonpCallback的默认的名为:JQuery_时间搓。
2,利用Ajax的前置过滤器,完成JSONP类型的过滤
jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
…..
}
参数 s:代表$.ajax对象
参数 originalSettings:1.7.1当中未用,新的版本用到
参数 jqXHR:JQuery包装的XMLHttpRequest对象
2.1判断该Ajax请求是否为表单请求,并且data属性存在并且有值
var inspectData = s.contentType === "application/x-www-form-urlencoded" &&
( typeof s.data === "string" );
2.2通过三种方式判断该Ajax请求处理是否是JSONP的请求
if ( s.dataTypes[ 0 ] === "jsonp" ||
s.jsonp !== false && ( jsre.test( s.url ) ||
inspectData && jsre.test( s.data ) ) ) {
…
}
方式一:dataTypes的第一值为JSONP;
方式二:jsonp存在并且url里面包含=?& =?$
方式三:该请求是表单请求并且data里面包含=?& =?$
2.3在请求的url或者data中添加上JSONP的请求参数callback=?
var responseContainer,
jsonpCallback = s.jsonpCallback =
jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback,
previous = window[ jsonpCallback ],
url = s.url,
data = s.data,
replace = "$1" + jsonpCallback + "$2";
if ( s.jsonp !== false ) {
url = url.replace( jsre, replace );
if ( s.url === url ) {
if ( inspectData ) {
data = data.replace( jsre, replace );
}
if ( s.data === data ) {
// Add callback manually
url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback;
}
}
}
2.4注册回调函数
// Install callback
window[ jsonpCallback ] = function( response ) {
responseContainer = [ response ];
};
Window[jsonpCallback]相当于在window对象中创建了一个新的属性函数。
当请求结束后,执行回调函数。
// Clean-up function
jqXHR.always(function() {
// Set callback back to previous value
window[ jsonpCallback ] = previous;
// Call if it was a function and we have a response
if ( responseContainer && jQuery.isFunction( previous ) ) {
window[ jsonpCallback ]( responseContainer[ 0 ] );
}
});
2.5转换返回值类型为JSON对象,数据类型为“JSON”,分发器类型为“SCRIPT”
// Use data converter to retrieve json after script execution
s.converters["script json"] = function() {
if ( !responseContainer ) {
jQuery.error( jsonpCallback + " was not called" );
}
return responseContainer[ 0 ];
};
// force json dataType
s.dataTypes[ 0 ] = "json";
// Delegate to script
return "script";
3执行script 分发器
绑定script分发器,通过在header中创建script标签异步载入js。
3.1动态创建<script></script>
script = document.createElement( "script" );
//设置成了异步请求
script.async = "async";
if ( s.scriptCharset ) {
script.charset = s.scriptCharset;
}
script.src = s.url;
3.2异步请求执行回调函数
script.onload = script.onreadystatechange = function( _, isAbort ) {
if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
// Handle memory leak in IE
script.onload = script.onreadystatechange = null;
// Remove the script
if ( head && script.parentNode ) {
head.removeChild( script );
}
// Dereference the script
script = undefined;
// Callback if not abort
if ( !isAbort ) {
callback( 200, "success" );
}
}
};
分析:
综上,我们可以看出,其实JQuery的JSONP处理,很简单,首先,拼接请求参数?Callback=?
接着,动态注册一个回调函数。然后,动态创建一个<script></script>异步发送请求,完成后,执行回调函数。