8049 words
40 minutes
Mastering XHR in JavaScript

مقدمة: ليه XHR مهم؟ 🕸️#

في عالم تطوير الويب، كان فيه زمان مشكلة كبيرة: إزاي نجيب بيانات من السيرفر من غير ما نعمل تحديث كامل للصفحة (reload)؟ تخيل إنك في التسعينات، عايز تعرض بيانات جديدة زي قايمة منتجات أو رسايل جديدة، بس لازم تعمل refresh للصفحة كلها، وده كان بيخلّي تجربة المستخدم بطيئة ومزعجة. هنا جات فكرة XMLHttpRequest (اختصارًا XHR)، اللي غيّرت قواعد اللعبة وخلّت المواقع تفاعلية أكتر.

في المقالة دي، هناخدك في رحلة كاملة من أول تاريخ XHR وإزاي اتطورت، لحد إزاي تستخدمها في مشاريعك بشكل احترافي. هنبدأ بالتاريخ، وبعدين هشرح التقنية بالتفصيل، هديلك أمثلة عملية، وأخيرًا نقارنها بـ Fetch API. كل سكشن هيبقى متصل باللي قبله، عشان تفهم الصورة الكبيرة 🚀

التاريخ: إزاي بدأت فكرة XHR؟ 🕰️#

المشكلة الأساسية (قبل 2000) 😓#

في التسعينات، مواقع الويب كانت “ثابتة” (static). لو عايز تجيب بيانات من السيرفر، كان لازم تعمل تحديث كامل للصفحة. المشكلة كانت:

  1. عايز تجيب بيانات؟ لازم تعمل reload.
  2. تجربة المستخدم كانت بطيئة ومش مريحة.
  3. مكانش فيه طريقة تتواصل مع السيرفر من غير refresh.
  4. الكود كان معقد، زي كده:
// لازم تعمل form submission كامل
document.querySelector('form').submit();
// أو تعمل page redirect
window.location.href = '/get-data';

الطريقة دي كانت بتبطّئ كل حاجة، وكانت بتحتاج وقت ومجهود عشان تعرض أي تحديث بسيط. هنا بدأت الحاجة لتقنية زي XHR.

البداية: اختراع XMLHTTP في Microsoft (1998) 🎯#

في 1998، فريق Internet Explorer في Microsoft قرر يحل المشكلة دي. صمموا XMLHTTP كـ ActiveX Control ضمن مكتبة MSXML (Microsoft XML Core Services). الهدف كان:

  • تواصل مع السيرفر: يسمحوا للمتصفح يبعت طلبات HTTP ويستقبل الردود بدون reload.
  • دعم XML: لأن XML كانت الصيغة القياسية للبيانات وقتها.
  • غير متزامن (asynchronous): الطلب يترسل في الخلفية من غير ما يوقف الصفحة.

إيه هو XMLHTTP؟ 🤔#

XMLHTTP هو اسم ActiveX Control صممته Microsoft عشان يخلّي المتصفح يبعت طلبات HTTP (زي GET أو POST) ويستقبل الردود بدون تحديث الصفحة. يعني ببساطة، هو أداة بتخلّيك تجيب بيانات (زي قايمة منتجات أو بيانات مستخدم) وتعرضها في الصفحة على طول.

ليه سموها XMLHTTP؟#

الاسم مركب من:

  • XML: لأن التقنية كانت مصممة عشان تتعامل مع بيانات بتنسيق XML (Extensible Markup Language)، اللي كانت شائعة جدًا في التسعينات لتنظيم البيانات.
  • HTTP: لأنها بتستخدم بروتوكول HTTP للتواصل مع السيرفر.

يعني XMLHTTP هي أداة بتبعت طلبات HTTP وتستقبل ردود غالبًا بتنسيق XML.

الكود كان بيبقى كده:#

var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
        alert(xmlhttp.responseText); // البيانات وصلت
    }
};
xmlhttp.open("GET", "data.xml", true);
xmlhttp.send();
  • new ActiveXObject("Microsoft.XMLHTTP"): بيخلّيك تنشئ كائن XMLHTTP.
  • open: بتحدد نوع الطلب (GET)، الـ URL، وهل هو asynchronous (true).
  • onreadystatechange: بيتابع حالة الطلب.
  • send: بيبعت الطلب للسيرفر.

إيه هو ActiveX Control؟ 🛠️#

ActiveX Control هو مكون برمجي صممته Microsoft عشان يضيف ميزات تفاعلية للمتصفح (زي Internet Explorer). فكر فيه زي “برنامج صغير” بيتركب جوا المتصفح عشان ينفذ مهام زي:

  • تشغيل فيديوهات.
  • عرض رسومات تفاعلية.
  • إرسال طلبات HTTP (زي XMLHTTP).

ActiveX كان جزء من تقنية COM (Component Object Model) اللي Microsoft استخدمتها في Windows. الكود كان كده:

var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
مشاكل ActiveX Controls:#
  • محدود لـ Microsoft: كان بيشتغل بس على Internet Explorer وWindows.
  • مشاكل أمان: كان ممكن يتنفذ كود خبيث على جهاز المستخدم.
  • تعقيد التنصيب: بعض Controls كانت بتحتاج موافقة المستخدم.
  • غير موحد: كل متصفح كان عنده طريقته الخاصة.

إيه هي MSXML؟ 📚#

MSXML (Microsoft XML Core Services) هي مكتبة برمجية بتساعد في معالجة بيانات XML في تطبيقات Windows، بما فيها Internet Explorer. بتوفر أدوات زي:

  • Parsing XML: قراءة وتحليل ملفات XML.
  • Validation: التأكد إن XML مكتوب صح.
  • Transformation: تحويل XML لتنسيقات تانية زي HTML باستخدام XSLT.
  • HTTP Requests: إرسال طلبات HTTP باستخدام XMLHTTP.

يعني MSXML هي الـ “محرك” اللي XMLHTTP كان جزء منه. الكود كان بيستدعي XMLHTTP من MSXML:

var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
ليه MSXML كانت مهمة؟#
  • XML شائعة: كانت الصيغة القياسية للبيانات.
  • دعم قوي: مدمجة مع Windows وInternet Explorer.
  • مرونة: بتدعم مهام زي تحليل XML وإرسال طلبات HTTP.
مشاكل MSXML:#
  • محدودة لـ Microsoft: خاصة بـ Windows وIE.
  • تعقيد: كان لازم تفهم المكتبة كلها عشان تستخدم XMLHTTP.
  • تطور بطيء: مكانتش مرنة لما JSON بقى شائع.

التطور: AJAX وتوسع XHR (2005) 🚀#

في 2005، Jesse James Garrett كتب مقالة عن “AJAX” (Asynchronous JavaScript and XML)، وده خلّى XHR الركيزة الأساسية للويب التفاعلي. المتصفحات زي Firefox وSafari بدأت تدعم XMLHttpRequest كـ standard object (مش ActiveX)، والناس حبته عشان:

  • سهّل التواصل مع السيرفر بدون reload.
  • خلّى المواقع زي Gmail وFacebook ممكنة.

لكن XHR القديم كان له مشاكل:

  • الكود كان معقد ومش منظم.
  • لازم تتعامل مع readyState وstatus يدويًا.
  • مكانش فيه دعم قوي للـ error handling.
  • صعب تتعامل مع أكتر من طلب في نفس الوقت.

إيه كان تأثير AJAX؟ 💥#

AJAX غيّرت الويب كليًا:

  • Web 2.0: ظهور مواقع تفاعلية زي Google Maps، Gmail، وFlickr.
  • User Experience: المستخدمين بقوا يتوقعوا مواقع سريعة ومتجاوبة.
  • Single Page Applications (SPAs): بقى ممكن تعمل تطبيق كامل في صفحة واحدة.
// مثال AJAX في 2005
function loadContent(url) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                document.getElementById('content').innerHTML = xhr.responseText;
            } else {
                alert('حصل خطأ: ' + xhr.status);
            }
        }
    };
    xhr.open('GET', url, true);
    xhr.send();
}

المعيار الحديث: XHR في ES5/ES6 (2010-2015) 📋#

مع تطور JavaScript، XHR بقى جزء من مواصفات WHATWG وW3C، واتضافوا ميزات زي:

  • دعم responseType: لمعالجة JSON، Blob، وArrayBuffer.
  • متابعة التقدم: باستخدام onprogress.
  • دعم CORS: للتواصل مع سيرفرات خارجية.
  • Upload Events: لمتابعة تقدم رفع الملفات.
  • Timeout Support: لتحديد وقت انتهاء الطلب.

الكود بقى أنظف بكتير:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.responseType = 'json';
xhr.timeout = 5000; // 5 ثواني

xhr.onload = function() {
    if (xhr.status === 200) {
        console.log(xhr.response);
    }
};

xhr.onerror = function() {
    console.error('حصل خطأ!');
};

xhr.ontimeout = function() {
    console.error('انتهت مهلة الطلب!');
};

xhr.send();

ظهور Fetch API (2015): المنافس الجديد 🎉#

مع ES6، ظهرت Fetch API كبديل أسهل وأحدث:

fetch('/api/data')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('حصل خطأ:', error));

لكن XHR لسه مستخدم لأنه:

  • متوافق مع المتصفحات القديمة.
  • بيديك تحكم أكتر في الطلبات (زي onprogress).
  • بيشتغل مع بيانات معقدة زي FormData وBlobs.
  • بيدعم synchronous requests (مش recommended بس ممكن).

دلوقتي فهمنا التاريخ، خلينا ندخل في التفاصيل التقنية لـ XHR.

إيه هو XHR أصلاً؟ 🤔#

XMLHttpRequest (XHR) هو واجهة برمجية (API) في JavaScript بتتيح لك تبعت طلبات HTTP/HTTPS للسيرفر وتستقبل الردود بدون reload. فكر فيه زي ساعي بريد سريع: بياخد طلبك (زي GET أو POST) ويرجعلك بالبيانات من غير ما توقف الصفحة.

