Progressive upload plików: przewodnik UX i techniczny
Buduj progresywne doświadczenia uploadu plików z drag-and-drop, paskami postępu i eleganckim obsługiwaniem błędów dla lepszego UX.
CSIRT NASK odnotował, że użytkownicy porzucający narzędzia do transferu plików w połowie procesu często sięgają po mniej bezpieczne alternatywy — czyli dobry UX to nie tylko wygoda, to też bezpieczeństwo. Progresywny upload plików daje użytkownikom natychmiastową, wiarygodną informację zwrotną na każdym etapie: w chwili upuszczenia pliku — potwierdzenie wyboru; podczas uploadu — płynny pasek postępu z realistycznym ETA; przy błędzie — konkretne opcje ponawiania; po zakończeniu — jasny stan sukcesu z następnymi akcjami. Składniki techniczne to drag-and-drop przez HTML5 DataTransfer API, fragmentowane uploady z możliwością wznawiania, strumieniowanie fetch z ReadableStream do dokładnego śledzenia bajtów i zarządzanie stanem przeżywające przeładowania kart przez IndexedDB.
Co "progresywny" naprawdę tu oznacza
Progresywny ma dwa znaczenia w tym kontekście. Pierwsze: progresywne ulepszanie, żeby upload działał jako zwykłe <input type="file"> POST w przeglądarce z 2012, i zyskiwał drag-and-drop, fragmentowanie i ponawiania gdy JavaScript jest dostępny. Drugie: progresywne odkrywanie, gdzie interfejs ujawnia złożoność tylko gdy potrzeba — pokazuje procent podczas uploadu, ale szczegóły ponawiania tylko przy błędzie. Oba znaczenia wskazują na tę samą zasadę: użytkownik nigdy nie powinien trafić w ślepy zaułek i nigdy nie czekać bez informacji.
Błąd, którego unikać, to "kręcące się koło zagłady" — generyczna animacja ładowania, która nie podaje żadnych informacji o postępie, ETA ani czy coś poszło nie tak. Użytkownicy anulują uploady, którym nie ufają.
Drag-and-drop, który nie walczy z przeglądarką
HTML5 drag-and-drop API jest znane z błędów. Kilka zasad, które to czyni tolerowanym:
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]);
});
Wywołaj preventDefault na dragover inaczej cel upuszczenia nie przyjmie upuszczenia. Używaj e.dataTransfer.items zamiast files jeśli potrzebujesz akceptować foldery przez webkitGetAsEntry() — to jedyny sposób na rekurencyjne przechwytywanie zawartości katalogów w Chrome i Firefox.
Spraw też by fallback był używalny: widoczny <label> owijający stylizowany <input type="file" multiple> działa dla 100% użytkowników, łącznie z nawigacją klawiaturą i czytnikami ekranu.
Pokazywanie postępu, któremu użytkownicy wierzą
Paski postępu skaczą z trzech powodów: nierówne rozmiary fragmentów, TCP slow-start i buforowanie w stosie sieciowym. Wygładź je 2-sekundową średnią ruchomą:
const samples = [];
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);
}
Obliczaj ETA jako (totalBytes - uploadedBytes) / throughput(), ogranicz wyświetlanie do co najmniej 5 sekund i formatuj po ludzku: "około 2 minuty" nie "124,3 sekundy". Pokazuj zarówno procent, jak i licznik bajtów ("340 MB z 2,1 GB") — użytkownicy krzyżowo sprawdzają te dwie wartości, gdy coś wydaje się nie tak.
Stany błędów z możliwymi do podjęcia działaniami
Generyczne komunikaty "upload nie powiódł się" niszczą zaufanie. Klasyfikuj błędy do pięciu grup i wyświetlaj każdą osobno:
- Zerwanie sieci (zdarzenie offline, reset TCP): "Łączę ponownie..." z automatycznym ponowieniem
- Serwer 5xx: "Błąd serwera, ponawianie za 5s" z przyciskiem ręcznego ponowienia
- Serwer 4xx (413 zbyt duży, 415 zły typ): "Plik odrzucony: zbyt duży" z możliwością zamiany pliku
- Sesja wygasła (401, 403): "Sesja wygasła, zaloguj się, żeby kontynuować"
- Crash klienta (błąd JS, przeglądarka zabiła kartę): Odtwarzanie z IndexedDB przy przeładowaniu
Połącz komunikat z jednym działaniem, które go rozwiązuje. Jeśli użytkownik jest offline, pokaż stan online/offline monitorowany przez navigator.onLine i zdarzenie online.
Śledzenie bajtów przez strumienie Fetch
XMLHttpRequest.upload.onprogress był tradycyjnym sposobem śledzenia postępu uploadu, ale jest zawodny na HTTP/3 i traci bajty zbuforowane w buforze nadawania. Nowoczesne podejście używa ReadableStream do liczenia bajtów w miarę ich produkcji:
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);
}
}
});
}
Przekaż strumień jako body do fetch z duplex: 'half'. Obsługa żądania strumieniowego w Safari ląduje w 17.4 — przed tym wróć do XMLHttpRequest.
Pauza, wznawianie i anulowanie
Użytkownicy oczekują przycisku pauzy dla wszystkiego, co trwa dłużej niż minutę. Przy fragmentowanych uploadach pauza to po prostu "przestań wysyłać nowe fragmenty", a wznowienie podejmuje skąd skończyła się kolejka pracy. Anulowanie używa AbortController:
const ctrl = new AbortController();
cancelButton.onclick = () => ctrl.abort();
await fetch(url, { method: 'PUT', body: blob, signal: ctrl.signal });
Po przerwaniu posprzątaj: usuń sesję uploadu na serwerze, żeby storage nie wyciekał, wyczyść wpis IndexedDB, wróć do stanu początkowego. Pauza powinna zachowywać stan; anulowanie powinno go niszczyć. Uwidocznij rozróżnienie w interfejsie.
Przeżywanie przeładowań kart
Utrwalaj stan uploadu po każdym udanym fragmencie:
await idb.put('uploads', {
sessionId, fileFingerprint, fileName, fileSize,
completedChunks: [...done], updatedAt: Date.now()
}, sessionId);
Odcisk cyfrowy to SHA-256 pierwszego 1 MB pliku plus rozmiar i lastModified — wystarczy do ponownej identyfikacji pliku gdy użytkownik wybierze go ponownie po przeładowaniu. Przy ładowaniu strony sprawdź IndexedDB dla sesji sprzed mniej niż godziny i zaproponuj wznowienie: "Masz upload w toku sprzed 12 minut. Wznowić?" Nie wznawiaj automatycznie bez zgody — użytkownicy czasem przeładowują właśnie po to, żeby anulować.
Dostępne i przyjazne dla klawiatury interakcje
Strefa upuszczania reagująca tylko na przeciąganie myszą zawodzi użytkowników czytników ekranu i klawiatury. Dodaj:
role="button"itabindex="0"na strefie upuszczania- Obsługę klawisza Enter/Spacja klikającą input pliku
aria-live="polite"na regionie postępu, żeby czytniki ekranu ogłaszały kamienie milowe- Widoczne style fokusa, nie tylko hover
- Jasne etykiety — "Prześlij plik" bije "Przeglądaj" które bije gołą ikonę
Testowanie klawiaturą jest szybkie: odłącz mysz na 10 minut i spróbuj ukończyć upload. Jeśli nie możesz, nie może też część twoich użytkowników.
HexaTransfer używa dokładnie tego progresywnego wzorca — fallback zwykłego formularza, ulepszenie drag-and-drop, strumieniowanie fetch, wznowienie oparte na IndexedDB i konkretne odtwarzanie błędów. Wypróbuj na https://hexatransfer.com — za darmo, bez konta, maks. 10 GB.
Szczegóły, które użytkownicy faktycznie zauważają
Polska, która odróżnia zapomniane uploaderу od świetnych, żyje w małych momentach: animacja upuszczenia potwierdzająca przechwycenie pliku, pasek postępu wypełniający się płynnie zamiast skakania, ETA stający się dokładniejszym z czasem zamiast chaotycznego skakania, konkretne komunikaty błędów mówiące co zrobić dalej, monit wznowienia po przypadkowym odświeżeniu, stan ukończenia trwający wystarczająco długo by skopiować link udostępniania i zachowanie anulowania, które faktycznie natychmiast zatrzymuje upload. Każdy z nich to kilka linii kodu. Wyślij je wszystkie a twój uploader poczuje się rzędem wielkości lepiej niż domyślne traktowanie <input type="file">.
Wysyłaj duże pliki bezpiecznie z szyfrowaniem end-to-end
Przesyłaj pliki do 10 GB za darmo z szyfrowaniem end-to-end. Bez rejestracji. Twoje pliki są szyfrowane w przeglądarce przed przesłaniem — nikt inny nie może ich odczytać.
Wyślij plik