浅谈JSONP

栏目: Json · 发布时间: 4年前

内容简介:同源政策很简单,它的含义是指两个网页:一旦以上三点中有任意一点不同,两个网站都不能称为同源。举例:同源政策的目的其实就是为了保证用户信息的安全,防止恶意的网站数据窃取。 在阮一峰的博客中,在同源政策一节中对其作用描述如下:

同源政策很简单,它的含义是指两个网页:

  1. 协议相同
  2. 域名相同
  3. 端口相同

一旦以上三点中有任意一点不同,两个网站都不能称为同源。举例:

http://www.example.com/xxx
http://www.example.com/yyy
以上两个网站是同源的,满足协议,域名,端口都相同(http协议默认端口为80)
---------------------------
http://example.com/xxx
http://www.example.com/xxx
以上两个网站是非同源的,因为域名不同
---------------------------
http://127.0.0.1:8080/xxx
http://127.0.0.1:8888/xxx
以上两个网站是非同源的,因为端口号不同
复制代码

为什么要有同源政策

同源政策的目的其实就是为了保证用户信息的安全,防止恶意的网站数据窃取。 在阮一峰的博客中,在同源政策一节中对其作用描述如下:

"设想这样一种情况:
A网站是一家银行,用户登录以后,又去浏览其他网站。
如果其他网站可以读取A网站的 Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。
更可怕的是:
Cookie 往往用来保存用户的登录状态。
如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。"
复制代码

所以自1995起,"同源政策"由网景引入浏览器后,所有浏览器都开始效仿了这一政策。不过同源政策带来的安全保障的同时,也带来了一些限制,其中一个限制就是 AJAX 请求不能发送

聊一聊XMLHttpRequest

上文说到同源政策的限制之一就是AJAX请求无法发送,我们知道AJAX的核心就是XMLHttpRequest,所以借机我也简单谈一谈XMLHttpRequest。先看一个示例:

在我的hosts文件中,我事先已经写好了ip与域名的映射。

浅谈JSONP

代码如下:

var http = require('http')
var fs = require('fs')
var url = require('url')
var port = process.argv[2]

if(!port){
    console.log('Please appoint the port number\n Like node server.js 8888')
    process.exit(1)
}

var server = http.createServer(function(request, response){
    var parsedUrl = url.parse(request.url, true)
    var pathWithQuery = request.url
    var queryString = ''
    var query = parsedUrl.query
    var path = parsedUrl.pathname
    if(path.indexOf('?') >= 0){ queryString = pathWithQuery.substring(pathWithQuery.indexOf('?')) }
    var method = request.method
    console.log('HTTP Path:\n'+path)
    if(path ==='/'){
        // sync是同步,async代表异步
        let string = fs.readFileSync('./index.html','utf8');
        response.statusCode = 200
        response.setHeader('Content-Type','text/html;charset=utf-8')
        response.write(string);
        response.end();
    }else if(path ==='/xxx'){
        response.statusCode = 200
        response.setHeader('Content-Type','text/json;charset=utf-8')
        response.write(`
           {
              "info":{
                 "name":"DobbyKim",
                 "age":"25",
                 "hobby":"唱跳rap篮球",
                 "girlfriend":"rightHand"
              }
           } 
        `)
        response.end();
    }
    else{
        response.statusCode = 404
        response.setHeader('Content-Type','text/html;charset=utf-8')
        response.write('wrong')
        response.end()
    }


    console.log(method+''+request.url)
})

server.listen(port)
console.log('Listen'+port+'Success\n Please open http://localhost:'+port)

复制代码

