1. Qué crearás
En este codelab, crearás un blog itinerante con un mapa colaborativo en tiempo real con lo más reciente de nuestra biblioteca de Angular: AngularFire. La última app web consistirá en un blog de viajes en el que puedes subir imágenes a cada ubicación a la que hayas viajado.
Se usará AngularFire para compilar la app web, Emulator Suite para pruebas locales, Authentication para realizar un seguimiento de los datos del usuario, Firestore y Storage para conservar datos y contenido multimedia, con la tecnología de Cloud Functions, y, por último, Firebase Hosting para implementar la app.
Qué aprenderás
- Cómo desarrollar localmente con productos de Firebase mediante Emulator Suite
- Cómo mejorar tu app web con AngularFire
- Cómo conservar tus datos en Firestore
- Cómo conservar contenido multimedia en Storage
- Cómo implementar tu app en Firebase Hosting
- Cómo usar Cloud Functions para interactuar con tus bases de datos y APIs
Requisitos
- Node.js (versión 10 o una posterior)
- Una Cuenta de Google para la creación y administración de tu proyecto de Firebase
- La versión 11.14.2 o una posterior de Firebase CLI
- Un navegador de tu elección, como Chrome
- Conocimientos básicos sobre Angular y JavaScript
2. Obtén el código de muestra
Clona el repositorio de GitHub del codelab desde la línea de comandos:
git clone https://github.com/firebase/codelab-friendlychat-web
Como alternativa, si no tienes instalado Git, puedes descargar el repositorio como un archivo ZIP.
El repositorio de GitHub contiene proyectos de muestra para varias plataformas.
En este codelab, solo se usa el repositorio de webframework:
- minSdkVersion webframework: El código inicial en el que te basarás durante este codelab.
Instala dependencias
Después de la clonación, instala las dependencias en la carpeta raíz y functions
antes de compilar la app web.
cd webframework && npm install
cd functions && npm install
Instala Firebase CLI
Instala Firebase CLI con el siguiente comando en una terminal:
npm install -g firebase-tools
Vuelve a verificar que tu versión de Firebase CLI sea superior a 11.14.2 con el siguiente comando:
firebase --version
Si tu versión es anterior a la 11.14.2, actualízala con el siguiente comando:
npm update firebase-tools
3. Crea y configura un proyecto de Firebase
Crea un proyecto de Firebase
- Accede a Firebase.
- En Firebase console, haz clic en Agregar proyecto y, luego, asígnale el nombre <tu-proyecto>. Recuerda el ID de tu proyecto de Firebase.
- Haz clic en Crear proyecto.
Importante: Tu proyecto de Firebase se llamará <tu-proyecto>, pero Firebase le asignará automáticamente un ID de proyecto único con el formato <tu-proyecto>-1234. Este identificador único es la forma en que se identifica en realidad tu proyecto (incluso en la CLI), mientras que <your-project> es solo un nombre visible.
La aplicación que vamos a compilar usa los productos de Firebase que están disponibles para las aplicaciones web:
- Firebase Authentication para permitir que los usuarios accedan a tu app con facilidad.
- Cloud Firestore, a fin de guardar datos estructurados en la nube y recibir notificaciones al instante cuando se modifiquen los datos
- Cloud Storage para Firebase, a fin de guardar archivos en la nube
- Firebase Hosting, a fin de alojar y entregar sus recursos
- Funciones para interactuar con las APIs internas y externas.
Algunos de estos productos necesitan configuraciones especiales o se deben habilitar mediante Firebase console.
Agrega una app web de Firebase al proyecto
- Haga clic en el ícono de Web para crear una nueva aplicación web de Firebase.
- En el siguiente paso, verás un objeto de configuración. Copia el contenido de este objeto en el archivo
environments/environment.ts
.
Habilitar el Acceso con Google para Firebase Authentication
Para permitir que los usuarios accedan a la app web con sus Cuentas de Google, usaremos el método de acceso de Google.
Para habilitar el Acceso con Google, haz lo siguiente:
- En Firebase console, busca la sección Build en el panel izquierdo.
- Haz clic en Authentication y, luego, en la pestaña Sign-in method (o haz clic aquí para ir directamente allí).
- Habilita el proveedor de acceso de Google y, luego, haz clic en Guardar.
- Establece el nombre público de tu app como <your-project-name> y elige un Correo electrónico de asistencia del proyecto en el menú desplegable.
Habilita Cloud Firestore
- En la sección Compilación de Firebase console, haz clic en Base de datos de Firestore.
- Haz clic en Crear base de datos en el panel de Cloud Firestore.
- Configura la ubicación en la que se almacenan tus datos de Cloud Firestore. Puedes dejar esta opción con la configuración predeterminada o elegir una región cercana.
Habilita Cloud Storage
La aplicación web usa Cloud Storage para Firebase con el objetivo de almacenar, subir y compartir fotos.
- En la sección Compilación de Firebase console, haz clic en Almacenamiento.
- Si el botón Comenzar no aparece, significa que Cloud Storage ya está disponible.
y no necesitas seguir los pasos que se indican a continuación.
- Haz clic en Get Started.
- Lee la renuncia de responsabilidad sobre las reglas de seguridad de tu proyecto de Firebase y, luego, haz clic en Siguiente.
- La ubicación de Cloud Storage está preseleccionada con la misma región que elegiste para tu base de datos de Cloud Firestore. Haz clic en Listo para completar la configuración.
Con las reglas de seguridad predeterminadas, cualquier usuario autenticado puede escribir lo que sea en Cloud Storage. Más adelante en este codelab, mejoraremos la seguridad de nuestro almacenamiento.
4. Conéctate a tu proyecto de Firebase
La interfaz de línea de comandos (CLI) de Firebase te permite usar Firebase Hosting para entregar tu app web de forma local, así como implementar la app web en tu proyecto de Firebase.
Asegúrate de que la línea de comandos acceda al directorio webframework
local de tu app.
Conecta el código de la aplicación web a tu proyecto de Firebase. Primero, accede a Firebase CLI en la línea de comandos:
firebase login
Luego, ejecuta el siguiente comando para crear un alias para el proyecto. Reemplaza $YOUR_PROJECT_ID
por el ID del proyecto de Firebase.
firebase use $YOUR_PROJECT_ID
Agrega AngularFire
Para agregar AngularFire a la app, ejecuta el siguiente comando:
ng add @angular/fire
Luego, sigue las instrucciones de la línea de comandos y selecciona las funciones que existen en tu proyecto de Firebase.
Inicializa Firebase
Para inicializar el proyecto de Firebase, ejecuta el siguiente comando:
firebase init
Luego, sigue las indicaciones de la línea de comandos y selecciona las funciones y los emuladores que usaste en el proyecto de Firebase.
Cómo iniciar los emuladores
Desde el directorio webframework
, ejecuta el siguiente comando para iniciar los emuladores:
firebase emulators:start
Finalmente, deberías ver algo como esto:
$ firebase emulators:start
i emulators: Starting emulators: auth, functions, firestore, hosting, functions
i firestore: Firestore Emulator logging to firestore-debug.log
i hosting: Serving hosting files from: public
✔ hosting: Local server: http://localhost:5000
i ui: Emulator UI logging to ui-debug.log
i functions: Watching "/functions" for Cloud Functions...
✔ functions[updateMap]: firestore function initialized.
┌─────────────────────────────────────────────────────────────┐
│ ✔ All emulators ready! It is now safe to connect your app. │
│ i View Emulator UI at http://localhost:4000 │
└─────────────────────────────────────────────────────────────┘
┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator │ Host:Port │ View in Emulator UI │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting │ localhost:5000 │ n/a │
└────────────────┴────────────────┴─────────────────────────────────┘
Emulator Hub running at localhost:4400
Other reserved ports: 4500
Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
Una vez que veas el mensaje ✔All emulators ready!
, los emuladores están listos para usarse.
Deberías ver la IU de tu app de viajes, que (aún) no está funcionando:
Ahora, comencemos a desarrollar.
5. Conecta la app web a los emuladores
Según la tabla de los registros del emulador, el emulador de Cloud Firestore escucha en el puerto 8080 y el emulador de Authentication escucha en el puerto 9099.
Abre la IU del emulador.
En tu navegador web, ve a http://127.0.0.1:4000/. Deberías ver la IU de Emulator Suite.
Enruta la app para que use los emuladores
En src/app/app.module.ts
, agrega el siguiente código a la lista de importaciones de AppModule
:
@NgModule({
declarations: [...],
imports: [
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => {
const auth = getAuth();
if (location.hostname === 'localhost') {
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
}
return auth;
}),
provideFirestore(() => {
const firestore = getFirestore();
if (location.hostname === 'localhost') {
connectFirestoreEmulator(firestore, '127.0.0.1', 8080);
}
return firestore;
}),
provideFunctions(() => {
const functions = getFunctions();
if (location.hostname === 'localhost') {
connectFunctionsEmulator(functions, '127.0.0.1', 5001);
}
return functions;
}),
provideStorage(() => {
const storage = getStorage();
if (location.hostname === 'localhost') {
connectStorageEmulator(storage, '127.0.0.1', 5001);
}
return storage;
}),
...
]
Ahora, la app está configurada para usar emuladores locales, lo que permite realizar pruebas y desarrollo de forma local.
6. Agregar autenticación
Ahora que los emuladores están configurados para la app, podemos agregar funciones de Authentication para garantizar que cada usuario acceda antes de publicar mensajes.
Para ello, podemos importar funciones signin
directamente desde AngularFire y realizar un seguimiento del estado de autenticación del usuario con la función authState
. Modifica las funciones de la página de acceso para que la página verifique el estado de autenticación del usuario en el momento de la carga.
Cómo insertar la autenticación de AngularFire
En src/app/pages/login-page/login-page.component.ts
, importa Auth
desde @angular/fire/auth
y, luego, inyéctalo en LoginPageComponent
. Los proveedores de autenticación, como Google, y funciones como signin
y signout
también se pueden importar directamente desde el mismo paquete y usarse en la app.
import { Auth, GoogleAuthProvider, signInWithPopup, signOut, user } from '@angular/fire/auth';
export class LoginPageComponent implements OnInit {
private auth: Auth = inject(Auth);
private provider = new GoogleAuthProvider();
user$ = user(this.auth);
constructor() {}
ngOnInit(): void {}
login() {
signInWithPopup(this.auth, this.provider).then((result) => {
const credential = GoogleAuthProvider.credentialFromResult(result);
return credential;
})
}
logout() {
signOut(this.auth).then(() => {
console.log('signed out');}).catch((error) => {
console.log('sign out error: ' + error);
})
}
}
Ahora la página de acceso funciona. Intenta acceder y consulta los resultados en el emulador de Authentication.
7. Configura Firestore
En este paso, agregarás funcionalidad para publicar y actualizar entradas de blog de viajes almacenadas en Firestore.
Al igual que Authentication, las funciones de Firestore vienen empaquetadas previamente desde AngularFire. Cada documento pertenece a una colección y cada documento también puede tener colecciones anidadas. Se requiere conocer el path
del documento en Firestore para crear y actualizar una entrada de blog de viajes.
Cómo implementar TravelService
Dado que se necesitarán muchas páginas diferentes para leer y actualizar documentos de Firestore en la app web, podemos implementar las funciones en src/app/services/travel.service.ts
, para evitar inyectar repetidamente las mismas funciones de AngularFire en cada página.
Comienza por insertar Auth
, de manera similar al paso anterior, así como Firestore
en nuestro servicio. También es útil definir un objeto user$
observable que escuche el estado de autenticación actual.
import { doc, docData, DocumentReference, Firestore, getDoc, setDoc, updateDoc, collection, addDoc, deleteDoc, collectionData, Timestamp } from "@angular/fire/firestore";
export class TravelService {
firestore: Firestore = inject(Firestore);
auth: Auth = inject(Auth);
user$ = authState(this.auth).pipe(filter(user => user !== null), map(user => user!));
router: Router = inject(Router);
Agregar una publicación de viajes
Las publicaciones de viajes existirán como documentos que se almacenan en Firestore y, como los documentos deben existir dentro de las colecciones, la colección que contiene todas las publicaciones de viajes tendrá el nombre travels
. Por lo tanto, la ruta de cualquier publicación de viajes será travels/
.
Con la función addDoc
de AngularFire, se puede insertar un objeto en una colección:
async addEmptyTravel(userId: String) {
...
addDoc(collection(this.firestore, 'travels'), travelData).then((travelRef) => {
collection(this.firestore, `travels/${travelRef.id}/stops`);
setDoc(travelRef, {... travelData, id: travelRef.id})
this.router.navigate(['edit', `${travelRef.id}`]);
return travelRef;
})
}
Actualiza y borra datos
Con el UID de cualquier publicación de viaje, se puede deducir la ruta del documento almacenado en Firestore, que luego se puede leer, actualizar o borrar con las funciones updateFoc
y deleteDoc
de AngularFire:
async updateData(path: string, data: Partial<Travel | Stop>) {
await updateDoc(doc(this.firestore, path), data)
}
async deleteData(path: string) {
const ref = doc(this.firestore, path);
await deleteDoc(ref)
}
Lectura de datos como un elemento observable
Dado que los postes y las paradas de viaje a lo largo del camino se pueden modificar después de su creación, sería más útil obtener los objetos del documento como observables para suscribirse a cualquier cambio que se realice. Las funciones docData
y collectionData
de @angular/fire/firestore
ofrecen esta funcionalidad.
getDocData(path: string) {
return docData(doc(this.firestore, path), {idField: 'id'}) as Observable<Travel | Stop>
}
getCollectionData(path: string) {
return collectionData(collection(this.firestore, path), {idField: 'id'}) as Observable<Travel[] | Stop[]>
}
Cómo agregar paradas a una publicación de viajes
Ahora que se configuraron las operaciones de publicación de viajes, es momento de considerar las paradas, que se encontrarán en una subcolección de una publicación de viajes, de esta manera: travels/
Esto es casi idéntico a crear una publicación de viajes, así que anímate a implementarla por tu cuenta o consulta la siguiente implementación:
async addStop(travelId: string) {
...
const ref = await addDoc(collection(this.firestore, `travels/${travelId}/stops`), stopData)
setDoc(ref, {...stopData, id: ref.id})
}
¡Genial! Las funciones de Firestore se implementaron en el servicio de viajes, por lo que ahora puedes verlas en acción.
Usa funciones de Firestore en la app
Navega a src/app/pages/my-travels/my-travels.component.ts
y, luego, inserta TravelService
para usar sus funciones.
travelService = inject(TravelService);
travelsData$: Observable<Travel[]>;
stopsList$!: Observable<Stop[]>;
constructor() {
this.travelsData$ = this.travelService.getCollectionData(`travels`) as Observable<Travel[]>
}
Se llama a TravelService
en el constructor para obtener un array observable de todos los viajes.
En el caso en que solo se necesiten los viajes del usuario actual, usa la función query
.
Otros métodos para garantizar la seguridad incluyen implementar reglas de seguridad o usar Cloud Functions con Firestore, como se explora en los pasos opcionales que aparecen a continuación.
Luego, simplemente llama a las funciones implementadas en TravelService
.
async createTravel(userId: String) {
this.travelService.addEmptyTravel(userId);
}
deleteTravel(travelId: String) {
this.travelService.deleteData(`travels/${travelId}`)
}
Ahora la página Mis viajes debería estar funcionando. Comprueba lo que sucede en tu emulador de Firestore cuando creas una publicación de viaje nueva.
Luego, repite el proceso para las funciones de actualización en /src/app/pages/edit-travels/edit-travels.component.ts
:
travelService: TravelService = inject(TravelService)
travelId = this.activatedRoute.snapshot.paramMap.get('travelId');
travelData$: Observable<Travel>;
stopsData$: Observable<Stop[]>;
constructor() {
this.travelData$ = this.travelService.getDocData(`travels/${this.travelId}`) as Observable<Travel>
this.stopsData$ = this.travelService.getCollectionData(`travels/${this.travelId}/stops`) as Observable<Stop[]>
}
updateCurrentTravel(travel: Partial<Travel>) {
this.travelService.updateData(`travels${this.travelId}`, travel)
}
updateCurrentStop(stop: Partial<Stop>) {
stop.type = stop.type?.toString();
this.travelService.updateData(`travels${this.travelId}/stops/${stop.id}`, stop)
}
addStop() {
if (!this.travelId) return;
this.travelService.addStop(this.travelId);
}
deleteStop(stopId: string) {
if (!this.travelId || !stopId) {
return;
}
this.travelService.deleteData(`travels${this.travelId}/stops/${stopId}`)
this.stopsData$ = this.travelService.getCollectionData(`travels${this.travelId}/stops`) as Observable<Stop[]>
}
8. Cómo configurar el almacenamiento
Ahora implementarás Storage para almacenar imágenes y otros tipos de contenido multimedia.
Cloud Firestore funciona mejor para almacenar datos estructurados, como objetos JSON. Cloud Storage está diseñado para almacenar archivos o BLOB. En esta app, la usarás para permitir que los usuarios compartan sus fotos de viajes.
De forma similar con Firestore, almacenar y actualizar archivos con Storage requiere un identificador único para cada archivo.
Implementemos las funciones en TraveService
:
Carga un archivo
Navega a src/app/services/travel.service.ts
y, luego, inserta Storage desde AngularFire:
export class TravelService {
firestore: Firestore = inject(Firestore);
auth: Auth = inject(Auth);
storage: Storage = inject(Storage);
Implementa la función de carga:
async uploadToStorage(path: string, input: HTMLInputElement, contentType: any) {
if (!input.files) return null
const files: FileList = input.files;
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (file) {
const imagePath = `${path}/${file.name}`
const storageRef = ref(this.storage, imagePath);
await uploadBytesResumable(storageRef, file, contentType);
return await getDownloadURL(storageRef);
}
}
return null;
}
La diferencia principal entre acceder a documentos desde Firestore y archivos desde Cloud Storage es que, aunque ambos siguen rutas de acceso estructuradas por carpetas, la combinación de URL base y ruta de acceso se obtiene a través de getDownloadURL
, que luego se puede almacenar y usar en un archivo .
Cómo usar la función en la app
Navega a src/app/components/edit-stop/edit-stop.component.ts
y llama a la función de carga con el siguiente comando:
async uploadFile(file: HTMLInputElement, stop: Partial<Stop>) {
const path = `/travels/${this.travelId}/stops/${stop.id}`
const url = await this.travelService.uploadToStorage(path, file, {contentType: 'image/png'});
stop.image = url ? url : '';
this.travelService.updateData(path, stop);
}
Cuando se sube la imagen, el archivo multimedia se sube al almacenamiento y la URL se almacena según corresponda en el documento en Firestore.
9. Implementa la aplicación
Ya estamos listos para implementar la aplicación.
Copia los parámetros de configuración firebase
de src/environments/environment.ts
en src/environments/environment.prod.ts
y ejecuta lo siguiente:
firebase deploy
Deberías ver algo como esto:
✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.
=== Deploying to 'friendly-travels-b6a4b'...
i deploying storage, firestore, hosting
i firebase.storage: checking storage.rules for compilation errors...
✔ firebase.storage: rules file storage.rules compiled successfully
i firestore: reading indexes from firestore.indexes.json...
i cloud.firestore: checking firestore.rules for compilation errors...
✔ cloud.firestore: rules file firestore.rules compiled successfully
i storage: latest version of storage.rules already up to date, skipping upload...
i firestore: deploying indexes...
i firestore: latest version of firestore.rules already up to date, skipping upload...
✔ firestore: deployed indexes in firestore.indexes.json successfully for (default) database
i hosting[friendly-travels-b6a4b]: beginning deploy...
i hosting[friendly-travels-b6a4b]: found 6 files in .firebase/friendly-travels-b6a4b/hosting
✔ hosting[friendly-travels-b6a4b]: file upload complete
✔ storage: released rules storage.rules to firebase.storage
✔ firestore: released rules firestore.rules to cloud.firestore
i hosting[friendly-travels-b6a4b]: finalizing version...
✔ hosting[friendly-travels-b6a4b]: version finalized
i hosting[friendly-travels-b6a4b]: releasing new version...
✔ hosting[friendly-travels-b6a4b]: release complete
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendly-travels-b6a4b/overview
Hosting URL: https://friendly-travels-b6a4b.web.app
10. ¡Felicitaciones!
Ahora tu aplicación debería estar completa y, luego, implementarse en Firebase Hosting. Ahora podrás acceder a todos los datos y análisis desde Firebase console.
Para obtener más funciones relacionadas con AngularFire, Functions y las reglas de seguridad, no olvides consultar los pasos opcionales que se incluyen a continuación, así como otros Codelabs de Firebase.
11. Opcional: Protecciones de autenticación de AngularFire
Junto con Firebase Authentication, AngularFire también ofrece protecciones basadas en la autenticación en las rutas, de modo que se puedan redireccionar a los usuarios que no tienen acceso suficiente. Esto ayuda a proteger la app contra el acceso de los usuarios a datos protegidos.
En src/app/app-routing.module.ts
, importa
import {AuthGuard, redirectLoggedInTo, redirectUnauthorizedTo} from '@angular/fire/auth-guard'
Luego, puedes definir funciones como cuándo y dónde se debe redireccionar a los usuarios en determinadas páginas:
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['signin']);
const redirectLoggedInToTravels = () => redirectLoggedInTo(['my-travels']);
Luego, solo tienes que agregarlos a tus rutas:
const routes: Routes = [
{path: '', component: LoginPageComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectLoggedInToTravels}},
{path: 'signin', component: LoginPageComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectLoggedInToTravels}},
{path: 'my-travels', component: MyTravelsComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectUnauthorizedToLogin}},
{path: 'edit/:travelId', component: EditTravelsComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectUnauthorizedToLogin}},
];
12. Opcional: reglas de seguridad
Firestore y Cloud Storage usan reglas de seguridad (firestore.rules
y security.rules
, respectivamente) para aplicar la seguridad y validar los datos.
En este momento, los datos de Firestore y Storage tienen acceso abierto para lecturas y escrituras, pero no es conveniente que las personas cambien las publicaciones de los demás. Puedes usar reglas de seguridad para restringir el acceso a tus colecciones y documentos.
Reglas de Firestore
Para permitir que solo los usuarios autenticados vean las publicaciones de viajes, ve al archivo firestore.rules
y agrega lo siguiente:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/travels {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
}
}
Las reglas de seguridad también se pueden usar para validar datos:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/posts {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
&& "author" in request.resource.data
&& "text" in request.resource.data
&& "timestamp" in request.resource.data;
}
}
Reglas de almacenamiento
De manera similar, podemos usar reglas de seguridad para aplicar de manera forzosa el acceso a las bases de datos de almacenamiento en storage.rules
. Ten en cuenta que también podemos usar funciones para verificaciones más complejas:
rules_version = '2';
function isImageBelowMaxSize(maxSizeMB) {
return request.resource.size < maxSizeMB * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
service firebase.storage {
match /b/{bucket}/o {
match /{userId}/{postId}/{filename} {
allow write: if request.auth != null
&& request.auth.uid == userId && isImageBelowMaxSize(5);
allow read;
}
}
}