摘要:但是有可能還不太它內部具體是如何實現一個的,從請求的發出,到指定的成功或失敗回調函數的執行。服務端會解析請求的至少拿到一個回調函數比如參數之后將數據放入其中返回給客戶端。
前言
原文地址
倉庫地址
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
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) $("").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的實現原理說清楚了,歡迎大家拍磚。
如果對你有一點點幫助,點擊這里,加一個小星星好不好呀
如果對你有一點點幫助,點擊這里,加一個小星星好不好呀
如果對你有一點點幫助,點擊這里,加一個小星星好不好呀
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87049.html
摘要:形如源代碼在的原型上添加了相關方法。類似源代碼每個表單的和都通過編碼最后通過符號分割有了的基礎,就是將相應的和都通過編碼,然后用符號進行分割,也就達到了我們要的結果。 前言 JavaScript最初的一個應用場景就是分擔服務器處理表單的責任,打破處處依賴服務器的局面,這篇文章主要介紹zepto中form模塊關于表單處理的幾個方法,serialize、serializeArray、sub...
摘要:對象字面量形式通過構造函數創建中是如何判斷的呢判斷是否為純粹的對象,必須滿足首先必須是對象不是對象并且原型要和的原型相等方法返回指定對象的原型即內部屬性的值如果沒有繼承屬性,則返回。 前言 時間過得可真快,轉眼間2017年已去大半有余,你就說嚇不嚇人,這一年你成長了多少,是否荒度了很多時光,亦或者天天向上,收獲滿滿。今天主要寫一些看Zepto基礎模塊時,比較實用的部分內部方法,在我們日...
閱讀 1002·2021-09-30 09:58
閱讀 2829·2021-09-09 11:55
閱讀 2001·2021-09-01 11:41
閱讀 991·2019-08-30 15:55
閱讀 3350·2019-08-30 12:50
閱讀 3495·2019-08-29 18:37
閱讀 3295·2019-08-29 16:37
閱讀 2011·2019-08-29 13:00