Ajax(異步請(qǐng)求)

2021-09-15 15:17 更新

Ajax指的是不刷新頁面,發(fā)出異步請(qǐng)求,向服務(wù)器端要求數(shù)據(jù),然后再進(jìn)行處理的方法。

XMLHttpRequest對(duì)象

XMLHttpRequest對(duì)象用于從JavaScript發(fā)出HTTP請(qǐng)求,下面是典型用法。

// 新建一個(gè)XMLHttpRequest實(shí)例對(duì)象
var xhr = new XMLHttpRequest();

// 指定通信過程中狀態(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ā)送過來的數(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()

open方法用于指定發(fā)送HTTP請(qǐng)求的參數(shù),它有三個(gè)參數(shù)如下:

  • 發(fā)送方法,一般來說為“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()

setRequestHeader方法用于設(shè)置HTTP請(qǐng)求的頭信息。

send()

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ā)送許多類型的數(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類型可以用來發(fā)送二進(jìn)制數(shù)據(jù),這使得通過Ajax上傳文件成為可能。

FormData類型可以用于構(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ì)象也能用來模擬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);

readyState屬性和readyStateChange事件

在通信過程中,每當(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ù)器傳送回來的數(shù)據(jù)。

progress事件

上傳文件時(shí),XMLHTTPRequest對(duì)象的upload屬性有一個(gè)progress,會(huì)不斷返回上傳的進(jìn)度。

假定網(wǎng)頁上有一個(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);
}

服務(wù)器返回的信息

(1)status屬性

status屬性表示返回的HTTP狀態(tài)碼。一般來說,如果通信成功的話,這個(gè)狀態(tài)碼是200。

(2)responseText屬性

responseText屬性表示服務(wù)器返回的文本數(shù)據(jù)。

setRequestHeader方法

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ù)長度;最后發(fā)送JSON數(shù)據(jù)。

overrideMimeType方法

該方法用來指定服務(wù)器返回?cái)?shù)據(jù)的MIME類型。

傳統(tǒng)上,如果希望從服務(wù)器取回二進(jìn)制數(shù)據(jù),就要使用這個(gè)方法,人為將數(shù)據(jù)類型偽裝成文本數(shù)據(jù)。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);

// 強(qiáng)制將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();

上面代碼中,因?yàn)閭骰貋淼氖嵌M(jìn)制數(shù)據(jù),首先用xhr.overrideMimeType方法強(qiáng)制改變它的MIME類型,偽裝成文本數(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的方法。

responseType屬性

XMLHttpRequest對(duì)象有一個(gè)responseType屬性,用來指定服務(wù)器返回?cái)?shù)據(jù)(xhr.response)的類型。

XHR 2允許用戶自行設(shè)置這個(gè)屬性,也就是指定返回?cái)?shù)據(jù)的類型,可以設(shè)置如下的值:

  • 'text':返回類型為字符串,這是默認(rèn)值。
  • 'arraybuffer':返回類型為ArrayBuffer。
  • 'blob':返回類型為Blob。
  • 'document':返回類型為Document。
  • 'json':返回類型為JSON object。

text類型適合大多數(shù)情況,而且直接處理文本也比較方便,document類型適合返回XML文檔的情況,blob類型適合讀取二進(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() 方法。也就是說,你從xhr.response屬性(注意,不是xhr.responseText屬性)得到的不是文本,而是一個(gè)JSON對(duì)象。

XHR2支持Ajax的返回類型為文檔,即xhr.responseType="document" 。這意味著,對(duì)于那些打開CORS的網(wǎng)站,我們可以直接用Ajax抓取網(wǎng)頁,然后不用解析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è)文件;如果沒有這個(gè)屬性,則一次只能選擇一個(gè)文件。

file對(duì)象的files屬性,返回一個(gè)FileList對(duì)象,包含了用戶選中的文件。

var fileSelect = document.getElementById('file-select');
var files = fileSelect.files;

然后,新建一個(gè)FormData對(duì)象的實(shí)例,用來模擬發(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);

可以看到,上面這種寫法比FormData的寫法,要簡單很多。

JSONP

