2009. szeptember 17., csütörtök

Statikus file-ok organizációja

Előszó

Egyszer már neki fogtam ennek a cikk megírásának, ellenben az idő ahogy múlt, maga az írás is idejét múlt lett, újabb ötletek és próbálkozások történtek a módszerrel, finomodtak a műveletek.

Most újra nekifogok, ezúttal gyorsan és hatékonyan, hogy ne járjon el felette az idő vasfoga.

Bevezetés

Sokan írtak mostnában arról, hogy ők hogyan is rendezik a CSS-en belül a tulajdonságokat, hogyan generálják saját CSS Framework-jükkel a file-okat. Gondoltam itt az idő, hogy én is megosszam a saját módszeremet.

Mikor kialakítottam ezt a rendszert, mindvégig az volt a szemem előtt, hogy a látogatóknak a lehető leggyorsabban jelenjen meg minden, illetőleg a lehető leggazdaságosabban tudjuk őket kiszolgálni – kapcsolatok és kérések minimalizálása, file méretek csökkentése –, ugyanakkor fejleszteni se legyen egy rémálom, hogy minden egy 100K-s CSS file-ban van. De ne ugorjunk ennyire előre, kezdjük a legelején.

Tehát az egész megoldást a következő szemszögekből kell megvizsgálni:

  • a látogató;
  • és a fejlesztő.

A látogató

A látogatónak csak egy fontos: amit meg akar nézni, azt gyorsan megkapja. Hogy az oldal betöltésénél ne kelljen várnia több másodpercet és a betöltődött oldal ne egye meg a gépét.

Először Stoyan Stefanov említette meg a "High Performance Web pages" prezentációjában, amit azóta a legtöbb Yahoo! YSlow-val, illetve front-end optimalizációval kapcsolatos blogban és prezentációban hangoztatnak, miszerint:

  • We won't tolerate slow pages
  • 500 ms slower = 20% drop in traffic (Google)
  • 100 ms slower = 1% drop in sales (Amazon)

Ebből is látszik, hogy mennyire nagy "teher" van a front-end web fejlesztőkön, Rajtunk!

A fejlesztő

Most nézzük meg, hogy számunkra mi az, ami fontos. A CSS, JavaScript és egyéb állományokat jól struktúráltan kell tárolni. Ennek két megközelítése lehet:

  • oldalanként szeparált könyvtárba;
  • típusonként (modulonként) könyvtárba.

Eleinte az első megközelítést alkalmaztam, ellenben túl sok refaktorálással járt, mikor egy-egy állomány az oldal fejlődésével "közös" kódra épült. Ilyenkor mindig ki kellett emelni a saját könyvtrából, elhelyezni a közös kódoknak fenntartott konyvtárban, minden hivatkozást átírni, stb. Egy szóval jellemezve: macerás volt.

A második esetben már alapból egységnyi modulokra szétbontva tároljuk a CSS file-okat, így jóval ritkább esetben fordul elő, hogy egy új oldal/funkció létrehozásakor refaktorálnunk kelljen az állományainkat.

A struktúra

A struktúra, amit jelenleg használunk a következő (az itt látható könyvtárak a statikus állományokat kiszolgáló domain gyökerétől értendők):

[css] [common] browser_hacks.css footer.css header.css layout.css [component] captcha.css … [css_module] date.css nick.css … [loader] commonLoader1.css … cssModuleLoader1.css … componentsLoader1.css … pagesLoader1.css … [page] [shared] comments.css index_index.css style.css [img] [js] [swf] [build] (style-pack.php)

Amint látható, a CSS, JavaScript és kép illetve flash file-ok teljesen elkülönítve vannak tárolva a fejlesztés alatt. Azért írtam, hogy a fejlesztés alatt, mert a publikálás során az összes állomány átkerül a [build] könyvtárba ömlesztve, ezzel is csökkentve a kiszolgáláskor szükséges útvonal és a hozzá tartozó feloldáshoz szükséges idő hosszát.

A [css] könyvtár

A [common] tartalmazza a teljesen általános, minden oldalon használt – vagy használható – stílusokat, mint pl. az oldal struktúra, fejléc, lábléc vagy az általános, minden böngészőben használandó "hack"-ek.

A nálunk használt keretrendszerben lehetőség van a programból előállított, több helyen használt egységek – úgynevezett blokkok és komponensek – használatára. Ezek elkülönített stíluslapjai találhatóak a [component] könyvtárban. Ilyen például a captcha megjelenítő, vagy egy értékelő form megjelenítő.

