Условная загрузка JavaScript и CSS для шорткодов в WordPress
Я выпустил плагин, который создает шорткод и требует загрузки JavaScript и CSS файлов на страницах, содержащих этот шорткод. Я мог бы просто загружать скрипты и стили на всех страницах, но это не лучшая практика. Мне нужно загружать файлы только на страницах, где вызывается шорткод. Я нашел два метода, но у обоих есть проблемы.
Метод 1 устанавливает флаг в true внутри функции обработки шорткода, а затем проверяет это значение внутри коллбэка wp_footer
. Если флаг true, он использует wp_print_scripts()
для загрузки JavaScript. Проблема в том, что это работает только для JavaScript, но не для CSS, так как CSS должен объявляться внутри <head>
, что можно сделать только на ранних хуках типа init
или wp_head
.
Метод 2 срабатывает раньше и "заглядывает вперед", чтобы проверить, есть ли шорткод в контенте текущей страницы. Этот метод мне нравится больше, но проблема в том, что он не обнаруживает случаи, когда шаблон вызывает do_shortcode()
.
Я склоняюсь ко второму методу с дополнительной проверкой шаблона на наличие шорткода. Но прежде чем реализовывать, хочу узнать, есть ли лучшие способы.
Обновление: Я интегрировал решение в свой плагин. Если хотите увидеть реализацию, можете скачать его или посмотреть код.
Обновление 2: Начиная с WordPress 3.3, можно вызывать wp_enqueue_script()
прямо внутри коллбэка шорткода, и JavaScript файл будет загружен в футере документа. Технически это возможно и для CSS файлов, но это плохая практика, так как вывод CSS вне тега <head>
нарушает спецификации W3C, может вызвать FOUC и заставить браузер перерисовывать страницу.

Основываясь на собственном опыте, я использовал комбинацию методов 1 и 2 - архитектуру и скрипты в подвале из первого метода и технику "опережающего просмотра" из второго.
Что касается "опережающего просмотра", я использую регулярные выражения вместо stripos
; личное предпочтение, быстрее и позволяет проверять на "некорректные" шорткоды:
preg_match( '#\[ *shortcode([^\]])*\]#i', $content );
Если вас беспокоит, что авторы могут вручную использовать do_shortcode
, я бы рекомендовал проинструктировать их вручную добавлять ваши предварительно зарегистрированные стили через очередь.
ОБНОВЛЕНИЕ: Для ленивых авторов, которые никогда не читают документацию, можно вывести сообщение, чтобы подчеркнуть ошибку их подхода ;)
function my_shortcode()
{
static $enqueued;
if ( ! isset( $enqueued ) )
$enqueued = wp_style_is( 'my_style', 'done' ); // кэшируем, чтобы не повторять при многократном вызове
// выполняем шорткод
$output = '';
if ( ! $enqueued )
// можно вывести сообщение только при первом появлении, обернув его в предыдущее условие
$output .= <<<HTML
<p>Внимание! Вы должны самостоятельно добавить таблицу стилей шорткода, если вызываете <code>do_shortcode()</code> напрямую!</p>
<p>Используйте <code>wp_enqueue_style( 'my_style' );</code> перед вызовом <code>get_header()</code> в вашем шаблоне.</p>
HTML;
return $output;
}

Хорошее замечание. В идеале мне хотелось бы, чтобы это работало без необходимости дополнительных действий с их стороны — потому что в половине случаев они, скорее всего, не прочитают FAQ заранее и просто решат, что что-то сломалось. Но, возможно, мне придется пойти этим путем. Я могу регистрировать скрипты на каждой странице, но подключать их только при обнаружении шорткода. Тогда пользователи смогут подключиться к хуку init и вызывать функции enqueue в конкретных шаблонах, где это необходимо, если к тому моменту выполнение еще не зашло слишком далеко. Кроме того, в WP уже есть встроенная функция get_shortcode_regex().

Если пользователи умеют работать с do_shortcode()
, разве не разумно предположить, что они так же способны следовать инструкциям по подключению стилей для шорткода?

Верно, но regex будет получен для всех шорткодов, а не только для вашего ;) "Я могу регистрировать скрипты на каждой странице" — вероятно, это тоже более правильный метод! Заметьте, им не обязательно подключаться к хуку init
, достаточно сделать это до wp_head
. Для ленивых разработчиков: проверьте wp_style_is( 'my_style_handle', 'done' )
внутри вашего шорткода. Если вернется false, выведите видимую ошибку с инструкциями, что делать.

