O autor do material, cuja tradução publicamos hoje, diz que seu último experimento no campo da arquitetura de projetos de software foi a criação de um aplicativo da Web em funcionamento usando apenas a linguagem Rust e com o mínimo possível de uso de código de modelo. Neste artigo, ele deseja compartilhar com os leitores o que descobriu ao desenvolver um aplicativo e responder à
pergunta se o Rust está pronto para usá-lo em várias áreas do desenvolvimento da web.

Visão Geral do Projeto
O código do projeto que será discutido aqui pode ser encontrado no
GitHub . As partes cliente e servidor do aplicativo estão localizadas no mesmo repositório, isso é feito para simplificar a manutenção do projeto. Deve-se notar que o Cargo precisará compilar os aplicativos de front-end e back-end com diferentes dependências.
Aqui você pode dar uma olhada em um aplicativo em funcionamento.
Nosso projeto é uma demonstração simples do mecanismo de autenticação. Ele permite que você efetue login com o nome de usuário e senha selecionados (eles devem ser os mesmos).
Se o nome de usuário e a senha forem diferentes, a autenticação falhará. Após a autenticação bem-sucedida, o token
JWT (JSON Web Token) é armazenado no cliente e no servidor. Normalmente, não é necessário armazenar o token no servidor nesses aplicativos, mas fiz exatamente isso para fins de demonstração. Isso, por exemplo, pode ser usado para descobrir quantos usuários estão conectados. O aplicativo inteiro pode ser configurado usando um único arquivo
Config.toml , por exemplo, especificando credenciais para acessar o banco de dados ou o endereço e o número da porta do servidor. Veja como é o código padrão desse arquivo para o nosso aplicativo.
[server] ip = "127.0.0.1" port = "30080" tls = false [log] actix_web = "debug" webapp = "trace" [postgres] host = "127.0.0.1" username = "username" password = "password" database = "database"
Desenvolvimento de aplicativos cliente
Para desenvolver o lado do cliente do aplicativo, decidi usar o
teixo . Esta é uma estrutura Rust moderna inspirada em Elm, Angular e React. Ele foi projetado para criar partes do cliente de aplicativos da Web com vários threads usando o
WebAssembly (Wasm). Atualmente, este projeto está em desenvolvimento ativo, embora não haja muitas versões estáveis.
A estrutura do
yew
depende da ferramenta de
carga da web , projetada para compilar o código no Wasm.
A ferramenta
web de carga é uma dependência direta de
yew
que simplifica a compilação cruzada do código Rust no Wasm. Aqui estão três objetivos principais para compilar o Wasm disponíveis através desta ferramenta:
asmjs-unknown-emscripten
- usa asm.js por meio do Emscripten.wasm32-unknown-emscripten
- usa o WebAssembly por meio do Emscriptenwasm32-unknown-unknown
- usa o WebAssembly com o back-end Rust nativo para o WebAssembly
WebassemblyDecidi usar a última opção, que requer o uso da montagem "noturna" do compilador Rust, mas, na melhor das hipóteses, demonstra os recursos nativos Wasm do Rust.
Se falamos sobre o WebAssembly, falar sobre o Rust hoje é o assunto mais quente. Uma grande quantidade de trabalho está sendo feita na compilação cruzada do Rust in Wasm e na integração ao ecossistema Node.js. (usando pacotes npm). Decidi implementar o projeto sem nenhuma dependência de JavaScript.
Ao iniciar o frontend de um aplicativo da Web (no meu projeto, isso é feito com o comando
make frontend
), o
cargo-web
compila o aplicativo no Wasm e o compacta, adicionando alguns materiais estáticos.
cargo-web
inicia um servidor da Web local, que permite interagir com o aplicativo para fins de desenvolvimento. Aqui está o que acontece no console quando você executa o comando acima:
> make frontend Compiling webapp v0.3.0 (file:///home/sascha/webapp.rs) Finished release [optimized] target(s) in 11.86s Garbage collecting "app.wasm"... Processing "app.wasm"... Finished processing of "app.wasm"! If you need to serve any extra files put them in the 'static' directory in the root of your crate; they will be served alongside your application. You can also put a 'static' directory in your 'src' directory. Your application is being served at '/app.js'. It will be automatically rebuilt if you make any changes in your code. You can access the web server at `http://0.0.0.0:8000`.
A estrutura do
yew
tem algumas características muito interessantes. Entre eles está o suporte para arquiteturas de componentes reutilizáveis. Esse recurso simplificou a divisão do meu aplicativo em três componentes principais:
RootComponent . Este componente é montado diretamente na tag
<body>
do site. Ele decide qual componente filho deve ser carregado a seguir. Se, na primeira entrada da página, for encontrado um token JWT, ele tentará atualizar esse token entrando em contato com a parte do servidor do aplicativo. Se isso falhar, a transição para o componente
LoginComponent
é
LoginComponent
.
LoginComponent . Este componente é um descendente do componente
RootComponent
e contém um formulário com campos para inserir credenciais. Além disso, ele interage com o back-end do aplicativo para organizar um esquema de autenticação simples com base na verificação do nome de usuário e senha e, no caso de autenticação bem-sucedida, salva o JWT em um cookie. Além disso, se o usuário puder se autenticar, ele fará a transição para o componente
ContentComponent
.
Aparência do componente LoginComponentContentComponent Este componente é outro descendente do componente
RootComponent
. Ele contém o que é exibido na página principal do aplicativo (no momento, é apenas um título e um botão para sair do sistema). O acesso a ele pode ser obtido através do
RootComponent
(se o aplicativo, na inicialização, conseguiu encontrar um token de sessão válido) ou através do
LoginComponent
(no caso de autenticação bem-sucedida). Este componente troca dados com o back-end quando o usuário clica no botão logout.
Componente ContentComponentRouterComponent Este componente armazena todas as rotas possíveis entre componentes que contêm conteúdo. Além disso, ele contém o estado inicial do
loading
e
error
do aplicativo. Está diretamente conectado ao
RootComponent
.
Um dos seguintes conceitos-chave do
yew
que discutiremos agora são os serviços. Eles permitem reutilizar a mesma lógica em diferentes componentes. Digamos que possam ser interfaces ou ferramentas de log para suportar o uso de
cookies . Os serviços não armazenam algum estado global; eles são criados quando os componentes são inicializados. Além dos serviços, o
yew
apóia o conceito de agentes. Eles podem ser usados para organizar o compartilhamento de dados entre vários componentes, para manter o estado geral do aplicativo, como o necessário para o agente responsável pelo roteamento. Para organizar o sistema de roteamento de nossa aplicação, cobrindo todos os componentes,
nosso próprio agente e serviço de roteamento foram implementados aqui. Não há roteador padrão no
yew
, mas no repositório da estrutura, você pode encontrar
um exemplo de implementação de roteador que suporta uma variedade de operações de URL.
Tenho o prazer de observar que o
yew
usa
a API Web Workers para executar agentes em vários threads e usa um agendador local anexado ao thread para resolver tarefas paralelas. Isso possibilita o desenvolvimento de aplicativos de navegador com um alto grau de multithreading no Rust.
Cada componente implementa sua própria
característica Renderizável , o que nos permite incluir o código HTML diretamente no código-fonte do Rust usando a
macro html! {} .
Esse é um ótimo recurso e, é claro, seu uso adequado é controlado pelo compilador. Aqui está o
Renderable
implementação
Renderable
no componente
LoginComponent
.
impl Renderable<LoginComponent> for LoginComponent { fn view(&self) -> Html<Self> { html! { <div class="uk-card uk-card-default uk-card-body uk-width-1-3@s uk-position-center",> <form onsubmit="return false",> <fieldset class="uk-fieldset",> <legend class="uk-legend",>{"Authentication"}</legend> <div class="uk-margin",> <input class="uk-input", placeholder="Username", value=&self.username, oninput=|e| Message::UpdateUsername(e.value), /> </div> <div class="uk-margin",> <input class="uk-input", type="password", placeholder="Password", value=&self.password, oninput=|e| Message::UpdatePassword(e.value), /> </div> <button class="uk-button uk-button-default", type="submit", disabled=self.button_disabled, onclick=|_| Message::LoginRequest,>{"Login"}</button> <span class="uk-margin-small-left uk-text-warning uk-text-right",> {&self.error} </span> </fieldset> </form> </div> } } }
A conexão entre o front-end e o back-end é baseada nas conexões
WebSocket usadas por cada cliente. A força da tecnologia WebSocket é o fato de ser adequado para a transmissão de mensagens binárias, bem como o fato de o servidor, se necessário, poder enviar notificações por push aos clientes.
yew
possui um serviço WebSocket padrão, mas eu decidi criar sua
própria versão para fins de demonstração, principalmente por causa da inicialização "lenta" das conexões diretamente dentro do serviço. Se o serviço WebSocket fosse criado durante a inicialização do componente, eu precisaria monitorar muitas conexões.
Protocolo Cap'n ProtoDecidi usar o protocolo
Cap'n Proto (em vez de algo como
JSON ,
MessagePack ou
CBOR ) como uma camada para transmitir dados de aplicativos por motivos de velocidade e compacidade. Vale ressaltar que eu não usei a interface do protocolo
RPC que o Cap'n Proto possui, pois sua implementação Rust não é compilada para o WebAssembly (devido às
dependências do
toxio-rs Unix). Isso complicou a seleção de solicitações e respostas dos tipos corretos, mas esse problema pode ser resolvido usando uma
API bem estruturada . Aqui está a declaração do protocolo Cap'n Proto para o aplicativo.
@0x998efb67a0d7453f; struct Request { union { login :union { credentials :group { username @0 :Text; password @1 :Text; } token @2 :Text; } logout @3 :Text; # The session token } } struct Response { union { login :union { token @0 :Text; error @1 :Text; } logout: union { success @2 :Void; error @3 :Text; } } }
Você pode ver que aqui temos duas versões diferentes da solicitação de login.
Um é para
LoginComponent
(aqui, para obter um token, um nome e senha são usados) e outro é para
RootComponent
(é usado para atualizar um token existente). Tudo o que é necessário para o protocolo funcionar é fornecido no serviço de
protocolo , graças ao qual é conveniente reutilizar os recursos correspondentes em várias partes do front-end.
UIkit - uma estrutura de front-end modular compacta para o desenvolvimento de interfaces da web rápidas e poderosasA interface do usuário da parte do cliente do aplicativo é baseada na estrutura do
UIkit , sua versão 3.0.0 será lançada em um futuro próximo. Um script
build.rs especialmente preparado
baixa automaticamente todas as dependências necessárias do UIkit e compila a folha de estilo resultante. Isso significa que você pode adicionar seus próprios estilos a um único arquivo
style.scss , que pode ser aplicado em todo o aplicativo. É muito conveniente
▍Teste de front-end
Acredito que existem alguns problemas ao testar nossa solução. O fato é que é muito simples testar serviços individuais, mas o
yew
não fornece ao desenvolvedor uma maneira conveniente de testar componentes e agentes. Agora, na estrutura do Rust puro, a integração e o teste de ponta a ponta do frontend não estão disponíveis. Aqui você pode usar projetos como
Cypress ou
Transferidor , mas com essa abordagem, você precisará incluir muitos códigos JavaScript / TypeScript no projeto, então decidi abandonar a implementação desses testes.
A propósito, aqui está uma idéia para um novo projeto: uma estrutura para testes de ponta a ponta, escrita em Rust.
Desenvolvimento do lado do servidor de aplicativos
Para implementar o lado do servidor do aplicativo, escolhi a estrutura
actix-web . Essa é uma estrutura de
modelo de ator compacta, prática e muito rápida, baseada em Rust. Ele suporta todas as tecnologias necessárias, como WebSockets, TLS e
HTTP / 2.0 . Essa estrutura suporta vários manipuladores e recursos, mas em nosso aplicativo apenas algumas rotas principais foram usadas:
/ws
é o principal recurso para comunicações WebSocket./
- o manipulador principal que dá acesso ao aplicativo front-end estático.
Por padrão, o
actix-web
inicia os fluxos de trabalho na quantidade correspondente ao número de núcleos de processador disponíveis no computador local. Isso significa que, se o aplicativo tiver um estado, ele deverá ser compartilhado com segurança entre todos os threads, mas graças aos modelos confiáveis de computação paralela Rust, isso não é um problema. Seja como for, o back-end deve ser um sistema sem estado, pois muitas cópias dele podem ser implantadas em paralelo em um ambiente em nuvem (como o
Kubernetes ). Como resultado, os dados que formam o estado do aplicativo devem ser separados do back-end. Por exemplo, eles podem estar dentro de uma instância separada de um contêiner do
Docker .
Projeto DBMS e Diesel PostgreSQLComo o principal data warehouse, decidi usar o DBMS do
PostgreSQL . Porque Essa escolha determinou a existência de um maravilhoso projeto
Diesel que já suporta o PostgreSQL e oferece um sistema ORM seguro e extensível e uma ferramenta de criação de consultas para ele. Tudo isso corresponde perfeitamente às necessidades do nosso projeto, já que o
actix-web
já suporta o Diesel. Como resultado, aqui, para executar operações CRUD com informações sobre sessões no banco de dados, você pode usar um idioma especial que leve em consideração as especificidades do Rust. Aqui está um exemplo do manipulador
actix-web
para o
actix-web
baseado no Diesel.rs.
impl Handler<UpdateSession> for DatabaseExecutor { type Result = Result<Session, Error>; fn handle(&mut self, msg: UpdateSession, _: &mut Self::Context) -> Self::Result {
O projeto
r2d2 é usado para estabelecer uma conexão entre o
actix-web
e o Diesel. Isso significa que temos (além do aplicativo com seus fluxos de trabalho) o estado compartilhado do aplicativo, que suporta muitas conexões com o banco de dados como um único pool de conexões. Isso simplifica bastante o dimensionamento sério do back-end, tornando esta solução flexível.
Aqui você pode encontrar o código responsável pela criação da instância do servidor.
▍Teste de back-end
O
teste de integração de back-end em nosso projeto é realizado iniciando uma instância do servidor de teste e conectando-se a um banco de dados já em execução. Em seguida, você pode usar o cliente WebSocket padrão (usei
tungstenita ) para enviar dados gerados usando o protocolo Cap'n Proto para o servidor e comparar os resultados com os esperados. Essa configuração de teste provou ser excelente. Não
usei servidores de teste especiais
actix-web , pois não é necessário muito mais trabalho para configurar e executar um servidor real. O teste de unidade de back-end acabou sendo, como esperado, uma tarefa bastante simples; a realização desses testes não causa problemas.
Implantação do projeto
O aplicativo é muito fácil de implantar usando a imagem do Docker.
DockerUsando o
make deploy
é possível criar uma imagem chamada
webapp
que contém executáveis de back-end vinculados estaticamente, o arquivo
Config.toml
atual, certificados TLS e conteúdo de front-end estático. A montagem de executáveis totalmente estaticamente vinculados no Rust é implementada usando uma versão modificada da imagem do Docker do
rust-musl-builder . Um aplicativo Web concluído pode ser testado usando o comando
make run
, que inicia um contêiner habilitado para rede. O contêiner do PostgreSQL deve ser executado em paralelo com o contêiner do aplicativo para garantir que o sistema funcione. Em geral, o processo de implantação de nosso sistema é bastante simples. Além disso, graças às tecnologias usadas aqui, podemos falar sobre sua flexibilidade suficiente, simplificando sua possível adaptação às necessidades de um aplicativo em desenvolvimento.
Tecnologias utilizadas no desenvolvimento de projetos
Aqui está o diagrama de dependência do aplicativo.
Tecnologias usadas para desenvolver uma aplicação web no RustO único componente usado pelo front-end e back-end é a versão Rust do Cap'n Proto, que requer a criação do compilador Cap'n Proto instalado localmente.
Os resultados. O Rust está pronto para produção na Web?
Esta é uma grande questão. Aqui está o que eu posso responder. Do ponto de vista dos servidores, estou inclinado a responder "sim", pois o ecossistema Rust, além do
actix-web
, possui uma
pilha HTTP muito madura e muitas
estruturas diferentes para o rápido desenvolvimento de APIs e serviços de servidor.
Se falamos sobre o front-end, graças à atenção geral ao WebAssembly, muito trabalho está sendo feito agora. No entanto, os projetos criados nesta área devem atingir a mesma maturidade que os projetos de servidor. Isso é especialmente verdadeiro para os recursos de estabilidade e teste da API. Então agora digo "não" ao uso do Rust no front-end, mas não posso deixar de notar que ele está se movendo na direção certa.
Caros leitores! Você usa Rust no desenvolvimento web?