Usando modifyClass para alterar o comportamento principal

Para temas e plugins avançados, o Discourse oferece o sistema modifyClass. Isso permite estender e substituir a funcionalidade em muitas das classes javascript do núcleo.

Quando usar modifyClass

modifyClass deve ser o último recurso, quando sua personalização não pode ser feita através das APIs de personalização mais estáveis do Discourse (por exemplo, métodos de api de plugin, outlets de plugin, transformers).

O código do núcleo pode mudar a qualquer momento. E, portanto, personalizações feitas via modifyClass podem quebrar a qualquer momento. Ao usar esta API, você deve garantir que possui controles implementados para capturar esses problemas antes que eles cheguem a um ambiente de produção. Por exemplo, você pode adicionar testes automatizados ao tema/plugin, ou pode usar um site de staging para testar as atualizações de entrada do Discourse contra seu tema/plugin.

Uso Básico

api.modifyClass pode ser usado para modificar as funções e propriedades de qualquer classe que seja acessível através do resolver Ember. Isso inclui rotas, controllers, serviços e componentes do Discourse.

modifyClass aceita dois argumentos:

  • resolverName (string) - construa isso usando o tipo (ex: component/controller/etc.), seguido por dois pontos, seguido pelo nome da classe (com hifens, ou dasherized). Por exemplo: component:d-button, component:modal/login, controller:user, route:application, etc.

  • callback (function) - uma função que recebe a definição da classe existente e, em seguida, retorna uma versão estendida.

Por exemplo, para modificar a ação click() no d-button:

api.modifyClass(
  "component:d-button",
  (Superclass) =>
    class extends Superclass {
      @action
      click() {
        console.log("botão foi clicado");
        super.click();
      }
    }
);

A sintaxe class extends ... mimetiza a de classes filhas do JS. Em geral, qualquer sintaxe/recurso suportado por classes filhas pode ser aplicado aqui. Isso inclui super, propriedades/funções estáticas e mais.

No entanto, existem algumas limitações. O sistema modifyClass detecta apenas mudanças no prototype do JS da classe. Praticamente, isso significa:

  • introduzir ou modificar um constructor() não é suportado

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // Isso não é suportado. O construtor será ignorado
          }
        }
    );
    
  • introduzir ou modificar campos de classe não é suportado (embora alguns campos de classe decorados, como @tracked, possam ser usados)

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          someField = "foo"; // NÃO SUPORTADO - não copie
          @tracked someOtherField = "foo"; // Isto é permitido
        }
    );
    
  • campos de classe simples na implementação original não podem ser substituídos de forma alguma (embora, como acima, campos @tracked possam ser substituídos por outro campo @tracked)

    // Código do núcleo:
    class Foo extends Component {
      // Este campo do núcleo não pode ser substituído
      someField = "original";
    
      // Este campo tracked do núcleo pode ser substituído incluindo
      // `@tracked someTrackedField =` na chamada modifyClass
      @tracked someTrackedField = "original";
    }
    

Se você se encontrar querendo fazer estas coisas, então seu caso de uso pode ser melhor atendido criando um PR para introduzir novas APIs no núcleo (por exemplo, outlets de plugin, transformers ou APIs personalizadas).

Atualizando Sintaxe Legada

No passado, modifyClass era chamado usando uma sintaxe de literal de objeto como esta:

// Sintaxe desatualizada - não use
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " alguma alteração";
  }
  pluginId: "some-unique-id"
});

Esta sintaxe não é mais recomendada e tem bugs conhecidos (por exemplo, substituir getters ou @actions). Qualquer código que use esta sintaxe deve ser atualizado para usar a sintaxe de classe nativa descrita acima. Em geral, a conversão pode ser feita por:

  1. remover pluginId - isso não é mais necessário
  2. Atualizar para a sintaxe de classe nativa moderna descrita acima
  3. Testar suas alterações

Solução de Problemas

Classe já inicializada

Ao usar modifyClass em um inicializador, você pode ver este aviso no console:

Attempted to modify "{name}", but it was already initialized earlier in the boot process (Tentativa de modificar “{nome}”, mas ele já foi inicializado anteriormente no processo de boot)

No desenvolvimento de temas/plugins, existem duas maneiras de este erro ser introduzido normalmente:

  • Adicionar um lookup() causou o erro

    Se você fizer lookup() de um singleton muito cedo no processo de boot, isso fará com que quaisquer chamadas posteriores a modifyClass falhem. Nesta situação, você deve tentar mover o lookup para ocorrer mais tarde. Por exemplo, você mudaria algo como isto:

    // Procura o serviço no inicializador, então o usa em tempo de execução (ruim!)
    export default apiInitializer((api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    Para isto:

    // Procura de serviço 'Just in time' (bom!)
    export default apiInitializer((api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • Adicionar um novo modifyClass causou o erro

    Se o erro for introduzido pela sua adição de um modifyClass no tema/plugin, então você precisará movê-lo mais cedo no processo de boot. Isso acontece comumente ao substituir métodos em serviços (ex: topicTrackingState), e em modelos que são inicializados cedo no processo de boot do aplicativo (ex: um model:user é inicializado para service:current-user).

    Mover a chamada modifyClass mais cedo no processo de boot normalmente significa mover a chamada para um pre-initializer, e configurá-lo para rodar antes do inicializador ‘inject-discourse-objects’ do Discourse. Por exemplo:

    // (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", (Superclass) => class extends Superclass {
          myNewUserFunction() {
            return "hello world";
          },
        });
      },
    
      initialize() {
        withPluginApi(this.initializeWithApi);
      },
    };
    

    Esta modificação do modelo de usuário agora deve funcionar sem imprimir um aviso, e o novo método estará disponível no objeto currentUser.


Este documento é controlado por versão - sugira alterações no github.

16 curtidas