@Chip - Я не беспокоюсь, что они не способны следовать инструкциям, просто они могут не знать, что должны это делать, поскольку в 99% случаев не требуется делать ничего дополнительного.

@Ian - Я думал, что добавление do_shortcode()
в шаблон уже является "деланием чего-то дополнительного" — и пользователи, которые сделают это что-то дополнительное, либо уже будут знать о необходимости подключить стили, либо будут более готовы/склонны следовать особым инструкциям.

@TheDeadMedic - Я интегрировал это со своим кодом и обновил вопрос ссылками на него, на случай если кто-то захочет увидеть реализацию. Ещё раз спасибо за помощь :)

@Ian Отлично! Спасибо, что вернулись с обновлением, как для нас, так и для гуглеров, которые наткнутся на это позже ;)

Хорошо, но я должен не согласиться, что preg_match
быстрее, чем strpos
http://lzone.de/articles/php-string-search.htm

@Bainternet Не обязательно - preg_match
против stripos
(без учета регистра) - в любом случае, мы говорим о незначительных временных затратах. Оглядываясь назад, думаю, мне следовало избегать термина "быстрее" - на самом деле я хотел сказать, что это более эффективно для данной задачи (в отличие от множественных вызовов stripos
для обработки различных форматов шорткодов).

Я опоздал с ответом на этот вопрос, но так как Ян начал это обсуждение сегодня в списке рассылки wp-hackers, это заставило меня задуматься о том, что стоит ответить, особенно учитывая, что я планировал добавить такую функцию в некоторые плагины, над которыми работаю.
Один из подходов, который стоит рассмотреть, — это проверка при первой загрузке страницы, используется ли шорткод, а затем сохранение статуса использования шорткода в мета-ключе записи. Вот как это можно сделать:
Пошаговое руководство
- Установите флаг
$shortcode_used
в значение'no'
. - В самой функции шорткода установите флаг
$shortcode_used
в значение'yes'
. - Установите хук для
'the_content'
с приоритетом12
(после того, как WordPress обработает шорткоды) и проверьте метаданные записи на наличие пустого значения''
с ключом"_has_{$shortcode_name}_shortcode"
. (Значение''
возвращается, если мета-ключ не существует для ID записи.) - Используйте хук
'save_post'
для удаления метаданных записи, сбрасывая флаг на случай, если пользователь изменит использование шорткода. - Также в хуке
'save_post'
используйтеwp_remote_request()
для отправки неблокирующего HTTP GET запроса на постоянную ссылку записи, чтобы активировать первую загрузку страницы и установку флага. - Наконец, установите хук
'wp_print_styles'
и проверьте метаданные записи на значения'yes'
,'no'
или''
с ключом"_has_{$shortcode_name}_shortcode"
. Если значение'no'
— не загружайте внешние ресурсы. Если значение'yes'
или''
— загружайте их.
И это должно сработать. Я написал и протестировал пример плагина, чтобы показать, как это работает.
Пример кода плагина
Плагин активируется при использовании шорткода [trigger-css]
, который устанавливает стиль элементов <h2>
на странице как белый текст на красном фоне, чтобы было легко увидеть его работу. Он предполагает наличие подкаталога css
с файлом style.css
, содержащим следующий CSS:
/*
* Filename: css/style.css
*/
h2 {
color: white;
background: red;
}
А ниже приведен код работающего плагина:
<?php
/**
* Plugin Name: CSS on Shortcode
* Description: Shows how to conditionally load a shortcode
* Author: Mike Schinkel <mike@newclarity.net>
*/
class CSS_On_Shortcode {
/**
* @var CSS_On_Shortcode
*/
private static $_this;
/**
* @var string 'yes'/'no' vs. true/false as get_post_meta() returns '' for false and not found.
*/
var $shortcode_used = 'no';
/**
* @var string
*/
var $HAS_SHORTCODE_KEY = '_has_trigger-css_shortcode';
/**
*
*/
function __construct() {
self::$_this = $this;
add_shortcode( 'trigger-css', array( $this, 'do_shortcode' ) );
add_filter( 'the_content', array( $this, 'the_content' ), 12 ); // AFTER WordPress' do_shortcode()
add_action( 'save_post', array( $this, 'save_post' ) );
add_action( 'wp_print_styles', array( $this, 'wp_print_styles' ) );
}
/**
* @return CSS_On_Shortcode
*/
function this() {
return self::$_this;
}
/**
* @param array $arguments
* @param string $content
* @return string
*/
function do_shortcode( $arguments, $content ) {
/**
* If this shortcode is being used, capture the value so we can save to post_meta in the 'the_content' filter.
*/
$this->shortcode_used = 'yes';
return '<h2>THIS POST WILL ADD CSS TO MAKE H2 TAGS WHITE ON RED</h2>';
}
/**
* Delete the 'has_shortcode' meta value so that it can be regenerated
* on first page load in case shortcode use has changed.
*
* @param int $post_id
*/
function save_post( $post_id ) {
delete_post_meta( $post_id, $this->HAS_SHORTCODE_KEY );
/**
* Now load the post asynchronously via HTTP to pre-set the meta value for $this->HAS_SHORTCODE_KEY.
*/
wp_remote_request( get_permalink( $post_id ), array( 'blocking' => false ) );
}
/**
* @param array $args
*
* @return array
*/
function wp_print_styles( $args ) {
global $post;
if ( 'no' != get_post_meta( $post->ID, $this->HAS_SHORTCODE_KEY, true ) ) {
/**
* Only bypass if set to 'no' as '' is unknown.
*/
wp_enqueue_style( 'css-on-shortcode', plugins_url( 'css/style.css', __FILE__ ) );
}
}
/**
* @param string $content
* @return string
*/
function the_content( $content ) {
global $post;
if ( '' === get_post_meta( $post->ID, $this->HAS_SHORTCODE_KEY, true ) ) {
/**
* This is the first time the shortcode has ever been seen for this post.
* Save a post_meta key so that next time we'll know this post uses this shortcode
*/
update_post_meta( $post->ID, $this->HAS_SHORTCODE_KEY, $this->shortcode_used );
}
/**
* Remove this filter now. We don't need it for this post again.
*/
remove_filter( 'the_content', array( $this, 'the_content' ), 12 );
return $content;
}
}
new CSS_On_Shortcode();
Примеры скриншотов
Вот серия скриншотов:
Базовый редактор записи, без контента
Отображение записи без контента
Базовый редактор записи с шорткодом [trigger-css]
Отображение записи с шорткодом [trigger-css]
Не уверен на 100%
Я считаю, что вышеописанное должно работать практически во всех случаях, но так как я только что написал этот код, я не могу быть уверен на 100%. Если вы найдете ситуации, когда он не работает, я был бы рад узнать об этом, чтобы исправить код в плагинах, куда я это добавил. Заранее спасибо.

