el's blog

A noder, FEer. use express, vue...etc

js跨域的几种方式
ajax跨域方法有很多种。常用的有jsonp请求,xhr2,后台代理方式,基于iframe实现跨域。

jsonp请求

ajax 本身是不可以跨域的,通过产生一个 script 标签来实现跨域。因为 script 标签的 src 属性是没有跨域的限制的。
jquery 其实设置了 dataType: ‘jsonp’ 后,$.ajax 方法就和 ajax XmlHttpRequest 没什么关系了,取而代之的则是 JSONP 协议。
JSONP 是一个非官方的协议,它允许在服务器端集成 Script tags 返回至客户端,通过 javascript callback 的形式实现跨域访问。

jQuery并没有用其它新奇的技术,只不是对 <script src=""/> 做了个封装,以实现jsonp跨域的方式。

我们封装一层jsonp的实现,废话不说,上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var jsonp = function (opts) {
//to produce random string
var generateRandomAlphaNum = function (len) {
var rdmString = '';
for (; rdmString.length < len; rdmString += Math.random().toString(36).substr(2));
return rdmString.substr(0, len);
}
var url = typeof opts === 'string' ? opts : opts.url,
callbackName = opts.callbackName || 'jsonpCallback' + generateRandomAlphaNum(10),
callbackFn = opts.callbackFn || function () {};
if (url.indexOf('callback') === -1) {
url += url.indexOf('?') === -1 ? '?callback=' + callbackName :
'&callback=' + callbackName;
}
if (typeof opts === 'object') {
var params = (function(obj){
var str = '';
for(var prop in obj){
str += prop + '=' + obj[prop] + '&'
}
str = str.slice(0, str.length - 1);
return str;
})(opts.data);
url += '&' + params;
}
var eleScript= document.createElement('script');
eleScript.type = 'text/javascript';
eleScript.id = 'jsonp';
eleScript.src = url;
document.getElementsByTagName('HEAD')[0].appendChild(eleScript);
// window[callbackName] = callbackFn;
//return promise
return new Promise(function (resolve, reject) {
window[callbackName] = function (json) {
resolve(json);
}
//onload are executed just after the sync request is comple,
//please use 'onreadystatechange' if need support IE9-
eleScript.onload = function () {
//delete the script element when a request done。
document.getElementById('jsonp').outerHTML = '';
eleScript = null;
};
eleScript.onerror = function () {
document.getElementById('jsonp').outerHTML = '';
eleScript = null;
reject('error');
}
});
};
jsonp({
url: 'http://erp.souche.com/pc/car/carpricetagaction/carPriceInfo.jsonp', /*url写异域的请求地址*/
data: {
carId: '0a499720d77f4e55a0ac490ed115fc4e',
picNum: 5
}
})
.then(function (d) {
console.log(d.data);
});

jsonp的缺点:只支持GET请求,也容易被运营商劫持插入奇怪的广告。

jsonp的优点:能支持老的浏览器。

xhr2

XHR2在众多HTML5新特性中算是比较低调的一个了。我翻了几本HTML5的书,只有《HTML5高级程序设计》弱弱地讲了几页。网上的资料也不多,很大的一个原因我估计是目前有浏览器还压根不兼容,看看下面的兼容列表:

  • Chrome 7.0及以上
  • Firefox 3.5以上
  • Internet Explorer 10及以上
  • Opera 12.1及以上
  • Safari 5.0及以上
  • android broswer 3及以上
  • ios safari 5.1及以上

XHR2这么叫多少有点误导的感觉,实际上没有什么XHR1,XHR2这样的概念,XHR2只是一套新的规范,在原有XHR对象上新增了一些功能:跨域访问,全新的事件,还有请求进度以及响应进度。 所以呢,要实现XHR2特性还是使用XMLHttpRequest对象,前提是浏览器要支持XHR2:
http发现这是一个跨源的ajax请求后就自动在头信息添加一个origin字段,服务器根据这个字段判断是否同意这次请求,如果同意这次请求则在响应头中插入字段 Access-Control-Allow-Origin

这是常见的一个 http response header 里关于 xhr2 的设置

1
2
3
4
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, TT, _security_token
Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS, DELETE
Access-Control-Allow-Origin: http://xxx.domain.com

请注意,如果需要请求中带上 cookie 则把 Access-Control-Allow-Credentials 设置为 true。但是如果 Access-Control-Allow-Origin 为 * 时,这么设置将会报错。

优点是支持所有类型的http请求

缺点是 Access-Control-Allow-Origin 这个头所跨的域本人实践写多个域名无效,那么就只能跨一个网站。

后台代理方式

这种方式可以解决所有跨域问题,也就是将后台作为代理,每次对其它域的请求转交给本域的后台,本域的后台通过模拟http请求去访问其它域,再将返回的结果返回给前台。

优点是无论访问的是文档,还是js文件都可以实现跨域。

缺点为每一个请求都需要一个后端去写转发的支持。

iframe跨域

对于主域相同而子域不同的例子,可以通过设置document.domain的办法来解决。具体的做法是可以在 http://www.a.com/a.htmlhttp://script.a.com/b.html 两个文件中分别加上 document.domain = 'a.com';然后通过 a.html 文件中创建一个 iframe ,去控制 iframe 的 contentDocument,这样两个js文件之间就可以“交互”了。当然这种办法只能解决主域相同而二级域名不同的情况,如果你异想天开的把script.a.com的domian设为alibaba.com那显然是会报错地!代码如下:

www.a.com 上的 a.html

1
2
3
4
5
6
7
8
9
10
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

1
document.domain = 'a.com';

问题:

  • 安全性,当一个站点(b.a.com)被攻击后,另一个站点(c.a.com)会引起安全漏洞。
  • 如果一个页面中引入多个iframe,要想能够操作所有iframe,必须都得设置相同domain。

综述

实际开发过程中,我的做法是本地用后台代理方式,也就是node中转一层去获取数据,关于这一点比方说 webpack 的配置项 proxyTable 就提供了这样的功能。下面提供了一段简单的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var devConf = {
env: 'dev',
globalConfig: {
NODE_ENV: JSON.stringify("development"),
TEST: JSON.stringify("/api/get")
},
proxyTable: {
'/api/get': {
target: 'http://test.sqaproxy.xx.com',
changeOrigin: true,
pathRewrite: {
'/api/get': ''
}
}
}
};

那么具体文件中访问 globalConfig 中的 TEST 变量就是代理后的地址

而到了预发和线上时,自然有后端返回的 Access-Control-Allow-Origin 头来做 xhr2 跨域。

参考