Subidas de archivos HTML5 con arrastrar y soltar en JavaScript
Las subidas con arrastrar y soltar resultan naturales, reducen la fricción y se pueden implementar por completo en el front end con las API modernas del navegador. En esta guía construirás un componente de subida de archivos HTML5 listo para producción que valida los archivos, muestra el progreso y gestiona varios archivos de forma eficaz.
Introducción a las capacidades de subida de archivos de HTML5
HTML5 introdujo las interfaces File, FileList, FileReader y DataTransfer, que dan
al código JavaScript acceso de primera clase a los archivos seleccionados por el usuario. Combinadas
con fetch, FormData y
XMLHttpRequest, ahora puedes transmitir subidas, realizar validación del lado del
cliente y ofrecer vistas previas de archivos sin depender de plugins.
Estas API te permiten:
- Inspeccionar metadatos de archivo como
name,sizeytype. - Generar vistas previas del lado del cliente para imágenes, texto u otros tipos de archivo.
- Implementar la funcionalidad de arrastrar y soltar para mejorar la experiencia de usuario.
- Supervisar el progreso de la transferencia (principalmente con
XMLHttpRequest) o usar bibliotecas modernas que lo abstraen.
Configura un formulario básico de arrastrar y soltar
Esta es la estructura HTML de nuestro componente de subida de archivos:
<form id="upload-form">
<input type="file" id="file-input" multiple hidden />
<div id="drop-zone" class="drop-zone" tabindex="0" role="button">Drop files here or click to browse</div>
<div id="file-preview"></div>
<progress id="upload-progress" value="0" max="100" hidden></progress>
<p id="upload-status" class="status-message" aria-live="polite"></p>
<button type="button" id="upload-button" disabled>Upload Files</button>
</form>
Hemos añadido tabindex="0" al drop-zone para que se pueda enfocar y aria-live="polite" al párrafo
upload-status por accesibilidad.
Implementa las interacciones de arrastrar y soltar
JavaScript gestiona los eventos de arrastrar y soltar y la selección de archivos:
const dropZone = document.getElementById('drop-zone')
const fileInput = document.getElementById('file-input')
const uploadBtn = document.getElementById('upload-button')
const preview = document.getElementById('file-preview')
const uploadProgress = document.getElementById('upload-progress')
const uploadStatusEl = document.getElementById('upload-status')
let filesToUpload = [] // Renamed for clarity
// Helper functions for status messages
function showError(message) {
uploadStatusEl.textContent = message
uploadStatusEl.className = 'status-message error'
}
function showSuccess(message) {
uploadStatusEl.textContent = message
uploadStatusEl.className = 'status-message success'
}
// Click-to-browse fallback and keyboard accessibility
dropZone.addEventListener('click', () => fileInput.click())
dropZone.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
fileInput.click()
}
})
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
handleFiles(fileInput.files)
}
})
// Prevent default browser behavior for drag events
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault()
e.stopPropagation()
})
})
// Add visual feedback for drag events
;['dragenter', 'dragover'].forEach((eventName) => {
dropZone.addEventListener(eventName, () => dropZone.classList.add('drag-over'))
})
;['dragleave', 'drop'].forEach((eventName) => {
dropZone.addEventListener(eventName, () => dropZone.classList.remove('drag-over'))
})
dropZone.addEventListener('drop', (e) => {
if (e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files)
fileInput.files = e.dataTransfer.files // Synchronize fileInput.files for consistency
}
})
Valida los archivos seleccionados
La validación del lado del cliente ofrece retroalimentación inmediata al usuario. Recuerda que la validación del lado del servidor es crucial para la seguridad.
function handleFiles(fileList) {
filesToUpload = Array.from(fileList)
preview.innerHTML = '' // Clear previous previews
uploadBtn.disabled = true
showError('') // Clear previous errors
const validTypes = ['image/jpeg', 'image/png', 'application/pdf']
const maxSize = 5 * 1024 * 1024 // 5 MB
const validatedFiles = filesToUpload.filter((file) => {
if (!validTypes.includes(file.type)) {
showError(`${file.name}: Invalid file type. Allowed types: JPEG, PNG, PDF.`)
return false
}
if (file.size > maxSize) {
showError(`${file.name}: File is too large. Maximum size is 5 MB.`)
return false
}
addPreview(file)
return true
})
filesToUpload = validatedFiles
uploadBtn.disabled = filesToUpload.length === 0
if (filesToUpload.length > 0 && filesToUpload.length === fileList.length) {
showSuccess(`${filesToUpload.length} file(s) ready for upload.`)
}
}
Muestra vistas previas en línea
Mostrar vistas previas ayuda a los usuarios a confirmar su selección:
function addPreview(file) {
const wrapper = document.createElement('div')
wrapper.className = 'file-item'
const fileName = document.createElement('span')
fileName.className = 'file-name'
fileName.textContent = file.name
const fileSize = document.createElement('span')
fileSize.className = 'file-size'
fileSize.textContent = `${(file.size / 1024).toFixed(1)} KB`
if (file.type.startsWith('image/')) {
const img = document.createElement('img')
img.alt = file.name
const reader = new FileReader()
reader.onload = (e) => {
img.src = e.target.result
}
reader.readAsDataURL(file)
wrapper.appendChild(img)
} else {
const extensionIcon = document.createElement('span')
extensionIcon.className = 'file-extension'
extensionIcon.textContent = file.name.split('.').pop().toUpperCase()
wrapper.appendChild(extensionIcon)
}
wrapper.appendChild(fileName)
wrapper.appendChild(fileSize)
preview.appendChild(wrapper)
}
Elige Fetch API o XMLHttpRequest para las subidas
La Fetch API es el estándar moderno para las solicitudes de red. Sin embargo, al momento de escribir
esto, no admite de forma nativa los eventos de progreso de subida. Para el progreso de subida, XMLHttpRequest (XHR) sigue
siendo de uso habitual, o puedes optar por una biblioteca como Uppy que se encarga de ello.
// Attach event listener to the upload button
uploadBtn.addEventListener('click', () => {
if (filesToUpload.length === 0) {
showError('Please select valid files to upload.')
return
}
uploadBtn.disabled = true
uploadProgress.value = 0
uploadProgress.hidden = false
// For this demo, we use XMLHttpRequest to show progress.
// The uploadWithFetch function (shown below) is an alternative if progress isn't needed.
uploadWithXHR(filesToUpload)
})
Sube con la Fetch API (sin indicación de progreso)
async function uploadWithFetch(file) {
const formData = new FormData()
formData.append('file', file)
// Hide progress bar as Fetch API doesn't support upload progress
uploadProgress.hidden = true
try {
const response = await fetch('/upload', { method: 'POST', body: formData })
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
const responseData = await response.text() // Or response.json() if applicable
showSuccess(`${file.name} uploaded successfully. Server: ${responseData}`)
} catch (error) {
showError(`Upload of ${file.name} failed: ${error.message}`)
} finally {
uploadBtn.disabled = filesToUpload.length === 0 // Re-enable if there are still files or based on other logic
}
}
Sube con XMLHttpRequest (con progreso habilitado)
function uploadWithXHR(fileArray) {
const uploadPromises = fileArray.map((file) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
const formData = new FormData()
formData.append('file', file)
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100
uploadProgress.value = percentComplete
// Note: For multiple files, this updates based on the latest event.
// A more complex UI might show individual progress or aggregate total progress.
}
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({ file, statusText: xhr.statusText })
} else {
reject(new Error(`Upload of ${file.name} failed: ${xhr.statusText || xhr.status}`))
}
}
xhr.onerror = () => {
reject(new Error(`Network error during upload of ${file.name}.`))
}
// To allow cancellation, you could store the xhr object and call xhr.abort().
// For example: file.xhr = xhr; then provide a UI to call file.xhr.abort();
xhr.open('POST', '/upload', true)
xhr.send(formData)
})
})
Promise.all(uploadPromises)
.then((results) => {
showSuccess(`${results.length} file(s) uploaded successfully.`)
uploadProgress.hidden = true
filesToUpload = [] // Clear the list after successful upload
preview.innerHTML = '' // Clear previews
})
.catch((error) => {
showError(error.message) // Shows the first error encountered
uploadProgress.hidden = true
})
.finally(() => {
uploadBtn.disabled = filesToUpload.length === 0
})
}
Integra una biblioteca moderna (Uppy)
Si prefieres no gestionar tú mismo las complejidades de las subidas de archivos, considera usar una biblioteca dedicada como Uppy. Uppy es un componente de subida de archivos modular y de código abierto que se encarga de la interfaz, la validación, los reintentos, las subidas reanudables (mediante el protocolo tus) y la integración con distintos back-ends.
import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import XHRUpload from '@uppy/xhr-upload'
// Assuming you have a div with id="uppy-dashboard-container"
// const uppy = new Uppy({
// restrictions: { maxFileSize: 5 * 1024 * 1024, allowedFileTypes: ['image/jpeg', 'image/png', '.pdf'] },
// autoProceed: false
// })
// .use(Dashboard, {
// inline: true,
// target: '#uppy-dashboard-container', // Or your existing drop-zone: '#drop-zone'
// proudlyDisplayPoweredByUppy: false,
// height: 300
// })
// .use(XHRUpload, {
// endpoint: '/upload',
// fieldName: 'file'
// });
// uppy.on('complete', (result) => {
// console.log('Uppy upload complete!', result.successful);
// result.successful.forEach(file => showSuccess(`${file.name} uploaded via Uppy.`));
// result.failed.forEach(file => showError(`Uppy upload of ${file.name} failed.`));
// });
El uso de Uppy está comentado, pero sus importaciones requieren que @uppy/core, @uppy/dashboard y
@uppy/xhr-upload estén instalados. Descomenta y adapta el uso cuando reemplaces la interfaz personalizada,
e incluye los estilos del Dashboard de @uppy/dashboard/css/style.min.css.
Uppy ofrece una interfaz de usuario rica y gestiona muchos casos límite desde el primer momento.
Refuerza la seguridad
Las comprobaciones del lado del cliente son para la experiencia de usuario; valida siempre en el servidor:
- Protección CSRF: implementa tokens CSRF e inclúyelos en tus solicitudes de subida.
- Validación del lado del servidor: vuelve a validar los tipos de archivo, los tamaños y el contenido en el servidor. Nunca confíes solo en las comprobaciones del lado del cliente.
- Verificación del Content-Type: en el servidor, verifica la cabecera
Content-Typey, lo que es más importante, inspecciona los magic bytes reales del archivo para determinar su tipo verdadero. - Límites de tamaño de archivo: aplica límites máximos de tamaño de archivo en el servidor.
- Almacenamiento seguro: guarda los archivos subidos fuera del directorio raíz web. Usa nombres de archivo aleatorios e imposibles de adivinar para evitar el acceso directo o los ataques de enumeración.
- Análisis de virus: analiza todos los archivos subidos con un motor antivirus actualizado antes de seguir procesándolos o de hacerlos accesibles.
Hazlo accesible
Asegúrate de que tu componente de subida de archivos sea usable para todo el mundo:
- La zona de arrastre se puede enfocar (
tabindex="0") y responde a las teclasSpace/Enterpara activar la selección de archivos. - Las actualizaciones de estado (por ejemplo, errores o mensajes de éxito) se anuncian a las
tecnologías de asistencia mediante
aria-live="polite"en el contenedor del mensaje de estado. - Ofrece indicadores visuales de foco claros para la navegación con teclado.
- Garantiza un contraste de color suficiente para el texto y los elementos de la interfaz.
Aplica estilos al widget
Un CSS básico puede mejorar el atractivo visual y la experiencia de usuario:
.drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 25px;
text-align: center;
font-family: Arial, sans-serif;
color: #555;
cursor: pointer;
transition:
border-color 0.3s ease,
background-color 0.3s ease;
background: #f8f9fa;
}
.drop-zone.drag-over {
border-color: #2196f3; /* Blue border when dragging over */
background-color: #e3f2fd; /* Light blue background */
}
.drop-zone:focus {
outline: 2px solid #2196f3;
outline-offset: 2px;
}
#file-preview {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.file-item {
width: 120px;
padding: 8px;
border: 1px solid #eee;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
font-size: 12px;
word-break: break-all;
}
.file-item img {
max-width: 100px;
max-height: 80px;
margin-bottom: 5px;
object-fit: cover;
}
.file-extension {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
background: #f1f1f1;
color: #333;
font-size: 18px;
font-weight: bold;
border-radius: 4px;
margin-bottom: 5px;
}
.file-name {
font-weight: bold;
margin-bottom: 3px;
}
.file-size {
color: #777;
}
#upload-progress {
width: 100%;
margin-top: 10px;
}
.status-message {
margin-top: 10px;
font-weight: bold;
}
.status-message.success {
color: #2e7d32; /* Green for success */
}
.status-message.error {
color: #c62828; /* Red for error */
}
#upload-button {
margin-top: 15px;
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
#upload-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
#upload-button:hover:not(:disabled) {
background-color: #0056b3;
}
Conclusión
Ya tienes un componente de subida de archivos HTML5 con arrastrar y soltar funcional, con validación del lado del cliente, vistas previas de archivos e indicación de progreso. Esto ofrece una base sólida para gestionar subidas de archivos en aplicaciones web modernas. Para entornos de producción, recuerda implementar una validación del lado del servidor y medidas de seguridad robustas. Para ampliar aún más las capacidades con funciones como las subidas reanudables y componentes de interfaz avanzados, considera integrar una biblioteca como Uppy, que funciona a la perfección con servicios como Transloadit para un potente procesamiento de archivos en el back-end.
