给 axios 添加 jsonp 请求

7,555 阅读2分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

从跨域报错讲起

假如我有一个测试服务器,地址是 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"            
});

function callback(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' 的函数,那么这个函数就会执行,参数就是后台真正的返回值。

var cb_callback = function(data){
    alert(data.Error)
}

document.write('<script src="http://digi.duodiangame.com/digMineral/interfaces/task/reportDailySignin.do?callback=cb_callback&user_id=AAA"></script>')

大家可以把这段代码复制到控制台看一下效果

image.png

以上就是 jsonp 能够获取数据的原理了。

封装 jsonp

我们封装一个简单的 jsonp 来解释 jsonp 应该如何来实现

function jsonp (url,data,fn){
    if(!url)
        throw new Error('url is necessary')
    const callback = 'CALLBACK' + Math.random().toString().substr(9,18)
    const JSONP = document.createElement('script')
          JSONP.setAttribute('type','text/javascript')

    const headEle = document.getElementsByTagName('head')[0]

    let ret = '';
    if(data){
        if(typeof data === 'string')
            ret = '&' + data;
        else if(typeof data === 'object') {
            for(let key in data)
                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)
      delete window[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)
        throw new Error('url is necessary')
    const callback = 'CALLBACK' + Math.random().toString().substr(9,18)
    const JSONP = document.createElement('script')
          JSONP.setAttribute('type','text/javascript')

    const headEle = document.getElementsByTagName('head')[0]

    let ret = '';
    if(data){
        if(typeof data === 'string')
            ret = '&' + data;
        else if(typeof data === 'object') {
            for(let key in data)
                ret += '&' + key + '=' + encodeURIComponent(data[key]);
        }
        ret += '&_time=' + Date.now();
    }
    JSONP.src = `${url}?callback=${callback}${ret}`;
    return new Promise( (resolve,reject) => {
        window[callback] = r => {
          resolve(r)
          headEle.removeChild(JSONP)
          delete window[callback]
        }
        headEle.appendChild(JSONP)
    })
    
}

调用方式也发生了变化:

axios.jsonp(url, params)  
     .then(res => console.log(res))        
     .catch(err => console.log(err))

到了这里文本的内容就到此结束了,文中有任何错误,请在评论区留言哦~