Rusty Full Stack
El blog para los amantes de Rust, Ionic y Vuejs
El blog para los amantes de Rust, Ionic y Vuejs
Hola! En este artículo continuaremos aprendiendo sobre desarrollo web con Rust y actix-web, especificamente sobre el estado o "State" y aplicaremos lo aprendido sobre manejo de archivos estáticos para poder utilizar templates de HTML.
Antes de aprender a utilizar templates HTML co actix-web, vamos a hablar un poco sobre el "State" de la aplicación y por qué puede ser importante. El State o es mas que recursos que pueden ser compartidos por las rutas de la aplicación, ya sea todas ellas o solo algunas en específico 🤓.
A lo mejor la definición anterior pueda sonar un poco confusa, pero consideremos el siguiente ejemplo: imagina que tu aplicación web quiera utilizar ua base de datos, lo más probable es que la necesites en varias partes de tu aplicación y abrir una conexión cada vez que un usuario visite unan página no parece ser lo más eficiente, en cambio, si la conexión es abierta desde un principio, y esta es compartida por todas. las rutas, entonces, se podrán mejorar tiempos de respuesta y gastar menos recursos de procesamiento. Para facilitar esta tarea de compartir el recurso, podemos utilizar los estados.
Ahora, ¿qué tiene que ver el State y los templates HTML? la respuesta a esa pregunta será más clara al momento de ver el código, pero, en resumen, podemos tratar los templates HTML como un conjunto de archivos y compartir en toda la aplicación los métodos o funciones que leen esos archivos y responden con el contenido HTML al navegador.
Ahora que sabemos, al menos de forma conceptual, qué es el State, es momento de hablar sobre los templates HTML.
Si revisas el código que colocamos en nuestro primero artículo de introducción sobre actix-web, colocamos una respuesta como la siguiente:
HttpResponse::Ok().body(
r#"
<!doctype html>
<html lang="es">
<body>
<form method="post" action="/formulario">
<button type="submit">Adios</button>
</form>
</body>
</html>
"#,
)
En la introducción a esta serie, vimos que retornar un String de forma totalmente cruda, no es fácil de mantener en el tiempo, el código del ejemplo anterior es para colocar un simple formulario con un botón, pero nuestras aplicaciones web, serán mucho más complicadas que solamente un botón y debemos tener una forma más sencilla de manejar estos escenarios.
Los templates HTML facilitan la tarea de retornar respuestas en formato HTML si necesidad de construir en el código de nuestros métodos los String HTML, mas bien, podremos crear archivos HTML y pasarle datos a través de u contexto para poder mostrar datos de forma dinámica. Si vienes del mundo de Django o Laravel (y muchos otros) esto te sonará muy familiar.
Para manejar templates HTML, podremos hacer uso de un crate llamado tera, el cual está inspirado en jinja2. Tera es un motor para procesar templates HTML el cual se integra bastante bien con actix-web o cualquier otro framework web creado con Rust.
Para poder comprender tanto el state como el manejo de templates HTML con tera, vamos a hacer un ejemplo en el cual iremos construyendo paso a paso un pequeño blog que se verá similar a la siguiente imagen:
Ok, sé que no es el blog más hermoso del mundo 🥲, pero nos servirá para seguir aprendiendo actix-web.
Ahora vamos a crear nuestro ejemplo del blog para poder comprender un poco mejor el manejo de los templates HTML con tera y el State de actix-web. Lo primero que vamos a hacer es crear nuestro proyecto. Desde una terminal ejecuta los siguientes comandos:
cargo new mi_blog
cd mi_blog
Recuerda que puedes ver el código completo del ejemplo en el repositorio en github.
Ahora vamos a agregar las dependencias que utilizaremos, estas las podremos agregar utilizando cargo add o directamente en nuestro archivo Cargo.toml, para este ejemplo, vamos a partir desde el código del artículo anterior en el cual aprendimos a manejar archivos estáticos con Rusty y actix-web.
Por el momento agreguemos las dependencias, en este caso directamente desde nuestro archivo Cargo.toml
// Cargo.toml
[package]
name = "mi_blog"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-files = "0.6.5"
actix-web = "4.5.1"
serde = {version="1.0.196", features=["derive"]}
tera = "1.19.1"
Ahora vamos a crear la estructura de carpetas que vamos a necesitar y colocaremos algunos archivos estáticos de una vez dentro de las carpetas.
Desde la raiz de nuestro proyecto ejecutemos los siguientes comados:
mkdir static
mkdir templates
El primer folder, "static" nos servirá para colocar nuestros archivos estáticos como los css, js, imágenes, etc.
El segundo folder "templates" servirá para colocar nuestros templates HTML.
Ahora dentro de "static", vamos a crear 3 carpetas más: "css", "js" y finalmente "img"
Tu estructura de carpetas debería verse de la siguiente manera:
Finalmente coloca los siguientes archivos estáticos, dentro de static/css, descarga este archivo:
Dentro de static/js descarga este archivo:
tus archivos deberían de verse de esta forma:
Ahora con nuestra estructura de carpetas y archivos estáticos, estamos listos para iniciar con nuestro código. Lo primero que vamos a hacer es crear el método para leer los archivos estáticos. Para ello vamos a reutilizar el código que hemos visto en el artículo anterior, por lo que no habrá mucha explicación al respecto, sólo que está vez, vamos a crearlo en un archivo o módulo a parte para mejor organización de nuestro código y que sea más fácil de leer la parte de los templates de HTML, recuerda que tenemos un artículo sobre organización de código en diferentes archivos por si te puede servir 😉
Dentro de la carpeta src, vamos a crear un nuevo archivo al que llamaremos archivos_estaticos.rs y dentro de ese archivo (src/archivos_estaticos.rs) colocamos el siguiente código el cual es el mismo del artículo pasadoL
// src/archivos_estaticos.rs
use actix_files::NamedFile;
use actix_web::{get, Error, HttpRequest, Result};
use std::path::PathBuf;
#[get("/static/{filename:.*}")]
async fn leer_archivo_estatico(req: HttpRequest) -> Result<NamedFile, Error> {
let ruta: PathBuf = req.match_info().query("filename").parse().unwrap();
let mut ruta_string = ruta.into_os_string().into_string().unwrap();
ruta_string = format!("./static/{}", ruta_string);
let archivo = NamedFile::open(ruta_string)?;
Ok(archivo.use_last_modified(true))
}
Ahora en nuestro archivo src/main.rs vamos a iniciar nuestro servidor web y agregar la ruta para leer los archivos estáticos, puesto que esto ya lo hemos hecho en artículos anteriores tampoco pondré mucha explicación al respecto, tu archivo src/main.rs debería verse así:
// src/main.rs
mod archivos_estaticos;
use actix_web::{App, HttpServer};
use archivos_estaticos::leer_archivo_estatico;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(leer_archivo_estatico))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Si ahora ejecutamos nuestra app web con el comando:
cargo run
y desde un navegador accedemos a:
http://localhost:8080/static/css/bootstrap.min.css
deberíamos poder leer el archivo estático boostrap.min.css tal cual lo hemos hecho anteriormente en el artículo pasado.
Ahora podemos agregar tera a nuestra app para manejar nuestros templates HTML, como mencionamos anteriormente, vamos a necesitar una sola configuración de tera para toda nuestra app, por lo tanto, es importante poder agregar la instancia configurada de tera al state.
Algo a tomar en cuenta, es que Rust es un lenguaje que trata de manejar la memoria de forma segura, por lo que es importante conocer los tamaños de nuestras estructuras. Cuando se utilizan estructuras de las cuales no tenemos certeza de su tamaño, Rust, facilita el uso de "smart pointers" de los cuales hablaremos en otro artículo, pero al mismo tiempo actix-web nos provee algo similar para manejar de forma segura la memoria de estructuras de las cuales no siempre tenemos la certeza de su tamaño (como en este caso para la instancia de tera), para este caso podemos utilizar web::Data de actix-web y colocar ahi nuestra instancia de tera.
Ahora modifica el código en src/main.rs para que se vea de la siguiente forma:
// src/main.rs
mod archivos_estaticos;
use actix_web::{web, App, HttpServer};
use archivos_estaticos::leer_archivo_estatico;
use tera::Tera;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
// Creamos una configuracion de Tera, en la cual se especificamos
// que todos los templates se encuetran en la carpeta "templates"
// en la raiz del proyecto y que ademas dicha carpeta puede contener sub carpetas
let tera: Tera = Tera::new("templates/**/*").unwrap();
App::new()
.service(leer_archivo_estatico)
// Aca agregamos al "State" nuestra configuracio de tera
// para agregar al State, utilizamos el metodo .app_data
// al cual le pasamos de parametro un web::Data para poder
// manejar de forma segura la memoria junto con la configuracion
// de tera
.app_data(web::Data::new(tera))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Como puedes ver, por el momento estamos creando la configuración de tera dentro del closure de HttpServer::new (también se puede definir afuera, pero deberás colocarle un "move" al closure y pasar tera.clone() en lugar de solo tera al .app_data)
let tera: Tera = Tera::new("templates/**/*").unwrap();
Nos permite configurar una instancia de Tera en la cual le indicamos que todos los templates HTML estarán en el folder "templates" dentro de la raíz del proyecto y que, si asi lo queremos, "templates" puede tener también sub carpetas.
Luego con .app_data, estamos agregando la instancia al State para que podamos utilizarla en cualquier momento que necesitemos. Es importante notar que estamos. pasando la instancia como un web::Data el cual previene cualquier error por mal uso de la memoria. Algo importante a tomar e cuenta, es que podemos poner tantos .app_data como necesitemos guardar en el State.
.app_data(web::Data::new(tera))
Hasta este momento solo agregamos tera al State, pero aún no tenemos ningún template HTML, para ello vamos a crear u template que nos permita mostrar un "Hola" y un nombre que podamos pasar mediante un "Context". Empecemos creando el template. Dentro de la carpeta templates crearemos un archivo index.html y vamos a colocarle este código HTML:
<!-- templates/index.html -->
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>Hola {{nombre}}!</h1>
</body>
</html>
Como puedes observar, no deja de ser un archivo html cualquiera con una sola diferencia en esta línea:
<h1>Hola {{nombre}}!</h1>
{{nombre}} es un valor que nuestro template espera para poderlo reemplazar por ese valor. Esos valores se pasan mediante un "Context".
Hasta acá, solo tenemos nuestro template listo, pero ahora crearemos una ruta en la cual invoquemos nuestro template y podamos colocarle el "nombre" que nosotros queramos de forma dinámica, nuestra ruta deberá ser capaz de obtener la instancia de Tera desde el State. Modifiquemos nuestro archivo src/main.rs para poner el siguiente código:
// src/main.rs
mod archivos_estaticos;
use actix_web::{get, web, App, HttpResponse, HttpServer};
use archivos_estaticos::leer_archivo_estatico;
use tera::{Context, Tera};
// Es importante notar que para acceder al State que se guardo
// con .app_data, se coloca como parametro en la definicion del metodo
// el web::Data<T> donde T es el tipo de dato que pasamos en .app_data
// Lo genial es que si no necesitamos utilizar Tera, simplemente podemos
// quitarlo de la definicion, es decir, no es obligatorio poner en las
// definicionnes de las funciones todo lo que se ecuentre en el state
#[get("/")]
async fn index(tera: web::Data<Tera>) -> HttpResponse {
// Ahora creamos el Context para pasarle valores a nuestro template HTML
// es importante notar que le agregamos un "mut" porque vamos a modificar
// sus valores
let mut context = Context::new();
// Ahora agregamos el {{nombre}} que espera nuestro template
// para ello el key debe coincidir con lo especificado en el template
// en este caso el valor que se pasa a "nombre" es "Jaime", pero tu puedes
// cambiarlo por tu propio nombre
context.insert("nombre", "Jaime");
// Ahora hacemos el render de nuestro template, pasando los valores
// del contexto para ser reemplazados al momento del render.
let respuesta_html = tera.render("index.html", &context).unwrap();
HttpResponse::Ok().body(respuesta_html)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
// Creamos una configuracion de Tera, en la cual se especificamos
// que todos los templates se encuetran en la carpeta "templates"
// en la raiz del proyecto y que ademas dicha carpeta puede contener sub carpetas
let tera: Tera = Tera::new("templates/**/*").unwrap();
App::new()
.service(leer_archivo_estatico)
// Aca agregamos al "State" nuestra configuracio de tera
// para agregar al State, utilizamos el metodo .app_data
// al cual le pasamos de parametro un web::Data para poder
// manejar de forma segura la memoria junto con la configuracion
// de tera
.app_data(web::Data::new(tera))
.service(index)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Es importante notar que acceder al state se hace desde la definición de la función, también es importante mencionar que el acceso al state es opcional, es decir, si hemos colocado 5 structs en el state, no es necesario poner todas en la definición, únicamente aquellas estructuras que necesitamos:
async fn index(tera: web::Data<Tera>) -> HttpResponse
Para acceder a Tera, lo definimos en el signature como un web Data <Tera>, de la misma forma que se manejan los tipos genéricos.
Como Tera se configuró antes de ponerlo en el State, ya no es necesario volverlo a configurar puesto que en State ya se colocó con la configuración que necesitamos.
Estas líneas se encargan de hacer el "render" o reemplazar el template con los valores del context, en este caso {{nombre}}, otra cosa a tomar en cuenta es que hay que pasarle la ruta del template que queremos mostrar:
let mut context = Context::new();
context.insert("nombre", "Jaime"); // Coloca aca tu nombre
let respuesta_html = tera.render("index.html", &context).unwrap();
HttpResponse::Ok().body(respuesta_html)
También debes notar que se está a agregando la ruta en App:
.service(index)
Ahora, si volvemos a ejecutar nuestro servidor con el comando:
cargo run
Y accedemos a:
http://localhost:8080
Veremos que ahora nos muestra nuestro template HTML con el "nombre" que le pasamos en el contexto, es decir ya no necesitamos generar por nuestra propia cuenta un String larguísimo y difícil de mantener 😎.
Genial, ya sabemos como utilizar el State y hacer el render de un template HTML!
Ahora hagamos el ejemplo más interesante y modifiquemos nuestro template y código para simular la imagen del blog que está un poco más arriba en este artículo, además de, por fin, utilizar nuestros archivos estáticos en un template html.
Lo primero que vamos a hacer es modificar nuestro archivo templates/index.html para agregarle nuestros archivos estáticos, algo que no había mencionado anteriormente, es que estos archivos estáticos servirán para utilizar el framework de css llamado bootstrap, aunque tu puedes utilizar el que ha ti te guste mayormente.
En nuestro archivo templates/index.html puedes colocar el siguiente código:
<!-- templates/index.html -->
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mi Blog</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
</head>
<body>
<h1>Hola {{nombre}}!</h1>
<script src="/static/js/bootstrap.min.js"></script>
</body>
</html>
del código anterior lo importante en revisar son estas líneas:
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
<script src="/static/js/bootstrap.min.js"></script>
Esas líneas hacen cargar nuestros archivos estáticos, pero es importante el no confundirnos co las referencias, las rutas en ambas líneas NO hacen referencia a nuestra carpeta local "static", sino, que esas rutas en realidad están haciendo referencia a la ruta:
http://localhost:8080/static ...
Es decir, que nuestros archivos estáticos se está leyendo utilizando actix-web. Si ahora volvemos a ejecutar nuestro proyecto con:
cargo run
Y volvemos a ingresar a:
http://localhost:8080
Vamos a ver algunas diferencias en el tipo de letra y márgenes y esto es debido a que ahora nuestro template utiliza bootstrap leído como un archivo estático con actix-web.
Ahora vamos a reemplazar el mensaje de saludo y crearemos un banner para nuestro blog, para ello modifiquemos nuestro archivo templates/index.html y colocamos el siguiente código:
<!-- templates/index.html -->
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mi Blog</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
</head>
<body>
<section class="jumbotron text-center">
<div class="container">
<h1 class="jumbotron-heading">Mi Blog</h1>
<p class="lead text-muted">Bienvenido a mi blog!</p>
</div>
</section>
<script src="/static/js/bootstrap.min.js"></script>
</body>
</html>
Disculpa de antemano si no me detengo a explicar mucho el código HTML pues o es parte del alcance de esta serie el aprender sobre HTML o css, sin embargo, intentaré mostrar con imágenes los resultados y también aquellas líneas que considere más importantes. Si ahora volvemos a ejecutar:
cargo run
E ingresamos a:
http://localhost:8080
Veremos el siguiente banner:
Lo siguiente que vamos a hacer es mostrar los posts de nuestro blog, pero, algo a considerar, es que nuestros posts no serán fijos, es decir, que vamos a obtenerlos desde nuestro backend con actix-web, caso contrario, nos tocaría crear páginas individuales por artículos o posts y esto sería bastante difícil de mantener.
Ya que aún o hemos visto como conectar actix-web con un gestor de bases de datos, vamos a manejar los posts "in-memory", para ello vamos a colocarlos todos dentro de un vector.
Por motivos de orden, vamos a crear este vector y la estructura que representa a los posts en un nuevo módulo, este módulo será un nuevo archivo bajo "src", el cual llamaremos "posts.rs". Y colocamos el siguiente código (src/posts.rs)
// src/posts.rs
use serde::Serialize;
#[derive(Serialize)]
pub struct Post {
pub titulo: String,
pub description_corta: String,
pub autor: String,
pub avatar: String,
pub contenido: String,
pub publicado: bool,
pub fecha_publicacion: String,
}
Si recuerdas, en nuestro archivo Cargo.toml agregamos serde para poder serializar estructuras (si no conoces sobre serde puedes leer el artículo "Convertir una estructura a Json con Rust"), la serialización será importante para poder pasar nuestras estructuras al "Context".
La estructura "Post" contiene una serie de atributos que servirán para mostrar nuestros posts:
Ahora vamos a crear una función para en la que devolveremos un vector de Posts, modifica el archivo src/posts.rs para poner el siguiente código:
// src/posts.rs
use serde::Serialize;
#[derive(Serialize)]
pub struct Post {
pub titulo: String,
pub description_corta: String,
pub autor: String,
pub avatar: String,
pub imagen_encabezado: String,
pub contenido: String,
pub publicado: bool,
pub fecha_publicacion: String,
}
pub fn obtener_todos_los_posts() -> Vec<Post> {
vec![
Post {
titulo: String::from("Mi Post"),
description_corta: String::from("Este es mi <b>primer post</b>"),
autor: String::from("Rusty Full Stack"),
avatar: String::from("/static/img/avatar.jpg"),
imagen_encabezado: String::from("/static/img/post1.jpg"),
contenido: String::from(
r#"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> Proin sit amet dictum ipsum, eu volutpat nulla. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam porttitor felis non fringilla fringilla. Sed vitae ultrices eros. Suspendisse quis nibh vel sapien volutpat venenatis sit amet ut eros. Aliquam sit amet tellus non tortor dapibus scelerisque. Etiam finibus hendrerit nibh, nec vestibulum nisl porta et. Nullam imperdiet est sit amet scelerisque tempus. Ut nibh ipsum, fringilla vitae nisi ut, gravida blandit erat. Praesent venenatis ante eu venenatis pretium. Morbi mollis arcu sapien, id condimentum eros porta ac.
Aliquam feugiat eros vitae nisi ultrices, dictum tincidunt risus volutpat. Ut eleifend turpis eget fringilla congue. Aenean in tortor lobortis, vehicula eros ac, fringilla arcu. Fusce volutpat justo ut turpis convallis facilisis. Cras et blandit purus. Nunc convallis consequat libero. Donec mattis tortor sit amet vestibulum gravida. Donec nec egestas quam, quis pharetra est. Praesent id ante sed mauris auctor malesuada. Donec lobortis ultricies feugiat. Nullam vestibulum feugiat porta."#,
),
publicado: true,
fecha_publicacion: String::from("2024-02-09"),
},
Post {
titulo: String::from("Creado Templates HTML con actix-web"),
description_corta: String::from("Aprendiendo actix-web con <a href='https://rustyfullstack.com'>rustyfullstack.com!</a>"),
autor: String::from("Rusty Full Stack"),
imagen_encabezado: String::from("/static/img/post2.jpg"),
avatar: String::from("/static/img/avatar.jpg"),
contenido: String::from(
r#"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin sit amet dictum ipsum, eu volutpat nulla.</p> Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam porttitor felis non fringilla fringilla. Sed vitae ultrices eros. Suspendisse quis nibh vel sapien volutpat venenatis sit amet ut eros. Aliquam sit amet tellus non tortor dapibus scelerisque. Etiam finibus hendrerit nibh, nec vestibulum nisl porta et. Nullam imperdiet est sit amet scelerisque tempus. Ut nibh ipsum, fringilla vitae nisi ut, gravida blandit erat. Praesent venenatis ante eu venenatis pretium. Morbi mollis arcu sapien, id condimentum eros porta ac.
Aliquam feugiat eros vitae nisi ultrices, dictum tincidunt risus volutpat. Ut eleifend turpis eget fringilla congue. Aenean in tortor lobortis, vehicula eros ac, fringilla arcu. Fusce volutpat justo ut turpis convallis facilisis. Cras et blandit purus. Nunc convallis consequat libero. Donec mattis tortor sit amet vestibulum gravida. Donec nec egestas quam, quis pharetra est. Praesent id ante sed mauris auctor malesuada. Donec lobortis ultricies feugiat. Nullam vestibulum feugiat porta."#,
),
publicado: true,
fecha_publicacion: String::from("2024-02-09"),
},
]
}
En el código anterior, solo hemos agregado un método de nombre:
"obtener_todos_los_posts", el cual únicamente crea dos posts y los devuelve como vector, no te preocupes si por el momento crees que es una función muy grande, en otro artículo lo haremos más simples utilizando bases de datos 😉
Por el momento, basta con comprender que esa función retorna dos posts como Vector.
Ahora vamos a integrar nuestros posts en nuestro archivo src/main.rs, para ello vamos a modificar nuestra función "index" en la cual vamos a obtener todos los posts y a colocarlos en el Context. Antes de mostrarte el código completo, me gustaría mostrarte como se verá la función para poderla explicar mejor:
#[get("/")]
async fn index(tera: web::Data<Tera>) -> HttpResponse {
// Ahora creamos el Context para pasarle valores a nuestro template HTML
// es importante notar que le agregamos un "mut" porque vamos a modificar
// sus valores
let mut context = Context::new();
// Ahora agregamos los posts que colocaremos en nuestro template
// para ello el key debe coincidir con lo especificado en el template
// en este caso el valor que se pasa a "posts" seran todos los posts serializados con Serde
let posts: Vec<Post> = obtener_todos_los_posts();
context.insert("posts", &posts);
// Ahora hacemos el render de nuestro template, pasando los valores
// del contexto para ser reemplazados al momento del render.
let respuesta_html = tera.render("index.html", &context).unwrap();
HttpResponse::Ok().body(respuesta_html)
}
Como puedes ver, la función es bastante simple, simplemente se obtienen todos los posts que serán serializados con Serde.
let posts: Vec<Post> = obtener_todos_los_posts();
y se colocan los posts en el Context, que luego serán renderizados por nuestro template.
context.insert("posts", &posts);
Ahora nuestro código completo en src/main.rs es el siguiente:
// src/main.rs
mod archivos_estaticos;
mod posts;
use actix_web::{get, web, App, HttpResponse, HttpServer};
use archivos_estaticos::leer_archivo_estatico;
use posts::{obtener_todos_los_posts, Post};
use tera::{Context, Tera};
// Es importante notar que para acceder al State que se guardo
// con .app_data, se coloca como parametro en la definicion del metodo
// el web::Data<T> donde T es el tipo de dato que pasamos en .app_data
// Lo genial es que si no necesitamos utilizar Tera, simplemente podemos
// quitarlo de la definicion, es decir, no es obligatorio poner en las
// definicionnes de las funciones todo lo que se ecuentre en el state
#[get("/")]
async fn index(tera: web::Data<Tera>) -> HttpResponse {
// Ahora creamos el Context para pasarle valores a nuestro template HTML
// es importante notar que le agregamos un "mut" porque vamos a modificar
// sus valores
let mut context = Context::new();
// Ahora agregamos el {{nombre}} que espera nuestro template
// para ello el key debe coincidir con lo especificado en el template
// en este caso el valor que se pasa a "posts" son todos los Posts que seran serializados por
// Serde
let posts: Vec<Post> = obtener_todos_los_posts();
context.insert("posts", &posts);
// Ahora hacemos el render de nuestro template, pasando los valores
// del contexto para ser reemplazados al momento del render.
let respuesta_html = tera.render("index.html", &context).unwrap();
HttpResponse::Ok().body(respuesta_html)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
// Creamos una configuracion de Tera, en la cual se especificamos
// que todos los templates se encuetran en la carpeta "templates"
// en la raiz del proyecto y que ademas dicha carpeta puede contener sub carpetas
let tera: Tera = Tera::new("templates/**/*").unwrap();
App::new()
.service(leer_archivo_estatico)
// Aca agregamos al "State" nuestra configuracio de tera
// para agregar al State, utilizamos el metodo .app_data
// al cual le pasamos de parametro un web::Data para poder
// manejar de forma segura la memoria junto con la configuracion
// de tera
.app_data(web::Data::new(tera))
.service(index)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Antes de ejecutar nuestro programa debemos hacer dos cosas, la primera de ella es que si observas el vector de los posts, puedes ver que hay algunas rutas de imágenes que deberían de estar en static/img, por lo que puedes descargar las siguiente imágenes y colocarla en dicha carpeta (static/img):
Tus carpetas de archivos estáticos se debe ver de esta forma:
Lo siguiente que vamos a hacer es modificar nuestro template para poder mostrar los posts, como te comentaba anteriormente, los posts serán dinámicos, es decir, a medida agregues o quites posts, estos también deberán verse reflejado en tu aplicación web, lo más sencillo es utilizar un bucle en nuestro template que recorra todo nuestro vector y mostrar la información de cada post.
Tera nos permite poder ejecutar bucles mediante la instrucción "for", modifiquemos nuestro archivo templates/index.html para poner el siguiente código:
<!-- templates/index.html -->
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mi Blog</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
</head>
<body>
<section class="jumbotron text-center">
<div class="container">
<h1 class="jumbotron-heading">Mi Blog</h1>
<p class="lead text-muted">Bienvenido a mi blog!</p>
</div>
</section>
<div class="bg-light">
<div class="container">
{% for post in posts %}
<div class="card mb-3">
<img
src="{{ post.imagen_encabezado}}"
class="card-img-top"
alt="..."
style="height: 40vh"
/>
<div class="card-body">
<h5 class="card-title">{{ post.titulo }}</h5>
<div class="container-fluid p-0">
<img
src="{{ post.avatar }}"
alt=""
class="rounded-circle"
style="width: 3em"
/>
<span><i>{{ post.autor }}</i></span>
</div>
<p class="card-text">{{ post.description_corta }}</p>
<p class="card-text">
<small class="text-body-secondary"
>Publicado: {{ post.fecha_publicacion }}</small
>
</p>
</div>
</div>
{% endfor %}
</div>
</div>
<script src="/static/js/bootstrap.min.js"></script>
</body>
</html>
Algo muy interesante que verás en el código anterior es que nuestro template ahora tiene estas líneas:
{% for post in posts %}
...
{% endfor %}
El cual es el equivalente a un "for" y todo lo que esté dentro de ese "for" se mostrará item a item que en este caso hemos llamado "post". Luego dentro de nuestro "for" en el template, podemos acceder a los atributos de cada elemento individualmente como en esta línea:
<p class="card-text">{{ post.description_corta }}</p>
Si ahora ejecutamos nuestro programa con:
cargo run
y accedemos a:
http://localhost:8080
Veremos que ahora nuestros posts se están mostrando en nuestra aplicación web! 🥳
Pero hay algo extraño!! 🤨
Si observas bien la zona donde estamos mostrando la descripción corta, podrás observar que donde queremos que coloque un texto en negrito con el tag HTML "<b>", en lugar de mostrarlo como queremos, está mostrando el tag en crudo.
Lo mismo está pasando donde queremos mostrar un link:
Esto se debe a que Tera nos protege de posibles ataques mediante inyección de código, pero podemos pasarle un "filtro" para hacerle saber que es seguro colocar el contenido HTML, para ello vamos a modificar esta línea para agregar el filtro:
<!-- De esta linea -->
<p class="card-text">{{ post.description_corta }}</p>
<!-- pasara a esto -->
<p class="card-text">{{ post.description_corta | safe }}</p>
el filtro que pasamos con el caracter "|" es el conocido como "safe", el cual le indica a tera que muestre el contenido como un HTML. El código completo de templates/index.html es el siguiente:
<!-- templates/index.html -->
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mi Blog</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
</head>
<body>
<section class="jumbotron text-center">
<div class="container">
<h1 class="jumbotron-heading">Mi Blog</h1>
<p class="lead text-muted">Bienvenido a mi blog!</p>
</div>
</section>
<div class="bg-light">
<div class="container">
{% for post in posts %}
<div class="card mb-3">
<img
src="{{ post.imagen_encabezado}}"
class="card-img-top"
alt="..."
style="height: 40vh"
/>
<div class="card-body">
<h5 class="card-title">{{ post.titulo }}</h5>
<div class="container-fluid p-0">
<img
src="{{ post.avatar }}"
alt=""
class="rounded-circle"
style="width: 3em"
/>
<span><i>{{ post.autor }}</i></span>
</div>
<p class="card-text">{{ post.description_corta | safe }}</p>
<p class="card-text">
<small class="text-body-secondary"
>Publicado: {{ post.fecha_publicacion }}</small
>
</p>
</div>
</div>
{% endfor %}
</div>
</div>
<script src="/static/js/bootstrap.min.js"></script>
</body>
</html>
Sobre filtros con tera, hablaremos con más detalles en otro artículo (incluso puedes crear tus propios filtros! 😃) pero por si quieres profundizar en ello puedes leer su documentación.
Ahora si volvemos a ejecutar nuestra aplicación con:
cargo run
Veremos que ahora si muestra el contenido como HTML.
Otro flujo de control que podemos utilizar con Tera, es el "if" tal cual como en cualquier otro lenguaje de programación, podemos poner condiciones y de esta forma decidir que mostrar o no mostrar en nuestro template HTML, por ejemplo podemos verificar si el campo "publicado" de nuestro "Post" es verdadero, entonces lo mostramos, pero si es falso, entonces no lo mostramos, para ello podemos modificar nuestro archivo templates/index.html con el siguiente código:
<!-- templates/index.html -->
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mi Blog</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
</head>
<body>
<section class="jumbotron text-center">
<div class="container">
<h1 class="jumbotron-heading">Mi Blog</h1>
<p class="lead text-muted">Bienvenido a mi blog!</p>
</div>
</section>
<div class="bg-light">
<div class="container">
{% for post in posts %}
{% if post.publicado %}
<div class="card mb-3">
<img
src="{{ post.imagen_encabezado}}"
class="card-img-top"
alt="..."
style="height: 40vh"
/>
<div class="card-body">
<h5 class="card-title">{{ post.titulo }}</h5>
<div class="container-fluid p-0">
<img
src="{{ post.avatar }}"
alt=""
class="rounded-circle"
style="width: 3em"
/>
<span><i>{{ post.autor }}</i></span>
</div>
<p class="card-text">{{ post.description_corta | safe }}</p>
<p class="card-text">
<small class="text-body-secondary"
>Publicado: {{ post.fecha_publicacion }}</small
>
</p>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<script src="/static/js/bootstrap.min.js"></script>
</body>
</html>
Como puedes observar, dentro del template, ahora hemos colocado el siguiente código:
{% for post in posts %}
{% if post.publicado %}
<div class="card mb-3">
...
{% endif %}
{% endfor %}
El "if" se encargará de mostrar la tarjeta del post únicamente si el post ha sido publicado (post.publicado es verdadero), en caso contrario no lo mostrará.
Ahora modifiquemos nuestro archivo src/posts.rs para colocar el segundo post con el valor de "publicado" en false y verifiquemos que no lo publique:
// src/posts.rs
use serde::Serialize;
#[derive(Serialize)]
pub struct Post {
pub titulo: String,
pub description_corta: String,
pub autor: String,
pub avatar: String,
pub imagen_encabezado: String,
pub contenido: String,
pub publicado: bool,
pub fecha_publicacion: String,
}
pub fn obtener_todos_los_posts() -> Vec<Post> {
vec![
Post {
titulo: String::from("Mi Post"),
description_corta: String::from("Este es mi <b>primer post</b>"),
autor: String::from("Rusty Full Stack"),
avatar: String::from("/static/img/avatar.jpg"),
imagen_encabezado: String::from("/static/img/post1.jpg"),
contenido: String::from(
r#"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> Proin sit amet dictum ipsum, eu volutpat nulla. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam porttitor felis non fringilla fringilla. Sed vitae ultrices eros. Suspendisse quis nibh vel sapien volutpat venenatis sit amet ut eros. Aliquam sit amet tellus non tortor dapibus scelerisque. Etiam finibus hendrerit nibh, nec vestibulum nisl porta et. Nullam imperdiet est sit amet scelerisque tempus. Ut nibh ipsum, fringilla vitae nisi ut, gravida blandit erat. Praesent venenatis ante eu venenatis pretium. Morbi mollis arcu sapien, id condimentum eros porta ac.
Aliquam feugiat eros vitae nisi ultrices, dictum tincidunt risus volutpat. Ut eleifend turpis eget fringilla congue. Aenean in tortor lobortis, vehicula eros ac, fringilla arcu. Fusce volutpat justo ut turpis convallis facilisis. Cras et blandit purus. Nunc convallis consequat libero. Donec mattis tortor sit amet vestibulum gravida. Donec nec egestas quam, quis pharetra est. Praesent id ante sed mauris auctor malesuada. Donec lobortis ultricies feugiat. Nullam vestibulum feugiat porta."#,
),
publicado: true,
fecha_publicacion: String::from("2024-02-09"),
},
Post {
titulo: String::from("Creado Templates HTML con actix-web"),
description_corta: String::from("Aprendiendo actix-web con <a href='https://rustyfullstack.com'>rustyfullstack.com!</a>"),
autor: String::from("Rusty Full Stack"),
imagen_encabezado: String::from("/static/img/post2.jpg"),
avatar: String::from("/static/img/avatar.jpg"),
contenido: String::from(
r#"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin sit amet dictum ipsum, eu volutpat nulla.</p> Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam porttitor felis non fringilla fringilla. Sed vitae ultrices eros. Suspendisse quis nibh vel sapien volutpat venenatis sit amet ut eros. Aliquam sit amet tellus non tortor dapibus scelerisque. Etiam finibus hendrerit nibh, nec vestibulum nisl porta et. Nullam imperdiet est sit amet scelerisque tempus. Ut nibh ipsum, fringilla vitae nisi ut, gravida blandit erat. Praesent venenatis ante eu venenatis pretium. Morbi mollis arcu sapien, id condimentum eros porta ac.
Aliquam feugiat eros vitae nisi ultrices, dictum tincidunt risus volutpat. Ut eleifend turpis eget fringilla congue. Aenean in tortor lobortis, vehicula eros ac, fringilla arcu. Fusce volutpat justo ut turpis convallis facilisis. Cras et blandit purus. Nunc convallis consequat libero. Donec mattis tortor sit amet vestibulum gravida. Donec nec egestas quam, quis pharetra est. Praesent id ante sed mauris auctor malesuada. Donec lobortis ultricies feugiat. Nullam vestibulum feugiat porta."#,
),
publicado: false,
fecha_publicacion: String::from("2024-02-09"),
},
]
}
Si ahora ejecutamos nuevamente nuestra aplicación web con:
cargo run
y entramos a:
http://localhost:8080
Veremos que únicamente un post se muestra y es el que tiene el atributo "publicado" en verdadero.
Hasta este momento hemos aprendido sobre el State, mostrar templates HTML, hacer bucles y condiciones en nuestro template y lo hemos integrado con nuestros archivos estáticos. Sin embargo, es probable que ahora tengas muchas dudas todavía, como por ejemplo, cómo poder crear un template para mostrar el detalle del blog, la conexión con bases de datos, e incluso si no hay alguna forma de mejorar la organización de nuestros templates sin necesidad de estar creando toda la página o el poder reutilizar templates HTML. Todas esas preguntas las intentaremos responder durante esta serie, pero por el momento dejaremos este artículo hasta acá. puesto que ya hemos aprendido muchas cosas y hemos avanzado aún más en nuestro camino de desarrollo web co Rust y actix-web 😊.
Recuerda que puedes ver el código completo del ejemplo en el repositorio en github.
Si esta publicación te ha sido de utilidad, compártela con tus amigos y en las redes sociales. No te olvides de seguirme en X (twitter) ni de suscribirte al canal de youtube.
Si esta información te ha sido de utilidad y te gustaría apoyar a la creación de más artículos y contenidos puedes convertirte en mecenas desde patreon te estaré sumamente agradecido 🦀
println!("Hasta la próxima!");