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 o ComponentLink
  • view renderiza o componente
  • update é chamado quando uma Message é enviada ao componente, implementando a lógica de passagem de mensagem
  • change renderiza novamente as alterações, otimizando a velocidade de renderização
  • renderizado é chamado uma vez após view , 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 tarefa para manter 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.

Visualização detalhada Item de pendências concluído
A visão detalhada de um item de pendências completo
Visualização detalhada Open To-Do Item
A visão detalhada de um item de pendências aberto

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.

Visualização de lista
A exibição de lista

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 .