Entwicklung von Webanwendungen in Rust

Der Autor des Materials, dessen Übersetzung wir heute veröffentlichen, sagt, dass sein jüngstes Experiment auf dem Gebiet der Architektur von Softwareprojekten die Erstellung einer funktionierenden Webanwendung war, die nur die Sprache Rust verwendet und den Vorlagencode so wenig wie möglich verwendet. In diesem Artikel möchte er den Lesern mitteilen, was er bei der Entwicklung einer Anwendung herausgefunden und die Frage beantwortet hat , ob Rust bereit ist, sie in verschiedenen Bereichen der Webentwicklung einzusetzen.



Projektübersicht


Der Code für das Projekt, der hier besprochen wird, ist auf GitHub zu finden. Die Client- und Serverteile der Anwendung befinden sich im selben Repository. Dies wird durchgeführt, um die Wartung des Projekts zu vereinfachen. Es ist zu beachten, dass Cargo die Frontend- und Backend-Anwendungen mit unterschiedlichen Abhängigkeiten kompilieren muss. Hier können Sie sich eine funktionierende Anwendung ansehen.

Unser Projekt ist eine einfache Demonstration des Authentifizierungsmechanismus. Sie können sich mit dem ausgewählten Benutzernamen und Passwort anmelden (diese müssen identisch sein).

Wenn der Benutzername und das Kennwort unterschiedlich sind, schlägt die Authentifizierung fehl. Nach erfolgreicher Authentifizierung wird das JWT- Token (JSON Web Token) sowohl auf der Client- als auch auf der Serverseite gespeichert. Das Speichern des Tokens auf dem Server in solchen Anwendungen ist normalerweise nicht erforderlich, aber ich habe genau das zu Demonstrationszwecken getan. Auf diese Weise können Sie beispielsweise herausfinden, wie viele Benutzer angemeldet sind. Die gesamte Anwendung kann mithilfe einer einzelnen Datei Config.toml konfiguriert werden, z. B. unter Angabe der Anmeldeinformationen für den Zugriff auf die Datenbank oder der Adresse und Portnummer des Servers. So sieht der Standardcode für diese Datei für unsere Anwendung aus.

