5139 sujets

Le Bar du forum

Bonsoir à tous,

Bien sûr, beaucoup d'entre vous sont des dev's chevronnés mais, tout à la joie de ma trouvaille de la soirée, je vous partage une petite astuce axée sur le développement front : il s'agit de coordonner en front des scripts chargés en asynchrone et, qui plus est, appelés eux-même à un moment ultérieur du chargement initial de la page via un script en front.

On dit communément pour un script front qui dépend d'un autre qu'il se doit d'être chargé de manière synchrone, au pire on utilise l'attribut "defer" et c'est OK :
<script src="/libraries/leaflet/leaflet.js" defer></script>
<script src="/scripts/map.js" defer></script>

Oui, mais si ces scripts ne sont pas injectés côté serveur mais côté client ? Par exemple on injecterait une librairie pour une carte, ainsi que son fichier de configuration, seulement si présence d'une div avec la classe ".map" dans la page :
/**
 * @warning Les scripts chargés par ce biais doivent éviter des modifications trop importantes du DOM ou de repeindre la page (reflow and repaint).
 * @param {string} url : une url de script
 * @param {string} hook : le placement du script, 'head' ou 'footer', footer par défaut.
 */
const getScript = (url, hook = 'footer') =>
  new Promise((resolve, reject) => {
    // @see  https://stackoverflow.com/questions/16839698#61903296
 
    const script = document.createElement('script')
    script.src = url
    script.async = 1 // c'est ici que l'on pourrait faussement imaginer pouvoir utiliser "defer" de manière efficiente, mais nop.
    script.onerror = reject
    script.onload = script.onreadystatechange = function () {
      const loadState = this.readyState
      if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
      script.onload = script.onreadystatechange = null
      resolve()
    }
    if (hook === 'footer') document.body.appendChild(script)
    else if (hook === 'head') document.head.appendChild(script)
    else console.error("Error: le choix de l'élement html pour getScript() n'est pas correct.")
  })

const getScriptRequests = (() => {
  if (document.querySelector('[class*=language-]')) getScript('/libraries/prism/prism.js') // exemple pour un autre script
  if (document.querySelector('.map')) getScript('/libraries/leaflet/leaflet.js') // la fameuse librairie
  if (document.querySelector('.map')) getScript('/scripts/map.js') // son fichier de configuration
})()

Dans ce cas là le "defer" il ne sert plus à rien. Là, on a une chance sur deux que le script de configuration soit chargé avant la librairie dont il dépend et même, vu le poids d'une librairie par rapport à un fichier de config', bien plus qu'une chance sur deux ! Alors comment on fait ?

On fait comme ceci :
/**
 * Test la présence de la bibliothèque Leaflet.js chargée en asynchrone et, si oui, exécution du script de configuration.
 * @param {object} L est une variable globale produite par Leaflet, elle nous permettra de tester l'initialisation de la bibliothèque.
 */
window.addEventListener('load', () => {
  // @note Pour cette première stratégie on part du postulat que la librairie a été chargée en même temps que le reste du DOM, donc via un lien produit côté serveur.
  if (typeof L !== 'undefined') maps() // @note typeof permet de tester la variable sans que celle-ci produise une erreur si elle n'est pas encore définie.

  document.addEventListener('readystatechange', () => {
    // @note Écoute les changement d'un document, typiquement ici l'écouteur réagira à l'injection en asynchrone du script de la bibliothèque Leaflet dans le DOM.
    if (document.readyState === 'complete' && typeof L !== 'undefined') maps()
  })
})

Donc, c'est chargé en asynchrone, à n'importe quel moment du cycle de vie de la page web, mais les deux scripts seront synchronisés quand-même.

Exemple en ligne : Maps

Voilà voilà. Bonne semaine à tous.
Modifié par Olivier C (30 Jan 2024 - 02:15)