Entwerfen eines JavaScript-Plugin-Systems

Avatar of Bryan Braun
Bryan Braun am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

WordPress hat Plugins. jQuery hat Plugins. Gatsby, Eleventy und Vue auch.

Plugins sind ein übliches Merkmal von Bibliotheken und Frameworks, und das aus gutem Grund: Sie ermöglichen es Entwicklern, Funktionalität sicher und skalierbar hinzuzufügen. Das macht das Kernprojekt wertvoller und baut eine Community auf – alles ohne zusätzlichen Wartungsaufwand. Was für ein Schnäppchen!

Wie baut man also ein Plugin-System? Beantworten wir diese Frage, indem wir eines selbst in JavaScript erstellen.

Ich verwende das Wort „Plugin“, aber diese Dinge werden manchmal auch anders genannt, wie „Erweiterungen“, „Add-ons“ oder „Module“. Wie auch immer Sie sie nennen, das Konzept (und der Nutzen) ist derselbe.

Lassen Sie uns ein Plugin-System bauen

Beginnen wir mit einem Beispielprojekt namens BetaCalc. Ziel von BetaCalc ist es, ein minimalistischer JavaScript-Rechner zu sein, zu dem andere Entwickler „Buttons“ hinzufügen können. Hier ist etwas Basisfunktionalität, um uns den Einstieg zu erleichtern.

// The Calculator
const betaCalc = {
  currentValue: 0,
  
  setValue(newValue) {
    this.currentValue = newValue;
    console.log(this.currentValue);
  },
  
  plus(addend) {
    this.setValue(this.currentValue + addend);
  },
  
  minus(subtrahend) {
    this.setValue(this.currentValue - subtrahend);
  }
};


// Using the calculator
betaCalc.setValue(3); // => 3
betaCalc.plus(3);     // => 6
betaCalc.minus(2);    // => 4

Wir definieren unseren Rechner als Objekt-Literal, um die Dinge einfach zu halten. Der Rechner funktioniert, indem er sein Ergebnis über console.log ausgibt.

Die Funktionalität ist derzeit sehr begrenzt. Wir haben eine setValue-Methode, die eine Zahl entgegennimmt und sie auf dem „Bildschirm“ anzeigt. Wir haben auch plus- und minus-Methoden, die eine Operation auf dem aktuell angezeigten Wert durchführen.

Es ist Zeit, mehr Funktionalität hinzuzufügen. Beginnen wir mit der Erstellung eines Plugin-Systems.

Das kleinste Plugin-System der Welt

Wir beginnen mit der Erstellung einer register-Methode, die andere Entwickler verwenden können, um ein Plugin bei BetaCalc zu registrieren. Die Aufgabe dieser Methode ist einfach: Sie nimmt das externe Plugin, holt seine exec-Funktion und hängt sie als neue Methode an unseren Rechner an.

// The Calculator
const betaCalc = {
  // ...other calculator code up here


  register(plugin) {
    const { name, exec } = plugin;
    this[name] = exec;
  }
};

Und hier ist ein Beispiel-Plugin, das unserem Rechner einen „Quadrat“-Button gibt.

// Define the plugin
const squaredPlugin = {
  name: 'squared',
  exec: function() {
    this.setValue(this.currentValue * this.currentValue)
  }
};


// Register the plugin
betaCalc.register(squaredPlugin);

In vielen Plugin-Systemen ist es üblich, dass Plugins zwei Teile haben:

  1. Code, der ausgeführt werden soll
  2. Metadaten (wie Name, Beschreibung, Versionsnummer, Abhängigkeiten usw.)

In unserem Plugin enthält die exec-Funktion unseren Code, und der name sind unsere Metadaten. Wenn das Plugin registriert wird, wird die Exec-Funktion direkt an unser betaCalc-Objekt als Methode angehängt, wodurch sie Zugriff auf das this von BetaCalc erhält.

BetaCalc hat nun einen neuen „Quadrat“-Button, der direkt aufgerufen werden kann.

betaCalc.setValue(3); // => 3
betaCalc.plus(2);     // => 5
betaCalc.squared();   // => 25
betaCalc.squared();   // => 625

An diesem System gibt es vieles, was gefällt. Das Plugin ist ein einfaches Objekt-Literal, das an unsere Funktion übergeben werden kann. Das bedeutet, dass Plugins über npm heruntergeladen und als ES6-Module importiert werden können. Einfache Distribution ist super wichtig!

