Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Milo

Rollen Ihres eigenen Frameworks

by
Length:LongLanguages:
This post is part of a series called Rolling Your Own Framework.
Rolling Your Own Framework: A Practical Example

German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)

Das Erstellen eines Frameworks von Grund auf neu ist nicht unser Ziel. Du musst verrückt sein, oder? Welche mögliche Motivation könnten wir angesichts der Fülle von JavaScript-Frameworks haben, unsere eigenen zu entwickeln?

Wir suchten ursprünglich nach einem Framework für den Aufbau des neuen Content-Management-Systems für die Daily Mail-Website. Das Hauptziel bestand darin, den Bearbeitungsprozess viel interaktiver zu gestalten, wobei alle Elemente eines Artikels (Bilder, Einbettungen, Beschriftungsfelder usw.) ziehbar, modular und selbstverwaltend sind.

Alle Frameworks, die wir in die Hände bekommen konnten, wurden für eine mehr oder weniger statische Benutzeroberfläche entwickelt, die von Entwicklern definiert wurde. Wir mussten einen Artikel mit bearbeitbarem Text und dynamisch gerenderten UI-Elementen erstellen.

Das Rückgrat war zu niedrig. Es war kaum mehr als die Bereitstellung einer grundlegenden Objektstruktur und eines Messaging. Wir müssten viel Abstraktion über dem Backbone-Fundament aufbauen, also haben wir beschlossen, dieses Fundament lieber selbst zu bauen.

AngularJS wurde zu unserem bevorzugten Framework für die Erstellung kleiner bis mittlerer Browseranwendungen mit relativ statischen Benutzeroberflächen. Leider ist AngularJS eine Black Box - es bietet keine praktische API zum Erweitern und Bearbeiten der Objekte, die Sie damit erstellen - Direktiven, Controller, Dienste. Während AngularJS reaktive Verbindungen zwischen Ansichten und Bereichsausdrücken bereitstellt, ist es nicht möglich, reaktive Verbindungen zwischen Modellen zu definieren, sodass jede Anwendung mittlerer Größe einer jQuery-Anwendung mit den Spaghetti von Ereignis-Listenern und Rückrufen sehr ähnlich wird, mit dem einzigen Unterschied, dass Anstelle von Ereignis-Listenern verfügt eine Winkelanwendung über Beobachter, und anstatt DOM zu manipulieren, manipulieren Sie Bereiche.

Was wir immer wollten, war ein Rahmen, der es erlauben würde;

  • Deklarative Entwicklung von Anwendungen mit reaktiven Bindungen von Modellen an Ansichten.
  • Erstellen reaktiver Datenbindungen zwischen verschiedenen Modellen in der Anwendung, um die Datenverbreitung in einem deklarativen und nicht in einem imperativen Stil zu verwalten.
  • Einfügen von Validatoren und Übersetzern in diese Bindungen, damit Ansichten an Datenmodelle gebunden werden können, anstatt Modelle wie in AngularJS anzuzeigen.
  • Präzise Kontrolle über Komponenten, die mit DOM-Elementen verknüpft sind.
  • Flexibilität bei der Ansichtsverwaltung, mit der Sie sowohl DOM-Änderungen automatisch bearbeiten als auch einige Abschnitte mithilfe einer Vorlagen-Engine neu rendern können, wenn das Rendern effizienter ist als die DOM-Manipulation.
  • Möglichkeit zum dynamischen Erstellen von Benutzeroberflächen.
  • In der Lage sein, Mechanismen hinter der Datenreaktivität zu nutzen und Ansichtsaktualisierungen und Datenfluss präzise zu steuern.
  • In der Lage sein, die Funktionalität der vom Framework bereitgestellten Komponenten zu erweitern und neue Komponenten zu erstellen.

Wir konnten in vorhandenen Lösungen nicht finden, was wir brauchten, und haben daher begonnen, Milo parallel zu der Anwendung zu entwickeln, die es verwendet.

Warum Milo?

Milo wurde wegen Milo Minderbinder, einem Kriegsprofiteur aus Catch 22 von Joseph Heller, als Name gewählt. Nachdem er angefangen hatte, Mess-Operationen zu verwalten, baute er sie zu einem profitablen Handelsunternehmen aus, das alle mit allem verband, und an dem Milo und alle anderen "einen Anteil haben".

Milo, das Framework, verfügt über den Modulbinder, der DOM-Elemente an Komponenten bindet (über ein spezielles ml-bind-Attribut), und den Modulbinder, mit dem reaktive Live-Verbindungen zwischen verschiedenen Datenquellen hergestellt werden können (Modell- und Datenfacette on Komponenten sind solche Datenquellen).

