J’essaie d’utiliser la bibliothèque Mediaelement.js pour remplacer la boîte audio personnalisée, et j’y suis parvenu lorsque la page est chargée/actualisée, cependant, lorsque je navigue d’un sujet à un autre, elle ne semble pas se recharger, affichant à la place la boîte audio native de Discourse.
Je suis à peu près sûr que je fais quelque chose de mal avec le chargement de mejs mais je me suis dit que peut-être pas et que je devais faire quelque chose de spécial avec onPageChange ou autre chose.
Ceci sera un post un peu long puisque vous avez posté dans Development. Cependant, il est très général et vise davantage à expliquer les modèles à utiliser dans Discourse, indépendamment de la bibliothèque que vous souhaitez intégrer ou des éléments de publication que vous visez. Il se trouve simplement que le script que vous avez choisi sert d’exemple principal.
Ainsi, MediaElement vous offre plusieurs façons d’initialiser un nouveau lecteur audio.
Vous pouvez ajouter une classe CSS et quelques attributs à l’élément, et il s’en chargera automatiquement.
Vous l’initialisez manuellement.
Vous utilisez actuellement la méthode #1, alors examinons-la un peu. Lorsqu’un script vous propose une « initialisation automatique sur un élément », il s’agit généralement d’une amélioration de l’expérience utilisateur ajoutée par l’auteur du script. Dans le script, ils écoutent généralement l’événement de chargement du document et effectuent certaines opérations sur les éléments du DOM qui possèdent la classe ou les attributs qu’ils vous indiquent d’ajouter.
Ok, alors pourquoi cela ne fonctionne-t-il pas ? Comme vous l’avez vu, cela fonctionne parfaitement lors du chargement initial de la page, mais pas lors de la navigation ultérieure dans l’application. Que se passe-t-il ?
La réponse courte est que Discourse est une application monopage. Des éléments comme les balises <HTML> et <body> ne sont envoyés qu’une seule fois. Donc, dans un sens, le document ne se charge qu’une seule fois. Ainsi, lorsque vous naviguez après le chargement initial de la page, il n’y a plus d’événements de « chargement » natifs qui sont envoyés. Rappelez-vous, le document a déjà été chargé lors de la première vue de la page. Tout ce qui se passe après cela est géré par Discourse.
Bien sûr, cela ne signifie pas qu’il n’y a aucun événement déclenché lors de la navigation ultérieure. Cependant, ce sont des événements spécifiques à Discourse. Ainsi, les auteurs de scripts tiers n’auraient aucun moyen de les connaître à l’avance. Imaginez être un auteur de script et devoir s’adapter à des centaines de plateformes différentes ? Pas idéal, n’est-ce pas ?
Nous ne pouvons donc pas utiliser la méthode « qualité de vie » que l’auteur du script a si gentiment ajoutée. Que faire ensuite ? Eh bien, rappelez-vous que nous pouvons toujours initialiser manuellement le script sur les éléments cibles. Alors, essayons de le faire.
Plus tôt, j’ai mentionné qu’il n’y a qu’un seul événement de chargement natif (au niveau du navigateur), mais une plateforme comme Discourse ne fonctionnerait pas bien sans son propre système d’événements. Par exemple, l’API des plugins possède une méthode qui vous permet de déclencher des scripts lors de la navigation virtuelle entre les pages.
Devriez-vous utiliser cette méthode ? Non. Cette méthode est très utile pour des choses comme l’analytique, etc. Il n’y a aucun intérêt à déclencher un script qui ne gère que les balises <audio> sur chaque page, surtout si la page n’en contient aucune.
Alors, que faire ensuite ? Eh bien, la bonne nouvelle est que vous l’avez déjà compris. decorateCookedElement est la méthode correcte à utiliser ici.
Cela vous offre un moyen de… attendez… décorer les publications
Discourse garantit que tout décorateur que vous ajoutez s’appliquera à chaque publication.
Eh bien, vous chargez le script dans un décorateur de publication, donc il devrait être ajouté et fonctionner. Pourquoi ne fonctionne-t-il pas lors de la navigation ultérieure ?
Pour cela, vous devez comprendre comment loadScript() fonctionne. Votre code vérifie déjà s’il existe des éléments cibles valides avant de charger le script, donc
Cependant, imaginez une situation où vous avez 20 à 30 publications à la suite contenant toutes des éléments valides. Serait-il logique de charger le script 20 à 30 fois ? Évidemment non.
loadScript() est assez intelligent pour détecter si le script a déjà été chargé. Il ne chargera pas de doublons et ne rechargera pas non plus un script s’il est déjà terminé. Vous pouvez le voir ici.
fullUrl ci-dessus est l’URL que vous passez à loadScript() lorsque vous l’appelez, tout comme dans votre exemple.
Donc, maintenant que nous savons cela, nous pouvons comprendre pourquoi cela ne fonctionne pas lors de la navigation ultérieure.
Vous visitez le sujet-a > il contient un élément audio > loadScript() charge le script > le script effectue la chose « auto-init » sophistiquée > le script s'initialise sur vos éléments > vous obtenez des éléments audio personnalisés
ensuite...
vous visitez le sujet-b > il contient des éléments audio > loadScript() voit que le script est déjà chargé > pas de « auto-init » sophistiqué > vous obtenez les éléments audio par défaut > tristesse s'ensuit
Alors, comment résoudre ce problème ? Eh bien, c’est là que la méthode #2 mentionnée plus tôt intervient.
Vous pouvez ajouter une classe CSS et quelques attributs à l’élément, et il s’en chargera automatiquement.
Vous l’initialisez manuellement.
Faisons donc cela. Cela est déjà documenté sur la page que vous avez partagée. Nous devons appeler cela sur notre élément cible comme suit :
// Vous pouvez utiliser soit une chaîne pour l'ID du lecteur (c'est-à-dire `player`),
// soit `document.querySelector()` pour n'importe quel sélecteur
var player = new MediaElementPlayer("player", {
// ... options
});
Votre code gère déjà chaque élément audio individuellement :+1, nous devons donc simplement modifier ceci :
Mettions cela de côté pour l’instant et regardons le reste du décorateur. Voici ce que nous avons jusqu’à présent :
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 vous remarquez, vous appelez loadScript() sur deux scripts différents. Je ne sais pas si c’est intentionnel, mais vous n’en avez besoin que d’un seul. Considérez-le comme un bundle complet et un bundle léger. Vous voulez le lecteur audio personnalisé. Donc, vous avez besoin du bundle complet. Supprimons l’autre.
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 }
);
Vous vérifiez s’il y a des lecteurs audio dans la publication et chargez conditionnellement le script en fonction de cela. Cela peut être simplifié comme suit. D’abord, vérifiez directement la longueur.
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 }
);
ensuite, déplacez cela vers le haut et faites simplement un return si la longueur est fausse (length < 0). J’ai également supprimé les commentaires dans le code.
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 }
);
Puisque le src du script ne changera jamais, déplaçons-le dans une const. loadScript() est aussi toujours le même. Faisons-en une const également.
++ 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 }
);
et mettons celui-ci de côté également. Avant de continuer, nous devons parler un peu plus du fonctionnement de loadScript().
Si vous voulez utiliser une partie d’un script, vous voulez vous assurer qu’il est chargé avant de faire quoi que ce soit, n’est-ce pas ? Eh bien, loadScript() s’en charge pour vous. Il retourne une Promise. Les Promesses peuvent sembler effrayantes au début, mais elles sont vraiment simples. Une Promise est littéralement cela… une promesse.
Vous voulez faire un travail… vous promettez au navigateur que vous le préviendrez lorsque le travail sera terminé… le navigateur vous attend. C’est aussi simple que cela. Le reste n’est que la compréhension de la syntaxe.
Je ne passerai pas beaucoup de temps là-dessus car c’est un peu hors sujet pour ce sujet.
Continuons. loadScript() est basé sur les Promesses. Discourse promet au navigateur de l’informer lorsque le script est complètement chargé, que le script n’existe pas et doit être chargé, ou simplement pour vérifier s’il a déjà été chargé.
Donc, si nous faisons quelque chose comme ceci :
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(() => {
++ // ceci ne se déclenchera QUE si le script a été/est chargé
++ console.log("mon script a chargé");
++ });
// forEach used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
Maintenant, nous pouvons revenir à notre boucle forEach précédente et l’ajouter directement là, et nous serions sûrs que le script sera disponible.
et ensuite un peu de CSS pour charger le CSS du script et éviter les soubresauts pendant que le script remplace les éléments.
common > css
@import "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelementplayer.min.css";
.cooked {
--audio-player-height: 40px;
.mejs__container,
audio {
// faire correspondre la hauteur du lecteur Media-Element.js pour éviter les soubresauts
height: var(--audio-player-height);
display: block;
}
}
Vous devriez maintenant voir le lecteur personnalisé sur chaque publication contenant des éléments valides.
Ceci étant dit, vous devriez noter que la bibliothèque que vous avez choisie est assez ancienne. Elle est transpilée pour des navigateurs archaïques et tente de polyfiller de nombreuses fonctionnalités qui sont depuis devenues standards.
Si vous savez pourquoi vous voulez l’utiliser, c’est très bien. Cependant, si vous l’utilisez simplement pour personnaliser l’apparence du lecteur, je vous recommande de l’éviter. Je n’ai pas vérifié, mais il existe probablement des alternatives modernes beaucoup plus légères.
La meilleure chose dans tout cela est que l’implémentation ne change pas par rapport à ce qui précède. Peu importe les éléments que vous souhaitez cibler et les scripts que vous souhaitez utiliser. Le même modèle s’applique. La seule chose qui change est l’initialisation du script personnalisé. Chaque bibliothèque décente dispose d’une documentation assez bonne qui vous guidera à travers cela. Ensuite, vous l’intégrez simplement dans le modèle ci-dessus.
C’est dans des moments comme ceux-ci que j’aimerais que Meta utilise Discourse Reactions, car un cœur ne suffit pas pour l’amour et la gratitude que je ressens en ce moment.
J’espérais que quelqu’un me donnerait au moins un conseil et pourtant vous avez pris le temps d’écrire une explication très approfondie de la façon dont je suis arrivé là où j’en étais et m’avez guidé sur la façon de le faire fonctionner, m’apprenant beaucoup en cours de route (même avec les petits détails sur le jitter !)
J’apprends comment la structure de Discourse peut aider à encourager un tel comportement, en signifiant que si je réponds bien une fois, d’autres peuvent le voir et je n’aurai pas à y répondre à nouveau — ce qui m’encourage à continuer à construire des communautés avec cela ; et pourtant, je ne pense pas que cela explique entièrement pourquoi vous m’avez écrit cela et votre volonté de le faire peut m’encourager à utiliser cette plateforme encore plus.
Merci.
Concernant Mediaelement, oui, c’est vieux, mais il s’associe bien au site Wordpress que j’ai et je l’ai beaucoup personnalisé là-bas, en essayant de fournir un aspect familier à l’utilisateur (et aussi en n’essayant pas d’apprendre une autre bibliothèque pour le moment )
Une question de suivi et peut-être une question stupide : j’essaie de charger plusieurs scripts maintenant, car Mediaelement autorise les scripts de plugin. Je veux m’assurer que tous les scripts se chargent avant de retourner la promesse.
J’ai essayé de le faire en parcourant les constantes des sources de scripts, puis en créant un tableau de promesses, puis en utilisant Promise.all() pour initialiser les lecteurs, et pourtant, lorsque je le fais, je reçois une erreur indiquant que mejs n’est pas trouvé, ce qui, je crois, est l’espace de noms ou quelque chose pour appeler différentes fonctions dans mediaelement-and-player.
Dans ce cas, je n’utilise que quelques scripts, donc je les ai tous tapés manuellement, j’étais juste curieux de savoir si je manquais quelque chose d’évident à propos de la fonction Promise.all(), ou s’il existe une fonction Discourse qui me permet de charger plusieurs scripts à partir d’un tableau.
Votre code devrait fonctionner correctement. Vous êtes juste tombé sur un bug dans Discourse.
loadScript() ne définit pas l’attribut async pour les scripts qu’il charge. Il utilise donc par défaut async="true", ce qui perturbe votre ordre de chargement. C’est une bizarrerie du navigateur. Vous devez forcer async="false" pour les scripts chargés avec JS.
Les plugins sont plus petits, ils se chargent donc plus rapidement que le paquet principal, mais le fait qu’ils soient async signifie qu’ils ne respectent plus l’ordre de chargement - attendez que le paquet principal soit chargé et exécuté avant de s’exécuter.
Cela est probablement passé inaperçu car loadScript n’est imbriqué nulle part dans le cœur, pour autant que je sache. Vous regrouperiez normalement les fichiers qui doivent fonctionner ensemble. Donc, pour répondre à votre autre question. Non, il n’y a pas de fonctions Discourse qui gèrent ce genre de choses.
Votre autre extrait devrait également fonctionner. Pour le rendre un peu plus facile à lire, essayez peut-être de les enchaîner sans les imbriquer.
// Ceci va en dehors du décorateur
const PLUGINS = {
speed: "https://example.com/foo.js",
skipBack: "https://example.com/bar.js",
jumpForward: "https://example.com/baz.js"
};
// Ensuite, faites votre travail à l'intérieur du décorateur
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);
});
});