JavaScript en WordPress es un infierno... y he aquí por qué

Publicado: 2022-05-05

La pila de desarrollo de WordPress ha cambiado mucho en los últimos años. Desde la llegada de Gutenberg, el papel de JavaScript en nuestro CMS favorito es más importante que nunca. En este blog ya hemos hablado largo y tendido de las ventajas que esto supone para los desarrolladores (por citar algunos ejemplos, hemos hablado de extensiones para Gutenberg, consejos sobre TypeScript y React, herramientas de desarrollo, ejemplos de plugins, etc.) pero cada La historia tiene su lado oscuro … y de eso hablaremos aquí hoy.

En la publicación de hoy, voy a compartir con usted los 3 problemas principales que usted, un desarrollador de complementos de WordPress, podría enfrentar en el futuro. Y lo divertido es que cada uno de ellos tiene un culpable diferente: ya sea el propio WordPress, otros desarrolladores o usted mismo. Así que aquí vamos: los dolores de cabeza de JavaScript más comunes que puede encontrar y lo que puede hacer para evitarlos/solucionarlos.

Complementos n.º 1 de WPO que romperán su complemento y su sitio

Comencemos con el problema que ha sido fuente de muchos tickets aquí en Nelio: los complementos de WPO.

Estoy seguro de que ha leído muchos artículos y publicaciones de blog que describen la importancia de tener un sitio web liviano que cargue rápido. Quiero decir, ¡también hemos escrito en varias ocasiones al respecto! Entre los consejos que suelen dar, encontrarás cosas como cambiar a un mejor proveedor de alojamiento, usar complementos de caché y CDN, mantener actualizado tu servidor y WordPress o (y aquí viene el primer problema) instalar un complemento WPO. Algunos ejemplos de esto último incluyen:

  • W3 Total Cache con más de 1 millón de instalaciones
  • SiteGround Optimizer con más de 1 millón de instalaciones también (una de las cuales, por cierto, usamos en nuestro sitio web)
  • WordPress WPO Tweaks & Optimizations por nuestro buen amigo Fernando Tellado

Estos complementos prometen acelerar su sitio web a través de una serie de optimizaciones sensibles de las que, en general, “cualquier sitio de WordPress puede beneficiarse”. Estas optimizaciones incluyen:

  • Eliminación de secuencias de comandos innecesarias en la interfaz, como emojis o Dashicons
  • Almacenamiento en caché de páginas y consultas de bases de datos
  • Reducir la cantidad de información incluida en el encabezado
  • Combinar y minimizar scripts de JavaScript y estilos CSS
  • Minimizar HTML
  • Eliminar el argumento de consulta de versión de las URL de sus activos estáticos
  • Aplazar scripts de JavaScript y/o cargarlos de forma asíncrona
  • etc.

Como decía, este tipo de optimizaciones pueden ser, en general, beneficiosas. Pero en nuestra experiencia, todas las optimizaciones de JS en un sitio web de WordPress tienden a generar más problemas , lo que hace que sus supuestas mejoras sean inútiles. Ejemplos reales que he visto a lo largo de los años son:

  • Combinación de guiones. Cuantos menos scripts tenga que solicitar su navegador, mejor. Por eso tiene sentido combinar todos los scripts en uno. Sin embargo, esto puede ser problemático. En general, si un script de JavaScript falla, su ejecución finaliza y el error se informa en la consola de su navegador. Pero solo se detiene la ejecución de ese script; sus otros scripts se ejecutarán normalmente. Pero si los combina todos... bueno, tan pronto como un script falla, los otros scripts (incluido quizás el suyo) no se ejecutarán, y sus usuarios pensarán que es su complemento el que no funciona como se esperaba.
  • Minimizar guiones. He visto algunos procesos de minificación que, créanlo o no, rompieron expresiones regulares y dieron como resultado scripts JS que tenían errores de sintaxis. Claro, ha pasado un tiempo desde la última vez que me encontré con este, pero... :-/
  • Argumentos de consulta. Cuando pone en cola un script en WordPress, puede hacerlo con su número de versión (que, por cierto, probablemente fue generado automáticamente por @wordpress/scripts ). Los números de versión son extremadamente útiles: si actualiza su complemento y su secuencia de comandos cambió, este nuevo número de versión garantizará que todos los visitantes vean una URL diferente y, por lo tanto, sus navegadores solicitarán la nueva versión. Desafortunadamente, si un complemento de WPO elimina la cadena de consulta, es posible que sus visitantes no se den cuenta de que la secuencia de comandos ha cambiado y usarán una copia en caché de dicha secuencia de comandos... lo que puede o no tener consecuencias no deseadas. ¡Maldita sea!

Un completo desastre, ¿no? Pero espera hasta que escuches el siguiente:

Aplazamiento de guiones

En Nelio hemos implementado un plugin de Test A/B con el que hacer un seguimiento de tus visitantes y descubrir qué diseño y contenido consigue más conversiones. Como puede imaginar, nuestro script de seguimiento se parece a esto:

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

Es decir, expone una función de init que tenemos que llamar para que el script sepa sobre las pruebas que se están ejecutando actualmente. Para llamar a este método, ponemos en cola un script en línea en PHP de la siguiente manera:

 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' );

lo que da como resultado las siguientes etiquetas HTML:

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

Pero, ¿qué sucede cuando su complemento WPO agrega el atributo defer a nuestro 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> ...

Bueno, el script ahora está aplazado... lo que significa que el fragmento anterior es equivalente a este:

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

y, como resultado, nab-tracking-js ya no se carga cuando se supone que debe hacerlo y, por lo tanto, el script en línea que viene después y depende de él simplemente fallará: nab-tracking-js-after usa NelioABTesting.init que , gracias a la directiva defer , aún no está disponible. ¡Horrible!

