Các thủ thuật mới trong XMLHttpRequest2

Giới thiệu

XMLHttpRequest là một trong những anh hùng thầm lặng trong vũ trụ HTML5. Nói một cách chính xác, XHR2 không phải là HTML5. Tuy nhiên, đây là một phần trong những điểm cải tiến gia tăng mà các nhà cung cấp trình duyệt đang thực hiện cho nền tảng cốt lõi. Tôi sẽ đưa XHR2 vào bộ công cụ mới của chúng ta vì nó đóng vai trò không thể thiếu trong các ứng dụng web phức tạp hiện nay.

Hóa ra, người bạn cũ của chúng ta đã được làm mới hoàn toàn nhưng nhiều người chưa biết đến các tính năng mới của ứng dụng này. XMLHttpRequest cấp 2 giới thiệu một loạt các tính năng mới giúp chấm dứt các cuộc tấn công phức tạp trong ứng dụng web của chúng ta; chẳng hạn như các yêu cầu trên nhiều nguồn gốc, sự kiện tải lên tiến trình và hỗ trợ tải lên/tải xuống dữ liệu nhị phân. Các API này cho phép AJAX hoạt động cùng với nhiều API HTML5 tiên tiến như API Hệ thống tệp, API Âm thanh trên web và WebGL.

Hướng dẫn này nêu bật một số tính năng mới trong XMLHttpRequest, đặc biệt là những tính năng có thể dùng để làm việc với tệp.

Tìm nạp dữ liệu

Việc tìm nạp tệp dưới dạng blob nhị phân đã gặp nhiều khó khăn với XHR. Về mặt kỹ thuật, điều này thậm chí còn không thể. Một mẹo đã được ghi nhận rõ ràng là ghi đè loại mime bằng một bộ ký tự do người dùng xác định như dưới đây.

Cách cũ để tìm nạp hình ảnh:

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

Mặc dù cách này hoạt động, nhưng nội dung bạn thực sự nhận được trong responseText không phải là một blob nhị phân. Đây là một chuỗi nhị phân đại diện cho tệp hình ảnh. Chúng ta đang lừa máy chủ truyền dữ liệu trở lại mà chưa được xử lý. Mặc dù viên ngọc nhỏ này hoạt động, nhưng tôi sẽ gọi nó là ma thuật đen và khuyên bạn không nên sử dụng. Bất cứ khi nào bạn dùng đến các thủ thuật mã ký tự và thao tác chuỗi để ép dữ liệu thành một định dạng mong muốn, đó đều là vấn đề.

Chỉ định định dạng phản hồi

Trong ví dụ trước, chúng ta đã tải hình ảnh xuống dưới dạng "tệp" nhị phân bằng cách ghi đè loại mime của máy chủ và xử lý văn bản phản hồi dưới dạng chuỗi nhị phân. Thay vào đó, hãy tận dụng các thuộc tính responseTyperesponse mới của XMLHttpRequest để thông báo cho trình duyệt về định dạng mà chúng ta muốn dữ liệu được trả về.

xhr.responseType
Trước khi gửi yêu cầu, hãy đặt xhr.responseType thành "text", "arraybuffer", "blob" hoặc "document", tuỳ thuộc vào nhu cầu dữ liệu của bạn. Lưu ý: việc đặt xhr.responseType = '' (hoặc bỏ qua) sẽ đặt mặc định phản hồi thành "văn bản".
xhr.response
Sau khi yêu cầu thành công, thuộc tính phản hồi của xhr sẽ chứa dữ liệu được yêu cầu dưới dạng DOMString, ArrayBuffer, Blob hoặc Document (tuỳ thuộc vào giá trị đã đặt cho responseType.)

Với tính năng mới tuyệt vời này, chúng ta có thể làm lại ví dụ trước, nhưng lần này, hãy tìm nạp hình ảnh dưới dạng Blob thay vì chuỗi:

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

Tốt hơn nhiều!

Phản hồi ArrayBuffer

ArrayBuffer là một vùng chứa có độ dài cố định chung cho dữ liệu nhị phân. Các mảng này rất hữu ích nếu bạn cần một vùng đệm dữ liệu thô chung, nhưng sức mạnh thực sự đằng sau các mảng này là bạn có thể tạo "thành phần hiển thị" của dữ liệu cơ bản bằng cách sử dụng mảng được nhập bằng JavaScript. Trên thực tế, bạn có thể tạo nhiều thành phần hiển thị từ một nguồn ArrayBuffer. Ví dụ: bạn có thể tạo một mảng số nguyên 8 bit có cùng ArrayBuffer với một mảng số nguyên 32 bit hiện có từ cùng một dữ liệu. Dữ liệu cơ bản vẫn giữ nguyên, chúng ta chỉ tạo các bản trình bày khác nhau của dữ liệu đó.

Ví dụ: đoạn mã sau đây sẽ tìm nạp hình ảnh tương tự dưới dạng ArrayBuffer, nhưng lần này, tạo một mảng số nguyên 8 bit chưa ký từ vùng đệm dữ liệu đó:

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

Phản hồi blob

Nếu bạn muốn làm việc trực tiếp với Blob và/hoặc không cần thao tác với bất kỳ byte nào của tệp, hãy sử dụng 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();

Bạn có thể sử dụng Blob ở một số vị trí, bao gồm cả việc lưu vào indexedDB, ghi vào Hệ thống tệp HTML5 hoặc tạo URL Blob, như trong ví dụ này.

Gửi dữ liệu

