JavaScript на WordPress — это ад… и вот почему

Опубликовано: 2022-05-05

Стек разработки WordPress сильно изменился за последние годы. С появлением Gutenberg роль JavaScript в нашей любимой CMS важна как никогда. В этом блоге мы уже подробно говорили о преимуществах, которые это влечет за собой для разработчиков (в качестве примера мы говорили о расширениях для Гутенберга, советах по TypeScript и React, инструментах разработки, примерах плагинов и многом другом), но каждый у этой истории есть и темная сторона … и об этом мы сегодня поговорим.

В сегодняшней статье я поделюсь с вами тремя основными проблемами , с которыми вы, разработчик плагинов для WordPress, можете столкнуться в будущем. И самое смешное, что у каждого из них есть свой виновник: либо сам WordPress, либо другие разработчики, либо вы сами. Итак, начнем: самые распространенные проблемы с JavaScript, с которыми вы можете столкнуться, и что вы можете сделать, чтобы их избежать/исправить.

Плагины WPO №1, которые сломают ваш плагин и ваш сайт

Начнем с проблемы, из-за которой здесь, в Nelio, было получено множество тикетов: плагины WPO.

Я уверен, что вы читали множество статей и сообщений в блогах, в которых подчеркивается важность наличия легкого веб-сайта, который быстро загружается. Я имею в виду, мы тоже не раз писали об этом! Среди советов, которые они обычно дают, вы найдете такие вещи, как переход на лучшего хостинг-провайдера, использование плагинов кеша и CDN, поддержание вашего сервера и WordPress в актуальном состоянии или (и здесь возникает первая проблема) установка плагина WPO. Некоторые примеры последних включают:

  • W3 Total Cache с более чем 1 миллионом установок
  • SiteGround Optimizer с более чем 1 миллионом установок (один из которых, кстати, мы используем на нашем веб-сайте)
  • WordPress WPO Tweaks & Optimizations от нашего хорошего друга Фернандо Телладо

Эти плагины обещают ускорить ваш сайт за счет ряда разумных оптимизаций, от которых, как правило, «может выиграть любой сайт WordPress». Эти оптимизации включают в себя:

  • Удаление из очереди ненужных скриптов во внешнем интерфейсе, таких как эмодзи или Dashicons.
  • Кэширование страниц и запросов к базе данных
  • Уменьшение количества информации, включаемой в заголовок
  • Объединение и минимизация скриптов JavaScript и стилей CSS
  • Минификация HTML
  • Удаление аргумента запроса версии из URL-адресов ваших статических ресурсов
  • Откладывание сценариев JavaScript и/или их асинхронная загрузка
  • и т.д

Как я уже говорил, эти типы оптимизации могут быть в целом полезными. Но по нашему опыту, все оптимизации JS на веб-сайте WordPress, как правило, приводят к большему количеству проблем , что делает их предполагаемые улучшения бесполезными. Реальные примеры, которые я видел за эти годы:

  • Объединение скриптов. Чем меньше скриптов будет запрашивать ваш браузер, тем лучше. Поэтому имеет смысл объединить все скрипты в один. Однако это может быть проблематично. Как правило, если сценарий JavaScript дает сбой, его выполнение прекращается, а в консоли вашего браузера сообщается об ошибке. Но останавливается только выполнение этого скрипта; другие ваши скрипты будут работать нормально. Но если вы объедините их все… ну, как только один скрипт выйдет из строя, другие скрипты (в том числе, возможно, и ваш) не будут работать, и ваши пользователи будут думать, что это ваш плагин, который не работает должным образом.
  • Минифицирующие скрипты. Я видел некоторые процессы минификации, которые, хотите верьте, хотите нет, нарушали регулярные выражения и приводили к JS-скриптам с синтаксическими ошибками. Конечно, прошло много времени с тех пор, как я в последний раз сталкивался с этим, но… :-/
  • Аргументы запроса. Когда вы ставите скрипт в очередь в WordPress, вы можете сделать это с его номером версии (который, кстати, вероятно, был автоматически сгенерирован @wordpress/scripts ). Номера версий чрезвычайно полезны: если вы обновите свой плагин и ваш скрипт изменится, этот новый номер версии гарантирует, что все посетители увидят другой URL-адрес, и, следовательно, их браузеры запросят новую версию. К сожалению, если подключаемый модуль WPO удалит строку запроса, ваши посетители могут не понять, что сценарий был изменен, и они будут использовать кэшированную копию указанного сценария… что может привести или не привести к непредвиденным последствиям. Проклятие!

Полная катастрофа, не так ли? Но подождите, пока не услышите следующее:

Отложенные скрипты

В Nelio мы внедрили плагин для A/B-тестирования, с помощью которого можно отслеживать посетителей и определять, какой дизайн и контент получают наибольшее количество конверсий. Как вы понимаете, наш скрипт отслеживания выглядит примерно так:

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

То есть он предоставляет функцию init , которую мы должны вызвать, чтобы сообщить сценарию о тестах, которые в данный момент выполняются. Чтобы вызвать этот метод, мы ставим встроенный скрипт в PHP следующим образом:

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

что приводит к следующим тегам HTML:

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

Но что произойдет, если ваш плагин WPO добавит в наш скрипт атрибут defer ?

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

Что ж, скрипт теперь отложен… что означает, что предыдущий фрагмент эквивалентен этому:

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

и, как следствие, nab-tracking-js больше не загружается, когда должен, и, следовательно, встроенный скрипт, который идет после него и опирается на него, просто выйдет из строя: nab-tracking-js-after использует NelioABTesting.init , который , благодаря директиве defer , пока недоступен. Ужасный!

Решение

