J'ai testé pour vous, un framework web que-tout-le-monde-s'en-fout, ou presque.
Mon blog n'est pas juste une vitrine sur mes étonnants travaux de graphiste, l'idée de départ était de l'utiliser pour recycler des connaissances en re-écrivant entièrement une pièce ou l'autre. Voire tout.
C'est un peu pour ça que j'utiliser une API + un "frontend" statique qui sont dans des repos séparés à une époque ou tout le monde utilise juste des générateurs de sites statiques.
En pratique le frontend et le backend ont déjà changé une fois chacun. Je sais que tout le monde s'en fout mais en gros:
- Backend: Play Framework -> Spring Boot
- Frontend: Polymer (lol) -> Truc pas net en "Vanilla" JS dans une énorme variable globale
Je me suis dit que ce serait pas mal d'avoir un truc plus léger parce qu'on parle tout de même de ~150MB de mémoire pour mon bidule Spring Boot. Puis c'est Java aussi, c'est un peu la honte. Je crois.
Ceci dit je suis toujours content de Spring Boot, super projet facile à prendre en main et plein de ressources en ligne pour toutes les situations imaginables, le tout facile à déployer n'importe où.
Actix Web en gros
Actix web ressemble un peu à Express.js. On abandonne les vieux concepts moisis de "contrôleur" et tout ça (MVC LOL) au profit d'une approche plus fonctionnelle et... Flexible. Je vais utiliser flexible plutôt que simple. Vous aussi vous aimez quand c'est flexible?
Je précise que j'ai testé avec Actix web version 3, parce que le projet est pas mal en travaux.
La doc officielle est pas mal foutue pour se lancer rapidement à ceci près qu'elle ne comment que très peu la structure du projet (c'est flexible, vous vous souvenez?) et qu'il y a plusieurs moyens de s'y prendre pour déclarer ses handlers.
Parce qu'au final, c'est ça le but, un peu à la mode d'Expess en JS, vous allez déclarer que telle fonction doit être appelée sur telle URL avec telle méthode HTTP (et éventuellement d'autres conditions plus poussées de routage).
Async/await
Async/await en Rust c'est déjà toute une aventure en soi. Vous ne devez pas nécessairement connaître son fonctionnement de manière précise, mais juste quelques bases:
- Bien qu'il existe des primitives async/await dans le langage, ça sert a rien du tout sans un runtime qui est une librairie à part que vous allez devoir ajouter et choisir. Heureusement (je crois), avec Actix ils ont choisi Tokio pour vous.
- Les fonctions "async" peuvent être appelées dans d'autres fonctions "async" en les suivant d'un ".await".
- Votre "runtime" de choix devrait embarquer sa propre version de librairie pour l'IO, par ex. tokio::fs au lieu de std::fs. Attention, les structures et fonctions s'utilisent de manière légèrement/totalement différente selon les cas.
- C'est pas possible de déclarer une closure comme async, donc fini les jolies chaines de map() et filter() dans pas mal de cas et bonjour les boucles for partout.
- Votre "runtime" de choix propose un mécanisme pour créer vos propres "tâches bloquantes" qui sont en fait juste secrètement des threads mais peuvent être utilisées avec async/await.
- Créer votre propre bidule async sans passer par les librairies déjà faites est possible mais c'est chaud du slip et je conseille pas. Bourrez-les dans des Task du runtime ou utilisez des bons vieux threads à l'ancienne.
- On s'en fout un peu en vrai mais Rust utilise le concept de Future similaire à celui de Scala/Java plutôt que les "promesses" comme en JS. Utiliser ".await" déballe le contenu du Future, c'est pour ça que c'est pas hyper important.
Déclarer le serveur
Le serveur Actix est créé par un constructeur puis un builder pattern qui se termine en fonction async.
Ce qui implique que main() est censé être async aussi. Il y a une annotation pour ça avec Tokio, Actix ajoute sa propre annotation qui à mon avis fait exactement la même chose mais ils préfèrent sans doute rester vaguement agnostiques quant à la librairie utilisée pour async/await **bruit de prout**.
Vous déclarez ensuite une structure App qui contient, entre autres, toute la config de routage. Un peu comme quand on fait ça avec ExpressJS:
app.get('/', (req, res) => {
res.send('Hello World!')
})
Ben là c'est un peu similaire:
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(|| {
HttpResponse::Ok().body("Hello World!")
}))
/* REST OF THE ROUTING CONFIG */
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Où on utilise une méthode main qui renvoie un Result pour pouvoir utiliser l'opérateur "?" ce qui est toujours une bonne pratique.
Le handler renvoie ici une HttpResponse. C'est possible de renvoyer quelque chose qui implémente un trait d'Actix qui s'appelle Responder et que vous pouvez implémenter sur vos propres types ou bien compter sur quelques uns qui existent déjà — notamment celui qui vous permet de juste renvoyer un String comme réponse.
Les méthodes handlers peuvent être déclarées de plusieurs manières et prendre différentes formes. Plus haut j'ai utilisé une closure mais c'est mieux de grouper tous vos handlers dans un module séparé.
Si votre routage est complexe c'est encore mieux d'éclater ce module séparé en plusieurs sous-modules.
Exemple de structure:
- app
- mod.rs
- handlers
- mod.rs
- users.rs
- admin.rs
- stats.rs
- db
- mod.rs
- main.rs
Je vais pas vous montrer toutes les possibilités f'organisation, mais je trouvais ça intéressant de montrer à quel point il y a moyen que ça ressemble à Spring Boot ou .NET MVC, avec un exemple de handler défini par annotation, avec une variable récupérée automatiquement dans l'URL:
use actix_web::{get, web, App, HttpServer, HttpResponse};
use serde_json::json;
#[get("/users/{user_id}")]
async fn users(path: web::Path<(u32,)>) -> HttpResponse {
let user_id = path.into_inner().0;
HttpResponse::Ok().json(json!({ "user": "Frank", "id": user_id }))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(users)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Je déclare simplement un "extracteur" de type web::Path dans mes arguments et je lui donne le type d'argument de chemin que je m'attend à pouvoir désérialiser. Ici, un tuple avec just un u32 dedans. Mais en vrai ça pourrait être n'importe quoi qui implémente le trait Deserialize de la librairie Serde (les noms de champs doivent correspondre à ce qui est déclaré dans le chemin pour qu'il s'y retrouve). Et/ou Serialize tant qu'à faire, c'est facile de juste dériver les deux tfaçon.
De la même manière, j'ai répondu avec du JSON en utilisant le macro json! de la librairie serde_json parce que c'était facile, mais vous pouvez donner n'importe quoi qui implémente Serialize à cette méthode json(), y compris vos propres struct, tableaux de trucs machins etc. et ça fonctionne tout seul.
Remarquez qu'au niveau config de routage j'ai juste déclaré un service et les annotations font le reste.
Mais alors c'est bien alors?
Le démon est dans les détails (?), vous aurez rapidement besoin d'un contrôle d'erreur pratique. Qui peut être facilement implémenté en renvoyer un Result avec vos handlers, dont le type d'erreur implémente un trait particulier d'Actix qui lui permet de générer une réponse HTTP.
Cool, mais pas suffisant. Par ex., si on reprend l'exemple avec variable "user_id" dans le chemin plus haut, le routage va renvoyer une erreur HTTP si quelqu'un essayer d'entrer un nombre négatif dans le chemin, et cette erreur, outre son statut d'erreur HTTP, n'a au mieux pas du tout de corps de réponse, au pire un truc sans en-tête d'encodage et en anglais et texte brut.
Personaliser cette erreur en conservant le fonctionnement tout simple de l'extracteur de valeur de chemin, c'est très loin d'être simple et on se heurte au majeur problème d'Actix: dès que l'on sort un peu des sentiers battus, il y a peu de ressources en ligne (STACKOVERFLOW LOL) parce qu'en soi, le projet a peu d'utilisateurs.
Les concepts de middleware et variants existent aussi dans Actix mais créer le vôtre est un peu plus compliqué que ça en a l'air également.
Fort heureusement il y a un repo plein d'exemples que je vous conseille si vous voulez vous lancer dans l'aventure, c'est une bonne source d'inspiration:
https://github.com/actix/examples
Conclusion
Ouais faut que je m'arrête parce que cette "brève" est de nouveau trop longue.
Actix est extrêment efficace (son nombre de threads est configurable — Utilise le nombre de coeurs logiques par défaut) et peu gourmand en mémoire tout en permettant de très rapidement construire une API simple et pas trop trop ambitieuse en terme de comportements qui sortent un petit peu trop des sentiers battus.
Le truc c'est que Node n'est pas si gourmand en mémoire non plus... A part qu'il n'a vraiment qu'un seul thead. Mais si vous êtes en mode serveur du ghetto comme moi, c'est plutôt un avantage.
J'aimerais bien recommander Rust et notamment Actix pour tous mes copains soucieux de tirer le maximum de leur matériel mais ce cas d'utilisation où "le serveur est un peu nul mais il s'attend quand même à être bien bombardé" est un peu bizarre.
Peut-être pour des systèmes embarqués?
Reste que Rust en général est bien plus performant pour certaines choses, par ex. traiter d'énormes quantités de chaines de caractères dans devoir en faire 1000000 de copies. Son modèle de multithreading est également aussi rapide que robuste. Ces considérations n'ont rien à voir avec Actix, c'est juste Rust quoi.
Et alors, si vous devez travailler à plusieurs là dessus... A moins d'avoir une équipe très, très atypique, je vois pas comment je pourrais recommander Rust. Mais je dis ça comme je dirais que je vois pas comment je pourrais recommander du C++ non plus, sauf qu'il reste une tonne de dinosaures du C++ et qu'il semble y avoir assez peu de vétérans du Rust.
Mais bon moi j'aime bien. Avoir des tests intégrés (Actix embarque des helpers de tests d'intégration qui euh... S'intègrent dans les tests standard de Rust), une chaine d'outil qui va bien avec étrangement peu de blagues et un sentiment de sécurité et de "bien faire les choses" presque constant, c'est quand même CHOUETTE.
Juste un truc... Je me moque toujours des projets "Gatsby" vierges dont le dossier source pèse au moins 120MB, mais là... Mon projet Actix fait plus de 8GB:
>>> du -sh
8.2G
Il y a peut-être deux ou trois trucs que j'ai oublié de nettoyer hein... Mais ça fait un sacré paquet de fois JQuery cette histoire.
Commentaires
Il faut JavaScript activé pour écrire des commentaires ici
#1
#2