Caricamento Condizionale di JavaScript/CSS per Shortcode
Ho rilasciato un plugin che crea uno shortcode e richiede il caricamento di un file JavaScript e un file CSS su qualsiasi pagina che contenga quello shortcode. Potrei semplicemente far caricare lo script/stile su tutte le pagine, ma non è la migliore pratica. Voglio caricare i file solo sulle pagine che chiamano lo shortcode. Ho trovato due metodi per farlo, ma entrambi presentano problemi.
Metodo 1 imposta un flag a true all'interno della funzione handler dello shortcode, e poi controlla quel valore all'interno di un callback wp_footer
. Se è true, utilizza wp_print_scripts()
per caricare il JavaScript. Il problema con questo metodo è che funziona solo per JavaScript e non per CSS, perché il CSS dovrebbe essere dichiarato all'interno di <head>
, cosa che puoi fare solo durante un hook precoce come init
o wp_head
.
Metodo 2 viene eseguito presto e "sbircia in avanti" per vedere se lo shortcode esiste nel contenuto della pagina corrente. Questo metodo mi piace molto più del primo, ma il problema è che non rileva se il template chiama do_shortcode()
.
Quindi, sono propenso a usare il secondo metodo e poi cercare di rilevare se è assegnato un template e, in tal caso, analizzarlo per lo shortcode. Prima di farlo, però, volevo verificare se qualcuno conosce un metodo migliore.
Aggiornamento: Ho integrato la soluzione nel mio plugin. Se qualcuno è curioso di vederlo sviluppato in un ambiente live, puoi scaricarlo o visualizzarlo.
Aggiornamento 2: A partire da WordPress 3.3, è ora possibile chiamare wp_enqueue_script()
direttamente all'interno di un callback di shortcode, e il file JavaScript verrà chiamato nel footer del documento. Tecnicamente è possibile anche per i file CSS, ma dovrebbe essere considerata una cattiva pratica perché l'output del CSS al di fuori del tag <head>
viola le specifiche W3C, può causare FOUC (Flash of Unstyled Content) e può forzare il browser a ri-renderizzare la pagina.

Basandomi sulla mia esperienza personale, ho utilizzato una combinazione dei metodi 1 e 2 - l'architettura e gli script del footer del primo, e la tecnica 'look-ahead' del secondo.
Per quanto riguarda il 'look-ahead', però, utilizzo le regex al posto di stripos
; preferenza personale, più veloce e può verificare la presenza di shortcode 'malformati';
preg_match( '#\[ *shortcode([^\]])*\]#i', $content );
Se sei preoccupato che gli autori utilizzino manualmente do_shortcode
, opterei per istruirli a utilizzare una chiamata ad un'azione accodare manualmente il tuo stile pre-registrato.
AGGIORNAMENTO: Per l'autore pigro che non legge mai il manuale, mostra un messaggio per evidenziare l'errore commesso ;)
function my_shortcode()
{
static $enqueued;
if ( ! isset( $enqueued ) )
$enqueued = wp_style_is( 'my_style', 'done' ); // memorizza in cache così non si ripete se chiamato più volte
// esegui lo shortcode
$output = '';
if ( ! $enqueued )
// puoi mostrare il messaggio solo alla prima occorrenza racchiudendolo nel precedente if
$output .= <<<HTML
<p>Attenzione! Devi accodare manualmente il foglio di stile dello shortcode se chiami direttamente <code>do_shortcode()</code>!</p>
<p>Usa <code>wp_enqueue_style( 'my_style' );</code> prima della tua chiamata a <code>get_header()</code> nel tuo template.</p>
HTML;
return $output;
}

È un punto valido. Idealmente, vorrei che funzionasse senza che debbano fare nulla di extra – perché la metà delle volte probabilmente non leggeranno prima le FAQ, quindi penseranno semplicemente che sia rotto – ma potrei finire per farlo. Potrei registrare gli script su ogni pagina, ma caricarli solo se rilevo uno shortcode. Quindi, gli utenti potrebbero agganciarsi a init e chiamare le funzioni di enqueue in template specifici dove necessario, assumendo che non sia già troppo tardi nell'esecuzione a quel punto. Inoltre, WP ha già integrato get_shortcode_regex().

