اذهب إلى المحتوى

الكائن XMLHttpRequest في جافاسكربت


ابراهيم الخضور

هو كائن مدمَج في المتصفح، يسمح بإرسال طلبات HTTP في جافاسكريبت JavaScript، ويمكنه العمل مع أي نوع من البيانات، وليس فقط بيانات "XML" على الرغم من وجود الكلمة "XML" في تسميته. يساعدنا هذا الكائن في عمليات رفع وتنزيل الملفات وتتبع تقدمها وغير ذلك الكثير، ولا بدّ من الإشارة إلى وجود أسلوب أكثر حداثةً منه وهو استخدام fetch التي ألغت استخدامه بشكل أو بآخر.

يُستخدم XMLHttpRequest في تقنيات تطوير الويب العصرية لثلاثة أسباب، هي الآتية:

  1. أسباب تاريخية: عند الحاجة إلى دعم سكربت موجود يستخدم XMLHttpRequest.
  2. الحاجة إلى دعم المتصفحات القديمة دون استخدام شيفرة "polyfill".
  3. إنجاز شيء لا يمكن تنفيذه باستخدام fetch مثل تتبع تقدم رفع ملف.

إن كنت تحتاج ما ذكرناه تابع قراءة المقال وإلا فتوجه إلى فصل استخدام Fetch.

الأساسيات

يعمل الكائن XMLHttpRequest وفق النمطين المتزامن وغير المتزامن، سنتحدث بدايةً عن الوضع غير المتزامن، لأنه يستخدم في أغلب الحالات.

يُنجز طلب HTTP وفق 4 خطوات:

الخطوة الأولى، إنشاء الكائن XMLHttpRequest:

let xhr = new XMLHttpRequest();

ليس للدالة البانية أي وسطاء.

الخطوة الثانية، تهيئة الكائن، وتكون بعد إنشائه عادةً:

xhr.open(method, URL, [async, user, password])

يؤمن التابع open المعاملات الرئيسية للطلب، وهي:

  • method: تحدد نوع طلب HTTP، عادةً يكون GET أو POST.
  • URL: عنوان الموقع الذي نرسل الطلب إليه، ويمكن استخدام الكائن URL.
  • async: تكون العملية متزامنةً إذا أسندت لها القيمة false صراحةً، وسنغطي ذلك لاحقًا.
  • user وpassword: اسم المستخدم وكلمة المرور للاستيثاق، عند الحاجة.

لا يفتح التابع open الاتصال على الرغم من اسمه، بل يهيئ الطلب فقط، ولا يبدأ الاتصال عبر الشبكة إلا باستدعاء send.

الخطوة الثالثة، إرسال الطلب:

xhr.send([body])

يفتح هذا التابع الاتصال ويرسل الطلب إلى الخادم، حيث يحتوي المعامل body على جسم الطلب.ولا تمتلك الطلبات مثل GET جسماً، وتستخدم طلبات أخرى مثل POST الجسم لإرسال البيانات إلى الخادم، وسنرى أمثلةً عن ذلك لاحقًا.

الخطوة الرابعة، الاستماع إلى أحداث الكائن xhr لتلقي الاستجابة، وإليك أكثر الأحداث استخدامًا:

  • load: عندما يكتمل الطلب ونحصل على الاستجابة كاملةً، حتى لو أعاد الخادم رمز الحالة 500 أو 400 مثلًا.
  • error: عندما يتعذر تنفيذ الطلب، كأن تكون الشبكة معطلةً أو العنوان خاطئًا.
  • progress: ويقع دوريًا أثناء تنزيل الاستجابة، ويوضح الكمية التي نُزِّلت.
xhr.onload = function() {
  alert(`Loaded: ${xhr.status} ${xhr.response}`);
};

xhr.onerror = function() { // عندما لا تحدث الاستجابة إطلاقًا
  alert(`Network Error`);
};

xhr.onprogress = function(event) { // يقع دوريًا
  // event.loaded - الحجم الذي نزل بالبايت
  // event.lengthComputable = true إن أرسل الخادم ترويسة طول المحتوى
  // event.total - العدد الكلي للبايتات
  alert(`Received ${event.loaded} of ${event.total}`);
};

إليك مثالًا كاملًا، حيث تحمّل الشيفرة الموالية العنوان الآتي

 article/xmlhttprequest/example/load/

