Programación reactiva

En la primera parte de la serie, hablamos sobre los componentes que le permiten administrar diferentes comportamientos mediante facetas y cómo Milo administra la mensajería..

En este artículo, veremos otro problema común en el desarrollo de aplicaciones de navegador: la conexión de modelos a vistas. Desentrañaremos parte de la "magia" que hace posible el enlace de datos bidireccional en Milo, y para terminar, crearemos una aplicación de Tareas totalmente funcional en menos de 50 líneas de código..

Modelos (o Eval no es malo)

Hay varios mitos sobre JavaScript. Muchos desarrolladores creen que eval es malvado y nunca debe usarse. Esa creencia hace que muchos desarrolladores no puedan decir cuándo se puede y se debe usar eval.

Mantras como "evaluar es malvado ”solo puede ser perjudicial cuando estamos tratando con algo que es esencialmente una herramienta. Una herramienta solo es "buena" o "mala" cuando se le da un contexto. No dirías que un martillo es malo, ¿verdad? Realmente depende de cómo lo uses. Cuando se usa con un clavo y algunos muebles, "el martillo es bueno". Cuando se usa para untar con mantequilla su pan, "el martillo es malo".

Si bien definitivamente estamos de acuerdo en que evaluar tiene sus limitaciones (por ejemplo, rendimiento) y riesgos (especialmente si evaluamos el código ingresado por el usuario), existen bastantes situaciones en las que evaluar es la única forma de lograr la funcionalidad deseada.

Por ejemplo, muchos motores de plantillas utilizan evaluar dentro del alcance de con el operador (otro gran no-no entre los desarrolladores) para compilar plantillas para funciones de JavaScript.

Cuando pensábamos en lo que queríamos de nuestros modelos, consideramos varios enfoques. Una era tener modelos poco profundos como Backbone con los mensajes emitidos en los cambios de modelo. Aunque son fáciles de implementar, estos modelos tendrían una utilidad limitada: la mayoría de los modelos de la vida real son profundos.

Consideramos utilizar objetos JavaScript simples con el Objeto.observar API (que eliminaría la necesidad de implementar cualquier modelo). Mientras que nuestra aplicación solo necesitaba trabajar con Chrome, Objeto.observar solo recientemente se habilitó de forma predeterminada; anteriormente se requería activar el indicador de Chrome, lo que habría dificultado tanto la implementación como el soporte.

Queríamos modelos que pudiéramos conectar a las vistas, pero de tal manera que pudiéramos cambiar la estructura de la vista sin cambiar una sola línea de código, sin cambiar la estructura del modelo y sin tener que administrar explícitamente la conversión del modelo de la vista al modelo de datos.

También queríamos poder conectar modelos entre sí (ver programación reactiva) y suscribirse a los cambios de modelo. Las máquinas de implementos angulares comparan los estados de los modelos y esto se vuelve muy ineficiente con los modelos grandes y profundos..

Después de un poco de discusión, decidimos que implementaríamos nuestra clase de modelo que admitiría una simple API get / set para manipularlos y que permitiría suscribirse a cambios dentro de ellos:

var m = nuevo modelo; m ('. info.name'). set ('angular'); console.log (m ('. info'). get ()); // logs: nombre: 'angular' m.on ('. info.name', onNameChange); function onNameChange (msg, data) console.log ('Nombre cambiado de', data.oldValue, 'to', data.newValue);  m ('. info.name'). set ('milo'); // logs: nombre cambiado de angular a milo console.log (m.get ()); // logs: info: name: 'milo' console.log (m ('. info'). get ()); // logs: nombre: 'milo'

Esta API es similar al acceso normal a las propiedades y debería proporcionar un acceso profundo y seguro a las propiedades, cuando obtener se llama en las rutas de propiedad no existentes que devuelve indefinido, y cuando conjunto se llama, crea el árbol de matriz / objeto faltante según sea necesario.

