Embora o Rust seja conhecido por seus recursos de desenvolvimento de back-end da web, o advento do WebAssembly (Wasm) tornou possível construir aplicativos de front-end ricos em Rust.
Para aqueles que desejam explorar o front-end do desenvolvimento do Rust, aprenderemos como construir um aplicativo da web de front-end muito básico usando a estrutura da web Yew.
Se você está familiarizado com o React ou outras estruturas de front-end JavaScript, se sentirá em casa com o Yew; ele usa uma sintaxe e estrutura de aplicativo semelhantes ao JSX.
Para demonstrar a interoperabilidade de Rust e Yew, nosso aplicativo de front-end conterá uma lista de tarefas simples (original, eu sei!) que usa JSONPlaceholder como back-end para buscar dados. A lista fornecerá uma visualização de lista, uma visualização detalhada para cada opção de tarefa e uma opção para atualizar os dados.
É importante observar, no entanto, que o ecossistema Wasm e o Teixo ainda estão no início de seu desenvolvimento, portanto, embora este tutorial seja preciso hoje, algumas funcionalidades do Wasm e do Teixo estão sujeitas a alterações no futuro. Isso pode afetar levemente a configuração e o ecossistema da biblioteca, mas ainda podemos construir aplicativos Rust reais usando esta pilha.
Agora, vamos começar!
Configurando o aplicativo da web
Certifique-se de Rust 1.50 ou superior e Trunk estão instalados. Trunk é uma ferramenta de construção e pipeline para aplicativos Wasm baseados em Rust que fornece um servidor de desenvolvimento local, observação automática de arquivos e simplifica o envio de código Rust para Wasm.
Para entender como funciona o estrutura Yew para desenvolver aplicativos, consulte os Yew docs .
Criando um projeto Rust
Vamos começar criando um novo projeto Rust com o seguinte:
cargo novo--lib rust-frontend-exemplo-teixo cd rust-frontend-example-yew
Adicione as dependências necessárias para editar o arquivo Cargo.toml com o código abaixo:
[dependências] teixo="0,18" wasm-bindgen="0,2,67" serde="1" serde_derive="1" serde_json="1" de qualquer maneira="1" yew-router="0.15.0"
Adicionando Yew e o Yew-Router , podemos começar a trabalhar na estrutura do Yew. Também adicionamos anyhow para tratamento básico de erros, serde para trabalhar com JSON e [wasm-bindgen] para usar o JavaScript do Rust.
Com a configuração resolvida, vamos começar a construir.
Configuração de HTML com tronco
Como estamos construindo um aplicativo da web de front-end, precisamos de uma base HTML. Usando Trunk , podemos criar um index.html mínimo na raiz do nosso projeto usando o seguinte:
Exemplo de front-end do Rust com Yew
Com um esqueleto HTML mínimo e alguns CSS muito básicos, Trunk cria dist/index.html com um body injetado, mantendo o ponto de entrada para nosso aplicativo Wasm.
Abrindo o arquivo src/lib.rs , agora podemos criar o básico para nosso Yew aplicativo da web.
Configurando TodoApp com roteamento básico
Implementando o roteamento básico, podemos trabalhar desde as definições de rota de alto nível até as implementações de rota reais.
Primeiro, vamos criar um tipo para TodoApp :
struct TodoApp { link: ComponentLink , todos: Opção >, fetch_task: Opção ,
} # [derivar (desserializar, clonar, PartialEq, depurar)]
# [serde (rename_all="camelCase")]
pub struct Todo { pub user_id: u64, id do pub: u64, título do pub: String, pub concluído: bool,
}
Esta estrutura inclui o link que registra retornos de chamada dentro deste componente. Também definiremos uma lista de tarefas opcional com Option e um fetch_task para buscar dados.
Para criar um componente raiz como um ponto de entrada, devemos implementar o traço Component :
enum Msg { MakeReq, Resp (Resultado , de qualquer forma:: Erro>),
} Componente impl para TodoApp { tipo Message=Msg; tipo Propriedades=(); fn create (_: Self:: Properties, link: ComponentLink )-> Self { Auto { link, todos: nenhum, fetch_task: Nenhum, } } fn update (& mut self, msg: Self:: Message)-> ShouldRender { verdadeiro } fn change (& mut self, _props: Self:: Properties)-> ShouldRender { falso } fn view (& self)-> Html { html! { ... } }
}
Ao definir a estrutura Msg , o tipo da Message do componente, podemos orquestrar a passagem da mensagem dentro do componente. Em nosso caso, definiremos a mensagem MakeReq e a mensagem Resp para fazer uma solicitação HTTP e receber a resposta.
Mais tarde, usaremos esses estados para construir uma máquina de estados que diga ao nosso aplicativo como reagir quando disparamos uma solicitação e a resposta chega.
O traço Component define seis funções do ciclo de vida:
-
createé um construtor que pega adereços e oComponentLink -
viewrenderiza o componente -
updateé chamado quando umaMessageé enviada ao componente, implementando a lógica de passagem de mensagem -
changerenderiza novamente as alterações, otimizando a velocidade de renderização -
renderizadoé chamado uma vez apósview, mas antes das atualizações do navegador, diferenciando entre o primeiro e consecutivo renderizações -
destroyé chamado quando um componente é desmontado e operações de limpeza são necessárias
Visto que nossos componentes raiz não têm nenhum suporte, podemos deixar change retornar falso.
Não implementaremos nada em update ainda, então definiremos que o componente deve ser renderizado novamente sempre que uma Message entrar.
Na view , usaremos a macro html! para construir um div externo básico e as classes! macro para criar classes HTML para ele, que implementaremos mais tarde.
Para renderizar este componente, precisamos do seguinte snippet de código:
# [wasm_bindgen (iniciar)]
pub fn run_app () { App:: :: new (). Mount_to_body ();
}
Este snippet usa wasm-bindgen e define esta função como nosso ponto de entrada, montando o componente TodoApp como a raiz dentro do corpo.
Buscando dados
Ótimo, agora que o básico está definido, vamos ver como podemos buscar alguns dados.
Começaremos alterando o método de ciclo de vida create para enviar uma mensagem MakeReq quando o componente for criado para buscar dados imediatamente:
fn create (_: Self:: Properties, link: ComponentLink)-> Self { link.send_message (Msg:: MakeReq); Auto { link, todos: nenhum, fetch_task: Nenhum, } }
Em seguida, implementamos update :
fn update (& mut self, msg: Self:: Message)-> ShouldRender { corresponder msg { Msg:: MakeReq=> { self.todos=Nenhum; let req=Request:: get ("https://jsonplaceholder.typicode.com/todos") .body (nada) .expect ("pode fazer req para jsonplaceholder"); let cb=self.link.callback ( | resposta: Resposta , de qualquer forma:: Erro >>> | { deixe Json (dados)=resposta.into_body (); Msg:: Resp (dados) }, ); let task=FetchService:: fetch (req, cb).expect ("pode criar tarefa"); self.fetch_task=Alguns (tarefa); () } Msg:: Resp (resp)=> { se deixar Ok (dados)=resp { self.todos=Alguns (dados); } } } verdadeiro }
É um pouco de código, então vamos percorrê-lo juntos para entendê-lo.
O Yew fornece serviços que são abstrações pré-construídas para coisas como registrar ou usar HTTP fetch () (o JavaScript fetch () ).
Em nosso código, podemos definir self.todos como None , e os dados sempre são redefinidos quando estamos fazendo a busca. Adicionando o FetchService , criamos uma solicitação HTTP GET para JSONPlaceholder .
Definir um retorno de chamada analisa a resposta para JSONPlaceholder e envia uma mensagem Msg:: Resp com os dados retornados.
Conforme ativamos a chamada fetch () preparada com a solicitação e o retorno de chamada, também definimos o fetch_task do componente como o FetchService:: fetch fetch-task ativo.
O tratamento da resposta é simples: se uma Msg:: Resp entrar, podemos verificar se há dados ou não. Se houver dados, podemos definir self.todos para esses dados.
É aqui também que podemos tratar de alguns erros e definir uma mensagem de erro a ser exibida se uma solicitação falhar ou se os dados forem inválidos.
Finalmente, devemos exibir nossos dados recém-obtidos usando o método view :
visualização fn (& self)-> Html { let todos=self.todos.clone (); let cb=self.link.callback (| _ | Msg:: MakeReq); ConsoleService:: info (& format! ("Render TodoApp: {:?}", Todos)); html! { {"atualizar"} } }
Obtendo as tarefas e usando o ConsoleService , podemos registrá-las toda vez que renderizarmos esse componente, o que é útil para depuração.
A criação de um botão atualizar simples com um manipulador onclick nos permite chamar nosso pipeline de busca de dados, permitindo-nos chamar ações de dentro do html! marcação.
Passando as tarefas para o componente todo:: list:: List , podemos exibir as tarefas no aplicativo da web.
Adicionando o componente Lista
Para começar a construir nosso componente List , devemos criar uma pasta todo com um mod.rs contendo lista de mod de pub e um arquivo list.rs .
Em list.rs , devemos implementar nosso componente List da mesma maneira que implementamos TodoApp :
# [derivar (Propriedades, Clone, PartialEq)]
pub struct Props { pub todos: Opção >,
} pub struct List { adereços: adereços,
} pub enum Msg {} Componente impl para Lista { tipo Propriedades=adereços; tipo Message=Msg; fn create (props: Self:: Properties, _link: ComponentLink )-> Self { Auto {adereços} } fn view (& self)-> Html { html! { {self.render_list (& self.props.todos)} } } fn update (& mut self, _msg: Self:: Message)-> ShouldRender { verdadeiro } fn change (& mut self, props: Self:: Properties)-> ShouldRender { self.props=adereços; verdadeiro }
}
Ao definir a estrutura List para as propriedades do componente list , podemos incluir a lista de tarefas. Em seguida, implementamos o traço Component .
Sempre que ocorre uma mudança nos adereços, devemos definir os adereços e refazer a renderização. Como não temos nenhuma passagem de mensagem, podemos ignorar a estrutura Msg e a função update .
Em view , vamos criar um div para chamar self.render_list .
Podemos implementar essa renderização na própria List :
Lista
impl { fn render_list (& self, todos: & Option >)-> Html { se deixar Some (t)=todos { html! { {t.iter (). map (| todo | self.view_todo (todo)). coletar:: ()} } } senão { html! { {"carregando..."} } } } fn view_todo (& self, todo: & Todo)-> Html { deixe concluído=se todo.completed { Alguns ("concluído") } senão { Nenhum }; html! { {& todo.title} } }
}
Se não tivermos nenhuma tarefa em render_list , podemos mostrar carregando… renderizado no navegador para indicar que os dados estão sendo buscados.
Se os dados já estiverem lá, podemos usar a sintaxe de expressão de Yew dentro de html! para iterar a lista de tarefas. Chame view_todo para cada um deles e colete-o em Html para ser renderizado dentro de html! .
Também adicionamos um estilo condicional ao nosso aplicativo, definindo as tarefas como concluídas em view_todo quando são marcadas como concluídas no navegador; se não estiverem marcados como completos, nenhum estilo CSS será aplicado.
Para criar títulos para cada tarefa, simplesmente criamos um div na marcação de cada tarefa para conter o título correspondente.
A próxima etapa é transformar este título em um link para que possamos alternar da visualização de lista para a visualização detalhada. Mas, para isso, devemos primeiro configurar a navegação, também conhecida como roteamento, em nosso aplicativo.
Roteamento de aplicativo básico com Yew
Para criar um roteamento básico para nosso aplicativo, usaremos o Yew-router .
Na enum Switch e pub , podemos definir nossas rotas em AppRoute :
# [derivar (alternar, clonar, depurar)]
pub enum AppRoute { # [to="/todo/{id}"] Detalhe (i32), # [to="/"] Casa,
}
Definir a rota Detalhe neste enum leva uma ID de tarefa em /todo/$ id e a rota Home , que é nossa visualização de lista.
Agora, devemos adaptar nosso método view para incluir o seguinte mecanismo de roteamento:
visualização fn (& self)-> Html { let todos=self.todos.clone (); let cb=self.link.callback (| _ | Msg:: MakeReq); ConsoleService:: info (& format! ("Render TodoApp: {:?}", Todos)); html! { {"Home"} render=Router:: render (move | switch: AppRoute | { combinar interruptor { AppRoute:: Detail (todo_id)=> { html! { } } AppRoute:: Home=> { html! { {"atualizar"} } } } }) /> } }
Acima de nossa lista, podemos criar um div de navegação que inclui um link de volta para Home para que possamos navegar de volta a qualquer momento.
Abaixo disso, podemos definir um conteúdo div que inclui um Router. Neste roteador, podemos definir uma função render que informa ao roteador o que renderizar com base na rota atual.
Dentro do método render , podemos ativar o AppRoute fornecido para mostrar a lista de tarefas em Home e Detalhe e um botão atualizar em Home .
Finalmente, devemos adaptar a função view_todo em list.rs para incluir um link para as páginas detalhadas de tarefas:
fn view_todo (& self, todo: & Todo)-> Html { deixe concluído=se todo.completed { Alguns ("concluído") } senão { Nenhum }; html! { {& todo.title} } }
Para esse propósito, usaremos o componente Yew-router do Anchor . Este mecanismo conveniente nos permite rotear dentro do aplicativo usando nosso enum AppRoute , eliminando a possibilidade de erros de tipo. Isso significa que temos verificações de tipo no nível do compilador para nossas rotas. Muito legal!
Para terminar nosso aplicativo, vamos implementar a visualização detalhada para uma única tarefa.
Implementando a visão detalhada
Para começar a implementar a visualização detalhada de tarefas para nosso aplicativo, abra a pasta todo , adicione pub mod detail; a mod.rs e adicione um arquivo detail.rs .
E agora podemos implementar outro componente. No entanto, para torná-lo mais interessante, iremos (desnecessariamente neste caso), implementar alguma busca de dados. Como apenas passamos o ID da tarefa para a visualização detalhada, teremos que buscar novamente os dados da tarefa na visualização detalhada.
Embora o uso desse recurso em nosso exemplo não agregue muito valor, as lojas da web com várias fontes de dados, grandes listas de produtos e páginas de detalhes ricos podem se beneficiar da eficiência da busca de dados.
Novamente, começando com o básico, adicionaremos o seguinte:
# [derivar (Propriedades, Clone, PartialEq)]
pub struct Props { pub todo_id: i32,
} pub struct Detail { adereços: adereços, link: ComponentLink , todo: Opção , fetch_task: Opção ,
} pub enum Msg { MakeReq (i32), Resp (Resultado ),
}
A estrutura Detalhe de nosso componente inclui o link e fetch_task para buscar dados e os adereços que contêm o ID da tarefa.
A implementação do traço Component é semelhante àquela em nosso componente TodoApp :
Componente
impl para Detalhe { tipo Propriedades=adereços; tipo Message=Msg; fn criar (props: Self:: Properties, link: ComponentLink )-> Self { link.send_message (Msg:: MakeReq (props.todo_id)); Auto { adereços, link, todo: nenhum, fetch_task: Nenhum, } } fn view (& self)-> Html { html! { {self.render_detail (& self.todo)} } } fn update (& mut self, msg: Self:: Message)-> ShouldRender { corresponder msg { Msg:: MakeReq (id)=> { let req=Request:: get (& format! ( "https://jsonplaceholder.typicode.com/todos/{}", eu ia )) .body (nada) .expect ("pode fazer req para jsonplaceholder"); deixe cb= self.link .callback (| resposta: Resposta >> | { deixe Json (dados)=resposta.into_body (); Msg:: Resp (dados) }); let task=FetchService:: fetch (req, cb).expect ("pode criar tarefa"); self.fetch_task=Alguns (tarefa); () } Msg:: Resp (resp)=> { se deixar Ok (dados)=resp { self.todo=Alguns (dados); } } } verdadeiro } fn change (& mut self, props: Self:: Properties)-> ShouldRender { self.props=adereços; verdadeiro }
}
Novamente, usando FetchService para buscar dados de /todos/$ todo_id , podemos definir os dados retornados em nosso componente.
Vamos implementar o método render_detail diretamente em Detalhe também neste caso:
impl Detalhe { fn render_detail (& self, todo: & Option )-> Html { match todo { Algum (t)=> { deixe concluído=se t.completo { Alguns ("concluído") } senão { Alguns ("não concluído") }; html! { {& t.title} {"("} {t.id} {")"}
{"por usuário"} {t.user_id} {if t.completed {"done"} else {"not done"}} } } Nenhum=> { html! { {"carregando..."} } } } }
}
Novamente, mostramos uma mensagem simples de carregando… se ainda não tivermos dados. Com base no status concluído na tarefa com a qual estamos trabalhando, podemos definir uma classe diferente para colorir o texto de verde se estiver completo ou vermelho se estiver incompleto, conforme mostrado no navegador.
Executando o projeto final do Rust
Quando executamos nosso projeto usando trunk serve localmente, um servidor inicia em http://localhost: 8080 ; agora vemos como um belo aplicativo da web de front-end baseado em Rust.
Clicar em um item de tarefa nos direciona para sua página de detalhes e clicar em Página inicial nos leva de volta à visualização de lista.
Com o aplicativo de tarefas concluído, você pode encontrar o código completo para este exemplo no GitHub .
Conclusão
O advento do WebAssembly tornou possível construir aplicativos da web de front-end com Rust como o que acabamos de construir, expandindo as oportunidades de desenvolvimento para desenvolvedores.
E embora todas as bibliotecas, frameworks e tecnologias neste post ainda estejam no início do desenvolvimento, os recursos e capacidades disponíveis já estão amadurecendo e estáveis, abrindo a possibilidade para projetos maiores no futuro.
A postagem Crie um aplicativo da web de front-end Rust + WebAssembly com Yew apareceu primeiro no LogRocket Blog .