Наиболее эффективное решение очевидно: скажите своим пользователям отключить оптимизацию скриптов и закругляться. В конце концов, управление зависимостями в JavaScript в целом крайне сложно (особенно если мы используем директивы defer и async ), и WordPress не является исключением. Просто взгляните на эту дискуссию 12-летней давности!

Но если это невозможно (а я знаю, что это невозможно), я рекомендую вам сделать то же самое, что и мы: избавиться от метода init и поменять местами обязанности ваших обычных и встроенных скриптов. То есть добавьте встроенный скрипт перед обычным скриптом и используйте его для определения глобальной переменной с требуемыми настройками:

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

чтобы результирующий HTML выглядел так:

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

и поэтому не имеет значения, задерживается выполнение внешнего скрипта или нет — он всегда будет появляться после встроенного скрипта, тем самым удовлетворяя зависимость между ними.

Наконец, если вы хотите убедиться, что никто не собирается изменять ваши настройки, объявите переменную как const и зафиксируйте ее значение с помощью Object.freeze :

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

который поддерживается всеми современными браузерами.

#2 Зависимости в WordPress, которые могут работать, а могут и не работать…

Управление зависимостями также может быть проблематичным в WordPress, особенно когда речь идет о встроенных скриптах WordPress. Позволь мне объяснить.

Представьте, например, что мы создаем небольшое расширение для Гутенберга, как мы объяснили здесь. В исходном коде нашего плагина, вероятно, будут такие операторы import :

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

Когда этот исходный код JS будет перенесен, Webpack (или инструмент, который вы используете) упакует все зависимости и ваш собственный исходный код в один файл JS. Это файл, который вы позже поставите в очередь из WordPress, чтобы все работало так, как вы ожидаете.

Если вы использовали @wordpress/scripts для создания такого файла, некоторые зависимости не будут включены в выходной файл, поскольку встроенный процесс предполагает, что пакеты будут доступны в глобальной области. Это означает, что предыдущий импорт будет преобразован во что-то похожее на это:

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

Чтобы убедиться, что вы не пропустите ни одной из зависимостей вашего скрипта, @wordpress/scripts не только транспилирует ваш JS-код, но также сгенерирует PHP-файл с зависимостями WordPress:

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

Довольно аккуратно, да? Итак, в чем проблема? Ну, эти пакеты WordPress находятся в постоянном развитии, и они довольно часто меняются, добавляя новые функции и улучшения. Поэтому, если вы разрабатываете свой плагин с использованием последней версии WordPress, вы можете непреднамеренно использовать функции или функции, доступные в этой последней версии (и, следовательно, все работает как надо), но не в «старых» версиях WordPress…

Как вы можете сказать?

Решение

Мой совет здесь очень прост: разрабатывайте свои плагины, используя последнюю версию WordPress, но тестируйте свои выпуски на более старых версиях. В частности, я бы посоветовал вам протестировать свой плагин, по крайней мере, с минимальной версией WordPress, которую должен поддерживать ваш плагин. Минимальную версию вы найдете в readme.txt вашего плагина:

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

Переключиться с одной версии WordPress на другую так же просто, как запустить следующую команду WP CLI:

 wp core update --version=5.4 --force

#3 Стрелочные функции сложнее, чем вы думаете

Наконец, позвольте мне поделиться одной из последних проблем, с которыми я столкнулся всего несколько дней назад и которая сводила меня с ума. Короче говоря, у нас был файл JavaScript, похожий на этот:

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

который инициализирует ваши Nelio Forms на внешнем интерфейсе. Сценарий довольно прост, не так ли? Он определяет анонимную функцию, которая вызывается, когда DOM готов. Эта функция использует вспомогательную (стрелочную) функцию initForm . Ну, как оказалось, такой простой пример может дать сбой! Но только при определенных обстоятельствах (например, если скрипт был «оптимизирован» плагином WPO с использованием атрибута defer ).

Вот как JS выполняет предыдущий скрипт:

  1. Анонимная функция внутри domReady определена
  2. domReady работает
  3. Если DOM еще не готов (а обычно это не так, когда загружается скрипт), domReady не запускает функцию обратного вызова. Вместо этого он просто отслеживает его, чтобы вызвать его позже.
  4. JavaScript продолжает синтаксический анализ файла и загружает функцию initForm
  5. Как только DOM готов, наконец вызывается функция обратного вызова.

А что, если к тому времени, когда мы дойдем до третьего шага, DOM будет готов и, следовательно, domReady анонимную функцию напрямую? Что ж, в этом случае скрипт вызовет ошибку undefined, потому что initForm по-прежнему undefined .

На самом деле, самое любопытное во всем этом то, что эти два решения эквивалентны:

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

линтер JavaScript выдаст ошибку только в первом, но не в последнем.

Решение

Возможны два решения: либо вы определяете вспомогательную функцию с помощью ключевого слова function и забываете о стрелочной функции, либо перемещаете оператор domReady в конец, после того как все вспомогательные функции определены:

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

Если вам интересно, почему первое решение работает, если оно, по-видимому, эквивалентно исходному, которое у нас было, то все дело в том, как работает подъем JavaScript. Короче говоря, в JavaScript вы можете использовать функцию (определяемую с помощью function ) перед ее определением, но вы не можете сделать то же самое с переменными и константами (и, следовательно, со стрелочными функциями).

В итоге

В JavaScript есть огромное количество вещей, которые могут пойти не так. К счастью, у всех есть решение, особенно если мы обращаем внимание на то, что делаем. Я надеюсь, что вы сегодня узнали что-то новое, и я верю, что благодаря ошибкам и ошибкам, которые я совершил в прошлом, вы сможете избежать их страданий на собственной плоти в будущем.

Избранное изображение Яна Штауффера на Unsplash.