Utilizzo di modifyClass per cambiare il comportamento di base

Per temi e plugin avanzati, Discourse offre il sistema modifyClass. Questo ti permette di estendere e sovrascrivere la funzionalità di molte classi JavaScript principali.

Quando usare modifyClass

modifyClass dovrebbe essere l’ultima risorsa, quando la tua personalizzazione non può essere effettuata tramite le API di personalizzazione più stabili di Discourse (ad es. metodi plugin-api, plugin outlets, transformers).

Il codice principale può cambiare in qualsiasi momento. Pertanto, le personalizzazioni effettuate tramite modifyClass potrebbero interrompersi in qualsiasi momento. Quando usi questa API, dovresti assicurarti di avere controlli in atto per catturare questi problemi prima che raggiungano un sito di produzione. Ad esempio, potresti aggiungere test automatici al tema/plugin, oppure potresti usare un sito di staging per testare gli aggiornamenti in arrivo di Discourse rispetto al tuo tema/plugin.

Utilizzo di base

api.modifyClass può essere utilizzato per modificare le funzioni e le proprietà di qualsiasi classe accessibile tramite il risolutore Ember. Ciò include route, controller, servizi e componenti di Discourse.

modifyClass accetta due argomenti:

  • resolverName (stringa) - costruiscilo usando il tipo (ad es. component/controller/ecc.), seguito da due punti, seguito dal nome del file (dasherizzato) della classe. Ad esempio: component:d-button, component:modal/login, controller:user, route:application, ecc.

  • callback (funzione) - una funzione che riceve la definizione della classe esistente e quindi restituisce una versione estesa.

Ad esempio, per modificare l’azione click() su d-button:

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

La sintassi class extends ... imita quella delle classi figlie JS. In generale, qualsiasi sintassi/funzionalità supportata dalle classi figlie può essere applicata qui. Ciò include super, proprietà/funzioni statiche e altro ancora.

Tuttavia, ci sono alcune limitazioni. Il sistema modifyClass rileva solo le modifiche al prototype JS della classe. In pratica, ciò significa:

  • l’introduzione o la modifica di un constructor() non è supportata

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // Questo non è supportato. Il costruttore verrà ignorato
          }
        }
    );
    
  • l’introduzione o la modifica di campi di classe non è supportata (sebbene alcuni campi di classe decorati, come @tracked, possano essere utilizzati)

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          someField = "foo"; // NON SUPPORTATO - non copiare
          @tracked someOtherField = "foo"; // Questo va bene
        }
    );
    
  • i semplici campi di classe nell’implementazione originale non possono essere sovrascritti in alcun modo (sebbene, come sopra, i campi @tracked possano essere sovrascritti da un altro campo @tracked)

    // Codice principale:
    class Foo extends Component {
      // Questo campo principale non può essere sovrascritto
      someField = "original";
    
      // Questo campo tracciato principale può essere sovrascritto includendo
      // `@tracked someTrackedField =` nella chiamata modifyClass
      @tracked someTrackedField = "original";
    }
    

Se ti trovi a voler fare queste cose, allora il tuo caso d’uso potrebbe essere meglio soddisfatto creando una PR per introdurre nuove API nel codice principale (ad es. plugin outlets, transformers, o API su misura).

Aggiornamento della sintassi legacy

In passato, modifyClass veniva chiamato utilizzando una sintassi a oggetto letterale come questa:

// Sintassi obsoleta - non usare
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " some change";
  },
  pluginId: "some-unique-id",
});

Questa sintassi non è più raccomandata e presenta bug noti (ad es. sovrascrittura di getter o @actions). Qualsiasi codice che utilizza questa sintassi dovrebbe essere aggiornato per utilizzare la sintassi nativa di classe descritta sopra. In generale, la conversione può essere effettuata tramite:

  1. rimuovere pluginId - questo non è più richiesto
  2. Aggiornare alla sintassi nativa di classe moderna descritta sopra
  3. Testare le tue modifiche

Risoluzione dei problemi

Classe già inizializzata

Quando si utilizza modifyClass in un inizializzatore, potresti vedere questo avviso nella console:

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

Nello sviluppo di temi/plugin, ci sono due modi in cui questo errore viene normalmente introdotto:

  • L’aggiunta di un lookup() ha causato l’errore

    Se esegui lookup() di un singleton troppo presto nel processo di avvio, ciò causerà il fallimento di eventuali chiamate modifyClass successive. In questa situazione, dovresti provare a spostare il lookup in modo che avvenga più tardi. Ad esempio, cambieresti qualcosa del genere:

    // Lookup del servizio nell'inizializzatore, quindi usalo al runtime (male!)
    export default apiInitializer("0.8", (api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    A questo:

    // Lookup del servizio 'Just in time' (bene!)
    export default apiInitializer("0.8", (api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • L’aggiunta di un nuovo modifyClass ha causato l’errore

    Se l’errore è introdotto dalla chiamata modifyClass aggiunta dal tuo tema/plugin, dovrai spostarla prima nel processo di avvio. Questo accade comunemente quando si sovrascrivono metodi sui servizi (ad es. topicTrackingState), e sui modelli che vengono inizializzati presto nel processo di avvio dell’app (ad es. un model:user viene inizializzato per service:current-user).

    Spostare la chiamata modifyClass prima nel processo di avvio significa normalmente spostare la chiamata a un pre-initializer e configurarlo per essere eseguito prima dell’inizializzatore ‘inject-discourse-objects’ di Discourse. Ad esempio:

    // (plugin)/assets/javascripts/discourse/pre-initializers/extend-user-for-my-plugin.js
    // o
    // (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);
      },
    };
    

    Questa modifica del modello utente dovrebbe ora funzionare senza stampare un avviso, e il nuovo metodo sarà disponibile sull’oggetto currentUser.


Questo documento è controllato dalla versione - suggerisci modifiche su github.

16 Mi Piace

Presumo sia impossibile o quantomeno inaffidabile tentare di utilizzare modifyClass (all’interno dei casi d’uso legali menzionati sopra) all’interno di un plugin su un Component di un altro plugin nella stessa installazione?

Anche se si tratta di un plugin incluso nel core (ad esempio, Chat o Poll)?

1 Mi Piace

Se entrambi i plugin sono installati e abilitati, dovrebbe funzionare senza problemi. Se il target non è installato/abilitato, riceverai un avviso nella console. Ma puoi usare il parametro ignoreMissing per silenziarlo.

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

Naturalmente, valgono ancora i consigli standard su modifyClass: dovrebbe essere un’ultima risorsa e può smettere di funzionare in qualsiasi momento, quindi dovresti assicurarti che i tuoi test siano abbastanza buoni da identificare rapidamente i problemi. Usare transformer sarebbe una strategia molto più sicura.

3 Mi Piace

Quindi, come funziona, rimanda l’applicazione finché tutti i componenti di tutti i plugin non sono stati registrati e caricati?

Penso di avere un caso che non sembra funzionare.

1 Mi Piace

Tutti i moduli ES6 (inclusi i componenti) vengono definiti per primi, quindi eseguiamo i pre-initializer, quindi eseguiamo gli initializer regolari. Quindi sì, nel momento in cui viene eseguito un qualsiasi initializer, tutti i componenti sono risolvibili.

Sarò felice di dare un’occhiata se puoi condividere uno snippet o un branch :occhi:

4 Mi Piace

Scusate, dovete fare attenzione a fornire il percorso completo!

ad esempio:

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

né:

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

né tantomeno

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

sono sufficienti!

5 Mi Piace