Extensiones Extensibles en JavaScript

Hola Habr!

Llamamos su atención a la tan esperada copia adicional del libro " JavaScript expresivo ", que acaba de llegar de la imprenta.


Para aquellos que no están familiarizados con el trabajo del autor del libro (por toda la naturaleza enciclopédica, a los principiantes también les gustará), le sugerimos que se familiarice con el artículo de su blog; El artículo describe las ideas sobre la organización de extensiones en JavaScript.

Hoy en día, se ha puesto de moda estructurar sistemas grandes en forma de muchos paquetes separados. La idea principal que subyace a este enfoque es que es mejor no limitar a las personas a una función específica (propuesta por usted) mediante la implementación de una función, sino proporcionar esta función como un paquete separado que una persona puede descargar junto con el paquete básico del sistema.
Para hacer esto, en términos generales, necesitará ...

  • La capacidad de no descargar funciones que no necesita es especialmente útil en los sistemas cliente.
  • La capacidad de reemplazar con otra implementación una pieza de funcionalidad que no cumple con sus objetivos. Por lo tanto, la carga en los módulos del núcleo también se reduce: no es necesario cubrir todos los casos prácticos posibles con su ayuda.
  • Comprobación de las interfaces del kernel en condiciones reales: al implementar características básicas en la parte superior de la interfaz orientada hacia el lado del cliente, se ve obligado a hacer que esta interfaz sea al menos tan potente que pueda admitir estas características. Por lo tanto, puede estar seguro de que será posible construir sobre él cosas similares escritas por desarrolladores externos.
  • Aislamiento entre partes del sistema. Los participantes del proyecto pueden simplemente buscar el paquete que les interesa. Los paquetes pueden versionarse, marcarse como no deseados o reemplazarse sin afectar el código central.

Este enfoque generalmente se asocia con una mayor complejidad. Para facilitar el inicio de los usuarios, puede proporcionarles un paquete envoltorio en el que "todo está incluido", pero tarde o temprano probablemente tendrán que deshacerse de este shell e instalar y configurar paquetes auxiliares específicos, que a veces es más difícil que simplemente cambiar a Otra característica de la biblioteca monolítica.

En este artículo, trataremos de analizar las opciones para los mecanismos de expansión que admiten "extensibilidad a gran escala" y le permiten crear nuevos puntos de extensión donde esto no se proporcionó.

Extensibilidad


¿Qué queremos de un sistema extensible? En primer lugar, por supuesto, debería tener la capacidad de expandir sus propias capacidades utilizando código externo.

Pero esto no es suficiente. Déjame dudar sobre un estúpido problema que una vez encontré. Estoy desarrollando un editor de código. En una de las versiones anteriores de este editor, puede establecer el estilo para una línea específica en el código del cliente. Fue genial - diseño de línea selectiva.

Excepto en el caso de que los intentos de cambiar la apariencia de la línea se realicen inmediatamente desde dos secciones del código, y estos intentos comienzan a fallar. La segunda extensión aplicada a la línea sobrescribe el estilo de la primera extensión. O, cuando el primer código intenta eliminar el diseño que hizo en algún lugar de la parte posterior del código, sobrescribe el estilo introducido por el segundo fragmento de código.

Logramos encontrar una solución, brindando la capacidad de agregar (y eliminar) la extensión, y no configurarla, para que las dos extensiones pudieran interactuar con la misma línea sin sabotear el trabajo del otro.

En un sentido más general, es necesario garantizar que las extensiones se puedan combinar, incluso si no son conscientes de la existencia de las demás, sin causar conflictos entre ellas.

Para hacer esto, debe asegurarse de que cualquier número de actores pueda afectar cada punto de expansión. Cómo se procesarán exactamente los efectos múltiples dependerá de la situación. Aquí hay algunos enfoques que pueden resultarle útiles:

  • Todos ellos surten efecto. Por ejemplo, al agregar una clase CSS a un elemento o al mostrar un widget, ambas características se agregan al mismo tiempo. A menudo, todavía tendrán que clasificarse de alguna manera: los widgets deben mostrarse en una secuencia predecible y bien definida.
  • Se alinean en forma de transportador. Un ejemplo es un controlador que puede filtrar los cambios que se agregan a un documento antes de que se realicen. Cada cambio se alimenta primero a un controlador, que, a su vez, puede cambiarlo adicionalmente. Ordenar en este caso no es crítico, pero puede marcar la diferencia.
  • Puede aplicar un enfoque de orden de llegada a los controladores de eventos. Cada controlador tiene la oportunidad de servir el evento hasta que uno de ellos diga que ya se ha ocupado de él, después de lo cual los controladores que están en la cola ya no son interrogados.
  • También sucede que realmente necesita elegir un valor específico; por ejemplo, determinar el valor de un parámetro de configuración específico. Puede ser aconsejable utilizar un determinado operador (digamos, lógico y, lógico o, mínimo o máximo) para limitar el número de valores de entrada para una posición. Por ejemplo, un editor puede cambiar al modo de solo lectura si alguna extensión lo ordena. Puede establecer el valor máximo del documento o el número mínimo de valores que se informan a esta opción.

En muchos de estos casos, el orden es importante. Esto significa que la prioridad de los efectos aplicados debe ser controlable y predecible.

Es en este frente que los sistemas de extensión imperativos basados ​​en el uso de efectos secundarios generalmente no pueden hacer frente. Por ejemplo, la operación addEventListener realizada por el modelo DOM del navegador hace que los controladores de eventos se invoquen exactamente en el orden en que se registraron. Esto es normal si todas las llamadas están controladas por un solo sistema, o si el orden de las operaciones no es realmente importante, sin embargo, cuando tiene que lidiar con muchos fragmentos de software que agregan controladores de forma independiente, puede ser muy difícil predecir cuál de ellos se llamará en primer lugar.

Enfoque simple


Para darle un ejemplo simple: primero apliqué una estrategia modular a ProseMirror, un sistema para editar texto enriquecido. El núcleo de este sistema en sí mismo es, en principio, inútil: se basa completamente en paquetes adicionales que describen la estructura de los documentos, las claves vinculantes y el historial de cancelaciones. Aunque es realmente un poco difícil usar este sistema, se ha adoptado en productos que requieren un diseño de texto personalizado, que no está disponible en editores clásicos.

El mecanismo de extensión utilizado en ProseMirror es relativamente sencillo. Al crear el editor, el código del cliente indica una sola matriz de objetos conectados. Cada uno de estos complementos puede afectar de alguna manera el trabajo del editor, por ejemplo, agregar datos de estado o manejar eventos de interfaz.

Todos estos aspectos están diseñados para trabajar con una variedad de valores de configuración utilizando las estrategias descritas en la sección anterior. Por ejemplo, al especificar asignaciones de teclas múltiples, el orden en que se especifican las instancias de complementos de mapas de teclas determina su prioridad. El primer mapa de teclas que sabe cómo manejarlo recibe una clave específica en el procesamiento.

Por lo general, este mecanismo es bastante poderoso y se usa activamente. Sin embargo, en cierta etapa, se vuelve complicado y se vuelve incómodo trabajar con él.

  • Si el complemento tiene muchos efectos, puede esperar que en ese orden se apliquen a otros complementos, o tendrá que dividirlos en complementos más pequeños para poder organizarlos correctamente.
  • En general, la organización de complementos se vuelve muy sensible, ya que el usuario final no siempre comprende qué complementos pueden afectar el funcionamiento de otros complementos si obtienen una mayor prioridad. Todos los errores generalmente aparecen solo en tiempo de ejecución, cuando se utiliza una funcionalidad específica; por lo tanto, son fáciles de omitir.
  • Los complementos basados ​​en otros complementos deberían documentar este hecho, y es de esperar que los usuarios no olviden habilitar sus dependencias (en el orden correcto).

CodeMirror en la versión 6 es un editor reescrito del mismo nombre . En la sexta versión, trato de desarrollar un enfoque modular. Esto requiere un sistema de extensión más expresivo. Veamos algunos de los desafíos asociados con el diseño de dicho sistema.

