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 cda 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:8911y 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:8910al 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: weby api. Los espacios de trabajo de Yarn permiten que cada lado tenga su propia ruta en el código base.

webcontiene nuestro código de interfaz para:

  • paginas
  • Diseños
  • Componentes

apicontiene 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 prismadirectorio 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 weblateral, 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.jscontiene 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 idcontenedor redwood-appy 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 RedwoodProvideretiquetas. 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 FatalErrorBoundarycomponente que se utiliza FatalErrorPagecomo 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

RouterContiene 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 notfoundruta que genera una ruta única Routecon un notfoundaccesorio 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 pagecomando CLI de Redwood para crear una función de ruta con nombre de llamada home. Esto representa el HomePagecomponente cuando coincide con la ruta URL a /.

También podemos usar rwen lugar de redwoody gen lugar de generatepara 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.jscon 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 Routeque web/src/Routes.jsasigna la /ruta al HomePagecomponente.

Página principal

Si vamos a web/src/pagesveremos un HomePagedirectorio que contiene un HomePage.jsarchivo. 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 Linky routestambié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 abouten 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 Linke routesimportar 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.jsveremos nuestras nuevas rutas para homey 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 layoutsigue el nombre del diseño, en lugar de rw g pageseguir 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 Linky routesdesde @redwoodjs/routerhaciaBlogLayout.js
  • Crear un Link to={}/Linkcomponente para cada enlace
  • Pase una función de ruta con nombre, como routes.home(), al to={}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 BlogLayoutpero no lo hemos importado a ninguna página. Entonces BlogLayout, importemos HomePagey ajustemos la declaración completa returncon las BlogLayoutetiquetas.

// 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 BlogLayoutla 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.gqle 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 Postcolección y las publicaciones Indexaparecerán sin cambios si ejecutamos las consultas predeterminadas en el shell, pero Fauna crea un PostPagetipo intermediario que tiene un dataobjeto.

Lenguaje de definición de esquema de Redwood

Este dataobjeto contiene una matriz con todos los Postobjetos de la base de datos. Usaremos estos tipos para crear otro lenguaje de definición de esquema que se encuentre dentro de nuestro graphqldirectorio al apilado 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 postsservicio envía una consulta a la API Fauna GraphQL. Esta consulta solicita una serie de publicaciones, específicamente el titley bodypara cada una. Estos están contenidos en el dataobjeto 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-requestconsultarlos. Ahora necesitamos establecer una conexión entre graphql-requestFauna, lo cual haremos importándola graphql-requesty db.jsusándola para consultar un endpointque 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 GraphQLClientpara 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 titley 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.stringifyen la página donde se importa la celda. Haremos algunos cambios para realizar la consulta y representar los datos que necesitamos. Entonces vamos:

  • Cambiar blogPostsa posts.
  • Cambiar BlogPostsQuerya POSTS.
  • Cambie la consulta en sí para devolver el titley bodyde cada publicación.
  • Mapa sobre el dataobjeto en el componente de éxito.
  • Cree un componente con el titley bodydel postsdevuelto a través del dataobjeto.

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 POSTSconsulta envía una consulta para postsy, cuando se consulta, obtenemos un dataobjeto que contiene una serie de publicaciones. Necesitamos sacar el dataobjeto para poder recorrerlo y obtener las publicaciones reales. Hacemos esto con la desestructuración de objetos para obtener el dataobjeto y luego usamos la map()función para mapear el dataobjeto y extraer cada publicación. El titlede cada publicación se representa con un h2interior headery el cuerpo se representa con una petiqueta.

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.tomly cambiemos el apiProxyPatha "/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_SECRETtoken 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

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir