使用 Fetch

Avatar of Zell Liew
Zell Liew

DigitalOcean 为您的旅程每个阶段提供云产品。 立即开始使用 $200 免费信用额度!

无论何时我们使用 JavaScript 发送或检索信息,我们都会启动一个称为 Ajax 调用的操作。 Ajax 是一种在幕后发送和检索信息的技术,无需刷新页面。 它允许浏览器发送和检索信息,然后对获取的信息进行操作,例如添加或更改页面上的 HTML。

让我们回顾一下它的历史,然后将自己更新到最新状态。

只想要基本的 fetch 代码片段吗? 这里有。
fetch(URL)
  .then(response => response.json())
  .then(data => {
    console.log(data)
  });

这里还要注意,我们将在本文中的所有演示中使用 ES6 语法。

几年前,启动 Ajax 调用的最简单方法是使用 jQuery 的 ajax 方法

$.ajax('some-url', {
  success: (data) => { /* do something with the data */ },
  error: (err) => { /* do something when an error happens */}
});

我们可以不使用 jQuery 来执行 Ajax,但我们必须编写一个 XMLHttpRequest,这 相当复杂

值得庆幸的是,如今的浏览器已经有了很大的改进,它们支持 Fetch API,这是一种使用 Ajax 的现代方法,无需 jQuery 或 Axios 等辅助库。 在本文中,我将向您展示如何使用 Fetch 处理成功和错误。

Fetch 的支持

让我们首先消除支持问题。

此浏览器支持数据来自 Caniuse,其中包含更多详细信息。 数字表示浏览器在该版本及更高版本上支持该功能。

桌面

ChromeFirefoxIEEdgeSafari
42391410.1

移动设备/平板电脑

Android ChromeAndroid FirefoxAndroidiOS Safari
12712712710.3

Fetch 的支持非常好! 所有主要浏览器(除了 Opera Mini 和旧版 IE)都原生支持它,这意味着您可以在项目中安全地使用它。 如果您需要在任何不支持的地方获得支持,您始终可以依赖于 这个便捷的 polyfill

使用 Fetch 获取数据

使用 Fetch 获取数据很容易。 您只需要向 Fetch 提供您尝试获取的资源(很元!)。

假设我们尝试获取 Chris 在 Github 上的存储库列表。 根据 Github 的 API,我们需要对 api.github.com/users/chriscoyier/repos 发起一个 get 请求。

这将是 fetch 请求

fetch('https://api.github.com/users/chriscoyier/repos');

太简单了! 接下来的步骤是什么?

Fetch 返回一个 Promise,这是一种在不需要回调的情况下处理异步操作的方法。

要在获取资源后执行某些操作,您需要在 .then 调用中编写它

fetch('https://api.github.com/users/chriscoyier/repos')
  .then(response => {/* do something */})

如果这是您第一次接触 Fetch,您可能会对 Fetch 返回的 response 感到惊讶。 如果您在控制台中打印 response,您将获得以下信息

{
  body: ReadableStream
  bodyUsed: false
  headers: Headers
  ok : true
  redirected : false
  status : 200
  statusText : "OK"
  type : "cors"
  url : "http://some-website.com/some-url"
  __proto__ : Response
}

在这里,您可以看到 Fetch 返回了一个响应,该响应告诉您请求的状态。 我们可以看到请求是成功的(ok 为 true 且 status 为 200),但 Chris 的存储库列表在任何地方都没有!

事实证明,我们从 Github 请求的内容隐藏在 body 中,它是一个可读流。 我们需要调用一个 适当的方法 将此可读流转换为我们可以消费的数据。

由于我们使用的是 GitHub,我们知道响应是 JSON。 我们可以调用 response.json 来转换数据。

还有其他方法可以处理不同类型的响应。 如果您请求 XML 文件,则应该调用 response.text。 如果您请求图像,则调用 response.blob

所有这些转换方法(response.json 等)都返回另一个 Promise,因此我们可以使用另一个 .then 调用来获取我们想要的数据。

fetch('https://api.github.com/users/chriscoyier/repos')
  .then(response => response.json())
  .then(data => {
    // Here's a list of repos!
    console.log(data)
  });

呼! 这就是使用 Fetch 获取数据所需做的全部! 简短明了,不是吗? :)

接下来,让我们看看如何使用 Fetch 发送一些数据。

使用 Fetch 发送数据

使用 Fetch 发送数据也很简单。 您只需要使用三个选项来配置您的 fetch 请求。

fetch('some-url', options);

您需要设置的**第一个选项**是您的请求方法,可以是 postputdel。 如果您省略 method,Fetch 会自动将其设置为 get,这就是获取资源需要更少步骤的原因。

**第二个选项**是设置您的标头。 由于我们主要在当今时代发送 JSON 数据,因此我们需要将 Content-Type 设置为 application/json

**第三个选项**是设置包含 JSON 内容的正文。 由于需要 JSON 内容,因此您通常需要在设置 body 时调用 JSON.stringify

实际上,使用这三个选项的 post 请求看起来像这样

let content = {some: 'content'};

