相信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攻擊等,優點自然就是實現簡單,然後還能解決跨域問題。