Aber unser System hat einige Schwächen.

Indem wir Plugins Zugriff auf das this von BetaCalc geben, erhalten sie Lese-/Schreibzugriff auf den gesamten Code von BetaCalc. Das ist zwar nützlich, um den currentValue abzurufen und festzulegen, aber es ist auch gefährlich. Wenn ein Plugin eine interne Funktion (wie setValue) neu definieren würde, könnte dies unerwartete Ergebnisse für BetaCalc und andere Plugins zur Folge haben. Dies verstößt gegen das Offen-Geschlossen-Prinzip, das besagt, dass eine Software-Entität offen für Erweiterungen, aber geschlossen für Modifikationen sein sollte.

Außerdem arbeitet die „Quadrat“-Funktion durch die Erzeugung von Seiteneffekten. Das ist in JavaScript nicht ungewöhnlich, aber es fühlt sich nicht gut an – besonders, wenn andere Plugins da drin sein und denselben internen Zustand manipulieren könnten. Ein stärker funktionaler Ansatz würde viel dazu beitragen, unser System sicherer und vorhersehbarer zu machen.

Eine bessere Plugin-Architektur

Lassen Sie uns einen weiteren Anlauf für eine bessere Plugin-Architektur nehmen. Das nächste Beispiel ändert sowohl den Rechner als auch seine Plugin-API.

// The Calculator
const betaCalc = {
  currentValue: 0,
  
  setValue(value) {
    this.currentValue = value;
    console.log(this.currentValue);
  },
 
  core: {
    'plus': (currentVal, addend) => currentVal + addend,
    'minus': (currentVal, subtrahend) => currentVal - subtrahend
  },


  plugins: {},    


  press(buttonName, newVal) {
    const func = this.core[buttonName] || this.plugins[buttonName];
    this.setValue(func(this.currentValue, newVal));
  },


  register(plugin) {
    const { name, exec } = plugin;
    this.plugins[name] = exec;
  }
};
  
// Our Plugin
const squaredPlugin = { 
  name: 'squared',
  exec: function(currentValue) {
    return currentValue * currentValue;
  }
};


betaCalc.register(squaredPlugin);


// Using the calculator
betaCalc.setValue(3);      // => 3
betaCalc.press('plus', 2); // => 5
betaCalc.press('squared'); // => 25
betaCalc.press('squared'); // => 625

Wir haben hier ein paar bemerkenswerte Änderungen.

Erstens haben wir die Plugins von den „Core“-Rechnermethoden (wie plus und minus) getrennt, indem wir sie in ein eigenes Objekt plugins gepackt haben. Das Speichern unserer Plugins in einem plugin-Objekt macht unser System sicherer. Jetzt können Plugins, die darauf zugreifen, die BetaCalc-Eigenschaften nicht sehen – sie sehen nur die Eigenschaften von betaCalc.plugins.

Zweitens haben wir eine press-Methode implementiert, die die Funktion des Buttons anhand des Namens nachschlägt und dann aufruft. Jetzt übergeben wir beim Aufruf der exec-Funktion eines Plugins den aktuellen Rechnerwert (currentValue) und erwarten, dass sie den neuen Rechnerwert zurückgibt.

Im Wesentlichen wandelt diese neue press-Methode alle unsere Rechnerbuttons in reine Funktionen um. Sie nehmen einen Wert entgegen, führen eine Operation durch und geben das Ergebnis zurück. Dies hat viele Vorteile:

  • Es vereinfacht die API.
  • Es erleichtert das Testen (sowohl von BetaCalc als auch von den Plugins selbst).
  • Es reduziert die Abhängigkeiten unseres Systems und macht es locker gekoppelter.

Diese neue Architektur ist limitierter als das erste Beispiel, aber auf gute Weise. Wir haben im Wesentlichen Leitplanken für Plugin-Autoren aufgestellt und sie auf nur die Art von Änderungen beschränkt, die wir von ihnen wünschen.

Tatsächlich könnte sie zu restriktiv sein! Jetzt können unsere Rechnerplugins nur Operationen auf dem currentValue durchführen. Wenn ein Plugin-Autor erweiterte Funktionen wie einen „Memory“-Button oder eine Möglichkeit zur Nachverfolgung des Verlaufs hinzufügen möchte, könnte er das nicht tun.

