JSONP從原理到實現

      相信AJAX大家都熟悉,以前使用AJAX做前後端交互經常會碰到請求跨域,關於什麼是跨域可以自行百度(不是很難),所以引入本文的主角JSONP,一個有效的跨域解決方案。本文儘量以最簡單的方式解釋什麼是JSONP,以及JS代碼的實現。

什麼是JSONP

      首先跟JSON沒有半毛錢關係,它與AJAX一樣都是客戶端向服務器發送請求,然後獲取數據的方式。AJAX屬於同源策略,JSONP則屬於非同源策略,可以進行跨域請求。

JSONP原理

     原理其實非常簡單,就是利用了<script>標籤沒有跨域限制的漏洞,仔細想想以前引入外部JS文件的時候,是不是src裏填怎樣的地址都行。那麼怎麼利用這個漏洞來實現數據請求呢,首先請看下面這裏例子

<html>
<head></head>
<body>
<script>
   function getData(data) {
      console.log(data);
   }
</script>
<script>
   getData({errno: false, id: "aaaa"});
</script>
<body>
</html>

我們在第一個<script>標籤裏定義函數,然後在第二個<script>標籤裏調用這個函數,同時傳入參數。其實JSONP就是這個思路,也就是說我們在第一個<script>標籤裏定義回調函數getData,然後通過動態創建第二<script>標籤,然後指定src,假設src="http://localhost:3000/test",並插入至body中,然後服務端響應一個字符串: "getData({errno: false, id: "aaaa"})"。此時創建出來的<script>就會接到服務端響應的文本,並解析執行,這裏顯然執行的就是getData回調函數。示例代碼如下

首先是用於測試的服務端代碼(這裏先粘貼出來,使用nodejs):

const http = require("http");
const queryString = require("querystring");
const url = require("url");
const server = http.createServer((req, res) => {
    const pathname = url.parse(req.url).pathname;
    const query = queryString.parse(url.parse(req.url).query);
    console.log(query, pathname);
    const method = req.method;
    if (method === "GET") {
        if (pathname === "/test") {
            res.end(`getData({errno: false, id: "aaaa"});`);  // 服務端發給客戶端的內容           
        }
    }
});
server.listen(3000, err => console.log(err || "listen in 3000"));

那麼簡單的JSONP實現就如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    !function (window, document) { // 防止全局變量污染
        function getData(data) {
            console.log(data)
        }
        window.getData = getData; // 由於使用了閉包來防止全局變量污染,所以需要把getData函數掛到window上,不然訪問不到getData
        function JSONP(url) {
            var script = document.createElement("script");
            var body = document.body;
            script.src = url;
            body.appendChild(script);
            script.onload = function () { // 加載好了就刪掉,不然每調用一次都往body裏插script標籤
             body.removeChild(script)
            }
        }
        JSONP("http://localhost:3000/test");
    }(this, document)
</script>
</body>
</html>

瀏覽器上跑一遍,結果如下

到這裏,是不是發現有點感覺了,最簡單的JSONP就是這個樣子的,那麼如果要發數據給服務端怎麼辦,其實只需要將地址改成:"http://localhost:3000/test?a=xxx&b=yyy" 這種形式就可以了,因爲使用的是GET方法,所以服務端能接到通過GET方法發過來的數據。有了這個基礎,我們來將上面的代碼封裝一下,我們希望能像AJAX那樣,使用回調或者Promise對象來接收服務端的結果,下面來看看這兩種風格的實現。

回調風格的JSONP實現如下:

function stringify(object) { // 工具函數 做如下轉換 {a:1,b:2} => a=1&b=2 , {a:[1,2,3]} => a=1&a=2&a=3       
    if (!object) return "";
    return Object.keys(object).reduce(function (acc, cur) {
        return acc + (cur + "=" + object[cur] + "&");
    }, "").replace(/&$/, "");
}

function JSONP(options, callback) { // 調用如下JSONP({url: "xxx", data: {x:[1,2]}})
    var url = options.url || "";
    var params = stringify(options.data);
    var script = document.createElement("script");
    var callbackHash = "__callback__" + Date.now(); // 稍微給掛到window上的回調函數做一個hash,防止衝突
    window[callbackHash] = function (data) { // callback 臨時掛載到window上,調用完就刪除
        (typeof callback === "function") && callback(data);
    };
    //拼接url,需要注意的是在最後面加上callback字段,或者其他也行,這個可以與服務端約定好
    script.src = url + "?" + params + "&callback=" + callbackHash;
    document.body.appendChild(script);
    script.onload = function () { // 加載完成
        document.body.removeChild(script);
        delete window[callbackHash]; // 刪除掉該回調函數,不然會一直掛載在window上
    }
}

修改服務端代碼

const http = require("http");
const queryString = require("querystring");
const url = require("url");

http.createServer((req, res) => {
    if (req.method === "GET") {
        if (url.parse(req.url).pathname === "/jsonp") {
            const params = queryString.parse(url.parse(req.url).query);
            console.log(params);
            let data = {value: "hello world", err: false};
            res.end(`${params.callback}(${JSON.stringify(data)})`);
        }
    }
}).listen(8000);

簡單測試測試

JSONP({
    url: "http://localhost:8000/jsonp",
    data: {
        name: "sundial dreams",
        age: 21
    }
}, function (data) {
    console.log(data);
});

可以看到瀏覽器正常輸出獲取的結果 

接下來封裝成Promise對象也是很容易的,就直接給出代碼了

function JSONP_Promise({url = "", data = null}) {
    return new Promise((resolve, reject) => {
        try {
            const cb = `__callback__${Date.now()}`;
            window[cb] = function (data) {
                resolve(data);
            };
            const script = document.createElement("script");
            script.src = `${url}?${stringify(data)}&callback=${cb}`;
            document.body.appendChild(script);
            script.onload = function () {
                document.body.removeChild(this);
                delete window[cb];
            };
        } catch (e) {
            reject(e)
        }
    });
}

 使用的話,如下

JSONP_Promise({url: "http://localhost:8000/jsonp", data: {v: 1}}).then(data => {
    console.log(data);
})

JSONP大概就是這樣的一個東西,是不是發現並沒有那麼難,其實它實現跨域的同時也有缺點,即僅支持GET方法,然後就是容易遭受XSS攻擊等,優點自然就是實現簡單,然後還能解決跨域問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章