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ő:
- loader file-ok;
- common, component, css_module és page file-ok;
- 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.