Отзывы о «on-discourse» JavaScript для настройки пользовательского JS на каждой странице?

Я пишу приложение на Svelte, которое должно устанавливаться на каждой странице. Я разобрался, как добавить свой JS-файл в секцию head и заставить его загружаться при первой загрузке страницы. Однако, экспериментируя, я понял, что Discourse загружает новый контент через XHR и заменяет определённые разделы, поэтому моё приложение не переинициализировалось при загрузке новой страницы.

Я пробовал разные способы получить уведомление об изменении страницы, но, похоже, в Ember нет соответствующих хуков, и мне не удалось найти какие-либо пользовательские события, на которые можно было бы подписаться.

Один из вариантов — добавить наблюдателя за изменениями (MutationObserver) к DOM и отслеживать изменения. Я обнаружил, что пересоздаётся именно div с id “#topic” (и его атрибуты меняются, так что можно просто отслеживать изменения атрибутов). Я настроил MutationObserver именно там (MutationObserver — это новый и производительный способ отслеживать изменения в DOM). Принцип работы следующий: отслеживать изменения, и когда они происходят, выполнять обратный вызов для перезапуска моего приложения Svelte на этой странице.

Мне нравится этот подход, и я хотел бы получить обратную связь.

Один вопрос: не лучше ли отслеживать изменения URL? Это более разумная идея — зарегистрировать слушатель для события popstate?

Чтобы использовать это, добавьте следующее в ваш theme/head:

<script src="https://files.extrastatic.dev/community/on-discourse.js"></script>
<script src="https://files.extrastatic.dev/community/index.js"></script>
<link rel="stylesheet" type="text/css" href="https://files.extrastatic.dev/community/index.css">

Затем внутри вашей библиотеки вы можете вызывать on-discourse следующим образом:

function log(...msg) {
  console.log('svelte', ...msg);
}

// Это код установки Svelte
function setup() {
  try {
    const ID = 'my-special-target-id';
    log('Inside setup()');
    const el = document.getElementById(ID);
    if (el) {
      log('Removed existing element', ID);
      el.remove();
    }
    const target = document.createElement("div");
    target.setAttribute("id", ID);
    log('Created target');
    document.body.appendChild(target);
    log('Appended child to body');
    const app = new App({
      // eslint-disable-next-line no-undef
      target
    });
    log('Created app and installed');
  } catch(err) {
    console.error('Unable to complete setup()', err.toString() );
  }
}

(function start() {
  log('Starting custom Svelte app');
  // Переустановка при изменениях
  window.onDiscourse && window.onDiscourse( setup );
  // Загрузка приложения при первой загрузке страницы
  window.addEventListener('load', () => {
    setup();
  });
  log('Finished custom Svelte app');  
})();

По сути, вам нужно просто вызвать window.onDiscourse(callback) с вашим обратным вызовом (и вы можете вызывать это несколько раз для установки нескольких обратных вызовов), и тогда при возникновении изменения будет выполнен этот обратный вызов для инициализации вашего приложения.

