ScriptLoader, řešení pro css a js
- uestla
- Backer | 799
Byla by škoda, aby kvůli zcvrkávačům kódu tato konstrukce zanikla
(lépe řečeno její používání), na druhou stranu mě nenapadá
přijatelný způsob, jak to řešit (nahradit konstrukci obsahem importovaného
souboru je brutální prasárna nehledě na to, že importovat styly lze např.
pouze pro tisk – @import url('styl.css') print;
a importovat
shrinkovaný soubor je nemožné v případě, kdy je seshrinkovaný s více
soubory – nelze porovnat hashe).
Editoval uestla (6. 9. 2009 14:24)
- Honza Marek
- Člen | 1664
V dohledné době bych měl zveřejnit verzi, která bude nahrazovat url
odpovídající variantou po přesunu do jiné složky. Zatím to jde řešit
generováním css souboru do stejné složky s původním stylem. I tak ale
bude potřeba mít importy jen v prvním vloženém souboru, protože
@import
funguje jen na začátku souboru.
- Honza Marek
- Člen | 1664
Je mi ctí vám představit druhou verzi mého WebLoaderu. Novinky oproti první jsou především následující:
- Adresy v CSS souborech jsou při nastavení proměnné $sourceUri absolutizovány, takže nevadí přesun generovaných stylů do jiné složky.
- Na obsah generovaného souboru je možno aplikovat filtry, což je vlastně
jakýkoliv callback. Napsal jsem tedy třídu
VariablesFilter
, která umožňuje přidat do CSS a JS souborů proměnné. Dále ukážu příklad s komprimací JavaScriptu.
Příklady
CSS s proměnnými
Továrnička v presenteru:
protected function createComponentCss() {
$css = new CssLoader;
// cesta na disku ke zdroji
$css->sourcePath = WWW_DIR . "/css";
// cesta na webu ke zdroji (kvůli absolutizaci cest v css souboru)
$css->sourceUri = Environment::getVariable("baseUri") . "css";
// cesta na webu k cílovému adresáři
$css->tempUri = Environment::getVariable("baseUri") . "webtemp";
// cesta na disku k cílovému adresáři
$css->tempPath = WWW_DIR . "/webtemp";
// proměnné filtru je možné nastavit buď přímo v konstruktoru
$filter = new VariablesFilter(array(
"cervena" => "red",
"zelena" => "green",
));
// nebo pomocí speciální metody
$filter->setVariable("modra", "blue");
// podobu proměnné lze nastavit také
// $filter->setDelimiter("{", "}");
// výchozí hodnoty jsou {{$ a }}
// nastavení filtru
$css->filter[] = array($filter, "apply");
return $css;
}
CSS soubor:
body {background: {{$cervena}}}
h1 {color: {{$modra}}}
p {color: {{$zelena}}}
Výsledek:
body {background: red}
h1 {color: blue}
p {color: green}
Proměnné v JavaScriptu vypadají nesmírně užitečně při nastavování
cest v Texyla (náhled – $this->link("Texyla:preview")
a
podobně).
Minimalizovaný javascript
za použití třídy JavaScriptPacker pro PHP 5
Presenter:
protected function createComponentJs() {
$js = new JavaScriptLoader;
$js->tempUri = Environment::getVariable("baseUri") . "webtemp";
$js->tempPath = WWW_DIR . "/webtemp";
$js->sourcePath = WWW_DIR . "/js";
$js->filters[] = array($this, "packJs");
return $js;
}
public function packJs($code) {
$packer = new JavaScriptPacker($code, "None");
return $packer->pack();
}
Plácnutý zdroják
<?php
/**
* Web loader
*
* @author Jan Marek
* @license MIT
*/
abstract class WebLoader extends Control
{
/** @var string */
public $sourcePath;
/** @var string */
public $sourceUri;
/** @var string */
public $tempPath;
/** @var string */
public $tempUri;
/** @var array */
public $filters = array();
/** @var array */
protected $files = array();
/**
* Get html element including generated content
* @param string $source
* @return Html
*/
abstract public function getElement($source);
/**
* Get file list
* @return array
*/
public function getFiles() {
return $this->files;
}
/**
* Add file
* @param string $file filename
*/
public function addFile($file)
{
if (in_array($file, $this->files) || !file_exists($this->sourcePath . "/" . $file)) {
return;
}
$this->files[] = $file;
}
/**
* Add files
* @param array $files list of files
*/
public function addFiles(array $files)
{
foreach ($files as $file) {
$this->addFile($file);
}
}
/**
* Remove file
* @param string $file filename
*/
public function removeFile($file)
{
$this->removeFiles(array($file));
}
/**
* Remove files
* @param array $files list of files
*/
public function removeFiles(array $files)
{
$this->files = array_diff($this->files, $files);
}
/**
* Remove all files
*/
public function clear() {
$this->files = array();
}
/**
* Generate compiled file and render link
*/
public function render()
{
$hasArgs = func_num_args() > 0;
if ($hasArgs) {
$backup = $this->files;
$this->clear();
$this->addFiles(func_get_args());
}
if (empty($this->files)) return;
$filename = $this->generate();
echo $this->getElement($this->tempUri . "/" . $filename);
if ($hasArgs) {
$this->files = $backup;
}
}
/**
* Apply filters to a string
* @param string $s
* @return string
*/
protected function applyFilters($s) {
foreach ($this->filters as $filter) {
$s = call_user_func($filter, $s);
}
return $s;
}
/**
* Load file
* @param string $path
* @return string
*/
protected function loadFile($file) {
return file_get_contents($this->sourcePath . "/" . $file);
}
/**
* Get joined content of all files
* @return string
*/
public function getContent()
{
$content = "";
foreach ($this->files as $file) {
$content .= $this->loadFile($file);
}
return $this->applyFilters($content);
}
/**
* Get last modified timestamp of newest file
* @return int
*/
public function getLastModified()
{
$modified = 0;
foreach ($this->files as $file) {
$path = $this->sourcePath . "/" . $file;
$modified = max($modified, filemtime($path));
}
return $modified;
}
/**
* Filename of generated file
* @return string
*/
public function getGeneratedFilename()
{
return "generated-" . md5(implode("/", $this->files) . $this->getLastModified());
}
/**
* Load content and save file
* @return string filename of generated file
*/
protected function generate()
{
$name = $this->getGeneratedFilename();
$path = $this->tempPath . "/" . $name;
if (!file_exists($path)) {
if (!in_array(SafeStream::PROTOCOL, stream_get_wrappers())) {
SafeStream::register();
}
file_put_contents("safe://" . $path, $this->getContent());
}
return $name;
}
}
<?php
/**
* Css loader
*
* @author Jan Marek
* @license MIT
*/
class CssLoader extends WebLoader
{
/** @var string */
public $media;
/** @var bool */
public $absolutizeUrls = true;
/**
* Make relative url absolute
* @param string $url
* @param string $file
* @param string $sourceUri
* @return string
*/
public static function absolutizeUrl($url, $file, $sourceUri) {
$lastPos = strrpos($file, "/");
$fileFolder = $lastPos === false ? "" : "/" . substr($file, 0, $lastPos);
return $sourceUri . $fileFolder . "/" . $url;
}
private function absolutizeUrls($s, $file) {
return preg_replace_callback(
"(url\(['\"]?([^\)'\"]*)['\"]?\))",
create_function(
'$matches',
'return "url(\'" . CssLoader::absolutizeUrl($matches[1], "' .
addslashes($file) . '", "' . addslashes($this->sourceUri) .
'") . "\')";'
),
$s
);
}
/**
* Load file
* @param string $path
* @return string
*/
protected function loadFile($file) {
$content = parent::loadFile($file);
if ($this->absolutizeUrls && !empty($this->sourceUri)) {
$content = $this->absolutizeUrls($content, $file);
}
return $content;
}
/**
* Filename of generated CSS file
* @return string
*/
public function getGeneratedFilename()
{
return parent::getGeneratedFilename() . ".css";
}
/**
* Get link element
* @param string $source
* @return Html
*/
public function getElement($source)
{
return Html::el("link")
->rel("stylesheet")
->type("text/css")
->media($this->media)
->href($source);
}
}
<?php
/**
* JavaScript loader
*
* @author Jan Marek
* @license MIT
*/
class JavaScriptLoader extends WebLoader
{
/**
* Filename of generated JS file
* @return string
*/
public function getGeneratedFilename()
{
return parent::getGeneratedFilename() . ".js";
}
/**
* Get script element
* @param string $source
* @return Html
*/
public function getElement($source)
{
return Html::el("script")->type("text/javascript")->src($source);
}
}
<?php
/**
* Variables filter for WebLoader
*
* @author Honza
*/
class VariablesFilter extends Object
{
/** @var string */
private $startVariable = "{{\$";
/** @var string */
private $endVariable = "}}";
/** @var array */
private $variables;
/**
* Construct
* @param array $variables
*/
public function __construct(array $variables = array()) {
$this->variables = $variables;
}
/**
* Set variable
* @param string $name
* @param string $value
*/
public function setVariable($name, $value)
{
$this->variables[$name] = $value;
}
/**
* Set delimiter
* @param string $start
* @param string $end
*/
public function setDelimiter($start, $end)
{
$this->startVariable = $start;
$this->endVariable = $end;
}
/**
* Apply string
* @param string $s
* @return string
*/
public function apply($s)
{
$variables = array();
$values = array();
foreach ($this->variables as $key => $value) {
$variables[] = $this->startVariable . $key . $this->endVariable;
$values[] = $value;
}
return str_replace($variables, $values, $s);
}
}
Abych to závěrem zhodnotil, komponenta začíná být zralá k výhodnému používání a nalití do extras.
- nAS
- Člen | 277
Paráda, konečně se řeší k mé spokojenosti tento problém. Velké díky!
Edit: Když už jsi se do toho tak hezky vrhnul,
plánuješ udělat i podporu pro ten css @import?</del>
Edit 2: Edit 1 je blbost, koukám, že už se tu řešilo, že vložit @import obsah nejde kvůli různým typům médií, a s různými cestami se tato verze už vyrovná.
Editoval nAS (10. 9. 2009 22:34)
- Honza Marek
- Člen | 1664
Co si mám představit pod podporou @importu? Vyzobnout ten příkaz a dát ho v generovaném souboru nahoru aby fungoval? Netuším… Pokud budou importy jen v prvním souboru, tak to teď taky fungovat bude.
- nAS
- Člen | 277
Než jsem to stihl zeditovat, tak už jsi zareagoval :)
Šlo mi o slévání souborů, ale nerozmyslel jsem dostatečně jednotlivé typy médií. Krom toho HTTP/1.1 už stejně nemusí otevírat nová spojení pro každý soubor, takže se tím neušetří tolik datového přenosu/výkonu serveru, aby to za to stálo.
- uestla
- Backer | 799
Dovolím si hnidopišskou poznámku k reguláru pro zabsolutnění URL ve
stylech, a sice že na Windowsech (nevím, jak je tomu jinde) může mít soubor
ve svém názvu kulaté závorky. Navíc tomu reguláru vyhoví i tvar
url('ahoj.css");
, což lze snadno zpětnou referencí
„opravit“:
$regexp = "~url\((['\"]?)([^'\"]*)\\1\)~";
A v těle callbacku samozřejmě narhadit $matches[1]
za
$matches[2]
.
- Honza Marek
- Člen | 1664
Já se určitě nezlobím za pomoc s regulérníma výrazama, sám je nemám
moc rád. Ale tenhle má taky jednu vadu. Selže, pokud url obrázku bude
zadána zcela bez uvozovek – url(images/soubor.png)
. To se děje
třeba ve stylech jQuery UI témat.
- kravčo
- Člen | 721
Obaja tam máte chybku :)
Honza: Úvodzovky nie sú v žiadnom vzťahu, navyše okrem
url("bg.png')
to matchne aj url("bg.png)
alebo
url(bg.png')
. Toto pri správne napísaných štýloch nevadí…
Zátvorka by (veľmi teoreticky) mohla…
uestla: Fixol si vzťah úvodzoviek, no fixom zátvorky si
spôsobil nepríjemnú chybku – výraz totiž matchne aj celé
url(bg.png); url(bg.png)
(len v prípade url bez úvodzoviek)…
greedy je greedy…
Navyše ani jeden z vašich regulárov nematchne úplne správny názov
súboru (na linuxe) url("i'am_batman.png")
, či trochu úchylné,
ale správne
url( bg/i\'am\ batman\ \(grey\ version\).png )
ekvivalentné url("bg/i'am batman (grey version).png")
.
Regulár, ktorý myslí na tieto veci by mohol vyzerať takto (zapísaný ako php string):
'~
(?<![a-z])
url\( ## url(
\s* ## optional whitespace
([\'"])? ## optional single/double quote
( (?: (?:\\\\.)+ ## escape sequences
| [^\'"\\\\,()\s]+ ## safe characters
| (?(1) (?!\1)[\'"\\\\,() \t] ## allowed special characters
| ^ ## (none, if not quoted)
)
)* ## (greedy match)
)
(?(1)\1) ## optional single/double quote
\s* ## optional whitespace
\) ## )
~xs'
CSS2spec:
The format of a URI value is
url(
followed by optional white space followed by an optional single quote ('
) or double quote ("
) character followed by the URI itself, followed by an optional single quote ('
) or double quote ("
) character followed by optional white space followed by)
. The two quote characters must be the same.Some characters appearing in an unquoted URI, such as parentheses, commas, white space characters, single quotes (
'
) and double quotes ("
), must be escaped with a backslash so that the resulting URI value is a URI token:\(
,\
)
,\,
.
No stále to nahradí aj povedzme
a:after { content: "url(something)"; }
, čo by samozrejme
nemalo…
- Honza Marek
- Člen | 1664
uestla: selže znamené, že to matchlo kilometr dlouhý řetězec počínaje adresou, konče nevím čím.
kravco: pěkné, díky. Koukám, že pak budu muset řešit ještě nějaké odslashování případně.
- Honza Marek
- Člen | 1664
I tak hnidopišské příspěvky považuju za podnětné. Budou-li důkladně promyšlené, tím líp.
- wotaen
- Člen | 82
Ahoj,
dovolil jsem si poupravit getElement u JavaScriptLoader.php, nově je tam
<?php
public function getElement($source)
{
if (Environment::isProduction()) {
$element = Html::el("script")->type("text/javascript")->src($source);
} else {
$element = '';
foreach($this->getFiles() as $file) {
$element.= Html::el("script")->type("text/javascript")->src($this->sourceUri.'/js/'.$file);
}
}
return $element;
}
?>
Tj. pokud jsem na lokále, tak se nic nedělá a skripty se stáhnout v původní podobě a množství – kvůli debugování firebugem atd.
To stejné jde pochopitelně udělat s getElement u CssLoader.
Je to jenom koncept, ale funguje
Editoval wotaen (15. 9. 2009 14:18)
- Ondřej Mirtes
- Člen | 1536
wotaen: Spíš než na Environment::isProduction bych to nechal závislé na
nějaké property, např. $this->joinFiles = true/false
. Ať si
to každý může nastavit. Jinak je to dobrý nápad.
Včera jsem se dostal k vyzkoušení této komponenty a je skutečně
geniální. Na produkčním serveru minifikuju pomocí filtrů
CSS i javascript soubory a původní (zdrojové) si přesunul do
app/templates/WebModule/css/ (resp. js/), aby byly schované. To widgetové
volání ve spojení s dědičností šablon je opravdu
killer feature
.
Tleskám tleskám!
- Honza Kuchař
- Člen | 1662
wotaen: Když se tak na to dívám, asi to má jednu chybku, když se nahrazují ty proměné v JS a css, tak to asi nebude fungovat. Protože tohle načte opravdu zdrojový soubor. Takže by to bylo potřeba aby to nahradilo jenom ty proměnné a tento soubor by to načetlo. Jinak dobrý nápad. ;)
- Honza Marek
- Člen | 1664
LastHunter napsal(a):
Na produkčním serveru minifikuju pomocí filtrů CSS i javascript soubory
Našel nebo vyrobil jsi nějaké pěkné minimalizovátko CSSka? Poděl se…
- Honza Marek
- Člen | 1664
Přidal jsem volitelnost spojování souborů, poladil jsem absolutizaci adres v css i díky kravčovu vychytanému reguláru a hodil komponentu do extras.
- Ondřej Mirtes
- Člen | 1536
Honza M. napsal(a):
LastHunter napsal(a):
Na produkčním serveru minifikuju pomocí filtrů CSS i javascript soubory
Našel nebo vyrobil jsi nějaké pěkné minimalizovátko CSSka? Poděl se…
Našel jsem jednořádkové řešení Jakuba Vrány (matfyz se prostě nezapře), které z CSSka vytvoří skutečně jednořádkový soubor :)
adminer.org – SVN – trunk – compile.php, funkce minify_css:
return preg_replace('~\\s*([:;{},])\\s*~', '\\1', $input);
- kravčo
- Člen | 721
LastHunter napsal(a):
Našel jsem jednořádkové řešení Jakuba Vrány (matfyz se prostě nezapře), které z CSSka vytvoří skutečně jednořádkový soubor :)
Vo väčšine prípadov…
body { background: url('obrázok, ktorý má zvláštny názov.jpg'); }
span.email:after { content: ", email address"; }
span.xmpp:after { content: ", jabber account"; }
Vyššie uvedený štýl zminimalizuje zle – zminimalizovaná verzia nebude funkčne ekvivalentná tej pôvodnej.
Aby ste mi rozumeli, použitie tohto reguláru na zminimalizovanie je fajn, pretože je jednoduchý a zrejmý na prvý pohľad. Avšak je dôležité vedieť, že nefunguje na 100%, prečo nefunguje na 100% a prečo si ho môžme dovoliť.
Ani môj super regulár, ktorý Honza opisoval nefunguje na 100% a aby fungoval, potreboval by asi ďalšie štyri riadky, otázkou je, či to je dobre… Ja sa nerád spolieham na určitý formát csska/js/whatever, preto sa snažím pokryť všetky prípady, no niekedy je tvorba nepriestrelných regulárnych výrazov naozaj zbytočná…