Для продвинутых тем и плагинов Discourse предлагает систему modifyClass. Она позволяет расширять и переопределять функциональность многих классов ядра на JavaScript.
Когда использовать modifyClass
modifyClass следует использовать только в крайнем случае, когда вашу кастомизацию невозможно реализовать через более стабильные API кастомизации Discourse (например, методы plugin-api, outlet-ы плагинов, трансформеры).
Код ядра может измениться в любой момент. Следовательно, кастомизации, сделанные через modifyClass, могут перестать работать в любой момент. При использовании этого API убедитесь, что у вас есть механизмы контроля для выявления таких проблем до того, как они попадут на продакшн-сайт. Например, вы можете добавить автоматизированные тесты к теме/плагину или использовать тестовый сайт (staging) для проверки входящих обновлений Discourse в контексте вашей темы/плагина.
Базовое использование
api.modifyClass можно использовать для изменения функций и свойств любого класса, доступного через резолвер Ember. Это включает маршруты, контроллеры, сервисы и компоненты Discourse.
modifyClass принимает два аргумента:
-
resolverName(строка) — формируется путем указания типа (например, component/controller/etc.), затем двоеточия, а затем имени файла класса (через дефис). Например: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 обнаруживает изменения только в prototype класса на уровне JS. На практике это означает:
-
создание или изменение
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 в ядро (например, outlet-ы плагинов, трансформеры или специфические 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.