Я пытаюсь использовать библиотеку Mediaelement.js для переопределения стандартного аудиоблока (onebox), и у меня это работает при загрузке или обновлении страницы. Однако при переходе от одной темы к другой она, похоже, не перезагружается, и вместо этого отображается нативный аудиоблок Discourse.
Я почти уверен, что допускаю ошибку при загрузке mejs, но возможно, что проблема не в этом, и мне нужно сделать что-то особенное с onPageChange или чем-то подобным.
Это будет немного длинный пост, так как вы написали в Development. Однако тема довольно общая: речь идёт о паттернах, которые стоит использовать в Discourse независимо от того, какую библиотеку вы хотите интегрировать или какие элементы постов хотите обработать. Просто в качестве основного примера используется скрипт, который вы выбрали.
Итак, MediaElement предлагает несколько способов инициализации нового аудиоплеера.
Вы можете добавить CSS-класс и несколько атрибутов к элементу, и скрипт автоматически обработает это за вас.
Вы можете инициализировать его вручную.
Сейчас вы используете вариант №1, поэтому давайте немного разберёмся с ним. Когда скрипт предоставляет «автоматическую инициализацию на элементе», это обычно улучшение качества жизни, которое добавляет автор скрипта. В самом скрипте он обычно слушает событие загрузки документа и выполняет определённую работу с элементами DOM, у которых есть указанные классы или атрибуты.
Хорошо, но почему это не работает? Как вы заметили, всё работает отлично при первоначальной загрузке страницы, но не работает при последующей навигации внутри приложения. В чём дело?
Короткий ответ: Discourse — это одностраничное приложение (SPA). Элементы вроде тегов <HTML> и <body> отправляются только один раз. В некотором смысле документ загружается лишь однажды. Поэтому, когда вы переходите по страницам после первоначальной загрузки, больше не происходит нативных событий «загрузки». Помните: документ уже был загружен при первом просмотре страницы. Всё, что происходит после этого, обрабатывается самим Discourse.
Конечно, это не значит, что при последующей навигации вообще не происходят события. Однако это специфичные для Discourse события. Авторы сторонних скриптов не могут заранее знать о них. Представьте себя автором скрипта, которому нужно адаптироваться к сотням различных платформ? Это не сработает, верно?
Таким образом, мы не можем использовать тот удобный способ, который так красиво добавил автор скрипта. Что делать дальше? Помните, что мы всё ещё можем вручную инициализировать скрипт на целевых элементах. Попробуем это сделать.
Ранее я упоминал, что существует только одно нативное (на уровне браузера) событие загрузки, но платформа вроде Discourse не могла бы нормально функционировать без собственной системы событий. Например, API плагинов предоставляет метод, который позволяет запускать скрипты при виртуальной навигации по страницам.
Стоит ли использовать этот метод? Нет. Этот метод очень полезен для таких задач, как аналитика и тому подобное. Нет смысла запускать скрипт, который обрабатывает только теги <audio>, на каждой странице — особенно если на странице вообще нет таких тегов.
Так что же дальше? Хорошая новость в том, что вы уже сами это поняли. decorateCookedElement — это правильный метод для использования здесь.
Он предоставляет способ… подождите… декорировать посты
Discourse гарантирует, что любой декоратор, который вы добавите, будет применяться ко всем постам.
Хорошо, вы загружаете скрипт внутри декоратора постов, значит, он должен быть добавлен и работать. Почему же это не происходит при последующей навигации?
Для этого нужно понять, как работает loadScript(). Ваш код уже проверяет наличие валидных целевых элементов перед загрузкой скрипта, так что
Однако представьте ситуацию, когда у вас 20–30 постов подряд, и во всех есть валидные элементы. Имеет ли смысл загружать скрипт 20–30 раз? Очевидно, нет.
loadScript() достаточно умён, чтобы определить, был ли скрипт уже загружен. Он не будет загружать дубликаты и не перезагрузит скрипт, если он уже полностью скачан. Вы можете увидеть это здесь.
fullUrl выше — это URL, который вы передаёте в loadScript() при вызове, точно так же, как в вашем примере.
Итак, теперь, когда мы это знаем, мы можем понять, почему это не работает при последующей навигации.
Вы заходите на тему-а → там есть аудиоэлемент → loadScript() загружает скрипт → скрипт выполняет свою «автоматическую инициализацию» → скрипт инициализируется на ваших элементах → вы получаете кастомные аудиоплееры
затем...
вы заходите на тему-б → там тоже есть аудиоэлементы → loadScript() видит, что скрипт уже загружен → никакой «автоматической инициализации» → вы получаете стандартные аудиоплееры → наступает грусть
Итак, как это исправить? Для этого как раз и нужен вариант №2 из начала.
Вы можете добавить CSS-класс и несколько атрибутов к элементу, и скрипт автоматически обработает это за вас.
Вы можете инициализировать его вручную.
Давайте сделаем это. Это уже описано на странице, которую вы поделили. Нам нужно вызвать это на целевом элементе следующим образом:
// Вы можете использовать либо строку с ID плеера (например, `player`),
// либо `document.querySelector()` для любого селектора
var player = new MediaElementPlayer("player", {
// ... опции
});
Ваш код уже обрабатывает каждый аудиоэлемент отдельно , поэтому нам нужно лишь изменить этот блок:
Давайте пока остановимся на этом и посмотрим на остальную часть декоратора. Вот что у нас есть на данный момент:
let loadScript = require("discourse/lib/load-script").default;
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
// console.log("player: " + audioplayers[0]);
if (Object.entries(audioplayers).length > 0) {
// console.log("audioplayers has length");
loadScript(
`https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
);
loadScript(
`https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
);
}
// forEach использовался здесь
},
{ id: "mediaelement-js", onlyStream: true }
);
Если вы заметили, вы вызываете loadScript() для двух разных скриптов. Не уверен, что это было намеренно, но вам нужен только один из них. Представьте это как полный пакет и облегчённую версию. Вам нужен кастомный аудиоплеер, поэтому вам нужен полный пакет. Давайте удалим другой.
let loadScript = require("discourse/lib/load-script").default;
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
// console.log("player: " + audioplayers[0]);
if (Object.entries(audioplayers).length > 0) {
// console.log("audioplayers has length");
-- loadScript(
-- `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
-- );
loadScript(
`https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
);
}
// forEach использовался здесь
},
{ id: "mediaelement-js", onlyStream: true }
);
Вы проверяете наличие аудиоплееров в посте и условно загружаете скрипт на основе этого. Это можно упростить следующим образом. Сначала проверьте длину напрямую.
let loadScript = require("discourse/lib/load-script").default;
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
// console.log("player: " + audioplayers[0]);
-- if (Object.entries(audioplayers).length > 0) {
++ if (audioplayers.length) {
// console.log("audioplayers has length");
loadScript(
`https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
);
}
// forEach использовался здесь
},
{ id: "mediaelement-js", onlyStream: true }
);
затем переместите это в начало и просто return, если длина ложна (length < 0). Я также убрал комментарии из кода.
let loadScript = require("discourse/lib/load-script").default;
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
++ if (!audioplayers.length) {
++ return;
++ }
loadScript(
`https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
);
// forEach использовался здесь
},
{ id: "mediaelement-js", onlyStream: true }
);
Поскольку src скрипта никогда не изменится, давайте вынесем его в const. loadScript() тоже всегда одинаковый. Сделаем его тоже const.
++ const loadScript = require("discourse/lib/load-script").default;
++ const MEDIA_ELEMENT_SRC =
++ "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
if (!audioplayers.length) {
return;
}
++ loadScript(MEDIA_ELEMENT_SRC);
// forEach использовался здесь
},
{ id: "mediaelement-js", onlyStream: true }
);
Давайте пока оставим и это. Прежде чем продолжить, нужно ещё немного поговорить о том, как работает loadScript().
Если вы хотите использовать какую-то часть скрипта, вы хотите убедиться, что он загружен, прежде чем выполнять любую работу, верно? loadScript() решает это за вас. Он возвращает Promise. Промисы сначала могут показаться пугающими, но на самом деле они очень просты. Промис — это буквально это… обещание.
Вы хотите выполнить какую-то работу… вы обещаете браузеру, что сообщите ему, когда работа будет завершена… браузер ждёт вас. Всё так просто. Остальное — это просто понимание синтаксиса.
Я не буду тратить на это много времени, так как это немного выходит за рамки данной темы.
Продолжим. loadScript() работает на основе промисов. Discourse обещает браузеру сообщить, когда скрипт полностью загрузится — независимо от того, нужно ли его загружать, потому что его нет, или просто проверить, был ли он уже загружен.
Так что, если мы сделаем что-то вроде этого:
const loadScript = require("discourse/lib/load-script").default;
const MEDIA_ELEMENT_SRC =
"https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
if (!audioplayers.length) {
return;
}
++ loadScript(MEDIA_ELEMENT_SRC).then(() => {
++ // это сработает ТОЛЬКО если скрипт загрузился или загружается
++ console.log("мой скрипт загрузился");
++ });
// forEach использовался здесь
},
{ id: "mediaelement-js", onlyStream: true }
);
Теперь мы можем вернуться к нашему циклу forEach из начала и добавить его прямо туда. Мы будем точно знать, что скрипт будет доступен.
Теперь вы должны видеть кастомный плеер в каждом посте, где есть валидные элементы.
Разобравшись с этим, стоит отметить, что выбранная вами библиотека довольно старая. Она транслируется для древних браузеров и пытается полифиллить множество функций, которые с тех пор стали стандартом.
Если вы знаете, зачем хотите её использовать, это нормально. Однако, если вы просто хотите кастомизировать внешний вид плеера, я рекомендую отказаться от неё. Я не проверял, но, вероятно, есть гораздо более лёгкие современные альтернативы.
Самое лучшее во всём этом то, что реализация не меняется по сравнению с приведённым выше примером. Независимо от того, какие элементы вы хотите обработать и какие скрипты хотите использовать. Тот же паттерн применим. Меняется только инициализация кастомного скрипта. Каждая приличная библиотека имеет довольно хорошую документацию, которая проведёт вас через этот процесс. Затем вы просто вставляете это в приведённый выше паттерн.
В такие моменты я жалею, что Meta не использует Discourse Reactions, потому что одно лишь сердце не может выразить ту любовь и искреннюю благодарность, которые я испытываю сейчас.
Я надеялся, что кто-нибудь хотя бы просто даст мне подсказку, а вы нашли время написать подробное объяснение того, как я пришёл к текущему результату, и пошагово провели меня через процесс настройки, научив при этом многому (даже таким мелким деталям, как джиттер!)
Я учусь понимать, как структура Discourse может способствовать такому поведению: если я однажды дам хороший ответ, другие смогут его увидеть, и мне не придётся отвечать снова. Это мотивирует меня продолжать создавать сообщества с помощью этой платформы. Однако я не думаю, что это полностью объясняет, почему вы потратили время на написание этого для меня. Ваша готовность сделать это, возможно, ещё больше подтолкнёт меня к использованию этой платформы.
Спасибо.
Что касается Mediaelement, да, он старый, но он хорошо сочетается с моим сайтом на WordPress, который я сильно кастомизировал, чтобы пользователям был знакомый интерфейс (и чтобы не учить ещё одну библиотеку прямо сейчас )
Дополнительный вопрос, возможно, глупый: я пытаюсь загрузить несколько скриптов, так как Mediaelement позволяет использовать скрипты плагинов. Мне нужно убедиться, что все скрипты загрузятся перед возвратом промиса.
Я пробовал сделать это, перебирая константы с источниками скриптов, создавая массив промисов и используя Promise.all() для инициализации плееров, но при этом получаю ошибку, что mejs не найден. Я полагаю, что это пространство имён или что-то подобное для вызова различных функций внутри mediaelement-and-player.
В данном случае я использую всего несколько скриптов, поэтому просто прописал их вручную. Мне просто интересно, упускаю ли я что-то очевидное в функции Promise.all(), или существует ли функция Discourse, позволяющая загружать несколько скриптов из массива.
Ваш код должен работать нормально. Вы просто наткнулись на ошибку в Discourse.
loadScript() не устанавливает атрибут async для загружаемых скриптов. Поэтому по умолчанию он принимает значение async="true", что нарушает порядок загрузки. Это особенность браузера. Для скриптов, загружаемых через JS, нужно явно указывать async="false".
Плагины меньше по размеру, поэтому загружаются быстрее основного пакета, но из-за атрибута async они больше не соблюдают порядок загрузки — то есть не ждут загрузки и выполнения основного пакета перед своим выполнением.
Скорее всего, это осталось незамеченным, потому что loadScript, насколько мне известно, нигде не вложен в ядро. Обычно файлы, которые должны работать вместе, объединяют в один пакет. Так что, отвечая на ваш другой вопрос: нет, в Discourse нет функций, которые решают такую задачу.
Ваш другой фрагмент тоже должен работать. Чтобы сделать его чуть более читаемым, попробуйте использовать цепочку вызовов без вложенности.
// Это размещается вне декоратора
const PLUGINS = {
speed: "https://example.com/foo.js",
skipBack: "https://example.com/bar.js",
jumpForward: "https://example.com/baz.js"
};
// Затем выполняйте свою работу внутри декоратора
loadScript(MEDIA_ELEMENT_SRC)
.then(() => loadScript(PLUGINS.speed))
.then(() => loadScript(PLUGINS.skipBack))
.then(() => loadScript(PLUGINS.jumpForward))
.then(() => {
audioplayers.forEach(function (el) {
new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
});
});