Значит, пять плагинов, использующих ваш подход, будут вызывать пять удалённых запросов каждый раз при сохранении записи? Я бы предпочёл использовать регулярное выражение для post_content
. А как насчёт шорткодов в виджетах?

@toscho На самом деле загрузка поста по триггеру опциональна; она нужна только для того, чтобы пользователь не увидел первый загруженный страницу с внешними ресурсами. Кроме того, это неблокирующий вызов, так что теоретически вы его не заметите. В нашем коде мы делаем это в базовом классе, чтобы он мог обрабатывать вызов только один раз. Мы могли бы использовать хук 'pre_http_request'
и отключить множественные вызовы к одному и тому же URL, пока активен хук 'save_post'
, но я бы хотел подождать, пока действительно увижу в этом необходимость, согласны? Что касается виджетов, это можно доработать, но я пока не рассматривал этот кейс.

@toscho - Кроме того, вы не можете быть уверены, что шорткод останется, так как другой хук может его очистить. Единственный способ убедиться — это если функция шорткода действительно сработает. Так что подход с регулярными выражениями не на 100% надёжен.

Я знаю. Не существует абсолютно надежного способа вставки CSS для шорткодов (кроме использования <style>
).

Я работаю с реализацией, основанной на этом решении, и просто хотел отметить, что этот метод не будет подключаться для предварительного просмотра записи/страницы.

Мое решение проблемы с предпросмотром: https://gist.github.com/mor7ifer/e50a9864a372a05b4a3b

Поиск в Google подсказал мне потенциальный ответ. Я говорю "потенциальный", потому что он выглядит рабочим, но я не на 100% уверен, что это лучший способ:
add_action( 'wp_print_styles', 'yourplugin_include_css' );
function yourplugin_include_css() {
// Проверяем, есть ли шорткод в контенте страницы или записи
global $post;
// Убрал закрывающую скобку ' ] '... чтобы можно было передавать аргументы.
if ( strstr( $post->post_content, '[yourshortcode ' ) ) {
echo $csslink;
}
}
Этот код должен проверять, использует ли текущая запись шорткод, и соответственно добавлять таблицу стилей в элемент <head>
. Но я не думаю, что он будет работать на страницах архива (где выводится несколько записей в цикле)... Также этот код из двухлетней записи в блоге, так что я не уверен, будет ли он работать с WP 3.1.X.

