Încărcarea condiționată a JavaScript/CSS pentru shortcode-uri
Am lansat un plugin care creează un shortcode și necesită încărcarea unui fișier JavaScript și a unui fișier CSS pe orice pagină care conține acel shortcode. Aș putea pur și simplu să fac ca scriptul/stilul să se încarce pe toate paginile, dar asta nu este cea mai bună practică. Vreau să încarc fișierele doar pe paginile care folosesc shortcode-ul. Am găsit două metode de a face acest lucru, dar ambele au probleme.
Metoda 1 setează un flag la true în interiorul funcției de gestionare a shortcode-ului și apoi verifică această valoare într-un callback wp_footer
. Dacă este true, folosește wp_print_scripts()
pentru a încărca JavaScript-ul. Problema cu această metodă este că funcționează doar pentru JavaScript și nu pentru CSS, deoarece CSS-ul ar trebui declarat în <head>
, ceea ce poți face doar în timpul unui hook timpuriu precum init
sau wp_head
.
Metoda 2 se execută mai devreme și "se uită înainte" pentru a verifica dacă shortcode-ul există în conținutul paginii curente. Îmi place această metodă mult mai mult decât prima, dar problema este că nu va detecta dacă șablonul apelează do_shortcode()
.
Așadar, mă orientez spre utilizarea celei de-a doua metode și apoi încerc să detectez dacă este atribuit un șablon și, dacă da, să-l analizez pentru shortcode. Înainte de a face acest lucru, însă, am vrut să verific dacă cineva cunoaște o metodă mai bună.
Actualizare: Am integrat soluția în plugin-ul meu. Dacă cineva este curios să o vadă implementată într-un mediu real, puteți descărca plugin-ul sau răsfoi codul.
Actualizare 2: Începând cu WordPress 3.3, este posibil să apelezi wp_enqueue_script()
direct în interiorul unui callback de shortcode, iar fișierul JavaScript va fi încărcat în footer-ul documentului. Acest lucru este tehnic posibil și pentru fișierele CSS, dar ar trebui considerată o practică proastă deoarece afișarea CSS-ului în afara tag-ului <head>
încalcă specificațiile W3C, poate cauza FOUC (Flash of Unstyled Content) și poate forța browserul să re-randeze pagina.

Bazat pe experiența mea personală, am folosit o combinație dintre metodele 1 și 2 - arhitectura și scripturile din footer ale metodei 1 și tehnica de "look-ahead" din metoda 2.
Totuși, pentru "look-ahead" folosesc regex în loc de stripos
; preferință personală, mai rapid și poate verifica shortcode-uri 'malformate':
preg_match( '#\[ *shortcode([^\]])*\]#i', $content );
Dacă ești îngrijorat de autorii care folosesc manual do_shortcode
, aș recomanda să le instruiești să folosească un apel de acțiune încarce manual stilul pre-înregistrat.
ACTUALIZARE: Pentru autorul leneș care nu citește niciodată manualul, afișează un mesaj care să sublinieze greșeala lor ;)
function my_shortcode()
{
static $enqueued;
if ( ! isset( $enqueued ) )
$enqueued = wp_style_is( 'my_style', 'done' ); // cache pentru a nu repeta dacă e apelat de mai multe ori
// procesează shortcode-ul
$output = '';
if ( ! $enqueued )
// poți afișa mesajul doar la prima apariție încadrându-l în condiția anterioară
$output .= <<<HTML
<p>Atenție! Trebuie să încarci manual fișierul de stiluri pentru shortcode dacă folosești <code>do_shortcode()</code> direct!</p>
<p>Folosește <code>wp_enqueue_style( 'my_style' );</code> înainte de apelul <code>get_header()</code> în template-ul tău.</p>
HTML;
return $output;
}

Acesta este un punct bun. În mod ideal, aș dori să funcționeze fără ca ei să facă ceva în plus - pentru că în jumătate din cazuri probabil nu vor citi mai întâi FAQ-ul, așa că vor presupune pur și simplu că nu funcționează - dar s-ar putea să ajung să fac asta. Aș putea înregistra scripturile pe fiecare pagină, dar să le încarc doar dacă detectez un shortcode. Atunci, utilizatorii ar putea să se conecteze la init și să apeleze funcțiile de încărcare în șabloane specifice unde este nevoie, presupunând că nu este deja prea târziu în execuție în acel moment. De asemenea, WP are get_shortcode_regex() încorporat.

