对于跨域的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的赋值,但是这样做也是为了前端更加友好地处理回调数据。