Dans la première partie de la série, nous avons parlé de composants permettant de gérer différents comportements à l'aide de facettes et de la façon dont Milo gère la messagerie..
Dans cet article, nous examinerons un autre problème courant lié au développement d’applications de navigateur: la connexion de modèles à des vues. Nous allons démêler une partie de la «magie» qui rend possible la liaison de données bidirectionnelle dans Milo et, pour conclure, nous allons construire une application de tâches entièrement fonctionnelle en moins de 50 lignes de code..
Il existe plusieurs mythes sur JavaScript. Beaucoup de développeurs croient que eval est diabolique et ne devrait jamais être utilisé. Cette conviction empêche de nombreux développeurs d’indiquer quand eval peut et doit être utilisé.
Les mantras aimenteval
est mal »ne peut être dommageable que lorsque nous traitons avec quelque chose qui est essentiellement un outil. Un outil n’est «bon» ou «mauvais» que dans un contexte. Vous ne diriez pas qu'un marteau est diabolique, non? Cela dépend vraiment de la façon dont vous l'utilisez. Utilisé avec un clou et des meubles, «marteau, c'est bon». Quand on a l'habitude de beurrer votre pain, "le marteau est mauvais".
Bien que nous soyons absolument d’accord que eval
a ses limites (performances, par exemple) et ses risques (en particulier si nous évaluons le code entré par l'utilisateur), il existe de nombreuses situations dans lesquelles eval est le seul moyen d'obtenir la fonctionnalité souhaitée..
Par exemple, de nombreux moteurs de templates utilisent eval
dans le cadre de l'opérateur with (autre gros non-non parmi les développeurs) pour compiler des modèles avec des fonctions JavaScript.
Lorsque nous avons réfléchi à ce que nous voulions de nos modèles, nous avons envisagé plusieurs approches. L'une consistait à avoir des modèles peu profonds comme le fait Backbone avec les messages émis lors des modifications de modèle. Bien que faciles à mettre en œuvre, ces modèles auraient une utilité limitée - la plupart des modèles réels sont profonds.
Nous avons envisagé d’utiliser des objets JavaScript simples avec le Objet.observer
API (ce qui éliminerait la nécessité de mettre en œuvre des modèles). Alors que notre application devait uniquement fonctionner avec Chrome, Objet.observer
récemment activé par défaut - auparavant, il fallait activer l'indicateur Chrome, ce qui aurait rendu le déploiement et la prise en charge difficiles.
Nous voulions des modèles que nous puissions connecter à des vues, mais de manière à pouvoir changer de structure de vue sans changer une seule ligne de code, sans changer la structure du modèle et sans avoir à gérer explicitement la conversion du modèle de vue en modèle de données.
Nous voulions également pouvoir connecter des modèles les uns aux autres (voir Programmation réactive) et souscrire aux modifications de modèle. Angular implémente les montres en comparant les états des modèles, ce qui devient très inefficace avec les grands modèles profonds.
Après quelques discussions, nous avons décidé d'implémenter notre classe de modèle qui prendrait en charge une simple API get / set pour les manipuler et qui permettrait de souscrire aux modifications en leur sein:
var m = nouveau modèle; m ('. info.name'). set ('angular'); console.log (m ('. info'). get ()); // logs: name: 'angular' m.on ('. info.name', onNameChange); function onNameChange (msg, data) console.log ('Nom changé de', data.oldValue, 'à', data.newValue); m ('. info.name'). set ('milo'); // logs: nom changé d'angulaire en milo console.log (m.get ()); // journaux: info: name: 'milo' console.log (m ('. info'). get ()); // logs: name: 'milo'
Cette API ressemble à l’accès normal aux propriétés et devrait fournir un accès sécurisé en profondeur aux propriétés - lorsque obtenir
est appelé sur les chemins de propriété inexistants, il retourne indéfini
, et quand ensemble
est appelé, cela crée un objet manquant / un tableau comme requis.
Cette API a été créée avant sa mise en œuvre et la principale inconnue à laquelle nous avons été confrontés était de savoir comment créer des objets qui étaient également des fonctions appelables. Il s'avère que pour créer un constructeur qui renvoie des objets pouvant être appelés, vous devez renvoyer cette fonction depuis le constructeur et définir son prototype pour en faire une instance du Modèle
classe en même temps:
function Model (data) // modelPath doit renvoyer un objet ModelPath // avec des méthodes pour obtenir / définir les propriétés du modèle, // pour s'abonner aux modifications de propriété, etc. var model = function modelPath (path) retourne new ModelPath (model, chemin); model .__ proto__ = Model.prototype; model._data = data; model._messenger = new Messenger (model, Messenger.defaultMethods); modèle de retour; Model.prototype .__ proto__ = Modèle .__ proto__;
Tandis que le __proto__
Il est généralement préférable d’éviter les propriétés de l’objet, c’est toujours le seul moyen de changer le prototype de l’instance de l’objet et celui du constructeur..
L'instance de ModelPath
qui devrait être retourné quand le modèle est appelé (par exemple. m ('. info.name')
ci-dessus) a présenté un autre défi de mise en œuvre. ModelPath
les instances doivent avoir des méthodes qui définissent correctement les propriétés des modèles passés à model quand il a été appelé (.info.name
dans ce cas). Nous avons envisagé de les implémenter en analysant simplement les propriétés transmises en tant que chaînes à chaque accès à ces propriétés, mais nous avons réalisé que cela aurait entraîné des performances inefficaces..
Au lieu de cela, nous avons décidé de les mettre en œuvre de telle sorte que m ('. info.name')
, par exemple, retourne un objet (une instance de ModelPath
"Classe") qui a toutes les méthodes d'accès (obtenir
, ensemble
, del
et épissure
) synthétisé sous forme de code JavaScript et converti en fonctions JavaScript à l'aide de eval
.
Nous avons également mis en cache toutes ces méthodes synthétisées, une fois que tout modèle utilisé .info.name
toutes les méthodes d'accès pour ce «chemin d'accès» sont mises en cache et peuvent être réutilisées pour tout autre modèle.
La première implémentation de la méthode get ressemblait à ceci:
fonction synthesizeGetter (path, parsedPath) var getter; var getterCode = 'getter = valeur de la fonction ()' + '\ n var m =' + modelAccessPrefix + '; \ n return'; var modelDataProperty = 'm'; pour (var i = 0, compte = chemin parsed.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;
Mais le ensemble
La méthode semblait bien pire et était très difficile à suivre, à lire et à maintenir, car le code de la méthode créée était fortement entrecoupé du code qui l'avait générée. Pour cette raison, nous avons opté pour le moteur de modélisation DoT pour générer le code des méthodes d'accès..
C'était le getter après le passage à l'utilisation de modèles:
var dotDef = modelAccessPrefix: 'this._model._data',; var getterTemplate = 'méthode = valeur de la fonction () \ var m = # def.modelAccessPrefix; \ var modelDataProperty = "m"; \ return \ for (var i = 0, nombre = 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);
Cela s'est avéré être une bonne approche. Cela nous a permis de créer le code pour toutes les méthodes d’accès que nous avons (obtenir
, ensemble
, del
et épissure
) très modulaire et maintenable.
Le modèle d’API que nous avons développé s’est avéré tout à fait utilisable et performant. Il a évolué pour prendre en charge la syntaxe des éléments de tableau, épissure
méthode pour les tableaux (et les méthodes dérivées, telles que pousser
, pop
, etc.), et interpolation d'accès propriété / objet.
Ce dernier a été introduit pour éviter de synthétiser les méthodes d'accès (opération beaucoup plus lente que l'accès à une propriété ou à un élément) lorsque la seule chose qui change est une propriété ou un index d'élément. Cela arriverait si les éléments du tableau à l'intérieur du modèle devaient être mis à jour dans la boucle.
Considérons cet exemple:
pour (var i = 0; i < 100; i++) var mPath = m('.list[' + i + '].name'); var name = mPath.get(); mPath.set(capitalize(name));
A chaque itération, un ModelPath
instance est créée pour accéder à la propriété name de l’élément de tableau du modèle et la mettre à jour. Toutes les instances ont des chemins de propriété différents et il faudra synthétiser quatre méthodes d’accesseur pour chacun des 100 éléments à l’aide de eval
. Ce sera une opération considérablement lente.
Avec l’interpolation d’accès aux propriétés, la deuxième ligne de cet exemple peut être remplacée par:
var mPath = m ('. list [$ 1] .name', i);
Non seulement cela a l'air plus lisible, mais c'est beaucoup plus rapide. Alors que nous créons encore 100 ModelPath
les instances de cette boucle, elles partageront toutes les mêmes méthodes d’accesseur. Ainsi, au lieu de 400, nous ne synthétisons que quatre méthodes..
Vous êtes invités à estimer la différence de performance entre ces échantillons.
Milo a mis en œuvre une programmation réactive utilisant des modèles observables qui émettent des notifications lorsque leurs propriétés changent. Cela nous a permis d'implémenter des connexions de données réactives à l'aide de l'API suivante:
var connecteur = gardien (m1, '<<<->>> ', m2 ('. info ')); // crée une connexion réactive bidirectionnelle // entre le modèle m1 et la propriété «.info» du modèle m2 // avec une profondeur de 2 (les propriétés et les sous-propriétés // des modèles sont connectées).
Comme vous pouvez le voir ci-dessus, ModelPath
retourné par m2 ('. info')
devrait avoir la même API que le modèle, ce qui signifie qu'elle possède la même API de messagerie que le modèle et constitue également une fonction:
var mPath = m ('. info); mPath ('.nom'). set ("); // définit la propriété '.info.name' dans m mPath.on ('.nom', onNameChange); // identique à m ('.nom.name') .on (", onNameChange) // identique à m.on ('. info.name', onNameChange);
De la même manière, nous pouvons connecter des modèles à des vues. Les composants (voir la première partie de la série) peuvent avoir une facette de données qui sert d'API pour manipuler DOM comme s'il s'agissait d'un modèle. Il a la même API que le modèle et peut être utilisé dans des connexions réactives.
Donc, ce code, par exemple, connecte une vue DOM à un modèle:
var connecteur = gardien (m, '<<<->>> ', comp.data);
Cela sera démontré plus en détail ci-dessous dans l'exemple d'application To-Do.
Comment fonctionne ce connecteur? Sous le capot, le connecteur souscrit simplement aux modifications apportées aux sources de données des deux côtés de la connexion et transmet les modifications reçues d'une source de données à une autre. Une source de données peut être un modèle, un chemin de modèle, une facette de données du composant ou tout autre objet implémentant la même API de messagerie que le modèle..
La première implémentation de connecteur était assez simple:
// ds1 et ds2 - sources de données connectées // mode définit la direction et la profondeur de la fonction de connexion Connector (ds1, mode, ds2) var parsedMode = mode.match (/ ^ (\<*)\-+(\>*) $ /); _.extend (this, ds1: ds1, ds2: ds2, mode: mode, profondeur1: parsedMode [1] .length, profondeur2: parsedMode [2] .length, isOn: false); this.on (); _.extendProto (Connector, on: on, on: off: off); function on () var subscriptionPath = this._subscriptionPath = new Array (this.depth1 || this.depth2) .join ('*'); var self = this; if (this.depth1) linkDataSource ('_ link1', '_link2', this.ds1, this.ds2, subscriptionPath); if (this.depth2) linkDataSource ('_ link2', '_link1', this.ds2, this.ds1, subscriptionPath); this.isOn = true; function linkDataSource (linkName, stopLink, linkToDS, linkedDS, subscriptionPath) var onData = function onData (chemin, données) // empêche la boucle de messages sans fin // pour les connexions bidirectionnelles if (onData .__ stopLink) return; var dsPath = linkToDS.path (chemin); if (dsPath) self [stopLink] .__ stopLink = true; dsPath.set (data.newValue); supprimer soi-même [stopLink] .__ stopLink; linkedDS.on (subscriptionPath, onData); self [linkName] = onData; renvoyer onData; function off () var self = this; unlinkDataSource (this.ds1, '_link2'); unlinkDataSource (this.ds2, '_link1'); this.isOn = false; fonction unlinkDataSource (linkedDS, LinkName) if (self [nom de lien]) linkedDS.off (self._subscriptionPath, self [nom de lien]); supprimer soi-même [nom du lien];
À ce jour, les connexions réactives dans milo ont considérablement évolué - elles peuvent modifier les structures de données, modifier les données elles-mêmes et également effectuer des validations de données. Cela nous a permis de créer un générateur d’interface utilisateur / formulaire très puissant que nous prévoyons de créer en open-source..
Beaucoup d'entre vous connaissent le projet TodoMVC: une collection d'implémentations d'applications To-Do réalisées à l'aide de divers cadres MV *. L'application To-Do est un test parfait de tout framework car il est assez simple à construire et à comparer, mais nécessite une gamme assez large de fonctionnalités comprenant des opérations CRUD (créer, lire, mettre à jour et supprimer), des interactions DOM et des vues / modèles contraignant pour n'en nommer que quelques-uns.
À différentes étapes du développement de Milo, nous avons essayé de créer des applications simples à faire. À tout le moins, le logiciel a mis en évidence des bugs ou des défauts du framework. Même au plus profond de notre projet principal, lorsque Milo était utilisé pour supporter une application beaucoup plus complexe, nous avons trouvé de petits bugs de cette façon. A présent, le cadre couvre la plupart des domaines nécessaires au développement d'applications Web et nous trouvons que le code nécessaire à la création de l'application To-Do est assez succinct et déclaratif..
Tout d'abord, nous avons le balisage HTML. C'est un standard standard HTML avec un peu de style pour gérer les éléments cochés. Dans le corps nous avons un ml-bind
attribut pour déclarer la liste des tâches à effectuer, et il s’agit d’un simple composant avec le liste
facette ajoutée. Si nous voulions avoir plusieurs listes, nous devrions probablement définir une classe de composants pour cette liste.
Dans la liste se trouve notre exemple d’article, qui a été déclaré à l’aide d’une commande personnalisée. Faire
classe. Bien que la déclaration d'une classe ne soit pas nécessaire, la gestion des enfants du composant est beaucoup plus simple et modulaire..
À faire
Pour que nous puissions courir milo.binder ()
maintenant, nous devons d'abord définir le Faire
classe. Cette classe devra avoir le article
et sera essentiellement responsable de la gestion du bouton de suppression et de la case à cocher qui se trouve sur chaque Faire
.
Avant qu'un composant puisse opérer ses enfants, il doit d’abord attendre le les enfants
événement à être tiré sur elle. Pour plus d'informations sur le cycle de vie des composants, consultez la documentation (lien vers la documentation du composant)..
// Création d'une nouvelle classe de composants à facettes avec la facette 'item'. // Ceci serait généralement défini dans son propre fichier. // Remarque: la facette de l'élément "requiert" dans // les facettes "conteneur", "données" et "dom" var Todo = _.createSubclass (milo.Component, 'Todo'); milo.registry.components.add (Todo); // Ajout de notre propre méthode d'initialisation personnalisée _.extendProto (Todo, init: Todo $ init); function Todo $ init () // Appel de la méthode init héritée. milo.Component.prototype.init.apply (this, arguments); // Ecoute de 'childrenbound' qui est déclenché après la fin du classeur // avec tous les enfants de ce composant. this.on ('childrenbound', function () // Nous obtenons la portée (les composants enfants vivent ici) var scope = this.container.scope; // Et configurons deux abonnements, l'un aux données de la case à cocher // La syntaxe de la souscription permet de transmettre le contexte scope.checked.data.on (", subscriber: checkTodo, context: this); // et l’événement" clic "du bouton de suppression. Scope.deleteBtn.events.on ('click', subscriber: removeTodo, context: this);); // Lorsque la case à cocher change, nous allons définir la classe de la fonction Todo en conséquence checkTodo (chemin, données) this.el.classList.toggle ('todo-item-vérifié', data.newValue); // Pour supprimer l'élément, nous utilisons la méthode 'removeItem' de la fonction de facette 'item' removeTodo (eventType, event) this.item.removeItem () ;
Maintenant que nous avons cette configuration, nous pouvons appeler le classeur pour attacher des composants aux éléments DOM, créer un nouveau modèle avec une connexion bidirectionnelle à la liste via sa facette de données.
// Milo ready function, fonctionne comme la fonction ready de jQuery. milo (function () // Appelez le classeur sur le document. // Il attache des composants aux éléments DOM avec l'attribut ml-bind var scope = milo.binder (); // Accédez à nos composants via l'objet scope var todos = scope.todos // Liste des tâches, newTodo = scope.newTodo // Nouvelle entrée de tâche, addBtn = scope.addBtn // Bouton Add, modelView = scope.modelView; // Où nous imprimons le modèle // Configurons notre modèle, cela contenir le tableau de todos var m = new milo.Model; // Cet abonnement nous montrera le contenu du modèle // à tout moment en dessous de todos m.on (/.*/, fonction showModel (msg, data) modelView.data.set (JSON.stringify (m.get ()));); // Créez une liaison bidirectionnelle profonde entre notre modèle et la facette de données de la liste todos. // Les chevrons les plus internes indiquent le sens de la connexion (peut soit aussi un sens), // le reste définit la profondeur de la connexion - 2 niveaux dans ce cas, pour inclure // les propriétés des éléments du tableau. milo.minder (m, '<<<->>> ', todos.data); // Abonnement pour cliquer sur l'événement du bouton d'ajout addBtn.events.on ('click', addTodo); // Cliquez sur le gestionnaire de la fonction du bouton d'ajout addTodo () // Nous conditionnons la saisie 'newTodo' en tant qu'objet // La propriété 'text' correspond au balisage de l'item. var itemData = text: newTodo.data.get (); // Nous introduisons ces données dans le modèle. // La vue sera mise à jour automatiquement! m.push (itemData); // Et enfin, définissez à nouveau l'entrée vide. newTodo.data.set ("););
Cet échantillon est disponible dans jsfiddle.
L'échantillon To-Do est très simple et montre une très petite partie de la puissance impressionnante de Milo. Milo possède de nombreuses fonctionnalités qui ne sont pas abordées dans cet article et dans les articles précédents, notamment le glisser-déposer, le stockage local, les utilitaires http et Websockets, les utilitaires DOM avancés, etc..
De nos jours, milo alimente le nouveau CMS de dailymail.co.uk (ce CMS contient des dizaines de milliers de code javascript frontal et est utilisé pour créer plus de 500 articles chaque jour).
Milo est une source ouverte et est toujours en phase bêta. C’est donc un bon moment pour expérimenter et peut-être même contribuer. Nous aimerions vos retours.
Notez que cet article a été écrit par Jason Green et Evgeny Poberezkin..