Potresti non aver bisogno di Axios
L'API Fetch al soccorso!
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.
- Panoramica
- Confronto delle funzionalità
- Ricette con
fetch- Ottenere JSON da un URL
- Intestazioni personalizzate
- Gestione degli errori HTTP
- Esempio CORS
- Inviare JSON
- Inviare un HTML
<form> - Dati codificati in formato form
- Caricare un file
- Caricare più file
- Timeout
- Helper per il progresso del download
- Helper per il riprova ricorsivo
- Gestione dei redirect HTTP
- Annullare una richiesta
fetch✨nuovo✨
- Compatibilità
Il tuo caso d’uso non è elencato? Fammi sapere ✉️
Confronto delle funzionalità
| fetch | axios | request | |
|---|---|---|---|
| Interceptare richiesta e risposta | ✅ | ✅ | ✅ |
| Trasformare dati richiesta e risposta | ✅ | ✅ | ✅ |
| Annullare le richieste | ✅ | ✅ | ❌ |
| Trasformazioni automatiche per dati JSON | richiede 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
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
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
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.
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
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>
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.
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
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:
<input type='file' multiple class='files' name='files' />Successivamente, utilizzarlo in modo simile a:
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.
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.
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)
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:
// 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():
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):
// 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)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
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
/** * 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)Gestione delle Reindirizzazioni 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)Annullamento di una richiesta 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); });}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:
npm install node-fetchDopo polyfill+node-fetch: compatibilità al 99,99% ✅
Fammi sapere su Twitter se desideri vedere altri Casi d’uso. ❤️