حيل جديدة في XMLHttpRequest2

مقدمة

من بين الأبطال المجهولين في عالم HTML5، XMLHttpRequest. من الناحية الدقيقة، لا يُعدّ XHR2 من تنسيق HTML5. ومع ذلك، يُعدّ ذلك جزءًا من التحسينات المتزايدة التي يُجريها مورّدو المتصفّحات على المنصة الأساسية. سأضيف XHR2 إلى مجموعة الميزات الجديدة لأنّه يلعب دورًا أساسيًا في تطبيقات الويب المعقّدة الحالية.

لقد أجرينا تغييرات كبيرة على هذه الميزة المألوفة، ولكن لا يعرف الكثير من المستخدمين ميزاتها الجديدة. XMLHttpRequest من المستوى 2: يقدّم XMLHttpRequest من المستوى 2 مجموعة كبيرة من الإمكانات الجديدة التي تضع حدًا للاختراقات المعقّدة في تطبيقات الويب، ويشمل ذلك طلبات من مصادر متعددة وأحداث تحميل التقدّم وإمكانية تحميل/تنزيل البيانات الثنائية. تسمح هذه التقنيات لواجهة AJAX بالعمل مع العديد من واجهات برمجة تطبيقات HTML5 الحديثة، مثل File System API وWeb Audio API وWebGL.

يسلّط هذا الدليل التعليمي الضوء على بعض الميزات الجديدة في XMLHttpRequest، ولا سيما تلك التي يمكن استخدامها للعمل مع الملفات.

استرجاع البيانات

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

الطريقة القديمة لجلب صورة:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);

// Hack to pass bytes through unprocessed.
xhr.overrideMimeType('text/plain; charset=x-user-defined');

xhr.onreadystatechange = function(e) {
  if (this.readyState == 4 && this.status == 200) {
    var binStr = this.responseText;
    for (var i = 0, len = binStr.length; i < len; ++i) {
      var c = binStr.charCodeAt(i);
      //String.fromCharCode(c & 0xff);
      var byte = c & 0xff;  // byte at offset i
    }
  }
};

xhr.send();

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

تحديد تنسيق الردّ

في المثال السابق، نزّلنا الصورة كـ "ملف" ثنائي، من خلال إلغاء نوع mime الخاص بالخادم ومعالجة نص الاستجابة كسلسلة ثنائية. بدلاً من ذلك، لنستفيد من السمتَين الجديدتَين responseType وresponse في XMLHttpRequest لإعلام المتصفّح بالتنسيق الذي نريد عرض البيانات به.

xhr.responseType
قبل إرسال طلب، اضبط xhr.responseType على "نص" أو "مخطّط بيانات صفيف" أو "مخطّط بيانات ثنائية الأبعاد" أو "مستند"، حسب احتياجات بياناتك. يُرجى العِلم أنّ ضبط القيمة xhr.responseType = '' (أو حذفها) سيؤدي تلقائيًا إلى ضبط الاستجابة على "نص".
xhr.response
بعد إتمام الطلب بنجاح، ستتضمّن سمة استجابة xhr البيانات المطلوبة على شكل DOMString أو ArrayBuffer أو Blob أو Document (حسب ما تم ضبطه لملف responseType).

باستخدام هذه الميزة الجديدة الرائعة، يمكننا إعادة صياغة المثال السابق، ولكن هذه المرة، سنسترِج الصورة كعنصر Blob بدلاً من سلسلة:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    // Note: .response instead of .responseText
    var blob = new Blob([this.response], {type: 'image/png'});
    ...
  }
};

xhr.send();

أفضل بكثير.

استجابات ArrayBuffer

ArrayBuffer هي حاوية عامة ذات طول ثابت للبيانات الثنائية. وهي مفيدة جدًا إذا كنت تحتاج إلى ملف تخزين مؤقت عام للبيانات الأولية، ولكن الميزة الحقيقية لهذه العناصر هي أنّه يمكنك إنشاء "طرق عرض" للبيانات الأساسية باستخدام صفائف JavaScript من النوع. في الواقع، يمكن إنشاء مشاهدات متعددة من مصدر ArrayBuffer واحد. على سبيل المثال، يمكنك إنشاء مصفوفة أعداد صحيحة بسعة 8 بت تتشارك ArrayBuffer نفسها مع مصفوفة أعداد صحيحة حالية بسعة 32 بت من البيانات نفسها. تظل البيانات الأساسية كما هي، ولكنّنا ننشئ تمثيلات مختلفة لها.

على سبيل المثال، يُستخدَم الرمز البرمجي التالي لاسترداد الصورة نفسها بتنسيق ArrayBuffer، ولكن هذه المرة، يتم إنشاء صفيف عدد صحيح غير موقَّت مكوّن من 8 بت من وحدة تخزين مؤقت للبيانات هذه:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  var uInt8Array = new Uint8Array(this.response); // this.response == uInt8Array.buffer
  // var byte3 = uInt8Array[4]; // byte at offset 4
  ...
};

xhr.send();

الردود على طلبات البحث عن محتوى

