从跨域报错讲起
假如我有一个测试服务器,地址是 10.128.119.119,端口是默认的 80 端口,那么一个简单的接口的地址可以是这样的:http://10.128.119.119/api/get/username
那么这个接口可不可以被自由访问呢?
答案是可以的。你用 postman 在任何网络下都可以访问这个接口。
但是如果你通过浏览器在本地访问,比如在页面http://localhost:8000/usercenter.html
中调用这个接口
$.get('http://10.128.119.119/api/get/username',function(res){console.log(res)})
打开一看,报错了
No 'Access-Control-Allow-Origin' header is present on the requested resource.'
这是因为浏览器有一个安全策略,叫做同源策略。
什么是同源策略?
所谓同源,是指两个地址的"协议","域名","端口"这三者都相同。
只有当两个地址同源时,浏览器才允许它们之间进行数据通信,这就是同源策略。
同源策略(Same origin policy),最开始是由网景公司(Netscape)提出的安全策略并引入浏览器中,现在已经成为浏览器最基础的安全策略。它确保了应用的资源只能被应用自身访问。从而帮用户避免了大量来自第三方的恶意攻击,比如 XSS(跨站脚本攻击)、CSRF(跨站请求伪造) 等。
所以,当你看到上面的No 'Access-Control-Allow-Origin' header is present on the requested resource.
报错信息,其实是浏览器拦截了从后端返回的数据,不让当前的页面访问这些数据,从而一方面避免对本地数据造成污染,一方面也避免用户数据比如 cookie 信息、账号、密码等个人信息的泄漏风险。
开发环境下的同源策略
当然了,同源策略是好的,但偶尔也会对我们造成阻碍。
比如在软件的开发环境下,我们本地的服务一般是http://localhost
,这肯定和接口地址是不同源的呀,这时候同源策略貌似成了我们的阻碍。
针对这种情况,网上有非常多的解决办法,很多社区也有自己成熟的方案
比如,我们可以通过 nodeJS 搭建一个微服务参见,再利用 http-proxy-middleware 对 ajax 请求进行转发来实现跨域,或者利用 webpack 跨域代理配置参见也可以解决。
不过,本文今天想说的不是上面的,而是很早以前就出现的 jsonp。
jsonp 的前世今生
在 vue、react 出现之前,jsonp 可以说是大名鼎鼎了。jquery 中的 ajax 默认就添加了 jsonp 请求
$.ajax({url:"http://10.128.119.119/api/get/username",type:"GET",dataType:"jsonp",jsonpCallback:"callback"});functioncallback(res){}
你可能在之前就发现了,对于接口,不同源就报错,但是对于 img、js 这种文件资源,当你使用<img src="xxx" />
或者<script src="xxx"></script>
的时候,似乎同源策略并没有发生作用。
是的,html 元素的 src 属性是一个例外,src 属性不受到同源策略的限制,也正是基于此,才有了 jsonp,也因此,jsonp 只能支持 get 请求。
一个最最简单的 jsonp 请求如下:
http://digi.duodiangame.com/digMineral/interfaces/task/reportDailySignin.do?callback=cb_callback&user_id=AAA
当你把这个 url 放入浏览器地址栏里打开,你会看到这个 url 返回了一个函数的调用:cb_callback({"Error":"用户数据错误"});
如果当前页面恰好有个叫做 'cb_callback' 的函数,那么这个函数就会执行,参数就是后台真正的返回值。
varcb_callback=function(data){alert(data.Error)}document.write('<scriptsrc="http://digi.duodiangame.com/digMineral/interfaces/task/reportDailySignin.do?callback=cb_callback&user_id=AAA"></script>')
大家可以把这段代码复制到控制台看一下效果
以上就是 jsonp 能够获取数据的原理了。
封装 jsonp
我们封装一个简单的 jsonp 来解释 jsonp 应该如何来实现
functionjsonp(url,data,fn){if(!url)thrownewError('urlisnecessary')constcallback='CALLBACK'+Math.random().toString().substr(9,18)constJSONP=document.createElement('script')JSONP.setAttribute('type','text/javascript')constheadEle=document.getElementsByTagName('head')[0]letret='';if(data){if(typeofdata==='string')ret='&'+data;elseif(typeofdata==='object'){for(letkeyindata)ret+='&'+key+'='+encodeURIComponent(data[key]);}ret+='&_time='+Date.now();}JSONP.src=`${url}?callback=${callback}${ret}`;window[callback]=function(r){fn&&fn(r)headEle.removeChild(JSONP)deletewindow[callback]}headEle.appendChild(JSONP)}
封装完成以后,简单的写个后台接口,这里要注意,因为 jsonp 要求后台返回的是一个函数的调用,就是类似func(response)
,所以我们要动态的获取函数名,然后返回函数名的调用
<?php$data=".......";$callback=$_GET@['callback'];echo$callback.'('.json_encode($data).')';exit;?>
一个简单的封装就完成了。
axios中如何使用jsonp
当 jsonp 和 axios 碰撞的时候,会发出什么样的火花呢?
因为 axios 不支持 jsonp,所以我们需要在 axios 上添加 jsonp 方法,另外我们可以通过 promise 来替代回调参数
axios.jsonp=(url,data)=>{if(!url)thrownewError('urlisnecessary')constcallback='CALLBACK'+Math.random().toString().substr(9,18)constJSONP=document.createElement('script')JSONP.setAttribute('type','text/javascript')constheadEle=document.getElementsByTagName('head')[0]letret='';if(data){if(typeofdata==='string')ret='&'+data;elseif(typeofdata==='object'){for(letkeyindata)ret+='&'+key+'='+encodeURIComponent(data[key]);}ret+='&_time='+Date.now();}JSONP.src=`${url}?callback=${callback}${ret}`;returnnewPromise((resolve,reject)=>{window[callback]=r=>{resolve(r)headEle.removeChild(JSONP)deletewindow[callback]}headEle.appendChild(JSONP)})}
调用方式也发生了变化:
axios.jsonp(url,params).then(res=>console.log(res)).catch(err=>console.log(err))
到了这里文本的内容就到此结束了,文中有任何错误,请在评论区留言哦~
作者:晴天同学