Solución

La solución más efectiva es clara: dígales a sus usuarios que deshabiliten la optimización de secuencias de comandos y den por terminado. Después de todo, la gestión de dependencias en JavaScript es, en general, extremadamente complicada (especialmente si usamos directivas defer y async ), y WordPress no es una excepción. ¡Solo eche un vistazo a esta discusión de 12 años sobre el tema!

Pero si eso no es factible (y sé que no lo es), le recomiendo que haga lo mismo que hicimos nosotros: deshacerse del método init e invertir las responsabilidades de sus scripts normales y en línea. Es decir, agregue un script en línea antes del script normal y utilícelo para definir una variable global con la configuración requerida:

 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' );

para que el HTML resultante se vea así:

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

por lo tanto, no importa si la ejecución del script externo se retrasa o no, siempre aparecerá después del script en línea, satisfaciendo así la dependencia entre ellos.

Finalmente, si quiere asegurarse de que nadie vaya a modificar su configuración, declare la variable como const y congele su valor con Object.freeze :

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

que es compatible con todos los navegadores modernos.

#2 Dependencias en WordPress que pueden o no funcionar…

La gestión de dependencias también puede ser problemática en WordPress, especialmente cuando se habla de los scripts integrados de WordPress. Dejame explicar.

Imagina, por ejemplo, que estamos creando una pequeña extensión para Gutenberg, como explicamos aquí. El código fuente de nuestro complemento probablemente tendrá algunas declaraciones de import como estas:

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

Cuando se transpila este código fuente JS, Webpack (o la herramienta que usa) empaquetará todas las dependencias y su propio código fuente en un solo archivo JS. Este es el archivo que luego pondrás en cola desde WordPress para que todo funcione como esperas.

Si usó @wordpress/scripts para crear dicho archivo, algunas de las dependencias no se incluirán en el archivo de salida, porque el proceso incorporado asume que los paquetes estarán disponibles en el ámbito global. Esto significa que las importaciones anteriores se transpilarán en algo similar a esto:

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

Para asegurarse de que no se pierda ninguna de las dependencias de su script, @wordpress/scripts no solo transpilará su código JS, sino que también generará un archivo PHP con sus dependencias de WordPress:

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

Bastante ordenado, ¿eh? Entonces, ¿cuál es el problema? Bueno, estos paquetes de WordPress están en continuo desarrollo y cambian con bastante frecuencia, agregando nuevas funciones y mejoras. Por lo tanto, si desarrollas tu complemento usando la última versión de WordPress, podrías terminar usando sin darte cuenta funciones o características que están disponibles en esa última versión (y por lo tanto todo funciona como debería) pero no en versiones “antiguas” de WordPress…

¿Cómo puedes saberlo?

Solución

Mi consejo aquí es muy simple: desarrolle sus complementos utilizando la última versión de WordPress, pero pruebe sus lanzamientos en versiones anteriores. En particular, le sugiero que pruebe su complemento con, al menos, la versión mínima de WordPress que se supone que admite su complemento. Una versión mínima que encontrará en el archivo readme.txt de su complemento:

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

Cambiar de una versión de WordPress a otra es tan fácil como ejecutar el siguiente comando WP CLI:

 wp core update --version=5.4 --force

#3 Las funciones de flecha son más complicadas de lo que crees

Finalmente, permítanme compartir uno de los últimos problemas que encontré hace solo unos días y que me volvió loco. En pocas palabras, teníamos un archivo JavaScript similar a este:

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

que inicializa tus Nelio Forms en el front-end. El guión es bastante sencillo, ¿no? Define una función anónima que se llama cuando el DOM está listo. Esta función utiliza una función auxiliar (flecha) llamada initForm . Bueno, resulta que un ejemplo tan simple puede colapsar. Pero solo en algunas circunstancias específicas (es decir, si el script fue "optimizado" por un complemento de WPO usando el atributo defer ).

Así es como JS ejecuta el script anterior:

  1. La función anónima dentro domReady está definida
  2. domReady se ejecuta
  3. Si el DOM aún no está listo (y generalmente no lo está cuando se carga un script), domReady no ejecuta la función de devolución de llamada. En cambio, simplemente realiza un seguimiento de él para que pueda llamarlo más tarde.
  4. JavaScript continúa analizando el archivo y carga la función initForm
  5. Una vez que el DOM está listo, finalmente se llama a la función de devolución de llamada

Ahora, ¿qué pasa si, para cuando lleguemos al tercer paso, el DOM está listo y, por lo tanto, domReady llama directamente a la función anónima? Bueno, en ese caso, el script generará un error indefinido, porque initForm aún no está undefined .

De hecho, lo más curioso de todo esto es que estas dos soluciones siendo equivalentes:

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

el linter de JavaScript solo arrojará un error en el primero, pero no en el último.

Solución

Hay dos soluciones posibles: o define la función de ayuda usando la palabra clave function y se olvida de la función de flecha, o mueve la instrucción domReady al final, después de que se hayan definido todas las funciones de ayuda:

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

Si se pregunta por qué funciona la primera solución si aparentemente es equivalente a la original que teníamos, se trata de cómo funciona la elevación de JavaScript. En resumen, en JavaScript puedes usar una función (definida con function ) antes de su definición, pero no puedes hacer lo mismo con variables y constantes (y por lo tanto funciones de flecha).

En resumen

Hay una gran cantidad de cosas que pueden salir mal en JavaScript. Por suerte, todos tienen solución, sobre todo si prestamos atención a lo que hacemos. Espero que hoy hayas aprendido algo nuevo y confío en que gracias a los errores y equivocaciones que he cometido en el pasado, podrás evitar sufrirlos en carne propia en el futuro.

Imagen destacada de Ian Stauffer en Unsplash.