Desarrollo de aplicaciones web en Rust

El autor del material, cuya traducción publicamos hoy, dice que su último experimento en el campo de la arquitectura de proyectos de software fue la creación de una aplicación web que funciona utilizando solo el lenguaje Rust y con el mínimo uso posible de código de plantilla. En este artículo, quiere compartir con los lectores lo que descubrió al desarrollar una aplicación y responder a la pregunta de si Rust está listo para usarlo en varias áreas del desarrollo web.



Resumen del proyecto


El código para el proyecto que se discutirá aquí se puede encontrar en GitHub . Las partes cliente y servidor de la aplicación se encuentran en el mismo repositorio, esto se hace para simplificar el mantenimiento del proyecto. Cabe señalar que Cargo necesitará compilar las aplicaciones frontend y backend con diferentes dependencias. Aquí puede ver una aplicación que funciona.

Nuestro proyecto es una simple demostración del mecanismo de autenticación. Le permite iniciar sesión con el nombre de usuario y la contraseña seleccionados (deben ser los mismos).

Si el nombre de usuario y la contraseña son diferentes, la autenticación fallará. Después de una autenticación exitosa, el token JWT (JSON Web Token) se almacena en el lado del cliente y del servidor. Por lo general, no es necesario almacenar el token en el servidor en tales aplicaciones, pero lo hice solo con fines de demostración. Esto, por ejemplo, se puede utilizar para averiguar cuántos usuarios están conectados. La aplicación completa se puede configurar usando un solo archivo Config.toml , por ejemplo, especificando credenciales para acceder a la base de datos, o la dirección y el número de puerto del servidor. Así es como se ve el código estándar para este archivo para nuestra aplicación.

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

Desarrollo de clientes de aplicaciones


Para desarrollar el lado del cliente de la aplicación, decidí usar tejo . Este es un marco moderno Rust inspirado en Elm, Angular y React. Está diseñado para crear porciones de clientes de aplicaciones web multiproceso utilizando WebAssembly (Wasm). Actualmente, este proyecto está en desarrollo activo, mientras que no hay muchas versiones estables.

El marco de trabajo de yew basa en la herramienta web de carga , que está diseñada para compilar código en Wasm.

La herramienta web de carga es una dependencia directa del yew que simplifica la compilación cruzada del código Rust en Wasm. Aquí hay tres objetivos principales para compilar Wasm que están disponibles a través de esta herramienta:

  • asmjs-unknown-emscripten : utiliza asm.js a través de Emscripten.
  • wasm32-unknown-emscripten : utiliza WebAssembly a través de Emscripten
  • wasm32-unknown-unknown : utiliza WebAssembly con el backend nativo de Rust para WebAssembly


Montaje web

Decidí usar la última opción, que requiere el uso del ensamblaje "nocturno" del compilador Rust, pero en el mejor de los casos demuestra las capacidades nativas de Wasm de Rust.
Si hablamos de WebAssembly, hablar de Rust hoy es el tema más candente. Se está realizando una gran cantidad de trabajo en la compilación cruzada de Rust in Wasm y su integración en el ecosistema Node.js (utilizando paquetes npm). Decidí implementar el proyecto sin ninguna dependencia de JavaScript.

Al iniciar el frontend de una aplicación web (en mi proyecto esto se hace con el comando make frontend ), cargo-web compila de forma cruzada la aplicación en Wasm y la empaqueta, agregando algunos materiales estáticos. cargo-web lanza un servidor web local, que le permite interactuar con la aplicación con fines de desarrollo. Esto es lo que sucede en la consola cuando ejecuta el comando anterior:

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

El marco de yew tiene algunas características muy interesantes. Entre ellos se encuentra el soporte para arquitecturas de componentes reutilizables. Esta característica ha simplificado el desglose de mi aplicación en tres componentes principales:

RootComponent . Este componente está montado directamente en la etiqueta <body> del sitio web. Él decide qué componente secundario se debe cargar a continuación. Si, en la primera entrada de la página, se encuentra un token JWT, intenta actualizar este token poniéndose en contacto con el servidor de la aplicación. Si esto falla, se realiza la transición al componente LoginComponent .

LoginComponent . Este componente es un descendiente del componente RootComponent ; contiene un formulario con campos para ingresar credenciales. Además, interactúa con el back-end de la aplicación para organizar un esquema de autenticación simple basado en la verificación del nombre de usuario y la contraseña y, en caso de autenticación exitosa, guarda el JWT en una cookie. Además, si el usuario pudo autenticarse, realiza la transición al componente ContentComponent .


Apariencia del Componente LoginComponent

ContentComponent Este componente es otro descendiente del componente RootComponent . Contiene lo que se muestra en la página principal de la aplicación (en este momento es solo un título y un botón para salir del sistema). El acceso al mismo se puede obtener a través de RootComponent (si la aplicación, al inicio, logró encontrar un token de sesión válido), o a través de LoginComponent (en caso de autenticación exitosa). Este componente intercambia datos con el back-end cuando el usuario hace clic en el botón de cerrar sesión.


