2018年3月1号更,之前的文章是17年5月写的,通过近一年的工作,对前端跨域有了更深刻的认识。
首先,什么是跨域?
只有协议
域名
及 端口
三个都相同才算作同源,其他的都属于跨域请求。
比如:
http://www.baidu.com:8080
http://www.baidu.com:1000 // 端口不同
https://www.baidu.com:8080 // 协议不同
http://www.qq.com // 域名不同
通常浏览器会给出这样的错误提示:
如何解决跨域?
通常的解决方案是:
- jsonp
- CORS
- nginx代理
- domain
- postMessage
下面逐个介绍这些方案:
Jsonp
特点:仅支持 GET 请求,需要后台也做处理。
jsonp(json with padding)顾名思义就是把data进行一层包装,利用script可以外链的特点来实现跨域请求。
在js中,link,a,script可以使用外链,不受同源策略限制。
jsonp用原生js实现起来,比较冗余。因为你得创建元素,插入元素。
所以通常的方法是使用jquery。
function originJsonp (url, opts, fn) {
let prefix = opts.prefix || '__jp'
let id = opts.name || (prefix + '0')
let param = opts.param || 'callback'
let target = document.getElementsByTagName('script')[0] || document.head
let script
let timer
// 清除script 及 定时器
function clear () {
if (script.parentNode) {
script.parentNode.removeChild(script)
}
window[id] = {}
timer && clearTimeout(timer)
}
window[id] = function (data) {
clear()
fn && fn(data)
}
// 不用判断 > -1 因为~-1 === 0
url += (~url.indexOf('?') ? '&' : '?') + param + '=' + encodeURIComponent(id);
url = url.replace('?&', '?');
script = document.createElement('script')
script.src = url
target.parentNode.insertBefore(script, target)
}
// 把回调函数promise化
function jsonp (url, opts) {
return new Promise((resolve, reject) => {
originJsonp(url, opts, (data) => {
resolve(data)
})
})
}
通过创建script标签,然后在window上定义一个callback函数,当脚本请求回来之后,会形如 callback({username: 'SpawN'})
,所以就会调用你在window上定义的callback函数,从而获取数据。
从上面的例子中可以看出,jsonp
和 ajax
没有联系,但是jQuery中为了实现方法的内聚性,把jsonp
也封装在了 ajax
中
ajax是XHR对象发起request。
jsonp是添加script标签
$.ajax({
type: 'get', // jquery只支持是get跨域哦~
url: '你请求的地址的url',
data: {
这里存入你的数据
},
dataType: 'jsonp', // 希望服务器返回的数据类型
jsonp: callback, // 默认是callback,这个是在request是带着的
jsonpCallback: 'callback' // 默认是jquery自动生成的,很长,可以自己命名一个,比较容易管理,这个是从服务器返回过来的用来包裹json的。 callback({"username":"jack"})
})
使用 jsonp
的时候通常会遇到的问题:
//报错提示 Unexpected token :
我之前面试的时候,让请求一个接口,当时对于跨域问题的研究还非常浅,就直接用jquery
的 jsonp
去请求,结果报错。最后才知道是因为接口不支持jsonp。
其实上面介绍 jsonp
用法的时候已经知道,后台返回来的是由函数包裹的json,那么自然是需要后台去进行处理支持。
那么后台如何支持jsonp呢?
就是这个jsonp: callback 这个属性。如果是callback,那么后台若是php代码的话,就需要在echo的外围包个callback(名字需要和jsonp设置的一致,但是基本上都是callback)
<?php
$arr = array ('username'=>'jack','age'=>21,'gender'=>'male');
echo $_GET['callback']."(".json_encode($arr).")";
?>
CORS
CORS (Cross origin resource shared)是HTML5出的规范,这个是需要在服务器端设置
Access-Control-Allow-Origin: http://xxx.xxx.com
后面意思是允许该源对我服务器发起的请求。其他的一概不响应。
也可以粗暴的设置为*,允许任何源对我服务器的请求。
浏览器会将CORS请求分为两类: 简单请求 和 非简单请求。
在进行接口访问的时候,浏览器会发送一个预请求来检测是否可以正常访问,当服务器正确响应之后,浏览器才会继续发送正确的请求。
当你的项目中是以 cookie
来做用户身份判断的时候,那你就不能把
Access-Control-Allow-Origin
设置为 *
,只能够明确指定域名
Nginx
Nginx是一个http和反向代理服务器。能实现跨域,就是利用了其反向代理的特性。所谓反向代理就是以代理服务器来实现跨域的访问。
如何使用呢?
在Nginx中找到conf/nginx.conf中:
server {
listen 80; //默认的端口号
server_name localhost; // 默认的域名
#charset koi8-r;
#access_log logs/host.access.log main;
location / { // 默认的配置
root html;
index index.html index.htm;
}
location /login { // 为了实现跨域的配置
rewrite .* /mall/member/login.htm break;
proxy_pass http://shangchengcmbs.fangshangqu.com;
}
}
/ login 是一个别名。 我们可以在HTML中请求的时候在url里只写/login就ok了
rewrite: 是对从proxy_pass过来的请求的拦截,并重写,其中/mall/member/login.htm 是添加到proxy_pass后面的。 另外break是对匹配第一个之后就停止匹配。
proxy_pass: 是把请求代理到的主机地址。
这些配置好了之后,打开cmd 输入
start nginx 就可以开启nginx服务器
nginx.exe -s quit 关闭服务器
nginx.exe -s reload 重启服务器
nginx -v 查看nginx版本号
最后打开localhost 就可以了~
常用
不管是 webpack
还是 gulp
,我们都会使用 http-proxy-middleware
中间件来处理跨域问题。包括我们熟知的 vue-cli
和 create-react-app
都是如此。
举个例子:
当我们的接口是:
https://m.mock.com/api/getPageData?id=2
https://m.mock.com/wechat/getOAuth
这种场景的配置如下:
proxy: {
'/apis': {
target: 'https://m.mock.com',
changeOrigin: true,
pathRewrite: {
'^/apis': ''
}
}
}
axios.defaults.baseURL = '/apis'
因为我们注意到接口里面并没有一个标志字符串,所以我们就自己创造一个 /apis
来进行跨域标志,然后再把其置为空字符串。
这个写法是所有 /apis
的请求都会被代理到 https://m.mock.com
下,而 changeOrigin: true
是更改 referer
,在跨域配置中是个必须项,然后再把所有 /apis
替换为 ''
而如果是这种场景:
https://m.mock.com/api/getPageData?id=2
https://m.mock.com/api/wechat/getOAuth
那我们可以看到,我们的所有接口都是 /api
作为前缀的,这也是符合接口规范的。
那这样配置就ok了
proxy: {
'/api': {
target: 'https://m.mock.com',
changeOrigin: true
}
}
axios.defaults.baseURL = '/api'