Estou tentando usar a biblioteca Mediaelement.js para substituir o onebox de áudio personalizado, e consegui fazer funcionar quando a página é carregada/atualizada, no entanto, quando navego de um tópico para outro, ele não parece recarregar, mostrando em vez disso o onebox de áudio nativo do Discourse.
Tenho certeza de que estou fazendo algo errado com o carregamento do mejs, mas pensei que talvez não fosse isso e que eu tivesse que fazer algo especial com o onPageChange ou algo assim.
Este será um post um pouco longo, já que você postou em Development. No entanto, é muito geral e trata mais de explicar os padrões a serem usados no Discourse, independentemente da biblioteca que deseja integrar ou dos elementos de post que deseja segmentar. Acontece apenas que o script que você escolheu é usado como exemplo principal.
Então, o MediaElement oferece algumas maneiras de inicializar um novo player de áudio.
Você pode adicionar uma classe CSS e alguns atributos ao elemento, e ele cuidará disso automaticamente.
Você o inicializa manualmente.
Você está usando atualmente a opção #1, então vamos analisar isso por um momento. Quando um script oferece “inicialização automática em um elemento”, geralmente é uma melhoria de qualidade de vida adicionada pelo autor do script. No script, eles geralmente ouvem o evento de carregamento do documento e realizam algum trabalho nos elementos do DOM que possuem a classe/os atributos que instruem você a adicionar.
Ok, então por que não está funcionando? Como você viu, funciona bem no carregamento inicial da página, mas não na navegação subsequente dentro do aplicativo. O que houve?
A resposta curta é que o Discourse é um aplicativo de página única. Elementos como as tags <HTML> e <body> são enviados apenas uma vez. Então, em certo sentido, o documento só é carregado uma vez. Assim, quando você navega após o carregamento inicial da página, não há mais eventos nativos de “carregamento” enviados. Lembre-se, o documento já foi carregado na visualização inicial da página. Tudo o que acontece depois disso é tratado pelo Discourse.
Claro, isso não significa que não haja eventos disparados na navegação subsequente. No entanto, esses são eventos específicos do Discourse. Portanto, os autores de scripts de terceiros não teriam como saber disso com antecedência. Imagine ser um autor de script e ter que atender a centenas de plataformas diferentes? Não é bom, né?
Então, não podemos usar a maneira de qualidade de vida que o autor do script adicionou tão gentilmente. E agora? Bem, lembre-se de que ainda podemos inicializar o script manualmente nos elementos de destino. Então, vamos tentar fazer isso.
Anteriormente, mencionei que há apenas um evento de carregamento nativo (no nível do navegador), mas uma plataforma como o Discourse não funcionaria bem se não tivesse seu próprio sistema de eventos. Por exemplo, a API de plugins tem um método que permite disparar scripts na navegação virtual de páginas.
Você deve usar esse método? Não. Esse método é muito útil para coisas como análise de dados e afins. Não há sentido em disparar um script que lida apenas com tags <audio> em todas as páginas — especialmente se a página não tiver nenhuma delas.
Então, qual é o próximo passo? Bem, a boa notícia é que você já descobriu. decorateCookedElement é o método correto a ser usado aqui.
Ele oferece uma maneira de… espere por isso… decorar posts
O Discourse garante que qualquer decorador que você adicionar será aplicado a todos os posts.
Bem, você está carregando o script em um decorador de post, então ele deve ser adicionado e deve funcionar. Por que não funciona na navegação subsequente?
Para isso, você precisa entender como loadScript() funciona. Seu código já verifica se há elementos de destino válidos antes de carregar o script, então
No entanto, imagine uma situação em que você tenha 20 a 30 posts seguidos onde todos possuem elementos válidos. Faria sentido carregar o script 20 a 30 vezes? Obviamente não.
loadScript() é inteligente o suficiente para detectar se o script já foi carregado. Ele não carregará duplicatas e também não recarregará um script se já tiver terminado o download. Você pode ver isso aqui.
fullUrl acima é a URL que você passa para loadScript() ao chamá-lo, assim como no seu exemplo.
Então, agora que sabemos disso, podemos entender por que não funciona na navegação subsequente.
Você visita o tópico-a > ele tem um elemento de áudio > loadScript() carrega o script > o script faz a coisa sofisticada de "inicialização automática" > o script é inicializado nos seus elementos > você obtém elementos de áudio personalizados
então...
você visita o tópico-b > ele tem elementos de áudio > loadScript() vê que o script já foi carregado > nenhuma "inicialização automática" sofisticada > você obtém os elementos de áudio padrão > tristeza se segue
Então, como corrigir isso? Bem, é para isso que serve a opção #2 mencionada anteriormente.
Você pode adicionar uma classe CSS e alguns atributos ao elemento, e ele cuidará disso automaticamente.
Você o inicializa manualmente.
Então, vamos fazer isso. Já está documentado na página que você compartilhou. Precisamos chamar isso no nosso elemento de destino assim:
// Você pode usar uma string para o ID do player (ou seja, `player`),
// ou `document.querySelector()` para qualquer seletor
var player = new MediaElementPlayer("player", {
// ... opções
});
Seu código já trata cada elemento de áudio individualmente , então só precisamos modificar isso:
Vamos deixar isso de lado por enquanto e olhar o restante do decorador. Aqui está o que temos até agora:
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 usado anteriormente estava aqui
},
{ id: "mediaelement-js", onlyStream: true }
);
Se você notar, está chamando loadScript() em dois scripts diferentes. Não tenho certeza se isso é intencional, mas você só precisa de um deles. Pense nisso como um pacote completo e um leve. Você quer o player de áudio personalizado. Então, você precisa do pacote completo. Vamos remover o outro.
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 usado anteriormente estava aqui
},
{ id: "mediaelement-js", onlyStream: true }
);
Você está verificando se há players de áudio no post e carregando o script condicionalmente com base nisso. Isso pode ser simplificado assim. Primeiro, verifique o comprimento diretamente.
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 usado anteriormente estava aqui
},
{ id: "mediaelement-js", onlyStream: true }
);
em seguida, mova isso para o topo e apenas retorne se o comprimento for falso (comprimento < 0). Também removi os comentários do 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 usado anteriormente estava aqui
},
{ id: "mediaelement-js", onlyStream: true }
);
Como o src do script nunca mudará, vamos movê-lo para um const. loadScript() também é sempre o mesmo. Vamos torná-lo um const também.
++ 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 usado anteriormente estava aqui
},
{ id: "mediaelement-js", onlyStream: true }
);
e vamos deixar este também de lado por enquanto. Antes de continuarmos, precisamos falar um pouco mais sobre como loadScript() funciona.
Se você quiser usar alguma parte de um script, deseja garantir que ele seja carregado antes de fazer qualquer trabalho, não é? Bem, loadScript() cuida disso para você. Ele retorna uma Promessa. Promessas parecem assustadoras à primeira vista, mas são realmente simples. Uma Promessa é literalmente isso… uma promessa.
Você quer fazer algum trabalho… você promete ao navegador que avisará quando o trabalho estiver concluído… o navegador espera por você. É realmente tão simples quanto isso. O resto é apenas entender a sintaxe.
Não vou gastar muito tempo com isso, pois está um pouco fora do escopo deste tópico.
Vamos continuar. loadScript() é baseado em promessa. O Discourse promete ao navegador avisá-lo quando o script tiver sido totalmente carregado — seja porque o script não existe e precisa ser carregado ou simplesmente para verificar se já foi carregado.
Então, se fizermos algo assim:
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(() => {
++ // isso só será disparado se o script tiver sido/carregado
++ console.log("meu script foi carregado");
++ });
// forEach usado anteriormente estava aqui
},
{ id: "mediaelement-js", onlyStream: true }
);
Agora, podemos voltar ao nosso loop forEach anterior e adicioná-lo exatamente ali, e saberemos com certeza que o script estará disponível.
e então um pouco de CSS para carregar o CSS do script e evitar tremores enquanto o script troca os 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 a altura do player do Media-Element.js para evitar tremores
height: var(--audio-player-height);
display: block;
}
}
Agora você deve ver o player personalizado em todos os posts que possuem elementos válidos.
Com isso resolvido, você deve notar que a biblioteca que você escolheu é bastante antiga. Ela é transpilada para navegadores antigos e tenta fazer polyfill de muitas funcionalidades que desde então se tornaram padrão.
Se você sabe por que quer usá-la, tudo bem. No entanto, se você está apenas usando-a para personalizar a aparência do player, recomendo evitá-la. Não verifiquei, mas provavelmente há alternativas modernas muito mais leves.
O melhor de tudo isso é que a implementação não muda do que foi mostrado acima. Não importa quais elementos você deseja segmentar ou quais scripts deseja usar. O mesmo padrão se aplica. A única coisa que muda é a inicialização do script personalizado. Toda biblioteca decente tem uma documentação muito boa que o guiará por isso. Então, você apenas coloca isso no padrão acima.
É em momentos como este que eu gostaria que a Meta estivesse usando Discourse Reactions, porque um coração não parece suficiente para o amor e a pura gratidão que sinto agora.
Eu esperava que alguém pelo menos me desse uma dica e, no entanto, você dedicou tempo para escrever uma explicação muito completa de como cheguei onde cheguei e me guiou em como fazer funcionar, ensinando-me muito ao longo do caminho (até mesmo com os pequenos detalhes sobre jitter!)
Estou aprendendo como a estrutura do Discourse pode ajudar a incentivar tal comportamento, significando que se eu responder bem uma vez, outros poderão ver e eu não terei que responder novamente — o que me incentiva a continuar construindo comunidades com isso; e, no entanto, não acho que isso explique totalmente por que você escreveu isso para mim e sua disposição em fazê-lo pode me incentivar a usar esta plataforma ainda mais.
Obrigado.
Em relação ao Mediaelement, sim, é antigo, mas combina bem com o site WordPress que tenho e o personalizei bastante lá, tentando fornecer uma aparência familiar ao usuário (e também não tentando aprender mais uma biblioteca no momento )
Uma pergunta de acompanhamento e talvez uma pergunta boba: estou tentando carregar vários scripts agora, já que o Mediaelement permite scripts de plugin. Quero ter certeza de que todos os scripts sejam carregados antes de retornar a promessa.
Tentei fazer isso percorrendo as constantes das fontes de script e, em seguida, criando uma matriz de promessas, depois usando Promise.all() para inicializar os players e, ainda assim, quando o faço, recebo um erro dizendo que mejs não é encontrado, o que acredito ser o namespace ou algo para chamar diferentes funções dentro do mediaelement-and-player.
Neste caso, estou usando apenas alguns scripts, então digitei todos manualmente, estava apenas curioso se estou perdendo algo óbvio sobre a função Promise.all(), ou se há uma função do Discourse que me permite carregar vários scripts de um array.
Seu código deve funcionar bem. Você acabou de tropeçar em um bug no Discourse.
loadScript() não define o atributo async para os scripts que carrega. Portanto, ele é definido como async="true" por padrão e isso bagunça sua ordem de carregamento. É uma peculiaridade do navegador. Você tem que forçar async="false" para scripts carregados com JS.
Os plugins são menores, então carregam mais rápido que o pacote principal, mas como eles são async, eles não respeitam mais a ordem de carregamento - espere o pacote principal carregar e executar antes de executar.
Provavelmente passou despercebido porque loadScript não está aninhado em nenhum lugar no core, até onde eu sei. Normalmente, você empacotaria arquivos que precisam funcionar juntos. Então, para responder à sua outra pergunta. Não, não há funções do Discourse que lidem com esse tipo de coisa.
Seu outro trecho também deve funcionar. Para torná-lo um pouco mais fácil de ler, talvez tente encadeá-los sem aninhá-los.
// Isso vai fora do decorador
const PLUGINS = {
speed: "https://example.com/foo.js",
skipBack: "https://example.com/bar.js",
jumpForward: "https://example.com/baz.js"
};
// Então faça seu trabalho dentro do 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);
});
});