基于 JSONP 实现跨域请求的原理

目录

1、同源策略

2、JSONP 原理

3、JSONP 原理剖析

4、封装 JSONP 脚本

5、JSONP 原理概括总结

6、JSONP 方式跨域的优缺点


1、同源策略

源:在任何网站的控制台输入:window.origin location.origin 可以得到当前源。源 = 协议+域名+端口号。

在说跨域之前,不得不先说一下浏览器的 同源策略同源策略是浏览器的一个安全功能,同源是指两个网址的 协议、域名、端口号 都相同,三者中只要有一个不同就是不同源。受同源策略的影响,运行在不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源,比如 abc.com 下的 JavaScript 脚本采用 Ajax 读取 xyz.com 里面的文档数据是会被拒绝的。这种浏览器的同源策略本意是防御非法攻击的,但是在前端开发中由于同源策略的存在,使得 Ajax 请求跨域数据时失败。针对这一问题衍生出了很多种解决跨域问题的方式,其中具有代表性的有 JSONP 和 CORS。

简而言之,同源策略:不同源的页面之间,不准互相访问数据。

上面的举例:abc.com 下的 JavaScript 脚本采用 Ajax 读取 xyz.com 里面的文档数据是会被拒绝的。只是为了方便描述和理解同源策略,实际上 Ajax 请求并不会被拦截。其背后 Ajax 请求跨域获取数据失败的真正原理是:浏览器禁止使用与当前网页不同来源的数据。当客户端浏览器向服务器发送跨域的 Ajax 请求时:(1)浏览器其实是发出了请求的、(2)请求也确实成功了,服务器也返回结果给客户端浏览器了、(3)但是因为响应结果中带着响应头,记录着数据来源的 IP 地址。所以,当浏览器查验服务端返回的数据时,发现响应头激活中的 IP 不是当前网页的 IP,就不会使用请求回来的数据。

问题 1:如果客户端电脑直接通过网址访问服务器,是不是就可以拿到数据了?答案是可以的,但是考虑到实际情况,一般不会出现这种情况的,因为基本上都是用户访问一个网站,在网站中做一些操作会发送一些服务器请求。

问题 2:为什么会出现跨域问题?现实情况中,为了保证项目的性能优化,和服务器的负载均衡,一般都会把服务器进行拆分几个部分:(1)web服务器:处理静态资源、(2)data服务器:做业务逻辑和数据分析、(3)图片服务器:专门为图片和视频等媒体资源设置一个服务器。如果这几个服务器的协议、域名、端口号有不同,就会导致了跨域的问题。

2、JSONP 原理

在前端开发的过程中,常见的 html 标签,例如:<a>、<img>、<script>、<link> 以及 Ajax 都可以指向一个资源地址,或者说发起对一个资源的请求,那么这里所说的请求就存在是同域请求还是跨域请求两种请求方式。同域的 Ajax 请求不会有什么问题,但是跨域的 Ajax 请求受到同源策略的影响,就会导致请求被拦截,JSONP 可以解决上面这种跨域请求被拦截的问题。

JSONP(JSON with Padding)是 JSON 的一种使用模式,可以解决主流浏览器的跨域问题,在早两年的前端解决跨域问题种经常出现这类解决方案。JSONP 的原理Ajax 请求受到同源策略的影响,不允许进行跨域请求,而 <img> <link> <script> 标签的属性都可以实现跨域,因为牵扯到请求数据,所以还是使用 <script> 标签比较适合做跨域请求。 <script> 标签的 src 属性中的链接可以访问跨域的 JavaScript 脚本,利用这个特性,服务器不再返回给客户端 JSON 格式的数据,而是返回一段填充(Padding)了数据的 JavaScript 代码给客户端,客户端浏览器通过 JavaScript 引擎去解析这段填充(Padding)了数据的 JavaScript 代码,从而拿到服务器返回给客户端的数据。

为什么可以跨域使用 CSS、JS 和图片等?、答:同源策略限制的是数据访问,我们引用 CSS、JS 和图片的时候,其实并不知道其内容,我们只是在引用,并不知道 CSS 的第一个字符是什么?

