簡介
XMLHttpRequest
是 HTML5 宇宙中的一個無名英雄之一。嚴格說來,XHR2 並不是 HTML5。然而,這是瀏覽器廠商對核心平台所做的漸進式改善的一部分。在現今複雜的網路應用程式中,我把 XHR2 融入我們新推出的套裝應用程式中
因此可以發揮這項重要功效
後來,我們的老朋友經過大改造,但許多人 不知道其中有哪些新功能XMLHttpRequest 等級 2 推出了大量新功能,可針對網頁應用程式中的複雜的駭客手法進行全面性破壞,包括跨來源要求、上傳進度事件,以及上傳/下載二進位資料等。這些 API 可讓 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
設為「文字」、「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();
切割檔案並上傳每個部分
使用檔案 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 等級 2 規格
- 跨源資源共享 (CORS) 規格
- File API 規格
- FileSystem API 規格