Dacă utilizatorii sunt pricepuți în implementarea do_shortcode()
, nu este rezonabil să presupunem că sunt la fel de pricepuți în urmărirea instrucțiunilor pentru încărcarea stilului shortcode-ului?

Adevărat, dar va obține regex pentru toate shortcode-urile, nu doar pentru ale tale ;) "Aș putea înregistra scripturile pe fiecare pagină" Probabil și metoda mai bună! Reține că nu trebuie să se conecteze la init
, doar oriunde înainte de wp_head
. Pentru dezvoltatorul leneș, verifică wp_style_is( 'my_style_handle', 'done' )
în interiorul shortcode-ului tău. Dacă este fals, afișează o eroare vizibilă care le instruiește ce să facă.

@Chip - Nu mă îngrijorează că nu sunt capabili să urmărească instrucțiunile, ci doar că nu vor ști că ar trebui să o facă, deoarece în 99% din cazuri nu este nevoie să faci nimic în plus.

@Ian, gândirea mea era că adăugarea lui do_shortcode()
în șablon este deja "a face ceva în plus" - iar utilizatorii care ar face acel ceva-în-plus ar ști deja despre necesitatea de a încărca stilul sau ar fi mai dispuși/mai probabil să urmărească instrucțiuni speciale.

@TheDeadMedic - Am integrat-o în codul meu și am actualizat întrebarea cu linkuri către el în caz că cineva vrea să o vadă detaliată. Mulțumesc din nou pentru ajutor :)

@Ian Super! Mulțumim că ai revenit cu o actualizare, atât pentru noi cât și pentru cei care vor ajunge aici ulterior prin Google ;)

Bun punct, dar trebuie să nu fiu de acord că preg_match
ar fi mai rapid decât strpos
http://lzone.de/articles/php-string-search.htm

@Bainternet Nu neapărat - preg_match
vs stripos
(versiunea insensibilă la majuscule) - oricum vorbim de diferențe minuscule de timp. Privind în urmă, cred că ar fi trebuit să evit termenul "mai rapid" - voiam mai degrabă să sugerez că este mai eficient pentru sarcina respectivă (față de folosirea mai multor stripos
pentru a acoperi diverse formate de shortcode).

