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 выполняет предыдущий скрипт:
- Анонимная функция внутри
domReady
определена -
domReady
работает - Если DOM еще не готов (а обычно это не так, когда загружается скрипт),
domReady
не запускает функцию обратного вызова. Вместо этого он просто отслеживает его, чтобы вызвать его позже. - JavaScript продолжает синтаксический анализ файла и загружает функцию
initForm
- Как только 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.