商会资讯

 找回密码
 立即注册

QQ登录

只需一步,快速开始

用新浪微博连接

一步搞定

搜索
热搜: 活动 交友 discuz

聊聊如何post请求跨域接口,并处理数据回调

已有 2512 次阅读2016-3-16 09:55 |个人分类:thinkphp| 接口, 如何

对于跨域的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.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
}

小结

文中提到跨域方案无非就是利用了两种比较经典的解决方式,即隐藏iframe+form和window.name,相对于其他方式,文中的方案算是兼容性 比较强的方式了,也无需修改服务器配置和引入额外的Flash文件,唯一不太友好的地方是后端的返回方式比较奇怪,要用<script>包住 window.name的赋值,但是这样做也是为了前端更加友好地处理回调数据。 关于跨域的好文推荐: http://www.cnblogs.com/rainman/archive/2011/02/20/1959325.html

收藏 分享邀请 转发到微博 举报

评论 (0 个评论)

facelist doodle 涂鸦板

您需要登录后才可以评论 登录 | 立即注册

回顶部