Răspund târziu la această întrebare, dar deoarece Ian a inițiat acest fir de discuție pe lista wp-hackers astăzi, m-am gândit că merită să răspund, mai ales având în vedere că am planificat să adaug o astfel de funcționalitate la unele pluginuri la care lucrez.
O abordare de luat în considerare este să verifici la prima încărcare a paginii dacă shortcode-ul este folosit efectiv și apoi să salvezi starea utilizării shortcode-ului într-o cheie de meta pentru post. Iată cum:
Ghid Pas cu Pas
- Setează un flag
$shortcode_used
la'no'
. - În funcția shortcode-ului însuși, setează flagul
$shortcode_used
la'yes'
. - Setează un hook
'the_content'
cu prioritatea12
(după ce WordPress a procesat shortcode-urile) și verifică meta pentru post pentru o valoare''
folosind cheia"_has_{$shortcode_name}_shortcode"
. (O valoare''
este returnată când o cheie de meta pentru post nu există pentru ID-ul postului.) - Folosește un hook
'save_post'
pentru a șterge meta-ul postului, eliminând flagul persistent pentru acel post în cazul în care utilizatorul modifică utilizarea shortcode-ului. - De asemenea, în hook-ul
'save_post'
, foloseștewp_remote_request()
pentru a trimite un HTTP GET non-blocant către permalink-ul propriu al postului pentru a declanșa prima încărcare a paginii și setarea flagului persistent. - În final, setează un hook
'wp_print_styles'
și verifică meta-ul postului pentru o valoare de'yes'
,'no'
sau''
folosind cheia"_has_{$shortcode_name}_shortcode"
. Dacă valoarea este'no'
, nu încărca fișierul extern. Dacă valoarea este'yes'
sau''
, încarcă fișierul extern.
Și asta ar trebui să fie suficient. Am scris și testat un plugin exemplu pentru a arăta cum funcționează toate acestea.
Cod Exemplu pentru Plugin
Pluginul se activează la un shortcode [trigger-css]
care setează elementele <h2>
ale paginii pe alb pe roșu, astfel încât să poți vedea ușor că funcționează. Se presupune un subdirector css
care conține un fișier style.css
cu următorul CSS:
/*
* Nume fișier: css/style.css
*/
h2 {
color: white;
background: red;
}
Și mai jos este codul într-un plugin funcțional:
<?php
/**
* Plugin Name: CSS pe Shortcode
* Description: Arată cum să încarci condiționat un 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 deoarece get_post_meta() returnează '' pentru false și pentru chei inexistente.
*/
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 ); // DUPĂ do_shortcode() din 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 ) {
/**
* Dacă acest shortcode este folosit, captează valoarea pentru a o salva în post_meta în filtrul 'the_content'.
*/
$this->shortcode_used = 'yes';
return '<h2>ACEST POST VA ADAUGA CSS PENTRU A FACE ELEMENTELE H2 ALBE PE ROȘU</h2>';
}
/**
* Șterge valoarea meta 'has_shortcode' pentru a putea fi regenerată
* la prima încărcare a paginii, în cazul în care utilizarea shortcode-ului s-a schimbat.
*
* @param int $post_id
*/
function save_post( $post_id ) {
delete_post_meta( $post_id, $this->HAS_SHORTCODE_KEY );
/**
* Acum încarcă postul asincron prin HTTP pentru a pre-set valoarea meta pentru $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 ) ) {
/**
* Doar ocolește dacă este setat la 'no', deoarece '' este necunoscut.
*/
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 ) ) {
/**
* Aceasta este prima dată când shortcode-ul este văzut pentru acest post.
* Salvează o cheie post_meta pentru ca data viitoare să știm că acest post folosește acest shortcode.
*/
update_post_meta( $post->ID, $this->HAS_SHORTCODE_KEY, $this->shortcode_used );
}
/**
* Elimină acest filtru acum. Nu avem nevoie de el pentru acest post din nou.
*/
remove_filter( 'the_content', array( $this, 'the_content' ), 12 );
return $content;
}
}
new CSS_On_Shortcode();
Capturi de Ecran Exemplu
Iată o serie de capturi de ecran
Editor de Post de Bază, Fără Conținut
Afișare Post, Fără Conținut
Editor de Post de Bază cu Shortcode [trigger-css]
Afișare Post cu Shortcode [trigger-css]
Nu Sunt Sigur că e 100%
Cred că cele de mai sus ar trebui să funcționeze în aproape toate cazurile, dar cum tocmai am scris acest cod, nu pot fi 100% sigur. Dacă găsești situații în care nu funcționează, aș dori să știu pentru a putea repara codul în unele pluginuri la care tocmai am adăugat această funcționalitate. Mulțumesc anticipat.

Deci cinci pluginuri care folosesc abordarea ta vor declanșa cinci cereri la distanță de fiecare dată când un articol este salvat? Aș prefera să folosesc o expresie regulată pe post_content
. Și cum rămâne cu shortcode-urile în widget-uri?

@toscho Declanșarea încărcării postărilor este de fapt opțională; există doar pentru a se asigura că utilizatorul nu va vedea prima încărcare a paginii cu resursele externe încărcate. De asemenea, este un apel non-blocant, deci teoretic nu ar trebui să-l observi. Pentru codul nostru facem asta într-o clasă de bază, astfel încât clasa de bază poate gestiona faptul că se face o singură dată. Am putea folosi hook-ul 'pre_http_request'
și să dezactivăm apelurile multiple către aceeași URL cât timp hook-ul 'save_post'
este activ, dar aș aștepta până când chiar vedem nevoia pentru asta, nu? În ceea ce privește widget-urile, s-ar putea îmbunătăți pentru a le gestiona, dar nu este un caz de utilizare pe care l-am analizat încă.

@toscho - De asemenea, nu poți fi sigur că shortcode-ul este acolo, deoarece un alt hook l-ar putea șterge. Singura modalitate de a fi sigur este dacă funcția shortcode se execută efectiv. Deci abordarea cu regex nu este 100% sigură.

