JavaScript w WordPressie to piekło… i oto dlaczego

Opublikowany: 2022-05-05

Stos deweloperski WordPressa bardzo się zmienił w ostatnich latach. Od czasu pojawienia się Gutenberga rola JavaScript w naszym ulubionym CMS jest ważniejsza niż kiedykolwiek. W tym blogu omówiliśmy już szczegółowo korzyści, jakie to pociąga za sobą dla programistów (by wymienić kilka przykładów, rozmawialiśmy o rozszerzeniach dla Gutenberga, poradach dotyczących TypeScript i React, narzędziach programistycznych, przykładach wtyczek i nie tylko), ale każdy historia ma swoją ciemną stronę … io tym dzisiaj porozmawiamy.

W dzisiejszym poście podzielę się z wami 3 głównymi problemami , z którymi, jako twórca wtyczek do WordPressa, może się zmierzyć w przyszłości. Zabawne jest to, że każdy z nich ma innego winowajcę: sam WordPress, innych programistów lub Ciebie. A więc zaczynamy: najczęstsze problemy związane z JavaScriptem, jakie możesz napotkać i co możesz zrobić, aby je uniknąć/naprawić.

#1 Wtyczki WPO, które zepsują Twoją wtyczkę i witrynę

Zacznijmy od problemu, który był źródłem wielu biletów w Nelio: wtyczki WPO.

Jestem pewien, że przeczytałeś wiele artykułów i postów na blogu, które podkreślają znaczenie posiadania lekkiej witryny, która szybko się ładuje. To znaczy, pisaliśmy o tym również kilkakrotnie! Wśród wskazówek, które zwykle dają, znajdziesz takie rzeczy, jak przejście na lepszego dostawcę hostingu, korzystanie z wtyczek pamięci podręcznej i sieci CDN, aktualizowanie serwera i WordPressa lub (i tu pojawia się pierwszy problem) zainstalowanie wtyczki WPO. Niektóre przykłady tych ostatnich obejmują:

  • Całkowita pamięć podręczna W3 z ponad milionem instalacji
  • SiteGround Optimizer z ponad 1 milionem instalacji (z których, nawiasem mówiąc, używamy w naszej witrynie)
  • Poprawki i optymalizacje WordPress WPO autorstwa naszego dobrego przyjaciela Fernando Tellado

Wtyczki te obiecują przyspieszyć działanie Twojej witryny dzięki serii rozsądnych optymalizacji, z których ogólnie „może skorzystać każda witryna WordPress”. Te optymalizacje obejmują:

  • Usuwanie z kolejki niepotrzebnych skryptów w interfejsie, takich jak emotikony lub Dashicons
  • Buforowanie stron i zapytań do bazy danych
  • Zmniejszenie ilości informacji zawartych w nagłówku
  • Łączenie i minimalizowanie skryptów JavaScript i stylów CSS
  • Minimalizowanie kodu HTML
  • Usunięcie argumentu zapytania o wersję z adresów URL zasobów statycznych
  • Odraczanie skryptów JavaScript i/lub ładowanie ich asynchronicznie
  • itp

Jak już mówiłem, tego typu optymalizacje mogą być generalnie korzystne. Ale z naszego doświadczenia wynika, że ​​wszystkie optymalizacje JS w witrynie WordPress powodują więcej problemów , przez co ich rzekome ulepszenia stają się bezużyteczne. Prawdziwe przykłady, które widziałem na przestrzeni lat to:

  • Łączenie skryptów. Im mniej skryptów ma żądać Twoja przeglądarka, tym lepiej. Dlatego warto połączyć wszystkie skrypty w jeden. Może to jednak być problematyczne. Ogólnie rzecz biorąc, jeśli skrypt JavaScript ulegnie awarii, jego wykonanie zostanie zakończone, a błąd zostanie zgłoszony w konsoli przeglądarki. Ale tylko wykonanie tego skryptu jest zatrzymywane; Twoje inne skrypty będą działać normalnie. Ale jeśli połączysz je wszystkie… cóż, jak tylko jeden skrypt ulegnie awarii, inne (w tym może twój) nie będą działać, a użytkownicy pomyślą, że to twoja wtyczka nie działa zgodnie z oczekiwaniami.
  • Skrypty minifikacyjne. Widziałem procesy minifikacji, które, wierzcie lub nie, łamały wyrażenia regularne i powodowały, że skrypty JS miały błędy składniowe. Jasne, minęło trochę czasu od ostatniego spotkania z tym, ale… :-/
  • Argumenty zapytania. Kiedy umieszczasz skrypt w kolejce w WordPressie, możesz to zrobić za pomocą jego numeru wersji (który, nawiasem mówiąc, został prawdopodobnie automatycznie wygenerowany przez @wordpress/scripts ). Numery wersji są niezwykle pomocne: jeśli zaktualizujesz wtyczkę, a skrypt się zmieni, ten nowy numer wersji zagwarantuje, że wszyscy odwiedzający zobaczą inny adres URL, a zatem ich przeglądarki zażądają nowej wersji. Niestety, jeśli wtyczka WPO usunie ciąg zapytania, odwiedzający mogą nie zdawać sobie sprawy, że skrypt się zmienił i będą używać buforowanej kopii tego skryptu… co może, ale nie musi, spowodować niezamierzone konsekwencje. Cholerny!

