Verwendung von modifyClass, um das Kernverhalten zu ändern

Für fortgeschrittene Themes und Plugins bietet Discourse das modifyClass-System. Dies ermöglicht es Ihnen, die Funktionalität vieler JavaScript-Klassen des Kerns zu erweitern und zu überschreiben.

Wann modifyClass verwendet werden sollte

modifyClass sollte als letztes Mittel eingesetzt werden, wenn Ihre Anpassung nicht über die stabileren Anpassungs-APIs von Discourse (z. B. plugin-api-Methoden, plugin outlets, transformers) vorgenommen werden kann.

Der Kerncode kann sich jederzeit ändern. Daher könnten Anpassungen, die über modifyClass vorgenommen werden, jederzeit fehlschlagen. Bei der Verwendung dieser API sollten Sie sicherstellen, dass Sie Kontrollen implementiert haben, um diese Probleme abzufangen, bevor sie eine Produktionsumgebung erreichen. Sie könnten beispielsweise automatisierte Tests zum Theme/Plugin hinzufügen oder eine Staging-Umgebung nutzen, um eingehende Discourse-Updates gegen Ihr Theme/Plugin zu testen.

Grundlegende Verwendung

api.modifyClass kann verwendet werden, um die Funktionen und Eigenschaften jeder Klasse zu ändern, die über den Ember-Resolver zugänglich ist. Dazu gehören Routen, Controller, Services und Komponenten von Discourse.

modifyClass nimmt zwei Argumente entgegen:

  • resolverName (string) - Konstruieren Sie dies, indem Sie den Typ (z. B. component/controller/etc.) gefolgt von einem Doppelpunkt und dem (dasherisierten) Dateinamen der Klasse verwenden. Zum Beispiel: component:d-button, component:modal/login, controller:user, route:application usw.

  • callback (function) - eine Funktion, die die vorhandene Klassendefinition empfängt und dann eine erweiterte Version zurückgibt.

Zum Beispiel, um die click()-Aktion auf d-button zu ändern:

api.modifyClass(
  "component:d-button",
  (Superclass) =>
    class extends Superclass {
      @action
      click() {
        console.log("button was clicked");
        super.click();
      }
    }
);

Die class extends ...-Syntax spiegelt die von JS-Kindklassen wider. Im Allgemeinen kann jede Syntax/Funktion, die von Kindklassen unterstützt wird, hier angewendet werden. Dazu gehören super, statische Eigenschaften/Funktionen und mehr.

Es gibt jedoch einige Einschränkungen. Das modifyClass-System erkennt nur Änderungen am JavaScript-prototype der Klasse. Praktisch bedeutet dies:

  • Das Einführen oder Ändern eines constructor() wird nicht unterstützt

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // Dies wird nicht unterstützt. Der Konstruktor wird ignoriert
          }
        }
    );
    
  • Das Einführen oder Ändern von Klassenfeldern wird nicht unterstützt (obwohl einige dekorierte Klassenfelder wie @tracked verwendet werden können)

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          someField = "foo"; // NICHT UNTERSTÜTZT - nicht kopieren
          @tracked someOtherField = "foo"; // Dies ist in Ordnung
        }
    );
    
  • Einfache Klassenfelder in der ursprünglichen Implementierung können in keiner Weise überschrieben werden (obwohl, wie oben erwähnt, @tracked-Felder durch ein anderes @tracked-Feld überschrieben werden können)

    // Kerncode:
    class Foo extends Component {
      // Dieses Kernfeld kann nicht überschrieben werden
      someField = "original";
    
      // Dieses Kern-Tracked-Feld kann überschrieben werden, indem
      // `@tracked someTrackedField =` im modifyClass-Aufruf enthalten ist
      @tracked someTrackedField = "original";
    }
    

Wenn Sie feststellen, dass Sie diese Dinge tun möchten, dann ist Ihr Anwendungsfall möglicherweise besser durch das Einreichen eines PRs zur Einführung neuer APIs im Kern (z. B. plugin outlets, transformers oder spezielle APIs) abgedeckt.

Aufrüsten von Legacy-Syntax

In der Vergangenheit wurde modifyClass mit einer Objekt-Literal-Syntax wie dieser aufgerufen:

// Veraltete Syntax - nicht verwenden
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " some change";
  },
  pluginId: "some-unique-id",
});

Diese Syntax wird nicht mehr empfohlen und hat bekannte Fehler (z. B. Überschreiben von Gettern oder @actions). Jeglicher Code, der diese Syntax verwendet, sollte auf die oben beschriebene native Klassen-Syntax aktualisiert werden. Im Allgemeinen kann die Konvertierung erfolgen durch:

  1. Entfernen von pluginId - dies ist nicht mehr erforderlich
  2. Aktualisieren auf die moderne native Klassen-Syntax, wie oben beschrieben
  3. Testen Sie Ihre Änderungen

Fehlerbehebung

Klasse bereits initialisiert

