Erweiterbare Erweiterungen in JavaScript

Hallo habr

Wir machen Sie auf das lang erwartete Zusatzexemplar des gerade aus der Druckerei stammenden Buches " Expressive JavaScript " aufmerksam.


Für diejenigen, die nicht mit der Arbeit des Autors des Buches vertraut sind (bei aller Enzyklopädie wird es auch Anfängern gefallen) - wir empfehlen, dass Sie sich mit dem Artikel aus seinem Blog vertraut machen; Der Artikel beschreibt Gedanken zum Organisieren von Erweiterungen in JavaScript.

Heutzutage ist es Mode geworden, große Systeme in Form vieler separater Pakete zu strukturieren. Die Grundidee dieses Ansatzes besteht darin, die Benutzer nicht durch Implementierung einer Funktion auf eine bestimmte (von Ihnen vorgeschlagene) Funktion zu beschränken, sondern diese Funktion als separates Paket bereitzustellen, das eine Person zusammen mit dem Basissystempaket herunterladen kann.
Dazu benötigen Sie im Allgemeinen ...

  • Die Möglichkeit, nicht benötigte Funktionen nicht herunterzuladen, ist auf Client-Systemen besonders nützlich.
  • Die Möglichkeit, eine Funktionalität, die Ihre Ziele nicht erfüllt, durch eine andere Implementierung zu ersetzen. Dadurch wird auch die Belastung der Kernelmodule reduziert - es ist nicht erforderlich, alle möglichen praktischen Fälle mit ihrer Hilfe abzudecken.
  • Überprüfen von Kernel-Schnittstellen unter realen Bedingungen - Indem Sie grundlegende Funktionen auf der dem Client zugewandten Oberfläche implementieren, müssen Sie diese Oberfläche mindestens so leistungsfähig machen, dass sie diese Funktionen unterstützt. Auf diese Weise können Sie sicher sein, dass es möglich ist, darauf ähnliche Dinge aufzubauen, die von Drittentwicklern geschrieben wurden.
  • Isolierung zwischen Anlagenteilen. Die Projektteilnehmer können einfach nach dem Paket suchen, an dem sie interessiert sind. Pakete können versioniert, als unerwünscht markiert oder ersetzt werden, ohne den Kerncode zu beeinflussen.

Dieser Ansatz ist üblicherweise mit einer erhöhten Komplexität verbunden. Um Benutzern den Einstieg zu erleichtern, können Sie ihnen ein "All-inclusive" -Umhüllungspaket zur Verfügung stellen. Früher oder später müssen sie diese Shell jedoch möglicherweise entfernen und bestimmte Hilfspakete installieren und konfigurieren, was manchmal schwieriger ist, als nur zu wechseln ein weiteres Merkmal in der monolithischen Bibliothek.

In diesem Artikel werden wir versuchen, Optionen für Erweiterungsmechanismen zu erörtern, die eine „umfangreiche Erweiterbarkeit“ unterstützen, und Ihnen das Erstellen neuer Erweiterungspunkte zu ermöglichen, wenn dies nicht vorgesehen ist.

Erweiterbarkeit


Was wollen wir von einem erweiterbaren System? Zuallererst sollte es natürlich die Fähigkeit haben, seine eigenen Fähigkeiten unter Verwendung von externem Code zu erweitern.

Das reicht aber kaum. Lassen Sie mich über ein dummes Problem schweifen, auf das ich einmal gestoßen bin. Ich entwickle einen Code-Editor. In einer der früheren Versionen dieses Editors konnten Sie den Stil für eine bestimmte Zeile im Clientcode festlegen. Es war großartig - selektives Linienlayout.

Mit Ausnahme des Falls, wenn versucht wird, das Erscheinungsbild der Linie sofort aus zwei Abschnitten des Codes zu ändern - und diese Versuche beginnen zu stoßen. Die zweite auf die Linie angewendete Erweiterung überschreibt den Stil der ersten Erweiterung. Wenn der erste Code versucht, das Design zu entfernen, das er irgendwo im weiteren Teil des Codes erstellt hat, überschreibt er den durch das zweite Codefragment eingeführten Stil.

Wir haben es geschafft, eine Lösung zu finden, die es ermöglicht, die Erweiterung hinzuzufügen (und zu löschen) und nicht festzulegen, sodass die beiden Erweiterungen mit derselben Leitung interagieren können, ohne die Arbeit des jeweils anderen zu sabotieren.