Se gli utenti sono capaci di implementare do_shortcode()
, non è ragionevole assumere che siano altrettanto capaci di seguire le istruzioni per il caricamento dello stile dello shortcode?

Vero, ma otterrà la regex per tutti gli shortcode, non solo i tuoi ;) "Potrei registrare gli script su ogni pagina" Probabilmente anche il metodo migliore! Nota che non devono agganciarsi a init
, basta ovunque prima di wp_head
. Per lo sviluppatore pigro, controlla wp_style_is( 'my_style_handle', 'done' )
dentro il tuo shortcode. Se è falso, stampa un errore visibile che li istruisca su cosa fare.

@Chip - Non sono preoccupato che non siano in grado di seguire le istruzioni, solo che non sappiano che dovrebbero farlo, dato che il 99% delle volte non c'è bisogno di fare nulla di extra.

@Ian il mio pensiero era che aggiungere do_shortcode()
al template è già "fare qualcosa di extra" - e gli utenti che farebbero quel qualcosa in più o saprebbero già della necessità di accodare lo stile, oppure sarebbero più disposti/propensi a seguire istruzioni speciali.

@TheDeadMedic - L'ho integrato con il mio codice e ho aggiornato la domanda con i link nel caso qualcuno volesse vederlo sviluppato. Grazie ancora per l'aiuto :)

@Ian Bello! Grazie per essere tornato con un aggiornamento, sia per noi che per i googler che ci atterreranno in seguito ;)

Bello ma non sono d'accordo sul fatto che preg_match
sia più veloce di strpos
http://lzone.de/articles/php-string-search.htm

@Bainternet Non necessariamente - preg_match
vs stripos
(case-insensitive) - in ogni caso stiamo parlando di tempistiche irrisorie. Col senno di poi, penso che avrei dovuto evitare il termine "più veloce" - in realtà intendevo che era più efficiente per il compito in questione (invece di usare più stripos
per gestire vari formati di shortcode).

