JavaScript su WordPress è l'inferno... ed ecco perché

Pubblicato: 2022-05-05

Lo stack di sviluppo di WordPress è cambiato molto negli ultimi anni. Dall'arrivo di Gutenberg, il ruolo di JavaScript nel nostro CMS preferito è più importante che mai. In questo blog abbiamo già parlato a lungo dei vantaggi che ciò comporta per gli sviluppatori (per citare alcuni esempi, abbiamo parlato di estensioni per Gutenberg, consigli su TypeScript e React, strumenti di sviluppo, esempi di plugin e altro) ma ogni la storia ha il suo lato oscuro … ed è di questo che parleremo qui oggi.

Nel post di oggi, condividerò con te i 3 problemi principali che tu, uno sviluppatore di plugin per WordPress, potresti dover affrontare in futuro. E la parte divertente è che ognuno di loro ha un colpevole diverso: lo stesso WordPress, altri sviluppatori o te stesso. Quindi eccoci qui: i mal di testa JavaScript più comuni che potresti incontrare e cosa puoi fare per evitarli/correggerli.

Plugin WPO n. 1 che interromperanno il tuo plug-in e il tuo sito

Cominciamo con il problema che è stato all'origine di molti ticket qui su Nelio: i plugin WPO.

Sono sicuro che hai letto molti articoli e post di blog che sottolineano l'importanza di avere un sito Web leggero che si carica velocemente. Voglio dire, ne abbiamo anche scritto in diverse occasioni! Tra i suggerimenti che di solito danno, troverai cose come passare a un provider di hosting migliore, utilizzare plug-in di cache e CDN, mantenere aggiornati il ​​tuo server e WordPress o (e qui arriva il primo problema) installare un plug-in WPO. Alcuni esempi di quest'ultimo includono:

  • W3 Total Cache con oltre 1 milione di installazioni
  • SiteGround Optimizer con oltre 1 milione di installazioni (una delle quali, tra l'altro, utilizziamo nel nostro sito Web)
  • WordPress WPO Tweaks & Optimization dal nostro buon amico Fernando Tellado

Questi plugin promettono di velocizzare il tuo sito web attraverso una serie di ottimizzazioni sensate di cui, in generale, "qualsiasi sito WordPress può trarre vantaggio". Queste ottimizzazioni includono:

  • Eliminare gli script non necessari sul frontend, come emoji o Dashicon
  • Caching di pagine e query di database
  • Ridurre la quantità di informazioni incluse nell'intestazione
  • Combinazione e minimizzazione di script JavaScript e stili CSS
  • Minimizzare l'HTML
  • Rimozione della query di versione arg dagli URL delle tue risorse statiche
  • Rinviare gli script JavaScript e/o caricarli in modo asincrono
  • eccetera

Come dicevo, questi tipi di ottimizzazioni potrebbero essere, in generale, vantaggiosi. Ma nella nostra esperienza, tutte le ottimizzazioni JS su un sito Web WordPress tendono a causare più problemi , rendendo così inutili i loro presunti miglioramenti. Esempi reali che ho visto negli anni sono:

  • Combinazione di script. Meno script deve richiedere il tuo browser, meglio è. Ecco perché ha senso combinare tutti gli script in uno. Tuttavia, questo può essere problematico. In generale, se uno script JavaScript va in crash, la sua esecuzione termina e l'errore viene segnalato nella console del tuo browser. Ma solo l'esecuzione di quello script viene interrotta; gli altri tuoi script verranno eseguiti normalmente. Ma se li combini tutti... beh, non appena uno script fallisce, gli altri script (incluso forse il tuo) non verranno eseguiti e i tuoi utenti penseranno che sia il tuo plug-in quello che non funziona come previsto.
  • Script minimizzanti. Ho visto alcuni processi di minimizzazione che, che ci crediate o no, hanno rotto le espressioni regolari e hanno prodotto script JS con errori di sintassi. Certo, è passato un po' di tempo dall'ultima volta che l'ho incontrato ma... :-/
  • Argomenti di query. Quando accodi uno script in WordPress, puoi farlo con il suo numero di versione (che, tra l'altro, è stato probabilmente generato automaticamente da @wordpress/scripts ). I numeri di versione sono estremamente utili: se aggiorni il tuo plugin e il tuo script è cambiato, questo nuovo numero di versione garantirà che tutti i visitatori vedano un URL diverso e, quindi, i loro browser richiederanno la nuova versione. Sfortunatamente, se un plug-in WPO rimuove la stringa di query, i tuoi visitatori potrebbero non rendersi conto che lo script è cambiato e utilizzeranno una copia memorizzata nella cache di tale script... il che potrebbe o meno causare conseguenze indesiderate. Dannazione!

Un completo disastro, non è vero? Ma aspetta di sentire il prossimo:

Rimandare gli script

In Nelio abbiamo implementato un plug-in A/B Testing con cui tracciare i tuoi visitatori e scoprire quale design e contenuto ottiene il maggior numero di conversioni. Come puoi immaginare, il nostro script di monitoraggio è simile a questo:

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

Cioè, espone una funzione init che dobbiamo chiamare per far conoscere allo script i test attualmente in esecuzione. Per chiamare questo metodo, accodiamo uno script inline in PHP come segue:

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

che si traduce nei seguenti tag HTML:

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

Ma cosa succede quando il tuo plug-in WPO aggiunge l'attributo di defer al nostro 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> ...

Bene, lo script è ora posticipato... il che significa che lo snippet precedente è equivalente a questo:

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

e, di conseguenza, nab-tracking-js non viene più caricato quando dovrebbe e, quindi, lo script inline che viene dopo e si basa su di esso fallirà semplicemente: nab-tracking-js-after usa NelioABTesting.init che , grazie alla direttiva defer , non è ancora disponibile. Terribile!

Soluzione

La soluzione più efficace è chiara: dì ai tuoi utenti di disabilitare l'ottimizzazione degli script e di farla finita. Dopotutto, la gestione delle dipendenze in JavaScript è, in generale, estremamente complicata (soprattutto se utilizziamo direttive defer e async ) e WordPress non fa eccezione. Basta dare un'occhiata a questa discussione di 12 anni sull'argomento!

Ma se ciò non è fattibile (e so che non lo è), ti consiglio di fare la stessa cosa che abbiamo fatto noi: sbarazzarsi del metodo init e invertire le responsabilità degli script regolari e inline. Cioè, aggiungi uno script inline prima dello script normale e usalo per definire una variabile globale con le impostazioni richieste:

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

in modo che l'HTML risultante assomigli a questo:

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

e quindi non importa se l'esecuzione dello script esterno è ritardata o meno: apparirà sempre dopo lo script inline, soddisfacendo così la dipendenza tra di loro.

Infine, se vuoi assicurarti che nessuno modifichi le tue impostazioni, dichiara la variabile come const e blocca il suo valore con Object.freeze :

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

che è supportato da tutti i browser moderni.

#2 Dipendenze in WordPress che potrebbero funzionare o meno...

Anche la gestione delle dipendenze può essere problematica in WordPress, soprattutto quando si parla di script integrati di WordPress. Lasciatemi spiegare.

Immagina, ad esempio, di creare una piccola estensione per Gutenberg, come abbiamo spiegato qui. Il codice sorgente del nostro plugin avrà probabilmente alcune istruzioni di import come queste:

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

Quando questo codice sorgente JS viene trasferito, Webpack (o lo strumento che usi) impacchetta tutte le dipendenze e il tuo codice sorgente in un unico file JS. Questo è il file che in seguito accoderai da WordPress in modo che tutto funzioni come previsto.

Se hai usato @wordpress/scripts per creare un tale file, alcune delle dipendenze non saranno incluse nel file di output, perché il processo integrato presuppone che i pacchetti saranno disponibili nell'ambito globale. Ciò significa che le importazioni precedenti verranno trasferite in qualcosa di simile a questo:

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

Per assicurarti di non perdere nessuna delle dipendenze del tuo script, @wordpress/scripts non solo trasporterà il tuo codice JS, ma genererà anche un file PHP con le sue dipendenze da WordPress:

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

Abbastanza pulito, eh? Allora, qual è il problema? Bene, questi pacchetti WordPress sono in continuo sviluppo e cambiano abbastanza spesso, aggiungendo nuove funzionalità e miglioramenti. Pertanto, se sviluppi il tuo plug-in utilizzando l'ultima versione di WordPress, potresti finire inavvertitamente a utilizzare funzioni o funzionalità disponibili in quell'ultima versione (e quindi tutto funziona come dovrebbe) ma non in versioni "precedenti" di WordPress...

Come puoi dirlo?

Soluzione

Il mio consiglio qui è molto semplice: sviluppa i tuoi plugin usando l'ultima versione di WordPress, ma testa le tue versioni su versioni precedenti. In particolare, ti suggerisco di testare il tuo plugin almeno con la versione minima di WordPress che il tuo plugin dovrebbe supportare. Una versione minima la troverai nel readme.txt del tuo plugin:

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

Passare da una versione di WordPress a un'altra è facile come eseguire il seguente comando WP CLI:

 wp core update --version=5.4 --force

# 3 Le funzioni delle frecce sono più complicate di quanto pensi

Infine, permettetemi di condividere uno degli ultimi problemi che ho riscontrato solo pochi giorni fa e che mi ha fatto impazzire. In poche parole, avevamo un file JavaScript simile a questo:

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

che inizializza i moduli Nelio sul front-end. La sceneggiatura è abbastanza semplice, vero? Definisce una funzione anonima che viene chiamata quando il DOM è pronto. Questa funzione utilizza una funzione di supporto (freccia) chiamata initForm . Bene, a quanto pare, un esempio così semplice può andare in crash! Ma solo in alcune circostanze specifiche (ad esempio se lo script è stato "ottimizzato" da un plug-in WPO utilizzando l'attributo defer ).

Ecco come JS esegue lo script precedente:

  1. Viene definita la funzione anonima all'interno domReady
  2. domReady viene eseguito
  3. Se il DOM non è ancora pronto (e di solito non lo è quando viene caricato uno script), domReady non esegue la funzione di callback. Invece, ne tiene semplicemente traccia in modo che possa chiamarlo in seguito
  4. JavaScript continua ad analizzare il file e carica la funzione initForm
  5. Una volta che il DOM è pronto, viene finalmente chiamata la funzione di callback

Ora, cosa succede se, quando raggiungiamo il terzo passaggio, il DOM è pronto e, quindi, domReady chiama direttamente la funzione anonima? Bene, in tal caso, lo script attiverà un errore non definito, perché initForm è ancora undefined .

In effetti, la cosa più curiosa di tutto questo è che queste due soluzioni sono equivalenti:

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

il linter JavaScript genererà un errore solo sul primo, ma non sull'ultimo.

Soluzione

Ci sono due possibili soluzioni: o si definisce la funzione helper usando la parola chiave function e si dimentica la funzione freccia, oppure si sposta l'istruzione domReady alla fine, dopo che tutte le funzioni helper sono state definite:

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

Se ti stai chiedendo perché la prima soluzione funziona se apparentemente è equivalente a quella originale che avevamo, è tutto su come funziona il sollevamento JavaScript. In breve, in JavaScript puoi usare una funzione (definita con function ) prima della sua definizione, ma non puoi fare lo stesso con variabili e costanti (e quindi funzioni freccia).

In sintesi

C'è un numero enorme di cose che possono andare storte in JavaScript. Fortunatamente, hanno tutti una soluzione, soprattutto se prestiamo attenzione a ciò che facciamo. Spero che tu abbia imparato qualcosa di nuovo oggi e confido che grazie agli errori e agli errori che ho commesso in passato, in futuro potrai evitare di subirli nella tua stessa carne.

Immagine in primo piano di Ian Stauffer su Unsplash.