JavaScript 跨域问题解决方案
写在开头的:
JSONP 是使用 JSON 格式 + 动态 script ,在客户端解决跨域访问问题的一种方案。
【最后详细解释了 JSONP 的实现过程】
跨域问题背景:
1. 跨域访问,简单来说就是 A 网站的 javascript 代码试图访问 B 网站,包括提交内容和获取内容;比如想从 A 网站的页面中执行另外一个 B 网站内某页面中的 JS 对象、或者想在 A 网站的页面中用 JS 去解析 B 网站内某页面的 dom 元素等;出现这种跨域访问问题的应用场景一般是 iframe 中嵌入不同域的页面、或者向不同域发送 Ajax 请求等;
2. 同源策略:由于安全原因,跨域访问是被各大浏览器所默认禁止的;但是浏览器并不禁止在页面中引用其他域的 JS 文件,并可以自由执行引入的 JS 文件中的 function (包括操作 cookie 、 Dom 等等)。根据这一点,可以方便地通过创建 script 节点的方法来实现完全跨域的通信;【至关重要! JSONP 的原理关键】
3. 是否跨域的判断规则为对三者进行比较:域名、协议、端口;三者中若有一个不相同,则会出现跨域问题;我们经常说的跨域问题一般指域名不同,因为这种场景出现的几率最高而且有一些办法可以解决;比如前面提到的 taobao.com 域下的二级域名跨域问题;
4. 如果是协议和端口造成的跨域问题,想从 Web 端来解决是完全不可能的,只能通过服务端 Proxy 的方案来解决;
但是有时候我们希望能够做一些合理的跨域访问的事情,那么怎么办呢?
理论依据:
依据一: 在浏览器中不能直接来跨域访问,而在服务器端没有跨域安全限制。这样的话,可以在服务端完成跨域访问,而在客户端来取得结果就可以了。
依据二: 同源策略不阻止动态脚本元素插入,脚本访问可以跨域。
Web 页面上调用 js 文件时则不受是否跨域的影响(不仅如此,凡是拥有 "src" 这个属性的标签都拥有跨域的能力,比如 <script> 、 <img> 、 <iframe> );
解决它们之间跨域的方案:
方案 1 : document.domain+ 隐藏的 iframe
注意: 对于主域相同而子域不同的例子,才可以通过设置 document.domain 的办法来解决。具体的做法是可以在 http://www.a.com/a.html 和 http://script.a.com/b.html 两个文件中分别加上 document.domain = ‘a.com’ ;然后通过 a.html 文件中创建一个 iframe ,去控制 iframe 的 contentDocument ,这样两个 js 文件之间就可以 “ 交互 ” 了。当然这种办法只能解决主域相同而二级域名不同的情况。
Example :
www.a.com 上的 a.html
document.domain = 'a.com';
var ifr = document.createElement('iframe');
ifr.src = 'http://script.a.com/b.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
var doc = ifr.contentDocument || ifr.contentWindow.document;
// 在这里操纵 b.html
alert(doc.getElementsByTagName("h1")[0].childNodes[0].nodeValue);
};
script.a.com 上的 b.html
document.domain = 'a.com';
方案 2 :利用 iframe+location.hash
这个办法比较绕,但是可以解决完全跨域情况下的脚步置换问题。
原理: 利用 location.hash 来进行传值。在 url : http://a.com#helloword 中的 ‘#helloworld’ 就是 location.hash ,改变 hash 并不会导致页面刷新,所以可以利用 hash 值来进行数据传递,当然数据容量是有限的。
思路: 假设域名 a.com 下的文件 cs1.html 要和 cnblogs.com 域名下的 cs2.html 传递信息, cs1.html 首先创建自动创建一个隐藏的 iframe , iframe 的 src 指向 cnblogs.com 域名下的 cs2.html 页面,这时的 hash 值可以做参数传递用。 cs2.html 响应请求后再将通过修改 cs1.html 的 hash 值来传递数据(由于两个页面不在同一个域下 IE 、 Chrome 不允许修改 parent.location.hash 的值,所以要借助于 a.com 域名下的一个代理 iframe ; Firefox 可以修改)。同时在 cs1.html 上加一个定时器,隔一段时间来判断 location.hash 的值有没有变化,一点有变化则获取获取 hash 值。
Example :
先是 a.com 下的文件 cs1.html 文件:
function startRequest(){
var ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = 'http://www.cnblogs.com/lab/cscript/cs2.html#paramdo';
document.body.appendChild(ifr);
}
function checkHash() {
try {
var data = location.hash ? location.hash.substring(1) : '';
if (console.log) {
console.log('Now the data is '+data);
}
} catch(e) {};
}
setInterval(checkHash, 2000);
cnblogs.com 域名下的 cs2.html:
// 模拟一个简单的参数处理操作
switch(location.hash){
case '#paramdo':
callBack();
break;
case '#paramset':
//do something……
break;
}
function callBack(){
try {
parent.location.hash = 'somedata';
} catch (e) {
// ie 、 chrome 的安全机制无法修改 parent.location.hash ,
// 所以要利用一个中间的 cnblogs 域下的代理 iframe
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://a.com/test/cscript/cs3.html#somedata'; // 注意该文件在 "a.com" 域下
document.body.appendChild(ifrproxy);
}
}
a.com 下的域名 cs3.html
// 因为 parent.parent 和自身属于同一个域,所以可以改变其 location.hash 的值
parent.parent.location.hash = self.location.hash.substring(1);
当然这样做也存在很多缺点,诸如数据直接暴露在了 url 中,数据容量和类型都有限等 ……
方案 3 :服务器 Proxy
域 A 的页面 JS 需要访问域 B 下的链接获取数据,该方案在域 A 的服务器端建立一个 Proxy 程序 ( 可能是 ASP 、 servlet 等任何服务端程序 ) ,域 A 的页面 JS 直接调用本域下的 Proxy 程序, proxy 程序负责将请求发送给域 B 下的链接并获取到数据,最后再通过 Proxy 将数据返回给页面 JS 使用。
经过的访问流程就是: 域 A 下 JS --> 域 A 下 Proxy -- > 域 B 下的链接。
方案 4 :通过 Script 标签 :
对于浏览器来说, script 标签的 src 属性所指向资源就跟 img 标签的 src 属性所指向的资源一样,都是一个静态资源,浏览器会在适当的时候自动去加载这些资源,而不会出现所谓的跨域问题。
这样我们就可以通过该属性将要访问的数据对象引用进当前页面而绕过 js 跨域问题。通过 script 标签远程引用其他域名下的脚本文件的。
而且, script 标签的 src 属性不一定必须是一个存在的 js 文件,也可以是一个 http handler 的 url ,只要这个 http handler 返回的是一个 text/javascript 类型的响应就可以了。
JSONP 就是基于这种原理产生的:
1、 JSON 是 js 原生支持的数据格式;
2、 客户端在对 JSON 文件调用成功之后,也就获得了自己所需的数据,剩下的就是按照自己需求进行处理和展现了,这种获取远程数据的方式看起来非常像 AJAX ;
3、 为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作 JSONP ,该协议的一个要点就是允许用户传递一个 callback 参数给服务端,然后服务端返回数据时会将这个 callback 参数作为函数名来包裹住 JSON 数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。
JSONP 的具体实现:
不管 jQuery 也好, extjs 也罢,又或者是其他支持 jsonp 的框架,他们幕后所做的工作都是一样的,下面我来循序渐进的说明一下 jsonp 在客户端的实现:
1 、我们知道,哪怕跨域 js 文件中的代码(当然指符合 web 脚本安全策略的), web 页面也是可以无条件执行的。
远程服务器 remoteserver.com 根目录下有个 remote.js 文件代码如下:
alert(' 我是远程文件 ');
本地服务器 localserver.com 下有个 jsonp.html 页面代码如下:
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml">
< head >
< title ></ title >
< script type ="text/javascript" src ="http://remoteserver.com/remote.js"></ script >
</ head >
< body >
</ body >
</ html >
页面将会弹出一个提示窗体,显示跨域调用成功。
2 、现在我们在 jsonp.html 页面定义一个函数,然后在远程 remote.js 中传入数据进行调用。
jsonp.html 页面代码如下:
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml">
< head >
< title ></ title >
< script type ="text/javascript">
var localHandler = function (data){
alert(' 我是本地函数,可以被跨域的 remote.js 文件调用,远程 js 带来的数据是: ' + data.result);
};
</ script >
< script type ="text/javascript" src ="http://remoteserver.com/remote.js"></ script >
</ head >
< body >
</ body >
</ html >
remote.js 文件代码如下:
localHandler({"result":" 我是远程 js 带来的数据 "});
运行之后查看结果,页面成功弹出提示窗口,显示本地函数被跨域的远程 js 调用成功,并且还接收到了远程 js 带来的数据。跨域远程获取数据的目的基本实现了,但是又一个问题出现了,我怎么让远程 js 知道它应该调用的本地函数叫什么名字呢?毕竟是 jsonp 的服务者都要面对很多服务对象,而这些服务对象各自的本地函数都不相同啊?
3 、聪明的开发者很容易想到,只要服务端提供的 js 脚本是动态生成的就行了呗,这样调用者可以传一个参数过去告诉服务端 “ 我想要一段调用 XXX 函数的 js 代码,请你返回给我 ” ,于是服务器就可以按照客户端的需求来生成 js 脚本并响应了。
看 jsonp.html 页面的代码:
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml">
< head >
< title ></ title >
< script type ="text/javascript">
// 得到航班信息查询结果后的回调函数
var flightHandler = function (data){
alert(' 你查询的航班结果是:票价 ' + data.price + ' 元, ' + ' 余票 ' + data.tickets + ' 张。 ');
};
// 提供 jsonp 服务的 url 地址(不管是什么类型的地址,最终生成的返回值都是一段 javascript 代码)
var url = "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998&callback=flightHandler";
// 创建 script 标签,设置其属性
var script = document.createElement('script');
script.setAttribute('src', url);
// 把 script 标签加入 head ,此时调用开始
document.getElementsByTagName('head')[0].appendChild(script);
</ script >
</ head >
< body >
</ body >
</ html >
不再直接把远程 js 文件写死,而是编码实现动态查询,而这正是 jsonp 客户端实现的核心部分,本例中的重点也就在于如何完成 jsonp 调用的全过程。
我们看到调用的 url 中传递了一个 code 参数,告诉服务器我要查的是 CA1998 次航班的信息,而 callback 参数则告诉服务器,我的本地回调函数叫做 flightHandler ,所以请把查询结果传入这个函数中进行调用。
OK ,服务器很聪明,这个叫做 flightResult.aspx 的页面生成了一段这样的代码提供给 jsonp.html (服务端的实现这里就不演示了,与你选用的语言无关,说到底就是拼接字符串):
flightHandler({
"code": "CA1998",
"price": 1780,
"tickets": 5
});
我们看到,传递给 flightHandler 函数的是一个 json ,它描述了航班的基本信息。运行一下页面,成功弹出提示窗口, jsonp 的执行全过程顺利完成!
4 、到这里为止的话,相信你已经能够理解 jsonp 的客户端实现原理了吧?剩下的就是如何把代码封装一下,以便于与用户界面交互,从而实现多次和重复调用。
jQuery 封装了 jsonp 调用代码(我们依然沿用上面那个航班信息查询的例子,假定返回 jsonp 结果不变):
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< 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">
jQuery(document).ready( function (){
$.ajax({
type: "get",
async: false ,
url: "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998",
dataType: "jsonp",
jsonp: "callback", // 传递给请求处理程序或页面的,用以获得 jsonp 回调函数名的参数名 ( 一般默认为 :callback)
jsonpCallback:"flightHandler", // 自定义的 jsonp 回调函数名称,默认为 jQuery 自动生成的随机函数名,也可以写 "?" , jQuery 会自动为你处理数据
success: function (json){
alert(' 您查询到航班信息:票价: ' + json.price + ' 元,余票: ' + json.tickets + ' 张。 ');
},
error: function (){
alert('fail');
}
});
});
</ script >
</ head >
< body >
</ body >
</ html >
是不是有点奇怪?为什么我这次没有写 flightHandler 这个函数呢?而且竟然也运行成功了!这就是 jQuery 的功劳了, jquery 在处理 jsonp 类型的 ajax 时(还是忍不住吐槽,虽然 jquery 也把 jsonp 归入了 ajax ,但其实它们真的不是一回事儿),自动帮你生成回调函数并把数据取出来供 success 属性方法来调用。