A [css_module] könyvtárban található egységek hasonlítanak a [component]-ben található stíluslapokhoz, ellenben az itt elhelyezett különálló modulok nem programból generált tartalmak. Ilyenek a beviteli mezők, a felhasználói nick-ek, gombok, linkek, dátumok, címkék, stb. megjelenítése.

Az összes oldalhoz tartozik egy külön CSS file, még akkor is, ha az adott oldal megjelenése ezt nem követeli meg, tehát kvázi egy üres CSS file-ról van szó. Ezeket a file-okat a [page] könyvtárban helyezzük el. Amennyiben két vag több oldal megjelenése struktúrálisan megegyezne, ellenben azok nem modul szinten azonosak – tehát nem lehet sem a [component], sem a [css_module] könyvtárakban logikusan elhelyezni –, a [page]-en belül található [shared] könyvtárba rakjuk. Ezeket a file-okat az összes őt használó page szintű CSS file-nak a legelején importáljuk, így könnyen nyomon lehet követni, hogy mely oldalak használják azt.

Utoljára hagytam a fejlesztés és publikálás szempontjából legfontosabb könyvtárat, a [loader]-t. Ebben a könyvtárban találhatóak azok a loader típusú CSS file-ok, melyek az összes korábbi elsődleges könyvtárban (common, component, css_module és page) található file-okat importálják magukba. Erre azért van szükség, mert sajnálatos módon az Internet Explorer 6-os nem képes, csak maximum 31 @import szabályt feldolgozni egy CSS file-ból, és mindezt maximum 4 szint mélységig. A fentebb részletezett struktúrával elértük azt, hogyaz ilyen import mélység maximum 3 mélységű legyen.

Speciális CSS file-ok

style.css 

A style.css gyakorlatilag nem tartalmaz mást, mint a [loader] könyvtárban található loader típusú CSS file-okat importálja be. Így a három mélységünk a style.css-től számolva a következő:

  1. loader file-ok;
  2. common, component, css_module és page file-ok;
  3. a page file-okban található esetleges shared file-ok.

browser_hacks.css 

Jelenleg nálam a következőket tartalmaza:

  • Safari 3 és 4 szöveg anti-alias megjelenítési bug;
  • Mozilla Firefox alatt button és gomb típusú input-okon :hover esetén nem jelenik meg a text-decoration formázás;
  • Általánosan a képeket inline-block-ra állítani;
  • Internet Explorer 6-nál korábbi böngészők alatt a linkek-re a kurzor beállítása;
  • Internet Explorer 7 és újabbak, illetve a modern böngészők alatt a hidden input-ok elrejtése;
  • Internet Explorer alatt az input-ok dupla padding hack-je.

Mintakód

html, body, input, select, textarea, button { text-shadow: rgba(0, 0, 0, 0.01) 0 0 0; } input[type=hidden] { display: none !important; } @-moz-document url-prefix() { button, input[type=button], input[type=submit] { display: inline-block; } } a { cursor: pointer; cursor: hand; } img { display: inline-block; } input { _overflow: visible; }

Oldal stíluslapok

Ezek a stíluslapok találhatóak a [page] könyvtárban. A HTML-ben minden oldal kap egy, az oldal tartalmát tökéletesen beazonosító azonosítót. Ezt általában a body elemre rakott id attribútum használatával érhetjük el. Ennek segítségével oldható meg, hogy – bár az összes oldal stíluslapja be van mindig töltve, – csak a megfelelő oldal stílusai legyenek a tartalmunkra érvényesítve. Ez az azonosító megegyezik a CSS file nevével. Amennyiben egy máshol definiált komponensnek kicsit másképp kell kinéznie az adott oldalon, úgy az ilyen kivételek is ebbe a file-ba kerülnek.

Hogy jobban szemléletessem, íme egy mintakód, melyben a belépő oldalon a beviteli mezőkben nagyobb a szövegméret, mint az általános:

#user_login span.textInput { height: 26px; } #user_login span.textInput input { font-size: 18px; height: 22px; }

Kódolási konvenciók

Akár egyedül, akár többen dolgoznak is egy projekten, fontos, hogy minden típusú állománynak meglegyen a saját (lefektetett) írásmódja (kódolási konvenció). Nálunk jelenleg a következő minta alapján történik mindez:

h1, #myDiv .className { display: inline-block; color: #000; /* Azert kell, mert IE-ben kulonben nem jelenik meg a tartalom. */ _overlay: inline; }

