Usare modifyClass per cambiare il comportamento core

Per i temi e i plugin avanzati, Discourse offre il sistema modifyClass. Questo consente di estendere e sovrascrivere la funzionalità in molte delle classi javascript del core.

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 esempio, metodi plugin-api, plugin outlet, transformer).

Il codice del core può cambiare in qualsiasi momento. E di conseguenza, le personalizzazioni effettuate tramite modifyClass potrebbero interrompersi in qualsiasi momento. Quando si utilizza questa API, è necessario assicurarsi di disporre di controlli per rilevare tali problemi prima che raggiungano un sito di produzione. Ad esempio, potresti aggiungere test automatizzati al tema/plugin, oppure potresti utilizzare 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 resolver Ember. Ciò include rotte, controller, servizi e componenti di Discourse.

modifyClass accetta due argomenti:

  • resolverName (stringa) - costruiscilo usando il tipo (ad esempio, component/controller/ecc.), seguito da due punti, seguito dal nome (in formato dasherized) del file 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 di JS. In generale, qualsiasi sintassi/funzionalità supportata dalle classi figlie può essere applicata qui. Ciò include super, proprietà/funzioni statiche e altro.

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 dei campi di classe non è supportata (anche se alcuni campi di classe decorati, come @tracked, possono essere utilizzati)

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

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

Se ti trovi a voler fare queste cose, il tuo caso d’uso potrebbe essere meglio soddisfatto creando una PR per introdurre nuove API nel core (ad esempio, plugin outlet, transformer o API su misura).

Aggiornamento della sintassi legacy

In passato, modifyClass veniva chiamato utilizzando una sintassi a letterale oggetto 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ù consigliata e presenta bug noti (ad esempio, la sovrascrittura di getter o @actions). Qualsiasi codice che utilizza questa sintassi dovrebbe essere aggiornato per utilizzare la sintassi nativa della classe descritta sopra. In generale, la conversione può essere eseguita tramite:

  1. rimozione di pluginId - questo non è più richiesto
  2. Aggiornamento alla sintassi nativa della classe moderna descritta sopra
  3. Test delle modifiche

Risoluzione dei problemi

Classe già inizializzata

Quando si utilizza modifyClass in un inizializzatore, si potrebbe visualizzare 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 una lookup() ha causato l’errore

    Se si esegue la lookup() di un singleton troppo presto nel processo di avvio, ciò causerà il fallimento di qualsiasi chiamata modifyClass successiva. In questa situazione, si dovrebbe tentare di spostare la lookup in modo che avvenga più tardi. Ad esempio, si cambierebbe qualcosa di simile a questo:

    // Esegue la lookup del servizio nell'inizializzatore, quindi lo usa a 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, allora dovrai spostarla prima nel processo di avvio. Ciò accade comunemente quando si sovrascrivono metodi sui servizi (ad esempio, topicTrackingState), e sui modelli che vengono inizializzati presto nel processo di avvio dell’app (ad esempio, un model:user viene inizializzato per service:current-user).

    Spostare la chiamata modifyClass prima nel processo di avvio significa normalmente spostare la chiamata in 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", (Superclass) => class extends Superclass {
          myNewUserFunction() {
            return "hello world";
          },
        });
      },
    
      initialize() {
        withPluginApi(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 tramite 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