簡介
XMLHttpRequest
是 HTML5 領域中默默無聞的英雄。嚴格來說,XHR2 並非 HTML5。不過,這項功能是瀏覽器供應商對核心平台所做的漸進式改善措施之一。我會在新的好康包中加入 XHR2,因為它在當今複雜的網路應用程式中扮演著不可或缺的角色。
原來這位老朋友經過了大幅改造,但許多人並不知道它有哪些新功能。XMLHttpRequest Level 2 推出了一系列新功能,可終結網頁應用程式中複雜的駭客攻擊,例如跨來源要求、上傳進度事件,以及上傳/下載二進位資料的支援功能。這些功能可讓 AJAX 與許多最新的 HTML5 API 協同運作,例如 File System API、Web 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
的新 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 位元整數陣列,讓該陣列與來自相同資料的現有 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
已限制我們在一段時間內傳送 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
可讓您在 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 傳送 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
:
啟用跨來源要求很簡單,因此如果您的資料是公開的,請務必啟用 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);
})();
這裡沒有顯示的部分是用於在伺服器上重建檔案的程式碼。
參考資料
- XMLHttpRequest Level 2 規格
- 跨源資源共享 (CORS) 規範
- File API 規格
- FileSystem API 規格