CRUD en Javascript i Firebase

De Jose Castillo Aliaga
Ir a la navegación Ir a la búsqueda

Aquest article és un tutorial i no pretén aprofundir en la teoria de Javascript o de Firebase. Sols explicarem les coses que necessitem per a l'objectiu.

Les possibilitats quan ens plantegem cóm fer una web amb frontend fet en Javascript i un backend. En aquest article sols anem a tractar una: Frontend fet en Javascript pur a partir de ES6 front a un API REST generat per una base de dades Realtime del servici Firebase. Els avantatges de fer-ho així són:

  • Quasi total compatibilitat en qualsevol navegador amb un cost de computació mínim, ja que no necessitem llibreries externes ni frameworks.
  • Simplicitat si el projecte és menut.
  • Velocitat i alta disponibilitat de les dades gràcies a un servici extern molt fiable.
  • Simplicitat al tractar amb una base de dades en JSON.
  • Avantatges dels protocols REST.
  • No tindre que programar un backend tradicional (PHP o JAVA, MySQL...)

Els desavantatges són:

  • No comptar amb els beneficis d'un framework com Angular, Vue o React que simplifiquen la programació de grans projectes.
  • Possibilitat de fer-ho mal o insegur al no utilitzar llibreries per comunicar amb el servidor.
  • Dependència d'un servici extern.
  • No poder programar el backend i comptar sols amb una simple base de dades que s'ha de gestionar en el frontend.

Tan sols anem a fer una gestió de productes i llistes de productes amb la possibilitat de Crear Llegir Actualitzar i Esborrar (CRUD).

Configuració de Firebase

La documentació de Firebase és molt completa i de gran qualitat, per tant no cal entrar en detalls. Sols cal crear una base de dades Realtime a la que, de moment, li ficarem com a regles de control d'accés que tots puguen llegir i escriure sense autenticar:

{
  "rules": {
    ".read": "now < 1621116000000",  // 2021-5-16
    ".write": "now < 1621116000000",  // 2021-5-16
  }
}

En l'exemple anterior tenen un límit de data que es pot ampliar si volem.

En quant a les dades de la base de dades, anem a començar en un JSON molt simple de productes i llistes de productes:

{"listas":{"Lista1":{"nombre":"lista1","productos":[1,2,3,4]},"Lista2":{"nombre":"lista2","productos":[1,2,3,5]}},"productos":{"1":{"id":1,"marca":"Nikon","precio":649.95,"referencia":"D3400"},"2":{"id":2,"marca":"Nikon","precio":6499.95,"referencia":"D5"},"3":{"id":3,"marca":"Nikon","precio":1999.95,"referencia":"D500"},"4":{"id":4,"marca":"Nikon","precio":1049.95,"referencia":"D7200"},"5":{"id":5,"marca":"Nikon","precio":1999.95,"referencia":"D500"},"6":{"id":6,"marca":"Canon","precio":3999.95,"referencia":"EOS 5D"},"7":{"id":7,"marca":"Canon","precio":1800.95,"referencia":"EOS 7D"},"8":{"id":8,"marca":"Canon","precio":2645.95,"referencia":"EOS 80D"},"9":{"id":9,"marca":"Canon","precio":989.95,"referencia":"EOS Rebel T6"},"10":{"id":10,"marca":"Canon","precio":753.95,"referencia":"EOS-1D"},"11":{"id":11,"marca":"Canon","precio":457.95,"referencia":"EOS 1D X"}}}

És important que els elements del JSON tinguen clau:valor, és a dir, no utilitzem [] a no ser que siguen ja valors molt concrets.

En aquest cas, tenim unes llistes de productes que apunten als identificadors dels productes. Com es veu, la clau de cada producte és igual al valor de l'atribut id del producte, això és redundant i es pot llevar. En versions posteriors el llevarem.

Amb aquestes dades, si volem accedir a:

  • La llista de tots els productes anirem a la URL: http://<url de firebase>/productos.json
  • Un producte en concret: http://<url de firebase>/productos/2.json
  • La llista de productes de lista1: http://<url de firebase>/listas/Lista1/productos.json

Ho podem provar amb curl:

curl https://<url>/listas/Lista1/productos.json

O amb algun programa més còmode per fer moltes proves com és Postman


Pàgina web HTML inicial

