DanLevy.net

Potresti non aver bisogno di Axios

L'API Fetch al soccorso!

Hero image for Potresti non aver bisogno di Axios

Potresti non aver bisogno di Axios

Questo non è un attacco a Axios.
Piuttosto, è una promozione dell’API fetch che si è rivelata molto capace. 🦄

Panoramica

Questo articolo è una raccolta di snippets di codice fetch e casi d’uso comuni che vorrei fossero più facili da trovare.

Il tuo caso d’uso non è elencato? Fammi sapere ✉️


Confronto delle funzionalità

fetchaxiosrequest
Interceptare richiesta e risposta
Trasformare dati richiesta e risposta
Annullare le richieste
Trasformazioni automatiche per dati JSONrichiede helper manuali
Protezione lato client contro XSRF
Progresso
Streaming
Redirect


Quando ho iniziato questo articolo (fine 2018, aggiornato nel 2024) assumevo di finire con una tabella di caselle di controllo miste. Certamente esistevano casi d’uso specifici che giustificavano axios, request, r2, superagent, got, ecc.

Beh, come si è rivelato, ho sopravvalutato la necessità di librerie HTTP di terze parti.

Nonostante l’uso di fetch per diversi anni (inclusi compiti non banali: caricamento di file e supporto per errori/riprova), avevo ancora misconcezioni sulle capacità e i limiti di fetch.

Il fetch nativo non analizza automaticamente le risposte JSON né converte in stringa i corpi delle richieste JSON. Devi chiamare response.json() al ritorno e JSON.stringify() in uscita. Axios mantiene comunque queste comodità; l’argomento a favore di fetch è che un piccolo helper copre spesso il divario.

Beh, vediamo cosa può fare fetch

Ottenere JSON da un 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))

Intestazioni personalizzate

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

Gestione degli errori 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))

Esempio CORS

CORS viene principalmente verificato dal server, quindi assicurati che la tua configurazione sia corretta lato server.

L’opzione credentials controlla se i tuoi cookie vengono automaticamente inclusi.

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

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

Invio di un 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))
}

Dati codificati in form

Per inviare dati con un Content-Type di application/x-www-form-urlencoded utilizzeremo URLSearchParams per codificare i dati come una query string.

Ad esempio, new URLSearchParams({a: 1, b: 2}) produce 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))
}

Caricamento di un file

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

Caricamento di più file

Configura un elemento di caricamento file con l’attributo multiple:

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

Successivamente, utilizzarlo in modo simile a:

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

Timeout

Ecco un timeout generico per le Promise, utilizzando il pattern “Partial Application”. Funziona con qualsiasi interfaccia Promise. Non eseguire troppo lavoro nella catena di promise fornita, continuerà a eseguirsi - e qualsiasi fallimento può generare perdite di memoria a lungo termine.

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

E un esempio più complesso, con un flag di tracciamento __timeout per intercettare qualsiasi lavoro costoso.

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

Helper per il Progresso del Download

Il progresso di upload è attualmente un po’ instabile al di fuori di Chrome.

La tecnica del gestore di progresso mostrata di seguito evita di avvolgere la chiamata fetch in una closure. 👍

progressHelper ha l’interfaccia seguente (sorgente disponibile qui sotto)

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_

Analizziamo un esempio d’uso:

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

Un downloader riutilizzabile per immagini potrebbe assomigliare a getBlob():

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

Per inciso, un Blob è un Binary Large Object.

È importante scegliere UNA delle due modalità d’uso elencate di seguito (sono funzionalmente equivalenti):

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)

La mia preferenza è Opzione #1. Tuttavia, la progettazione del tuo ambito potrebbe costringerti a utilizzare Opzione #2.

Infine, ecco l’ultima parte di questa ricetta, il nostro progressHelper:

Fonte: Helper di Progresso
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)
})
}
}
})
)
}
}

credito: Ringraziamenti speciali ad Anthum Chris e al suo fantastico PoC Progress+Fetch mostrato qui

Helper di Riprova Ricorsivo

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)

Gestione delle Reindirizzazioni 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)

Annullamento di una richiesta 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);
});
}

Compatibilità

Fino al 2022, l’API fetch è ampiamente supportata in tutti i browser moderni e nelle versioni più recenti di NodeJS v18+.

Se devi supportare IE, puoi aggiungere un polyfill a fetch utilizzando il pacchetto github/fetch (mantenuto da un fantastico team di GitHub). È possibile estendere il supporto fino a IE8 - I risultati possono variare.

Le versioni precedenti di NodeJS possono sfruttare l’API fetch tramite il pacchetto node-fetch:

Terminal window
npm install node-fetch

Dopo polyfill+node-fetch: compatibilità al 99,99%

Fammi sapere su Twitter se desideri vedere altri Casi d’uso. ❤️