Usar modifyClass para cambiar el comportamiento del núcleo

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

Cuándo usar modifyClass

modifyClass debe ser un último recurso, cuando la personalización no pueda realizarse mediante las APIs de personalización más estables de Discourse (p. ej., 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 mediante modifyClass podrían romperse en cualquier momento. Al usar esta API, debe asegurarse de tener controles en su lugar para detectar esos problemas antes de que lleguen a un sitio de producción. Por ejemplo, podría agregar pruebas automatizadas al tema/plugin, o podría usar un sitio de pruebas (staging) para probar las actualizaciones entrantes de Discourse contra su 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 resolver de Ember. Esto incluye las rutas, controladores, servicios y componentes de Discourse.

modifyClass toma dos argumentos:

  • resolverName (cadena) - constrúyalo utilizando el tipo (p. ej., component/controller/etc.), seguido de dos puntos, seguido del nombre de archivo (en formato guion) de la clase. Por ejemplo: component:d-button, component:modal/login, controller:user, route:application, etc.

  • callback (función) - 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 la de las clases hijas de JS. En general, cualquier sintaxis/funcionalidad admitida por las clases hijas puede aplicarse 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 JS de la clase. En la práctica, esto significa:

  • introducir o modificar un constructor() no está admitido

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // This is not supported. The constructor will be ignored
          }
        }
    );
    
  • introducir o modificar campos de clase no está admitido (aunque algunos campos de clase decorados, como @tracked, pueden usarse)

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          someField = "foo"; // NOT SUPPORTED - do not copy
          @tracked someOtherField = "foo"; // This is ok
        }
    );
    
  • los campos de clase simples en la implementación original no pueden sobrescribirse de ninguna manera (aunque, como se mencionó anteriormente, los campos @tracked pueden sobrescribirse por otro campo @tracked)

    // Core code:
    class Foo extends Component {
      // This core field cannot be overridden
      someField = "original";
    
      // This core tracked field can be overridden by including
      // `@tracked someTrackedField =` in the modifyClass call
      @tracked someTrackedField = "original";
    }
    

Si se encuentra queriendo hacer estas cosas, es posible que su caso de uso se satisfaga mejor creando una PR para introducir nuevas APIs en el núcleo (p. ej., plugin outlets, transformers o APIs específicas).

Actualización de la sintaxis legada

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

// Outdated syntax - do not use
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 (p. ej., sobrescribir getters o @actions). Cualquier código que utilice esta sintaxis debe actualizarse para usar la sintaxis de clase nativa descrita anteriormente. En general, la conversión se puede realizar de la siguiente manera:

  1. eliminar pluginId - ya no es necesario
  2. Actualice a la sintaxis moderna de clase nativa descrita anteriormente
  3. Pruebe sus cambios

Solución de problemas

Clase ya inicializada

Al usar modifyClass en un inicializador, es posible que vea 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 normalmente se introduce este error:

  • Agregar un lookup() causó el error

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

    // Lookup service in initializer, then use it at runtime (bad!)
    export default apiInitializer((api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    A esto:

    // 'Just in time' lookup of service (good!)
    export default apiInitializer((api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • Agregar un nuevo modifyClass causó el error

    Si el error se introduce porque su tema/plugin agrega una llamada a modifyClass, deberá moverla más temprano en el proceso de arranque. Esto suele ocurrir al sobrescribir métodos en servicios (p. ej., topicTrackingState) y en modelos que se inicializan temprano en el proceso de arranque de la aplicación (p. ej., 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 configurarla 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
    // or
    // (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 debería funcionar ahora sin imprimir una advertencia, y el nuevo método estará disponible en el objeto currentUser.


Este documento está bajo control de versiones - sugiera 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

El ejemplo para api.modifyClass en plugin-api.gjs todavía utiliza sintaxis heredada. ¿Quizás requiere una actualización?

4 Me gusta