Uno de los casos de uso más habituales cuando de elaborar formularios se trata es la carga de archivos a un servidor. Por eso, en este tutorial voy a mostrar mediante un ejemplo como hacerlo con el stack MEAN. Obviamente como todo lo que es interesante en la vida, siempre hay más de una forma de hacer las cosas por lo que seguramente puede ser optimizado.
Lo primero que necesitamos es una herramienta en el back-end que nos ayude a “capturar” y procesar archivos. Pues bien, en el caso de Node.js una de las dependencias más conocidas para llevar a cabo esta tarea se llama multer. Para configurarla debemos especificar en donde se van a guardar los archivos que serán enviados al servidor, si en disco o en memoria, como se van a nombrar y listo, tendremos la posibilidad de manipularlos como si de parámetros pasados por GET o POST se tratase para guardar su información en la base de datos o lo que se requiera.
//Archivo server/server.jsconst storage=multer.diskStorage({
destination:function(req,file,cb){//Indica la carpeta de destino
cb(null,path.join(__dirname,'/public/uploads'));
},
filename:function(req,file,cb){//Indica el nombre del archivo
cb(null,`${file.fieldname}-${Date.now()}.${file.mimetype.split('/')[1]}`);
}
});
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:false}));
const uploader=multer({storage});//Se guardara la imagen en el servidor
app.use(express.static(path.join(__dirname,'/public')));//Directorio para archivos staticos
app.use('/uploads',express.static(path.join(__dirname,'/uploads')));//Directorio de imagenes
app.post('/picture',uploader.single('file'),(req,res)=>{
const {file,body}=req;//req.file existe gracias al middleware de multer
res.status(200).json({
body:body,
file:file
})
})
app.listen(PORT,functionlisten(){
console.log(`Server listening on port ${PORT}`);
});
El stogare determina la forma como se guardan los archivos. Para el ejemplo, usé el DiskStorage. Aquí le digo a multer que vamos a guardarlos en un directorio. La propiedad destination , que es una función, le dice que deben ser guardados en el directorio uploads, mientras que la propiedad filename, que también es una función, le dice que su nombre debe ser una concatenación del nombre original del archivo subido, el timestamp y la extensión del mismo. A partir de esa configuración se crea uploader que es un middleware usado por express, porque usé express para crear la API, donde los archivos son procesados. Como se ve en el fragmento de código, uploader solicita el parámetro file, que es el name del campo del formulario que contiene el archivo y que se envía por POST para este ejemplo, y luego le asigna a req la propiedad file que contiene todo lo que debemos saber del archivo; el nombre, el mimetype, el peso en bytes y etc. También podemos ver que en el ejemplo usé body-parser para acceder a los demás parámetros enviados en el formulario.
Como llega el archivo desde el front-end es mediante una petición POST, como ya mencioné. Para esto cree un componente en Angular que contiene un formulario en el cual se puede seleccionar un archivo, para el ejemplo una imagen, ingresar un nombre, para que el body-parser del servidor tenga utilidad, y un botón que haga el submit. Además, agregue un thumbnail donde se puede ver la imagen que acabamos de seleccionar antes de enviarla al servidor.
//Archivo image-form.component.html
<form [formGroup]="imageForm" (ngSubmit)="onSubmit()">
<mat-form-field >
<input matInput
placeholder="Nombre"
formControlName=name>
</mat-form-field>
<input
type="file"
id="file"
placeholder="File"
(change)="onFileChange($event)"#fileInput
formControlName="file">
<img *ngIf="image" id="thumbnail" title="thumb" src={{image}} class="thumbnail"/>
<p></p>
<button mat-raised-buttoncolor="primary" type="submit" [disabled]="!imageForm.valid">Enviar</button>
</form>
//Archivo image-form.component.ts
@Component({
selector:"app-image-form",
templateUrl:"./image-form.component.html",
styles:[
'.thumbnail {'+
' width:200px; heigth:200px;'
],
providers:[ImageService]
})
export class ImageFormComponent implements OnInit{
imageForm:FormGroup;
image:any;
file:any;
constructor(public snackBar:MatSnackBar,
private imageService:ImageService,
private router:Router){}
ngOnInit(){
this.imageForm=new FormGroup({
name: new FormControl(null,Validators.required),
file:new FormControl(null, Validators.required)
});
}
onFileChange(event){
if(event.target.files && event.target.files.length>0){//Identifica si hay archivos
const file=event.target.files[0];
if(file.type.includes("image")){//Evaluar si es una imagen
const reader= new FileReader();
reader.readAsDataURL(file);
reader.onload=function load(){
this.image=reader.result; //Asignar al thumbnail
}.bind(this);
this.file=file;
}else{
this.snackBar.openFromComponent(ImageErrorComponent, {//Muestra el error
duration: 500,
});
}
}
}
onSubmit(){
const form=this.imageForm;
if(form.valid){//Verifica que el formulario sea válido y pasa parámetrosthis.imageService.addPicture(form.value.name,this.file)
.subscribe(
(data:any)=>{
console.log(data);
this.router.navigate([`/show/${data.file.filename}`]);
},
err=>console.log
)
}
}
}
El componente ImageForm tiene dos partes que son importantes. La primera es la función onFileChange que se ejecuta cada vez que se selecciona una imagen en el formulario. Lo primero que hace es acceder al target del evento y preguntar si tiene objetos de tipo File, si así se queda solo con el primero del arreglo, si el input tuviera el atributo multiple el arreglo de objetos File podría tener más de un elemento y sería conveniente recorrerlo todo para procesar todos los archivos, sin embargo en este caso no es necesario porque tenemos solo uno. Después de tener el File verifica que sea una imagen y si no es así usa otro de los componentes creados para mostrar el error. Luego, hace dos cosas, primero asigna el File a this.file, para que pueda ser usado en la función onSubmit, y segundo usa FileReader para leer el contenido del objeto. Esta API tiene una función llamada readAsDataUrl que como resultado convierte el archivo en Base64. Cuando se dispara el evento onload esa respuesta se guarda en this.image que luego se usa en el thumbnail para mostrar la imagen.
La segunda parte importante del componente es la función onSubmit que tiene como tarea enviar el contenido del formulario haciendo uso del ImageService.
//Archivo image.service.ts
addPicture(name:string,file:File):Observable<Object>{
constform= new FormData();//Crea un formularioform.append('name',name);
form.append('file',file,'form-data');//Asigna el campo Filereturn this.http.post<Object>(this.api,form).
pipe(
catchError(this.handleError)
);
}
En la función addPicture tanto el input file como el input name se asignan a un objeto FormData que hace las veces de un formulario ficticio. Este formulario se envía a la API de express para ser procesado. Cuando recibe una respuesta positiva el componente ImageForm, que esta suscrito a lo que suceda con el servicio, redirecciona al componente ShowImage que muestra la imagen cargada.
//Archivo show-image.component.html
<div><p></p><imgsrc={{filename}}title="file"/><p></p><buttonmat-raised-buttoncolor="primary"routerLink=''>Atras</button></div>
//Archivo show-image.component.ts
@Component({
selector:'app-show-image',
templateUrl:'./show-component.component.html',
styles:[
'img {'+
' width:300px; heigth:300px;'
]
})
export classShowImageComponentimplementsOnInit{
filename:string;
constructor(private router:ActivatedRoute){
this.filename='http://localhost:3000/uploads/';
}
ngOnInit(){
this.router.params.subscribe((params)=>{
this.filename=this.filename+params.filename;
})
}
}
Este componente accede a los parámetros de la URL y toma el nombre del archivo para acceder al mismo en el servidor y mostrarlo en pantalla gracias a que se configuró el directorio uploads para servir contenido estatico.
Pues bien, eso es todo:
El ejemplo funcionando puede verse en https://multer-tuto.herokuapp.com/ .
El código fuente del ejemplo esta en el repositorio https://github.com/daespuor/multer-tutorial.
Las referencias son https://nehalist.io/uploading-files-in-angular2/
Gracias por tu ayuda me sirvió bastante. UmU