Использование modifyClass для изменения базового поведения

Для продвинутых тем и плагинов Discourse предлагает систему modifyClass. Она позволяет расширять и переопределять функциональность во многих классах JavaScript ядра.

Когда использовать modifyClass

modifyClass следует использовать как крайнюю меру, когда кастомизация невозможна с помощью более стабильных API кастомизации Discourse (например, методов plugin-api, plugin outlets, transformers).

Код ядра может измениться в любой момент. Следовательно, кастомизации, выполненные через modifyClass, могут сломаться в любой момент. При использовании этого API вы должны убедиться, что у вас есть механизмы для выявления таких проблем до того, как они попадут на продакшн-сайт. Например, вы можете добавить автоматические тесты в тему/плагин или использовать тестовый (staging) сайт для проверки входящих обновлений Discourse на совместимость с вашей темой/плагином.

Базовое использование

api.modifyClass можно использовать для изменения функций и свойств любого класса, доступного через резолвер Ember. Сюда входят маршруты, контроллеры, сервисы и компоненты Discourse.

modifyClass принимает два аргумента:

  • resolverName (строка) — формируется из типа (например, component/controller и т. д.), за которым следует двоеточие, а затем (дефисное) имя файла класса. Например: component:d-button, component:modal/login, controller:user, route:application и т. д.

  • callback (функция) — функция, которая принимает существующее определение класса и возвращает расширенную версию.

Например, чтобы изменить действие click() в d-button:

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

Синтаксис class extends ... имитирует синтаксис дочерних классов JS. В целом, здесь можно применять любой синтаксис и функции, поддерживаемые дочерними классами. Сюда входят super, статические свойства/функции и многое другое.

Однако существуют некоторые ограничения. Система modifyClass обнаруживает изменения только в JS prototype класса. Практически это означает:

  • введение или изменение constructor() не поддерживается

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // Это не поддерживается. Конструктор будет проигнорирован
          }
        }
    );
    
  • введение или изменение полей класса не поддерживается (хотя некоторые декорированные поля класса, такие как @tracked, могут использоваться)

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          someField = "foo"; // НЕ ПОДДЕРЖИВАЕТСЯ - не копируйте
          @tracked someOtherField = "foo"; // Это нормально
        }
    );
    
  • простые поля класса в исходной реализации не могут быть переопределены каким-либо образом (хотя, как указано выше, поля @tracked могут быть переопределены другим полем @tracked)

    // Код ядра:
    class Foo extends Component {
      // Это поле ядра не может быть переопределено
      someField = "original";
    
      // Это отслеживаемое поле ядра может быть переопределено путем включения
      // `@tracked someTrackedField =` в вызов modifyClass
      @tracked someTrackedField = "original";
    }
    

Если вы обнаружите, что хотите сделать эти вещи, возможно, ваш сценарий использования будет лучше удовлетворен путем создания PR для введения новых API в ядро (например, plugin outlets, transformers или специализированных API).

Обновление устаревшего синтаксиса

В прошлом modifyClass вызывался с использованием синтаксиса объектного литерала, похожего на этот:

// Устаревший синтаксис - не используйте
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " some change";
  }
  pluginId: "some-unique-id"
});

Этот синтаксис больше не рекомендуется и имеет известные ошибки (например, переопределение геттеров или @actions). Любой код, использующий этот синтаксис, должен быть обновлен до использования синтаксиса нативных классов, описанного выше. В целом, конвертация может быть выполнена следующим образом:

  1. Удалите pluginId — это больше не требуется
  2. Обновите до современного синтаксиса нативных классов, описанного выше
  3. Протестируйте свои изменения

Устранение неполадок

Класс уже инициализирован

При использовании modifyClass в инициализаторе вы можете увидеть это предупреждение в консоли:

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

В разработке тем/плагинов эта ошибка обычно возникает двумя способами:

  • Добавление lookup() вызвало ошибку

    Если вы вызываете lookup() для синглтона слишком рано в процессе загрузки, это приведет к сбою любых последующих вызовов modifyClass. В этой ситуации вам следует попытаться переместить вызов lookup на более поздний этап. Например, вы бы изменили что-то вроде этого:

    // Поиск сервиса в инициализаторе, а затем его использование во время выполнения (плохо!)
    export default apiInitializer((api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    На это:

    // Поиск сервиса «точно в срок» (хорошо!)
    export default apiInitializer((api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • Добавление нового modifyClass вызвало ошибку

    Если ошибка возникает из-за добавления вызова modifyClass вашей темой/плагином, вам нужно переместить его на более ранний этап процесса загрузки. Это часто происходит при переопределении методов в сервисах (например, topicTrackingState) и в моделях, которые инициализируются на раннем этапе загрузки приложения (например, model:user инициализируется для service:current-user).

    Перемещение вызова modifyClass на более ранний этап процесса загрузки обычно означает перемещение вызова в pre-initializer и настройку его на выполнение до инициализатора Discourse ‘inject-discourse-objects’. Например:

    // (plugin)/assets/javascripts/discourse/pre-initializers/extend-user-for-my-plugin.js
    // или
    // (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);
      },
    };
    

    Это изменение модели пользователя теперь должно работать без вывода предупреждения, и новый метод будет доступен в объекте currentUser.


Этот документ находится под контролем версий — предложите изменения на github.

16 лайков