DanLevy.net

אולי לא צריך Axios

Fetch API בא להציל!

Hero image for אולי לא צריך Axios

ייתכן שלא תזדקק ל-Axios

זו אינה מתקפה על Axios.
אלא, זוהי הסברה לטובת ה-fetch API שהפך לבעל יכולות מרשימות. 🦄

סקירה כללית

מאמר זה הוא אוסף של קטעי קוד fetch “החסרים” ומקרי שימוש נפוצים שהייתי רוצה שיהיה קל יותר למצוא.

מקרה השימוש שלך לא מופיע? יידע אותי ✉️


השוואת תכונות

fetchaxiosrequest
יירוט בקשה ותגובה
שינוי נתוני בקשה ותגובה
ביטול בקשות
המרות אוטומטיות לנתוני JSONעזרים ידניים
תמיכה בצד הלקוח להגנה מפני XSRF
התקדמות
סטרימינג …
fetchaxiosrequest
יירוט בקשות ותגובות
שינוי נתוני בקשה ותגובה
ביטול בקשות
המרות אוטומטיות לנתוני JSONעזרים ידניים
תמיכה בצד הלקוח להגנה מפני XSRF
התקדמות
סטרימינג
הפניות


כשהתחלתי לכתוב מאמר זה (סוף 2018, עודכן ב‑2024) הנחתי שאסיים עם טבלה של תיבות סימון מעורבות. אין ספק שיש מקרי שימוש מיוחדים המצדיקים את axios, request, r2, superagent, got ועוד.

ובכן, מסתבר שהערכתי יתר על המידה את הצורך בספריות HTTP של צד שלישי.

למרות שהשתמשתי ב‑fetch במשך כמה שנים (כולל למשימות לא טריוויאליות: העלאות קבצים ותמיכה בשגיאות/ניסיונות חוזרים) עדיין היו לי תפיסות שגויות לגבי היכולות והמגבלות של fetch.

fetch המקורי אינו מנתח אוטומטית תגובות JSON או ממיר גופי בקשה ל‑JSON. אתה קורא ל‑response.json() בדרך חזרה ול‑JSON.stringify() בדרך החוצה. Axios עדיין מנצח בקטע הארגונומי הזה; הטיעון בעד fetch הוא שעזר קטן לעתים קרובות מכסה את הפער.

ובכן, בואו נראה מה fetch יכול לעשות…

מתכוני Fetch

קבלת JSON מכתובת 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))

כותרות מותאמות אישית

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

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

CORS נבדק בעיקר בצד השרת – לכן ודא שהתצורה שלך בצד השרת תקינה.

האפשרות credentials קובעת אם העוגיות שלך נכללות אוטומטית.

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

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

שליחת טופס 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))
}

נתונים מקודדים בטופס

כדי לשלוח נתונים עם Content-Type של application/x-www-form-urlencoded נשתמש ב-URLSearchParams כדי לקודד את הנתונים כמחרוזת שאילתה.

לדוגמה, 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))
}

העלאת קובץ

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

העלאת קבצים מרובים

הגדר רכיב העלאת קבצים עם התכונה multiple:

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

לאחר מכן השתמשו במשהו כמו:

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 (פסקי זמן)

הנה פסק זמן גנרי של Promise, באמצעות תבנית “Partial Application”. זה יעבוד עם כל ממשק Promise. אל תבצעו יותר מדי עבודה בשרשרת ה-promise שסופקה, היא תמשיך לרוץ – ולכשלים יש נטייה ליצור דליפות זיכרון ארוכות טווח.

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

ודוגמה מורכבת יותר, הכוללת דגל מעקב __timeout כך שתוכלו ליירט כל עבודה יקרה.

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

עוזר התקדמות הורדה

התקדמות העלאה כרגע מעט בעייתית מחוץ ל-Chrome.

מטפל ההתקדמות הטכניקה המוצגת להלן נמנעת מעטיפה של קריאת ה-fetch בקלוז’ר. 👍

ל-progressHelper יש את הממשק הבא (המקור זמין להלן)

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_

בואו נסתכל על דוגמת שימוש:

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

מוריד תמונות לשימוש חוזר עשוי להיראות כמו getBlob():

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

דרך אגב, Blob הוא אובייקט בינארי גדול.

חשוב לבחור באחת משתי תבניות השימוש להלן (הן שקולות מבחינה פונקציונלית):

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)

ההעדפה שלי היא Option #1. עם זאת, עיצוב ההיקף שלך עשוי לאלץ אותך להשתמש ב-Option #2.

לבסוף, הנה החלק האחרון של המתכון הזה, progressHelper שלנו:

מקור: 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)
})
}
}
})
)
}
}

קרדיט: תודה מיוחדת לאנת’ום כריס ול-PoC Progress+Fetch המופלא שלו המוצג כאן

עוזר ניסיון חוזר רקורסיבי

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)

טיפול בהפניות 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)

ביטול בקשת 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);
});
}

תאימות

נכון לשנת 2022, ממשק ה-fetch נתמך באופן נרחב בכל הדפדפנים המודרניים ובגרסאות עדכניות יותר של NodeJS v18+.

אם אתה חייב לתמוך ב-IE, תוכל להוסיף polyfill ל-fetch באמצעות החבילה github/fetch (מתוחזקת על ידי צוות מעולה ב-GitHub). אפשר להגיע אפילו עד IE8התוצאות עשויות להשתנות.

גרסאות NodeJS מוקדמות יותר יכולות לנצל את ממשק ה-fetch באמצעות החבילה node-fetch:

Terminal window
npm install node-fetch

לאחר polyfill+node-fetch: תאימות של 99.99%

אנא צייצו אליי אם יש לכם מקרי שימוש נוספים שתרצו לראות. ❤️