Știu. Nu există nicio metodă infallibilă de a injecta CSS pentru shortcode-uri (cu excepția utilizării <style>
).

Lucrez cu o implementare bazată pe această soluție și voiam să subliniez că această metodă nu va încărca stilurile pentru previzualizarea unei postări/pagini.

Soluția mea pentru problema previzualizării: https://gist.github.com/mor7ifer/e50a9864a372a05b4a3b

Căutând pe Google am găsit un posibil răspuns. Spun "posibil" pentru că pare bun, ar trebui să funcționeze, dar nu sunt 100% convins că este cea mai bună metodă:
add_action( 'wp_print_styles', 'yourplugin_include_css' );
function yourplugin_include_css() {
// Verifică dacă shortcode-ul există în conținutul paginii sau postării
global $post;
// Am eliminat ' ] ' de la final... pentru a accepta argumente.
if ( strstr( $post->post_content, '[yourshortcode ' ) ) {
echo $csslink;
}
}
Această soluție ar trebui să verifice dacă postarea curentă utilizează un shortcode și să adauge o foaie de stil în elementul <head>
în mod corespunzător. Dar nu cred că va funcționa pentru o pagină de index (adică mai multe postări în buclă)... De asemenea, provine de la un post de blog vechi de 2 ani, așa că nu sunt sigur că va funcționa cu WP 3.1.X.

Acest lucru nu va funcționa dacă shortcode-ul are argumente. Dacă chiar vrei să mergi pe această cale, care este lentă, folosește get_shortcode_regex()
din WP pentru căutare.

Cum am spus, un răspuns "potențial" pe care nu l-am testat încă ... :-)

În esență, este la fel ca metoda 2, dar tot nu verifică template-ul pentru apelurile do_shortcode().

De ce ar trebui să facă asta? Dacă apelezi manual do_shortcode()
în template, atunci deja știi că vei rula shortcode-ul

Nu eu apelez shortcode-ul, ci utilizatorul. Acesta este un plugin distribuit, nu unul privat.

ok, deci de asta vrei markup valid :) Spui că plugin-ul tău instruiește utilizatorul să editeze fișierele de template și să apeleze acea funcție? Atunci, dacă nu creezi propriul tău parser PHP, nu există nicio modalitate prin care să știi unde și când este apelată acea funcție...

Întotdeauna vreau un markup valid, indiferent de circumstanțe... Pluginul nu îi spune utilizatorului să apeleze funcția, dar aceasta rămâne întotdeauna o opțiune. Un utilizator poate apela orice shortcode fie tastându-l în zona de conținut a unui post/pagină, fie prin apelarea do_shortcode() într-un template, așadar ambele situații trebuie gestionate. Și nu cred că trebuie să scriu propriul meu parser PHP, ci doar să analizez fișierul de template asociat paginii curente (dacă există unul) pentru a verifica dacă shortcode-ul se află în el.

Presupunând că ai putea afla care fișier de template este folosit de pagina curentă (ceea ce mă îndoiesc), cum ai face parsing fără un parser? regex-uri? Dar dacă linia cu codul este comentată? Există zeci de modalități de a comenta codul PHP

Template-ul asociat este pur și simplu stocat în tabelul post_meta, ce e atât de greu în a-l identifica? Poți folosi hook-ul the_posts pentru a accesa variabila $post curentă înainte ca orice să fie afișat. Prin 'parser PHP' am presupus că te refereai la întregul motor, nu doar la o funcție strpos sau regex. Liniile comentate sunt un caz extrem de care nu sunt prea îngrijorat.

îți confunzi șabloanele de pagini (ca în post-type page) cu fișierele de temă folosite ca șabloane

