Yew est un analogue de React and Elm, entièrement écrit en rouille et compilé en un honnête assemblage Web. Dans l'article, Denis Kolodin, développeur Yew, explique comment créer un framework sans garbage collector, garantir efficacement l'immuable, sans avoir besoin de copier l'état en raison des règles de propriété des données Rust, et quelles sont les fonctionnalités lors de la traduction de Rust en WebAssembly.
Le message a été préparé sur la base du rapport de Denis lors de la conférence HolyJS 2018 Piter . Sous la coupe - transcription vidéo et texte du rapport.Denis Kolodin travaille pour Bitfury Group, une société qui développe diverses solutions de blockchain. Depuis plus de deux ans maintenant, il codait en Rust, un langage de programmation de Mozilla Research. Pendant ce temps, Denis a réussi à étudier en profondeur ce langage et à l'utiliser pour développer diverses applications système, un backend. Maintenant, dans le cadre de l'avènement de la norme WebAssembly, j'ai commencé à regarder vers le front-end.Agenda
Aujourd'hui, nous apprendrons ce qu'est Yew (le nom du framework se lit comme le mot anglais "you" - you; "if" est un arbre d'if traduit de l'anglais).
Parlons un peu des aspects architecturaux, des idées sur lesquelles le cadre est construit, des possibilités qui y sont intégrées, ainsi que des fonctionnalités que Rust nous offre en plus par rapport aux autres langages.
À la fin, je vais vous montrer comment commencer à utiliser Yew et WebAssembly dès aujourd'hui.
Qu'est-ce que l'if?
Tout d'abord, c'est WebAssembly, c'est-à-dire bytecode exécutable qui fonctionne dans les navigateurs. Il est nécessaire pour exécuter des algorithmes complexes côté utilisateur, par exemple, la cryptographie, le codage / décodage. Il est plus facile de l'implémenter dans les langages système que de visser des béquilles.
WebAssembly est une norme clairement décrite, comprise et prise en charge par tous les navigateurs modernes. Il vous permet d'utiliser différents langages de programmation. Et cela est intéressant principalement parce que vous pouvez réutiliser le code créé par la communauté dans d'autres langues.
Si vous le souhaitez, vous pouvez complètement écrire une application sur WebAssembly, et Yew vous permet de le faire, mais il est important de ne pas oublier que même dans ce cas, JavaScript reste dans le navigateur. Il est nécessaire de préparer WebAssembly - prenez le module (WASM), ajoutez-y l'environnement et exécutez-le. C'est-à-dire JavaScript est indispensable. Par conséquent, WebAssembly mérite d'être considéré comme une extension plutôt qu'une alternative révolutionnaire à JS.
À quoi ressemble le développement