Kompletna katastrofa, prawda? Ale poczekaj, aż usłyszysz następny:

Odroczenie skryptów

W Nelio wdrożyliśmy wtyczkę A/B Testing, za pomocą której można śledzić odwiedzających i odkrywać, który projekt i treść uzyskuje najwięcej konwersji. Jak możesz sobie wyobrazić, nasz skrypt śledzący wygląda podobnie do tego:

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

Oznacza to, że udostępnia funkcję init , którą musimy wywołać, aby powiadomić skrypt o aktualnie uruchomionych testach. Aby wywołać tę metodę, umieszczamy w kolejce skrypt wbudowany w PHP w następujący sposób:

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

co skutkuje następującymi tagami HTML:

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

Ale co się stanie, gdy twoja wtyczka WPO doda atrybut defer do naszego skryptu?

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

Cóż, skrypt jest teraz odroczony… co oznacza, że ​​poprzedni fragment jest odpowiednikiem tego:

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

w rezultacie nab-tracking-js nie jest już ładowany, kiedy powinien, a zatem skrypt wbudowany, który pojawia się po nim i na nim polega, po prostu się nie powiedzie: nab-tracking-js-after używa NelioABTesting.init , który , dzięki dyrektywie defer , nie jest jeszcze dostępny. Okropny!

Rozwiązanie

Najskuteczniejsze rozwiązanie jest jasne: powiedz swoim użytkownikom, aby wyłączyli optymalizację skryptów i zadzwoń do tego dnia. W końcu zarządzanie zależnościami w JavaScript jest na ogół niezwykle skomplikowane (zwłaszcza jeśli używamy dyrektyw defer i async ), a WordPress nie jest wyjątkiem. Wystarczy spojrzeć na tę 12-letnią dyskusję na ten temat!

Ale jeśli nie jest to możliwe (a wiem, że nie jest), radzę zrobić to samo, co zrobiliśmy: pozbyć się metody init i odwrócić obowiązki zwykłych i wbudowanych skryptów. Oznacza to, że dodaj skrypt wbudowany przed zwykłym skryptem i użyj go do zdefiniowania zmiennej globalnej z wymaganymi ustawieniami:

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

aby wynikowy kod HTML wyglądał tak:

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

więc nie ma znaczenia, czy wykonanie zewnętrznego skryptu jest opóźnione, czy nie — zawsze pojawi się po skrypcie wbudowanym, spełniając w ten sposób zależność między nimi.

Na koniec, jeśli chcesz się upewnić, że nikt nie będzie modyfikował twoich ustawień, zadeklaruj zmienną jako const i zamroź jej wartość za pomocą Object.freeze :

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

który jest obsługiwany przez wszystkie nowoczesne przeglądarki.

#2 Zależności w WordPressie, które mogą, ale nie muszą działać…

Zarządzanie zależnościami również może być problematyczne w WordPressie, zwłaszcza gdy mówimy o wbudowanych skryptach WordPressa. Pozwól mi wyjaśnić.

Wyobraź sobie na przykład, że tworzymy małe rozszerzenie dla Gutenberga, jak wyjaśniliśmy tutaj. Kod źródłowy naszej wtyczki prawdopodobnie będzie zawierał kilka instrukcji import , takich jak te:

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

Kiedy ten kod źródłowy JS zostanie przetransponowany, Webpack (lub narzędzie, którego używasz) spakuje wszystkie zależności i twój własny kod źródłowy w pojedynczym pliku JS. Jest to plik, który później dodasz do kolejki z WordPressa, aby wszystko działało zgodnie z oczekiwaniami.

Jeśli użyłeś @wordpress/scripts do utworzenia takiego pliku, niektóre zależności nie zostaną uwzględnione w pliku wyjściowym, ponieważ wbudowany proces zakłada, że ​​pakiety będą dostępne w zasięgu globalnym. Oznacza to, że poprzednie importy zostaną przeniesione na coś podobnego do tego:

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

