DanLevy.net

Возможно, вам не нужен Axios

Fetch API на выручку!

Hero image for Возможно, вам не нужен Axios

Возможно, вам не нужен Axios

Это не нападение на Axios.
Скорее, это рекомендация использовать API fetch, который стал довольно мощным. 🦄

Обзор

В этой статье собраны «недостающие» фрагменты кода fetch и типичные сценарии использования, которые хотелось бы видеть в одном месте.

Ваш случай использования не указан? Сообщите мне ✉️


Сравнение функций

fetchaxiosrequest
Перехват запросов и ответов
Преобразование данных запросов и ответов
Отмена запросов
Автоматическое преобразование JSON‑данныхручные помощники
Защита от XSRF на клиенте
Прогресс
Потоковая передача
Перенаправления


Когда я начинал писать эту статью (конец 2018 г., обновление 2024), я предполагал закончить её таблицей с разными галочками. Конечно, существуют особые Use Cases, которые оправдывают использование axios, request, r2, superagent, got и т.п.

Но, как оказалось, я переоценил необходимость сторонних HTTP‑библиотек.

Хотя я использую fetch уже несколько лет (в том числе для нетривиальных задач: загрузка файлов, поддержка ошибок и повторных попыток), у меня всё ещё были заблуждения о возможностях и ограничениях fetch.

Нативный fetch не парсит JSON‑ответы автоматически и не сериализует тело запроса в JSON. Нужно явно вызвать response.json() при получении и JSON.stringify() при отправке. В этом плане Axios остаётся удобнее; аргумент в пользу fetch — что небольшая вспомогательная функция легко закрывает этот пробел.

Итак, посмотрим, что умеет fetch

Fetch Recipes

Получить JSON из URL

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 определяет, будут ли ваши cookies автоматически включены.

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

Данные в формате form‑encoded

Чтобы отправить данные с заголовком Content-Type: application/x-www-form-urlencoded, используем 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. Не загружайте в передаваемую цепочку слишком много работы — она будет продолжать исполняться, а любые ошибки могут привести к долговременным утечкам памяти.

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.

Техника обработчика прогресса, показанная ниже, избегает оборачивания вызова 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:

Source: Progress Helper
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 и его fantastic Progress+Fetch PoC shown here

Recursive Retry Helper

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 год API fetchшироко поддерживается во всех современных браузерах и в более новых версиях Node JS v18+.

Если требуется поддержка IE, можно подключить полифил для fetch через пакет github/fetch (поддерживается отличной командой GitHub). При желании можно откатиться даже до IE8 — результат может различаться.

Для более старых версий Node JS можно воспользоваться API fetch через пакет node-fetch:

Terminal window
npm install node-fetch

После полифила + node-fetch: совместимость 99,99 %

Пожалуйста, напишите мне в Твиттере, если у вас есть другие случаи использования, которые хотелось бы увидеть. ❤️