Verzionisanje asset-a u Zend Framework 2
Autor: Nikola Poša |
Datum:
  • # Frameworks

U ovom članku će biti predstavljeno ZF2 rešenje za problem osvežavanja asset-a ukeširanih od strane browser-a, nakon što se isti izmene i puste u produkciju.

Terminologija

Pored samog HTML-a, web stranica sa sobom obično nosi i određene resurse, najčešće u vidu fajlova, zadužene za njeno dekorisanje (CSS i slike), ali i obogaćivanje i unapređivanje korisničkog interfejsa (JS). U web development terminologiji, za sve njih postoji jedan zajednički izraz – assets, kojeg ćemo koristiti u nastavku teksta.

Potreba za verzionisanjem asset-a

Do problema sa asset-ima dolazi nakon deploy-ovanja određenih izmena nad njima u produkciju, kada se očekuje se da one odmah budu na snazi, vidljive krajnjem korisniku web aplikacije/sajta. Kako ih ne bi preuzimao sa servera pri svakom otvaranju određene stranice, web browser će keširati asset-e, u slučaju da je sam server adekvatno podešen (Expires headers). Imajući to u vidu, korisnik, da bi video izmene, mora da preduzme dodatne zahvate (reload stranice, reset keša i slično), a kako je velika verovatnoća da taj posetilac sajta baš i nije kompjuterski ekspert, te za tako nešto nije ni sposoban, isti će biti veoma razočaran i ljut zbog toga što ne vidi najsvežiju verziju sajta.

Rešenje je u verzionisanju asset-a, kako bi oni bili automatski osveženi nakon što se nova verzija deploy-uje i pusti u produkciju.

Omogućavanje keširanja

Pre nego što detaljnije pređemo na samo verzionisanje asset-a, u prethodnom delu sam pomenuo jedan bitan detalj. Da bi browser uopšte bio u stanju da kešira asset-e, naš server mora biti adekvatno podešen. To ćemo postići slanjem određenih instrukcija browser-u, za svaki tip asset-a koje mu serviramo. Primer koji sledi podrazumeva da se naša aplikacija "vrti" na Apache-u, ali princip je isti u slučaju bilo kog drugog servera, npr. Nginx, razlika je samo u sintaksi. Dakle, da bismo omogućili keširanje naših CSS i JS fajlova od strane browser-a, u glavni .htaccess  fajl naše ZF2 aplikacije, smešten u document root-u (public folder), potrebno je dodati ovako nešto:

<IfModule mod_expires.c>
    ExpiresActive on

    # CSS
    ExpiresByType text/css "access plus 1 year"

    # JavaScript
    ExpiresByType application/javascript "access plus 1 year"
</IfModule>

Ovo u prevodu znači da ukoliko je dostupan mod_expires Apache modul, a najčešće jeste, resursi koji su tipa text/css (CSS) i application/javascript (JS) imaju rok trajanja od godinu dana, što praktično znači da ih browser, nakon što ih prvi put preuzme, neće potraživati duži vremenski period.

Postupak verzionisanja asset-a

Da bismo omogućili automatsko osvežavanje ukeširanih asset-a, potrebno je da browser-u nekako stavimo do znanja da je određeni fajl promenjen. Nailazio sam na različite načine za ostvarivanje ovog zadatka, a svi oni su zapravo neznatne varijacije jednog istog principa, koji se svodi na dve stvari:

  1. dinamičko umetanje broja verzije u imena fajlova asset-a koje serviramo
  2. rewrite-ovanje rezultujućih HTTP request-ova na originalan fajl

Što se prvog koraka tiče, ideja je da u našem HTML-u, umesto na primer:

<link rel="stylesheet" href="css/style.css">

… imamo:

<link rel="stylesheet" href="css/style__3.css">

 … pri čemu broj 3 u ovom slučaju predstavlja verziju.

Na taj način, nakon deploy-ovanja nove verzije, browser će shvatiti da se radi o "novom" fajlu (do tada se taj asset servirao kao css/style__2.css ), što ga automatski primorava da pošalje novi HTTP request za taj resurs, koji je zapravo isti fajl, ali sa novim, izmenjenim sadržajem.

Da bismo ovo ostvarili u okviru neke ZF2 aplikacije, bilo da se radi o CSS ili JS asset-ima, a podrazumevajući da za njihovo učitavanje koristimo HeadLink  i  HeadScript /InlineScript  view helper-e, napravićemo custom varijante ovih helper-a, koji će dinamički umetati broj verzije u rezultujući tag za učitavanje odgovarajućeg asset-a:

namespace Application\View\Helper;

use Zend\View\Helper\HeadLink as ZfHeadLink;
use stdClass;
use Application\Module;