إذا كنت تريد العمل مباشرةً مع Blob و/أو لم تكن بحاجة إلى التلاعب بأي من وحدات البايت في الملف، استخدِم xhr.responseType='blob':

window.URL = window.URL || window.webkitURL;  // Take care of vendor prefixes.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    var blob = this.response;

    var img = document.createElement('img');
    img.onload = function(e) {
      window.URL.revokeObjectURL(img.src); // Clean up after yourself.
    };
    img.src = window.URL.createObjectURL(blob);
    document.body.appendChild(img);
    ...
  }
};

xhr.send();

يمكن استخدام Blob في عدد من المواضع، بما في ذلك حفظه في indexedDB أو كتابته في نظام الملفات في HTML5 أو إنشاء عنوان URL لملف Blob، كما هو موضّح في هذا المثال.

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

من الرائع أن نتمكّن من تنزيل البيانات بتنسيقات مختلفة، ولكنّ ذلك لن يؤدي إلى أي نتيجة مفيدة إذا لم نتمكّن من إرسال هذه التنسيقات الغنية إلى القاعدة الأساسية (الخادم). فرضت شركة XMLHttpRequest علينا إرسال بيانات DOMString أو Document (بتنسيق XML) لبعض الوقت. لم يعُد ذلك صحيحًا. تم إلغاء طريقة send() المعدَّلة لقبول أيّ من الأنواع التالية: DOMString وDocument وFormData وBlob File وArrayBuffer. توضِّح الأمثلة الواردة في بقية هذا القسم كيفية إرسال البيانات باستخدام كل نوع.

إرسال بيانات السلسلة: xhr.send(DOMString)

function sendText(txt) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) {
    if (this.status == 200) {
      console.log(this.responseText);
    }
  };

  xhr.send(txt);
}

sendText('test string');
function sendTextNew(txt) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.responseType = 'text';
  xhr.onload = function(e) {
    if (this.status == 200) {
      console.log(this.response);
    }
  };
  xhr.send(txt);
}

sendTextNew('test string');

ما مِن محتوى جديد هنا، إلا أنّ المقتطف الأيمن مختلف قليلاً. يتم ضبط responseType='text' للمقارنة. مرة أخرى، يؤدي حذف هذا السطر إلى الحصول على النتائج نفسها.

إرسال النماذج: xhr.send(FormData)

من المرجّح أنّ العديد من الأشخاص اعتادوا استخدام مكوّنات jQuery الإضافية أو مكتبات أخرى لمعالجة عمليات إرسال النماذج باستخدام AJAX. بدلاً من ذلك، يمكننا استخدام FormData، وهو نوع بيانات جديد آخر تم إنشاؤه لبروتوكول XHR2. FormData يُعدّ مناسبًا لإنشاء <form> HTML أثناء التشغيل، في JavaScript. يمكن بعد ذلك إرسال هذا النموذج باستخدام AJAX:

function sendForm() {
  var formData = new FormData();
  formData.append('username', 'johndoe');
  formData.append('id', 123456);

  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);
}

في الأساس، ما نفعله هو إنشاء <form> ديناميكيًا وإضافة قيم <input> إليه من خلال استدعاء طريقة الإضافة.

بالطبع، لست بحاجة إلى إنشاء <form> من البداية. يمكن إعداد عناصر FormData من HTMLFormElement حالية على الصفحة. على سبيل المثال:

<form id="myform" name="myform" action="/server">
  <input type="text" name="username" value="johndoe">
  <input type="number" name="id" value="123456">
  <input type="submit" onclick="return sendForm(this.form);">
</form>
function sendForm(form) {
  var formData = new FormData(form);

  formData.append('secret_token', '1234567890'); // Append extra data before send.

  var xhr = new XMLHttpRequest();
  xhr.open('POST', form.action, true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);

  return false; // Prevent page from submitting.
}

يمكن أن يتضمّن نموذج HTML عمليات تحميل ملفات (مثل <input type="file">) ويمكن أن يعالج FormData ذلك أيضًا. ما عليك سوى إلحاق الملفات وسينشئ المتصفّح طلب multipart/form-data عند استدعاء send():

function uploadFiles(url, files) {
  var formData = new FormData();

  for (var i = 0, file; file = files[i]; ++i) {
    formData.append(file.name, file);
  }

  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);  // multipart/form-data
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  uploadFiles('/server', this.files);
}, false);

تحميل ملف أو ملف نصي: xhr.send(Blob)

يمكننا أيضًا إرسال بيانات File أو Blob باستخدام طلبات HTTP المتعدّدة. يُرجى العِلم أنّ جميع File هي Blob، لذا يمكن استخدام أيّ منهما هنا.

ينشئ هذا المثال ملفًا نصيًا جديدًا من البداية باستخدام Blob() constructor ويحمّل هذا Blob إلى الخادم. يُعدّ الرمز أيضًا معالِجًا لإعلام المستخدم بتقدّم عملية التحميل:

