Vielleicht brauchen Sie Axios nicht
Fetch‑API rettet!
Sie benötigen möglicherweise kein Axios
Das ist kein Angriff auf Axios.
Vielmehr ist es eine Befürwortung der fetch‑API, die inzwischen sehr leistungsfähig ist. 🦄
Überblick
Dieser Artikel ist eine Sammlung der „fehlenden“ fetch‑Code‑Snippets und gängiger Anwendungsfälle, die ich mir leichter zugänglich wünschte.
- Übersicht
- Feature‑Vergleich
- Fetch‑Rezepte
- JSON von einer URL holen
- Benutzerdefinierte Header
- HTTP‑Fehlerbehandlung
- CORS‑Beispiel
- JSON senden
- Ein HTML‑
<form>senden - Form‑kodierte Daten
- Datei hochladen
- Mehrere Dateien hochladen
- Timeouts
- Download‑Progress‑Hilfsfunktion
- Recursive‑Retry‑Hilfsfunktion
- HTTP‑Redirects verarbeiten
- Einen fetch‑Request abbrechen ✨neu✨
- Kompatibilität
Fehlt Ihr Anwendungsfall? Lassen Sie es mich wissen ✉️
Feature‑Vergleich
| fetch | axios | request | |
|---|---|---|---|
| Anfragen und Antworten abfangen | ✅ | ✅ | ✅ |
| Daten von Anfragen und Antworten transformieren | ✅ | ✅ | ✅ |
| Anfragen abbrechen | ✅ | ✅ | ❌ |
| Automatische Transformation für JSON‑Daten | manuelle Hilfsfunktionen | ✅ | ✅ |
| Client‑seitiger Schutz vor XSRF | ✅ | ✅ | ✅ |
| Fortschritt | ✅ | ✅ | ✅ |
| Streaming | ✅ | ✅ | ✅ |
| Redirects | ✅ | ✅ | ✅ |
Als ich diesen Artikel Anfang 2018 (aktualisiert 2024) begann, ging ich davon aus, dass ich mit einer Tabelle gemischter Häkchen enden würde. Sicherlich gibt es spezielle Use Cases, die den Einsatz von axios, request, r2, superagent, got usw. rechtfertigen.
Nun, wie sich herausstellt, habe ich den Bedarf an Drittanbieter‑HTTP‑Bibliotheken überschätzt.
Obwohl ich fetch seit mehreren Jahren nutze (auch für komplexere Aufgaben wie Datei‑Uploads und Fehler‑/Retry‑Handling), hatte ich immer noch Fehlvorstellungen über die Fähigkeiten und Grenzen von fetch.
Der native fetch parst JSON‑Antworten nicht automatisch und serialisiert keine JSON‑Request‑Bodies. Man muss response.json() beim Rückweg und JSON.stringify() beim Ausgang aufrufen. Bei dieser Ergonomie liegt Axios immer noch vorn; das Argument für fetch ist, dass ein winziger Helfer die Lücke meist schließt.
Nun, schauen wir uns an, was fetch leisten kann…
Fetch‑Rezepte
JSON von einer URL abrufen
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))Benutzerdefinierte Header
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-Fehlerbehandlung
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‑Beispiel
CORS wird hauptsächlich auf dem Server geprüft – stellen Sie also sicher, dass Ihre Server‑Konfiguration korrekt ist.
Die Option credentials bestimmt, ob Ihre Cookies automatisch mitgesendet werden.
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 senden
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))}Posting eines 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‑kodierte Daten
Um Daten mit dem Content‑Type application/x-www-form-urlencoded zu senden, verwenden wir URLSearchParams, um die Daten wie eine Query‑String zu kodieren.
Beispielsweise liefert 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))}Datei hochladen
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))}Mehrere Dateien hochladen
Richten Sie ein Datei‑Upload‑Element mit dem Attribut multiple ein:
<input type='file' multiple class='files' name='files' />Verwenden Sie es dann etwa so:
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))}Timeouts
Hier ein generischer Promise‑Timeout, implementiert nach dem „Partial‑Application“-Muster. Er funktioniert mit jeder Promise‑Schnittstelle. Vermeiden Sie, im übergebenen Promise‑Chain zu viel Arbeit zu erledigen; sie läuft weiter und Fehler können langfristige Speicherlecks erzeugen.
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]) }}Und ein komplexeres Beispiel, das ein Tracking‑Flag __timeout verwendet, sodass Sie beliebige aufwändige Vorgänge abfangen können.
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))Download‑Progress‑Hilfsfunktion
Upload‑Progress ist derzeit außerhalb von Chrome etwas fehlerhaft.
Die Progress‑Handler‑Technik, die im Folgenden gezeigt wird, vermeidet das Einhüllen des fetch‑Aufrufs in eine Closure. 👍
progressHelper besitzt die folgende Schnittstelle (Quellcode unten verfügbar)
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_Betrachten wir ein Anwendungsbeispiel:
// 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..."> })Ein wiederverwendbarer Bild‑Downloader könnte so aussehen: getBlob():
const getBlob = url => fetch(url) .then(progressHelper(console.log)) // progressHelper used inside the .then() .then(response => response.blob())Übrigens ist ein Blob ein Binary Large Object.
Es ist wichtig, EINE der beiden folgenden Verwendungsmuster zu wählen (sie sind funktional äquivalent):
// 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)Meine Präferenz ist Option #1. Ihr Anwendungs‑Design kann Sie jedoch dazu zwingen, Option #2 zu verwenden.
Abschließend noch der letzte Teil dieses Rezepts, unser progressHelper:
Quelle: 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) }) } } }) ) }}_Dank: Besonderer Dank gilt Anthum Chris und seinem fantastischen Progress+Fetch‑PoC, das hier zu sehen ist
Rekursiver 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)Umgang mit HTTP‑Redirects
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)Abbrechen einer fetch‑Anfrage
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); });}Kompatibilität
Stand 2022 wird die fetch‑API breit unterstützt in allen modernen Browsern und in neueren Node‑JS‑Versionen ab v18+.
Müssen Sie noch Internet Explorer unterstützen, können Sie die fetch‑API mit dem github/fetch‑Paket polyfillen (maintained by einem großartigen Team bei GitHub). Es ist sogar möglich, bis zu IE8 zurückzugehen – Ergebnisse können variieren.
Ältere Node‑JS‑Versionen können die fetch‑API über das Paket node-fetch nutzen:
npm install node-fetchNach Polyfill + node-fetch: 99,99 % kompatibel ✅
Bitte tweeten Sie mir, wenn Sie weitere Use Cases sehen möchten. ❤️