من الخادم، وتطبع تقدم العملية:

// 1. إنشاء الكائن
let xhr = new XMLHttpRequest();

// 2. URL /article/.../load  تهيئته مع العنوان
xhr.open('GET', '/article/xmlhttprequest/example/load');

// 3. إرسال الطلب عبر الشبكة 
xhr.send();

// 4. سيستدعى هذا الجزء عند تلقي الاستجابة
xhr.onload = function() {
  if (xhr.status != 200) { // analyze HTTP status of the response
    alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found
  } else { // show the result
    alert(`Done, got ${xhr.response.length} bytes`); // response is the server response
  }
};

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    alert(`Received ${event.loaded} of ${event.total} bytes`);
  } else {
    alert(`Received ${event.loaded} bytes`); // no Content-Length
  }

};

xhr.onerror = function() {
  alert("Request failed");
};

نستقبل نتيجة الطلب باستخدام خصائص الكائن xhr التالية حالما يستجيب الخادم:

  • status: رمز حالة HTTP، عدد، مثل 200 و404 و403 وهكذا، كما يمكن أن يكون 0 عند حدوث خطأ لا علاقة له بالنقل وفق بروتوكول HTTP.
  • statusText: رسالة حالة HTTP، نص، مثل OK لرمز الحالة 200 وNot Found لرمز الحالة 404، وForbidden لرمز الحالة 403.
  • response: قد تستخدم السكربتات القديمة responseText، وتمثل جسم الاستجابة التي يرسلها الخادم.

يمكن أن نحدد أيضًا زمن انتهاء timeout مستخدمين الخاصية الموافقة:

xhr.timeout = 10000; // زمن الانتهاء بالميلي ثانية

إن لم ينجح الطلب خلال الفترة الزمنية المحددة فسيُلغى وسيقع الحدث timeout.

إضافة معاملات بحث إلى العنوان URL

اقتباس

يمكننا استخدام الكائن URL، لإضافة معاملات بحث مثل: name=value?، والتأكد من ترميزها بطريقة صحيحة :

let url = new URL('https://google.com/search');
url.searchParams.set('q', 'test me!');

// 'q' ترميز المعامل 
xhr.open('GET', url); // https://google.com/search?q=test+me%21

نوع الاستجابة

من الممكن استخدام الخاصية ResponseType لضبط تنسيق الاستجابة:

  • "": القيمة الافتراضية، الحصول على الاستجابة كنص.
  • "text": الحصول على الاستجابة كنص.
  • "arraybuffer": الحصول على الاستجابة على شكل كائن ArrayBuffer، للحصول على بيانات ثنائية.
  • "blob": الحصول على الاستجابة على شكل كائن Blob، للحصول على بيانات ثنائية.
  • "document": الحصول على مستند XML، باستخدام اللغة XPath أو غيرها.
  • "json": الحصول على الاستجابة بصيغة JSON، تفسر الاستجابة تلقائيًا.

لنستقبل الاستجابة بصيغة JSON مثلًا:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/example/json');

xhr.responseType = 'json';

xhr.send();

//  {"message": "Hello, world!"} الاستجابة هي 
xhr.onload = function() {
  let responseObj = xhr.response;
  alert(responseObj.message); // Hello, world!
};
اقتباس

قد ترى في السكربتات القديمة الخاصية xhr.responseXML وكذلك xhr.responseText، وهي طرق موجودة قديمًا للحصول على نص أو مستند XML، ينبغي حاليًا تحديد تنسيق الاستجابة باستخدام xhr.responseType، واستقبالها باستخدام xhr.response كما أشرنا سابقًا.

حالات الجاهزية Ready States

يمر الكائن بعدة حالات عند تقدم تنفيذ الطلب، ويمكن الوصول إلى الحالة من خلال xhr.readyState، وإليك الحالات جميعها كما وردت في التوصيفات:

UNSENT = 0; // الحالة الأساسية
OPENED = 1; //open استدعاء التابع 
HEADERS_RECEIVED = 2; // استقبال ترويسة الاستجابة
LOADING = 3; // تلقي جسم الاستجابة
DONE = 4; // اكتمال الطلب