Это не будет работать, если шорткод имеет аргументы. Если вы действительно хотите пойти этим путем, что медленно, используйте функцию WordPress get_shortcode_regex()
для поиска.

Как я и сказал, это "потенциальный" ответ, который я еще не тестировал ... :-)

Это, по сути, то же самое, что и метод 2, но он все равно не проверяет шаблон на вызовы do_shortcode().

Зачем это нужно? Если вы вручную вызываете do_shortcode()
в шаблоне, вы уже знаете, что будете выполнять шорткод

Я не тот, кто вызывает шорткод, это делает пользователь. Это плагин для распространения, а не приватный.

Хорошо, поэтому вам нужна валидная разметка :) Вы говорите, что ваш плагин инструктирует пользователя редактировать файлы шаблонов и вызывать эту функцию? Тогда, если вы не создадите собственный PHP-парсер, нет способа узнать, где и когда вызывается эта функция...

Мне всегда нужна валидная разметка, независимо от обстоятельств... Плагин не говорит пользователю вызывать функцию, но это всегда вариант. Пользователь всегда может вызвать любой шорткод, либо введя его в область содержимого записи/страницы, либо просто вызвав do_shortcode() в шаблоне, поэтому обе ситуации должны обрабатываться. И я не думаю, что мне нужно писать собственный PHP-парсер, мне просто нужно разобрать файл шаблона, назначенный текущей странице (если он есть), чтобы проверить, содержится ли в нем шорткод.

Предполагая, что ты сможешь узнать, какой файл шаблона используется текущей страницей (в чем я сомневаюсь), как ты собираешься его разбирать без парсера? Регулярные выражения? А если строка с кодом закомментирована? Существуют десятки способов комментирования PHP-кода

Назначенный шаблон просто хранится в таблице post_meta, что сложного в его определении? Можно подключиться к хуку the_posts, чтобы получить доступ к текущему $post до вывода чего-либо. Под "PHP-парсером" я предполагал весь движок, а не просто функцию strpos или регулярное выражение. Закомментированные строки — это крайний случай, о котором я особо не беспокоюсь.

вы путаете шаблоны страниц (как в случае с типом записи page) с файлами темы, используемыми в качестве шаблонов

А, вы говорите о стандартных шаблонах (например, single.php, home.php) в отличие от кастомного шаблона, созданного специально для страницы (например, map.php), верно? Это хорошее замечание. Держу пари, что есть способ определить, какой именно используется, но я не помню его сходу.

Используя комбинацию ответа TheDeadMedic и документации по get_shortcode_regex() (которая, кстати, не нашла мои шорткоды), я создал простую функцию для подключения скриптов для нескольких шорткодов. Поскольку wp_enqueue_script() в шорткодах добавляет скрипты только в подвал, эта функция может быть полезна, так как позволяет обрабатывать скрипты как для заголовка, так и для подвала.
function add_shortcode_scripts() {
global $wp_query;
$posts = $wp_query->posts;
$scripts = array(
array(
'handle' => 'map',
'src' => 'http://maps.googleapis.com/maps/api/js?sensor=false',
'deps' => '',
'ver' => '3.0',
'footer' => false
),
array(
'handle' => 'contact-form',
'src' => get_template_directory_uri() . '/library/js/jquery.validate.min.js',
'deps' => array( 'jquery' ),
'ver' => '1.11.1',
'footer' => true
)
);
foreach ( $posts as $post ) {
foreach ( $scripts as $script ) {
if ( preg_match( '#\[ *' . $script['handle'] . '([^\]])*\]#i', $post->post_content ) ) {
// подключаем css и/или js
if ( wp_script_is( $script['handle'], 'registered' ) ) {
return;
} else {
wp_register_script( $script['handle'], $script['src'], $script['deps'], $script['ver'], $script['footer'] );
wp_enqueue_script( $script['handle'] );
}
}
}
}
}
add_action( 'wp', 'add_shortcode_scripts' );

