Fazendo upload com controle de progresso com JavaScript

Trabalha com desenvolvimento web há mais de 12 anos, atuando com projetos open source, palestrante, instrutor, e sócio fundador da Nasc, além de ser curador e idealizador da ​BrazilJS. Também um GDE.

Bom, já falamos sobre as várias formas de fazer Input de arquivos com JavaScript e também sobre como capturar fotos e gravar vídeos e áudios com getUserMedia.
Agora, vamos ver como podemos demonstrar para o usuário que estes dados estão sendo enviados para o servidor.

E vamos usar uma API nova, o XMLHttpRequest… Mas, isso não é novo!
Pois é. A especificação da última versão do XHR nos oferece algumas funcionalidades novas, entre elas, o envio de dados binários (os nossos blobs, discutidos e preparados devidamente nos posts anteriores) e, também, uma API para acompanharmos o progresso do upload.

Progressivo

Faremos isso de forma progressiva para que não tenhamos problemas com navegadores que não venham a suportar essas features.

Para acompanharmos o progresso do upload, vamos nos apoiar na propriedade upload do XHR; portanto, podemos usá-la como referência para feature detection.
Assim, podemos escolher qual será o comportamento de nossa interface enquanto acontece o upload. Dessa forma, nos certificaremos de que o navegador não irá disparar nenhum erro e que a experiência do usuário será agradável.


  if (xhr.upload) {
    // exibiremos a barra de progresso
  } else {
    // podemos exibir alguma animação que fique se repetindo
    // enquanto o upload não for finalizado
  }

Função de upload

Digamos que, ao clicar em um determinado botão, nós vamos usar o arquivo já colhido e fazer o upload. Para isso, usaremos um objeto XHR em modo POST e precisaremos dar um “append” do arquivo ao formulário que será enviado ao servidor.

Essa nossa função será uma promise, a ser resolvida assim que o upload finalizar, sendo rejeitada em caso de falha no upload.
Usaremos uma instância de FormData para enviarmos os dados ao servidor.
Neste exemplo, enviaremos os dados do form e arquivos para o path /, mas você precisará apontar este path para a rota em seu servidor que deverá tratar o form submetido.
Para este exemplo, este servidor terá acesso ao arquivo que estamos enviando por meio do nome “the-file”, mas sinta-se à vontade para renomear esta variável, e claro, para enviar mais arquivos sob outros nomes. Aos olhos do servidor, este nome está para o arquivo enviado como o name está para os inputs for form.


function uploadFile (file) {
  return new Promise( (resolve, reject) => {
    const xhr = new XMLHttpRequest()

    let fd = new FormData()
    // adicione ao fd as demais informações que você pretende enviar por POST
    // ao servidor, além do(s) arquivo(s)
   
    // agora é hora de adicionarmos o(s) arquivo(s)
    fd.append('the-file', file) // nome para referência ao arquivo no formulário
    xhr.open('post', '/') // path da rota no servidor

    xhr.send(fd) // iniciando a requisição, enviando o FormData
  }
}

Eventos

É hora de escutarmos alguns eventos para sabermos o estado de nosso XHR. Os mais tradicionais são o onload e o onerror
Antes de nosso .send, vamos acrescentar o seguinte:


    xhr.onerror = reject // em caso de erro, rejeitamos a promise
    xhr.onload = event => {
      // o envio ocorreu com sucesso
      resolve() // resolvemos nossa promise
    }

Acompanhando o progresso

Até aqui, tudo bem. Estamos falando de um XHR tradicional. É hora de acompanharmos o progresso desse upload.
Com acesso a xhr.upload, poderemos usar um evento novo, o onprogress. Este evento será disparado, passando para nós um objeto do tipo Progress que terá as propriedades loaded e total, em que loaded é a quantidade dos bytes que já foram carregados e total é o total de bytes a serem enviados.
Como você deve ter deduzido, sim, precisaremos fazer um pequeno cálculo com essas informações se quisermos exibir a percentagem do upload.


    if (xhr.upload) {
      // caso tenhamos acesso a esta informação
      xhr.upload.onprogress = progress => {
        console.log(Math.round((progress.loaded * 100) / progress.total) + '%')
      }
    } else {
      // tratamento em navegadores que não suportam xhr.upload
    }

Não confunda este evento com o progress do próprio objeto XHR. Este outro evento é dedicado ao progresso do download, não do upload.
E você também pode optar por escutar o evento abort para saber quando o envio ou download foi cancelado.

Concluindo

Legal, né? Mais uma daquelas coisas que costumávamos ouvir o pessoal dizendo que precisaríamos de alguma outra tecnologia para fazer, como o Flash, ActiveX, Applets ou Java. Só pra constar, três desses quatro caras já estão mortos! #LenhaNaFogueira :p

Em resumo, nossa função de upload ficou assim:


function uploadFile (file) {
  return new Promise( (resolve, reject) => {
    const xhr = new XMLHttpRequest()

    let fd = new FormData()
    // adicione ao fd as demais informações que você pretende enviar por POST
    // ao servidor, além do(s) arquivo(s)
   
    // agora é hora de adicionarmos o(s) arquivo(s)
    fd.append('the-file', file) // nome para referência ao arquivo no formulário
    xhr.open('post', '/') // path da rota no servidor

    xhr.onerror = reject // em caso de erro, rejeitamos a promise
    xhr.onload = event => {
      // o envio ocorreu com sucesso
      resolve() // resolvemos nossa promise
    }

    if (xhr.upload) {
      // caso tenhamos acesso a esta informação
      xhr.upload.onprogress = progress => {
        console.log(Math.round((progress.loaded * 100) / progress.total) + '%')
      }
    } else {
      // tratamento em navegadores que não suportam xhr.upload
    }

    xhr.send(fd) // iniciando a requisição, enviando o FormData
  }
}

Não deixe de ler nossos outros artigos sobre as tecnologias relacionadas aqui, para se sentir mais à vontade na hora de trabalhar com essas funcionalidades.


BrazilJS é uma iniciativa NASC