Automatiser le front-end dans un projet Symfony2

Dans tout projet Web, nous sommes confrontés à des tâches répétitives :

  1. Télécharger les différentes librairies ;
  2. Compiler les assets (Sass, Less, Stylus, CoffeeScript, TypeScript, …) ;
  3. Optimiser les images ;
  4. Gérer la concaténation des fichiers javascript, puis la minification ;
  5. Et beaucoup d’autres tâches…

Je vous propose donc de travailler simplement avec deux outils automatisant ces étapes: bower et grunt.

Pour une présentation plus complète de ces outils, je vous invite à visionner la conférence (slide ou vidéo) faite par ma collègue Claire Coloma lors du PHPTour Lyon 2014.

Prérequis

Avoir Node.js, bower et grunt installés.

Bower – Gestionnaire de packages

Bower est un gestionnaire de paquets pour les librairies et frameworks frontend. Autrement dit, c’est un outil qui s’occupe de récupérer les bonnes versions de chacune de vos dépendances, à l’instar de Composer dans le monde PHP.

Grunt – Gestionnaire de tâches

Grunt est un outil d’automatisation de tâches. Très complet, il est très souvent utilisé pour les tâches répétitives que les développeurs front doivent faire, afin de nous simplifier grandement la vie !

L’automatisation des tâches s’écrit en Javascript, et permet de faire plein de choses :

  • Création de lien symbolique ;
  • Concaténation de fichiers ;
  • Compilation de fichiers CoffeeScript ;
  • Compilation de fichiers TypeScript ;
  • Compilation de fichiers sass ;
  • Minification de fichiers JavaScript ;
  • Minification de fichiers CSS ;
  • Lancement d’un serveur PHP ;
  • Lancement d’un proxy livereload ;
  • Optimisation des images ;
  • Gestion de fichier (copie, suppression, création …) ;

Installation de Symfony2

$ composer create-project symfony/framework-standard-edition /path/Site

Supprimer Assetic

Avec un tel outil, nul besoin d’Assetic. Vous pouvez le supprimer dans le fichier app/AppKernel.php :

new Symfony\Bundle\AsseticBundle\AsseticBundle(),

Enfin ne pas oublier de le supprimer dans la configuration app/config.yml :

assetic:
    debug: "%kernel.debug%"
    use_controller: false
    bundles:        [ ]
    #java: /usr/bin/java
    filters:
        cssrewrite: ~
        #closure:
        #   jar: "%kernel.root_dir%/Resources/java/compiler.jar"
        #yui_css:
        #   jar: "%kernel.root_dir%/Resources/java/__-2.4.7.jar"

Dans le fichier app/config/parameters.yml :

use_assetic_controller: true

Suivi du fichier app/config_dev.php :

assetic:
    use_controller: %use_assetic_controller%

Et enfin avec la commande de composer pour supprimer du fichier composer.json :

$ composer remove "symfony/assetic-bundle"

À titre d’exemple, nous allons travailler sur le bundle de démo fourni par Symfony2.

Gestion des dépendances

Grunt s’appuyant sur différents modules, nous allons utiliser un fichier package.json permettant de définir nos dépendances Node.js.

Ouvrez votre terminal et exécutez la commande suivante :

$ npm init

Exemple d’un fichier package.json

{
  "name": "Symfony2-demo",
  "version": "0.0.1",
  "description": "Automatiser le front-end dans un projet Symfony2",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "lbrunet@jolicode.com",
  "license": "MIT",
  "dependencies": {
    "grunt": "^0.4.5",
    "load-grunt-tasks": "^0.6.0",
    "grunt-contrib-compass": "^1.0.1",
    "grunt-contrib-uglify": "^0.6.0",
    "grunt-contrib-cssmin": "^0.10.0",
    "grunt-contrib-watch": "^0.6.1",
    "grunt-contrib-copy": "^0.6.0",
    "grunt-typescript": "^0.3.8"
  }
}

Bower

Nous allons créer également un fichier bower.json pour toutes nos dépendances front-end.

Ouvrez votre terminal et exécutez la commande suivante :

$ bower init

Exemple d’un fichier bower.json

{
  "name": "Symfony2-demo",
  "version": "0.0.1",
  "authors": [
    "Laurent Brunet "
  ],
  "license": "MIT",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "app/Resources/lib",
    "test",
    "tests"
  ],
  "dependencies": {
    "bootstrap-sass-official": "~3.2.0",
    "jquery": "2.0.1",
    "Fontello": "https://github.com/BrandyMint/brandymint-fontello/archive/master.zip"
  }
}

Par défaut, Bower va créer un dossier bower_components pour y stocker nos dépendances. Nous pouvons décider de définir une autre structure que celle par défaut, il vous suffit de créer un fichier .bowerrc

Exemple d’un fichier .bowerrc

{
  "directory" : "app/Resources/lib",
  "json" : "bower.json"
}
  • directory : Indique dans quel répertoire de votre projet les composants Bower doivent être copiés.
  • json : Indique le nom du fichier contenant la liste des composants qui doivent être téléchargés par Bower.