Ниже приведён полный код для on-discourse.js. (правка: я обновил его, чтобы использовать #topic, что кажется хорошим выбором для отслеживания, поскольку атрибуты меняются при загрузке страницы, и тогда наблюдателю нужно отслеживать только изменения атрибутов, а не просматривать всё дерево DOM для #main-outlet)

let mutationObservers = [];

function log(...msg) {
  console.log("on-discourse", ...msg);
}

function observeMainOutlet() {
  log('Observing main outlet');
  // Выбираем узел, за которым будем следить на предмет изменений
  const targetNode = document.getElementById("topic");
 
  if (targetNode) {
    // Настройки наблюдателя (какие изменения отслеживать)
    const config = { attributes: true };
    
   // Создаём экземпляр наблюдателя для сброса при изменении childList
    const observer = new MutationObserver(function(mutations) {
      let reset = false;
      mutations.forEach(function(mutation) {
	if (mutation.type === 'attributes') {
	  log('Found main-outlet mutation, running callbacks');
	    mutationObservers.forEach( (s,i) => {
		try {
		    log(`Running div callback ${i+1}`);	    
		    s();
		    log(`Finished div callback ${i+1}`);	    
		} catch( err ) {
		    log(`Div callback error (${i+1})`, err );	    	      
		}
	 });
       }
      });
    });
    
    // Запускаем наблюдение за целевым узлом с указанными настройками
    observer.observe(targetNode, config);
    
    // Позже вы можете прекратить наблюдение
    // observer.disconnect();
    log('Done with outlet observer');
  } else {
    console.error('on-discourse FATAL ERROR: Unable to find main-outlet');
  }
}

window.addDiscourseDivMutationObserver = (cb) => {
    log('Adding on-discourse div mutation callback');  
    mutationObservers.push(cb);
    log('Added on-discourse div mutation callback');    
}

window.addEventListener("load", () => {
    log('Setting up topic observer');
    if (mutationObservers.length > 0) {
	observeMainOutlet();
    }
    log('Created topic observer');  
});

log('Completed setup of on-discourse.js');

Я думаю, вы хотите поместить свой код в инициализатор, а не в head. Изучите руководство для разработчиков и посмотрите, как можно разместить ваш JS в отдельных файлах.

Спасибо большое за ответ!

Думаю, отчасти именно поэтому я запутался: мне трудно найти какие-либо ссылки на расширение Discourse путём добавления собственного JavaScript.

Когда я делаю поиск в DuckDuckGo по запросу «Discourse developer guide», первая ссылка ведёт на репозиторий GitHub.

Следующая ссылка ведёт на «Discourse Advanced Developer Install Guide». Это руководство объясняет, как настроить Rails для разработки, но, насколько я вижу, не содержит ссылок о том, как установить пользовательский JavaScript. Я стараюсь избежать сложного процесса сборки, о котором помню со времён работы с Rails. Мне бы очень хотелось разрабатывать этот код расширения JavaScript изолированно, а затем просто добавить тег script на свой сайт. Поэтому я совсем не хочу настраивать локальную среду Rails только для того, чтобы собрать его; возможно, я упускаю какую-то пользу от этого? Но мне очень нравится возможность просто обновлять Docker-контейнер, использующий тему с несколькими тегами .

Следующая ссылка — «Beginner’s guide to developing Discourse Themes», которая посвящена разработке тем, а не тому, что нужно мне, верно?

Я вижу ссылки на Discourse API, что, очевидно, не то, что мне нужно.

Если я поищу «discourse javascript initializer», то найду эту ссылку пятилетней давности: Execute JavaScript code from a plugin once after load. Но это похоже на подключение к Rails, а мне кажется, что должен быть более простой способ. К тому же эта тема, похоже, так и осталась без решения?

Другая ссылка по запросу «discourse javascript initializer» предлагает делать то же, что и я, для установки JavaScript, но не даёт советов о том, как гарантировать выполнение кода при каждом изменении содержимого страницы (будь то полная перезагрузка страницы или XHR-запрос, похожий на «turbolinks»): https://stackoverflow.com/questions/48611621/how-do-i-add-an-external-javascript-file-into-discourse

Это ли обсуждение, которое мне следует изучить? A versioned API for client side plugins

Или, может быть, это? На первый взгляд я не понимаю синтаксис (эти аннотации не выглядят как JavaScript, неужели это соглашения Rails?), поэтому не уверен, что это именно то, что мне нужно: Using Plugin Outlet Connectors from a Theme or Plugin

Да, это не всегда тривиально.

Discourse — это приложение на EmberJS.

В идеале расширения JavaScript должны быть написаны в рамках фреймворка EmberJS с использованием компонента темы (или плагина), API JavaScript Discourse (где это уместно) и точек расширения плагинов.

Основная проблема, с которой вы столкнетесь при использовании разрозненных внешних скриптов, — обеспечение их запуска в нужное время.

Чтобы гарантировать это, необходимо связать их с действиями в компоненте (который можно настроить на срабатывание при вставке или обновлении).

Отличное подтверждение. Но должен сказать, я очень доволен своим кодом с MutationObserver. Он корректно определяет изменения содержимого страницы и позволяет запускать мой собственный JS-код. Всё работает прекрасно, при этом мне не пришлось изучать Ember или как-либо модифицировать приложение на RoR. Пока я очень доволен этим решением. Спасибо за все комментарии.

Я только что попробовал использовать обработчик события popstate. Если бы это сработало, код состоял бы из 5 строк вместо 20. Однако клики по ссылкам, похоже, не вызывают это событие. Если же я использую кнопки «вперёд» или «назад», событие действительно срабатывает. Я, конечно, не до конца понимаю popstate, но пока остаюсь на варианте с MutationObserver для div.

Я понял, что совершал здесь одну глупость. Я изменял тему и добавлял код в секцию . Если переключиться на другую тему, эти изменения теряются. Правильный способ — добавлять код с помощью плагина. Я использовал шаблон здесь: GitHub - discourse/discourse-plugin-skeleton: Template for Discourse plugins · GitHub. Затем я добавил JavaScript-код следующим образом:

export default {
  name: 'alert',
    initialize() {
        const scripts = [
            'https://files.extrastatic.dev/community/on-discourse.js',
            'https://files.extrastatic.dev/community/index.js'
        ];
        scripts.forEach( s => {
            const script = document.createElement('script');
            script.setAttribute('src', s);
            document.head.appendChild(script);
        });

        const link = document.createElement('link');
        link.setAttribute('rel', "stylesheet");
        link.setAttribute('type', "text/css");
        link.setAttribute('href', "https://files.extrastatic.dev/community/index.css");
        document.head.appendChild(link);
    }
};

Затем я выполнил ./launcher rebuild app и включил плагин.

Наконец, мне нужно было добавить политику CSP в настройках, чтобы разрешить загрузку этих скриптов. Я перешёл в админ-панель → настройки → безопасность и добавил files.extrastatic.dev в поле «content security policy script src», затем нажал «Применить».

Это неверно.

Если вы не изменяете API (например, используя только JavaScript), нет необходимости использовать плагины, которые сложнее развернуть и заменить.

Если вы только модифицируете или добавляете JavaScript, достаточно компонента темы.

Хорошо. Но когда я переключаю темы (или если я позволяю пользователю выбрать свою тему), не сталкиваюсь ли я с проблемой, что мне нужно убедиться, что каждая тема: 1) редактируема, чтобы я мог добавлять новые теги head, и, что ещё хуже, 2) мне нужно поддерживать мой код во всех темах?