Aby upewnić się, że nie przegapisz żadnej z zależności twojego skryptu, @wordpress/scripts nie tylko przetranspiluje twój kod JS, ale także wygeneruje plik PHP z jego zależnościami od WordPressa:

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

Całkiem fajnie, co? Więc w czym problem? Cóż, te pakiety WordPress są w ciągłym rozwoju i dość często się zmieniają, dodając nowe funkcje i ulepszenia. Dlatego, jeśli opracujesz swoją wtyczkę przy użyciu najnowszej wersji WordPressa, możesz przypadkowo użyć funkcji lub funkcji dostępnych w tej najnowszej wersji (a zatem wszystko działa tak, jak powinno), ale nie w „starszych” wersjach WordPressa…

Jak możesz powiedzieć?

Rozwiązanie

Moja rada tutaj jest bardzo prosta: rozwijaj swoje wtyczki korzystając z najnowszej wersji WordPressa, ale testuj swoje wydania na starszych wersjach. W szczególności sugeruję przetestowanie wtyczki przynajmniej z minimalną wersją WordPressa, którą ma obsługiwać wtyczka. Minimalna wersja, którą znajdziesz w pliku readme.txt swojej wtyczki:

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

Przełączanie się z jednej wersji WordPress na inną jest tak proste, jak uruchomienie następującego polecenia WP CLI:

 wp core update --version=5.4 --force

#3 Funkcje strzałek są trudniejsze niż myślisz

Na koniec podzielę się jednym z ostatnich problemów, które napotkałem zaledwie kilka dni temu i który doprowadzał mnie do szału. W skrócie, mieliśmy plik JavaScript podobny do tego:

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

który inicjuje Twoje formularze Nelio na interfejsie użytkownika. Scenariusz jest dość prosty, prawda? Definiuje funkcję anonimową, która jest wywoływana, gdy DOM jest gotowy. Ta funkcja używa funkcji pomocniczej (strzałki) o nazwie initForm . Jak się okazuje, taki prosty przykład może się zawiesić! Ale tylko w określonych okolicznościach (np. jeśli skrypt został „zoptymalizowany” przez wtyczkę WPO przy użyciu atrybutu defer ).

Oto jak JS wykonuje poprzedni skrypt:

  1. Zdefiniowano funkcję anonimową wewnątrz domReady
  2. domReady działa
  3. Jeśli DOM nie jest jeszcze gotowy (i zwykle nie jest, gdy skrypt jest ładowany), domReady nie uruchamia funkcji zwrotnej. Zamiast tego po prostu śledzi to, aby móc zadzwonić później
  4. JavaScript kontynuuje parsowanie pliku i ładuje funkcję initForm
  5. Gdy DOM jest gotowy, w końcu wywoływana jest funkcja wywołania zwrotnego

A co, jeśli do czasu trzeciego kroku DOM jest gotowy, a zatem domReady wywołuje bezpośrednio funkcję anonimową? Cóż, w takim przypadku skrypt wywoła niezdefiniowany błąd, ponieważ initForm nadal jest undefined .

W rzeczywistości najciekawsze w tym wszystkim jest to, że te dwa rozwiązania są równoważne:

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

linter JavaScript zwróci błąd tylko na pierwszym, ale nie na ostatnim.

Rozwiązanie

Możliwe są dwa rozwiązania: albo definiujesz funkcję pomocniczą za pomocą słowa kluczowego function i zapominasz o funkcji strzałki, albo przenosisz instrukcję domReady na koniec, po zdefiniowaniu wszystkich funkcji pomocniczych:

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

Jeśli zastanawiasz się, dlaczego pierwsze rozwiązanie działa, jeśli najwyraźniej jest równoważne oryginalnemu, które mieliśmy, to wszystko dotyczy tego, jak działa przenoszenie JavaScript. Krótko mówiąc, w JavaScript możesz użyć funkcji (zdefiniowanej za pomocą function ) przed jej definicją, ale nie możesz zrobić tego samego ze zmiennymi i stałymi (a zatem funkcjami strzałek).

W podsumowaniu

W JavaScript jest wiele rzeczy, które mogą pójść nie tak. Na szczęście wszystkie mają rozwiązanie, zwłaszcza jeśli zwracamy uwagę na to, co robimy. Mam nadzieję, że dzisiaj nauczyłeś się czegoś nowego i ufam, że dzięki błędom i błędom, które popełniłem w przeszłości, będziesz w stanie uniknąć ich cierpienia we własnym ciele w przyszłości.

Polecane zdjęcie Iana Stauffera na Unsplash.