Generell muss sichergestellt werden, dass die Erweiterungen kombiniert werden können, auch wenn sie sich ihrer Existenz nicht bewusst sind - ohne Konflikte zwischen ihnen zu verursachen.

Dazu müssen Sie sicherstellen, dass eine beliebige Anzahl von Akteuren jeden Erweiterungspunkt beeinflussen kann. Wie genau mehrere Effekte verarbeitet werden, hängt von der jeweiligen Situation ab. Hier sind einige Ansätze, die Sie nützlich finden könnten:

  • Alle von ihnen werden wirksam. Wenn Sie beispielsweise einem Element eine CSS-Klasse hinzufügen oder ein Widget anzeigen, werden beide Funktionen gleichzeitig hinzugefügt. Oft müssen sie noch sortiert werden: Widgets sollten in einer vorhersehbaren, genau definierten Reihenfolge angezeigt werden.
  • Sie reihen sich in Form eines Förderers aneinander. Ein Beispiel ist ein Handler, der Änderungen filtern kann, die einem Dokument hinzugefügt wurden, bevor sie vorgenommen wurden. Jede Änderung wird zuerst einem Handler zugeführt, der sie wiederum zusätzlich ändern kann. Die Bestellung ist in diesem Fall unkritisch, kann aber einen Unterschied machen.
  • Sie können einen First-Come-First-Serve-Ansatz für Event-Handler anwenden. Jeder Handler hat die Möglichkeit, dem Ereignis zu dienen, bis einer von ihnen angibt, dass er sich bereits damit befasst hat. Danach werden die angestellten Handler nicht mehr befragt.
  • Es kommt auch vor, dass Sie wirklich einen bestimmten Wert auswählen müssen - zum Beispiel den Wert eines bestimmten Konfigurationsparameters bestimmen. Es kann ratsam sein, einen bestimmten Operator (z. B. logisch und logisch oder Minimum oder Maximum) zu verwenden, um die Anzahl der Eingabewerte für eine Position zu begrenzen. Beispielsweise kann ein Editor in den schreibgeschützten Modus wechseln, wenn eine Erweiterung dies anordnet. Sie können entweder den Maximalwert des Dokuments oder die Minimalanzahl der Werte festlegen, die an diese Option gemeldet werden.

In vielen solchen Fällen ist die Reihenfolge wichtig. Dies bedeutet, dass die Priorität der angewendeten Effekte steuerbar und vorhersehbar sein sollte.

Auf dieser Front sind zwingende Erweiterungssysteme, die auf der Verwendung von Nebenwirkungen beruhen, normalerweise nicht in der Lage, damit umzugehen. Beispielsweise addEventListener die vom Browser-DOM-Modell addEventListener Operation addEventListener dass die Ereignishandler genau in der Reihenfolge aufgerufen werden, in der sie registriert wurden. Dies ist normal, wenn alle Aufrufe von einem einzigen System gesteuert werden oder wenn die Reihenfolge der Vorgänge nicht wirklich wichtig ist. Wenn Sie jedoch mit vielen Softwarefragmenten arbeiten müssen, die unabhängig voneinander Handler hinzufügen, kann es sehr schwierig sein, vorherzusagen, welche von ihnen überhaupt aufgerufen werden.

Einfacher Ansatz


Um Ihnen ein einfaches Beispiel zu geben: Ich habe zunächst eine modulare Strategie auf ProseMirror angewendet, ein System zum Bearbeiten von Rich-Text. Der Kern dieses Systems an sich ist im Prinzip nutzlos - es basiert ausschließlich auf zusätzlichen Paketen, die die Struktur von Dokumenten beschreiben, Schlüssel binden und die Geschichte der Stornierungen anführen. Obwohl die Verwendung dieses Systems sehr schwierig ist, wurde es für Produkte übernommen, die ein benutzerdefiniertes Textdesign erfordern, das in klassischen Editoren nicht verfügbar ist.

Der in ProseMirror verwendete Erweiterungsmechanismus ist relativ einfach. Beim Erstellen des Editors gibt der Client-Code ein einzelnes Array verbundener Objekte an. Jedes dieser Plugins kann sich auf die Arbeit des Editors auswirken, z. B. Statusdaten hinzufügen oder Schnittstellenereignisse verarbeiten.

