JavaScript pe WordPress este iadul... și iată de ce

Publicat: 2022-05-05

Stack-ul de dezvoltare WordPress s-a schimbat foarte mult în ultimii ani. De la apariția lui Gutenberg, rolul JavaScript în CMS-ul nostru preferat este mai important ca niciodată. În acest blog am vorbit deja pe larg despre avantajele pe care le presupune acest lucru pentru dezvoltatori (pentru a numi câteva exemple, am vorbit despre extensii pentru Gutenberg, sfaturi pentru TypeScript și React, instrumente de dezvoltare, exemple de pluginuri și multe altele) dar fiecare povestea are partea ei întunecată ... și despre asta vom vorbi astăzi aici.

În postarea de astăzi, voi împărtăși cu voi cele 3 probleme principale pe care le-ați putea confrunta în viitor, un dezvoltator de plugin WordPress. Și partea amuzantă este că fiecare dintre ei are un vinovat diferit: fie WordPress însuși, alți dezvoltatori, fie dumneavoastră. Așa că iată-ne: cele mai frecvente dureri de cap JavaScript pe care le puteți întâlni și ce puteți face pentru a le evita/remedia.

Pluginurile WPO #1 care vă vor distruge pluginul și site-ul

Să începem cu problema care a fost sursa multor bilete aici la Nelio: pluginuri WPO.

Sunt sigur că ați citit o mulțime de articole și postări pe blog care subliniază importanța de a avea un site web ușor, care se încarcă rapid. Adică am scris și noi de mai multe ori despre asta! Printre sfaturile pe care le oferă de obicei, veți găsi lucruri precum trecerea la un furnizor de găzduire mai bun, utilizarea pluginurilor cache și a CDN-urilor, menținerea serverului și WordPress la zi sau (și aici apare prima problemă) instalarea unui plugin WPO. Câteva exemple ale acestora din urmă includ:

  • W3 Total Cache cu peste 1 milion de instalări
  • SiteGround Optimizer cu peste 1 milion de instalări și (dintre care una, de altfel, o folosim pe site-ul nostru web)
  • Modificări și optimizări WPO WordPress de la bunul nostru prieten Fernando Tellado

Aceste plugin-uri promit să vă accelereze site-ul printr-o serie de optimizări sensibile de care, în general, „orice site WordPress poate beneficia”. Aceste optimizări includ:

  • Scoaterea din coadă a scripturilor inutile de pe front-end, cum ar fi emoji-urile sau Dashicon-urile
  • Memorarea în cache a paginilor și a interogărilor de baze de date
  • Reducerea cantității de informații incluse în antet
  • Combinarea și reducerea scripturilor JavaScript și a stilurilor CSS
  • Minimizarea HTML
  • Eliminarea argumentului de interogare de versiune din adresele URL ale elementelor dvs. statice
  • Amânarea scripturilor JavaScript și/sau încărcarea lor asincron
  • etc

După cum spuneam, aceste tipuri de optimizări ar putea fi, în general, benefice. Dar, din experiența noastră, toate optimizările JS de pe un site web WordPress tind să aibă ca rezultat mai multe probleme , făcând astfel inutile presupusele lor îmbunătățiri. Exemplele reale pe care le-am văzut de-a lungul anilor sunt:

  • Combinarea scripturilor. Cu cât browserul dvs. trebuie să solicite mai puține scripturi, cu atât mai bine. De aceea, este logic să combinați toate scripturile într-unul singur. Cu toate acestea, acest lucru poate fi problematic. În general, dacă un script JavaScript se blochează, execuția acestuia se încheie și eroarea este raportată în consola browserului dvs. Dar numai execuția acelui script este oprită; celelalte script-uri vor rula normal. Dar dacă le combini pe toate... ei bine, de îndată ce un script eșuează, celelalte script-uri (inclusiv poate al tău) nu vor rula, iar utilizatorii tăi vor crede că pluginul tău nu funcționează conform așteptărilor.
  • Minimizarea scripturilor. Am văzut câteva procese de minificare care, credeți sau nu, au rupt expresiile regulate și au dus la scripturi JS care aveau erori de sintaxă. Sigur, a trecut ceva timp de la ultima dată când l-am întâlnit pe acesta, dar... :-/
  • Argumente de interogare. Când puneți în coadă un script în WordPress, puteți face acest lucru cu numărul său de versiune (care, apropo, a fost probabil generat automat de @wordpress/scripts ). Numerele versiunilor sunt extrem de utile: dacă actualizați pluginul și scriptul este schimbat, acest număr de versiune nouă va garanta că toți vizitatorii vor vedea o adresă URL diferită și, prin urmare, browserele lor vor solicita noua versiune. Din păcate, dacă un plugin WPO elimină șirul de interogare, este posibil ca vizitatorii să nu realizeze că scriptul s-a schimbat și vor folosi o copie în cache a respectivului script... ceea ce poate duce sau nu la consecințe nedorite. La naiba!

Un dezastru total, nu-i așa? Dar așteptați până când îl auziți pe următorul:

Amânarea scripturilor

La Nelio am implementat un plugin de testare A/B cu care să vă urmăriți vizitatorii și să descoperiți ce design și conținut înregistrează cele mai multe conversii. După cum vă puteți imagina, scriptul nostru de urmărire arată similar cu acesta:

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

Adică, expune o funcție init pe care trebuie să o apelăm pentru a informa scriptul despre testele care rulează în prezent. Pentru a apela această metodă, punem în coadă un script inline în PHP, după cum urmează:

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

care are ca rezultat următoarele etichete HTML:

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

Dar ce se întâmplă când pluginul tău WPO adaugă atributul defer la scriptul nostru?

 <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> ...

Ei bine, scriptul este acum amânat... ceea ce înseamnă că fragmentul anterior este echivalent cu acesta:

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

și, ca rezultat, nab-tracking-js nu mai este încărcat atunci când trebuie și, prin urmare, scriptul inline care vine după el și se bazează pe el pur și simplu va eșua: nab-tracking-js-after folosește NelioABTesting.init care , datorită directivei de defer , nu este încă disponibilă. Îngrozitor!

Soluţie

Cea mai eficientă soluție este clară: spuneți-le utilizatorilor să dezactiveze optimizarea scripturilor și să apeleze la aceasta. La urma urmei, gestionarea dependențelor în JavaScript este, în general, extrem de complicată (mai ales dacă folosim directive defer și async ), iar WordPress nu face excepție. Aruncă o privire la această discuție de 12 ani pe această temă!

Dar dacă acest lucru nu este fezabil (și știu că nu este), vă recomand să faceți același lucru pe care l-am făcut noi: scăpați de metoda init și inversați responsabilitățile scripturilor dvs. obișnuite și inline. Adică, adăugați un script inline înainte de scriptul obișnuit și utilizați-l pentru a defini o variabilă globală cu setările necesare:

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

astfel încât HTML rezultat să arate astfel:

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

și deci nu contează dacă execuția scriptului extern este întârziată sau nu — va apărea întotdeauna după scriptul inline, satisfăcând astfel dependența dintre ele.

În cele din urmă, dacă doriți să vă asigurați că nimeni nu vă va modifica setările, declarați variabila ca const și înghețați-i valoarea cu Object.freeze :

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

care este acceptat de toate browserele moderne.

#2 Dependențe în WordPress care pot sau nu funcționa...

Gestionarea dependenței poate fi, de asemenea, problematică în WordPress, mai ales când vorbim despre scripturile încorporate ale WordPress. Lasă-mă să explic.

Imaginați-vă, de exemplu, că creăm o mică extensie pentru Gutenberg, așa cum am explicat aici. Codul sursă al pluginului nostru va avea probabil câteva declarații de import precum acestea:

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

Când acest cod sursă JS este transpilat, Webpack (sau instrumentul pe care îl utilizați) va împacheta toate dependențele și propriul cod sursă într-un singur fișier JS. Acesta este fișierul pe care îl veți pune ulterior în coadă de la WordPress, astfel încât totul să funcționeze așa cum vă așteptați.

Dacă ați folosit @wordpress/scripts pentru a crea un astfel de fișier, unele dintre dependențe nu vor fi incluse în fișierul de ieșire, deoarece procesul încorporat presupune că pachetele vor fi disponibile în domeniul global. Aceasta înseamnă că importurile anterioare vor fi translate în ceva similar cu acesta:

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