ينتقل الكائن XMLHttpRequest بين الحالات السابقة بالترتيب 0 ثم 1 ثم 2 ثم 3 ثم 4، وتتكرر الحالة 3 في كل مرة تُستقبل فيها حزمة بيانات عبر الشبكة، ويمكننا تتبع هذه الحالات باستخدام الحدث readystatechange.

xhr.onreadystatechange = function() {
  if (xhr.readyState == 3) {
    // تحميل
  }
  if (xhr.readyState == 4) {
    // انتهاء الطلب
  }
};

قد تجد مستمعات للأحداث readystatechange حتى في الشيفرات القديمة، وذلك لأنّ أحداثًا مثل load وغيرها، لم تكن موجودةً في فترة ما، لكن معالجات الأحداث load/error/progress قد ألغت استخدامها.

طلب الإلغاء Aborting request

يمكن إلغاء الطلب في أي لحظة، وذلك باستدعاء التابع ()xhr.abort :

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

يحرّض استدعاء التابع الحدث abort وتأخذ الخاصية xhr.status القيمة 0.

الطلبات المتزامنة

عند إسناد القيمة false إلى المعامل الثالث async للتابع open، فسيُنفَّذ الطلب بتزامن Synchronously. وبعبارة أخرى، ستتوقف JavaScript مؤقتًا عن التنفيذ عند إرسال الطلب ()send ثم تتابع عند تلقي الاستجابة، وهذا يشابه تعليمتي alert وprompt.

إليك المثال السابق وقد أعيدت كتابته مع وجود المعامل الثالث للتابع open:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/hello.txt', false);

try {
  xhr.send();
  if (xhr.status != 200) {
    alert(`Error ${xhr.status}: ${xhr.statusText}`);
  } else {
    alert(xhr.response);
  }
} catch(err) { // onerror بدلًا من
  alert("Request failed");
}

قد يبدو الأمر جيدًا لكنها طريقة يندر استخدامها، لأنها تعيق تنفيذ شيفرة JavaScript حتى يكتمل التحميل، وقد يصبح من المستحيل تمرير الصفحة لعرض بقية المحتويات أثناء تنفيذها في بعض المتصفحات، وقد يقترح المتصفح أيضًا إغلاق الصفحة العالقة إذا تأخر تنفيذ الطلب المتزامن كثيرًا.

ولا تتاح العديد من الإمكانيات المتقدمة للكائن XMLHttpRequest، مثل الطلب من نطاق آخر أو تحديد زمن الانتهاء من الطلب، عند إرسال الطلبات بتزامن، ولا مؤشرات أيضًا على تقدم العملية، لذلك لا تُنفّذ الطلبات المتزامنة إلا نادرًا.

ترويسات HTTP

