Introdução
Um dos heróis anônimos do universo HTML5 é XMLHttpRequest
.
Estritamente falando, XHR2 não é HTML5. No entanto, ele faz parte das melhorias incrementais
que os fornecedores de navegadores estão fazendo na plataforma principal.
Nosso velho amigo fez uma grande reforma, mas muitas pessoas não sabiam dos novos recursos. O XMLHttpRequest nível 2 introduz uma série de novos recursos que colocam o fim de invasões complicadas em nossos apps da Web, como solicitações de origem cruzada, upload de eventos de progresso e suporte para upload/download de dados binários. Isso permite que o AJAX funcione em conjunto com muitas APIs HTML5 de última geração, como API File System, API Web Audio e WebGL.
Neste tutorial, destacamos alguns dos novos recursos do XMLHttpRequest
,
especialmente os que podem ser usados para trabalhar com arquivos.
Buscando dados
Tem sido difícil buscar um arquivo como um blob binário com XHR. Tecnicamente, isso nem era possível. Um truque que tem sido bem documentado envolve a substituição do tipo MIME por um conjunto de caracteres definido pelo usuário, conforme mostrado abaixo.
O método antigo de buscar uma imagem é:
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();
Embora isso funcione, o que você realmente recebe no responseText
não é um blob binário. É uma string binária que representa o arquivo de imagem.
Estamos enganando o servidor para retornar os dados não processados.
Mesmo que esta pequena preciosidade funcione, vou chamá-la de magia negra e aconselhar sobre o contrário. Sempre que você recorre a hacks de código de caracteres e manipulação de
string para coagir os dados a um formato desejável, isso é um problema.
Como especificar um formato de resposta
No exemplo anterior, fizemos o download da imagem como um "arquivo" binário
substituindo o tipo MIME do servidor e processando o texto de resposta como uma string binária.
Em vez disso, vamos aproveitar as novas propriedades
responseType
e response
de XMLHttpRequest
para informar
ao navegador em que formato queremos que os dados sejam retornados.
- xhr.responseType
- Antes de enviar uma solicitação, defina
xhr.responseType
como "text", "arraybuffer", "blob" ou "document", dependendo das necessidades dos dados. Ao definirxhr.responseType = ''
(ou sua omissão), a resposta será padronizada como "texto". - xhr.response
- Após a solicitação, a propriedade de resposta do xhr conterá os dados solicitados como
DOMString
,ArrayBuffer
,Blob
ouDocument
(dependendo do que foi definido pararesponseType
).
Com essa nova novidade, podemos refazer o exemplo anterior, mas, desta vez,
buscamos a imagem como Blob
em vez de uma string:
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();
Muito melhor!
Respostas de ArrayBuffer
Um ArrayBuffer
é um contêiner genérico de comprimento fixo para dados binários. Eles são muito úteis se você
precisar de um buffer generalizado de dados brutos, mas a vantagem real por trás deles é que
é possível criar "visualizações" dos dados subjacentes usando matrizes tipadas em JavaScript.
Na verdade, várias visualizações podem ser criadas a partir de uma única origem de ArrayBuffer
.
Por exemplo, você pode criar uma matriz de números inteiros de 8 bits que compartilhe o mesmo ArrayBuffer
de uma matriz de números inteiros de 32 bits com os mesmos dados. Os dados subjacentes
permanecem os mesmos, apenas criamos representações diferentes deles.
Por exemplo, o comando a seguir busca a mesma imagem que um ArrayBuffer
,
mas cria uma matriz de números inteiros de 8 bits não assinado a partir desse buffer de dados:
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();
Respostas Blob
Se você quiser trabalhar diretamente com um Blob
e/ou
não precisar manipular os bytes do arquivo, use 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();
Um Blob
pode ser usado em vários lugares, incluindo salvá-lo
no indexedDB, gravá-lo no sistema de arquivos HTML5
ou criar um URL do Blob, como mostrado
neste exemplo.
Enviar dados
A capacidade de fazer o download de dados em diferentes formatos é ótima, mas não nos leva a lugar nenhum se não pudermos enviar esses formatos avançados de volta para a base inicial (o servidor).
XMLHttpRequest
nos limitou a enviar dados DOMString
ou Document
(XML) por algum tempo. Não teria. Um método send()
reformulado foi substituído para aceitar qualquer um destes tipos:
DOMString
, Document
, FormData
, Blob
,
File
ou ArrayBuffer
. Os exemplos no restante desta seção
demonstram o envio de dados usando cada tipo.
Envio de dados de string: 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');
Não há nada de novo aqui, embora o snippet à direita seja um pouco diferente.
Ele define responseType='text'
para comparação. Mais uma vez, omitir essa linha
gera os mesmos resultados.
Como enviar formulários: xhr.send(FormData)
Muitas pessoas provavelmente estão acostumadas a usar plug-ins jQuery ou outras bibliotecas para processar envios de formulários AJAX. Em vez disso, podemos usar FormData
,
outro novo tipo de dados desenvolvido para XHR2. O FormData
é prático para criar um <form>
HTML rapidamente, em JavaScript.
Esse formulário pode ser enviado usando 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);
}
Essencialmente, estamos apenas criando dinamicamente um <form>
e adicionando os valores
<input>
a ele, chamando o método de anexação.
Obviamente, você não precisa criar um <form>
do zero.
Objetos FormData
podem ser inicializados de um HTMLFormElement
atual na página. Exemplo:
<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.
}
Um formulário HTML pode incluir uploads de arquivos (por exemplo, <input type="file">
)
e FormData
também pode lidar com isso. Basta anexar os arquivos, e o navegador criará uma solicitação multipart/form-data
quando send()
for chamado:
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);
Como fazer upload de um arquivo ou blob: xhr.send(Blob)
Também podemos enviar dados File
ou Blob
usando XHR.
Lembre-se de que todos os File
s são Blob
s, então qualquer um funciona aqui.
Este exemplo cria um novo arquivo de texto do zero usando o construtor Blob()
e faz upload desse Blob
para o servidor. O código também configura um gerenciador
para informar o usuário sobre o progresso do upload:
<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'}));
Upload de um bloco de bytes: xhr.send(ArrayBuffer)
Por último, mas não menos importante, podemos enviar ArrayBuffer
s como o payload do 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);
}
Compartilhamento de recursos de origem cruzada (CORS)
O CORS permite que aplicativos da Web em um domínio façam solicitações AJAX entre domínios para outro. É muito simples ativar, exigindo apenas que um único cabeçalho de resposta seja enviado pelo servidor.
Como ativar solicitações CORS
Digamos que seu aplicativo esteja armazenado em example.com
e você
quer extrair dados de www.example2.com
. Normalmente, se você
tentasse fazer esse tipo de chamada AJAX, a solicitação falharia e o navegador
geraria um erro de incompatibilidade de origem. Com o CORS, www.example2.com
pode permitir solicitações de example.com
simplesmente adicionando um cabeçalho:
Access-Control-Allow-Origin: http://example.com
A Access-Control-Allow-Origin
pode ser adicionada a um único recurso em um site ou em todo o domínio. Para permitir que qualquer domínio faça
uma solicitação para você, defina:
Access-Control-Allow-Origin: *
De fato, este site (html5rocks.com) ativou o CORS em todas as suas páginas. Acione
as Ferramentas para desenvolvedores e verá o Access-Control-Allow-Origin
na nossa resposta:
É fácil ativar solicitações de origem cruzada. Ative o CORS se seus dados forem públicos.
Como fazer uma solicitação entre domínios
Se o endpoint do servidor tiver ativado o CORS, fazer a solicitação de origem cruzada
será igual a uma solicitação XMLHttpRequest
normal. Por exemplo, veja aqui uma solicitação que example.com
agora pode fazer para 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();
Exemplos práticos
Fazer o download e salvar arquivos no sistema de arquivos HTML5
Digamos que você tenha uma galeria de imagens e queira buscar várias imagens e salvá-las localmente usando o sistema de arquivos HTML5.
Uma maneira de fazer isso seria solicitar imagens como Blob
s
e gravá-las usando 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();
Dividir um arquivo e fazer upload de cada parte
Com as APIs File, podemos minimizar o trabalho de upload de um arquivo grande. A técnica é dividir o upload em vários fragmentos, gerar um XHR para cada parte e reunir o arquivo no servidor. O processo é semelhante ao modo como o Gmail faz upload de anexos grandes tão rapidamente. Essa técnica também pode ser usada para contornar o limite de solicitação http de 32 MB do 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);
})();
O que não é mostrado aqui é o código para reconstruir o arquivo no servidor.
Referências
- Especificação XMLHttpRequest nível 2
- Especificação de Compartilhamento de recursos entre origens (CORS)
- Especificação da API File
- Especificação da API FileSystem