تخيل إنك بتطلب قهوة من مقهى:

  1. بتفتح الطلب (open): بتقول للويتر نوع القهوة.
  2. بتبعت الطلب (send): الويتر بيروح للمطبخ.
  3. بتستنى الرد (onreadystatechange): المطبخ بيجهز القهوة.
  4. بتاخد القهوة (response): لو كل حاجة تمام، القهوة بتوصلك.
  5. لو في مشكلة (onerror): لو القهوة خلّصت، بيجيلك إشعار.

حالات الـ readyState 📊#

XHR بيمر بـ 5 حالات:

  • 0 (UNSENT): الطلب لسه ما اتفتحش.
  • 1 (OPENED): الطلب اتفتح بس لسه ما اتابعش.
  • 2 (HEADERS_RECEIVED): السيرفر رجّع الـ headers.
  • 3 (LOADING): البيانات بتتحمل.
  • 4 (DONE): الطلب خلّص (نجح أو فشل).

شرح تفصيلي للكود 🔍#

ده مثال كود أساسي لـ XHR:

const xhr = new XMLHttpRequest();

// 1️⃣ فتح الطلب
xhr.open('GET', 'https://api.example.com/data', true); // true = asynchronous

// 2️⃣ تحديد نوع الـ response
xhr.responseType = 'json';

// 3️⃣ التعامل مع الـ response
xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.response); // البيانات وصلت
    } else {
        console.error('حصل خطأ:', xhr.status);
    }
};

// 4️⃣ التعامل مع الأخطاء
xhr.onerror = function() {
    console.error('مشكلة في الشبكة أو السيرفر');
};

// 5️⃣ إرسال الطلب
xhr.send();

تفاصيل أكتر عن الـ open#

  1. الـ open بتاخد 3 parameters:

    xhr.open(method, url, async);
    // method: 'GET', 'POST', 'PUT', etc.
    // url: رابط الـ API
    // async: true (asynchronous) أو false (synchronous)
    
  2. ممكن تستخدم مع credentials:

    xhr.open('GET', '/api/data', true, username, password);
    
  3. بتجهز الطلب بس مش بتبعته:

    • الـ open بيحدد إعدادات الطلب بس ما بيبعتش حاجة للسيرفر.

تفاصيل أكتر عن الـ send#

  • بتبعت الطلب للسيرفر:

    xhr.send(); // للـ GET
    xhr.send(data); // للـ POST (ممكن FormData, JSON, etc.)
    
  • بتتعامل مع أنواع بيانات مختلفة:

    // إرسال JSON
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.send(JSON.stringify({ name: 'Ahmed' }));
    
    // إرسال FormData
    const formData = new FormData();
    formData.append('file', fileInput.files[0]);
    xhr.send(formData);
    

دلوقتي إحنا عارفين XHR إزاي بيشتغل، خلينا نشوف ليه بنستخدمه وإيه مميزاته.

ليه بنستخدم XHR؟ 🎯#

  1. التحكم الكامل في الطلبات:

    • بيديك تحكم دقيق في الـ headers، progress، وevents.
    • بيسمح بمعالجة البيانات الكبيرة (مثل progress للتحميل).
  2. التوافق مع المتصفحات القديمة:

    • XHR بيشتغل حتى في المتصفحات القديمة زي IE6.
  3. دعم ميزات متقدمة:

    • زي onprogress لمتابعة تحميل البيانات.
    • دعم responseType لمعالجة JSON، Blobs، وArrayBuffers.
  4. مرونة في إدارة الـ State:

    • بيخليك تتحكم في كل مرحلة من مراحل الطلب.
    • ممكن تلغي الطلبات باستخدام abort().
  5. دعم Upload Progress:

    • الـ Fetch API لسه مش بيدعم upload progress بشكل كامل.

عشان نفهم XHR أكتر، خلينا نشوف الـ methods والـ events الأساسية اللي بتديله القوة دي.

الـ Methods والـ Events الأساسية 🛠️#

1. open()#

بتحدد نوع الطلب (GET, POST) والـ URL وهل هو asynchronous أو لا.

xhr.open('POST', '/api/submit', true);

Parameters مفصلة:

  • method: HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
  • url: الـ URL اللي هتبعتله الطلب
  • async: (optional) true للـ asynchronous، false للـ synchronous
  • user: (optional) اسم المستخدم للـ authentication
  • password: (optional) كلمة المرور للـ authentication

2. send()#

بتبعت الطلب للسيرفر، وممكن تبعت معاه بيانات.

xhr.send(); // GET request
xhr.send(JSON.stringify({ key: 'value' })); // POST with JSON
xhr.send(formData); // POST with FormData
xhr.send(blob); // POST with Blob

3. setRequestHeader()#

بتضيف headers للطلب.

xhr.setRequestHeader('Authorization', 'Bearer token');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-Custom-Header', 'value');

ملحوظة مهمة:

  • لازم تستدعي setRequestHeader بعد open() وقبل send().
  • بعض الـ headers محظورة لأسباب أمان زي User-Agent وHost.

4. abort()#

بتلغي الطلب لو لسه في الـ process.

const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/large-data', true);
xhr.send();

// لو عايز تلغي الطلب
setTimeout(() => {
    xhr.abort();
    console.log('تم إلغاء الطلب');
}, 3000);

5. Events المهمة#

onload#

بيتنفذ لما الطلب ينجح (status 200-299).

xhr.onload = function() {
    console.log('الطلب اكتمل بنجاح');
    console.log('البيانات:', xhr.response);
};

onerror#

بيتنفذ لو في مشكلة في الشبكة أو السيرفر.

xhr.onerror = function() {
    console.error('حصل خطأ في الشبكة');
};

onprogress#

بيتنفذ أثناء تحميل البيانات، مفيد للـ progress bars.

xhr.onprogress = function(event) {
    if (event.lengthComputable) {
        const percentComplete = (event.loaded / event.total) * 100;
        console.log(`تم تحميل ${percentComplete.toFixed(2)}%`);
    }
};

onreadystatechange#

بيتنفذ مع كل تغيير في الـ readyState.

xhr.onreadystatechange = function() {
    console.log('ReadyState:', xhr.readyState);
    if (xhr.readyState === 4) {
        if (xhr.status === 200) {
            console.log('نجح الطلب');
        }
    }
};

ontimeout#

بيتنفذ لو الطلب اخد وقت أكتر من المحدد.

xhr.timeout = 5000; // 5 ثواني
xhr.ontimeout = function() {
    console.error('انتهت مهلة الطلب');
};

Upload Events#

للـ upload، في events خاصة:

xhr.upload.onprogress = function(event) {
    const percentComplete = (event.loaded / event.total) * 100;
    console.log(`تم رفع ${percentComplete.toFixed(2)}%`);
};

xhr.upload.onload = function() {
    console.log('تم رفع الملف بنجاح');
};

xhr.upload.onerror = function() {
    console.error('حصل خطأ أثناء الرفع');
};

6. Properties مهمة#

responseType#

بتحدد نوع البيانات المتوقعة:

xhr.responseType = 'json'; // JSON object
xhr.responseType = 'text'; // String (default)
xhr.responseType = 'blob'; // Blob object
xhr.responseType = 'arraybuffer'; // ArrayBuffer
xhr.responseType = 'document'; // XML Document

withCredentials#

للـ CORS requests اللي محتاجة credentials:

xhr.withCredentials = true;

status وstatusText#

بيوصفوا حالة الـ response:

xhr.onload = function() {
    console.log('Status:', xhr.status); // 200, 404, 500, etc.
    console.log('Status Text:', xhr.statusText); // "OK", "Not Found", etc.
};

دلوقتي خلينا نشوف XHR في الاستخدام العملي مع أمثلة متقدمة.

أمثلة عملية متقدمة 💡#

1. جلب بيانات مع Error Handling محترف#

function fetchUserData(userId) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', `https://api.example.com/users/${userId}`, true);
        xhr.responseType = 'json';
        xhr.timeout = 10000; // 10 ثواني

        xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(xhr.response);
            } else {
                reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
            }
        };

        xhr.onerror = function() {
            reject(new Error('مشكلة في الشبكة'));
        };

        xhr.ontimeout = function() {
            reject(new Error('انتهت مهلة الطلب'));
        };

        xhr.send();
    });
}

// الاستخدام
fetchUserData(123)
    .then(user => {
        console.log('بيانات المستخدم:', user);
        document.getElementById('userName').textContent = user.name;
    })
    .catch(error => {
        console.error('خطأ:', error.message);
        document.getElementById('error').textContent = 'فشل في جلب البيانات';
    });

2. رفع ملف مع Progress Bar#

function uploadFileWithProgress(file, onProgress) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', '/api/upload', true);

        // متابعة تقدم الرفع
        xhr.upload.onprogress = function(event) {
            if (event.lengthComputable) {
                const percentComplete = (event.loaded / event.total) * 100;
                onProgress(percentComplete);
            }
        };

        xhr.upload.onload = function() {
            console.log('تم رفع الملف كاملاً');
        };

        xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject(new Error(`فشل الرفع: ${xhr.status}`));
            }
        };

        xhr.onerror = function() {
            reject(new Error('مشكلة في الشبكة أثناء الرفع'));
        };

        const formData = new FormData();
        formData.append('file', file);
        formData.append('description', 'ملف مرفوع من XHR');
        
        xhr.send(formData);
    });
}

// الاستخدام مع Progress Bar
const fileInput = document.getElementById('fileInput');
const progressBar = document.getElementById('progressBar');

fileInput.addEventListener('change', function(event) {
    const file = event.target.files[0];
    if (file) {
        uploadFileWithProgress(file, (percent) => {
            progressBar.style.width = percent + '%';
            progressBar.textContent = Math.round(percent) + '%';
        })
        .then(result => {
            console.log('نجح الرفع:', result);
            progressBar.textContent = 'تم الرفع بنجاح!';
        })
        .catch(error => {
            console.error('فشل الرفع:', error);
            progressBar.textContent = 'فشل الرفع';
        });
    }
});

3. إرسال JSON Data مع Authentication#