Vous avez une source, il y a un compilateur. Vous traduisez tout cela dans un format binaire et l'exécutez dans un navigateur. Si le navigateur est ancien, sans prise en charge de WebAssembly, alors emscripten est requis. Il s'agit, en gros, d'un émulateur WebAssembly pour un navigateur.
If - framework wasm prêt à l'emploi
Passons à Yew. J'ai développé ce cadre à la fin de l'année dernière. Ensuite, j'ai écrit une sorte d'application de crypto-monnaie sur Elm et face au fait qu'en raison de restrictions linguistiques, je ne peux pas créer une structure récursive. Et à ce moment, j'ai pensé: à Rust, mon problème serait résolu très facilement. Et puisque 99% du temps j'écris en Rust et que j'aime ce langage précisément pour ses fonctionnalités, j'ai décidé d'expérimenter - pour compiler l'application avec la même fonction de mise à jour dans Rust.
Le premier croquis m'a pris plusieurs heures, j'ai dû comprendre comment compiler WebAssembly. Je l'ai lancé et j'ai réalisé qu'en quelques heures, il avait posé le noyau, ce qui est très facile à développer. Il ne m'a fallu que quelques jours pour tout apporter au moteur de framework minimum.
Je l'ai posté en open source, mais je ne m'attendais pas à ce qu'il soit populaire. Cependant, aujourd'hui, il a collecté plus de 4 000 étoiles sur GitHub. Vous pouvez voir le projet
ici . Il existe de nombreux exemples.
Le cadre est entièrement écrit en Rust. Yew prend en charge la compilation directement sur WebAssembly (cible wasm32-unknown-unknown) sans emscripten. Si nécessaire, vous pouvez utiliser emscripten.
L'architecture
Maintenant, quelques mots sur la façon dont le cadre diffère des approches traditionnelles qui existent dans le monde JavaScript.
Tout d'abord, je vais vous montrer les restrictions linguistiques que j'ai rencontrées dans Elm. Prenons le cas quand il y a un modèle et qu'il y a un message qui vous permet de transformer ce modèle.
type alias Model = { value : Int } type Msg = Increment | Decrement
case msg of Increment -> { value = model.value + 1 } Decrement -> { value = model.value - 1 }
Dans Elm, nous créons simplement un nouveau modèle et l'afficher à l'écran. La version précédente du modèle reste inchangée. Pourquoi je me concentre là-dessus? Parce que dans Yew, le modèle est mutable, et c'est l'une des questions les plus courantes. Ensuite, je vais expliquer pourquoi cela est fait.
Au départ, j'ai suivi le chemin classique lors de la recréation du modèle. Mais au fur et à mesure que le cadre s'est développé, j'ai vu que cela n'avait aucun sens de stocker la version précédente du modèle. Rust vous permet de suivre la durée de vie de toutes les données, qu'elles soient modifiées ou non. Et donc je peux changer le modèle en toute sécurité, sachant que Rust contrôle l'absence de conflit.
struct Model { value: i64, } enum Msg { Increment, Decrement, }
match msg { Msg::Increment => { self.value += 1; } Msg::Decrement => { self.value -= 1; } }
Ceci est le premier moment. Deuxième point: pourquoi avons-nous besoin de l'ancienne version du modèle? Dans le même orme, il n'y a guère de problème d'accès concurrentiel. L'ancien modèle n'est nécessaire que pour comprendre quand effectuer le rendu. La prise de conscience de ce moment m'a permis de me débarrasser complètement de l'immuable et de ne pas conserver l'ancienne version.

Regardez l'option lorsque nous avons la fonction de
update
et deux champs -
value
et
name
. Une valeur est enregistrée lorsque nous entrons des données dans le champ de
input
. Le modèle change.

Il est important que la
value
ne
value
pas impliquée dans le rendu. Et donc nous pouvons le changer autant que nous voulons. Mais nous n'avons pas besoin d'influencer l'arborescence DOM et nous n'avons pas besoin d'initier ces changements.
Cela m'a conduit à l'idée que seul le développeur peut savoir le bon moment où le rendu doit vraiment être lancé. Pour commencer, j'ai commencé à utiliser l'indicateur - juste une valeur booléenne -
ShouldRender
, qui signale que le modèle a changé et que nous devons commencer le rendu. Dans le même temps, il n'y a pas de surcharge pour les comparaisons constantes, il n'y a pas de consommation de mémoire - les applications écrites en Yew sont les plus efficaces.
Dans l'exemple ci-dessus, il n'y avait aucune allocation de mémoire, à l'exception du message généré et envoyé. Le modèle a conservé son état, et cela ne s'est reflété dans le rendu qu'à l'aide d'un drapeau.
Les possibilités
Écrire un framework qui fonctionne dans WebAssembly n'est pas une tâche facile. Nous avons JavaScript, mais il devrait créer une sorte d'environnement avec lequel il faut interagir, ce qui représente une énorme quantité de travail. La version initiale de ces bundles ressemblait à ceci:

