JavaScript sur WordPress c'est l'enfer… et voici pourquoi

Publié: 2022-05-05

La pile de développement WordPress a beaucoup changé ces dernières années. Depuis l'arrivée de Gutenberg, le rôle de JavaScript dans notre CMS préféré est plus important que jamais. Dans ce blog, nous avons déjà longuement parlé des avantages que cela implique pour les développeurs (pour ne citer que quelques exemples, nous avons parlé d'extensions pour Gutenberg, de conseils sur TypeScript et React, d'outils de développement, d'exemples de plugins, etc.) mais chaque l'histoire a son côté sombre … et c'est ce dont nous allons parler ici aujourd'hui.

Dans l'article d'aujourd'hui, je vais partager avec vous les 3 principaux problèmes auxquels vous, développeur de plugin WordPress, pourriez être confronté à l'avenir. Et le plus drôle, c'est que chacun d'eux a un coupable différent : soit WordPress lui-même, soit d'autres développeurs, soit vous-même. Alors allons-y : les maux de tête JavaScript les plus courants que vous pourriez rencontrer et ce que vous pouvez faire pour les éviter/les résoudre.

#1 Plugins WPO qui vont casser votre plugin et votre site

Commençons par le problème qui a été la source de nombreux tickets ici chez Nelio : les plugins WPO.