function sendUserData(userData, authToken) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', '/api/users', true);
        
        // إعداد الـ headers
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.setRequestHeader('Authorization', `Bearer ${authToken}`);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        
        xhr.responseType = 'json';
        
        xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(xhr.response);
            } else if (xhr.status === 401) {
                reject(new Error('غير مصرح لك بالوصول'));
            } else if (xhr.status === 422) {
                reject(new Error('بيانات غير صحيحة'));
            } else {
                reject(new Error(`خطأ في السيرفر: ${xhr.status}`));
            }
        };

        xhr.onerror = function() {
            reject(new Error('مشكلة في الاتصال'));
        };

        xhr.send(JSON.stringify(userData));
    });
}

// الاستخدام
const userData = {
    name: 'أحمد محمد',
    email: 'ahmed@example.com',
    age: 25
};

sendUserData(userData, 'your-auth-token')
    .then(result => {
        console.log('تم حفظ البيانات:', result);
    })
    .catch(error => {
        console.error('فشل في حفظ البيانات:', error.message);
    });

4. تحميل ملف كبير مع إمكانية الإلغاء#

class FileDownloader {
    constructor() {
        this.xhr = null;
        this.isDownloading = false;
    }

    download(url, onProgress, onComplete, onError) {
        if (this.isDownloading) {
            throw new Error('تحميل آخر قيد التنفيذ');
        }

        this.xhr = new XMLHttpRequest();
        this.isDownloading = true;

        this.xhr.open('GET', url, true);
        this.xhr.responseType = 'blob';

        this.xhr.onprogress = function(event) {
            if (event.lengthComputable && onProgress) {
                const progress = {
                    loaded: event.loaded,
                    total: event.total,
                    percentage: (event.loaded / event.total) * 100
                };
                onProgress(progress);
            }
        };

        this.xhr.onload = () => {
            this.isDownloading = false;
            if (this.xhr.status >= 200 && this.xhr.status < 300) {
                const blob = this.xhr.response;
                const downloadUrl = window.URL.createObjectURL(blob);
                
                // إنشاء رابط تحميل تلقائي
                const a = document.createElement('a');
                a.href = downloadUrl;
                a.download = url.split('/').pop() || 'file';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                window.URL.revokeObjectURL(downloadUrl);
                
                if (onComplete) onComplete(blob);
            } else if (onError) {
                onError(new Error(`فشل التحميل: ${this.xhr.status}`));
            }
        };

        this.xhr.onerror = () => {
            this.isDownloading = false;
            if (onError) onError(new Error('مشكلة في الشبكة'));
        };

        this.xhr.onabort = () => {
            this.isDownloading = false;
            console.log('تم إلغاء التحميل');
        };

        this.xhr.send();
    }

    cancel() {
        if (this.xhr && this.isDownloading) {
            this.xhr.abort();
        }
    }
}

// الاستخدام
const downloader = new FileDownloader();
const progressElement = document.getElementById('downloadProgress');
const cancelButton = document.getElementById('cancelButton');

downloader.download(
    'https://example.com/large-file.zip',
    (progress) => {
        progressElement.textContent = `${progress.percentage.toFixed(1)}%`;
        console.log(`تم تحميل ${progress.loaded} من ${progress.total} بايت`);
    },
    (blob) => {
        console.log('تم التحميل بنجاح، حجم الملف:', blob.size);
        progressElement.textContent = 'تم التحميل!';
    },
    (error) => {
        console.error('فشل التحميل:', error.message);
        progressElement.textContent = 'فشل التحميل';
    }
);

cancelButton.addEventListener('click', () => {
    downloader.cancel();
});

5. إرسال طلبات متعددة (Batch Requests)#

function batchRequests(urls) {
    const promises = urls.map(url => {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.responseType = 'json';
            xhr.timeout = 5000;

            xhr.onload = function() {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve({
                        url: url,
                        data: xhr.response,
                        status: xhr.status
                    });
                } else {
                    reject({
                        url: url,
                        error: `HTTP ${xhr.status}`,
                        status: xhr.status
                    });
                }
            };

            xhr.onerror = () => reject({
                url: url,
                error: 'مشكلة في الشبكة'
            });

            xhr.ontimeout = () => reject({
                url: url,
                error: 'انتهت المهلة'
            });

            xhr.send();
        });
    });

    return Promise.allSettled(promises);
}

// الاستخدام
const apiUrls = [
    '/api/users',
    '/api/products',
    '/api/orders'
];

batchRequests(apiUrls)
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`نجح ${apiUrls[index]}:`, result.value.data);
            } else {
                console.error(`فشل ${apiUrls[index]}:`, result.reason.error);
            }
        });
    });

الأمثلة دي بتوريك إزاي XHR مرن ومقدر يتعامل مع حالات معقدة، بس ممكن تواجه مشاكل. خلينا نشوف إزاي نتعامل معاها.

المشاكل الشائعة وحلولها 🔧#

1. نسيان الـ Error Handling#

الغلط:

const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onload = function() {
    console.log(xhr.response);
};
xhr.send();

الصح:

const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.responseType = 'json';

xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.response);
    } else {
        console.error('حصل خطأ:', xhr.status, xhr.statusText);
    }
};

xhr.onerror = function() {
    console.error('مشكلة في الشبكة');
};

xhr.ontimeout = function() {
    console.error('انتهت مهلة الطلب');
};

xhr.send();

2. التعامل مع الـ CORS#

الغلط:

xhr.open('GET', 'https://another-domain.com/api', true);
xhr.send(); // هيفشل بسبب CORS

الصح:

xhr.open('GET', 'https://another-domain.com/api', true);
xhr.withCredentials = true; // لو محتاج credentials
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

// التأكد إن السيرفر بيدعم CORS
xhr.onload = function() {
    console.log('Access-Control-Allow-Origin:', xhr.getResponseHeader('Access-Control-Allow-Origin'));
};

xhr.send();

3. عدم إلغاء الطلبات القديمة#

الغلط:

let currentXHR;

function searchUsers(query) {
    currentXHR = new XMLHttpRequest();
    currentXHR.open('GET', `/api/search?q=${query}`, true);
    currentXHR.onload = function() {
        displayResults(currentXHR.response);
    };
    currentXHR.send();
}

// المشكلة: لو المستخدم كتب بسرعة، الطلبات القديمة لسه شغالة

الصح:

let currentXHR;

function searchUsers(query) {
    // إلغاء الطلب القديم لو موجود
    if (currentXHR) {
        currentXHR.abort();
    }

    currentXHR = new XMLHttpRequest();
    currentXHR.open('GET', `/api/search?q=${encodeURIComponent(query)}`, true);
    currentXHR.responseType = 'json';
    
    currentXHR.onload = function() {
        if (xhr.status === 200) {
            displayResults(currentXHR.response);
        }
        currentXHR = null; // تنظيف المرجع
    };

    currentXHR.onerror = function() {
        console.error('فشل البحث');
        currentXHR = null;
    };

    currentXHR.onabort = function() {
        console.log('تم إلغاء البحث');
        currentXHR = null;
    };

    currentXHR.send();
}

4. عدم التعامل مع responseType بشكل صحيح#

الغلط:

xhr.responseType = 'json';
xhr.onload = function() {
    const data = JSON.parse(xhr.responseText); // خطأ!
};

الصح:

xhr.responseType = 'json';
xhr.onload = function() {
    const data = xhr.response; // صح! XHR هيعمل parse تلقائياً
    console.log(data);
};

5. مشكلة Memory Leaks مع Event Listeners#

الغلط:

function makeRequest() {
    const xhr = new XMLHttpRequest();
    xhr.onload = function() {
        // الـ function دي هتفضل في الـ memory
        console.log(xhr.response);
    };
    // مكنتش بنضف الـ references
}

الصح:

function makeRequest() {
    const xhr = new XMLHttpRequest();
    
    const cleanup = () => {
        xhr.onload = null;
        xhr.onerror = null;
        xhr.onprogress = null;
    };
    
    xhr.onload = function() {
        console.log(xhr.response);
        cleanup(); // تنظيف الـ references
    };
    
    xhr.onerror = function() {
        console.error('حصل خطأ');
        cleanup();
    };
    
    xhr.open('GET', '/api/data', true);
    xhr.send();
}

المشاكل دي بتخلّينا نفكر: إزاي XHR بيشتغل من جواه؟ خلينا نفهم ده من خلال محاكاة بسيطة.

إزاي عملوا Implementation للـ XHR 🧠#

عشان نفهم XHR بعمق، هنعمل كائن بسيط يحاكي سلوكه (مش التنفيذ الحقيقي لأنه في المتصفحات بيستخدم C++):

class SimpleXHR {
    constructor() {
        // Constants للـ readyState
        this.UNSENT = 0;
        this.OPENED = 1;
        this.HEADERS_RECEIVED = 2;
        this.LOADING = 3;
        this.DONE = 4;
        
        // Properties
        this.readyState = this.UNSENT;
        this.status = 0;
        this.statusText = '';
        this.response = null;
        this.responseText = '';
        this.responseType = 'text';
        this.timeout = 0;
        this.withCredentials = false;
        
        // Events
        this.onload = null;
        this.onerror = null;
        this.onprogress = null;
        this.onreadystatechange = null;
        this.ontimeout = null;
        this.onabort = null;
        
        // Internal properties
        this._headers = new Map();
        this._method = '';
        this._url = '';
        this._async = true;
        this._aborted = false;
        this._timeoutId = null;
    }

    open(method, url, async = true, user, password) {
        // إعادة تعيين الحالة
        this.readyState = this.UNSENT;
        this.status = 0;
        this.statusText = '';
        this.response = null;
        this.responseText = '';
        this._aborted = false;
        
        // حفظ المعاملات
        this._method = method.toUpperCase();
        this._url = url;
        this._async = async;
        
        // تغيير الحالة إلى OPENED
        this.readyState = this.OPENED;
        this._notifyStateChange();
    }

    setRequestHeader(header, value) {
        if (this.readyState !== this.OPENED) {
            throw new Error('يجب استدعاء open() أولاً');
        }
        
        // Headers محظورة لأسباب أمان
        const forbiddenHeaders = [
            'accept-charset', 'accept-encoding', 'access-control-request-headers',
            'access-control-request-method', 'connection', 'content-length',
            'cookie', 'cookie2', 'date', 'dnt', 'expect', 'host', 'keep-alive',
            'origin', 'referer', 'te', 'trailer', 'transfer-encoding', 'upgrade',
            'user-agent', 'via'
        ];
        
        if (!forbiddenHeaders.includes(header.toLowerCase())) {
            this._headers.set(header, value);
        }
    }

    send(data = null) {
        if (this.readyState !== this.OPENED) {
            throw new Error('يجب استدعاء open() أولاً');
        }

        this._aborted = false;
        
        // إعداد timeout
        if (this.timeout > 0) {
            this._timeoutId = setTimeout(() => {
                this._handleTimeout();
            }, this.timeout);
        }

        // محاكاة الطلب باستخدام fetch (في الواقع، المتصفح بيستخدم C++)
        this._makeRequest(data);
    }

    abort() {
        if (this.readyState === this.UNSENT || this.readyState === this.DONE) {
            return;
        }
        
        this._aborted = true;
        this.readyState = this.DONE;
        this.status = 0;
        this.statusText = '';
        
        if (this._timeoutId) {
            clearTimeout(this._timeoutId);
        }
        
        this._notifyStateChange();
        
        if (this.onabort) {
            this.onabort();
        }
    }

    getResponseHeader(header) {
        if (this.readyState < this.HEADERS_RECEIVED) {
            return null;
        }
        return this._responseHeaders.get(header.toLowerCase()) || null;
    }

    getAllResponseHeaders() {
        if (this.readyState < this.HEADERS_RECEIVED) {
            return '';
        }
        
        let headers = '';
        this._responseHeaders.forEach((value, key) => {
            headers += `${key}: ${value}\r\n`;
        });
        return headers;
    }

    // Internal methods
    _makeRequest(data) {
        // تغيير الحالة إلى HEADERS_RECEIVED
        this.readyState = this.HEADERS_RECEIVED;
        this._notifyStateChange();

        // محاكاة إعداد fetch
        const fetchOptions = {
            method: this._method,
            headers: Object.fromEntries(this._headers),
            credentials: this.withCredentials ? 'include' : 'same-origin'
        };

        if (data && this._method !== 'GET' && this._method !== 'HEAD') {
            fetchOptions.body = data;
        }

        // محاكاة الطلب
        fetch(this._url, fetchOptions)
            .then(response => {
                if (this._aborted) return;
                
                this.status = response.status;
                this.statusText = response.statusText;
                this._responseHeaders = new Map();
                
                // حفظ response headers
                response.headers.forEach((value, key) => {
                    this._responseHeaders.set(key.toLowerCase(), value);
                });

                // تغيير الحالة إلى LOADING
                this.readyState = this.LOADING;
                this._notifyStateChange();

                // محاكاة progress events
                this._simulateProgress();

                // قراءة البيانات حسب responseType
                switch (this.responseType) {
                    case 'json':
                        return response.json();
                    case 'blob':
                        return response.blob();
                    case 'arraybuffer':
                        return response.arrayBuffer();
                    case 'document':
                        return response.text().then(text => {
                            const parser = new DOMParser();
                            return parser.parseFromString(text, 'text/xml');
                        });
                    default:
                        return response.text();
                }
            })
            .then(data => {
                if (this._aborted) return;
                
                this.response = data;
                this.responseText = typeof data === 'string' ? data : '';
                
                // تغيير الحالة إلى DONE
                this.readyState = this.DONE;
                this._notifyStateChange();
                
                // تنظيف timeout
                if (this._timeoutId) {
                    clearTimeout(this._timeoutId);
                }
                
                // تنفيذ onload
                if (this.onload) {
                    this.onload();
                }
            })
            .catch(error => {
                if (this._aborted) return;
                
                this.readyState = this.DONE;
                this._notifyStateChange();
                
                if (this._timeoutId) {
                    clearTimeout(this._timeoutId);
                }
                
                if (this.onerror) {
                    this.onerror();
                }
            });
    }

    _simulateProgress() {
        // محاكاة بسيطة للـ progress
        let loaded = 0;
        const total = 1000; // حجم وهمي
        
        const progressInterval = setInterval(() => {
            if (this._aborted || this.readyState === this.DONE) {
                clearInterval(progressInterval);
                return;
            }
            
            loaded += Math.random() * 200;
            if (loaded > total) loaded = total;
            
            if (this.onprogress) {
                this.onprogress({
                    loaded: loaded,
                    total: total,
                    lengthComputable: true
                });
            }
            
            if (loaded >= total) {
                clearInterval(progressInterval);
            }
        }, 100);
    }

    _handleTimeout() {
        this._aborted = true;
        this.readyState = this.DONE;
        this.status = 0;
        this.statusText = '';
        
        this._notifyStateChange();
        
        if (this.ontimeout) {
            this.ontimeout();
        }
    }

    _notifyStateChange() {
        if (this.onreadystatechange) {
            // استخدام setTimeout عشان نحاكي الـ asynchronous behavior
            setTimeout(() => {
                this.onreadystatechange();
            }, 0);
        }
    }
}

// مثال للاستخدام
const xhr = new SimpleXHR();
xhr.open('GET', '/api/test', true);
xhr.responseType = 'json';
xhr.timeout = 5000;

xhr.onreadystatechange = function() {
    console.log('ReadyState تغيرت إلى:', xhr.readyState);
};

xhr.onprogress = function(event) {
    console.log(`Progress: ${event.loaded}/${event.total}`);
};

xhr.onload = function() {
    console.log('تم بنجاح:', xhr.response);
};

xhr.onerror = function() {
    console.error('حصل خطأ');
};

xhr.send();

الفرق بين التنفيذ الحقيقي والمحاكاة#

التنفيذ الحقيقي لـ XHR في المتصفحات:

  • مكتوب بـ C++: لأداء أفضل وأمان أكتر.
  • متصل بـ Network Stack: بيستخدم مكونات الشبكة في نظام التشغيل.
  • Thread Management: بيستخدم threads منفصلة عشان ما يبطئش الـ UI.
  • Security Checks: بيعمل فحوصات أمان معقدة للـ CORS وSame-Origin Policy.

استخدامات متقدمة لـ XHR 🚀#

1. إنشاء HTTP Client Library#

class HTTPClient {
    constructor(baseURL = '', defaultHeaders = {}) {
        this.baseURL = baseURL;
        this.defaultHeaders = defaultHeaders;
        this.interceptors = {
            request: [],
            response: []
        };
    }

    // إضافة interceptor للطلبات
    addRequestInterceptor(fn) {
        this.interceptors.request.push(fn);
    }

    // إضافة interceptor للردود
    addResponseInterceptor(fn) {
        this.interceptors.response.push(fn);
    }

    async request(config) {
        // تطبيق request interceptors
        for (const interceptor of this.interceptors.request) {
            config = await interceptor(config);
        }

        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            const url = this.baseURL + config.url;
            
            xhr.open(config.method || 'GET', url, true);
            xhr.responseType = config.responseType || 'json';
            xhr.timeout = config.timeout || 10000;

            // إضافة default headers
            Object.entries(this.defaultHeaders).forEach(([key, value]) => {
                xhr.setRequestHeader(key, value);
            });

            // إضافة headers مخصصة
            if (config.headers) {
                Object.entries(config.headers).forEach(([key, value]) => {
                    xhr.setRequestHeader(key, value);
                });
            }

            xhr.onload = async function() {
                let response = {
                    data: xhr.response,
                    status: xhr.status,
                    statusText: xhr.statusText,
                    headers: xhr.getAllResponseHeaders(),
                    config: config
                };

                // تطبيق response interceptors
                for (const interceptor of this.interceptors.response) {
                    response = await interceptor(response);
                }

                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve(response);
                } else {
                    reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
                }
            }.bind(this);

            xhr.onerror = () => reject(new Error('مشكلة في الشبكة'));
            xhr.ontimeout = () => reject(new Error('انتهت مهلة الطلب'));

            xhr.send(config.data);
        });
    }

    // Shorthand methods
    get(url, config = {}) {
        return this.request({ ...config, method: 'GET', url });
    }

    post(url, data, config = {}) {
        return this.request({ ...config, method: 'POST', url, data });
    }

    put(url, data, config = {}) {
        return this.request({ ...config, method: 'PUT', url, data });
    }

    delete(url, config = {}) {
        return this.request({ ...config, method: 'DELETE', url });
    }
}

// الاستخدام
const client = new HTTPClient('https://api.example.com', {
    'Authorization': 'Bearer token',
    'X-App-Version': '1.0.0'
});

// إضافة request interceptor للـ logging
client.addRequestInterceptor(async (config) => {
    console.log('إرسال طلب:', config.method, config.url);
    return config;
});

// إضافة response interceptor للـ error handling
client.addResponseInterceptor(async (response) => {
    if (response.status === 401) {
        // إعادة توجيه للـ login
        window.location.href = '/login';
    }
    return response;
});

// استخدام الـ client
client.get('/users')
    .then(response => {
        console.log('المستخدمين:', response.data);
    })
    .catch(error => {
        console.error('خطأ:', error.message);
    });

2. Upload Manager متقدم#

class AdvancedUploadManager {
    constructor() {
        this.uploads = new Map();
        this.maxConcurrent = 3;
        this.queue = [];
        this.activeUploads = 0;
    }

    upload(file, options = {}) {
        const uploadId = this._generateId();
        const upload = {
            id: uploadId,
            file: file,
            options: options,
            xhr: null,
            status: 'pending',
            progress: 0,
            speed: 0,
            startTime: null,
            estimatedTime: null
        };

        this.uploads.set(uploadId, upload);
        this.queue.push(upload);
        this._processQueue();

        return uploadId;
    }

    cancel(uploadId) {
        const upload = this.uploads.get(uploadId);
        if (upload && upload.xhr) {
            upload.xhr.abort();
            upload.status = 'cancelled';
            this.activeUploads--;
            this._processQueue();
        }
    }

    getStatus(uploadId) {
        return this.uploads.get(uploadId);
    }

    _processQueue() {
        while (this.queue.length > 0 && this.activeUploads < this.maxConcurrent) {
            const upload = this.queue.shift();
            this._startUpload(upload);
        }
    }