class HeadLink extends ZfHeadLink
{
    public function itemToString(stdClass $item)
    {
        if ($item->rel == 'stylesheet' && isset($item->href)) {
            $item->href = preg_replace('#\.css$#', '.__' . Module::VERSION_CODE . '.css', $item->href);
        }
        return parent::itemToString($item);
    }
}
namespace Application\View\Helper;

use Zend\View\Helper\HeadScript as ZfHeadScript;
use Application\Module;

class HeadScript extends ZfHeadScript
{
    public function itemToString($item, $indent, $escapeStart, $escapeEnd)
    {
        if (isset($item->attributes['src'])) {
            $src = $item->attributes['src'];
            $item->attributes['src'] = preg_replace('#\.js$#', '.__' . Module::VERSION_CODE . '.js', $src);
        }

        return parent::itemToString($item, $indent, $escapeStart, $escapeEnd);
    }
}
namespace Application\View\Helper;

class InlineScript extends HeadScript
{
    protected $regKey = 'Zend_View_Helper_InlineScript';

    public function __invoke($mode = HeadScript::FILE, $spec = null, $placement = 'APPEND', array $attrs = array(), $type = 'text/javascript')
    {
        return parent::__invoke($mode, $spec, $placement, $attrs, $type);
    }
}
namespace Application;

class Module
{
    const VERSION_CODE = 1;

    //…
}

Kao što se može videti na osnovu kôda, helper-e sam izveo iz ZF-ovih, redefinisao njihove itemToString()  metode, kako bih u njima izvršio dinamičko umetanje verzije. Za potrebe ovog primera, čuvanje informacije o verziji sam maksimalno uprostio, postavivši ga u samoj Module  klasi našeg Application  modula, u vidu statičkog integer-a. Naravno, u realnim uslovima, taj brojač bi se verovatno čuvao u nekom fajlu, koji bi se automatski inkrementirao prilikom deploy-a, itd.

Da bi naši helper-i bili aktivni umesto ZF-ovih, potrebno je da override-ujemo prijave tih servisa u plugin manager-u zaduženom za view helper-e, tačnije da registrujemo naše pod istim nazivima. To možemo uraditi kroz module.config.php  našeg modula:

'view_helpers' => array(
    'invokables' => array(
        'headlink'=>'Application\View\Helper\HeadLink',
        'headscript'=>'Application\View\Helper\HeadScript',
        'inlinescript'=>'Application\View\Helper\InlineScript',
    ),
)

Ono što nam preostaje (drugi korak) jeste da vršimo rewrite rezultujućih, verzionisanih URL-ova asset-a na originalne fajlove. To ćemo postići dodavanjem odgovarajućeg rewrite rule-a u .htaccess :

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)\.__\d+\.(js|css)$ $1.$2 [L]

Kompletan izgled .htaccess fajla:

# Set proper MIME types for files
<IfModule mod_mime.c>
    # JavaScript
    AddType application/javascript js

    # Other…
</IfModule>

# ETag removal
<IfModule mod_headers.c>
    Header unset ETag
</IfModule>

FileETag None

# Expires headers
<IfModule mod_expires.c>
    ExpiresActive on

    # CSS
    ExpiresByType text/css "access plus 1 year"

    # JavaScript
    ExpiresByType application/javascript "access plus 1 year"

    # Other…
</IfModule>

# Filename-based versioning
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)\.__\d+\.(js|css)$ $1.$2 [L]

# If the requested filename - serve it,
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]

# …otherwise, rewrite all other queries to index.php.
RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$
RewriteRule ^(.*) - [E=BASE:%1]
RewriteRule ^(.*)$ %{ENV:BASE}index.php [NC,L]

Dodavanje query string-a na naziv fajl

Očekujem da će biti dosta onih koji će komentarisati kako je mnogo jednostavniji način nalepiti broj verzije na sam naziv asset-a, u vidu query string-a, tako da na primer imamo: http://www.test.com/style.css?v=123. Ovo je POGREŠNO, jer se request-ovi sa query string-om NE keširaju od strane browser-a! Više o svemu tome možete saznati ovde: http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/.

Rezime

Jedini, pravi način za rešavanje problema automatskog osvežavanja asset-a nakon što oni budu ukeširani od strane browser-a jeste njihovim verzionisanjem, i to umetanjem verzije u sam naziv fajla, tj u sam URL asset-a. Konkretan primer  u ovom članku je u vidu rešenja verzionisanja asset-a u ZF2-baziranoj aplikaciji, ali isti princip je primenljiv u slučaju bilo kog drugog PHP framework-a, pa i bilo kog drugog programskog jezika.

Linkovi