Наконец я нашел решение для условной загрузки CSS, которое работает для моего плагина www.mapsmarker.com, и хочу поделиться им с вами. Оно проверяет, используется ли мой шорткод в текущем файле шаблона и в header.php/footer.php, и если да, подключает необходимую таблицу стилей в заголовке:
function prefix_template_check_shortcode( $template ) {
$searchterm = '[mapsmarker';
$files = array( $template, get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'header.php', get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'footer.php' );
foreach( $files as $file ) {
if( file_exists($file) ) {
$contents = file_get_contents($file);
if( strpos( $contents, $searchterm ) ) {
wp_enqueue_style('
leafletmapsmarker', LEAFLET_PLUGIN_URL . 'leaflet-dist/leaflet.css');
break;
}
}
}
return $template;
}
add_action('template_include','prefix_template_check_shortcode' );

Небольшое отступление, но разве это не предполагает, что люди используют header.php и footer.php? А как насчёт методов обёртывания тем, таких как описанные в http://scribu.net/wordpress/theme-wrappers.html? Или тем вроде Roots, где шаблонные части хранятся в других местах?

Для своего плагина я обнаружил, что иногда пользователи используют конструктор тем, в котором шорткоды хранятся в метаданных записи. Вот что я использую для определения, присутствует ли шорткод моего плагина в текущей записи или её метаданных:
function abcd_load_my_shorcode_resources() {
global $post, $wpdb;
// определяем, содержит ли страница шорткод "my_shortcode"
$shortcode_found = false;
if ( has_shortcode($post->post_content, 'my_shortcode') ) {
$shortcode_found = true;
} else if ( isset($post->ID) ) {
$result = $wpdb->get_var( $wpdb->prepare(
"SELECT count(*) FROM $wpdb->postmeta " .
"WHERE post_id = %d and meta_value LIKE '%%my_shortcode%%'", $post->ID ) );
$shortcode_found = ! empty( $result );
}
if ( $shortcode_found ) {
wp_enqueue_script(...);
wp_enqueue_style(...);
}
}
add_action( 'wp_enqueue_scripts', 'abcd_load_my_shorcode_resources' );

потому что CSS должен объявляться внутри
<head>
Для CSS файлов вы можете загружать их прямо в выводе шорткода:
<style type="text/css">
@import "путь/к/вашему.css";
</style>
Установите константу или что-то подобное после этого, например MY_CSS_LOADED
(включать CSS только если константа не установлена).
Оба ваших метода медленнее, чем этот подход.
Для JS файлов вы можете сделать то же самое, если загружаемый скрипт уникален и не имеет внешних зависимостей. Если это не так, загружайте его в подвале (футере), но используйте константу для определения необходимости загрузки...

Загрузка CSS вне элемента <head>
не является правильной разметкой. Действительно, валидация — это всего лишь рекомендация, но если мы пытаемся следовать этим рекомендациям, то загрузка таблицы стилей внутри вывода шорткода — плохая идея.

Встроенные блоки CSS являются валидной разметкой, даже в XHTML, насколько я помню. Нет причин не использовать их, когда у вас нет других приемлемых альтернатив.

Согласно инструменту валидации W3C: <style type="text/css">
Указанный выше элемент был найден в контексте, где он не разрешён. Это может означать, что вы неправильно вложили элементы — например, элемент "style" в разделе "body" вместо "head"
. Таким образом, встроенные стили (<element style="..."></element>
) являются валидными, но встроенные элементы <style>
— нет.

Да, я знаю, что такой вариант возможен, но это считается плохой практикой. Это может вызвать FOUC (мигание нестилизованного контента), заставить браузер перерисовывать страницу и не пройти валидацию W3C. Также это не будет работать с wp_enqueue_style(), что лишит других разработчиков тем/плагинов возможности легко подменить свои собственные скрипты/стили.

@EAMann. Верно, моя ошибка, я думал что это пройдет валидацию. Хотя, впрочем, валидация W3C ничего не значит...

@One Trick Pony Эм-м... Для меня это кое-что значит. И для всех остальных разработчиков, которых я уважаю, тоже. Я не следую этому слепо, но редко когда считаю приемлемым решение, которое нарушает стандарты.

Если вы не следуете этому слепо, то можете привести причины, почему эта практика плохая?

Это считается плохой практикой, но именно так работает шорткод галереи в ядре WordPress: выводит CSS блок прямо в коде. Это немного лучше, чем использование атрибута style, и работает. Мой плагин со шорткодом просто выводит CSS в head на каждой странице. Его не так много :)

@mfields, если я так сделаю, то другие разработчики тем и плагинов не смогут заменить файлы своими. Не важно, делает ли это ядро, это всё равно плохая практика.

сделайте это фильтруемым, и любой другой плагин или тема смогут поступить с этим как захотят. Если они настроят фильтр на возврат пустой строки - ничего выводиться не будет.

Ты не привёл никаких объективных причин против такой практики. В любом случае это не важно; я вижу только два варианта: всегда загружать CSS/скрипты (оптимизировав их по размеру) или условные инлайн-стили.

@mfields, это хорошее замечание, но я предпочитаю подход с enqueue_style.