    _startUpload(upload) {
        this.activeUploads++;
        upload.status = 'uploading';
        upload.startTime = Date.now();

        const xhr = new XMLHttpRequest();
        upload.xhr = xhr;

        xhr.open('POST', upload.options.url || '/api/upload', true);

        // إضافة headers
        if (upload.options.headers) {
            Object.entries(upload.options.headers).forEach(([key, value]) => {
                xhr.setRequestHeader(key, value);
            });
        }

        // متابعة التقدم
        xhr.upload.onprogress = (event) => {
            if (event.lengthComputable) {
                const now = Date.now();
                const elapsed = (now - upload.startTime) / 1000;
                const loaded = event.loaded;
                const total = event.total;
                
                upload.progress = (loaded / total) * 100;
                upload.speed = loaded / elapsed; // bytes per second
                
                const remaining = total - loaded;
                upload.estimatedTime = remaining / upload.speed;

                if (upload.options.onProgress) {
                    upload.options.onProgress({
                        uploadId: upload.id,
                        progress: upload.progress,
                        speed: upload.speed,
                        estimatedTime: upload.estimatedTime,
                        loaded: loaded,
                        total: total
                    });
                }
            }
        };

        xhr.onload = () => {
            this.activeUploads--;
            
            if (xhr.status >= 200 && xhr.status < 300) {
                upload.status = 'completed';
                if (upload.options.onSuccess) {
                    upload.options.onSuccess({
                        uploadId: upload.id,
                        response: xhr.response
                    });
                }
            } else {
                upload.status = 'failed';
                if (upload.options.onError) {
                    upload.options.onError({
                        uploadId: upload.id,
                        error: `HTTP ${xhr.status}: ${xhr.statusText}`
                    });
                }
            }
            
            this._processQueue();
        };

        xhr.onerror = () => {
            this.activeUploads--;
            upload.status = 'failed';
            
            if (upload.options.onError) {
                upload.options.onError({
                    uploadId: upload.id,
                    error: 'مشكلة في الشبكة'
                });
            }
            
            this._processQueue();
        };

        // تحضير البيانات
        const formData = new FormData();
        formData.append('file', upload.file);
        
        if (upload.options.fields) {
            Object.entries(upload.options.fields).forEach(([key, value]) => {
                formData.append(key, value);
            });
        }

        xhr.send(formData);
    }

    _generateId() {
        return 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
    }
}

// الاستخدام
const uploadManager = new AdvancedUploadManager();

document.getElementById('fileInput').addEventListener('change', function(event) {
    const files = Array.from(event.target.files);
    
    files.forEach(file => {
        const uploadId = uploadManager.upload(file, {
            url: '/api/upload',
            headers: {
                'Authorization': 'Bearer token'
            },
            fields: {
                'category': 'images',
                'userId': '123'
            },
            onProgress: (data) => {
                console.log(`Upload ${data.uploadId}: ${data.progress.toFixed(1)}%`);
                console.log(`السرعة: ${(data.speed / 1024).toFixed(1)} KB/s`);
                console.log(`الوقت المتبقي: ${data.estimatedTime.toFixed(1)} ثانية`);
            },
            onSuccess: (data) => {
                console.log('تم الرفع بنجاح:', data.uploadId);
            },
            onError: (data) => {
                console.error('فشل الرفع:', data.uploadId, data.error);
            }
        });
        
        console.log('بدء رفع الملف:', file.name, 'ID:', uploadId);
    });
});

3. Real-time Data Streaming#

class DataStreamer {
    constructor(url, options = {}) {
        this.url = url;
        this.options = options;
        this.xhr = null;
        this.buffer = '';
        this.isStreaming = false;
        this.onData = options.onData || (() => {});
        this.onError = options.onError || (() => {});
    }

    start() {
        if (this.isStreaming) return;

        this.xhr = new XMLHttpRequest();
        this.isStreaming = true;
        this.buffer = '';

        this.xhr.open('GET', this.url, true);
        
        // إعداد streaming
        this.xhr.onprogress = (event) => {
            const responseText = this.xhr.responseText;
            const newData = responseText.substring(this.buffer.length);
            this.buffer = responseText;

            if (newData) {
                this._processStreamData(newData);
            }
        };

        this.xhr.onload = () => {
            this.isStreaming = false;
            console.log('انتهى الـ stream');
        };

        this.xhr.onerror = () => {
            this.isStreaming = false;
            this.onError(new Error('مشكلة في الـ stream'));
        };

        this.xhr.send();
    }

    stop() {
        if (this.xhr && this.isStreaming) {
            this.xhr.abort();
            this.isStreaming = false;
        }
    }

    _processStreamData(data) {
        // تقسيم البيانات حسب الأسطر (للـ JSON streaming)
        const lines = data.split('\n');
        
        lines.forEach(line => {
            line = line.trim();
            if (line) {
                try {
                    const jsonData = JSON.parse(line);
                    this.onData(jsonData);
                } catch (e) {
                    // تجاهل الأسطر اللي مش JSON صحيح
                    console.warn('سطر غير صحيح:', line);
                }
            }
        });
    }
}

// الاستخدام للـ real-time notifications
const notificationStreamer = new DataStreamer('/api/notifications/stream', {
    onData: (notification) => {
        console.log('إشعار جديد:', notification);
        showNotification(notification.message, notification.type);
    },
    onError: (error) => {
        console.error('خطأ في الـ stream:', error);
    }
});

notificationStreamer.start();

4. Request Caching System#

class XHRCache {
    constructor(maxSize = 100, ttl = 5 * 60 * 1000) { // 5 دقائق
        this.cache = new Map();
        this.maxSize = maxSize;
        this.ttl = ttl;
        this.accessOrder = [];
    }

    _generateKey(method, url, data) {
        const dataString = data ? JSON.stringify(data) : '';
        return `${method}:${url}:${dataString}`;
    }

    _isExpired(item) {
        return Date.now() - item.timestamp > this.ttl;
    }

    _evictOldest() {
        if (this.accessOrder.length > 0) {
            const oldestKey = this.accessOrder.shift();
            this.cache.delete(oldestKey);
        }
    }

    get(method, url, data) {
        const key = this._generateKey(method, url, data);
        const item = this.cache.get(key);

        if (item && !this._isExpired(item)) {
            // تحديث access order
            this.accessOrder = this.accessOrder.filter(k => k !== key);
            this.accessOrder.push(key);
            
            return Promise.resolve(item.response);
        }

        return null;
    }

    set(method, url, data, response) {
        const key = this._generateKey(method, url, data);
        
        // إزالة العنصر القديم لو موجود
        if (this.cache.has(key)) {
            this.accessOrder = this.accessOrder.filter(k => k !== key);
        }

        // فحص الحد الأقصى
        if (this.cache.size >= this.maxSize) {
            this._evictOldest();
        }

        this.cache.set(key, {
            response: response,
            timestamp: Date.now()
        });
        
        this.accessOrder.push(key);
    }

    clear() {
        this.cache.clear();
        this.accessOrder = [];
    }
}

// HTTP Client مع Caching
class CachedHTTPClient {
    constructor() {
        this.cache = new XHRCache();
    }

    request(method, url, data = null, options = {}) {
        // فحص الـ cache أولاً
        if (method === 'GET' && !options.skipCache) {
            const cached = this.cache.get(method, url, data);
            if (cached) {
                console.log('تم جلب البيانات من الـ cache');
                return cached;
            }
        }

        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.responseType = 'json';

            if (options.headers) {
                Object.entries(options.headers).forEach(([key, value]) => {
                    xhr.setRequestHeader(key, value);
                });
            }

            xhr.onload = () => {
                if (xhr.status >= 200 && xhr.status < 300) {
                    const response = xhr.response;
                    
                    // حفظ في الـ cache للـ GET requests
                    if (method === 'GET') {
                        this.cache.set(method, url, data, response);
                    }
                    
                    resolve(response);
                } else {
                    reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
                }
            };

            xhr.onerror = () => reject(new Error('مشكلة في الشبكة'));

            if (data && method !== 'GET') {
                xhr.setRequestHeader('Content-Type', 'application/json');
                xhr.send(JSON.stringify(data));
            } else {
                xhr.send();
            }
        });
    }
}

// الاستخدام
const cachedClient = new CachedHTTPClient();

// أول طلب هيروح للسيرفر
cachedClient.request('GET', '/api/users')
    .then(data => console.log('من السيرفر:', data));

// تاني طلب هيجي من الـ cache
setTimeout(() => {
    cachedClient.request('GET', '/api/users')
        .then(data => console.log('من الـ cache:', data));
}, 1000);

5. Retry Logic مع Exponential Backoff#

class RetryableXHR {
    constructor(options = {}) {
        this.maxRetries = options.maxRetries || 3;
        this.baseDelay = options.baseDelay || 1000; // 1 ثانية
        this.maxDelay = options.maxDelay || 30000; // 30 ثانية
        this.retryCondition = options.retryCondition || this._defaultRetryCondition;
    }

    _defaultRetryCondition(xhr, attempt) {
        // إعادة المحاولة للأخطاء المؤقتة
        const retryableStatuses = [408, 429, 500, 502, 503, 504];
        return retryableStatuses.includes(xhr.status) || xhr.status === 0;
    }

    _calculateDelay(attempt) {
        // Exponential backoff مع jitter
        const delay = Math.min(
            this.baseDelay * Math.pow(2, attempt),
            this.maxDelay
        );
        
        // إضافة jitter عشان نتجنب الـ thundering herd
        const jitter = delay * 0.1 * Math.random();
        return delay + jitter;
    }

