无论何时我们使用 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,其中包含更多详细信息。 数字表示浏览器在该版本及更高版本上支持该功能。
桌面
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
42 | 39 | 否 | 14 | 10.1 |
移动设备/平板电脑
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
127 | 127 | 127 | 10.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);
您需要设置的**第一个选项**是您的请求方法,可以是 post
、put
或 del
。 如果您省略 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()...
对于眼尖的人来说,您会注意到每个 post
、put
或 del
请求都有一些样板代码。 理想情况下,我们可以重用我们的标头并在发送之前对内容调用 JSON.stringify
,因为我们已经知道我们发送的是 JSON 数据。
但即使使用样板代码,Fetch 对于发送任何请求来说仍然非常不错。
但是,使用 Fetch 处理错误并不像处理成功消息那样简单。 您很快就会明白为什么。
使用 Fetch 处理错误
尽管我们总是希望 Ajax 请求能够成功,但它们也可能失败。 请求失败的原因有很多,包括但不限于以下情况
- 您尝试获取不存在的资源。
- 您无权获取资源。
- 您输入了一些错误的参数
- 服务器抛出错误。
- 服务器超时。
- 服务器崩溃。
- 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 请求。 您将获得以下内容

.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"
}
大多数响应保持不变,除了 ok
、status
和 statusText
。 正如预期的那样,我们在 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));

这很棒。 我们正在取得进展,因为我们现在有了一种处理错误的方法。
但是,使用通用消息拒绝 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
状态文本,我们就会知道该如何处理它。
要将 status
和 statusText
传递到 .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。

仅仅告诉我们的 .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
语句中立即遇到一个错误,提示:

这是因为 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(以及本文介绍的信息),我们不再需要为正确处理错误而烦恼了。去尝试一下,让您的错误信息也变得有趣吧 :)
顺便说一下,如果您喜欢这篇文章,您可能也会喜欢我在 博客 上写的其他与前端相关的文章。欢迎随时光临并提出任何问题。我会尽快回复您。
我有一个问题和一个意见。
为什么有两个异步调用?它只调用资源一次,对吧?它肯定不会再次调用 URL 来获取主体。如果它没有两次调用 URL,为什么它需要异步,难道主体数据不已经存在于客户端了吗?
第二,Fetch 看起来非常酷。不过,我会说它并没有与 jQuery 直接竞争。对于大多数程序员来说,我们不会切换到多个像 zlFetch 和 Sizzle 这样的小库。当我在没有 jQuery 的情况下工作时,它确实非常棒。很棒的文章!
当你说到“两个异步调用”时,你指的是
fetch()
和response.json()
,对吧?response.json()
(以及.blob()
和.text()
等等)是异步的原因是,当fetch()
完成时,响应主体可能还没有全部到达(例如,服务器可能只发送了 50% 的响应)。为了使.json()
返回一个对象,它需要先等待完整的响应主体(同样,.text()
等等也是如此)。这意味着您可以实际在响应到达时对其进行流式处理。想象一下使用
fetch()
获取包含 100 万行的大型 CSV 文件,但您只需要第 50 行。调用.text()
并将其解析为 CSV 会非常非常慢,因为它必须等待整个文件。另一方面,调用.getReader().read()
(并根据需要再次调用.read()
)可以让您更快地到达第 50 行,并忽略其他行。请参见此处的示例:https://fetch.spec.whatwg.org/#fetch-api
考虑到您必须处理错误和无效响应的所有麻烦,这与 XMLHttpRequest 相比到底简单在哪里?
我认为文章中的示例可能有点夸张,是为了展示所有可能的场景。
在更现实的情况下,您只需要处理您特定 API 响应的样板代码。例如:
我相信大多数人会发现它比 XMLHttpRequest 的替代方案干净得多。
在您的 handleTextResponse() 中,您返回了 json 变量,而不是在箭头函数中定义的 text 变量…
“Fetch 返回一个 Promise,它是一种无需回调即可处理异步操作的方法。”
这真是一个糟糕的措辞!Promise 使用回调函数——只是它们的结构更便于理解。
不一定;您可以
await
一个 promise;)感谢您撰写这篇文章!需要注意的是,与 XHR 不同,您需要为受保护的资源显式声明:
{ credentials: "same-origin" }
(或"include"
用于 CORS)。感谢您对 fetch 的精彩介绍。
我认为 fetch API 非常棒,比 XMLHttpRequest 更易于使用,但正如您所展示的那样,fetch 与 XMLHttpRequest 一样,仍然需要相当多的额外处理才能使其正常工作。
在使用 jQuery 包装 XMLHttpRequest 的地方,我需要类似的东西来包装 Fetch,zlFetch 似乎可以胜任这项工作,但是为什么我会使用它而不是 Axios 呢?Axios 似乎更受欢迎,并且可以在浏览器和 Node.js 中使用相同的 API。
Axios 也不错。选择你喜欢的那一个吧!:)
非常棒的教程。谢谢!:)
关于 Promise 的问题是,它看起来和读起来比传统的 JS 复杂得多。你在文章中也拿 jQuery 做了比较,但最后又提到了另一个库。
我的意思是,每个开发人员都应该有一个 XMLHttpRequest 工具函数 - 你只需要写一次。代码最终比新的 Promise 语法更易读。对于人们所说的回调地狱,似乎应该记住 .then 仍然是一个回调。
Fetch 不支持中止/取消请求(并且它可能永远不会支持,因为 Promise 的取消是一个死功能),它不支持指定超时,也不支持上传进度。
也许这些不是你马上需要的东西,但除非你能保证你的项目生命周期内不需要这些东西,否则使用 fetch 并不是一个明智的决定。
没错!
我不明白为什么每个人都开始使用 fetch。
与 XMLHttpRequest 相比,它太有限了。
如果需要,它很容易将 XMLHttpRequest 包装成一个 Promise。
难以理解……
我使用 reader.cancel() 来取消请求。
reader = response.body.getReader()
reader.cancel()
就像这样。
但 Firefox 目前没有 response.body..
当请求被取消时
val.done 为 true
比如
reader.read().then(function(val) {
// 当取消时 val.done 为 true
});
我认为使用 fetch,你无法跟踪上传的进度……
“几年前,启动 Ajax 调用的最简单方法是使用 jQuery 的 ajax 方法”。
-- 从文章中,我并没有看到这种情况发生了改变 :)
写得很好,但我无法理解为什么开发人员假装 fetch API 比 jQuery 更简单。
我不是说 jQuery 证明了它的重量和所有东西,但为什么要过度强调 fetch API 是多么酷、简单、干净、神奇……?
很棒的库 zlFetch,正是我一直在寻找的:一个 fetch 的包装器,就像 Axios 对 xmlHTTPrequest 一样!
我认为这篇文章以与 Axios 如今的用例比较结尾会很好,尤其是进度处理(例如,用于图像获取)。如果我没记错的话,fetch 还不支持进度和取消,而 xmlHTTPrequest 支持(并且 Axios 也支持)。
在 handleTextResponse() 中,你应该返回“text”,而不是“json”,如果 (response.ok)