Usando modifyClass para cambiar el comportamiento central

Para temas y complementos avanzados, Discourse ofrece el sistema modifyClass. Esto te permite extender y anular la funcionalidad en muchas de las clases de JavaScript del núcleo.

Cuándo usar modifyClass

modifyClass debe ser el último recurso, cuando tu personalización no se puede realizar a través de las API de personalización más estables de Discourse (por ejemplo, métodos de plugin-api, plugin outlets, transformers).

El código del núcleo puede cambiar en cualquier momento. Por lo tanto, las personalizaciones realizadas a través de modifyClass podrían romperse en cualquier momento. Al usar esta API, debes asegurarte de tener controles implementados para detectar esos problemas antes de que lleguen a un sitio de producción. Por ejemplo, podrías agregar pruebas automatizadas al tema/complemento, o podrías usar un sitio de staging para probar las actualizaciones entrantes de Discourse contra tu tema/complemento.

Uso básico

api.modifyClass se puede usar para modificar las funciones y propiedades de cualquier clase que sea accesible a través del resolutor de Ember. Esto incluye las rutas, controladores, servicios y componentes de Discourse.

modifyClass toma dos argumentos:

  • resolverName (string) - constrúyelo usando el tipo (por ejemplo, component/controller/etc.), seguido de dos puntos, seguido del nombre del archivo (en formato guionizado) de la clase. Por ejemplo: component:d-button, component:modal/login, controller:user, route:application, etc.

  • callback (function) - una función que recibe la definición de la clase existente y luego devuelve una versión extendida.

Por ejemplo, para modificar la acción click() en d-button:

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

La sintaxis class extends ... imita la de las clases hijas de JS. En general, cualquier sintaxis/característica compatible con las clases hijas se puede aplicar aquí. Esto incluye super, propiedades/funciones estáticas y más.

Sin embargo, existen algunas limitaciones. El sistema modifyClass solo detecta cambios en el prototype de JS de la clase. En la práctica, eso significa:

  • la introducción o modificación de un constructor() no es compatible

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // Esto no es compatible. El constructor será ignorado
          }
        }
    );
    
  • la introducción o modificación de campos de clase no es compatible (aunque algunos campos de clase decorados, como @tracked, se pueden usar)

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          someField = "foo"; // NO SOPORTADO - no copiar
          @tracked someOtherField = "foo"; // Esto está bien
        }
    );
    
  • los campos de clase simples en la implementación original no se pueden anular de ninguna manera (aunque, como se mencionó anteriormente, los campos @tracked se pueden anular con otro campo @tracked)

    // Código del núcleo:
    class Foo extends Component {
      // Este campo del núcleo no se puede anular
      someField = "original";
    
      // Este campo rastreado del núcleo se puede anular incluyendo
      // `@tracked someTrackedField =` en la llamada a modifyClass
      @tracked someTrackedField = "original";
    }
    

Si te encuentras queriendo hacer estas cosas, entonces tu caso de uso podría estar mejor satisfecho haciendo un PR para introducir nuevas API en el núcleo (por ejemplo, plugin outlets, transformers, o API personalizadas).

Actualización de sintaxis heredada

En el pasado, modifyClass se llamaba usando una sintaxis de literal de objeto como esta:

// Sintaxis obsoleta - no usar
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " some change";
  },
  pluginId: "some-unique-id",
});

Esta sintaxis ya no se recomienda y tiene errores conocidos (por ejemplo, anulación de getters o @actions). Cualquier código que use esta sintaxis debe actualizarse para usar la sintaxis de clase nativa descrita anteriormente. En general, la conversión se puede hacer:

  1. eliminando pluginId - ya no es necesario
  2. Actualizando a la sintaxis moderna de clase nativa descrita anteriormente
  3. Probando tus cambios

Solución de problemas

Clase ya inicializada

Al usar modifyClass en un inicializador, es posible que veas esta advertencia en la consola:

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

En el desarrollo de temas/complementos, hay dos formas en que este error se introduce normalmente:

  • Agregar un lookup() causó el error

    Si haces lookup() a un singleton demasiado pronto en el proceso de arranque, causará que cualquier llamada posterior a modifyClass falle. En esta situación, deberías intentar mover el lookup para que ocurra más tarde. Por ejemplo, cambiarías algo como esto:

    // Buscar servicio en el inicializador, luego usarlo en tiempo de ejecución (¡mal!)
    export default apiInitializer("0.8", (api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    A esto:

    // Búsqueda de servicio 'Just in time' (¡bien!)
    export default apiInitializer("0.8", (api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • Agregar un nuevo modifyClass causó el error

    Si el error es introducido por tu tema/complemento al agregar una llamada modifyClass, entonces necesitarás moverla más temprano en el proceso de arranque. Esto ocurre comúnmente al anular métodos en servicios (por ejemplo, topicTrackingState), y en modelos que se inicializan temprano en el proceso de arranque de la aplicación (por ejemplo, un model:user se inicializa para service:current-user).

    Mover la llamada modifyClass más temprano en el proceso de arranque normalmente significa mover la llamada a un pre-initializer, y configurarlo para que se ejecute antes del inicializador ‘inject-discourse-objects’ de Discourse. Por ejemplo:

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

    Esta modificación del modelo de usuario ahora debería funcionar sin imprimir una advertencia, y el nuevo método estará disponible en el objeto currentUser.


Este documento está controlado por versiones - sugiere cambios en github.

16 Me gusta

Supongo que es imposible o al menos poco fiable intentar usar modifyClass (dentro de los casos de uso legales mencionados anteriormente) dentro de un plugin en el Component de otro plugin en la misma instalación?

¿Incluso si es un plugin incluido en el núcleo (por ejemplo, Chat o Encuesta)?

1 me gusta

Si ambos plugins están instalados y habilitados, debería funcionar sin problemas. Si el destino no está instalado/habilitado, recibirás una advertencia en la consola. Pero puedes usar el parámetro ignoreMissing para silenciarla.

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

Por supuesto, el consejo estándar de modifyClass sigue vigente: debería ser el último recurso y puede fallar en cualquier momento, por lo que debes asegurarte de que tus pruebas sean lo suficientemente buenas para identificar problemas rápidamente. Usar transformers sería una estrategia mucho más segura.

3 Me gusta

¿Entonces cómo funciona eso, pospone la aplicación hasta que todos los componentes de todos los plugins se hayan registrado y cargado?

Creo que tengo un caso que no parece funcionar.

1 me gusta

Todos los módulos ES6 (incluidos los componentes) se definen primero, luego ejecutamos los pre-inicializadores y después los inicializadores regulares. Así que sí, en el momento en que se ejecuta cualquier inicializador, todos los componentes son resolubles.

Estaré encantado de echarle un vistazo si puedes compartir un fragmento o una rama :ojos:

4 Me gusta

Lo siento, ¡tienes que tener cuidado de proporcionar la ruta completa!

por ejemplo:

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

ni:

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

ni siquiera:

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

¡son suficientes!

5 Me gusta