說起ajax,大家都不陌生。但是由于目前很多框架或者庫(kù)等都對(duì)網(wǎng)絡(luò)請(qǐng)求做了封裝,導(dǎo)致了很多初學(xué)者只知其然而不知其所以然。所以今天我們就詳細(xì)了解一下ajax的實(shí)現(xiàn)原理和封裝ajax的關(guān)鍵步驟。
ajax的核心是XMLHttpRequest對(duì)象。首先我們先創(chuàng)建一個(gè)XMLHTTPRequest對(duì)象var xhr = new XMLHttpRequest();。
注意:本文所提及的內(nèi)容不兼容古老的IE,有想了解的同學(xué)自行查閱ActiveXObject相關(guān)內(nèi)容。
XMLHttpRequest
在使用XMLHttpRequest對(duì)象的第一步,我們首先要調(diào)用open方法來(lái)初始化請(qǐng)求參數(shù),xhr.open('get','/test',true),雖然名字叫open,但是此時(shí)請(qǐng)求還并沒有發(fā)送。
open(method, url[, async, username, password])
- method:請(qǐng)求類型,例如GET,POST等
- url:請(qǐng)求地址(這里有同源限制,就是我們經(jīng)常會(huì)看到的跨域問題啦)
- async:是否發(fā)送異步請(qǐng)求。可選參數(shù),默認(rèn)為true。
- username&password:可選參數(shù),授權(quán)驗(yàn)證使用的,但是我們一般不這么用,使用后請(qǐng)求變成這個(gè)樣子了,http(s)://username:password@url。
如果調(diào)用了open方法后再次對(duì)它進(jìn)行調(diào)用,則相當(dāng)于調(diào)用了abort方法,abort方法我們?cè)诤竺娼榻B。
如果我們想為為請(qǐng)求綁定一些操作,這個(gè)時(shí)候就可以開始啦。常用的操作有如下幾個(gè):
setRequestHeader(key, value)
顧名思義,這個(gè)方法用于設(shè)置請(qǐng)求頭內(nèi)容。
- key:要設(shè)置的請(qǐng)求頭名稱
- value:對(duì)應(yīng)請(qǐng)求頭的值
overrideMimeType(type)
重寫服務(wù)器返回的MIME類型。通過這個(gè)方法可以告訴服務(wù)器你想要的數(shù)據(jù)類型。
注意:以上這些操作必須定義在send方法之前。否則,就拿setRequestHeader來(lái)說,你都把請(qǐng)求發(fā)出去了再設(shè)置還有什么用?
這個(gè)時(shí)候,我們就可以通過調(diào)用send 方法來(lái)發(fā)送請(qǐng)求了,xhr.send(null)。
send(data)
發(fā)送請(qǐng)求,如果是同步請(qǐng)求的話,會(huì)阻塞代碼的執(zhí)行,直至收到服務(wù)器響應(yīng)才會(huì)繼續(xù)。
- data:發(fā)送給服務(wù)器的數(shù)據(jù)。為了兼容不同的瀏覽器,即使是不需要傳數(shù)據(jù),也需要傳入?yún)?shù)null。
readyStateChanhe()
每次readyState的值改變的時(shí)候都會(huì)觸發(fā)這個(gè)函數(shù)。
getResponseHeader(name)
獲取指定響應(yīng)頭部的值,參數(shù)是響應(yīng)頭部的名稱,并且不區(qū)分大小寫。
getAllResponseHeaders()
獲取服務(wù)器發(fā)送的所有HTTP響應(yīng)的頭部。
在這里我們穿插幾個(gè)概念,readyState,這個(gè)屬性表明了請(qǐng)求的狀態(tài),伴隨HTTP請(qǐng)求的整個(gè)生命周期,它的值表明此時(shí)請(qǐng)求所處的階段,具體如下:
readyState
| 數(shù)值 | 描述 |
|---|---|
| 0 | 初始化,open()尚未調(diào)用 |
| 1 | open()已經(jīng)調(diào)用,但是send未調(diào)用 |
| 2 | 已獲取到返回頭信息 |
| 3 | 正在下載返回體信息 |
| 4 | 請(qǐng)求完成 |
還有幾個(gè)較為常用的屬性
| 名稱 | 含義 |
|---|---|
| responseText | 響應(yīng)的文本 |
| status | 響應(yīng)的狀態(tài)碼 |
| statusText | 響應(yīng)的狀態(tài)信息 |
| responseXML | 響應(yīng)內(nèi)容是“text/xml”或者是“application/xml”格式的時(shí)候,這個(gè)屬性的值就是響應(yīng)數(shù)據(jù)的XMLDOM文檔。 |
我們用下面這段代碼做個(gè)測(cè)試
var xhr = new XMLHttpRequest();
console.log(xhr.readyState)
xhr.onreadystatechange = function(){
console.log('------')
console.log('readyState:' + xhr.readyState)
console.log('ResponseHeaders:' + xhr.getAllResponseHeaders())
console.log('ResponseText:' + xhr.responseText.length)
console.log('------')
}
xhr.open('get','/')
xhr.send(null)
下圖我們可以直觀的看到在創(chuàng)建了XMLHttpRequest對(duì)象的時(shí)候,readyState的值為0。

