Novos truques em XMLHttpRequest2

HTML5 Rocks

Introdução

Um dos heróis não reconhecidos do universo HTML5 é XMLHttpRequest. Estritamente falando, XHR2 não é HTML5. No entanto, ele faz parte dos aprimoramentos incrementais que os fornecedores de navegador estão fazendo na plataforma central. Estou incluindo o XHR2 em nosso pacote de novidades porque ele é parte integrante dos complexos aplicativos da web de hoje.

Nosso velho amigo tem muita experiência, mas muitos colegas não conhecem os novos recursos. O XMLHttpRequest nível 2 introduz uma gama de novos recursos que colocam fim aos loucos esquemas de nossos aplicativos da web, coisas 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 junto com muitas APIs de HTML5 de sangria como API de sistema de arquivos, API de áudio da web e WebGL.

Esse tutorial destaca alguns dos novos recursos do XMLHttpRequest, especialmente os que podem ser usados para trabalhar com arquivos.

Busca de dados

Tem sido difícil buscar um arquivo como um blob binário com XHR. Tecnicamente, isso nunca foi possível. Um truque que tem sido bem documentado envolve a substituição do tipo de mime por um conjunto de caracteres definido pelo usuário, conforme mostrado abaixo.

O modo 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 obtém em 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. Embora essa pequena preciosidade funcione, vou chamá-la de magia negra e criticá-la. Sempre que você reclassifica os cortes do código de caracteres e manipula as strings para obrigar os dados a ficar em um formato desejável, encontra um problema.

Especificação de um formato de resposta

No exemplo anterior, fizemos download da imagem como um "arquivo" binário substituindo o tipo de mime do servidor e processando o texto de resposta como uma string binária. Em vez disso, vamos aproveitar as propriedades responseType e response do novo XMLHttpRequest para informar ao navegador qual deve ser o formato dos dados retornados.

xhr.responseType
Antes de enviar uma solicitação, defina xhr.responseType como "text", "arraybuffer", "blob" ou "document", dependendo de suas necessidades de dados. A definição de xhr.responseType = '' (ou sua omissão) padroniza a resposta para "text".
xhr.response
Depois de uma solicitação bem-sucedida, a propriedade de resposta do xhr terá os dados solicitados como DOMString, ArrayBuffer, Blob ou Document (dependendo do que foi definido para responseType).

Com esse novo conhecimento, podemos retrabalhar no exemplo anterior e, dessa vez, buscar a imagem como ArrayBuffer em vez de uma string. A execução do buffer na API BlobBuilder cria um Blob:

BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder;

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  if (this.status == 200) {
    var bb = new BlobBuilder();
    bb.append(this.response); // Note: not xhr.responseText

    var blob = bb.getBlob('image/png');
    ...
  }
};

xhr.send();

Muito mais legal!

Respostas ArrayBuffer

Um ArrayBuffer é um recipiente genérico de comprimento fixo para dados binários. Ele é muito útil quando você precisa de um buffer generalizado de dados brutos, mas a grande vantagem dele é a possibilidade de criar "visualizações" dos dados subjacentes usando matrizes tipo JavaScript. Na realidade, várias visualizações podem ser criadas a partir de uma única origem ArrayBuffer. Por exemplo, você pode criar uma matriz de inteiros de 8 bits que compartilhe o mesmo ArrayBuffer como uma matriz de inteiros de 32 bits existentes a partir dos mesmos dados. Os dados subjacentes continuam iguais; apenas criamos representações diferentes deles.

Por exemplo, a operação a seguir busca a mesma imagem como ArrayBuffer, mas, dessa vez, cria uma matriz de inteiros de 8 bits não assinada 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 desejar trabalhar diretamente com um Blob e/ou não precisar manipular nenhum byte 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 diversos lugares, inclusive sendo salvo em indexedDB, gravado no sistema de arquivos HTML5 ou criando um URL de blog, como mostra este exemplo.

Envio de dados

A possibilidade de fazer download dos dados em formatos diferentes é ótima, mas não nos leva a lugar nenhum se não conseguimos 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. Chega disso. Um método send() reformulado foi substituído para aceitar um dos seguintes tipos: DOMString, Document, FormData, Blob, File e ArrayBuffer. Os exemplos do restante desta seção demonstram como enviar 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);
}

sendText2('test string');

Não há nenhuma novidade aqui, embora o snippet correto seja ligeiramente diferente. Ele define responseType='text' para comparação. Novamente, a omissão dessa linha gera os mesmos resultados.

Envio de formulários: xhr.send(FormData)