Kiválasztó(k)

  • Minden kiválasztó szabályt külön sorba kell írni, azokat a specifikációban meghatározott módon vesszővel elválasztva. A vesszőt a szabály végén, közvetlenül – szóköz nélkül – kell írni.
  • A HTML elemeket kisbetűvel írjuk, követve az XHTML szabványt.
  • A class elnevezéseknek és azonosítóknak beszédesnek, de nem hosszúnak (maximum 15-20 karakteresnek) kell lenniük, és követniük kell a camelCase írásmódot.

A camelCase írásmód alól kivételt képeznek nálunk a rendszer jellegéből adódólag az oldalakat beazonosító id-k.

Személy szerint a következő ponttal ellent megyek a Google Page Speed elveinek, miszerint minél kevesebb kiválasztót használjunk, helyette minden elemnek adjunk saját class elnevezést vagy id-t. Ennek az ajánlásnak az az oka, mivel a böngészők a CSS selectorokat nem elölről, hanem hátulról kezdik el feldolgozni, emiatt a #myDiv div.container div ul li a span kiválasztónál először az összes span-t keresi meg a DOM fában, majd vissza felé lépkedve ebből az elem csoportból szűri ki a mintára nem illeszkedő elemeket. Ebből következik, hogy ha a cél span-ra rakunk egy containerLinkSpan class-t, jóval gyorsabban meg fogja találni az illeszkedő elemeket a DOM-ban a böngésző.

A kérdés persze az, hogy vajon miért is teszik ezt a böngészők? Illetve akkor mi értelme van egyáltalán a selector-oknak a CSS-ben és miért kellett újabbakat létrehozni a CSS3-ban? Illetve, hogy a HTML méret növekedése és a DOM-beli attribútumok számának növelése lassítja jobban a betöltést, vagy a "felesleges" CSS selector-ok?

  • A HTML kódban a lehető legkevesebb class elnevezést és id-t kell használni, helyettük használjuk a kiválasztókat.

Nyitó kapcsoszárójel

  • A nyitó kapcsoszárójelet minden esetben az utolsó kiválasztó után, szóközzel elválasztva kell írni, majd utána sortörést rakni.

Szabályok

  • Minden szabály külön sorba irandó, a szabály végén a pontosvessző kirakása kötelező;
  • A sorokat indenteljük egy tabulátor karakterrel;
  • A tulajdonságot és az értékét – betűcsalád és file útvonal kivételével – csupa kisbetűvel írjuk;
  • A HEXA színkódokat szintén kisbetűvel írjuk, és ahol #RRGGBB formátumú színt írunk, ott írjunk #RGB-t;
  • Amennyiben hack-et írunk, úgy azt plusz egy tabulátorral beljebb indentáljuk és felette megjegyzésben odaírjuk, hogy miért kell az adott hack-et használni.

Publikáló script-ek

Ezeknek a script-eknek nem más a feladata, mint az általunk gondosan szétválogatott és az előre megbeszélt kódolási konvenciók által megírt, kommentekkel ellátott CSS és JavaScript file-jainkból a lehető legkisebb, minden felesleges karaktertől megtisztított – a CSS file-ok esetében a kérések minimalizálása érdekében az importálandó file-okkal összevont (merge) – végleges file-okat hozzák létre.

Verziózás

Fontos továbbá az is, hogy amennyiben a Yahoo! YSlow és Google Page Speed ajánlásokat követve statikus állományainkat kellően hosszú lejárati idővel szolgáljuk ki, a megfelelő állományokra hivatkozásokat – például a CSS file-okban a képek – ellássa egy megfelelő verziószámmal, melynek hatására a böngészők új file-ként értelmezik őket, így nem a böngésző cache-ből, hanem a szerverről kérik le azt.

A verziószámmal való ellátásra kétféle megoldásunk lehet:

  • a file után irandó query string-ben helyezzük el (my_image.png?1234);
  • a file nevét egészítjük ki (my_image_1234.png).

CSS file-ok publikálása

A publikáláskor a script-ünk a style.css file-t dolgozza fel. A lépések, melyeket végrehajt:

File merge-elések

Az @import-ok helyére magának a file-nak a tartalmát "másolja" be rekurívan.

preg_replace('/\s*@import\s+(url\()?"(?P<file>.*?)"(?(1)\));/xe', '…', $cssLine)

Útvonalak normalizálása

Az így keletkezett tartalomban a képekre hivatkozások útvonalát normalizálja.

preg_replace('/(?<!@import )\s*url\(([^\'"].+?)\)/e', '\' url(\'.….'\'', $cssFileContent);

Ezekután a script megkeresi az összes képre való hivatkozást…

