אולי לא צריך Axios
Fetch API בא להציל!
ייתכן שלא תזדקק ל-Axios
זו אינה מתקפה על Axios.
אלא, זוהי הסברה לטובת ה-fetch API שהפך לבעל יכולות מרשימות. 🦄
סקירה כללית
מאמר זה הוא אוסף של קטעי קוד fetch “החסרים” ומקרי שימוש נפוצים שהייתי רוצה שיהיה קל יותר למצוא.
מקרה השימוש שלך לא מופיע? יידע אותי ✉️
השוואת תכונות
| fetch | axios | request | |
|---|---|---|---|
| יירוט בקשה ותגובה | ✅ | ✅ | ✅ |
| שינוי נתוני בקשה ותגובה | ✅ | ✅ | ✅ |
| ביטול בקשות | ✅ | ✅ | ❌ |
| המרות אוטומטיות לנתוני JSON | עזרים ידניים | ✅ | ✅ |
| תמיכה בצד הלקוח להגנה מפני XSRF | ✅ | ✅ | ✅ |
| התקדמות | ✅ | ✅ | ✅ |
| סטרימינג … |
| fetch | axios | request | |
|---|---|---|---|
| יירוט בקשות ותגובות | ✅ | ✅ | ✅ |
| שינוי נתוני בקשה ותגובה | ✅ | ✅ | ✅ |
| ביטול בקשות | ✅ | ✅ | ❌ |
| המרות אוטומטיות לנתוני 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
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))כותרות מותאמות אישית
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
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 קובעת אם העוגיות שלך נכללות אוטומטית.
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
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>
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.
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))}העלאת קובץ
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:
<input type='file' multiple class='files' name='files' />לאחר מכן השתמשו במשהו כמו:
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 שסופקה, היא תמשיך לרוץ – ולכשלים יש נטייה ליצור דליפות זיכרון ארוכות טווח.
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 כך שתוכלו ליירט כל עבודה יקרה.
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 יש את הממשק הבא (המקור זמין להלן)
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_בואו נסתכל על דוגמת שימוש:
// 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():
const getBlob = url => fetch(url) .then(progressHelper(console.log)) // progressHelper used inside the .then() .then(response => response.blob())דרך אגב, Blob הוא אובייקט בינארי גדול.
חשוב לבחור באחת משתי תבניות השימוש להלן (הן שקולות מבחינה פונקציונלית):
// 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)ההעדפה שלי היא Option #1. עם זאת, עיצוב ההיקף שלך עשוי לאלץ אותך להשתמש ב-Option #2.
לבסוף, הנה החלק האחרון של המתכון הזה, progressHelper שלנו:
מקור: 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) }) } } }) ) }}קרדיט: תודה מיוחדת לאנת’ום כריס ול-PoC Progress+Fetch המופלא שלו המוצג כאן
עוזר ניסיון חוזר רקורסיבי
/** * 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)טיפול בהפניות 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)ביטול בקשת 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); });}תאימות
נכון לשנת 2022, ממשק ה-fetch נתמך באופן נרחב בכל הדפדפנים המודרניים ובגרסאות עדכניות יותר של NodeJS v18+.
אם אתה חייב לתמוך ב-IE, תוכל להוסיף polyfill ל-fetch באמצעות החבילה github/fetch (מתוחזקת על ידי צוות מעולה ב-GitHub). אפשר להגיע אפילו עד IE8 – התוצאות עשויות להשתנות.
גרסאות NodeJS מוקדמות יותר יכולות לנצל את ממשק ה-fetch באמצעות החבילה node-fetch:
npm install node-fetchלאחר polyfill+node-fetch: תאימות של 99.99% ✅
אנא צייצו אליי אם יש לכם מקרי שימוש נוספים שתרצו לראות. ❤️