Fichier .gitignore

Afin d’éviter de commiter des fichiers inutiles, nous allons mettre à jour notre fichier .gitignore en ajoutant les dossiers ajoutés par node.js et Bower.

  • /node_modules/
  • /app/Resources/lib

Installations des dépendances

Maintenant que notre projet est en place et correctement paramétré, nous allons installer nos dépendances. Pour l’exemple, nous allons utiliser :

  • Bootstrap (Sass) ;
  • jQuery ;
  • Fontello (Pack d’icônes).

Vous pouvez récupérer le fichier bower.json plus haut et lancer cette commande :

$ bower install

Grunt

Nous allons utiliser les modules suivants :

  1. grunt
  2. load-grunt-tasks
  3. grunt-contrib-compass
  4. grunt-contrib-watch
  5. grunt-contrib-cssmin
  6. grunt-contrib-copy
  7. grunt-typescript
  8. grunt-contrib-uglify

Vous pouvez récupérer le fichier package.json plus haut et lancer cette commande :

$ npm install

Pour pouvoir configurer et utiliser nos modules Grunt, nous devons créer un fichier Gruntfile.js.

Exemple d’un fichier Gruntfile.js

module.exports = function(grunt) { 
  // Importation des différents modules grunt
  grunt.loadNpmTasks('grunt-contrib-copy');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-compass');
  ...

  // Configuration des plugins
  grunt.initConfig({});

  // Déclaration des différentes tâches
  grunt.registerTask('default', ['tache1', 'tache2']);
};

Nous allons nous intéresser au module load-grunt-tasks que nous avons installé. Ce module sert au chargement des différents modules que allons utiliser dans notre projet. Auparavant, nous aurions dû charger chaque plugin individuellement dans notre fichier Gruntfile.js.

grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-compass');
...

Mais grâce à ce module, le chargement sera automatique en ajoutant seulement cette ligne dans notre fichier Gruntfile.js.

require('load-grunt-tasks')(grunt);

Exemple d’un fichier Gruntfile.js avec le module load-grunt-tasks

module.exports = function(grunt) {
  // Chargement automatique de tous nos modules
  require('load-grunt-tasks')(grunt);

  // Configuration des plugins
  grunt.initConfig({});

  // Déclaration des différentes tâches
  grunt.registerTask('default', ['tache1', 'tache2']);
};

Nous allons maintenant mettre en application nos modules dans notre fichier Gruntfile.js.

Configuration du module compass

module.exports = function (grunt) {
  grunt.initConfig({
    compass: {
      sass: {
        options: {
          sassDir: 'src/Acme/DemoBundle/Resources/scss',
          cssDir: '.tmp/css',
          importPath: 'app/Resources/lib',
          outputStyle: 'expanded',
          noLineComments: true
        }
      }
    }, //end compass
  });

  grunt.registerTask('default', ['compass']);
}

Cette tâche va permettre la compilation des fichiers Sass en fichiers CSS en utilisant Compass.

  • sassDir : Le dossier source où se trouve vos fichiers Sass.
  • cssDir : Le dossier où se trouveront vos fichiers CSS après la compilation.
  • outputStyle : Type de compression CSS.
  • importPath : Rend possible @import de tous les fichiers du chemin spécifié.
  • noLineComments : Désactive les commentaires.

Configuration du module typescript

module.exports = function (grunt) {
  require('load-grunt-tasks')(grunt);

  grunt.initConfig({
    typescript: {
      base: {
        src: ['src/Acme/DemoBundle/Resources/ts/*ts'],
        dest: '.tmp/js',
        options: {
          target: 'es5', //or es3
          module: 'amd', //or commonjs
          sourceMap: true,
          declaration: true
        }
      }
    }
  });

  grunt.registerTask('javascript', ['typescript']);
}

Cette tâche va permettre la compilation des fichiers TypeScript en fichiers JavaScript.

  • src : Le dossier source où se trouvent vos fichiers TypeScript.
  • target : Définir le type de version de ECMAScript 'ES3' (par default) ou 'ES5'.
  • dest : Le dossier où se trouveront vos fichiers javascript après la compilation. Si le nom est suivi de '.js’, alors les fichiers seront concaténés.

Configuration du module cssmin

module.exports = function (grunt) {
  require('load-grunt-tasks')(grunt);

  grunt.initConfig({
    cssmin: {
      combine: {
        options:{
          report: 'gzip',
          keepSpecialComments: 0
        },
        files: {
          'web/built/min.css': [
            '.tmp/css/**/*.css'
          ]
        }
      }
    } //end cssmin
  });

  grunt.registerTask('css', ['compass','cssmin']);
}

Le module CSSMIN sert à concaténer et compresser tous les fichiers CSS.

  • report : Le dossier source où se trouve vos fichiers TypeScript.
  • keepSpecialComments : Garder ou supprimer les commentaires spéciaux, pour tout garder 'all’ (defaut), 1 pour garder le premier commentaire, et 0 pour tout supprimer.

Configuration du module watch

