XMLHttpRequest2 中的新秘訣

簡介

XMLHttpRequest 是 HTML5 領域中默默無聞的英雄。嚴格來說,XHR2 並非 HTML5。不過,這項功能是瀏覽器供應商對核心平台所做的漸進式改善措施之一。我會在新的好康包中加入 XHR2,因為它在當今複雜的網路應用程式中扮演著不可或缺的角色。

原來這位老朋友經過了大幅改造,但許多人並不知道它有哪些新功能。XMLHttpRequest Level 2 推出了一系列新功能,可終結網頁應用程式中複雜的駭客攻擊,例如跨來源要求、上傳進度事件,以及上傳/下載二進位資料的支援功能。這些功能可讓 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 設為「text」、「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();

切割檔案並上傳各部分

使用 File 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);

})();

這裡沒有顯示的部分是用於在伺服器上重建檔案的程式碼。

參考資料