IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Tutoriel pour apprendre à utiliser l'outil JavaScript Grunt - Configurer les tâches

Avec le précédent tutoriel sur l'API Grunt, vous avez eu un aperçu de comment accéder à la configuration et aux fichiers. Dans ce tutoriel, vous allez mettre en pratique ces connaissances dans le cadre de la configuration des tâches.

À la fin de votre lecture, vous saurez comment configurer les tâches Grunt. Vous aborderez en particulier les multitâches, ou tâches à cibles multiples, qui offrent des fonctionnalités intéressantes pour la manipulation de fichiers. Dès lors, vous serez prêt pour aborder la création des tâches Grunt.

6 commentaires Donner une note à l´article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Que contient ce tutoriel ?

Ce tutoriel introduit d'abord quelques généralités sur la configuration des tâches :

Passées ces généralités, nous entrerons dans le détail de la configuration.

D'abord ce qu'offre Grunt en termes de configuration pour toutes les tâches :

Puis, ce qui est spécifique aux multitâches :

II. Configurer une tâche

II-A. Comment ?

Dans tout code JavaScript où l'objet grunt existe, il est possible d'accéder à l'objet de configuration. Je vous renvoie aux articles précédents qui introduisent ce concept : Grunt : The JavaScript Task Runner, L'API Grunt.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
//Initialisation de la configuration
grunt.initConfig({
  uglify: {
    options: {
      banner: '/*Mon package : <%=grunt.template.today("yyyy-mm-dd")%>*/\n'
    },
    build: {
      src: 'source/monPackage.js',
      dest: 'target/monPackage.min.js'
    }
  }
});
 
//Modification de la configuration
grunt.config('uglify.build.src', 'src/mon-package.js');
grunt.config('uglify.build.dest', 'dest/mon-package.min.js');
grunt.config('uglify.build.newProperty', 123);
 
//Accès à une propriété
grunt.registerTask('default', function() {
    console.log(grunt.config('uglify.build'));
});

II-B. Pourquoi ?

Mais pourquoi se prendre la tête avec de la configuration puisqu'il est tout à fait possible de créer une tâche fonctionnelle qui s'en passe ?

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
module.exports = function(grunt) {
  grunt.registerTask('file-copy', function() {
    var srcFiles = [
      "./src/file-1.txt",
      "./src/file-2.txt",
      "./src/file-3.txt"
    ];
 
    var destFiles = [
      "./dest/file-1-copy.txt",
      "./dest/file-2-copy.txt",
      "./dest/file-3-copy.txt"
    ];
 
    for (var i=0; i<srcFiles.length; i++){
      try  {
        grunt.file.copy(srcFiles[i], destFiles[i]);
      }
      catch(e){
        console.log(e.message);
      }
    }
  });
};

L'intérêt de Grunt est l'automatisation de tâches récurrentes. Les tâches sont souvent les mêmes d'un projet à l'autre - voire apparaissent plusieurs fois au sein d'un même projet -, mais le contexte est propre à chaque situation. Configurer une tâche, c'est lui donner un niveau d'abstraction suffisant pour pouvoir être réutilisée dans un autre contexte.

Configurer une tâche permet également d'en améliorer la lisibilité en séparant les données manipulées de la logique de la tâche.

Si nous reprenons l'exemple précédent :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
module.exports = function(grunt) {
  //Initialisation de la configuration
  grunt.initConfig({
    filecopy: {
      srcFiles: [
        "./src/file-1.txt",
        "./src/file-2.txt",
        "./src/file-3.txt"
      ],
      destFiles: [
        "./dest/file-1-copy.txt",
        "./dest/file-2-copy.txt",
        "./dest/file-3-copy.txt"
      ]
    }
  });
 
  //La tâche filecopy utilise la configuration
  grunt.registerTask('filecopy', function() {
    var srcFiles = grunt.config("filecopy.srcFiles");
    var destFiles = grunt.config("filecopy.destFiles");
 
    for (var i=0; i>srcFiles.length; i++){
      try  {
        grunt.file.copy(srcFiles[i], destFiles[i]);
      }
      catch(e){
        console.log(e.message);
      }
    }
  });
};

Une tâche configurable pourra être packagée afin d'être utilisée dans un autre projet. C'est le principe des tâches-tiers mises à disposition par la communauté et que vous pouvez charger et utiliser dans vos projets.

II-C. Convention de nommage des tâches

