Возможно, вам не нужен Axios
Fetch API на выручку!
Возможно, вам не нужен Axios
Это не нападение на Axios.
Скорее, это рекомендация использовать API fetch, который стал довольно мощным. 🦄
Обзор
В этой статье собраны «недостающие» фрагменты кода fetch и типичные сценарии использования, которые хотелось бы видеть в одном месте.
- Обзор
- Сравнение функций
- Рецепты fetch
- Получить JSON по URL
- Пользовательские заголовки
- Обработка HTTP‑ошибок
- Пример CORS
- Отправка JSON
- Отправка HTML‑
<form> - Данные в формате form‑encoded
- Загрузка файла
- Загрузка нескольких файлов
- Таймауты
- Помощник отслеживания прогресса загрузки
- Помощник рекурсивных повторов
- Обработка HTTP‑перенаправлений
- Отмена запроса fetch ✨new✨
- Совместимость
Ваш случай использования не указан? Сообщите мне ✉️
Сравнение функций
| fetch | axios | request | |
|---|---|---|---|
| Перехват запросов и ответов | ✅ | ✅ | ✅ |
| Преобразование данных запросов и ответов | ✅ | ✅ | ✅ |
| Отмена запросов | ✅ | ✅ | ❌ |
| Автоматическое преобразование 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
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 определяет, будут ли ваши cookies автоматически включены.
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))}Данные в формате form‑encoded
Чтобы отправить данные с заголовком Content-Type: application/x-www-form-urlencoded, используем 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. Не загружайте в передаваемую цепочку слишком много работы — она будет продолжать исполняться, а любые ошибки могут привести к долговременным утечкам памяти.
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.
Техника обработчика прогресса, показанная ниже, избегает оборачивания вызова 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:
Source: Progress Helper
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
/** * 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 год API fetch широко поддерживается во всех современных браузерах и в более новых версиях Node JS v18+.
Если требуется поддержка IE, можно подключить полифил для fetch через пакет github/fetch (поддерживается отличной командой GitHub). При желании можно откатиться даже до IE8 — результат может различаться.
Для более старых версий Node JS можно воспользоваться API fetch через пакет node-fetch:
npm install node-fetchПосле полифила + node-fetch: совместимость 99,99 % ✅
Пожалуйста, напишите мне в Твиттере, если у вас есть другие случаи использования, которые хотелось бы увидеть. ❤️