Pour les thèmes et plugins avancés, Discourse propose le système modifyClass. Il vous permet d’étendre et de remplacer la fonctionnalité de nombreuses classes JavaScript du cœur de l’application.
Quand utiliser modifyClass
modifyClass doit être un dernier recours, lorsque votre personnalisation ne peut pas être réalisé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 se casser à tout moment. Lorsque vous utilisez cette API, vous devez vous assurer d’avoir des mécanismes de contrôle 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 pré-production pour tester les mises à jour entrantes de Discourse contre 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, contrôleurs, services et composants de Discourse.
modifyClass prend deux arguments :
-
resolverName(chaîne de caractères) - construisez-le en utilisant le type (par ex. component/controller/etc.), suivi d’un deux-points, puis du nom de fichier (avec des tirets) de la classe. Par exemple :component:d-button,component:modal/login,controller:user,route:application, etc. -
callback(fonction) - 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/les fonctionnalités prises en charge par les classes enfants peuvent être appliquées ici. Cela inclut super, les propriétés/fonctions statiques, et plus encore.
Cependant, il y a certaines limitations. Le système modifyClass ne détecte que les modifications du prototype JS de la classe. Concrètement, cela signifie :
-
l’introduction ou la modification d’un
constructor()n’est pas prise en chargeapi.modifyClass( "component:foo", (Superclass) => class extends Superclass { constructor() { // This is not supported. The constructor will be ignored } } ); -
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"; // NOT SUPPORTED - do not copy @tracked someOtherField = "foo"; // This is ok } ); -
les champs de classe simples de l’implémentation d’origine ne peuvent pas être remplacés de quelque manière que ce soit (bien que, comme ci-dessus, les champs
@trackedpuissent être remplacés par un autre champ@tracked)// 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"; }
Si vous vous retrouvez à vouloir faire ces choses, votre cas d’usage pourrait être mieux satisfait en soumettant une demande de tirage (PR) pour introduire de nouvelles API dans le cœur (par exemple, plugin outlets, transformers, ou des API spécifiques).
Mise à niveau de la syntaxe legacy
Dans le passé, modifyClass était appelé en utilisant une syntaxe littérale d’objet comme celle-ci :
// Outdated syntax - do not use
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 des getters ou des @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 :
- supprimant
pluginId- cela n’est plus nécessaire - Passer à la syntaxe de classe native moderne décrite ci-dessus
- Tester vos modifications
Dépannage
Classe déjà initialisée
Lors de l’utilisation de 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, il y a deux façons dont cette erreur est normalement introduite :
-
L’ajout d’un
lookup()a causé l’erreurSi vous effectuez un
lookup()d’un singleton trop tôt dans le processus de démarrage, cela entraînera l’échec de tout appel ultérieur àmodifyClass. Dans cette situation, vous devriez essayer de déplacer le lookup pour qu’il se produise plus tard. Par exemple, vous modifieriez quelque chose comme ceci :// 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(); }); });En ceci :
// 'Just in time' lookup of service (good!) export default apiInitializer((api) => { api.composerBeforeSave(async () => { const composerService = api.container.lookup("service:composer"); composerService.doSomething(); }); }); -
L’ajout d’un nouveau
modifyClassa causé l’erreurSi 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 ex. topicTrackingState) et sur des modèles qui sont initialisés tôt dans le processus de démarrage de l’application (par ex. unmodel:userest initialisé pourservice: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 // 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); }, };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 versionné - suggérez des modifications sur github.