Utiliser modifyClass pour changer le comportement principal

Pour les thèmes et plugins avancés, Discourse propose le système modifyClass. Cela vous permet d’étendre et de remplacer des fonctionnalités dans de nombreuses classes JavaScript du cœur.

Quand utiliser modifyClass

modifyClass doit être utilisé en dernier recours, lorsque votre personnalisation ne peut pas être effectuée via les API de personnalisation plus stables de Discourse (par exemple, les méthodes plugin-api, les plugin outlets, les transformers).

Le code du cœur peut changer à tout moment. Par conséquent, les personnalisations effectuées via modifyClass peuvent échouer à tout moment. Lorsque vous utilisez cette API, vous devez vous assurer que des contrôles sont en place pour détecter ces problèmes avant qu’ils n’atteignent un site de production. Par exemple, vous pourriez ajouter des tests automatisés au thème/plugin, ou utiliser un site de staging pour tester les mises à jour entrantes de Discourse par rapport à votre thème/plugin.

Utilisation de base

api.modifyClass peut être utilisé pour modifier les fonctions et les propriétés de toute classe accessible via le résolveur Ember. Cela inclut les routes, les contrôleurs, les services et les composants de Discourse.

modifyClass prend deux arguments :

  • resolverName (string) - construisez-le en utilisant le type (par exemple, component/controller/etc.), suivi d’un deux-points, suivi du nom du fichier de la classe (en tirets). Par exemple : component:d-button, component:modal/login, controller:user, route:application, etc.

  • callback (function) - une fonction qui reçoit la définition de classe existante, puis renvoie une version étendue.

Par exemple, pour modifier l’action click() sur d-button :

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

La syntaxe class extends ... imite celle des classes enfants JS. En général, toute syntaxe/fonctionnalité prise en charge par les classes enfants peut être appliquée ici. Cela inclut super, les propriétés/fonctions statiques, et plus encore.

Cependant, il existe certaines limitations. Le système modifyClass ne détecte que les modifications apportées au prototype JS de la classe. Concrètement, cela signifie :

  • l’introduction ou la modification d’un constructor() n’est pas prise en charge

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // Ceci n'est pas pris en charge. Le constructeur sera ignoré
          }
        }
    );
    
  • l’introduction ou la modification de champs de classe n’est pas prise en charge (bien que certains champs de classe décorés, comme @tracked, puissent être utilisés)

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          someField = "foo"; // NON PRIS EN CHARGE - ne pas copier
          @tracked someOtherField = "foo"; // Ceci est ok
        }
    );
    
  • les champs de classe simples de l’implémentation d’origine ne peuvent être remplacés d’aucune manière (bien que, comme ci-dessus, les champs @tracked puissent être remplacés par un autre champ @tracked)

    // Code du cœur :
    class Foo extends Component {
      // Ce champ du cœur ne peut pas être remplacé
      someField = "original";
    
      // Ce champ tracked du cœur peut être remplacé en incluant
      // `@tracked someTrackedField =` dans l'appel modifyClass
      @tracked someTrackedField = "original";
    }
    

Si vous vous retrouvez à vouloir faire ces choses, votre cas d’utilisation pourrait être mieux satisfait en faisant une PR pour introduire de nouvelles API dans le cœur (par exemple, des plugin outlets, des transformers, ou des API sur mesure).

Mise à niveau de la syntaxe héritée

Dans le passé, modifyClass était appelé en utilisant une syntaxe d’objet littéral comme ceci :

// Syntaxe obsolète - ne pas utiliser
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " some change";
  },
  pluginId: "some-unique-id",
});

Cette syntaxe n’est plus recommandée et présente des bugs connus (par exemple, le remplacement de getters ou de @actions). Tout code utilisant cette syntaxe doit être mis à jour pour utiliser la syntaxe de classe native décrite ci-dessus. En général, la conversion peut être effectuée en :

  1. supprimant pluginId - il n’est plus requis
  2. Mettre à jour vers la syntaxe de classe native moderne décrite ci-dessus
  3. Tester vos modifications

Dépannage

Classe déjà initialisée

Lorsque vous utilisez modifyClass dans un initialiseur, vous pouvez voir cet avertissement dans la console :

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

Dans le développement de thèmes/plugins, deux façons introduisent normalement cette erreur :

  • L’ajout d’un lookup() a provoqué l’erreur

    Si vous lookup() un singleton trop tôt dans le processus de démarrage, cela entraînera l’échec de tous les appels modifyClass ultérieurs. Dans cette situation, vous devriez essayer de reporter le lookup à plus tard. Par exemple, vous changeriez quelque chose comme ceci :

    // Lookup du service dans l'initialiseur, puis utilisation à l'exécution (mauvais !)
    export default apiInitializer("0.8", (api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    À ceci :

    // Lookup du service 'Just in time' (bon !)
    export default apiInitializer("0.8", (api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • L’ajout d’un nouveau modifyClass a provoqué l’erreur

    Si l’erreur est introduite par votre thème/plugin ajoutant un appel modifyClass, vous devrez le déplacer plus tôt dans le processus de démarrage. Cela se produit couramment lors du remplacement de méthodes sur des services (par exemple, topicTrackingState), et sur des modèles qui sont initialisés tôt dans le processus de démarrage de l’application (par exemple, un model:user est initialisé pour service:current-user).

    Déplacer l’appel modifyClass plus tôt dans le processus de démarrage signifie normalement déplacer l’appel vers un pre-initializer, et le configurer pour qu’il s’exécute avant l’initialiseur ‘inject-discourse-objects’ de Discourse. Par exemple :

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

    Cette modification du modèle utilisateur devrait maintenant fonctionner sans afficher d’avertissement, et la nouvelle méthode sera disponible sur l’objet currentUser.


Ce document est contrôlé par version - suggérez des modifications sur github.

16 « J'aime »

Je présume qu’il est impossible ou du moins peu fiable de tenter d’utiliser modifyClass (dans les cas d’utilisation légaux mentionnés ci-dessus) au sein d’un plugin sur le composant d’un autre plugin dans la même installation ?

Même s’il s’agit d’un plugin inclus dans le cœur (par exemple, Chat ou Sondage) ?

1 « J'aime »

Si les deux plugins sont installés et activés, cela devrait fonctionner sans problème. Si la cible n’est pas installée/activée, vous obtiendrez un avertissement dans la console. Mais vous pouvez utiliser le paramètre ignoreMissing pour le supprimer.

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

Bien sûr, les conseils standards concernant modifyClass s’appliquent toujours : cela devrait être un dernier recours, et cela peut échouer à tout moment, vous devez donc vous assurer que vos tests sont suffisamment bons pour identifier rapidement les problèmes. L’utilisation de transformers serait une stratégie beaucoup plus sûre.

3 « J'aime »

Alors, comment cela fonctionne-t-il, cela reporte-t-il l’application jusqu’à ce que tous les composants de tous les plugins aient été enregistrés et chargés ?

Je pense que j’ai un cas qui ne semble pas fonctionner.

1 « J'aime »

Tous les modules ES6 (y compris les composants) sont définis d’abord, puis nous exécutons les pré-initialiseurs, puis nous exécutons les initialiseurs réguliers. Donc oui, au moment où un initialiseur s’exécute, tous les composants sont résolubles.

Je serais ravi d’examiner si vous pouvez partager un extrait ou une branche :eyes:

4 « J'aime »

Désolé, vous devez faire attention à fournir le chemin complet !

par exemple :

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

ni :

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

ni même

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

ne suffisent !

5 « J'aime »