ScriptLoader, řešení pro css a js

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Honza Marek
Člen | 1664
+
0
-

To si nemyslim, že je dobrý… Defaultní musí být ta relativní.

uestla
Backer | 799
+
0
-

Jak se nástroje zachovají, když spojují vícero CSS souborů a v některém z nich se vkládá jiný soubor konstrukcí @import url('jiny_soubor.css')?

h4kuna
Backer | 740
+
0
-

uestla napsal(a):

Jak se nástroje zachovají, když spojují vícero CSS souborů a v některém z nich se vkládá jiný soubor konstrukcí @import url('jiny_soubor.css')?

ScriptLoader to odstrani a nenacite, musis definovat znovu, nema to zadny efekt pouziti @import

Honza Marek
Člen | 1664
+
0
-

Ten můj zatím nijak.

uestla
Backer | 799
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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.

jasir
Člen | 746
+
0
-

Vypadá to krásně, jdu si to vyzkoušet. Díky! Jinak do extras určitě a možná bych popřemýšlel i o githubu…

Editoval jasir (10. 9. 2009 23:33)

uestla
Backer | 799
+
0
-

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
+
0
-

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.

uestla
Backer | 799
+
0
-

Opravdu selže (Pokud tedy „selhat“ znamená nevyhovět…)? Testoval jsem to a mně tedy prošel… tomu prvnímu subvýrazu totiž vyhoví i prázdný řetězec (což už je ostatně i u tvého původního reguláru).

kravčo
Člen | 721
+
0
-

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
+
0
-

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ě.

uestla
Backer | 799
+
0
-

uestla: selže znamené, že to matchlo kilometr dlouhý řetězec počínaje adresou, konče nevím čím.

Konče pravou kulatou závorkou na konci kilometru.

Omlouvám se a slibuji, že než pošlu nějaký hnidopišský příspěvek, půjdu si vyvětrat hlavu a důkladně to promyslím ;-)

Honza Marek
Člen | 1664
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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. ;)

wotaen
Člen | 82
+
0
-

honzakuchar: Máš pravdu, tohle jsem neřešil, implementaci ponechám na dobu až to bude potřeba :)

Honza Kuchař
Člen | 1662
+
0
-

To je určitě rozumné. Nejdřív by se měla vyvynout ta původní komponenta.

Honza Marek
Člen | 1664
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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á…

buff
Člen | 63
+
0
-

Ahoj, ten WebLoader používá funkci ereg, která je deprecated v 5.3. Nešlo by to prosím změnit na odpovídající funkci PCRE, tedy preg_match? Stačí změnit ereg na preg_match a přidat delimiter na začátek a konec regulárního výrazu. Díky za super komponentu!

Honza Marek
Člen | 1664
+
0
-

ok