Zufälligerweise kann Milo als Akronym von MaIL Online gelesen werden, und ohne die einzigartige Arbeitsumgebung bei Mail Online hätten wir es nie schaffen können.

Ansichten verwalten

Bindemittel

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 Konzept zum Verwalten von UI-Elementen, aber das offensichtlichste, das mir in den Sinn kommt, ist Ext JS. Wir hatten intensiv mit Ext JS gearbeitet (die Legacy-Anwendung, die wir ersetzten, wurde damit erstellt) und wollten vermeiden, was wir als zwei Nachteile seines Ansatzes betrachteten.

Das erste ist, dass Ext JS es Ihnen nicht einfach macht, Ihr Markup zu verwalten. Die einzige Möglichkeit, eine Benutzeroberfläche zu erstellen, besteht darin, verschachtelte Hierarchien von Komponentenkonfigurationen zusammenzustellen. Dies führt zu unnötig komplexen gerenderten Markups und entzieht dem Entwickler die Kontrolle. Wir brauchten eine Methode zum Erstellen von Komponenten inline in unserem eigenen handgefertigten HTML-Markup. Hier kommt der Binder ins Spiel.

Binder durchsucht unser Markup nach dem ml-bind-Attribut, um Komponenten zu instanziieren und an das Element zu binden. Das Attribut enthält Informationen zu den Komponenten. Dies kann die Komponentenklasse und Facetten enthalten und muss den Komponentennamen enthalten.

Wir werden gleich über Facetten sprechen, aber jetzt schauen wir uns an, wie wir diesen Attributwert nehmen und die Konfiguration mit einem regulären Ausdruck daraus extrahieren können.

Mit diesen Informationen müssen wir lediglich alle ml-bind-Attribute durchlaufen, diese Werte extrahieren und Instanzen erstellen, um jedes Element zu verwalten.

Mit ein wenig Regex und etwas DOM-Durchquerung können Sie also Ihr eigenes Mini-Framework mit benutzerdefinierter Syntax erstellen, das Ihrer speziellen Geschäftslogik und Ihrem Kontext entspricht. Mit sehr wenig Code haben wir eine Architektur eingerichtet, die modulare, selbstverwaltende Komponenten ermöglicht, die beliebig verwendet werden können. Wir können eine bequeme und deklarative Syntax zum Instanziieren und Konfigurieren von Komponenten in unserem HTML-Code erstellen. Im Gegensatz zu Angular können wir diese Komponenten jedoch nach Belieben verwalten.

Verantwortungsorientiertes Design

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 schreiben, die eine bestimmte Komponente in einem Artikel haben könnte. Beispielsweise kann eine Komponente bearbeitet werden, auf Ereignisse warten, ein Ablageziel sein oder selbst ziehbar sein. Dies sind nur einige der erforderlichen Verhaltensweisen. Eine vorläufige Liste, die wir erstellt haben, enthielt ungefähr 15 verschiedene Arten von Funktionen, die für eine bestimmte Komponente erforderlich sein könnten.

Der Versuch, diese Verhaltensweisen in einer Art hierarchischer Struktur zu organisieren, wäre nicht nur ein großes Problem gewesen, sondern auch sehr einschränkend, falls wir jemals die Funktionalität einer bestimmten Komponentenklasse ändern wollten (was wir letztendlich viel getan haben). Wir haben uns entschlossen, ein flexibleres objektorientiertes Entwurfsmuster zu implementieren.

Wir haben uns über verantwortungsorientiertes Design informiert, das sich im Gegensatz zu dem 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.

Das Wichtigste, was wir RDD weggenommen haben, war das Konzept der Rollen. Eine Rolle ist eine Reihe von damit verbundenen Verantwortlichkeiten. Im Fall unseres Projekts haben wir unter anderem Rollen wie Bearbeiten, Ziehen, Ablegen, Auswählen oder Ereignisse identifiziert. Aber wie repräsentieren Sie diese Rollen im Code? Dafür haben wir uns das Dekorationsmuster geliehen.

Das Dekorationsmuster ermöglicht das Hinzufügen von Verhalten 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 Kapselung interessiert, die diese Idee bietet. Die Implementierung von Milo ist eine Art Hybrid, bei dem Objekte, sogenannte Facetten, 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 wir Facetten für jede Komponentenklasse anpassen können.

Sie können sich Facetten als erweiterte, konfigurierbare Mixins vorstellen, die einen eigenen Namespace für ihr Eigentümerobjekt und sogar eine eigene init-Methode erhalten, die von der Facetten-Unterklasse überschrieben werden muss.