El que anem a fer és confiar totalment la càrrega de les dades a Javascript amb les tècniques de AJAX. Per tant, l'HTML inicial serà molt simple. No obstant, utilitzarem Bootstrap per fer el disseny més agradable sense esforç. Necessitem un div contenidor on anar generant les llistes i els productes que inicialment el deixarem buit. També farem un menú per seleccionar el que necessitem.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf"
        crossorigin="anonymous"></script>
   <script src="script.js"></script>

</head>

<body>
    <header>
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <div class="container-fluid">
                <a class="navbar-brand" href="#">Navbar</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
                    data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
                    aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                        <li class="nav-item">
                            <a class="nav-link active" aria-current="page" href="#">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="#">Todos los productos</a>
                        </li>
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
                                data-bs-toggle="dropdown" aria-expanded="false">
                                Listas
                            </a>
                            <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
                                <li><a class="dropdown-item" href="#">Action</a></li>
                                <li><a class="dropdown-item" href="#">Another action</a></li>
                                <li>
                                    <hr class="dropdown-divider">
                                </li>
                                <li><a class="dropdown-item" href="#">Something else here</a></li>
                            </ul>
                        </li>
                    </ul>

                </div>
            </div>
        </nav>
    </header>
    <main>
        <section>
            <div id="container">

            </div>
        </section>

    </main>
</body>
</html>

Es tracta d'una web molt simple amb un menú superior en el que podem mostrar tots els productes, les llistes i un menú desplegable on ficarem un enllaç a cada llista.

Estructura del Javascript

Encara que siga una aplicació simple anem a intentar respectar els principis de MVC. D'aquesta manera, anem a fer un model per a productes i llistes encarregat de la comunicació amb el servidor, un controlador encarregat de comunicar el model en la vista i una vista encarregada de transformar les dades en elements del DOM i de proporcionar interacció amb l'usuari.

Per a fer-ho encara més fàcil de mantindre, podem fer una classe genèrica de model, vista i controlador per compartir coses comuns entre les diferents classes com l'accés al servidor o els mètodes de dibuixar o refrescar.

Model

La tasca del model passa per definir les classes, la manera d'aconseguir les dades del servidor i les dades que ja tenim. Tindrà funcions per a fer el CRUD contra el servidor i getters i setters per a obtindre o crear objectes de les classes. Amés, pot implementar el patró Observer per mantindre als altres informats dels canvis. En la primera versió al menys no anem a utilitzar l'Observer.

A l'hora de fer el model ens podem plantejar si és interessant mantindre'l sols per a la definició de les dades i dividir-ho en model-servici per gestionar l'accés al servidor en una classe separada. D'aquesta manera, el controlador demanarà coses al servici el qual mantindrà instàncies del model. Es pot fer tot junt, però d'aquesta manera s'entén millor el rol de cadascun. En el nostre cas, el model quedarà de forma testimonial, ja que el servici serà el que arreplegue les dades del servidor. Això és paregut amb el que passa en Angular, el model es pot equiparar a les Interfícies i els servicis ho fan tot. Com que en Javascript no hi ha tipat de dades ni interfícies, el model queda sols per a coses internes concretes de les dades que no passen en el servidor ni en la vista. Per exemple, el càlcul aleatori d'un identificador o similars.

Com que hem decidit crear una classe model base, quedarà així:

export { Model }
    class Model {
        static nombre;
        constructor(id) {
            this.id = id;
        }
        assign(plainObject) {   // El que vinga del servidor cal assignar-ho a la classe actual
            Object.assign(this, plainObject);
        }
    }

Com es pot veure, és molt simple, sols implementa la funció assign per a que quede més simple el codi del servici.

Aprofitant que ja tenim definida la classe model, anem a fer el model de productes:

import { Model } from "./model.js";
export { Product }

    class Product extends Model {
        constructor(id) {  // En realitat no necessitem indicar tots els camps perquè els assignarem amb assign
            super(id);
        }
        static nombre = 'productos'
    }

El model de productes no necessita res especial, aleshores sols crida a la classe pare per al constructor i defineix el atribut estàtic nombre per a que siga més fàcil obtindre les dades del servidor.

Per a la comunicació del model amb el servidor hem separat la lògica en model i servici. La classe genèrica de Service treballarà en Items que poden ser productes o qualsevol cosa, ja que el API REST de Firebase sempre funcionarà igual:

export { Service };

class Service {
	constructor(url,model) {
		this.url = url;
		this.Items = [];
		this.read();
		this.model = model;
	}

