淺談JSONP

個人JSONP學習筆記javascript

什麼是同源策略

在談JSONP以前首先要簡單說一說同源政策css

什麼是同源政策

同源政策很簡單,它的含義是指兩個網頁:html

  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
以上兩個網站是非同源的,由於端口號不一樣
複製代碼

爲何要有同源政策

同源政策的目的其實就是爲了保證用戶信息的安全,防止惡意的網站數據竊取。 在阮一峯的博客中,在同源政策一節中對其做用描述以下:java

"設想這樣一種狀況: A網站是一家銀行,用戶登陸之後,又去瀏覽其餘網站。 若是其餘網站能夠讀取A網站的 Cookie,會發生什麼? 很顯然,若是 Cookie 包含隱私(好比存款總額),這些信息就會泄漏。 更可怕的是: Cookie 每每用來保存用戶的登陸狀態。 若是用戶沒有退出登陸,其餘網站就能夠冒充用戶,隨心所欲。"
複製代碼

因此自1995起,"同源政策"由網景引入瀏覽器後,全部瀏覽器都開始效仿了這一政策。不過同源政策帶來的安全保障的同時,也帶來了一些限制,其中一個限制就是AJAX 請求不能發送node

聊一聊XMLHttpRequest

上文說到同源政策的限制之一就是AJAX請求沒法發送,咱們知道AJAX的核心就是XMLHttpRequest,因此藉機我也簡單談一談XMLHttpRequest。先看一個示例:
在個人hosts文件中,我事先已經寫好了ip與域名的映射。
程序員

6

代碼以下:

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的五個狀態值:ajax

0 :代理被建立,但還沒有調用open()方法
1 : open()方法已經被調用
2 : send()方法已經被調用
3 : 響應數據下載中
4 : 響應數據下載已完成
複製代碼

首先咱們開啓兩個node-server,它們指定的端口號分別爲:8888和8889。咱們在瀏覽器分別輸入URL:dobby.com:8888以及frank.com:8889。當咱們在dobby.com:8888下點擊按鈕時,在瀏覽器的控制檯上打印出了咱們接收到的JSON數據。
數據庫


可是,當咱們在 frank.com:8889下點擊按鈕,在控制檯上則會報錯:


這也就進一步驗證了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後,程序運行的結果以下:

1

當點擊付款按鈕時,form表單提交,頁面發生跳轉。

2

咱們能夠看到瀏覽器輸入框的路徑已經變成了pay,而且服務器返回了響應至瀏覽器即: response.write('success');,在頁面上咱們看到了success的字樣,後退至index.html頁面,並點擊刷新,咱們能夠看到,金額減小了一元錢。

3

其實,從功能上來說,這是沒有問題的。可是這卻給用戶形成了很差的體驗。由於,用戶每次點擊付款按鈕,頁面都會發生跳轉,並且用戶須要本身點擊後退按鈕並刷新頁面,才能夠看到本身的帳戶餘額。咱們但願的是:點擊付款後,瀏覽器會告訴咱們付款成功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>
複製代碼

4

當點擊付款按鈕時:

4

form表單的post發生在了頁面內嵌的iframe標籤中,可是金額仍是沒有刷新,咱們仍然須要本身手動刷新頁面。

放棄POST,使用GET

form表單最大的問題就是會刷新頁面或打開新的頁面,不過form表單卻有一個特性即:沒有跨域的問題。在上面的程序中,咱們若是將form標籤變爲<form action="http://www.baidu.com/pay" method="get">。實際上這個請求是能夠發送的。在知乎上有一個問題:爲何form表單提交沒有跨域問題,可是ajax提交有跨域問題?我在這裏面借用下方老師的答案 :-)


言歸正傳,爲了優化用戶的體驗,咱們不得不放棄使用form表單,而改用其餘的,可讓瀏覽器發起請求的標籤,這些標籤有:

  1. a標籤
  2. img標籤
  3. link標籤
  4. script標籤
  5. ......

a標籤能夠發起get請求,不過也會刷新或打開頁面,img標籤會發起get請求,可是隻能以圖片形式進行展現,通過多方面考慮,因而乎,當時的前端程序員決定使用script標籤,由於script標籤不只能發起請求,同時也能做爲腳本執行,最重要的是,script標籤支持跨域請求。接下來,咱們來看一個示例:
首先,在個人hosts文件中,我已經寫好了ip與域名的映射。

6

開啓兩個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的回答:


其實就是這樣。

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爲何不支持post

這是一道大機率會出現的面試題,回答以下:

  1. JSONP是經過動態建立script實現的
  2. 動態建立script只能發起get請求,沒法發起post請求

回答完畢~

文章若是出現問題,歡迎指出與批評。

相關文章
相關標籤/搜索