أساسيات JavaScript

واجهة Fetch وطلبات HTTP

45 دقيقة الدرس 42 من 60

مقدمة في واجهة Fetch

واجهة Fetch هي واجهة JavaScript حديثة ومدمجة لارسال طلبات HTTP من المتصفح. تم تقديمها لتحل محل كائن XMLHttpRequest (XHR) القديم، وتوفر نهجا انظف قائما على Promise للتواصل عبر الشبكة. Fetch مدعوم في جميع المتصفحات الحديثة وهو الطريقة القياسية للتفاعل مع واجهات REST API وتحميل البيانات من الخوادم وارسال النماذج برمجيا وتنزيل او رفع الملفات في تطبيقات الويب.

على عكس XMLHttpRequest الذي استخدم معالجة الاحداث القائمة على الاستدعاءات الراجعة وكان له واجهة محيرة، تستخدم واجهة Fetch الوعود (Promises) مما يجعلها متوافقة بشكل طبيعي مع صيغة async/await. كل استدعاء لـ fetch() يعيد Promise يتم حله الى كائن Response الذي يوفر طرقا للوصول الى جسم الاستجابة بتنسيقات مختلفة مثل JSON والنص او البيانات الثنائية. خلال هذا الدرس، ستتعلم كل جانب من جوانب واجهة Fetch من طلبات GET البسيطة الى رفع الملفات المعقد ومعالجة الاخطاء والغاء الطلبات وانماط التفاعل مع API في العالم الحقيقي.

صيغة fetch()

دالة fetch() تاخذ معاملين: عنوان URL للطلب وكائن اعدادات اختياري. عند استدعائها بعنوان URL فقط، تنفذ طلب GET بشكل افتراضي. كائن الاعدادات يسمح لك بتحديد طريقة HTTP والترويسات والجسم والوضع وبيانات الاعتماد وخيارات اخرى.

مثال: صيغة fetch() الاساسية

// ابسط شكل: طلب GET بعنوان URL فقط
fetch('https://api.example.com/data');

// الشكل الكامل مع كائن الاعدادات
fetch('https://api.example.com/data', {
    method: 'GET',             // طريقة HTTP
    headers: {                  // ترويسات الطلب
        'Accept': 'application/json',
        'Authorization': 'Bearer token123'
    },
    mode: 'cors',              // وضع CORS
    credentials: 'same-origin', // معالجة ملفات تعريف الارتباط
    cache: 'default',          // وضع التخزين المؤقت
    redirect: 'follow',        // معالجة اعادة التوجيه
    signal: null               // اشارة AbortController
});
ملاحظة: دالة fetch() متاحة عالميا في المتصفح -- لست بحاجة لاستيراد او تثبيت اي شيء. في Node.js، اصبح fetch متاحا اصلا بدءا من الاصدار 18. للاصدارات الاقدم من Node.js، ستحتاج الى مكتبة مثل node-fetch.

كائن Response

عندما يتم حل fetch()، يعيد كائن Response. هذا الكائن يحتوي على جميع المعلومات حول استجابة الخادم بما في ذلك رمز حالة HTTP والترويسات وطرق لقراءة جسم الاستجابة. فهم كائن Response ضروري للتعامل الصحيح مع استجابات API.

مثال: استكشاف كائن Response

async function exploreResponse() {
    const response = await fetch('https://api.example.com/users/1');

    // خصائص Response
    console.log('الحالة:', response.status);          // 200، 404، 500، الخ.
    console.log('نص الحالة:', response.statusText);   // "OK"، "Not Found"، الخ.
    console.log('ناجح:', response.ok);                // true اذا كانت الحالة 200-299
    console.log('العنوان:', response.url);             // العنوان النهائي بعد اعادة التوجيه
    console.log('اعيد توجيهه:', response.redirected); // true اذا تم اعادة التوجيه
    console.log('النوع:', response.type);              // "basic"، "cors"، "opaque"

    // قراءة ترويسات الاستجابة
    console.log('Content-Type:', response.headers.get('Content-Type'));
    console.log('Cache-Control:', response.headers.get('Cache-Control'));

    // التكرار على جميع الترويسات
    for (const [key, value] of response.headers) {
        console.log(key + ': ' + value);
    }

    // التحقق من وجود ترويسة
    console.log('لديه ETag:', response.headers.has('ETag'));
}

خاصية response.ok مهمة للغاية. تكون true عندما يكون رمز حالة HTTP في النطاق 200-299 (رموز النجاح) وfalse لكل شيء اخر (اخطاء العميل مثل 404 واخطاء الخادم مثل 500). هذه هي الطريقة الاساسية للتحقق مما اذا كان الطلب ناجحا قبل قراءة جسم الاستجابة.

