文章首發(fā)于我的github及個人博客,github請看https://github.com/huruji/blog/issues/9,轉載請注明出處。
JSONP的原理就不細說了,就是利用script可以跨域的特點來實現跨域,首先我們考慮一個最簡單的jsonp,就是簡簡單單創(chuàng)建script標簽,
添加url的功能,如下:
function jsonp(url) {
const script = document.createElement('script');
script.src = url;
script.type = 'text/javascript';
document.body.appendChild(script);
}
此時我們使用服務端的代碼如下:
const http = require('http');
const data = {'data':'hello'};
const url = require('url');
const queryString = require('querystring');
http.createServer(function(req, res) {
var params = url.parse(req.url);
console.log(params);
if(params.query && queryString.parse(params.query).callback) {
console.log(1231232);
const str = queryString.parse(params.query).callback + '(' + JSON.stringify(data) + ')';
return res.end(str)
}
res.end(JSON.stringify(data));
}).listen(5000);
這是我們調用jsonp,假設我們只是想要alert出返回的數據,如下:
function msg(res) {
alert(res.data);
}
jsonp('http://localhost:5000?callback=msg');
這時候我們運行代碼可以發(fā)現已經正確彈出了相應的數據。
但是我們會發(fā)現這里的callback回調函數是一個全局的,這是不可取的,因此我們需要進行一些修改,將處理修改為一個局部的,我們可以將其作為一個回調函數來處理,如下:
function jsonp(url, callback) {
window.jsonpCallback = callback;
const script = document.createElement('script');
script.src = url + '?callback=jsonpCallback';
script.type = 'text/javascript';
document.body.appendChild(script);
}
jsonp('http://localhost:5000', function(res) {
alert(res.data);
});
這時候我們會發(fā)現我們不再需要在url中聲明相應的callback了,但是我們還是會發(fā)現一個問題,就是我們將所有的callback都設置成了一個全局變量,這樣的原因是因為我們需要在數據請求完成之后調用這個方法,因此不得不設置為一個全局變量。但是當我們有多個請求,并且每個請求的處理都是不一樣的時候,這個變量將會被覆蓋。這是不行的,因此我們應該為每一次請求設置一個唯一且不會沖突的變量,因此增加一個生成callback名字的方法如下:
function generateJsonpCallback() {
return `jsonpcallback_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
}
function jsonp(url, callback) {
const funcName = generateJsonpCallback();
window[funcName] = callback;
const script = document.createElement('script');
script.src = `${url}?callback=${funcName}`;
script.type = 'text/javascript';
document.body.appendChild(script);
}
jsonp('http://localhost:5000', function(res) {
alert(res.data);
});
jsonp('http://localhost:5000', function(res) {
const text = document.createTextNode(res.data);
document.body.appendChild(text);
});
這時候我們會發(fā)現我們已經利用了一個類似于隨機ID的形式完成了多次請求。
但是還是有一個問題大量的請求之后,window中會含有大量的全局變量,而且還有大量的script標簽,這顯然不是我們想要的,所以我們可以在請求完成之后刪除變量和script標簽。
function generateJsonpCallback() {
return `jsonpcallback_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
}
function removeScript(id) {
document.body.removeChild(document.getElementById(id));
}
function removeFunc(name) {
delete window[name];
}
function jsonp(url, timeout = 3000, callback) {
const funcName = generateJsonpCallback();
window[funcName] = callback;
const script = document.createElement('script');
script.src = `${url}?callback=${funcName}`;
script.id = funcName;
script.type = 'text/javascript';
document.body.appendChild(script);
setTimeout(() => {
removeScript(funcName);
removeFunc(funcName);
}, timeout)
}
jsonp('http://localhost:5000', 3000, function(res) {
alert(res.data);
});
jsonp('http://localhost:5000', 3000, function(res) {
const text = document.createTextNode(res.data);
document.body.appendChild(text);
});
我們通過將利用一個timeout時間定時為我們清除相應的script標簽和全局變量就可以了,這個定時時間的作用類似于ajax的timeout時間。
我們所有的內容都是使用es6的,那為什么不使用Promise來處理呢,還要使用煩人的回調,接下來那就來Promise化吧。
function jsonp(url, options = {timeout:3000}) {
const timeout = options.timeout;
return new Promise((resolve, reject) => {
const funcName = generateJsonpCallback();
window[funcName] = (res) => {
resolve(res);
setTimeout(() => {
removeScript(funcName);
removeFunc(funcName);
}, timeout)
};
const script = document.createElement('script');
script.src = `${url}?callback=${funcName}`;
script.id = funcName;
script.type = 'text/javascript';
document.body.appendChild(script);
})
}
調用只需要如下就可以了
jsonp('http://localhost:5000').then((res) => alert(res.data));
jsonp('http://localhost:5000').then((res) => {
const text = document.createTextNode(res.data);
document.body.appendChild(text);
});
到目前為止,一個較為完整的jsonp就實現了,但是我們還是會覺得少了一些什么,相信你已經看出來了,那就是錯誤處理。
迄今為止,并沒有測試過如果這個script標簽加載不成功如何處理,判斷資源加載失敗,顯然使用的是onerror事件,我們這就把他加上:
function jsonp(url, options = {timeout:3000}) {
const timeout = options.timeout;
let timeId;
return new Promise((resolve, reject) => {
const funcName = generateJsonpCallback();
window[funcName] = (res) => {
resolve(res);
timeId = setTimeout(() => {
removeScript(funcName);
removeFunc(funcName);
}, timeout)
};
const script = document.createElement('script');
script.src = `${url}?callback=${funcName}`;
script.id = funcName;
script.type = 'text/javascript';
document.body.appendChild(script);
script.onerror = () => {
reject(new Error(`fetch ${url} failed`));
removeScript(funcName);
removeFunc(funcName);
if(timeId) clearTimeout(timeId);
}
})
}
我們可以測試一下,輸入一個不存在的url:
jsonp('http://localhost:7000').then((res) => alert(res.data));
可以發(fā)現這時正常處理錯誤了,可以在控制臺看到相應的url獲取失敗,至此,完工;
至此所有的代碼簡單封裝如下:
(function(global){
function generateJsonpCallback() {
return `jsonpcallback_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
}
function removeScript(id) {
document.body.removeChild(document.getElementById(id));
}
function removeFunc(name) {
delete global[name];
}
function jsonp(url, options = {timeout:3000}) {
const timeout = options.timeout;
let timeId;
return new Promise((resolve, reject) => {
const funcName = generateJsonpCallback();
global[funcName] = (res) => {
resolve(res);
timeId = setTimeout(() => {
removeScript(funcName);
removeFunc(funcName);
}, timeout)
};
const script = document.createElement('script');
script.src = `${url}?callback=${funcName}`;
script.id = funcName;
script.type = 'text/javascript';
document.body.appendChild(script);
script.onerror = () => {
reject(new Error(`fetch ${url} failed`));
removeScript(funcName);
removeFunc(funcName);
if(timeId) clearTimeout(timeId);
}
})
}
window.jsonp = jsonp;
})(window);
測試代碼如下:
jsonp('http://localhost:5000').then((res) => alert(res.data));
jsonp('http://localhost:5000').then((res) => {
const text = document.createTextNode(res.data);
document.body.appendChild(text);
});
jsonp('http://localhost:7000').then((res) => alert(res.data));
最后是一個廣告貼,最近新開了一個分享技術的公眾號,歡迎大家關注??