DanLevy.net

Vielleicht brauchen Sie Axios nicht

Fetch‑API rettet!

Hero image for Vielleicht brauchen Sie Axios nicht

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.

Fehlt Ihr Anwendungsfall? Lassen Sie es mich wissen ✉️


Feature‑Vergleich

fetchaxiosrequest
Anfragen und Antworten abfangen
Daten von Anfragen und Antworten transformieren
Anfragen abbrechen
Automatische Transformation für JSON‑Datenmanuelle 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

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

Benutzerdefinierte Header

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

HTTP-Fehlerbehandlung

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

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.

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

JSON senden

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

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

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.

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

Datei hochladen

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

Mehrere Dateien hochladen

Richten Sie ein Datei‑Upload‑Element mit dem Attribut multiple ein:

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

Verwenden Sie es dann etwa so:

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

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.

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

Und ein komplexeres Beispiel, das ein Tracking‑Flag __timeout verwendet, sodass Sie beliebige aufwändige Vorgänge abfangen können.

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

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)

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_

Betrachten wir ein Anwendungsbeispiel:

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

Ein wiederverwendbarer Bild‑Downloader könnte so aussehen: getBlob():

getBlob.js
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 Verwendungs­muster zu wählen (sie sind funktional äquivalent):

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)

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

​_Dank: Besonderer Dank gilt Anthum Chris und seinem fantastischen Progress+Fetch‑PoC, das hier zu sehen ist

Rekursiver Retry‑Helper

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)

Umgang mit HTTP‑Redirects

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)

Abbrechen einer fetch‑Anfrage

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

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:

Terminal window
npm install node-fetch

Nach Polyfill + node-fetch: 99,99 % kompatibel

Bitte tweeten Sie mir, wenn Sie weitere Use Cases sehen möchten. ❤️