簡要:$.ajax是zepto發送請求的核心方法,$.get,$.post,$.jsonp都是封裝了$.ajax方法。$.ajax將jsonp與異步請求的代碼格式統一起來,內部主要是先處理url,數據和請求頭部然后新建XMLHttpRequest對象發送請求。
代碼如下:
/** * ajax 請求 */ $.ajax = function(options){ var settings = $.extend({}, options || {}), //創建新的options對象,不影響options的值,創建新的副本 deferred = $.Deferred && $.Deferred(), //設置異步隊列 urlAnchor, hashIndex; //未傳 $.ajaxSettings里的值,復制$.ajaxSettings的mm值 for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key] //觸發全局事件 'ajaxStart',$(document).trigger('ajaxStart') ajaxStart(settings) //是否設置了跨域,未設置,需通過ip 協議 端口一致來判斷跨域 if (!settings.crossDomain) { urlAnchor = document.createElement('a') //如果沒有設置請求地址,則取當前頁面地址 urlAnchor.href = settings.url // cleans up URL for .href (IE only), see https://github.com/madrobby/zepto/pull/1049 urlAnchor.href = urlAnchor.href /* * for IE * a = document.createElement('a') * <a></a> * a.href = "/foobar" * "/foobar" * a.host * "" * a.href * "http://192.168.1.198/foobar" * a.href = a.href * "http://192.168.1.198/foobar" * a.host * "http://192.168.1.198:80" * */ // originAnchor.href = window.location.href; //通過ip 協議 端口來判斷跨域 location.host = host:port settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host) } //未設置url,取當前地址欄 if (!settings.url) settings.url = window.location.toString() //如果有hash,截掉hash,因為hash ajax不會傳遞到后台,舍棄#和#后面的 if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex) //將data進行轉換 serializeData(settings) // /\?.+=\?/.test('http://www.zhutao.cn/index.html?a=1?callback=?') // true //TODO: /\?.+=\?/.test(settings.url) 有xxx.html?a=1?=cccc類似形式,為jsonp var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url); if (hasPlaceholder) dataType = 'jsonp' //不設置緩存,加時間戳 '_=' + Date.now() // 當settings.cache === null時 if (settings.cache === false || ( (!options || options.cache !== true) && ('script' == dataType || 'jsonp' == dataType) )) //Date.now() == 1471504727756 settings.url = appendQuery(settings.url, '_=' + Date.now()) //如果是jsonp,調用$.ajaxJSONP,不走XHR,走script if ('jsonp' == dataType) { if (!hasPlaceholder) //判斷url是否有類似jsonp的參數 settings.url = appendQuery(settings.url, settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?') return $.ajaxJSONP(settings, deferred) } var mime = settings.accepts[dataType], //媒體類型 headers = { }, //TODO 為什么不直接用 headers[name.toLowerCase()] = [name, value] setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value] }, //設置請求頭的方法 //如果URL沒協議,讀取本地URL的協議 protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol, //獲取異步傳輸對象 xhr = settings.xhr(), nativeSetHeader = xhr.setRequestHeader, abortTimeout //將xhr設為只讀Deferred對象,不能更改狀態 if (deferred) deferred.promise(xhr) //如果沒有跨域 // x-requested-with XMLHttpRequest //表明是AJax異步 //x-requested-with null//表明同步,瀏覽器工具欄未顯示,在后台request可以獲取到 if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest') setHeader('Accept', mime || '*/*') //默認接受任何類型 // 先走||運算,再= if (mime = settings.mimeType || mime) { //媒體數據源里對應多個,如 script: 'text/javascript, application/javascript, application/x-javascript', //設置為最新的寫法, text/javascript等都是老瀏覽廢棄的寫法 if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0]; //對Mozilla的修正 // 來自服務器的響應沒有 XML mime-type 頭部(header),則一些版本的 Mozilla瀏覽器不能正常運行。 // 對於這種情況,xhr.overrideMimeType(mime); 語句將覆蓋發送給服務器的頭部,強制mime 作為 mime-type。*/ xhr.overrideMimeType && xhr.overrideMimeType(mime) } //如果不是Get請求,設置Content-Type //Content-Type: 內容類型 指定響應的 HTTP內容類型。決定瀏覽器將以什么形式、什么編碼讀取這個文件. 如果未指定 ContentType,默認為TEXT/HTML。 /** application/x-www-form-urlencoded:是一種編碼格式,窗體數據被編碼為名稱/值對,是標准的編碼格式。 當action為get時候,瀏覽器用x-www-form-urlencoded的編碼方式把form數據轉換成一個字串(name1=value1&name2=value2...),然后把這個字串append到url后面,用?分割,加載這個新的url。 當action為post時候,瀏覽器把form數據封裝到http body中,然后發送到server **/ /** * 如果有 type=file的話,需要設為multipart/form-data了。瀏覽器會把整個表單以控件為單位分割,並為每個部分加上 Content-Disposition(form-data或者file),Content-Type(默認為text/plain),name(控件 name)等信息,並加上分割符(boundary)。 */ // 如果method==get,則請求頭部不用設置Content-Type,若method==post,則請求頭部的Content-Type默認設置'application/x-www-form-urlencoded' // 請求頭部的Content-Type 表示參數的傳遞形式 if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET')) setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded') //設置請求頭 if (settings.headers) for (name in settings.headers) setHeader(name, settings.headers[name]) xhr.setRequestHeader = setHeader xhr.onreadystatechange = function(){ /** 0:請求未初始化(還沒有調用 open())。 1:請求已經建立,但是還沒有發送(還沒有調用 send())。 2:請求已發送,正在處理中(通常現在可以從響應中獲取內容頭)。 3:請求在處理中;通常響應中已有部分數據可用了,但是服務器還沒有完成響應的生成。 4:響應已完成;您可以獲取並使用服務器的響應了。 */ if (xhr.readyState == 4) { xhr.onreadystatechange = empty clearTimeout(abortTimeout) //清除超時 var result, error = false //根據狀態來判斷請求是否成功 //>=200 && < 300 表示成功 //304 文件未修改 成功 //xhr.status == 0 && protocol == 'file:' 未請求,打開的本地文件,非localhost ip形式 if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) { //獲取媒體類型 //mimeToDataType:轉換成易讀的類型 html,json,scirpt,xml,text等 dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type')) //響應值 result = xhr.responseText //對響應值,根據媒體類型,做數據轉換 try { // http://perfectionkills.com/global-eval-what-are-the-options/ // (1,eval)(result) (1,eval)這是一個典型的逗號操作符,返回最右邊的值 // (1,eval) eval 的區別是:前者是一個值,不可以再覆蓋。后者是變量,如var a = 1; (1,a) = 1;會報錯; // (1,eval)(result) eval(result) 的區別是:前者變成值后,只能讀取window域下的變量。而后者,遵循作用域鏈,從局部變量上溯到window域 // 顯然(1,eval)(result) 避免了作用域鏈的上溯操作,性能稍好 if (dataType == 'script') (1,eval)(result) else if (dataType == 'xml') result = xhr.responseXML else if (dataType == 'json') result = blankRE.test(result) ? null : $.parseJSON(result); } catch (e) { error = e } //解析出錯,拋出 'parsererror'事件 if (error) ajaxError(error, 'parsererror', xhr, settings, deferred) //執行success else ajaxSuccess(result, xhr, settings, deferred) } else { //如果請求出錯 // xhr.status = 0 / null 執行abort, 其他 執行error ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, deferred) } } } // 執行請求前置器,若返回false則中斷請求 if (ajaxBeforeSend(xhr, settings) === false) { xhr.abort() ajaxError(null, 'abort', xhr, settings, deferred) return xhr } // xhrFields 設置 如設置跨域憑證 withCredentials if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name] //'async' in settings ajaxSetting未設置過async為false,設置過,包括null,都為true // 這是一個小技巧,默認async為true,若settings里面設置了,則為設置的值 var async = 'async' in settings ? settings.async : true //准備xhr請求 xhr.open(settings.type, settings.url, async, settings.username, settings.password) //設置請求頭 for (name in headers) nativeSetHeader.apply(xhr, headers[name]) //超時處理:設置了settings.timeout,超時后調用xhr.abort()中斷請求 if (settings.timeout > 0) abortTimeout = setTimeout(function(){ xhr.onreadystatechange = empty; xhr.abort() ajaxError(null, 'timeout', xhr, settings, deferred) }, settings.timeout) // avoid sending empty string (#319) // 一般來說 post是settings.data get為null 因為get的查詢參數和url一起 xhr.send(settings.data ? settings.data : null) //這是返回的一個promise return xhr }
大致流程如下:
1:根據$.ajaxSettings 完善settings參數
2:觸發全局ajaxStart ,document.trgger("ajaxStart")事件
3:判斷是否跨域 ,確定settings.crossDomain的值
這里有一個獲取url的協議和host的簡單方法:
urlAnchor = document.createElement('a')
//如果沒有設置請求地址,則取當前頁面地址
urlAnchor.href = settings.url
// cleans up URL for .href (IE only), see https://github.com/madrobby/zepto/pull/1049
urlAnchor.href = urlAnchor.href
無論settings.url帶不帶host和protocol,最后由urlAnchor.host和urlAnchor.protocol會指出當前settings.url的主機名和協議名
這樣也就方便對比請求是否跨域了。簡單demo如下:
a = document.createElement('a')
<a></a>
a.href = "/foobar"
"/foobar" a.host "" a.href "http://192.168.1.198/foobar" a.href = a.href "http://192.168.1.198/foobar" a.host "http://192.168.1.198:80"
4:如果沒有設置settings.url ,則默認window.location,即當前url
5:判斷setting.url后面是否有#號,有則去掉#后面的
6:option.processData為true(默認為true,即默認以form-data格式來傳值),且option.data為對象。則$.param(option.data);若為get請求,
則url后面添上序列化的option.data。
/** * 序列化 * 針對options.data 轉換成 a=b&c=1 */ // 1:序列化options.data,2:如果是get請求,則將data加入到url后面 function serializeData(options) { //options.processData: 對於非get請求,是否將請求參數options.data轉換為字符串,processData = true,無視get或者post if (options.processData && options.data && $.type(options.data) != "string") //將data數據序列化為字符串, 轉換成 a=b&c=1 // options.traditional 決定是否深度序列化 options.data = $.param(options.data, options.traditional) // get請求,將序列化的數據追加到url后面 if (options.data && (!options.type || options.type.toUpperCase() == 'GET')) options.url = appendQuery(options.url, options.data), options.data = undefined } /** * 將查詢參數追加到URL后面 * @param url * @param query 查詢參數 * @returns {*} */ //('www.zhutao.cn' + '&' + 'param=1').replace(/[&?]{1,2}/, '?'); //"www.zhutao.cn?param=1" /* * appendQuery("www.zhutao.cn","name=zt&age=1"); × "www.zhutao.cn?name=zt&age=1" * appendQuery("www.zhutao.cn?sex=2&ape=3","name=zt&age=1"); × "www.zhutao.cn?sex=2&ape=3&name=zt&age=1" * */ function appendQuery(url, query) { if (query == '') return url //replace(/[&?]{1,2}/, '?') 匹配到的第一個[&?]{1,2} 替換成? return (url + '&' + query).replace(/[&?]{1,2}/, '?') }
7:根據url判斷dataType是否為jsonP類型。/\?.+=\?/.test
8:如果設置了緩存settings.catch === false,則url后面添加Date.now() ---->隨機數
Date.now() === new Date().getTime()
9:如果是jsonp類型,則$.ajaxJson();
10:聲明變量mine,header,setHeader(用來設置header對象),protocol(用來設置協議http),xhr,nativeSetHeader(xhr.setRequestHeader),abortTimeout
11:deferred.promise(xhr),xhr繼承deferred
12:判斷settings.crossDomain,若為true,則代表跨域同步請求,否則為Ajax異步,默認為異步請求
13:設置頭部Accept,信息先存入headers,默認為任何類型,此字段是告訴服務器,客戶端接受指定的數據類型
accepts: {
script: 'text/javascript, application/javascript, application/x-javascript',
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
},
var mime = settings.accepts[dataType], //媒體類型
setHeader('Accept', mime || '*/*') //默認接受任何類型
14:判斷有無mineType,有則強制服務器的頭部覆蓋返回類型即mineType字段加入到頭部,mineType主要是用來兼容瀏覽器用的
15:根據method,設置content-type,於headers中,在get類型的請求頭部中是不需要content-type的
在request中的content-type表示請求的數據格式,在response中的content-type表示返回的數據格式
16:將settings.header中的屬性加入到headers中去
17:設置xhr.onreadystatechange
1:如果狀態為4,則設置onreadystatechange為empty,清除計時器,
2:讀取dataType,獲取響應值,格式化返回的數據,觸發ajaxSuccess事件
18:觸發ajax發送之前的操作,若settings.beforeSend返回false,或者全局ajaxBeforeSend返回false,則觸發ajaxError("abort"),停止操作並返回。
19:xhrField設置,設置跨域憑證,xhr[name]=settings.xhrFields[name];
20:xhr.open
21:統一根據headers,設置請求頭部
22:設置timeout,請求超時
23:xhr.send
24:返回xhr(其實也是個promise)
zepto對於ajax的高層封裝有一定的技巧性:
/** * 參數轉換成ajax格式 * @param url * @param data * @param success * @param dataType * @returns {{url: *, data: *, success: *, dataType: *}} */ function parseArguments(url, data, success, dataType) { if ($.isFunction(data)) dataType = success, success = data, data = undefined //如果data是function,則認為它是請求成功后的回調 if (!$.isFunction(success)) dataType = success, success = undefined return { url: url , data: data //如果data不是function實例 , success: success , dataType: dataType //服務器返回的data類型 } } /** * 便捷方法 get請求 * @returns {*} */ $.get = function(/* url, data, success, dataType */){ return $.ajax(parseArguments.apply(null, arguments)) } /** * 便捷方法 post請求 * @returns {*} */ $.post = function(/* url, data, success, dataType */){ var options = parseArguments.apply(null, arguments) options.type = 'POST' return $.ajax(options) } /** * 便捷方法 響應數據類型為JSON * content-type: 'application/json' * @returns {*} */ $.getJSON = function(/* url, data, success */){ var options = parseArguments.apply(null, arguments) options.dataType = 'json' return $.ajax(options) } /** * 載入遠程 HTML 文件代碼並插入至 DOM 中 * @param url HTML 網頁網址 可以指定選擇符,來篩選載入的 HTML 文檔,DOM 中將僅插入篩選出的 HTML 代碼。語法形如 "url #some > selector"。 * @param data 發送至服務器的 key/value 數據 * @param success 載入成功時回調函數 * @returns {*} */ $.fn.load = function(url, data, success){ if (!this.length) return this var self = this, parts = url.split(/\s/), selector, options = parseArguments(url, data, success), callback = options.success //parts.length > 1 代表url后面有選擇符selector if (parts.length > 1) options.url = parts[0], selector = parts[1] options.success = function(response){ // response.replace(rscript, "") 過濾出script標簽 //$('<div>').html(response.replace(rscript, "")) innerHTML方式轉換成DOM self.html(selector ? $('<div>').html(response.replace(rscript, "")).find(selector) : response) //執行回調 callback && callback.apply(self, arguments); } $.ajax(options) return this }
這里的parseArguments方法是一個很好的對函數參數校驗的技巧,這個技巧在zepto里面很常見到。一般的方法是對各個參數是否為null進行校驗,利用if,else針對不同的情況調用不同的參數。但這里的parseArguments根據不同的情況調整參數的值。然后統一的調用函數。
對於封裝同一個函數的不同高層方法來說,提供一個公共方法來處理參數這是非常好的。
$.fn.load : 這對於異步遠程拉取html片段傳入到dom中是非常好用的。常見的處理場景是單頁面應用。
大致流程如下:
1:parseArguments處理參數;2:判斷url是否有selector,有則對其進行處理;3:對回調函數進行一次封裝,在返回html片段后,加入到dom中,然后執行回調函數。
具體demo如下:
$("body").load("/pageLoading.html",function(data){console.log(data)});
[<body class="background-color-#fff" style="overflow-x: hidden;">…</body>]
VM3067:2
<div class="pin-page-loading" >
<div class="opacity-bg" >
</div>
<div class="loading-content">
<div class="spi">
<div class="spinner-container container1">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
<div class="spinner-container container2">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
<div class="spinner-container container3">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
</div>
<div>
</div>
$("body").load("/pageLoading.html .opacity-bg",function(data){console.log(data)});
[<body class="background-color-#fff" style="overflow-x: hidden;">…</body>]
VM3070:2
<div class="pin-page-loading" >
<div class="opacity-bg" >
</div>
<div class="loading-content">
<div class="spi">
<div class="spinner-container container1">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
<div class="spinner-container container2">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
<div class="spinner-container container3">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
</div>
<div>
</div>
$("body").html();
"<div class="opacity-bg">
</div>"