JSONP是一種常見做法,用于服務(wù)器與客戶端之間的數(shù)據(jù)傳輸,主要為了規(guī)避瀏覽器的同域限制。因?yàn)锳jax只能向當(dāng)前網(wǎng)頁所在的域名發(fā)出HTTP請(qǐng)求(除非使用下文要提到的CORS,但并不是所有服務(wù)器都支持CORS),所以JSONP就采用在網(wǎng)頁中動(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)頁就可以直接處理example.com返回的數(shù)據(jù)了。

由于script元素返回的腳本文件,是直接作為代碼運(yùn)行的,不像Ajax請(qǐng)求返回的是JSON字符串,需要用JSON.parse方法將字符串轉(zhuǎn)為JSON對(duì)象。于是,為了方便起見,許多服務(wù)器支持JSONP指定回調(diào)函數(shù)的名稱,直接將JSON數(shù)據(jù)放入回調(diào)函數(shù)的參數(shù),如此一來就省略了將字符串解析為JSON對(duì)象的步驟。

請(qǐng)看下面的例子,假定訪問 http://example.com/ip ,返回如下JSON數(shù)據(jù):

{"ip":"8.8.8.8"}

現(xiàn)在服務(wù)器允許客戶端請(qǐng)求時(shí)使用callback參數(shù)指定回調(diào)函數(shù)。訪問 http://example.com/ip?callback=foo ,返回的數(shù)據(jù)變成:

foo({"ip":"8.8.8.8"})

這時(shí),如果客戶端定義了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

CORS的全稱是“跨域資源共享”(Cross-origin resource sharing),它提出一種方法,允許JavaScript代碼向另一個(gè)域名發(fā)出XMLHttpRequests請(qǐng)求,從而克服了傳統(tǒng)上Ajax只能在同一個(gè)域名下使用的限制(same origin security policy)。

所有主流瀏覽器都支持該方法,不過IE8和IE9的該方法不是部署在XMLHttpRequest對(duì)象,而是部署在XDomainRequest對(duì)象。檢查瀏覽器是否支持的代碼如下:

var request = new XMLHttpRequest();

if("withCredentials" in request) {
  // 發(fā)出跨域請(qǐng)求
}

CORS的原理其實(shí)很簡單,就是增加一條HTTP頭信息的查詢,詢問服務(wù)器端,當(dāng)前請(qǐng)求的域名是否在許可名單之中,以及可以使用哪些HTTP動(dòng)詞。如果得到肯定的答復(fù),就發(fā)出XMLHttpRequest請(qǐng)求。這種機(jī)制叫做“預(yù)檢”(preflight)。

“預(yù)檢”的專用HTTP頭信息是Origin。假定用戶正在瀏覽來自www.example.com的網(wǎng)頁,該網(wǎng)頁需要向Google請(qǐng)求數(shù)據(jù),這時(shí)瀏覽器會(huì)向該域名詢問是否同意跨域請(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)求來自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)站都開放,可以返回一個(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ù)器通過了預(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è)過程都是瀏覽器自動(dòng)后臺(tái)完成,不用用戶參與,所以對(duì)于開發(fā)者來說,使用Ajax跨域請(qǐng)求與同域請(qǐng)求沒有區(qū)別,代碼完全一樣。但是,這需要服務(wù)器的支持,所以在使用CORS之前,要查看一下所請(qǐng)求的網(wǎng)站是否支持。

CORS機(jī)制默認(rèn)不發(fā)送cookie和HTTP認(rèn)證信息,除非在Ajax請(qǐng)求中打開withCredentials屬性。

var request = new XMLHttpRequest();
request.withCredentials = true;

同時(shí),服務(wù)器返回HTTP頭信息時(shí),也必須打開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)頁一致的域名。同時(shí),cookie依然遵循同源政策,只有用服務(wù)器域名(前例是www.google.com)設(shè)置的cookie才會(huì)上傳,其他域名下的cookie并不會(huì)上傳,且網(wǎng)頁代碼中的document.cookie也無法讀取www.google.com域名下的cookie。