[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" 

Anwendungsclient-Entwicklung


Um die Client-Seite der Anwendung zu entwickeln, habe ich mich für die Verwendung von Eibe entschieden . Dies ist ein modernes Rust-Framework, das von Elm, Angular und React inspiriert wurde. Es wurde entwickelt, um Client-Teile von Multithread-Webanwendungen mit WebAssembly (Wasm) zu erstellen. Derzeit befindet sich dieses Projekt in der aktiven Entwicklung, während es nicht sehr viele stabile Releases gibt.

Das yew Framework basiert auf dem Cargo-Web- Tool, mit dem Code in Wasm übergreifend kompiliert werden kann.

Das Cargo-Web- Tool ist eine direkte Eibenabhängigkeit, die die Kreuzkompilierung von Rust-Code in Wasm vereinfacht. Hier sind drei Hauptziele für die Kompilierung von Wasm, die über dieses Tool verfügbar sind:

  • asmjs-unknown-emscripten - verwendet asm.js über Emscripten.
  • wasm32-unknown-emscripten - verwendet WebAssembly über Emscripten
  • wasm32-unknown-unknown - verwendet WebAssembly mit dem nativen Rust-Backend für WebAssembly


Webassembly

Ich habe mich für die letzte Option entschieden, die die Verwendung der "Night" -Baugruppe des Rust-Compilers erfordert, aber im besten Fall die nativen Wasm-Funktionen von Rust demonstriert.
Wenn wir über WebAssembly sprechen, dann ist es heute das heißeste Thema, über Rust zu sprechen. Es wird viel Arbeit geleistet, um Rust in Wasm zu kompilieren und in das Node.js-Ökosystem zu integrieren (mithilfe von npm-Paketen). Ich habe beschlossen, das Projekt ohne JavaScript-Abhängigkeiten zu implementieren.

Beim Starten des Frontends einer Webanwendung (in meinem Projekt erfolgt dies mit dem Befehl make frontend ) kompiliert cargo-web die Anwendung in Wasm und packt sie, wobei einige statische Materialien hinzugefügt werden. cargo-web startet cargo-web einen lokalen Webserver, mit dem Sie zu Entwicklungszwecken mit der Anwendung interagieren können. Folgendes passiert in der Konsole, wenn Sie den obigen Befehl ausführen:

 > 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`. 

Das yew Framework hat einige sehr interessante Funktionen. Dazu gehört die Unterstützung wiederverwendbarer Komponentenarchitekturen. Diese Funktion hat die Aufteilung meiner Anwendung in drei Hauptkomponenten vereinfacht:

RootComponent . Diese Komponente wird direkt in das <body> -Tag der Website eingebunden. Er entscheidet, welche untergeordnete Komponente als nächstes geladen werden soll. Wenn beim ersten Aufrufen der Seite ein JWT-Token gefunden wird, wird versucht, dieses Token zu aktualisieren, indem der Serverteil der Anwendung kontaktiert wird. Wenn dies fehlschlägt, wird der Übergang zur LoginComponent Komponente LoginComponent .

LoginComponent . Diese Komponente ist ein Nachkomme der RootComponent Komponente und enthält ein Formular mit Feldern zur Eingabe von Anmeldeinformationen. Darüber hinaus interagiert es mit dem Backend der Anwendung, um ein einfaches Authentifizierungsschema zu organisieren, das auf der Überprüfung des Benutzernamens und des Kennworts basiert, und speichert bei erfolgreicher Authentifizierung das JWT in einem Cookie. Wenn der Benutzer sich authentifizieren konnte, wechselt er außerdem zur ContentComponent Komponente.


Aussehen der LoginComponent-Komponente

ContentComponent Diese Komponente ist ein weiterer Nachkomme der RootComponent Komponente. Es enthält, was auf der Hauptseite der Anwendung angezeigt wird (im Moment ist es nur ein Titel und eine Schaltfläche zum Beenden des Systems). Der Zugriff darauf kann über RootComponent (wenn die Anwendung beim Start ein gültiges Sitzungstoken gefunden hat) oder über LoginComponent (bei erfolgreicher Authentifizierung) erfolgen. Diese Komponente tauscht Daten mit dem Backend aus, wenn der Benutzer auf die Schaltfläche zum Abmelden klickt.


ContentComponent-Komponente

RouterComponent Diese Komponente speichert alle möglichen Routen zwischen Komponenten, die Inhalt enthalten. Darüber hinaus enthält es den Anfangszustand des loading und error der Anwendung. Es ist direkt mit der RootComponent .

Eines der folgenden Schlüsselkonzepte der yew , die wir jetzt diskutieren werden, sind Dienstleistungen. Mit ihnen können Sie dieselbe Logik in verschiedenen Komponenten wiederverwenden. Angenommen, dies können Protokollierungsschnittstellen oder Tools sein, die die Verwendung von Cookies unterstützen . Services speichern keinen globalen Status, sondern werden beim Initialisieren von Komponenten erstellt. Neben Dienstleistungen unterstützt yew das Konzept der Agenten. Sie können verwendet werden, um die gemeinsame Nutzung von Daten zwischen verschiedenen Komponenten zu organisieren und den allgemeinen Status der Anwendung beizubehalten, z. B. den für den für das Routing verantwortlichen Agenten erforderlichen. Um das Routing-System unserer Anwendung zu organisieren, das alle Komponenten abdeckt, wurden hier unser eigener Agent und unser Routing-Service implementiert. Es yew keinen Standard-Router in yew , aber im Framework-Repository finden Sie ein Beispiel für eine Router-Implementierung, die eine Vielzahl von URL-Operationen unterstützt.

Ich freue mich feststellen zu können, dass yew die Web Workers-API verwendet , um Agenten in verschiedenen Threads auszuführen, und einen an den Thread angehängten lokalen Scheduler verwendet, um parallele Aufgaben zu lösen. Dies ermöglicht die Entwicklung von Browseranwendungen mit einem hohen Grad an Multithreading auf Rust.

Jede Komponente implementiert ihre eigene Renderable-Eigenschaft , mit der wir HTML-Code mithilfe des HTML! {} -Makros direkt in den Rust-Quellcode aufnehmen können.

Dies ist eine großartige Funktion, und natürlich wird die ordnungsgemäße Verwendung vom Compiler gesteuert. Hier ist der Renderable Implementierungscode in der LoginComponent Komponente.

 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>       }   } } 

Die Verbindung zwischen Frontend und Backend basiert auf WebSocket- Verbindungen, die von jedem Client verwendet werden. Die Stärke der WebSocket-Technologie liegt in der Tatsache, dass sie für die Übertragung von Binärnachrichten geeignet ist, sowie in der Tatsache, dass der Server bei Bedarf Push-Benachrichtigungen an Clients senden kann. yew hat einen Standard-WebSocket-Dienst, aber ich habe beschlossen, zu Demonstrationszwecken eine eigene Version zu erstellen, hauptsächlich wegen der "faulen" Initialisierung von Verbindungen direkt innerhalb des Dienstes. Wenn der WebSocket-Dienst während der Komponenteninitialisierung erstellt würde, müsste ich viele Verbindungen überwachen.


Protokoll Cap'n Proto

Ich habe mich aus Gründen der Geschwindigkeit und Kompaktheit entschieden, das Cap'n Proto- Protokoll (anstelle von JSON , MessagePack oder CBOR ) als Schicht für die Übertragung von Anwendungsdaten zu verwenden. Es ist erwähnenswert, dass ich die in Cap'n Proto verfügbare RPC- Protokollschnittstelle nicht verwendet habe, da die Rust-Implementierung nicht für WebAssembly kompiliert wurde (aufgrund der Unix-Abhängigkeiten von Toxio-rs ). Dies erschwerte die Auswahl der Anforderungen und Antworten der richtigen Typen etwas, aber dieses Problem kann mithilfe einer gut strukturierten API gelöst werden. Hier ist die Cap'n Proto-Protokolldeklaration für die Anwendung.

 @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;       }   } } 

Sie können sehen, dass wir hier zwei verschiedene Versionen der Anmeldeanforderung haben.

Eine ist für LoginComponent (hier werden zum LoginComponent eines Tokens ein Name und ein Kennwort verwendet) und eine andere für RootComponent (zum Aktualisieren eines vorhandenen Tokens). Alles, was für das Funktionieren des Protokolls erforderlich ist, ist im Protokolldienst gepackt, wodurch es bequem ist, die entsprechenden Funktionen in verschiedenen Teilen des Frontends wiederzuverwenden.


UIkit - ein kompaktes modulares Front-End-Framework für die Entwicklung schneller und leistungsstarker Webschnittstellen

Die Benutzeroberfläche des Client-Teils der Anwendung basiert auf dem UIkit- Framework. Die Version 3.0.0 wird in naher Zukunft veröffentlicht. Ein speziell vorbereitetes build.rs- Skript lädt automatisch alle erforderlichen UIkit-Abhängigkeiten herunter und kompiliert das resultierende Stylesheet. Dies bedeutet, dass Sie einer einzelnen style.scss- Datei eigene Stile hinzufügen können, die auf die gesamte Anwendung angewendet werden können. Es ist sehr bequem.

▍Test Frontend


Ich glaube, dass es einige Probleme beim Testen unserer Lösung gibt. Tatsache ist, dass es sehr einfach ist, einzelne Services zu testen, aber yew bietet dem Entwickler keine bequeme Möglichkeit, Komponenten und Agenten zu testen. Im Rahmen von Pure Rust ist die Integration und das End-to-End-Testen des Frontends nicht verfügbar. Hier könnten Sie Projekte wie Cypress oder Protractor verwenden , aber bei diesem Ansatz müssten Sie viel JavaScript / TypeScript-Code für Vorlagen in das Projekt aufnehmen, sodass ich beschlossen habe, die Implementierung solcher Tests abzubrechen.

Übrigens, hier ist eine Idee für ein neues Projekt: ein in Rust geschriebenes Framework für End-to-End-Tests.

Anwendungsserverseitige Entwicklung


Um die Serverseite der Anwendung zu implementieren, habe ich das actix-web- Framework ausgewählt. Dies ist ein kompaktes, praktisches und sehr schnelles Rust-basiertes Akteurmodell- Framework. Es unterstützt alle erforderlichen Technologien wie WebSockets, TLS und HTTP / 2.0 . Dieses Framework unterstützt verschiedene Handler und Ressourcen, aber in unserer Anwendung wurden nur einige Hauptrouten verwendet:

  • /ws ist die Hauptressource für die WebSocket-Kommunikation.
  • / - der Haupthandler, der den Zugriff auf die statische Front-End-Anwendung ermöglicht.

Standardmäßig startet actix-web Workflows in der Menge, die der Anzahl der auf dem lokalen Computer verfügbaren Prozessorkerne entspricht. Dies bedeutet, dass die Anwendung, wenn sie einen Status hat, sicher von allen Threads gemeinsam genutzt werden muss. Dank zuverlässiger Rust Parallel Template-Vorlagen ist dies jedoch kein Problem. Wie auch immer, das Backend sollte ein zustandsloses System sein, da viele Kopien davon parallel in einer Cloud-Umgebung (wie Kubernetes ) bereitgestellt werden können. Infolgedessen müssen die Daten, die den Status der Anwendung bilden, vom Backend getrennt werden. Beispielsweise können sie sich in einer separaten Instanz eines Docker- Containers befinden.


PostgreSQL DBMS- und Dieselprojekt

Als Haupt-Data-Warehouse habe ich mich für das PostgreSQL- DBMS entschieden. Warum? Diese Wahl hat die Existenz eines wunderbaren Dieselprojekts bestimmt , das PostgreSQL bereits unterstützt und ein sicheres und erweiterbares ORM-System und ein Tool zum Erstellen von Abfragen bietet. All dies entspricht perfekt den Anforderungen unseres Projekts, da actix-web Diesel bereits unterstützt. Um CRUD-Operationen mit Informationen zu Sitzungen in der Datenbank auszuführen, können Sie daher eine spezielle Sprache verwenden, die die Besonderheiten von Rust berücksichtigt. Hier ist ein Beispiel für einen UpdateSession Handler für actix-web basierend auf Diesel.rs.

 impl Handler<UpdateSession> for DatabaseExecutor {   type Result = Result<Session, Error>;   fn handle(&mut self, msg: UpdateSession, _: &mut Self::Context) -> Self::Result {       //         debug!("Updating session: {}", msg.old_id);       update(sessions.filter(id.eq(&msg.old_id)))           .set(id.eq(&msg.new_id))           .get_result::<Session>(&self.0.get()?)           .map_err(|_| ServerError::UpdateToken.into())   } } 

Mit dem Projekt r2d2 wird eine Verbindung zwischen actix-web und Diesel hergestellt. Dies bedeutet, dass wir (zusätzlich zur Anwendung mit ihren Workflows) den gemeinsam genutzten Status der Anwendung haben, der viele Datenbankverbindungen als einen einzigen Verbindungspool unterstützt. Dies vereinfacht die ernsthafte Skalierung des Backends erheblich und macht diese Lösung flexibel. Hier finden Sie den Code, der für die Erstellung der Serverinstanz verantwortlich ist.

▍Test Backend


Backend- Integrationstests in unserem Projekt werden durchgeführt, indem eine Testserverinstanz gestartet und eine Verbindung zu einer bereits ausgeführten Datenbank hergestellt wird. Anschließend können Sie den Standard-WebSocket-Client (ich habe Wolframit verwendet ) verwenden, um mit dem Cap'n Proto-Protokoll generierte Daten an den Server zu senden und die Ergebnisse mit den erwarteten zu vergleichen. Dieser Testaufbau hat sich als hervorragend erwiesen. Ich habe keine speziellen Actix-Web-Testserver verwendet , da zum Konfigurieren und Ausführen eines echten Servers nicht viel mehr Arbeit erforderlich ist. Backend-Unit-Tests erwiesen sich erwartungsgemäß als recht einfache Aufgabe, die Durchführung solcher Tests verursacht keine Probleme.

Projektbereitstellung


Die Anwendung ist mit dem Docker-Image sehr einfach bereitzustellen.


Docker

Mit dem make deploy Sie ein Image namens webapp , das statisch verknüpfte ausführbare Backend-Dateien, die aktuelle Datei Config.toml , TLS-Zertifikate und statischen Frontend-Inhalt enthält. Die Assemblierung vollständig statisch verknüpfter ausführbarer Dateien in Rust wird mithilfe einer modifizierten Version des Docker-Images des Rostmuslels erstellt . Eine fertige Webanwendung kann mit dem Befehl make run getestet werden, mit dem ein netzwerkfähiger Container gestartet wird. Der PostgreSQL-Container muss parallel zum Anwendungscontainer ausgeführt werden, um sicherzustellen, dass das System funktioniert. Im Allgemeinen ist der Prozess der Bereitstellung unseres Systems recht einfach. Dank der hier verwendeten Technologien können wir außerdem über seine ausreichende Flexibilität sprechen und die mögliche Anpassung an die Anforderungen einer sich entwickelnden Anwendung vereinfachen.

Technologien für die Projektentwicklung


Hier ist das Anwendungsabhängigkeitsdiagramm.


Technologien zur Entwicklung einer Webanwendung in Rust

Die einzige Komponente, die das Frontend und das Backend verwenden, ist die Rust-Version von Cap'n Proto, für deren Erstellung der lokal installierte Cap'n Proto-Compiler erforderlich ist.

Die Ergebnisse. Ist Rust bereit für die Webproduktion?


Das ist eine große Frage. Folgendes kann ich beantworten. Aus Sicht der Server neige ich zur Antwort „Ja“, da das Rust-Ökosystem neben actix-web über einen sehr ausgereiften HTTP-Stack und viele verschiedene Frameworks für die schnelle Entwicklung von Server-APIs und -Diensten verfügt.

Wenn wir über das Front-End sprechen, wird dank der allgemeinen Aufmerksamkeit für WebAssembly jetzt viel gearbeitet. In diesem Bereich erstellte Projekte müssen jedoch dieselbe Laufzeit haben wie Serverprojekte. Dies gilt insbesondere für API-Stabilität und Testfunktionen. Jetzt sage ich "Nein" zur Verwendung von Rust im Frontend, aber ich kann nicht anders, als zu bemerken, dass es sich in die richtige Richtung bewegt.

Liebe Leser! Verwenden Sie Rust in der Webentwicklung?

Source: https://habr.com/ru/post/de416933/


All Articles