/url\((.+?\.(?:gif|jpg|png))\)/

…majd az általunk kiválasztott módszerrel ellátja a verziószámmal, amely jöhet SVN, CVS vagy Git revízió számból, vagy a file utolsó módosításának timestamp-jéből is.

Tartalom minimalitzálása

  • Felesleges sor eleji és végi whitespace-ek és kommentek törlése;
  • Felesleges egynél hosszabb whitespace-ek cseréje egy space-re;
  • A szabalyokban lévő felesleges whitespace-ek törlése;
  • "a b a b" értékek átalakítása "a b"-re;
  • "a b c b" értékek átalakítása "a b c"-re;
  • Mértékegységgel rendelkező 0 értékek cseréje mértékegység nélkülire;
  • Felesleges whitespace-ek eltávolítása a kapcsoszárójelek és vesszők körül;
  • #rrggbb színkódokból #rgb-t csinálunk;
  • !important előtt felesleges a whitespace;
  • Szabályokban az utolsó pontosvessző felesleges, ezeket töröljük.

Mintakód

preg_replace( array( '/(^\s+|\s+$|\/\*.*?\*\/)/s', '/\s+/', '/(\w)\s*:\s*(.+?)\s*(?:;|(\}))\s*/', '/(\d+(?:[a-z]{2}|%)?)\s+(\d+(?:[a-z]{2}|%)?)\s+\\1\s+\\2/', '/(\d+(?:[a-z]{2}|%)?)\s+(\d+(?:[a-z]{2}|%)?)\s+(\d+(?:[a-z]{2}|%)?)\s+\\2/', '/(?<=\D)0(?:[a-z]{2}|%)?/', '/\s*([{},])\s*/', '/#([a-f0-9])\\1([a-f0-9])\\2([a-f0-9])\\3/i', '/\s+!important/', '/;\}/' ), array( '', ' ', '$1:$2;$3', '$1 $2', '$1 $2 $3', '0', '$1', '#$1$2$3', '!important', '}' ), $content);

Böngésző specifikus hack-ek alkalmazása

A feladata az általánosan ismert böngésző hiányosságok pótlása a tartalomból. Ennek a megoldásnak köszönhetően a fejlesztés során nem kell törődni a böngészők ilyen irányú hiányosságaival, a script a szabványosan megírt CSS tartalomból állítja elő számunkra a böngésző-független forrást.

Ezek a hack-ek jelenleg a következők:

  • A inline-block tulajdonságot Firefox 2.0 nem ismeri, helyette display: -moz-inline-box; értéket kell alkalmazni;
  • A min-height és min-width tulajdonságot az Internet Explorer 7 és korábbi böngészők nem ismerik;
  • A :hover-t nem támogatják az Internet Explorer 8-nál korábbi verziók, csak a elemeken, így ezekre JavaScript-es megoldáshoz létrehozunk egy .originalClassName.hover nevű selector-t is;
  • Az opacity tulajdonságot nem kezelik a Firefox 2.0 és korábbi és az Internet Explorer 8-nál korábbi böngészők.

Mintakód

preg_replace( array( '/\bdisplay\s*:\s*inline-block\b/is', '/\bmin-height\s*:\s(\d+[a-z]{2}|%)/is', '/\bmin-width\s*:\s(\d+[a-z]{2}|%)/is', '/([^},\n]*?(?:\s|>|\+|:[a-z-]+\()(?:[^a]|[a-z]{2,}|\*)(?:\.\w+)*):hover(\s+[^{,\n]+?)?(?=\s*[{,\n])/isx', '/\bopacity\s*:\s*(\d*\.\d+|\d+)/ise', ), array( 'display: -moz-inline-box; display: inline-block', 'min-height: $1; _height: $1', 'min-width: $1; _width: $1', '$1:hover$2, $1.hover$2', 'opacity($1)' ), $content);

opacity() metódus

function opacity($value) { $value = (string)max(min((float)$value, 1), 0); return '-moz-opacity: '.$value.';'. 'opacity: '.$value.';'. 'filter: alpha(opacity='.(int)($value * 100).')'; }

JavaScript file-ok publikálása

A cél az, hogy vonjuk össze a lehető legtöbb JavaScript file-t, azokat minimalizáljuk (pack) a lehető legjobban, majd helyezzük el az így kapott file-t a [build] könyvtárban. Természetesen a file-jainkat szeparáltan tároljuk a fejlesztés során, a kérdés mindig a kiszolgálás mikéntjében rejlik.

