DanLevy.net

你可能不需要 Axios

Fetch API 来拯救!

Hero image for 你可能不需要 Axios

你可能不需要 Axios

并不是对 Axios 的攻击
相反,这是 对已经相当强大的 fetch API 的倡导。🦄

概览

本文收集了我希望更容易找到的 “缺失” fetch 代码片段和常见用例。

你的使用场景没有列出?告诉我 ✉️


功能对比

fetchaxiosrequest
拦截请求和响应
转换请求和响应数据
取消请求
自动 JSON 数据转换手动辅助
客户端防 XSRF 支持
进度
流式传输
重定向


在撰写本文时(2018 年底,2024 年更新),我本以为会得到一张混合勾选框的表格。肯定有一些特殊的 使用场景 需要 axiosrequestr2superagentgot 等库。

事实证明,我高估了第三方 HTTP 库的需求

虽然我已经使用 fetch 好几年(包括文件上传、错误/重试等非平凡任务),但仍对 fetch 的能力和限制有误解。

原生 fetch 不会自动解析 JSON 响应或序列化 JSON 请求体。需要在返回时调用 response.json(),在发送时使用 JSON.stringify()。在这点上 Axios 仍然更省事;fetch 的论点是,一个小辅助函数就能填补这个差距。

好,来看看 fetch 能做到什么吧…

Fetch 食谱

从 URL 获取 JSON

basic-example.js
fetch('https://api.github.com/orgs/nodejs')
.then(response => response.json())
.then(data => {
console.log(data) // result from `response.json()` above
})
.catch(error => console.error(error))

自定义请求头

custom-headers.js
fetch('https://api.github.com/orgs/nodejs', {
headers: new Headers({
'User-agent': 'Mozilla/4.0 Custom User Agent'
})
})
.then(response => response.json())
.then(data => {
console.log(data)
})
.catch(error => console.error(error))

HTTP 错误处理

fetch-custom-error.js
const isOk = response => response.ok ? response.json() : Promise.reject(new Error('Failed to load data from server'))
fetch('https://api.github.com/orgs/nodejs')
.then(isOk) // <= Use `isOk` function here
.then(data => {
console.log(data) // Prints result from `response.json()`
})
.catch(error => console.error(error))

CORS 示例

CORS 主要在服务器端进行检查——因此请确保服务器端的配置正确。

credentials 选项决定是否自动包含 cookie。

cors-example.js
fetch('https://api.github.com/orgs/nodejs', {
credentials: 'include', // Useful for including session ID (and, IIRC, authorization headers)
})
.then(response => response.json())
.then(data => {
console.log(data) // Prints result from `response.json()`
})
.catch(error => console.error(error))

发送 JSON

posting-json.js
postRequest('http://example.com/api/v1/users', {user: 'Dan'})
.then(data => console.log(data)) // Result from the `response.json()` call
function postRequest(url, data) {
return fetch(url, {
credentials: 'same-origin', // 'include', default: 'omit'
method: 'POST', // 'GET', 'PUT', 'DELETE', etc.
body: JSON.stringify(data), // Use correct payload (matching 'Content-Type')
headers: { 'Content-Type': 'application/json' },
})
.then(response => response.json())
.catch(error => console.error(error))
}

提交 HTML <form>

post-form-data.js
postForm('http://example.com/api/v1/users', 'form#userEdit')
.then(data => console.log(data))
function postForm(url, formSelector) {
const formData = new FormData(document.querySelector(formSelector))
return fetch(url, {
method: 'POST', // 'GET', 'PUT', 'DELETE', etc.
body: formData // a FormData will automatically set the 'Content-Type'
})
.then(response => response.json())
.catch(error => console.error(error))
}

表单编码数据

要使用 application/x-www-form-urlencoded 的 Content-Type 发送数据,我们会使用 URLSearchParams 将数据编码成类似查询字符串的形式。

例如,new URLSearchParams({a: 1, b: 2}) 会得到 a=1&b=2

posting-form-encoded-data.js
postFormData('http://example.com/api/v1/users', {user: 'Mary'})
.then(data => console.log(data))
function postFormData(url, data) {
return fetch(url, {
method: 'POST', // 'GET', 'PUT', 'DELETE', etc.
body: new URLSearchParams(data),
headers: new Headers({
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
})
})
.then(response => response.json())
.catch(error => console.error(error))
}

