DanLevy.net

Vous n'avez pas besoin d'Axios

La Fetch API vient à la rescousse !

Hero image for Vous n'avez pas besoin d'Axios

Vous n’avez peut-être pas besoin d’Axios

Ce n’est pas une attaque contre Axios.
C’est plutôt un plaidoyer en faveur de l’API fetch qui est désormais très performante. 🦄

Aperçu

Cet article rassemble les extraits de code fetch “manquants” et les cas d’utilisation courants que j’aurais souhaité plus faciles à trouver.

Aperçu

Votre cas d’utilisation n’est pas listé ? Faites-le-moi savoir ✉️


Comparaison des fonctionnalités

fetchaxiosrequest
Intercepter les requêtes et réponses
Transformer les données de requête et réponse
Annuler les requêtes
Transformations automatiques pour le JSONhelpers manuels
Protection côté client contre les XSRF
Suivi de progression
Streaming
Redirections


Lors de la rédaction de cet article (fin 2018, mis à jour en 2024), je pensais conclure avec un tableau de cases cochées partielles. Il y avait certainement des cas d’utilisation spécifiques justifiant l’utilisation de bibliothèques tierces comme axios, request, r2, superagent, got, etc.

En réalité, j’ai surestimé le besoin d’utiliser des bibliothèques HTTP tierces.

Malgré l’utilisation de fetch pendant plusieurs années (y compris pour des tâches non triviales : uploads de fichiers et gestion d’erreurs/retries), j’avais encore des idées reçues sur ses capacités et ses limites.

fetch natif ne parse pas automatiquement les réponses JSON ni ne convertit les corps de requête JSON en chaîne. Vous appelez response.json() en retour et JSON.stringify() en envoi. Axios reste plus ergonomique sur ce point précis ; l’argument en faveur de fetch est qu’un petit helper comble souvent ce gap.

Voyons donc ce que fetch est capable de faire…

Recettes avec Fetch

Récupérer du JSON depuis une 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))

En-têtes personnalisées

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

Gestion des erreurs 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))

Exemple de CORS

CORS est principalement vérifié côté serveur, assurez-vous que votre configuration est correcte côté serveur.

L’option credentials contrôle si vos cookies sont automatiquement inclus.

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

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

Soumettre un formulaire 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))
}

Données encodées en formulaire

Pour envoyer des données avec un Content-Type de application/x-www-form-urlencoded, nous utiliserons URLSearchParams pour encoder les données comme une chaîne de requête.

Par exemple, new URLSearchParams({a: 1, b: 2}) produit 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))
}

Téléchargement d’un fichier

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

Téléchargement de plusieurs fichiers

Configurez un élément de téléchargement de fichier avec l’attribut multiple :

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

Puis utilisez-le avec quelque chose comme :

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

Délais d’expiration (timeouts)

Voici un délai d’expiration générique pour les promesses, utilisant le motif d‘“Application partielle”. Cela fonctionnera avec n’importe quelle interface de promesse. N’effectuez pas trop de travail dans la chaîne de promesses fournie, car elle continuera de s’exécuter - et toute erreur a tendance à créer des fuites de mémoire à long terme.

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

Et un exemple plus complexe, avec un indicateur de suivi __timeout pour intercepter tout travail coûteux.

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

Assistant de progression de téléchargement

La progression du téléversement est actuellement un peu bugguée en dehors de Chrome.

Le gestionnaire de progression la technique montrée ci-dessous évite d’encapsuler l’appel fetch dans une fermeture. 👍

progressHelper a l’interface suivante (source disponible ci-dessous)

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_

Examinons un exemple d’utilisation :

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 téléchargeur d’images réutilisable pourrait ressembler à getBlob() :

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

Au passage, un Blob est un Binary Large Object.

Il est important de choisir UN des deux schémas d’utilisation ci-dessous (ils sont fonctionnellement équivalents) :

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)

Je préfère l’Option #1. Cependant, la conception de votre portée pourrait vous obliger à utiliser l’Option #2.

Enfin, voici la dernière partie de cette recette, notre 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)
})
}
}
})
)
}
}

crédit : Remerciements particuliers à Anthum Chris et à son excellente preuve de concept Progress+Fetch présentée ici

Helper de réessai récursif

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)

Gestion des redirections 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)

Annulation d’une requête 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é

En 2022, l’API fetch est largement prise en charge dans tous les navigateurs modernes et dans les versions récentes de NodeJS v18+.

Si vous devez supporter IE, vous pouvez ajouter un polyfill pour fetch via le package github/fetch (maintenu par une équipe formidable de GitHub). Il est possible d’aller jusqu’à IE8 - Résultats variables selon les cas.

Les versions antérieures de NodeJS peuvent utiliser l’API fetch via le package node-fetch :

Terminal window
npm install node-fetch

Après polyfill+node-fetch : 99,99 % compatible

Veuillez m’envoyer un tweet si vous avez d’autres Cas d’utilisation que vous aimeriez voir. ❤️