XMLHttpRequest2 中的新秘訣

簡介

XMLHttpRequest 是 HTML5 宇宙中的一個無名英雄之一。嚴格說來,XHR2 並不是 HTML5。然而,這是瀏覽器廠商對核心平台所做的漸進式改善的一部分。在現今複雜的網路應用程式中,我把 XHR2 融入我們新推出的套裝應用程式中 因此可以發揮這項重要功效

後來,我們的老朋友經過大改造,但許多人 不知道其中有哪些新功能XMLHttpRequest 等級 2 推出了大量新功能,可針對網頁應用程式中的複雜的駭客手法進行全面性破壞,包括跨來源要求、上傳進度事件,以及上傳/下載二進位資料等。這些 API 可讓 AJAX 與許多邊緣 HTML5 API 搭配運作,例如 File System APIWeb Audio API 和 WebGL。

本教學課程重點介紹一些 XMLHttpRequest 的新功能,特別是可用來處理檔案的功能。

正在擷取資料

使用 XHR 時,將檔案擷取為二進位 blob 感覺很麻煩。從技術層面來說,根本不可能其中一個有詳盡記錄的技巧,就是使用使用者定義的字元集覆寫 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 的新 responseTyperesponse 屬性,告知瀏覽器要以哪種格式傳回資料。

xhr.responseType
在傳送要求之前,請根據資料需求,將 xhr.responseType 設為「文字」、「arraybuffer」、「blob」或「document」。請注意,設定 xhr.responseType = '' (或省略) 會將回應預設為「text」。
xhr.response
要求成功後,xhr 的回應屬性會包含 DOMStringArrayBufferBlobDocument (視 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 位元整數陣列,與相同資料中的現有 32 位元整數陣列共用相同的 ArrayBuffer。基礎資料會維持不變,我們只會建立不同的表示法。

舉例來說,下列程式碼會擷取與 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 檔案系統,或是建立 Blob 網址,如這個範例所示。

正在傳送資料

能夠以不同格式下載資料是很好的做法,但如果無法將這些互動式多媒體格式傳回主要伺服器 (伺服器),我們就不會隨時隨地下載資料。XMLHttpRequest 有限,我們無法傳送 DOMStringDocument (XML) 資料。然而今非昔比。經過修改的 send() 方法已經過覆寫,可接受下列任一類型:DOMStringDocumentFormDataBlobFileArrayBuffer。本節其餘內容的範例示範如何使用每種類型傳送資料。

正在傳送字串資料: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 可幫助您在 JavaScript 中即時建立 HTML <form>。接著,您可以使用 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 也可處理。只要附加檔案,瀏覽器在呼叫 send() 時就會建構 multipart/form-data 要求:

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)

我們還可以使用 XHR 傳送 FileBlob 資料。請注意,所有的 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

html5rocks.com 上的 Access-Control-Allow-Origin 標頭
html5rocks.com 上的「Access-Control-Allow-Origin」標頭

啟用跨來源要求很容易,因此如果您的資料是公開的,請啟用 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 快速上傳大型附件的方式類似。這類技術也可以用來規避 Google App Engine 的 32MB http 要求限制。

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

})();

這裡未顯示的程式碼是指在伺服器上重新建構檔案的程式碼。

參考資料