J'ai pris une démo d'un autre projet. Il existe de nombreux projets qui vont dans ce sens, mais cela conduit rapidement à une impasse. Après tout, le framework est un développement assez important et vous devez écrire beaucoup de code d'ancrage. J'ai commencé à utiliser des bibliothèques à Rust qui s'appellent des caisses, en particulier la
Stdweb
.
JS intégré
Avec l'aide des macros Rust, vous pouvez étendre la langue - nous pouvons intégrer des morceaux de JavaScript dans le code Rust, c'est une fonctionnalité très utile de la langue.
let handle = js! { var callback = @{callback}; var action = function() { callback(); }; var delay = @{ms}; return { interval_id: setInterval(action, delay), callback: callback, }; };
L'utilisation de macros et de Stdweb m'a permis d'écrire rapidement et efficacement tous les liens nécessaires.
Modèles Jsx
Au début, j'ai suivi le chemin Elm et j'ai commencé à utiliser des modèles implémentés à l'aide de code.
fn view(&self) -> Html<Context, Self> { nav("nav", ("menu"), vec![ button("button", (), ("onclick", || Msg::Clicked)), tag("section", ("ontop"), vec![ p("My text...") ]) ]) }
Je n'ai jamais été partisan de React. Mais quand j'ai commencé à écrire mon framework, j'ai réalisé que JSX dans React est une chose très cool. Voici une présentation très pratique des modèles de code.
En conséquence, j'ai pris une macro sur Rust et implémenté directement dans Rust la possibilité d'écrire du balisage HTML qui génère immédiatement des éléments d'arborescence virtuelle.
impl Renderable<Context, Model> for Model { fn view(&self) -> Html<Context, Self> { html! { <div> <nav class="menu",> <button onclick=|_| Msg::Increment,>{ "Increment" }</button> <button onclick=|_| Msg::Decrement,>{ "Decrement" }</button> </nav> <p>{ self.value }</p> <p>{ Local::now() }</p> </div> } } }
Nous pouvons dire que les modèles de type JSX sont des modèles de code pur, mais sur des stéroïdes. Ils sont présentés dans un format pratique. Notez également que j'insère ici une expression Rust directement dans le bouton (l'expression Rust peut être insérée à l'intérieur de ces modèles). Cela vous permet de vous intégrer très étroitement.
Composants assez structurés
Ensuite, j'ai commencé à développer des modèles et réalisé la possibilité d'utiliser des composants. Il s'agit du premier problème rencontré dans le référentiel. J'ai implémenté des composants qui peuvent être utilisés dans le code du modèle. Vous déclarez simplement une structure honnête dans Rust et lui écrivez quelques propriétés. Et ces propriétés peuvent être définies directement à partir du modèle.

Encore une fois, je note la chose importante que ces modèles sont du code Rust généré honnêtement. Par conséquent, toute erreur ici sera remarquée par le compilateur. C'est-à-dire vous ne pouvez pas vous tromper, comme c'est souvent le cas dans le développement JavaScript.
Zones dactylographiées
Une autre caractéristique intéressante est que lorsqu'un composant est placé à l'intérieur d'un autre composant, il peut voir le type de message du parent.

Le compilateur lie rigidement ces types et ne vous donnera pas la possibilité de faire une erreur. Lors du traitement des événements, les messages que le composant attend ou peut envoyer doivent correspondre entièrement au parent.
Autres fonctionnalités
J'ai transféré une implémentation de Rust directement vers le framework qui vous permet d'utiliser facilement différents formats de sérialisation / désérialisation (en lui fournissant des wrappers supplémentaires). Voici un exemple: nous allons au stockage local et, en restaurant les données, spécifions un certain wrapper - ce que nous attendons ici est json.
Msg::Store => { context.local_storage.store(KEY, Json(&model.clients)); } Msg::Restore => { if let Json(Ok(clients)) = context.local_storage.restore(KEY) { model.clients = clients; } }
Il peut s'agir de n'importe quel format, y compris binaire. En conséquence, la sérialisation et la désérialisation deviennent transparentes et pratiques.
L'idée d'une autre opportunité que j'ai implémentée est venue des utilisateurs du framework. Ils ont demandé de faire des fragments. Et là, je suis tombé sur une chose intéressante. Voyant en JavaScript la possibilité d'insérer des fragments dans l'arborescence DOM, j'ai d'abord décidé qu'il serait très facile d'implémenter une telle fonction dans mon framework. Mais j'ai essayé cette option, et il s'est avéré que cela ne fonctionne pas. Je devais le comprendre, marcher sur cet arbre, voir ce qui avait changé là-bas, etc.
Le framework Yew utilise une arborescence DOM virtuelle, tout y existe initialement. En fait, quand il y a des changements dans le modèle, ils se transforment en correctifs qui changent déjà l'arborescence DOM rendue.
html! { <> <tr><td>{ "Row" }</td></tr> <tr><td>{ "Row" }</td></tr> <tr><td>{ "Row" }</td></tr> </> }
Des avantages supplémentaires
La rouille offre de nombreuses autres fonctionnalités puissantes, je ne parlerai que des plus importantes.
Services: interaction avec le monde extérieur
La première occasion dont je veux parler concerne les services. Vous pouvez décrire les fonctionnalités nécessaires sous la forme d'un service, le publier en tant que caisse et le réutiliser.
Dans Rust, la possibilité de créer des bibliothèques, leur intégration, l'ancrage et le collage est très bien implémentée. En fait, vous pouvez créer diverses API pour interagir avec votre service, y compris celles en JavaScript. Dans le même temps, le framework peut interagir avec le monde extérieur, malgré le fait qu'il fonctionne à l'intérieur du runtime WebAssembly.
Exemples de services:
- TimeOutService;
- IntervalService;
- FetchService;
- WebSocketService;
- Services personnalisés ...
Services et caisses
antirouille :
crates.io .
Contexte: exigences de l'État
Une autre chose que j'ai mise en œuvre dans le cadre n'est pas entièrement traditionnelle, c'est le contexte. React a une API Context, mais j'ai utilisé Context dans un sens différent. Le cadre Yew se compose des composants que vous créez et le contexte est un état global. Les composants peuvent ne pas prendre en compte cet état global, mais peuvent faire certaines demandes - de sorte que l'entité globale remplit certains critères.
Disons que notre composant abstrait nécessite la possibilité de télécharger quelque chose sur S3.