    request(method, url, data = null, options = {}) {
        return new Promise((resolve, reject) => {
            let attempt = 0;

            const makeRequest = () => {
                const xhr = new XMLHttpRequest();
                xhr.open(method, url, true);
                xhr.responseType = options.responseType || 'json';
                xhr.timeout = options.timeout || 10000;

                if (options.headers) {
                    Object.entries(options.headers).forEach(([key, value]) => {
                        xhr.setRequestHeader(key, value);
                    });
                }

                xhr.onload = () => {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        resolve(xhr.response);
                    } else if (attempt < this.maxRetries && this.retryCondition(xhr, attempt)) {
                        attempt++;
                        const delay = this._calculateDelay(attempt);
                        
                        console.log(`المحاولة ${attempt} فشلت، إعادة المحاولة خلال ${delay}ms`);
                        
                        setTimeout(makeRequest, delay);
                    } else {
                        reject(new Error(`فشل الطلب نهائياً بعد ${attempt} محاولات: ${xhr.status}`));
                    }
                };

                xhr.onerror = () => {
                    if (attempt < this.maxRetries) {
                        attempt++;
                        const delay = this._calculateDelay(attempt);
                        
                        console.log(`خطأ في الشبكة، إعادة المحاولة ${attempt} خلال ${delay}ms`);
                        
                        setTimeout(makeRequest, delay);
                    } else {
                        reject(new Error(`فشل نهائياً بعد ${attempt} محاولات: مشكلة في الشبكة`));
                    }
                };

                xhr.ontimeout = () => {
                    if (attempt < this.maxRetries) {
                        attempt++;
                        const delay = this._calculateDelay(attempt);
                        
                        console.log(`انتهت المهلة، إعادة المحاولة ${attempt} خلال ${delay}ms`);
                        
                        setTimeout(makeRequest, delay);
                    } else {
                        reject(new Error(`فشل نهائياً بعد ${attempt} محاولات: انتهت المهلة`));
                    }
                };

                if (data && method !== 'GET') {
                    xhr.setRequestHeader('Content-Type', 'application/json');
                    xhr.send(JSON.stringify(data));
                } else {
                    xhr.send();
                }
            };

            makeRequest();
        });
    }
}

// الاستخدام
const retryableClient = new RetryableXHR({
    maxRetries: 5,
    baseDelay: 500,
    retryCondition: (xhr, attempt) => {
        // إعادة المحاولة للـ server errors و rate limiting
        return xhr.status >= 500 || xhr.status === 429 || xhr.status === 0;
    }
});

retryableClient.request('GET', '/api/unreliable-endpoint')
    .then(data => {
        console.log('نجح الطلب:', data);
    })
    .catch(error => {
        console.error('فشل نهائياً:', error.message);
    });

أفضل الممارسات (Best Practices) 📋#

1. دايماً استخدم Error Handling شامل#

function robustXHR(method, url, data = null) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open(method, url, true);
        xhr.responseType = 'json';
        xhr.timeout = 10000;

        // تعامل مع كل الـ error cases
        xhr.onload = () => {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(xhr.response);
            } else {
                const error = new Error(`HTTP ${xhr.status}: ${xhr.statusText}`);
                error.status = xhr.status;
                error.response = xhr.response;
                reject(error);
            }
        };

        xhr.onerror = () => reject(new Error('مشكلة في الشبكة'));
        xhr.ontimeout = () => reject(new Error('انتهت مهلة الطلب'));
        xhr.onabort = () => reject(new Error('تم إلغاء الطلب'));

        if (data) {
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.send(JSON.stringify(data));
        } else {
            xhr.send();
        }
    });
}

2. استخدم responseType المناسب#

// للـ JSON
xhr.responseType = 'json';
const data = xhr.response; // object جاهز

// للـ Binary Data
xhr.responseType = 'arraybuffer';
const buffer = xhr.response; // ArrayBuffer

// للـ Files
xhr.responseType = 'blob';
const blob = xhr.response; // Blob object

3. تنظيف الـ Event Listeners#

function cleanXHR() {
    const xhr = new XMLHttpRequest();
    
    const cleanup = () => {
        xhr.onload = null;
        xhr.onerror = null;
        xhr.onprogress = null;
        xhr.ontimeout = null;
        xhr.onabort = null;
        xhr.onreadystatechange = null;
    };

    xhr.onload = function() {
        console.log('نجح الطلب');
        cleanup();
    };

    xhr.onerror = function() {
        console.error('فشل الطلب');
        cleanup();
    };

    return xhr;
}

4. استخدام AbortController للتحكم الأفضل#

class ModernXHR {
    static request(method, url, options = {}) {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            
            // دعم AbortController
            if (options.signal) {
                options.signal.addEventListener('abort', () => {
                    xhr.abort();
                    reject(new Error('تم إلغاء الطلب'));
                });
            }

            xhr.onload = () => {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve(xhr.response);
                } else {
                    reject(new Error(`HTTP ${xhr.status}`));
                }
            };

            xhr.onerror = () => reject(new Error('مشكلة في الشبكة'));
            xhr.send(options.data);
        });
    }
}

// الاستخدام مع AbortController
const controller = new AbortController();

ModernXHR.request('GET', '/api/data', {
    signal: controller.signal
})
.then(data => console.log(data))
.catch(error => console.error(error));

// إلغاء الطلب بعد 5 ثواني
setTimeout(() => {
    controller.abort();
}, 5000);

مقارنة XHR مع Fetch API ⚖️#

دلوقتي بعد ما فهمنا XHR كويس، خلينا نشوف إزاي بيقارن بـ Fetch API الحديثة:

1. البساطة في الكود#

XHR:#

const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.responseType = 'json';

xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.response);
    } else {
        console.error('خطأ:', xhr.status);
    }
};

xhr.onerror = function() {
    console.error('مشكلة في الشبكة');
};

xhr.send();

Fetch:#

fetch('/api/data')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return response.json();
    })
    .then(data => console.log(data))
    .catch(error => console.error('خطأ:', error));

الفائز: Fetch أبسط وأقل في الكود.

2. Upload Progress#

XHR:#

const xhr = new XMLHttpRequest();
xhr.upload.onprogress = function(event) {
    const percent = (event.loaded / event.total) * 100;
    console.log(`${percent}% تم الرفع`);
};
// الكود بيشتغل تمام

Fetch:#

// Fetch مش بيدعم upload progress مباشرة
// محتاج حلول معقدة أو استخدام Streams API
fetch('/upload', { method: 'POST', body: formData })
    .then(response => response.json());
// مفيش طريقة سهلة لـ progress tracking

الفائز: XHR لأنه بيدي تحكم كامل في الـ upload progress.

3. Request Cancellation#

XHR:#

const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.send();

// إلغاء الطلب
xhr.abort();

Fetch:#

const controller = new AbortController();

fetch('/api/data', { signal: controller.signal })
    .then(response => response.json())
    .catch(error => {
        if (error.name === 'AbortError') {
            console.log('تم الإلغاء');
        }
    });

// إلغاء الطلب
controller.abort();

التعادل: كلاهما بيدعم الإلغاء، بس XHR أبسط.

4. التوافق مع المتصفحات#

XHR:#

  • دعم ممتاز: بيشتغل في كل المتصفحات من IE5.5+
  • مستقر: موجود من سنين طويلة

Fetch:#

  • دعم حديث: مدعوم من IE لا، Chrome 42+، Firefox 39+
  • محتاج Polyfill: للمتصفحات القديمة

الفائز: XHR للتوافق الأوسع.

5. الأداء#

XHR:#

// مُحسن للمتصفحات القديمة
// استهلاك ذاكرة أقل في بعض الحالات

Fetch:#

// مبني على Promises (أسرع)
// دعم أفضل للـ Streaming
// تكامل أفضل مع async/await

الفائز: Fetch في المتصفحات الحديثة، XHR في القديمة.

6. مرونة الـ Configuration#

XHR:#

xhr.timeout = 5000;
xhr.withCredentials = true;
xhr.responseType = 'arraybuffer';
xhr.overrideMimeType('text/plain');

Fetch:#

fetch('/api', {
    signal: controller.signal,
    credentials: 'include',
    // مفيش timeout مباشر
    // مفيش responseType مباشر
});

الفائز: XHR لأنه بيدي تحكم أكتر في التفاصيل.

الخلاصة: امتى نستخدم إيه؟#

استخدم XHR لما:#

  • محتاج upload progress tracking
  • بتشتغل مع متصفحات قديمة
  • عايز تحكم دقيق في الـ timeout
  • محتاج تتعامل مع binary data بشكل معقد
  • عايز تعمل monitoring مفصل للطلبات

استخدم Fetch لما:#

  • بتكتب كود جديد للمتصفحات الحديثة
  • عايز كود أبسط وأقل
  • بتستخدم async/await
  • محتاج Streaming responses
  • عايز تكامل أفضل مع Service Workers

نصائح الأداء والـ Security 🛡️#

1. تحسين الأداء#

استخدم Connection Pooling#

class ConnectionPool {
    constructor(maxConnections = 6) {
        this.maxConnections = maxConnections;
        this.activeConnections = 0;
        this.queue = [];
    }

    request(config) {
        return new Promise((resolve, reject) => {
            if (this.activeConnections < this.maxConnections) {
                this._executeRequest(config, resolve, reject);
            } else {
                this.queue.push({ config, resolve, reject });
            }
        });
    }

    _executeRequest(config, resolve, reject) {
        this.activeConnections++;
        
        const xhr = new XMLHttpRequest();
        xhr.open(config.method, config.url, true);
        
        xhr.onload = () => {
            this.activeConnections--;
            this._processQueue();
            
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(xhr.response);
            } else {
                reject(new Error(`HTTP ${xhr.status}`));
            }
        };

        xhr.onerror = () => {
            this.activeConnections--;
            this._processQueue();
            reject(new Error('مشكلة في الشبكة'));
        };

        xhr.send(config.data);
    }

    _processQueue() {
        if (this.queue.length > 0 && this.activeConnections < this.maxConnections) {
            const { config, resolve, reject } = this.queue.shift();
            this._executeRequest(config, resolve, reject);
        }
    }
}

تحسين البيانات المرسلة#

// ضغط البيانات قبل الإرسال
function compressAndSend(data) {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/api/data', true);
    
    // استخدم gzip encoding لو متاح
    xhr.setRequestHeader('Content-Encoding', 'gzip');
    xhr.setRequestHeader('Content-Type', 'application/json');
    
    // تقليل حجم JSON
    const compressedData = JSON.stringify(data, null, 0); // بدون spaces
    
    xhr.send(compressedData);
}

2. الأمان (Security)#

تجنب XSS في الـ responses#

function safeXHR(url) {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.responseType = 'json'; // تجنب responseText للبيانات غير الموثوقة
    
    xhr.onload = function() {
        if (xhr.status === 200) {
            const data = xhr.response;
            
            // تنظيف البيانات قبل العرض
            const cleanData = sanitizeData(data);
            displayData(cleanData);
        }
    };
    
    xhr.send();
}