前端代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>你咬我啊</title>
</head>
<body>
<button id="btn">你咬我啊</button>
<script>
    btn.addEventListener('click',()=>{
        // 创建XMLHttpRequest对象
        let request = new XMLHttpRequest();
        // 初始化
        request.open('POST','http://dobby.com:8888/xxx');
        // 发送请求
        request.send();
        request.onreadystatechange = ()=>{
            // 请求及响应均成功
            if(request.readyState === 4){
                if(request.status>=200 && request.status<300){
                    let string = request.responseText;
                    let obj = window.JSON.parse(string);
                    console.log(string);
                    console.log(obj);
                }else{console.log('fail');}
            }
        }
    })
</script>
</body>
</html>
复制代码

在前端script代码中,我们为按钮添加了事件,当按钮被click,当前页面就会向服务端发起请求,我们再来回想一下request.readyState的五个状态值:

0 :代理被创建,但尚未调用open()方法
1 : open()方法已经被调用
2 : send()方法已经被调用
3 : 响应数据下载中
4 : 响应数据下载已完成
复制代码

首先我们开启两个node-server,它们指定的端口号分别为:8888和8889。我们在浏览器分别输入URL: dobby.com:8888 以及 frank.com:8889 。当我们在 dobby.com:8888 下点击按钮时,在浏览器的控制台上打印出了我们接收到的JSON数据。

浅谈JSONP

但是,当我们在 frank.com:8889 下点击按钮,在控制台上则会报错:

浅谈JSONP

这也就进一步验证了AJAX受限于"同源政策",对于我们上述示例来说,实际上这是一次跨域请求的过程即:A网站想要给B网站发送请求。由于同源政策,AJAX只能请求于协议,域名,端口号相同的网站,而在实际开发中,又有很多跨域的需求,所以AJAX自然也会使用一些方法规避同源政策。其实这也很简单,我们只需在后端代码中添加一句话即可:

else if(path ==='/xxx'){
        response.statusCode = 200
        response.setHeader('Content-Type','text/json;charset=utf-8')
        // 添加了这句话以后,任何网站都可以请求dobbykim.com:8888
        // response.setHeader('Access-Control-Allow-Origin','*')
        response.setHeader('Access-Control-Allow-Origin','http://frank.com:8889')
        response.write(`
           {
              "info":{
                 "name":"DobbyKim",
                 "age":"25",
                 "hobby":"唱跳篮球rap",
                 "girlfriend":"rightHand"
              }
           } 
        `)
        response.end();
    }
复制代码

上面我们实际上用到了CORS机制,CORS即Cross-Origin-Resource-Sharing,翻译成跨域资源共享,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。有了CORS机制,可以使AJAX进行跨域请求,AJAX同时也支持多种请求方式:get,post,put,delete等等。那么在没有AJAX之前,我们是怎样进行跨域请求的呢?这就要引出我们今天的主角JSONP了,但是在谈JSONP之前,我们还要再聊一聊历史~

不得不说的历史

假设我们有一个文件db,这个文件db暂时作为我们的数据库进行数据的存储,文件存储着当前金额的数量100。 后台程序如下:

var http = require('http')
var fs = require('fs')
var url = require('url')
var port = process.argv[2]

if(!port){
    console.log('Please appoint the port number\n Like node server.js 8888')
    process.exit(1)
}

var server = http.createServer(function(request, response){
    var parsedUrl = url.parse(request.url, true)
    var pathWithQuery = request.url
    var queryString = ''
    var query = parsedUrl.query
    var path = parsedUrl.pathname
    if(path.indexOf('?') >= 0){ queryString = pathWithQuery.substring(pathWithQuery.indexOf('?')) }
    var method = request.method
   
    console.log('HTTP Path:\n'+path)
    if(path == '/'){
        var string = fs.readFileSync('./index.html','utf8')
        var amount = fs.readFileSync('./db','utf-8')
        string = string.replace('&amount',amount);
        response.setHeader('Content-Type','text/html;charset=utf8')
        response.write(string)
        response.end()
    }else if(path==='/pay' && method.toUpperCase()==='POST'){
        var amount = fs.readFileSync('./db','utf8')
        var newAmount = parseInt(amount) - 1;
        fs.writeFileSync('./db',newAmount);
        response.write('success');
        response.end()
    } else{
        response.statusCode = 404
        response.setHeader('Content-Type','text/html;charset=utf-8')
        response.write('找不到对应的路径')
        response.end()
    }


    console.log(method+''+request.url)
})

