טריקים חדשים ב-XMLHttpRequest2

מבוא

אחד מהגיבורים הלא מוכרים בעולם HTML5 הוא XMLHttpRequest. באופן מדויק, XHR2 הוא לא HTML5. עם זאת, הוא חלק מהשיפורים המצטברים שספקי הדפדפנים מבצעים בפלטפורמה המרכזית. הכנסתי את XHR2 לחבילת התוספים החדשה כי הוא חלק בלתי נפרד מאפליקציות האינטרנט המורכבות של היום.

מסתבר שהחבר הישן שלנו עבר שינוי משמעותי, אבל הרבה אנשים לא מודעים לתכונות החדשות שלו. XMLHttpRequest Level 2 מציגה מגוון יכולות חדשות שמסיימות את ההאקים המורכבים באפליקציות האינטרנט שלנו, כמו בקשות מ-origin שונים, אירועי התקדמות בהעלאה ותמיכה בהעלאה או בהורדה של נתונים בינאריים. הם מאפשרים ל-AJAX לפעול בשילוב עם הרבה ממשקי API מתקדמים של HTML5, כמו File System API,‏ Web Audio API ו-WebGL.

במדריך הזה נסקור כמה מהתכונות החדשות ב-XMLHttpRequest, במיוחד אלה שאפשר להשתמש בהן לעבודה עם קבצים.

אחזור נתונים

אחזור קובץ כ-blob בינארי היה תהליך קשה באמצעות 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 הוא לא blob בינארי. זוהי מחרוזת בינארית שמייצגת את קובץ התמונה. אנחנו מטעים את השרת כדי שיחזיר את הנתונים ללא עיבוד. אמנם הטריק הזה עובד, אבל אני אקרא לו 'קסם שחור' ואמליץ לא להשתמש בו. כל שימוש בהאקינג של קודי תווים ובמניפולציה של מחרוזות כדי לאלץ נתונים לעבור לפורמט רצוי הוא בעיה.

ציון פורמט התגובה

בדוגמה הקודמת, הורדנו את התמונה כ'קובץ' בינארי על ידי שינוי סוג ה-MIME של השרת ועיבוד טקסט התגובה כמחרוזת בינארית. במקום זאת, נשתמש במאפיינים החדשים responseType ו-response של XMLHttpRequest כדי להודיע לדפדפן באיזה פורמט אנחנו רוצים שהנתונים יחזרו.

xhr.responseType
לפני שליחת בקשה, מגדירים את xhr.responseType לערך 'text', 'arraybuffer', 'blob' או 'document', בהתאם לצורכי הנתונים שלכם. הערה: הגדרת 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, כתיבת אותו ב-File System של 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> באמצעות קריאה ל-method append.

כמובן, אין צורך ליצור <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 אם הנתונים שלכם גלויים לכולם.

שליחת בקשה בכמה דומיינים

אם נקודת הקצה (endpoint) של השרת מפעילה את 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 מעלה קבצים מצורפים גדולים במהירות רבה. אפשר להשתמש בשיטה כזו גם כדי לעקוף את המגבלה של Google App Engine על בקשות HTTP בגודל 32MB.

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

})();

הקוד לשחזור הקובץ בשרת לא מוצג כאן.

קובצי עזר