So können wir diese einfache Facet-Klasse unterordnen und spezifische Facetten für jede Art von Verhalten erstellen, die wir wollen. Milo wird mit einer Vielzahl von Facetten vorgefertigt geliefert, z. B. der DOM-Facette, die eine Sammlung von DOM-Dienstprogrammen bereitstellt, die mit dem Element der Eigentümerkomponente arbeiten, und den List- und Item-Facetten, die zusammenarbeiten, um Listen sich wiederholender Komponenten zu erstellen.

Diese Facetten werden dann durch ein sogenanntes FacetedObject zusammengeführt, eine abstrakte Klasse, von der alle Komponenten erben. Das FacetedObject verfügt über eine Klassenmethode namens createFacetedClass, die sich einfach selbst in Unterklassen unterteilt und alle Facetten an eine facets Eigenschaft in der Klasse anfügt. Auf diese Weise hat das FacetedObject beim Instanziieren Zugriff auf alle Facettenklassen und kann sie iterieren, um die Komponente zu booten.

In Milo haben wir etwas weiter abstrahiert, indem wir eine Basis Component Klasse mit einer übereinstimmenden Klassenmethode createComponentClass erstellt haben, aber das Grundprinzip ist dasselbe. Da wichtige Verhaltensweisen durch konfigurierbare Facetten verwaltet werden, können wir viele verschiedene Komponentenklassen in einem deklarativen Stil erstellen, ohne zu viel benutzerdefinierten Code schreiben zu müssen. Hier ist ein Beispiel mit einigen der sofort einsatzbereiten Facetten, die mit Milo geliefert werden.

Hier haben wir eine Komponentenklasse namens Panel erstellt, die Zugriff auf DOM-Dienstprogrammmethoden hat, ihre CSS-Klasse automatisch auf init setzt, auf DOM-Ereignisse wartet und einen Klick-Handler auf init einrichtet, sie kann herumgezogen werden und auch fungieren als Drop-Ziel. Die letzte Facette dort, container, stellt sicher, dass diese Komponente ihren eigenen Bereich einrichtet und tatsächlich untergeordnete Komponenten haben kann.

Umfang

Wir hatten eine Weile darüber diskutiert, ob alle an das Dokument angehängten Komponenten eine flache Struktur oder einen eigenen Baum bilden sollten, auf den Kinder nur von ihren Eltern aus zugreifen können.

Für einige Situationen hätten wir definitiv Bereiche benötigt, aber dies hätte auf Implementierungsebene und nicht auf Framework-Ebene erfolgen können. Zum Beispiel haben wir Bildgruppen, die Bilder enthalten. Für diese Gruppen wäre es unkompliziert gewesen, den Überblick über ihre untergeordneten Bilder zu behalten, ohne dass ein allgemeiner Bereich erforderlich wäre.

Wir haben uns schließlich entschlossen, einen Bereichsbaum von Komponenten im Dokument zu erstellen. Bereiche zu haben, erleichtert viele Dinge und ermöglicht uns eine allgemeinere Benennung von Komponenten, die jedoch offensichtlich verwaltet werden müssen. Wenn Sie eine Komponente zerstören, müssen Sie sie aus ihrem übergeordneten Bereich entfernen. Wenn Sie eine Komponente verschieben, muss sie von einer entfernt und einer anderen hinzugefügt werden.

Der Bereich ist ein spezieller Hash oder ein Kartenobjekt, wobei jedes der im Bereich enthaltenen untergeordneten Elemente als Eigenschaften des Objekts verwendet wird. Das Zielfernrohr in Milo befindet sich auf der Container-Facette, die selbst nur eine sehr geringe Funktionalität aufweist. Das Bereichsobjekt verfügt jedoch über eine Vielzahl von Methoden zum Bearbeiten und Iterieren. Um Namespace-Konflikte zu vermeiden, werden alle diese Methoden am Anfang mit einem Unterstrich versehen.

Messaging - Synchron oder Asynchron

Wir wollten eine lose Kopplung zwischen Komponenten, daher haben wir beschlossen, Messaging-Funktionen an alle Komponenten und Facetten anzuhängen.

Die erste Implementierung des Messenger war nur eine Sammlung von Methoden, mit denen Arrays von Abonnenten verwaltet wurden. Sowohl die Methoden als auch das Array wurden direkt in das Objekt gemischt, das Messaging implementiert hat.

Eine vereinfachte Version der ersten Messenger-Implementierung sieht ungefähr so aus:

Auf jedem Objekt, das dieses Mix-In verwendet hat, können Nachrichten (nach Objekt selbst oder nach einem anderen Code) mit der postMessage-Methode ausgegeben werden, und Abonnements für diesen Code können mit Methoden mit demselben Namen ein- und ausgeschaltet werden.

