前言
jsonp(JSON with padding)你一定不會陌生,前端向後端拿資料的方式之一,也是處理跨域請求的得利助手。
我們早已習慣,早已熟練了jQ或者zepto的ajax呼叫方式。但是有可能還不太它內部具體是如何實現一個jsonp的,從請求的發出,到指定的成功(success)或失敗(error)回撥函式的執行。
- 這中間前端需要做什麼?
- 後端又需要做些什麼來支援?
- 超時場景又該如何處理?
- 整個生命週期會有多個鉤子可以被觸發,而我們可以監聽哪些鉤子來得知請求的狀況?
讓我們從zepto.js的原始碼出發,一步步揭開它的面紗。
(該篇文章重點是想說jsonp實現過程,如果你想了解跨域相關的更多的知識,可以谷歌,度娘一把)
絮叨一下jsonp的基本原理
jsonp是伺服器與客戶端跨源通訊的常用方法之一,具有簡單易用,瀏覽器相容性好等特點。
基本思想是啥呢
客戶端利用
script
標籤可以跨域請求資源的性質,向網頁中動態插入script
標籤,來向服務端請求資料。服務端會解析請求的
url
,至少拿到一個回撥函式(比如callback=myCallback
)引數,之後將資料放入其中返回給客戶端。當然jsonp不同於平常的
ajax
請求,它僅僅支援get型別的方式
如何使用
這裡簡單的介紹一下zepto.js是如果使用jsonp形式請求資料的,然後從使用的角度出發一步步分析原始碼實現。
使用
$.ajax({
url: 'http://www.abc.com/api/xxx', // 請求的地址
type: 'get', // 當然引數可以省略
data: { // 傳給服務端的資料,被載入url?的後面
name: 'qianlongo',
sex: 'boy'
},
dataType: 'jsonp', // 預期伺服器返回的資料型別
jsonpCallback: 'globalCallback', // 全域性JSONP回撥函式的 字串(或返回的一個函式)名
timeout: 100, // 以毫秒為單位的請求超時時間, 0 表示不超時。
success: function (data) { // 請求成功之後呼叫
console.log('successCallback')
console.log(data)
},
error: function (err) { // 請求出錯時呼叫。 (超時,解析錯誤,或者狀態碼不在HTTP 2xx)
console.log('errorCallback')
console.log(err)
},
complete: function (data) { // 請求完成時呼叫,無論請求失敗或成功。
console.log('compelete')
console.log(data)
}
})
function globalCallback (data) {
console.log('globalCallback')
console.log(data)
}複製程式碼
在zepto中一個常見的jsonp請求配置就是這樣了,大家都很熟悉了。但是不知道大家有沒有發現.
- 如果設定了
timeout
超時了,並且沒有設定jsonpCallback
欄位,那麼控制檯幾乎都會出現一處報錯,如下圖
- 同樣還是發生在
timeout
,此時如果請求超時了,並且設定了jsonpCallback
欄位(注意這個時候是設定了),但是如果請求在超時之後完成了,你的jsonpCallback
還是會被執行。照理說這個函式應該是請求在超時時間內完成才會被執行啊!為毛這個時候超時了,還是會被執行啊!!!
不急等我們一步步分析完就會知道這個答案了。
先看一下完整的程式碼
因為zepto中完成jsonp請求的處理基本都在
$.ajaxJSONP
完成,我們直接從該函式出發開始分析。先整體看看這個函式,有一個大概的印象,已經加了大部分註釋。或者可以點選這裡檢視
$.ajaxJSONP = function (options, deferred) {
// 直接調ajaxJSONP沒有傳入type,去走$.ajax
if (!('type' in options)) return $.ajax(options)
// 獲取callback函式名,此時未指定為undefined
var _callbackName = options.jsonpCallback,
// jsonpCallback可以是一個函式或者一個字串
// 是函式時,執行該函式拿到其返回值作為callback函式
// 為字串時直接賦值
// 沒有傳入jsonpCallback,那麼使用類似'Zepto3726472347'作為函式名
callbackName = ($.isFunction(_callbackName) ?
_callbackName() : _callbackName) || ('Zepto' + (jsonpID++)),
// 建立一個script標籤用來傳送請求
script = document.createElement('script'),
// 先讀取全域性的callbackName函式,因為後面會對該函式重寫,所以需要先儲存一份
originalCallback = window[callbackName],
responseData,
// 中止請求,觸發script元素上的error事件, 後面帶的引數是回撥函式接收的引數
abort = function (errorType) {
$(script).triggerHandler('error', errorType || 'abort')
},
xhr = { abort: abort }, abortTimeout
if (deferred) deferred.promise(xhr)
// 給script元素新增load和error事件
$(script).on('load error', function (e, errorType) {
// 清除超時定時器
clearTimeout(abortTimeout)
// 移除新增的元素(注意這裡還off了,不然超時這種情況,請求回來了,還是會走回撥)
$(script).off().remove()
// 請求出錯或後端沒有給callback中塞入資料,將觸發error
if (e.type == 'error' || !responseData) {
ajaxError(null, errorType || 'error', xhr, options, deferred)
} else {
// 請求成功,呼叫成功回撥,請塞入資料responseData[0]
ajaxSuccess(responseData[0], xhr, options, deferred)
}
// 將originalCallback重新賦值回去
window[callbackName] = originalCallback
// 並且判斷originalCallback是不是個函式,如果是函式,便執行
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0])
// 清空閉包,釋放空間
originalCallback = responseData = undefined
})
if (ajaxBeforeSend(xhr, options) === false) {
abort('abort')
return xhr
}
// 重寫全域性上的callbackName
window[callbackName] = function () {
responseData = arguments
}
// 將回撥函式名追加到?後面
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
// 新增script元素
document.head.appendChild(script)
// 超時處理函式
if (options.timeout > 0) abortTimeout = setTimeout(function () {
abort('timeout')
}, options.timeout)
return xhr
}複製程式碼
引數的基本處理
在執行原理的第一步時,zepto會先處理一下我們傳入的引數。
我們先來看看針對上面的例子我們傳送請求的url最終會變成什麼樣子,而引數處理正是為了得到這條url
傳了jsonpCallback時的url
http://www.abc.com/api/xxx?name=qianlongo&sex=boy&_=1497193375213&callback=globalCallback
沒有傳jsonpCallback時的url
http://www.abc.com/api/xxx?name=qianlongo&sex=boy&_=1497193562726&callback=Zepto1497193562723
相信你已經看出來這兩條url有什麼不同之處了。
_後面跟的時間戳不一樣
callback後面跟的回撥函式名字不一樣
也就是說如果你指定了成功的回撥函式就用你的,沒指定他自己生成一個。
上引數處理程式碼
var jsonpID = +new Date()
var _callbackName = options.jsonpCallback,
callbackName = ($.isFunction(_callbackName) ?
_callbackName() : _callbackName) || ('Zepto' + (jsonpID++))複製程式碼
對於回撥函式名的處理其實挺簡單的,根據你是否在引數中傳了jsonpCallback
,傳了是個函式就用函式的返回值,不是函式就直接用。
否則的話,就生成類似Zepto1497193562723
的函式名。
繼續看
// 建立一個script標籤用來傳送請求
script = document.createElement('script'),
// 先讀取全域性的callbackName函式,因為後面會對該函式重寫,所以需要先儲存一份
originalCallback = window[callbackName],
// 請求完成後拿到的資料
responseData,
// 中止請求,觸發script元素上的error事件, 後面帶的引數是回撥函式接收的引數
abort = function (errorType) {
$(script).triggerHandler('error', errorType || 'abort')
},
xhr = { abort: abort }, abortTimeout
// 對.then或者.catch形式呼叫的支援,本文暫時不涉及這方面的解析
if (deferred) deferred.promise(xhr)複製程式碼
好啦,看到這裡我們主要要關注的是
originalCallback = window[callbackName]
abort
函式
對於1為什麼要把全域性的callbackName
函式先儲存一份呢?這裡涉及到一個問題。
請求回來的時候到底是不是直接執行的你傳入的jsonpCallback函式?
解決這個問題請看
// 重寫全域性上的callbackName
window[callbackName] = function () {
responseData = arguments
}複製程式碼
zepto中把全域性的callbackName
函式給重寫掉了,,導致後端返回資料時執行該函式,就幹了一件事,就是把資料賦值給了responseData
這個變數。
那說好的真正的callbackName
函式呢? 如果我傳了jsonpCallback
,我是會在裡面做一些業務邏輯的啊,你都把我給重寫了,我的邏輯怎麼辦?先留個疑問在這裡
對於關注點2abort函式
,這個函式的功能,就是手動觸發新增在建立好的script
元素身上的error
事件的回撥函式。後面的超時處理timeout
以及請求出錯都是利用的該函式。
超時處理
在看監聽
script
元素on error
事件回撥邏輯前,我們直接看最後一點東西
// 將回撥函式名追加到?後面
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
// 新增script元素
document.head.appendChild(script)
// 超時處理函式
if (options.timeout > 0) abortTimeout = setTimeout(function () {
abort('timeout')
}, options.timeout)複製程式碼
代理做了簡單的註釋,這裡除了將script
元素插入網頁還定義了一個超時處理函式,判斷條件是傳入的引數timeout
是否大於0,所以當你傳小於0或者負數啥的進去,是不會當做超時處理的。超時後其實就是觸發了script
元素的error
事件,並傳了引數timeout
真正的回撥邏輯處理
接下來就是本文的重點了,zepto通過監聽
script
元素的load
事件來監聽請求是否完成,以及給script
新增了error
事件,方便請求出錯和超時處理。而使用者需要的成功和失敗的處理也是在這裡面完成
clearTimeout(abortTimeout)
$(script).off().remove()
if (e.type == 'error' || !responseData) {
ajaxError(null, errorType || 'error', xhr, options, deferred)
} else {
ajaxSuccess(responseData[0], xhr, options, deferred)
}
window[callbackName] = originalCallback
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0])
originalCallback = responseData = undefined複製程式碼
script
元素真正的事件處理程式程式碼也不多,開頭有這兩句話
// 清楚超時定時器
clearTimeout(abortTimeout)
// 從網頁中移除建立的script元素以及將掛在它上面的所有事件都移除
$(script).off().remove()複製程式碼
起什麼作用呢?
第一句自然是針對超時處理,如果請求在指定超時時間之前完成,自然是要把他清除一下,不然指定的時間到了,超時的回撥還是會執行,這是不對的。
第二句話,把建立的script元素從網頁中給刪除掉,繫結的事件('load error')也全部移除,幹嘛要把事件都給移除呢?你想想,一個請求已經發出去了,我們還能讓他半途停止嗎?該是不能吧,但是我們能夠阻止請求回來之後要做的事情呀!而這個回撥不就是請求回來之後要做的事情麼。
請求成功或失敗的處理
if (e.type == 'error' || !responseData) {
ajaxError(null, errorType || 'error', xhr, options, deferred)
} else {
ajaxSuccess(responseData[0], xhr, options, deferred)
}複製程式碼
那麼再接下來,就是請求的成功或失敗的處理了。失敗的條件就是觸發了error
事件(不管是超時還是解析錯誤,又或者狀態碼不在HTTP 2xx),甚至如果後端沒有正確給到資料responseData
也是錯誤。
再回顧一下responseData是怎麼來的
// 重寫全域性上的callbackName
window[callbackName] = function () {
responseData = arguments
}複製程式碼
ajaxErro函式究竟做了些啥事呢?
ajaxError
// type: "timeout", "error", "abort", "parsererror"
function ajaxError(error, type, xhr, settings, deferred) {
var context = settings.context
// 執行使用者傳進去的error函式,注意這裡的context決定了error函式中的this執行
settings.error.call(context, xhr, type, error)
if (deferred) deferred.rejectWith(context, [xhr, type, error])
// 觸發全域性的鉤子ajaxError
triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type])
// 呼叫ajaxComplete函式
ajaxComplete(type, xhr, settings)
}複製程式碼
可以看到他呼叫了我們穿進去的error
函式,並且觸發了全域性的ajaxError
鉤子,所以我們其實可以在document
上監聽一個鉤子
$(document).on('ajaxError', function (e) {
console.log('ajaxError')
console.log(e)
})複製程式碼
這個時候便可以拿到請求出錯的資訊了
ajaxComplete
// status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
function ajaxComplete(status, xhr, settings) {
var context = settings.context
// 呼叫傳進來的complete函式
settings.complete.call(context, xhr, status)
// 觸發全域性的ajaxComplete鉤子
triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])
// 請求結束
ajaxStop(settings)
}複製程式碼
ajaxStop
function ajaxStop(settings) {
if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
}複製程式碼
同理我們可以監聽ajaxComplete
和ajaxStop
鉤子
$(document).on('ajaxComplete ajaxStop', function (e) {
console.log('ajaxComplete')
console.log(e)
})複製程式碼
處理完失敗的情況那麼接下來就是成功的處理了,主要呼叫了ajaxSuccess
函式
ajaxSuccess
function ajaxSuccess(data, xhr, settings, deferred) {
var context = settings.context, status = 'success'
// 呼叫傳進來的成功的回撥函式
settings.success.call(context, data, status, xhr)
if (deferred) deferred.resolveWith(context, [data, status, xhr])
// 觸發全域性的ajaxSuccess
triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data])
// 執行請求完成的回撥,成功和失敗都執行了該回撥
ajaxComplete(status, xhr, settings)
}複製程式碼
原來我們平時傳入的success
函式是在這裡被執行的。但是有一個疑問啊!,我們知道我們是可以不傳入success
函式的,當我們指定jsonpCallback
的時,請求成功同樣會走jsonpCallback
函式,但是好像ajaxSuccess
沒有執行這個函式,具體在處理的呢?
繼續往下看
// 重寫全域性上的callbackName
window[callbackName] = function () {
responseData = arguments
}
// 將originalCallback重新賦值回去
window[callbackName] = originalCallback
// 並且判斷originalCallback是不是個函式,如果是函式,便執行
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0])複製程式碼
為了徹底搞清楚zepto把我們指定的回撥函式重寫的原因,我再次加了重寫的程式碼在這裡。可以看出,重寫的目的,就是為了拿到後端返回的資料,而拿到資料之後便方便我們在其他地方靈活的處理了,當然指定的回撥函式還是要重新賦值回去(這也是開頭要保留一份該函式的本質原因),如果是個函式,就將資料,塞進去執行。
分析到這裡我相信你已經幾乎明白了jsonp實現的基本原理,文章頂部說的幾個問題,我們也在這個過程中解答了。
- 這中間前端需要做什麼?
- 後端又需要做些什麼來支援?(接下來以例子說明)
- 超時場景又該如何處理?
- 整個生命週期會有多個鉤子可以被觸發,而我們可以監聽哪些鉤子來得知請求的狀況?
砰砰砰!!!,親們還記得開頭的時候留了這兩個問題嗎?
在zepto中一個常見的jsonp請求配置就是這樣了,大家都很熟悉了。但是不知道大家有沒有發現.
- 如果設定了
timeout
超時了,並且沒有設定jsonpCallback
欄位,那麼控制檯幾乎都會出現一處報錯,如下圖
- 同樣還是發生在
timeout
,此時如果請求超時了,並且設定了jsonpCallback
欄位(注意這個時候是設定了),但是如果請求在超時之後完成了,你的jsonpCallback
還是會被執行。照理說這個函式應該是請求在超時時間內完成才會被執行啊!為毛這個時候超時了,還是會被執行啊!!!
問題1:為什麼會報錯呢?
對於沒有指定jsonpCallback
此時我們給後端的回撥函式名是類似Zepto1497193562723
window[callbackName] = originalCallback複製程式碼
超時的時候同樣會走load error
的回撥,當這句話執行的時候,Zepto1497193562723
被設定成了undefined,當然後端返回資料的時候去執行
Zepto1497193562723({xxx: 'yyy'})複製程式碼
自然就報錯了。
問題2呢? 其實同樣還是上面那句話,只不過此時我們指定了jsonpCallback
,超時的時候雖然取消了script
元素的的load error
事件,意味著在超時之後請求即便回來了,也不會走到對應的回撥函式中去。但是別忘記,超時我們手動觸發了script
元素的error
事件
$(script).triggerHandler('error', errorType || 'abort')複製程式碼
原本被重寫的callback函式也會被重新賦值回去,此刻,即便script
元素的load error
回撥不會被執行,但我們指定的jsonpCallback
還是會被執行的。這也就解了問題2.
用koa做服務端,zepto發jsonp請求
最後我們再用koa,模擬服務端的api,用zepto來請求他。
如果你對原始碼感興趣可以點選這裡檢視koa-todo-list
找到根目錄的testJsonp.js
檔案即是服務端主要程式碼
前端程式碼
html
<button>請求後端jsonp資料</button>複製程式碼
js
$('button').on('click', () => {
$.ajax({
type: 'get',
url: '/showData',
data: {
name: 'qianlongo',
sex: 'boy'
},
dataType: "jsonp",
success: function (res) {
console.log('success')
console.log(res)
$('<pre>').text(JSON.stringify(res)).appendTo('body')
},
error: function (res) {
console.log('error')
console.log(res)
}
})
})複製程式碼
服務端主要程式碼
var koa = require('koa');
var route = require('koa-route');
var path = require('path');
var parse = require('co-body');
var render = require('./app/lib/render.js');
var app = koa();
app.use(route.get('/showJsonpPage', showJsonpPage))
app.use(route.get('/showData', showData))
function * showJsonpPage () {
var sHtml = yield render('jsonp')
this.body = sHtml
}
function * showData (next) {
let {callback, name, sex, randomNum} = this.query
this.type = 'text/javascript'
let callbackData = {
status: 0,
message: 'ok',
data: {
name,
sex,
randomNum
}
}
this.body = `${callback}(${JSON.stringify(callbackData)})`
console.log(this.query)
}
app.listen(3000);
console.log('listening port 3000');複製程式碼
執行截圖
結尾
希望把jsonp的實現原理說清楚了,歡迎大家拍磚。
如果對你有一點點幫助,點選這裡,加一個小星星好不好呀
如果對你有一點點幫助,點選這裡,加一個小星星好不好呀
如果對你有一點點幫助,點選這裡,加一個小星星好不好呀