modifyClass zur Änderung des Kernverhaltens verwenden

Für fortgeschrittene Themes und Plugins bietet Discourse das modifyClass-System. Damit können Sie Funktionalitäten in vielen der JavaScript-Klassen des Kerns erweitern und überschreiben.

Wann modifyClass verwendet werden sollte

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

Der Code des Kerns kann sich jederzeit ändern. Daher können Anpassungen, die über modifyClass vorgenommen werden, jederzeit fehlerhaft werden. Wenn Sie diese API verwenden, sollten Sie sicherstellen, dass Sie Mechanismen implementiert haben, um solche Probleme zu erkennen, bevor sie auf einer Produktivumgebung auftreten. Zum Beispiel könnten Sie automatisierte Tests für das Theme/das Plugin hinzufügen oder eine Staging-Umgebung nutzen, um eingehende Discourse-Updates gegen Ihr Theme/Ihr Plugin zu testen.

Grundlegende Verwendung

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

modifyClass erwartet zwei Argumente:

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

  • callback (Funktion) – eine Funktion, die die vorhandene Klassendefinition erhält und dann eine erweiterte Version zurückgibt.

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

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

Die Syntax class extends ... imitiert die von JS-Unterklassen. Im Allgemeinen können hier alle Syntaxelemente/Funktionen angewendet werden, die von Unterklassen unterstützt werden. Dazu gehören super, statische Eigenschaften/Funktionen und mehr.

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

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

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // This is not supported. The constructor will be ignored
          }
        }
    );
    
  • 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"; // NOT SUPPORTED - do not copy
          @tracked someOtherField = "foo"; // This is ok
        }
    );
    
  • Einfache Klassenfelder in der ursprünglichen Implementierung können auf keine Weise überschrieben werden (obwohl, wie oben erwähnt, @tracked-Felder durch ein anderes @tracked-Feld überschrieben werden können)

    // Core code:
    class Foo extends Component {
      // This core field cannot be overridden
      someField = "original";
    
      // This core tracked field can be overridden by including
      // `@tracked someTrackedField =` in the modifyClass call
      @tracked someTrackedField = "original";
    }
    

Wenn Sie feststellen, dass Sie diese Dinge tun möchten, könnte Ihr Anwendungsfall besser durch das Erstellen eines Pull Requests abgedeckt werden, um neue APIs im Kern einzuführen (z. B. plugin outlets, transformers oder maßgeschneiderte APIs).

Aufrüstung der Legacy-Syntax

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

// Outdated syntax - do not use
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. das Überschreiben von Gettern oder @actions). Jeder Code, der diese Syntax verwendet, sollte so aktualisiert werden, dass die oben beschriebene Native-Class-Syntax verwendet wird. Im Allgemeinen kann die Konvertierung wie folgt erfolgen:

  1. Entfernen von pluginId – dies ist nicht mehr erforderlich
  2. Aktualisierung auf die oben beschriebene moderne Native-Class-Syntax
  3. Testen Ihrer Ä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 tritt dieser Fehler normalerweise auf zwei Arten auf:

  • Hinzufügen einer lookup() hat den Fehler verursacht

    Wenn Sie ein 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 später auszuführen. Zum Beispiel würden Sie etwas wie Folgendes ändern:

    // Lookup service in initializer, then use it at runtime (bad!)
    export default apiInitializer((api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    zu Folgendem:

    // 'Just in time' lookup of service (good!)
    export default apiInitializer((api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • Hinzufügen eines neuen modifyClass hat den Fehler verursacht

    Wenn der Fehler durch das Hinzufügen eines modifyClass-Aufrufs durch Ihr Theme/Ihr Plugin verursacht wird, müssen Sie diesen früher im Boot-Prozess verschieben. Dies passiert häufig, wenn Methoden auf Diensten (z. B. topicTrackingState) und auf Modellen überschrieben werden, die früh im Boot-Prozess der App 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 Discourse-Initializer ‘inject-discourse-objects’ ausgeführt wird. Zum Beispiel:

    // (plugin)/assets/javascripts/discourse/pre-initializers/extend-user-for-my-plugin.js
    // or
    // (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", (Superclass) => class extends Superclass {
          myNewUserFunction() {
            return "hello world";
          },
        });
      },
    
      initialize() {
        withPluginApi(this.initializeWithApi);
      },
    };
    

    Diese Änderung des User-Modells sollte jetzt ohne Ausgabe einer Warnung funktionieren, und die neue Methode wird auf dem currentUser-Objekt verfügbar sein.


Dieses Dokument wird versioniert verwaltet – 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“

Das Beispiel für api.modifyClass in plugin-api.gjs verwendet noch die veraltete Syntax. Vielleicht muss es aktualisiert werden?

4 „Gefällt mir“