Le nom d'une tâche est soumis à la convention de nommage suivante :

  • grunt-contrib-<nom de la tâche> : plugin officiellement supporté par l'équipe Grunt ;
  • grunt-<nom de la tâche> : plugin créé par la communauté ;
  • <nom de la tâche> : tâche personnelle.

Si une propriété de premier niveau de l'objet de configuration porte le même nom qu'une tâche déclarée, cette propriété est alors associée à cette tâche.

Vous pouvez nommer les tâches que vous créez pour vos projets comme vous le souhaitez tant que le nom de votre tâche correspond à un nom de propriété JavaScript valide, car je vous rappelle que Grunt cherchera une propriété portant le nom de votre tâche dans l'objet de configuration.

II-D. Structure de l'objet de configuration

L'objet de configuration peut contenir des propriétés nommées comme les tâches, mais également n'importe quelle donnée arbitraire pour autant qu'elle n'entre pas en conflit avec une propriété requise pour l'une des tâches.

La valeur d'une propriété n'est pas limitée aux objets JSON. Tout code JavaScript valide peut être utilisé. La configuration peut même être générée par programme.

La structure globale de la configuration est la suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
grunt.initConfig({
  simple_task: {
    // Configuration de la tâche "simple_task".
  },
  multi_task: {
    // Configuration de la tâche "multi_task".
    first_target : {
      // Configuration de la cible "first_target"
    },
    second_target : {
      // Configuration de la cible "second_target"
    }
  },
  // Propriétés arbitraires non spécifiques à une tâche.
  my_first_property: 'whatever',
  my_second_property: { /* Objet JSON */ },
  my_third_property : [ /* Tableau */ ]
});

III. Toutes les tâches

III-A. Hash

La configuration spécifique d'une tâche correspond à la valeur de la propriété portant le nom de la tâche dans l'objet de configuration de Grunt. La valeur attendue est une table de hachage : une association clé-valeur sous la forme d'un objet JavaScript. Si ce n'est pas une table de hachage cela peut quand même fonctionner, mais vous aurez quelques surprises en utilisant l'API Grunt.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
grunt.initConfig({
  simple_task: {
    prop1: 'Et',
    prop2: 1000,
    prop3 : '&',
    prop4: ['I', 'm', 'a', 'g', 'e', 's'],
    prop5 : { disco: 'Funk' }
  }
});

Toute propriété de la configuration de la tâche est accessible via l'API Grunt avec les méthodes grunt.config, grunt.config.get et grunt.config.getRaw :

 
Sélectionnez
1.
2.
3.
4.
5.
grunt.registerTask('simple_task', function() {
  console.log(grunt.config("simple_task.prop1"));
  console.log(grunt.config.get("simple_task.prop2"));
  console.log(grunt.config.getRaw("simple_task.prop4"));
});
Image non disponible

III-B. Options

Il est possible de mettre ce que l'on veut dans la configuration d'une tâche et d'y accéder via l'API dans une tâche. Par contre, les méthodes grunt.config, grunt.config.get et grunt.config.getRaw obligent à spécifier tout le chemin pour accéder à une propriété d'une tâche alors qu'il serait préférable de ne commencer la navigation qu'à partir de la configuration spécifique de la tâche.

Grunt permet de le faire via la propriété options de la configuration spécifique d'une tâche et la méthode this.options de l'API :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
module.exports = function(grunt) {
  //Initialisation de la configuration
  grunt.initConfig({
    //Configuration spécifique de la tâche "simple_task"
    simple_task: {
      //Options de la tâche
      options: {
        prop1: 'Et',
        prop2: 1000,
        prop3 : '&',
        prop4: ['I', 'm', 'a', 'g', 'e', 's'],
        prop5 : { disco: 'Funk' }
      },
      prop6: 'une autre propriété...'
    },
    prop7: 'une donnée arbitraire non liée à une tâche...'
  });
 
  grunt.registerTask('simple_task', function() {
    console.log("\n");
    console.log(this.options()); // accès à tout l'objet options
    console.log("\n");
    console.log(this.options().prop5); // accès à une propriété
  });
}
Image non disponible

De plus, Grunt propose un système de surcharge qui permet de définir des valeurs par défaut aux propriétés de l'objet options dans le corps de la tâche et ces valeurs sont remplacées par celles de la configuration si elles existent :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
module.exports = function(grunt) {
  //Initialisation de la configuration
  grunt.initConfig({
    //Configuration spécifique de la tâche "simple_task"
    simple_task: {
      //Options de la tâche
      options: {
        prop1: 'Et',
        prop2: 1000,
        prop3 : '&',
        prop4: ['I', 'm', 'a', 'g', 'e', 's']
      },
      prop6: 'une autre propriété...'
    },
    prop7: 'une donnée arbitraire non liée à une tâche...'
  });
 
  grunt.registerTask('simple_task', function() {
    // Configuration par défaut
    var options = this.options({
      new_property: 22/7,
      prop1: 'É',
      prop5 : { disco: 'Funk' }
    });
 
    console.log(options);
  });
}
Image non disponible