CORS機(jī)制與JSONP模式的使用目的相同,而且更強(qiáng)大。JSONP只支持GET請(qǐng)求,CORS可以支持所有類型的HTTP請(qǐng)求。在發(fā)生錯(cuò)誤的情況下,CORS可以得到更詳細(xì)的錯(cuò)誤信息,部署更有針對(duì)性的錯(cuò)誤處理代碼。JSONP的優(yōu)勢在于可以用于老式瀏覽器,以及可以向不支持CORS的網(wǎng)站請(qǐng)求數(shù)據(jù)。

Fetch API

基本用法

Ajax操作所用的XMLHttpRequest對(duì)象,已經(jīng)有十多年的歷史,它的API設(shè)計(jì)并不是很好,輸入、輸出、狀態(tài)都在同一個(gè)接口管理,容易寫出非?;靵y的代碼。Fetch API是一種新規(guī)范,用來取代XMLHttpRequest對(duì)象。它主要有兩個(gè)特點(diǎn),一是簡化接口,將API分散在幾個(gè)不同的對(duì)象上,二是返回Promise對(duì)象,避免了嵌套的回調(diào)函數(shù)。

檢查瀏覽器是否部署了這個(gè)API的代碼如下。

if (fetch in window){
  // 支持
} else {
  // 不支持
}

下面是一個(gè)Fetch API的簡單例子。

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寫法與Fetch寫法的不同。

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格式,不過這個(gè)方法返回的是一個(gè)Promise對(duì)象。

fetch()

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。因此,上面的代碼可以寫成下面這樣。

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)的類型,它有以下三個(gè)值。

  • basic:正常的同域請(qǐng)求
  • cors:CORS機(jī)制下的跨域請(qǐng)求
  • opaque:非CORS機(jī)制下的跨域請(qǐng)求,這時(shí)無法讀取返回的數(shù)據(jù),也無法判斷是否請(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ù)還可以用來配置其他值,比如指定cookie連同HTTP請(qǐng)求一起發(fā)出。

fetch(url, {
  credentials: 'include'
})

發(fā)出POST請(qǐng)求的寫法如下。

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還沒做到的地方,比如中途中斷HTTP請(qǐng)求,以及獲取HTTP請(qǐng)求的進(jìn)度。這些不足與Fetch返回的是Promise對(duì)象有關(guān)。

Headers

Fetch API引入三個(gè)新的對(duì)象(也是構(gòu)造函數(shù)):Headers, Request 和 Response。其中,Headers對(duì)象用來構(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方法添加屬性,也可以直接通過構(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í)例可以用來構(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)沒有太大用處,但是隨著Service Worker的部署,不久瀏覽器就可以向Service Worker發(fā)出HTTP回應(yīng)。

Request對(duì)象

Request對(duì)象用來構(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)求的來源,由瀏覽器設(shè)置,有可能是空字符串。另一個(gè)是context屬性,表示請(qǐng)求發(fā)出的上下文,如果是image,表示是從img標(biāo)簽發(fā)出,如果是worker,表示是從worker腳本發(fā)出,如果是fetch,表示是從fetch函數(shù)發(fā)出的。

Request對(duì)象實(shí)例的mode屬性,用來設(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!");
});

上面代碼中,如果用戶輸入的URL不是同域的,將會(huì)報(bào)錯(cuò),否則就會(huì)發(fā)出請(qǐng)求。

如果mode屬性為no-cors,就與默認(rèn)的瀏覽器行為沒有不同,類似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'});

Response

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ò),無法取得信息,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

body屬性

Request對(duì)象和Response對(duì)象都有body屬性,表示請(qǐng)求的內(nèi)容。body屬性可能是以下的數(shù)據(jù)類型。

  • 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ì)象都提供以下方法,用來讀取body。

  • arrayBuffer()
  • blob()
  • json()
  • text()
  • formData()

注意,上面這些方法都只能使用一次,第二次使用就會(huì)報(bào)錯(cuò),也就是說,body屬性只能讀取一次。Request對(duì)象和Response對(duì)象都有bodyUsed屬性,返回一個(gè)布爾值,表示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對(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還沒有讀取前調(diào)用,返回一個(gè)前的body,也就是說,需要使用幾次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;
  });
});

參考鏈接

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)