Introdução
Um dos heróis desconhecidos do universo HTML5 é o XMLHttpRequest
.
O XHR2 não é HTML5. No entanto, isso faz parte das melhorias incrementais
que os fornecedores de navegadores estão fazendo na plataforma principal. Estou incluindo o XHR2 na nossa nova sacola de
brindes porque ele desempenha uma parte importante nos apps da Web complexos de hoje.
Nosso velho amigo passou por uma grande transformação, mas muitas pessoas não sabem dos novos recursos. O XMLHttpRequest Level 2 apresenta vários novos recursos que acabam com hacks complicados nos nossos apps da Web, como solicitações entre origens, eventos de upload de progresso e suporte para upload/download de dados binários. Elas permitem que o AJAX funcione em conjunto com muitas das APIs HTML5 mais recentes, como a API File System, a API Web Audio e o WebGL.
Este tutorial destaca alguns dos novos recursos do XMLHttpRequest
,
principalmente aqueles que podem ser usados para trabalhar com arquivos.
Buscando dados
Buscar um arquivo como um blob binário foi difícil com o XHR. Tecnicamente, isso nem era possível. Um truque bem documentado envolve substituir o tipo MIME por um charset definido pelo usuário, conforme mostrado abaixo.
A maneira antiga 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ê recebe de volta no responseText
não é um blob binário. É uma string binária que representa o arquivo de imagem.
Estamos fazendo com que o servidor transmita os dados de volta sem processá-los.
Embora essa pequena joia funcione, vou chamá-la de magia negra e aconselhar
contra ela. Sempre que você recorrer a hacks de código de caracteres e manipulação de strings
para forçar dados em um formato desejável, isso será 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 qual formato queremos que os dados sejam retornados.
- xhr.responseType
- Antes de enviar uma solicitação, defina o
xhr.responseType
como "text", "arraybuffer", "blob" ou "document", dependendo das suas necessidades de dados. A configuraçãoxhr.responseType = ''
(ou a omissão dela) vai definir a resposta como "text". - xhr.response
- Depois de uma solicitação bem-sucedida, a propriedade de resposta do XHR vai
conter os dados solicitados como
DOMString
,ArrayBuffer
,Blob
ouDocument
(dependendo do que foi definido pararesponseType
).
Com essa nova funcionalidade, podemos retrabalhar o exemplo anterior, mas dessa vez,
buscar a imagem como um 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 o verdadeiro poder por trás deles é que
é possível criar "visualizações" dos dados subjacentes usando matrizes tipadas do JavaScript.
Na verdade, é possível criar várias visualizações com uma única origem ArrayBuffer
.
Por exemplo, é possível criar uma matriz de números inteiros de 8 bits que compartilhe o mesmo ArrayBuffer
que uma matriz de números inteiros de 32 bits dos mesmos dados. Os dados subjacentes
continuam os mesmos, mas criamos representações diferentes deles.
Como exemplo, o código a seguir busca a mesma imagem como ArrayBuffer
,
mas dessa vez, cria uma matriz de números inteiros de 8 bits não assinados 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 de blobs
Se você quiser trabalhar diretamente com um Blob
e/ou
não precisar manipular nenhum dos 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 o armazenamento
em indexedDB, a gravação no sistema de arquivos do HTML5
ou a criação de um URL de blob, como mostrado
neste exemplo.
Como enviar dados
É ótimo poder fazer o download de dados em formatos diferentes, mas não
vamos chegar a lugar nenhum se não pudermos enviar esses formatos avançados de volta à base (o servidor).
O XMLHttpRequest
limitou o envio de dados DOMString
ou Document
(XML) por algum tempo. Isso já não é mais necessário. Um método send()
reformulado foi substituído para aceitar qualquer um dos seguintes tipos:
DOMString
, Document
, FormData
, Blob
,
File
e ArrayBuffer
. Os exemplos no restante desta
seção demonstram o envio de dados usando cada tipo.
Como enviar 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, mas o snippet à direita é um pouco diferente.
Ele define responseType='text'
para comparação. Novamente, omitir essa linha
produz os mesmos resultados.
Como enviar formulários: xhr.send(FormData)
Muitas pessoas provavelmente estão acostumadas a usar plug-ins do jQuery
ou outras bibliotecas para processar envios de formulários AJAX. Em vez disso, podemos usar FormData
,
outro novo tipo de dados concebido para XHR2. FormData
é conveniente para criar um <form>
HTML em tempo real 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);
}
Basicamente, estamos criando dinamicamente um <form>
e anexando valores
<input>
a ele chamando o método de anexar.
Claro, você não precisa criar um <form>
do zero.
Os objetos FormData
podem ser inicializados e HTMLFormElement
existentes 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 o FormData
também pode processar isso. Basta anexar os arquivos, e o navegador vai
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 é possível enviar dados File
ou Blob
usando XHR.
Lembre-se de que todos os File
s são Blob
s, então qualquer um deles funciona aqui.
Este exemplo cria um novo arquivo de texto do zero usando o construtor Blob()
e faz o upload desse Blob
para o servidor. O código também configura um manipulador
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'}));
Fazer 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 domínio. É muito simples ativar, exigindo apenas um único cabeçalho de resposta enviado pelo servidor.
Como ativar as solicitações do CORS
Digamos que seu aplicativo esteja em example.com
e que você
queira extrair dados de www.example2.com
. Normalmente, se você
tentar fazer esse tipo de chamada AJAX, a solicitação falhará e o navegador
gerará um erro de correspondência 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
O Access-Control-Allow-Origin
pode ser adicionado 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: *
Na verdade, este site (html5rocks.com) ativou o CORS em todas as páginas. Ative
as Ferramentas para Desenvolvedores e você vai encontrar o Access-Control-Allow-Origin
na nossa resposta:
Ativar solicitações entre origens é fácil. Então, ative o CORS se os 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 entre origens
não será diferente de uma solicitação XMLHttpRequest
normal. Por exemplo,
esta é uma solicitação que example.com
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 é 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();
Como dividir um arquivo e fazer upload de cada parte
Usando as APIs de arquivos, podemos minimizar o trabalho para fazer o upload de um arquivo grande. A técnica é dividir o upload em vários pedaços, gerar um XHR para cada parte e juntar o arquivo no servidor. Isso é semelhante à forma como o Gmail faz upload de anexos grandes com rapidez. Essa técnica também pode ser usada para contornar o limite de 32 MB de solicitações HTTP 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 código para reconstruir o arquivo no servidor não é mostrado aqui.
Referências
- Especificação do XMLHttpRequest Level 2
- Especificação do Compartilhamento de recursos entre origens (CORS)
- Especificação da API File
- Especificação da API FileSystem