上传文件

uploading-files.js
postFile('http://example.com/api/v1/users', 'input[type="file"].avatar')
.then(data => console.log(data))
function postFile(url, fileSelector) {
const formData = new FormData()
const fileField = document.querySelector(fileSelector)
formData.append('username', 'abc123')
formData.append('avatar', fileField.files[0])
return fetch(url, {
method: 'POST', // 'GET', 'PUT', 'DELETE', etc.
body: formData // Coordinate the body type with 'Content-Type'
})
.then(response => response.json())
.catch(error => console.error(error))
}

上传多个文件

使用 multiple 属性设置文件上传元素:

file-upload-field.html
<input type='file' multiple class='files' name='files' />

然后可以这样使用:

uploading-multiple-files.js
postFile('http://example.com/api/v1/users', 'input[type="file"].files')
.then(data => console.log(data))
function postFile(url, fileSelector) {
const formData = new FormData()
const fileFields = document.querySelectorAll(fileSelector)
// Add all files to formData
Array.prototype.forEach.call(fileFields.files, f => formData.append('files', f))
// Alternatively for PHPeeps, use `files[]` for the name to support arrays
// Array.prototype.forEach.call(fileFields.files, f => formData.append('files[]', f))
return fetch(url, {
method: 'POST', // 'GET', 'PUT', 'DELETE', etc.
body: formData // Coordinate the body type with 'Content-Type'
})
.then(response => response.json())
.catch(error => console.error(error))
}

超时

下面是一个通用的 Promise 超时实现,采用“部分应用”模式。它可以配合任何 Promise 接口使用。不要在提供的 promise 链中做太多工作,否则它会一直运行——任何错误都可能导致长期的内存泄漏。

promised-timeout.js
function promiseTimeout(msec) {
return promise => {
const timeout = new Promise((yea, nah) => setTimeout(() => nah(new Error('Timeout expired')), msec))
return Promise.race([promise, timeout])
}
}
promiseTimeout(5000)(fetch('https://api.github.com/orgs/nodejs'))
.then(response => response.json())
.then(data => {
console.log(data) // Prints result from `response.json()` in getRequest
})
.catch(error => console.error(error)) // Catches any timeout (or other failure)
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
// Alternative example:
fetchTimeout(5000, 'https://api.github.com/orgs/nodejs')
.then(console.log)
// Alternative implementation:
function fetchTimeout(msec, ...args) {
return raceTimeout(fetch(...args))
function raceTimeout(promise) {
const timeout = new Promise((yea, nah) => setTimeout(() => nah(new Error('Timeout expired')), msec))
return Promise.race([promise, timeout])
}
}

还有一个更复杂的示例,使用跟踪标记 __timeout,以便 拦截任何耗时的操作

promise-timeout.js
function promiseTimeout(msec) {
return (promise) => {
let isDone = false
promise.then(() => isDone = true)
const timeout = new Promise((yea, nah) => setTimeout(() => {
if (!isDone) {
promise.__timeout = true
nah(new Error('Timeout expired'))
}
}, msec))
return Promise.race([promise, timeout])
}
}
promiseTimeout(5000)(fetch('https://api.github.com/orgs/nodejs'))
.then(response => response.json())
.then(data => {
console.log(data) // Prints result from `response.json()` in getRequest
})
.catch(error => console.error(error))

下载进度助手

上传进度在 Chrome 之外的浏览器中仍有些 bug。

下面展示的进度处理技术 避免了 在闭包中包装 fetch 调用。 👍

progressHelper 具有以下接口(源码见下)

progressHelper-signature.js
const progressHelper = require('./progressHelper.js')
const handler = ({loaded, total}) => {
console.log(`Downloaded ${loaded} of ${total}`)
}
// handler args: ({ loaded = Kb, total = 0-100% })
const streamProcessor = progressHelper(handler)
// => streamProcessor is a function for use with the response _stream_

下面来看一个使用示例:

progress-helper-usage.js
// The progressHelper could be inline w/ .then() below...
const streamProcessor = progressHelper(console.log)
fetch('https://fetch-progress.anthum.com/20kbps/images/sunrise-progressive.jpg')
.then(streamProcessor) // note: NO parentheses because `.then` needs to get a function
.then(response => response.blob())
.then(blobData => {
// ... set as base64 on an <img src="base64...">
})