La configuration par défaut de la tâche a bien été surchargée par la configuration initialisée avec grunt.initConfig :

  • new_property et prop5 n'ont pas été surchargées et conservent leur valeur par défaut ;
  • prop1 est redéfinie et sa valeur passe de 'É' à 'Et'.

III-C. Templates

Un template, spécifié par les délimiteurs <%= %>, est évalué récursivement et automatiquement :

Toutes ces méthodes appellent en interne la méthode grunt.template.process pour résoudre chaque template récursivement.

Un template peut s'exprimer comme :

  • la valeur d'une autre propriété ou sous-propriété de l'objet de configuration. Dans ce cas, l'objet de configuration est le contexte dans lequel les valeurs des propriétés peuvent être résolues. L'évaluation étant récursive, si la propriété référencée dans le template renvoie un template, celui-ci est également évalué ;
  • du code JavaScript valide arbitraire : un appel à une méthode de base du JavaScript ou de l'objet grunt, un appel à du code que vous avez écrit, etc.

Un template est toujours entre quotes ou doubles-quotes. Cependant, la valeur retournée à l'évaluation est du type de l'expression passée dans le template (string, int, objet, tableau…).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
module.exports = function(grunt) {
  grunt.initConfig({
    // Donnée arbitraire
    value: 123.45,
    // Configuration de "first_task"
    first_task: {
      options: {
        value: "<%= value %>",
        today: "<%= grunt.template.today('yyyy-mm-dd') %>"
      }
    },
    // Configuration de "second_task"
    second_task: {
      options: {
        value: "<%= first_task.options.value %>"
      }
    }
  });
 
  grunt.registerTask('first_task', function() {
    console.log(grunt.config("value")); // 123.45
    console.log(this.options().value);  // 123.45
    console.log(this.options().today);  // 2014-10-01
  });
 
  grunt.registerTask('second_task', function() {
    console.log(this.options().value * 2);  // 246.9
    /*
      initialement : "<%= first_task.options.value %>" * 2
      évaluation 1 : "<%= value %>" * 2
      évaluation 2 : 123.45 * 2 = 246.9
    */
  });
}

III-D. Données externes

Grâce à l'API Grunt, il est possible de lire des fichiers. Les méthodes de grunt.file peuvent être appelées dans les tâches, mais aussi dans la configuration grâce aux templates.

Les méthodes grunt.file.readJSON et grunt.file.readYAML permettent d'importer directement des données JSON ou YAML.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
// data.json
{
  name: 'My package',
  description: 'This is my package which does so much things.',
  prefix: 'my.package',
  version: '2.1.0'
}
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
grunt.initConfig({
  pck: '<%= grunt.file.readJSON('data.json') %>',
  minify: {
    name: '<%= pck.name %>',
    description: '<%= pck.description %>',
    src: '<%= pck.prefix %>-<%= pck.version %>.js',
    dest: '<%= pck.prefix %>-<%= pck.version %>.min.js',
  }
});

IV. Multitâche ou tâche « à cibles multiples »

Les multitâches, en plus de bénéficier des possibilités de configuration énoncées précédemment, disposent de mécanismes supplémentaires pour gérer les options et les fichiers. Mais tout d'abord, qu'est-ce qu'une multitâche ?

IV-A. Targets

Au lancement d'une tâche, Grunt cherche une propriété de configuration au nom de la tâche. Les multitâches, elles, peuvent avoir plusieurs sous-configurations appelées cibles ou « targets ». Si une target est spécifiée, la tâche s'exécute avec la configuration de la target. Si aucune target n'est spécifiée, la tâche est exécutée pour chaque target.

Prenons comme exemple une tâche compile dont la configuration varie en fonction de l'environnement de travail :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
grunt.initConfig({
  compile: {
    dev: {
      // Configuration pour le développement
    },
    test: {
      // Configuration pour l'intégration
    },
    uat: {
      // Configuration pour l'UAT
    },
    preprod: {
      // Configuration pour la préproduction
    }
    prod: {
      // Configuration pour la production
    }
  }
});

