Cree una aplicación JAMstack dinámica con GatsbyJS y FaunaDB
En este artículo, explicamos la diferencia entre aplicaciones de una sola página (SPA) y sitios estáticos, y cómo podemos reunir las ventajas de ambos mundos en una aplicación JAMstack dinámica utilizando GatsbyJS y FaunaDB. Construiremos una aplicación que extraiga algunos datos de FaunaDB durante el tiempo de compilación, preprocesará el HTML para una entrega rápida al cliente y luego cargará datos adicionales en el tiempo de ejecución a medida que el usuario interactúa con la página. Esta combinación de tecnologías nos brinda los mejores atributos de los sitios y SPA generados estáticamente.
En resumen... respira hondo... ¡sitios web distribuidos de escalamiento automático con baja latencia, interfaces de usuario ágiles, sin recargas y datos dinámicos para todos!
Backends pesados, aplicaciones de una sola página, sitios estáticos
En los viejos tiempos, cuando JavaScript era nuevo, se usaba principalmente solo para proporcionar efectos e interacciones mejoradas. Algunas animaciones aquí, un menú desplegable allá y eso fue todo. El trabajo pesado se realizó en el backend mediante Perl, Java o PHP.
Esto cambió a medida que pasó el tiempo: el código del cliente se volvió más pesado y JavaScript se hizo cargo cada vez más del frontend hasta que finalmente enviamos HTML en su mayor parte vacío y representamos toda la interfaz de usuario en el navegador, dejando que el backend nos proporcionara datos JSON.
Esto llevó a una clara separación de preocupaciones y nos permitió crear aplicaciones completas con JavaScript, llamadas aplicaciones de página única (SPA). La ventaja más importante de los SPA era la ausencia de recargas. Puede hacer clic en un enlace para cambiar lo que se muestra, sin provocar una recarga completa de la página. Esto en sí mismo proporcionó una experiencia de usuario superior. Sin embargo, los SPA aumentaron significativamente el tamaño del código del cliente; un cliente ahora tenía que esperar la suma de varias latencias:
- Servir latencia: recuperar HTML y JavaScript del servidor donde JavaScript era más grande de lo que solía ser
- Latencia de carga de datos: carga de datos adicionales solicitados por el cliente
- Latencia de representación del marco de frontend: una vez que se reciben los datos, un marco de frontend como React, Vue o Angular todavía tiene mucho trabajo por hacer para construir el HTML final.
Una metáfora real
Podemos comparar la carga de un SPA con la construcción y entrega de un castillo de juguete. El cliente necesita recuperar HTML y JavaScript, luego recuperar los datos y luego aún tiene que ensamblar la página. Los componentes básicos se entregan, pero aún es necesario ensamblarlos una vez entregados.
Si tan solo hubiera una manera de construir el castillo de antemano...
Ingrese a JAMstack
Las aplicaciones JAMstack constan de J avaScript, API y M arkup. Con los generadores de sitios estáticos actuales como Next.js y GatsbyJS, las partes de JavaScript y Markup se pueden agrupar en un paquete estático e implementar a través de una red de entrega de contenido (CDN) que entrega archivos a un navegador. Una CDN distribuye geográficamente los paquetes y otros activos en múltiples ubicaciones. Cuando el navegador de un usuario recupera el paquete y los recursos, puede recibirlos desde la ubicación más cercana en la red, lo que reduce la latencia de servicio.
Siguiendo con nuestra analogía con el castillo de juguete, las aplicaciones JAMstack se diferencian de las SPA en el sentido de que la página (o castillo) se entrega premontada. Tenemos una latencia menor ya que recibimos el castillo entero y ya no tenemos que construirlo.
Hacer que las aplicaciones JAMstack estáticas sean dinámicas con hidratación
En el enfoque JAMstack, comenzamos con una aplicación dinámica y renderizamos previamente páginas HTML estáticas para entregarlas a través de una CDN rápida. Pero, ¿qué pasa si un sitio completamente estático no es suficiente y necesitamos admitir algún contenido dinámico a medida que el usuario interactúa con componentes individuales, sin recargar toda la página? Ahí es donde entra en juego la hidratación del lado del cliente.
La hidratación es el proceso del lado del cliente mediante el cual nuestro marco de interfaz "riega" el HTML (DOM) renderizado del lado del servidor con controladores de eventos y/o componentes dinámicos para hacerlo más interactivo. Esto puede resultar complicado porque depende de conciliar el DOM original con un nuevo DOM virtual (VDOM) que se mantiene en la memoria mientras el usuario interactúa con la página. Si los árboles DOM y VDOM no coinciden, pueden surgir errores que provoquen que los elementos se muestren desordenados o que sea necesario reconstruir la página.
Afortunadamente, bibliotecas como GatsbyJS y NextJS se han diseñado para minimizar la posibilidad de que se produzcan errores relacionados con la hidratación, manejando todo por usted de inmediato con solo unas pocas líneas de código. El resultado es una aplicación web JAMstack dinámica que es a la vez más rápida y dinámica que el SPA equivalente.
Queda un detalle técnico: ¿de dónde vendrán los datos dinámicos?
¡Bases de datos distribuidas y amigables con el frontend!
Las aplicaciones JAMstack normalmente dependen de API (es decir, la "A" en JAM), pero si necesitamos cargar algún tipo de datos personalizados, necesitamos una base de datos. Y las bases de datos tradicionales siguen siendo un cuello de botella en el rendimiento para los sitios distribuidos globalmente que de otro modo se entregan a través de CDN, porque las bases de datos tradicionales solo están ubicadas en una región. En lugar de utilizar una base de datos tradicional, nos gustaría que nuestra base de datos esté en una red distribuida, como la CDN, que proporcione los datos desde una ubicación lo más cercana posible a donde estén nuestros clientes. Este tipo de base de datos se llama base de datos distribuida.
En este ejemplo, elegiremos FaunaDB ya que también es muy consistente, lo que significa que nuestros datos serán los mismos desde cualquier lugar desde donde mis clientes accedan a ellos y no se perderán. Otras características que funcionan particularmente bien con las aplicaciones JAMstack son que (a) se accede a la base de datos como una API (GraphQL o FQL) y no requiere que abra una conexión, y (b) la base de datos tiene una capa de seguridad que hace posible para acceder a datos públicos y privados de forma segura desde el frontend. Las implicaciones de esto son que podemos mantener las bajas latencias de JAMstack sin tener que escalar un backend, todo sin configuración.
Comparemos el proceso de cargar un sitio estático hidratado con la construcción del castillo de juguete. Todavía tenemos latencias más bajas gracias a la CDN, pero también menos datos, ya que la mayor parte del sitio se genera estáticamente y, por lo tanto, requiere menos renderizado. Sólo es necesario montar una pequeña parte del castillo (o la parte dinámica de la página) una vez entregado:
Aplicación de ejemplo con GatsbyJS FaunaDB
Creemos una aplicación de ejemplo que cargue datos de FaunaDB en el momento de la compilación y los represente en HTML estático, luego cargue datos dinámicos adicionales dentro del navegador del cliente en el tiempo de ejecución. Para este ejemplo, utilizamos GatsbyJS, un marco JAMstack basado en React que prerenderiza HTML estático. Como usamos GatsbyJS, podemos codificar nuestro sitio web completamente en React, generar y entregar páginas estáticas y luego cargar datos adicionales dinámicamente en tiempo de ejecución. Usaremos FaunaDB como nuestra solución de base de datos sin servidor totalmente administrada. Crearemos una aplicación donde podamos enumerar productos y reseñas.
Veamos un resumen de lo que tenemos que hacer para que nuestra aplicación de ejemplo esté en funcionamiento y luego repasemos cada paso en detalle.
- Configurar una nueva base de datos
- Agregar un esquema GraphQL a la base de datos
- Sembrar la base de datos con datos de maqueta
- Crea un nuevo proyecto GatsbyJS
- Instalar paquetes NPM
- Cree la clave del servidor para la base de datos.
- Actualice los archivos de configuración de GatsbyJS con la clave del servidor y una nueva clave de solo lectura
- Cargue los datos del producto prerenderizados en el momento de la compilación.
- Cargar las reseñas en tiempo de ejecución.
1. Configurar una nueva base de datos
Antes de comenzar, cree una cuenta en Dashboard.fauna.com. Una vez que tenga una cuenta, configuremos una nueva base de datos. Debería contener productos y sus reseñas, para que podamos cargar los productos en el momento de la compilación y las reseñas en el navegador.
2. Agregue un esquema GraphQL a la base de datos.
A continuación, usamos la clave del servidor para cargar un esquema GraphQL en nuestra base de datos. Para ello, creamos un nuevo archivo llamado esquema.gql que tiene el siguiente contenido:
type Product { title: String! description: String reviews: [Review] @relation}type Review { username: String! text: String! product: Product!}type Query { allProducts: [Product]}
Puede cargar su archivo esquema.gql a través de la consola FaunaDB haciendo clic en "GraphQL" en la barra lateral izquierda y luego haciendo clic en el botón "Importar esquema".
Al proporcionar a FaunaDB un esquema GraphQL, crea automáticamente las colecciones requeridas para las entidades en nuestro esquema (productos y reseñas). Además de eso, también crea los índices necesarios para interactuar con esas colecciones de manera significativa y eficiente. Ahora se le debería presentar un área de juegos GraphQL donde puede probar
3. Siembra la base de datos con datos de maqueta.
Para alimentar nuestra base de datos con productos y reseñas, podemos utilizar Shell en Dashboard.fauna.com:
Para crear algunos datos, usaremos Fauna Query Language (FQL), luego continuaremos con GraphQL para construir una aplicación de ejemplo. Pegue la siguiente consulta FQL en el Shell para crear tres documentos de producto:
Map( [ { title: "Screwdriver", description: "Drives screws." }, { title: "Hair dryer", description: "Dries your hair." }, { title: "Rocket", description: "Flies you to the moon and back." } ], Lambda("product", Create(Collection("Product"), { data: Var("product") }) ));
Luego podemos escribir una consulta que recupere los productos que acabamos de fabricar y cree un documento de revisión para cada documento de producto:
Map( Paginate(Match(Index("allProducts"))), Lambda("ref", Create(Collection("Review"), { data: { username: "Tina", text: "Good product!", product: Var("ref") } })));
Ambos tipos de documentos se cargarán a través de GraphQL. Sin embargo, existe una diferencia significativa entre productos y reseñas. El primero no cambiará mucho y es relativamente estático, mientras que el segundo está impulsado por el usuario. GatsbyJS nos permite cargar datos de dos formas:
- datos que se cargan en el momento de la compilación y que se utilizarán para generar el sitio estático.
- datos que se cargan en vivo en el momento de la solicitud cuando un cliente visita e interactúa con su sitio web.
En este ejemplo, elegimos permitir que los productos se carguen en el momento de la compilación y que las reseñas se carguen según demanda en el navegador. Por lo tanto, obtenemos páginas de productos HTML estáticas proporcionadas por una CDN que el usuario ve inmediatamente. Luego, cuando nuestro usuario interactúa con la página del producto, cargamos los datos para las reseñas.
4. Crea un nuevo proyecto GatsbyJS.
El siguiente comando crea un proyecto GatsbyJS basado en la plantilla inicial:
$ npx gatsby-cli new hello-world-gatsby-faunadb$ cd hello-world-gatsby-faunadb
5. Instalar paquetes npm
Para construir nuestro nuevo proyecto con Gatsby y Apollo, necesitamos algunos paquetes adicionales. Podemos instalar los paquetes con el siguiente comando:
$ npm i gatsby-source-graphql apollo-boost react-apollo
Usaremos gatsby-source-graphql como una forma de vincular las API GraphQL al proceso de compilación. Con esta biblioteca, puede realizar una llamada GraphQL desde la cual los resultados se proporcionarán automáticamente como propiedades de su componente de reacción. De esa manera, puede utilizar datos dinámicos para generar estáticamente su aplicación. El paquete apollo-boost es una biblioteca GraphQL fácilmente configurable que se utilizará para recuperar datos en el cliente. Finalmente, la biblioteca reaccionar-apolo se encargará del vínculo entre Apollo y React .
6. Cree la clave del servidor para la base de datos.
Crearemos una clave de servidor que Gatsby utilizará para prerenderizar la página. Recuerda copiar el secreto en algún lugar ya que lo usaremos más adelante. Proteja las claves del servidor con cuidado, ya que pueden usarse para crear, destruir o administrar la base de datos a la que están asignadas. Para crear la clave podemos ir al panel de fauna y crear la clave en la pestaña de seguridad.
7. Actualice los archivos de configuración de GatsbyJS con el servidor y nuevas claves de solo lectura
Para agregar soporte GraphQL a nuestro proceso de compilación, debemos agregar el siguiente código en nuestro graphql-config.js dentro de la sección de complementos donde insertaremos la clave del servidor FaunaDB que generamos hace unos momentos.
{ resolve: "gatsby-source-graphql", options: { typeName: "Fauna", fieldName: "fauna", url: "https://graphql.fauna.com/graphql", headers: { Authorization: "Bearer SERVER KEY", }, },}
Para que el acceso GraphQL funcione en el navegador, tenemos que crear una clave que solo tenga permisos para leer datos de las colecciones. FaunaDB tiene una extensa capa de seguridad en la que puedes definir eso. La forma más sencilla es ir a la consola de FaunaDB en Dashboard.fauna.com y crear una nueva función para su base de datos haciendo clic en "Seguridad" en la barra lateral izquierda, luego en "Administrar funciones" y luego en "Nueva función personalizada":
Llame al nuevo rol personalizado 'ClientRead' y asegúrese de agregar todas las colecciones e índices (estas son las colecciones que se crearon al importar el esquema GraphQL). Luego, seleccione Leer para cada uno de ellos. Tu pantalla debería verse así:
Probablemente hayas notado la pestaña Membresía en esta página. Aunque no lo usaremos en este tutorial, es bastante interesante explicarlo ya que es una forma alternativa de obtener tokens de seguridad. En la pestaña Membresía podemos especificar que las entidades de una colección (digamos que tenemos una colección de 'Usuarios') en FaunaDb son miembros de un rol particular. Eso significa que si se hace pasar por una de estas entidades en esa colección, se aplican los privilegios del rol. Usted se hace pasar por una entidad de base de datos (por ejemplo, un usuario) asociando credenciales con la entidad y utilizando la función de inicio de sesión, que devolverá un token. De esa manera también puedes implementar la autenticación basada en contraseña en FaunaDb. No lo usaremos en este tutorial, pero si le interesa, consulte el tutorial de autenticación de FaunaDB.
Ignoremos la Membresía por ahora, una vez que haya creado el rol, podemos crear una nueva clave con el nuevo rol. Como antes, haga clic en "Seguridad", luego en "Nueva clave", pero esta vez seleccione "ClientRead" en el menú desplegable Función:
Ahora, insertemos esta clave de solo lectura en el archivo de configuración gatsby-browser.js para poder llamar a la API GraphQL desde el navegador:
import React from "react"import ApolloClient from "apollo-boost"import { ApolloProvider } from "react-apollo"const client = new ApolloClient({ uri: "https://graphql.fauna.com/graphql", request: operation = { operation.setContext({ headers: { Authorization: "Bearer CLIENT_KEY", }, }) },})export const wrapRootElement = ({ element }) = ( ApolloProvider client={client}{element}/ApolloProvider)
GatsbyJS representará su componente Router como elemento raíz. Si queremos utilizar ApolloClient
todas partes de la aplicación en el cliente, debemos envolver este elemento raíz con el ApolloProvider
componente.
8. Cargue los datos del producto renderizados previamente en el momento de la compilación.
Ahora que todo está configurado, finalmente podemos escribir el código real para cargar nuestros datos. Comencemos con los productos que cargaremos en el momento de la compilación.
Para esto necesitamos modificar el archivo src/pages/index.js para que se vea así:
import React from "react"import { graphql } from "gatsby"Import Layout from "../components/Layout"const IndexPage = ({ data }) = ( Layout ul {data.fauna.allProducts.data.map(product = ( li{product.title} - {product.description}/li ))} /ul /Layout)export const query = graphql`{ fauna { allProducts { data { _id title description } } }}`export default IndexPage
GatsbyJS recogerá automáticamente la consulta exportada y la ejecutará antes de representar el componente IndexPage. El resultado de esa consulta se pasará como accesorio de datos al componente IndexPage. Si ahora ejecutamos el script de desarrollo, podremos ver los documentos pre-renderizados en el servidor de desarrollo en http://localhost:8000/
.
$ npm run develop
9. Cargue las reseñas en tiempo de ejecución.
Para cargar las reseñas de un producto en el cliente, tenemos que realizar algunos cambios en src/pages/index.js:
import { gql } from "apollo-boost"import { useQuery } from "@apollo/react-hooks"import { graphql } from "gatsby"import React, { useState } from "react"import Layout from "../components/layout"// Query for fetching at build-timeexport const query = graphql`{ fauna { allProducts { data { _id title description } } } } ` // Query for fetching on the client const GET_REVIEWS = gql ` query GetReviews($productId: ID!) { findProductByID(id: $productId) { reviews { data { _id username text } } } }`const IndexPage = props = { const [productId, setProductId] = useState(null) const { loading, data } = useQuery(GET_REVIEWS, { variables: { productId }, skip: !productId, })}export default IndexPage
Repasemos esto paso a paso.
Primero, necesitamos importar partes de los paquetes apollo-boost y apollo-react para poder usar el cliente GraphQL que configuramos previamente en el archivo gatsby-browser.js.
Luego, necesitamos implementar nuestra GET_REVIEWS
consulta. Intenta encontrar un producto por su ID y luego carga las reseñas asociadas de ese producto. La consulta toma una variable, que es productId.
En la función componente, usamos dos ganchos: useState
yuseQuery
El gancho useState realiza un seguimiento del ID del producto para el que queremos cargar reseñas. Si un usuario hace clic en un botón, el estado se establecerá en el ID del producto correspondiente a ese botón.
Luego, el useQuery
gancho aplica esto productId
para cargar reseñas de ese producto desde FaunaDB. El parámetro de omisión del enlace evita la ejecución de la consulta cuando la página se representa por primera vez porque productId será nulo.
Si ahora ejecutamos el servidor de desarrollo nuevamente y hacemos clic en los botones, nuestra aplicación debería ejecutar la consulta con diferentes ID de producto como se esperaba.
$ npm run develop
Conclusión
Una combinación de recuperación de datos del lado del servidor e hidratación del lado del cliente hace que las aplicaciones JAMstack sean bastante poderosas. Estos métodos permiten una interacción flexible con nuestros datos para que podamos adherirnos a las diferentes necesidades comerciales.
Por lo general, es una buena idea cargar la mayor cantidad de datos posible en el momento de la compilación para mejorar el rendimiento de la página. Pero si los datos no son necesarios para todos los clientes, o si son demasiado grandes para enviarlos al cliente todos a la vez, podemos dividirlos y cambiar a la carga bajo demanda en el cliente. Este es el caso de los datos específicos del usuario, la paginación o cualquier dato que cambie con bastante frecuencia y que pueda estar desactualizado cuando llegue al usuario.
En este artículo, implementamos un enfoque que carga parte de los datos en el momento de la compilación y luego carga el resto de los datos en la interfaz a medida que el usuario interactúa con la página.
Por supuesto, todavía no hemos implementado un inicio de sesión ni formularios para crear nuevas reseñas. ¿Cómo abordaríamos eso? Ese es material para otro tutorial donde podemos usar el control de acceso basado en atributos de FaunaDB para especificar qué puede leer y escribir una clave de cliente desde la interfaz.
El código de este tutorial se puede encontrar en este repositorio.
Deja una respuesta