Axios interceptors

תכנון נכון של קריאות AJAX באפליקציה ריאקטית וניהול השגיאות או ההצלחות עם פיצ׳ר נחמד של axios

כשאנו מבצעים קריאות 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 שמאפשרים לנו בקלות להוציא הודעות שגיאה גנריות, לוגים ודברים אחרים בכל סרוויס באופן פשוט וקל ובמקום אחד.

פוסטים נוספים שכדאי לקרוא

תמונת תצוגה של מנעול על מחשב
פתרונות ומאמרים על פיתוח אינטרנט

הגנה מפני XSS עם Trusted Types

תכונה ב-CSP שמאפשרת מניעה כמעט הרמטית להתקפות XSS שכל מפתח ווב צריך להכיר וכדאי שיכיר.

גלילה לראש העמוד