Construire un framework à partir de rien n’est pas quelque chose que nous voulions faire. Tu devrais être fou, non? Avec la pléthore de frameworks JavaScript, quelle motivation pourrions-nous avoir pour lancer notre propre?
À l'origine, nous recherchions un cadre pour construire le nouveau système de gestion de contenu du site Web The Daily Mail. L'objectif principal était de rendre le processus de rédaction beaucoup plus interactif, tous les éléments d'un article (images, incrustations, corbeilles d'appel, etc.) pouvant être déplacés, modulables et autogérés..
Tous les cadres sur lesquels nous avons pu mettre la main ont été conçus pour une interface utilisateur plus ou moins statique définie par les développeurs. Nous devions créer un article contenant à la fois du texte modifiable et des éléments d'interface utilisateur à rendu dynamique..
La colonne vertébrale était trop basse. Il ne faisait guère plus que fournir une structure et une messagerie de base aux objets. Nous devions construire beaucoup d'abstraction au-dessus de la fondation Backbone, nous avons donc décidé de construire cette fondation nous-mêmes..
AngularJS est devenu notre framework de choix pour la construction d’applications de navigateur de petite à moyenne taille avec des interfaces utilisateur relativement statiques. Malheureusement, AngularJS est vraiment une boîte noire - elle n’expose aucune API pratique pour étendre et manipuler les objets que vous créez avec elle - directives, contrôleurs, services. De même, si AngularJS fournit des connexions réactives entre les vues et les expressions de portée, il ne permet pas de définir des connexions réactives entre les modèles. Ainsi, toute application de taille moyenne devient très similaire à une application jQuery avec les spaghettis des écouteurs d’événement et des rappels, à au lieu des écouteurs d'événements, une application angulaire a des observateurs et au lieu de manipuler DOM, vous manipulez des étendues.
Ce que nous avons toujours voulu, c'était un cadre qui permettrait;
Nous n'avons pas trouvé ce dont nous avions besoin dans les solutions existantes. Nous avons donc commencé à développer Milo en parallèle avec l'application qui l'utilise..
Milo a été choisi comme nom en raison de Milo Minderbinder, un profiteur de guerre de Catch 22 par Joseph Heller. Ayant commencé à gérer les opérations de mess, il les a étendues à une entreprise commerciale rentable qui connectait tout le monde avec tout, et dans ce Milo et tous les autres "a une part".
Le framework Milo possède le classeur de modules, qui lie les éléments DOM aux composants (via des ml-bind
attribut), et le gestionnaire de module qui permet d’établir des connexions réactives actives entre différentes sources de données (les facettes Modèle et Données des composants sont de telles sources de données).
Par coïncidence, Milo peut être lu comme un acronyme de MaIL Online et sans l'environnement de travail unique de Mail Online, nous n'aurions jamais pu le construire..
Les vues dans Milo sont gérées par des composants, qui sont essentiellement des instances de classes JavaScript, responsables de la gestion d'un élément DOM. De nombreux frameworks utilisent des composants en tant que concept pour gérer les éléments d'interface utilisateur, mais le plus évident qui me vient à l'esprit est Ext JS. Nous avions beaucoup travaillé avec Ext JS (l'application héritée que nous remplacions avait été construite avec elle) et nous voulions éviter ce que nous considérions être deux inconvénients de son approche..
La première est qu'Ext JS ne vous facilite pas la gestion de votre marquage. La seule façon de créer une interface utilisateur est de rassembler des hiérarchies imbriquées de configurations de composants. Cela conduit à un balisage rendu inutilement complexe et prend le contrôle des mains du développeur. Nous avions besoin d'une méthode pour créer des composants en ligne, dans notre propre balisage HTML conçu à la main. C'est là que le liant entre.
Binder scanne notre balisage à la recherche du ml-bind
attribut afin qu’il puisse instancier des composants et les lier à l’élément. L'attribut contient des informations sur les composants; cela peut inclure la classe de composant, les facettes et doit inclure le nom du composant.
Notre composant milo
Nous parlerons des facettes dans une minute, mais pour l’instant, examinons comment prendre cette valeur d’attribut et en extraire la configuration à l’aide d’une expression régulière.
var bindAttrRegex = / ^ ([^ \: \ [\]] *) (?: \ [([^ \: \ [\]] *) \])? \ :? ([^:] *) $ / ; var result = valeur.match (bindAttrRegex); // result est un tableau avec // result [0] = 'ComponentClass [facet1, facet2]: composantName'; // result [1] = 'ComponentClass'; // résultat [2] = 'facet1, facet2'; // résultat [3] = 'nomcomposant';
Avec cette information, tout ce que nous avons à faire est de parcourir toutes les ml-bind
attributs, extraire ces valeurs et créer des instances pour gérer chaque élément.
var bindAttrRegex = / ^ ([^ \: \ [\]] *) (?: \ [([^ \: \ [\]] *) \])? \ :? ([^:] *) $ / ; classeur de fonctions (rappel) var scope = ; // nous obtenons tous les éléments avec l'attribut ml-bind var els = document.querySelectorAll ('[ml-bind]'); Array.prototype.forEach.call (els, fonction (el) var attrText = el.getAttribute ('ml-bind'); var result = attrText.match (bindAttrRegex); var className = result [1] || 'composant '; var facets = result [2] .split (', '); var compName = results [3]; // en supposant que nous ayons un objet de registre de toutes nos classes var comp = new classRegistry [className] (el); comp .addFacets (facettes); comp.name = compName; scope [compName] = comp; // nous conservons une référence au composant sur l'élément el .___ milo_component = comp;); rappel (portée); classeur (fonction (portée) console.log (portée););
Vous pouvez donc créer votre propre mini-cadre avec une syntaxe personnalisée en fonction de votre logique métier et de votre contexte. En très peu de code, nous avons mis en place une architecture permettant des composants modulaires à gestion automatique, qui peuvent être utilisés à votre guise. Nous pouvons créer une syntaxe commode et déclarative pour instancier et configurer des composants dans notre code HTML, mais contrairement à angular, nous pouvons gérer ces composants comme bon nous semble..
La deuxième chose que nous n’aimions pas à propos de Ext JS, c’est que sa hiérarchie de classes est très raide et rigide, ce qui aurait rendu difficile l’organisation de nos classes de composants. Nous avons essayé d’écrire une liste de tous les comportements qu’un composant donné d’un article pouvait avoir. Par exemple, un composant peut être éditable, écouter des événements, être une cible de dépôt ou être déplaçable. Ce ne sont là que quelques-uns des comportements nécessaires. Une liste préliminaire que nous avons rédigée comportait environ 15 types de fonctionnalités pouvant être requises pour un composant particulier..
Tenter d'organiser ces comportements dans une sorte de structure hiérarchique aurait été non seulement un casse-tête majeur, mais aussi très contraignant si nous voulions jamais changer les fonctionnalités d'une classe de composants donnée (ce que nous avons fini par faire beaucoup). Nous avons décidé de mettre en œuvre un modèle de conception orienté objet plus souple..
Nous avions étudié la conception axée sur la responsabilité qui, contrairement au modèle plus courant consistant à définir le comportement d'une classe avec les données qu'elle contient, est davantage concernée par les actions dont un objet est responsable. Cela nous convenait bien car nous avions affaire à un modèle de données complexe et imprévisible, et cette approche nous permettrait de laisser la mise en œuvre de ces détails à plus tard..
Le principal élément que nous avons retenu de RDD était le concept de rôles. Un rôle est un ensemble de responsabilités connexes. Dans le cas de notre projet, nous avons identifié des rôles tels que l'édition, le déplacement, la zone de dépôt, la sélection ou des événements, parmi bien d'autres. Mais comment représentez-vous ces rôles dans le code? Pour cela, nous avons emprunté le motif décorateur.
Le modèle de décorateur permet d'ajouter un comportement à un objet individuel, de manière statique ou dynamique, sans affecter le comportement des autres objets de la même classe. À présent, bien que la manipulation du comportement de classe à l'exécution n'ait pas été particulièrement nécessaire dans ce projet, nous étions très intéressés par le type d'encapsulation fourni par cette idée. L'implémentation de Milo est une sorte d'hybride impliquant des objets appelés facettes, attachés en tant que propriétés à l'instance du composant. La facette obtient une référence au composant, son propriétaire, et un objet de configuration, ce qui nous permet de personnaliser des facettes pour chaque classe de composant..
Vous pouvez considérer les facettes comme des mixins avancés et configurables qui obtiennent leur propre espace de noms sur leur objet propriétaire et même leur propre espace de noms. init
méthode, qui doit être remplacée par la sous-classe facette.
fonction Facette (propriétaire, config) this.name = this.constructor.name.toLowerCase (); this.owner = propriétaire; this.config = config || ; this.init.apply (this, arguments); Facet.prototype.init = fonction Facette $ init () ;
Nous pouvons donc sous-classer cette simple Facette
classe et créer des facettes spécifiques pour chaque type de comportement que nous voulons. Milo est livré pré-construit avec une variété de facettes, telles que le DOM
facette, qui fournit une collection d’utilitaires DOM qui agissent sur l’élément du composant propriétaire, ainsi que la liste
et Article
facettes, qui travaillent ensemble pour créer des listes de composants répétitifs.
Ces facettes sont ensuite réunies par ce que nous avons appelé un FacetedObject
, qui est une classe abstraite dont tous les composants héritent. le FacetedObject
a une méthode de classe appelée createFacetedClass
qui simplement se sous-classe, et attache toutes les facettes à un facettes
propriété sur la classe. De cette façon, quand le FacetedObject
est instancié, il a accès à toutes ses classes de facettes et peut les itérer pour amorcer le composant.
fonction FacetedObject (facetsOptions / *, autres arguments initiaux * /) facetsOptions = facetsOptions? _.clone (facetsOptions): ; var thisClass = this.constructor, facets = ; if (! thisClass.prototype.facets) renvoie une nouvelle erreur ('Aucune facette définie'); _.eachKey (this.facets, instantiateFacet, this, true); Object.defineProperties (this, facettes); if (this.init) this.init.apply (this, arguments); fonction instantiateFacet (facetClass, fct) var facetOpts = facetsOptions [fct]; supprimer facetsOptions [fct]; facets [fct] = enumerable: false, valeur: new facetClass (this, facetOpts); FacetedObject.createFacetedClass = fonction (nom, facetsClasses) var FacetedClass = _.createSubclass (this, nom, vrai); _.extendProto (FacetedClass, facets: facetsClasses); renvoyer FacetedClass; ;
Dans Milo, nous avons résumé un peu plus loin en créant une base Composant
classe avec une correspondance createComponentClass
méthode de classe, mais le principe de base est le même. Les comportements clés étant gérés par des facettes configurables, nous pouvons créer de nombreuses classes de composants différentes dans un style déclaratif sans avoir à écrire trop de code personnalisé. Voici un exemple utilisant certaines des facettes standard livrées avec Milo.
var Panel = Component.createComponentClass ('Panel', dom: cls: 'my-panel', tagName: 'div', événements: messages: 'clic': onPanelClick, faites glisser: messages: …, Déposez: messages: …, conteneur: non défini);
Ici, nous avons créé une classe de composant appelée Panneau
, qui a accès aux méthodes de l'utilitaire DOM, définira automatiquement sa classe CSS sur init
, il peut écouter les événements DOM et va configurer un gestionnaire de clic sur init
, il peut être déplacé et sert également de cible de chute. La dernière facette là, récipient
s'assure que ce composant configure sa propre portée et peut, en effet, avoir des composants enfants.
Nous avions discuté pendant un moment de savoir si tous les composants attachés au document devaient former une structure plate ou former leur propre arbre, où les enfants ne seraient accessibles que par leur parent..
Nous aurions certainement besoin d'étendues dans certaines situations, mais cela aurait pu être traité au niveau de la mise en œuvre, plutôt qu'au niveau du cadre. Par exemple, nous avons des groupes d'images contenant des images. Il aurait été simple pour ces groupes de garder une trace de leurs images enfants sans avoir besoin d'une portée générique..
Nous avons finalement décidé de créer une arborescence de composants dans le document. Avoir des étendues facilite beaucoup de choses et nous permet d'avoir une dénomination plus générique des composants, mais ceux-ci doivent évidemment être gérés. Si vous détruisez un composant, vous devez le supprimer de sa portée parent. Si vous déplacez un composant, il doit être supprimé et ajouté à un autre..
La portée est un hachage spécial, ou un objet de carte, avec chacun des enfants contenus dans la portée en tant que propriétés de l'objet. La portée, dans Milo, se trouve sur la facette conteneur, qui a très peu de fonctionnalités. Cependant, l’objet scope contient diverses méthodes de manipulation et d’itération, mais pour éviter les conflits d’espace de nommage, toutes ces méthodes sont nommées avec un soulignement au début..
var scope = myComponent.container.scope; scope._each (function (childComp) // itérer chaque composant enfant); // accéder à un composant spécifique de la portée var testComp = scope.testComp; // récupère le nombre total de composants enfants var total = scope._length (); // ajoute un nouveau composant à la portée scope._add (newComp);
Nous voulions un couplage lâche entre les composants, nous avons donc décidé d'associer une fonctionnalité de messagerie à tous les composants et à toutes les facettes..
La première implémentation du messager était juste un ensemble de méthodes permettant de gérer des tableaux d’abonnés. Les méthodes et le tableau ont été mélangés directement dans l'objet qui implémentait la messagerie.
Une version simplifiée de la première implémentation de messagerie ressemble à ceci:
var messengerMixin = initMessenger: initMessenger, on: on, on: off: off, postMessage: postMessage; fonction initMessenger () this._subscribers = ; function on (message, abonné) var msgSubscribers = this._subscribers [message] = this._subscribers [message] || []; if (msgSubscribers.indexOf (abonné) == -1) msgSubscribers.push (abonné); function off (message, abonné) var msgSubscribers = this._subscribers [message]; if (msgSubscribers) if (abonné) _.spliceItem (msgSubscribers, abonné); sinon, supprimez this._subscribers [message]; function postMessage (message, données) var msgSubscribers = this._subscribers [message]; if (msgSubscribers) msgSubscribers.forEach (fonction (abonné) subscriber.call (this, message, data););
Tout objet ayant utilisé cette combinaison peut se voir envoyer des messages (par l’objet même ou par tout autre code) avec postMessage
méthode et les abonnements à ce code peuvent être activés et désactivés avec des méthodes qui ont les mêmes noms.
De nos jours, les messagers ont considérablement évolué pour permettre:
Événements
La facette l'utilise pour exposer les événements DOM via Milo Messenger. Cette fonctionnalité est implémentée via une classe séparée MessageSource
et ses sous-classes.Les données
facet l'utilise pour convertir les événements de modification et d'entrée en DOM en événements de modification de données (voir Modèles ci-dessous). Cette fonctionnalité est implémentée via une classe séparée MessengerAPI et ses sous-classes..composant.on ('stateready', abonné: func, contexte: contexte);
une fois que
méthodepostMessage
(nous avons considéré un nombre variable d'arguments dans postMessage
, mais nous voulions une API de messagerie plus cohérente que celle que nous aurions avec des arguments variables)La principale erreur de conception que nous avons commise lors du développement de Messenger a été que tous les messages étaient envoyés de manière synchrone. Comme JavaScript est mono-thread, de longues séquences de messages comportant des opérations complexes peuvent facilement verrouiller l'interface utilisateur. Changer Milo pour rendre la distribution des messages asynchrone était facile (tous les abonnés sont appelés sur leurs propres blocs d’exécution à l’aide de setTimeout (abonné, 0)
, Il était plus difficile de modifier le reste de la structure et de l'application. Bien que la plupart des messages puissent être envoyés de manière asynchrone, de nombreux autres doivent encore être envoyés de manière synchrone (de nombreux événements DOM contenant des données ou des emplacements où preventDefault
est appelé). Par défaut, les messages sont maintenant envoyés de manière asynchrone et il existe un moyen de les rendre synchrones lorsque le message est envoyé:
composant.postMessageSync ('mymessage', data);
ou lorsque l'abonnement est créé:
composant.onSync ('mymessage', function (msg, data) //…);
Une autre décision de conception que nous avons prise a été la manière dont nous avons exposé les méthodes de messagerie sur les objets les utilisant. A l'origine, les méthodes étaient simplement mélangées dans l'objet, mais nous n'aimions pas que toutes les méthodes soient exposées et nous ne pouvions pas avoir de messagers autonomes. Donc, les messagers ont été ré-implémentés en tant que classe séparée basée sur une classe abstraite Mixin.
La classe Mixin permet d'exposer les méthodes d'une classe sur un objet hôte de telle sorte que lorsque des méthodes sont appelées, le contexte reste toujours Mixin plutôt que l'objet hôte..
Il s’est avéré être un mécanisme très pratique: nous pouvons avoir le plein contrôle sur les méthodes exposées et modifier les noms si nécessaire. Cela nous a également permis d’avoir deux messagers sur un objet, qui est utilisé pour les modèles.
En général, Milo Messenger s'est avéré être un logiciel très solide qui peut être utilisé seul, à la fois dans le navigateur et dans Node.js. Cela a été renforcé par l'utilisation dans notre système de gestion de contenu de production qui compte des dizaines de milliers de lignes de code.
Dans le prochain article, nous examinerons peut-être la partie la plus utile et la plus complexe de Milo. Les modèles Milo permettent non seulement un accès sécurisé et complet aux propriétés, mais également la souscription d'événements aux modifications de tout niveau..
Nous explorerons également notre implémentation de minder et la façon dont nous utilisons des objets de connecteur pour lier des sources de données dans un sens ou dans les deux sens..
Notez que cet article a été écrit par Jason Green et Evgeny Poberezkin..