Optimisez vos fichiers JS et CSS dans Symfony 1
Cet article fait suite à l'article que j'ai écris sur l'utilisation du composant Assetic de sf2 dans sf1 et sur l'article permettant de mettre en cache les fichiers YML.
Pour les besoins de ce site, j'ai dévelopé sous forme d'un plugin Symfony (1.4) un gestionnaire de fichier Javascript et CSS. J'ai prévu d'ajouter des fonctionnalités au fur et à mesur sur ce site, mes fichiers javascript et css vont donc évoluer avec le temps mais je ne veux pas attendre d'avoir fini le site pour compresser ces fichiers et réduire l'impact qu'ils ont sur le temps de chargement des pages.
1. Problèmatique
Je gère deux versions de mes fichiers JS et CSS. Une version facilement lisible et une version regroupant tous les fichiers du même type dans un seul fichier compressé. Pour faire simple ces versions correspondent aux environnements de développement et de production. Il faut donc gérer le chargement des fichiers en fonction de l'environnement et maintenir la liste des fichiers à compresser. J'ai trouvé plusieurs plugins faisant cela, mais ils utilisent la base de donnée pour gérer la liste des fichiers et je trouve abérant de faire une requête SQL pour ça.
2. Liste des fichiers en fonction de l'environnement
Pour maintenir la liste des fichiers, je vais utiliser un fichier YAML que je mettrai en cache via un configHandler que j'ai déjà fait. Ce configHandler est capable de différencier les environnements de Symfony ce qui est bien utile dans mon cas.
a. Les fichiers CSS
Pour réduire un peu plus la taille des CSS je sépare la CSS utilisée pour l'affichage de la partie visible du site et la CSS pour l'administration du site.
css: admin: ['back.css'] front: ['main.css']
b. Les fichiers JS
La même règle s'impose que pour les fichiers CSS mais en plus je crée 2 groupes de fichiers. Un groupe qui sera chargé dans le HEADER de la page, typiquement les librairies externes tel que ExtJs, JQuery; et un groupe qui sera chargé en bas de page, mon code javascript, pour optimiser le téléchargement des sources.
js: admin: head: [] foot: [] front: head: [] foot: []
c. Cache des navigateurs
Afin d'éviter tout problème avec le cache interne des navigateurs lors de la mise à jour des fichiers, j'ajoute un numéro de version dans le nom des fichiers compressés. Ce numéro sera incrémenté à chaque mise à jour du code du site et pour me simplifier les choses, j'ai créé une valeur spécifique dans le fichier app.yml. Si vous ne le saviez pas encore, il est possible de mettre du code PHP dans un fichier YAML, il sera interprété au moment du parsing.
Mon fichier de configuration final ressemble donc à cela:
default: js: admin: head: [] foot: [] front: head: [] foot: [] css: admin: ['back.css'] front: ['main.css'] prod: js: admin: head: foot: front: head: foot: css: admin: 'back_<?php echo sfConfig::get('app_site_version');?>.css' front: 'main_<?php echo sfConfig::get('app_site_version');?>.css'
d. Mise en cache du fichier YAML
J'utilise le plugin ulMyconfigHandlerPlugin que j'ai déjà fait pour cette tache. Il ne reste plus qu'à spécifier le fichier config_handler.yml:
plugins/ulAssetManagerPlugin/config/assets.yml: class: myDevConfigHandler
3. Création des fichiers pour le front
Pour génerer les fichiers du front je vais utiliser le composant Assetic de Symfony 2 que j'ai mis en plugin dans Symfony 1.4 et cela à travers une tache.
La compression des fichiers va se faire en 4 étapes:
1- je lis tous les fichiers avec la class FileAsset et je les rassemble dans un tableau.
2- je crée un objet CssCompressorFilter ou JsCompressorFilter qui utilisera la librairie Yahoo! yuicompressor
3- je crée une collection avec AssetCollection qui prend en paramêtre mon tableau et mon compressor
4- j'utilise AssetWriter pour écrire le fichier final.
Exemple avec les CSS:
class clsAssetCss { protected $sDir, $aCompresseurs = array(), $aAssetFiles = array(); public function __construct() { $this->sDir = sfConfig::get('sf_web_dir').'/css/'; $this->aCompresseurs[] = new Assetic\Filter\Yui\CssCompressorFilter(sfConfig::get('sf_plugins_dir').'/sf2AsseticPlugin/external/yuicompressor.jar'); } /** * vérifie l'existence des fichiers et les regroupe dans un tableau de FileAsset * * @author ulrich, 16/08/11 * @param array $aFiles * @throws clsAssetManagerException si le fichier n'existe pas. */ public function prepareSource(Array $aFiles) { for($i = 0, $n = count($aFiles); $i < $n; $i++) { $sFile = $aFiles[$i]; if(file_exists($this->sDir.$sFile)) { $this->aAssetFiles[] = new Assetic\Asset\FileAsset($this->sDir.$sFile); } else { throw new clsAssetManagerException('Le fichier: '.$this->sDir.$sFile.' n\'extiste pas.'); } } } /** * Ecrit le fichier unique regroupant tous les assets * * @author ulrich, 16/08/11 * @param String $sFileName: nom du fichier à écrire */ public function creerAsset($sFileName) { $oAssetCol = new Assetic\Asset\AssetCollection($this->aAssetFiles, $this->aCompresseurs); $oAssetCol->load(); $oAssetCol->setTargetPath($sFileName); $oWriter = new Assetic\AssetWriter($this->sDir); $oWriter->writeAsset($oAssetCol); } }
Il ne reste plus qu'à écrire une task Symfony qui va lire le fichier de config YAML sans prendre en charge l'environnement et qui va utiliser la classe ci dessus.
class compressAssetsTask extends sfBaseTask { protected $aConfig; protected function configure() { $this->addOption('env', null, sfCommandOption::PARAMETER_OPTIONAL, 'Changes the environment this task is run in', 'prod'); $this->namespace = 'UL'; $this->name = 'compressAssets'; $this->briefDescription = 'Compress les Assets'; $this->detailedDescription = 'Compress les Assets pour l\'environnement de production.'; } protected function execute($arguments = array(), $options = array()) { require_once dirname(__FILE__).'/../../../../config/ProjectConfiguration.class.php'; $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', $options['env'], true); sfContext::createInstance($configuration); $this->aConfig = clsYaml::chargerYamlWithoutCache(dirname(__FIlE__).'/../../config/assets.yml'); $this->compressCss(); return 0; } protected function getSourceEnv() { if(array_key_exists('dev', $this->aConfig)) return 'dev'; elseif(array_key_exists('default', $this->aConfig)) return 'default'; elseif(array_key_exists('all', $this->aConfig)) return 'all'; } protected function compressCss() { $sSource = $this->getSourceEnv(); $oAssetCSS = new clsAssetCss(); foreach($this->aConfig[$sSource]['css'] as $sApp => $aFiles) { try { $oAssetCSS->prepareSource($aFiles); } catch(clsAssetManagerException $e) { $this->logSection('Info:', $e->getMessage()); } foreach($this->aConfig as $sEnv => $aConfig) { if($sEnv == $sSource) continue; if( array_key_exists('css', $aConfig) && array_key_exists($sApp, $aConfig['css']) && !is_array($aConfig['css'][$sApp]) && null !== $aConfig['css'][$sApp]) { try { $oAssetCSS->creerAsset($aConfig['css'][$sApp]); } catch(clsAssetManagerException $e) { $this->logSection('Info:', $e->getMessage()); } } } } } }
Le traitement se fera avec la commande:
> php symfony UL:compressAssets
4. Chargement des fichiers en fonction de l'environnement
Symfony se charge pour moi de choisir les fichiers en fonction de l'environnement dans lequel je suis. Mais je dois ajouter moi même les fichiers à la réponse. Je ne peux malheureusement pas utiliser les routines de Symfony, je vais donc créer des helpers spécifiques. Pour optimiser les performances, je ne vais lire qu'une seule fois le fichier de configuration depuis le cache et regrouper mes helpers dans une classe. Afin de tenter de respecter les bonnes pratiques, ma classe sera un singleton pour ne pas avoir à l'instancier dans mon Layout.
class clsAssetsLoader { protected static $oInstance; protected $aConfig; private function __construct() { $this->aConfig = clsYaml::chargerYaml('plugins/ulAssetManagerPlugin/config/assets.yml'); sfContext::getInstance()->getConfiguration()->loadHelpers('Asset'); } public static function getInstance() { if(!self::$oInstance instanceof self) self::$oInstance = new self(); return self::$oInstance; } public function getCss($psApp = 'front') { if(!array_key_exists($psApp, $this->aConfig['css'])) throw new clsAssetManagerException('L\'application: '.$psApp.' est introuvable pour les CSS.'); if(!$this->aConfig['css'][$psApp]) return null; $sHtml = ''; if(!is_array($this->aConfig['css'][$psApp])) { $sHtml = stylesheet_tag($this->aConfig['css'][$psApp]); } else { for($i = 0, $n = count($this->aConfig['css'][$psApp]); $i < $n; $i++) { $sHtml .= stylesheet_tag($this->aConfig['css'][$psApp][$i]); } } return $sHtml; } public function getJsHead($psApp = 'front') { if(!array_key_exists($psApp, $this->aConfig['js'])) throw new clsAssetManagerException('L\'application: '.$psApp.' est introuvable pour les JS.'); return (!$this->aConfig['js'][$psApp]['head'])? null : $this->getJsTag($this->aConfig['js'][$psApp]['head']); } public function getJsFoot($psApp = 'front') { if(!array_key_exists($psApp, $this->aConfig['js'])) throw new clsAssetManagerException('L\'application: '.$psApp.' est introuvable pour les JS.'); return (!$this->aConfig['js'][$psApp]['foot'])? null : $this->getJsTag($this->aConfig['js'][$psApp]['foot']); } protected function getJsTag($aFiles) { $sHtml = ''; if(!is_array($aFiles)) { $sHtml = javascript_include_tag($aFiles); } else { for($i = 0, $n = count($aFiles); $i < $n; $i++) { $sHtml .= javascript_include_tag($aFiles[$i]); } } return $sHtml; } }
Il ne reste plus qu'à modifier les Layouts en appelant nos Helpers.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <?php include_title() ?> <?php include_http_metas() ?> <?php include_metas() ?> <?php echo clsAssetsLoader::getInstance()->getCss('front');?> <?php echo clsAssetsLoader::getInstance()->getJsHead('front');?> </head> <body> ..... <?php echo clsAssetsLoader::getInstance()->getJsFoot('front');?> </body> </html>
Bien entendu il n'est plus possible d'utiliser les fichiers view.yml pour gérer les fichiers javascript et css.
Vous pouvez télecharger les sources complètes là:ulAssetManagerPlugin.tgz
Ajouter un commentaire