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

Giới thiệu

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

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

Phần 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à các tính năng có thể dùng để làm việc với các tệp.

Đang tìm nạp dữ liệu

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

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

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 có hiệu quả, nhưng trên thực tế những gì bạn nhận lại 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 tôi đang lừa máy chủ chuyển dữ liệu về nhưng chưa được xử lý. Mặc dù viên ngọc nhỏ này có tác dụng, nhưng tôi vẫn sẽ gọi đó là ma thuật và khuyên bạn không nên. Bất cứ khi nào bạn dùng đến việc tấn công mã ký tự và thao túng chuỗi để ép buộc dữ liệu thành định dạng mong muốn, thì đó đề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 định dạng mà 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" (văn bản), "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ẽ mặc định phản hồi là "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 dữ liệu đã được thiết lập 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ì một 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();

Đẹp hơn nhiều!

Phản hồi ArrayBuffer

ArrayBuffer là vùng chứa có độ dài cố định chung cho dữ liệu nhị phân. Các định dạng này cực kỳ hữu ích nếu bạn cần một vùng đệm tổng quát của dữ liệu thô, nhưng sức mạnh thực sự đằng sau những kỹ thuật 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 đã nhập bằng JavaScript. Trên thực tế, bạn có thể tạo nhiều khung 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 dùng chung 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 tôi chỉ tạo các bản trình bày khác nhau.

Ví dụ: lệnh sau đây sẽ tìm nạp cùng một hình ảnh như ArrayBuffer, nhưng lần này sẽ 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 nhanh

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

Đang gửi dữ liệu

Có thể tải xuống dữ liệu ở nhiều định dạng là rất tốt, nhưng chúng tôi không đi đến đâu nếu không thể gửi các định dạng đa dạng này về cơ sở dữ liệu gốc (máy chủ). XMLHttpRequest đã hạn chế chúng tôi gửi dữ liệu DOMString hoặc Document (XML) trong một thời gian. Không còn nữa. Phương thức send() cải tiến đã bị 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 theo từng kiểu.

Đang 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 hơi khác. Phương thức này đặt responseType='text' để so sánh. Một lần nữa, 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)

Nhiều người có thể đã quen với việc 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 hình thành cho XHR2. FormData rất thuận tiện cho việc tạo HTML <form> một cách nhanh chóng trong JavaScript. Sau đó, biểu mẫu đó có thể được gửi 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ự động tạo một <form> và xử lý các giá trị <input> đối với đối tượng đó 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 và 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 tệp tải lên (ví dụ: <input type="file">) và FormData cũng có thể xử lý việc này. Bạn chỉ cần thêm(các) tệp và trình duyệt sẽ tạo 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 nên cả hai đều sẽ hoạt động ở đây.

Ví dụ này sẽ tạo một tệp văn bản mới từ đầu bằng cách sử dụ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 đoạn byte lên: xhr.send(ArrayBuffer)

Cuối cùng nhưng không kém phần quan trọ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 thực hiện 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ỉ yêu cầu máy chủ gửi một tiêu đề phản hồi.

Bật yêu cầu CORS

Giả sử ứng dụng của bạn có 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, thì yêu cầu sẽ không thành công và trình duyệt sẽ gửi lỗi nguồn gốc không khớp. 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 thêm 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 any 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 (html5rock.com) đã bật CORS trên tất cả các trang của trang web này. Kích hoạt Công cụ 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 html5rock.com
"Access-Control-Allow-Origin" tiêu đề "Access-Control-Allow-Origin" trên html5rock.com

Việc bật yêu cầu 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 ở chế độ công khai!

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

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

Khi sử dụng API Tệp, chúng ta có thể giảm thiểu công việc tải tệp lớn lên. Kỹ thuật là chia tệp tải lên thành nhiều phần, tạo XHR cho mỗi phần và đặt tệp lại với nhau 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 một cách nhanh chóng. Kỹ thuật như vậy cũng có thể được dùng để 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);

})();

Thông tin không được hiển thị ở đây là mã để tạo lại tệp trên máy chủ.

Tài liệu tham khảo