Rodando tu propio marco

Crear un marco desde cero no es algo que nos propongamos específicamente para hacer. Tendrías que estar loco, ¿verdad? Con la gran cantidad de marcos de JavaScript que hay por ahí, ¿qué posible motivación podríamos tener para rodar nuestra propia? 

Originalmente buscábamos un marco para construir el nuevo sistema de administración de contenido para el sitio web de The Daily Mail. El objetivo principal era hacer que el proceso de edición fuera mucho más interactivo, ya que todos los elementos de un artículo (imágenes, incrustaciones, cuadros de llamada, etc.) son arrastrables, modulares y autoadministrados..

Todos los marcos que podríamos poner en nuestras manos fueron diseñados para una UI más o menos estática definida por los desarrolladores. Necesitábamos hacer un artículo con texto editable y elementos UI renderizados dinámicamente..

La columna vertebral era un nivel demasiado bajo. Hizo poco más que proporcionar la estructura y los mensajes básicos de los objetos. Tendríamos que construir una gran cantidad de abstracción por encima de la base Backbone, por lo que decidimos que preferimos construir esta base nosotros mismos.

AngularJS se convirtió en nuestro marco preferido para crear aplicaciones de navegador de tamaño pequeño a mediano que tienen UI relativamente estáticas. Desafortunadamente, AngularJS es en gran medida una caja negra (no expone ninguna API conveniente para extender y manipular los objetos que crea con ella), directivas, controladores, servicios. Además, aunque AngularJS proporciona conexiones reactivas entre vistas y expresiones de alcance, no permite definir conexiones reactivas entre modelos, por lo que cualquier aplicación de tamaño mediano se vuelve muy similar a una aplicación jQuery con el espagueti de escuchas de eventos y devoluciones de llamada, con la única diferencia de que En lugar de escuchas de eventos, una aplicación angular tiene observadores y, en lugar de manipular DOM, manipula los ámbitos.

Lo que siempre quisimos era un marco que lo permitiera;

  • Desarrollo de aplicaciones de forma declarativa con enlaces reactivos de modelos a vistas.
  • Creación de enlaces de datos reactivos entre diferentes modelos en la aplicación para administrar la propagación de datos en un estilo declarativo en lugar de imperativo.
  • Insertar validadores y traductores en estos enlaces, por lo que podríamos enlazar vistas a modelos de datos en lugar de ver modelos como en AngularJS.
  • Control preciso sobre componentes vinculados a elementos DOM.
  • La flexibilidad de la gestión de vistas le permite manipular automáticamente los cambios de DOM y volver a renderizar algunas secciones utilizando cualquier motor de plantillas en los casos en que la representación es más eficiente que la manipulación de DOM.
  • Capacidad para crear dinámicamente UIs.
  • Poder conectarse a los mecanismos detrás de la reactividad de los datos y controlar con precisión las actualizaciones de la vista y el flujo de datos.
  • Ser capaz de ampliar la funcionalidad de los componentes suministrados por el marco y crear nuevos componentes..

No pudimos encontrar lo que necesitábamos en las soluciones existentes, por lo que comenzamos a desarrollar Milo en paralelo con la aplicación que lo usa..

Porque milo?

Milo fue elegido como nombre debido a Milo Minderbinder, un especulador de la guerra de 22 capturas por Joseph Heller. Habiendo comenzado desde la gestión de las operaciones de desorden, los expandió a una empresa comercial rentable que conectaba a todos con todo, y en eso Milo y todos los demás "tienen una parte".

Milo el marco tiene el módulo de enlace, que enlaza elementos DOM a componentes (a través de ml-bind atributo), y el módulo de atención que permite establecer conexiones reactivas en vivo entre diferentes fuentes de datos (el modelo y la faceta de datos de los componentes son dichas fuentes de datos).

Casualmente, Milo puede leerse como un acrónimo de MaIL Online, y sin el entorno de trabajo único en Mail Online, nunca hubiéramos podido construirlo.

Gestionando Vistas