注意:为什么服务器不能直接把 JSON 格式的数据返回给 <script> 标签?因为 <script>  标签只能解析 JavaScript 代码,如果通过 <script> 标签请求回来的不是 JavaScript 代码,而是别的格式的数据,将会报错。

3、JSONP 原理剖析

下面通过一个客户端从服务器请求数据案例,来一步步深入剖析 JSONP 的原理。

3.1、首先检测 Ajax 请求是否真的如上所述,会被浏览器的同源策略所影响,简要代码如下:

//服务器端:服务器的地址为:127.0.0.1
const http = require("http");       //引入支持接受请求,返回响应的模块http
http.createServer((req,res) => {    //创建服务端程序实例,每当有客户端发来请求时,自动调用回调函数
    var sentry = "这是客户端要的数据";
    res.write(sentry);
    res.end();
}.listen(3000);    //监听3000端口

//客户端:向服务器发送Ajax请求    --实际结果客户端并请求不到数据
$.ajax({
    url:'http://127.0.0.1:3000',
    success:function(result){
        alert(result);
    }
})

上面这两段代码需要用到两台主机,一台主机写服务端的代码,一台主机写客户端代码,按照上面这两段代码来看,理论上说客户端可以拿到服务端的数据,但是实际上并没有拿到,原因就是受到同源策略的影响,客户端发送的 Ajax 请求被拦截了

3.2、针对上面问题:受到同源策略的影响,客户端发送的 Ajax 请求被拦截了。我们会想到 <script> 标签可以实现跨域,那么我们就用 <script> 标签来测试一下:

//服务器端:服务器的地址为:127.0.0.1
const http = require("http");       //引入支持接受请求,返回响应的模块http
http.createServer((req,res) => {    //创建服务端程序实例,每当有客户端发来请求时,自动调用回调函数
    var sentry = "这是客户端要的数据";
    res.write(sentry);
    res.end();
}.listen(3000);    //监听3000端口

//客户端:通过<script>标签向服务器发请求
<script src="http://127.0.0.1:3000">

上述代码是直接用 <script> 标签来向服务器发送请求,但是最终报语法错误。原因是你通过 <script> 标签从服务器请求数据,服务器此时返回的是一串字符串,而不是 JavaScript 代码,然后 JavaScript 引擎会去解析这段字符串,所以这里就会报错,因为 JavaScript 引擎只认识 JavaScript 代码

3.3、针对上面问题,由于 JavaScript 引擎只能解析 JavaScript 代码,所以我们可以将要返回的实际数据,填充(Padding)进一条合法的 JavaScript 语句中,进而返回一条 JavaScript 语句到客户端被 <script> 自动执行

//服务器端:服务器的地址为:127.0.0.1
const http = require("http");       //引入支持接受请求,返回响应的模块http
http.createServer((req,res) => {    //创建服务端程序实例,每当有客户端发来请求时,自动调用回调函数
    var sentry = "这是客户端要的数据";
    res.write(`alert("${sentry}")`);    //这一步就是JSONP的Padding的由来
    res.end();
}.listen(3000);    //监听3000端口

//客户端:通过<script>标签向服务器发请求
<script src="http://127.0.0.1:3000">

上述代码是直接用 <script> 标签来向服务器发送请求,然后服务器将要返回的实际数据,填充进一条合法的 JavaScript 语句中,进而返回一条 JavaScript 语句到客户端被 <script> 自动执行,这样就可以拿到服务端返回给客户端的数据了。那么问题来了,有点人不想对数据进行 alert() 操作,而是进行 document.write() 操作,或者是将数据放在 <div> 标签里,等等。不同用户肯定会对请求来的数据有不同的需求,像上面这段代码一样,将返回给客户端的 JavaScript 代码写死肯定是众口难调的。

3.4、针对上面这个问题:要在客户端执行的 JavaScript 语句在服务端写死了,众口难调。我们可以提前在客户端定义一个函数,用于处理服务端返回的请求,服务端仅使用函数名拼接一条函数调用的 JavaScript 语句

//服务器端:服务器的地址为:127.0.0.1
const http = require("http");       //引入支持接受请求,返回响应的模块http
http.createServer((req,res) => {    //创建服务端程序实例,每当有客户端发来请求时,自动调用回调函数
    var sentry = "这是客户端要的数据";
    res.write(`show("${sentry}")`);    //服务端仅使用客户端定义的函数名拼接一条函数调用的JavaScript语句
    res.end();
}.listen(3000);    //监听3000端口

//客户端:
function show(sentry){    //提前在客户端定义一个函数,用于处理服务端返回的请求
    alert(sentry);        //用户可以在客户端函数里面随意写想要对数据执行的操作
    console.log(sentry);
    document.write(sentry);
}
<script src="http://127.0.0.1:3000">    //通过<script>标签向服务器发请求

上述代码是提前在客户端定义一个 show() 函数,用户可以在 show() 函数里面随意写想要对数据执行的操作,服务器端返回的是客户端定义的函数名 show 拼接的数据的 JavaScript 代码,客户端的 JavaScript 引擎通过解析这段 JavaScript 代码,就可以调用客户端定义的函数,这样一来,客户端想要对数据执行什么样的操作就可以在客户端函数中写了。那么问题又来了,本该在客户端定义的函数名在服务器被写死了,不可能所有用户写的函数名都叫 show()。

3.5、针对上面这个问题:本该在客户端定义的函数名在服务器被写死了,不可能所有用户写的函数名都叫 show()。我们可以用请求参数 callback 将函数名传递给服务器,服务端接收客户端传来的名为 callback 的参数中保存的函数名,先将该函数名保存下来,然后用保存的函数名拼上要返回的数据,将 JavaScript 代码返回给客户端。

//服务器端:服务器的地址为:127.0.0.1
const http = require("http");       //引入支持接受请求,返回响应的模块http
const url = require("url");         //引入url模块,目的是为了保存客户端传递过来的参数
http.createServer((req,res) => {    //创建服务端程序实例,每当有客户端发来请求时,自动调用回调函数
    var Url = url.parse(req.url,true);
    var callback = Url.query.callback;    //将客户端传递过来的callback参数中的函数名保存下来
    var sentry = "这是客户端要的数据";
    res.write(`${callback}("${sentry}")`);    //服务端利用保存下来的函数名拼接一条函数调用的JavaScript语句
    res.end();
}.listen(3000);    //监听3000端口

//客户端:
function show(sentry){    //提前在客户端定义一个函数,用于处理服务端返回的请求
    alert(sentry);        //用户可以在客户端函数里面随意写想要对数据执行的操作
    console.log(sentry);
    document.write(sentry);
}
<script src="http://127.0.0.1:3000"?callback=show>    //通过<script>标签向服务器发请求,并传递callback参数

上述代码是通过请求参数 callback 将函数名传递给服务器,服务端接收客户端传来的名为 callback 的参数中保存的函数名,然后用保存的函数名拼上要返回的数据,将 JavaScript 代码返回给客户端,这样以来,无论你客户端的函数名是什么,只要通过参数传递给服务器,服务器都能返回给客户端相同函数名的拼接了数据的 JavaScript 代码,这样以来用户就可以在客户端自己自由定义函数名了。上述代码其实基本完善了,但是还有一个问题,就是用来发送跨域请求的 <script> 标签位置写死了,它只能在页面加载过程中执行一次,无法按需反复执行,而且执行的时机用户是不可控制的,比如每次单击按钮时,随时发送请求。

3.6、针对上面问题:用来发送跨域请求的 <script> 标签位置写死了,它只能在页面加载过程中执行一次,而且执行的时机用户是不可控制的。我们可以通过设计动态脚本的方式,在页面添加一个按钮,点击按钮动态创建<script>脚本,来实现用户控制请求发送的时机和次数。

//服务器端:服务器的地址为:127.0.0.1
const http = require("http");       //引入支持接受请求,返回响应的模块http
const url = require("url");         //引入url模块,目的是为了保存客户端传递过来的参数
http.createServer((req,res) => {    //创建服务端程序实例,每当有客户端发来请求时,自动调用回调函数
    var Url = url.parse(req.url,true);
    var callback = Url.query.callback;    //将客户端传递过来的callback参数中的函数名保存下来
    var sentry = "这是客户端要的数据";
    res.write(`${callback}("${sentry}")`);    //服务端利用保存下来的函数名拼接一条函数调用的JavaScript语句
    res.end();
}.listen(3000);    //监听3000端口

//客户端:
//<button>点击按钮发送请求</button>    //这里写一个button按钮
function show(sentry){    //提前在客户端定义一个函数,用于处理服务端返回的请求
    alert(sentry);        //用户可以在客户端函数里面随意写想要对数据执行的操作
    console.log(sentry);
    document.write(sentry);
}
$("button").click(function(){    //给按钮添加点击事件,设计动态创建<script>脚本,用户点击按钮,发送跨域请求
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = `http://127.0.0.1:3000"?callback=show`;
    document.body.appendChild(script);
})

上述代码是通过给按钮添加动态创建<script>脚本事件,来实现用户控制请求发送的时机和次数。但是每次用户点击按钮就会创建一个<script>脚本添加到 body 的结尾发送请求,如果多次点击就会创建多个<script>标签,这些标签堆积在 body 里。

3.6、针对上面问题:动态创建的<script>在body中堆积。我们可以在客户端函数中追加一条行为,删除动态创建并添加到body追后的脚本<script>代码。

//服务器端:服务器的地址为:127.0.0.1
const http = require("http");       //引入支持接受请求,返回响应的模块http
const url = require("url");         //引入url模块,目的是为了保存客户端传递过来的参数
http.createServer((req,res) => {    //创建服务端程序实例,每当有客户端发来请求时,自动调用回调函数
    var Url = url.parse(req.url,true);
    var callback = Url.query.callback;    //将客户端传递过来的callback参数中的函数名保存下来
    var sentry = "这是客户端要的数据";
    res.write(`${callback}("${sentry}")`);    //服务端利用保存下来的函数名拼接一条函数调用的JavaScript语句
    res.end();
}.listen(3000);    //监听3000端口

//客户端:
//<button>点击按钮发送请求</button>    //这里写一个button按钮
function show(sentry){    //提前在客户端定义一个函数,用于处理服务端返回的请求
    alert(sentry);        //用户可以在客户端函数里面随意写想要对数据执行的操作
    console.log(sentry);
    document.write(sentry);
    $("body>script:last").remove();    //当函数执行完毕后,删除动态创建的<script>脚本
}
$("button").click(function(){    //给按钮添加点击事件,设计动态创建<script>脚本,用户点击按钮,发送跨域请求
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = `http://127.0.0.1:3000"?callback=show`;
    document.body.appendChild(script);
})

以上就是 JSONP 原理剖析的理论全过程。在实际开发的过程中并不会写上面这样的代码,因为 jQuery 已经给我们封装好了以 JSONP 的方式发送跨域的 Ajax 请求。

3.7、因为 jQuery 已经给我们封装好了以 JSONP 的方式发送跨域的 Ajax 请求,所以实际开发的过程中只需要在发送请求的时候加上一个 dataType 参数,值为 jsonp 就可以实现跨域了,代码如下:

//服务器端:服务器的地址为:127.0.0.1
const http = require("http");       //引入支持接受请求,返回响应的模块http
const url = require("url");         //引入url模块,目的是为了保存客户端传递过来的参数
http.createServer((req,res) => {    //创建服务端程序实例,每当有客户端发来请求时,自动调用回调函数
    var Url = url.parse(req.url,true);
    var callback = Url.query.callback;    //将客户端传递过来的callback参数中的函数名保存下来
    var sentry = "这是客户端要的数据";
    res.write(`${callback}("${sentry}")`);    //服务端利用保存下来的函数名拼接一条函数调用的JavaScript语句
    res.end();
}.listen(3000);    //监听3000端口

//客户端:向服务器发送Ajax请求    -也可以把如下代码放在单机事件中
$.ajax({
    url:'http://127.0.0.1:3000',
    type:"get",
    dataType:"jsonp",    //jQuery封装好了以jsonp的方式实现跨域请求的功能
    success:function(result){
        alert(result);
    }
})

4、封装 JSONP 脚本

了解了 JSONP 的跨域原理之后,我们尝试自己封装一个 JSONP,这样一来,即使不使用 jQuery.ajax,我们也可以很方便的发送多个请求,不至于没发送一个请求,都需要写一坨创建 <script> 标签等的操作代码。

//后端主要响应代码,监听127.0.0.1:3000
if(path = "/friends.js"){    //如果有人发送请求到127.0.0.1:3000/friends.js
    if(request.headers["referer"].indexOf("127.0.0.1:8000") === 0){   //referer校验,只允许127.0.0.1:8000的请求访问
        response.statusCode = 200;
        response.setHeader("Content-Type","text/javascript; charset=utf-8");
        const string = `window['{{xxx}}']({{data}})`;
        const data = fs. readFileSync("./public/friends.json").toString();    //读取数据
        const string2 = string.replace("{{data}}",data).replace('{{xxx}}', query.callback);    //拼接
        response.write(string2);                                        //query.callback是查询参数中携带的函数名
        response.end();
    }else{    //如果请求方的referer未通过检验,则返回404
        response.statusCode = 404;
        response.end();
    }
}

//前端封装代码,127.0.0.1:8000
function jsonp(url){    //封装jsonp
    return new Promise((resolve, reject) => {
        let random = "CallbackName" + Math.random();    //生成一个随机数作为回调函数名,防止callback名称冲突
        window[random] = data => {    //声明服务器返回的函数所要调用的全局函数
            resolve(data);
        };
        const script = document.createElement("script");    //动态生成 script 标签
        script.src = `${url}?callback=${random}`;    //约定俗成的callback,将回调函数名作为查询参数传递给服务器
        script.onload = () => { script.remove(); };   //如果请求成功了,就删除script标签
        script.onerror = () => { reject(); };
        document.body.appendChild(script);    //将script标签插入body中,浏览器检测到就会发请求
    });
}

jsonp('127.0.0.1:3000/friends.js')    //经过上述的封装,现在就可以很方便的通过jsonp发请求
    .then((data)>{
        console.Log(data)
    })

从上述代码可以看出 JSONP 有一个天生的弱点,它只能知道请求的成功或失败状态,却并不能拿到服务器响应的状态码。

5、JSONP 原理概括总结

我们在跨域的时候,由于当前浏览器不支持 CORS,或者因为某些原因,我们必须使用另外一种方式来实现跨域。这种方式就是 JSONP,JSONP 就是客户端会通过 script 标签(携带 callback)请求一个 JS 文件,这个 JS 文件会执行一个回调函数,回调里面就有我们想要通过跨域拿到的数据,当着和需要后端的支持。

注意:回调函数的名字是可以随机生成的,我们将这个随机生成的函数名,以 callback 的形式传递给后台,后台会将这个函数返回给客户端,并执行。

JSONP(JSON with Padding)的最终原理:动态创建 <script> 脚本,借助 <script> 脚本(其中有callback参数)发送跨域请求,服务器接收并保存 callback 传递过来的参数,并将其与要返回给客户端的数据拼接成一段 JavaScript 代码以通返回给客户端,客户端 JavaScript 引擎执行从服务器返回回来的 JavaScript 代码,拿到数据,并进行相应的后续操作。 

注意:JSONP 只支持 GET 请求,不支持 POST 请求。如果需要支持 POST 请求怎么办,那就需要使用 CORS 跨域

注意:是不是所有人都可以通过 JSONP 的方式访问这个后端接口?如果是重要数据怎么办?那岂不是数据泄露了?如果后端不做任何处理的话,答案是会的。但是后端可以 做 referer 检查,如果访问后端接口的客户端 referer 不是被允许的域名,那就不给它返回数据就完事了。

6、JSONP 方式跨域的优缺点

优点:兼容 IE、支持跨域

缺点:由于 JSONP 内部是使用的 script 标签,所以它只支持 GET 请求,不支持 POST。

JSONP 不像 Ajax 一样,可以读到服务端返回的精确的状态码,它只知道请求的成功或失败( 通过 onload() 和 onerror() )

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值