Pentru a vă asigura că nu pierdeți niciuna dintre dependențele scriptului dvs., @wordpress/scripts nu numai că va transpila codul dvs. JS, dar va genera și un fișier PHP cu dependențele sale WordPress:

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

Destul de îngrijit, nu? Deci, care este problema? Ei bine, aceste pachete WordPress sunt în continuă dezvoltare și se schimbă destul de des, adăugând noi funcții și îmbunătățiri. Prin urmare, dacă vă dezvoltați pluginul utilizând cea mai recentă versiune de WordPress, s-ar putea să ajungeți să utilizați din greșeală funcții sau caracteristici care sunt disponibile în acea ultimă versiune (și, prin urmare, totul funcționează așa cum ar trebui), dar nu și în versiunile „mai vechi” de WordPress...

Cum poți să spui?

Soluţie

Sfatul meu aici este foarte simplu: dezvoltați-vă pluginurile folosind cea mai recentă versiune WordPress, dar testați-vă versiunile pe versiuni mai vechi. În special, ți-aș sugera să-ți testezi pluginul cu cel puțin versiunea minimă de WordPress pe care ar trebui să o accepte pluginul tău. O versiune minimă pe care o veți găsi în fișierul readme.txt al pluginului dvs.:

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

Trecerea de la o versiune WordPress la alta este la fel de ușoară ca și rularea următoarei comenzi WP CLI:

 wp core update --version=5.4 --force

# 3 Funcțiile săgeților sunt mai complicate decât credeți

În sfârșit, permiteți-mi să vă împărtășesc una dintre cele mai recente probleme pe care le-am întâlnit acum câteva zile și care m-a înnebunit. Pe scurt, am avut un fișier JavaScript similar cu acesta:

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

care inițializează formularele Nelio pe front-end. Scenariul este destul de simplu, nu-i așa? Acesta definește o funcție anonimă care este apelată atunci când DOM-ul este gata. Această funcție folosește o funcție de ajutor (săgeată) numită initForm . Ei bine, după cum se dovedește, un exemplu atât de simplu se poate prăbuși! Dar numai în anumite circumstanțe specifice (adică dacă scriptul a fost „optimizat” de un plugin WPO folosind atributul defer ).

Iată cum JS execută scriptul anterior:

  1. Funcția anonimă din domReady este definită
  2. domReady rulează
  3. Dacă DOM-ul nu este încă gata (și de obicei nu este atunci când este încărcat un script), domReady nu rulează funcția de apel invers. În schimb, pur și simplu ține evidența acestuia, astfel încât să îl poată apela mai târziu
  4. JavaScript continuă să analizeze fișierul și încarcă funcția initForm
  5. Odată ce DOM-ul este gata, funcția de apel invers este în sfârșit apelată

Acum, ce se întâmplă dacă, până ajungem la a treia etapă, DOM-ul este gata și, prin urmare, domReady apelează direct funcția anonimă? Ei bine, în acest caz, scriptul va declanșa o eroare nedefinită, deoarece initForm este încă undefined .

De fapt, cel mai curios lucru despre toate acestea este că aceste două soluții sunt echivalente:

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

JavaScript linter va arunca o eroare doar pe primul, dar nu și pe cel mai recent.

Soluţie

Există două soluții posibile: fie definiți funcția helper folosind cuvântul cheie function și uitați de funcția săgeată, fie mutați instrucțiunea domReady la sfârșit, după ce toate funcțiile helper au fost definite:

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

Dacă vă întrebați de ce funcționează prima soluție dacă aparent este echivalentă cu cea originală pe care o aveam, totul este despre modul în care funcționează ridicarea JavaScript. Pe scurt, în JavaScript poți folosi o funcție (definită cu function ) înainte de definirea acesteia, dar nu poți face același lucru cu variabilele și constantele (și, prin urmare, cu funcțiile săgeată).

În concluzie

Există un număr mare de lucruri care pot merge prost în JavaScript. Din fericire, toate au o soluție, mai ales dacă suntem atenți la ceea ce facem. Sper că ați învățat ceva nou astăzi și am încredere că, datorită greșelilor și greșelilor pe care le-am făcut în trecut, veți putea evita să le suferi în carne și oase în viitor.

Imagine prezentată de Ian Stauffer pe Unsplash.