Aglutinante

Las vistas en Milo son administradas por componentes, que son básicamente instancias de clases de JavaScript, responsables de administrar un elemento DOM. Muchos marcos usan componentes como un concepto para administrar elementos de UI, pero el más obvio que se me ocurre es Ext JS. Habíamos trabajado extensamente con Ext JS (la aplicación heredada que estábamos reemplazando se construyó con ella), y queríamos evitar lo que considerábamos dos inconvenientes de su enfoque..

La primera es que Ext JS no le facilita la administración de su marca. La única forma de crear una IU es juntar jerarquías anidadas de configuraciones de componentes. Esto lleva a un marcado renderizado innecesariamente complejo y toma el control de las manos del desarrollador. Necesitábamos un método para crear componentes en línea, en nuestro propio formato HTML hecho a mano. Aquí es donde entra la carpeta.

Binder escanea nuestro marcado buscando el ml-bind atributo para que pueda instanciar componentes y enlazarlos al elemento. El atributo contiene información sobre los componentes; esto puede incluir la clase de componente, facetas y debe incluir el nombre del componente.

Nuestro componente milo

Hablaremos de facetas en un minuto, pero por ahora veamos cómo podemos tomar este valor de atributo y extraer la configuración de él usando una expresión regular.

var bindAttrRegex = / ^ ([^ \: \ [\]] *) (?: \ [([^ \: \ [\]] *) \])? \ :? ([^:] *) $ / ; var result = value.match (bindAttrRegex); // el resultado es una matriz con // resultado [0] = 'ComponentClass [facet1, facet2]: componentName'; // resultado [1] = 'ComponentClass'; // resultado [2] = 'facet1, facet2'; // resultado [3] = 'componentName';

Con esa información, todo lo que tenemos que hacer es iterar sobre todos los ml-bind atributos, extraiga estos valores y cree instancias para administrar cada elemento.

var bindAttrRegex = / ^ ([^ \: \ [\]] *) (?: \ [([^ \: \ [\]] *) \])? \ :? ([^:] *) $ / ; cuaderno de funciones (devolución de llamada) var scope = ; // obtenemos todos los elementos con el atributo ml-bind var els = document.querySelectorAll ('[ml-bind]'); Array.prototype.forEach.call (els, function (el) var attrText = el.getAttribute ('ml-bind'); var result = attrText.match (bindAttrRegex); var className = resultado [1] || 'Componente '; var facets = result [2] .split (', '); var compName = results [3]; // suponiendo que tenemos un objeto de registro de todas nuestras clases var comp = new classRegistry [className] (el); comp .addFacets (facetas); comp.name = compName; scope [compName] = comp; // mantenemos una referencia al componente en el elemento el .___ milo_component = comp;); devolución de llamada (alcance);  carpeta (función (alcance) console.log (alcance););

Por lo tanto, con solo un poco de expresiones regulares y algo de DOM transversal, puede crear su propio mini-marco con una sintaxis personalizada para adaptarse a su contexto y lógica de negocios en particular. En muy poco código, hemos configurado una arquitectura que permite componentes modulares y de autogestión, que se pueden utilizar como desee. Podemos crear una sintaxis conveniente y declarativa para crear instancias y configurar componentes en nuestro HTML, pero a diferencia de angular, podemos administrar estos componentes como nos guste.

Diseño impulsado por la responsabilidad

La segunda cosa que no nos gustó de Ext JS fue que tiene una jerarquía de clases muy inclinada y rígida, lo que hubiera dificultado la organización de nuestras clases de componentes. Intentamos escribir una lista de todos los comportamientos que podría tener cualquier componente dado dentro de un artículo. Por ejemplo, un componente podría ser editable, podría estar escuchando eventos, podría ser un destino para soltar o arrastrarse. Estos son sólo algunos de los comportamientos necesarios. Una lista preliminar que escribimos tenía aproximadamente 15 tipos diferentes de funcionalidad que podrían requerirse de cualquier componente en particular.