Je suis sûr que vous avez lu de nombreux articles et blogs qui soulignent l'importance d'avoir un site Web léger qui se charge rapidement. Je veux dire, nous avons aussi écrit à plusieurs reprises à ce sujet ! Parmi les conseils qu'ils donnent généralement, vous trouverez des choses comme passer à un meilleur fournisseur d'hébergement, utiliser des plugins de cache et des CDN, garder votre serveur et WordPress à jour, ou (et voici le premier problème) installer un plugin WPO. Voici quelques exemples de ces derniers :

  • W3 Total Cache avec plus d'un million d'installations
  • SiteGround Optimizer avec plus d'un million d'installations également (dont l'une, soit dit en passant, que nous utilisons sur notre site Web)
  • WordPress WPO Tweaks & Optimizations par notre bon ami Fernando Tellado

Ces plugins promettent d'accélérer votre site Web grâce à une série d'optimisations judicieuses dont, en général, "n'importe quel site WordPress peut bénéficier". Ces optimisations incluent :

  • Retirer de la file d'attente les scripts inutiles sur le frontend, tels que les emojis ou les Dashicons
  • Mise en cache des pages et des requêtes de base de données
  • Réduire la quantité d'informations incluses dans l'en-tête
  • Combiner et réduire les scripts JavaScript et les styles CSS
  • Minification du HTML
  • Suppression de l'argument de requête de version des URL de vos ressources statiques
  • Différer les scripts JavaScript et/ou les charger de manière asynchrone
  • etc

Comme je le disais, ces types d'optimisations pourraient être, en général, bénéfiques. Mais d'après notre expérience, toutes les optimisations JS sur un site WordPress ont tendance à entraîner plus de problèmes , rendant ainsi leurs améliorations supposées inutiles. De vrais exemples que j'ai vus au fil des ans sont:

  • Combinaison de scripts. Moins votre navigateur doit demander de scripts, mieux c'est. C'est pourquoi il est logique de combiner tous les scripts en un seul. Cependant, cela peut être problématique. En général, si un script JavaScript plante, son exécution se termine et l'erreur est signalée dans la console de votre navigateur. Mais seule l'exécution de ce script est arrêtée ; vos autres scripts s'exécuteront normalement. Mais si vous les combinez tous… eh bien, dès qu'un script échoue, les autres scripts (y compris peut-être le vôtre) ne fonctionneront pas, et vos utilisateurs penseront que c'est votre plugin qui ne fonctionne pas comme prévu.
  • Minification des scripts. J'ai vu des processus de minification qui, croyez-le ou non, cassaient des expressions régulières et entraînaient des scripts JS contenant des erreurs de syntaxe. Bien sûr, cela fait un moment depuis la dernière fois que j'ai rencontré celui-ci mais… :-/
  • Arguments de requête. Lorsque vous mettez un script en file d'attente dans WordPress, vous pouvez le faire avec son numéro de version (qui, soit dit en passant, a probablement été généré automatiquement par @wordpress/scripts ). Les numéros de version sont extrêmement utiles : si vous mettez à jour votre plugin et que votre script est modifié, ce nouveau numéro de version garantira que tous les visiteurs verront une URL différente et, par conséquent, leurs navigateurs demanderont la nouvelle version. Malheureusement, si un plugin WPO supprime la chaîne de requête, vos visiteurs peuvent ne pas se rendre compte que le script a changé et ils utiliseront une copie en cache dudit script… ce qui peut entraîner ou non des conséquences inattendues. Mince!

Un désastre complet, n'est-ce pas ? Mais attendez d'entendre le suivant :

Différer les scripts

Chez Nelio, nous avons mis en place un plugin de test A/B avec lequel suivre vos visiteurs et découvrir quel design et quel contenu obtient le plus de conversions. Comme vous pouvez l'imaginer, notre script de suivi ressemble à ceci :

 window.NelioABTesting = window.NelioABTesting || {}; window.NelioABTesting.init = ( settings ) => { // Add event listeners to track visitor events... console.log( settings ); };

C'est-à-dire qu'il expose une fonction init que nous devons appeler pour informer le script des tests en cours d'exécution. Pour appeler cette méthode, nous mettons en file d'attente un script en ligne dans PHP comme suit :

 function nab_enqueue_tracking_script() { wp_enqueue_script( 'nab-tracking', ... ); wp_add_inline_script( 'nab-tracking', sprintf( 'NelioABTesting.init( %s );', wp_json_encode( nab_get_tracking_settings() ) ) ); } add_action( 'wp_enqueue_scripts', 'nab_enqueue_tracking_script' );

qui se traduit par les balises HTML suivantes :

 <head> ... <script type="text/javascript" src="https://.../dist/tracking.js" ></script> <script type="text/javascript" > NelioABTesting.init( {"experiments":[...],...} ); </script> ... </head> <body> ...

Mais que se passe-t-il lorsque votre plugin WPO ajoute l'attribut defer à notre script ?

 <head> ... <script defer <!-- This delays its loading... --> type="text/javascript" src="https://.../dist/tracking.js" ></script> <script type="text/javascript" > NelioABTesting.init( {"experiments":[...],...} ); </script> ... </head> <body> ...

Eh bien, le script est maintenant différé… ce qui signifie que l'extrait précédent est équivalent à celui-ci :

 <head> ... <script type="text/javascript" > NelioABTesting.init( {"experiments":[...],...} ); </script> ... </head> <body> ... <script type="text/javascript" src="https://.../dist/tracking.js" ></script> </body> </html>

et, par conséquent, nab-tracking-js n'est plus chargé quand il est censé le faire et, par conséquent, le script en ligne qui vient après et s'appuie sur lui échouera tout simplement : nab-tracking-js-after utilise NelioABTesting.init qui , grâce à la directive defer , n'est pas encore disponible. Affreux!

Solution

La solution la plus efficace est claire : dites à vos utilisateurs de désactiver l'optimisation des scripts et arrêtez-vous. Après tout, la gestion des dépendances en JavaScript est, en général, extrêmement compliquée (surtout si nous utilisons les directives defer et async ), et WordPress ne fait pas exception. Jetez un œil à cette discussion de 12 ans sur le sujet !

Mais si ce n'est pas faisable (et je sais que ce n'est pas le cas), je vous recommande de faire la même chose que nous : débarrassez-vous de la méthode init et inversez les responsabilités de vos scripts réguliers et en ligne. Autrement dit, ajoutez un script en ligne avant le script normal et utilisez-le pour définir une variable globale avec les paramètres requis :

 function nab_enqueue_tracking_script() { wp_enqueue_script( 'nab-tracking', ... ); wp_add_inline_script( 'nab-tracking', sprintf( 'NelioABTestingSettings = %s;', wp_json_encode( nab_get_tracking_settings() ) ), 'before' ); } add_action( 'wp_enqueue_scripts', 'nab_enqueue_tracking_script' );

pour que le HTML résultant ressemble à ceci :

 <head> ... <script type="text/javascript" > NelioABTestingSettings = {"experiments":[...],...}; </script> <script type="text/javascript" src="https://.../dist/tracking.js" ></script> ... </head> <body> ...

et donc peu importe si l'exécution du script externe est retardée ou non - il apparaîtra toujours après le script en ligne, satisfaisant ainsi la dépendance entre eux.

Enfin, si vous voulez vous assurer que personne ne va modifier vos paramètres, déclarez la variable comme const et figez sa valeur avec Object.freeze :

 ... sprintf( 'const NelioABTestingSettings = Object.freeze( %s );', wp_json_encode( nab_get_tracking_settings() ) ), ...

qui est pris en charge par tous les navigateurs modernes.

#2 Dépendances dans WordPress qui peuvent ou non fonctionner…

La gestion des dépendances peut également être problématique dans WordPress, en particulier lorsqu'il s'agit des scripts intégrés de WordPress. Laisse-moi expliquer.

Imaginez, par exemple, que nous créons une petite extension pour Gutenberg, comme nous l'avons expliqué ici. Le code source de notre plugin contiendra probablement des instructions d' import comme celles-ci :

 import { RichTextToolbarButton } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { registerFormatType } from '@wordpress/rich-text'; // ...

Lorsque ce code source JS est transpilé, Webpack (ou l'outil que vous utilisez) regroupera toutes les dépendances et votre propre code source dans un seul fichier JS. C'est le fichier que vous mettrez plus tard dans la file d'attente de WordPress pour que tout fonctionne comme prévu.

Si vous avez utilisé @wordpress/scripts pour créer un tel fichier, certaines des dépendances ne seront pas incluses dans le fichier de sortie, car le processus intégré suppose que les packages seront disponibles dans la portée globale. Cela signifie que les importations précédentes seront transpilées en quelque chose de similaire à ceci :

 const { RichTextToolbarButton } = window.wp.blockEditor; const { __ } = window.wp.i18n; const { registerFormatType } = window.wp.richText; // ...

Afin de vous assurer de ne manquer aucune des dépendances de votre script, @wordpress/scripts va non seulement transpiler votre code JS, mais il va aussi générer un fichier PHP avec ses dépendances WordPress :

 <?php return array( 'dependencies' => array('wp-block-editor','wp-i18n','wp-rich-text'), 'version' => 'a12850ccaf6588b1e10968124fa4aba3', );

Plutôt chouette, hein ? Alors, quel est le problème ? Eh bien, ces packages WordPress sont en développement continu et changent assez souvent, ajoutant de nouvelles fonctionnalités et améliorations. Par conséquent, si vous développez votre plugin en utilisant la dernière version de WordPress, vous pourriez vous retrouver par inadvertance à utiliser des fonctions ou des fonctionnalités qui sont disponibles dans cette dernière version (et donc tout fonctionne comme il se doit) mais pas dans les « anciennes » versions de WordPress…

Comment pouvez-vous dire?

Solution

Mon conseil ici est très simple : développez vos plugins en utilisant la dernière version de WordPress, mais testez vos releases sur des versions plus anciennes. En particulier, je vous suggère de tester votre plugin avec, au moins, la version minimale de WordPress que votre plugin est censé prendre en charge. Une version minimale que vous trouverez dans le readme.txt de votre plugin :

 === Nelio Content === ... Requires PHP: 7.0 Requires at least: 5.4 Tested up to: 5.9 ...

Passer d'une version de WordPress à une autre est aussi simple que d'exécuter la commande WP CLI suivante :

 wp core update --version=5.4 --force

#3 Les fonctions fléchées sont plus délicates que vous ne le pensez

Enfin, permettez-moi de partager l'un des derniers problèmes que j'ai rencontrés il y a quelques jours à peine et qui m'a rendu fou. En un mot, nous avions un fichier JavaScript similaire à celui-ci :

 import domReady from '@wordpress/dom-ready'; domReady( () => [ ...document.querySelectorAll( '.nelio-forms-form' ) ] .forEach( initForm ) ); // Helpers // ------- const initForm = ( form ) => { ... } // ...

qui initialise vos Nelio Forms sur le front-end. Le script est assez simple, n'est-ce pas ? Il définit une fonction anonyme appelée lorsque le DOM est prêt. Cette fonction utilise une fonction d'assistance (flèche) appelée initForm . Eh bien, il s'avère qu'un exemple aussi simple peut planter ! Mais seulement dans certaines circonstances spécifiques (c'est-à-dire si le script a été "optimisé" par un plugin WPO utilisant l'attribut defer ).

Voici comment JS exécute le script précédent :

  1. La fonction anonyme à l'intérieur domReady est définie
  2. domReady s'exécute
  3. Si le DOM n'est pas encore prêt (et ce n'est généralement pas le cas lorsqu'un script est chargé), domReady n'exécute pas la fonction de rappel. Au lieu de cela, il en garde simplement une trace afin qu'il puisse l'appeler plus tard
  4. JavaScript continue d'analyser le fichier et charge la fonction initForm
  5. Une fois que le DOM est prêt, la fonction de rappel est finalement appelée

Maintenant, que se passe-t-il si, au moment où nous atteignons la troisième étape, le DOM est prêt et, par conséquent, domReady appelle directement la fonction anonyme ? Eh bien, dans ce cas, le script déclenchera une erreur indéfinie, car initForm est toujours undefined .

En fait, le plus curieux dans tout cela est que ces deux solutions étant équivalentes :

 domReady( aux ); const aux = () => {};
 domReady( () => aux() ); const aux = () => {}

le linter JavaScript ne lancera une erreur que sur le premier, mais pas sur le dernier.

Solution

Deux solutions sont possibles : soit vous définissez la fonction d'assistance à l'aide du mot-clé function et oubliez la fonction fléchée, soit vous déplacez l'instruction domReady à la fin, après que toutes les fonctions d'assistance ont été définies :

 domReady( aux ); function aux() { // ... }
 const aux = () => { // ... }; domReady( aux );

Si vous vous demandez pourquoi la première solution fonctionne si elle est apparemment équivalente à celle d'origine que nous avions, tout dépend du fonctionnement du levage JavaScript. En bref, en JavaScript, vous pouvez utiliser une fonction (définie avec function ) avant sa définition, mais vous ne pouvez pas faire de même avec des variables et des constantes (et donc des fonctions fléchées).

En résumé

Il y a un grand nombre de choses qui peuvent mal tourner en JavaScript. Heureusement, ils ont tous une solution, surtout si nous faisons attention à ce que nous faisons. J'espère que vous avez appris quelque chose de nouveau aujourd'hui et j'espère que grâce aux erreurs et erreurs que j'ai commises dans le passé, vous pourrez éviter de les subir dans votre propre chair à l'avenir.

Image sélectionnée par Ian Stauffer sur Unsplash.