Việc có thể tải dữ liệu xuống ở nhiều định dạng là rất tuyệt vời, nhưng điều này sẽ không giúp ích gì nếu chúng ta không thể gửi các định dạng đa dạng thức này trở lại cơ sở (máy chủ). XMLHttpRequest đã giới hạn chúng tôi gửi dữ liệu DOMString hoặc Document (XML) trong một khoảng thời gian. Không còn nữa. Phương thức send() được cải tiến đã được ghi đè để chấp nhận bất kỳ loại nào sau đây: DOMString, Document, FormData, Blob, File, ArrayBuffer. Các ví dụ trong phần còn lại của phần này minh hoạ cách gửi dữ liệu bằng từng loại.

Gửi dữ liệu chuỗi: 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');

Không có gì mới ở đây, mặc dù đoạn mã bên phải có hơi khác. Hàm này đặt responseType='text' để so sánh. Xin nhắc lại, việc bỏ qua dòng đó sẽ mang lại kết quả tương tự.

Gửi biểu mẫu: xhr.send(FormData)

Có thể nhiều người đã quen sử dụng trình bổ trợ jQuery hoặc các thư viện khác để xử lý việc gửi biểu mẫu AJAX. Thay vào đó, chúng ta có thể sử dụng FormData, một loại dữ liệu mới khác được tạo cho XHR2. FormData thuận tiện cho việc tạo <form> HTML ngay lập tức trong JavaScript. Sau đó, bạn có thể gửi biểu mẫu đó bằng 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);
}

Về cơ bản, chúng ta chỉ tạo động một <form> và thêm các giá trị <input> vào đó bằng cách gọi phương thức nối.

Tất nhiên, bạn không cần tạo <form> từ đầu. Bạn có thể khởi tạo các đối tượng FormData từ HTMLFormElement hiện có trên trang. Ví dụ:

<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.
}

Biểu mẫu HTML có thể bao gồm các tệp tải lên (ví dụ: <input type="file">) và FormData cũng có thể xử lý việc đó. Bạn chỉ cần thêm(các) tệp và trình duyệt sẽ tạo một yêu cầu multipart/form-data khi send() được gọi:

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

Tải tệp hoặc blob lên: xhr.send(Blob)

Chúng ta cũng có thể gửi dữ liệu File hoặc Blob bằng XHR. Xin lưu ý rằng tất cả File đều là Blob, vì vậy, cả hai đều hoạt động ở đây.

Ví dụ này tạo một tệp văn bản mới từ đầu bằng hàm khởi tạo Blob() và tải Blob đó lên máy chủ. Mã này cũng thiết lập một trình xử lý để thông báo cho người dùng về tiến trình tải lên:

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

Tải một phần byte lên: xhr.send(ArrayBuffer)

Cuối cùng, chúng ta có thể gửi ArrayBuffer dưới dạng tải trọng của 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);
}

Chia sẻ tài nguyên trên nhiều nguồn gốc (CORS)

CORS cho phép các ứng dụng web trên một miền tạo các yêu cầu AJAX trên nhiều miền đến một miền khác. Việc bật tính năng này rất đơn giản, chỉ cần máy chủ gửi một tiêu đề phản hồi duy nhất.

Bật yêu cầu CORS

Giả sử ứng dụng của bạn nằm trên example.com và bạn muốn lấy dữ liệu từ www.example2.com. Thông thường, nếu bạn cố gắng thực hiện loại lệnh gọi AJAX này, yêu cầu sẽ không thành công và trình duyệt sẽ gửi lỗi không khớp nguồn gốc. Với CORS, www.example2.com có thể chọn cho phép các yêu cầu từ example.com bằng cách chỉ cần thêm một tiêu đề:

Access-Control-Allow-Origin: http://example.com

Bạn có thể thêm Access-Control-Allow-Origin vào một tài nguyên duy nhất trong một trang web hoặc trên toàn bộ miền. Để cho phép mọi miền gửi yêu cầu đến bạn, hãy đặt:

Access-Control-Allow-Origin: *

Trên thực tế, trang web này (html5rocks.com) đã bật CORS trên tất cả các trang. Khởi động Công cụ dành cho nhà phát triển và bạn sẽ thấy Access-Control-Allow-Origin trong phản hồi của chúng tôi:

Tiêu đề Access-Control-Allow-Origin trên html5rocks.com
Tiêu đề`Access-Control-Allow-Origin` trên html5rocks.com

Việc bật yêu cầu trên nhiều nguồn gốc rất dễ dàng, vì vậy, vui lòng bật CORS nếu dữ liệu của bạn là công khai!

Tạo yêu cầu trên nhiều miền

Nếu điểm cuối máy chủ đã bật CORS, thì việc tạo yêu cầu trên nhiều nguồn gốc sẽ không khác gì một yêu cầu XMLHttpRequest thông thường. Ví dụ: sau đây là yêu cầu mà example.com hiện có thể đưa ra cho 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();

Ví dụ thực tế

Tải xuống + lưu tệp vào hệ thống tệp HTML5

Giả sử bạn có một thư viện hình ảnh và muốn tìm nạp một loạt hình ảnh, sau đó lưu các hình ảnh đó trên máy bằng Hệ thống tệp HTML5. Một cách để thực hiện việc này là yêu cầu hình ảnh dưới dạng Blob và ghi các hình ảnh đó bằng 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();

Cắt một tệp và tải từng phần lên

Bằng cách sử dụng API tệp, chúng ta có thể giảm thiểu công sức tải một tệp lớn lên. Kỹ thuật này là cắt nội dung tải lên thành nhiều phần, tạo một XHR cho mỗi phần và kết hợp tệp trên máy chủ. Điều này tương tự như cách Gmail tải các tệp đính kèm lớn lên nhanh chóng. Bạn cũng có thể sử dụng kỹ thuật này để vượt qua giới hạn yêu cầu http 32 MB của 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);

})();

Không hiển thị ở đây là mã để tạo lại tệp trên máy chủ.

Tài liệu tham khảo