Tratar de organizar estos comportamientos en algún tipo de estructura jerárquica hubiera sido no solo un gran dolor de cabeza, sino también muy limitante si alguna vez quisiéramos cambiar la funcionalidad de cualquier clase de componente dada (algo que terminamos haciendo mucho). Decidimos implementar un patrón de diseño orientado a objetos más flexible..

Habíamos estado leyendo sobre Diseño impulsado por responsabilidad, que, a diferencia del modelo más común de definición del comportamiento de una clase junto con los datos que posee, está más preocupado por las acciones de las que un objeto es responsable. Esto nos fue muy bien ya que estábamos tratando con un modelo de datos complejo e impredecible, y este enfoque nos permitiría dejar la implementación de estos detalles para más adelante.. 

La clave que le quitamos al RDD fue el concepto de Roles. Un rol es un conjunto de responsabilidades relacionadas. En el caso de nuestro proyecto, identificamos roles como editar, arrastrar, soltar zonas, seleccionables o eventos entre muchos otros. Pero, ¿cómo representan estos roles en el código? Para eso, tomamos prestado del patrón decorador..

El patrón decorador permite que el comportamiento se agregue a un objeto individual, ya sea de forma estática o dinámica, sin afectar el comportamiento de otros objetos de la misma clase. Ahora bien, aunque la manipulación en tiempo de ejecución del comportamiento de clase no ha sido particularmente necesaria en este proyecto, nos interesó mucho el tipo de encapsulación que proporciona esta idea. La implementación de Milo es un tipo de híbrido que incluye objetos llamados facetas, que se adjunta como propiedades a la instancia del componente. La faceta obtiene una referencia al componente, es 'propietario' y un objeto de configuración, que nos permite personalizar las facetas para cada clase de componente.. 

Puede pensar en las facetas como mixins avanzados y configurables que obtienen su propio espacio de nombres en su objeto propietario e incluso en su propio en eso Método, que debe ser sobrescrito por la subclase faceta.

función Facet (propietario, configuración) this.name = this.constructor.name.toLowerCase (); this.owner = propietario; this.config = config || ; this.init.apply (this, argumentos);  Facet.prototype.init = function Facet $ init () ;

Así podemos subclasificar este sencillo Faceta Clase y crear facetas específicas para cada tipo de comportamiento que queremos. Milo viene precargado con una variedad de facetas, tales como DOM faceta, que proporciona una colección de utilidades DOM que operan en el elemento del componente propietario, y la Lista y ít facetas, que trabajan juntas para crear listas de componentes que se repiten.

Estas facetas se unen por lo que llamamos un Objeto facetado, que es una clase abstracta de la que todos los componentes heredan. los Objeto facetado tiene un método de clase llamado createFacetedClass que simplemente se subclasifica, y adjunta todas las facetas a un facetas Propiedad en la clase. De esa manera, cuando el Objeto facetado se crea una instancia, tiene acceso a todas sus clases de facetas y puede iterarlas para arrancar el componente.

función FacetedObject (facetsOptions / *, otro init args * /) facetsOptions = facetsOptions? _.clone (facetsOptions): ; var thisClass = this.constructor, facets = ; if (! thisClass.prototype.facets) arroja un nuevo error ('No hay facetas definidas'); _.eachKey (this.facets, instantiateFacet, this, true); Object.defineProperties (esto, facetas); if (this.init) this.init.apply (this, argumentos); function instantiateFacet (facetClass, fct) var facetOpts = facetsOptions [fct]; eliminar facetsOptions [fct]; facets [fct] = enumerable: false, value: new facetClass (this, facetOpts);  FacetedObject.createFacetedClass = function (name, facetsClasses) var FacetedClass = _.createSubclass (this, name, true); _.extendProto (FacetedClass, facets: facetsClasses); volver FacetedClass; ;