La tâche compile:dev est exécutée avec la configuration de développement, alors que la tâche compile:prod s'exécutera avec la configuration de production. Par contre, exécuter la tâche compile revient à exécuter séquentiellement les tâches compile:dev, compile:test, compile:uat, compile:preprod et compile:prod.

IV-B. Options

Nous avons vu plus tôt l'objet de configuration optionsOptions accessible dans le contexte d'une tâche via la méthode this.options. Un objet options par défaut peut être spécifié dans le code de la tâche et ses propriétés sont surchargées par celles de l'objet options de la configuration de la tâche.

Avec les targets, il y a un niveau supplémentaire de surcharge :

  • implémentation de la tâche : options par défaut ;
  • configuration de la tâche : surcharge (1) ;
  • configuration de la target : surchage (2)
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
module.exports = function(grunt) {
  grunt.initConfig({
    multi_task: {
      // options au niveau de la tâche "multi_task"
      options: {
        foo: 0,
        bar: "highfive"
      },
      first_target: {
        // options au niveau de la target "first_target"
        options: {
          foo: 1,
          oque: false
        }
      },
      second_target: {
        // pas d'option au niveau de la target "second_target"
      }
    }
  });
 
  grunt.registerMultiTask("multi_task", "une tâche à cibles multiples", function() {
    var options = this.options({
      foo: 2,
      bar: "clap your hands !",
      oque: true
    });
 
    console.log(options);
  });
}
Image non disponible

IV-C. Fichiers

Il y a trois façons de décrire les correspondances de fichiers source-destination. N'importe quelle tâche saura interpréter ces formats, donc à vous de choisir celui qui convient le mieux à vos besoins. Dans le contexte d'une tâche, les correspondances de fichier source-destination sont accessibles via le tableau d'objets this.files.

Si les fichiers ciblés sont peu nombreux et clairement identifiés, il est simple de spécifier tous les chemins. Comme ce n'est généralement pas le cas, Grunt supporte un certain nombre de motifs via les packages node-glob et minimatch.

IV-C-1. Motifs

Les caractères génériques usuels :

  • * correspond à un nombre quelconque de caractères à l'exception de /

     
    Sélectionnez
    'f*' // correspond aussi bien à 'f', 'fo', 'foo' ou 'foo.js', mais pas à 'f/'
  • ? correspond à un caractère à l'exception de /

     
    Sélectionnez
    'f?' // correspond aussi bien à 'fa' ou 'fo', mais ni à 'f/', ni à 'foo'
  • ** correspond à un nombre quelconque de caractères, / inclus tant qu'il est l'unique caractère d'un segment du chemin

     
    Sélectionnez
    'f**' // correspond aussi bien à 'f', 'foo.js' ou 'foo/bar.js'
  • {} délimite une liste d'expression 'OU' à séparateur virgule

     
    Sélectionnez
    '{foo,bar}.js' // correspond à 'foo.js' ou 'bar.js'
  • ! au début d'un motif correspond à la négation du motif
 
Sélectionnez
'!{foo,bar}.js' // correspond à tout fichier .js sauf 'foo.js' et 'bar.js'

IV-C-2. Format compact

Les associations src-dest sont représentées sous la forme de deux propriétés src et dest. Ce format est couramment utilisé pour les tâches « lecture seule » où seule la propriété src est requise.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
grunt.initConfig({
  jshint: {
    foo: {
      src: ['src/aa.js', 'src/aaa.js']
    }
  },
  concat: {
    bar: {
      src: ['src/bb.js', 'src/bbb.js'],
      dest: 'dest/b.js'
    }
  }
});

IV-C-3. Format objet

Les associations src-dest sont représentées par un objet files. Ses propriétés sont les destinations et leur valeur, un tableau de sources.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
grunt.initConfig({
  concat: {
    foo: {
      files: {
        'dest/a.js': ['src/aa.js', 'src/aaa.js'],
        'dest/a1.js': ['src/aa1.js', 'src/aaa1.js']
      }
    },
    bar: {
      files: {
        'dest/b.js': ['src/bb.js', 'src/bbb.js'],
        'dest/b1.js': ['src/bb1.js', 'src/bbb1.js']
      }
    }
  }
});

IV-C-4. Format tableau

Les associations src-dest sont représentées par un tableau files. Chaque élément du tableau est un objet au format compact.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
grunt.initConfig({
  concat: {
    foo: {
      files: [
        {src: ['src/aa.js', 'src/aaa.js'], dest: 'dest/a.js'},
        {src: ['src/aa1.js', 'src/aaa1.js'], dest: 'dest/a1.js'}
      ]
    },
    bar: {
      files: [
        {src: ['src/bb.js', 'src/bbb.js'], dest: 'dest/b/', nonull: true},
        {src: ['src/bb1.js', 'src/bbb1.js'], dest: 'dest/b1/', filter: 'isFile'}
      ]
    }
  }
});