طرق جسم الاستجابة

يوفر كائن Response عدة طرق لقراءة جسم الاستجابة. كل طريقة تعيد Promise يتم حله بمحتوى الجسم بتنسيق محدد. يمكنك قراءة الجسم مرة واحدة فقط -- بعد استدعاء احدى هذه الطرق، يتم استهلاك تدفق الجسم ولا يمكن قراءته مرة اخرى الا اذا قمت بنسخ الاستجابة اولا.

مثال: طرق جسم الاستجابة المختلفة

// 1. response.json() -- تحليل كـ JSON (الاكثر شيوعا لـ APIs)
async function getJsonData() {
    const response = await fetch('/api/users');
    const data = await response.json();
    console.log(data); // كائن/مصفوفة JavaScript
}

// 2. response.text() -- قراءة كنص عادي
async function getTextData() {
    const response = await fetch('/api/readme');
    const text = await response.text();
    console.log(text); // سلسلة نصية عادية
}

// 3. response.blob() -- قراءة كـ Blob ثنائي (للملفات والصور)
async function getImageBlob() {
    const response = await fetch('/images/photo.jpg');
    const blob = await response.blob();
    const imageUrl = URL.createObjectURL(blob);
    document.getElementById('preview').src = imageUrl;
}

// 4. response.arrayBuffer() -- قراءة كبيانات ثنائية خام
async function getBinaryData() {
    const response = await fetch('/api/download/file.pdf');
    const buffer = await response.arrayBuffer();
    console.log('تم استلام ' + buffer.byteLength + ' بايت');
}

// 5. response.formData() -- قراءة ككائن FormData
async function getFormData() {
    const response = await fetch('/api/form-response');
    const formData = await response.formData();
    console.log(formData.get('username'));
}

// نسخ استجابة لقراءة الجسم عدة مرات
async function readBodyTwice() {
    const response = await fetch('/api/users');
    const clone = response.clone();

    const text = await response.text();    // قراءة الاصلي كنص
    const json = await clone.json();       // قراءة النسخة كـ JSON
    console.log('طول النص:', text.length);
    console.log('عدد العناصر:', json.length);
}
خطا شائع: محاولة قراءة جسم الاستجابة اكثر من مرة. استدعاء response.json() ثم response.text() على نفس كائن Response سيرمي خطا لان تدفق الجسم قد تم استهلاكه بالفعل. اذا كنت بحاجة لقراءة الجسم بتنسيقات متعددة، استخدم response.clone() قبل القراءة.

طلبات GET

طلبات GET هي اكثر انواع طلبات HTTP شيوعا. تستخدم لاسترداد البيانات من الخادم. بشكل افتراضي، fetch() تنفذ طلب GET لذلك تحتاج فقط لتقديم عنوان URL. طلبات GET يجب الا تعدل البيانات على الخادم -- فهي مصممة لتكون امنة وقابلة للتكرار.

مثال: طلبات GET مع معلمات الاستعلام

// طلب GET بسيط
async function getUsers() {
    const response = await fetch('/api/users');
    const users = await response.json();
    return users;
}

// GET مع معلمات الاستعلام باستخدام ربط النصوص
async function searchUsers(query, page = 1) {
    const response = await fetch(
        '/api/users?search=' + encodeURIComponent(query) + '&page=' + page
    );
    return response.json();
}

// GET مع معلمات الاستعلام باستخدام URLSearchParams (موصى به)
async function searchUsersClean(query, page = 1, limit = 20) {
    const params = new URLSearchParams({
        search: query,
        page: page,
        limit: limit,
        sort: 'name'
    });

    const response = await fetch('/api/users?' + params.toString());
    return response.json();
}

// GET مع ترويسات مخصصة
async function getUsersAuthenticated(token) {
    const response = await fetch('/api/users', {
        headers: {
            'Authorization': 'Bearer ' + token,
            'Accept': 'application/json'
        }
    });

    if (!response.ok) {
        throw new Error('فشل جلب المستخدمين: ' + response.status);
    }

    return response.json();
}
نصيحة احترافية: استخدم دائما URLSearchParams لبناء سلاسل الاستعلام بدلا من ربط النصوص يدويا. تتعامل تلقائيا مع ترميز URL للاحرف الخاصة مما يمنع عناوين URL المكسورة ومشاكل الامان المحتملة. يمكنك ايضا تمريرها مباشرة الى مُنشئ URL لنهج انظف.

طلبات POST

