Adrianistán

Yew, crea webapps al estilo Angular/React en Rust

01/01/2018
Hoy os traigo una librería muy potente y muy útil, ahora que Rust compila a WebAssembly de forma nativa. Se trata de Yew, una librería para diseñar frontends, single-page-applications y en general, una librería que es una alternativa a Angular, React, Vue y Elm.

En particular, Yew se basa en The Elm Architecture, por lo que los usuarios de Elm serán los que encuentren más familiar a Yew.

Yew significa tejo. He aquí uno de los tejos más famosos de Valladolid, en la plaza del Viejo Coso

Instalar cargo-web y el soporte a WebAssembly


Antes de empezar a programar necesitaremos instalar un plugin para Cargo, llamado cargo-web, que nos ayudará en el desarrollo web con Rust. Por otro lado, hace falta instalar el soporte de Rust a WebAssembly. Existen tres opciones actualmente: asmjs-unknown-emscripten, wasm32-unknown-emscripten y wasm32-unknown-unknown. Para los primeras opciones hace falta tener instalado Emscripten. Para la última, no hace falta nada, por lo que es mi opción recomendada. Por otro lado, wasm32 significa WebAssembly y asmjs es asm.js, que es simplemente JavaScript y será compatible con más navegadores.
cargo install cargo-web
rustup target add wasm32-unknown-unknown

The Elm Architecture


Los principios de The Elm Architecture se basan en: modelo, mensajes, actualización y vista.

Para este tutorial vamos a hacer la típica aplicación de una lista donde guardamos notas. Nuestro modelo se va a componer de una lista de tareas, para simplificar, pongamos que una tarea es simplemente un String y un ID. Entonces también nos hará falta almacenar un contador para ir poniendo IDs. También, al tener un campo de texto, nos hará falta una variable para ir almacenando temporalmente lo que escribe el usuario.
struct Model {
id: u32,
tasks: Vec<Task>,
input: String,
}

struct Task{
content: String,
id: u32,
}

Lo siguiente es diseñar los mensajes. Los mensajes interactúan con el modelo y desencadenan una actualización de la vista. En esta aplicación solo nos hacen falta dos mensajes: añadir mensaje y borrar mensaje. Pero como tenemos un campo de texto, tenemos que introducir un mensaje Change y siempre viene bien un mensaje que no haga nada.
enum Msg {
Add,
Remove(u32),
Change(String),
None,
}

Una vez hecho esto pasamos a crear la función update, en la que hacemos distintas cosas según el mensaje que recibamos.
fn update(context: &mut Context<Msg>, model: &mut Model, msg: Msg) {
match msg {
Msg::Add => {
let task = Task{
content: model.input.clone(),
id: model.id,
};
model.tasks.push(task);
model.id += 1;
}
Msg::Change(content) => {
model.input = content;
}
Msg::Remove(id) => {
let mut i = 0;
for task in model.tasks.iter(){
if task.id == id{
break;
}
i+=1;
}
model.tasks.remove(i);
}
_ => {

}
}
}

Así pues, si se lanza un mensaje Msg::Add lo que hacemos es copiar el valor de la variable temporal input, crear una nueva tarea con su ID y añadirla a la lista de tareas. Ya está. Yew mágicamente actualizará la página para reflejar que la lista de tareas ha sido modificada. Lo mismo pasa con Remove.

Ahora vamos a las vistas. Una vista es una función que devuelve Html<Msg> y se pueden componer varias funciones así. En nuestro caso, tenemos una vista principal donde se ve un campo de texto y un sitio donde se ejecuta un bucle for con las tareas del modelo. Y a cada tarea se le aplica la vista view_task.
fn view(model: &Model) -> Html<Msg> {
html! {
<div>
<ul>
{ for model.tasks.iter().map(view_task) }
</ul>
<input type="text", value=&model.input, oninput=|e: InputData| Msg::Change(e.value), onkeypress=|e: KeyData|{
if e.key == "Enter" {
Msg::Add
}else{
Msg::None
}
}, />
</div>
}
}

fn view_task(task: &Task) -> Html<Msg>{
let id = task.id;
html!{
<li><span>{&task.content}</span><button onclick=move |_| Msg::Remove(id),>{format!("X")}</button></li>
}
}

La macro html! nos permite escribir HTML directamente en Rust, con algunas diferencias (¡prestad atención a las comas!). También nos permite introducir código Rust (entre llaves) y closures (observad onclick, oninput y onkeypress).

Finalmente en el método main, inicializamos el modelo y llamamos a program, que empieza a ejecutar Yew.

El código final queda así.
#[macro_use]
extern crate yew;

use yew::html::*;

struct Model {
id: u32,
tasks: Vec<Task>,
input: String,
}

struct Task{
content: String,
id: u32,
}

enum Msg {
Add,
Remove(u32),
Change(String),
None,
}

fn update(context: &mut Context<Msg>, model: &mut Model, msg: Msg) {
match msg {
Msg::Add => {
let task = Task{
content: model.input.clone(),
id: model.id,
};
model.tasks.push(task);
model.id += 1;
}
Msg::Change(content) => {
model.input = content;
}
Msg::Remove(id) => {
let mut i = 0;
for task in model.tasks.iter(){
if task.id == id{
break;
}
i+=1;
}
model.tasks.remove(i);
}
_ => {

}
}
}

fn view(model: &Model) -> Html<Msg> {
html! {
<div>
<ul>
{ for model.tasks.iter().map(view_task) }
</ul>
<input type="text", value=&model.input, oninput=|e: InputData| Msg::Change(e.value), onkeypress=|e: KeyData|{
if e.key == "Enter" {
Msg::Add
}else{
Msg::None
}
}, />
</div>
}
}

fn view_task(task: &Task) -> Html<Msg>{
let id = task.id;
html!{
<li><span>{&task.content}</span><button onclick=move |_| Msg::Remove(id),>{format!("X")}</button></li>
}
}

fn main() {
let model = Model {
id: 0,
tasks: vec![],
input: String::from("")
};
program(model, update, view);
}

Ejecutando la aplicación web


Usando cargo web, es muy sencillo generar la aplicación web. Simplemente ejecuta:
cargo web start --target-webasm

El resultado, se montará en un mini servidor web. Si accedes a la URL que indica Cargo con tu navegador web, verás algo similar a esto:

Añade items y bórralos. Observa como la aplicación funciona perfectamente.

Distribuyendo la aplicación


Realmente cargo web ha hecho muchas cosas por nosotros. Si nosotros queremos usar Yew en la vida real, no usaremos cargo web. Para ello, compilamos la aplicación web:
cargo web build --target-webasm

Y accedemos a la carpeta target/wasm32-unknown-unknown/release. Allí encontraremos dos archivos que vamos a necesitar. Uno acabado en .js y otro acabado en .wasm. Ambos ficheros deberemos copiarlos donde queramos usarlos. Por último, será necesario un fichero HTML. En el fichero HTML solo hace falta cargar el fichero JS. Yew hará el resto.

Si quieres saber más, puedes descargarte el código de ejemplo que se encuentra en GitHub.
Tags: programacion yew webassembly linux rust tutorial elm web-framework emscripten web