Ir para o conteúdo
HexaTransfer
Voltar ao blog
Analises tecnicas

Upload Progressivo de Ficheiros: Guia UX e Técnico

Construa experiências de upload progressivo com drag-and-drop, barras de progresso e tratamento elegante de erros para melhor experiência de utilizador.

Um upload progressivo de ficheiros dá aos utilizadores feedback imediato e fiável em cada fase: no instante em que largam um ficheiro, uma confirmação de seleção; durante o upload, uma barra de progresso suave com ETA realista; numa falha, opções específicas de repetição; após a conclusão, um estado de sucesso claro com próximas ações. Os ingredientes técnicos são drag-and-drop via a DataTransfer API do HTML5, uploads em fragmentos com capacidade de retoma, streaming fetch com ReadableStream para progresso preciso em bytes, e gestão de estado que sobrevive a recarregamentos de separadores via IndexedDB. Bem feito, um utilizador a carregar um ficheiro de 5 GB nunca se pergunta se a app congelou.

O Que "Progressivo" Realmente Significa Aqui

Progressivo tem dois significados neste contexto. Um: progressive enhancement, para que o upload funcione como um POST de <input type="file"> simples num browser de 2012, e ganhe drag-and-drop, fragmentação e tentativas quando o JavaScript está disponível. Dois: progressive disclosure, onde a UI revela complexidade apenas quando necessário — mostra uma percentagem durante o upload, mas mostra detalhes de repetição apenas em caso de falha. Ambos os significados apontam para o mesmo princípio: o utilizador nunca deve chegar a um beco sem saída, nem esperar sem informação.

O modo de falha a evitar é o "spinner do doom" — uma animação de carregamento genérica que não dá nenhuma indicação de progresso, ETA, ou se algo correu mal. Os utilizadores cancelam uploads em que não confiam.

Drag-and-Drop Que Não Luta Com o Browser

A API de drag-and-drop do HTML5 é famosa pelos seus bugs. Algumas regras que a tornam tolerável:

const dropzone = document.querySelector('.dropzone');
dropzone.addEventListener('dragover', (e) => {
  e.preventDefault();
  dropzone.classList.add('dragging');
});
dropzone.addEventListener('dragleave', () => {
  dropzone.classList.remove('dragging');
});
dropzone.addEventListener('drop', (e) => {
  e.preventDefault();
  dropzone.classList.remove('dragging');
  handleFiles([...e.dataTransfer.files]);
});

Chame preventDefault em dragover ou a zona de drop não aceitará a largada. Use e.dataTransfer.items em vez de files se precisar de aceitar pastas via webkitGetAsEntry() — é a única forma de capturar recursivamente o conteúdo de diretórios no Chrome e Firefox.

Torne o fallback também utilizável: um <label> visível a envolver um <input type="file" multiple> estilizado funciona para 100% dos utilizadores, incluindo navegação por teclado e leitor de ecrã.

Mostrar Progresso em Que os Utilizadores Acreditam

As barras de progresso saltam por três razões: tamanhos de fragmento desiguais, TCP slow-start e buffering na pilha de rede. Suavize-as com uma média de arrasto de 2 segundos:

const samples = []; // [{ time, bytes }]
function recordSample(bytes) {
  const now = performance.now();
  samples.push({ time: now, bytes });
  while (samples.length > 1 && now - samples[0].time > 2000) samples.shift();
}
function throughput() {
  if (samples.length < 2) return 0;
  const delta = samples[samples.length - 1];
  const base = samples[0];
  return (delta.bytes - base.bytes) / ((delta.time - base.time) / 1000);
}

Calcule o ETA como (totalBytes - uploadedBytes) / throughput(), limite a apresentação a pelo menos 5 segundos, e formate-o em termos humanos: "cerca de 2 minutos" e não "124,3 segundos". Mostre tanto uma percentagem como um contador de bytes ("340 MB de 2,1 GB") — os utilizadores cruzam os dois quando algo parece errado.

Estados de Erro Com Recuperação Acionável

Mensagens genéricas de "upload falhado" destroem a confiança. Classifique as falhas em cinco categorias e apresente cada uma de forma distinta:

  • Queda de rede (evento offline, reset TCP): "A reconectar..." com tentativa automática
  • Servidor 5xx: "Erro de servidor, a tentar novamente em 5s" com botão de tentativa manual
  • Servidor 4xx (413 demasiado grande, 415 tipo errado): "Ficheiro rejeitado: demasiado grande" com substituição de ficheiro
  • Auth expirado (401, 403): "Sessão expirada, inicie sessão para continuar"
  • Crash do cliente (erro JS, browser fechou separador): Recuperação do IndexedDB no recarregamento