تستخدم طلبات POST لارسال البيانات الى الخادم عادة لانشاء موارد جديدة. مع واجهة Fetch، تحدد method: 'POST' في كائن الاعدادات وتضمن البيانات في خاصية body. التنسيق الاكثر شيوعا لارسال البيانات هو JSON الذي يتطلب تعيين ترويسة Content-Type الى application/json.

مثال: طلب POST بجسم JSON

async function createUser(userData) {
    const response = await fetch('/api/users', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        },
        body: JSON.stringify({
            name: userData.name,
            email: userData.email,
            role: userData.role || 'user'
        })
    });

    if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'فشل انشاء المستخدم');
    }

    const newUser = await response.json();
    console.log('تم انشاء المستخدم بالمعرف:', newUser.id);
    return newUser;
}

// الاستخدام
const user = await createUser({
    name: 'احمد حسن',
    email: 'ahmad@example.com',
    role: 'admin'
});

مثال: طلب POST ببيانات النموذج

// ارسال بيانات النموذج (مفيد لرفع الملفات)
async function submitForm(formElement) {
    const formData = new FormData(formElement);

    const response = await fetch('/api/submit', {
        method: 'POST',
        // لا تعين ترويسة Content-Type -- المتصفح يعينها تلقائيا
        // مع الحدود الصحيحة لـ multipart/form-data
        body: formData
    });

    return response.json();
}

// بناء FormData يدويا
async function uploadProfile(name, email, avatarFile) {
    const formData = new FormData();
    formData.append('name', name);
    formData.append('email', email);
    formData.append('avatar', avatarFile, avatarFile.name);

    const response = await fetch('/api/profile', {
        method: 'POST',
        body: formData
    });

    return response.json();
}
ملاحظة: عند ارسال FormData، لا تعين ترويسة Content-Type يدويا. المتصفح يعينها تلقائيا الى multipart/form-data مع سلسلة الحدود الصحيحة. تعيين الترويسة يدويا سيكسر الطلب لان الحدود لن تتطابق.

طلبات PUT و PATCH و DELETE

PUT تستخدم لاستبدال مورد بالكامل، و PATCH تستخدم لتحديث مورد جزئيا، و DELETE تستخدم لحذف مورد. هذه الطرق تتبع نفس نمط POST لكن بطرق HTTP مختلفة محددة.

مثال: طلبات PUT و PATCH و DELETE

// PUT -- استبدال المورد بالكامل
async function replaceUser(id, userData) {
    const response = await fetch('/api/users/' + id, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData) // يجب تضمين جميع الحقول
    });
    return response.json();
}

// PATCH -- تحديث جزئي للمورد
async function updateUserEmail(id, newEmail) {
    const response = await fetch('/api/users/' + id, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: newEmail }) // الحقل المتغير فقط
    });
    return response.json();
}

// DELETE -- حذف مورد
async function deleteUser(id) {
    const response = await fetch('/api/users/' + id, {
        method: 'DELETE',
        headers: {
            'Authorization': 'Bearer ' + getToken()
        }
    });

    if (!response.ok) {
        throw new Error('فشل حذف المستخدم: ' + response.status);
    }

    // بعض APIs تعيد 204 No Content للحذف الناجح
    if (response.status === 204) {
        return true;
    }

    return response.json();
}

ارسال بيانات JSON

JSON (ترميز كائنات JavaScript) هو تنسيق البيانات الاكثر شيوعا المستخدم في واجهات API الحديثة. عند ارسال بيانات JSON مع fetch، تحتاج لفعل شيئين: تعيين ترويسة Content-Type الى application/json وتحويل كائن JavaScript الى سلسلة JSON باستخدام JSON.stringify().

مثال: ارسال انواع مختلفة من بيانات JSON

// ارسال كائن بسيط
async function createPost(title, content, tags) {
    const response = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title, content, tags })
    });
    return response.json();
}

// ارسال كائنات متداخلة
async function createOrder(items, shippingAddress) {
    const orderData = {
        items: items.map(item => ({
            productId: item.id,
            quantity: item.qty,
            price: item.price
        })),
        shipping: {
            street: shippingAddress.street,
            city: shippingAddress.city,
            country: shippingAddress.country,
            postalCode: shippingAddress.zip
        },
        metadata: {
            source: 'web',
            timestamp: new Date().toISOString()
        }
    };

    const response = await fetch('/api/orders', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        },
        body: JSON.stringify(orderData)
    });

    if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message);
    }

    return response.json();
}

ارسال FormData

يستخدم FormData عندما تحتاج لارسال ملفات ثنائية (صور، مستندات، فيديو) او عندما تريد محاكاة ارسال نموذج HTML تقليدي. واجهة FormData تسمح لك ببناء ازواج مفتاح-قيمة ترسل كـ multipart/form-data وهو نوع الترميز المطلوب لرفع الملفات.

