Usando modifyClass para cambiar el comportamiento core

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

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 principal 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 agregar 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 las 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 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("botón fue clickeado");
        super.click();
      }
    }
);

La sintaxis class extends ... imita a las clases hijo de JS. En general, cualquier sintaxis/característica soportada por las clases hijo 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, eso significa:

  • introducir o modificar un constructor() no es compatible

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // Esto no es compatible. El constructor será ignorado
          }
        }
    );
    
  • introducir o modificar 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 principal:
    class Foo extends Component {
      // Este campo principal no se puede anular
      someField = "original";
    
      // Este campo tracked principal 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 satisfacerse mejor haciendo un PR para introducir nuevas API en el núcleo (ej. plugin outlets, transformers, o API a medida).

Actualización de Sintaxis Heredada

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

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

Esta sintaxis ya no se recomienda y tiene errores conocidos (ej. 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 realizar mediante:

  1. eliminar pluginId - esto ya no es necesario
  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, es posible que veas esta advertencia en la consola:

Intentó modificar "{name}", pero ya fue inicializado antes en el proceso de arranque

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 (¡malo!)
    export default apiInitializer((api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    A esto:

    // *Lookup* del servicio "Just in time" (¡bueno!)
    export default apiInitializer((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 al agregar una llamada a modifyClass, entonces necesitarás moverla más temprano 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 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", (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