Esta API se creó antes de que se implementara y la principal incógnita que enfrentamos fue cómo crear objetos que también eran funciones que se pueden llamar. Resulta que para crear un constructor que devuelva objetos a los que se pueda llamar, debe devolver esta función desde el constructor y configurar su prototipo para que sea una instancia del Modelo clase al mismo tiempo:

function Model (data) // modelPath debe devolver un objeto ModelPath // con métodos para obtener / configurar las propiedades del modelo, // para suscribirse a cambios de propiedad, etc. var model = function modelPath (ruta) devolver nuevo ModelPath (modelo, camino);  model .__ proto__ = Model.prototype; model._data = datos; model._messenger = nuevo Messenger (model, Messenger.defaultMethods); modelo de retorno  Modelo.prototipo .__ proto__ = Modelo .__ proto__;

Mientras que la __proto__ La propiedad del objeto generalmente es mejor evitarla, ya que es la única forma de cambiar el prototipo de la instancia del objeto y el prototipo constructor..

La instancia de ModelPath que debe devolverse cuando se llama el modelo (por ejemplo,. m ('. info.name') arriba) presentó otro desafío de implementación. ModelPath las instancias deben tener métodos que establezcan correctamente las propiedades de los modelos pasados ​​al modelo cuando se llamó (.info.name en este caso). Consideramos su implementación simplemente analizando las propiedades pasadas como cadenas cada vez que se accede a esas propiedades, pero nos dimos cuenta de que habría resultado en un rendimiento ineficiente..

En cambio, decidimos implementarlos de tal manera que m ('. info.name'), por ejemplo, devuelve un objeto (una instancia de ModelPath "Clase") que tiene todos los métodos de acceso (obtener, conjunto, del y empalme) sintetizado como código JavaScript y convertido a funciones JavaScript utilizando evaluar.

También hicimos todos estos métodos sintetizados en caché, así que una vez que usamos cualquier modelo .info.name todos los métodos de acceso para esta "ruta de propiedad" se almacenan en caché y se pueden reutilizar para cualquier otro modelo.

La primera implementación del método get se veía así:

función synthesizeGetter (ruta, parsedPath) var getter; var getterCode = 'getter = function value ()' + '\ n var m =' + modelAccessPrefix + '; \ n return'; var modelDataProperty = 'm'; para (var i = 0, count = parsedPath.length-1; i < count; i++)  modelDataProperty += parsedPath[i].property; getterCode += modelDataProperty + ' && ';  getterCode += modelDataProperty + parsedPath[count].property + ';\n ;'; try  eval(getterCode);  catch (e)  throw ModelError('ModelPath getter error; path: ' + path + ', code: ' + getterCode);  return getter; 

Pero el conjunto El método se veía mucho peor y era muy difícil de seguir, leer y mantener, porque el código del método creado estaba fuertemente entremezclado con el código que generaba el método. Debido a eso, cambiamos a usar el motor de plantillas doT para generar el código para los métodos de acceso.

Este fue el getter después de cambiar a usar plantillas:

var dotDef = modelAccessPrefix: 'this._model._data',; var getterTemplate = 'method = function value () \ var m = # def.modelAccessPrefix; \ var modelDataProperty = "m";  \ return \ for (var i = 0, count = it.parsedPath.length-1; \ i < count; i++)  \ modelDataProperty+=it.parsedPath[i].property; \  =modelDataProperty &&  \  \  =modelDataProperty=it.parsedPath[count].property; \ '; var getterSynthesizer = dot.compile(getterTemplate, dotDef); function synthesizeMethod(synthesizer, path, parsedPath)  var method , methodCode = synthesizer( parsedPath: parsedPath ); try  eval(methodCode);  catch (e)  throw Error('ModelPath method compilation error; path: ' + path + ', code: ' + methodCode);  return method;  function synthesizeGetter(path, parsedPath)  return synthesizeMethod(getterSynthesizer, path, parsedPath); 