مثال: رفع ملف باستخدام FormData

// رفع ملف واحد
async function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('description', 'تم الرفع عبر تطبيق الويب');

    const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
        // بدون ترويسة Content-Type! المتصفح يعينها تلقائيا.
    });

    if (!response.ok) {
        throw new Error('فشل الرفع: ' + response.status);
    }

    return response.json();
}

// رفع ملفات متعددة
async function uploadMultipleFiles(files) {
    const formData = new FormData();

    for (let i = 0; i < files.length; i++) {
        formData.append('files[]', files[i], files[i].name);
    }

    const response = await fetch('/api/upload/batch', {
        method: 'POST',
        body: formData
    });

    return response.json();
}

// من عنصر نموذج HTML
async function submitContactForm() {
    const form = document.getElementById('contactForm');
    const formData = new FormData(form);

    // يمكنك اضافة حقول اضافية برمجيا
    formData.append('submittedAt', new Date().toISOString());
    formData.append('userAgent', navigator.userAgent);

    const response = await fetch('/api/contact', {
        method: 'POST',
        body: formData
    });

    if (response.ok) {
        form.reset();
        alert('تم ارسال الرسالة بنجاح!');
    }
}

معالجة الاخطاء: اخطاء الشبكة مقابل اخطاء HTTP

احد اهم الاشياء التي يجب فهمها حول واجهة Fetch هي كيفية معالجتها للاخطاء. Promise الخاص بـ fetch() يرفض فقط عند اخطاء الشبكة -- عندما لا يستطيع المتصفح الوصول الى الخادم على الاطلاق (لا اتصال بالانترنت، فشل DNS، حظر CORS، الخادم معطل). هو لا يرفض عند رموز حالة HTTP الخاطئة مثل 404 (غير موجود) او 500 (خطا داخلي في الخادم). يجب عليك التحقق من response.ok او response.status يدويا لاكتشاف اخطاء HTTP.

مثال: معالجة صحيحة للاخطاء

async function fetchData(url) {
    try {
        const response = await fetch(url);

        // التحقق من اخطاء HTTP (fetch لا يرمي خطا لـ 4xx/5xx)
        if (!response.ok) {
            // محاولة قراءة رسالة الخطا من جسم الاستجابة
            let errorMessage = 'خطا HTTP: ' + response.status;
            try {
                const errorBody = await response.json();
                errorMessage = errorBody.message || errorMessage;
            } catch (parseError) {
                // جسم الاستجابة لم يكن JSON، استخدم الرسالة الافتراضية
            }
            throw new Error(errorMessage);
        }

        return await response.json();

    } catch (error) {
        // هذا يلتقط كلا من اخطاء الشبكة واخطاء HTTP التي رميناها
        if (error instanceof TypeError) {
            // خطا شبكة: لا اتصال، فشل DNS، حظر CORS
            console.error('خطا في الشبكة:', error.message);
            throw new Error('تعذر الاتصال بالخادم. تحقق من اتصالك بالانترنت.');
        }

        // اعادة رمي اخطاء HTTP والاخطاء الاخرى
        throw error;
    }
}

// غلاف قابل لاعادة الاستخدام يوحد معالجة الاخطاء
async function apiFetch(url, options = {}) {
    const defaultOptions = {
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            ...options.headers
        },
        ...options
    };

    try {
        const response = await fetch(url, defaultOptions);

        if (!response.ok) {
            const error = new Error('خطا API');
            error.status = response.status;
            error.statusText = response.statusText;

            try {
                error.data = await response.json();
            } catch (e) {
                error.data = null;
            }

            throw error;
        }

        // معالجة 204 No Content
        if (response.status === 204) {
            return null;
        }

        return response.json();

    } catch (error) {
        if (!error.status) {
            // هذا خطا شبكة وليس خطا HTTP
            error.status = 0;
            error.message = 'خطا شبكة: ' + error.message;
        }
        throw error;
    }
}
خطا شائع: افتراض ان fetch() سيرمي خطا لاستجابة 404 او 500. لن يفعل ذلك. Promise من fetch() يرفض فقط لفشل الشبكة. 404 Not Found او 500 Internal Server Error لا تزال استجابة HTTP ناجحة من منظور المتصفح. تحقق دائما من response.ok قبل معالجة جسم الاستجابة.

AbortController لالغاء الطلبات