Folosind o combinație între răspunsul lui TheDeadMedic și documentația get_shortcode_regex() (care de fapt nu a găsit shortcode-urile mele), am creat o funcție simplă folosită pentru a încărca scripturile pentru multiple shortcode-uri. Deoarece wp_enqueue_script() în shortcode-uri adaugă doar în footer, aceasta poate fi utilă deoarece poate gestiona atât scripturile din header cât și din 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 ) ) {
// încarcă css și/sau 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' );

În sfârșit am găsit o soluție pentru încărcarea condiționată a CSS-ului care funcționează pentru plugin-ul meu www.mapsmarker.com și aș dori să o împărtășesc cu voi. Aceasta verifică dacă shortcode-ul meu este folosit în fișierul template curent și în header.php/footer.php, iar dacă da, încarcă fișierul de stil necesar în 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 pic pe lângă subiect, dar nu presupune asta că oamenii folosesc header.php și footer.php? Dar despre metodele de înfășurare a temelor, cum ar fi cele descrise de http://scribu.net/wordpress/theme-wrappers.html? Sau teme precum roots care își păstrează părțile de șablon în altă parte?

Pentru plugin-ul meu am descoperit că uneori utilizatorii au un constructor de teme care stochează shortcode-uri în metadatele postării. Iată ce folosesc pentru a detecta dacă shortcode-ul meu este prezent în postarea curentă sau în metadatele postării:
function abcd_load_my_shorcode_resources() {
global $post, $wpdb;
// determină dacă această pagină conține shortcode-ul "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' );

deoarece CSS ar trebui declarat în interiorul
<head>
Pentru fișierele CSS le puteți încărca în interiorul output-ului shortcode-ului:
<style type="text/css">
@import "calea/către/fișierul-tău.css";
</style>
Setați o constantă sau ceva similar după aceasta, cum ar fi MY_CSS_LOADED
(includeți CSS-ul doar dacă constanta nu este setată).
Ambele metode propuse de tine sunt mai lente decât această abordare.
Pentru fișierele JS puteți face la fel dacă scriptul pe care îl încărcați este unic și nu are dependințe externe. Dacă acesta nu este cazul, încărcați-l în footer, dar folosiți constanta pentru a determina dacă trebuie încărcat sau nu...

Încărcarea CSS în afara elementului <head>
nu este un markup corect. Este adevărat că validarea este doar o linie directoare, dar dacă încercăm să respectăm această linie directoare, atunci încărcarea fișierului de stiluri în output-ul shortcode-ului este o idee proastă.

Blocurile CSS inline sunt markup valid, chiar și în XHTML din câte îmi amintesc. Nu există niciun motiv să nu le folosești atunci când nu ai altă alternativă acceptabilă.

Conform instrumentului de validare W3C: <style type="text/css">
Elementul menționat mai sus a fost găsit într-un context în care nu este permis. Aceasta poate însemna că ai imbricat incorect elementele -- cum ar fi un element "style" în secțiunea "body" în loc să fie în interiorul "head"
. Deci stilurile inline (<element style="..."></element>
) sunt valide, dar elementele <style>
inline nu sunt.

Da, știu că asta este o posibilitate, dar este considerată o practică proastă. Ar putea cauza FOUC, forța browserul să re-randeze pagina și să eșueze validarea W3C. De asemenea, nu va funcționa cu wp_enqueue_style(), făcând imposibil pentru alți dezvoltatori de teme/pluginuri să înlocuiască ușor propriile scripturi/stiluri.

@EAMann. Corect, greșeala mea, am crezut că va valida. Totuși, validarea W3C nu înseamnă nimic...

@One Trick Pony Um... Înseamnă ceva pentru mine. Și înseamnă ceva pentru fiecare alt dezvoltator pe care îl respect. Nu o urmez orbeste, dar ar fi rar pentru mine să consider orice ce încalcă standardele ca fiind un răspuns acceptabil.

Dacă nu o urmezi orbește, atunci poți să oferi motive pentru care această practică este rea?

Este considerată o practică rea, dar exact asta face shortcode-ul de galerie din WordPress core: Afișează un bloc CSS inline. E puțin mai bine decât să folosești atributul style și funcționează. Plugin-ul meu care are un shortcode doar afișează CSS-ul în head la fiecare încărcare. Nu e mult :)

@mfields, dacă fac asta, alți dezvoltatori de teme/plugin-uri nu vor putea înlocui fișierele cu cele proprii. Nu contează dacă miezul face acest lucru, rămâne o practică proastă.

fă-l filtrabil și orice alt plugin sau temă poate face ce dorește cu el. Dacă configurează filtrul să returneze un șir gol - nu se va afișa nimic.

Nu ai menționat motive obiective împotriva acestei practici. Oricum nu contează; văd doar două opțiuni aici: Încarcă mereu CSS/scripturi (optimizate pentru dimensiune), sau stiluri inline condiționate