Heutzutage haben sich Boten wesentlich weiterentwickelt, um Folgendes zu ermöglichen:

  • Anhängen externer Nachrichtenquellen (DOM-Nachrichten, Fenstermeldungen, Datenänderungen, ein anderer Messenger usw.) - z. Die Events facette verwendet sie, um DOM-Ereignisse über den Milo Messenger verfügbar zu machen. Diese Funktionalität wird über eine separate Klasse MessageSource und ihre Unterklassen implementiert.
  • Definieren von benutzerdefinierten Messaging-APIs, die sowohl Nachrichten als auch Daten externer Nachrichten in interne Nachrichten übersetzen. Z.B. Die Data facette verwendet sie, um Änderungen zu übersetzen und DOM-Ereignisse in Datenänderungsereignisse einzugeben (siehe Modelle unten). Diese Funktionalität wird über eine separate Klasse MessengerAPI und deren Unterklassen implementiert.
  • Musterabonnements (mit regulären Ausdrücken). Z.B. Modelle (siehe unten) verwenden intern Musterabonnements, um tiefgreifende Modellwechselabonnements zu ermöglichen.
  • Definieren eines beliebigen Kontexts (dessen Wert im Abonnenten) als Teil des Abonnements mit folgender Syntax:
  • Erstellen eines Abonnements, das mit der Methode once nur einmal versendet wurde
  • Rückruf als dritten Parameter in postMessage übergeben (wir haben die variable Anzahl von Argumenten in postMessage berücksichtigt, wollten aber eine konsistentere Messaging-API als bei variablen Argumenten)
  • usw.

Der Hauptfehler bei der Entwicklung von Messenger war, dass alle Nachrichten synchron versendet wurden. Da JavaScript Single-Threaded ist, würden lange Sequenzen von Nachrichten mit komplexen Operationen die Benutzeroberfläche recht einfach sperren. Das Ändern von Milo, um den Nachrichtenversand asynchron zu machen, war einfach (alle Abonnenten werden mit setTimeout(subscriber, 0) in ihren eigenen Ausführungsblöcken aufgerufen, der Rest des Frameworks wurde geändert und die Anwendung war schwieriger - während die meisten Nachrichten asynchron versendet werden können, gibt es solche Viele müssen noch synchron versendet werden (viele DOM-Ereignisse, in denen Daten enthalten sind, oder Orte, an denen preventDefault aufgerufen wird). Standardmäßig werden Nachrichten jetzt asynchron versendet, und es gibt eine Möglichkeit, sie entweder beim Senden der Nachricht synchron zu machen:

oder wenn ein Abonnement erstellt wird:

Eine weitere Designentscheidung, die wir getroffen haben, war die Art und Weise, wie wir die Messenger-Methoden für die Objekte, die sie verwenden, offengelegt haben. Ursprünglich wurden Methoden einfach in das Objekt gemischt, aber es hat uns nicht gefallen, dass alle Methoden verfügbar sind und wir keine eigenständigen Messenger haben konnten. Daher wurden Messenger als separate Klasse basierend auf einer abstrakten Klasse Mixin neu implementiert.

Mit der Mixin-Klasse können Methoden einer Klasse auf einem Host-Objekt so verfügbar gemacht werden, dass beim Aufrufen von Methoden der Kontext weiterhin Mixin und nicht das Host-Objekt ist.

Es hat sich als sehr praktischer Mechanismus erwiesen - wir können die volle Kontrolle darüber haben, welche Methoden verfügbar gemacht werden, und die Namen nach Bedarf ändern. Es erlaubte uns auch, zwei Boten auf einem Objekt zu haben, das für Modelle verwendet wird.

Im Allgemeinen stellte sich heraus, dass Milo Messenger eine sehr solide Software ist, die sowohl im Browser als auch in Node.js eigenständig verwendet werden kann. Es wurde durch die Verwendung in unserem Produktions-Content-Management-System mit Zehntausenden Codezeilen gehärtet.

Nächstes Mal

Im nächsten Artikel werden wir uns mit dem möglicherweise nützlichsten und komplexesten Teil von Milo befassen. Die Milo-Modelle ermöglichen nicht nur einen sicheren und tiefen Zugriff auf Eigenschaften, sondern auch ein Ereignisabonnement für Änderungen auf jeder Ebene.

Wir werden auch unsere Implementierung von Minder und die Verwendung von Connector-Objekten zum Ein- oder Zwei-Wege-Binden von Datenquellen untersuchen.

Beachten Sie, dass dieser Artikel sowohl von Jason Green als auch von Evgeny Poberezkin verfasst wurde.

Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.