Implementación de un sitio Jamstack sin servidor con RedwoodJS, Fauna y Vercel
Este artículo está dirigido a cualquier persona interesada en el ecosistema emergente de herramientas y tecnologías relacionadas con Jamstack y serverless. Usaremos la API GraphQL de Fauna como back-end sin servidor para un front-end Jamstack creado con el marco Redwood e implementado con un solo clic en Vercel.
En otras palabras, ¡mucho que aprender! Al final, no solo podrás sumergirte en Jamstack y los conceptos sin servidor, sino que también experimentarás una experiencia práctica con una combinación realmente genial de tecnología que creo que te gustará mucho.
Creando una aplicación Redwood
Redwood es un marco para aplicaciones sin servidor que reúne React (para componentes de front-end), GraphQL (para datos) y Prisma (para consultas de bases de datos).
Hay otros marcos de interfaz de usuario que podemos usar aquí. Un ejemplo es Bison, creado por Chris Ball. Aprovecha GraphQL de manera similar a Redwood, pero utiliza una línea ligeramente diferente de bibliotecas GraphQL, como Nexus en lugar de Apollo Client y GraphQL Codegen, en lugar de Redwood CLI. Pero solo han pasado unos meses, por lo que el proyecto aún es muy nuevo en comparación con Redwood, que ha estado en desarrollo desde junio de 2019.
Hay muchas plantillas iniciales excelentes de Redwood que podríamos usar para iniciar nuestra aplicación, pero quiero comenzar generando un proyecto estándar de Redwood y observando las diferentes piezas que componen una aplicación de Redwood. Luego construiremos el proyecto, pieza por pieza.
Necesitaremos instalar Yarn para usar la CLI de Redwood y comenzar. Una vez que esté listo, esto es lo que debe ejecutar en una terminal.
yarn create redwood-app ./csstricks
Ahora ingresaremos cd
a nuestro nuevo directorio de proyectos e iniciaremos nuestro servidor de desarrollo.
cd csstricksyarn rw dev
La interfaz de nuestro proyecto ahora se está ejecutando en localhost:8910
. Nuestro back-end está funcionando localhost:8911
y listo para recibir consultas GraphQL. De forma predeterminada, Redwood viene con un área de juegos GraphiQL que usaremos hacia el final del artículo.
Vayamos localhost:8910
al navegador. Si todo está bien, la página de inicio de Redwood debería cargarse.
Redwood se encuentra actualmente en la versión 0.21.0, al momento de escribir este artículo. Los documentos advierten contra su uso en producción hasta que llegue oficialmente a 1.0. También tienen un foro comunitario donde agradecen los comentarios y aportaciones de desarrolladores como usted.
Estructura de directorios
Redwood valora las convenciones sobre la configuración y toma muchas decisiones por nosotros, incluida la elección de tecnologías, cómo se organizan los archivos e incluso las convenciones de nombres. Esto puede resultar en una cantidad abrumadora de código repetitivo generado que es difícil de comprender, especialmente si estás investigando esto por primera vez.
Así es como está estructurado el proyecto:
├── api│ ├── prisma│ │ ├── schema.prisma│ │ └── seeds.js│ └── src│ ├── functions│ │ └── graphql.js│ ├── graphql│ ├── lib│ │ └── db.js│ └── services└── web ├── public │ ├── favicon.png │ ├── README.md │ └── robots.txt └── src ├── components ├── layouts ├── pages │ ├── FatalErrorPage │ │ └── FatalErrorPage.js │ └── NotFoundPage │ └── NotFoundPage.js ├── index.css ├── index.html ├── index.js └── Routes.js
No te preocupes demasiado por lo que significa todo esto todavía; Lo primero que hay que notar es que todo está dividido en dos directorios principales: web
y api
. Los espacios de trabajo de Yarn permiten que cada lado tenga su propia ruta en el código base.
web
contiene nuestro código de interfaz para:
- paginas
- Diseños
- Componentes
api
contiene nuestro código back-end para:
- Controladores de funciones
- Lenguaje de definición de esquemas
- Servicios para lógica empresarial back-end
- Cliente de base de datos
Redwood asume Prisma como almacén de datos, pero en su lugar usaremos Fauna. ¿Por qué Fauna cuando podríamos usar Firebase con la misma facilidad? Bueno, es sólo una preferencia personal. Después de que Google compró Firebase, lanzó una base de datos de documentos en tiempo real, Cloud Firestore, como sucesora de la Firebase Realtime Database original. Al integrarnos con el ecosistema más grande de Firebase, podríamos tener acceso a una gama más amplia de funciones que las que ofrece Fauna. Al mismo tiempo, hay incluso un puñado de proyectos comunitarios que han experimentado con Firestore y GraphQL, pero no hay soporte GraphQL de primera clase por parte de Google.
Como consultaremos a Fauna directamente, podemos eliminar el prisma
directorio y todo lo que contiene. También podemos eliminar todo el código en db.js
. Simplemente no elimine el archivo ya que lo usaremos para conectarnos al cliente Fauna.
índice.html
Comenzaremos echando un vistazo al web
lateral, ya que debería resultar familiar para los desarrolladores con experiencia en el uso de React u otros marcos de aplicaciones de una sola página.
Pero, ¿qué sucede realmente cuando creamos una aplicación React? Toma todo el sitio y lo mete todo en una gran bola de JavaScript dentro index.js
, luego mete esa bola de JavaScript en el nodo DOM “raíz”, que está en la línea 11 de index.html
.
!DOCTYPE htmlhtml head meta charset="UTF-8" / meta name="viewport" content="width=device-width, initial-scale=1.0" / link rel="icon" type="image/png" href="/favicon.png" / title%= htmlWebpackPlugin.options.title %/title /head body div/div // HIGHLIGHT /body/html
Si bien Redwood usa Jamstack en su documentación y marketing, Redwood aún no realiza pre-renderizado (como lo hacen Next o Gatsby), pero sigue siendo Jamstack en el sentido de que envía archivos estáticos y utiliza API con JavaScript para obtener datos.
index.js
index.js
contiene nuestro componente raíz (esa gran bola de JavaScript) que se representa en el nodo DOM raíz. document.getElementById()
selecciona un elemento con un id
contenedor redwood-app
y ReactDOM.render()
representa nuestra aplicación en el elemento DOM raíz.
RedwoodProveedor
El Routes /
componente (y por extensión todas las páginas de la aplicación) están contenidos dentro de las RedwoodProvider
etiquetas. Flash utiliza la API Context para pasar objetos de mensajes entre componentes profundamente anidados. Proporciona una unidad de visualización de mensajes típica para representar los mensajes proporcionados por FlashContext.
El componente proveedor de FlashContext está empaquetado con el RedwoodProvider /
componente, por lo que está listo para usarse de inmediato. Los componentes pasan objetos de mensaje suscribiéndose a ellos (piense, “enviar y recibir”) a través del gancho useFlash proporcionado.
Límite de error fatal
El proveedor en sí está contenido dentro del FatalErrorBoundary
componente que se utiliza FatalErrorPage
como accesorio. Esto hace que su sitio web muestre de forma predeterminada una página de error cuando todo lo demás falla.
import ReactDOM from 'react-dom'import { RedwoodProvider, FatalErrorBoundary } from '@redwoodjs/web'import FatalErrorPage from 'src/pages/FatalErrorPage'import Routes from 'src/Routes'import './index.css'ReactDOM.render( FatalErrorBoundary page={FatalErrorPage} RedwoodProvider Routes / /RedwoodProvider /FatalErrorBoundary, document.getElementById('redwood-app'))
Rutas.js
Router
Contiene todas nuestras rutas y cada ruta se especifica con un Route
. Redwood Router intenta hacer coincidir la URL actual con cada ruta, deteniéndose cuando encuentra una coincidencia y luego representa solo esa ruta. La única excepción es la notfound
ruta que genera una ruta única Route
con un notfound
accesorio cuando ninguna otra ruta coincide.
import { Router, Route } from '@redwoodjs/router'const Routes = () = { return ( Router Route notfound page={NotFoundPage} / /Router )}export default Routes
paginas
Ahora que nuestra aplicación está configurada, ¡comencemos a crear páginas! Usaremos el generate page
comando CLI de Redwood para crear una función de ruta con nombre de llamada home
. Esto representa el HomePage
componente cuando coincide con la ruta URL a /
.
También podemos usar rw
en lugar de redwood
y g
en lugar de generate
para ahorrar algo de escritura.
yarn rw g page home /
Este comando realiza cuatro acciones separadas:
- Crea
web/src/pages/HomePage/HomePage.js
. El nombre especificado en el primer argumento se escribe en mayúscula y se añade “Página” al final. - Crea un archivo de prueba
web/src/pages/HomePage/HomePage.test.js
con una única prueba aprobada para que pueda fingir que está realizando un desarrollo basado en pruebas. - Crea un archivo Storybook en
web/src/pages/HomePage/HomePage.stories.js
. - Agrega una novedad
Route
queweb/src/Routes.js
asigna la/
ruta alHomePage
componente.
Página principal
Si vamos a web/src/pages
veremos un HomePage
directorio que contiene un HomePage.js
archivo. Esto es lo que contiene:
// web/src/pages/HomePage/HomePage.jsimport { Link, routes } from '@redwoodjs/router'const HomePage = () = { return ( h1HomePage/h1 p Find me in code./web/src/pages/HomePage/HomePage.js/code /p p My default route is named codehome/code, link to me with ` Link to={routes.home()}Home/Link` /p / )}export default HomePage
Vamos a mover la navegación de nuestra página a un componente de diseño reutilizable, lo que significa que podemos eliminar las importaciones Link
y routes
también Link to={routes.home()}Home/Link
. Esto es lo que nos queda:
// web/src/pages/HomePage/HomePage.jsconst HomePage = () = { return ( h1RedwoodJS+FaunaDB+Vercel /h1 pTaking Fullstack to the Jamstack/p / )}export default HomePage
Acerca de la página
Para crear nuestro AboutPage
, ingresaremos casi exactamente el mismo comando que acabamos de hacer, pero con about
en lugar de home
. Tampoco necesitamos especificar la ruta ya que es el mismo que el nombre de nuestra ruta. En este caso, el nombre y la ruta se establecerán en about
.
yarn rw g page about
// web/src/pages/AboutPage/AboutPage.jsimport { Link, routes } from '@redwoodjs/router'const AboutPage = () = { return ( h1AboutPage/h1 p Find me in code./web/src/pages/AboutPage/AboutPage.js/code /p p My default route is named codeabout/code, link to me with ` Link to={routes.about()}About/Link` /p / )}export default AboutPage
Haremos algunas ediciones en la página Acerca de como hicimos con nuestra página de inicio. Eso incluye sacar Link
e routes
importar y eliminar Link to={routes.about()}About/Link
.
Aquí está el resultado final:
// web/src/pages/AboutPage/AboutPage.jsconst AboutPage = () = { return ( h1About /h1 pFor those who want to stack their Jam, fully/p / )}
Si volvemos a Routes.js
veremos nuestras nuevas rutas para home
y about
. ¡Qué bueno que Redwood haga esto por nosotros!
const Routes = () = { return ( Router Route path="/about" page={AboutPage} name="about" / Route path="/" page={HomePage} name="home" / Route notfound page={NotFoundPage} / /Router )}
Diseños
Ahora queremos crear un encabezado con enlaces de navegación que podamos importar fácilmente a nuestras diferentes páginas. Queremos usar un diseño para poder agregar navegación a tantas páginas como queramos importando el componente en lugar de tener que escribir el código en cada página.
BlogDiseño
Quizás ahora se pregunte: "¿Existe un generador de diseños?" La respuesta es… ¡por supuesto! El comando es casi idéntico a lo que hemos estado haciendo hasta ahora, excepto que rw g layout
sigue el nombre del diseño, en lugar de rw g page
seguir el nombre y la ruta de la ruta.
yarn rw g layout blog
// web/src/layouts/BlogLayout/BlogLayout.jsconst BlogLayout = ({ children }) = { return {children}/}export default BlogLayout
Para crear enlaces entre diferentes páginas necesitaremos:
- Importar
Link
yroutes
desde@redwoodjs/router
haciaBlogLayout.js
- Crear un
Link to={}/Link
componente para cada enlace - Pase una función de ruta con nombre, como
routes.home()
, alto={}
accesorio para cada ruta
// web/src/layouts/BlogLayout/BlogLayout.jsimport { Link, routes } from '@redwoodjs/router'const BlogLayout = ({ children }) = { return ( header h1RedwoodJS+FaunaDB+Vercel /h1 nav ul li Link to={routes.home()}Home/Link /li li Link to={routes.about()}About/Link /li /ul /nav /header main p{children}/p /main / )}export default BlogLayout
No veremos nada diferente en el navegador todavía. Creamos el BlogLayout
pero no lo hemos importado a ninguna página. Entonces BlogLayout
, importemos HomePage
y ajustemos la declaración completa return
con las BlogLayout
etiquetas.
// web/src/pages/HomePage/HomePage.jsimport BlogLayout from 'src/layouts/BlogLayout'const HomePage = () = { return ( BlogLayout pTaking Fullstack to the Jamstack/p /BlogLayout )}export default HomePage
Si hacemos clic en el enlace a la página Acerca de, seremos llevados allí, pero no podremos volver a la página anterior porque aún no BlogLayout
la hemos importado AboutPage
. Hagamos eso ahora:
// web/src/pages/AboutPage/AboutPage.jsimport BlogLayout from 'src/layouts/BlogLayout'const AboutPage = () = { return ( BlogLayout pFor those who want to stack their Jam, fully/p /BlogLayout )}export default AboutPage
¡Ahora podemos navegar hacia adelante y hacia atrás entre las páginas haciendo clic en los enlaces de navegación! A continuación, crearemos nuestro esquema GraphQL para que podamos comenzar a trabajar con datos.
Lenguaje de definición de esquemas de fauna.
Para que esto funcione, necesitamos crear un nuevo archivo llamado sdl.gql
e ingresar el siguiente esquema en el archivo. La fauna tomará este esquema y realizará algunas transformaciones.
// sdl.gqltype Post { title: String! body: String!}type Query { posts: [Post]}
Guarde el archivo y cárguelo en GraphQL Playground de Fauna. Tenga en cuenta que, en este punto, necesitará una cuenta de Fauna para continuar. Hay un nivel gratuito que funciona bien para lo que estamos haciendo.
Es muy importante que Redwood y Fauna estén de acuerdo con el SDL, por lo que no podemos usar el SDL original que se ingresó en Fauna porque ya no es una representación precisa de los tipos tal como existen en nuestra base de datos de Fauna.
La Post
colección y las publicaciones Index
aparecerán sin cambios si ejecutamos las consultas predeterminadas en el shell, pero Fauna crea un PostPage
tipo intermediario que tiene un data
objeto.
Lenguaje de definición de esquema de Redwood
Este data
objeto contiene una matriz con todos los Post
objetos de la base de datos. Usaremos estos tipos para crear otro lenguaje de definición de esquema que se encuentre dentro de nuestro graphql
directorio al api
lado de nuestro proyecto Redwood.
// api/src/graphql/posts.sdl.jsimport gql from 'graphql-tag'export const schema = gql` type Post { title: String! body: String! } type PostPage { data: [Post] } type Query { posts: PostPage }`
Servicios
El posts
servicio envía una consulta a la API Fauna GraphQL. Esta consulta solicita una serie de publicaciones, específicamente el title
y body
para cada una. Estos están contenidos en el data
objeto de PostPage
.
// api/src/services/posts/posts.jsimport { request } from 'src/lib/db'import { gql } from 'graphql-request'export const posts = async () = { const query = gql` { posts { data { title body } } } ` const data = await request(query, 'https://graphql.fauna.com/graphql') return data['posts']}
En este punto, podemos instalar graphql-request
, un cliente mínimo para GraphQL con una API basada en promesas que se puede usar para enviar solicitudes GraphQL:
cd apiyarn add graphql-request graphql
Adjunte el token de autorización de Fauna al encabezado de la solicitud
Hasta ahora, tenemos GraphQL para datos, Fauna para modelar esos datos y graphql-request
consultarlos. Ahora necesitamos establecer una conexión entre graphql-request
Fauna, lo cual haremos importándola graphql-request
y db.js
usándola para consultar un endpoint
que esté configurado en https://graphql.fauna.com/graphql
.
// api/src/lib/db.jsimport { GraphQLClient } from 'graphql-request'export const request = async (query = {}) = { const endpoint = 'https://graphql.fauna.com/graphql' const graphQLClient = new GraphQLClient(endpoint, { headers: { authorization: 'Bearer ' + process.env.FAUNADB_SECRET }, }) try { return await graphQLClient.request(query) } catch (error) { console.log(error) return error }}
Se crea una instancia de A GraphQLClient
para configurar el encabezado con un token de autorización, lo que permite que los datos fluyan a nuestra aplicación.
Crear
Usaremos Fauna Shell y ejecutaremos un par de comandos de Fauna Query Language (FQL) para inicializar la base de datos. Primero, crearemos una publicación de blog con title
y body
.
Create( Collection("Post"), { data: { title: "Deno is a secure runtime for JavaScript and TypeScript.", body: "The original creator of Node, Ryan Dahl, wanted to build a modern, server-side JavaScript framework that incorporates the knowledge he gained building out the initial Node ecosystem." } })
{ ref: Ref(Collection("Post"), "282083736060690956"), ts: 1605274864200000, data: { title: "Deno is a secure runtime for JavaScript and TypeScript.", body: "The original creator of Node, Ryan Dahl, wanted to build a modern, server-side JavaScript framework that incorporates the knowledge he gained building out the initial Node ecosystem." }}
Creemos otro.
Create( Collection("Post"), { data: { title: "NextJS is a React framework for building production grade applications that scale.", body: "To build a complete web application with React from scratch, there are many important details you need to consider such as: bundling, compilation, code splitting, static pre-rendering, server-side rendering, and client-side rendering." } })
{ ref: Ref(Collection("Post"), "282083760102441484"), ts: 1605274887090000, data: { title: "NextJS is a React framework for building production grade applications that scale.", body: "To build a complete web application with React from scratch, there are many important details you need to consider such as: bundling, compilation, code splitting, static pre-rendering, server-side rendering, and client-side rendering." }}
Y tal vez uno más sólo para llenar las cosas.
Create( Collection("Post"), { data: { title: "Vue.js is an open-source front end JavaScript framework for building user interfaces and single-page applications.", body: "Evan You wanted to build a framework that combined many of the things he loved about Angular and Meteor but in a way that would produce something novel. As React rose to prominence, Vue carefully observed and incorporated many lessons from React without ever losing sight of their own unique value prop." } })
{ ref: Ref(Collection("Post"), "282083792286384652"), ts: 1605274917780000, data: { title: "Vue.js is an open-source front end JavaScript framework for building user interfaces and single-page applications.", body: "Evan You wanted to build a framework that combined many of the things he loved about Angular and Meteor but in a way that would produce something novel. As React rose to prominence, Vue carefully observed and incorporated many lessons from React without ever losing sight of their own unique value prop." }}
Células
Las celdas proporcionan un enfoque simple y declarativo para la obtención de datos. Contienen la consulta GraphQL junto con los estados de carga, vacío, error y éxito. Cada uno se representa automáticamente dependiendo del estado en el que se encuentre la celda.
BlogPublicacionesCelda
yarn rw generate cell BlogPostsexport const QUERY = gql` query BlogPostsQuery { blogPosts { id } }`export const Loading = () = divLoading.../divexport const Empty = () = divEmpty/divexport const Failure = ({ error }) = divError: {error.message}/divexport const Success = ({ blogPosts }) = { return JSON.stringify(blogPosts)}
De forma predeterminada, tenemos la consulta que representa los datos JSON.stringify
en la página donde se importa la celda. Haremos algunos cambios para realizar la consulta y representar los datos que necesitamos. Entonces vamos:
- Cambiar
blogPosts
aposts
. - Cambiar
BlogPostsQuery
aPOSTS
. - Cambie la consulta en sí para devolver el
title
ybody
de cada publicación. - Mapa sobre el
data
objeto en el componente de éxito. - Cree un componente con el
title
ybody
delposts
devuelto a través deldata
objeto.
Así es como se ve:
// web/src/components/BlogPostsCell/BlogPostsCell.jsexport const QUERY = gql` query POSTS { posts { data { title body } } }`export const Loading = () = divLoading.../divexport const Empty = () = divEmpty/divexport const Failure = ({ error }) = divError: {error.message}/divexport const Success = ({ posts }) = { const {data} = posts return data.map(post = ( header h2{post.title}/h2 /header p{post.body}/p / ))}
La POSTS
consulta envía una consulta para posts
y, cuando se consulta, obtenemos un data
objeto que contiene una serie de publicaciones. Necesitamos sacar el data
objeto para poder recorrerlo y obtener las publicaciones reales. Hacemos esto con la desestructuración de objetos para obtener el data
objeto y luego usamos la map()
función para mapear el data
objeto y extraer cada publicación. El title
de cada publicación se representa con un h2
interior header
y el cuerpo se representa con una p
etiqueta.
Importar BlogPostsCell a la página de inicio
// web/src/pages/HomePage/HomePage.jsimport BlogLayout from 'src/layouts/BlogLayout'import BlogPostsCell from 'src/components/BlogPostsCell/BlogPostsCell.js'const HomePage = () = { return ( BlogLayout pTaking Fullstack to the Jamstack/p BlogPostsCell / /BlogLayout )}export default HomePage
Vercel
Mencionamos a Vercel en el título de esta publicación y finalmente llegamos al punto donde lo necesitamos. Específicamente, lo estamos usando para construir el proyecto e implementarlo en la plataforma alojada de Vercel, que ofrece vistas previas de la compilación cuando el código se envía al repositorio del proyecto. Entonces, si aún no tiene una, obtenga una cuenta Vercel. Nuevamente, el nivel de precios gratuito funciona bien para este trabajo.
¿Por qué Vercel en lugar de, digamos, Netlify? Es una buena pregunta. Redwood incluso comenzó con Netlify como su objetivo de implementación original. Redwood todavía tiene muchas integraciones de Netlify bien documentadas. A pesar de la estrecha integración con Netlify, Redwood busca ser universalmente portátil para la mayor cantidad posible de objetivos de implementación. Esto ahora incluye soporte oficial para Vercel junto con integraciones comunitarias para el marco Serverless, AWS Fargate y PM2. Entonces, sí, podríamos usar Netlify aquí, pero es bueno que tengamos una variedad de servicios disponibles.
Sólo tenemos que hacer un cambio en la configuración del proyecto para integrarlo con Vercel. Abramos netlify.toml
y cambiemos el apiProxyPath
a "/api"
. Luego, iniciemos sesión en Vercel y hagamos clic en el botón "Importar proyecto" para conectar su servicio al repositorio del proyecto. Aquí es donde ingresamos la URL del repositorio para que Vercel pueda verlo, luego activamos una compilación y la implementamos cuando detecta cambios.
Redwood tiene un comando de compilación preestablecido que funciona de inmediato en Vercel:
Estamos bastante avanzados, pero aunque el sitio ahora está "activo", la base de datos no está conectada:
Para solucionarlo, agregaremos el FAUNADB_SECRET
token de nuestra cuenta de Fauna a nuestras variables de entorno en Vercel:
¡Ahora nuestra aplicación está completa!
Demostración en vivoRepositorio de GitHub
¡Lo hicimos! Espero que esto no solo te entusiasme mucho por trabajar con Jamstack y sin servidor, sino que también te dé una idea de algunas tecnologías nuevas en el proceso.
Deja un comentario