Для продвинутых тем и плагинов 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). Любой код, использующий этот синтаксис, должен быть обновлен до использования синтаксиса нативных классов, описанного выше. В целом, конвертация может быть выполнена следующим образом:
- Удалите
pluginId— это больше не требуется - Обновите до современного синтаксиса нативных классов, описанного выше
- Протестируйте свои изменения
Устранение неполадок
Класс уже инициализирован
При использовании 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.