Muitas pessoas provavelmente estão acostumadas a usar plug-ins jQuery ou outras bibliotecas para manipular envio de formulários AJAX. Em vez disso, podemos usar FormData, outro novo tipo de dados desenvolvido para XHR2. FormData é prático para criar um <form> HTML instantaneamente, em JavaScript. Esse formulário pode ser enviado com o uso de 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 apenas criando um <form> de modo dinâmico e associando valores de <input> a ele chamando o método append.

Obviamente, você não precisa criar um <form> do zero. Os objetos FormData podem ser inicializados a partir do HTMLFormElement existente na página. Por 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 arquivo (por exemplo, <input type="file">) e FormData também pode manipular 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);

Upload de um arquivo ou blob: xhr.send(Blob)

Também podemos enviar dados File ou Blob usando XHR. Tenha em mente que todos os Files são Blobs, ou seja, qualquer um funciona aqui.

Esse exemplo cria um novo arquivo de texto dos zero usando a API BlobBuilder e envia esse 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);
}

// Take care of vendor prefixes.
BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder;

var bb = new BlobBuilder();
bb.append('hello world');

upload(bb.getBlob('text/plain'));

Upload de um grupo de bytes: xhr.send(ArrayBuffer)

Finalmente, mas não menos importante, podemos enviar ArrayBuffers como a carga útil 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);
}

CORS (Compartilhamento de recursos de origem cruzada)

O CORS permite que aplicativos da web de um domínio façam solicitações AJAX de domínio cruzado para outro domínio. É muito fácil ativar; o servidor só precisa enviar um único cabeçalho de resposta.

Ativação de solicitações CORS

Digamos que seu aplicativo resida em example.com e você queira receber dados de www.example2.com. Normalmente, se você tentasse fazer esse tipo de chamada AJAX, a solicitação falharia, e o navegador lançaria um erro de não correspondência de origem. Com o CORS, www.example2.com pode optar por permitir solicitações de example.com simplesmente adicionando um cabeçalho:

Access-Control-Allow-Origin: http://example.com

Access-Control-Allow-Origin pode ser adicionado a um único recurso em um site ou no domínio inteiro. Para permitir que qualquer domínio faça uma solicitação para você, defina:

Access-Control-Allow-Origin: *

Na realidade, este site (html5rocks.com) ativou o CORS em todas as suas páginas. Acione as ferramentas de desenvolvedor e você verá Access-Control-Allow-Origin em nossa resposta:

Access-Control-Allow-Origin header on html5rocks.com
Cabeçalho Access-Control-Allow-Origin em html5rocks.com

É fácil ativar solicitações de origem cruzada. Ative o CORS se seus dados forem públicos.

Como fazer uma solicitação de domínio cruzado

Se o ponto de extremidade do servidor tiver ativado o CORS, fazer a solicitação de origem cruzada será o mesmo que fazer uma solicitação XMLHttpRequest normal. Por exemplo, eis 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 download e salvar arquivos no sistema de arquivos HTML5

Digamos que você tenha uma galeria de imagens e queira buscar um grupo de imagens a serem salvas localmente com o sistema de arquivos HTML5. Uma maneira de fazer isso seria solicitar imagens como ArrayBuffers, criar um Blob a partir dos dados e gravar o blob 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 = 'arraybuffer';

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 bb = new BlobBuilder();
        bb.append(xhr.response);

        writer.write(bb.getBlob('image/png'));

      }, onError);
    }, onError);
  }, onError);
};

xhr.send();

Observação: para usar esse código, consulte suporte do navegador e limitações de armazenamento no tutorial "Como explorar as APIs FileSystem".

Divisão de um arquivo e upload de cada parte

Usando as APIs de File, podemos minimizar o trabalho de upload de um arquivo grande. A técnica é dividir o upload em vários blocos, designar um XHR para cada parte e colocar o arquivo junto no servidor. É semelhante ao modo como o Gmail faz upload de anexos grandes tão rapidamente. Essa técnica também poderia ser usada para resolver a limitação das solicitações http de 32 MB do mecanismo do Google Apps.

window.BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder ||
                     window.BlobBuilder;

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) {

    // Note: blob.slice has changed semantics and been prefixed. See http://goo.gl/U9mE5.
    if ('mozSlice' in blob) {
      var chunk = blob.mozSlice(start, end);
    } else {
      var chunk = blob.webkitSlice(start, end);
    }

    upload(chunk);

    start = end;
    end = start + BYTES_PER_CHUNK;
  }
}, false);

})();

Aqui não será mostrado o código que reconstrói o arquivo no servidor.

Experimente!

#bytes/chunk:

Referências

Comments

0