En Milo, nos abstraemos un poco más al crear una base Componente clase con una coincidencia createComponentClass Método de clase, pero el principio básico es el mismo. Con los comportamientos clave administrados por facetas configurables, podemos crear muchas clases de componentes diferentes en un estilo declarativo sin tener que escribir demasiado código personalizado. Aquí hay un ejemplo que utiliza algunas de las facetas listas para usar que vienen con Milo..

var Panel = Component.createComponentClass ('Panel', dom: cls: 'my-panel', tagName: 'div', eventos: mensajes: 'clic': onPanelClick, arrastre: mensajes:  ..., soltar: mensajes: …, contenedor: indefinido);

Aquí hemos creado una clase de componente llamada Panel, que tiene acceso a los métodos de utilidad DOM, establecerá automáticamente su clase CSS en en eso, puede escuchar eventos DOM y configurará un controlador de clic en en eso, Puede ser arrastrado alrededor, y también actuar como un objetivo de caída. La última faceta allí., envase asegura que este componente configure su propio alcance y, en efecto, puede tener componentes secundarios.

Alcance

Habíamos discutido por un momento si todos los componentes adjuntos al documento debían formar una estructura plana o formar su propio árbol, donde los niños solo son accesibles desde sus padres.

Definitivamente, necesitaríamos ámbitos para algunas situaciones, pero se podría haber manejado a nivel de implementación, en lugar de a nivel de marco. Por ejemplo, tenemos grupos de imágenes que contienen imágenes. Habría sido sencillo para estos grupos mantener un registro de sus imágenes secundarias sin la necesidad de un alcance genérico..

Finalmente decidimos crear un árbol de alcance de componentes en el documento. Tener ámbitos hace que muchas cosas sean más fáciles y nos permite tener nombres más genéricos de componentes, pero obviamente tienen que ser gestionados. Si destruye un componente, debe eliminarlo de su ámbito principal. Si mueve un componente, debe eliminarse de uno y agregarse a otro.

El alcance es un hash especial, u objeto de mapa, con cada uno de los elementos secundarios contenidos en el alcance como propiedades del objeto. El alcance, en Milo, se encuentra en la faceta del contenedor, que a su vez tiene muy poca funcionalidad. El objeto de alcance, sin embargo, tiene una variedad de métodos para manipular e iterarse, pero para evitar conflictos de espacio de nombres, todos esos métodos se nombran con un subrayado al principio.