	read() {  // funció cridada per les demés per actualitzar les dades
		fetch(this.url + '.json')
			.then(response => response.json())
			.then(datosItems => {
				// En firebase retorna un objecte amb tots els productes. Serà més fàcil si fem un array
				// de pas, afegim el key com a id

				this.Items = Object.entries(datosItems).map(entrie => { 
					entrie[1].id = entrie[0];
					let item = new this.model(entrie[1].id); 
					item.assign(entrie[1]);
					return item; 
				});
				console.log(this.Items);
				this.onCambioItems(this.Items); // cridem al callback de la vista associat per el cotrolador amb notificarcambios
			});
	}

	add(Item) {
		console.log('add', Item);
		fetch(this.url + '.json', { method: 'post', headers: { "Content-type": "application/json; charset=UTF-8" }, body: JSON.stringify(Item) })
			.then(response => response.json())
			.then(datos => {
				this.read();
			});
	}
	update(Item) {
		// Com que volem actualitzar, els Items tenen id, que no és necessari en firebase, ja que és la clau
		// primer li llevem el id:
		let key = Item.id;
		delete Item.id;
		console.log('update', key);
		fetch(`${this.url}/${key}.json`,
			{ method: 'put', headers: { "Content-type": "application/json; charset=UTF-8" }, body: JSON.stringify(Item) })
			.then(response => response.json())
			.then(datos => {
				this.read();
			});


	}
	remove(id) {
		console.log('remove', id);
		fetch(`${this.url}/${id}.json`, { method: 'delete', headers: { "Content-type": "application/json; charset=UTF-8" }, body: {} })
			.then(response => response.json())
			.then(datos => {
				this.read();
			});
	}

	setNotificarCambios(callback) {  // Funció per a que el controlador associe els canvis amb la vista
		this.onCambioItems = callback; // callback serà una funció de la vista
	}

}

Les funcions del CRUD s'entenen sense problemes. Amb fetch li fem la petició al servidor i en complir la promesa cridem a this.read() que torna a demanar la llista sencera al servidor per actualitzar. Això pot resultar ineficient, així que en el futur deuríem sols actualitzar els ítems modificats i no tornar a demanar al servidor totes les dades. Per simplicitat no ho hem fet en aquest exercici.

La funció més complicada és la de setNotificarCambios. Aquesta rep un callback i l'assigna a la funció 'onCambioItems del servici. D'aquesta manera el servici no sap en temps de programació què fer quan acabe de demanar coses al servidor. Serà el controlador el que li diga què fer. En aquest cas serà una funció de la vista, com veurem després.

El servici de productes no necessita res especial que no tinga la classe genèrica, així que sols implementarà l'herència:

import { Product } from "../models/model_product.js";
import { Service } from "./service.js"
export { ProductService }

class ProductService extends Service {
	constructor() {
		super(`${app.url}${Product.nombre}`,Product);
	
	}
}

Com es veu, afegim l'atribut estàtic nombre per a tindre centralitzat el nom en l'API i la classe Product per a que el servici puga crear productes a partir de les dades.

Vista

La vista sols s'encarrega de mostrar les dades i d'informar de les interaccions de l'usuari. Les interaccions que suposen un canvi en les dades o una petició al servidor no seran ateses per la vista. No obstant la vista sí pot atendre alguns esdeveniments relacionats amb la interfície.

La vista tindrà algun mecanisme de notificacions cap al controlador. En el nostre cas, en principi el que farem és associar funcions del controlador a esdeveniments. D'aquesta manera la vista detecta l'esdeveniment i executa el callback que el controlador li ha manat.

Per una altra banda, la vista ha de ser notificada dels canvis de les dades en el model-servici o el controlador. Això serà amb funcions públiques que seran invocades pel controlador.

Controlador

La construcció del controlador cridarà als constructors de la vista i del model-servici. El controlador té com a funció bàsica connectar la vista en el model i, si cal, fer la lògica del negoci en mig. En el nostre exemple, el controlador serà molt lleuger però imprescindible. El constructor del controlador relacionarà els esdeveniments del model i la vista entre ells. Per fer-ho més senzill d'entendre, es crearan unes funcions intermedies que seran enviades com a callback al model o a la vista.



https://www.natapuntes.es/patron-mvc-en-vanilla-javascript/

https://github.com/Fictizia/Curso-JS-Avanzado-para-desarrolladores-Front-end_ed3/blob/master/teoria/clase18.md

https://github.com/Fictizia/Curso-JS-Avanzado-para-desarrolladores-Front-end_ed3