واجهة AbortController تسمح لك بالغاء طلبات fetch الجارية. هذا ضروري لسيناريوهات مثل البحث اثناء الكتابة (حيث تريد الغاء عمليات البحث السابقة عندما يكتب المستخدم احرفا جديدة) والتنقل بعيدا عن صفحة بينما لا تزال البيانات تحمل او تنفيذ مهلات زمنية للطلبات.

مثال: الغاء طلب Fetch

// مثال الغاء اساسي
const controller = new AbortController();
const signal = controller.signal;

fetch('/api/large-dataset', { signal })
    .then(response => response.json())
    .then(data => console.log('تم استلام البيانات:', data))
    .catch(error => {
        if (error.name === 'AbortError') {
            console.log('تم الغاء الطلب');
        } else {
            console.error('خطا Fetch:', error);
        }
    });

// الغاء الطلب بعد 3 ثوان
setTimeout(() => controller.abort(), 3000);

// تنفيذ غلاف المهلة الزمنية
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal
        });
        return response;
    } catch (error) {
        if (error.name === 'AbortError') {
            throw new Error('انتهت مهلة الطلب بعد ' + timeoutMs + ' مللي ثانية');
        }
        throw error;
    } finally {
        clearTimeout(timeoutId);
    }
}

مثال: البحث اثناء الكتابة مع الالغاء

class SearchController {
    constructor() {
        this.currentController = null;
    }

    async search(query) {
        // الغاء اي طلب بحث سابق
        if (this.currentController) {
            this.currentController.abort();
        }

        // انشاء متحكم جديد لهذا البحث
        this.currentController = new AbortController();

        try {
            const params = new URLSearchParams({ q: query, limit: 10 });
            const response = await fetch('/api/search?' + params, {
                signal: this.currentController.signal
            });

            if (!response.ok) {
                throw new Error('فشل البحث');
            }

            const results = await response.json();
            return results;

        } catch (error) {
            if (error.name === 'AbortError') {
                // هذا متوقع عندما يلغي بحث جديد البحث القديم
                console.log('تم الغاء البحث السابق');
                return null;
            }
            throw error;
        }
    }
}

// الاستخدام مع حقل بحث
const searcher = new SearchController();
const searchInput = document.getElementById('search');

searchInput.addEventListener('input', async (event) => {
    const query = event.target.value.trim();
    if (query.length < 2) return;

    const results = await searcher.search(query);
    if (results) {
        displayResults(results);
    }
});

Fetch مع Async/Await

دمج fetch مع async/await ينشئ كودا نظيفا وقابلا للقراءة للتفاعلات المعقدة مع API. اليك نمطا شاملا يجمع كل ما تعلمناه حتى الان.

مثال: فئة خدمة API كاملة

class ApiService {
    constructor(baseUrl, token = null) {
        this.baseUrl = baseUrl;
        this.token = token;
    }

    getHeaders(customHeaders = {}) {
        const headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            ...customHeaders
        };

        if (this.token) {
            headers['Authorization'] = 'Bearer ' + this.token;
        }

        return headers;
    }

    async request(endpoint, options = {}) {
        const url = this.baseUrl + endpoint;
        const config = {
            headers: this.getHeaders(options.headers),
            ...options
        };

        // حذف Content-Type لـ FormData (المتصفح يعينها)
        if (options.body instanceof FormData) {
            delete config.headers['Content-Type'];
        }

        const response = await fetch(url, config);

        if (!response.ok) {
            const error = new Error('فشل طلب API');
            error.status = response.status;
            try {
                error.data = await response.json();
            } catch (e) {
                error.data = { message: response.statusText };
            }
            throw error;
        }

        if (response.status === 204) return null;
        return response.json();
    }

    async get(endpoint, params = {}) {
        const query = new URLSearchParams(params).toString();
        const url = query ? endpoint + '?' + query : endpoint;
        return this.request(url);
    }

    async post(endpoint, data) {
        return this.request(endpoint, {
            method: 'POST',
            body: data instanceof FormData ? data : JSON.stringify(data)
        });
    }

    async put(endpoint, data) {
        return this.request(endpoint, {
            method: 'PUT',
            body: JSON.stringify(data)
        });
    }

    async patch(endpoint, data) {
        return this.request(endpoint, {
            method: 'PATCH',
            body: JSON.stringify(data)
        });
    }

    async delete(endpoint) {
        return this.request(endpoint, { method: 'DELETE' });
    }
}

// الاستخدام
const api = new ApiService('https://api.example.com', 'my-auth-token');

async function main() {
    // GET جميع المستخدمين
    const users = await api.get('/users', { page: 1, limit: 20 });

    // POST مستخدم جديد
    const newUser = await api.post('/users', {
        name: 'فاطمة',
        email: 'fatima@example.com'
    });

    // PATCH تحديث
    await api.patch('/users/' + newUser.id, { role: 'editor' });

    // DELETE حذف
    await api.delete('/users/' + newUser.id);
}

