L'auteur du matériel, dont nous publions la traduction aujourd'hui, dit que sa dernière expérience dans le domaine de l'architecture des projets logiciels a été la création d'une application Web fonctionnelle utilisant uniquement le langage Rust et avec le minimum possible d'utilisation du code modèle. Dans cet article , il veut partager avec les lecteurs du fait qu'il a découvert la conception de l'application et de répondre à la
question de savoir si déjà Rust prêt à l' utiliser dans différents domaines du développement web.

Aperçu du projet
Le code du projet qui sera discuté ici peut être trouvé sur
GitHub . Les parties client et serveur de l'application sont situées dans le même référentiel, ceci pour simplifier la maintenance du projet. Il convient de noter que Cargo devra compiler les applications frontend et backend avec différentes dépendances.
Ici, vous pouvez jeter un œil à une application qui fonctionne.
Notre projet est une simple démonstration du mécanisme d'authentification. Il vous permet de vous connecter avec le nom d'utilisateur et le mot de passe sélectionnés (ils doivent être identiques).
Si le nom d'utilisateur et le mot de passe sont différents, l'authentification échouera. Une fois l'authentification réussie, le jeton
JWT (jeton Web JSON) est stocké à la fois côté client et côté serveur. Le stockage du jeton sur le serveur dans de telles applications n'est généralement pas nécessaire, mais je l'ai fait à des fins de démonstration. Ceci, par exemple, peut être utilisé pour savoir combien d'utilisateurs sont connectés. L'application entière peut être configurée à l'aide d'un seul fichier
Config.toml , par exemple, en spécifiant les informations d'identification pour accéder à la base de données, ou l'adresse du serveur et le numéro de port. Voici à quoi ressemble le code standard de ce fichier pour notre application.
[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"
Développement du client d'application
Pour développer le côté client de l'application, j'ai décidé d'utiliser l'
if . Il s'agit d'un cadre Rust moderne inspiré par Elm, Angular et React. Il est conçu pour créer des parties clientes d'applications Web multithread à l'aide de
WebAssembly (Wasm). Actuellement, ce projet est en développement actif, alors qu'il n'y a pas beaucoup de versions stables.
Le framework
yew
repose sur l'outil
cargo-web , qui est conçu pour la compilation croisée de code dans Wasm.
L'outil
cargo-web est une dépendance directe à l'
yew
qui simplifie la compilation croisée du code Rust dans Wasm. Voici trois objectifs principaux pour compiler Wasm qui sont disponibles via cet outil:
asmjs-unknown-emscripten
- utilise asm.js via Emscripten.wasm32-unknown-emscripten
- utilise WebAssembly via Emscriptenwasm32-unknown-unknown
- utilise WebAssembly avec le backend Rust natif pour WebAssembly
WebassemblyJ'ai décidé d'utiliser la dernière option, qui nécessite l'utilisation de l'assemblage «nuit» du compilateur Rust, mais qui illustre au mieux les capacités natives de Wasm de Rust.
Si nous parlons de WebAssembly, parler de Rust aujourd'hui est le sujet le plus chaud. Une énorme quantité de travail est en cours sur la compilation croisée de Rust dans Wasm et son intégration dans l'écosystème Node.js (à l'aide des packages npm). J'ai décidé de mettre en œuvre le projet sans aucune dépendance JavaScript.
Lors du démarrage du frontend d'une application web (dans mon projet, cela se fait avec la commande
make frontend
),
cargo-web
compile l'application dans Wasm et l'emballe, en ajoutant des matériaux statiques.
cargo-web
lance
cargo-web
un serveur web local, qui vous permet d'interagir avec l'application à des fins de développement. Voici ce qui se passe dans la console lorsque vous exécutez la commande ci-dessus:
> 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`.
Le framework
yew
présente des fonctionnalités très intéressantes. Parmi eux, la prise en charge des architectures de composants réutilisables. Cette fonctionnalité a simplifié la répartition de mon application en trois composants principaux:
RootComponent . Ce composant est directement monté sur la
<body>
du site Web. Il décide quel composant enfant doit être chargé ensuite. Si, à la première entrée de la page, un jeton JWT est trouvé, il essaie de mettre à jour ce jeton en contactant la partie serveur de l'application. Si cela échoue, la transition vers le composant
LoginComponent
est
LoginComponent
.
LoginComponent . Ce composant est un descendant du composant
RootComponent
; il contient un formulaire avec des champs pour entrer les informations d'identification. De plus, il interagit avec le backend de l'application pour organiser un schéma d'authentification simple basé sur la vérification du nom d'utilisateur et du mot de passe et, en cas d'authentification réussie, enregistre le JWT dans un cookie. De plus, si l'utilisateur a pu s'authentifier, il effectue la transition vers le composant
ContentComponent
.
Apparence du composant LoginComponentContentComponent Ce composant est un autre descendant du composant
RootComponent
. Il contient ce qui est affiché sur la page principale de l'application (pour le moment c'est juste un titre et un bouton pour quitter le système). L'accès peut être obtenu via
RootComponent
(si l'application, au démarrage, a réussi à trouver un jeton de session valide), ou via
LoginComponent
(en cas d'authentification réussie). Ce composant échange des données avec le backend lorsque l'utilisateur clique sur le bouton de déconnexion.
Composant ContentComponentRouterComponent Ce composant stocke tous les itinéraires possibles entre les composants contenant du contenu. De plus, il contient l'état initial du
loading
et de l'
error
de l'application. Il est directement connecté au
RootComponent
.
L'un des concepts clés suivants de l'
yew
dont nous allons discuter maintenant est celui des services. Ils vous permettent de réutiliser la même logique dans différents composants. Imaginons qu'il s'agisse d'interfaces de journalisation ou d'outils pour prendre en charge l'utilisation de
cookies . Les services ne stockent aucun état global; ils sont créés lors de l'initialisation des composants. En plus des services,
yew
soutient le concept d'agent. Ils peuvent être utilisés pour organiser le partage des données entre les différents composants, pour maintenir l'état général de l'application, tel que celui nécessaire à l'agent responsable du routage. Pour organiser le système de routage de notre application, couvrant tous les composants,
notre propre agent et service de routage ont été mis en place ici. Il
yew
pas de routeur standard dans
yew
, mais dans le référentiel du framework, vous pouvez trouver
un exemple d'implémentation de routeur qui prend en charge diverses opérations d'URL.
Je suis heureux de noter que
yew
utilise
l'API Web Workers pour exécuter des agents sur divers threads et utilise un planificateur local attaché au thread pour résoudre des tâches parallèles. Cela permet de développer des applications de navigateur avec un haut degré de multithreading sur Rust.
Chaque composant implémente sa propre
caractéristique Renderable , qui nous permet d'inclure du code HTML directement dans le code source de Rust à l'aide de la
macro html! {} .
C'est une excellente fonctionnalité et, bien sûr, son utilisation correcte est contrôlée par le compilateur. Voici le
Renderable
implémentation de
Renderable
dans le composant
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> } } }
La connexion entre le frontend et le backend est basée sur les connexions
WebSocket utilisées par chaque client. La force de la technologie WebSocket réside dans le fait qu'elle convient à la transmission de messages binaires, ainsi que le fait que le serveur, si nécessaire, peut envoyer des notifications push aux clients.
yew
dispose d'un service WebSocket standard, mais j'ai décidé de créer sa
propre version à des fins de démonstration, principalement en raison de l'initialisation "paresseuse" des connexions directement à l'intérieur du service. Si le service WebSocket devait être créé lors de l'initialisation du composant, je devrais surveiller de nombreuses connexions.
Protocole Cap'n ProtoJ'ai décidé d'utiliser le protocole
Cap'n Proto (au lieu de quelque chose comme
JSON ,
MessagePack ou
CBOR ) comme couche pour transmettre des données d'application pour des raisons de vitesse et de compacité. Il convient de noter que je n'ai pas utilisé l'interface de protocole
RPC de Cap'n Proto, car son implémentation Rust ne compile pas pour WebAssembly (en raison des dépendances Unix de
toxio-rs ). Cela a quelque peu compliqué la sélection des demandes et des réponses des types corrects, mais ce problème peut être résolu en utilisant une
API bien structurée . Voici la déclaration du protocole Cap'n Proto pour l'application.
@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; } } }
Vous pouvez voir que nous avons ici deux versions différentes de la demande de connexion.
L'un est pour
LoginComponent
(ici, pour obtenir un jeton, un nom et un mot de passe sont utilisés), et un autre pour
RootComponent
(il est utilisé pour mettre à jour un jeton existant). Tout ce qui est nécessaire pour que le protocole fonctionne est emballé dans le service de
protocole , grâce auquel il est pratique de réutiliser les capacités correspondantes dans diverses parties du frontend.
UIkit - un framework frontal modulaire compact pour développer des interfaces web rapides et puissantesL'interface utilisateur de la partie client de l'application est basée sur le framework
UIkit , sa version 3.0.0 sera publiée dans un futur proche. Un script
build.rs spécialement préparé télécharge automatiquement toutes les dépendances UIkit nécessaires et compile la feuille de style résultante. Cela signifie que vous pouvez ajouter vos propres styles à un seul fichier
style.scss , qui peut être appliqué à l'ensemble de l'application. C'est très pratique.
▍Frontend de test
Je pense qu'il y a des problèmes avec le test de notre solution. Le fait est qu'il est très simple de tester des services individuels, mais l'
yew
ne fournit pas au développeur un moyen pratique de tester les composants et les agents. Désormais, dans le cadre de Pure Rust, l'intégration et les tests de bout en bout du frontend ne sont pas disponibles. Ici, vous pouvez utiliser des projets comme
Cypress ou
Protractor , mais avec cette approche, vous devrez inclure beaucoup de modèles de code JavaScript / TypeScript dans le projet, j'ai donc décidé d'abandonner la mise en œuvre de ces tests.
Soit dit en passant, voici une idée pour un nouveau projet: un cadre pour les tests de bout en bout écrit en Rust.
Développement côté serveur d'applications
Pour implémenter le côté serveur de l'application, j'ai choisi le
framework actix-web . Il s'agit d'un cadre de
modèle d'acteur basé sur Rust, compact, pratique et très rapide. Il prend en charge toutes les technologies nécessaires, telles que WebSockets, TLS et
HTTP / 2.0 . Ce cadre prend en charge divers gestionnaires et ressources, mais dans notre application, seuls quelques itinéraires principaux ont été utilisés:
/ws
est la principale ressource pour les communications WebSocket./
- le gestionnaire principal qui donne accès à l'application frontale statique.
Par défaut,
actix-web
démarre les workflows dans la quantité correspondant au nombre de cœurs de processeur disponibles sur l'ordinateur local. Cela signifie que si l'application a un état, elle devra être partagée en toute sécurité entre tous les threads, mais grâce à des modèles informatiques parallèles fiables de Rust, ce n'est pas un problème. Quoi qu'il en soit, le backend devrait être un système sans état, car de nombreuses copies peuvent être déployées en parallèle dans un environnement cloud (comme
Kubernetes ). Par conséquent, les données constituant l'état de l'application doivent être séparées du backend. Par exemple, ils peuvent se trouver dans une instance distincte d'un conteneur
Docker .
SGBD PostgreSQL et projet DieselEn tant que principal entrepôt de données, j'ai décidé d'utiliser le SGBD
PostgreSQL . Pourquoi? Ce choix a déterminé l'existence d'un merveilleux projet
Diesel qui prend déjà en charge PostgreSQL et propose un système ORM sécurisé et extensible et un outil de création de requêtes pour lui. Tout cela correspond parfaitement aux besoins de notre projet, car
actix-web
prend déjà en charge Diesel. Par conséquent, ici, pour effectuer des opérations CRUD avec des informations sur les sessions dans la base de données, vous pouvez utiliser un langage spécial qui prend en compte les spécificités de Rust. Voici un exemple de gestionnaire
UpdateSession
pour
actix-web
basé sur Diesel.rs.
impl Handler<UpdateSession> for DatabaseExecutor { type Result = Result<Session, Error>; fn handle(&mut self, msg: UpdateSession, _: &mut Self::Context) -> Self::Result {
Le projet
r2d2 est utilisé pour établir une connexion entre
actix-web
et Diesel. Cela signifie que nous avons (en plus de l'application avec ses flux de travail) l'état partagé de l'application, qui prend en charge de nombreuses connexions de base de données en tant que pool de connexions unique. Cela simplifie considérablement la mise à l'échelle sérieuse du backend, ce qui rend cette solution flexible. Vous trouverez ici le code responsable de la création de l'instance de serveur.
▍Tester le backend
Les
tests d'intégration backend dans notre projet sont effectués en lançant une instance de serveur de test et en se connectant à une base de données déjà en cours d'exécution. Ensuite, vous pouvez utiliser le client WebSocket standard (j'ai utilisé de la
tungsténite ) pour envoyer au serveur les données générées à l'aide du protocole Cap'n Proto et comparer les résultats avec ceux attendus. Cette configuration de test s'est avérée excellente. Je n'ai pas utilisé de
serveurs de test spéciaux
actix-web , car beaucoup plus de travail n'est pas nécessaire pour configurer et exécuter un vrai serveur. Les tests unitaires d'arrière-plan se sont avérés, comme prévu, être une tâche assez simple; la réalisation de tels tests ne pose aucun problème.
Déploiement de projet
L'application est très facile à déployer à l'aide de l'image Docker.
DockerÀ l'aide de la
make deploy
vous pouvez créer une image appelée
webapp
qui contient des exécutables backend liés statiquement, le fichier
Config.toml
actuel, des certificats TLS et du contenu frontal statique. L'assemblage d'exécutables entièrement liés statiquement dans Rust est implémenté à l'aide d'une version modifiée de l'image Docker de
rust-musl-builder . Une application Web terminée peut être testée à l'aide de la commande
make run
, qui lance un conteneur compatible réseau. Le conteneur PostgreSQL doit être exécuté en parallèle avec le conteneur d'application pour garantir le fonctionnement du système. En général, le processus de déploiement de notre système est assez simple, de plus, grâce aux technologies utilisées ici, nous pouvons parler de sa flexibilité suffisante, simplifiant son adaptation possible aux besoins d'une application en développement.
Technologies utilisées dans le développement de projets
Voici le diagramme de dépendance d'application.
Technologies utilisées pour développer une application web à RustLe seul composant utilisé par le frontend et le backend est la version Rust de Cap'n Proto, qui nécessite la création du compilateur Cap'n Proto installé localement.
Les résultats. La rouille est-elle prête pour la production Web?
C'est une grande question. Voici ce que je peux répondre. Du point de vue des serveurs, je suis enclin à répondre «oui», car l'écosystème Rust, en plus d'
actix-web
, possède une
pile HTTP très mature et de nombreux
frameworks différents pour le développement rapide d'API et de services serveurs.
Si nous parlons du front-end, alors, grâce à l'attention générale portée à WebAssembly, beaucoup de travail se poursuit actuellement. Cependant, les projets créés dans ce domaine doivent atteindre la même maturité que les projets serveur. Cela est particulièrement vrai pour la stabilité de l'API et les capacités de test. Alors maintenant, je dis «non» à l'utilisation de Rust à l'avant, mais je ne peux m'empêcher de noter qu'il se déplace dans la bonne direction.
Chers lecteurs! Utilisez-vous Rust dans le développement Web?