Usando modifyClass para alterar o comportamento central

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 core.

Quando usar modifyClass

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

O código do core pode mudar a qualquer momento. E, portanto, personalizações feitas via modifyClass podem falhar a qualquer momento. Ao usar esta API, você deve garantir que tenha controles implementados para capturar esses problemas antes que cheguem a um site de produção. Por exemplo, você pode adicionar testes automatizados ao tema/plugin, ou pode usar um site de staging para testar atualizações futuras do Discourse em relação ao 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 resolvedor Ember. Isso inclui rotas, controllers, services e components do Discourse.

modifyClass recebe dois argumentos:

  • resolverName (string) - construa isso usando o tipo (por exemplo, component/controller/etc.), seguido por dois pontos, seguido pelo nome do arquivo (em formato dasherized) da classe. 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() em d-button:

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

A sintaxe class extends ... imita 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 alterações no prototype JS da classe. Na prática, 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"; // Isso está ok
        }
    );
    
  • 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 Core:
    class Foo extends Component {
      // Este campo do core não pode ser substituído
      someField = "original";
    
      // Este campo tracked do core pode ser substituído incluindo
      // `@tracked someTrackedField =` na chamada modifyClass
      @tracked someTrackedField = "original";
    }
    

Se você se encontrar querendo fazer essas coisas, seu caso de uso pode ser melhor atendido fazendo um PR para introduzir novas APIs no core (por exemplo, plugin outlets, 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 + " some change";
  },
  pluginId: "some-unique-id"
});

Esta sintaxe não é mais recomendada e tem bugs conhecidos (por exemplo, substituição de 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 moderna de classe nativa 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

No desenvolvimento de temas/plugins, existem duas maneiras pelas quais este erro é normalmente introduzido:

  • Adicionar um lookup() causou o erro

    Se você lookup() um singleton muito cedo no processo de inicialização, isso fará com que quaisquer chamadas posteriores de modifyClass falhem. Nessa situação, você deve tentar fazer o lookup acontecer mais tarde. Por exemplo, você mudaria algo como isto:

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

    Para isto:

    // Lookup do service 'Just in time' (bom!)
    export default apiInitializer("0.8", (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 adição de uma chamada modifyClass pelo seu tema/plugin, então você precisará movê-la mais cedo no processo de inicialização. Isso geralmente acontece ao substituir métodos em services (por exemplo, topicTrackingState), e em models que são inicializados cedo no processo de inicialização do aplicativo (por exemplo, um model:user é inicializado para service:current-user).

    Mover a chamada modifyClass mais cedo no processo de inicialização 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", {
          myNewUserFunction() {
            return "hello world";
          },
        });
      },
    
      initialize() {
        withPluginApi("0.12.1", this.initializeWithApi);
      },
    };
    

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


Este documento é versionado - sugira alterações no github.

16 curtidas

Presumo que seja impossível ou, pelo menos, não confiável tentar usar modifyClass (dentro dos casos de uso legais mencionados acima) em um plugin para o Componente de outro plugin na mesma instalação?

Mesmo que seja um plugin incluído no core (por exemplo, Chat ou Poll)?

1 curtida

Se ambos os plugins estiverem instalados e ativados, ele deverá funcionar sem problemas. Se o destino não estiver instalado/ativado, você receberá um aviso no console. Mas você pode usar o parâmetro ignoreMissing para silenciar isso.

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

Claro, o conselho padrão do modifyClass ainda se aplica: deve ser um último recurso e pode quebrar a qualquer momento, então você deve garantir que seus testes sejam bons o suficiente para identificar problemas rapidamente. Usar transformers seria uma estratégia muito mais segura.

3 curtidas

Então, como isso funciona, ele adia a aplicação até que todos os componentes de todos os plugins tenham sido registrados e carregados?

Acho que tenho um caso que não parece funcionar.

1 curtida

Todos os módulos ES6 (incluindo componentes) são definidos primeiro, depois executamos os pré-inicializadores, em seguida executamos os inicializadores regulares. Então, sim, no momento em que qualquer inicializador é executado, todos os componentes são resolvíveis.

Ficarei feliz em dar uma olhada se você puder compartilhar um trecho ou branch :eyes:

4 curtidas

Desculpe, você precisa ter cuidado e fornecer o caminho completo!

por exemplo:

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

nem:

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

nem mesmo

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

são suficientes!

5 curtidas