你可能不需要 Axios
Fetch API 来拯救!
你可能不需要 Axios
这 并不是对 Axios 的攻击。
相反,这是 对已经相当强大的 fetch API 的倡导。🦄
概览
本文收集了我希望更容易找到的 “缺失” fetch 代码片段和常见用例。
你的使用场景没有列出?告诉我 ✉️
功能对比
| fetch | axios | request | |
|---|---|---|---|
| 拦截请求和响应 | ✅ | ✅ | ✅ |
| 转换请求和响应数据 | ✅ | ✅ | ✅ |
| 取消请求 | ✅ | ✅ | ❌ |
| 自动 JSON 数据转换 | 手动辅助 | ✅ | ✅ |
| 客户端防 XSRF 支持 | ✅ | ✅ | ✅ |
| 进度 | ✅ | ✅ | ✅ |
| 流式传输 | ✅ | ✅ | ✅ |
| 重定向 | ✅ | ✅ | ✅ |
在撰写本文时(2018 年底,2024 年更新),我本以为会得到一张混合勾选框的表格。肯定有一些特殊的 使用场景 需要 axios、request、r2、superagent、got 等库。
事实证明,我高估了第三方 HTTP 库的需求。
虽然我已经使用 fetch 好几年(包括文件上传、错误/重试等非平凡任务),但仍对 fetch 的能力和限制有误解。
原生 fetch 不会自动解析 JSON 响应或序列化 JSON 请求体。需要在返回时调用 response.json(),在发送时使用 JSON.stringify()。在这点上 Axios 仍然更省事;fetch 的论点是,一个小辅助函数就能填补这个差距。
好,来看看 fetch 能做到什么吧…
Fetch 食谱
从 URL 获取 JSON
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))自定义请求头
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 错误处理
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。
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
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>
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。
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))}上传文件
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 属性设置文件上传元素:
<input type='file' multiple class='files' name='files' />然后可以这样使用:
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 链中做太多工作,否则它会一直运行——任何错误都可能导致长期的内存泄漏。
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,以便 拦截任何耗时的操作。
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 具有以下接口(源码见下)
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_下面来看一个使用示例:
// 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():
const getBlob = url => fetch(url) .then(progressHelper(console.log)) // progressHelper used inside the .then() .then(response => response.blob())顺便说下,Blob 是 Binary Large Object(二进制大对象)。
下面的两种使用模式功能等价,务必只选其一:
// OPTION #1: no temp streamProcessor varfetch(...) .then(progressHelper(console.log))
// ⚠️ OR️ ️⚠️
// OPTION #2: define a `streamProcessor` to hold our console loggerconst streamProcessor = progressHelper(console.log)fetch(...) .then(streamProcessor)我更倾向于 Option #1,但如果你的作用域设计迫使你使用 Option #2,也可以。
最后,展示本教程的最后一块——我们的 progressHelper:
来源:进度助手
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 示例
递归重试助手
/** * 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())
// Usageconst 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)/** 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())
// Usageretry(() => getJson('https://api.github.com/orgs/elite-libs')) .then(console.log) .catch(console.error)处理 HTTP 重定向
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 请求
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:
npm install node-fetch使用 polyfill + node-fetch 后:兼容性达 99.99% ✅
如有其他想看到的 使用场景,请给我发推特 ❤️