Brown Rock Formation, de Ian Stauffer

El stack de desarrollo en WordPress ha cambiado muchísimo en los últimos años. Desde la llegada de Gutenberg, el peso que tiene JavaScript en nuestro CMS favorito es mayor que nunca. En este blog ya hemos hablado largo y tendido de las ventajas que ello supone para los desarrolladores (viendo extensiones para Gutenberg, consejos sobre TypeScript y React, herramientas de desarrollo, ejemplos de plugins, etc) pero toda historia tiene su lado oscuro… y de eso hablaremos aquí hoy.

En la entrada de hoy, voy a comentarte los 3 problemas principales con los que, si has desarrollado un plugin para WordPress, vas a encontrarte sí o sí en algún momento. ¿Y sabes qué es lo mejor de todo? Que cada uno de los problemas que veremos es culpa de alguien diferente: o bien del propio WordPress, o bien de los demás desarrolladores, o bien de ti mismo. Aquí van, pues, algunos de los quebraderos de cabeza más habituales en JavaScript y qué puedes hacer para evitarlos o solucionarlos.

#1 Plugins de optimización que rompen tu plugin (y tu web)

Empecemos por el problema que ha generado más tickets en Nelio y que, por lo tanto, más frustrado me tiene: los «plugins de optimización». Estoy seguro de que te habrás hartado a leer artículos y blogs en donde se habla de la importancia de tener una web que cargue rápido y que sea ligera; ¡si es que hasta nosotros hemos escrito en diversas ocasiones sobre ello!

Entre los consejos que se dan para conseguir esa ansiada velocidad están cosas como alojar la web en un mejor proveedor de hosting; usar plugins de cache y CDNs; mantener tu servidor y WordPress actualizados; o (y aquí viene el primer problema) instalar alguno de los numerosos «plugins de optimización» disponibles en WordPress. Algunos ejemplos son:

Estos plugins prometen acelerar tu web a través de una serie de optimizaciones, en general muy razonables, de las que, en teoría, «cualquier WordPress se puede beneficiar». ¿Que de qué tipo de optimizaciones estamos hablando, preguntas? Pues cosas como estas:

  • Desencolado de scripts innecesarios en el frontend, como los emojis o los Dashicons
  • Cache de páginas y consultas a la base de datos
  • Reducción de la cantidad de información que se incluye en la cabecera
  • Combinado y minificación de scripts JavaScript y estilos CSS
  • Minificado del código HTML de la página
  • Eliminación de los parámetros de versión en las URLs de los recursos estáticos a la web
  • Retardo de la carga de scripts JavaScript usando directivas defer y/o async
  • etc

Como te decía, este tipo de optimizaciones son, en general, beneficiosas. Pero en nuestra experiencia, todas las optimizaciones que afectan a los ficheros JavaScript de una web en WordPress dan más problemas que soluciones. Ejemplos reales que he visto a lo largo de los años:

  • Combinado de scripts. Cuando un script JavaScript peta, se detiene su ejecución y se reporta el error en la consola de tu navegador. Pero solo se detiene la ejecución de ese script; si hay otros, esos se ejecutarán con normalidad. Ahora bien, si combinas todos los scripts en un único script y alguno de los scripts originales contenía un error… como ahora hay un único script, la ejecución se detendrá y cosas que antes funcionaban, ahora pueden no funcionar.
  • Minificación de scripts. Este es menos habitual y en la mayoría de casos ya está corregido… pero mejor no te cuento la de veces que minificando scripts el plugin de optimización contenía algún error y se rompía alguna expresión regular. :-/
  • Eliminación de parámetros de versión. Cuando encolas un script en WordPress, puedes indicar un número de versión para el mismo. Es más, de hecho hoy en día, usando paquetes como @wordpress/scripts, dicho número de versión se genera automáticamente. Pues bien, esto es ideal porque, si por lo que sea cambias tu script (y generas un nuevo número de versión), cuando tus usuarios actualicen el plugin y se encole tu script en sus webs, el enlace URL al script será diferente (por el número de versión) y todos sus visitantes se verán «forzados» a pedir la nueva versión. Hasta que, claro está, llega el plugin de WPO de turno, quita dicho parámetro y resulta que tus visitantes quizás están usando una versión cacheada por su navegador de tu script. ¡Maldita la gracia!