Esto resultó ser un buen enfoque. Nos permitió hacer el código para todos los métodos de acceso que tenemos (obtener, conjunto, del y empalme) muy modular y mantenible.

El modelo de API que desarrollamos demostró ser bastante útil y eficaz. Evolucionó para soportar la sintaxis de elementos de matriz., empalme método para matrices (y métodos derivados, tales como empujar, popular, etc.), y la interpolación de acceso de propiedad / elemento.

Este último se introdujo para evitar la síntesis de métodos de acceso (que es una operación mucho más lenta que el acceso a una propiedad o elemento) cuando lo único que cambia es alguna propiedad o índice de elementos. Ocurriría si los elementos de la matriz dentro del modelo tienen que actualizarse en el bucle.

Considera este ejemplo:

para (var i = 0; i < 100; i++)  var mPath = m('.list[' + i + '].name'); var name = mPath.get(); mPath.set(capitalize(name)); 

En cada iteración, una ModelPath La instancia se crea para acceder y actualizar la propiedad de nombre del elemento de matriz en el modelo. Todas las instancias tienen diferentes rutas de propiedad y requerirá la síntesis de cuatro métodos de acceso para cada uno de los 100 elementos usando evaluar. Será una operación considerablemente lenta..

Con la interpolación de acceso a la propiedad, la segunda línea de este ejemplo se puede cambiar a:

var mPath = m ('. list [$ 1] .name', i);

No solo parece más legible, sino que es mucho más rápido. Mientras aún creamos 100 ModelPath en este bucle, todos compartirán los mismos métodos de acceso, por lo que en lugar de 400 sintetizamos solo cuatro métodos.

Le invitamos a estimar la diferencia de rendimiento entre estas muestras.

Programación reactiva

Milo ha implementado programación reactiva utilizando modelos observables que emiten notificaciones sobre sí mismos cada vez que cambia alguna de sus propiedades. Esto nos ha permitido implementar conexiones de datos reactivas utilizando la siguiente API:

conector var = cuidador (m1, '<<<->>> ', m2 ('. info ')); // crea una conexión reactiva bidireccional // entre el modelo m1 y la propiedad ".info" del modelo m2 // con la profundidad de 2 (propiedades y subpropiedades // de los modelos están conectados).

Como se puede ver desde la línea superior., ModelPath devuelto por m2 ('. info') debe tener la misma API que el modelo, lo que significa que tiene la misma API de mensajería que el modelo y también es una función:

var mPath = m ('. info); mPath ('. name'). set ("); // establece poperty '.info.name' en m mPath.on ('. name', onNameChange); // igual que m ('. info.name') .on (", onNameChange) // igual que m.on ('. info.name', onNameChange);

De manera similar, podemos conectar modelos a vistas. Los componentes (consulte la primera parte de la serie) pueden tener una faceta de datos que sirve como API para manipular DOM como si fuera un modelo. Tiene la misma API que el modelo y se puede usar en conexiones reactivas.

Entonces, este código, por ejemplo, conecta una vista DOM a un modelo:

conector var = cuidador (m, '<<<->>> ', comp.data);

Se demostrará con más detalle a continuación en la aplicación de ejemplo To-Do..

¿Cómo funciona este conector? Bajo el capó, el conector simplemente se suscribe a los cambios en las fuentes de datos en ambos lados de la conexión y pasa los cambios recibidos de una fuente de datos a otra fuente de datos. Un origen de datos puede ser un modelo, una ruta de acceso de modelo, una faceta de datos del componente o cualquier otro objeto que implemente la misma API de mensajería que el modelo..

La primera implementación del conector fue bastante simple:

// ds1 y ds2: fuentes de datos conectadas // el modo define la dirección y la profundidad de la función de conexión Conector (ds1, mode, ds2) var parsedMode = mode.match (/ ^ (\<*)\-+(\>PS _.extender (esto, ds1: ds1, ds2: ds2, modo: modo, depth1: parsedMode [1] .length, depth2: parsedMode [2] .length, isOn: false); Esto en();  _.extendProto (Connector, on: on, off: off); funciona en () var SubscriptionPath = this._subscriptionPath = new Array (this.depth1 || this.depth2) .join ('*'); var self = esto; if (this.depth1) linkDataSource ('_ link1', '_link2', this.ds1, this.ds2, membershipPath); if (this.depth2) linkDataSource ('_ link2', '_link1', this.ds2, this.ds1, membershipPath); this.isOn = true; function linkDataSource (linkName, stopLink, linkToDS, linkedDS, membershipPath) var onData = function onData (ruta, datos) // evita un bucle de mensajes sin fin // para conexiones bidireccionales si (onData .__ stopLink) vuelve; var dsPath = linkToDS.path (ruta); if (dsPath) self [stopLink] .__ stopLink = true; dsPath.set (data.newValue); borrar self [stopLink] .__ stopLink; linkedDS.on (subscribibath, onData); self [linkName] = onData; return onData;  función off () var self = this; unlinkDataSource (this.ds1, '_link2'); unlinkDataSource (this.ds2, '_link1'); this.isOn = false; la función unlinkDataSource (linkedDS, linkName) if (self [linkName]) linkedDS.off (self._subscriptionPath, self [linkName]); borrar self [linkName]; 

Hasta ahora, las conexiones reactivas en milo han evolucionado sustancialmente: pueden cambiar las estructuras de datos, cambiar los datos en sí y también realizar validaciones de datos. Esto nos ha permitido crear un generador de UI / formulario muy potente que también planeamos hacer de código abierto.

Construyendo una aplicación de tareas

Muchos de ustedes estarán al tanto del proyecto TodoMVC: una colección de implementaciones de aplicaciones To-Do realizadas con una variedad de diferentes marcos MV *. La aplicación To-Do es una prueba perfecta de cualquier marco, ya que es bastante simple de construir y comparar, pero requiere una amplia gama de funcionalidades que incluyen operaciones CRUD (crear, leer, actualizar y eliminar), interacción DOM, y visualización / modelo vinculante sólo para nombrar unos pocos.

En varias etapas del desarrollo de Milo, intentamos crear aplicaciones sencillas de tareas pendientes, y sin fallar, resaltó fallas o fallas en el marco. Incluso en lo más profundo de nuestro proyecto principal, cuando Milo estaba siendo usado para soportar una aplicación mucho más compleja, hemos encontrado pequeños errores de esta manera. Por ahora, el marco de trabajo cubre la mayoría de las áreas requeridas para el desarrollo de aplicaciones web y encontramos que el código requerido para construir la aplicación To-Do es bastante sucinto y declarativo..

En primer lugar, tenemos el marcado HTML. Es un texto estándar en HTML con un poco de estilo para administrar los elementos marcados. En el cuerpo tenemos una ml-bind atributo para declarar la lista de tareas, y esto es solo un componente simple con el lista faceta añadida. Si quisiéramos tener varias listas, probablemente deberíamos definir una clase de componente para esta lista.

Dentro de la lista se encuentra nuestro artículo de muestra, que se ha declarado utilizando una costumbre Que hacer clase. Si bien no es necesario declarar una clase, hace que el manejo de los hijos del componente sea mucho más simple y modular..

            

Para hacer

Modelo

Para que podamos correr milo.binder () Ahora, primero tendremos que definir el Que hacer clase. Esta clase necesitará tener el ít faceta, y básicamente será responsable de administrar el botón de eliminar y la casilla de verificación que se encuentra en cada Que hacer.

Antes de que un componente pueda operar en sus elementos secundarios, primero debe esperar a que para niños evento para ser despedido en él. Para obtener más información sobre el ciclo de vida del componente, consulte la documentación (enlace a la documentación del componente).

// Crear una nueva clase de componente facetada con la faceta 'item'. // Esto usualmente sería definido en su propio archivo. // Nota: La faceta del elemento 'requerirá' en // las facetas 'contenedor', 'datos' y 'dom' var Todo = _.createSubclass (milo.Component, 'Todo'); milo.registry.components.add (Todo); // Agregar nuestro propio método de inicio personalizado _.extendProto (Todo, init: Todo $ init); function Todo $ init () // Llamando al método init heredado. milo.Component.prototype.init.apply (esto, argumentos); // La escucha de 'childrenbound' que se activa después de la carpeta // ha terminado con todos los hijos de este componente. this.on ('childrenbound', function () // Obtenemos el alcance (los componentes secundarios viven aquí) var scope = this.container.scope; // Y configuramos dos suscripciones, una para los datos de la casilla de verificación // La sintaxis de suscripción permite que se pase el contexto scope.checked.data.on (", suscriptor: checkTodo, context: this); // y uno para el evento de 'clic' del botón de eliminar. Scope.deleteBtn.events.on ('click', suscriptor: removeTodo, context: this);); // Cuando cambie la casilla de verificación, estableceremos la clase de la función de verificación CheckTodo (ruta, datos) this.el.classList.toggle ('todo-item-checked', data.newValue); // Para eliminar el elemento, usamos el método 'removeItem' de la función de faceta 'item' removeTodo (eventType, event) this.item.removeItem () ;

Ahora que tenemos esa configuración, podemos llamar a la carpeta para adjuntar componentes a elementos DOM, crear un nuevo modelo con conexión bidireccional a la lista a través de su faceta de datos.

// Función Milo ready, funciona como la función jQuery ready. milo (function () // Call binder en el documento. // Adjunta componentes a elementos DOM con el atributo ml-bind var scope = milo.binder (); // Obtenga acceso a nuestros componentes a través del objeto de alcance var todos = scope.todos // Todos list, newTodo = scope.newTodo // New input input, addBtn = scope.addBtn // Botón Agregar, modelView = scope.modelView; // Donde imprimimos el modelo // Configurar nuestro modelo, esto lo hará mantenga la matriz de todos var m = new milo.Model; // Esta suscripción nos mostrará el contenido del // modelo en todo momento debajo de todos m.on (/.*/, función showModel (msg, datos)  modelView.data.set (JSON.stringify (m.get ())); // Cree un enlace bidireccional profundo entre nuestro modelo y la faceta de datos de la lista de todos. // Los chevrones más internos muestran la dirección de conexión (puede también puede ser de una manera), // el resto define la profundidad de conexión - 2 niveles en este caso, para incluir // las propiedades de los elementos de la matriz. milo.minder (m, '<<<->>> ', todos.data); // Suscripción para hacer clic en el evento del botón agregar addBtn.events.on ('click', addTodo); // Haga clic en el controlador de la función de agregar botón addTodo () // Empaquetamos la entrada 'newTodo' como un objeto // La propiedad 'texto' corresponde a la marca del elemento. var itemData = text: newTodo.data.get (); // Insertamos esos datos en el modelo. // ¡La vista se actualizará automáticamente! m.push (itemData); // Y finalmente pon la entrada en blanco otra vez. newTodo.data.set (");); 

Esta muestra está disponible en jsfiddle..

Conclusión

La muestra de tareas es muy simple y muestra una parte muy pequeña del increíble poder de Milo. Milo tiene muchas características que no están cubiertas en este y en los artículos anteriores, como arrastrar y soltar, almacenamiento local, utilidades http y websockets, utilidades DOM avanzadas, etc..

Hoy en día, milo impulsa el nuevo CMS de dailymail.co.uk (este CMS tiene decenas de miles de códigos front-end de javascript y se utiliza para crear más de 500 artículos todos los días).

Milo es de código abierto y todavía está en una fase beta, por lo que es un buen momento para experimentar con él y tal vez incluso contribuir. Nos encantaría recibir tus comentarios.


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