var scope = myComponent.container.scope; scope._each (function (childComp) // iterar cada componente secundario); // acceder a un componente específico en el ámbito var testComp = scope.testComp; // obtener el número total de componentes secundarios var total = scope._length (); // agregar un nuevo componente al alcance scope._add (newComp);

Mensajería - Sincrónica vs. Asíncrona

Queríamos tener un acoplamiento suelto entre los componentes, por lo que decidimos tener una funcionalidad de mensajería adjunta a todos los componentes y facetas.

La primera implementación del mensajero fue solo una colección de métodos que administraban matrices de suscriptores. Tanto los métodos como la matriz se mezclaron directamente en el objeto que implementó la mensajería.

Una versión simplificada de la primera implementación de messenger se parece a esto:

var messengerMixin = initMessenger: initMessenger, on: on, off: off, postMessage: postMessage; function initMessenger () this._subscribers = ;  función en (mensaje, suscriptor) var msgSubscribers = this._subscribers [message] = this._subscribers [message] || []; if (msgSubscribers.indexOf (suscriptor) == -1) msgSubscribers.push (suscriptor);  función desactivada (mensaje, suscriptor) var msgSubscribers = this._subscribers [mensaje]; if (msgSubscribers) si (suscriptor) _.spliceItem (msgSubscribers, suscriptor); de lo contrario, elimine this._subscribers [mensaje];  function postMessage (message, data) var msgSubscribers = this._subscribers [mensaje]; if (msgSubscribers) msgSubscribers.forEach (función (suscriptor) subscriber.call (this, message, data););  

Cualquier objeto que usó esta mezcla puede tener mensajes emitidos (por el propio objeto o por cualquier otro código) con mensaje posterior El método y las suscripciones a este código se pueden activar y desactivar con métodos que tengan los mismos nombres.

Hoy en día, los mensajeros han evolucionado sustancialmente para permitir: 

  • Adjuntar fuentes externas de mensajes (mensajes DOM, mensajes de ventana, cambios de datos, otro mensajero, etc.) - por ejemplo. Eventos facet lo usa para exponer eventos DOM a través de Milo Messenger. Esta funcionalidad se implementa a través de una clase separada. MessageSource y sus subclases.
  • Definición de API de mensajería personalizadas que traducen tanto mensajes como datos de mensajes externos a mensajes internos. P.ej. Datos facet lo usa para traducir cambios e ingresar eventos DOM a eventos de cambios de datos (ver Modelos a continuación). Esta funcionalidad se implementa a través de una clase separada MessengerAPI y sus subclases.
  • Suscripciones de patrones (utilizando expresiones regulares). P.ej. los modelos (ver más abajo) usan suscripciones de patrones internamente para permitir suscripciones de cambio profundo de modelos.
  • Definir cualquier contexto (el valor de esto en el suscriptor) como parte de la suscripción con esta sintaxis:
component.on ('stateready', suscriptor: func, context: context);
  • Creando suscripción que solo se despacha una vez con el una vez método
  • Pasando callback como un tercer parámetro en mensaje posterior (consideramos el número variable de argumentos en mensaje posterior, pero queríamos una API de mensajería más consistente que la que tendríamos con argumentos variables)
  • etc.

El principal error de diseño que cometimos al desarrollar Messenger fue que todos los mensajes se enviaban de forma sincrónica. Dado que JavaScript es de un solo hilo, largas secuencias de mensajes con operaciones complejas que se llevan a cabo bloquearían fácilmente la interfaz de usuario. Cambiar Milo para hacer el envío de mensajes asíncrono fue fácil (todos los suscriptores son llamados en sus propios bloques de ejecución usando setTimeout (suscriptor, 0), cambiar el resto del marco y la aplicación fue más difícil; mientras que la mayoría de los mensajes se pueden enviar de forma asíncrona, hay muchos que aún deben enviarse de forma sincrónica (muchos eventos DOM que tienen datos en ellos o lugares donde prevenirDefault se llama). De forma predeterminada, los mensajes ahora se envían de forma asíncrona, y hay una manera de hacerlos sincrónicos cuando se envía el mensaje:

component.postMessageSync ('mymessage', data);

o cuando se crea la suscripción:

component.onSync ('mymessage', function (msg, data) //…); 

Otra decisión de diseño que tomamos fue la forma en que expusimos los métodos de messenger en los objetos que los usan. Originalmente, los métodos se mezclaban simplemente con el objeto, pero no nos gustaba que todos los métodos estuvieran expuestos y no pudiéramos tener mensajeros independientes. Así que los mensajeros se volvieron a implementar como una clase separada basada en una clase abstracta Mixin. 

La clase Mixin permite exponer los métodos de una clase en un objeto host de tal manera que cuando se llaman métodos, el contexto aún será Mixin en lugar del objeto host.

Resultó ser un mecanismo muy conveniente: podemos tener control total sobre los métodos expuestos y cambiar los nombres según sea necesario. También nos permitió tener dos mensajeros en un objeto, que se usa para modelos.

En general, Milo Messenger resultó ser un software muy sólido que se puede utilizar solo, tanto en el navegador como en Node.js. Se ha endurecido por el uso en nuestro sistema de gestión de contenido de producción que tiene decenas de miles de líneas de código.

La próxima vez

En el siguiente artículo, veremos posiblemente la parte más útil y compleja de Milo. Los modelos Milo no solo permiten un acceso seguro y profundo a las propiedades, sino también la suscripción de eventos a cambios en cualquier nivel. 

También exploraremos nuestra implementación de minder, y cómo utilizamos los objetos de conector para realizar un enlace de una o dos vías de las fuentes de datos..

Tenga en cuenta que este artículo fue escrito por Jason Green y Evgeny Poberezkin.