כשאנו מבצעים קריאות fetch, יש דרך מהירה ומעט בעייתית לעבוד ויש דרך טובה. אני אתן דוגמה של דרך בעייתית לעבוד. הנה קומפוננטה שבה אני לוחץ על כפתור ומקבל בדיחת אבא. השתמשתי פה לא ב-fetch אלא ב-axios. ספריה אולטרא פופולרית לניהול קריאות מהשרת. אני מאמין שהקוד הזה יהיה מאוד מובן למי שמכיר את ריאקט.
// DadJoke.tsx
import { useState } from 'react';
import axios from 'axios';
const DAD_JOKE_API_URL = 'https://icanhazdadjoke.com/';
const DadJoke: React.FC = () => {
const [joke, setJoke] = useState<string | null>(null);
const fetchJoke = async () => {
try {
const response = await axios.get(DAD_JOKE_API_URL, {
headers: {
'Accept': 'application/json'
}
});
setJoke(response.data.joke);
} catch (error) {
console.error("Error fetching dad joke", error);
setJoke("Oops! Couldn't fetch a dad joke right now.");
}
};
return (
<div>
<button onClick={fetchJoke}>Get a Dad Joke</button>
<div>
{joke}
</div>
</div>
);
}
export default DadJoke;
טוב, זה אחלה. אבל מה אם אני אצטרך לכתוב קומפוננטה אחרת? כזו שקוראת לאותו API? ומה יקרה כשיהיו לי 10 קומפוננטות כאלו ואז בדיוק ה-API ישתנה לי? זה קצת בלגן. בגלל זה אנו בוחרים בדרך כלל לבנות קוד מסודר יותר וליצור פונקציה שמייצרת instance של axios client וכל מי שצריך קורא לה. מדובר בעצם בארכיטקטורה קטנה ונחמדה שבה אנו מפצלים את הקוד – לקוד שמבצע קריאה שכל קומפוננטה יכולה לקרוא לו. מדובר באמת במשהו פשוט. הקוד שמבצע את הקריאה נקרא service והקוד שלו הוא קוד טהור שמבצע קריאה.
הנה ה-service:
// jokeService.ts
import axios from 'axios';
const DAD_JOKE_API_URL = 'https://icanhazdadjoke.com/';
export const fetchDadJoke = async () => {
try {
const response = await axios.get(DAD_JOKE_API_URL, {
headers: {
'Accept': 'application/json'
}
});
return response.data.joke;
} catch (error) {
console.error("Error fetching dad joke", error);
throw new Error("Couldn't fetch a dad joke right now.");
}
};
והנה הקומפוננטה שקוראת ל-service. הלוגיקה של הקריאה היא פשוטה כי אני רק קורא אליו:
// DadJoke.tsx
import { useState } from 'react';
import { fetchDadJoke } from '../services/jokeService';
const DadJoke:React.FC = () => {
const [joke, setJoke] = useState<string | null>(null);
const handleFetchJoke = async () => {
try {
const newJoke = await fetchDadJoke();
setJoke(newJoke);
} catch (error: any) {
setJoke(error.message);
}
};
return (
<div>
<button onClick={handleFetchJoke}>Get a Dad Joke</button>
<div>
{joke}
</div>
</div>
);
}
export default DadJoke;
זו דרך טובה לעבוד עם axios. אבל עכשיו נשאלת השאלה – מה קורה אם יש תקלה ואני רוצה להקפיץ הודעה למשתמש? או אם אנו רוצים לבצע לוגינג? או אפילו לבצע קריאה אחרת?
במקרה הזה, יש לנו את axios interceptors – פונקציות שמופעלות לפני ו/או אחרי כל קריאה באופן גלובלי שכל השירותים של axios יכולים להשתמש בהן. אני יכול להגדיר פונקציה כזו שתפעל אחרי כל קריאה ותכנס לפעולה אם יש שגיאה או תקלה כלשהי. אם אין שגיאה – היא מעבירה את ה-response הלאה. יש? אני יכול להגדיר לה ליצור למשל אלמנט <dialog> שיקפיץ הודעה למשתמש.
איך זה עובד? אנו יוצרים קובץ בשם axiosConfig.ts ובו ה-instance של ה-axios נוצר. אחרי כן אנו מצמידים אליו interceptors- אפשר ל-request ואפשר ל-response. במקרה שלנו זה ל-response. אני יכול לבחור שם מה עושים אם יש קריאה תקינה (במקרה שלנו כלום אבל אפשר להציג אולי איזשהו סטטוס) ולבחור מה עושים במקרה של שגיאה – במקרה הזה קוד שמציג dialog.
// axiosConfig.ts
import axios from 'axios';
const instance = axios.create();
instance.interceptors.response.use(
response => response,
error => {
// Displaying the dialog for the error
const dialog = document.createElement('dialog');
dialog.textContent = error.message || 'Something went wrong!';
document.body.appendChild(dialog);
// Provide a way to close the dialog after a few seconds or on click
setTimeout(() => {
dialog.close();
dialog.remove();
}, 5000);
dialog.addEventListener('click', () => {
dialog.close();
dialog.remove();
});
dialog.showModal();
return Promise.reject(error);
}
);
export default instance;
את זה אני שם בתיקיה בפרויקט לפי בחירתי. ב-service אני יכול לבחור להשתמש בו באמצעות import ממנו ולא מה-axios הרגיל:
// jokeService.ts
import axios from '../axiosConfig'; // <-THIS WAS CHANGED
const DAD_JOKE_API_URL = 'https://icanhazdadjoke.com/';
export const fetchDadJoke = async () => {
try {
const response = await axios.get(DAD_JOKE_API_URL, {
headers: {
'Accept': 'application/json'
}
});
return response.data.joke;
} catch (error) {
// Since the interceptor will handle displaying errors, you can just propagate the error here
throw error;
}
};
ואם תהיה שגיאה אני אקבל תצוגה של בעיה.
שדרוג עם axios config ב-Vite
אפשר לקחת את זה קדימה – במיוחד אם אתם משתמשים ב-vite – אני מניח שאתם מכירים אותו. הבעיה המרכזית היא שאני, כמפתח, צריך לזכור לעשות import כך:
import axios from '../axiosConfig';
האם אנחנו יכולים לגרום לכך שה-interceptors יעבדו לנו אוטומטית עם import axios from 'axios? התשובה היא כן. אם אנו משתמשים ב-Vite זה אפילו קל (קל זה לא אומר שאנחנו רוצים לעשות את זה כמובן).
על מנת לעשות את זה, אנחנו צריכים להשתמש במערכת ה-alias החזקה של Vite שמאפשרת לנו ליצור מודולים ״מדומים״. במקרה הזה, אם אני אכנס ל-vite.config.ts, אני אוכל ליצור alias ל-axios שיפנה ל-axiosConfig.ts שלי.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
resolve: {
alias: {
'axios': '/src/axiosConfig.ts'
}
},
plugins: [react()],
})
השינוי היחידי שאני אצטרך לעשות ב-axiosConfig.ts זה לצרוך את axios ישירות מה-node_modules כי אם אני אכניס לשם את axios ה-alias יפעל ותהיה לי הפניה מעגלית. כך הוא יראה:
// axiosConfig.ts
import axios from '../node_modules/axios/index.js'; //<-THIS IS THE CHANGE
const instance = axios.create();
instance.interceptors.response.use(
response => response,
error => {
// Displaying the dialog for the error
const dialog = document.createElement('dialog');
dialog.textContent = error.message || 'Something went wrong!';
document.body.appendChild(dialog);
// Provide a way to close the dialog after a few seconds or on click
setTimeout(() => {
dialog.close();
dialog.remove();
}, 5000);
dialog.addEventListener('click', () => {
dialog.close();
dialog.remove();
});
dialog.showModal();
return Promise.reject(error);
}
);
export default instance;
הבעיה היא שזה הופך את התשתית ללא שקופה עבור המתכנתים שמשתמשים בה. כלומר זה נוח ומקבלים דברים out of the box – ובמקרה הזה למשל תצוגה נאה של שגיאה. אבל זה אומר שאם יש בעיות צריך באמת לדעת איפה ה-axios נמצא. גם אנחנו נהיה בסיכון של הפניות מעגליות אז פתרון טוב הוא ליצור alias דומה כמו axiosimproved או משהו בסגנון. תלוי בכם ובסגנון האישי.
לסיכום, כשאנחנו יוצאים מעולם ה-Hello World לאפליקציות יותר מורכבות, כדאי להשקיע קצת זמן בתכנון של הפרדה בין הסרוויסים השונים וכדאי להכיר את ה-interceptors שמאפשרים לנו בקלות להוציא הודעות שגיאה גנריות, לוגים ודברים אחרים בכל סרוויס באופן פשוט וקל ובמקום אחד.
5 תגובות
נושא חשוב, זה באמת כלי שמאפשר דברים יפים ואלגנטיים מאוד. לדוגמה להגדיר באופן גלובלי שאם השרת מחזיר קוד שגיאה 401 בתגובה לקריאת HTTP כלשהי, לנתק את הסשן בקליינט, להקפיץ הודעת שגיאה ולהעביר לדף לוגין.
לטעמי, עדיין חסר… לדוגמא: אם נוסיף טיפול במספר קריאות, כמו במקרה אופייני של שליפת 100+ פוסטים מוורדפרס, ואז נדרש ביצוע של כמה קריאות במקביל… ואם vite הוא לא אופציה, לא ברור איך להתקדם.
בקיצור, מפתיע שאין פתרון (service) אמין וקיים. בינתיים המצאתי את הגלגל בעצמי – והוא עובד אבל לא גנרי כמו שהייתי רוצה.
אם יש המלצה למשהו בכיוון שתיארתי יהיה נפלא
לא הבנתי מה הבעיה. ה-axios interceptor הוא סוג של middleware שאמור לעבוד גם עם כמה קריאות במקביל.
הבעיה היא התלות ב vite
כפי שהבנתי, הפתרון לא יתאים למי שאינו משתמש ב vite.
לא, מדובר בפיצ׳ר של axios עצמו. הוא יעבוד היטב בכל סביבה. הנה דוגמה של קוד בונילה שעובד מעולה, הדבק אותו כ-index.html והרץ אותו עם
(הנקודה אחרי ה-serve) ותראה שהוא עובד יופי 🙂