Ordenar


Es fácil diseñar un sistema que proporcione un control completo sobre el orden de las extensiones. Pero es muy difícil diseñar un sistema de este tipo, que al mismo tiempo será agradable de usar y le permitirá combinar el código de extensiones independientes sin una intervención manual exhaustiva y exhaustiva.

Cuando se trata de ordenar, tira para aplicar valores de prioridad. Un ejemplo similar es la propiedad CSS z-index , que le permite establecer un número que indica qué tan profundo será el elemento en la pila.

Dado que las hojas de estilo a veces tienen valores de z-index ridículamente grandes, es obvio que esta forma de indicar la prioridad es problemática. Un módulo particular individualmente "no sabe" qué valores de prioridad indican otros módulos. Las opciones son solo puntos en un rango de números indefinido. Puede especificar valores prohibitivamente altos (o valores profundamente negativos), con la esperanza de llegar al final de esta escala, pero todo lo demás es un juego de adivinanzas.

Esta situación puede mejorarse de alguna manera definiendo un conjunto limitado de categorías de prioridad claramente definidas para que las extensiones puedan clasificarse por el "nivel" aproximado de su prioridad. Pero aún tiene que romper de alguna manera los lazos dentro de estas categorías.

Agrupación y deduplicación


Como mencioné anteriormente, una vez que comience a confiar seriamente en las extensiones, pueden surgir situaciones en las que algunas extensiones usen otras. La gestión de dependencia mutua no se escala bien, por lo que sería bueno si pudieras extraer un grupo de extensiones a la vez.

Sin embargo, no solo eso, en este caso, el problema del pedido se agravará aún más; surgirá otro problema. Muchas otras extensiones pueden depender de una extensión particular a la vez, y si las representa como valores, entonces puede surgir la situación con descargas múltiples de la misma extensión. En algunos casos, por ejemplo, al asignar claves o manejar controladores de eventos, esto es normal. En otros, por ejemplo, al rastrear el historial de cancelaciones o al trabajar con una biblioteca de información sobre herramientas, este enfoque sería un desperdicio de recursos con el riesgo de romper algo.

Por lo tanto, al permitir la composición de extensiones, nos vemos obligados a pasar a la parte del sistema de extensión de la complejidad asociada con la gestión de dependencias. Debe poder reconocer aquellas extensiones que no deben duplicarse y descargar solo una instancia de cada una de ellas.

Sin embargo, dado que en la mayoría de los casos las extensiones se pueden configurar, y todas las instancias de una extensión en particular serán algo diferentes entre sí, no podemos simplemente tomar una instancia de la extensión y usarla; tendremos que combinarlas de alguna manera significativa (o informar un error, cuando esto no es posible).

Proyecto


Aquí describiré lo que se ha hecho en CodeMirror 6. Propongo este ejemplo como una solución, y no como la única solución verdadera. Es posible que este sistema se desarrolle aún más a medida que la biblioteca se estabilice.

La primitiva principal en este enfoque se llama comportamiento. Los comportamientos son solo aquellas cosas que puede expandir indicando valores. Como ejemplo, considere el comportamiento de un campo de estado, donde con la ayuda de extensiones puede agregar nuevos campos, dando una descripción de cada campo. O el comportamiento de un controlador de eventos basado en el navegador, donde puede agregar sus propios controladores utilizando extensiones.

Desde el punto de vista del comportamiento del consumidor, esos comportamientos que se configuran en una instancia particular del editor dan una secuencia ordenada de valores, donde los valores con mayor prioridad son lo primero. Cada comportamiento tiene un tipo y sus valores deben coincidir con ese tipo.

Un comportamiento se representa como un valor utilizado tanto para declarar una instancia de un comportamiento como para referirse a los valores que el comportamiento puede tener. Por ejemplo, una extensión que define el fondo de un número de línea puede definir un comportamiento que permite que otro código agregue nuevos marcadores a este fondo.

Una extensión es un valor que se puede usar al configurar el editor. Se informa una serie de extensiones durante la inicialización. Cada extensión está permitida en cero o más comportamientos.