Emparelhe a mensagem com a única ação que a resolve. Se o utilizador está offline, mostre o estado online/offline monitorizado via navigator.onLine e o evento online.

Rastrear Bytes com Fetch Streams

XMLHttpRequest.upload.onprogress tem sido a forma tradicional de rastrear o progresso do upload, mas é instável em HTTP/3 e perde bytes em fila no buffer de envio. A abordagem moderna usa ReadableStream para contar bytes à medida que são produzidos:

function trackedStream(blob, onBytes) {
  let sent = 0;
  return new ReadableStream({
    async pull(controller) {
      const reader = blob.stream().getReader();
      while (true) {
        const { done, value } = await reader.read();
        if (done) { controller.close(); return; }
        sent += value.byteLength;
        onBytes(sent);
        controller.enqueue(value);
      }
    }
  });
}

Passe o stream como body ao fetch com duplex: 'half'. O suporte do Safari para streams de pedido chegou no 17.4; antes disso, faça fallback para XMLHttpRequest. Isto dá progresso preciso ao milissegundo vinculado a bytes reais entregues à pilha de rede.

Pausa, Retoma e Cancelamento

Os utilizadores esperam um botão de pausa em qualquer coisa que demore mais de um minuto. Com uploads em fragmentos, a pausa é simplesmente "parar de despachar novos fragmentos", e a retoma recomeça onde a fila de trabalho parou. O cancelamento usa AbortController:

const ctrl = new AbortController();
cancelButton.onclick = () => ctrl.abort();
await fetch(url, { method: 'PUT', body: blob, signal: ctrl.signal });

Ao abortar, limpe: DELETE a sessão de upload no servidor para que o armazenamento não vaze, limpe a entrada no IndexedDB, regresse ao estado inicial. A pausa deve preservar o estado; o cancelamento deve destruí-lo. Torne a distinção visível na UI.

Sobreviver a Recarregamentos de Separadores

Persista o estado de upload após cada fragmento bem-sucedido:

await idb.put('uploads', {
  sessionId, fileFingerprint, fileName, fileSize,
  completedChunks: [...done], updatedAt: Date.now()
}, sessionId);

A impressão digital é um SHA-256 do primeiro 1 MB do ficheiro mais tamanho e lastModified — suficiente para re-identificar o ficheiro quando o utilizador o escolhe novamente após o recarregamento. No carregamento da página, verifique o IndexedDB para sessões com menos de uma hora e ofereça retoma: "Tem um upload em curso de há 12 minutos. Retomar?" Não retome automaticamente sem consentimento — os utilizadores por vezes recarregam especificamente para cancelar.

Interações Acessíveis e Compatíveis com Teclado

Uma zona de drop que apenas responde a arrastos com rato falha os utilizadores de leitor de ecrã e de teclado. Adicione:

  • role="button" e tabindex="0" na zona de drop
  • Handler de tecla Enter/Espaço que clica no input de ficheiro
  • aria-live="polite" na região de progresso para que os leitores de ecrã anunciem marcos
  • Estilos de foco visíveis, não apenas hover
  • Rótulos claros — "Carregar ficheiro" supera "Procurar" que supera um ícone simples

O teste de teclado é rápido: desconecte o rato por 10 minutos e tente completar um upload. Se não conseguir, também não consegue uma parte dos utilizadores.

O uploader do HexaTransfer usa exatamente este padrão progressivo — fallback de formulário simples, melhoria de drag-and-drop, fetch streaming, retoma suportada por IndexedDB e recuperação de erros específica. Experimente em hexatransfer.com — gratuito, sem conta necessária, máximo de 10 GB.

Os Detalhes Que os Utilizadores Realmente Notam

O polimento que separa os uploaders esquecíveis dos excelentes vive em pequenos momentos: uma animação de drop que confirma que o ficheiro foi capturado, uma barra de progresso que se preenche suavemente em vez de saltar, um ETA que se torna mais preciso ao longo do tempo em vez de variar wildly, mensagens de erro específicas que dizem o que fazer a seguir, um prompt de retoma após uma atualização acidental, um estado de conclusão que persiste tempo suficiente para copiar a ligação de partilha, e comportamento de cancelamento que realmente para o upload de imediato. Cada um destes é algumas linhas de código. Lance todos e o uploader parece uma ordem de magnitude melhor do que o tratamento padrão de <input type="file">.

Envie arquivos grandes com segurança e criptografia de ponta a ponta

Transfira arquivos de até 10 GB gratuitamente com criptografia de ponta a ponta. Sem necessidade de conta. Seus arquivos são criptografados no navegador antes do envio — ninguém mais pode lê-los.

Enviar um arquivo