Vous n'avez pas besoin d'Axios
La Fetch API vient à la rescousse !
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
- Aperçu
- Comparaison des fonctionnalités
- Recettes avec
fetch- Obtenir du JSON depuis une URL
- En-têtes personnalisés
- Gestion des erreurs HTTP
- Exemple CORS
- Soumettre du JSON
- Soumettre un formulaire HTML
<form> - Données encodées en formulaire
- Téléverser un fichier
- Téléverser plusieurs fichiers
- Délais d’attente
- Aide pour le suivi de progression de téléchargement
- Aide pour les tentatives récursives
- Gestion des redirections HTTP
- Annulation d’une requête fetch ✨nouveau✨
- Compatibilité
Votre cas d’utilisation n’est pas listé ? Faites-le-moi savoir ✉️
Comparaison des fonctionnalités
| fetch | axios | request | |
|---|---|---|---|
| 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 JSON | helpers 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
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
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
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.
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
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>
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.
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
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 :
<input type='file' multiple class='files' name='files' />Puis utilisez-le avec quelque chose comme :
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.
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.
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)
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 :
// 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() :
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) :
// 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)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
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
/** * 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)Gestion des redirections 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)Annulation d’une requête 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é
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 :
npm install node-fetchAprès polyfill+node-fetch : 99,99 % compatible ✅
Veuillez m’envoyer un tweet si vous avez d’autres Cas d’utilisation que vous aimeriez voir. ❤️