// The actual fetch request
fetch('some-url', {
  method: 'post',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(content)
})
// .then()...

对于眼尖的人来说,您会注意到每个 postputdel 请求都有一些样板代码。 理想情况下,我们可以重用我们的标头并在发送之前对内容调用 JSON.stringify,因为我们已经知道我们发送的是 JSON 数据。

但即使使用样板代码,Fetch 对于发送任何请求来说仍然非常不错。

但是,使用 Fetch 处理错误并不像处理成功消息那样简单。 您很快就会明白为什么。

使用 Fetch 处理错误

尽管我们总是希望 Ajax 请求能够成功,但它们也可能失败。 请求失败的原因有很多,包括但不限于以下情况

  1. 您尝试获取不存在的资源。
  2. 您无权获取资源。
  3. 您输入了一些错误的参数
  4. 服务器抛出错误。
  5. 服务器超时。
  6. 服务器崩溃。
  7. API 发生了改变。

如果您的请求失败,事情就不会很顺利。 想象一下,您尝试在网上购买东西。 发生了错误,但编码网站的人没有处理它。 结果,在点击购买后,没有任何反应。 页面只是停在那里...您不知道是否发生了任何事情。 您的卡是否通过了? 😱.

现在,让我们尝试获取一个不存在的错误,并了解如何使用 Fetch 处理错误。 在本例中,假设我们将 chriscoyier 拼写错误为 chrissycoyier

// Fetching chrissycoyier's repos instead of chriscoyier's repos
fetch('https://api.github.com/users/chrissycoyier/repos')

我们已经知道应该会得到一个错误,因为 Github 上没有 chrissycoyier。 要处理 promise 中的错误,我们使用 catch 调用。

鉴于我们现在的了解,您可能会写出以下代码

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));

执行您的 fetch 请求。 您将获得以下内容

Fetch 失败了,但执行的代码是第二个 .then 而不是 .catch

为什么我们的第二个 .then 调用被执行了? promise 不应该使用 .catch 处理错误吗? 太糟糕了! 😱😱😱

如果您现在打印 response,您将看到略微不同的值

{
  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"
}

大多数响应保持不变,除了 okstatusstatusText。 正如预期的那样,我们在 Github 上没有找到 chrissycoyier。

此响应告诉我们,Fetch 不关心您的 AJAX 请求是否成功。 它只关心发送请求并从服务器接收响应,这意味着如果请求失败,我们需要抛出一个错误。

因此,初始 then 调用需要进行重写,以便它仅在请求成功时才调用 response.json。 最简单的方法是检查 response 是否为 ok

fetch('some-url')
  .then(response => {
    if (response.ok) {
      return response.json()
    } else {
      // Find some way to get to execute .catch()
    }
  });

一旦我们知道请求不成功,我们可以选择 throw 一个 Error 或 reject 一个 Promise 来激活 catch 调用。

// throwing an Error
else {
  throw new Error('something went wrong!')
}

// rejecting a Promise
else {
  return Promise.reject('something went wrong!')
}

选择其中一个,因为它们都会激活 .catch 调用。

在这里,我选择使用 Promise.reject,因为它更容易实现。 Error 也很酷,但它们更难实现,而 Error 的唯一好处是堆栈跟踪,而这在 Fetch 请求中将不存在。

因此,到目前为止,代码看起来像这样

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 中

这很棒。 我们正在取得进展,因为我们现在有了一种处理错误的方法。

但是,使用通用消息拒绝 promise(或抛出 Error)还不够好。 我们无法知道出了什么问题。 我敢肯定,您不希望在遇到这样的错误时成为接收方...

是的...我知道出了问题...但究竟是什么问题? 🙁

出了什么问题? 服务器超时了吗? 我的连接断开了吗? 我无从得知! 我们需要一种方法来告诉我们请求出了什么问题,以便我们可以适当处理它。

让我们再看一下响应,看看我们可以做些什么

{
  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 状态文本,我们就会知道该如何处理它。

要将 statusstatusText 传递到 .catch 调用中,我们可以拒绝一个 JavaScript 对象

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:未授权
  • 404:未找到
  • 408:连接超时

但对于以下这个特殊的“坏家伙”来说就不够用了:

  • 400:错误请求.

什么构成了错误请求?它可能是各种各样的东西!例如,Stripe 在请求缺少必需参数时返回 400。

Stripe 解释说,如果请求缺少必需字段,它会返回 400 错误。

仅仅告诉我们的 .catch 语句发生了错误请求是不够的。我们需要更多信息来判断到底是什么东西缺失了。是用户忘记了他们的名字、电子邮件,还是信用卡信息?我们无从得知!

理想情况下,在这种情况下,您的服务器会返回一个对象,告诉您发生了什么以及失败请求的信息。如果您使用 Node 和 Express,这样的响应看起来像这样。

res.status(400).send({
  err: 'no first name'
})

在这里,我们不能在最初的 .then 调用中拒绝 Promise,因为来自服务器的错误对象只有在 response.json 之后才能读取。

解决方法是返回一个包含两个 then 调用的 promise。这样,我们可以先读取 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)
      }
    })
}

