Jak zbudować prosty uploader HTML5: drag&drop, podgląd, postęp
Wrzucanie plików powinno być szybkie i czytelne. Coraz więcej osób oczekuje podglądu przed wysłaniem, drag and drop oraz jasnego paska postępu. Dobra wiadomość jest taka, że w HTML5 da się to zrobić prosto i bez ciężkich bibliotek.
W tym artykule zobaczysz, jak zbudować mały uploader krok po kroku. Poznasz strukturę HTML, obsługę drag and drop, podgląd miniatur, walidację, wysyłkę z postępem, a także proste zabezpieczenia po stronie serwera.
1. Co dokładnie zrobimy: prosty uploader z drag&drop i podglądem
Stworzymy strefę przeciągania, podgląd miniatur, walidację oraz wysyłkę z paskiem postępu i anulowaniem.
Uploader przyjmie jeden lub wiele plików. Użytkownik przeciągnie je na strefę lub wybierze z dysku. Zobaczy miniatury obrazów, a podczas wysyłki pasek postępu i komunikaty. Dodamy anulowanie, obsługę błędów i bezpieczny fallback dla starszych przeglądarek.
2. Prosta struktura HTML i miejsca na strefę drag&drop
Potrzebujemy kontenera na strefę zrzutu, ukrytego pola wyboru plików, miejsca na miniatury i paska postępu.
<div id="uploader">
<div id="dropzone" role="button" tabindex="0">
Przeciągnij pliki tutaj lub kliknij, aby wybrać
</div>
<input id="fileInput" type="file" multiple accept="image/*" style="display:none">
<div id="preview"></div>
<div id="progressWrap">
<div id="progressBar" style="width:0%"></div>
</div>
<div id="messages" aria-live="polite"></div>
<button id="uploadBtn">Wyślij</button>
<button id="cancelBtn">Anuluj</button>
</div>
Dropzone działa także jako przycisk. Kliknięcie otwiera selektor plików. Atrybut aria-live zapewnia czytelne komunikaty dla czytników ekranu.
3. Obsługa drag&drop: eventy i zapobieganie domyślnym zachowaniom
Należy zablokować domyślne otwieranie plików przez przeglądarkę i reagować na najechanie oraz upuszczenie.
const dropzone = document.getElementById('dropzone')
const input = document.getElementById('fileInput')
dropzone.addEventListener('click', () => input.click())
dropzone.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') input.click()
})
const prevent = e => {
e.preventDefault()
e.stopPropagation()
}
document.addEventListener('dragover', prevent)
document.addEventListener('drop', prevent)
dropzone.addEventListener('dragenter', e => {
prevent(e)
dropzone.classList.add('is-hover')
})
dropzone.addEventListener('dragleave', e => {
prevent(e)
dropzone.classList.remove('is-hover')
})
dropzone.addEventListener('drop', e => {
prevent(e)
dropzone.classList.remove('is-hover')
handleFiles(e.dataTransfer.files)
})
input.addEventListener('change', e => handleFiles(e.target.files))
Wywołanie preventDefault i stopPropagation zatrzymuje otwieranie plików w oknie. Klasa is-hover pozwala na wizualne podświetlenie strefy.
4. Tworzenie podglądu obrazów i miniatur przed wysłaniem
Do podglądu obrazów użyjemy FileReader, który wczytuje dane jako adres data URL.
const preview = document.getElementById('preview')
let selectedFiles = []
function handleFiles(fileList) {
selectedFiles = []
preview.innerHTML = ''
for (const file of fileList) {
selectedFiles.push(file)
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = e => {
const img = document.createElement('img')
img.src = e.target.result
img.alt = file.name
img.width = 120
img.height = 120
img.loading = 'lazy'
preview.appendChild(img)
}
reader.readAsDataURL(file)
} else {
const el = document.createElement('div')
el.textContent = file.name
preview.appendChild(el)
}
}
}
Miniatury obrazów przyspieszają decyzję o wysyłce. Dla innych typów pokażemy po prostu nazwę pliku.
5. Wysyłanie plików z pokazaniem postępu przez XHR i fetch
Pasek postępu najprościej uzyskać przez XMLHttpRequest. Fetch działa prosto, lecz bez natywnego postępu wysyłki.
const progressBar = document.getElementById('progressBar')
const messages = document.getElementById('messages')
const uploadBtn = document.getElementById('uploadBtn')
const cancelBtn = document.getElementById('cancelBtn')
let xhr
uploadBtn.addEventListener('click', () => {
if (!selectedFiles.length) {
showMessage('Nie wybrano plików')
return
}
const form = new FormData()
for (const file of selectedFiles) form.append('files', file)
xhr = new XMLHttpRequest()
xhr.open('POST', '/upload')
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
const percent = Math.round(e.loaded * 100 / e.total)
progressBar.style.width = percent + '%'
}
}
xhr.onload = () => {
if (xhr.status === 200) showMessage('Wysłano pliki')
else showMessage('Błąd wysyłki')
}
xhr.onerror = () => showMessage('Błąd sieci')
xhr.send(form)
})
cancelBtn.addEventListener('click', () => {
if (xhr) xhr.abort()
progressBar.style.width = '0%'
showMessage('Wysłanie anulowane')
})
function showMessage(text) {
messages.textContent = text
}
Alternatywnie można użyć fetch i AbortController. Postęp wysyłki nie jest powszechnie dostępny, ale anulowanie działa.
const controller = new AbortController()
const form = new FormData()
for (const file of selectedFiles) form.append('files', file)
fetch('/upload', {
method: 'POST',
body: form,
signal: controller.signal
}).then(r => {
if (r.ok) showMessage('Wysłano pliki')
else showMessage('Błąd wysyłki')
}).catch(() => showMessage('Błąd sieci'))
// Anulowanie
controller.abort()
Gdy kluczowy jest postęp, lepszy będzie XMLHttpRequest. Gdy potrzebna jest prostota i strumienie, można rozważyć fetch.
6. Walidacja typów i rozmiarów plików po stronie klienta
Wstępna walidacja oszczędza transfer i poprawia doświadczenie użytkownika.
- Akceptowane typy za pomocą atrybutu accept i sprawdzenia MIME.
- Limit rozmiaru pojedynczego pliku i łączny limit paczki.
- Liczba plików w jednym wysłaniu.
const MAX_FILE = 5 * 1024 * 1024
const MAX_TOTAL = 20 * 1024 * 1024
function validate(files) {
let total = 0
for (const f of files) {
if (!f.type || !f.type.startsWith('image/')) {
showMessage('Dozwolone są tylko obrazy')
return false
}
if (f.size > MAX_FILE) {
showMessage('Plik jest zbyt duży')
return false
}
total += f.size
}
if (total > MAX_TOTAL) {
showMessage('Łączny rozmiar jest zbyt duży')
return false
}
return true
}
// użycie w handleFiles
function handleFiles(fileList) {
if (!validate(fileList)) return
// dalej tworzenie podglądu
}
Walidacja po stronie klienta nie zastępuje weryfikacji po stronie serwera. Traktuj ją jako wygodę i filtr wstępny.
7. Obsługa błędów, anulowanie i responsywne komunikaty dla użytkownika
Jasne komunikaty obniżają frustrację i skracają czas działania.
- Komunikaty w aria-live informują także użytkowników czytników ekranu.
- Pokazuj stany: gotowy, w trakcie, anulowany, błąd, sukces.
- Wykrywaj offline i proponuj ponowienie po powrocie sieci.
- Po błędzie zostaw podgląd, aby nie tracić wybranych plików.
window.addEventListener('offline', () => showMessage('Brak sieci'))
window.addEventListener('online', () => showMessage('Sieć dostępna'))
Warto dodać automatyczne ponawianie z krótkim opóźnieniem i limit prób. Użytkownik powinien widzieć licznik prób i mieć dostęp do przycisku wyślij ponownie.
8. Proste zabezpieczenia i ograniczenia po stronie serwera
Serwer musi zaufać tylko własnym kontrolom. Walidacja po stronie klienta jest pomocna, lecz niewystarczająca.
- Wymuś typ i rozmiar na poziomie serwera. Zwracaj spójne kody i wiadomości.
- Odrzucaj pliki po nagłówkach i po zawartości. Sprawdzaj początkowe bajty pliku.
- Ustal limit liczby plików i łącznego rozmiaru jednego żądania.
- Przy przesyłaniu multipart zapisuj pod bezpieczną, generowaną nazwą. Bezpieczny katalog poza katalogiem publicznym.
- Ustaw maksymalny rozmiar ciała żądania. Zwracaj kod 413 dla zbyt dużych plików.
- Włącz kontrolę uprawnień, token anty-CSRF i uwierzytelnianie, jeśli to zasób chroniony.
- Serwuj po HTTPS. Dodaj skanowanie antywirusowe, jeśli przechowujesz pliki użytkowników.
- Waliduj rozszerzenia dopiero po weryfikacji faktycznego typu pliku.
Odpowiedź JSON ułatwia spójne komunikaty po stronie klienta. Logi powinny zawierać identyfikator żądania, rozmiar i wynik walidacji.
9. Czy można obsłużyć przesyłanie wielu plików jednocześnie?
Tak, można wysyłać wiele plików równolegle lub w kolejce z ograniczoną współbieżnością.
- Równolegle skróci czas, ale obciąży łącze i serwer.
- Kolejka jest stabilna. Możesz wysyłać kilka na raz, resztę czeka.
async function uploadOne(file) {
return new Promise((resolve, reject) => {
const form = new FormData()
form.append('files', file)
const x = new XMLHttpRequest()
x.open('POST', '/upload')
x.onload = () => x.status === 200 ? resolve() : reject()
x.onerror = () => reject()
x.send(form)
})
}
async function uploadQueue(files) {
for (const file of files) {
try {
await uploadOne(file)
showMessage('Wysłano ' + file.name)
} catch (e) {
showMessage('Błąd pliku ' + file.name)
}
}
}
Wersja równoległa to pojedyncze wywołania w pętli i zbieranie wyników. Współbieżność można ograniczyć prostą pulą pracowników.
10. Jak zapewnić zgodność z przeglądarkami mobilnymi i starszymi?
Wdrożymy progresywne ulepszanie. Drag and drop to dodatek. Podstawą jest przycisk wyboru plików.
- Na urządzeniach mobilnych drag and drop bywa ograniczony. Klikalna strefa i input to bezpieczny fundament.
- Duże przyciski i kontrast. Dobre etykiety i focus klawiatury.
- Oszczędzaj dane. Kompresuj obrazy po stronie klienta tylko za zgodą użytkownika.
- Utrzymuj czytelny pasek postępu i prosty fallback. Gdy brak wsparcia XHR, użyj zwykłego formularza.
- Testuj w trybie oszczędzania danych i przy słabym zasięgu. Zadbaj o działanie offline po wznowieniu sieci.
Dzięki takiemu podejściu każdy użytkownik może wysłać plik. Ci ze wsparciem HTML5 mają wygodne dodatki, reszta korzysta z solidnego podstawowego przepływu.
Prosty uploader z podglądem i postępem podnosi komfort i zmniejsza liczbę błędów, a dzięki walidacji i zabezpieczeniom działa stabilnie w różnych warunkach i na różnych urządzeniach.
Chcesz wdrożyć taki uploader w swoim serwisie i dostosować go do potrzeb projektu? Napisz i umów krótką konsultację.