Когда я начал использовать разные темы, некоторые указывают, что для редактирования темы необходимо изменить репозиторий GitHub. Это кажется действительно обременительным и негибким. Но поддерживать мои скриптовые теги в каждой теме кажется очень подверженным ошибкам и ещё более серьёзной проблемой.

Что я упускаю? Есть ли способ решить эти проблемы, используя только темы?

В этом случае вы можете сделать его компонентом темы и добавить этот компонент во все темы. (Признаюсь, это добавляет дополнительную сложность).

Если вы планируете запускать сайт профессионально, то размещение всего вашего кода на GitHub — действительно отличная идея.

Однако на начальном этапе, когда вы только пробуете разные идеи, вы, безусловно, можете экспериментировать с ним «локально», если захотите.

Когда изменения в коде стабилизируются, вам, вероятно, стоит поместить их в систему контроля версий, но я бы утверждал, что чем раньше, тем лучше.

Один из способов совместить быструю эволюцию с GitHub — использовать это:

И развернуть тестовый компонент темы в тестовой теме… но теперь мы действительно начинаем усложнять…

Это позволяет развертывать изменения на лету, после чего вы можете зафиксировать свои изменения в репозитории Git, как только будете удовлетворены результатом.

Посмотрите на GitHub - discourse/discourse-theme-skeleton: Template for Discourse themes · GitHub

Вот что вам нужно: How do you force a script to refire on every page load in Discourse? - #5 by simon

<script type="text/discourse-plugin" version="0.8">
    api.onPageChange(() =>{
        // код
    });
</script>

Вау, @RGJ, это выглядит просто идеально! Спасибо!

Привет, @RGJ! Я пытаюсь разобраться, как это сделать, и должен признать, что запутался. Похоже, API плагинов немного изменился за эти годы, и я не уверен, как получить актуальную информацию или увидеть пример.

Ваш код, похоже, должен размещаться на какой-то HTML-странице, так как он обернут в теги script. Я использовал код вроде этого, который является самим JS-файлом.

https://extrastatic.dev/publicdo/publicdo-discourse-plugin/-/blob/main/assets/javascripts/discourse/initializers/kanji.js?ref_type=heads

Как мне изменить свой плагин, чтобы использовать предоставленный вами код? Этот код, который вы предложили, предназначен для плагина, или его нужно разместить в теме, или как-то иначе добавить на страницы?

Я попробовал использовать код вроде этого:

export default {
  name: 'publicdo',
    initialize() {
     withPluginApi('0.1', api => {
                api.onPageChange(() => {
                   console.log('Выполните здесь мой код.');
                });
      });
    }
}

Но это завершается ошибкой Uncaught (in promise) ReferenceError: withPluginApi is not defined, поэтому очевидно, что это не то, что обычно загружается в JS-файлах.

Вам достаточно просто

import { withPluginApi } from "discourse/lib/plugin-api";

Вам действительно стоит изучить исходный код по ссылкам в #theme-component (экосистема почти полностью с открытым исходным кодом — пользуйтесь на здоровье!).

Вы заметите, что импорты часто необходимы и распространены (как и использование api.onPageChange и других полезных функций API).