server.listen(port)
console.log('Listen'+port+'Success\n Please open http://localhost:'+port)

复制代码

前端代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <link rel="stylesheet" href="./style.css">
</head>
<body>
<h5>您的账余额是 <span id="amount">&amount</span></h5>
<form action="/pay" method="post">
    <input type="submit" value="付款">
</form>
</body>
</html>
复制代码

form表单的核心功能就是提交。如本例:当我们点击submit进行提交时,浏览器会跳转到pay这个路径下 如果 path==='/pay' && method.toUpperCase()==='POST' ,我们就会将db文件存储的金额-1,然后返回一个"success"。开启server后,程序运行的结果如下:

浅谈JSONP

当点击付款按钮时,form表单提交,页面发生跳转。

浅谈JSONP

我们可以看到浏览器输入框的路径已经变成了pay,并且服务器返回了响应至浏览器即: response.write('success'); ,在页面上我们看到了success的字样,后退至index.html页面,并点击刷新,我们可以看到,金额减少了一元钱。

浅谈JSONP

其实,从功能上来讲,这是没有问题的。但是这却给用户造成了不好的体验。因为,用户每次点击付款按钮,页面都会发生跳转,而且用户需要自己点击后退按钮并刷新页面,才可以看到自己的账户余额。我们希望的是:点击付款后,浏览器会告诉我们付款成功or失败,在不刷新页面的情况下我们可以实时看到自己的账户余额。很显然,form表单是做不到的。为什么呢?仔细想一想,form表单在提交时,必定会发生页面的跳转,当然有一种方法可以做出稍稍的改进。在"远古时期"人们会使用iframe标签让form表单每次post都跳转到当前页面的内嵌的iframe中:

<form action="/pay" method="post" target="result">
    <input type="submit" value="付款">
</form>
<iframe name="result" src="about:blank" frameborder="0" height="200"></iframe>
复制代码
浅谈JSONP

当点击付款按钮时:

浅谈JSONP

form表单的post发生在了页面内嵌的iframe标签中,但是金额还是没有刷新,我们仍然需要自己手动刷新页面。

放弃POST,使用GET

form表单最大的问题就是会刷新页面或打开新的页面,不过form表单却有一个特性即:没有跨域的问题。在上面的程序中,我们如果将form标签变为 <form action="http://www.baidu.com/pay" method="get"> 。实际上这个请求是可以发送的。在知乎上有一个问题:为什么form表单提交没有跨域问题,但是ajax提交有跨域问题?我在这里面借用下 方老师 的答案 :-)

浅谈JSONP

言归正传,为了优化用户的体验,我们不得不放弃使用form表单,而改用其他的,可以让浏览器发起请求的标签,这些标签有:

  1. a标签
  2. img标签
  3. link标签
  4. script标签
  5. ......

a标签可以发起get请求,不过也会刷新或打开页面,img标签会发起get请求,但是只能以图片形式进行展示,经过多方面考虑,于是乎,当时的前端 程序员 决定使用script标签,因为script标签不仅能发起请求,同时也能作为脚本执行,最重要的是,script标签支持跨域请求。接下来,我们来看一个示例:

首先,在我的hosts文件中,我已经写好了ip与域名的映射。

浅谈JSONP

开启两个node-server,分别为: http://dobby.com:8888 以及 http://frank.com:8889 ,模拟dobby.com向frank.com发起跨域请求。