Itt két megközelítés lehetséges. Az egyiknél a fejlesztés során van könnyebb dolgunk, a másiknál pedig a látogatónak kedvezünk. Amint azt korábban is már írtam, nem szabad csak a látogatót figyelembe venni a döntések során, a fejlesztő is ugyanolyan fontos!

Ha eldöntöttük, hogy melyik módszert alkalmazzuk, már csak a pack-elés módját kell kiválasztani. Számos mások által megírt megoldás közül is választhatunk. A megoldások után írt érték egy 120KB-s jQuery file-on végrehajtott méretcsökkenést jelent.

  • A Mozilla Rhino például a változóneveket lerövidíti a lehető legkisebbre, ügyelve azok láthatóságára. (43%)
  • Használhatjuk Dean Edwards által írt JavaScript Packer-t. Ez egy JavaScript-ben írt tömörítő. A hátránya, hogy némely kódok – például régebbi jQuery plugin-ek, stb. – nem működnek vele, illetve mivel maga a kicsomagoló JavaScript-ben íródott, ezért mire a becsomagolt script-ünk a letöltés után mire elérhetővé – értelmezetté – válik, eléggé sokáig tart. (66%)
  • Yahoo! YUI Compressor: sajnos ez idáig nem volt még vele tapasztalatom, mivel korábban a fenti két megoldást ötvözve használtam, az átállás egy hosszabb folyamat lenne, mert a YUI Compressor-ral is le kellene tesztelni minden eddig megírt – több, mint 1MB – JavaScript file működését az összes általunk támogatott böngészőn. Ez nagyon nagy munka, de idővel meg kell majd lépnünk, mert a JavaScript Packer a legkülönfélébb helyenek tud hibákat produkálni, melyek felderítése időigényes. (53%)

Minden JavaScript file-t külön pack-elünk

Nem szükséges minden módosítás után build-elni, mivel minden file-t be tudunk húzni a HTML-ben, amire szükségünk van.

Példával illusztrálva, ha nekünk van egy jQuery-nk (jquery.js), hozzá plugin-ek (jquery.scrollto.js és jquery.autocomplete.js) és a saját függvényeinket tartalmazó file (base.js), akkor ezeket egyenként fogjuk betölteni a HTML-ünkben mind a fejlesztés, mint éles környezetben. Ennek előnye, hogy a módosításokat azonnal tesztelhetjük, hátránya, hogy megnöveljük a betöltődés idejét, mivel megnöveltük a lekérések számát (minimum 4 JavaScript file betöltése mindig).

JavaScript file-ok összevonása egy file-ba

Ekkor a HTML-ben csak egy file-ra hivatkozunk, emiatt minden módosítás után szükséges az összevont file újboli létrehozása. Ez megnehezíti a kód tesztelését, bár szerintem idővel hozzászokik az ember, illetve a programozáshoz és teszteléshez való hozzáállást is javítja. Megfigyeltem, hogy mikor ezt a módszert alkalmaztam, egy idő után kevesebb hibát vétettem a programokban.

Persze lehet a build-elést automatizálni, akár lekéréskor a fejlesztői környezetben generálni a pack-elt JavaScrip file-t, de ez a betöltési időt növeli meg még akkor is, ha éppen nem a JavaScript-jeinkkel dolgozunk.

Képek

Ha már a publikáló script-jeink elvégeznek helyettünk mindent, miért is ne foglalkozzanak az oldalon használt képek optimalizálásával is. Ezzel mind mi spórolhatunk sávszélességet, mind a látogatónak gyorsabban töltődnek azok be.

Mi a fejlesztés során akár használhatunk minden esetben 24 bit-es PNG-ket – Internet Explorer 6 miatt átlátszóság nélkül –, azokból a programunk generálhat majd GIF vagy PNG8 képeket, annak megfelelően, hogy melyik eredményezi a kisebb file méretet.

Fotók esetén természetesen a JPEG formátumot használjuk, ellenben ezeken a file-okon is lehet optimalizációt végezni. Akár megcsinálhatjuk azt is, hogy a JPEG-jeinket mindig 100%-os minőségben mentjük ki. A publikáló script fog azokból egy általánosan elfogadott minőséget generálni – mondjuk egy konfigurációs file-ban előre, képenként külön is meghatározható beállítások szerint.

Ami viszont lényeges file méret csökkenést okoz, ha a JPEG és PNG file-jaink header-jeit csonkítjuk meg a lehető legjobban. Erre léteznek külön programok is, mint pl. a jpegtran vagy az OptiPNG. Ezen programok haszbálatával akár 40-50%-kal is csökkenthetjük a képi állományaink összméretét.