Vielleicht ist das in Ordnung. Das Ausmaß der Macht, das Sie Plugin-Autoren geben, ist eine feine Balance. Ihnen zu viel Macht zu geben, könnte die Stabilität Ihres Projekts beeinträchtigen. Aber ihnen zu wenig Macht zu geben, macht es ihnen schwer, ihre Probleme zu lösen – in diesem Fall könnten Sie genauso gut keine Plugins haben.

Was könnten wir noch tun?

Es gibt noch viel mehr, was wir tun könnten, um unser System zu verbessern.

Wir könnten Fehlerbehandlung hinzufügen, um Plugin-Autoren zu benachrichtigen, wenn sie vergessen, einen Namen zu definieren oder einen Wert zurückzugeben. Es ist gut, wie ein QA-Entwickler zu denken und sich vorzustellen, wie unser System brechen könnte, damit wir diese Fälle proaktiv behandeln können.

Wir könnten den Umfang dessen erweitern, was ein Plugin tun kann. Derzeit kann ein BetaCalc-Plugin einen Button hinzufügen. Aber was wäre, wenn es auch Rückrufe für bestimmte Lebenszyklusereignisse registrieren könnte – z. B. wenn der Rechner kurz davor steht, einen Wert anzuzeigen? Oder was wäre, wenn es einen dedizierten Ort gäbe, an dem es einen Teil des Zustands über mehrere Interaktionen hinweg speichern könnte? Würde das einige neue Anwendungsfälle eröffnen?

Wir könnten auch die Plugin-Registrierung erweitern. Was wäre, wenn ein Plugin mit einigen anfänglichen Einstellungen registriert werden könnte? Könnte das die Plugins flexibler machen? Was wäre, wenn ein Plugin-Autor eine ganze Suite von Buttons registrieren möchte, anstatt nur einen – wie ein „BetaCalc Statistics Pack“? Welche Änderungen wären erforderlich, um das zu unterstützen?

Ihr Plugin-System

Sowohl BetaCalc als auch sein Plugin-System sind bewusst einfach gehalten. Wenn Ihr Projekt größer ist, sollten Sie sich mit einigen anderen Plugin-Architekturen befassen.

Ein guter Ausgangspunkt ist die Betrachtung bestehender Projekte auf Beispiele erfolgreicher Plugin-Systeme. Für JavaScript könnte das jQuery, Gatsby, D3, CKEditor oder andere sein.

Sie sollten sich auch mit verschiedenen JavaScript-Designmustern vertraut machen. (Addy Osmani hat ein Buch dazu.) Jedes Muster bietet eine andere Schnittstelle und einen anderen Grad an Kopplung, was Ihnen viele gute Plugin-Architekturoptionen zur Auswahl gibt. Das Bewusstsein für diese Optionen hilft Ihnen, die Bedürfnisse aller Nutzer Ihres Projekts besser abzuwägen.

Neben den Mustern selbst gibt es viele gute Prinzipien der Softwareentwicklung, auf die Sie zurückgreifen können, um solche Entscheidungen zu treffen. Ich habe im Laufe des Artikels einige erwähnt (wie das Offen-Geschlossen-Prinzip und lose Kopplung), aber einige andere relevante sind das Gesetz von Demeter und Dependency Injection.

Ich weiß, es klingt nach viel, aber Sie müssen recherchieren. Nichts ist schmerzhafter, als alle ihre Plugins neu schreiben zu lassen, nur weil Sie die Plugin-Architektur ändern mussten. Es ist ein schneller Weg, Vertrauen zu verlieren und Leute davon abzuhalten, in Zukunft beizutragen.

Fazit

Ein gutes Plugin-System von Grund auf zu schreiben, ist schwierig! Sie müssen viele Überlegungen abwägen, um ein System zu entwickeln, das die Bedürfnisse aller erfüllt. Ist es einfach genug? Leistungsfähig genug? Wird es langfristig funktionieren?

Es ist die Mühe wert. Ein gutes Plugin-System hilft allen. Entwickler erhalten die Freiheit, ihre Probleme zu lösen. Endnutzer erhalten eine große Anzahl von optionalen Funktionen zur Auswahl. Und Sie können ein Ökosystem und eine Community rund um Ihr Projekt aufbauen. Es ist eine Win-Win-Win-Situation.