<progress min="0" max="100" value="0">0% complete</progress>
function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  // Listen to the upload progress.
  var progressBar = document.querySelector('progress');
  xhr.upload.onprogress = function(e) {
    if (e.lengthComputable) {
      progressBar.value = (e.loaded / e.total) * 100;
      progressBar.textContent = progressBar.value; // Fallback for unsupported browsers.
    }
  };

  xhr.send(blobOrFile);
}

upload(new Blob(['hello world'], {type: 'text/plain'}));

تحميل مجموعة من البايتات: xhr.send(ArrayBuffer)

أخيرًا وليس آخرًا، يمكننا إرسال ArrayBuffers كحمولة طلب البيانات عبر HTTP.

function sendArrayBuffer() {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  var uInt8Array = new Uint8Array([1, 2, 3]);

  xhr.send(uInt8Array.buffer);
}

مشاركة الموارد المتعدّدة المصادر (CORS)

تسمح CORS لتطبيقات الويب في نطاق واحد بإجراء طلبات AJAX من نطاق إلى نطاق آخر. إنّ تفعيل هذه الميزة سهل جدًا، ولا يتطلّب سوى إرسال عنوان استجابة واحد من الخادم.

تفعيل طلبات سياسة مشاركة الموارد المتعددة المصادر (CORS)

لنفترض أنّ تطبيقك متوفّر على example.com وأحد التطبيقات الأخرى www.example2.com تريد سحب البيانات منه. في العادة، إذا حاولت إجراء هذا النوع من طلبات AJAX، سيتعذّر إكمال الطلب وسيُرسِل المتصفح خطأ عدم تطابق المصدر. باستخدام بروتوكول مشاركة الموارد المشتركة المنشأ (CORS)، يمكن www.example2.com اختيار السماح بالطلبات الواردة من example.com من خلال إضافة عنوان ببساطة:

Access-Control-Allow-Origin: http://example.com

يمكن إضافة Access-Control-Allow-Origin إلى مورد واحد ضمن موقع إلكتروني أو على مستوى النطاق بأكمله. للسماح لأي نطاق بتقديم طلب إليك، اضبط ما يلي:

Access-Control-Allow-Origin: *

في الواقع، فعَّل هذا الموقع الإلكتروني (html5rocks.com) تقنية CORS على جميع صفحاته. شغِّل أدوات المطوّرين وسيظهر لك الرمز Access-Control-Allow-Origin في ردّنا:

عنوان Access-Control-Allow-Origin على html5rocks.com
رأس Access-Control-Allow-Origin على html5rocks.com

من السهل تفعيل طلبات المصادر المتعددة، لذا يُرجى تفعيل طلبات CORS إذا كانت بياناتك علنية.

تقديم طلب على مستوى نطاقات متعددة

إذا كانت نقطة نهاية الخادم قد فعّلت بروتوكول CORS، لن يختلف تقديم طلب من مصدر مختلف عن طلب XMLHttpRequest عادي. على سبيل المثال، إليك طلب يمكن أن يقدّمه example.com الآن إلى www.example2.com:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.example2.com/hello.json');
xhr.onload = function(e) {
  var data = JSON.parse(this.response);
  ...
}
xhr.send();

أمثلة عملية

تنزيل الملفات وحفظها في نظام ملفات HTML5

لنفترض أنّ لديك معرض صور وتريد جلب مجموعة من الصور ثم حفظها على الجهاز باستخدام نظام ملفات HTML5. تتمثل إحدى طرق تحقيق ذلك في طلب الصور بتنسيق Blob وكتابتها باستخدام FileWriter:

window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

function onError(e) {
  console.log('Error', e);
}

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {

  window.requestFileSystem(TEMPORARY, 1024 * 1024, function(fs) {
    fs.root.getFile('image.png', {create: true}, function(fileEntry) {
      fileEntry.createWriter(function(writer) {

        writer.onwrite = function(e) { ... };
        writer.onerror = function(e) { ... };

        var blob = new Blob([xhr.response], {type: 'image/png'});

        writer.write(blob);

      }, onError);
    }, onError);
  }, onError);
};

xhr.send();

تقسيم ملف وتحميل كل جزء

باستخدام File APIs، يمكننا تقليل المجهود المبذول لتحميل ملف كبير. وتتم هذه العملية من خلال تقسيم عملية التحميل إلى أجزاء متعددة، وإنشاء طلب XHR لكل جزء، ثم تجميع الملف على الخادم. يشبه ذلك الطريقة التي يحمِّل بها Gmail المرفقات الكبيرة بسرعة كبيرة. يمكن أيضًا استخدام هذه التقنية للتغلب على الحد الأقصى لطلبات HTTP الذي يبلغ 32 ميغابايت في Google App Engine.

function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };
  xhr.send(blobOrFile);
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  var blob = this.files[0];

  const BYTES_PER_CHUNK = 1024 * 1024; // 1MB chunk sizes.
  const SIZE = blob.size;

  var start = 0;
  var end = BYTES_PER_CHUNK;

  while(start < SIZE) {
    upload(blob.slice(start, end));

    start = end;
    end = start + BYTES_PER_CHUNK;
  }
}, false);

})();

لا يظهر هنا الرمز البرمجي لإعادة إنشاء الملف على الخادم.

المراجع