一个可复用的图片下载函数可以写成 getBlob()

getBlob.js
const getBlob = url => fetch(url)
.then(progressHelper(console.log)) // progressHelper used inside the .then()
.then(response => response.blob())

顺便说下,Blob 是 Binary Large Object(二进制大对象)。

下面的两种使用模式功能等价,务必只选其一:

progressHelper-usage.js
// OPTION #1: no temp streamProcessor var
fetch(...)
.then(progressHelper(console.log))
// ⚠️ OR️ ️⚠️
// OPTION #2: define a `streamProcessor` to hold our console logger
const streamProcessor = progressHelper(console.log)
fetch(...)
.then(streamProcessor)

我更倾向于 Option #1,但如果你的作用域设计迫使你使用 Option #2,也可以。

最后,展示本教程的最后一块——我们的 progressHelper

来源:进度助手
progressHelper.js
function progressHelper(onProgress) {
return (response) => {
if (!response.body) return response
let loaded = 0
const contentLength = response.headers.get('content-length')
const total = !contentLength ? -1 : parseInt(contentLength, 10)
return new Response(
new ReadableStream({
start(controller) {
const reader = response.body.getReader()
return read()
function read() {
return reader.read()
.then(({ done, value }) => {
if (done) return void controller.close()
loaded += value.byteLength
onProgress({ loaded, total })
controller.enqueue(value)
return read()
})
.catch(error => {
console.error(error)
controller.error(error)
})
}
}
})
)
}
}

致谢:特别感谢 Anthum Chris 以及他在此处展示的精彩 Progress+Fetch 示例

递归重试助手

fetch-retry-with-curry.js
/**
* A **Smarter** retry wrapper with currying!
*/
function retryCurry(fn, retriesLeft = 5) {
const retryFn = (...args) => fn(...args)
.catch(err => retriesLeft > 0
? retryFn(fn, retriesLeft - 1)
: Promise.reject(err)
})
return retryFn
}
const getJson = (url) => fetch(url)
.then(response => response.json())
// Usage
const retryGetJson = retryCurry(getJson, 3);
// Now you can pass any arguments through to your function!
retryGetJson('https://api.github.com/orgs/elite-libs')
.then(console.log)
.catch(console.error)
fetch-with-retries.js
/** Basic retry wrapper for Promises */
function retryPromise(fn, retriesLeft = 5) {
return fn()
.catch(err => retriesLeft > 0
? retryPromise(fn, retriesLeft - 1)
: Promise.reject(err)
})
}
const getJson = (url) => fetch(url)
.then(response => response.json())
// Usage
retry(() => getJson('https://api.github.com/orgs/elite-libs'))
.then(console.log)
.catch(console.error)

处理 HTTP 重定向

location-redirect.js
const checkForRedirect = (response) => {
// Check for temporary redirect (307), or permanent (308)
if (response.status === 307 || response.status === 308) {
const location = response.headers.get('location')
if (!location) {
return Promise.reject(new Error('Invalid HTTP Redirect! No Location header.'));
}
// You can change the behavior here to any custom logic:
// e.g. open a "confirm" modal, log the redirect url, etc.
return fetch(location)
// Bonus: this will handle recursive redirects ✨
.then(checkForRedirect)
}
return response
};
fetch('https://api.github.com/orgs/elite-libs')
// Next line will handle redirects
.then(checkForRedirect)
.then(response => response.json())
.then(console.log)
.catch(console.error)

取消 fetch 请求

cancel-fetch.js
const httpWithTimeout = (timeout = 5000, url) => {
const controller = new AbortController();
// Set an Nsec cancellation timeout
const timer = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal })
.then(response => {
clearTimeout(timer); // not required but closes open ref
return response.text();
}).then(text => {
console.log(text);
});
}

兼容性

截至 2022 年,fetch API 已在所有现代浏览器以及较新版本的 NodeJS v18+ 中得到广泛支持

如果必须兼容 IE,可以使用 github/fetch 包进行fetch polyfill(由 GitHub 的优秀团队维护)。甚至可以追溯到 IE8——实际效果因环境而异

较早的 NodeJS 版本可以通过node-fetch 包来使用 fetch API:

Terminal window
npm install node-fetch

使用 polyfill + node-fetch 后:兼容性达 99.99%

如有其他想看到的 使用场景,请给我发推特 ❤️