IV-C-5. Propriétés additionnelles

Le format compact supporte d'autres propriétés que src et dest. Le format tableau étant un dérivé du format compact, il supporte également ces propriétés.

  • filter string ou function : un nom de méthode fs.Stats valide ou une fonction prenant en argument un chemin de fichier et retournant un booléen.
  • nonull bool : Si true, conserve les motifs, même s'il n'y a aucune correspondance.
  • dot bool : Si true, autorise les motifs à remonter les fichiers commençant par un ., même si le motif ne le permet pas explicitement.
  • matchBase bool : Si true, les motifs sans / fonctionnent comme s'ils étaient préfixés de **/.
  • expand bool : Si true, autorise la génération dynamique de la correspondance de fichiers.

Pour gérer un grand nombre de fichiers, il est possible de construire dynamiquement la liste de fichiers en mettant la propriété expand à true. Dès lors, il est possible d'utiliser les propriétés suivantes :

  • cwd string : toutes les correspondances de sources src sont relatives à ce chemin sans l'inclure ;
  • src string array : motif(s) de correspondance, relatifs à cwd ;
  • dest string : préfixe du chemin de destination ;
  • ext string : remplace toute extension existante par cette valeur dans les chemins de destination générés ;
  • extDot string : indique où le point de l'extension se situe. Peut prendre les valeurs 'first' ou 'last', et est mis par défaut à 'first'. 'first' signifie que l'extension commence après le premier point du nom de fichier ; 'last', après le dernier point ;
  • flatten bool : supprime toute l'arborescence de destination dest générée ;
  • rename function : appelée pour chaque fichier src après le renommage de l'extension et l'aplanissement (cf. ext et flatten). Le dest et le chemin src correspondant sont passés à la fonction et la fonction doit retourner une nouvelle valeur dest. Si le même dest est retourné plusieurs fois, chaque src qui correspond sera ajouté à un tableau de source pour cette destination.

D'autres propriétés sont supportées. Pour une liste exhaustive, voir la documentation des packages node-glob et minimatch.

Prenons l'arborescence de répertoires et de fichiers suivante :

Image non disponible

La tâche sample affiche les sources, et pour la target expand les destinations.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
module.exports = function(grunt) {
  grunt.initConfig({
    sample: {
      filter: {
        src: [ 'src/**/*' ], // motif sur la totalité de l arborescence src
        filter: 'isFile'     // filtre sur les fichiers
      },
      nonull: {
        src: [ 'unknown.js' ], // motif sans correspondance
        nonull: true           // le motif remonte quand même dans this.files
      },
      expand: {
        files: [
          {
            expand: true,     // activation de la correspondance dynamique
            cwd: 'src/',      // chemin de la source
            src: ['**/*.js'], // motif des fichiers .js de l arborescence src
            dot: true,        // les fichiers commençant par . sont pris en compte
            dest: 'dest/',    // chemin de destination
            ext: '.min.js',   // remplace l extension '.min.js'
            extDot: 'last',   // si 'first', '.period.js' devient '.min.js'
            flatten: false    // si true, 'src/{folder(s)}/{file}.{ext}' devient
                              // 'dest/{file}.{ext}' au lieu de 'dest/{folder(s)}/{file}.{ext}'
          }
        ]
      }
    }
  });
 
  grunt.registerMultiTask("sample", "Gestion des fichiers", function() {
    this.files.forEach(function(file) {
      if (this.target == "expand"){
        console.log(JSON.stringify(file.src) + " >> " + file.dest);
      }
      else{
        console.log(file.src);
      }
    }.bind(this));
  });
}
Image non disponible

V. Et après ?

Dans ce tutoriel, vous avez pu mettre en application une partie de vos acquis précédents sur l'API Grunt. Vous avez appris à configurer les tâches Grunt, en particulier pour la gestion des fichiers en mode src-dest, source-destination.

Le prochain tutoriel de la série consacrée à Grunt abordera la création de tâches Grunt !

VI. Remerciements

Nous remercions la société SOAT qui nous a autorisés à publier ce tutoriel.

Nous remercions également Winjerome pour la mise au gabarit et Claude Leloup pour la relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2016 Nourdine FALOLA. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.