fetch 使用
1. 浏览器支持情况
fetch是相对较新的技术,当然就会存在浏览器兼容性的问题,当前各个浏览器低版本的情况下都是不被支持的,因此为了在所有主流浏览器中使用fetch 需要考虑 fetch 的 polyfill 了
require('es6-promise').polyfill(); require('isomorphic-fetch');12
引入这两个文件,就可以支持主流浏览器了
fetch和XMLHttpRequest
如果看网上的fetch教程,会首先对比XMLHttpRequest和fetch的优劣,然后引出一堆看了很快会忘记的内容(本人记性不好)。因此,我写一篇关于fetch的文章,为了自己看着方便,毕竟工作中用到的也就是一些很基础的点而已。
fetch,说白了,就是XMLHttpRequest的一种替代方案。如果有人问你,除了Ajax获取后台数据之外,还有没有其他的替代方案?
这是你就可以回答,除了XMLHttpRequest对象来获取后台的数据之外,还可以使用一种更优的解决方案fetch。
如何获取fetch
到现在为止,fetch的支持性还不是很好,但是在谷歌浏览器中已经支持了fetch。fetch挂在在BOM中,可以直接在谷歌浏览器中使用。
查看fetch的支持情况:fetch的支持情况
当然,如果不支持fetch也没有问题,可以使用第三方的ployfill来实现只会fetch:whatwg-fetch
fetch的helloworld
下面我们来写第一个fetch获取后端数据的例子:
// 通过fetch获取百度的错误提示页面
</>code
- fetch('https://www.baidu.com/search/error.html') // 返回一个Promise对象
- .then((res)=>{ return res.text() // res.text()是一个Promise对象
- })
- .then((res)=>{ console.log(res) // res是最终的结果
- })
是不是很简单?可能难的地方就是Promise的写法,这个可以看阮一峰老师的ES6教程来学习。
说明一点,下面演示的GET请求或POST请求,都是采用百度中查询到的一些接口,可能传递的有些参数这个接口并不会解析,但不会影响这个接口的使用。
GET请求
GET请求初步
完成了helloworld,这个时候就要来认识一下GET请求如何处理了。
上面的helloworld中这是使用了第一个参数,其实fetch还可以提供第二个参数,就是用来传递一些初始化的信息。
这里如果要特别指明是GET请求,就要写成下面的形式:
</>code
- // 通过fetch获取百度的错误提示页面
- fetch('https://www.baidu.com/search/error.html', {
- method: 'GET'
- })
- .then((res)=>{ return res.text()
- })
- .then((res)=>{ console.log(res)
- })
GET请求的参数传递
GET请求中如果需要传递参数怎么办?这个时候,只能把参数写在URL上来进行传递了。
</>code
- // 通过fetch获取百度的错误提示页面
- fetch('https://www.baidu.com/search/error.html?a=1&b=2', { // 在URL中写上传递的参数
- method: 'GET'
- })
- .then((res)=>{ return res.text()
- })
- .then((res)=>{ console.log(res)
- })
POST请求
POST请求初步
与GET请求类似,POST请求的指定也是在fetch的第二个参数中:
</>code
- // 通过fetch获取百度的错误提示页面
- fetch('https://www.baidu.com/search/error.html', {
- method: 'POST' // 指定是POST请求
- })
- .then((res)=>{ return res.text()
- })
- .then((res)=>{ console.log(res)
- })
POST请求参数的传递
众所周知,POST请求的参数,一定不能放在URL中,这样做的目的是防止信息泄露。
</>code
- // 通过fetch获取百度的错误提示页面
- fetch('https://www.baidu.com/search/error.html', {
- method: 'POST',
- body: new URLSearchParams([["foo", 1],["bar", 2]]).toString() // 这里是请求对象
- })
- .then((res)=>{ return res.text()
- })
- .then((res)=>{ console.log(res)
- })
其实除了对象URLSearchParams外,还有几个其他的对象,可以参照:常用的几个对象来学习使用。
设置请求的头信息
在POST提交的过程中,一般是表单提交,可是,经过查询,发现默认的提交方式是:Content-Type:text/plain;charset=UTF-8,这个显然是不合理的。下面咱们学习一下,指定头信息:
</>code
- // 通过fetch获取百度的错误提示页面
- 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)
- })
这个时候,在谷歌浏览器的Network中查询,会发现,请求方式已经变成了content-type:application/x-www-form-urlencoded。
通过接口得到JSON数据
上面所有的例子中都是返回一个文本,那么除了文本,有没有其他的数据类型呢?肯定是有的,具体查询地址:Body的类型
由于最常用的是JSON数据,那么下面就简单演示一下获取JSON数据的方式:
</>code
- // 通过fetch获取百度的错误提示页面
- 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中写上传递的参数
- method: 'GET',
- headers: new Headers({ 'Accept': 'application/json' // 通过头指定,获取的数据类型是JSON
- })
- })
- .then((res)=>{ return res.json() // 返回一个Promise,可以解析成JSON
- })
- .then((res)=>{ console.log(res) // 获取JSON数据
- })
强制带Cookie
默认情况下, fetch 不会从服务端发送或接收任何 cookies, 如果站点依赖于维护一个用户会话,则导致未经认证的请求(要发送 cookies,必须发送凭据头).
</>code
- // 通过fetch获取百度的错误提示页面
- fetch('https://www.baidu.com/search/error.html', {
- method: 'GET',
- credentials: 'include' // 强制加入凭据头
- })
- .then((res)=>{ return res.text()
- })
- .then((res)=>{ console.log(res)
- })
简单封装一下fetch
最后了,介绍了一大堆内容,有没有发现,在GET和POST传递参数的方式不同呢?下面咱们就来封装一个简单的fetch,来实现GET请求和POST请求参数的统一。
</>code
- /**
- * 将对象转成 a=1&b=2的形式
- * @param obj 对象
- */
- function obj2String(obj, arr = [], idx = 0) {
- for (let item in obj) {
- arr[idx++] = [item, obj[item]]
- }
- return new URLSearchParams(arr).toString()
- }
- /**
- * 真正的请求
- * @param url 请求地址
- * @param options 请求参数
- * @param method 请求方式
- */
- function commonFetcdh(url, options, method = 'GET') {
- const searchStr = obj2String(options)
- let initObj = {}
- if (method === 'GET') { // 如果是GET请求,拼接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请求
- * @param url 请求地址
- * @param options 请求参数
- */
- function GET(url, options) {
- return commonFetcdh(url, options, 'GET')
- }
- /**
- * POST请求
- * @param url 请求地址
- * @param options 请求参数
- */
- function POST(url, options) {
- return commonFetcdh(url, options, 'POST')
- }
</>code
- 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
</>code
- fetch(url,{ // url: 请求地址
- method: "GET", // 请求的方法POST/GET等
- headers : { // 请求头(可以是Headers对象,也可是JSON对象)
- 'Content-Type': 'application/json',
- 'Accept': 'application/json'
- },
- body: , // 请求发送的数据 blob、BufferSource、FormData、URLSearchParams(get或head方法中不能包含body)
- cache : 'default', // 是否缓存这个请求
- credentials : 'same-origin', //要不要携带 cookie 默认不携带 omit、same-origin 或者 include
- mode : "",
- /*
- mode,给请求定义一个模式确保请求有效
- same-origin:只在请求同域中资源时成功,其他请求将被拒绝(同源策略)
- cors : 允许请求同域及返回CORS响应头的域中的资源,通常用作跨域请求来从第三方提供的API获取数据
- cors-with-forced-preflight:在发出实际请求前执行preflight检查
- no-cors : 目前不起作用(默认)
- */
- }).then(resp => {
- /*
- Response 实现了 Body, 可以使用 Body 的 属性和方法:
- resp.type // 包含Response的类型 (例如, basic, cors).
- resp.url // 包含Response的URL.
- resp.status // 状态码
- resp.ok // 表示 Response 的成功还是失败
- resp.headers // 包含此Response所关联的 Headers 对象 可以使用
- resp.clone() // 创建一个Response对象的克隆
- resp.arrayBuffer() // 返回一个被解析为 ArrayBuffer 格式的promise对象
- resp.blob() // 返回一个被解析为 Blob 格式的promise对象
- resp.formData() // 返回一个被解析为 FormData 格式的promise对象
- resp.json() // 返回一个被解析为 Json 格式的promise对象
- resp.text() // 返回一个被解析为 Text 格式的promise对象
- */
- if(resp.status === 200) return resp.json();
- // 注: 这里的 resp.json() 返回值不是 js对象,通过 then 后才会得到 js 对象
- throw New Error ('false of json');
- }).then(json => {
- console.log(json);
- }).catch(error => {
- consolr.log(error);
- })
常用情况
1. 请求 json
</>code
- fetch('http://xxx/xxx.json').then(res => { return res.json();
- }).then(res => {
- console.log(res);
- })
2. 请求文本
</>code
- fetch('/xxx/page').then(res => { return res.text();
- }).then(res => {
- console.log(res);
- })
3. 发送普通 json 数据
</>code
- fetch('/xxx', {
- method: 'post',
- body: JSON.stringify({
- username: '',
- password: ''
- })
- });
4. 发送form 表单数据
</>code
- var form = document.querySelector('form');
- fetch('/xxx', {
- method: 'post',
- body: new FormData(form)
- });
5. 获取图片
</>code
- URL.createObjectURL()
- fetch('/xxx').then(res => { return res.blob();
- }).then(res => {
- document.querySelector('img').src = URL.createObjectURL(imageBlob);
- })
6. 上传
</>code
- var file = document.querySelector('.file') var data = new FormData()
- data.append('file', file.files[0])
- fetch('/xxx', {
- method: 'POST',
- body: data
- })
4. 封装
</>code
- 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('请求失败!');
- }
- })
- }
- 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响应成功,但是仍会有问题出现:
可能尝试获取不存在的资源
没有权限获取资源
输入参数有误
服务器抛出异常
服务器超时
服务器崩溃
API更改
...
假设我们试图获取不存在错误,并了解如何处理错误。下面的例子我将chriscoyier
拼错为chrissycoyier
</>code
- // 获取chrissycoyier's repos 而不是 chriscoyier's repos
- fetch('https://api.github.com/users/chrissycoyier/repos')
为了处理此错误,我们需要使用catch
方法。
也许我们会用下面这种方法:
</>code
- 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));
然而却得到下面这样结果:
获取失败,但是第二个.then
方法会执行。
如果console.log
此次响应,会看出不同:
</>code
- { 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上没有发现chrissycoyier
。
上面响应告诉我们Fetch不会关心AJAX是否成功,他只关心从服务器发送请求和接收响应,如果响应失败我们需要抛出异常。
因此,初始的then
方法需要被重写,以至于如果响应成功会调用response.json
。最简单方法是检查response
是否为ok
。
</>code
- fetch('some-url')
- .then(response => { if (response.ok) { return response.json()
- } else { // Find some way to get to execute .catch()
- }
- });
一旦我们知道请求是不成功的,我可以throw
异常或reject
Promise来调用catch
。
</>code
- // throwing an Errorelse { throw new Error('something went wrong!')
- }// rejecting a Promiseelse { return Promise.reject('something went wrong!')
- }
这里选择Promise.reject
,是因为容易扩展。抛出异常方法也不错,但是无法扩展,唯一益处在于便于栈跟踪。
所以,到现在代码应该是这样的:
</>code
- 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));
这样错误就会进入catch
语句中。
但是reject
Promise时,只输出字符串不太好。这样不清楚哪里出错了,你肯定也不会想在异常时,输出下面这样:
让我们在看看响应:
</>code
- { 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"
- }
在这个例子中,我们知道资源是不存在。所以我们可以返回404
状态或Not Found
原因短语,然而我们就知道如何处理。
为了在.catch
中获取status
或statusText
,我们可以reject
一个JavaScript对象:
</>code
- 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
- }
- })
上面的错误处理方法对于下面这些不需要解释的HTTP状态很适用。
401: Unauthorized
404: Not found
408: Connection timeout
...
但对于下面这些特定的错误不适用:
400:Bad request
例如,如果请求错误缺少必要的参数,就会返回400.
光在catch
中告诉状态及原因短语并不足够。我们需要知道缺少什么参数。
所以服务器需要返回一个对象,告诉造成错误请求原因。如果使用Node和Express,会返回像下面这样的响应:
</>code
- res.status(400).send({ err: 'no first name'
- })
无法在最初的.then
方法中reject
,因为错误对象需要response.json
来解析。
解决的方法是需要两个then
方法。这样可以首先通过response.json
读取,然后决定怎么处理。
</>code
- fetch('some-error')
- .then(handleResponse)function handleResponse(response) { return response.json()
- .then(json => { if (response.ok) { return json
- } else { return Promise.reject(json)
- }
- })
- }
首先我们调用response.json
读取服务器发来的JSON数据,response.json
返回Promise,所以可以链式调用.then
方法。
在第一个.then
中调用第二个.then
,因为我们仍希望通过repsonse.ok
判断响应是否成功。
如果想发送状态和原因短语,可以使用Object.assign()
将二者结合为一个对象。
</>code
- let error = Object.assign({}, json, { status: response.status, statusText: response.statusText
- })return Promise.reject(error)
可以使用这样新的handleResponse
函数,让数据能自动的进入.then
和.catch
中。
</>code
- fetch('some-url')
- .then(handleResponse)
- .then(data => console.log(data))
- .catch(error => console.log(error))
到现在,我们只处理JSON格式的响应,而返回JSON格式数据大约占90%。
至于其他的10%呢?
假设上面的例子返回的是XML格式的响应,也许会收到下面异常:
这是因为XML格式不是JSON格式,我们无法使用response.json
,事实上,我们需要response.text
,所以我们需要通过判断响应的头部来决定内容格式:
</>code
- .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...
- }
- });
当我遇见这种问题时,我尝试使用ExpressJWT处理身份验证,我不知道可以发生JSON响应数据,所以我将XML格式设为默认。
这是我们到现在完整代码:
</>code
- 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
函数,所以可以不用生成此函数,不需要担心响应来处理数据和错误。
典型的zlfetch像下面这样:
</>code
- zlFetch('some-url', options)
- .then(data => console.log(data))
- .catch(error => console.log(error));
使用之前,需要安装zlFetch
</>code
- npm install zl-fetch --save
接着,需要引入到你的代码中,如果你需要polyfill,确保加入zlFetch之前引入它。
</>code
- // 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还能无须转换成JSON格式就能发送JSON数据。
下面两个函数做了同样事情,zlFetch加入Content-type
然后将内容转换为JSON格式。
</>code
- 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处理身份认证也很容易。
常用方法是在头部加入Authorization
,其值设为Bearer your-token-here
。如果你需要增加token
选项,zlFetch会帮你创建此域。
所以,下面两种代码是一样的:
</>code
- 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是很好的方法,能发送和接收数据。不需要在编写XHR请求或依赖于jQuery。
尽管Fetch很好,但是其错误处理不是很直接。在处理之前,需要让错误信息进入到catch
方法中。
使用zlFetch库,就不需要担心错误处理了。
如对本文有疑问,请提交到交流论坛,广大热心网友会为你解答!! 点击进入论坛