Un desastre todo, la verdad. Pero es que los problemas no acaban ahí… el problema más común que estoy viendo últimamente es el que te cuento a continuación.

Retardo de scripts

Como ya te hemos comentado en varias ocasiones, en Nelio hemos implementado un plugin de A/B Testing con el que monitorizar las acciones de los visitantes y descubrir qué diseño y contenido consigue mejores ratios de conversión. Sin entrar en más detalles, puedes imaginar que el script de tracking de Nelio A/B Testing tendrá un esqueleto parecido a esto:

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

en el que, básicamente, se define una función init que recibe información sobre las pruebas A/B que hay en ejecución en ese momento y añade los listeners necesarios para poder monitorizar qué hacen los visitantes.

Pues bien, para poder usar este script en WordPress debemos encolarlo así:

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 cual da como resultado un código HTML parecido a este:

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

En el ejemplo que acabo de compartir contigo, el resultado debería ser ver por consola el objeto con la lista de experimentos y tal, ¿no? Pues bien, como tengas un plugin de WPO que añade la directiva defer a un script:

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

el HTML anterior pasa a ser equivalente a este otro:

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

Es decir, el script nab-tracking-js ya no se carga cuando toca, sino que lo hace al final de todo, con lo que la ejecición del script en línea nab-tracking-js-after falla porque la función NelioABTesting.init de la que depende no está disponible. Y cuando por fin el navegador carga nab-tracking-js, este no hará absolutamente nada porque está a la espera de que alguien llame a su método init correctamente… pero es que ese tren ya hace tiempo que pasó de largo (en el head, para ser más precisos). ¡Horrible!

Nelio A/B Testing

Pruebas A/B nativas en WordPress

Usa tu editor de páginas favorito en WordPress para crear variaciones y lanza pruebas A/B con solo un par de clics. No se necesita saber nada de programación para que funcione.

Solución

La solución más eficaz está clara: pídeles a tus usuarios que desactiven la optimización de scripts si quieren que todo les funcione debidamente. La gestión de dependencias en JavaScript cuando aparecen las directivas defer y async (de la cual no hemos hablado, pero cuyos efectos secundarios son aún peores) es un verdadero follón. No está nada claro cómo se puede solucionar correctamente, y esto es algo que atestigua esta discusión (¡de 12 años!) en el trac de WordPress.

Pero si eso no es viable, te recomiendo que hagas lo mismo que hicimos nosotros en algunos plugins. Olvídate de crear un script que exponga un método init al que llamas con un script en línea que generas dinámicamente desde WordPress e invierte las responsabilidades. En lugar de eso, añade un script en línea que defina una variable con los ajustes y úsala desde tu script:

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

de tal forma que el HTML resultante quede así:

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

y ya poco importe si se retarda la ejecución del script externo o no; a fin de cuentas, siempre aparecerá después del script en línea, satisfaciéndose así la dependencia entre ellos.

Y ya si quieres asegurarte de que nadie va a tocar tus ajustes, declara la variable como const y congela el objeto con Object.freeze:

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

lo cual está soportado por todos los navegadores modernos.

#2 Dependencias en WordPress que no funcionan… o sí, ¡yo qué sé!

Otro de los problemas habituales que uno se encuentra cuando desarrolla en JavaScript para WordPress son las dependencias y, en concreto, con las dependencias a scripts que vienen incluidos en el propio WordPress. Permíteme que me explique.

Imagina, por ejemplo, que estamos creando una pequeña extensión para Gutenberg, tal y como te explicábamos aquí. En el código fuente de nuestro plugin tendremos diferentes import a paquetes de WordPress (y otros):

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

