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

מבוא

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

מתברר שהחבר הוותיק שלנו קיבל מהפך עצום, אבל אנשים רבים לא מודעים לתכונות החדשות שלו. XMLHttpRequest Level 2 כולל מגוון רחב של יכולות חדשות, שחוסמות פריצות מורכבות באפליקציות האינטרנט שלנו, כמו בקשות ממקורות שונים, העלאה של אירועי התקדמות ותמיכה בהעלאה/הורדה של נתונים בינאריים. התכונות האלה מאפשרות ל-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 של השרת ועיבוד טקסט התגובה כמחרוזת בינארית. במקום זאת, נשתמש במאפיינים החדשים של XMLHttpRequest responseType ו-response כדי להודיע לדפדפן באיזה פורמט רוצים שהנתונים יוחזרו.

xhr.responseType
לפני שליחת בקשה, צריך להגדיר את xhr.responseType בתור "text" , "arraybuffer" , "blob" או "document", בהתאם לצורכי הנתונים. שימו לב שאם מגדירים את הטקסט xhr.responseType = '' (או משמיטים אותו) כברירת מחדל התשובה ל-'text' מוגדרת כברירת מחדל.
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, כתיבתו ב-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 נוח ליצירה של קוד 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, כך ששניהם מתאימים כאן.

בדוגמה הזו נוצר קובץ טקסט חדש מאפס באמצעות ה-constructor של Blob() ומעלה את הקובץ Blob לשרת. הקוד גם מגדיר handler לעדכון המשתמשים בהתקדמות ההעלאה:

<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 בתור המטען הייעודי (payload) של 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();

חיתוך קובץ והעלאת כל חלק

באמצעות File APIs אנחנו יכולים לצמצם את העבודה על מנת להעלות קובץ גדול. השיטה הזו היא לפצל את ההעלאה לכמה מקטעים, להפעיל XHR לכל חלק ולחבר אותם בשרת. הדבר דומה לאופן שבו Gmail מעלה קבצים מצורפים גדולים כל כך מהר. אפשר להשתמש בשיטה כזו גם כדי לעקוף את מגבלת הבקשות של Google App Engine בנפח 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);

})();

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

קובצי עזר