module.exports = function (grunt) {
  require('load-grunt-tasks')(grunt);

  grunt.initConfig({
    watch: {
      css: {
        files: ['src/Acme/*/Resources/scss/**/*.scss'],
        tasks: ['css']
      },
      javascript: {
        files: ['src/Acme/*/Resources/ts/*.ts'],
        tasks: ['javascript']
      }
    }
  });

  grunt.registerTask('default', ['css','javascript']);
  grunt.registerTask('javascript', ['typescript']);
  grunt.registerTask('css', ['compass','cssmin']);
}

Vous pouvez maintenant lancer la commande :

$ grunt watch

Configuration du module uglify

module.exports = function (grunt) {
  require('load-grunt-tasks')(grunt);

  grunt.initConfig({
    uglify: {
      options: {
        mangle: false,
        sourceMap: true,
        sourceMapName: 'web/built/app.map'
      },
      dist: {
        files: {
          'web/built/app.min.js':[
            'app/Resources/lib/jquery/jquery.js',
            'app/Resources/lib/bootstrap-sass-official/asset/javascripts/bootstrap.js'
            '.tmp/js/**/*.js'
          ]
        }
      }
    }
  });

  grunt.registerTask('javascript', ['typescript', 'uglify']);
}

Cette tâche va permettre de minifier nos fichiers javascript. On peut aussi concaténer nos fichiers dans un ordre bien précis.

  • mangle : Empêcher que tous les noms de nos variables et fonctions soient renommées.
  • sourceMap : Activer la génération des fichiers source map.
  • sourceMapName : Définir le chemin ainsi que le nom du fichier source map.

Nous allons maintenant voir la mise en place de la librairie Fontello récupérée avec Bower. Pour cela, nous allons créer un dossier fonts dans notre projet, où nous pourrons stocker toutes les polices de notre application.

Configuration du module copy

module.exports = function (grunt) {
  require('load-grunt-tasks')(grunt);

  grunt.initConfig({
    copy: {
      dist: {
        files: [{
          expand: true,
          cwd: 'app/Resources/lib/Fontello/fonts',
          dest: 'web/fonts',
          src: ['**']
        }]
      }
    }
  });

  grunt.registerTask('cp', ['copy']);
}

Avec le module copy, nous allons pouvoir copier notre font dans le dossier web/fonts. Vous pouvez lancer la commande, si le dossier n’existe pas, il sera créé automatiquement :

$ grunt cp

Pour utiliser notre lib Fontello, on va utiliser la tâche cssmin afin de combiner cette librairie avec notre propre fichier .scss. Ajouter dans un de vos fichiers .scss ce code :

    @font-face {
        font-family: 'fontello';
        src: url('../fonts/fontello.eot?65928365');
        src: url('../fonts/fontello.eot?65928365#iefix') format('embedded-opentype'),
        url('../fonts/fontello.woff?65928365') format('woff'),
        url('../fonts/fontello.ttf?65928365') format('truetype'),
        url('../fonts/fontello.svg?65928365#fontello') format('svg');
        font-weight: normal;
        font-style: normal;
    }
       
    [class^="fontello-icon-"]:before, [class*=" fontello-icon-"]:before {
        font-family: "fontello";
        font-style: normal;
        font-weight: normal;
        speak: none;
        display: inline-block;
        text-decoration: inherit;
        width: 1em;
        margin-right: .2em;
        text-align: center;
        font-variant: normal;
        text-transform: none;
        line-height: 1em;
        margin-left: .2em;
    }

Nous pouvons donc utiliser la tâche cssmin afin ajouter la librairie Fontello :

/app/Resources/lib/Fontello/fontello.css

Configuration du module cssmin

cssmin: {
  combine: {
    options:{
      report: 'gzip',
      keepSpecialComments: 0
    },
    files: {
      'web/built/min.css': [
        '.tmp/css/**/*.css',
        // Nouvelle ligne à ajouter
        'app/Resources/lib/Fontello/fontello-codes.css'
      ]
    }
  }
},

Dans votre template Twig, vous pouvez utiliser :

<i class="fontello-icon-ambulance"></i>

Remettons à jour le fichier .gitignore :

  • /.tmp
  • /.sass-cache
  • /node_modules/
  • /app/Resources/lib
  • /web/built
  • /web/fonts

Vers l’infini et au-delà !

À l’avenir, nous allons continuer d’utiliser Grunt et Bower dans tous nos projets. Ces outils nous apportent un gain de temps réel dans notre processus de développement.

Grunt et Bower ont eu un impact énorme sur le développement d’applications côté front-end. Nous pouvons nous concentrer sur le développement d’applications Web sans avoir à télécharger manuellement toutes librairies, et pouvons effectuer toutes les tâches répétitives simplement. Cela n’empêche pas pour autant la veille constante sur d’autres outils dans le but d’optimiser et d’élargir notre workflow.

Nous avons donc pu voir Gulp, Brunch mais surtout notre choix s’est porté sur Yeoman, un outil très intéressant qui s’appuie sur d’autres librairies, et dont nous parlerons prochainement.

blog comments powered by Disqus