1. Descripción general
Objetivos
En este codelab, compilarás una app web para recomendar restaurantes con la tecnología de Cloud Firestore.
Qué aprenderás
- Cómo leer y escribir datos en Cloud Firestore desde una app web
- Cómo detectar cambios en datos de Cloud Firestore en tiempo real
- Cómo usar Firebase Authentication y reglas de seguridad para proteger datos de Cloud Firestore
- Cómo escribir consultas complejas de Cloud Firestore
Requisitos
Antes de comenzar este codelab, asegúrate de haber instalado lo siguiente:
2. Crea y configura un proyecto de Firebase
Crea un proyecto de Firebase
- En Firebase console, haz clic en Agregar proyecto y asígnale el nombre FriendlyEats al proyecto.
Recuerda el ID de tu proyecto de Firebase.
- Haz clic en Crear proyecto.
La aplicación que compilaremos usa algunos servicios de Firebase disponibles en la Web:
- Firebase Authentication para identificar a tus usuarios con mayor facilidad
- Cloud Firestore para guardar datos estructurados en la nube y recibir notificaciones instantáneas cuando se actualizan los datos
- Firebase Hosting para alojar y entregar tus recursos estáticos
Para este codelab específico, ya configuramos Firebase Hosting. Sin embargo, en el caso de Firebase Auth y Cloud Firestore, te explicaremos cómo configurar y habilitar los servicios con Firebase console.
Cómo habilitar la autenticación anónima
Si bien la autenticación no es el tema central de este codelab, es importante tener algún tipo de autenticación en nuestra app. Usaremos el acceso anónimo, lo que significa que al usuario se le brindará acceso, de forma silenciosa, sin que se le solicite.
Deberás habilitar el acceso anónimo.
- En Firebase console, busca la sección Build en el panel de navegación 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 Anónimo y, luego, haz clic en Guardar.
Esto permitirá que la aplicación les brinde, de manera silenciosa, acceso a tus usuarios cuando ingresen a la aplicación web. Para obtener más información, consulta la documentación sobre autenticación anónima.
Habilita Cloud Firestore
La app usa Cloud Firestore para guardar y recibir información sobre los restaurantes y las calificaciones.
Deberás habilitar Cloud Firestore. En la sección Build de Firebase console, haz clic en Base de datos de Firestore. Haz clic en Crear base de datos en el panel de Cloud Firestore.
Las reglas de seguridad controlan el acceso a los datos en Cloud Firestore. Hablaremos más sobre las reglas más adelante en este codelab, pero primero debemos establecer algunas reglas básicas en nuestros datos para comenzar. En la pestaña Reglas de Firebase console, agrega las siguientes reglas y, luego, haz clic en Publicar.
rules_version = '2'; service cloud.firestore { // Determine if the value of the field "key" is the same // before and after the request. function unchanged(key) { return (key in resource.data) && (key in request.resource.data) && (resource.data[key] == request.resource.data[key]); } match /databases/{database}/documents { // Restaurants: // - Authenticated user can read // - Authenticated user can create/update (for demo purposes only) // - Updates are allowed if no fields are added and name is unchanged // - Deletes are not allowed (default) match /restaurants/{restaurantId} { allow read: if request.auth != null; allow create: if request.auth != null; allow update: if request.auth != null && (request.resource.data.keys() == resource.data.keys()) && unchanged("name"); // Ratings: // - Authenticated user can read // - Authenticated user can create if userId matches // - Deletes and updates are not allowed (default) match /ratings/{ratingId} { allow read: if request.auth != null; allow create: if request.auth != null && request.resource.data.userId == request.auth.uid; } } } }
Más adelante en el codelab, analizaremos estas reglas y cómo funcionan.
3. Obtén el código de muestra
Clona el repositorio de GitHub desde la línea de comandos:
git clone https://github.com/firebase/friendlyeats-web
El código de muestra se debería haber clonado en el directorio 📁friendlyeats-web
. A partir de este momento, asegúrate de ejecutar todos los comandos desde este directorio:
cd friendlyeats-web/vanilla-js
Importa la app de partida
Con tu IDE (WebStorm, Atom, Sublime, Visual Studio Code, etc.), abre o importa el directorio 📁friendlyeats-web
. Este directorio contiene el código de partida para el codelab que consiste en una app para recomendar restaurantes que todavía no es funcional. La haremos funcional a lo largo de este codelab, por lo que pronto deberás editar el código en ese directorio.
4. Instala la interfaz de línea de comandos de Firebase
La interfaz de línea de comandos (CLI) de Firebase te permite publicar tus aplicaciones web a nivel local y, luego, implementarlas en Firebase Hosting.
- Para instalar la CLI, ejecuta el siguiente comando npm:
npm -g install firebase-tools
- Ejecuta el siguiente comando para verificar que la CLI se haya instalado de forma correcta:
firebase --version
Asegúrate de que la versión de Firebase CLI sea v7.4.0 o posterior.
- Ejecuta el siguiente comando para autorizar Firebase CLI:
firebase login
Configuramos la plantilla de app web para extraer la configuración de tu app para Firebase Hosting de los archivos y el directorio locales de tu app. Sin embargo, para ello, debemos asociar tu app con tu proyecto de Firebase.
- Asegúrate de que la línea de comandos acceda al directorio local de la app.
- Ejecuta el siguiente comando para asociar tu app con el proyecto de Firebase:
firebase use --add
- Cuando se te solicite, selecciona el ID del proyecto y, luego, asígnale un alias a tu proyecto de Firebase.
Un alias es útil si tienes varios entornos (producción, etapa de pruebas, etcétera). Sin embargo, en este codelab, solo usaremos el alias de default
.
- Sigue las instrucciones restantes en la línea de comandos.
5. Ejecuta el servidor local
Ya está todo listo para comenzar a trabajar en nuestra app. Ejecutemos nuestra app de forma local.
- Ejecuta el siguiente comando de Firebase CLI:
firebase emulators:start --only hosting
- Tu línea de comandos debería mostrar la siguiente respuesta:
hosting: Local server: http://localhost:5000
Usamos el emulador de Firebase Hosting para entregar nuestra app de forma local. Ahora, la app web debería estar disponible en http://localhost:5000.
- Abre tu app en http://localhost:5000.
Deberías ver tu copia de FriendlyEats, que se conectó a tu proyecto de Firebase.
La app se conectó automáticamente a tu proyecto de Firebase y te permitió acceder, de forma silenciosa, como usuario anónimo.
6. Cómo escribir datos en Cloud Firestore
En esta sección, escribiremos algunos datos en Cloud Firestore para poder propagar la IU de la app. Esto se puede hacer de forma manual a través de Firebase console, pero lo haremos en la app para demostrar una operación de escritura básica de Cloud Firestore.
Modelo de datos
Los datos de Firestore se dividen en colecciones, documentos, campos y subcolecciones. Almacenaremos cada restaurante como un documento en una colección de nivel superior llamada restaurants
.
Más adelante, almacenaremos cada opinión en una subcolección llamada ratings
en cada restaurante.
Agrega restaurantes a Firestore
El objeto principal del modelo en nuestra app es un restaurante. Escribamos código que agregue un documento de restaurante a la colección restaurants
.
- En los archivos descargados, abre
scripts/FriendlyEats.Data.js
. - Busca la función
FriendlyEats.prototype.addRestaurant
. - Reemplaza toda la función por el siguiente código.
FriendlyEats.Data.js
FriendlyEats.prototype.addRestaurant = function(data) { var collection = firebase.firestore().collection('restaurants'); return collection.add(data); };
El código anterior agrega un documento nuevo a la colección restaurants
. Los datos del documento provienen de un objeto JavaScript simple. Para ello, primero debemos obtener una referencia a una colección de Cloud Firestore restaurants
y, luego, add
los datos.
¡Agregaremos restaurantes!
- Regresa a la app de FriendlyEats en tu navegador y actualízala.
- Haz clic en Add Mock Data.
La app generará automáticamente un conjunto aleatorio de objetos de restaurantes y, luego, llamará a tu función addRestaurant
. Sin embargo, aún no verás los datos en tu aplicación web real porque aún debemos implementar la opción para recuperar los datos (en la próxima sección del codelab).
Sin embargo, si navegas hasta la pestaña Cloud Firestore en Firebase console, deberías ver documentos nuevos en la colección restaurants
.
¡Felicitaciones! Acabas de escribir datos en Cloud Firestore desde una app web.
En la siguiente sección, aprenderás a recuperar datos de Cloud Firestore y mostrarlos en tu app.
7. Cómo mostrar datos de Cloud Firestore
En esta sección, aprenderás a recuperar datos de Cloud Firestore y mostrarlos en tu app. Los dos pasos clave consisten en crear una consulta y agregar un objeto de escucha de instantáneas. Este objeto de escucha recibirá una notificación de todos los datos existentes que coincidan con la consulta y recibirá actualizaciones en tiempo real.
Primero, construyamos la consulta que entregará la lista predeterminada de restaurantes sin filtros.
- Regresa al archivo
scripts/FriendlyEats.Data.js
. - Busca la función
FriendlyEats.prototype.getAllRestaurants
. - Reemplaza toda la función por el siguiente código.
FriendlyEats.Data.js
FriendlyEats.prototype.getAllRestaurants = function(renderer) { var query = firebase.firestore() .collection('restaurants') .orderBy('avgRating', 'desc') .limit(50); this.getDocumentsInQuery(query, renderer); };
En el código anterior, construimos una consulta que recuperará hasta 50 restaurantes de la colección de nivel superior llamada restaurants
, que se ordenan por la calificación promedio (en este momento, todos en cero). Después de declarar esta consulta, la pasamos al método getDocumentsInQuery()
, que es responsable de cargar y renderizar los datos.
Para ello, agregaremos un objeto de escucha de instantáneas.
- Regresa al archivo
scripts/FriendlyEats.Data.js
. - Busca la función
FriendlyEats.prototype.getDocumentsInQuery
. - Reemplaza toda la función por el siguiente código.
FriendlyEats.Data.js
FriendlyEats.prototype.getDocumentsInQuery = function(query, renderer) { query.onSnapshot(function(snapshot) { if (!snapshot.size) return renderer.empty(); // Display "There are no restaurants". snapshot.docChanges().forEach(function(change) { if (change.type === 'removed') { renderer.remove(change.doc); } else { renderer.display(change.doc); } }); }); };
En el código anterior, query.onSnapshot
activará su devolución de llamada cada vez que haya un cambio en el resultado de la consulta.
- La primera vez, la devolución de llamada se activa con todo el conjunto de resultados de la consulta, es decir, toda la colección
restaurants
de Cloud Firestore. Luego, pasa todos los documentos individuales a la funciónrenderer.display
. - Cuando se borra un documento,
change.type
es igual aremoved
. En este caso, llamaremos a una función que quite el restaurante de la IU.
Ahora que implementamos ambos métodos, actualiza la app y verifica que los restaurantes que vimos antes en Firebase console ahora se puedan observar en la app. Si completaste esta sección con éxito, tu app ahora lee y escribe datos con Cloud Firestore.
A medida que cambie tu lista de restaurantes, este objeto de escucha se seguirá actualizando automáticamente. Prueba ir a Firebase console y borrar, de forma manual, un restaurante o cambiar su nombre. Observarás que los cambios aparecen en el sitio de inmediato.
8. Datos de Get()
Hasta ahora, mostramos cómo usar onSnapshot
para recuperar actualizaciones en tiempo real. Sin embargo, no siempre es lo que queremos hacer. A veces, tiene más sentido solo recuperar los datos una vez.
Queremos implementar un método que se active cuando un usuario haga clic en un restaurante específico de tu app.
- Regresa al archivo
scripts/FriendlyEats.Data.js
. - Busca la función
FriendlyEats.prototype.getRestaurant
. - Reemplaza toda la función por el siguiente código.
FriendlyEats.Data.js
FriendlyEats.prototype.getRestaurant = function(id) { return firebase.firestore().collection('restaurants').doc(id).get(); };
Después de implementar este método, podrás ver las páginas de cada restaurante. Solo haz clic en un restaurante de la lista y verás la página de detalles del restaurante:
Por ahora, no puedes agregar calificaciones, ya que aún debemos implementarlas más adelante en el codelab.
9. Ordena y filtra datos
Actualmente, nuestra app muestra una lista de restaurantes, pero el usuario no tiene manera de filtrar según sus necesidades. En esta sección, usarás las consultas avanzadas de Cloud Firestore para habilitar filtros.
A continuación, se muestra un ejemplo de una consulta simple para recuperar todos los restaurantes Dim Sum
:
var filteredQuery = query.where('category', '==', 'Dim Sum')
Como su nombre lo indica, el método where()
hará que nuestra consulta descargue solo los miembros de la colección cuyos campos cumplan con las restricciones que configuramos. En este caso, solo descargará restaurantes en los que category
sea Dim Sum
.
En nuestra app, el usuario puede encadenar varios filtros para crear consultas específicas, como "Pizza en San Francisco" o "Mariscos en Los Ángeles ordenados por popularidad".
Crearemos un método que compile una consulta que filtre nuestros restaurantes según varios criterios que seleccionen nuestros usuarios.
- Regresa al archivo
scripts/FriendlyEats.Data.js
. - Busca la función
FriendlyEats.prototype.getFilteredRestaurants
. - Reemplaza toda la función por el siguiente código.
FriendlyEats.Data.js
FriendlyEats.prototype.getFilteredRestaurants = function(filters, renderer) { var query = firebase.firestore().collection('restaurants'); if (filters.category !== 'Any') { query = query.where('category', '==', filters.category); } if (filters.city !== 'Any') { query = query.where('city', '==', filters.city); } if (filters.price !== 'Any') { query = query.where('price', '==', filters.price.length); } if (filters.sort === 'Rating') { query = query.orderBy('avgRating', 'desc'); } else if (filters.sort === 'Reviews') { query = query.orderBy('numRatings', 'desc'); } this.getDocumentsInQuery(query, renderer); };
El código anterior agrega varios filtros where
y una sola cláusula orderBy
para compilar una consulta compuesta según las entradas del usuario. Ahora, nuestra consulta solo mostrará restaurantes que coincidan con los requisitos del usuario.
Actualiza la app de FriendlyEats en el navegador y, luego, verifica que puedas filtrar por precio, ciudad y categoría. Mientras realizas las pruebas, verás errores en la Consola de JavaScript de tu navegador que se ven de la siguiente manera:
The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=...
Estos errores se producen porque Cloud Firestore requiere índices para la mayoría de las consultas compuestas. Exigir la indexación en las consultas mantiene la velocidad de Cloud Firestore a gran escala.
Si abres el vínculo desde el mensaje de error, se abrirá automáticamente la IU para crear índices en Firebase console con los parámetros correctos ya completos. En la siguiente sección, escribiremos e implementaremos los índices necesarios para esta aplicación.
10. Cómo implementar índices
Si no queremos explorar todas las rutas de nuestra app y seguir cada uno de los vínculos para crear índices, podemos implementar fácilmente muchos índices a la vez con Firebase CLI.
- En el directorio local descargado de tu app, encontrarás un archivo
firestore.indexes.json
.
Este archivo describe todos los índices que se necesitan para todas las combinaciones posibles de filtros.
firestore.indexes.json
{ "indexes": [ { "collectionGroup": "restaurants", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "city", "order": "ASCENDING" }, { "fieldPath": "avgRating", "order": "DESCENDING" } ] }, ... ] }
- Implementa estos índices con el siguiente comando:
firebase deploy --only firestore:indexes
Después de unos minutos, se publicarán tus índices y desaparecerán los mensajes de error.
11. Cómo escribir datos en una transacción
En esta sección, agregaremos la capacidad de que los usuarios envíen opiniones a los restaurantes. Hasta ahora, todas nuestras operaciones de escritura han sido atómicas y relativamente sencillas. Si se produjo un error en alguna de ellas, es probable que solo debas solicitarle al usuario que vuelva a escribir, o nuestra app volverá a intentarlo automáticamente.
Nuestra app tendrá muchos usuarios que desean agregar una calificación para un restaurante, por lo que debemos coordinar varias lecturas y escrituras. Primero, se debe enviar la opinión, y luego se deben actualizar las calificaciones count
y average rating
del restaurante. Si uno de estos falla, pero el otro no, se produce un estado incoherente en el que los datos de una parte de la base de datos no coinciden con los de otra.
Afortunadamente, Cloud Firestore brinda una funcionalidad de transacción que nos permite realizar varias operaciones de lectura y escritura en una sola operación atómica, lo que garantiza la coherencia de nuestros datos.
- Regresa al archivo
scripts/FriendlyEats.Data.js
. - Busca la función
FriendlyEats.prototype.addRating
. - Reemplaza toda la función por el siguiente código.
FriendlyEats.Data.js
FriendlyEats.prototype.addRating = function(restaurantID, rating) { var collection = firebase.firestore().collection('restaurants'); var document = collection.doc(restaurantID); var newRatingDocument = document.collection('ratings').doc(); return firebase.firestore().runTransaction(function(transaction) { return transaction.get(document).then(function(doc) { var data = doc.data(); var newAverage = (data.numRatings * data.avgRating + rating.rating) / (data.numRatings + 1); transaction.update(document, { numRatings: data.numRatings + 1, avgRating: newAverage }); return transaction.set(newRatingDocument, rating); }); }); };
En el bloque anterior, activamos una transacción para actualizar los valores numéricos de avgRating
y numRatings
en el documento del restaurante. Al mismo tiempo, agregamos el nuevo rating
a la subcolección ratings
.
12. Protege los datos
Al principio de este codelab, configuramos las reglas de seguridad de nuestra app para restringir el acceso a ella.
firestore.rules
rules_version = '2'; service cloud.firestore { // Determine if the value of the field "key" is the same // before and after the request. function unchanged(key) { return (key in resource.data) && (key in request.resource.data) && (resource.data[key] == request.resource.data[key]); } match /databases/{database}/documents { // Restaurants: // - Authenticated user can read // - Authenticated user can create/update (for demo purposes only) // - Updates are allowed if no fields are added and name is unchanged // - Deletes are not allowed (default) match /restaurants/{restaurantId} { allow read: if request.auth != null; allow create: if request.auth != null; allow update: if request.auth != null && (request.resource.data.keys() == resource.data.keys()) && unchanged("name"); // Ratings: // - Authenticated user can read // - Authenticated user can create if userId matches // - Deletes and updates are not allowed (default) match /ratings/{ratingId} { allow read: if request.auth != null; allow create: if request.auth != null && request.resource.data.userId == request.auth.uid; } } } }
Estas reglas restringen el acceso para garantizar que los clientes solo realicen cambios seguros. Por ejemplo:
- Las actualizaciones de un documento de restaurante solo pueden cambiar las calificaciones, pero no el nombre ni otros datos inmutables.
- Solo se pueden crear calificaciones si el ID del usuario coincide con el usuario que accedió, lo que evita la falsificación de identidad.
Como alternativa a Firebase console, puedes usar Firebase CLI para implementar reglas en tu proyecto de Firebase. El archivo firestore.rules de tu directorio de trabajo ya contiene las reglas anteriores. Para implementar estas reglas desde tu sistema de archivos local (en lugar de usar Firebase console), ejecuta el siguiente comando:
firebase deploy --only firestore:rules
13. Conclusión
En este codelab, aprendiste a realizar operaciones de lectura y escritura básicas y avanzadas con Cloud Firestore, así como a proteger el acceso a los datos con reglas de seguridad. Puedes encontrar la solución completa en el repositorio de quickstarts-js.
Para obtener más información sobre Cloud Firestore, consulta los siguientes recursos:
14. [Opcional] Aplica la política con la Verificación de aplicaciones
La Verificación de aplicaciones de Firebase proporciona protección, ya que ayuda a validar y evitar el tráfico no deseado en tu app. En este paso, agregarás la Verificación de aplicaciones con reCAPTCHA Enterprise para proteger el acceso a tus servicios.
Primero, deberás habilitar la Verificación de aplicaciones y reCAPTCHA.
Cómo habilitar reCaptcha Enterprise
- En la consola de Cloud, busca y selecciona reCAPTCHA Enterprise en Seguridad.
- Habilita el servicio según se te solicite y haz clic en Create Key.
- Ingresa un nombre visible según se te solicite y selecciona Sitio web como el tipo de plataforma.
- Agrega tus URLs implementadas a la lista de dominios y asegúrate de que la opción "Usar el desafío de la casilla de verificación" esté sin seleccionar.
- Haz clic en Create Key y almacena la clave generada en un lugar seguro. La necesitarás más adelante en este paso.
Cómo habilitar la Verificación de aplicaciones
- En Firebase console, busca la sección Build en el panel izquierdo.
- Haz clic en Verificación de aplicaciones y, luego, en el botón Comenzar (o redirecciona directamente a console).
- Haz clic en Registrar y, cuando se te solicite, ingresa tu clave de reCaptcha Enterprise. Luego, haz clic en Guardar.
- En la vista de APIs, selecciona Almacenamiento y haz clic en Aplicar. Haz lo mismo con Cloud Firestore.
La Verificación de aplicaciones ahora debería aplicarse. Actualiza la app y prueba crear o ver un restaurante. Deberías recibir el siguiente mensaje de error:
Uncaught Error in snapshot listener: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
Esto significa que la Verificación de aplicaciones bloquea las solicitudes no validadas de forma predeterminada. Ahora, agreguemos la validación a tu app.
Navega al archivo FriendlyEats.View.js, actualiza la función initAppCheck
y agrega tu clave de reCaptcha para inicializar la Verificación de aplicaciones.
FriendlyEats.prototype.initAppCheck = function() {
var appCheck = firebase.appCheck();
appCheck.activate(
new firebase.appCheck.ReCaptchaEnterpriseProvider(
/* reCAPTCHA Enterprise site key */
),
true // Set to true to allow auto-refresh.
);
};
La instancia de appCheck
se inicializa con un ReCaptchaEnterpriseProvider
con tu clave, y isTokenAutoRefreshEnabled
permite que los tokens se actualicen automáticamente en tu app.
Para habilitar las pruebas locales, busca la sección en la que se inicializa la app en el archivo FriendlyEats.js y agrega la siguiente línea a la función FriendlyEats.prototype.initAppCheck
:
if(isLocalhost) {
self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;
}
Esto registrará un token de depuración en la consola de tu app web local similar al siguiente:
App Check debug token: 8DBDF614-649D-4D22-B0A3-6D489412838B. You will need to add it to your app's App Check settings in the Firebase console for it to work.
Ahora, ve a la vista de apps de la Verificación de aplicaciones en Firebase console.
Haz clic en el menú ampliado y selecciona Administrar tokens de depuración.
Luego, haz clic en Add debug token y pega el token de depuración de tu consola cuando se te solicite.
¡Felicitaciones! App Check debería funcionar en tu app.