Estoy intentando usar la biblioteca Mediaelement.js para anular el audio onebox personalizado, y lo he conseguido cuando la página se carga/actualiza, sin embargo, cuando navego de un tema a otro, no parece recargarse, mostrando en su lugar el audio onebox nativo de Discourse.
Estoy bastante seguro de que estoy haciendo algo mal con la carga de mejs pero pensé que tal vez no y que tenía que hacer algo especial con el onPageChange o algo así.
Este será un post ligeramente largo, ya que publicaste en Development. Sin embargo, es muy general y se trata más de explicar los patrones a utilizar en Discourse, independientemente de la biblioteca que quieras integrar o de los elementos del post que desees apuntar. Solo que resulta que utiliza el script que elegiste como ejemplo principal.
Así que, MediaElement te ofrece varias formas de inicializar un nuevo reproductor de audio.
Puedes agregar una clase CSS y algunos atributos al elemento, y luego lo manejará automáticamente por ti.
Lo inicializas manualmente.
Actualmente estás usando la opción 1, así que veamos eso por un momento. Cuando un script te ofrece “inicialización automática en un elemento”, generalmente es una mejora de calidad de vida que el autor del script añade. En el script, normalmente escuchan el evento de carga del documento y realizan algunas tareas en los elementos del DOM que tienen la clase o los atributos que te indican que agregues.
Bien, ¿por qué no está funcionando? Como viste, funciona perfectamente en la carga inicial de la página, pero no en la navegación posterior dentro de la aplicación. ¿Qué sucede?
La respuesta corta es que Discourse es una aplicación de una sola página (SPA). Elementos como las etiquetas <HTML> y <body> se envían una sola vez. En cierto sentido, el documento solo se carga una vez. Por lo tanto, cuando navegas después de que la página inicial se ha cargado, ya no se envían más eventos nativos de “carga”. Recuerda, el documento ya se cargó en la vista inicial de la página. Todo lo que sucede después de eso es manejado por Discourse.
Por supuesto, eso no significa que no haya eventos que se disparen en la navegación posterior. Sin embargo, esos son eventos específicos de Discourse. Por lo tanto, los autores de scripts de terceros no tendrían forma de conocerlos con anticipación. Imagina ser un autor de scripts y tener que adaptarte a cientos de plataformas diferentes. No es bueno, ¿verdad?
Así que no podemos usar la forma de calidad de vida que el autor del script añadió tan amablemente. ¿Qué sigue? Bueno, recuerda que aún podemos inicializar manualmente el script en los elementos objetivo. Así que intentemos hacerlo.
Antes mencioné que solo hay un evento de carga nativo (a nivel de navegador), pero una plataforma como Discourse no funcionaría bien si no tuviera su propio sistema de eventos. Por ejemplo, la API de plugins tiene un método que te permite ejecutar scripts en la navegación virtual de páginas.
¿Deberías usar ese método? No. Ese método es muy útil para cosas como análisis, etc. No tiene sentido ejecutar un script que solo maneja etiquetas <audio> en cada página, especialmente si la página no tiene ninguna de ellas.
Entonces, ¿qué sigue? Bueno, la buena noticia es que ya lo has descubierto. decorateCookedElement es el método correcto a utilizar aquí.
Te ofrece una forma de… espera por ello… decorar publicaciones
Discourse garantiza que cualquier decorador que agregues se aplicará a cada publicación.
Bueno, estás cargando el script en un decorador de publicaciones, por lo que debería agregarse y debería funcionar. ¿Por qué no funciona en la navegación posterior?
Para esto, tienes que entender cómo funciona loadScript(). Tu código ya verifica si hay elementos objetivo válidos antes de cargar el script, así que
Sin embargo, imagina una situación en la que tienes 20 o 30 publicaciones seguidas que todas tienen elementos válidos. ¿Tendría sentido cargar el script 20 o 30 veces? Obviamente no.
loadScript() es lo suficientemente inteligente como para detectar si el script ya ha sido cargado. No cargará duplicados y tampoco recargará un script si ya ha terminado de descargarse. Puedes ver eso aquí.
fullUrl arriba es la URL que pasas a loadScript() cuando la llamas, igual que en tu ejemplo.
Así que, ahora que sabemos esto, podemos ver por qué no funciona en la navegación posterior.
Visitas el tema-a > tiene un elemento de audio > loadScript() carga el script > el script hace la cosa elegante de "inicialización automática" > el script se inicializa en tus elementos > obtienes elementos de audio personalizados
luego...
visitas el tema-b > tiene elementos de audio > loadScript() ve que el script ya está cargado > no hay "inicialización automática" elegante > obtienes los elementos de audio predeterminados > sobreviene la tristeza
Entonces, ¿cómo solucionas esto? Bueno, para eso sirve la opción 2 de antes.
Puedes agregar una clase CSS y algunos atributos al elemento, y luego lo manejará automáticamente por ti.
Lo inicializas manualmente.
Así que hagámoslo. Ya está documentado en la página que compartiste. Necesitamos llamar a esto en nuestro elemento objetivo de la siguiente manera:
// Puedes usar una cadena para el ID del reproductor (es decir, `player`),
// o `document.querySelector()` para cualquier selector
var player = new MediaElementPlayer("player", {
// ... opciones
});
Tu código ya maneja cada elemento de audio individualmente por separado , así que solo necesitamos modificar esto:
Dejemos esto en espera por ahora y veamos el resto del decorador. Esto es lo que tenemos hasta ahora:
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 used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
Si te fijas, estás llamando a loadScript() en dos scripts diferentes. No estoy seguro si esto es intencional, pero solo necesitas uno de ellos. Piénsalo como un paquete completo y uno ligero. Quieres el reproductor de audio personalizado. Por lo tanto, necesitas el paquete completo. Eliminemos el otro.
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 used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
Estás verificando si hay reproductores de audio en la publicación y cargas el script condicionalmente basado en eso. Esto se puede simplificar de la siguiente manera. Primero, verifica la longitud directamente.
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 used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
luego muévelo a la parte superior y simplemente return si la longitud es falsa (longitud < 0). También eliminé los comentarios del código.
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 used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
Dado que el src del script nunca cambiará, movámoslo a una const. loadScript() también es siempre el mismo. Hagámoslo una const también.
++ 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 used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
y dejemos esto en espera también. Antes de continuar, necesitamos hablar un poco más sobre cómo funciona loadScript().
Si quieres usar alguna parte de un script, quieres asegurarte de que esté cargado antes de hacer cualquier trabajo, ¿verdad? Bueno, loadScript() se encarga de eso por ti. Devuelve una Promesa. Las promesas suenan aterradoras al principio, pero son realmente simples. Una promesa es literalmente eso… una promesa.
Quieres hacer algún trabajo… prometes al navegador que le avisarás cuando el trabajo esté terminado… el navegador espera por ti. Es tan simple como eso. El resto es solo entender la sintaxis.
No me extenderé mucho en eso porque está un poco fuera del alcance de este tema.
Continuemos. loadScript() está basado en promesas. Discourse le promete al navegador que le avisará cuando el script se haya cargado completamente, ya sea que el script no exista y tenga que cargarse o simplemente para verificar si ya se cargó.
Así que, si hacemos algo como esto:
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(() => {
++ // esto SOLO se disparará si el script se ha cargado o está cargando
++ console.log("mi script se ha cargado");
++ });
// forEach used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
Ahora podemos volver a nuestro bucle forEach de antes y agregarlo justo allí, y sabremos con certeza que el script estará disponible.
y luego un poco de CSS para cargar el CSS del script y evitar el parpadeo mientras el script cambia los elementos.
common > css
@import "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelementplayer.min.css";
.cooked {
--audio-player-height: 40px;
.mejs__container,
audio {
// igualar la altura del reproductor de Media-Element.js para evitar el parpadeo
height: var(--audio-player-height);
display: block;
}
}
Ahora deberías ver el reproductor personalizado en cada publicación que tenga elementos válidos.
Con eso fuera del camino, debes notar que la biblioteca que elegiste es bastante antigua. Está transpilada para navegadores antiguos y trata de polifill muchas características que desde entonces se han convertido en estándar.
Si sabes por qué quieres usarla, está bien. Sin embargo, si solo la estás usando para personalizar la apariencia del reproductor, te recomiendo que la evites. No lo he verificado, pero probablemente haya alternativas modernas mucho más ligeras.
Lo mejor de todo esto es que la implementación no cambia de lo anterior. No importa qué elementos quieras apuntar y qué scripts quieras usar. El mismo patrón se aplica. Lo único que cambia es la inicialización del script personalizado. Cada biblioteca decente tiene una documentación bastante buena que te guiará a través de eso. Luego, solo tienes que poner eso en el patrón anterior.
Es en momentos como estos que desearía que Meta usara Discourse Reactions, porque un corazón no se siente suficiente para el amor y la pura gratitud que siento ahora mismo.
Esperaba que alguien al menos me diera un consejo y, sin embargo, te tomaste el tiempo de escribir una explicación muy completa de cómo llegué a donde llegué y me guiaste sobre cómo hacerlo funcionar, enseñándome mucho en el camino (¡incluso con los pequeños detalles sobre el jitter!).
Estoy aprendiendo cómo la estructura de Discourse puede ayudar a fomentar tal comportamiento, al significar que si respondo bien una vez, otros pueden verlo y no tendré que responderlo de nuevo, lo que me anima a seguir construyendo comunidades con esto; y, sin embargo, no creo que eso explique completamente por qué me escribiste esto y tu disposición a hacerlo puede animarme a usar esta plataforma aún más.
Gracias.
En cuanto a Mediaelement, sí, es antiguo, pero combina bien con el sitio de WordPress que tengo y lo he personalizado mucho allí, tratando de ofrecer una apariencia familiar al usuario (y tampoco quiero aprender otra biblioteca en este momento )
Una pregunta de seguimiento y quizás tonta: estoy intentando cargar varios scripts ahora, ya que Mediaelement permite scripts de complementos. Quiero asegurarme de que todos los scripts se carguen antes de devolver la promesa.
Lo he intentado haciendo un bucle a través de las constantes de las fuentes de scripts y luego creando una matriz de promesas, luego usando Promise.all() para inicializar los reproductores, y aun así, cuando lo hago, recibo un error que dice que mejs no se encuentra, que creo que es el espacio de nombres o algo para llamar a diferentes funciones dentro de mediaelement-and-player.
En este caso, solo estoy usando un puñado de scripts, así que los escribí manualmente, solo tenía curiosidad si me estoy perdiendo algo obvio sobre la función Promise.all(), o si hay una función de Discourse que me permita cargar varios scripts desde una matriz.
Tu código debería funcionar bien. Acabas de tropezar con un error en Discourse.
loadScript() no establece el atributo async para los scripts que carga. Por lo tanto, por defecto es async="true" y eso altera tu orden de carga. Es una peculiaridad del navegador. Tienes que forzar async="false" para los scripts cargados con JS.
Los plugins son más pequeños, por lo que se cargan más rápido que el paquete principal, pero al ser async ya no respetan el orden de carga: esperan a que el paquete principal se cargue y ejecute antes de ejecutarse.
Probablemente pasó desapercibido porque loadScript no está anidado en ninguna parte del núcleo, hasta donde yo sé. Normalmente agruparías archivos que necesitan trabajar juntos. Así que, para responder a tu otra pregunta. No, no hay funciones de Discourse que manejen ese tipo de cosas.
Tu otro fragmento también debería funcionar. Para que sea un poco más fácil de leer, quizás intenta encadenarlos sin anidarlos.
// Esto va fuera del decorador
const PLUGINS = {
speed: "https://example.com/foo.js",
skipBack: "https://example.com/bar.js",
jumpForward: "https://example.com/baz.js"
};
// Luego haz tu trabajo dentro del decorador
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);
});
});