Cuando transpilas tu código fuente JavaScript, normalmente Webpack (o la herramienta que uses) se encarga de empaquetar todas las dependencias y el código de tu app en un único fichero. Este es el fichero que luego encolarás desde WordPress para que todo funcione como esperas.

Pues bien, si usas @wordpress/scripts para crear dicho fichero, algunas de las dependencias no formarán parte del resultado final y el script resultante supondrá que dichas variables están disponibles en el scope global. O sea, que los import anteriores pasarán a ser algo tal que así:

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

lo cual no es un problema, porque @wordpress/scripts también genera un fichero PHP con la lista de dependencias de nuestro plugin, para que no nos olvidemos encolar ninguna:

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

¿Cuál es el problema? Que tus dependencias están en continua evolución y, por lo tanto, es posible que si desarrollas usando la última versión de WordPress, estés usando funciones o características que están en esa última versión (y, por lo tanto, todo funciona como debería) pero no en WordPress 5.7 (con lo cual, el plugin deja de funcionar en instalaciones «antiguas»).

Solución

Mi consejo aquí es muy sencillo. Desarrolla tus plugins usando siempre la última versión de WordPress, pero antes de lanzar una nueva versión de tu plugin, no olvides probarlo en la versión de WordPress mínima a la que tu plugin da soporte. Esta versión mínima es la que pones en el readme.txt de tu plugin:

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

y la puedes activar usando WP CLI así:

wp core update --version=5.4 --force

#3 Funciones con flecha que dejan de funcionar cuando los astros se alinean

Finalmente, quería comentarte un problema con el que nos encontramos hace unos días y que me dejó todo loco. Básicamente, teníamos un fichero JavaScript similar a este:

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

que metíamos en el front-end de la web para inicializar los formularios de Nelio Forms que haya en la página. Nada raro, verdad? Definimos una función anónima que debe llamarse cuando la página se haya cargado, la cual internamente usa una función auxiliar llamada initForm la cual hemos definido como función con flecha.

Pues bien, el problema es que este código a veces peta… especialmente si el script que lo incluye ha sido optimizado con la directiva defer que he comentado antes. ¿Por qué? Muy fácil: cuando el DOM aún no está preparado, la ejecución del script anterior sigue el siguiente orden:

  1. Se define la función anónima que hay dentro de domReady
  2. Se ejecuta domReady
  3. Como el DOM aún no está listo, domReady internamente se apunta que, cuando esté todo cargado, deberá invocar la función anónima
  4. JavaScript sigue parseando el fichero y carga la función initForm

Ahora bien, si llegado el punto 3 el DOM ya está listo, domReady llama directamente a la función anónima y, por lo tanto, se genera un error, porque initForm, en ese momento, aún está undefined.

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

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

el linter de JavaScript marca un error en la primera alternativa (bien, me gusta) pero no en la segunda (¡nooooo!).

Solución

En este caso, tienes dos soluciones. O bien defines la función auxiliar usando la palabra clave function y te olvidas de la función flecha o bien pones el domReady al final, después de haber definido todas las funciones auxiliares:

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

Si te preguntas por qué la primera solución funciona si, aparentemente, es equivalente al código original que teníamos… es una cuestión de cómo funciona el hoisting en JavaScript. En resumidas cuentas, en JavaScript puedes usar una función (definida con function) antes de haberla definido; pero las variables y constantes (lo cual incluye las funciones con flecha), no.

En definitiva…

Como has podido ver, hay una enorme cantidad de cosas que pueden salir mal en JavaScript. Por suerte, todas tienen solución, especialmente si prestamos atención a lo que hacemos. Espero que hoy hayas aprendido algo nuevo y confío que gracias a los errores y gazapos que yo he cometido en el pasado, tú podrás evitar sufrirlos en tus propias carnes en el futuro.

Imagen destacada de Ian Stauffer en Unsplash.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *