Chorda Tratando de hacerlo declarativamente

Cada vez que necesito sentarme para crear una nueva aplicación, caigo en un estupor fácil. Mi cabeza está dando vueltas por la necesidad de elegir qué biblioteca o marco tomar esta vez. La última vez que escribí en la biblioteca X, pero ahora el marco Y ha crecido y se ha entregado, y todavía hay un UI Kit Z genial, y queda mucho trabajo de proyectos anteriores.


Desde algún punto me di cuenta de que el marco realmente no importa: lo que necesito, lo puedo hacer en cualquiera de ellos. Aquí parece que deberías ser feliz, toma algo con el máximo de estrellas en el github y cálmate. Pero de todos modos, un deseo irresistible surge constantemente de hacer algo propio, su propia bicicleta. Pues bien. Algunos pensamientos generales sobre este tema y un marco llamado Chorda te están esperando bajo el corte.


De hecho, el problema no es que la decisión de otra persona sea mala o ineficaz. No El problema es que la decisión de otra persona nos hace pensar de una manera que puede no ser conveniente para nosotros. Pero espera ¿Qué significa "conveniente-inconveniente" y cómo puede esto afectar el desarrollo? Recuerde que, de hecho, existe una DX: un conjunto de prácticas personales establecidas y generalmente aceptadas. Desde aquí podemos decir que es conveniente para nosotros cuando nuestro propio DX coincide con el DX del autor de la biblioteca o marco. Y en el caso de que diverjan, surge la incomodidad, la irritación y la búsqueda de algo nuevo.


Un poco de historia


Cuando desarrolla una IU para una aplicación empresarial, se enfrenta a una gran cantidad de formularios de usuario. Y un día se me ocurre un pensamiento brillante: ¿por qué creo un formulario web cada vez que simplemente puedo enumerar los campos en JSON y alimentar la estructura resultante al generador? Y, aunque este enfoque no funciona demasiado bien en el mundo de la empresa sangrienta (por qué, esta es una conversación separada), pero la idea de pasar de un estilo imperativo a uno declarativo generalmente no es mala. Prueba de ello es la gran cantidad de generadores de formularios web, páginas e incluso sitios completos que se pueden encontrar fácilmente en la Web.


Entonces, en algún momento, no era ajeno al deseo de mejorar mi código debido a la transición a la declaración. Pero tan pronto como necesitábamos no solo elementos html estándar, sino componentes de widgets complejos e interactivos, no podíamos escaparnos con un generador simple. Los requisitos de reutilización de código, integrabilidad, extensibilidad, etc., se agregaron rápidamente a esto. El desarrollo de su propia biblioteca de componentes con una API declarativa no se hizo esperar.


Pero aquí, la felicidad no sucedió. Probablemente la mejor situación reflejará la opinión de mi colega, que debía usar la biblioteca creada. Miró los ejemplos, la documentación y dijo: "La biblioteca es genial. Hermosa, dinámica. Pero, ¿cómo hago ahora una aplicación con todo esto?" Y tenía razón. Resultó que hacer un componente no es lo mismo que combinar varios componentes y hacer que funcionen sin problemas.


Ha pasado mucho tiempo desde entonces. Y cuando una vez más fui visitado por el deseo de reunir pensamientos e ideas, decidí hacer un poco diferente y no ir de abajo hacia arriba, sino de arriba hacia abajo.


Gestión de aplicaciones == Gestión de estado


Estoy más acostumbrado a considerar la aplicación como una máquina de estados finitos con algún conjunto de estados clonados. Y el trabajo de la aplicación como un conjunto de transiciones de un estado a otro, en el que cambiar el modelo conduce a la creación de una nueva versión de la vista. En el futuro , llamaré a algunos datos fijos (un objeto, una matriz, un tipo primitivo, etc.) relacionados con su única representación: un documento .