Wenn Sie modifyClass in einem Initializer verwenden, sehen Sie möglicherweise diese Warnung in der Konsole:

Attempted to modify "{name}", but it was already initialized earlier in the boot process

In der Theme/Plugin-Entwicklung wird dieser Fehler normalerweise auf zwei Arten eingeführt:

  • Hinzufügen eines lookup() verursachte den Fehler

    Wenn Sie einen Singleton zu früh im Boot-Prozess lookup()en, führt dies dazu, dass spätere modifyClass-Aufrufe fehlschlagen. In dieser Situation sollten Sie versuchen, den Lookup so zu verschieben, dass er später erfolgt. Zum Beispiel würden Sie etwas wie dieses ändern:

    // Service im Initializer nachschlagen, dann zur Laufzeit verwenden (schlecht!)
    export default apiInitializer("0.8", (api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    Dazu:

    // 'Just in time' Lookup des Services (gut!)
    export default apiInitializer("0.8", (api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • Hinzufügen eines neuen modifyClass verursachte den Fehler

    Wenn der Fehler durch das Hinzufügen eines modifyClass-Aufrufs durch Ihr Theme/Plugin verursacht wird, müssen Sie ihn früher im Boot-Prozess verschieben. Dies geschieht häufig beim Überschreiben von Methoden auf Services (z. B. topicTrackingState) und auf Modellen, die früh im App-Boot-Prozess initialisiert werden (z. B. wird ein model:user für service:current-user initialisiert).

    Das Verschieben des modifyClass-Aufrufs früher im Boot-Prozess bedeutet normalerweise, den Aufruf in einen pre-initializer zu verschieben und ihn so zu konfigurieren, dass er vor dem ‘inject-discourse-objects’-Initialisierer von Discourse ausgeführt wird. Zum Beispiel:

    // (plugin)/assets/javascripts/discourse/pre-initializers/extend-user-for-my-plugin.js
    // oder
    // (theme)/javascripts/discourse/pre-initializers/extend-user-for-my-plugin.js
    
    import { withPluginApi } from "discourse/lib/plugin-api";
    
    export default {
      name: "extend-user-for-my-plugin",
      before: "inject-discourse-objects",
    
      initializeWithApi(api) {
        api.modifyClass("model:user", {
          myNewUserFunction() {
            return "hello world";
          },
        });
      },
    
      initialize() {
        withPluginApi("0.12.1", this.initializeWithApi);
      },
    };
    

    Diese Modifikation des Benutzer-Modells sollte nun ohne Warnung funktionieren, und die neue Methode wird auf dem currentUser-Objekt verfügbar sein.


Dieses Dokument ist versionskontrolliert - schlagen Sie Änderungen auf github vor.

16 „Gefällt mir“

Ich nehme an, es ist unmöglich oder zumindest unzuverlässig, zu versuchen, modifyClass (innerhalb der oben genannten legalen Anwendungsfälle) in einem Plugin für die Komponente eines anderen Plugins in derselben Installation zu verwenden?

Selbst wenn es sich um ein Plugin handelt, das im Kern enthalten ist (z. B. Chat oder Umfrage)?

1 „Gefällt mir“

Wenn beide Plugins installiert und aktiviert sind, sollte es problemlos funktionieren. Wenn das Ziel nicht installiert/aktiviert ist, erhalten Sie eine Warnung in der Konsole. Sie können jedoch den Parameter ignoreMissing verwenden, um diese zu unterdrücken.

api.modifyClass(
  "component:some-component",
  (Superclass) => ...,
  { ignoreMissing: true }
);

Natürlich gelten weiterhin die üblichen Ratschläge für modifyClass: Es sollte die letzte Möglichkeit sein und kann jederzeit fehlschlagen, daher sollten Sie sicherstellen, dass Ihre Tests gut genug sind, um Probleme schnell zu erkennen. Die Verwendung von Transformers wäre eine wesentlich sicherere Strategie.

3 „Gefällt mir“

Wie funktioniert das also, es verzögert die Anwendung, bis alle Komponenten von allen Plugins registriert und geladen wurden?

Ich glaube, ich habe einen Fall, der nicht zu funktionieren scheint.

1 „Gefällt mir“

Alle ES6-Module (einschließlich Komponenten) werden zuerst definiert, dann führen wir Pre-Initializers aus, dann führen wir reguläre Initializers aus. Also ja, zu dem Zeitpunkt, an dem ein Initializer ausgeführt wird, sind alle Komponenten auflösbar.

Gerne schaue ich es mir an, wenn Sie einen Ausschnitt oder einen Branch teilen können :eyes:

4 „Gefällt mir“

Mein Fehler, Sie müssen vorsichtig sein und den vollständigen Pfad angeben!

z.B.:

api.modifyClass("component:chat/modal/create-channel", :white_check_mark:

weder:

api.modifyClass("component:create-channel", :cross_mark:

noch sogar

api.modifyClass("component:modal/create-channel", :cross_mark:

reichen aus!

5 „Gefällt mir“