对于跨域的GET请求,我们最常用的是jsonp的方式,jQuery的ajax方法也对jsonp也有很好的封装,我们甚至可以利用http.getJSONP(url, data, callback)这样简洁的方式让开发人员只关注请求的url,数据以及回调方法。但是如果传输的数据量比较大,或者数据信息比较敏感的话,则需要POST大神出手了。那么跨域的post请求是否也能做到如此优雅地调用方式呢?
现在假设在a.com有个login.html页面,我们要用户的用户名密码提交到b.com/post接口做校验,校验结果返回a.com/login.html做提示。我们也来打造这样一个方法:
/* 跨域post请求方法
* url 请求地址
* data post表单数据
* callback 回调方法
*/
http.postJSONP = function(url, data, callback) {
//TO DO
}
//请求方式
http.postJSONP('b.com/post', {
name: "garygao",
password: "******"
}, function(json) {
//TO DO json
});
//或者像jsonp一样
http.postJSONP('b.com/post?postcallback=xxoo', {
name: "garygao",
password: "******"
});
//postcallback回调方法
function xxoo(json) {
//TO DO json
}
要实现这样一个方法,我们有两个难点:
1、如何实现跨域的post请求?
2、如何捕获回调数据,优雅地执行?
如何实现跨域POST请求?
1、CORS
利用CORS,让目标服务器设置标头"Access-Control-Allow-Origin:xxx.com ",表示允许设定的域向我们的服务端提交请求。
优点:W3C标准,配置简单;
缺点:需要服务端配合,IE67不支持。
2、Server Proxy
当前域实现一个代理,所有向外部域名发送的请求都径由该代理中转。
缺点:每个使用方都需要部署代理,数据中转低效,对js有侵入。
3、Flash Proxy
服务端部署跨域策略文件crossdomain.xml,页面利用不可见的swf跨域post提交数据实现跨域通信。
优点:兼容性好,传输数据量大;
缺点:依赖flash。
4、Invisible Iframe
概述:通过js动态生成不可见表单和iframe,将表单的target设为iframe的name以此通过iframe做post提交。
优点:兼容性佳,原生JS即可完全实现;
缺点:无法直接读取响应内容。
综合对比四种方式,还是第四种比较给力,兼容性好,不需要做服务器配置也不依赖Flash文件;
/* 跨域post请求方法
* url 请求地址
* data post表单数据
* callback 回调方法
*/
http.postJSONP = function(url, data, callback) {
var form = document.createElement("form");
form.id = form.name = 'postForm';
//创建表单数据
if (data) {
for(var key in data) {
var input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = data[key];
form.appendChild(input);
}
}
//创建iframe
var iframe = null;
//try&catch是为了解决IE67创建iframe新开窗问题
try {
iframe = document.createElement('<iframe name="postIframe">');
} catch (ex) {
iframe = document.createElement('iframe');
}
iframe.id = iframe.name = "postIframe";
iframe.width = "1";
iframe.height = "1";
iframe.style.display = "none";
document.body.appendChild(iframe);
//表单提交
document.body.appendChild(form);
form.action = url;
form.target = iframe.name;
form.method = "post";
form.submit();
}
看上面的代码,隐藏Iframe+form实现的POST原理其实很简单,让form的target指向iframe,这样表单提交的时候只刷新iframe的body,这样就像Ajax一样实现了页面无刷新请求。至此,我们就利用隐藏iframe和form可以跨域POST的特性实现了跨域POST请求。但是我们如何处理数据回调呢?
如何处理通过Iframe+form实现的POST请求数据回调?
我们借助iframe,POST请求之后的response也都在iframe内部,我们如何获取iframe内部返回的数据对象?这个我们可以借助window.name,关于window.name如何解决跨域的办法可以参考一下这篇文章http://www.planabc.net/2008/09/01/window_name_transport。文中我得知,name 在浏览器环境中是window对象的一个属性,且当在frame中加载新页面(也可以是跨域页面)时,name 的属性值依旧保持不变,并且name可以保存2M的数据量。
实现思路:将后端返回的数据写入window.name,然后将iframe的location刷新为源域的一个空白页面,这样iframe就跟我们页面同域了,因为window.name在iframe中加载新页面数据不会丢失,所以就可以通过iframe.contentWindow.name获取到POST接口返回的数据。
直接上代码:
http.postJSONP = function(url, data, fn) {
var form = document.createElement("form");
form.id = form.name = 'postForm';
//创建表单数据
if (data) {
for(var key in data) {
var input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = data[key];
form.appendChild(input);
}
}
//创建iframe
var iframe = null;
//try&catch是为了解决IE67创建iframe新开窗问题
try {
iframe = document.createElement('<iframe name="postIframe">');
} catch (ex) {
iframe = document.createElement('iframe');
}
iframe.id = iframe.name = "postIframe";
iframe.width = "1";
iframe.height = "1";
iframe.style.display = "none";
document.body.appendChild(iframe);
//表单提交
document.body.appendChild(form);
form.action = url;
form.target = iframe.name;
form.method = "post";
form.submit();
//事件处理
if(iframe.attachEvent){
iframe.attachEvent("onload", _loadFn);
}else{
iframe.onload = _loadFn;
}
//记录iframe的加载状态
iframe.state = 0;
function _loadFn() {
if (iframe.state === 1) {
var data = '';
//获取window.name保存的数据
try{
data = iframe.contentWindow.name;
}catch(e){
console.log(e);
}
var json = data;
try {
json = Kg.JSON.parse(data);
} catch(e){}
//执行回调方法
fn && fn(json);
//iframe清除
iframe.onload = null;
document.body.removeChild(iframe);
} else if (iframe.state === 0) {
//form提交完成之后,将location置为同域
state = 1;
//proxy.html只是一个源域里的一个空白页面,
//如果不考虑IE,也可以这样:iframe.contentWindow.location = "about:blank";
iframe.contentWindow.location = "/static/html/proxy.html";
}
}
return false;
}
为了在接口回调中给iframe中的window.name赋值,后端的返回需要这样写:
return "<script>window.name=\"".addslashes(json_encode($result))."\"</script>";
<script>是为了给window.name的赋值提供script执行环境。
OK,现在我们的请求方式可以是这样了:
//post请求
http.postJSONP('b.com/post', {
name: "garygao",
password: "******"
}, function(json) {
//TO DO json
});
为了更加贴近jsonp的请求的方式,我们还需要解析请求url拿到postcallback的方法:
http.postJSONP = function(url, data, fn) {
var form = document.createElement("form");
form.id = form.name = 'postForm';
//创建表单数据
if (data) {
for(var key in data) {
var input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = data[key];
form.appendChild(input);
}
}
//创建iframe
var iframe = null;
//try&catch是为了解决IE67创建iframe新开窗问题
try {
iframe = document.createElement('<iframe name="postIframe">');
} catch (ex) {
iframe = document.createElement('iframe');
}
iframe.id = iframe.name = "postIframe";
iframe.width = "1";
iframe.height = "1";
iframe.style.display = "none";
document.body.appendChild(iframe);
//表单提交
document.body.appendChild(form);
form.action = url;
form.target = iframe.name;
form.method = "post";
form.submit();
//事件处理
if(iframe.attachEvent){
iframe.attachEvent("onload", _loadFn);
}else{
iframe.onload = _loadFn;
}
//记录iframe的加载状态
iframe.state = 0;
function _loadFn() {
if (iframe.state === 1) {
var data = '';
//获取window.name保存的数据
try{
data = iframe.contentWindow.name;
}catch(e){
console.log(e);
}
var json = data;
try {
json = Kg.JSON.parse(data);
} catch(e){}
//执行回调方法
_callback(json);
//iframe清除
iframe.onload = null;
document.body.removeChild(iframe);
} else if (iframe.state === 0) {
//form提交完成之后,将location置为同域
state = 1;
iframe.removeAttribute('name');//解决IE10+获取不到window.name的问题
iframe.contentWindow.location = "/static/html/blank.html";
}
}
function _callback(json) {
//默认执行传入的回调方法
if (fn && typeof fn === "function") {
fn(json);
} else {
//没有回调方法则解析postcallback
var svalue = url.match(new RegExp("[\?\&]postcallback=([^\&]*)(\&?)"));
fn = window[svalue ? svalue[1] : svalue];
if (fn && typeof fn === "function") {
fn(json);
}
}
}
return false;
}
大功告成,现在我们可以像jsonpcallback那样通过传入postcallback的方式使用方法啦:
http.postJSONP('b.com/post?postcallback=xxoo', {
name: "garygao",
password: "******"
});
//postcallback回调方法
function xxoo(json) {
//TO DO json
}
等等!在测试IE10+的时候,发现iframe.contentWindow.name的值竟然等于postIframe,如此一来就获取不到真实的数据结果了,悲剧~通过调试发现,iframe中如果设置了name属性,iframe.contentWindow.name拿到的值就等于iframe.name的值,所以在获取name值的时候,要把iframe的那么属性干掉,iframe.removeAttribute('name')这样就OK啦~
小结
文中提到跨域方案无非就是利用了两种比较经典的解决方式,即隐藏iframe+form和window.name,相对于其他方式,文中的方案算是兼容性比较强的方式了,也无需修改服务器配置和引入额外的Flash文件,唯一不太友好的地方是后端的返回方式比较奇怪,要用<script>包住window.name的赋值,但是这样做也是为了前端更加友好地处理回调数据。