基本原理
基本原理: 主要就是利用了 script
標(biāo)簽的src
沒有跨域限制來完成的艇挨。
執(zhí)行過程
執(zhí)行過程:
- 前端定義一個(gè)解析函數(shù)(如:
jsonpCallback = function (res) {}
) - 通過
params
的形式包裝script
標(biāo)簽的請(qǐng)求參數(shù),并且聲明執(zhí)行函數(shù)(如cb=jsonpCallback
) - 后端獲取到前端聲明的執(zhí)行函數(shù)(
jsonpCallback
)骚腥,并以帶上參數(shù)且調(diào)用執(zhí)行函數(shù)的方式傳遞給前端 - 前端在
script
標(biāo)簽返回資源的時(shí)候就會(huì)去執(zhí)行jsonpCallback
并通過回調(diào)函數(shù)的方式拿到數(shù)據(jù)了夭谤。
優(yōu)缺點(diǎn)
缺點(diǎn):
- 只能進(jìn)行
GET
請(qǐng)求
優(yōu)點(diǎn):
- 兼容性好论熙,在一些古老的瀏覽器中都可以運(yùn)行
案例分析
先來看看我們要實(shí)現(xiàn)一個(gè)什么效果:
在一個(gè)叫index.html
的文件中有以下代碼:
<script type='text/javascript'>
window.jsonpCallback = function (res) {
console.log(res)
}
</script>
<script src='http://localhost:8080/api/jsonp?id=1&cb=jsonpCallback' type='text/javascript'></script>
然后我本地有一個(gè)文件server.js
它會(huì)使用node
提供一個(gè)服務(wù)富俄,來模擬服務(wù)器。
并且定義一個(gè)接口/api/jsonp
來查詢id
對(duì)應(yīng)的數(shù)據(jù)截歉。
當(dāng)我打開index.html
的時(shí)候就會(huì)加載script
標(biāo)簽胖腾,并執(zhí)行了此次跨域請(qǐng)求。
前期準(zhǔn)備
- 我在本地新建一個(gè)文件夾
node-cors
- 并在此目錄下
npm init
瘪松,初始化package.json
- 安裝
koa
(node
的一個(gè)輕量級(jí)框架) - 新建文件夾
jsonp
咸作,并新建index.html
和server.js
,一個(gè)寫前端代碼宵睦,一個(gè)寫后端
mkdir node-cors && cd node-cors
npm init
cnpm i --save-dev koa
mkdir jsonp && cd jsonp
touch index.html
touch server.js
后端代碼
由于JSONP
的實(shí)現(xiàn)需要前后端配合记罚,先來寫一下后端的實(shí)現(xiàn):
(看不懂沒關(guān)系,下面的前端簡單實(shí)現(xiàn)會(huì)做解釋)
const Koa = require('koa');
const app = new Koa();
const items = [{ id: 1, title: 'title1' }, { id: 2, title: 'title2' }]
app.use(async (ctx, next) => {
if (ctx.path === '/api/jsonp') {
const { cb, id } = ctx.query;
const title = items.find(item => item.id == id)['title']
ctx.body = `${cb}(${JSON.stringify({title})})`;
return;
}
})
console.log('listen 8080...')
app.listen(8080);
寫完之后壳嚎,保存桐智。
并在jsonp
這個(gè)文件夾下執(zhí)行:
node server.js
來啟動(dòng)服務(wù),可以看到編輯器的控制臺(tái)中會(huì)打印出"listen 8080..."
前端簡單實(shí)現(xiàn)
OK??诬辈,后端已經(jīng)實(shí)現(xiàn)了,現(xiàn)在讓我們來看看前端最簡單的一種實(shí)現(xiàn)方式荐吉,也就是寫死一個(gè)script
并發(fā)送請(qǐng)求:
index.html
中:
<script type='text/javascript'>
window.jsonpCallback = function (res) {
console.log(res)
}
</script>
<script src='http://localhost:8080/api/jsonp?id=1&cb=jsonpCallback' type='text/javascript'></script>
這兩個(gè)script
的意思是:
- 第一個(gè)焙糟,創(chuàng)建一個(gè)
jsonpCallback
函數(shù)。但是它還沒有被調(diào)用 - 第二個(gè)样屠,加載
src
中的資源穿撮,并等待請(qǐng)求的內(nèi)容返回
整個(gè)過程就是:
當(dāng)執(zhí)行到第二個(gè)
script
的時(shí)候缺脉,由于請(qǐng)求了我們的8080
端口,并且把id
和cb
這兩個(gè)參數(shù)放到URL
里悦穿。那么后臺(tái)就可以拿到URL
里的這兩個(gè)參數(shù)攻礼。也就是在后端代碼中的
const { id, cb } = ctx.query
這里獲取到了。那么后端在拿到這兩個(gè)參數(shù)之后栗柒,可能就會(huì)根據(jù)
id
來進(jìn)行一些查詢礁扮,當(dāng)然,我這里只是模擬的查詢瞬沦,用了一個(gè)簡單的find
來進(jìn)行一個(gè)查找太伊。查找到id
為1
的那項(xiàng)并且取title
。第二個(gè)參數(shù)
cb
逛钻,拿到的就是"jsonpCallback"
了僚焦,這里也就是告訴后端,前端那里是會(huì)有一個(gè)叫做jsonpCallback
的函數(shù)來接收后端想要返回的數(shù)據(jù)曙痘,而后端你只需要在返回體中寫入jsonpCallback()
就可以了芳悲。前端在得到了后端返回的內(nèi)容
jsonpCallback({"title":"title1"})
,發(fā)現(xiàn)里面是一段執(zhí)行函數(shù)的語句边坤,因此就會(huì)去執(zhí)行第一個(gè)script
中的jsonpCallback
方法了名扛,并且又是帶了參數(shù)的,所以此時(shí)瀏覽器控制臺(tái)會(huì)打印出{ title: 'title1' }
以此來達(dá)到一個(gè)簡單的跨域的效果惩嘉。
其實(shí)你想想罢洲,如果我們把第二個(gè)script
標(biāo)簽換成以下代碼,是不是也能達(dá)到同樣的效果呢文黎?
<!-- <script src='http://localhost:8080/api/jsonp?id=1&cb=jsonpCallback' type='text/javascript'></script> -->
<script type="text/javascript">
jsonpCallback({ title: 'title1' })
</script>
jQuery中ajax實(shí)現(xiàn)
上面??我們介紹了用script
標(biāo)簽來實(shí)現(xiàn)惹苗,在jQuery
的$.ajax()
方法其實(shí)也提供了jsonp
。
讓我們一起來看看:
<script src="https://cdn.bootcss.com/jquery/3.5.0/jquery.min.js"></script>
<script>
$.ajax({
url: "http://localhost:8080/api/jsonp",
dataType: "jsonp",
type: "get",
data: {
id: 1
},
jsonp: "cb",
success: function (data) {
console.log(data);
}
});
</script>
在success
回調(diào)中同樣可以拿到數(shù)據(jù)耸峭。
封裝一個(gè)JSONP方法
(此章會(huì)一步一步教你如何封裝一個(gè)比較完美的JSONP
方法)
簡易版
先看下我們要實(shí)現(xiàn)的功能
定義一個(gè)JSONP
方法桩蓉,它接收四個(gè)參數(shù):
- url
- params
- callbackKey:與后臺(tái)約定的回調(diào)函數(shù)是用哪個(gè)字段(如
cb
) - callback:拿到數(shù)據(jù)之后執(zhí)行的回調(diào)函數(shù)
<script>
function JSONP({
url,
params = {},
callbackKey = 'cb',
callback
}) {
// 定義本地的一個(gè)callback的名稱
const callbackName = 'jsonpCallback';
// 把這個(gè)名稱加入到參數(shù)中: 'cb=jsonpCallback'
params[callbackKey] = callbackName;
// 把這個(gè)callback加入到window對(duì)象中,這樣就能執(zhí)行這個(gè)回調(diào)了
window[callbackName] = callback;
// 得到'id=1&cb=jsonpCallback'
const paramString = Object.keys(params).map(key => {
return `${key}=${params[key]}`
}).join('&')
// 創(chuàng)建 script 標(biāo)簽
const script = document.createElement('script');
script.setAttribute('src', `${url}?${paramString}`);
document.body.appendChild(script);
}
JSONP({
url: 'http://localhost:8080/api/jsonp',
params: { id: 1 },
callbackKey: 'cb',
callback (res) {
console.log(res)
}
})
</script>
這樣寫打開頁面也可是可以看到效果的劳闹。
同時(shí)多個(gè)請(qǐng)求
上面我們雖然實(shí)現(xiàn)了JSONP
院究,但有一個(gè)問題,那就是如果我同時(shí)多次調(diào)用JSONP
:
JSONP({
url: 'http://localhost:8080/api/jsonp',
params: { id: 1 },
callbackKey: 'cb',
callback (res) {
console.log(res) // No.1
}
})
JSONP({
url: 'http://localhost:8080/api/jsonp',
params: { id: 2 },
callbackKey: 'cb',
callback (res) {
console.log(res) // No.2
}
})
可以看到這里我調(diào)用了兩次JSONP
本涕,只是傳遞的參數(shù)不同业汰。但是并不會(huì)按我們預(yù)期的在No.1和No.2
中分別打印,而是都會(huì)在No.2
中打印出結(jié)果菩颖。這是因?yàn)楹竺嬉粋€(gè)callback
把JSONP
里封裝的第一個(gè)callback
給覆蓋了样漆,它們都是共用的同一個(gè)callbackName
,也就是jsonpCallback
晦闰。如下所示:
兩次結(jié)果都是從76
行打印出來的放祟。
所以我們得改造一下上面的JSONP
方法:
- 讓
callbackName
是一個(gè)唯一的鳍怨,可以使用遞增 - 不要把回調(diào)定義在
window
中這樣會(huì)污染全局變量,可以把它扔到JSON.xxx
中
OK??跪妥,來看看改造之后的代碼:
<script>
function JSONP({
url,
params = {},
callbackKey = 'cb',
callback
}) {
// 定義本地的唯一callbackId鞋喇,若是沒有的話則初始化為1
JSONP.callbackId = JSONP.callbackId || 1;
let callbackId = JSONP.callbackId;
// 把要執(zhí)行的回調(diào)加入到JSON對(duì)象中,避免污染window
JSONP.callbacks = JSONP.callbacks || [];
JSONP.callbacks[callbackId] = callback;
// 把這個(gè)名稱加入到參數(shù)中: 'cb=JSONP.callbacks[1]'
params[callbackKey] = `JSONP.callbacks[${callbackId}]`;
// 得到'id=1&cb=JSONP.callbacks[1]'
const paramString = Object.keys(params).map(key => {
return `${key}=${params[key]}`
}).join('&')
// 創(chuàng)建 script 標(biāo)簽
const script = document.createElement('script');
script.setAttribute('src', `${url}?${paramString}`);
document.body.appendChild(script);
// id自增眉撵,保證唯一
JSONP.callbackId++;
}
JSONP({
url: 'http://localhost:8080/api/jsonp',
params: { id: 1 },
callbackKey: 'cb',
callback (res) {
console.log(res)
}
})
JSONP({
url: 'http://localhost:8080/api/jsonp',
params: { id: 2 },
callbackKey: 'cb',
callback (res) {
console.log(res)
}
})
</script>
可以看到現(xiàn)在調(diào)用了兩次回調(diào)侦香,但是會(huì)分別執(zhí)行JSONP.callbacks[1]
和JSONP.callbacks[2]
:
最終版JSONP方法
其實(shí)上面已經(jīng)算比較完美了,但是還會(huì)有一個(gè)小問題执桌,比如下面這種情況:
我改一下后端的代碼
const Koa = require('koa');
const app = new Koa();
const items = [{ id: 1, title: 'title1' }, { id: 2, title: 'title2' }]
app.use(async (ctx, next) => {
if (ctx.path === '/api/jsonp') {
const { cb, id } = ctx.query;
const title = items.find(item => item.id == id)['title']
ctx.body = `${cb}(${JSON.stringify({title})})`;
return;
}
if (ctx.path === '/api/jsonps') {
const { cb, a, b } = ctx.query;
ctx.body = `${cb}(${JSON.stringify({ a, b })})`;
return;
}
})
console.log('listen 8080...')
app.listen(8080);
增加了一個(gè)/api/jsonps
的接口鄙皇。
然后前端代碼增加了一個(gè)這樣的請(qǐng)求:
JSONP({
url: 'http://localhost:8080/api/jsonps',
params: {
a: '2&b=3',
b: '4'
},
callbackKey: 'cb',
callback (res) {
console.log(res)
}
})
可以看到,參數(shù)的a
中也會(huì)有b
這個(gè)字符串仰挣,這樣就導(dǎo)致我們獲取到的數(shù)據(jù)不對(duì)了:
后臺(tái)并不知道a
的參數(shù)是一個(gè)字符串伴逸,它只會(huì)按照&
來截取參數(shù)。
所以為了解決這個(gè)問題膘壶,可以使用URI編碼错蝴。
也就是使用:
encodeURIComponent('2&b=3')
// 結(jié)果為
"2%26b%3D3"
只需要改一下JSONP
方法中參數(shù)的生成:
// 得到'id=1&cb=JSONP.callbacks[1]'
const paramString = Object.keys(params).map(key => {
return `${key}=${encodeURIComponent(params[key])}`
}).join('&')
來看一下完整版的JSONP
方法:
<script>
function JSONP({
url,
params = {},
callbackKey = 'cb',
callback
}) {
// 定義本地的唯一callbackId,若是沒有的話則初始化為1
JSONP.callbackId = JSONP.callbackId || 1;
let callbackId = JSONP.callbackId;
// 把要執(zhí)行的回調(diào)加入到JSON對(duì)象中颓芭,避免污染window
JSONP.callbacks = JSONP.callbacks || [];
JSONP.callbacks[callbackId] = callback;
// 把這個(gè)名稱加入到參數(shù)中: 'cb=JSONP.callbacks[1]'
params[callbackKey] = `JSONP.callbacks[${callbackId}]`;
// 得到'id=1&cb=JSONP.callbacks[1]'
const paramString = Object.keys(params).map(key => {
return `${key}=${encodeURIComponent(params[key])}`
}).join('&')
// 創(chuàng)建 script 標(biāo)簽
const script = document.createElement('script');
script.setAttribute('src', `${url}?${paramString}`);
document.body.appendChild(script);
// id自增顷锰,保證唯一
JSONP.callbackId++;
}
JSONP({
url: 'http://localhost:8080/api/jsonps',
params: {
a: '2&b=3',
b: '4'
},
callbackKey: 'cb',
callback (res) {
console.log(res)
}
})
JSONP({
url: 'http://localhost:8080/api/jsonp',
params: {
id: 1
},
callbackKey: 'cb',
callback (res) {
console.log(res)
}
})
</script>
注意??:
encodeURI
和encodeURIComponent
的區(qū)別:
-
encodeURI()
不會(huì)對(duì)本身屬于URI的特殊字符進(jìn)行編碼,例如冒號(hào)亡问、正斜杠官紫、問號(hào)和井字號(hào); - 而
encodeURIComponent()
則會(huì)對(duì)它發(fā)現(xiàn)的任何非標(biāo)準(zhǔn)字符進(jìn)行編碼
例如:
var url = 'https://lindaidai.wang'
encodeURI(url) // "https://lindaidai.wang"
encodeURIComponent(url) // "https%3A%2F%2Flindaidai.wang"
另外州藕,可以使用decodeURIComponent
來解碼束世。
decodeURIComponent("https%3A%2F%2Flindaidai.wang")
// 'https://lindaidai.wang'