Usando modifyClass para cambiar el comportamiento central

Para temas y plugins 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 APIs de personalización más estables de Discourse (por ejemplo, métodos plugin-api, plugin outlets, transformers).

El código del núcleo puede cambiar en cualquier momento. Y por lo tanto, las personalizaciones realizadas a través de modifyClass pueden 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 añadir pruebas automatizadas al tema/plugin, o podrías usar un sitio de staging para probar las actualizaciones entrantes de Discourse contra tu tema/plugin.

Uso Básico

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

modifyClass toma dos argumentos:

  • resolverName (string) - constrúyelo usando el tipo (ej. component/controller/etc.), seguido de dos puntos, seguido del nombre (guionizado) del archivo 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 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 a las clases hijas de JS. En general, cualquier sintaxis/característica soportada por las clases hijas se puede aplicar aquí. Esto incluye super, propiedades/funciones estáticas, y más.

Sin embargo, hay algunas limitaciones. El sistema modifyClass solo detecta cambios en el prototype de JS de la clase. Prácticamente, esto significa:

  • introducir o modificar un constructor() no es soportado

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // Esto no es soportado. El constructor será ignorado
          }
        }
    );
    
  • introducir o modificar campos de clase no es soportado (aunque algunos campos de clase decorados, como @tracked pueden ser usados)

    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 pueden ser anulados de ninguna manera (aunque, como se mencionó anteriormente, los campos @tracked pueden ser anulados incluyendo @tracked someTrackedField = en la llamada a modifyClass)

    // Código del núcleo:
    class Foo extends Component {
      // Este campo del núcleo no puede ser anulado
      someField = "original";
    
      // Este campo tracked del núcleo puede ser anulado 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 satisfacerse mejor haciendo un PR para introducir nuevas APIs en el núcleo (ej. plugin outlets, transformers, o APIs a medida).

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 (ej. anular 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 - esto ya no es requerido
  2. Actualizar a la sintaxis moderna de clase nativa descrita anteriormente
  3. Probar tus cambios

Solución de Problemas

Clase ya inicializada

Al usar modifyClass en un inicializador, podrías ver esta advertencia en la consola:

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

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

  • Añadir un lookup() causó el error

    Si haces lookup() de 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:

    // Busca el servicio en el inicializador, luego úsalo 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 'Just in time' del servicio (¡bien!)
    export default apiInitializer("0.8", (api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • Añadir un nuevo modifyClass causó el error

    Si el error es introducido por tu tema/plugin añadiendo una llamada a modifyClass, entonces necesitarás moverla antes en el proceso de arranque. Esto sucede comúnmente cuando se anulan métodos en servicios (ej. topicTrackingState), y en modelos que se inicializan temprano en el arranque de la aplicación (ej. un model:user se inicializa para service:current-user).

    Mover la llamada a modifyClass antes en el proceso de arranque normalmente significa mover la llamada a un pre-initializer, y configurarlo para que se ejecute antes del inicializador de ‘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", (Superclass) => class extends Superclass {
          myNewUserFunction() {
            return "hello world";
          },
        });
      },
    
      initialize() {
        withPluginApi(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