fetch 使用
1. 瀏覽器支持情況
fetch是相對(duì)較新的技術(shù),當(dāng)然就會(huì)存在瀏覽器兼容性的問題,當(dāng)前各個(gè)瀏覽器低版本的情況下都是不被支持的,因此為了在所有主流瀏覽器中使用fetch 需要考慮 fetch 的 polyfill 了
require('es6-promise').polyfill(); require('isomorphic-fetch');12
引入這兩個(gè)文件,就可以支持主流瀏覽器了
fetch和XMLHttpRequest
如果看網(wǎng)上的fetch教程,會(huì)首先對(duì)比XMLHttpRequest和fetch的優(yōu)劣,然后引出一堆看了很快會(huì)忘記的內(nèi)容(本人記性不好)。因此,我寫一篇關(guān)于fetch的文章,為了自己看著方便,畢竟工作中用到的也就是一些很基礎(chǔ)的點(diǎn)而已。
fetch,說白了,就是XMLHttpRequest的一種替代方案。如果有人問你,除了Ajax獲取后臺(tái)數(shù)據(jù)之外,還有沒有其他的替代方案?
這是你就可以回答,除了XMLHttpRequest對(duì)象來獲取后臺(tái)的數(shù)據(jù)之外,還可以使用一種更優(yōu)的解決方案fetch。
如何獲取fetch
到現(xiàn)在為止,fetch的支持性還不是很好,但是在谷歌瀏覽器中已經(jīng)支持了fetch。fetch掛在在BOM中,可以直接在谷歌瀏覽器中使用。
查看fetch的支持情況:fetch的支持情況
當(dāng)然,如果不支持fetch也沒有問題,可以使用第三方的ployfill來實(shí)現(xiàn)只會(huì)fetch:whatwg-fetch
fetch的helloworld
下面我們來寫第一個(gè)fetch獲取后端數(shù)據(jù)的例子:
// 通過fetch獲取百度的錯(cuò)誤提示頁面
fetch('https://www.baidu.com/search/error.html') // 返回一個(gè)Promise對(duì)象 .then((res)=>{ return res.text() // res.text()是一個(gè)Promise對(duì)象 }) .then((res)=>{ console.log(res) // res是最終的結(jié)果 })
是不是很簡單?可能難的地方就是Promise的寫法,這個(gè)可以看阮一峰老師的ES6教程來學(xué)習(xí)。
說明一點(diǎn),下面演示的GET請(qǐng)求或POST請(qǐng)求,都是采用百度中查詢到的一些接口,可能傳遞的有些參數(shù)這個(gè)接口并不會(huì)解析,但不會(huì)影響這個(gè)接口的使用。
GET請(qǐng)求
GET請(qǐng)求初步
完成了helloworld,這個(gè)時(shí)候就要來認(rèn)識(shí)一下GET請(qǐng)求如何處理了。
上面的helloworld中這是使用了第一個(gè)參數(shù),其實(shí)fetch還可以提供第二個(gè)參數(shù),就是用來傳遞一些初始化的信息。
這里如果要特別指明是GET請(qǐng)求,就要寫成下面的形式:
// 通過fetch獲取百度的錯(cuò)誤提示頁面 fetch('https://www.baidu.com/search/error.html', { method: 'GET' }) .then((res)=>{ return res.text() }) .then((res)=>{ console.log(res) })
GET請(qǐng)求的參數(shù)傳遞
GET請(qǐng)求中如果需要傳遞參數(shù)怎么辦?這個(gè)時(shí)候,只能把參數(shù)寫在URL上來進(jìn)行傳遞了。
// 通過fetch獲取百度的錯(cuò)誤提示頁面 fetch('https://www.baidu.com/search/error.html?a=1&b=2', { // 在URL中寫上傳遞的參數(shù) method: 'GET' }) .then((res)=>{ return res.text() }) .then((res)=>{ console.log(res) })
POST請(qǐng)求
POST請(qǐng)求初步
與GET請(qǐng)求類似,POST請(qǐng)求的指定也是在fetch的第二個(gè)參數(shù)中:
// 通過fetch獲取百度的錯(cuò)誤提示頁面 fetch('https://www.baidu.com/search/error.html', { method: 'POST' // 指定是POST請(qǐng)求 }) .then((res)=>{ return res.text() }) .then((res)=>{ console.log(res) })
POST請(qǐng)求參數(shù)的傳遞
眾所周知,POST請(qǐng)求的參數(shù),一定不能放在URL中,這樣做的目的是防止信息泄露。
// 通過fetch獲取百度的錯(cuò)誤提示頁面 fetch('https://www.baidu.com/search/error.html', { method: 'POST', body: new URLSearchParams([["foo", 1],["bar", 2]]).toString() // 這里是請(qǐng)求對(duì)象 }) .then((res)=>{ return res.text() }) .then((res)=>{ console.log(res) })
其實(shí)除了對(duì)象URLSearchParams外,還有幾個(gè)其他的對(duì)象,可以參照:常用的幾個(gè)對(duì)象來學(xué)習(xí)使用。
設(shè)置請(qǐng)求的頭信息
在POST提交的過程中,一般是表單提交,可是,經(jīng)過查詢,發(fā)現(xiàn)默認(rèn)的提交方式是:Content-Type:text/plain;charset=UTF-8,這個(gè)顯然是不合理的。下面咱們學(xué)習(xí)一下,指定頭信息:
// 通過fetch獲取百度的錯(cuò)誤提示頁面 fetch('https://www.baidu.com/search/error.html', { method: 'POST', headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' // 指定提交方式為表單提交 }), body: new URLSearchParams([["foo", 1],["bar", 2]]).toString() }) .then((res)=>{ return res.text() }) .then((res)=>{ console.log(res) })
這個(gè)時(shí)候,在谷歌瀏覽器的Network中查詢,會(huì)發(fā)現(xiàn),請(qǐng)求方式已經(jīng)變成了content-type:application/x-www-form-urlencoded。
通過接口得到JSON數(shù)據(jù)
上面所有的例子中都是返回一個(gè)文本,那么除了文本,有沒有其他的數(shù)據(jù)類型呢?肯定是有的,具體查詢地址:Body的類型
由于最常用的是JSON數(shù)據(jù),那么下面就簡單演示一下獲取JSON數(shù)據(jù)的方式:
// 通過fetch獲取百度的錯(cuò)誤提示頁面 fetch('https://www.baidu.com/rec?platform=wise&ms=1&rset=rcmd&word=123&qid=11327900426705455986&rq=123&from=844b&baiduid=A1D0B88941B30028C375C79CE5AC2E5E%3AFG%3D1&tn=&clientWidth=375&t=1506826017369&r=8255', { // 在URL中寫上傳遞的參數(shù) method: 'GET', headers: new Headers({ 'Accept': 'application/json' // 通過頭指定,獲取的數(shù)據(jù)類型是JSON }) }) .then((res)=>{ return res.json() // 返回一個(gè)Promise,可以解析成JSON }) .then((res)=>{ console.log(res) // 獲取JSON數(shù)據(jù) })
強(qiáng)制帶Cookie
默認(rèn)情況下, fetch 不會(huì)從服務(wù)端發(fā)送或接收任何 cookies, 如果站點(diǎn)依賴于維護(hù)一個(gè)用戶會(huì)話,則導(dǎo)致未經(jīng)認(rèn)證的請(qǐng)求(要發(fā)送 cookies,必須發(fā)送憑據(jù)頭).
// 通過fetch獲取百度的錯(cuò)誤提示頁面 fetch('https://www.baidu.com/search/error.html', { method: 'GET', credentials: 'include' // 強(qiáng)制加入憑據(jù)頭 }) .then((res)=>{ return res.text() }) .then((res)=>{ console.log(res) })
簡單封裝一下fetch
最后了,介紹了一大堆內(nèi)容,有沒有發(fā)現(xiàn),在GET和POST傳遞參數(shù)的方式不同呢?下面咱們就來封裝一個(gè)簡單的fetch,來實(shí)現(xiàn)GET請(qǐng)求和POST請(qǐng)求參數(shù)的統(tǒng)一。
/** * 將對(duì)象轉(zhuǎn)成 a=1&b=2的形式 * @param obj 對(duì)象 */ function obj2String(obj, arr = [], idx = 0) { for (let item in obj) { arr[idx++] = [item, obj[item]] } return new URLSearchParams(arr).toString() } /** * 真正的請(qǐng)求 * @param url 請(qǐng)求地址 * @param options 請(qǐng)求參數(shù) * @param method 請(qǐng)求方式 */ function commonFetcdh(url, options, method = 'GET') { const searchStr = obj2String(options) let initObj = {} if (method === 'GET') { // 如果是GET請(qǐng)求,拼接url url += '?' + searchStr initObj = { method: method, credentials: 'include' } } else { initObj = { method: method, credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }), body: searchStr } } fetch(url, initObj).then((res) => { return res.json() }).then((res) => { return res }) } /** * GET請(qǐng)求 * @param url 請(qǐng)求地址 * @param options 請(qǐng)求參數(shù) */ function GET(url, options) { return commonFetcdh(url, options, 'GET') } /** * POST請(qǐng)求 * @param url 請(qǐng)求地址 * @param options 請(qǐng)求參數(shù) */ function POST(url, options) { return commonFetcdh(url, options, 'POST') }
GET('https://www.baidu.com/search/error.html', {a:1,b:2}) POST('https://www.baidu.com/search/error.html', {a:1,b:2})
API
fetch(url,{ // url: 請(qǐng)求地址 method: "GET", // 請(qǐng)求的方法POST/GET等 headers : { // 請(qǐng)求頭(可以是Headers對(duì)象,也可是JSON對(duì)象) 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: , // 請(qǐng)求發(fā)送的數(shù)據(jù) blob、BufferSource、FormData、URLSearchParams(get或head方法中不能包含body) cache : 'default', // 是否緩存這個(gè)請(qǐng)求 credentials : 'same-origin', //要不要攜帶 cookie 默認(rèn)不攜帶 omit、same-origin 或者 include mode : "", /* mode,給請(qǐng)求定義一個(gè)模式確保請(qǐng)求有效 same-origin:只在請(qǐng)求同域中資源時(shí)成功,其他請(qǐng)求將被拒絕(同源策略) cors : 允許請(qǐng)求同域及返回CORS響應(yīng)頭的域中的資源,通常用作跨域請(qǐng)求來從第三方提供的API獲取數(shù)據(jù) cors-with-forced-preflight:在發(fā)出實(shí)際請(qǐng)求前執(zhí)行preflight檢查 no-cors : 目前不起作用(默認(rèn)) */ }).then(resp => { /* Response 實(shí)現(xiàn)了 Body, 可以使用 Body 的 屬性和方法: resp.type // 包含Response的類型 (例如, basic, cors). resp.url // 包含Response的URL. resp.status // 狀態(tài)碼 resp.ok // 表示 Response 的成功還是失敗 resp.headers // 包含此Response所關(guān)聯(lián)的 Headers 對(duì)象 可以使用 resp.clone() // 創(chuàng)建一個(gè)Response對(duì)象的克隆 resp.arrayBuffer() // 返回一個(gè)被解析為 ArrayBuffer 格式的promise對(duì)象 resp.blob() // 返回一個(gè)被解析為 Blob 格式的promise對(duì)象 resp.formData() // 返回一個(gè)被解析為 FormData 格式的promise對(duì)象 resp.json() // 返回一個(gè)被解析為 Json 格式的promise對(duì)象 resp.text() // 返回一個(gè)被解析為 Text 格式的promise對(duì)象 */ if(resp.status === 200) return resp.json(); // 注: 這里的 resp.json() 返回值不是 js對(duì)象,通過 then 后才會(huì)得到 js 對(duì)象 throw New Error ('false of json'); }).then(json => { console.log(json); }).catch(error => { consolr.log(error); })
常用情況
1. 請(qǐng)求 json
fetch('http://xxx/xxx.json').then(res => { return res.json(); }).then(res => { console.log(res); })
2. 請(qǐng)求文本
fetch('/xxx/page').then(res => { return res.text(); }).then(res => { console.log(res); })
3. 發(fā)送普通 json 數(shù)據(jù)
fetch('/xxx', { method: 'post', body: JSON.stringify({ username: '', password: '' }) });
4. 發(fā)送form 表單數(shù)據(jù)
var form = document.querySelector('form'); fetch('/xxx', { method: 'post', body: new FormData(form) });
5. 獲取圖片
URL.createObjectURL() fetch('/xxx').then(res => { return res.blob(); }).then(res => { document.querySelector('img').src = URL.createObjectURL(imageBlob); })
6. 上傳
var file = document.querySelector('.file') var data = new FormData() data.append('file', file.files[0]) fetch('/xxx', { method: 'POST', body: data })
4. 封裝
require('es6-promise').polyfill(); require('isomorphic-fetch'); export default function request(method, url, body) { method = method.toUpperCase(); if (method === 'GET') { body = undefined; } else { body = body && JSON.stringify(body); } return fetch(url, { method, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body }).then((res) => { if (res.status >= 200 && res.status < 300) { return res; } else { return Promise.reject('請(qǐng)求失??!'); } }) } export const get = path => request('GET', path); export const post = (path, body) => request('POST', path, body); export const put = (path, body) => request('PUT', path, body); export const del = (path, body) => request('DELETE', path, body);
雖然希望Ajax響應(yīng)成功,但是仍會(huì)有問題出現(xiàn):
可能嘗試獲取不存在的資源
沒有權(quán)限獲取資源
輸入?yún)?shù)有誤
服務(wù)器拋出異常
服務(wù)器超時(shí)
服務(wù)器崩潰
API更改
...
假設(shè)我們?cè)噲D獲取不存在錯(cuò)誤,并了解如何處理錯(cuò)誤。下面的例子我將chriscoyier
拼錯(cuò)為chrissycoyier
// 獲取chrissycoyier's repos 而不是 chriscoyier's repos fetch('https://api.github.com/users/chrissycoyier/repos')
為了處理此錯(cuò)誤,我們需要使用catch
方法。
也許我們會(huì)用下面這種方法:
fetch('https://api.github.com/users/chrissycoyier/repos') .then(response => response.json()) .then(data => console.log('data is', data)) .catch(error => console.log('error is', error));
然而卻得到下面這樣結(jié)果:
獲取失敗,但是第二個(gè).then
方法會(huì)執(zhí)行。
如果console.log
此次響應(yīng),會(huì)看出不同:
{ body: ReadableStream bodyUsed: true headers: Headers ok: false // Response is not ok redirected: false status: 404 // HTTP status is 404. statusText: "Not Found" // Request not found type: "cors" url: "https://api.github.com/users/chrissycoyier/repos" }
大部分是一樣的,只有ok
、status
和statusText
是不同的,正如所料,GitHub上沒有發(fā)現(xiàn)chrissycoyier
。
上面響應(yīng)告訴我們Fetch不會(huì)關(guān)心AJAX是否成功,他只關(guān)心從服務(wù)器發(fā)送請(qǐng)求和接收響應(yīng),如果響應(yīng)失敗我們需要拋出異常。
因此,初始的then
方法需要被重寫,以至于如果響應(yīng)成功會(huì)調(diào)用response.json
。最簡單方法是檢查response
是否為ok
。
fetch('some-url') .then(response => { if (response.ok) { return response.json() } else { // Find some way to get to execute .catch() } });
一旦我們知道請(qǐng)求是不成功的,我可以throw
異?;?code>rejectPromise來調(diào)用catch
。
// throwing an Errorelse { throw new Error('something went wrong!') }// rejecting a Promiseelse { return Promise.reject('something went wrong!') }
這里選擇Promise.reject
,是因?yàn)槿菀讛U(kuò)展。拋出異常方法也不錯(cuò),但是無法擴(kuò)展,唯一益處在于便于棧跟蹤。
所以,到現(xiàn)在代碼應(yīng)該是這樣的:
fetch('https://api.github.com/users/chrissycoyier/repos') .then(response => { if (response.ok) { return response.json() } else { return Promise.reject('something went wrong!') } }) .then(data => console.log('data is', data)) .catch(error => console.log('error is', error));
這樣錯(cuò)誤就會(huì)進(jìn)入catch
語句中。
但是reject
Promise時(shí),只輸出字符串不太好。這樣不清楚哪里出錯(cuò)了,你肯定也不會(huì)想在異常時(shí),輸出下面這樣:
讓我們?cè)诳纯错憫?yīng):
{ body: ReadableStream bodyUsed: true headers: Headers ok: false // Response is not ok redirected: false status: 404 // HTTP status is 404. statusText: "Not Found" // Request not found type: "cors" url: "https://api.github.com/users/chrissycoyier/repos" }
在這個(gè)例子中,我們知道資源是不存在。所以我們可以返回404
狀態(tài)或Not Found
原因短語,然而我們就知道如何處理。
為了在.catch
中獲取status
或statusText
,我們可以reject
一個(gè)JavaScript對(duì)象:
fetch('some-url') .then(response => { if (response.ok) { return response.json() } else { return Promise.reject({ status: response.status, statusText: response.statusText }) } }) .catch(error => { if (error.status === 404) { // do something about 404 } })
上面的錯(cuò)誤處理方法對(duì)于下面這些不需要解釋的HTTP狀態(tài)很適用。
401: Unauthorized
404: Not found
408: Connection timeout
...
但對(duì)于下面這些特定的錯(cuò)誤不適用:
400:Bad request
例如,如果請(qǐng)求錯(cuò)誤缺少必要的參數(shù),就會(huì)返回400.
光在catch
中告訴狀態(tài)及原因短語并不足夠。我們需要知道缺少什么參數(shù)。
所以服務(wù)器需要返回一個(gè)對(duì)象,告訴造成錯(cuò)誤請(qǐng)求原因。如果使用Node和Express,會(huì)返回像下面這樣的響應(yīng):
res.status(400).send({ err: 'no first name' })
無法在最初的.then
方法中reject
,因?yàn)殄e(cuò)誤對(duì)象需要response.json
來解析。
解決的方法是需要兩個(gè)then
方法。這樣可以首先通過response.json
讀取,然后決定怎么處理。
fetch('some-error') .then(handleResponse)function handleResponse(response) { return response.json() .then(json => { if (response.ok) { return json } else { return Promise.reject(json) } }) }
首先我們調(diào)用response.json
讀取服務(wù)器發(fā)來的JSON數(shù)據(jù),response.json
返回Promise,所以可以鏈?zhǔn)秸{(diào)用.then
方法。
在第一個(gè).then
中調(diào)用第二個(gè).then
,因?yàn)槲覀內(nèi)韵Mㄟ^repsonse.ok
判斷響應(yīng)是否成功。
如果想發(fā)送狀態(tài)和原因短語,可以使用Object.assign()
將二者結(jié)合為一個(gè)對(duì)象。
let error = Object.assign({}, json, { status: response.status, statusText: response.statusText })return Promise.reject(error)
可以使用這樣新的handleResponse
函數(shù),讓數(shù)據(jù)能自動(dòng)的進(jìn)入.then
和.catch
中。
fetch('some-url') .then(handleResponse) .then(data => console.log(data)) .catch(error => console.log(error))
到現(xiàn)在,我們只處理JSON格式的響應(yīng),而返回JSON格式數(shù)據(jù)大約占90%。
至于其他的10%呢?
假設(shè)上面的例子返回的是XML格式的響應(yīng),也許會(huì)收到下面異常:
這是因?yàn)閄ML格式不是JSON格式,我們無法使用response.json
,事實(shí)上,我們需要response.text
,所以我們需要通過判斷響應(yīng)的頭部來決定內(nèi)容格式:
.then(response => { let contentType = response.headers.get('content-type') if (contentType.includes('application/json')) { return response.json() // ... } else if (contentType.includes('text/html')) { return response.text() // ... } else { // Handle other responses accordingly... } });
當(dāng)我遇見這種問題時(shí),我嘗試使用ExpressJWT處理身份驗(yàn)證,我不知道可以發(fā)生JSON響應(yīng)數(shù)據(jù),所以我將XML格式設(shè)為默認(rèn)。
這是我們到現(xiàn)在完整代碼:
fetch('some-url') .then(handleResponse) .then(data => console.log(data)) .then(error => console.log(error))function handleResponse (response) { let contentType = response.headers.get('content-type') if (contentType.includes('application/json')) { return handleJSONResponse(response) } else if (contentType.includes('text/html')) { return handleTextResponse(response) } else { // Other response types as necessary. I haven't found a need for them yet though. throw new Error(`Sorry, content-type ${contentType} not supported`) } }function handleJSONResponse (response) { return response.json() .then(json => { if (response.ok) { return json } else { return Promise.reject(Object.assign({}, json, { status: response.status, statusText: response.statusText })) } }) }function handleTextResponse (response) { return response.text() .then(text => { if (response.ok) { return json } else { return Promise.reject({ status: response.status, statusText: response.statusText, err: text }) } }) }
zlFetch庫就是上例中handleResponse
函數(shù),所以可以不用生成此函數(shù),不需要擔(dān)心響應(yīng)來處理數(shù)據(jù)和錯(cuò)誤。
典型的zlfetch像下面這樣:
zlFetch('some-url', options) .then(data => console.log(data)) .catch(error => console.log(error));
使用之前,需要安裝zlFetch
npm install zl-fetch --save
接著,需要引入到你的代碼中,如果你需要polyfill,確保加入zlFetch之前引入它。
// Polyfills (if needed)require('isomorphic-fetch') // or whatwg-fetch or node-fetch if you prefer// ES6 Importsimport zlFetch from 'zl-fetch';// CommonJS Importsconst zlFetch = require('zl-fetch');
zlFetch還能無須轉(zhuǎn)換成JSON格式就能發(fā)送JSON數(shù)據(jù)。
下面兩個(gè)函數(shù)做了同樣事情,zlFetch加入Content-type
然后將內(nèi)容轉(zhuǎn)換為JSON格式。
let content = {some: 'content'}// Post request with fetch fetch('some-url', { method: 'post', headers: {'Content-Type': 'application/json'} body: JSON.stringify(content) });// Post request with zlFetch zlFetch('some-url', { method: 'post', body: content });
zlFetch處理身份認(rèn)證也很容易。
常用方法是在頭部加入Authorization
,其值設(shè)為Bearer your-token-here
。如果你需要增加token
選項(xiàng),zlFetch會(huì)幫你創(chuàng)建此域。
所以,下面兩種代碼是一樣的:
let token = 'someToken' zlFetch('some-url', { headers: { Authorization: `Bearer ${token}` } });// Authentication with JSON Web Tokens with zlFetch zlFetch('some-url', {token});
下面就是使用zlFetch來從GitHub上獲取repos:
Fetch是很好的方法,能發(fā)送和接收數(shù)據(jù)。不需要在編寫XHR請(qǐng)求或依賴于jQuery。
盡管Fetch很好,但是其錯(cuò)誤處理不是很直接。在處理之前,需要讓錯(cuò)誤信息進(jìn)入到catch
方法中。
使用zlFetch庫,就不需要擔(dān)心錯(cuò)誤處理了。
如對(duì)本文有疑問,請(qǐng)?zhí)峤坏浇涣髡搲?,廣大熱心網(wǎng)友會(huì)為你解答!! 點(diǎn)擊進(jìn)入論壇