Rispondo in ritardo a questa domanda ma dato che Ian ha avviato questa discussione sulla mailing list wp-hackers oggi, mi ha fatto pensare che valesse la pena rispondere, soprattutto considerando che stavo pianificando di aggiungere una funzionalità simile ad alcuni plugin su cui sto lavorando.
Un approccio da considerare è verificare al primo caricamento della pagina se lo shortcode è effettivamente utilizzato e poi salvare lo stato di utilizzo dello shortcode in una meta chiave del post. Ecco come fare:
Procedura Passo-Passo
- Imposta un flag
$shortcode_used
a'no'
. - Nella funzione dello shortcode stesso, imposta il flag
$shortcode_used
a'yes'
. - Imposta un hook
'the_content'
con priorità12
(dopo che WordPress ha processato gli shortcode) e controlla i meta del post per un valore vuoto''
usando la chiave"_has_{$shortcode_name}_shortcode"
. (Un valore''
viene restituito quando una meta chiave non esiste per l'ID del post.) - Usa un hook
'save_post'
per eliminare la meta del post, cancellando il flag persistente per quel post nel caso l'utente modifichi l'utilizzo dello shortcode. - Anche nell'hook
'save_post'
usawp_remote_request()
per inviare una richiesta HTTP GET non bloccante al permalink del post stesso, per attivare il primo caricamento della pagina e l'impostazione del flag persistente. - Infine imposta un hook
'wp_print_styles'
e controlla i meta del post per un valore'yes'
,'no'
o''
usando la chiave"_has_{$shortcode_name}_shortcode"
. Se il valore è'no'
, non servire il file esterno. Se il valore è'yes'
o''
, procedi a servire il file esterno.
E questo dovrebbe essere tutto. Ho scritto e testato un plugin di esempio per mostrare come funziona.
Codice del Plugin di Esempio
Il plugin si attiva con uno shortcode [trigger-css]
che imposta gli elementi <h2>
della pagina con testo bianco su sfondo rosso, così puoi vedere facilmente che funziona. Assume una sottodirectory css
contenente un file style.css
con questo CSS:
/*
* Nome file: css/style.css
*/
h2 {
color: white;
background: red;
}
Ecco il codice funzionante del plugin:
<?php
/**
* Plugin Name: CSS on Shortcode
* Description: Mostra come caricare condizionalmente uno shortcode
* Author: Mike Schinkel <mike@newclarity.net>
*/
class CSS_On_Shortcode {
/**
* @var CSS_On_Shortcode
*/
private static $_this;
/**
* @var string 'yes'/'no' invece di true/false poiché get_post_meta() restituisce '' per false o chiave non trovata.
*/
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 ); // DOPO il do_shortcode() di WordPress
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 ) {
/**
* Se questo shortcode è utilizzato, cattura il valore così possiamo salvarlo nei post_meta nel filtro 'the_content'.
*/
$this->shortcode_used = 'yes';
return '<h2>QUESTO POST AGGIUNGERÀ CSS PER RENDERE I TAG H2 BIANCO SU ROSSO</h2>';
}
/**
* Elimina il meta 'has_shortcode' così che possa essere rigenerato
* al primo caricamento della pagina nel caso l'uso dello shortcode sia cambiato.
*
* @param int $post_id
*/
function save_post( $post_id ) {
delete_post_meta( $post_id, $this->HAS_SHORTCODE_KEY );
/**
* Ora carica il post in modo asincrono via HTTP per preimpostare il meta value per $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 ) ) {
/**
* Salta solo se impostato su 'no' poiché '' è sconosciuto.
*/
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 ) ) {
/**
* Questa è la prima volta che questo shortcode viene visto per questo post.
* Salva una meta chiave così che la prossima volta sapremo che questo post usa questo shortcode
*/
update_post_meta( $post->ID, $this->HAS_SHORTCODE_KEY, $this->shortcode_used );
}
/**
* Rimuovi questo filtro ora. Non ci serve più per questo post.
*/
remove_filter( 'the_content', array( $this, 'the_content' ), 12 );
return $content;
}
}
new CSS_On_Shortcode();
Screenshot di Esempio
Ecco una serie di screenshot
Editor di Post Base, Senza Contenuto
Visualizzazione Post, Senza Contenuto
Editor di Post Base con Shortcode [trigger-css]
Visualizzazione Post con Shortcode [trigger-css]
Non Sono Sicuro al 100%
Credo che quanto sopra dovrebbe funzionare in quasi tutti i casi, ma avendo appena scritto questo codice non posso esserne certo al 100%. Se trovi situazioni in cui non funziona, mi piacerebbe saperlo così posso correggere il codice in alcuni plugin a cui ho appena aggiunto questa funzionalità. Grazie in anticipo.

Quindi cinque plugin che utilizzano il tuo approccio genereranno cinque richieste remote ogni volta che un articolo viene salvato? Preferirei usare una regex su post_content
. E per quanto riguarda gli shortcode nei widget?

@toscho In realtà il caricamento del post è opzionale; serve solo per assicurarsi che un utente non debba vedere il primo caricamento della pagina con gli elementi esterni. È anche una chiamata non bloccante, quindi in teoria non dovresti accorgertene. Nel nostro codice lo facciamo in una classe base in modo che la classe base possa gestire il fatto che avvenga solo una volta. Potremmo agganciarci all'hook 'pre_http_request'
e disabilitare chiamate multiple allo stesso URL mentre l'hook 'save_post'
è attivo, ma vorrei aspettare finché non vedremo una reale necessità, no? Per quanto riguarda i Widget, potrebbe essere migliorato per gestirli ma non è un caso d'uso che ho ancora esaminato.

@toscho - Inoltre non puoi essere sicuro che lo shortcode sia presente poiché un altro hook potrebbe rimuoverlo. L'unico modo per esserne certi è se la funzione dello shortcode viene effettivamente eseguita. Quindi l'approccio con regex non è affidabile al 100%.

Lo so. Non esiste un modo sicuro al 100% per iniettare CSS per gli shortcode (tranne usando <style>
).