On peut voir ci-dessous qu'il utilise ce téléchargement, c'est-à-dire envoie des données à S3. Un tel composant peut se présenter sous la forme d'un rack. L'utilisateur qui télécharge ce composant et l'ajoute à l'intérieur du modèle à son application rencontrera une erreur - le compilateur lui demandera où est le support S3? L'utilisateur devra implémenter ce support. Après cela, le composant commence automatiquement à vivre pleinement.
Où est-il nécessaire? Imaginez: vous créez un composant avec une cryptographie intelligente. Il a des exigences que le contexte environnant lui permette de se connecter quelque part. Il vous suffit d'ajouter un formulaire d'autorisation dans le modèle et de mettre en place dans votre contexte la connexion avec votre service. C'est-à-dire ce sera littéralement trois lignes de code. Après cela, le composant commence à fonctionner.
Imaginez que nous ayons des dizaines de composants différents. Et ils ont tous la même exigence. Cela vous permet d'implémenter une sorte de fonctionnalité une fois pour relancer tous les composants et récupérer les données nécessaires. Hors contexte. Et le compilateur ne vous permettra pas de faire une erreur: si vous n'avez pas d'interface qui nécessite un composant, rien ne fonctionnera.
Par conséquent, vous pouvez facilement créer des boutons très pointilleux qui demanderont une API ou d'autres fonctionnalités. Grâce à Rust et au système de ces interfaces (elles sont appelées trait dans Rust), il devient possible de déclarer les exigences des composants.
Le compilateur ne vous laissera pas faire d'erreur
Imaginez que nous créons un composant avec certaines propriétés, dont la possibilité de définir le rappel. Et, par exemple, nous avons défini la propriété et manqué une lettre dans son nom.

En essayant de compiler, Rust répond rapidement. Il dit que nous nous sommes trompés et qu'il n'y a pas une telle propriété:

Comme vous pouvez le voir, Rust utilise directement ce modèle et peut afficher toutes les erreurs à l'intérieur de la macro. Il vous dit comment la propriété doit vraiment s'appeler. Si vous avez réussi le compilateur, vous n'aurez pas d'erreurs d'exécution idiotes comme des fautes de frappe.
Imaginez maintenant, nous avons un bouton qui demande à notre contexte global de pouvoir se connecter à S3. Et créez un contexte qui n'implémente pas le support S3. Voyons ce qui se passe.

Le compilateur signale que nous avons inséré un bouton, mais cette interface n'est pas implémentée pour le contexte.