ترويسات الطلب

ترويسات HTTP تسمح لك بارسال معلومات اضافية مع طلبك. تتحكم في المصادقة وتنسيق المحتوى والتخزين المؤقت والمزيد. اليك الترويسات الاكثر استخداما في تطوير الويب.

مثال: ترويسات الطلب الشائعة

async function demonstrateHeaders() {
    const response = await fetch('/api/data', {
        headers: {
            // المصادقة -- الانماط الاكثر شيوعا
            'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...',
            // او للمصادقة الاساسية:
            // 'Authorization': 'Basic ' + btoa('username:password'),

            // التفاوض على المحتوى
            'Accept': 'application/json',         // التنسيق المطلوب
            'Content-Type': 'application/json',    // التنسيق المرسل
            'Accept-Language': 'ar,en;q=0.9',      // اللغة المفضلة

            // ترويسات مخصصة (عادة مسبوقة بـ X-)
            'X-Request-ID': 'req-' + Date.now(),
            'X-Client-Version': '2.1.0',

            // التخزين المؤقت
            'Cache-Control': 'no-cache',
            'If-None-Match': 'W/"abc123"',         // ETag للطلبات الشرطية

            // الامان
            'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
        }
    });

    return response.json();
}
نصيحة احترافية: عند العمل مع واجهات API التي تتطلب حماية CSRF (مثل Laravel)، ضمن رمز CSRF من علامة meta في صفحتك. Laravel يتوقع الرمز في ترويسة X-CSRF-TOKEN او ترويسة X-XSRF-TOKEN. يمكنك قراءته من ملف تعريف الارتباط او علامة meta التي يضمنها Laravel في تخطيط الصفحة.

اساسيات CORS

CORS (مشاركة الموارد عبر الاصول) هي الية امان تتحكم في المواقع التي يمكنها ارسال طلبات الى خادم معين. عندما يحاول كود JavaScript على https://mysite.com جلب بيانات من https://api.other.com، يفرض المتصفح قواعد CORS. يجب ان يتضمن الخادم ترويسات محددة في استجابته للسماح بالطلبات عبر الاصول.

مثال: CORS وخيارات وضع Fetch

// السلوك الافتراضي: وضع cors
// المتصفح يرسل طلب OPTIONS تمهيدي للطلبات غير البسيطة
async function crossOriginRequest() {
    const response = await fetch('https://api.external.com/data', {
        mode: 'cors',         // الافتراضي -- يفرض CORS
        credentials: 'include' // ارسال ملفات تعريف الارتباط مع الطلبات عبر الاصول
    });
    return response.json();
}

// طلب من نفس الاصل (افتراضي لنفس النطاق)
async function sameOriginRequest() {
    const response = await fetch('/api/data', {
        mode: 'same-origin'   // فشل اذا لم يكن نفس الاصل
    });
    return response.json();
}

// معالجة اخطاء CORS بشكل جيد
async function safeCrossOrigin(url) {
    try {
        const response = await fetch(url);
        return await response.json();
    } catch (error) {
        if (error instanceof TypeError &&
            error.message.includes('Failed to fetch')) {
            console.error(
                'خطا CORS او فشل شبكة. ' +
                'الخادم على ' + url + ' قد لا يسمح ' +
                'بالطلبات من هذا الاصل.'
            );
        }
        throw error;
    }
}
ملاحظة: CORS يفرضه المتصفح وليس الخادم. الخادم يعين ترويسات CORS (مثل Access-Control-Allow-Origin) في استجابته والمتصفح يقرر ما اذا كان سيسمح لـ JavaScript بالوصول الى الاستجابة. اذا كنت تتحكم بالخادم، يمكنك اعداده للسماح بالطلبات من نطاق الواجهة الامامية. اخطاء CORS لا تحدث في الطلبات من خادم الى خادم لان CORS هو ميزة امان خاصة بالمتصفح فقط.

امثلة تفاعل API من العالم الحقيقي

دعنا نلقي نظرة على امثلة كاملة بجودة انتاجية توضح تفاعلات API الشائعة في العالم الحقيقي.

المثال 1: تحميل البيانات المقسمة لصفحات

مثال: تحميل بيانات مقسمة لصفحات

