快来和fetch玩耍吧

12/28/2020 JS

# fetch 是什么?

fetch 是一种使用 promise 为构建块的现代异步网络请求方法.是当今进行异步网络请求的新标准.除了 IE 之外,在各大浏览器中的兼容性都还可以,在caniuse (opens new window)上查询 fetch 的浏览器兼容性,不支持的浏览器可以使用 fetch polyfill (opens new window).其本质是一种标准,该标准定义了请求,响应和绑定的流程,还定义了 Fetch 的 JavaScript API.而 Fetch API 提供了 fetch() 方法.它被定义在 BOM 的 window 对象中,返回一个 Promise 对象,因此我们能够对返回的结果进行检索.

# fetch 怎么用?

为了方便测试,我们选择自己用 express 来写一个简单的接口.当然大家也可以使用在线接口 (opens new window)来测试fetch的功能.这个在线接口地址的例子就是用 fetch 来写的.

# get 请求

下面是接口代码,先是简单的定义了一个 get 请求,返回一个字符串.

const express = require('express')
const app = new express()

// 设置接口允许跨域
app.all('*', (req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  next()
})

// 编写一个get请求
app.get('/test', (req, res, next) => {
  res.send('Hello world')
})
const port = process.env.port || 8686
app.listen(port, () => {
  console.log('Server start in http://localhost:%s', port)
})

fetch() 的第一个参数是要获取资源的 url,第二个参数可选,是个配置选项.

fetch('http://localhost:8686/test')
  .then((response) => response.text())
  .then((res) => {
    console.log(res) // Hello world
  })
  .catch((error) => {
    console.log(error)
  })

fetch 方法的 then 会接收一个 Response 实例.但值得注意的是,fetch 方面的第二个 then 接收的才是后台传过来的真正数据,而前一个 then 则是对数据进行处理或者异常捕获.在上面的例子中, response => response.text() 就是在第一个 then 里面对数据进行处理,返回一个处理后的数据.这里使用的是 text() 方法,它可以将主体内容作为字符串返回.其他的方法还有 json() , blob() , formDataarrayBuffer(). 我们再来看看 json() 的用处,它可以将数据反序列化为一个对象. 在接口代码中,添加如下:

app.get('/test2', (req, res, next) => {
  res.json({
    name: 'zhangsan',
    age: 12,
  })
})

若结果还是用 text() 返回的话,则是 {"name":"zhangsan","age":12} 字符串,但是若用 json() 返回,就是一个对象了 {name: "zhangsan", age: 12}.很明显,在这里使用 json() 就比 text() 要好.

第一个 then 里面的处理方法总结

  • text() 返回一个被解析为 USVString 格式的 Promise 对象
  • json() 返回一个被解析为 JSON 格式的 Promise 对象
  • blob() 返回一个被解析为 Blob 格式的 Promise 对象
  • formData() 返回一个被解析为 FormData 格式的 Promise 对象
  • arrayBuffer() 返回一个被解析为 ArrayBuffer 格式的 Promise 对象

# 使用 async 改写

上面所有的方法都会返回 Promsie ,所以我们可以在后面继续使用一个 then 和一个 catch.当然了,我们也可以使用 asyncawait 来改写上面的代码.

!(async () => {
  const response = await fetch('http://localhost:8686/test2')
  const res = await response.json()
  console.log(res) // {name: "zhangsan", age: 12}
})()

# 错误捕获

前面提到了,可以在第一个 then 里面对数据进行处理,也可以捕获异常.我们将请求的 url 改为一个不存在的地址.

fetch('http://localhost:8686/test666')
  .then((response) => {
    if (!response.ok) {
      throw new Error(response.statusText) // throw an Error
      // return Promise.reject({  // rejecting a Promise
      //   status:response.status,
      //   statusText:response.statusText
      // })
    }
    return response.json()
  })
  .then((res) => {
    console.log(res)
  })
  .catch((error) => {
    console.log(error) // Error: Not Found
  })

上面的代码将会抛出一个异常,我们也可以通过Promisereject 来调用 catch.

# post 请求

上面差不多把一个最简单的 get 请求讲完了,接下来我们继续说一说 post 请求要怎么发送.我们先修改服务端的代码如下,新增加了一个 post 请求的接口.这个接口将请求时发送的数据外加一个表示结果的字段一起打包返回回去.

const express = require('express')
const router = express.Router()
const bodyParser = require('body-parser')
const app = new express()
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

// 设置接口允许跨域
app.all('*', (req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Headers', 'Content-Type')
  next()
})

// 编写一个get请求
app.get('/test', (req, res, next) => {
  res.send('Hello world')
})

app.get('/test2', (req, res, next) => {
  console.log(req)
  res.json({
    name: 'zhangsan',
    age: 12,
  })
})

app.post('/create', (req, res, next) => {
  let obj = Object.assign({ result: 'success' }, req.body)
  res.status(200).json(obj)
})

const port = process.env.port || 8686
app.listen(port, () => {
  console.log('Server start in http://localhost:%s', port)
})

前端发送 fetch 请求的代码如下:

