原文地址javascript
倉庫地址html
jsonp(JSON with padding)你必定不會陌生,前端向後端拿數據的方式之一,也是處理跨域請求的得利助手。前端
咱們早已習慣,早已熟練了jQ或者zepto的ajax調用方式。可是有可能還不太它內部具體是如何實現一個jsonp的,從請求的發出,到指定的成功(success)或失敗(error)回調函數的執行。java
這中間前端須要作什麼?git
後端又須要作些什麼來支持?github
超時場景又該如何處理?ajax
整個生命週期會有多個鉤子能夠被觸發,而咱們能夠監聽哪些鉤子來得知請求的情況?json
讓咱們從zepto.js的源碼出發,一步步揭開它的面紗。後端
(該篇文章重點是想說jsonp實現過程,若是你想了解跨域相關的更多的知識,能夠谷歌,度娘一把)api
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,模擬服務端的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的實現原理說清楚了,歡迎你們拍磚。
若是對你有一點點幫助,點擊這裏,加一個小星星好很差呀
若是對你有一點點幫助,點擊這裏,加一個小星星好很差呀
若是對你有一點點幫助,點擊這裏,加一個小星星好很差呀