Hay un problema obvio: para muchos valores del modelo, es necesario describir muchas opciones para el documento. Aquí se usan dos enfoques:


  1. Plantillas Utilizamos nuestro lenguaje de marcado favorito y lo complementamos con directivas de ramificación y bucle.
  2. Las funciones Describimos en nuestras funciones nuestras ramas y bucles en nuestro lenguaje de programación favorito.

Como regla, ambos enfoques se declaran declarativos. El primero se considera declarativo, porque se basa, aunque ligeramente ampliado, en las reglas del lenguaje de marcado. El segundo, porque se centra en la composición de funciones, algunas de las cuales actúan como reglas. Lo que es digno de mención, ahora no existe un límite claro entre plantillas y funciones.


Por un lado, me gustan las plantillas, pero por otro, quería usar de alguna manera las características de JavaScript. Por ejemplo, algo como esto:


createFromConfig({ data: { name: 'Alice' }, tag: 'div', class: 'clickable box', onClick: function () { alert('Click') } }) 

El resultado es una configuración JS que describe un estado específico completo. Para describir los muchos estados, será necesario lograr la extensibilidad de esta configuración. ¿Y cuál es la forma más conveniente de hacer que un conjunto de opciones sea extensible? No inventaremos nada aquí: las opciones de sobrecarga han existido durante mucho tiempo. Se puede ver cómo funciona en el ejemplo de Vue con su API de opciones. Pero, a diferencia del mismo Vue, me preguntaba si el estado completo, incluidos los datos y el documento, podría describirse de la misma manera.


Estructura de aplicación y declarativa.


El término "componente" se ha vuelto demasiado vago, especialmente después de la aparición del llamado Componentes funcionales. A medida que avanzamos en la estructura de la aplicación, llamaré al componente un elemento estructural .

Muy rápidamente, llegué a la conclusión de que el elemento estructural (componente) no es un elemento de documento, sino una entidad que:


  1. combina datos y documentos (enlace y eventos)
  2. conectado con otras entidades similares (estructura de árbol)

Como señalé anteriormente, si percibes la aplicación como un conjunto de estados, entonces para estos estados debes tener un método de descripción. Además, es necesario encontrar dicho método para que no contenga operadores imperativos "espurios". Estamos hablando de esos elementos muy auxiliares que se introducen en las plantillas: #if , #elsif , v-for , etc. Creo que muchas personas ya conocen la solución: es necesario transferir la lógica al modelo, dejando a nivel de presentación una API que le permite controlar elementos estructurales a través de tipos de datos simples.


Por gerencia, entiendo la presencia de variabilidad y ciclicidad.


Variabilidad (si no)