让我们分解一下代码。首先,我们调用 response.json 来读取服务器发送的 JSON 数据。由于 response.json 返回一个 Promise,我们可以立即调用 .then 来读取它的内容。

我们希望在第一个 .then 中调用第二个 .then,因为我们仍然需要访问 response.ok 来确定响应是否成功。

如果您想将状态和状态文本与 JSON 一起发送到 .catch,可以使用 Object.assign() 将它们合并成一个对象。

let error = Object.assign({}, json, {
  status: response.status,
  statusText: response.statusText
})
return Promise.reject(error)

使用这个新的 handleResponse 函数,您可以这样编写代码,数据会自动传递到 .then.catch 中。

fetch('some-url')
  .then(handleResponse)
  .then(data => console.log(data))
  .catch(error => console.log(error))

不幸的是,我们还没有完成对响应的处理 :(

处理其他响应类型

到目前为止,我们只讨论了使用 Fetch 处理 JSON 响应。这已经解决了 90% 的用例,因为如今 API 大都返回 JSON。

那么剩下的 10% 呢?

假设您使用上述代码接收到了一个 XML 响应。您将在 catch 语句中立即遇到一个错误,提示:

解析无效 JSON 会产生语法错误。

这是因为 XML 不是 JSON。我们无法返回 response.json。相反,我们需要返回 response.text。为此,我们需要通过访问响应头来检查内容类型。

.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...
  }
});

您可能好奇,为什么还会遇到 XML 响应?

嗯,我在尝试使用 ExpressJWT 在我的服务器上处理身份验证时遇到了这种情况。当时,我不知道可以将 JSON 发送作为响应,所以我保留了它的默认值,即 XML。这只是您会遇到的许多意想不到的可能性之一。想再尝试一个吗?尝试获取 some-url :)

无论如何,以下是我们迄今为止涵盖的所有代码:

fetch('some-url')
  .then(handleResponse)
  .then(data => console.log(data))
  .catch(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 text
      } else {
        return Promise.reject({
          status: response.status,
          statusText: response.statusText,
          err: text
        })
      }
    })
}

如果您使用 Fetch,那么需要编写/复制和粘贴大量代码。由于我在项目中大量使用 Fetch,我围绕 Fetch 创建了一个库,它可以准确地执行我在本文中描述的操作(以及更多)。

介绍 zlFetch

zlFetch 是一个库,它抽象了 handleResponse 函数,因此您可以跳过前面的步骤,直接处理数据和错误,而无需担心响应。

典型的 zlFetch 看起来像这样:

zlFetch('some-url', options)
  .then(data => console.log(data))
  .catch(error => console.log(error));

要使用 zlFetch,您首先需要安装它。

npm install zl-fetch --save

然后,您需要将它导入您的代码中。(如果您不是使用 ES6 导入,请注意 default)。如果您需要 polyfill,请确保在添加 zlFetch 之前导入它。

// Polyfills (if needed)
require('isomorphic-fetch') // or whatwg-fetch or node-fetch if you prefer

// ES6 Imports
import zlFetch from 'zl-fetch';

// CommonJS Imports
const zlFetch = require('zl-fetch');

zlFetch 不仅可以消除处理 Fetch 响应的需要,它还可以帮助您发送 JSON 数据,而无需编写头文件或将您的主体转换为 JSON。

以下两个函数执行相同的操作。zlFetch 在幕后添加了 Content-Type 并将您的内容转换为 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 还使使用 JSON Web Token 进行身份验证变得容易。

身份验证的标准做法是在头文件中添加一个 Authorization 密钥。此 Authorization 密钥的内容设置为 Bearer your-token-here。如果您添加了 token 选项,zlFetch 可以帮助您创建此字段。

因此,以下两段代码是等效的。

let token = 'someToken'
zlFetch('some-url', {
  headers: {
    Authorization: `Bearer ${token}`
  }
});

// Authentication with JSON Web Tokens with zlFetch
zlFetch('some-url', {token});

这就是 zlFetch 的全部功能。它只是一个方便的包装函数,可以帮助您在使用 Fetch 时编写更少的代码。如果您感兴趣,请查看 zlFetch。否则,您可以随意创建自己的库!

以下是一个用于玩转 zlFetch 的 CodePen 示例:

总结

Fetch 是一项了不起的技术,它使发送和接收数据变得轻而易举。我们不再需要手动编写 XHR 请求,也不需要依赖像 jQuery 这样的大型库。

尽管 Fetch 很棒,但使用 Fetch 进行错误处理并不简单。在您能够正确处理错误之前,您需要相当多的样板代码来将信息传递到您的 .catch 调用中。

使用 zlFetch(以及本文介绍的信息),我们不再需要为正确处理错误而烦恼了。去尝试一下,让您的错误信息也变得有趣吧 :)


顺便说一下,如果您喜欢这篇文章,您可能也会喜欢我在 博客 上写的其他与前端相关的文章。欢迎随时光临并提出任何问题。我会尽快回复您。