Alle diese Aspekte sind so konzipiert, dass sie mit einer Reihe von Konfigurationswerten unter Verwendung der im vorherigen Abschnitt beschriebenen Strategien funktionieren. Wenn Sie beispielsweise mehrere Tastenzuweisungen angeben, bestimmt die Reihenfolge, in der die Keymap-Plug-In-Instanzen angegeben werden, deren Priorität. Die erste Keymap, die damit umgehen kann, erhält einen bestimmten Schlüssel in der Verarbeitung.

Normalerweise ist dieser Mechanismus ziemlich leistungsfähig und wird aktiv genutzt. In einem bestimmten Stadium wird es jedoch kompliziert und es wird unangenehm, damit zu arbeiten.

  • Wenn das Plugin viele Effekte hat, können Sie entweder hoffen, dass sie in dieser Reihenfolge auf andere Plugins angewendet werden, oder Sie müssen sie in kleinere Plugins aufteilen, damit Sie sie richtig organisieren können.
  • Im Allgemeinen wird das Organisieren von Plugins sehr sensibel, da der Endbenutzer nicht immer versteht, welche Plugins den Betrieb anderer Plugins beeinflussen können, wenn sie eine höhere Priorität erhalten. Alle Fehler treten normalerweise nur zur Laufzeit auf, wenn bestimmte Funktionen verwendet werden - daher sind sie leicht zu übersehen.
  • Plugins, die auf anderen Plugins basieren, sollten dies dokumentieren - und es bleibt zu hoffen, dass Benutzer nicht vergessen, ihre Abhängigkeiten zu aktivieren (in der richtigen Reihenfolge).

CodeMirror in Version 6 ist ein umgeschriebener Editor mit demselben Namen . In der sechsten Version versuche ich, einen modularen Ansatz zu entwickeln. Dies erfordert ein aussagekräftigeres Erweiterungssystem. Sehen wir uns einige der Herausforderungen an, die mit dem Entwurf eines solchen Systems verbunden sind.

Bestellung


Es ist einfach, ein System zu entwerfen, das die vollständige Kontrolle über die Reihenfolge der Erweiterungen bietet. Es ist jedoch sehr schwierig, ein solches System zu entwerfen, das gleichzeitig angenehm zu bedienen ist und es Ihnen ermöglicht, den Code unabhängiger Erweiterungen ohne umfangreiche und gründliche manuelle Eingriffe zu kombinieren.

Wenn es um die Bestellung geht, zieht es vor, Prioritätswerte anzuwenden. Ein ähnliches Beispiel ist die CSS z-index Eigenschaft z-index , mit der Sie eine Zahl festlegen können, die angibt, wie tief sich das Element auf dem Stapel befindet.

Da Stylesheets manchmal lächerlich große z-index Werte haben, ist es offensichtlich, dass diese Art der Prioritätsangabe problematisch ist. Ein bestimmtes Modul "weiß" einzeln nicht, welche Prioritätswerte andere Module anzeigen. Optionen sind nur Punkte in einem undefinierten Zahlenbereich. Sie können unerschwinglich hohe (oder zutiefst negative) Werte angeben, um diese Skala zu erreichen, aber alles andere ist ein Ratespiel.

Diese Situation kann etwas verbessert werden, indem eine begrenzte Anzahl klar definierter Prioritätskategorien definiert wird, sodass Nebenstellen nach dem ungefähren „Level“ ihrer Priorität klassifiziert werden können. Trotzdem muss man die Verbindungen innerhalb dieser Kategorien irgendwie aufbrechen.

Gruppierung und Deduplizierung


Wie oben erwähnt, kann es zu Situationen kommen, in denen einige Erweiterungen andere verwenden, sobald Sie sich ernsthaft auf Erweiterungen verlassen. Das gegenseitige Abhängigkeitsmanagement lässt sich nicht gut skalieren. Daher wäre es hilfreich, wenn Sie eine Gruppe von Erweiterungen gleichzeitig abziehen könnten.