Sto lavorando con un'implementazione basata su questa soluzione e volevo solo far notare che questo metodo non verrà accodato per l'anteprima di un articolo/pagina.

La mia soluzione per il problema dell'anteprima: https://gist.github.com/mor7ifer/e50a9864a372a05b4a3b

Cercando su Google ho trovato una potenziale risposta. Dico "potenziale" perché sembra valida, dovrebbe funzionare, ma non sono sicuro al 100% che sia il modo migliore per farlo:
add_action( 'wp_print_styles', 'yourplugin_include_css' );
function yourplugin_include_css() {
// Verifica se lo shortcode è presente nel contenuto della pagina o del post
global $post;
// Ho rimosso la chiusura ' ] '... così può accettare argomenti.
if ( strstr( $post->post_content, '[yourshortcode ' ) ) {
echo $csslink;
}
}
Questo codice dovrebbe verificare se il post corrente utilizza uno shortcode e aggiungere un foglio di stile all'elemento <head>
in modo appropriato. Tuttavia, non credo che funzioni per una pagina indice (ad esempio, più post nel loop)... Inoltre, proviene da un post di blog di 2 anni fa, quindi non sono nemmeno sicuro che funzioni con WP 3.1.X.

Questo non funzionerà se lo shortcode ha argomenti. Se vuoi davvero procedere in questo modo, che è lento, usa get_shortcode_regex()
di WP per la ricerca.

Questo è fondamentalmente lo stesso del metodo 2, ma non controlla ancora il template per le chiamate a do_shortcode().

Perché dovrebbe farlo? Se chiami do_shortcode()
manualmente nel template, sai già che eseguirai lo shortcode

Non sono io a chiamare lo shortcode, ma l'utente. Questo è per un plugin distribuito, non privato.

ok, quindi è per questo che vuoi un markup valido :) Stai dicendo che il tuo plugin istruisce l'utente a modificare i file template e chiamare quella funzione? Allora, a meno che non crei il tuo parser PHP, non c'è modo di sapere dove e quando quella funzione viene chiamata...

Voglio sempre un markup valido, indipendentemente dalle circostanze... Il plugin non dice all'utente di chiamare la funzione, ma è sempre un'opzione. Un utente può sempre chiamare qualsiasi shortcode digitandolo nell'area di contenuto di un post/pagina o semplicemente chiamando do_shortcode() in un template, quindi entrambe le situazioni devono essere gestite. E non credo di dover scrivere il mio parser PHP, ho solo bisogno di analizzare il file template assegnato alla pagina corrente (se ce n'è uno) per vedere se lo shortcode è al suo interno.

Supponendo che tu possa scoprire quale file template viene utilizzato dalla pagina corrente (cosa di cui dubito), come lo analizzeresti senza un parser? regex? E se la riga con il codice è commentata? Ci sono dozzine di modi per commentare il codice PHP

Il template assegnato è semplicemente memorizzato nella tabella post_meta, cosa c'è di così difficile da scoprire? Puoi agganciarti a the_posts per ottenere accesso al $post corrente prima che venga emesso qualsiasi output. Con 'parser PHP' intendevo l'intero motore, non solo una funzione strpos o regex. Le righe commentate sono un caso limite di cui non sono veramente preoccupato.

stai confondendo i template delle pagine (come nel post-type page) con i file del tema utilizzati come template

Ah, stai parlando dei template predefiniti (ad esempio single.php, home.php) in contrapposizione a un template personalizzato creato specificamente per una pagina (ad esempio map.php), giusto? È un buon punto. Scommetto che esiste un modo per capire quale viene utilizzato, ma al momento non lo ricordo.