Veamos cómo puede controlar las opciones de visualización utilizando el ejemplo de un componente de tarjeta en Chorda:


 const isHeaderOnly = true const card = new Html({ $header: { /*  */ }, $footer: { /*  */ }, components: {header: true, footer: !isHeaderOnly} //    }) 

Al establecer el valor de la opción de componentes , puede controlar los componentes que se muestran. Y al vincular componentes con almacenamiento reactivo, obtenemos que nuestra estructura pasará a la gestión de datos. Hay una advertencia: el objeto se usa como valor y las claves no están ordenadas, lo que impone algunas restricciones a los componentes .


Ciclo (para)


Trabajar con datos cuya cantidad solo se conoce en tiempo de ejecución requerirá iteración sobre las listas.


 const drinks = ['Coffee', 'Tea', 'Milk'] const html = new Html({ html: 'ul', css: 'list', defaultItem: { html: 'li', css: 'list-item' }, items: drinks }) 

El valor de la opción de elementos es Array, respectivamente, obtenemos un conjunto ordenado de componentes. La vinculación de elementos al almacenamiento, como en el caso de los componentes, transferirá el control a los datos.


Los elementos estructurales están conectados entre sí en una jerarquía de árbol. Si combinamos los ejemplos anteriores, para mostrar la lista en el cuerpo de la tarjeta obtenemos lo siguiente:


 //   const state = { struct: { header: true, footer: false, }, drinks: ['Coffee', 'Tea', 'Milk'] } //  const card = new Html({ $header: { /*  */ }, $content: { html: 'ul', css: 'list', defaultItem: { html: 'li', css: 'list-item' }, items: state.drinks }, $footer: { /*  */ }, components: state.struct }) 

Aproximadamente de esta manera, se crea la estructura de datos de la aplicación. Es suficiente tener dos tipos de generadores, basados ​​en Object y en Array. Solo queda entender cómo se produce la transformación de elementos estructurales en un documento.


Cuando todo ya está inventado para nosotros


En general, estoy a favor del hecho de que el sistema de representación de documentos debe implementarse en el nivel del navegador (aunque al menos el mismo VDOM). Y nuestra tarea solo será conectarlo cuidadosamente al árbol de componentes. Después de todo, no importa cuánto aumente la velocidad de la biblioteca, el navegador la tiene de todos modos.


Honestamente intenté hacer que mi renderizado funcione en algún momento, pero después de un tiempo me di por vencido, porque no puedo dibujar más rápido que VanillaJS (¡tristemente!). Ahora está de moda usar VDOM para renderizar, y sus implementaciones son, tal vez, incluso abundantes. Entonces, más una implementación más del árbol virtual, decidí no agregarlo a la alcancía del github: solo el siguiente marco es suficiente.


Inicialmente, se creó un adaptador para la biblioteca Maquette en Chorda para renderizar, pero tan pronto como comenzaron a aparecer las tareas "del mundo real", resultó que era más práctico tener un cajón en React. En este caso, por ejemplo, simplemente puede usar React DevTools existente y no escribir el suyo.


Para conectar VDOM con elementos estructurales, necesita algo así como el diseño . Se le puede llamar una función de documento de un elemento estructural. Lo importante es una función pura.


Considere un ejemplo con una tarjeta que tiene un encabezado, cuerpo y sótano. Ya se ha mencionado que los componentes no están ordenados, es decir Si comenzamos a encender / apagar los componentes durante la operación, aparecerán cada vez en un nuevo orden. Veamos cómo esto se resuelve con el diseño:


 function orderedByKeyLayout (h, type, props, components) { return h(type, props, components.sort((a, b) => a.key - b.key).map(c => c.render())) } const html = new Html({ $header: {}, $content: {}, $footer: {}, layout: orderedByKeyLayout //     }) 

El diseño le permite configurar el llamado El elemento host con el que está asociado el componente y sus elementos secundarios ( elementos y componentes ). Por lo general, un diseño estándar también es suficiente, pero en algunos casos el diseño requiere la presencia de elementos de envoltura (por ejemplo, para cuadrículas) o la asignación de clases especiales, que no queremos llevar al nivel de componentes.


Una pizca de reactividad


Después de declarar y dibujar la estructura de los componentes, obtenemos un estado correspondiente a un conjunto de datos específico. A continuación, debemos describir los muchos conjuntos de datos y la reacción a su cambio.


Cuando trabajaba con datos, no me gustaban dos cosas:


  • Inmunidad Una buena cosa para realizar un seguimiento de los cambios es el versionado para los pobres, que funciona muy bien en objetos primitivos y planos. Pero tan pronto como la estructura se vuelve más compleja y aumenta el número de inversiones, se hace difícil mantener la inmunidad de un objeto complejo.
  • Sustitución. Si coloco algún objeto en el almacén de datos, cuando lo solicite, puedo devolver una copia u otro objeto o proxy en general que tenga similitud estructural con él.

Quería tener un repositorio que se comportara como inmutable, pero dentro de él contiene datos mutables, que también mantienen la persistencia del enlace. En el caso ideal, se vería así: creo un repositorio, le escribo un objeto vacío, empiezo a ingresar datos desde el formulario de solicitud y, después de hacer clic en el botón Enviar, obtengo el mismo objeto (¡enlazo el mismo!) Con las propiedades rellenas. Yo llamo a este caso ideal, ya que a menudo no sucede que el modelo de almacenamiento coincida con el modelo de presentación.


Otra tarea que debe resolverse es entregar datos desde el almacenamiento a los elementos estructurales. Nuevamente, no inventaremos nada y usaremos el enfoque de conectarnos a un contexto común. En el caso de Chorda, no tenemos acceso al contexto en sí, sino solo a su visualización, llamada alcance . Además, el alcance del componente es el contexto de sus componentes secundarios. Este enfoque le permite limitar, expandir o reemplazar datos relacionados en cualquier nivel de nuestra aplicación, y estos cambios serán aislados.


Un ejemplo de cómo se distribuyen los datos contextuales en un árbol de componentes:


 const html = new Html({ //     scope: { drink: 'Coffee' }, $component1: { scope: { cups: 2 }, $content: { $myDrink: { //      ,    drinkChanged: function (v) { //    drink   text this.opt('text', v) } }, $numCups: { cupsChanged: function (v) { this.opt('text', v + ' cups') } } } }, $component2: { scope: { drink: 'Tea' //      drink }, drinkChanged: function (v) { //    drink   text this.opt('text', v) } } }) //    // <div> // <div> // <div> // <div>Coffee</div> // <div>2 cups</div> // </div> // </div> // <div>Tea</div> // </div> 

El momento más difícil de entender es que cada componente tiene su propio contexto, y no el declarado en la parte superior de la estructura, como solemos hacer cuando trabajamos con plantillas.


¿Qué pasa con la sobrecarga de opciones?


Seguramente se enfrenta a una situación en la que hay un componente grande y es necesario cambiar un pequeño componente anidado en algún lugar en el interior. Dicen que la granulación y la composición deberían ayudar aquí. Y también, que los componentes y la arquitectura deben diseñarse de inmediato. La situación se vuelve muy triste si el componente grande no es suyo, sino que es parte de una biblioteca desarrollada por otro equipo o incluso una comunidad independiente. ¿Qué pasaría si pudieran realizar fácilmente cambios en el componente base, incluso si no se planificaron originalmente?


Por lo general, los componentes en las bibliotecas se diseñan como clases, luego se pueden usar como base para crear nuevos componentes. Pero aquí está oculta una pequeña característica que nunca me gustó: a veces creamos una clase solo para aplicarla en un solo lugar. Esto es raro Por ejemplo, estoy acostumbrado a usar clases para escribir, construir relaciones entre grupos de objetos y no usarlas para resolver el problema de descomposición.


Veamos cómo funcionan las clases con la configuración en Chorda.


 //      class Card extends Html { config () { return { css: 'box', $header: {}, $content: {}, $footer: {} } } } const html = new Html({ css: 'panel', $card: { as: Card, $header: { //       title $title: { css: 'title', text: 'Card title' } } } }) 

Esta opción me gusta más que crear una clase especial de TitledCard que se usará solo una vez. Y si necesita formar parte de las opciones, puede utilizar el mecanismo de impurezas. Bueno, nadie canceló Object.assign.


En Chorda, una clase es esencialmente un contenedor para la configuración y desempeña el papel de un tipo especial de impureza.


¿Por qué otro marco?


Repito que, en mi opinión, el marco trata más sobre la forma de pensar y la experiencia que sobre la tecnología. Mis hábitos y DX pidieron declaratividad en JS, que no pude encontrar en otras soluciones. Pero la implementación de una característica sacó otras nuevas, y después de un tiempo simplemente dejaron de encajar en el marco de una biblioteca especializada.


Por el momento, Chorda está en desarrollo activo. Las direcciones principales ya son visibles, pero los detalles cambian constantemente.


Gracias por leer hasta el final. Estaría encantado de comentarios.


Donde puedo ver


La documentación


Fuentes de GitHub


Ejemplos de CodePen

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


All Articles