Das Erstellen eines Frameworks von Grund auf ist nicht etwas, das wir uns ausdrücklich vorgenommen haben. Du musst verrückt sein, richtig? Mit der Fülle von JavaScript-Frameworks draußen, welche mögliche Motivation könnten wir haben, um unsere eigenen zu rollen??
Wir waren ursprünglich auf der Suche nach einem Rahmen, um das neue Content-Management-System für die Daily Mail-Website aufzubauen. Das Hauptziel bestand darin, den Bearbeitungsprozess viel interaktiver zu gestalten, da alle Elemente eines Artikels (Bilder, Einbettungen, Callout-Boxen usw.) durch Ziehen verschoben, modular und selbstverwaltend sind.
Alle Frameworks, die wir einsetzen konnten, wurden für mehr oder weniger statische Benutzeroberflächen entwickelt, die von Entwicklern definiert wurden. Wir mussten einen Artikel mit bearbeitbarem Text und dynamisch gerenderten UI-Elementen erstellen.
Rückgrat war zu niedrig. Es tat nicht viel mehr als die Bereitstellung grundlegender Objektstrukturen und Messaging. Wir müssten viel Abstraktion über der Backbone-Stiftung aufbauen, also entschieden wir uns, diese Basis lieber selbst aufzubauen.
AngularJS wurde zum bevorzugten Framework für die Erstellung kleiner bis mittelgroßer Browseranwendungen mit relativ statischen Benutzeroberflächen. Leider ist AngularJS eine Black Box - es bietet keine praktische API, um die von Ihnen erstellten Objekte zu erweitern und zu bearbeiten - Direktiven, Controller, Services. Während AngularJS reaktive Verbindungen zwischen Ansichten und Bereichsausdrücken bereitstellt, können keine reaktiven Verbindungen zwischen Modellen definiert werden. Daher wird jede Anwendung mittlerer Größe einer jQuery-Anwendung mit den Spaghetti von Ereignis-Listenern und Callbacks sehr ähnlich, mit dem einzigen Unterschied Anstelle von Ereignis-Listenern verfügt eine eckige Anwendung über Beobachter. Statt DOM zu manipulieren, können Sie Bereiche bearbeiten.
Was wir immer wollten, war ein Rahmen, der es erlauben würde;
Wir konnten in bestehenden Lösungen nicht finden, was wir brauchten. Daher haben wir begonnen, Milo parallel zu der Anwendung zu entwickeln, die es verwendet.
Milo wurde wegen Milo Minderbinder, einem Kriegsprofiteur aus, als Name gewählt Fang 22 von Joseph Heller. Er begann mit dem Management von Messebetrieben und baute sie zu einem profitablen Handelsunternehmen aus, das alle mit allem verband, und in dem Milo und alle anderen "einen Anteil haben"..
Milo das Framework hat den Modulordner, der DOM-Elemente mit Komponenten verbindet (über spezielle Elemente) ml-bind
Attribut) und die Modulverwaltung, mit der reaktive reaktive Verbindungen zwischen verschiedenen Datenquellen hergestellt werden können (Modell- und Datenfacette von Komponenten sind solche Datenquellen).
Zufälligerweise kann Milo als Abkürzung für MaIL Online gelesen werden, und ohne die einzigartige Arbeitsumgebung bei Mail Online hätten wir es niemals schaffen können.
Ansichten in Milo werden von Komponenten verwaltet, bei denen es sich im Wesentlichen um Instanzen von JavaScript-Klassen handelt, die für die Verwaltung eines DOM-Elements verantwortlich sind. Viele Frameworks verwenden Komponenten als ein Konzept zur Verwaltung von Benutzeroberflächenelementen. Das offensichtlichste Element ist dabei Ext JS. Wir hatten ausgiebig mit Ext JS gearbeitet (die ältere Anwendung, die wir ersetzten, wurde damit erstellt) und wollten vermeiden, was wir als zwei Nachteile seines Ansatzes betrachteten.
Der erste ist, dass Ext JS es Ihnen nicht leicht macht, Ihre Markierungen zu verwalten. Die einzige Möglichkeit, eine Benutzeroberfläche zu erstellen, besteht darin, verschachtelte Hierarchien von Komponentenkonfigurationen zusammenzustellen. Dies führt zu unnötig komplex gerenderten Markups und nimmt dem Entwickler die Kontrolle. Wir brauchten eine Methode zum Erstellen von Komponenten inline, in unserem eigenen, handgefertigten HTML-Markup. Hier kommt Binder ins Spiel.
Binder scannt unser Markup nach dem ml-bind
Attribut, damit es Komponenten instanziieren und an das Element binden kann. Das Attribut enthält Informationen zu den Komponenten. Dies kann die Komponentenklasse und Facetten umfassen und muss den Komponentennamen enthalten.
Unsere Milo-Komponente
Wir werden in einer Minute über Facetten sprechen, aber zunächst wollen wir uns ansehen, wie wir diesen Attributwert nehmen und die Konfiguration mithilfe eines regulären Ausdrucks daraus extrahieren können.
var bindAttrRegex = / ^ ([^ \: \ [\]] *) (?: \ [([^ \: \ [\]] *) \])? \ :? ([^:] *) $ / ; var result = value.match (bindAttrRegex); // Ergebnis ist ein Array mit // Ergebnis [0] = 'ComponentClass [Facet1, Facet2]: Komponentenname'; // result [1] = 'ComponentClass'; // result [2] = 'facet1, facet2'; // Ergebnis [3] = 'Komponentenname';
Mit diesen Informationen müssen wir nur noch alles durchlaufen ml-bind
Attribute, extrahieren Sie diese Werte und erstellen Sie Instanzen, um jedes Element zu verwalten.
var bindAttrRegex = / ^ ([^ \: \ [\]] *) (?: \ [([^ \: \ [\]] *) \])? \ :? ([^:] *) $ / ; Funktionsbinder (Callback) var scope = ; // wir erhalten alle Elemente mit dem ml-bind-Attribut var els = document.querySelectorAll ('[ml-bind]'); Array.prototype.forEach.call (els, function (el) var attrText = el.getAttribute ('ml-bind')); var Ergebnis = attrText.match (bindAttrRegex); var Klassenname = Ergebnis [1] || 'Komponente var facets = result [2] .split (','); var compName = results [3]; // vorausgesetzt, wir haben ein Registrierungsobjekt aller unserer Klassen. var comp = new classRegistry [className] (el); comp .addFacets (Facetten); comp.name = compName; scope [compName] = comp; // Wir behalten einen Verweis auf die Komponente auf dem Element el .___ milo_component = comp;); Rückruf (Umfang); binder (function (scope) console.log (scope););
Mit nur ein bisschen Regex und etwas DOM-Traversal können Sie also Ihr eigenes Mini-Framework mit benutzerdefinierter Syntax erstellen, um sich Ihrer speziellen Geschäftslogik und Ihrem Kontext anzupassen. In sehr wenig Code haben wir eine Architektur eingerichtet, die modulare, sich selbst verwaltende Komponenten ermöglicht, die beliebig verwendet werden können. Wir können eine bequeme und deklarative Syntax für das Instantiieren und Konfigurieren von Komponenten in unserem HTML-Code erstellen. Im Gegensatz zu angle können wir diese Komponenten jedoch wie gewünscht verwalten.
Das zweite, was uns an Ext JS nicht gefallen hat, war, dass es eine sehr steile und starre Klassenhierarchie hat, was es schwierig gemacht hätte, unsere Komponentenklassen zu organisieren. Wir haben versucht, eine Liste aller Verhaltensweisen zu erstellen, die eine bestimmte Komponente innerhalb eines Artikels haben könnte. Beispielsweise kann eine Komponente bearbeitet werden, sie kann auf Ereignisse warten, sie kann ein Ablageziel sein oder selbst ziehen. Dies sind nur einige der erforderlichen Verhaltensweisen. Eine vorläufige Liste, die wir aufgeschrieben haben, enthielt etwa 15 verschiedene Arten von Funktionen, die für eine bestimmte Komponente erforderlich sein könnten.
Der Versuch, diese Verhaltensweisen in einer hierarchischen Struktur zu organisieren, hätte nicht nur große Kopfschmerzen verursacht, sondern auch sehr einschränkend, falls wir jemals die Funktionalität einer bestimmten Komponentenklasse ändern möchten (etwas, das wir letztendlich viel getan haben). Wir haben uns entschieden, ein flexibleres objektorientiertes Entwurfsmuster zu implementieren.
Wir hatten uns mit Responsibility-Driven Design beschäftigt, das sich im Gegensatz zum allgemeineren Modell der Definition des Verhaltens einer Klasse zusammen mit den darin enthaltenen Daten eher mit den Aktionen befasst, für die ein Objekt verantwortlich ist. Dies passte gut zu uns, da es sich um ein komplexes und unvorhersehbares Datenmodell handelte, und dieser Ansatz würde es uns ermöglichen, die Implementierung dieser Details später zu überlassen.
Der Schlüssel, den wir uns von RDD genommen haben, war das Konzept der Rollen. Eine Rolle ist eine Reihe verwandter Verantwortlichkeiten. Bei unserem Projekt haben wir Rollen wie Bearbeiten, Ziehen, Drop-Zone, Auswählbare oder Ereignisse unter vielen anderen identifiziert. Aber wie repräsentieren Sie diese Rollen im Code? Dafür haben wir uns vom Dekorateur-Muster geborgt.
Das Dekoratormuster ermöglicht das Hinzufügen eines Verhaltens zu einem einzelnen Objekt, entweder statisch oder dynamisch, ohne das Verhalten anderer Objekte derselben Klasse zu beeinflussen. Während die Laufzeitmanipulation des Klassenverhaltens in diesem Projekt nicht besonders notwendig war, waren wir sehr an der Art der Verkapselung interessiert, die diese Idee bietet. Die Implementierung von Milo ist eine Art Hybrid, bei dem Objekte als Facetten bezeichnet werden, die als Eigenschaften an die Komponenteninstanz angehängt werden. Die Facette erhält einen Verweis auf die Komponente, ihren Eigentümer und ein Konfigurationsobjekt, mit dem Facetten für jede Komponentenklasse angepasst werden können.
Sie können sich Facetten als fortgeschrittene, konfigurierbare Mixins vorstellen, die ihren eigenen Namespace für ihr Eigentümerobjekt und sogar ihren eigenen erhalten drin
Methode, die von der Facettenunterklasse überschrieben werden muss.
Funktion Facet (Eigentümer, Konfig) this.name = this.constructor.name.toLowerCase (); this.owner = Besitzer; this.config = config || ; this.init.apply (this, argumente); Facet.prototype.init = function Facet $ init () ;
So können wir diese einfache Unterklasse bilden Facette
klassifizieren und spezifische Facetten für jede Art von Verhalten erstellen, die wir möchten. Milo ist mit einer Vielzahl von Facetten, wie z DOM
facet, das eine Sammlung von DOM-Dienstprogrammen bereitstellt, die mit dem Element der Besitzerkomponente arbeiten, und die Liste
und Artikel
Facetten, die zusammenarbeiten, um Listen sich wiederholender Komponenten zu erstellen.
Diese Facetten werden dann durch das, was wir als a bezeichnet haben, zusammengeführt FacetedObject
, Dies ist eine abstrakte Klasse, von der alle Komponenten erben. Das FacetedObject
hat eine Klassenmethode aufgerufen createFacetedClass
das untergliedert sich einfach selbst und bringt alle Facetten an Facetten
Eigenschaft in der Klasse. So, wenn die FacetedObject
wird instanziiert, hat Zugriff auf alle Facettenklassen und kann sie zum Bootstrap der Komponente durchlaufen.
Funktion FacetedObject (facetsOptions / *, andere init args * /) facetsOptions = facetsOptions? _.clone (facetsOptions): ; var thisClass = this.constructor, facets = ; if (! thisClass.prototype.facets) werfen neuen Fehler ('Keine Facetten definiert'); _.eachKey (this.facets, instantiateFacet, this, true); Object.defineProperties (this, facets); if (this.init) this.init.apply (this, Argumente); Funktion instantiateFacet (facetClass, fct) var facetOpts = facetsOptions [fct]; Facetten löschenOptionen [fct]; facets [fct] = Aufzählungszeichen: false, Wert: new facetClass (this, facetOpts); FacetedObject.createFacetedClass = function (name, facetsClasses) var FacetedClass = _.createSubclass (this, name, true); _.extendProto (FacetedClass, facets: facetsClasses); return FacetedClass; ;
In Milo haben wir etwas weiter abstrahiert, indem wir eine Basis geschaffen haben Komponente
Klasse mit einem passenden createComponentClass
Klassenmethode, aber das Grundprinzip ist das gleiche. Da Schlüsselverhalten von konfigurierbaren Facetten verwaltet wird, können wir viele verschiedene Komponentenklassen im deklarativen Stil erstellen, ohne zu viel benutzerdefinierten Code schreiben zu müssen. Hier ein Beispiel, in dem einige der Standardfacetten von Milo verwendet werden.
var Panel = Component.createComponentClass ('Panel', dom: cls: 'my-panel', TagName: 'div', Ereignisse: messages: 'click': onPanelClick, ziehen Sie: messages: …, Drop: messages: …, container: undefined);
Hier haben wir eine Komponentenklasse erstellt Panel
, Wenn dieser Zugriff auf DOM-Dienstprogrammmethoden hat, wird die CSS-Klasse automatisch aktiviert drin
, Er kann DOM-Ereignisse überwachen und einen Click-Handler einrichten drin
, es kann gezogen werden und dient auch als Ablageziel. Die letzte Facette dort, Container
stellt sicher, dass diese Komponente ihren eigenen Bereich einrichtet und über untergeordnete Komponenten verfügen kann.
Wir hatten lange diskutiert, ob alle an das Dokument angefügten Komponenten eine flache Struktur bilden sollten oder einen eigenen Baum bilden sollten, in dem Kinder nur von ihren Eltern aus zugänglich sind.
In einigen Situationen hätten wir definitiv Spielräume benötigt, aber es hätte auf der Implementierungsebene und nicht auf der Rahmenebene gehandhabt werden können. Zum Beispiel haben wir Bildgruppen, die Bilder enthalten. Es wäre einfach für diese Gruppen gewesen, ihre untergeordneten Bilder ohne generischen Geltungsbereich zu verfolgen.
Wir haben uns schließlich entschlossen, einen Gültigkeitsbereich mit Komponenten im Dokument zu erstellen. Die Verwendung von Gültigkeitsbereichen macht vieles einfacher und ermöglicht eine allgemeinere Benennung von Komponenten, die jedoch offensichtlich verwaltet werden müssen. Wenn Sie eine Komponente zerstören, müssen Sie sie aus dem übergeordneten Bereich entfernen. Wenn Sie eine Komponente verschieben, muss sie aus einer Komponente entfernt und einer anderen hinzugefügt werden.
Der Bereich ist ein spezielles Hash- oder Kartenobjekt, wobei jedes der untergeordneten Objekte als Eigenschaften des Objekts im Bereich enthalten ist. Der Geltungsbereich von Milo befindet sich auf der Containerfacette, die selbst sehr wenig Funktionalität aufweist. Das Bereichsobjekt verfügt jedoch über eine Vielzahl von Methoden zum Manipulieren und Iterieren selbst. Um Namensraumkonflikte zu vermeiden, werden alle diese Methoden am Anfang mit einem Unterstrich bezeichnet.
var scope = meinComponent.container.scope; scope._each (function (childComp) // jede untergeordnete Komponente durchlaufen); // auf eine bestimmte Komponente im Gültigkeitsbereich zugreifen var testComp = scope.testComp; // Die Gesamtzahl der untergeordneten Komponenten abrufen var total = scope._length (); // füge eine neue Komponente im Geltungsbereich hinzu bereich._add (newComp);
Wir wollten eine lose Kopplung zwischen den Komponenten. Daher haben wir beschlossen, die Messaging-Funktion an alle Komponenten und Facetten anzubringen.
Die erste Implementierung des Messenger war lediglich eine Sammlung von Methoden zur Verwaltung von Abonnentenarrays. Sowohl die Methoden als auch das Array wurden direkt in das Objekt gemischt, in dem das Messaging implementiert wurde.
Eine vereinfachte Version der ersten Messenger-Implementierung sieht in etwa so aus:
var messengerMixin = initMessenger: initMessenger, ein: ein, aus: aus, postMessage: postMessage; Funktion initMessenger () this._subscribers = ; Funktion an (Nachricht, Teilnehmer) var msgSubscribers = this._subscribers [Nachricht] = this._subscribers [Nachricht] || []; if (msgSubscribers.indexOf (subscriber) == -1) msgSubscribers.push (subscriber); Funktion aus (Nachricht, Teilnehmer) var msgSubscribers = this._subscribers [Nachricht]; if (msgSubscribers) if (Abonnent) _.spliceItem (msgSubscribers, Abonnent); sonst lösche das. Abonnenten [Nachricht]; Funktion postMessage (Nachricht, Daten) var msgSubscribers = this._subscribers [Nachricht]; if (msgSubscribers) msgSubscribers.forEach (function (subscriber) subscriber.call (this, message, data););
Bei jedem Objekt, das dieses Mix-In verwendet hat, können Nachrichten (vom Objekt selbst oder von einem anderen Code) mit gesendet werden POST-Meldung
Methoden und Abonnements für diesen Code können mit gleichnamigen Methoden ein- und ausgeschaltet werden.
Heutzutage haben sich Boten wesentlich weiterentwickelt, um Folgendes zu ermöglichen:
Veranstaltungen
facet verwendet es, um DOM-Ereignisse über Milo Messenger anzuzeigen. Diese Funktionalität wird über eine separate Klasse implementiert MessageSource
und seine Unterklassen.Daten
facet verwendet es, um die Änderung und Eingabe von DOM-Ereignissen in Datenänderungsereignisse zu übersetzen (siehe Modelle unten). Diese Funktionalität wird über eine separate Klasse MessengerAPI und ihre Unterklassen implementiert.component.on ('stateready', subscriber: func, context: context);
Einmal
MethodePOST-Meldung
(Wir haben die variable Anzahl von Argumenten in betrachtet POST-Meldung
, aber wir wollten eine konsistentere Messaging-API, als wir es mit variablen Argumenten hätten.Der Hauptfehler bei der Entwicklung von Messenger bestand darin, dass alle Nachrichten synchron versendet wurden. Da es sich bei JavaScript um einen einzigen Thread handelt, sperren lange Sequenzen von Nachrichten, bei denen komplexe Operationen ausgeführt werden, die Benutzeroberfläche. Es war einfach, Milo so zu ändern, dass der Nachrichtenversand asynchron ist (alle Abonnenten werden in ihren eigenen Ausführungsblöcken aufgerufen setTimeout (Teilnehmer, 0)
, Der Rest des Frameworks und der Anwendung zu ändern, war schwieriger - während die meisten Nachrichten asynchron versendet werden können, gibt es viele, die noch synchron abgesetzt werden müssen (viele DOM-Ereignisse, die Daten enthalten oder an denen sich Orte befinden) Standard verhindern
wird genannt). Standardmäßig werden Nachrichten jetzt asynchron abgesetzt, und es gibt eine Möglichkeit, sie synchron zu machen, entweder wenn die Nachricht gesendet wird:
component.postMessageSync ('mymessage', data);
oder wenn ein Abonnement erstellt wird:
component.onSync ('mymessage', Funktion (msg, data) //…);
Eine andere Designentscheidung, die wir getroffen haben, war die Art und Weise, wie wir die Botenmethoden für die Objekte, die sie verwenden, darstellen. Ursprünglich wurden Methoden einfach in das Objekt gemischt, aber es hat uns nicht gefallen, dass alle Methoden offengelegt werden und wir keine eigenständigen Boten haben könnten. So wurden Boten als separate Klasse basierend auf einer abstrakten Klasse Mixin implementiert.
Mit der Mixin-Klasse können Methoden einer Klasse auf einem Hostobjekt so angezeigt werden, dass beim Aufruf von Methoden der Kontext immer noch Mixin ist und nicht das Hostobjekt.
Es hat sich als sehr praktischer Mechanismus erwiesen. Wir haben die vollständige Kontrolle darüber, welche Methoden verfügbar gemacht werden, und die Namen können bei Bedarf geändert werden. Es erlaubte uns auch, zwei Boten an einem Objekt zu haben, das für Modelle verwendet wird.
Im Allgemeinen hat sich Milo Messenger als sehr solide Software erwiesen, die sowohl im Browser als auch in Node.js verwendet werden kann. Es wurde durch die Verwendung in unserem Produktions-Content-Management-System mit zehntausenden Codezeilen gehärtet.
Im nächsten Artikel werden wir den möglicherweise nützlichsten und komplexesten Teil von Milo betrachten. Die Milo-Modelle ermöglichen nicht nur einen sicheren, tiefen Zugriff auf Eigenschaften, sondern auch die Ereignisabonnementierung von Änderungen auf jeder Ebene.
Wir werden auch unsere Implementierung von Minder kennenlernen und wie wir Connector-Objekte verwenden, um ein- oder beidseitig Datenquellen zu binden.
Beachten Sie, dass dieser Artikel sowohl von Jason Green als auch von Evgeny Poberezkin geschrieben wurde.