حيل جديدة في XMLHttpRequest2

مقدمة

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

تبين أن الصديق القديم تعرض عملية تطوير كبيرة لكن الكثير من الأشخاص لا يعلمون بميزاته الجديدة. يقدّم XMLHttpRequest المستوى 2 مجموعة كبيرة من الإمكانات الجديدة التي تساعد في إنهاء عمليات الاختراق المعقّدة في تطبيقات الويب، مثل الطلبات المشتركة المصدر، وتحميل أحداث التقدّم، وإمكانية تحميل/تنزيل البيانات الثنائية. وهي تسمح لتطبيق AJAX بالعمل مع العديد من واجهات برمجة تطبيقات HTML5 المتطورة مثل واجهة برمجة تطبيقات نظام الملفات وواجهة برمجة تطبيقات Web Audio و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

إذا كنت تريد العمل مباشرةً على 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 سهل لإنشاء ملف HTML <form> بشكل آني وفي 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);

تحميل ملف أو كائن blob: xhr.send(Blob)

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

في هذا المثال، يتم إنشاء ملف نصي جديد من البداية باستخدام الدالة الإنشائية Blob() وتحميل 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)

أخيرًا وليس آخرًا، يمكننا إرسال ArrayBuffer كحمولة XHR.

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();

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

باستخدام واجهات برمجة تطبيقات الملفات، يمكننا تقليل العمل اللازم لتحميل ملف كبير. ويتمثل الأسلوب في تقسيم التحميل إلى أجزاء متعددة، وإنتاج 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);

})();

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

المراجع