前端代码如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
<h5>您的账余额是 <span id="amount">&amount</span></h5>
<button id="btn">付款</button>
<script>
   btn.addEventListener('click',()=>{
    // 动态创建script标签
    let script = document.createElement('script');
    // 随机生成函数名
    let functionName = 'dobby'+parseInt(Math.random()*10000,10);

    window[functionName] = (result)=>{
        if(result === 'success'){
            amount.innerText = amount.innerText - 1;
        }else{
            alert('fail');
        }
    }
    // 指定发起请求的地址
    script.src = 'http://frank.com:8889/pay?callback='+functionName;
    // 一定要将script加进去
    document.body.appendChild(script);
    script.onload = (e)=>{
        // 每次动态创建script标签之后,都将script标签删掉
        e.currentTarget.remove();
        // 无论script标签加载成功或失败都需要将window[functionName]属性删除
        delete window[functionName];
    }
    script.onerror = ()=>{
        alert('fail');
        delete window[functionName];
    }
})
</script>
</body>
</html>
复制代码

对于frank.com的后端来讲,只需要这样做即可:

else if(path==='/pay'){
        var amount = fs.readFileSync('./db','utf8')
        var newAmount = parseInt(amount) - 1;
        fs.writeFileSync('./db',newAmount);
        response.setHeader('Content-Type','application/javascript')
        response.statusCode = 200
        // query为path后面的查询参数
        response.write(`
            ${query.callback}.call(undefined,'success');
        `)
        response.end()
    }
复制代码

frank.com的后端程序员只需要拿到查询参数中的callback的值,并调用此方法,而前端程序员通过后端传入的参数进行判断,这样就做到了低耦合高复用的代码。实际上,这就是 JSONP

什么是JSONP

JSONP是一种动态script标签跨域请求技术。指的是请求方动态创建script标签,src指向响应方的服务器,同时传一个参数callback,callback后面是一个随机生成的functionName,当请求方向响应方发起请求时,响应方根据传过来的参数callback,构造并调用形如:xxx.call(undefined,'你要的数据'),其中'你要的数据'的传入格式是以JSON格式传入的,因为传入的JSON数据具有左右padding,因而得名JSONP。后端代码构造并调用了xxx,浏览器接收到了响应,就会执行xxx.call(undefined,'你要的数据'),于是乎,请求方就知道了他要的数据,这就是JSONP。在知乎上,看到了有关于JSONP的回答:

浅谈JSONP

其实就是这样。

jQuery的JSONP

我们首先需要引入jQuery,然后将代码中script标签里面的内容变为这样即可:

btn.addEventListener('click',function () {
        $.ajax({
            url: "http://jack.com:8001/pay",

            // The name of the callback parameter, as specified by the YQL service
            jsonp: "callback",

            // Tell jQuery we're expecting JSONP
            dataType: "jsonp",

            // Tell YQL what we want and that we want JSON
            data: {
                q: "select title,abstract,url from search.news where query=\"cat\"",
                format: "json"
            },

            // Work with the response
            success: function( response ) {
                if(response === 'success'){
                    amount.innerText = amount.innerText - 1;
                }
            }
        });
    })
复制代码

值得吐槽的一点是:调用jQuery的JSONP API里面出现了ajax这样的字眼,实际上JSONP和Ajax毛关系都没有。


以上所述就是小编给大家介绍的《浅谈JSONP》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

编程之魂

编程之魂

Federico Biancuzzi、Shane Warden / 闫怀志 / 电子工业出版社 / 2010-04 / 59.80元

本书是27位杰出的设计师与你分享他们的智慧和经验。书中以问答方式告诉为什么要创建某种编程语言、它在技术上如何开发、如何教授和学习,以及它如何顺应时代发展等。你会发现构建成功编程语言所需的思想和步骤,它广受欢迎的原因,以及如何处理程序员常见的问题。因此,如果你想深入学习设计成功编程语言的思想,本书会对你大有帮助。一起来看看 《编程之魂》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

随机密码生成器
随机密码生成器

多种字符组合密码

html转js在线工具
html转js在线工具

html转js在线工具