然后我們定義了onreadystatechange函數(shù),讓其打印一些屬性,并調(diào)用open方法,此時(shí)readyState變?yōu)?。

最后我們調(diào)用send方法,可以看到經(jīng)歷了如下過程:
- send方法調(diào)用之后,readyState變?yōu)?,此時(shí)responseHeader已經(jīng)獲取到了,responseText為空;
- 響應(yīng)數(shù)據(jù)開始下載,readyState變?yōu)?
- 響應(yīng)數(shù)據(jù)下載結(jié)束,readyState變?yōu)?.我們可以發(fā)現(xiàn)此時(shí)responseText的長(zhǎng)度比之前長(zhǎng)。

abort()
取消響應(yīng),調(diào)用這個(gè)方法會(huì)終止已發(fā)送的請(qǐng)求。我們嘗試在之前的代碼最后加一句。
xhr.abort();
console.log(xhr.readyState);


也就是說,send執(zhí)行以后,并沒有去嘗試請(qǐng)求數(shù)據(jù),而是直接取消掉了,并且我們發(fā)現(xiàn)abort會(huì)將readyState的值置為0。
除此之外,XMLHttpRequest還有一個(gè)很重要的屬性withCredentials,cookie在同域請(qǐng)求的時(shí)候,會(huì)被自動(dòng)攜帶在請(qǐng)求頭中,但是跨域請(qǐng)求則不會(huì),除非把withCredentials的值設(shè)為true(默認(rèn)為false)。同時(shí)需要在服務(wù)端的響應(yīng)頭部中設(shè)置Access-Control-Allow-Credentials:true。不僅如此Access-Control-Allow-Origin的值也必須為當(dāng)前頁(yè)面的域名。
封裝
到此為止,我們終于講完了XMLHttpRequest的一些常用概念。接下來(lái),我們嘗試自己封裝一個(gè)支持get和post的簡(jiǎn)易jax請(qǐng)求。
function ajax(url, option){
option = option || {};
var method = (option.method || 'GET').toUpperCase(),
async = option.async === undefined ? true : option.async,
params = handleParams(option.data);
var xhr = new XMLHttpRequest();
if(async){
xhr.onreadystatechange = function () {
if (xhr.readyState == 4){
callback(option,xhr);
}
};
}
if (method === 'GET'){
xhr.open("GET",url + '?' + params, async);
xhr.send(null)
}else if (method === 'POST'){
xhr.open('POST', url, async);
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.send(params);
}
if(!async){
callback(option,xhr);
}
function callback(opt,obj){
var status = obj.status;
if (status >= 200 && status < 300 ){
opt.success && opt.success(obj.responseText,obj.responseXML);
}else{
opt.fail && opt.fail(status);
}
}
function handleParams(data) {
var arr = [];
for (var i in data) {
arr.push(encodeURIComponent(i) + '=' + encodeURIComponent(data[i]));
}
return arr.join('&');
}
}
// 測(cè)試調(diào)用
ajax('/xxx',{
method:'POST',
data:{
key: 'test'
},
success:function(){
console.log('success')
},
fail:function(){
console.log('fail')
}
});
小結(jié)
其實(shí)ajax實(shí)現(xiàn)原理并不復(fù)雜,復(fù)雜的是容錯(cuò)、兼容性等的處理,使用的時(shí)候盡量使用庫(kù)或者框架提供的封裝,避免不必要的漏洞。