El tipo más simple de extensión es una instancia de comportamiento. Al establecer un valor para este comportamiento, obtenemos en respuesta el valor de la extensión que implementa este comportamiento.
Una secuencia de extensiones también se puede agrupar en una sola extensión. Por ejemplo, en la configuración del editor para un lenguaje de programación dado, se pueden extraer otras extensiones, en particular, una gramática para analizar y resaltar el idioma, información sobre cómo sangrar y también una fuente de información sobre la finalización que complementa de manera inteligente el código en ese idioma. Entonces obtienes una extensión de idioma, que simplemente recopila todas las extensiones necesarias que dan el valor acumulativo.

Al describir una versión simple de dicho sistema, podríamos detenernos en esto y simplemente ajustar las extensiones anidadas en una sola matriz de extensiones para comportamientos. Luego podrían agruparse por tipo de comportamiento y obtener secuencias ordenadas de valores de comportamiento.

Sin embargo, todavía no hemos descubierto la deduplicación, y necesitamos un control más completo sobre los pedidos.

Los valores del tercer tipo incluyen extensiones únicas ; este es el mecanismo para garantizar la deduplicación. Las extensiones que no desea instanciar dos veces en el mismo editor son solo eso. Para determinar dicha extensión, se especifica un tipo de especificación, es decir, el tipo de valor de configuración esperado por el constructor de la extensión, y una función de instanciación que toma una matriz de dichos valores de especificación y devuelve la extensión.
Las extensiones únicas complican el proceso de resolver una colección de extensiones en un conjunto de comportamientos. Siempre que haya extensiones únicas en el conjunto alineado de extensiones, el mecanismo de resolución debe seleccionar el tipo de extensión única, recopilar todas sus instancias, llamar a la función de instanciación con sus valores de especificación y reemplazarlos con el resultado (en una instancia).

(Hay un problema más: deben resolverse en un orden determinado. Si primero habilita la extensión X única, pero luego la extensión Y se resuelve en otra X, esto será un error, ya que todas las instancias de X deben combinarse juntas. Dado que crear instancias de las extensiones es una operación pura, el sistema, frente a él, lo ejecuta por prueba y error, reinicia el proceso y registra la información aclarada).
Finalmente, hablemos de prioridad. El enfoque básico en este caso es mantener el orden en que se informaron las extensiones. Las extensiones compuestas se alinean y se integran en este orden exactamente en la posición donde se encuentran por primera vez. El resultado de resolver una extensión única también se inserta en el lugar donde se produce por primera vez.

Pero las extensiones pueden asignar algunas de sus subextensiones a una categoría con una prioridad diferente. El sistema determina los tipos de tales categorías: revertir (surte efecto después de que sucedan otras cosas), por defecto, expandir (una prioridad más alta que el volumen) y redefinir (quizás debería ubicarse en la parte superior). El pedido real se realiza primero por categoría y luego por posición de inicio.

Por lo tanto, una extensión con una asignación de teclas de baja prioridad y un controlador de eventos con una prioridad normal nos puede dar una extensión compuesta construida sobre la base de una extensión con una asignación de teclas (en este caso, no es necesario saber qué comportamientos están incluidos en ella, con la prioridad "retroceso" más una instancia de comportamiento controlador de eventos

El principal logro parece ser que hemos adquirido la capacidad de combinar extensiones, independientemente de lo que se haga dentro de cada una de ellas. En las extensiones que hemos modelado hasta ahora, entre las cuales: dos sistemas de análisis con el mismo comportamiento sintáctico, resaltado de sintaxis, servicio de sangría inteligente, historial de cancelación, fondo de número de línea, cierre automático de paréntesis, asignación de teclas y selección múltiple: todo funciona bien.

Para utilizar dicho sistema, realmente debe dominar varios conceptos nuevos, y definitivamente es más complicado que los sistemas imperativos tradicionales aceptados en la comunidad JavaScript (llame a un método para agregar / eliminar un efecto). Sin embargo, la capacidad de vincular correctamente las extensiones parece justificar estos costos.

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


All Articles