?
This document uses PHP Chinese website manual Release
Ajax指的是不刷新頁(yè)面,發(fā)出異步請(qǐng)求,向服務(wù)器端要求數(shù)據(jù),然后再進(jìn)行處理的方法。
XMLHttpRequest對(duì)象
Open()
setRequestHeader()
send()
readyState屬性和readyStateChange事件
progress事件
服務(wù)器返回的信息
setRequestHeader方法
overrideMimeType方法
responseType屬性
文件上傳
JSONP
CORS
Fetch API
基本用法
fetch()
Headers
Request對(duì)象
Response
body屬性
參考鏈接
XMLHttpRequest對(duì)象用于從JavaScript發(fā)出HTTP請(qǐng)求,下面是典型用法。
// 新建一個(gè)XMLHttpRequest實(shí)例對(duì)象 var xhr = new XMLHttpRequest(); // 指定通信過(guò)程中狀態(tài)改變時(shí)的回調(diào)函數(shù) xhr.onreadystatechange = function(){ // 通信成功時(shí),狀態(tài)值為4 var completed = 4; if(xhr.readyState === completed){ if(xhr.status === 200){ // 處理服務(wù)器發(fā)送過(guò)來(lái)的數(shù)據(jù) }else{ // 處理錯(cuò)誤 } } }; // open方式用于指定HTTP動(dòng)詞、請(qǐng)求的網(wǎng)址、是否異步 xhr.open('GET', '/endpoint', true); // 發(fā)送HTTP請(qǐng)求 xhr.send(null);
open方法用于指定發(fā)送HTTP請(qǐng)求的參數(shù),它有三個(gè)參數(shù)如下:
發(fā)送方法,一般來(lái)說(shuō)為“GET”、“POST”、“PUT”和“DELETE”中的一個(gè)值。
網(wǎng)址。
是否異步,true表示異步,false表示同步。
下面發(fā)送POST請(qǐng)求的例子。
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請(qǐng)求,send方法送出實(shí)際的數(shù)據(jù)。
setRequestHeader方法用于設(shè)置HTTP請(qǐng)求的頭信息。
send方法用于實(shí)際發(fā)出HTTP請(qǐng)求。如果不帶參數(shù),就表示HTTP請(qǐng)求只包含頭信息,也就是只有一個(gè)URL,典型例子就是GET請(qǐng)求;如果帶有參數(shù),就表示除了頭信息,還帶有包含具體數(shù)據(jù)的信息體,典型例子就是POST請(qǐng)求。
在XHR 2之中,send方法可以發(fā)送許多類(lèi)型的數(shù)據(jù)。
void send(); void send(ArrayBuffer data); void send(Blob data); void send(Document data); void send(DOMString data); void send(FormData data);
Blob類(lèi)型可以用來(lái)發(fā)送二進(jìn)制數(shù)據(jù),這使得通過(guò)Ajax上傳文件成為可能。
FormData類(lèi)型可以用于構(gòu)造表單數(shù)據(jù)。
var formData = new FormData(); formData.append('username', '張三'); formData.append('email', 'zhangsan@example.com'); formData.append('birthDate', 1940); xhr.send(formData);
上面的代碼構(gòu)造了一個(gè)formData對(duì)象,然后使用send方法發(fā)送。它的效果與點(diǎn)擊下面表單的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對(duì)象還可以對(duì)現(xiàn)有表單添加數(shù)據(jù),這為我們操作表單提供了極大的靈活性。
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對(duì)象也能用來(lái)模擬File控件,進(jìn)行文件上傳。
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);
在通信過(guò)程中,每當(dāng)發(fā)生狀態(tài)變化的時(shí)候,readyState屬性的值就會(huì)發(fā)生改變。
這個(gè)值每一次變化,都會(huì)觸發(fā)readyStateChange事件。我們可以指定這個(gè)事件的回調(diào)函數(shù),對(duì)不同狀態(tài)進(jìn)行不同處理。尤其是當(dāng)狀態(tài)變?yōu)?的時(shí)候,表示通信成功,這時(shí)回調(diào)函數(shù)就可以處理服務(wù)器傳送回來(lái)的數(shù)據(jù)。
上傳文件時(shí),XMLHTTPRequest對(duì)象的upload屬性有一個(gè)progress,會(huì)不斷返回上傳的進(jìn)度。
假定網(wǎng)頁(yè)上有一個(gè)progress元素。
<progress min="0" max="100" value="0">0% complete</progress>
文件上傳時(shí),對(duì)upload屬性指定progress事件回調(diào)函數(shù),即可獲得上傳的進(jìn)度。
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'}));
下面是一個(gè)上傳ArrayBuffer對(duì)象的例子。
function sendArrayBuffer() { var xhr = new XMLHttpRequest(); xhr.open('POST', '/server', true); xhr.onload = function(e) { ... }; var uInt8Array = new Uint8Array([1, 2, 3]); xhr.send(uInt8Array.buffer); }
(1)status屬性
status屬性表示返回的HTTP狀態(tài)碼。一般來(lái)說(shuō),如果通信成功的話,這個(gè)狀態(tài)碼是200。
(2)responseText屬性
responseText屬性表示服務(wù)器返回的文本數(shù)據(jù)。
setRequestHeader方法用于設(shè)置HTTP頭信息。
xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Content-Length', JSON.stringify(data).length); xhr.send(JSON.stringify(data));
上面代碼首先設(shè)置頭信息Content-Type,表示發(fā)送JSON格式的數(shù)據(jù);然后設(shè)置Content-Length,表示數(shù)據(jù)長(zhǎng)度;最后發(fā)送JSON數(shù)據(jù)。
該方法用來(lái)指定服務(wù)器返回?cái)?shù)據(jù)的MIME類(lèi)型。
傳統(tǒng)上,如果希望從服務(wù)器取回二進(jìn)制數(shù)據(jù),就要使用這個(gè)方法,人為將數(shù)據(jù)類(lèi)型偽裝成文本數(shù)據(jù)。
var xhr = new XMLHttpRequest(); xhr.open('GET', '/path/to/image.png', true); // 強(qiáng)制將MIME改為文本類(lèi)型 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.send();
上面代碼中,因?yàn)閭骰貋?lái)的是二進(jìn)制數(shù)據(jù),首先用xhr.overrideMimeType方法強(qiáng)制改變它的MIME類(lèi)型,偽裝成文本數(shù)據(jù)。字符集必需指定為“x-user-defined”,如果是其他字符集,瀏覽器內(nèi)部會(huì)強(qiáng)制轉(zhuǎn)碼,將其保存成UTF-16的形式。字符集“x-user-defined”其實(shí)也會(huì)發(fā)生轉(zhuǎn)碼,瀏覽器會(huì)在每個(gè)字節(jié)前面再加上一個(gè)字節(jié)(0xF700-0xF7ff),因此后面要對(duì)每個(gè)字符進(jìn)行一次與運(yùn)算(&),將高位的8個(gè)位去除,只留下低位的8個(gè)位,由此逐一讀出原文件二進(jìn)制數(shù)據(jù)的每個(gè)字節(jié)。
這種方法很麻煩,在XMLHttpRequest版本升級(jí)以后,一般采用下面的指定responseType的方法。
XMLHttpRequest對(duì)象有一個(gè)responseType屬性,用來(lái)指定服務(wù)器返回?cái)?shù)據(jù)(xhr.response)的類(lèi)型。
XHR 2允許用戶(hù)自行設(shè)置這個(gè)屬性,也就是指定返回?cái)?shù)據(jù)的類(lèi)型,可以設(shè)置如下的值:
'text':返回類(lèi)型為字符串,這是默認(rèn)值。
'arraybuffer':返回類(lèi)型為ArrayBuffer。
'blob':返回類(lèi)型為Blob。
'document':返回類(lèi)型為Document。
'json':返回類(lèi)型為JSON object。
text類(lèi)型適合大多數(shù)情況,而且直接處理文本也比較方便,document類(lèi)型適合返回XML文檔的情況,blob類(lèi)型適合讀取二進(jìn)制數(shù)據(jù),比如圖片文件。
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'}); // ... } }; xhr.send();
如果將這個(gè)屬性設(shè)為ArrayBuffer,就可以按照數(shù)組的方式處理二進(jìn)制數(shù)據(jù)。
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();
如果將這個(gè)屬性設(shè)為“json”,支持JSON的瀏覽器(Firefox>9,chrome>30),就會(huì)自動(dòng)對(duì)返回?cái)?shù)據(jù)調(diào)用JSON.parse() 方法。也就是說(shuō),你從xhr.response屬性(注意,不是xhr.responseText屬性)得到的不是文本,而是一個(gè)JSON對(duì)象。
XHR2支持Ajax的返回類(lèi)型為文檔,即xhr.responseType="document" 。這意味著,對(duì)于那些打開(kāi)CORS的網(wǎng)站,我們可以直接用Ajax抓取網(wǎng)頁(yè),然后不用解析HTML字符串,直接對(duì)XHR回應(yīng)進(jìn)行DOM操作。
通常,我們使用file控件實(shí)現(xiàn)文件上傳。
<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屬性,指定可以一次選擇多個(gè)文件;如果沒(méi)有這個(gè)屬性,則一次只能選擇一個(gè)文件。
file對(duì)象的files屬性,返回一個(gè)FileList對(duì)象,包含了用戶(hù)選中的文件。
var fileSelect = document.getElementById('file-select'); var files = fileSelect.files;
然后,新建一個(gè)FormData對(duì)象的實(shí)例,用來(lái)模擬發(fā)送到服務(wù)器的表單數(shù)據(jù),把選中的文件添加到這個(gè)對(duì)象上面。
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對(duì)象的append方法,除了可以添加文件,還可以添加二進(jìn)制對(duì)象(Blob)或者字符串。
// Files formData.append(name, file, filename); // Blobs formData.append(name, blob, filename); // Strings formData.append(name, value);
append方法的第一個(gè)參數(shù)是表單的控件名,第二個(gè)參數(shù)是實(shí)際的值,第三個(gè)參數(shù)是可選的,通常是文件名。
最后,使用Ajax方法向服務(wù)器上傳文件。
var xhr = new XMLHttpRequest(); xhr.open('POST', 'handler.php', true); xhr.onload = function () { if (xhr.status !== 200) { alert('An error occurred!'); } }; xhr.send(formData);
目前,各大瀏覽器(包括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);
可以看到,上面這種寫(xiě)法比FormData的寫(xiě)法,要簡(jiǎn)單很多。
JSONP是一種常見(jiàn)做法,用于服務(wù)器與客戶(hù)端之間的數(shù)據(jù)傳輸,主要為了規(guī)避瀏覽器的同域限制。因?yàn)锳jax只能向當(dāng)前網(wǎng)頁(yè)所在的域名發(fā)出HTTP請(qǐng)求(除非使用下文要提到的CORS,但并不是所有服務(wù)器都支持CORS),所以JSONP就采用在網(wǎng)頁(yè)中動(dòng)態(tài)插入script元素的做法,向服務(wù)器請(qǐng)求腳本文件。
function addScriptTag(src){ var script = document.createElement('script'); script.setAttribute("type","text/javascript"); script.src = src; document.body.appendChild(script); } window.onload = function(){ addScriptTag("http://example.com/ip?callback=foo"); } function foo(data) { console.log('Your public IP address is: ' + data.ip); };
上面代碼使用了JSONP,運(yùn)行以后當(dāng)前網(wǎng)頁(yè)就可以直接處理example.com返回的數(shù)據(jù)了。
由于script元素返回的腳本文件,是直接作為代碼運(yùn)行的,不像Ajax請(qǐng)求返回的是JSON字符串,需要用JSON.parse方法將字符串轉(zhuǎn)為JSON對(duì)象。于是,為了方便起見(jiàn),許多服務(wù)器支持JSONP指定回調(diào)函數(shù)的名稱(chēng),直接將JSON數(shù)據(jù)放入回調(diào)函數(shù)的參數(shù),如此一來(lái)就省略了將字符串解析為JSON對(duì)象的步驟。
請(qǐng)看下面的例子,假定訪問(wèn) http://example.com/ip ,返回如下JSON數(shù)據(jù):
{"ip":"8.8.8.8"}
現(xiàn)在服務(wù)器允許客戶(hù)端請(qǐng)求時(shí)使用callback參數(shù)指定回調(diào)函數(shù)。訪問(wèn) http://example.com/ip?callback=foo ,返回的數(shù)據(jù)變成:
foo({"ip":"8.8.8.8"})
這時(shí),如果客戶(hù)端定義了foo函數(shù),該函數(shù)就會(huì)被立即調(diào)用,而作為參數(shù)的JSON數(shù)據(jù)被視為JavaScript對(duì)象,而不是字符串,因此避免了使用JSON.parse的步驟。
function foo(data) { console.log('Your public IP address is: ' + data.ip); };
jQuery的getJSON方法就是JSONP的一個(gè)應(yīng)用。
$.getJSON( "http://example.com/api", function (data){ .... })
$.getJSON方法的第一個(gè)參數(shù)是服務(wù)器網(wǎng)址,第二個(gè)參數(shù)是回調(diào)函數(shù),該回調(diào)函數(shù)的參數(shù)就是服務(wù)器返回的JSON數(shù)據(jù)。
CORS的全稱(chēng)是“跨域資源共享”(Cross-origin resource sharing),它提出一種方法,允許JavaScript代碼向另一個(gè)域名發(fā)出XMLHttpRequests請(qǐng)求,從而克服了傳統(tǒng)上Ajax只能在同一個(gè)域名下使用的限制(same origin security policy)。
所有主流瀏覽器都支持該方法,不過(guò)IE8和IE9的該方法不是部署在XMLHttpRequest對(duì)象,而是部署在XDomainRequest對(duì)象。檢查瀏覽器是否支持的代碼如下:
var request = new XMLHttpRequest(); if("withCredentials" in request) { // 發(fā)出跨域請(qǐng)求 }
CORS的原理其實(shí)很簡(jiǎn)單,就是增加一條HTTP頭信息的查詢(xún),詢(xún)問(wèn)服務(wù)器端,當(dāng)前請(qǐng)求的域名是否在許可名單之中,以及可以使用哪些HTTP動(dòng)詞。如果得到肯定的答復(fù),就發(fā)出XMLHttpRequest請(qǐng)求。這種機(jī)制叫做“預(yù)檢”(preflight)。
“預(yù)檢”的專(zhuān)用HTTP頭信息是Origin。假定用戶(hù)正在瀏覽來(lái)自www.example.com的網(wǎng)頁(yè),該網(wǎng)頁(yè)需要向Google請(qǐng)求數(shù)據(jù),這時(shí)瀏覽器會(huì)向該域名詢(xún)問(wèn)是否同意跨域請(qǐng)求,發(fā)出的HTTP頭信息如下:
OPTIONS /resources/post-here/ HTTP/1.1 Host: www.google.com User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Origin: http://www.example.com Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER
上面的HTTP請(qǐng)求,它的動(dòng)詞是OPTIONS,表示這是一個(gè)“預(yù)檢”請(qǐng)求。除了提供瀏覽器信息,里面關(guān)鍵的一行是Origin頭信息。
Origin: http://www.example.com
這行HTTP頭信息表示,請(qǐng)求來(lái)自www.example.com。服務(wù)端如果同意,就返回一個(gè)Access-Control-Allow-Origin頭信息。
預(yù)檢請(qǐng)求中,瀏覽器還告訴服務(wù)器,實(shí)際發(fā)出請(qǐng)求,將使用HTTP動(dòng)詞POST,以及一個(gè)自定義的頭信息X-PINGOTHER。
Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER
服務(wù)器收到預(yù)檢請(qǐng)求之后,做出了回應(yīng)。
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://www.example.com Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER Access-Control-Max-Age: 1728000 Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
上面的HTTP回應(yīng)里面,關(guān)鍵的是Access-Control-Allow-Origin頭信息。這表示服務(wù)器同意www.example.com的跨域請(qǐng)求。
Access-Control-Allow-Origin: http://www.example.com
如果不同意,服務(wù)器端會(huì)返回一個(gè)錯(cuò)誤。
如果服務(wù)器端對(duì)所有網(wǎng)站都開(kāi)放,可以返回一個(gè)星號(hào)(*)通配符。
Access-Control-Allow-Origin: *
服務(wù)器還告訴瀏覽器,允許的HTTP動(dòng)詞是POST、GET、OPTIONS,也允許自定義的頭信息X-PINGOTHER,
Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER Access-Control-Max-Age: 1728000
如果服務(wù)器通過(guò)了預(yù)檢請(qǐng)求,則以后每次瀏覽器正常的HTTP請(qǐng)求,都會(huì)有一個(gè)origin頭信息;服務(wù)器的回應(yīng),也都會(huì)有一個(gè)Access-Control-Allow-Origin頭信息。Access-Control-Max-Age頭信息表示,允許緩存該條回應(yīng)1728000秒(即20天),在此期間,不用發(fā)出另一條預(yù)檢請(qǐng)求。
由于整個(gè)過(guò)程都是瀏覽器自動(dòng)后臺(tái)完成,不用用戶(hù)參與,所以對(duì)于開(kāi)發(fā)者來(lái)說(shuō),使用Ajax跨域請(qǐng)求與同域請(qǐng)求沒(méi)有區(qū)別,代碼完全一樣。但是,這需要服務(wù)器的支持,所以在使用CORS之前,要查看一下所請(qǐng)求的網(wǎng)站是否支持。
CORS機(jī)制默認(rèn)不發(fā)送cookie和HTTP認(rèn)證信息,除非在Ajax請(qǐng)求中打開(kāi)withCredentials屬性。
var request = new XMLHttpRequest(); request.withCredentials = true;
同時(shí),服務(wù)器返回HTTP頭信息時(shí),也必須打開(kāi)Access-Control-Allow-Credentials選項(xiàng)。否則,瀏覽器會(huì)忽略服務(wù)器返回的回應(yīng)。
Access-Control-Allow-Credentials: true
需要注意的是,此時(shí)Access-Control-Allow-Origin不能指定為星號(hào),必須指定明確的、與請(qǐng)求網(wǎng)頁(yè)一致的域名。同時(shí),cookie依然遵循同源政策,只有用服務(wù)器域名(前例是www.google.com)設(shè)置的cookie才會(huì)上傳,其他域名下的cookie并不會(huì)上傳,且網(wǎng)頁(yè)代碼中的document.cookie也無(wú)法讀取www.google.com域名下的cookie。
CORS機(jī)制與JSONP模式的使用目的相同,而且更強(qiáng)大。JSONP只支持GET請(qǐng)求,CORS可以支持所有類(lèi)型的HTTP請(qǐng)求。在發(fā)生錯(cuò)誤的情況下,CORS可以得到更詳細(xì)的錯(cuò)誤信息,部署更有針對(duì)性的錯(cuò)誤處理代碼。JSONP的優(yōu)勢(shì)在于可以用于老式瀏覽器,以及可以向不支持CORS的網(wǎng)站請(qǐng)求數(shù)據(jù)。
Ajax操作所用的XMLHttpRequest對(duì)象,已經(jīng)有十多年的歷史,它的API設(shè)計(jì)并不是很好,輸入、輸出、狀態(tài)都在同一個(gè)接口管理,容易寫(xiě)出非?;靵y的代碼。Fetch API是一種新規(guī)范,用來(lái)取代XMLHttpRequest對(duì)象。它主要有兩個(gè)特點(diǎn),一是簡(jiǎn)化接口,將API分散在幾個(gè)不同的對(duì)象上,二是返回Promise對(duì)象,避免了嵌套的回調(diào)函數(shù)。
檢查瀏覽器是否部署了這個(gè)API的代碼如下。
if (fetch in window){ // 支持 } else { // 不支持 }
下面是一個(gè)Fetch API的簡(jiǎn)單例子。
var URL = 'http://some/path'; fetch(URL).then(function(response) { return response.json(); }).then(function(json) { someOperator(json); });
上面代碼向服務(wù)器請(qǐng)求JSON文件,獲取后再做進(jìn)一步處理。
下面比較XMLHttpRequest寫(xiě)法與Fetch寫(xiě)法的不同。
function reqListener() { var data = JSON.parse(this.responseText); console.log(data); } function reqError(err) { console.log('Fetch Error :-S', err); } var oReq = new XMLHttpRequest(); oReq.onload = reqListener; oReq.onerror = reqError; oReq.open('get', './api/some.json', true); oReq.send();
同樣的操作用Fetch實(shí)現(xiàn)如下。
fetch('./api/some.json') .then(function(response) { if (response.status !== 200) { console.log('請(qǐng)求失敗,狀態(tài)碼:' + response.status); return; } response.json().then(function(data) { console.log(data); }); }).catch(function(err) { console.log('出錯(cuò):', err); });
上面代碼中,因?yàn)镠TTP請(qǐng)求返回的response對(duì)象是一個(gè)Stream對(duì)象,所以需要使用response.json
方法轉(zhuǎn)為JSON格式,不過(guò)這個(gè)方法返回的是一個(gè)Promise對(duì)象。
fetch方法的第一個(gè)參數(shù)可以是URL字符串,也可以是后文要講到的Request對(duì)象實(shí)例。Fetch方法返回一個(gè)Promise對(duì)象,并將一個(gè)response對(duì)象傳給回調(diào)函數(shù)。
response對(duì)象還有一個(gè)ok屬性,如果返回的狀態(tài)碼在200到299之間(即請(qǐng)求成功),這個(gè)屬性為true,否則為false。因此,上面的代碼可以寫(xiě)成下面這樣。
fetch("./api/some.json").then(function(response) { if (response.ok) { response.json().then(function(data) { console.log(data); }); } else { console.log("請(qǐng)求失敗,狀態(tài)碼為", response.status); } }, function(err) { console.log("出錯(cuò):", err); });
response對(duì)象除了json方法,還包含了HTTP回應(yīng)的元數(shù)據(jù)。
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對(duì)象有很多屬性,其中的response.type
屬性比較特別,表示HTTP回應(yīng)的類(lèi)型,它有以下三個(gè)值。
basic:正常的同域請(qǐng)求
cors:CORS機(jī)制下的跨域請(qǐng)求
opaque:非CORS機(jī)制下的跨域請(qǐng)求,這時(shí)無(wú)法讀取返回的數(shù)據(jù),也無(wú)法判斷是否請(qǐng)求成功
如果需要在CORS機(jī)制下發(fā)出跨域請(qǐng)求,需要指明狀態(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方法的第二個(gè)參數(shù)還可以用來(lái)配置其他值,比如指定cookie連同HTTP請(qǐng)求一起發(fā)出。
fetch(url, { credentials: 'include' })
發(fā)出POST請(qǐng)求的寫(xiě)法如下。
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對(duì)象可以做到,但是Fetch API還沒(méi)做到的地方,比如中途中斷HTTP請(qǐng)求,以及獲取HTTP請(qǐng)求的進(jìn)度。這些不足與Fetch返回的是Promise對(duì)象有關(guān)。
Fetch API引入三個(gè)新的對(duì)象(也是構(gòu)造函數(shù)):Headers, Request 和 Response。其中,Headers對(duì)象用來(lái)構(gòu)造/讀取HTTP數(shù)據(jù)包的頭信息。
var content = "Hello World";var reqHeaders = new Headers(); reqHeaders.append("Content-Type", "text/plain"); reqHeaders.append("Content-Length", content.length.toString()); reqHeaders.append("X-Custom-Header", "ProcessThisImmediately");
Headers對(duì)象的實(shí)例,除了使用append方法添加屬性,也可以直接通過(guò)構(gòu)造函數(shù)一次性生成。
reqHeaders = new Headers({ "Content-Type": "text/plain", "Content-Length": content.length.toString(), "X-Custom-Header": "ProcessThisImmediately", });
Headers對(duì)象實(shí)例還提供了一些工具方法。
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實(shí)例以后,可以將它作為第二個(gè)參數(shù),傳入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實(shí)例可以用來(lái)構(gòu)造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); });
上面代碼中,構(gòu)造了一個(gè)HTTP回應(yīng)。目前,瀏覽器構(gòu)造HTTP回應(yīng)沒(méi)有太大用處,但是隨著Service Worker的部署,不久瀏覽器就可以向Service Worker發(fā)出HTTP回應(yīng)。
Request對(duì)象用來(lái)構(gòu)造HTTP請(qǐng)求。
var req = new Request("/index.html"); req.method // "GET" req.url // "http://example.com/index.html"
Request對(duì)象的第二個(gè)參數(shù),表示配置對(duì)象。
var uploadReq = new Request("/uploadImage", { method: "POST", headers: { "Content-Type": "image/png", }, body: "image data" });
上面代碼指定Request對(duì)象使用POST方法發(fā)出,并指定HTTP頭信息和信息體。
下面是另一個(gè)例子。
var req = new Request(URL, {method: 'GET', cache: 'reload'}); fetch(req).then(function(response) { return response.json(); }).then(function(json) { someOperator(json); });
上面代碼中,指定請(qǐng)求方法為GET,并且要求瀏覽器不得緩存response。
Request對(duì)象實(shí)例有兩個(gè)屬性是只讀的,不能手動(dòng)設(shè)置。一個(gè)是referrer屬性,表示請(qǐng)求的來(lái)源,由瀏覽器設(shè)置,有可能是空字符串。另一個(gè)是context屬性,表示請(qǐng)求發(fā)出的上下文,如果是image,表示是從img標(biāo)簽發(fā)出,如果是worker,表示是從worker腳本發(fā)出,如果是fetch,表示是從fetch函數(shù)發(fā)出的。
Request對(duì)象實(shí)例的mode屬性,用來(lái)設(shè)置是否跨域,合法的值有以下三種:same-origin、no-cors(默認(rèn)值)、cors。當(dāng)設(shè)置為same-origin時(shí),只能向同域的URL發(fā)出請(qǐng)求,否則會(huì)報(bào)錯(cuò)。
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!"); });
上面代碼中,如果用戶(hù)輸入的URL不是同域的,將會(huì)報(bào)錯(cuò),否則就會(huì)發(fā)出請(qǐng)求。
如果mode屬性為no-cors,就與默認(rèn)的瀏覽器行為沒(méi)有不同,類(lèi)似script標(biāo)簽加載外部腳本文件、img標(biāo)簽加載外部圖片。如果mode屬性為cors,就可以向部署了CORS機(jī)制的服務(wù)器,發(fā)出跨域請(qǐng)求。
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ā)出圖片請(qǐng)求的例子。
Request對(duì)象的一個(gè)很有用的功能,是在其他Request實(shí)例的基礎(chǔ)上,生成新的Request實(shí)例。
var postReq = new Request(req, {method: 'POST'});
fetch方法返回Response對(duì)象實(shí)例,它有以下屬性。
status:整數(shù)值,表示狀態(tài)碼(比如200)
statusText:字符串,表示狀態(tài)信息,默認(rèn)是“OK”
ok:布爾值,表示狀態(tài)碼是否在200-299的范圍內(nèi)
headers:Headers對(duì)象,表示HTTP回應(yīng)的頭信息
url:字符串,表示HTTP請(qǐng)求的網(wǎng)址
type:字符串,合法的值有五個(gè)basic、cors、default、error、opaque。basic表示正常的同域請(qǐng)求;cors表示CORS機(jī)制的跨域請(qǐng)求;error表示網(wǎng)絡(luò)出錯(cuò),無(wú)法取得信息,status屬性為0,headers屬性為空,并且導(dǎo)致fetch函數(shù)返回Promise對(duì)象被拒絕;opaque表示非CORS機(jī)制的跨域請(qǐng)求,受到嚴(yán)格限制。
Response對(duì)象還有兩個(gè)靜態(tài)方法。
Response.error() 返回一個(gè)type屬性為error的Response對(duì)象實(shí)例
Response.redirect(url, status) 返回的Response對(duì)象實(shí)例會(huì)重定向到另一個(gè)URL
Request對(duì)象和Response對(duì)象都有body屬性,表示請(qǐng)求的內(nèi)容。body屬性可能是以下的數(shù)據(jù)類(lèi)型。
ArrayBuffer
ArrayBufferView (Uint8Array等)
Blob/File
string
URLSearchParams
FormData
var form = new FormData(document.getElementById('login-form')); fetch("/login", { method: "POST", body: form })
上面代碼中,Request對(duì)象的body屬性為表單數(shù)據(jù)。
Request對(duì)象和Response對(duì)象都提供以下方法,用來(lái)讀取body。
arrayBuffer()
blob()
json()
text()
formData()
注意,上面這些方法都只能使用一次,第二次使用就會(huì)報(bào)錯(cuò),也就是說(shuō),body屬性只能讀取一次。Request對(duì)象和Response對(duì)象都有bodyUsed屬性,返回一個(gè)布爾值,表示body是否被讀取過(guò)。
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"); });
上面代碼中,第二次通過(guò)text方法讀取Response對(duì)象實(shí)例的body時(shí),就會(huì)報(bào)錯(cuò)。
這是因?yàn)閎ody屬性是一個(gè)stream對(duì)象,數(shù)據(jù)只能單向傳送一次。這樣的設(shè)計(jì)是為了允許JavaScript處理視頻、音頻這樣的大型文件。
如果希望多次使用body屬性,可以使用Response對(duì)象和Request對(duì)象的clone方法。它必須在body還沒(méi)有讀取前調(diào)用,返回一個(gè)前的body,也就是說(shuō),需要使用幾次body,就要調(diào)用幾次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; }); });
MDN, Using XMLHttpRequest
Eric Bidelman, New Tricks in XMLHttpRequest2
Matt West,
Monsur Hossain, Using CORS
Matt Gaunt, Introduction to fetch()
Nikhil Marathe, This API is so Fetching!
Ludovico Fischer, Introduction to the Fetch API