In diesem Fall wird sich das Problem der Bestellung jedoch noch verschärfen. Ein weiteres Problem wird entstehen. Viele andere Erweiterungen können gleichzeitig von einer bestimmten Erweiterung abhängen. Wenn Sie sie als Werte darstellen, kann es durchaus vorkommen, dass mehrere Downloads derselben Erweiterung durchgeführt werden. In einigen Fällen, beispielsweise beim Zuweisen von Schlüsseln oder beim Behandeln von Ereignishandlern, ist dies normal. In anderen Fällen, zum Beispiel beim Verfolgen des Abbruchverlaufs oder beim Arbeiten mit einer Tooltip-Bibliothek, wäre ein solcher Ansatz eine Verschwendung von Ressourcen mit dem Risiko, dass etwas kaputt geht.

Aus diesem Grund sind wir gezwungen, bei der Zusammenstellung von Erweiterungen auf das Erweiterungssystem zuzugreifen, da dies mit der Verwaltung von Abhängigkeiten verbunden ist. Sie müssen in der Lage sein, die Erweiterungen zu erkennen, die nicht dupliziert werden sollen, und nur eine Instanz von jeder dieser Erweiterungen herunterladen.

Da jedoch in den meisten Fällen Erweiterungen konfiguriert werden können und sich alle Instanzen einer bestimmten Erweiterung etwas voneinander unterscheiden, können wir nicht nur eine Instanz der Erweiterung nehmen und verwenden - wir müssen sie auf sinnvolle Weise kombinieren (oder einen Fehler melden, wenn dies nicht möglich ist).

Projekt


Hier werde ich beschreiben, was in CodeMirror 6 getan wurde. Ich schlage dieses Beispiel als Lösung vor und nicht als die einzig wahre Lösung. Es ist möglich, dass sich dieses System weiterentwickelt, wenn sich die Bibliothek stabilisiert.

Das Hauptgrundelement in diesem Ansatz heißt Verhalten. Verhalten ist genau das, was Sie durch Angabe von Werten erweitern können. Betrachten Sie als Beispiel das Verhalten eines Statusfelds, in dem Sie mithilfe von Erweiterungen neue Felder hinzufügen können, die eine Beschreibung jedes Felds enthalten. Oder das Verhalten eines browserbasierten Ereignishandlers, bei dem Sie mithilfe von Erweiterungen Ihre eigenen Handler hinzufügen können.

Aus Sicht des Verbraucherverhaltens geben diese Verhaltensweisen, die in einer bestimmten Instanz des Editors konfiguriert sind, eine geordnete Folge von Werten an, wobei Werte mit höherer Priorität an erster Stelle stehen. Jedes Verhalten hat einen Typ, und die Werte dafür müssen mit diesem Typ übereinstimmen.

Ein Verhalten wird als Wert dargestellt, der sowohl zum Deklarieren einer Instanz eines Verhaltens als auch zum Verweisen auf die Werte verwendet wird, die das Verhalten möglicherweise hat. Beispielsweise kann eine Erweiterung, die den Hintergrund einer Zeilennummer definiert, ein Verhalten definieren, das es einem anderen Code ermöglicht, diesem Hintergrund neue Markierungen hinzuzufügen.

Eine Erweiterung ist ein Wert, der bei der Konfiguration des Editors verwendet werden kann. Während der Initialisierung wird ein Array von Erweiterungen gemeldet. Jede Erweiterung ist in keinem oder mehreren Verhalten zulässig.

Die einfachste Art der Erweiterung ist eine Instanz des Verhaltens. Wenn Sie den Wert für dieses Verhalten festlegen, erhalten Sie als Antwort den Wert der Erweiterung, die dieses Verhalten implementiert.
Eine Folge von Nebenstellen kann auch zu einer einzigen Nebenstelle zusammengefasst werden. Beispielsweise können in der Konfiguration des Editors für eine bestimmte Programmiersprache eine Reihe anderer Erweiterungen abgerufen werden, insbesondere eine Grammatik zum Parsen und Hervorheben der Sprache, Informationen zum Einrücken sowie eine Informationsquelle zur Vervollständigung, die den Code in dieser Sprache intelligent ergänzt. So erhalten Sie eine Spracherweiterung, die einfach alle erforderlichen Erweiterungen sammelt, die den kumulativen Wert ergeben.

Wenn wir eine einfache Version eines solchen Systems beschreiben, könnten wir damit aufhören und einfach die verschachtelten Erweiterungen in ein einzelnes Array von Erweiterungen für das Verhalten einfügen. Dann könnten sie nach Verhaltenstypen gruppiert werden und geordnete Folgen von Verhaltenswerten erhalten.