function sanitizeData(data) {
    // إزالة الـ HTML tags الخطيرة
    if (typeof data === 'string') {
        return data.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
    }
    return data;
}

التأكد من الـ SSL/TLS#

function secureRequest(url, data) {
    // التأكد إن الـ URL بيستخدم HTTPS
    if (!url.startsWith('https://') && location.protocol === 'https:') {
        throw new Error('لا يمكن إرسال طلبات HTTP من صفحة HTTPS');
    }
    
    const xhr = new XMLHttpRequest();
    xhr.open('POST', url, true);
    
    // إضافة CSRF token
    const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
    if (csrfToken) {
        xhr.setRequestHeader('X-CSRF-Token', csrfToken);
    }
    
    xhr.send(JSON.stringify(data));
}

Validate Response Headers#

function validateResponse(xhr) {
    // فحص Content-Type
    const contentType = xhr.getResponseHeader('Content-Type');
    if (!contentType || !contentType.includes('application/json')) {
        throw new Error('نوع المحتوى غير متوقع');
    }
    
    // فحص CORS headers
    const allowOrigin = xhr.getResponseHeader('Access-Control-Allow-Origin');
    if (allowOrigin && allowOrigin !== window.location.origin && allowOrigin !== '*') {
        console.warn('CORS header غير متطابق');
    }
    
    return true;
}

استخدامات متخصصة 🎯#

1. Binary Data Processing#

function downloadAndProcessImage(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.responseType = 'arraybuffer';

        xhr.onload = function() {
            if (xhr.status === 200) {
                const arrayBuffer = xhr.response;
                const uint8Array = new Uint8Array(arrayBuffer);
                
                // معالجة البيانات
                const processedData = processImageData(uint8Array);
                
                // تحويل لـ Blob
                const blob = new Blob([processedData], { type: 'image/jpeg' });
                
                // إنشاء URL للعرض
                const imageUrl = URL.createObjectURL(blob);
                
                resolve(imageUrl);
            } else {
                reject(new Error(`فشل التحميل: ${xhr.status}`));
            }
        };

        xhr.onerror = () => reject(new Error('مشكلة في الشبكة'));
        xhr.send();
    });
}

function processImageData(uint8Array) {
    // مثال بسيط: تحويل الصورة لـ grayscale
    for (let i = 0; i < uint8Array.length; i += 4) {
        const r = uint8Array[i];
        const g = uint8Array[i + 1];
        const b = uint8Array[i + 2];
        
        // حساب الـ grayscale
        const gray = 0.299 * r + 0.587 * g + 0.114 * b;
        
        uint8Array[i] = gray;     // Red
        uint8Array[i + 1] = gray; // Green
        uint8Array[i + 2] = gray; // Blue
        // Alpha channel (i + 3) يفضل زي ما هو
    }
    
    return uint8Array;
}

2. Long Polling للـ Real-time Updates#

class LongPoller {
    constructor(url, options = {}) {
        this.url = url;
        this.options = options;
        this.isPolling = false;
        this.xhr = null;
        this.retryDelay = options.retryDelay || 5000;
        this.onData = options.onData || (() => {});
        this.onError = options.onError || (() => {});
    }

    start() {
        if (this.isPolling) return;
        this.isPolling = true;
        this._poll();
    }

    stop() {
        this.isPolling = false;
        if (this.xhr) {
            this.xhr.abort();
        }
    }

    _poll() {
        if (!this.isPolling) return;

        this.xhr = new XMLHttpRequest();
        this.xhr.open('GET', this.url, true);
        this.xhr.responseType = 'json';
        this.xhr.timeout = 30000; // 30 ثانية

        this.xhr.onload = () => {
            if (this.xhr.status === 200) {
                const data = this.xhr.response;
                this.onData(data);
                
                // إعادة الـ polling فوراً
                setTimeout(() => this._poll(), 100);
            } else {
                this.onError(new Error(`HTTP ${this.xhr.status}`));
                // إعادة المحاولة بعد delay
                setTimeout(() => this._poll(), this.retryDelay);
            }
        };

        this.xhr.onerror = () => {
            this.onError(new Error('مشكلة في الشبكة'));
            setTimeout(() => this._poll(), this.retryDelay);
        };

        this.xhr.ontimeout = () => {
            this.onError(new Error('انتهت مهلة الطلب'));
            setTimeout(() => this._poll(), this.retryDelay);
        };

        this.xhr.send();
    }
}

// الاستخدام للـ notifications
const notificationPoller = new LongPoller('/api/notifications/check', {
    retryDelay: 3000,
    onData: (notifications) => {
        notifications.forEach(notification => {
            showNotification(notification.message);
        });
    },
    onError: (error) => {
        console.error('خطأ في الـ polling:', error);
    }
});

notificationPoller.start();

3. File Upload مع Drag & Drop#

class DragDropUploader {
    constructor(container, options = {}) {
        this.container = container;
        this.options = options;
        this.isDragging = false;
        this.uploadQueue = [];
        this.maxConcurrent = options.maxConcurrent || 3;
        this.activeUploads = 0;
        
        this._setupEventListeners();
    }

    _setupEventListeners() {
        // Drag events
        this.container.addEventListener('dragover', (e) => {
            e.preventDefault();
            this.isDragging = true;
            this.container.classList.add('dragging');
        });

        this.container.addEventListener('dragleave', (e) => {
            e.preventDefault();
            this.isDragging = false;
            this.container.classList.remove('dragging');
        });

        this.container.addEventListener('drop', (e) => {
            e.preventDefault();
            this.isDragging = false;
            this.container.classList.remove('dragging');
            
            const files = Array.from(e.dataTransfer.files);
            this._handleFiles(files);
        });

        // Click to upload
        this.container.addEventListener('click', () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.multiple = true;
            input.accept = this.options.accept || '*/*';
            
            input.addEventListener('change', (e) => {
                const files = Array.from(e.target.files);
                this._handleFiles(files);
            });
            
            input.click();
        });
    }

    _handleFiles(files) {
        files.forEach(file => {
            if (this._validateFile(file)) {
                this.uploadQueue.push(file);
                this._processQueue();
            }
        });
    }

    _validateFile(file) {
        // فحص نوع الملف
        if (this.options.accept && this.options.accept !== '*/*') {
            const acceptedTypes = this.options.accept.split(',');
            const fileType = file.type;
            
            if (!acceptedTypes.some(type => {
                if (type.endsWith('/*')) {
                    return fileType.startsWith(type.replace('/*', ''));
                }
                return fileType === type;
            })) {
                this.options.onError?.(`نوع الملف ${file.name} غير مدعوم`);
                return false;
            }
        }

        // فحص حجم الملف
        if (this.options.maxSize && file.size > this.options.maxSize) {
            this.options.onError?.(`الملف ${file.name} كبير جداً`);
            return false;
        }

        return true;
    }

    _processQueue() {
        while (this.uploadQueue.length > 0 && this.activeUploads < this.maxConcurrent) {
            const file = this.uploadQueue.shift();
            this._uploadFile(file);
        }
    }

    _uploadFile(file) {
        this.activeUploads++;
        
        const xhr = new XMLHttpRequest();
        const formData = new FormData();
        formData.append('file', file);
        
        // إضافة بيانات إضافية
        if (this.options.extraData) {
            Object.entries(this.options.extraData).forEach(([key, value]) => {
                formData.append(key, value);
            });
        }

        xhr.open('POST', this.options.uploadUrl, true);
        
        // إضافة headers
        if (this.options.headers) {
            Object.entries(this.options.headers).forEach(([key, value]) => {
                xhr.setRequestHeader(key, value);
            });
        }

        // متابعة التقدم
        xhr.upload.onprogress = (event) => {
            if (event.lengthComputable) {
                const percent = (event.loaded / event.total) * 100;
                this.options.onProgress?.(file, percent);
            }
        };

        xhr.onload = () => {
            this.activeUploads--;
            
            if (xhr.status >= 200 && xhr.status < 300) {
                const response = JSON.parse(xhr.responseText);
                this.options.onSuccess?.(file, response);
            } else {
                this.options.onError?.(`فشل رفع ${file.name}: ${xhr.status}`);
            }
            
            this._processQueue();
        };

        xhr.onerror = () => {
            this.activeUploads--;
            this.options.onError?.(`مشكلة في الشبكة أثناء رفع ${file.name}`);
            this._processQueue();
        };

        xhr.send(formData);
    }
}

// الاستخدام
const uploader = new DragDropUploader(document.getElementById('uploadArea'), {
    uploadUrl: '/api/upload',
    accept: 'image/*,application/pdf',
    maxSize: 10 * 1024 * 1024, // 10MB
    maxConcurrent: 2,
    extraData: {
        category: 'documents',
        userId: '123'
    },
    headers: {
        'Authorization': 'Bearer token'
    },
    onProgress: (file, percent) => {
        console.log(`${file.name}: ${percent.toFixed(1)}%`);
    },
    onSuccess: (file, response) => {
        console.log(`تم رفع ${file.name}:`, response);
    },
    onError: (error) => {
        console.error('خطأ:', error);
    }
});

4. API Rate Limiting مع Queue#

class RateLimitedXHR {
    constructor(options = {}) {
        this.maxRequests = options.maxRequests || 10;
        this.timeWindow = options.timeWindow || 60000; // 1 دقيقة
        this.queue = [];
        this.requestCount = 0;
        this.lastReset = Date.now();
        this.isProcessing = false;
    }

    request(method, url, data = null, options = {}) {
        return new Promise((resolve, reject) => {
            const request = {
                method,
                url,
                data,
                options,
                resolve,
                reject,
                timestamp: Date.now()
            };

            this.queue.push(request);
            this._processQueue();
        });
    }

    _processQueue() {
        if (this.isProcessing || this.queue.length === 0) return;

        // إعادة تعيين العداد كل timeWindow
        if (Date.now() - this.lastReset > this.timeWindow) {
            this.requestCount = 0;
            this.lastReset = Date.now();
        }

        // فحص إذا كان ممكن نعمل request جديد
        if (this.requestCount >= this.maxRequests) {
            // انتظار حتى نهاية الـ timeWindow
            const waitTime = this.timeWindow - (Date.now() - this.lastReset);
            setTimeout(() => this._processQueue(), waitTime);
            return;
        }

        this.isProcessing = true;
        const request = this.queue.shift();
        this.requestCount++;

        this._executeRequest(request);
    }