Componente de componente de contenido

RouterComponent Este componente almacena todas las rutas posibles entre componentes que contienen contenido. Además, contiene el estado inicial de la loading de la aplicación y el error . Está directamente conectado al RootComponent .

Uno de los siguientes conceptos clave de yew que discutiremos ahora son los servicios. Le permiten reutilizar la misma lógica en diferentes componentes. Digamos que estos pueden ser interfaces o herramientas de registro para admitir el uso de cookies . Los servicios no almacenan un estado global; se crean cuando se inicializan los componentes. Además de los servicios, yew apoya el concepto de agentes. Se pueden usar para organizar el intercambio de datos entre varios componentes, para mantener el estado general de la aplicación, como el necesario para el agente responsable del enrutamiento. Para organizar el sistema de enrutamiento de nuestra aplicación, que cubre todos los componentes, nuestro propio agente y servicio de enrutamiento se implementaron aquí. No hay un enrutador estándar en yew , pero en el repositorio de framework puede encontrar un ejemplo de implementación de enrutador que admite una variedad de operaciones de URL.

Me complace observar que yew usa la API de Web Workers para ejecutar agentes en varios subprocesos y usa un programador local adjunto al hilo para resolver tareas paralelas. Esto hace posible desarrollar aplicaciones de navegador con un alto grado de subprocesamiento múltiple en Rust.

Cada componente implementa su propio rasgo Renderable , que nos permite incluir código HTML directamente en el código fuente de Rust usando la macro html! {} .

Esta es una gran característica y, por supuesto, el compilador controla su uso adecuado. Aquí está el Renderable implementación Renderable en el 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>       }   } } 

La conexión entre el frontend y el backend se basa en las conexiones WebSocket utilizadas por cada cliente. La fortaleza de la tecnología WebSocket es el hecho de que es adecuada para transmitir mensajes binarios, así como el hecho de que el servidor, si es necesario, puede enviar notificaciones push a los clientes. yew tiene un servicio estándar de WebSocket, pero decidí crear su propia versión con fines de demostración, principalmente debido a la inicialización "perezosa" de las conexiones directamente dentro del servicio. Si el servicio WebSocket se creara durante la inicialización del componente, tendría que monitorear muchas conexiones.


Protocolo Cap'n Proto

Decidí usar el protocolo Cap'n Proto (en lugar de algo como JSON , MessagePack o CBOR ) como una capa para transmitir datos de la aplicación por razones de velocidad y compacidad. Vale la pena señalar que no utilicé la interfaz de protocolo RPC que tiene Cap'n Proto, ya que su implementación de Rust no se compila para WebAssembly (debido a las dependencias de toxio-rs Unix). Esto complica un poco la selección de solicitudes y respuestas de los tipos correctos, pero este problema se puede resolver utilizando una API bien estructurada . Aquí está la declaración del protocolo Cap'n Proto para la aplicación.

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

Puede ver que aquí tenemos dos versiones diferentes de la solicitud de inicio de sesión.

Uno es para LoginComponent (aquí, para obtener un token, se usa un nombre y una contraseña), y otro es para RootComponent (se usa para actualizar un token existente). Todo lo que se necesita para que el protocolo funcione está empaquetado en el servicio de protocolo , gracias al cual es conveniente reutilizar las capacidades correspondientes en varias partes de la interfaz.


UIkit: un marco frontal compacto y modular para desarrollar interfaces web rápidas y potentes

La interfaz de usuario de la parte cliente de la aplicación se basa en el marco UIkit , su versión 3.0.0 se lanzará en un futuro próximo. Un script build.rs especialmente preparado descarga automáticamente todas las dependencias UIkit necesarias y compila la hoja de estilo resultante. Esto significa que puede agregar sus propios estilos a un solo archivo style.scss , que puede aplicarse en toda la aplicación. Es muy conveniente.

▍ Interfaz de prueba


Creo que hay algunos problemas al probar nuestra solución. El hecho es que es muy simple probar servicios individuales, pero yew no proporciona al desarrollador una forma conveniente de probar componentes y agentes. Ahora, en el marco de Rust puro, la integración y las pruebas de extremo a extremo de la interfaz no están disponibles. Aquí podría usar proyectos como Cypress o Protractor , pero con este enfoque, tendría que incluir una gran cantidad de código JavaScript de plantilla / TypeScript en el proyecto, por lo que decidí abandonar la implementación de tales pruebas.

Por cierto, aquí hay una idea para un nuevo proyecto: un marco para pruebas de extremo a extremo escrito en Rust.

Desarrollo del lado del servidor de aplicaciones