Wir haben jedoch noch keine Deduplizierung herausgefunden und benötigen eine umfassendere Kontrolle über die Bestellung.

Zu den Werten des dritten Typs gehören eindeutige Erweiterungen , mit denen die Deduplizierung sichergestellt wird. Erweiterungen, die Sie nicht zweimal im selben Editor instanziieren möchten, sind genau das. Um eine solche Erweiterung zu bestimmen, wird ein Spezifikationstyp angegeben, dh der Typ des vom Erweiterungskonstruktor erwarteten Konfigurationswerts, und eine Instanziierungsfunktion, die ein Array solcher Spezifikationswerte verwendet und die Erweiterung zurückgibt.
Einzigartige Erweiterungen erschweren das Auflösen einer Sammlung von Erweiterungen in eine Reihe von Verhaltensweisen. Solange die ausgerichteten Erweiterungen eindeutige Erweiterungen enthalten, sollte der Auflösungsmechanismus den Typ der eindeutigen Erweiterung auswählen, alle Instanzen erfassen, die Instanziierungsfunktion mit ihren Spezifikationswerten aufrufen und diese durch das Ergebnis (in einer Instanz) ersetzen.

(Es gibt noch ein Problem - sie müssen in einer bestimmten Reihenfolge gelöst werden. Wenn Sie zuerst die eindeutige Erweiterung X aktivieren, dann aber die Erweiterung Y in ein anderes X auflösen, ist dies ein Fehler, da alle Instanzen von X miteinander kombiniert werden müssen. Da das Instanziieren der Erweiterungen eine reine Operation ist, das damit konfrontierte System führt es durch Ausprobieren aus, startet den Prozess neu - und zeichnet die geklärten Informationen auf.)
Lassen Sie uns abschließend über die Priorität sprechen. In diesem Fall besteht der grundlegende Ansatz darin, die Reihenfolge beizubehalten, in der die Erweiterungen gemeldet wurden. Zusammengesetzte Erweiterungen werden genau an der Position ausgerichtet und in diese Reihenfolge eingebaut, an der sie sich zum ersten Mal treffen. Das Ergebnis der Auflösung einer eindeutigen Erweiterung wird auch an der Stelle eingefügt, an der sie zum ersten Mal auftritt.

Erweiterungen können jedoch einige ihrer Untererweiterungen einer Kategorie mit einer anderen Priorität zuweisen. Das System bestimmt die Arten solcher Kategorien: Rollback (wird wirksam, wenn andere Dinge geschehen), standardmäßig erweitern (höhere Priorität als der Bulk) und neu definieren (sollte sich möglicherweise ganz oben befinden). Die eigentliche Bestellung erfolgt zuerst nach Kategorie und dann nach Startposition.

Eine Erweiterung mit einer Tastenzuweisung mit niedriger Priorität und ein Ereignishandler mit normaler Priorität können also eine zusammengesetzte Erweiterung ergeben, die auf einer Erweiterung mit einer Tastenzuweisung basiert (in diesem Fall müssen Sie nicht wissen, welche Verhaltensweisen darin enthalten sind, mit der Priorität „Rollback“ und einer Instanz des Verhaltens Ereignishandler.

Die wichtigste Errungenschaft scheint zu sein, dass wir die Fähigkeit erworben haben, Erweiterungen zu kombinieren, unabhängig davon, was in jeder dieser Erweiterungen getan wird. In den Erweiterungen, die wir bisher modelliert haben, darunter: zwei Parsing-Systeme mit demselben syntaktischen Verhalten, Syntaxhervorhebung, intelligenter Einrückungsdienst, Stornierungsverlauf, Hintergrund der Zeilennummern, automatisches Schließen in Klammern, Schlüsselzuweisung und Mehrfachauswahl - alles funktioniert gut.

Um ein solches System zu verwenden, müssen Sie einige neue Konzepte beherrschen, und es ist definitiv komplizierter als herkömmliche imperative Systeme, die in der JavaScript-Community akzeptiert werden (nennen Sie eine Methode zum Hinzufügen / Entfernen eines Effekts). Die Fähigkeit, Erweiterungen korrekt zu verknüpfen, scheint diese Kosten jedoch zu rechtfertigen.

Source: https://habr.com/ru/post/de484470/


All Articles