مقدمة: ليه XHR مهم؟ 🕸️
في عالم تطوير الويب، كان فيه زمان مشكلة كبيرة: إزاي نجيب بيانات من السيرفر من غير ما نعمل تحديث كامل للصفحة (reload)؟ تخيل إنك في التسعينات، عايز تعرض بيانات جديدة زي قايمة منتجات أو رسايل جديدة، بس لازم تعمل refresh للصفحة كلها، وده كان بيخلّي تجربة المستخدم بطيئة ومزعجة. هنا جات فكرة XMLHttpRequest
(اختصارًا XHR)، اللي غيّرت قواعد اللعبة وخلّت المواقع تفاعلية أكتر.
في المقالة دي، هناخدك في رحلة كاملة من أول تاريخ XHR وإزاي اتطورت، لحد إزاي تستخدمها في مشاريعك بشكل احترافي. هنبدأ بالتاريخ، وبعدين هشرح التقنية بالتفصيل، هديلك أمثلة عملية، وأخيرًا نقارنها بـ Fetch API. كل سكشن هيبقى متصل باللي قبله، عشان تفهم الصورة الكبيرة 🚀
التاريخ: إزاي بدأت فكرة XHR؟ 🕰️
المشكلة الأساسية (قبل 2000) 😓
في التسعينات، مواقع الويب كانت “ثابتة” (static). لو عايز تجيب بيانات من السيرفر، كان لازم تعمل تحديث كامل للصفحة. المشكلة كانت:
- عايز تجيب بيانات؟ لازم تعمل reload.
- تجربة المستخدم كانت بطيئة ومش مريحة.
- مكانش فيه طريقة تتواصل مع السيرفر من غير refresh.
- الكود كان معقد، زي كده:
// لازم تعمل 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) ويرجعلك بالبيانات من غير ما توقف الصفحة.
تخيل إنك بتطلب قهوة من مقهى:
- بتفتح الطلب (open): بتقول للويتر نوع القهوة.
- بتبعت الطلب (send): الويتر بيروح للمطبخ.
- بتستنى الرد (onreadystatechange): المطبخ بيجهز القهوة.
- بتاخد القهوة (response): لو كل حاجة تمام، القهوة بتوصلك.
- لو في مشكلة (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
الـ open بتاخد 3 parameters:
xhr.open(method, url, async); // method: 'GET', 'POST', 'PUT', etc. // url: رابط الـ API // async: true (asynchronous) أو false (synchronous)
ممكن تستخدم مع credentials:
xhr.open('GET', '/api/data', true, username, password);
بتجهز الطلب بس مش بتبعته:
- الـ
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؟ 🎯
التحكم الكامل في الطلبات:
- بيديك تحكم دقيق في الـ headers، progress، وevents.
- بيسمح بمعالجة البيانات الكبيرة (مثل progress للتحميل).
التوافق مع المتصفحات القديمة:
- XHR بيشتغل حتى في المتصفحات القديمة زي IE6.
دعم ميزات متقدمة:
- زي
onprogress
لمتابعة تحميل البيانات. - دعم
responseType
لمعالجة JSON، Blobs، وArrayBuffers.
- زي
مرونة في إدارة الـ State:
- بيخليك تتحكم في كل مرحلة من مراحل الطلب.
- ممكن تلغي الطلبات باستخدام
abort()
.
دعم 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 للـ synchronoususer
: (optional) اسم المستخدم للـ authenticationpassword
: (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);
الخلاصة والتوصيات النهائية 🎯
ما تعلمناه في الرحلة دي:
- التاريخ والتطور: XHR بدأت من Microsoft في 1998 وطورت الويب التفاعلي
- الأساسيات: فهم الـ readyState، events، وmethods الأساسية
- الاستخدامات المتقدمة: upload progress، binary data، real-time updates
- أفضل الممارسات: error handling، security، performance optimization
- المقارنات: 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 بيخليك مطور أقوى وأقدر تتعامل مع حالات معقدة.
النقاط المهمة اللي لازم تتذكرها:
- XHR مرن: بيديك تحكم كامل في كل تفاصيل الطلب
- Error Handling مهم: دايماً تعامل مع كل أنواع الأخطاء
- Performance matters: استخدم الـ techniques المناسبة للتحسين
- Security first: فكر في الأمان قبل كل حاجة
- User Experience: خلي تجربة المستخدم سلسة ومريحة
كلمة أخيرة 💭
التكنولوجيا بتتطور، بس المبادئ الأساسية بتبقى. XHR علمتنا إزاي نفكر في الـ asynchronous programming، error handling، والـ user experience. المهارات دي مهمة حتى لو استخدمنا تقنيات أحدث.
شكراً لقراءة المقالة! 🚀
أتمنى إن المقالة دي تكون ساعدتك تفهم XHR بشكل شامل. لو عندك أي أسئلة أو تعليقات، متترددش في التواصل معايا!