class PaginatedList {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
        this.items = [];
        this.currentPage = 0;
        this.totalPages = 1;
        this.loading = false;
        this.hasMore = true;
    }

    async loadNextPage() {
        if (this.loading || !this.hasMore) return;

        this.loading = true;
        const nextPage = this.currentPage + 1;

        try {
            const params = new URLSearchParams({
                page: nextPage,
                per_page: 20
            });

            const response = await fetch(this.baseUrl + '?' + params);
            if (!response.ok) throw new Error('فشل تحميل الصفحة ' + nextPage);

            const data = await response.json();
            this.items = this.items.concat(data.items);
            this.currentPage = data.currentPage;
            this.totalPages = data.totalPages;
            this.hasMore = this.currentPage < this.totalPages;

            return data.items;

        } catch (error) {
            console.error('خطا في التصفح:', error.message);
            throw error;
        } finally {
            this.loading = false;
        }
    }

    async loadAll() {
        while (this.hasMore) {
            await this.loadNextPage();
        }
        return this.items;
    }
}

// الاستخدام مع التمرير اللانهائي
const productList = new PaginatedList('/api/products');

window.addEventListener('scroll', async () => {
    const nearBottom = window.innerHeight + window.scrollY
        >= document.body.offsetHeight - 500;

    if (nearBottom && productList.hasMore && !productList.loading) {
        const newItems = await productList.loadNextPage();
        if (newItems) {
            renderProducts(newItems);
        }
    }
});

المثال 2: تطبيق CRUD كامل

مثال: ادارة المهام CRUD

class TaskAPI {
    constructor() {
        this.baseUrl = '/api/tasks';
        this.token = localStorage.getItem('authToken');
    }

    getHeaders() {
        return {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': 'Bearer ' + this.token
        };
    }

    // انشاء
    async createTask(title, description, dueDate) {
        const response = await fetch(this.baseUrl, {
            method: 'POST',
            headers: this.getHeaders(),
            body: JSON.stringify({
                title: title,
                description: description,
                due_date: dueDate,
                status: 'pending'
            })
        });

        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.message || 'فشل انشاء المهمة');
        }

        return response.json();
    }

    // قراءة (قائمة)
    async getTasks(filters = {}) {
        const params = new URLSearchParams();
        if (filters.status) params.append('status', filters.status);
        if (filters.sort) params.append('sort', filters.sort);
        if (filters.page) params.append('page', filters.page);

        const url = this.baseUrl + (params.toString() ? '?' + params : '');
        const response = await fetch(url, {
            headers: this.getHeaders()
        });

        if (!response.ok) throw new Error('فشل تحميل المهام');
        return response.json();
    }

    // قراءة (مفرد)
    async getTask(id) {
        const response = await fetch(this.baseUrl + '/' + id, {
            headers: this.getHeaders()
        });

        if (response.status === 404) {
            throw new Error('المهمة غير موجودة');
        }
        if (!response.ok) throw new Error('فشل تحميل المهمة');
        return response.json();
    }

    // تحديث
    async updateTask(id, updates) {
        const response = await fetch(this.baseUrl + '/' + id, {
            method: 'PATCH',
            headers: this.getHeaders(),
            body: JSON.stringify(updates)
        });

        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.message || 'فشل تحديث المهمة');
        }

        return response.json();
    }

    // حذف
    async deleteTask(id) {
        const response = await fetch(this.baseUrl + '/' + id, {
            method: 'DELETE',
            headers: this.getHeaders()
        });

        if (!response.ok) throw new Error('فشل حذف المهمة');
        return response.status === 204 ? true : response.json();
    }

    // عمليات دفعية
    async markMultipleComplete(taskIds) {
        const response = await fetch(this.baseUrl + '/batch-update', {
            method: 'PATCH',
            headers: this.getHeaders(),
            body: JSON.stringify({
                ids: taskIds,
                updates: { status: 'completed' }
            })
        });

        if (!response.ok) throw new Error('فشل التحديث الدفعي');
        return response.json();
    }
}

// استخدام TaskAPI
const taskApi = new TaskAPI();

async function initTaskManager() {
    try {
        // تحميل جميع المهام المعلقة
        const tasks = await taskApi.getTasks({
            status: 'pending',
            sort: 'due_date'
        });
        renderTaskList(tasks);

        // انشاء مهمة جديدة
        const newTask = await taskApi.createTask(
            'مراجعة طلبات السحب',
            'مراجعة طلبات السحب الثلاثة المعلقة على المستودع الرئيسي',
            '2025-06-15'
        );
        console.log('تم انشاء المهمة:', newTask.id);

        // تحديث المهمة
        await taskApi.updateTask(newTask.id, {
            status: 'in_progress',
            priority: 'high'
        });

        // حذف مهمة
        await taskApi.deleteTask(newTask.id);

    } catch (error) {
        console.error('خطا مدير المهام:', error.message);
    }
}