Para implementar el lado del servidor de la aplicación, elegí el marco actix-web . Este es un marco de modelo de actor compacto, práctico y muy rápido basado en Rust. Es compatible con todas las tecnologías necesarias, como WebSockets, TLS y HTTP / 2.0 . Este marco admite varios manejadores y recursos, pero en nuestra aplicación solo se usaron un par de rutas principales:

  • /ws es el recurso principal para las comunicaciones WebSocket.
  • / - el controlador principal que da acceso a la aplicación frontal estática.

De forma predeterminada, actix-web inicia los flujos de trabajo en la cantidad correspondiente al número de núcleos de procesador disponibles en la computadora local. Esto significa que si la aplicación tiene un estado, deberá compartirse de manera segura entre todos los subprocesos, pero gracias a las confiables plantillas de cómputo paralelo Rust, esto no es un problema. Sea como fuere, el backend debería ser un sistema sin estado, ya que muchas copias de este se pueden implementar en paralelo en un entorno de nube (como Kubernetes ). Como resultado, los datos que forman el estado de la aplicación deben estar separados del backend. Por ejemplo, pueden estar dentro de una instancia separada de un contenedor Docker .


PostgreSQL DBMS y Proyecto Diesel

Como el almacén de datos principal, decidí usar el DBMS PostgreSQL . Por qué Esta elección determinó la existencia de un maravilloso proyecto Diesel que ya es compatible con PostgreSQL y ofrece un sistema ORM seguro y extensible y una herramienta de creación de consultas para él. Todo esto corresponde perfectamente a las necesidades de nuestro proyecto, ya que actix-web ya es compatible con Diesel. Como resultado, aquí, para realizar operaciones CRUD con información sobre sesiones en la base de datos, puede usar un lenguaje especial que tenga en cuenta los detalles de Rust. Aquí hay un ejemplo de controlador UpdateSession para actix-web basado en 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())   } } 

El proyecto r2d2 se utiliza para establecer una conexión entre actix-web y Diesel. Esto significa que tenemos (además de la aplicación con sus flujos de trabajo) el estado compartido de la aplicación, que admite muchas conexiones de bases de datos como un único grupo de conexiones. Esto simplifica enormemente la escala seria del backend, haciendo que esta solución sea flexible. Aquí puede encontrar el código responsable de crear la instancia del servidor.

▍ Prueba de backend


Las pruebas de integración de back-end en nuestro proyecto se realizan iniciando una instancia de servidor de prueba y conectándose a una base de datos que ya se está ejecutando. Luego puede usar el cliente estándar de WebSocket (usé tungstenite ) para enviar datos generados usando el protocolo Cap'n Proto al servidor y comparar los resultados con los esperados. Esta configuración de prueba ha demostrado ser excelente. No utilicé servidores de prueba especiales actix-web , ya que no se requiere mucho más trabajo para configurar y ejecutar un servidor real. Las pruebas de la unidad de back-end resultaron, como se esperaba, una tarea bastante simple; realizar tales pruebas no causa ningún problema.

Despliegue del proyecto


La aplicación es muy fácil de implementar utilizando la imagen Docker.


Docker

Con el make deploy puede crear una imagen llamada webapp que contenga ejecutables backend estáticamente vinculados, el archivo Config.toml actual, certificados TLS y contenido de interfaz estático. El ensamblaje de ejecutables totalmente vinculados estáticamente en Rust se implementa utilizando una versión modificada de la imagen Docker rust-musl-builder . Una aplicación web terminada se puede probar con el comando make run , que inicia un contenedor habilitado para la red. El contenedor PostgreSQL debe ejecutarse en paralelo con el contenedor de la aplicación para garantizar que el sistema funcione. En general, el proceso de implementación de nuestro sistema es bastante simple, además, gracias a las tecnologías utilizadas aquí, podemos hablar de su suficiente flexibilidad, simplificando su posible adaptación a las necesidades de una aplicación en desarrollo.

Tecnologías utilizadas en el desarrollo de proyectos.


Aquí está el diagrama de dependencia de la aplicación.


Tecnologías utilizadas para desarrollar una aplicación web en Rust

El único componente que utilizan el frontend y el backend es la versión Rust de Cap'n Proto, que requiere el compilador Cap'n Proto instalado localmente para crear.

Los resultados ¿Está listo el óxido para la producción web?


Esta es una gran pregunta. Esto es lo que puedo responder. Desde el punto de vista de los servidores, me inclino a responder "sí", ya que el ecosistema Rust, además de actix-web , tiene una pila HTTP muy madura y muchos marcos diferentes para el rápido desarrollo de servicios y API de servidores.

Si hablamos del front-end, entonces, gracias a la atención general a WebAssembly, se está trabajando mucho ahora. Sin embargo, los proyectos creados en esta área deben alcanzar la misma madurez que los proyectos de servidor. Esto es especialmente cierto para la estabilidad de API y las capacidades de prueba. Así que ahora digo "no" al uso de Rust en el extremo frontal, pero no puedo evitar notar que se está moviendo en la dirección correcta.

Estimados lectores! ¿Usas Rust en el desarrollo web?

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


All Articles