เทคนิคใหม่ใน XMLHttpRequest2

เกริ่นนำ

หนึ่งในฮีโร่ที่ยังไม่ได้ร้องในจักรวาล HTML5 คือ XMLHttpRequest พูดง่ายๆ ก็คือ XHR2 ไม่ใช่ HTML5 แต่นี่เป็นส่วนหนึ่งของการปรับปรุงเพิ่มเติมที่ผู้ให้บริการเบราว์เซอร์ทำกับแพลตฟอร์มหลัก ผมได้รวม XHR2 ไว้ในกระเป๋าสารพัดอันใหม่ของเรา เพราะว่าแอปนี้มีบทบาทสำคัญมากในเว็บแอปที่ซับซ้อนในปัจจุบัน

ปรากฏว่าเพื่อนเก่าของเรามีการแปลงโฉมครั้งใหญ่ แต่หลายๆ คนยังไม่ทราบเกี่ยวกับฟีเจอร์ใหม่ XMLHttpRequest ระดับ 2 เปิดตัวความสามารถใหม่ๆ มากมายเพื่อขจัดการแฮ็กที่ซับซ้อนในเว็บแอปของเรา สิ่งต่างๆ เช่น คำขอข้ามต้นทาง การอัปโหลดเหตุการณ์ความคืบหน้า และการรองรับการอัปโหลด/ดาวน์โหลดข้อมูลไบนารี ซึ่งช่วยให้ AJAX ทำงานร่วมกับ API ของ HTML5 แบบ Bleed Edge ได้หลายรายการ เช่น 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 เขียนลงใน ระบบไฟล์ 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();

ตัดไฟล์และอัปโหลดแต่ละส่วน

การใช้ API ไฟล์ช่วยให้เราลดเวลาในการอัปโหลดไฟล์ขนาดใหญ่ได้ เทคนิคคือแบ่งการอัปโหลดออกเป็นหลายๆ ส่วน สร้าง XHR สำหรับแต่ละส่วน แล้วรวมไฟล์ไว้ด้วยกันบนเซิร์ฟเวอร์ ซึ่งคล้ายกับวิธีที่ Gmail อัปโหลดไฟล์แนบขนาดใหญ่ได้อย่างรวดเร็ว เทคนิคดังกล่าวอาจช่วยหลีกเลี่ยงขีดจำกัดคำขอ http ที่ 32 MB ของ 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);

})();

สิ่งที่ไม่ได้แสดงที่นี่คือโค้ดสำหรับสร้างไฟล์ใหม่บนเซิร์ฟเวอร์

รายการอ้างอิง