المثال 3: التعامل مع انتهاء المصادقة

مثال: تحديث تلقائي للرمز عند 401

class AuthenticatedFetch {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
        this.token = localStorage.getItem('accessToken');
        this.refreshToken = localStorage.getItem('refreshToken');
        this.refreshing = null; // يحتفظ بوعد التحديث لتجنب التكرار
    }

    async fetch(endpoint, options = {}) {
        const url = this.baseUrl + endpoint;
        const config = {
            ...options,
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + this.token,
                ...options.headers
            }
        };

        let response = await fetch(url, config);

        // اذا كان غير مصرح، حاول تحديث الرمز
        if (response.status === 401 && this.refreshToken) {
            await this.doRefresh();

            // اعادة محاولة الطلب الاصلي بالرمز الجديد
            config.headers['Authorization'] = 'Bearer ' + this.token;
            response = await fetch(url, config);
        }

        if (!response.ok) {
            throw new Error('فشل الطلب: ' + response.status);
        }

        if (response.status === 204) return null;
        return response.json();
    }

    async doRefresh() {
        // اذا كان التحديث جاريا بالفعل، انتظر اكتماله
        if (this.refreshing) {
            return this.refreshing;
        }

        this.refreshing = (async () => {
            try {
                const response = await fetch(this.baseUrl + '/auth/refresh', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ refresh_token: this.refreshToken })
                });

                if (!response.ok) {
                    throw new Error('فشل التحديث');
                }

                const data = await response.json();
                this.token = data.access_token;
                this.refreshToken = data.refresh_token;
                localStorage.setItem('accessToken', this.token);
                localStorage.setItem('refreshToken', this.refreshToken);

            } catch (error) {
                // فشل التحديث -- المستخدم يحتاج لتسجيل الدخول مرة اخرى
                localStorage.removeItem('accessToken');
                localStorage.removeItem('refreshToken');
                window.location.href = '/login';
                throw error;
            } finally {
                this.refreshing = null;
            }
        })();

        return this.refreshing;
    }
}

// الاستخدام
const api = new AuthenticatedFetch('https://api.example.com');

async function loadUserData() {
    const profile = await api.fetch('/profile');
    const orders = await api.fetch('/orders?limit=5');
    return { profile, orders };
}

افضل ممارسات اعدادات Fetch

اليك ملخص لانماط الاعدادات الموصى بها لانواع مختلفة من الطلبات.

مثال: مرجع الاعدادات

// قراءة البيانات (GET)
fetch(url, {
    method: 'GET',
    headers: { 'Accept': 'application/json' },
    credentials: 'same-origin'
});

// ارسال JSON (POST/PUT/PATCH)
fetch(url, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    },
    body: JSON.stringify(data)
});

// رفع الملفات
fetch(url, {
    method: 'POST',
    // بدون ترويسة Content-Type لـ FormData!
    body: formData
});

// طلب مصادق
fetch(url, {
    headers: { 'Authorization': 'Bearer ' + token }
});

// طلب مع مهلة زمنية
const controller = new AbortController();
setTimeout(() => controller.abort(), 10000);
fetch(url, { signal: controller.signal });

// عبر الاصول مع ملفات تعريف الارتباط
fetch(url, {
    mode: 'cors',
    credentials: 'include'
});

تمرين عملي

قم ببناء تطبيق عميل مدونة كامل باستخدام واجهة Fetch و async/await. استخدم API مجاني مثل JSONPlaceholder (https://jsonplaceholder.typicode.com) كخلفية لك. انشئ فئة BlogService مع دوال لجميع عمليات CRUD: getPosts(page, limit) لجلب المنشورات المقسمة لصفحات وgetPost(id) لجلب منشور واحد وcreatePost(title, body) لانشاء منشور جديد وupdatePost(id, updates) لتحديث منشور باستخدام PATCH وdeletePost(id) لحذف منشور. اضف دالة getPostWithComments(id) تستخدم Promise.all() لجلب المنشور وتعليقاته بالتوازي. نفذ معالجة اخطاء صحيحة تميز بين اخطاء الشبكة واخطاء HTTP. اضف دالة بحث قائمة على AbortController تلغي طلبات البحث السابقة عند اجراء بحث جديد. اضف دالة مساعدة fetchWithRetry() تعيد محاولة الطلبات الفاشلة حتى 3 مرات مع تراجع اسي. اكتب دالة downloadPostsAsJson() تجلب جميع المنشورات وتنشئ ملف Blob قابل للتنزيل. اختبر كل دالة وتحقق من ان رسائل اخطاء الشبكة مختلفة عن رسائل اخطاء HTTP.