Il ne reste plus qu'à entrer dans l'éditeur, ajouter un lien vers Amazon dans le contexte, et tout commencera. Vous pouvez créer des services prêts à l'emploi avec une sorte d'API, puis simplement ajouter au contexte, y substituer un lien et le composant prend immédiatement vie. Cela vous permet de faire des choses très intéressantes: vous ajoutez des composants, créez un contexte, remplissez-le de services. Et tout cela fonctionne de manière entièrement automatique; des efforts minimes sont nécessaires pour tout lier ensemble.
Comment commencer à utiliser Yew?
Par où commencer si vous souhaitez essayer de compiler une application WebAssembly? Et comment cela peut-il être fait en utilisant le cadre Yew?
Compilation de rouille à wasm
Tout d'abord, vous devez installer le compilateur. Il existe un outil de rustup pour cela:
curl https://sh.rustup.rs -sSf | sh
De plus, vous devrez peut-être emscripten. À quoi cela peut-il être utile? La plupart des bibliothèques écrites pour les langages de programmation système, en particulier pour Rust (à l'origine un système), sont développées pour Linux, Windows et d'autres systèmes d'exploitation à part entière. De toute évidence, le navigateur n'a pas beaucoup de fonctionnalités.
Par exemple, la génération de nombres aléatoires dans un navigateur ne se fait pas de la même manière que sous Linux. emscripten est utile si vous souhaitez utiliser des bibliothèques qui nécessitent une API système.
Les bibliothèques et l'ensemble de l'infrastructure passent tranquillement à un assemblage Web honnête, et emscripten n'est plus requis (vous utilisez des capacités basées sur JavaScript pour générer des nombres aléatoires et d'autres choses), mais si vous devez créer quelque chose qui n'est pas du tout pris en charge dans le navigateur, vous ne pouvez pas vous passer d'emscripten .
Je recommande également d'utiliser cargo-web:
cargo install cargo-web
Il est possible de compiler WebAssembly sans utilitaires supplémentaires. Mais cargo-web est un outil génial qui fournit plusieurs choses utiles aux développeurs JavaScript. En particulier, il surveillera les fichiers: si vous apportez des modifications, il commencera à compiler immédiatement (le compilateur ne fournit pas de telles fonctions). Dans ce cas, Cargo-web vous permettra d'accélérer le développement. Il existe différents systèmes de construction pour Rust, mais le fret représente 99,9% de tous les projets.
Un nouveau projet est créé comme suit:
cargo new --bin my-project
[package]
name = "my-project"
version = "0.1.0"
[dependencies]
yew = "0.3.0"
Ensuite, lancez simplement le projet:
cargo web start --target wasm32-unknown-unknown
J'ai donné un exemple de WebAssembly honnête. Si vous devez compiler sous emscripten (le compilateur rust peut connecter emscripten lui-même), vous pouvez insérer le mot
emscripten
dans le tout dernier élément
unknown
, ce qui vous permet d'utiliser plus de caisses. N'oubliez pas que emscripten est un kit supplémentaire assez volumineux pour votre fichier. Par conséquent, il est préférable d'écrire du code WebAssembly honnête.
Restrictions existantes
Toute personne ayant une expérience du codage dans les langages de programmation système peut être frustrée par les limitations existantes dans le cadre. Toutes les bibliothèques ne peuvent pas être utilisées dans WebAssembly. Par exemple, dans un environnement JavaScript, il n'y a pas de threads. WebAssembly ne le déclare pas en principe, et bien sûr vous pouvez l'utiliser dans un environnement multi-thread (c'est une question ouverte), mais JavaScript est toujours un environnement single-thread. Oui, il y a des travailleurs, mais c'est l'isolement, donc il n'y aura pas de flux là-bas.
Il semblerait que vous puissiez vivre sans flux. Mais si vous souhaitez utiliser des bibliothèques basées sur des threads, par exemple, vous souhaitez ajouter une sorte d'exécution, cela peut ne pas décoller.
De plus, il n'y a pas d'API système, à l'exception de celle que vous transférerez de JavaScript à WebAssembly. Par conséquent, de nombreuses bibliothèques ne seront pas portées. Vous ne pouvez pas écrire et lire des fichiers directement, les sockets ne peuvent pas être ouverts et vous ne pouvez pas écrire sur le réseau. Si vous voulez créer un socket Web, par exemple, vous devez le faire glisser depuis JavaScript.
Un autre inconvénient est que le débogueur WASM existe, mais personne ne l'a vu. Il est encore dans un état brut tel qu'il est peu probable qu'il vous soit utile. Le débogage de WebAssembly est donc une question délicate.
Lors de l'utilisation de Rust, presque tous les problèmes d'exécution seront associés à des erreurs de logique métier, ils seront faciles à résoudre. Mais très rarement des bogues de bas niveau apparaissent - par exemple, l'une des bibliothèques fait le mauvais amarrage - et c'est déjà une question difficile. Par exemple, pour le moment, il y a un tel problème: si je compile le cadre avec emscripten et qu'il y a une cellule de mémoire variable, dont la possession est supprimée, elle est donnée, emscripten se désagrège quelque part au milieu (et je ne suis même pas sûr que ce soit emscripten). Sachez que si vous rencontrez un problème quelque part dans le middleware à un niveau bas, il sera difficile de le résoudre pour le moment.
L'avenir du cadre
Comment évoluera Yew? Je vois son objectif principal dans la création de composants monolithiques. Vous aurez un fichier WebAssembly compilé, et vous le collez simplement dans l'application. Par exemple, il peut fournir des capacités cryptographiques, un rendu ou une édition.
Intégration JS
L'intégration avec JavaScript sera renforcée. JavaScript a écrit un grand nombre de bibliothèques intéressantes qui sont faciles à utiliser. Et il y a des exemples dans le référentiel où je montre comment vous pouvez utiliser la bibliothèque JavaScript existante directement à partir du cadre Yew.
CSS typé
Étant donné que Rust est utilisé, il est évident que vous pouvez ajouter du CSS typé qui peut être généré avec la même macro que dans l'exemple d'un moteur de modèle de type JSX. Dans ce cas, le compilateur vérifiera, par exemple, si vous avez attribué un autre attribut au lieu de la couleur. Cela vous fera économiser beaucoup de temps.
Composants prêts
Je regarde également vers la création de composants prêts à l'emploi. Sur le framework, vous pouvez faire des fissures qui fourniront, par exemple, un ensemble de quelques boutons ou éléments qui seront connectés en tant que bibliothèque, ajoutés aux modèles et utilisés.
Amélioration des performances dans les affaires privées
La performance est une question très délicate et complexe. WebAssembly est-il plus rapide que JavaScript? Je n'ai aucune preuve confirmant une réponse positive ou négative. On dirait et selon certains tests très simples que j'ai menés, WebAssembly est très rapide. Et je suis convaincu que ses performances seront supérieures à celles de JavaScript, simplement parce que c'est un bytecode de bas niveau où l'allocation de mémoire n'est pas requise et il existe de nombreux autres moments nécessitant des ressources.
Plus de contributeurs
J'aimerais attirer plus de contributeurs. Les portes pour participer au cadre sont toujours ouvertes. Tous ceux qui souhaitent mettre à niveau quelque chose, comprendre le noyau et transformer les outils avec lesquels un grand nombre de développeurs travaillent peuvent facilement se connecter et proposer leurs propres modifications.
De nombreux contributeurs ont déjà participé au projet. Mais il n'y a pas de contributeurs Core pour le moment, car pour cela vous devez comprendre le vecteur de développement du framework, mais il n'a pas encore été clairement formulé. Mais il y a une colonne vertébrale, des gars qui connaissent très bien Yew - environ 30 personnes. Si vous souhaitez également ajouter quelque chose au cadre, veuillez toujours envoyer une demande d'extraction.
La documentation
Un point obligatoire dans mes plans est la création d'une grande quantité de documentation sur la façon d'écrire des applications sur Yew. De toute évidence, l'approche de développement dans ce cas est différente de ce que nous avons vu dans React et Elm.
Parfois, les gars me montrent des cas intéressants sur la façon d'utiliser le cadre. Pourtant, créer un cadre n'est pas la même chose que d'y écrire professionnellement. Des pratiques d'utilisation du cadre sont toujours en cours d'élaboration.
Essayez-le, installez Rust, développez vos capacités en tant que développeur. La maîtrise de WebAssembly sera utile à chacun de nous, car la création d'applications très complexes est le moment que nous attendons depuis longtemps. En d'autres termes, WebAssembly ne concerne pas seulement un navigateur Web, mais c'est généralement un runtime qui se développe définitivement et se développera encore plus activement.
Si vous avez aimé le rapport, faites attention: du 24 au 25 novembre, un nouveau HolyJS se tiendra à Moscou, et il y aura aussi beaucoup de choses intéressantes. — , ( ).