const data = {
  name: 'zhangsan',
  age: 12,
}

fetch('http://localhost:8686/create', {
  method: 'post',
  body: JSON.stringify(data), // 请求的body信息,get和head方法是不能包含body信息
  headers: {
    // 配置请求的头信息,包含与请求关联的Headers对象
    'Content-Type': 'application/json;charset=utf-8',
  },
})
  .then((response) => {
    return response.json()
  })
  .then((res) => {
    console.log(res) // {result: "success", name: "zhangsan", age: 12}
  })
  .catch((error) => {
    console.log(error)
  })

# 第二个参数的配置项

在上面的代码中,我们使用了 fetch 方法的第二个参数,即配置选项.其中, method 表示请求方式,默认为 get, 其他的还有 post, put, head, delete 等. body 为请求的 body 信息,gethead 是没有 body 的.而 headers 则是请求头相关信息.在这个配置选项中,还有一些配置项目,我们来简单的说一下.

  • credentials 要让浏览器发送包含凭证(cookie)的请求(即使是跨域源),就要将此属性设置为 include, same-origin 则是请求 url 和调用脚本位于同源时发送凭证,若是不想发送凭证则是 omit.这里就表现出了 fetch 和 ajax 的一个区别:默认情况下,fetch 不会接受或者发送 cookies.
  • mode 设置请求模式,属性值有 cros no-cors same-origin.其中 same-origin 表示必须同源,禁止跨域,否则会报 Request mode is "same-origin" but the URL's origin is not same as the request origin xxx 的错误.若设置了 cros 则需要服务端配合设置响应头 Access-Control-Allow-Origin 为相应的域名或 *.no-cors 的效果就是无论外域服务器是否设置了允许跨域的响应头,浏览器都 可以对外域发送请求,但是它不接受响应.
  • cache 设置请求的缓存模式,属性值有 default reload no-cache no-store force-cache only-if-cached
  • referrer 来源地址 no-referrer client.注意了,这里采用的 referrer 这种正确的拼法,而不是 referer 这种错误的拼写.有关 referrer 的拼法的历史趣闻,可看我文末的第二个参考链接.

# Request 构造器函数

除了上面介绍的 fetch 的使用方法外,我们还可以通过一个 Request 构造器函数来创建一个新的请求对象.

let req = new Request('http://localhost:8686/test', {
  method: 'get',
})
fetch(req)
  .then((response) => response.text())
  .then((res) => {
    console.log(res) // Hello world
  })
  .catch((err) => {
    console.log(err)
  })

# Headers 构造器函数

我们也可以通过一个 Headers 构造器函数来设置我们的请求头

myHeader = new Headers()
myHeader.append('Content-Type','application/json;charset=utf-8')
...
{
    headers:myHeader
}

# fetch 与 ajax 有什么关系?

fetch 是 XMLHttpRequest 的一种替代方案,fetct 就是原生的 js,不是 ajax 的进一步封装.

# 区别

  • fetch 返回的 promise 不会拒绝 http 的错误状态,即使响应是404或者500,它们不被认为是网络错误.只有当网络故障或者请求被阻止时,才会标记为 reject.因此成功的 fetch()检查不仅要包含 promise 被 resolve,还要包括response.ok属性为 true,该属性是检查 response 的状态是否在 200-299 这个范围内来确定的.
  • 默认情况下,fetch 不会接受或者发送 cookies

# fetch 的优势

  • fetch 请求相对来说语法简洁,代码更少,更具语义化,且数据处理过程更加清晰
  • 基于标准的 Promise 实现,且支持 async/await,避免了回调地狱,
  • 接口更加的合理化,因为 ajax 是将所有不同性质的接口都放在了 XHR 对象身上,而 fetch 则是分散在不同的对象上,如 Headers, Response, Request 等
  • 可以在 ServiceWorker 中使用

# fetch 会出现 reject 的情况

上面说到了,只有当网络故障或者请求被阻止的时候,才会标记为 reject.下面我们来看下这两种情况.

# 网络故障

我们去 Chrome 浏览器中,将 Network 中的网络设置为 Offline 离线状态,再次调用如下代码,就会发现控制台打印出错误.请求结果进入到 reject 状态了.

fetch('http://localhost:8686/test')
  .then((response) => response.text())
  .then((res) => {
    console.log(res)
  })
  .catch((error) => {
    console.log(error) // TypeError: Failed to fetch
  })

# 请求中止

fetch 本身并没有提供中止请求的方法.但是部分浏览器有实现 AbortController,可以用来中止 fetch 请求.

const controller = new AbortController()
const signal = controller.signal
setTimeout(() => {
  controller.abort()
}, 200) // 自己调整定时器的时长查看效果,可以配合Chrome浏览器中的Network,将网络调整为慢速3G

fetch('http://localhost:8686/test', {
  signal, // 将上面的signal加入到配置项中
})
  .then((response) => response.text())
  .then((res) => {
    console.log(res)
  })
  .catch((error) => {
    console.log(error) // DOMException: The user aborted a request.
  })

# 总结

fetch

参考链接:

    希望像星光一样闪烁
    文雀