يتيح الكائن XMLHttpRequest إرسال ترويسات مخصصة وقراءة ترويسات الاستجابة، وهنالك ثلاثة توابع للتعامل مع الترويسات:

  • (setRequestHeader(name, value: يعطي اسمًا name وقيمةً value لترويسة الطلب، وإليك مثالًا عن ذلك:
xhr.setRequestHeader('Content-Type', 'application/json')
اقتباس

محدودية الترويسات: تُدار بعض الترويسات حصريًا من قبل المتصفح مثل Referer وHost (ستجد القائمة الكاملة في التوصيفات)، فلا يسمح للكائن XMLHttpRequest بتغييرها، وذلك لضمان أمن المستخدم وصحة الطلب المُرسَل.

لا يمكن إزالة الترويسة: من ميزات الكائن XMLHttpRequest أيضًا عدم القدرة على إزالة الترويسة setRequestHeader، فحالما تضبط قيم الترويسة سينتهي الأمر، وستضيف بقية الاستدعاءات معلومات إلى الترويسة لكنها لن تلغي ما هو موجود.

فيما يلي مثال لتوضيح ذلك:

xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');
// ستكون الترويسة:
// X-Auth: 123, 456
  • (getResponseHeader(name: تعيد ترويسة الاستجابة ذات الاسم المحدد name، عدا Set-Cookie وSet-Cookie2، إليك مثالًا:
xhr.getResponseHeader('Content-Type')
  • ()getAllResponseHeaders: تعيد كل ترويسات الاستجابة، عدا Set-Cookie وSet-Cookie2، وتُعاد القيم ضمن سطر مفرد، حيث سيكون الفاصل هو "\r\n" (لا يتعلق بنظام تشغيل محدد) بين كل ترويستين، وبالتالي يمكن فصلها إلى ترويسات مفردة، وإليك مثالًا:
Cache-Control: max-age=31536000
Content-Length: 4260 
Content-Type: image/png 
Date: Sat, 08 Sep 2012 16:53:16 GMT

تفصل بين اسم الترويسة وقيمتها النقطتان المتعامدتان، يليها فراغ " :"، وهذا أمر ثابت في التوصيفات، فلو أردنا مثلًا الحصول على كائن له اسم وقيمةP فلا بد من تنفيذ الأمر بالشكل التالي، مفترضين أنه عند وجود ترويستين بنفس الاسم، وعندها ستحل الأخيرة منهما مكان الأولى:

let headers = xhr  
   .getAllResponseHeaders()  
   .split('\r\n')  
   .reduce((result, current) => {    
     let [name, value] = current.split(': ');
     result[name] = value;    
     return result;  
    }, {});

 // headers['Content-Type'] = 'image/png'

الطلب POST والكائن FormData

يمكن استخدام الكائن FormData لإرسال طلب HTTP-POST:

إليك الشيفرة اللازمة:

let formData = new FormData([form]); //<form> إنشاء كائن يُملأ اختيارًا من  
formData.append(name, value); // ربط الحقل

تنشئ الشيفرة السابقة الكائن FormData وتملؤه بقيم من نموذج form اختياريًا، وتضيف append حقولًا أخرى إن اقتضى الأمر، ومن ثم:

  1. نستخدم الأمر (...,'xhr.open('POST: لنؤسس الطلب POST.
  2. نستخدم الأمر (xhr.send(formData: لإرسال النموذج إلى الخادم.

إليك المثال التالي:

<form name="person">
  <input name="name" value="John">
  <input name="surname" value="Smith">
</form>

<script>
  // form ملؤها من 
  let formData = new FormData(document.forms.person);

  // إضافة حقل جديد
  formData.append("middle", "Lee");

  // إرساله
  let xhr = new XMLHttpRequest();
  xhr.open("POST", "/article/xmlhttprequest/post/user");
  xhr.send(formData);

  xhr.onload = () => alert(xhr.response);
</script>

يُرسَل النموذج بترميز multipart/form-data، فإذا أردنا استخدام JSON فلا بدّ من تنفيذ الأمر JSON.stringify ثم إرساله في هيئة نص، إلى جانب ضبط الترويسة Content-Type على القيمة application/json، وتفكِّك العديد من إطارات العمل مع الخادم محتوى JSON تلقائيًا بهذه الطريقة.

let xhr = new XMLHttpRequest();

let json = JSON.stringify({
  name: "John",
  surname: "Smith"
});

xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');

xhr.send(json);

ويمكن للتابع أن يُرسل أي جسم للطلب بما في ذلك كائنات Blob وBufferSource.

تقدم عمليات رفع البيانات

يقع الحدث progress في مرحلة التنزيل فقط، فلو نشرنا شيئًا ما باستخدام الطلب POST، فسيرفع الكائن XMLHttpRequest البيانات -جسم الطلب- أولًا ومن ثم ينزل الاستجابة.

وسيكون تتبع تقدم عملية رفع البيانات خاصةً إن كانت ضخمة؛ أمرًا هامًا، لكن لن نستفيد من الحدث xhr.onprogress في حالتنا، يوجد كائن آخر لا يمتلك توابعًا، وهو مخصص حصرًا لتتبع أحداث رفع البيانات وهو xhr.upload، الذي يولِّد أحداثًا كما يفعلxhr، لكنها تقع فقط عند رفع البيانات:

  • loadstart: يقع عندما يبدأ رفع البيانات.
  • progress: يقع دوريًا مع تقدم الرفع.
  • abort: يقع عند إلغاء الرفع.
  • error: يقع عند وقوع خطأ لا يتعلق بالبروتوكول HTTP.
  • load: يقع عند نجاح عملية الرفع.
  • timeout: يقع عند انتهاء الوقت المخصص لرفع البيانات، إذا ضُبطت الخاصية timeout.
  • loadend: يقع عند انتهاء الرفع بنجاح أو بإخفاق.

إليك أمثلةً عن معالجات هذه الأحداث:

xhr.upload.onprogress = function(event) {
  alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
};

xhr.upload.onload = function() {
  alert(`Upload finished successfully.`);
};

xhr.upload.onerror = function() {
  alert(`Error during the upload: ${xhr.status}`);
};

إليك أيضًا مثالًا واقعيًا عن رفع ملف مع مؤشرات على تقدم العملية:

<input type="file" onchange="upload(this.files[0])">

<script>
function upload(file) {
  let xhr = new XMLHttpRequest();

  //  تعقب تقدم عملية الرفع 
  xhr.upload.onprogress = function(event) {
    console.log(`Uploaded ${event.loaded} of ${event.total}`);
  };

  // تعقب الانتهاء، بنجاح أو إخفاق 
  xhr.onloadend = function() {
    if (xhr.status == 200) {
      console.log("success");
    } else {
      console.log("error " + this.status);
    }
  };

  xhr.open("POST", "/article/xmlhttprequest/post/upload");
  xhr.send(file);
}
</script>

الطلبات ذات الأصول المختلطة

يمكن للكائن XMLHttpRequest تنفيذ طلبات الأصل المختلط مستخدمًا سياسة CORS، تمامًا كما يفعل fetch، ولن يُرسل ملفات تعريف الارتباط cookies أو معلومات استيثاق إلى مواقع ذات أصل مختلف افتراضيًا. ولتمكين ذلك لا بدّ من ضبط الخاصية xhr.withCredentials على القيمة true.

let xhr = new XMLHttpRequest();
xhr.withCredentials = true;

xhr.open('POST', 'http://anywhere.com/request');
...

اطلع على فصل "استخدام Fetch مع الطلبات ذات الأصل المختلط" لمعلومات أكثر.

خلاصة

تمثل الشيفرة التالية، الشيفرة النموذجية لطلب GET باستخدام XMLHttpRequest

let xhr = new XMLHttpRequest();

xhr.open('GET', '/my/url');

xhr.send();

xhr.onload = function() {
  if (xhr.status != 200) { // HTTP error?
    // معالجة الخطأ
    alert( 'Error: ' + xhr.status);
    return;
  }

  //  xhr.response الحصول على الاستجابة من  
};

xhr.onprogress = function(event) {
  // إعطاء تقرير عن التقدم 
  alert(`Loaded ${event.loaded} of ${event.total}`);
};

xhr.onerror = function() {
  // handle non-HTTP error (e.g. network down)
};

هنالك أحداث أكثر في التوصيفات الحديثة، والتي وُضِعت في قائمة مرتبة وفق دورة حياة كل حدث:

  • loadstart: عندما يبدأ الطلب.
  • progress: عند وصول حزمة بيانات، وتكون الاستجابة الكاملة في هذه اللحظة ضمن الاستجابة response.
  • ()abort: عند إلغاء الطلب باستدعاء التابع.
  • error: عند وقوع خطأ في الاتصال، مثل اسم نطاق خاطئ ولم يحدث نتيجة خطأ HTTP مثل 404.
  • load: عند انتهاء الطلب بنجاح.
  • timeout: عند إلغاء الطلب نتيجة تجاوز الوقت المخصص، ويحدث عندما تُضبَط هذه الخاصية.
  • loadend: ويقع بعد الأحداث load أو error أو timeout أو abort، وهذه الأحداث متنافية فيما بينها، أي لا يمكن وقوع سوى حدث واحد منها.

إن أكثر الأحداث استخدامًا هما حدث إكمال التحميل load وحدث إخفاق التحميل error، كما يمكن استعمال معالج الحدث loadend والتحقق من خصائص الكائن xhr للتأكد من طبيعة الحدث الذي وقع.

لقد تعرفنا في هذا المقال أيضًا على الحدث readystatechange، والذي ظهر منذ زمن بعيد قبل استقرار التوصيفات، ولا حاجة حاليًا لاستخدامه، حيث يُستبدل بالأحداث الأكثر عصريةً. لكن مع ذلك قد نصادفه في بعض السكربتات القديمة.

إذا أردنا تعقب تقدم رفع البيانات، فلا بدّ من الاستماع إلى نفس أحداث التنزيل لكن باستخدام الكائن xhr.upload.

ترجمة -وبتصرف- للفصل XMLHttpRequest من سلسلة The Modern JavaScript Tutorial.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...