    _executeRequest(request) {
        const xhr = new XMLHttpRequest();
        xhr.open(request.method, request.url, true);
        xhr.responseType = 'json';

        // إضافة headers
        if (request.options.headers) {
            Object.entries(request.options.headers).forEach(([key, value]) => {
                xhr.setRequestHeader(key, value);
            });
        }

        xhr.onload = () => {
            this.isProcessing = false;
            
            if (xhr.status >= 200 && xhr.status < 300) {
                request.resolve(xhr.response);
            } else {
                request.reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
            }
            
            // معالجة الطلب التالي
            setTimeout(() => this._processQueue(), 100);
        };

        xhr.onerror = () => {
            this.isProcessing = false;
            request.reject(new Error('مشكلة في الشبكة'));
            setTimeout(() => this._processQueue(), 100);
        };

        if (request.data) {
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.send(JSON.stringify(request.data));
        } else {
            xhr.send();
        }
    }

    getQueueStatus() {
        return {
            queueLength: this.queue.length,
            requestCount: this.requestCount,
            maxRequests: this.maxRequests,
            timeWindow: this.timeWindow,
            timeUntilReset: Math.max(0, this.timeWindow - (Date.now() - this.lastReset))
        };
    }
}

// الاستخدام
const rateLimitedClient = new RateLimitedXHR({
    maxRequests: 5,
    timeWindow: 60000 // 5 requests per minute
});

// إرسال طلبات متعددة
for (let i = 0; i < 20; i++) {
    rateLimitedClient.request('GET', `/api/data/${i}`)
        .then(data => console.log(`Request ${i}:`, data))
        .catch(error => console.error(`Request ${i} failed:`, error));
}

// مراقبة حالة الـ queue
setInterval(() => {
    const status = rateLimitedClient.getQueueStatus();
    console.log('Queue status:', status);
}, 5000);

الخلاصة والتوصيات النهائية 🎯#

ما تعلمناه في الرحلة دي:#

  1. التاريخ والتطور: XHR بدأت من Microsoft في 1998 وطورت الويب التفاعلي
  2. الأساسيات: فهم الـ readyState، events، وmethods الأساسية
  3. الاستخدامات المتقدمة: upload progress، binary data، real-time updates
  4. أفضل الممارسات: error handling، security، performance optimization
  5. المقارنات: XHR vs Fetch API ومتى نستخدم إيه

متى تستخدم XHR؟ 🤔#

استخدم XHR لما:#

  • ✅ محتاج upload progress tracking
  • ✅ بتشتغل مع متصفحات قديمة (IE6+)
  • ✅ عايز تحكم دقيق في الـ timeout والـ headers
  • ✅ محتاج تتعامل مع binary data بشكل معقد
  • ✅ عايز تعمل monitoring مفصل للطلبات
  • ✅ محتاج دعم synchronous requests (مش recommended بس ممكن)

استخدم Fetch لما:#

  • ✅ بتكتب كود جديد للمتصفحات الحديثة
  • ✅ عايز كود أبسط وأقل
  • ✅ بتستخدم async/await بشكل مكثف
  • ✅ محتاج Streaming responses
  • ✅ عايز تكامل أفضل مع Service Workers

نصائح نهائية مهمة 💡#

1. دايماً فكر في الـ User Experience#

// مثال: إظهار loading state
function userFriendlyXHR(url) {
    const loadingEl = document.getElementById('loading');
    const contentEl = document.getElementById('content');
    
    loadingEl.style.display = 'block';
    contentEl.style.display = 'none';
    
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    
    xhr.onload = function() {
        loadingEl.style.display = 'none';
        contentEl.style.display = 'block';
        
        if (xhr.status === 200) {
            contentEl.innerHTML = xhr.responseText;
        } else {
            contentEl.innerHTML = '<p class="error">عذراً، حدث خطأ في تحميل البيانات</p>';
        }
    };
    
    xhr.onerror = function() {
        loadingEl.style.display = 'none';
        contentEl.innerHTML = '<p class="error">مشكلة في الاتصال بالإنترنت</p>';
    };
    
    xhr.send();
}

2. استخدم Error Boundaries#

class XHRErrorBoundary {
    static async request(config) {
        try {
            return await this._makeRequest(config);
        } catch (error) {
            this._handleError(error, config);
            throw error;
        }
    }

    static _handleError(error, config) {
        // تسجيل الخطأ
        console.error('XHR Error:', error);
        
        // إرسال للـ error tracking service
        if (window.errorTracker) {
            window.errorTracker.captureException(error, {
                tags: { component: 'xhr' },
                extra: { config }
            });
        }
        
        // إظهار رسالة للمستخدم
        this._showUserMessage(error);
    }

    static _showUserMessage(error) {
        let message = 'حدث خطأ غير متوقع';
        
        if (error.message.includes('Network')) {
            message = 'مشكلة في الاتصال بالإنترنت';
        } else if (error.message.includes('timeout')) {
            message = 'انتهت مهلة الطلب، يرجى المحاولة مرة أخرى';
        } else if (error.message.includes('401')) {
            message = 'يجب تسجيل الدخول مرة أخرى';
        }
        
        // إظهار notification للمستخدم
        this._showNotification(message, 'error');
    }

    static _showNotification(message, type) {
        const notification = document.createElement('div');
        notification.className = `notification ${type}`;
        notification.textContent = message;
        
        document.body.appendChild(notification);
        
        setTimeout(() => {
            notification.remove();
        }, 5000);
    }
}

3. اعمل Testing شامل#

// Unit Tests للـ XHR functions
describe('XHR Functions', () => {
    beforeEach(() => {
        // Mock XMLHttpRequest
        global.XMLHttpRequest = jest.fn(() => ({
            open: jest.fn(),
            send: jest.fn(),
            setRequestHeader: jest.fn(),
            onload: null,
            onerror: null,
            status: 200,
            response: '{"success": true}'
        }));
    });

    test('should make successful request', (done) => {
        const xhr = new XMLHttpRequest();
        xhr.onload = () => {
            expect(xhr.status).toBe(200);
            done();
        };
        
        xhr.open('GET', '/api/test');
        xhr.send();
        
        // Simulate response
        xhr.onload();
    });

    test('should handle network errors', (done) => {
        const xhr = new XMLHttpRequest();
        xhr.onerror = () => {
            expect(true).toBe(true);
            done();
        };
        
        xhr.open('GET', '/api/test');
        xhr.send();
        
        // Simulate error
        xhr.onerror();
    });
});

4. اعمل Documentation جيد#

/**
 * يقوم بجلب بيانات المستخدم من API
 * @param {number} userId - معرف المستخدم
 * @param {Object} options - خيارات إضافية
 * @param {number} options.timeout - مهلة الطلب بالمللي ثانية
 * @param {boolean} options.cache - هل يتم استخدام الـ cache
 * @returns {Promise<Object>} بيانات المستخدم
 * @throws {Error} في حالة فشل الطلب
 * 
 * @example
 * const user = await fetchUserData(123, { timeout: 5000 });
 * console.log(user.name); // "أحمد محمد"
 */
function fetchUserData(userId, options = {}) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', `/api/users/${userId}`, true);
        xhr.responseType = 'json';
        xhr.timeout = options.timeout || 10000;

        xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(xhr.response);
            } else {
                reject(new Error(`فشل جلب بيانات المستخدم: ${xhr.status}`));
            }
        };

        xhr.onerror = () => reject(new Error('مشكلة في الشبكة'));
        xhr.ontimeout = () => reject(new Error('انتهت مهلة الطلب'));

        xhr.send();
    });
}

المستقبل: إيه اللي جاي؟ 🔮#

1. Web Streams API#

// المستقبل: Streaming مع XHR
function streamData(url) {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    
    // دعم الـ streaming في المستقبل
    xhr.onprogress = function(event) {
        const chunk = event.target.responseText;
        // معالجة البيانات بشكل stream
        processChunk(chunk);
    };
    
    xhr.send();
}

2. Service Workers Integration#

// XHR مع Service Workers
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js').then(() => {
        // الـ XHR requests هتتم intercept من الـ Service Worker
        console.log('Service Worker registered');
    });
}

3. WebAssembly Integration#

// معالجة البيانات المعقدة مع WebAssembly
async function processWithWasm(data) {
    const wasmModule = await WebAssembly.instantiateStreaming(
        fetch('/processor.wasm')
    );
    
    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/api/process', true);
    xhr.responseType = 'arraybuffer';
    
    xhr.onload = function() {
        const result = wasmModule.instance.exports.process(xhr.response);
        console.log('تمت المعالجة:', result);
    };
    
    xhr.send(data);
}

الخاتمة 🎉#

XHR هي تقنية قوية ومرنة، وعلى الرغم من ظهور Fetch API، لسه لها مكان مهم في تطوير الويب. الفهم الجيد لـ XHR بيخليك مطور أقوى وأقدر تتعامل مع حالات معقدة.

النقاط المهمة اللي لازم تتذكرها:#

  1. XHR مرن: بيديك تحكم كامل في كل تفاصيل الطلب
  2. Error Handling مهم: دايماً تعامل مع كل أنواع الأخطاء
  3. Performance matters: استخدم الـ techniques المناسبة للتحسين
  4. Security first: فكر في الأمان قبل كل حاجة
  5. User Experience: خلي تجربة المستخدم سلسة ومريحة

كلمة أخيرة 💭#

التكنولوجيا بتتطور، بس المبادئ الأساسية بتبقى. XHR علمتنا إزاي نفكر في الـ asynchronous programming، error handling، والـ user experience. المهارات دي مهمة حتى لو استخدمنا تقنيات أحدث.


شكراً لقراءة المقالة! 🚀

أتمنى إن المقالة دي تكون ساعدتك تفهم XHR بشكل شامل. لو عندك أي أسئلة أو تعليقات، متترددش في التواصل معايا!

Join our whatsapp group here
My Channel here