Utilizzando una combinazione della risposta di TheDeadMedic e della documentazione di get_shortcode_regex()
(documentazione) (che in realtà non trovava i miei shortcode), ho creato una semplice funzione per caricare gli script per più shortcode. Dato che wp_enqueue_script()
negli shortcode aggiunge solo nel footer, questo può essere utile perché gestisce sia gli script per l'header che per il footer.
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 ) ) {
// carica css e/o 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' );

Finalmente ho trovato anche una soluzione per il caricamento condizionale del CSS che funziona per il mio plugin www.mapsmarker.com e vorrei condividerla con voi. Controlla se il mio shortcode è utilizzato all'interno del file template corrente e in header.php/footer.php e, in caso affermativo, carica il foglio di stile necessario nell'header:
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' );

un po' fuori tema, ma questo non presuppone che le persone stiano usando header.php e footer.php? E i metodi di wrapping dei temi come quelli descritti da http://scribu.net/wordpress/theme-wrappers.html? O temi come Roots che mantengono le loro parti di template altrove?

Per il mio plugin ho scoperto che a volte gli utenti hanno un theme builder che memorizza gli shortcode nei metadati del post. Ecco cosa sto usando per rilevare se lo shortcode del mio plugin è presente nell'attuale post o nei metadati del post:
function abcd_load_my_shorcode_resources() {
global $post, $wpdb;
// determina se questa pagina contiene lo shortcode "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' );

perché il CSS dovrebbe essere dichiarato all'interno di
<head>
Per i file CSS puoi caricarli all'interno dell'output del tuo shortcode:
<style type="text/css">
@import "percorso/del/tuo.css";
</style>
Imposta una costante o qualcosa di simile dopo questo, come MY_CSS_LOADED
(includi il CSS solo se la costante non è impostata).
Entrambi i tuoi metodi sono più lenti rispetto a questo approccio.
Per i file JS puoi fare lo stesso se lo script che stai caricando è unico e non ha dipendenze esterne. Se non è questo il caso, caricalo nel footer, ma usa la costante per determinare se deve essere caricato o meno...

Caricare CSS al di fuori dell'elemento <head>
non è un markup corretto. Vero, la validazione è solo una linea guida, ma se stiamo cercando di seguirla, rende l'idea di caricare il foglio di stile all'interno dell'output dello shortcode una cattiva idea.

I blocchi CSS inline sono markup validi, anche in XHTML da quello che ricordo. Non c'è motivo di non usarli quando non hai altre alternative accettabili.

Secondo lo strumento di validazione W3C: <style type="text/css">
L'elemento menzionato sopra è stato trovato in un contesto dove non è consentito. Questo potrebbe significare che hai annidato gli elementi in modo errato -- come un elemento "style" nella sezione "body" invece che all'interno di "head"
. Quindi gli stili inline (<element style="..."></element>
) sono validi, ma gli elementi <style>
inline non lo sono.

Sì, so che è una possibilità, ma è considerata una cattiva pratica. Potrebbe causare FOUC, costringere il browser a ri-renderizzare la pagina e far fallire il validatore W3C. Inoltre non funzionerebbe con wp_enqueue_style(), rendendo impossibile per altri sviluppatori di temi/plugin sostituire facilmente i propri script/stili.

@EAMann. Vero, errore mio, pensavo che sarebbe stato valido. Comunque, la validazione W3C non significa nulla...

@One Trick Pony Ehm... Significa qualcosa per me. E significa qualcosa per ogni altro sviluppatore che rispetto. Non la seguo ciecamente, ma sarebbe raro che consideri accettabile qualsiasi cosa che violi gli standard.

Se non lo segui ciecamente, puoi fornire delle ragioni per cui questa pratica è sbagliata?

È considerata una cattiva pratica ma questo è esattamente quello che fa lo shortcode della galleria nel core di WordPress: stampa un blocco CSS inline. È un po' meglio che usare l'attributo style e funziona. Il mio plugin che ha uno shortcode stampa semplicemente il CSS nell'head in ogni visualizzazione. Non ce n'è molto :)

@mfields, se faccio così allora altri sviluppatori di temi/plugin non possono sostituire i file con i propri. Non importa se il core lo fa, rimane comunque una cattiva pratica.

rendilo filtrabile e qualsiasi altro plugin o tema può fare come preferisce con esso. Se configurano il filtro per restituire una stringa vuota - non verrà stampato nulla.

Non hai menzionato alcuna ragione oggettiva contro questa pratica. Comunque non importa; vedo solo due opzioni qui: Caricare sempre CSS/script (ottimizzandoli per dimensione), o stili inline condizionali
