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

บทนำ

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

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

หากต้องการทำงานกับ 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> เข้าไปโดยเรียกใช้เมธอด 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 ดังนั้นจะใช้ 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 วิธีหนึ่งในการทำเช่นนี้คือการขอรูปภาพเป็น Blobs และเขียนรูปภาพโดยใช้ 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 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);

})();

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

ข้อมูลอ้างอิง