En la revisión de temas necesarios para hacer una aplicación siempre existe la necesidad de cargar (upload) ficheros desde el Navegador.
Existen varias estrategias para enviar archivos desde el navegador al servidor, cada una con sus ventajas y desafíos. Las más comunes son:
1. Envío tradicional con formularios HTML (multipart/form-data
)
- El navegador gestiona automáticamente la codificación del archivo.
- El servidor (como PHP) recibe los datos en
$_FILES
, lo que facilita la validación y almacenamiento. - Sin embargo, este método puede ser menos flexible en aplicaciones SPA (Single Page Application) como las que se construyen con Svelte.
2. Envío mediante JavaScript usando FormData
- Permite una integración más dinámica con el frontend.
- Se puede combinar con
fetch
oXMLHttpRequest
para enviar los datos sin recargar la página. - Ideal para validar y preprocesar los archivos en el cliente antes de enviarlos.
3. Codificación en base64 y envío como JSON
- El archivo se convierte en una cadena base64 y se envía como parte de un objeto JSON.
- Útil cuando se quiere incrustar la imagen directamente en el HTML o en una base de datos.
- Aunque aumenta el tamaño del archivo (~33%), permite una visualización inmediata sin necesidad de rutas físicas
Objetivo
Codificar un ejemplo simple, donde se pueda revisar el funcionamiento de los campos de tipo «file» y cómo se accede a la estructura de datos del fichero/s y cómo se accede al contenido del mismo.
También, cómo se pueden tratar esos ficheros en el server (PHP SLIM 4.0)
DEMO: https://fhumanes.com/my-upload-app/
Solución Técnica
Este ejemplo tiene 2 partes, el frontend, que es Svelte5 y el backend, que PHP SLIM 4.0.
Uno de los aspectos más importantes que me ha motivado para este artículo es la estructura interna que se recoge de un campo de tipo «file» y cómo podemos tratar toda esa información en el frontend y después enviar al server la información de todos los campos del formulario, incluido la de los ficheros.
Validaciones en frameworks JavaScript: una ventaja estratégica
En entornos como Svelte, toda la lógica de validación se realiza en el cliente. Aunque esto puede parecer una carga, en realidad ofrece ventajas importantes:
- Experiencia de usuario mejorada: Las validaciones son instantáneas, sin necesidad de esperar respuesta del servidor.
- Control total: Puedes definir reglas personalizadas, mostrar mensajes en tiempo real y evitar envíos innecesarios.
- Flexibilidad: Puedes decidir si enviar el archivo como
FormData
, base64 o incluso fragmentarlo para subirlo por partes.
Este enfoque convierte al cliente en un filtro inteligente que reduce la carga del servidor y mejora la eficiencia general del sistema.
Estructura del array de ficheros seleccionados
Cuando el usuario selecciona uno o varios archivos en un <input type="file">
, el navegador genera una estructura tipo array que se puede recorrer y manipular desde JavaScript. Esta estructura es una instancia de FileList
, que se comporta como un array indexado.
Cada elemento del array es un objeto de tipo File
, con propiedades clave como:
name
: nombre del archivo.size
: tamaño en bytes.type
: tipo MIME (por ejemplo,image/png
).lastModified
: fecha de última modificación.webkitRelativePath
: útil en cargas de carpetas.
Este array permite iterar sobre los archivos seleccionados, aplicar validaciones (tipo, tamaño, extensión), y decidir cómo se enviarán al servidor. En Svelte, puedes vincular esta estructura a variables reactivas, lo que facilita la gestión visual y lógica del proceso de subida.
Código Svelte5
<script> let file; let preview = $state(''); let message = $state(''); async function handleUpload() { if (!file) return; const reader = new FileReader(); console.log("Archivo seleccionado:", file); reader.onload = async () => { preview = reader.result; // Mostrar imagen completa (Base64 con encabezado) console.log("Preview (Base64 con encabezado):", preview); const base64 = reader.result.split(',')[1]; // Solo los datos // URL base de tu API REST de PHP // Asegúrate de que esta URL sea la correcta para tu PUBLIC folder let appHost = ''; appHost = window.location.hostname; // Para obtener el host de la URL console.log("estoy conectado al Host: ",appHost); let API_BASE_URL = 'http://localhost/upload-server/v1/upload'; if (appHost == 'localhost') { API_BASE_URL = 'http://localhost/upload-server/v1/upload'; } else { API_BASE_URL = 'https://fhumanes.com/upload-server/v1/upload'; } const payload = { filename: file.name, mimetype: file.type, size: file.size, base64: base64 }; console.log("Información de la foto:", payload); try { const res = await fetch(API_BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); message = data.message; } catch (err) { message = '❌ Error al enviar al servidor'; } }; reader.readAsDataURL(file); } function mostrarImagen(e) { file = e.target.files[0]; const reader = new FileReader(); reader.onload = () => { preview = reader.result; // Mostrar imagen completa (Base64 con encabezado) }; reader.readAsDataURL(file); } </script> <style> .preview { max-width: 100px; margin-top: 1rem; border: 1px solid #ccc; border-radius: 8px; } </style> <h2>📤 Subir imagen PNG/JPG</h2> <input type="file" accept="image/png, image/jpeg" onchange="{mostrarImagen}" > <button onclick="{handleUpload}">Subir al servidor</button> {#if preview} <img class="preview" src="{preview}" alt="Vista previa" /> {/if} {#if message} <p>{message}</p> {/if}
Disponemos de «function mostrarImagen», que se utiliza para mostrar la imagen en el momento que selecciona un fichero (en el ejemplo sólo se puede selecciona uno). En el código ppdemos identificar el array «files», el elemento «0, es el primero y en nuestro caso único fichero y cómo cargamos si contenido (cómo imagen embebida, es decir, sin URL, ya que no tiene referencia externa. Esto se hace en línea 61, utilizando la clase definida en la línea 57.
También, debéis revisar la «function handleUpload», que es la que recoge todos los atributos del fichero y los envía al server (el contenido del fichero lo pasa a Base64). Hay una parte del código que identifica si se está ejecutando el «localhost», para definir la URL del server. Esto es una forma de tener le mismo código para el despliegue en Local y en Remoto (hosting).
Código PHP SLIM 4.X
<?php /** * * @About: API Interface * @File: index.php * @Date: $Date:$ Ene 2025 * @Version: $Rev:$ 1.0 * @Developer: Federico Guzman || Modificado por Fernando Humanes para PHP 8.1 **/ /* Los headers permiten acceso desde otro dominio (CORS) a nuestro REST API o desde un cliente remoto via HTTP * Removiendo las lineas header() limitamos el acceso a nuestro RESTfull API a el mismo dominio * Nótese los métodos permitidos en Access-Control-Allow-Methods. Esto nos permite limitar los métodos de consulta a nuestro RESTfull API * Mas información: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS **/ // $dominioPermitido = "http://localhost:3000"; // header("Access-Control-Allow-Origin: $dominioPermitido"); // Para restringir desde dónde se pueden hacer peticines header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Headers: Content-Type, authorization, Authorization, token-user "); // header("Access-Control-Allow-Headers: *"); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Methods: PUT, GET, POST, DELETE, OPTIONS'); // header("Access-Control-Allow-Headers: X-Requested-With"); header('Content-Type: text/html; charset=utf-8'); // header('Content-Type: multipart/form-data'); // header('Content-Type: application/x-www-form-urlencoded'); header('Content-Type: application/json'); header('P3P: CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"'); // session_cache_limiter(false); include_once '../include/Config.php'; // Configuration Rest Api // require_once("../../include/dbcommon.php"); // DataBase PHPRunner // Debug // $debugCode = false; // custom_error(1,"URL ejecutada: ".$_SERVER["REQUEST_URI"]); // To debug // $debugCode = false; // use App\Models\Db; // Utilizamos la conexión de PHPRunner use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface as RequestHandler; use Slim\Factory\AppFactory; // use DI\Container; use Slim\Routing\RouteCollectorProxy; use Slim\Middleware\BodyParsingMiddleware; require_once __DIR__ . '/../libs/autoload.php'; // Library SLIM v4 $app = AppFactory::create(); $app->addRoutingMiddleware(); // $app->add(new BasePathMiddleware($app)); // No usar si se ejecuta en subdirectorio $app->addErrorMiddleware(true, true, true); $app->addBodyParsingMiddleware(); $app->setBasePath(SCRIPTS_DIR); // Indica el directorio desde donde está trabajando // require_once '../include/DbMovies.php'; // $db = new DbMovies(); // -------------------------------------------------------------------------------------- // $app->post('/upload', function (Request $request, Response $response) { // Obtener los datos de Body $data = json_decode($request->getBody()->getContents(), true); $filename = basename($data['filename']); $mimetype = $data['mimetype']; $size = $data['size']; $base64 = $data['base64']; $decoded = base64_decode($base64); $path = __DIR__ . "/../uploads/$filename"; file_put_contents($path, $decoded); $response->getBody()->write(json_encode([ 'message' => "Archivo $filename guardado correctamente" ])); return $response->withHeader('Content-Type', 'application/json'); }); /* Runner the aplication */ $app->run()
He utilizado en parte, la plantilla utilizada en el articulo S-006, que es la que os pido que utilicéis para escribir programas de este tipo.
Lo único que hace es validad los datos y crear el fichero, con el mismo nombre que tenía en el frontend, en el directorio «upload».
El ejemplo es simple y muy poco código. Espero que os guste, que os haya facilitado información de cómo se gestionan los campos de tipo «file» y que a partir de ahí, veáis todas las cosas que podemos hacer antes de enviar el fichero al server.
Como siempre, os dejo los fuentes, para que los descarguéis y hagáis todas los cambios que necesitéis.