JSONP實現原理
jsonp,其實就是單純爲了實現跨域請求而創造的一個欺騙(trick)。
雖然,因爲同源策略的影響,不能通過XMLHttpRequest請求不同域上的數據(Cross -origin reads)。但是,在頁面上引入不同域上的js腳本文件卻是可以的(Cross -origin embedding)。因此,在js文件載入完畢之後,觸發回調,可以將需要的data作爲參數傳入
注意,實現方式(需前後端配合)
優點
兼容性好(兼容低版本IE)
缺點
JSONP只支持GET請求,XMLHttpRequest相對於JSONP有着更好的錯誤處理機制。
實現
1.服務端採用的是Node,服務端處理請求方法如下
router.get('/', function(req, res, next) {
console.log('收到客戶端的請求:', req.query);
// 傳回到客戶端的數據
let data = JSON.stringify({
'status':200,
'result':{
'name':'柳成蔭',
'site':'123456'
}
});
// 獲取方法名稱 - 這是客戶端傳過來的方法名參數
// 因爲這個方法名必須是客戶端有的,必須要客戶端告訴服務端是哪個方法
let methodName = req.query.callback;
let methodStr = methodName + '(' + data + ')';
// 快速結束沒有任何數據的響應,客戶端會執行這個方法,從而獲取到服務端返回的數據
res.end(methodStr)
});
2.封裝JSONP
(function (w) {
/**
* jsonp的實現
* @param {Object}option
*/
function jsonp(option) {
// 把success函數掛載在全局的getDate函數上
w.getData = option.success;
// 處理url,拼接參數 - 回調方法是getData
option.url = option.url + '?callback=getData';
// 創建script標籤,並插入body
let scriptEle = document.createElement('script');
scriptEle.src = option.url;
document.body.appendChild(scriptEle);
}
// 全局掛載一個jsonp函數
w.jsonp = jsonp;
})(window);
/**
* 把對象轉換成拼接字符串
* 把形如
data:{
"sex":"男",
"name":"九月"
}
轉換成sex=男&name=九月
* @param paramObj 對象參數
* @param words
* @returns {string} 字符串
*/
function getStrWithObject(paramObj,words){
let resArr = [];
// 1.轉換對象
for(let key in paramObj){
let str = key + '=' + paramObj[key];
resArr.push(str);
}
resArr.push(words);
// 3.數組轉換成字符串
return resArr.join("&");
}
看似代碼沒有任何問題。但是,我們測試一下,多次調用jsonp方法。
jsonp({
url:'http://localhost:3000/',
data:{
"sex":"男",
"name":"九月"
},
success:function (data) {
console.log(data);
alert(1);
}
});
jsonp({
url:'http://localhost:3000/',
data:{
"sex":"男",
"name":"九月"
},
success:function (data) {
console.log(data);
alert(2);
}
});
結果會怎麼樣?的確,還是會執行2次,但是每次執行的都是第二個jsonp方法。這是爲什麼?
問題出現在這裏,多次調用,會發生函數覆蓋。
解決方案就是讓每一次調用函數名不一致,在JS裏有很多方法,比如通過隨機數、時間戳之類的。這裏採用的是隨機數,修改如下。
// 0.產生不同的函數名 - 解決了調用多次請求,造成覆蓋的問題
// 如果方法名相同,調用多次,都會執行最後一個,出現覆蓋現象
let callBackName = 'lcy' + Math.random().toString().substr(2)
+ Math.random().toString().substr(2);
// 把success函數掛載在全局的getDate函數上
w[callBackName] = option.success;
// 處理url,拼接參數
option.url = option.url + '?' + getStrWithObject(option.data,'callback='+callBackName);
好,現在看似完美解決。然而,我們發現,每次調用jsonp方法,都會在body裏插入一個script標籤。我們並不希望在調用jsonp之後,添加這樣一個標籤,我們需要在調用完之後,將其移除。
怎麼做呢?我們在將success函數掛載在全局時,我們在success外層再套個函數。在這個函數裏調用success方法之後,即已經請求到數據之後,我們就去把這個script標籤給它移除就行了。修改如下
// 1.函數掛載在全局
w[callBackName] = function(data){
option.success(data);
// 刪除script標籤
document.body.removeChild(scriptEle);
};
整個過程就是這樣,以下是完整代碼。
(function (w) {
/**
* jsonp的實現
* @param {Object}option
*/
function jsonp(option) {
// 0.產生不同的函數名 - 解決了調用多次請求,造成覆蓋的問題
// 如果方法名相同,調用多次,都會執行最後一個,出現覆蓋現象
let callBackName = 'lcy' + Math.random().toString().substr(2) + Math.random().toString().substr(2);
// 1.函數掛載在全局
w[callBackName] = function(data){
option.success(data);
// 刪除script標籤
document.body.removeChild(scriptEle);
};
// 2.處理url
option.url = option.url + '?' + getStrWithObject(option.data,'callback='+callBackName);
// 3.創建script標籤插入body
let scriptEle = document.createElement('script');
scriptEle.src = option.url;
document.body.appendChild(scriptEle);
}
w.jsonp = jsonp;
})(window);
/**
* 把對象轉換成拼接字符串
* @param paramObj 對象參數
* @returns {string} 字符串
*/
function getStrWithObject(paramObj,words){
let resArr = [];
// 1.轉換對象
for(let key in paramObj){
let str = key + '=' + paramObj[key];
resArr.push(str);
}
resArr.push(words);
// 3.數組轉換成字符串
return resArr.join("&");
}