瀏覽器與服務器之間,采用HTTP協(xié)議通信。用戶在瀏覽器地址欄鍵入一個網址,或者通過網頁表單向服務器提交內容,這時瀏覽器就會向服務器發(fā)出HTTP請求。
1999年,微軟公司發(fā)布IE瀏覽器5.0版,第一次引入新功能:允許JavaScript腳本向服務器發(fā)起HTTP請求。這個功能當時并沒有引起注意,直到2004年Gmail發(fā)布和2005年Google Map發(fā)布,才引起廣泛重視。2005年2月,AJAX這個詞第一次正式提出,指圍繞這個功能進行開發(fā)的一整套做法。從此,AJAX成為腳本發(fā)起HTTP通信的代名詞,W3C也在2006年發(fā)布了它的國際標準。
具體來說,AJAX包括以下幾個步驟。
概括起來,就是一句話,AJAX通過原生的XMLHttpRequest
對象發(fā)出HTTP請求,得到服務器返回的數據后,再進行處理。
AJAX可以是同步請求,也可以是異步請求。但是,大多數情況下,特指異步請求。因為同步的Ajax請求,對瀏覽器有“堵塞效應”。
XMLHttpRequest
對象用來在瀏覽器與服務器之間傳送數據。
var ajax = new XMLHttpRequest();
ajax.open('GET', 'http://www.example.com/page.php', true);
上面代碼向指定的服務器網址,發(fā)出GET請求。
然后,AJAX指定回調函數,監(jiān)聽通信狀態(tài)(readyState
屬性)的變化。
ajax.onreadystatechange = handleStateChange;
一旦拿到服務器返回的數據,AJAX不會刷新整個網頁,而是只更新相關部分,從而不打斷用戶正在做的事情。
注意,AJAX只能向同源網址(協(xié)議、域名、端口都相同)發(fā)出HTTP請求,如果發(fā)出跨源請求,就會報錯(詳見《同源政策》和《CORS機制》兩節(jié))。
雖然名字里面有XML
,但是實際上,XMLHttpRequest可以報送各種數據,包括字符串和二進制,而且除了HTTP,它還支持通過其他協(xié)議傳送(比如File和FTP)。
下面是XMLHttpRequest
對象的典型用法。
var xhr = new XMLHttpRequest();
// 指定通信過程中狀態(tài)改變時的回調函數
xhr.onreadystatechange = function(){
// 通信成功時,狀態(tài)值為4
if (xhr.readyState === 4){
if (xhr.status === 200){
console.log(xhr.responseText);
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
};
// open方式用于指定HTTP動詞、請求的網址、是否異步
xhr.open('GET', '/endpoint', true);
// 發(fā)送HTTP請求
xhr.send(null);
open
方法的第三個參數是一個布爾值,表示是否為異步請求。如果設為false
,就表示這個請求是同步的,下面是一個例子。
var request = new XMLHttpRequest();
request.open('GET', '/bar/foo.txt', false);
request.send(null);
if (request.status === 200) {
console.log(request.responseText);
}
readyState
是一個只讀屬性,用一個整數和對應的常量,表示XMLHttpRequest請求當前所處的狀態(tài)。
UNSENT
,表示XMLHttpRequest實例已經生成,但是open()
方法還沒有被調用。OPENED
,表示send()
方法還沒有被調用,仍然可以使用setRequestHeader()
,設定HTTP請求的頭信息。HEADERS_RECEIVED
,表示send()
方法已經執(zhí)行,并且頭信息和狀態(tài)碼已經收到。LOADING
,表示正在接收服務器傳來的body部分的數據,如果responseType
屬性是text
或者空字符串,responseText
就會包含已經收到的部分信息。DONE
,表示服務器數據已經完全接收,或者本次接收已經失敗了。在通信過程中,每當發(fā)生狀態(tài)變化的時候,readyState
屬性的值就會發(fā)生改變。這個值每一次變化,都會觸發(fā)readyStateChange
事件。
if (ajax.readyState == 4) {
// Handle the response.
} else {
// Show the 'Loading...' message or do nothing.
}
上面代碼表示,只有readyState
變?yōu)?時,才算確認請求已經成功,其他值都表示請求還在進行中。
onreadystatechange
屬性指向一個回調函數,當readystatechange
事件發(fā)生的時候,這個回調函數就會調用,并且XMLHttpRequest實例的readyState
屬性也會發(fā)生變化。
另外,如果使用abort()
方法,終止XMLHttpRequest請求,onreadystatechange
回調函數也會被調用。
var xmlhttp = new XMLHttpRequest();
xmlhttp.open( 'GET', 'http://example.com' , true );
xmlhttp.onreadystatechange = function () {
if ( XMLHttpRequest.DONE != xmlhttp.readyState ) {
return;
}
if ( 200 != xmlhttp.status ) {
return;
}
console.log( xmlhttp.responseText );
};
xmlhttp.send();
response
屬性為只讀,返回接收到的數據體(即body部分)。它的類型可以是ArrayBuffer、Blob、Document、JSON對象、或者一個字符串,這由XMLHttpRequest.responseType
屬性的值決定。
如果本次請求沒有成功或者數據不完整,該屬性就會等于null
。
responseType
屬性用來指定服務器返回數據(xhr.response
)的類型。
text類型適合大多數情況,而且直接處理文本也比較方便,document類型適合返回XML文檔的情況,blob類型適合讀取二進制數據,比如圖片文件。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status == 200) {
var blob = new Blob([this.response], {type: 'image/png'});
// 或者
var blob = oReq.response;
}
};
xhr.send();
如果將這個屬性設為ArrayBuffer,就可以按照數組的方式處理二進制數據。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
var uInt8Array = new Uint8Array(this.response);
for (var i = 0, len = binStr.length; i < len; ++i) {
// var byte = uInt8Array[i];
}
};
xhr.send();
如果將這個屬性設為“json”,支持JSON的瀏覽器(Firefox>9,chrome>30),就會自動對返回數據調用JSON.parse()
方法。也就是說,你從xhr.response屬性(注意,不是xhr.responseText屬性)得到的不是文本,而是一個JSON對象。
XHR2支持Ajax的返回類型為文檔,即xhr.responseType=”document” 。這意味著,對于那些打開CORS的網站,我們可以直接用Ajax抓取網頁,然后不用解析HTML字符串,直接對XHR回應進行DOM操作。
responseText
屬性返回從服務器接收到的字符串,該屬性為只讀。如果本次請求沒有成功或者數據不完整,該屬性就會等于null
。
如果服務器返回的數據格式是JSON,就可以使用responseText
屬性。
var data = ajax.responseText;
data = JSON.parse(data);
responseXML
屬性返回從服務器接收到的Document對象,該屬性為只讀。如果本次請求沒有成功,或者數據不完整,或者不能被解析為XML或HTML,該屬性等于null
。
返回的數據會被直接解析為DOM對象。
/* 返回的XML文件如下
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<book>
<chapter id="1">(Re-)Introducing JavaScript</chapter>
<chapter id="2">JavaScript in Action</chapter>
</book>
*/
var data = ajax.responseXML;
var chapters = data.getElementsByTagName('chapter');
如果服務器返回的數據,沒有明示Content-Type
頭信息等于text/xml
,可以使用overrideMimeType()
方法,指定XMLHttpRequest對象將返回的數據解析為XML。
status
屬性為只讀屬性,表示本次請求所得到的HTTP狀態(tài)碼,它是一個整數。一般來說,如果通信成功的話,這個狀態(tài)碼是200。
基本上,只有2xx和304的狀態(tài)碼,表示服務器返回是正常狀態(tài)。
if (ajax.readyState == 4) {
if ( (ajax.status >= 200 && ajax.status < 300)
|| (ajax.status == 304) ) {
// Handle the response.
} else {
// Status error!
}
}
statusText
屬性為只讀屬性,返回一個字符串,表示服務器發(fā)送的狀態(tài)提示。不同于status
屬性,該屬性包含整個狀態(tài)信息,比如”200 OK“。
timeout
屬性等于一個整數,表示多少毫秒后,如果請求仍然沒有得到結果,就會自動終止。如果該屬性等于0,就表示沒有時間限制。
var xhr = new XMLHttpRequest();
xhr.ontimeout = function () {
console.error("The request for " + url + " timed out.");
};
xhr.onload = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback.apply(xhr, args);
} else {
console.error(xhr.statusText);
}
}
};
xhr.open("GET", url, true);
xhr.timeout = timeout;
xhr.send(null);
}
XMLHttpRequest第一版,只能對onreadystatechange
這一個事件指定回調函數。該事件對所有情況作出響應。 XMLHttpRequest第二版允許對更多的事件指定回調函數。
abort()
方法xhr.onload = function() {
var responseText = xhr.responseText;
console.log(responseText);
// process the response.
};
xhr.onerror = function() {
console.log('There was an error!');
};
注意,如果發(fā)生網絡錯誤(比如服務器無法連通),onerror
事件無法獲取報錯信息,所以只能顯示報錯。
withCredentials
屬性是一個布爾值,表示跨域請求時,用戶信息(比如Cookie和認證的HTTP頭信息)是否會包含在請求之中,默認為false
。即向example.com
發(fā)出跨域請求時,不會發(fā)送example.com
設置在本機上的Cookie(如果有的話)。
如果你需要通過跨域AJAX發(fā)送Cookie,需要打開withCredentials
。
xhr.withCredentials = true;
為了讓這個屬性生效,服務器必須顯式返回Access-Control-Allow-Credentials
這個頭信息。
Access-Control-Allow-Credentials: true
.withCredentials
屬性打開的話,不僅會發(fā)送Cookie,還會設置遠程主機指定的Cookie。注意,此時你的腳本還是遵守同源政策,無法 從document.cookie
或者HTTP回應的頭信息之中,讀取這些Cookie。
abort
方法用來終止已經發(fā)出的HTTP請求。
ajax.open('GET', 'http://www.example.com/page.php', true);
var ajaxAbortTimer = setTimeout(function() {
if (ajax) {
ajax.abort();
ajax = null;
}
}, 5000);
上面代碼在發(fā)出5秒之后,終止一個AJAX請求。
getAllResponseHeaders
方法返回服務器發(fā)來的所有HTTP頭信息。格式為字符串,每個頭信息之間使用CRLF
分隔,如果沒有受到服務器回應,該屬性返回null
。
getResponseHeader
方法返回HTTP頭信息指定字段的值,如果還沒有收到服務器回應或者指定字段不存在,則該屬性為null
。
function getHeaderTime () {
console.log(this.getResponseHeader("Last-Modified"));
}
var oReq = new XMLHttpRequest();
oReq.open("HEAD", "yourpage.html");
oReq.onload = getHeaderTime;
oReq.send();
如果有多個字段同名,則它們的值會被連接為一個字符串,每個字段之間使用“逗號+空格”分隔。
XMLHttpRequest
對象的open
方法用于指定發(fā)送HTTP請求的參數,它的使用格式如下,一共可以接受五個參數。
void open(
string method,
string url,
optional boolean async,
optional string user,
optional string password
);
method
:表示HTTP動詞,比如“GET”、“POST”、“PUT”和“DELETE”。url
: 表示請求發(fā)送的網址。async
: 格式為布爾值,默認為true
,表示請求是否為異步。如果設為false
,則send()
方法只有等到收到服務器返回的結果,才會有返回值。user
:表示用于認證的用戶名,默認為空字符串。password
:表示用于認證的密碼,默認為空字符串。如果對使用過open()
方法的請求,再次使用這個方法,等同于調用abort()
。
下面發(fā)送POST請求的例子。
xhr.open('POST', encodeURI('someURL'));
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {};
xhr.send(encodeURI('dataString'));
上面方法中,open方法向指定URL發(fā)出POST請求,send方法送出實際的數據。
下面是一個同步AJAX請求的例子。
var request = new XMLHttpRequest();
request.open('GET', '/bar/foo.txt', false);
request.send(null);
if (request.status === 200) {
console.log(request.responseText);
}
send
方法用于實際發(fā)出HTTP請求。如果不帶參數,就表示HTTP請求只包含頭信息,也就是只有一個URL,典型例子就是GET請求;如果帶有參數,就表示除了頭信息,還帶有包含具體數據的信息體,典型例子就是POST請求。
ajax.open('GET'
, 'http://www.example.com/somepage.php?id=' + encodeURIComponent(id)
, true
);
// 等同于
var data = 'id=' + encodeURIComponent(id));
ajax.open('GET', 'http://www.example.com/somepage.php', true);
ajax.send(data);
上面代碼中,GET
請求的參數,可以作為查詢字符串附加在URL后面,也可以作為send
方法的參數。
下面是發(fā)送POST請求的例子。
var data = 'email='
+ encodeURIComponent(email)
+ '&password='
+ encodeURIComponent(password);
ajax.open('POST', 'http://www.example.com/somepage.php', true);
ajax.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
ajax.send(data);
如果請求是異步的(默認為異步),該方法在發(fā)出請求后會立即返回。如果請求為同步,該方法只有等到收到服務器回應后,才會返回。
注意,所有XMLHttpRequest的監(jiān)聽事件,都必須在send()
方法調用之前設定。
send
方法的參數就是發(fā)送的數據。多種格式的數據,都可以作為它的參數。
void send();
void send(ArrayBufferView data);
void send(Blob data);
void send(Document data);
void send(String data);
void send(FormData data);
如果發(fā)送Document
數據,在發(fā)送之前,數據會先被串行化。
發(fā)送二進制數據,最好使用ArrayBufferView
或Blob
對象,這使得通過Ajax上傳文件成為可能。
下面是一個上傳ArrayBuffer
對象的例子。
function sendArrayBuffer() {
var xhr = new XMLHttpRequest();
var uInt8Array = new Uint8Array([1, 2, 3]);
xhr.open('POST', '/server', true);
xhr.onload = function(e) { ... };
xhr.send(uInt8Array.buffer);
}
FormData類型可以用于構造表單數據。
var formData = new FormData();
formData.append('username', '張三');
formData.append('email', 'zhangsan@example.com');
formData.append('birthDate', 1940);
var xhr = new XMLHttpRequest();
xhr.open("POST", "/register");
xhr.send(formData);
上面的代碼構造了一個formData
對象,然后使用send方法發(fā)送。它的效果與點擊下面表單的submit按鈕是一樣的。
<form id='registration' name='registration' action='/register'>
<input type='text' name='username' value='張三'>
<input type='email' name='email' value='zhangsan@example.com'>
<input type='number' name='birthDate' value='1940'>
<input type='submit' onclick='return sendForm(this.form);'>
</form>
FormData也可以將現有表單構造生成。
var formElement = document.querySelector("form");
var request = new XMLHttpRequest();
request.open("POST", "submitform.php");
request.send(new FormData(formElement));
FormData對象還可以對現有表單添加數據,這為我們操作表單提供了極大的靈活性。
function sendForm(form) {
var formData = new FormData(form);
formData.append('csrf', 'e69a18d7db1286040586e6da1950128c');
var xhr = new XMLHttpRequest();
xhr.open('POST', form.action, true);
xhr.onload = function(e) {
// ...
};
xhr.send(formData);
return false;
}
var form = document.querySelector('#registration');
sendForm(form);
FormData對象也能用來模擬File控件,進行文件上傳。
function uploadFiles(url, files) {
var formData = new FormData();
for (var i = 0, file; file = files[i]; ++i) {
formData.append(file.name, file); // 可加入第三個參數,表示文件名
}
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.onload = function(e) { ... };
xhr.send(formData); // multipart/form-data
}
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
uploadFiles('/server', this.files);
}, false);
FormData也可以加入JavaScript生成的文件。
// 添加JavaScript生成的文件
var content = '<a id="a"><b id="b">hey!</b></a>';
var blob = new Blob([content], { type: "text/xml"});
formData.append("webmasterfile", blob);
setRequestHeader
方法用于設置HTTP頭信息。該方法必須在open()
之后、send()
之前調用。如果該方法多次調用,設定同一個字段,則每一次調用的值會被合并成一個單一的值發(fā)送。
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Content-Length', JSON.stringify(data).length);
xhr.send(JSON.stringify(data));
上面代碼首先設置頭信息Content-Type
,表示發(fā)送JSON格式的數據;然后設置Content-Length
,表示數據長度;最后發(fā)送JSON數據。
該方法用來指定服務器返回數據的MIME類型。該方法必須在send()
之前調用。
傳統(tǒng)上,如果希望從服務器取回二進制數據,就要使用這個方法,人為將數據類型偽裝成文本數據。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
// 強制將MIME改為文本類型
xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.onreadystatechange = function(e) {
if (this.readyState == 4 && this.status == 200) {
var binStr = this.responseText;
for (var i = 0, len = binStr.length; i < len; ++i) {
var c = binStr.charCodeAt(i);
var byte = c & 0xff; // 去除高位字節(jié),留下低位字節(jié)
}
}
};
xhr.send();
上面代碼中,因為傳回來的是二進制數據,首先用xhr.overrideMimeType
方法強制改變它的MIME類型,偽裝成文本數據。字符集必需指定為“x-user-defined”,如果是其他字符集,瀏覽器內部會強制轉碼,將其保存成UTF-16的形式。字符集“x-user-defined”其實也會發(fā)生轉碼,瀏覽器會在每個字節(jié)前面再加上一個字節(jié)(0xF700-0xF7ff),因此后面要對每個字符進行一次與運算(&),將高位的8個位去除,只留下低位的8個位,由此逐一讀出原文件二進制數據的每個字節(jié)。
這種方法很麻煩,在XMLHttpRequest版本升級以后,一般采用指定responseType
的方法。
var xhr = new XMLHttpRequest();
xhr.onload = function(e) {
var arraybuffer = xhr.response;
// ...
}
xhr.open("GET", url);
xhr.responseType = "arraybuffer";
xhr.send();
readyState
屬性的值發(fā)生改變,就會觸發(fā)readyStateChange事件。
我們可以通過onReadyStateChange
屬性,指定這個事件的回調函數,對不同狀態(tài)進行不同處理。尤其是當狀態(tài)變?yōu)?的時候,表示通信成功,這時回調函數就可以處理服務器傳送回來的數據。
上傳文件時,XMLHTTPRequest對象的upload屬性有一個progress,會不斷返回上傳的進度。
假定網頁上有一個progress元素。
<progress min="0" max="100" value="0">0% complete</progress>
文件上傳時,對upload屬性指定progress事件回調函數,即可獲得上傳的進度。
function upload(blobOrFile) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/server', true);
xhr.onload = function(e) { ... };
// Listen to the upload progress.
var progressBar = document.querySelector('progress');
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
progressBar.value = (e.loaded / e.total) * 100;
progressBar.textContent = progressBar.value; // Fallback for unsupported browsers.
}
};
xhr.send(blobOrFile);
}
upload(new Blob(['hello world'], {type: 'text/plain'}));
load事件表示服務器傳來的數據接收完畢,error事件表示請求出錯,abort事件表示請求被中斷。
var xhr = new XMLHttpRequest();
xhr.addEventListener("progress", updateProgress);
xhr.addEventListener("load", transferComplete);
xhr.addEventListener("error", transferFailed);
xhr.addEventListener("abort", transferCanceled);
xhr.open();
function updateProgress (oEvent) {
if (oEvent.lengthComputable) {
var percentComplete = oEvent.loaded / oEvent.total;
// ...
} else {
// 回應的總數據量未知,導致無法計算百分比
}
}
function transferComplete(evt) {
console.log("The transfer is complete.");
}
function transferFailed(evt) {
console.log("An error occurred while transferring the file.");
}
function transferCanceled(evt) {
console.log("The transfer has been canceled by the user.");
}
abort
、load
和error
這三個事件,會伴隨一個loadend
事件,表示請求結束,但不知道其是否成功。
req.addEventListener("loadend", loadEnd);
function loadEnd(e) {
alert("請求結束(不知道是否成功)");
}
HTML網頁的<form>
元素能夠以四種格式,向服務器發(fā)送數據。
POST
方法,將enctype
屬性設為application/x-www-form-urlencoded
,這是默認方法。<form action="register.php" method="post" onsubmit="AJAXSubmit(this); return false;">
</form>
POST
方法,將enctype
屬性設為text/plain
。<form action="register.php" method="post" enctype="text/plain" onsubmit="AJAXSubmit(this); return false;">
</form>
POST
方法,將enctype
屬性設為multipart/form-data
。<form action="register.php" method="post" enctype="multipart/form-data" onsubmit="AJAXSubmit(this); return false;">
</form>
GET
方法,enctype
屬性將被忽略。<form action="register.php" method="get" onsubmit="AJAXSubmit(this); return false;">
</form>
某個表單有兩個字段,分別是foo
和baz
,其中foo
字段的值等于bar
,baz
字段的值一個分為兩行的字符串。上面四種方法,都可以將這個表單發(fā)送到服務器。
第一種方法是默認方法,POST發(fā)送,Encoding type為application/x-www-form-urlencoded。
Content-Type: application/x-www-form-urlencoded
foo=bar&baz=The+first+line.%0D%0AThe+second+line.%0D%0A
第二種方法是POST發(fā)送,Encoding type為text/plain。
Content-Type: text/plain
foo=bar
baz=The first line.
The second line.
第三種方法是POST發(fā)送,Encoding type為multipart/form-data。
Content-Type: multipart/form-data; boundary=---------------------------314911788813839
-----------------------------314911788813839
Content-Disposition: form-data; name="foo"
bar
-----------------------------314911788813839
Content-Disposition: form-data; name="baz"
The first line.
The second line.
-----------------------------314911788813839--
第四種方法是GET請求。
?foo=bar&baz=The%20first%20line.%0AThe%20second%20line.
通常,我們使用file控件實現文件上傳。
<form id="file-form" action="handler.php" method="POST">
<input type="file" id="file-select" name="photos[]" multiple/>
<button type="submit" id="upload-button">上傳</button>
</form>
上面HTML代碼中,file控件的multiple屬性,指定可以一次選擇多個文件;如果沒有這個屬性,則一次只能選擇一個文件。
file對象的files屬性,返回一個FileList對象,包含了用戶選中的文件。
var fileSelect = document.getElementById('file-select');
var files = fileSelect.files;
然后,新建一個FormData對象的實例,用來模擬發(fā)送到服務器的表單數據,把選中的文件添加到這個對象上面。
var formData = new FormData();
for (var i = 0; i < files.length; i++) {
var file = files[i];
if (!file.type.match('image.*')) {
continue;
}
formData.append('photos[]', file, file.name);
}
上面代碼中的FormData對象的append方法,除了可以添加文件,還可以添加二進制對象(Blob)或者字符串。
append方法的第一個參數是表單的控件名,第二個參數是實際的值,第三個參數是可選的,通常是文件名。
最后,使用Ajax方法向服務器上傳文件。
目前,各大瀏覽器(包括IE 10)都支持Ajax上傳文件。
除了使用FormData接口上傳,也可以直接使用File API上傳。
var file = document.getElementById('test-input').files[0];
var xhr = new XMLHttpRequest();
xhr.open('POST', 'myserver/uploads');
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
可以看到,上面這種寫法比FormData的寫法,要簡單很多。
Ajax操作所用的XMLHttpRequest
對象,已經有十多年的歷史,它的API設計并不是很好,輸入、輸出、狀態(tài)都在同一個接口管理,容易寫出非常混亂的代碼。Fetch API是一種新規(guī)范,用來取代XMLHttpRequest
對象。它主要有兩個特點,一是接口合理化,Ajax是將所有不同性質的接口都放在XHR對象上,而Fetch是將它們分散在幾個不同的對象上,設計更合理;二是Fetch操作返回Promise
對象,避免了嵌套的回調函數。
下面的代碼檢查瀏覽器是否部署了Fetch API。
if ("fetch" in window){
// 支持
} else {
// 不支持
}
下面是一個Fetch API的簡單例子。
fetch(url).then(function (response) {
return response.json();
}).then(function (jsonData) {
console.log(jsonData);
}).catch(function () {
console.log('出錯了');
});
上面代碼向指定的URL發(fā)出請求,得到回應后,將其轉為JSON格式,輸出到控制臺。如果出錯,則輸出一條提示信息。注意,fetch
方法返回的是一個Promise對象。
作為比較,XMLHttpRequest
的寫法如下。
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function() {
console.log(xhr.response);
};
xhr.onerror = function() {
console.log('出錯了');
};
xhr.send();
Fetch API最大的特點是,除了返回Promise
對象,還有一點就是數據傳送是以數據流(stream)的形式進行的。對于大文件,數據是一段一段得到的。
response.text().then(function (responseText) {
console.log(responseText);
}
上面代碼中的text()
,其實就是一個數據流讀取器,并使用指定格式解讀。
Fetch API提供以下五個數據流讀取器。
.text()
:返回字符串.json()
:返回一個JSON對象.formData()
:返回一個FormData
對象.blob()
:返回一個blob
對象.arrayBuffer()
:返回一個二進制數組數據流只能讀取一次,一旦讀取,數據流就空了。再次讀取就不會得到結果。解決方法是在讀取之前,先使用.clone()
方法,復制一份一模一樣的副本。
var url = 'LargeFile.txt';
var progress = 0;
var contentLength = 0;
fetch(url).then(function (response) {
// 本次請求總的數據長度
contentLength = response.headers.get('Content-Length');
var getStream = function (reader) {};
return getStream(response.body.getReader());
})
.catch(function (error) {
console.log(error);
});
上面代碼中,response.body.getReader()
返回的就是數據流之中的一段。處理數據流的getStream
函數代碼如下。
var progress = 0;
var contentLength = 0;
var getStream = function (reader) {
return reader.read().then(function (result) {
// 如果數據已經讀取完畢,直接返回
if (result.done) {
return;
}
// 取出本段數據(二進制格式)
var chunk = result.value;
var text = '';
// 假定數據是UTF-8編碼,前三字節(jié)是數據頭,
// 而且每個字符占據一個字節(jié)(即都為英文字符)
for (var i = 3; i < chunk.byteLength; i++) {
text += String.fromCharCode(chunk[i]);
}
// 將本段數據追加到網頁之中
document.getElementById('content').innerHTML += text;
// 計算當前進度
progress += chunk.byteLength;
console.log(((progress / contentLength) * 100) + '%');
// 遞歸處理下一段數據
return getStream(reader);
};
};
上面這樣的數據流處理,可以提高網站性能表現,減少內存占用,對于請求大文件或者網速慢的場景相當有用。傳統(tǒng)的XMLHTTPRequest
對象不支持數據流,所有的數據必須放在緩存里,等到全部拿到后,再一次性吐出來。
fetch
方法的第一個參數可以是URL字符串,也可以是后文要講到的Request
對象實例。Fetch
方法返回一個Promise
對象,并將一個response
對象傳給回調函數。
response
對象有一個ok
屬性,如果返回的狀態(tài)碼在200到299之間(即請求成功),這個屬性為true
,否則為false
。因此,判斷請求是否成功的代碼可以寫成下面這樣。
fetch('./api/some.json').then(function (response) {
if (response.ok) {
response.json().then(function (data) {
console.log(data);
});
} else {
console.log('請求失敗,狀態(tài)碼為', response.status);
}
}, function(err) {
console.log('出錯:', err);
});
response
對象除了json
方法,還包含了服務器HTTP回應的元數據。
fetch('users.json').then(function(response) {
console.log(response.headers.get('Content-Type'));
console.log(response.headers.get('Date'));
console.log(response.status);
console.log(response.statusText);
console.log(response.type);
console.log(response.url);
});
上面代碼中,response
對象有很多屬性,其中的response.type
屬性比較特別,表示HTTP回應的類型,它有以下三個值。
如果需要在CORS機制下發(fā)出跨域請求,需要指明狀態(tài)。
fetch('http://some-site.com/cors-enabled/some.json', {mode: 'cors'})
.then(function(response) {
return response.text();
})
.then(function(text) {
console.log('Request successful', text);
})
.catch(function(error) {
log('Request failed', error)
});
除了指定模式,fetch方法的第二個參數還可以用來配置其他值,比如指定cookie連同HTTP請求一起發(fā)出。
fetch(url, {
credentials: 'include'
})
發(fā)出POST請求的寫法如下。
fetch("http://www.example.org/submit.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "firstName=Nikhil&favColor=blue&password=easytoguess"
}).then(function(res) {
if (res.ok) {
console.log("Perfect! Your settings are saved.");
} else if (res.status == 401) {
console.log("Oops! You are not authorized.");
}
}, function(e) {
console.log("Error submitting form!");
});
目前,還有一些XMLHttpRequest
對象可以做到,但是Fetch API還沒做到的地方,比如中途中斷HTTP請求,以及獲取HTTP請求的進度。這些不足與Fetch返回的是Promise對象有關。
Fetch API引入三個新的對象(也是構造函數):Headers
, Request
和Response
。其中,Headers
對象用來構造/讀取HTTP數據包的頭信息。
var content = 'Hello World';
var headers = new Headers();
headers.append("Accept", "application/json");
headers.append("Content-Type", "text/plain");
headers.append("Content-Length", content.length.toString());
headers.append("X-Custom-Header", "ProcessThisImmediately");
Headers
對象的實例,除了使用append
方法添加屬性,也可以直接通過構造函數一次性生成。
reqHeaders = new Headers({
"Content-Type": "text/plain",
"Content-Length": content.length.toString(),
"X-Custom-Header": "ProcessThisImmediately",
});
Headers對象實例還提供了一些工具方法。
reqHeaders.has("Content-Type") // true
reqHeaders.has("Set-Cookie") // false
reqHeaders.set("Content-Type", "text/html")
reqHeaders.append("X-Custom-Header", "AnotherValue")
reqHeaders.get("Content-Length") // 11
reqHeaders.getAll("X-Custom-Header") // ["ProcessThisImmediately", "AnotherValue"]
reqHeaders.delete("X-Custom-Header")
reqHeaders.getAll("X-Custom-Header") // []
生成Header實例以后,可以將它作為第二個參數,傳入Request方法。
var headers = new Headers();
headers.append('Accept', 'application/json');
var request = new Request(URL, {headers: headers});
fetch(request).then(function(response) {
console.log(response.headers);
});
同樣地,Headers實例可以用來構造Response方法。
var headers = new Headers({
'Content-Type': 'application/json',
'Cache-Control': 'max-age=3600'
});
var response = new Response(
JSON.stringify({photos: {photo: []}}),
{'status': 200, headers: headers}
);
response.json().then(function(json) {
insertPhotos(json);
});
上面代碼中,構造了一個HTTP回應。目前,瀏覽器構造HTTP回應沒有太大用處,但是隨著Service Worker的部署,不久瀏覽器就可以向Service Worker發(fā)出HTTP回應。
Request對象用來構造HTTP請求。
var req = new Request("/index.html");
req.method // "GET"
req.url // "http://example.com/index.html"
Request對象的第二個參數,表示配置對象。
var uploadReq = new Request("/uploadImage", {
method: "POST",
headers: {
"Content-Type": "image/png",
},
body: "image data"
});
上面代碼指定Request對象使用POST方法發(fā)出,并指定HTTP頭信息和信息體。
下面是另一個例子。
var req = new Request(URL, {method: 'GET', cache: 'reload'});
fetch(req).then(function(response) {
return response.json();
}).then(function(json) {
someOperator(json);
});
上面代碼中,指定請求方法為GET,并且要求瀏覽器不得緩存response。
Request對象實例有兩個屬性是只讀的,不能手動設置。一個是referrer屬性,表示請求的來源,由瀏覽器設置,有可能是空字符串。另一個是context屬性,表示請求發(fā)出的上下文,如果是image,表示是從img標簽發(fā)出,如果是worker,表示是從worker腳本發(fā)出,如果是fetch,表示是從fetch函數發(fā)出的。
Request對象實例的mode屬性,用來設置是否跨域,合法的值有以下三種:same-origin、no-cors(默認值)、cors。當設置為same-origin時,只能向同域的URL發(fā)出請求,否則會報錯。
var arbitraryUrl = document.getElementById("url-input").value;
fetch(arbitraryUrl, { mode: "same-origin" }).then(function(res) {
console.log("Response succeeded?", res.ok);
}, function(e) {
console.log("Please enter a same-origin URL!");
});
上面代碼中,如果用戶輸入的URL不是同域的,將會報錯,否則就會發(fā)出請求。
如果mode屬性為no-cors,就與默認的瀏覽器行為沒有不同,類似script標簽加載外部腳本文件、img標簽加載外部圖片。如果mode屬性為cors,就可以向部署了CORS機制的服務器,發(fā)出跨域請求。
var u = new URLSearchParams();
u.append('method', 'flickr.interestingness.getList');
u.append('api_key', '<insert api key here>');
u.append('format', 'json');
u.append('nojsoncallback', '1');
var apiCall = fetch('https://api.flickr.com/services/rest?' + u);
apiCall.then(function(response) {
return response.json().then(function(json) {
// photo is a list of photos.
return json.photos.photo;
});
}).then(function(photos) {
photos.forEach(function(photo) {
console.log(photo.title);
});
});
上面代碼是向Flickr API發(fā)出圖片請求的例子。
Request對象的一個很有用的功能,是在其他Request實例的基礎上,生成新的Request實例。
var postReq = new Request(req, {method: 'POST'});
fetch方法返回Response對象實例,它有以下屬性。
status
:整數值,表示狀態(tài)碼(比如200)Response對象還有兩個靜態(tài)方法。
fetch("https://example.com", init)
.then(function (response) {
// Check that the response is a 200
if (response.status === 200) {
alert("Content type: " + response.headers.get('Content-Type'));
}
});
Request對象和Response對象都有body屬性,表示請求的內容。body屬性可能是以下的數據類型。
var form = new FormData(document.getElementById('login-form'));
fetch("/login", {
method: "POST",
body: form
})
上面代碼中,Request對象的body屬性為表單數據。
Request對象和Response對象都提供以下方法,用來讀取body。
注意,上面這些方法都只能使用一次,第二次使用就會報錯,也就是說,body屬性只能讀取一次。Request對象和Response對象都有bodyUsed屬性,返回一個布爾值,表示body是否被讀取過。
var res = new Response("one time use");
console.log(res.bodyUsed); // false
res.text().then(function(v) {
console.log(res.bodyUsed); // true
});
console.log(res.bodyUsed); // true
res.text().catch(function(e) {
console.log("Tried to read already consumed Response");
});
上面代碼中,第二次通過text方法讀取Response對象實例的body時,就會報錯。
這是因為body屬性是一個stream對象,數據只能單向傳送一次。這樣的設計是為了允許JavaScript處理視頻、音頻這樣的大型文件。
如果希望多次使用body屬性,可以使用Response對象和Request對象的clone方法。它必須在body還沒有讀取前調用,返回一個新的body,也就是說,需要使用幾次body,就要調用幾次clone方法。
addEventListener('fetch', function(evt) {
var sheep = new Response("Dolly");
console.log(sheep.bodyUsed); // false
var clone = sheep.clone();
console.log(clone.bodyUsed); // false
clone.text();
console.log(sheep.bodyUsed); // false
console.log(clone.bodyUsed); // true
evt.respondWith(cache.add(sheep.clone()).then(function(e) {
return sheep;
});
});
Matt West, Uploading Files with AJAX
更多建議: