All posts in Javascript

Décorateurs TypeScript et Web Components

Chez DocDoku nous animons des formations sur les outils et frameworks pour le développement front-end. Les trois principaux acteurs du marché sont aujourd’hui React, Vue et Angular. Ils offrent tous les trois la possibilité d’écrire du code de manière déclarative pour créer nos interfaces.Néanmoins, si nous devions réaliser une petite application, choisir un de ces framework pourrait paraître surdimensionné. Alors pourquoi ne pas réaliser un petit framework maison ? Il y a en effet plusieurs technologies à notre disposition pour ce faire. Nous vous proposons d’en étudier quelques-unes dans cet article.

Les deux technologies que nous allons aborder sont les Web Components (suite de technologies HTML5) et les décorateurs TypeScript qui vont nous permettre d’effleurer la surface de la réalisation d’un framework.

Les Web Components

Les Web Components sont un regroupement de trois technologies HTML5 :

  • Les custom elements
  • Les shadow root
  • Les templates

La principale caractéristique des Web Components est la possibilité de créer des éléments personnalisés, par exemple enrichir des éléments existants comme un champ de texte, un bouton, etc. C’est aussi la possibilité de créer de nouveaux éléments HTML (balises) avec un apport sémantique dans la construction de nos applicatifs, par exemple une nouvelle balise HTML my-todo-list qui construirait elle même ses sous-éléments. Nous parlons alors de custom elements.

Voici le code le plus simple pour définir une nouvelle balise :


class MyTodoList extends HTMLElement {
  constructor() {
    super();
  }
}
customElements.define("my-todo-list", MyTodoList);

Une autre caractéristique notable est l’isolement (encapsulation) de l’HTML sous-jacent. Notre nouvelle balise et son contenu enfant peuvent être totalement « détachés » du reste de la page et ainsi ne subir aucun impact du reste du DOM, avoir sa propre feuille de style locale, et ne pas impacter les autres éléments voisins. C’est le rôle du shadow DOM.

Finalement nous pouvons nous passer de l’écriture fastidieuse en JavaScript de la construction des éléments enfants en utilisant la balise template. Cette balise nous permet d’écrire le contenu de nos éléments personnalisés directement en HTML. Aussi nous pouvons combiner les templates avec l’utilisation de la balise slot afin de rendre nos templates paramétrables.

Utilisation de la balise template :



 

Instanciation d’un élément depuis ce template :


let template = document.getElementById("my-todo-list-template");
let templateContent = template.content;
document.body.appendChild(templateContent);

Exemple complet avec ces trois technologies :



   Hello todo list 1




class MyTodoList extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('my-todo-list-template');
    const templateContent = template.content;

    this.attachShadow({ mode: 'open' }).appendChild(
      templateContent.cloneNode(true)
    );
  }
}
customElements.define('my-todo-list',MyTodoList)

Les décorateurs TypeScript

TypeScript est une extension du langage JavaScript qui nous apporte le confort des contraintes. Etudions ce code JavaScript :

var i = 2
i = "hello"

Nous ne savons pas lors de l’écriture du code si la variable i est un nombre ou une chaine de caractères et cela peut être fatal si nous ne faisons pas l’effort de vérifier son type lors de son utilisation.

Avec TypeScript nous sommes contraints à garder le type défini lors de l’initialisation.

let i : number = 2
i = "hello" // Interdit

Cet exemple nous montre seulement un des aspects de TypeScript. Ce qui nous intéresse ici est l’apport des décorateurs.

Imaginons que nous souhaitions automatiser l’exécution de code pour deux classes différentes. Par exemple une classe Voiture et une classe Animal qui auraient toutes les deux leur table associée dans la base de données.


class Animal {
  static {
    // code pour initialiser la table ANIMAL
  }
}

class Voiture {
  static {
    // code pour initialiser la table VOITURE
  }
}

Dans cet exemple, difficile de mutualiser du code, nos classes ont un bloc d’initialisation static qui peut contenir de plus en plus de choses, difficile de maintenir, etc.

Avec les décorateurs TypeScript nous pouvons imaginer écrire nos classes différemment :

@CreateTable("ANIMAL")
class Animal {
}

@CreateTable("VOITURE")
class Voiture {
}

Et pourquoi pas leur ajouter d’autres fonctionnalités, par exemple l’utilisation d’un utilitaire de log différent :

@CreateTable("ANIMAL")
@Logger(SomeLogger)
class Animal {
}

@CreateTable("VOITURE")
@Logger(AnOtherLogger)
class Voiture {
}

Derrière ces décorateurs se cachent en fait de simples fonctions. Celles-ci ne sont exécutées qu’une fois au chargement de la classe, et non lors de nouvelles instances de celles-ci.

const CreateTable = (name: string) => {
  return (target: Function) => {
    console.log(name)    // Le nom passé en paramètre, ici "ANIMAL"
    console.log(target)  // L'objet de type class, ici class Animal
    console.log(Object.getOwnPropertyDescriptors(target.prototype)) // Liste des méthodes
  };
};


@CreateTable("ANIMAL")
class Animal {
    _age: number
    get age(){ return this._age }

    manger(){
        //...
    }
}

/*
ANIMAL
[class Animal]
{
  constructor: {
    value: [class Animal],
    writable: true,
    enumerable: false,
    configurable: true
  },
  age: {
    get: [Function: get age],
    set: undefined,
    enumerable: false,
    configurable: true
  },
  manger: {
    value: [Function: manger],
    writable: true,
    enumerable: false,
    configurable: true
  }
}
*/

Début d’un framework basé sur ces deux technologies

En combinant les Web Components et les décorateurs TypeScript, nous pouvons alors imaginer réaliser un début de framework maison inspiré d’Angular.

Avec Angular, la création d’un composant est déclarative, et son utilisation très simple depuis un fichier HTML :


@Component({
  selector: 'app-my-component',
  template: '

{{ title }}

', styles: ['h1 { font-weight: normal; }'] }) class MyComponent { title = "Hello world" }

Nous allons nous inspirer de cela pour écrire un début de framework. Nous avons besoin d’un décorateur qui va créer automatiquement son shadow DOM depuis le template passé dans les arguments du décorateur « Component ».


// Paramètres du décorateur
interface ComponentOptions {
  selector: string; // Nom de la nouvelle balise
  template: string; // Le template associé
  style: string; // Un peu de css
}

// Fonction décorateur
function Component(options: ComponentOptions) {
  return function < T extends { new (..._args: any[]): HTMLElement } >(
    target: T
  ) {
    const cls = class extends target {
      constructor(..._args: any[]) {
        super();

        // Creation du shadow DOM
        let root = this.attachShadow({ mode: "open" });

        // Création des balises enfants avec EJS (moteur de template)
        root.innerHTML = options.template;

        // Ajout de style local
        let style = document.createElement("style");
        style.textContent = options.style;
        root.appendChild(style);
      }
    };

    // Enregistrement automatique en tant que custom element
    customElements.define(options.selector, cls);

    return cls;
  };
}

@Component({
  selector: "my-todo-list",
  template: `
   

<%= title %>

  • Do this
  • Do that
`, style: `h2 {color: blue}`, }) export default class HelloWorld extends HTMLElement { title = "Hello world !"; }

Bien entendu, notre framework maison est encore loin d’être complet. Nous aurions besoin d’y ajouter une détection des changements, du routing, une couche HTTP, etc. Seule la partie de création de composants et d’affichage a été abordée dans cet article.

ElasticSearch ILM et répartition des données

Description

La gestion d’une centaine de milliards de documents (données issues de logs applicatifs, équipements réseau, middlewares, etc.) s’avère coûteuse (machines, disques, espace de sauvegarde, etc.).
Pour alléger ce coût d’infrastructure nous devons distinguer les données brûlantes des données tièdes et froides (voir gelées). C’est ce que propose ElasticSearch avec l’ILM (Index Lifecycle Management). Nous vous proposons ainsi d’analyser sa mise en œuvre sur un cluster de production d’un de nos clients.
Les données considérées brûlantes doivent être exploitables dans des temps de réponse très courts, nous devons donc les placer sur des machines dimensionnées correctement. Les requêtes sur les données tièdes et froides n’ont pas besoin d’être aussi performantes, ces données peuvent donc automatiquement être déplacées vers des machines plus modestes grâce aux règles de rétention offertes par ILM.

Limitations des écrans dans Kibana et Cerebro

Kibana, l’interface graphique de la suite Elastic ne propose pas une vue synthétique de cette répartition. Nous pouvons obtenir cette information de répartition grâce à plusieurs requêtes, mais nous devons alors croiser les résultats, ce qui est fastidieux.
Cerebro, une interface graphique issue de la communauté open source, est excellent dans la visualisation de la répartition de la charge, mais ne propose pas cette visualisation au niveau du cycle de vie.
Même constat pour Elasticvue, qu’il s’agisse de sa version desktop, extension Chrome ou webapp.
Un moyen d’obtenir les informations souhaitées est donc de passer par l’API d’ElasticSearch.
Avec une première API nous pouvons obtenir les rôles des machines :

GET _nodes/settings?filter_path=nodes.*.roles,nodes.*.name

{
  "nodes": {
    "ECNRRDP2SUKmcu3s9qJgnA": {
      "name": "es3",
      "roles": [
        "data_cold",
        "ingest",
        "master"
      ]
    },
    "NXZNCa_BQ_SE613oSnDf-g": {
      "name": "es6",
      "roles": [
        "data_cold",
        "ingest",
        "master"
      ]
    },
    "jqmdwzdeQHG84601oQjpGw": {
      "name": "es1",
      "roles": [
        "data",
        "data_hot",
        "ingest",
        "master"
      ]
    },
    "4z2k13n8SZS4t2JmLielaQ": {
      "name": "es4",
      "roles": [
        "data",
        "data_hot",
        "ingest",
        "master"
      ]
    },
    "AFRMIIagRWK4Cu-ZhbLH2A": {
      "name": "es5",
      "roles": [
        "data_warm",
        "ingest",
        "master"
      ]
    },
    "sxljzKF-Q1app0VSkVgTxg": {
      "name": "es2",
      "roles": [
        "data_warm",
        "ingest",
        "master"
      ]
    }
  }
}

Puis avec une deuxième API nous pouvons récupérer le cycle de vie des indices (index au pluriel).

GET /*/_settings?filter_path=*.settings.index.routing.allocation.include,*.settings.index.uuid,*.settings.index.provided_name

{
  ".ds-my-index-1-2024.09.17-003772": {
    "settings": {
      "index": {
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_hot"
            }
          }
        },
        "provided_name": ".ds-my-index-1-2024.09.17-003772",
        "uuid": "HyELPU6oTpC_EfskAxP5BQ"
      }
    }
  },
  ".ds-my-index-1-2024.09.17-003748": {
    "settings": {
      "index": {
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_warm,data_hot"
            }
          }
        },
        "provided_name": ".ds-my-index-1-2024.09.17-003748",
        "uuid": "QNC-Z2loT4SCKFb2-LJKAw"
      }
    }
  },
  ".ds-my-index-1-2024.09.17-003714": {
    "settings": {
      "index": {
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_cold,data_warm,data_hot"
            }
          }
        },
        "provided_name": ".ds-my-index-1-2024.09.17-003714",
        "uuid": "zIP72E-qTBSfKbZdAYmFEQ"
      }
    }
  },
  [...]
}

Et avec une troisième API nous pouvons restituer le détails des blocs de données correspondant aux indices :

GET /_cat/shards?format=json&h=index,node,state,prirep,docs

[
  {
    "index": ".ds-my-index-1-2024.09.17-003848",
    "node": "es1",
    "state": "STARTED",
    "prirep": "p",
    "docs": "120"
  },
  {
    "index": ".ds-my-index-1-2024.09.17-003848",
    "node": "es4",
    "state": "STARTED",
    "prirep": "r",
    "docs": "47"
  },
  {
    "index": ".ds-my-index-1-2024.09.17-003828",
    "node": "es5",
    "state": "STARTED",
    "prirep": "p",
    "docs": "29"
  },
  {
    "index": ".ds-my-index-1-2024.09.17-003828",
    "node": "es2",
    "state": "STARTED",
    "prirep": "r",
    "docs": "147"
  },
  [...]
]

En croisant les résultats de ces trois requêtes, nous pouvons donc savoir si un bloc de données d’un index « brûlant » est bien placé sur un noeud « brûlant ». Mais pour des milliers d’index, cela devient beaucoup plus laborieux.

Développement d’une solution de visualisation

Quelques lignes de JavaScript (64), d’HTML (80) et de CSS (118) plus tard et voici un aperçu d’un rapport réalisé sur un cluster ElasticSearch en local.

ElasticSearch ILM test

Résultat de l’analyse du cluster local

L’outil génère un fichier de rapport en format HTML, consultable depuis n’importe quel navigateur. Alors, en un coup d’œil on peut savoir si un bloc est à la bonne place.
Le code source est disponible sur Github.
Pour résumer ce que fait techniquement ce code source :

  • Lancement des 3 requêtes précédemment étudiées
  • Injection de ces données dans un moteur de template HTML
  • Sauvegarde du résultat dans un fichier HTML

Premier rapport réalisé sur une infrastructure de production et analyse

Ensuite, nous avons intégré l’outil dans une chaîne d’intégration continue de Gitlab pour automatiser la génération des rapports. Nous pouvons ainsi désormais nous interfacer avec le cluster à analyser et donc générer un premier rapport.
Quelques données sur la taille du cluster :

  • 10 machines ElasticSearch
  • 110 milliards de documents
  • 125 To de données stockées
  • 25 000 événements par seconde

Le premier rapport montre une répartition de la charge logique comme ci-dessous :

ElasticSearch ILM

Résultat de l’analyse du cluster de production

Les nœuds ayant le rôle data_warm reçoivent bien les blocs data_warm. Les nœuds ayant le rôle data (tout les rôles) reçoivent tout type de blocs.
Cependant ce n’est pas encore optimisé pour réduire les coûts car :

  • Il reste des blocs data_warm non alloués à des nœuds data_warm car ElasticSearch cherche l’équilibre en termes de nombre de blocs par machine.
  • Aucun bloc de données froides n’apparaît car les règles de rétention ne définissent pas de phase « cold ». Des nœuds data_cold doivent être ajoutés au cluster.
  • Il y a légèrement trop de données brûlantes en proportion, la durée de rétention en phase « hot » doit être revue à la baisse.

Evolution du cluster

Ainsi, après réflexions avec les équipes en charge de la maintenance du cluster, nous avons définit la cible à atteindre :

  • 3 nœuds cold (à venir)
  • 5 nœuds warm (node-3 node-9 node-10 node-1 node-2)
  • 2 nœuds hot warm master (node-4 node-5)
  • 3 nœuds hot ingest master content (node-6 node-7 node-8)

Les raisons de ces choix dépendent des caractéristiques des machines à disposition (CPU, disques, RAM).
Les mouvements de blocs vont être nombreux, et nous réfléchissons déjà à la procédure de migration afin de perturber au minimum le service. Il faut bien prendre en compte que l’espace disque pris par chaque bloc est de 50Go, et que chaque déplacement prend entre 30 minutes et 1 heure sur cette infrastructure réseau.
Nous avons ensuite planifié l’exécution de l’outil pour fournir un rapport tous les jours afin de suivre l’évolution de la répartition. Nous aurons donc un joli jeu de couleurs d’ici quelques semaines 😉

8 thématiques incontournables à connaître pour maîtriser HTML5

L’HTML va bientôt fêter ses 30 ans. Ce langage de balisage a beaucoup évolué depuis sa création, avec l’intégration de JavaScript et CSS (Cascading Style Sheets) qui sont venus grossir les rangs des technologies web.
Et côté utilisateurs, dynamiser dans son ensemble la navigation web.

Développer des applications web modernes comme le permet HTML5 et CSS3 ne s’improvise pas.
Voici le top 8 des sujets incontournables à connaître pour maîtriser ce langage :

  • Une brique essentielle : JavaScript
  • Les API de communication
  • Le stockage côté client
  • Graphisme et multimédia
  • Les nouveaux tags HTML5
  • La présentation avec CSS3
  • Architecture et conception
  • L’outillage et l’environnement de développement

Vous êtes Architectes, développeurs ou webmasters et certains de ces sujets vous échappent encore ?

Nous avons une piste pour vous permettre de mettre à jour rapidement vos compétences : rejoignez notre prochaine session de formation Développer des applications HTML 5.

Prochaines sessions HTML5 à Toulouse :
Du 26 au 28 février 2020 (dernières places disponibles!)
Du 18 au 20 mai 2020

Prochaines sessions HTML5 à Paris :
Du 22 au 24 janvier 2020
Du 18 au 20 mars 2020

 

Le meilleur d’AngularJS et de Cordova : ngCordova

 cta-image

ngCordova

Si vous voulez développer rapidement une application Phonegap/Cordova sans vouloir utiliser toute la stack mean.io ou ionic, je vous conseille vivement ngCordova.

L’idée est d’encapsuler des plugins Phonegap/Cordova dans des services AngularJS. Mais dans quel but exactement… ?

Ne pas ré-inventer la roue

Bénéficiant d’une communauté active, ngCordova implémente la plupart des plugins nécessaires (photo, network, vidéo…). Trouver son bonheur parmi tous ces plugins n’est donc pas chose difficile. En quelques étapes d’installation, notre plugin est prêt à être utilisé et s’intègre parfaitement dans nos contrôleurs AngularJS. Pas besoin donc de créer soi-même l’encapsulation de ces services, si ce n’est pour savoir le faire.

Installation

L’installation de ngCordova se fait de la même façon que n’importe quel autre composant bower.

bower install ngCordova

Les sources sont téléchargées et copiées dans le sous-répertoire bower_components.

bower_components/ngCordova/
├── bower.json
├── CHANGELOG.md
├── dist
│   ├── ng-cordova.js
│   ├── ng-cordova.min.js
│   ├── ng-cordova-mocks.js
│   └── ng-cordova-mocks.min.js
├── LICENSE
├── package.json
└── README.md

Les sources de ngCordova sont maintenant prêtes à être injectées dans votre application AngularJS. Pour cela, il faut renseigner le fichier dans index.html et la dépendance dans votre fichier app.js.

index.html

<script src="lib/ngCordova/dist/ng-cordova.js"></script>
<script src="cordova.js"></script>

app.js

angular.module('myApp', ['ngCordova'])

Et voila !

Utilisation

Tous les services ngCordova sont directement injectables dans vos composants AngularJS, par exemple pour utiliser le service des dialogues :

module.controller('MyCtrl', function($scope, $cordovaDialogs) {

  $cordovaDialogs.alert('message', 'title', 'button name')
    .then(function() {
      // Le bouton a été cliqué, écrire le code correspondant ...
    });

});

Et c’est la même chose pour tous les autres services : autre exemple pour utiliser le service de fichiers et des photos :

module.controller('MyCtrl', function($scope, $cordovaFile) {

    $cordovaFile.getFreeDiskSpace()
      .then(function (success) {
         // success in kilobytes
      }, function (error) {
          // error
      });

});

module.controller('MyCtrl', function($scope, $cordovaMedia) {

    var media = $cordovaMedia.newMedia("/src/audio.mp3").then(function() {
        // success
    }, function () {
        // error
    });

    media.play();

});

Il devient alors très facile d’imbriquer tous ces composants afin de construire une application HTML5 riche. Toutes les fonctionnalités de base sont couvertes et sont disponibles sur Github.

ngCordovaMocks

Tester son code sur du vrai matériel n’est pas toujours possible. Pour cela, il nous faut une solution pour tester ses composants sur notre navigateur préféré. C’est là qu’intervient ngCordovaMocks.

Comme son nom l’indique, cette librairie vas nous permettre de “mocker” les appels à certains services.

L’installation se fait de la même manière que pour ngCordova : via bower.

bower install ng-cordova-mocks

Ensuite, il faut rajouter le script dans sa page :

<script src="lib/ngCordova/dist/ng-cordova-mocks.js"></script>

Et dans son fichier app.js :

angular.module('myApp', ['ngCordovaMocks'])

Ceci dit, on ne peut pas ajouter ngCordova et ngCordovaMocks sur la même page, et il devient très pénible de devoir changer ces deux lignes à chaque changement d’environnement. Pour cela on va utiliser gulp (site officiel) et son plugin gulp-preprocess (npm) pour éviter de le faire manuellement. Pour une installation et utilisation de gulp, voir cet article.

La page html devient alors :

<!-- @if NODE_ENV='DEVICE-DEVELOPMENT' -->
<script src="lib/ngCordova/dist/ng-cordova.js"></script>
<!-- @endif -->

<!-- @if NODE_ENV='DESKTOP-DEVELOPMENT' -->
<script src="lib/ngCordova/dist/ng-cordova-mocks.js"></script>
<!-- @endif -->

Le fichier app.js devient :

// @if NODE_ENV == 'DEVICE-DEVELOPMENT'
angular.module('myApp', ['ngCordova'])
// @endif

// @if NODE_ENV == 'DESKTOP-DEVELOPMENT'
angular.module('myApp', ['ngCordovaMocks'])
// @endif

Et le gulpfile associé :

gulp.task('device-development', function() {
    gulp.src('./www/gulp_preprocess_me/*.js')
    .pipe(preprocess({context: { NODE_ENV: 'DEVICE-DEVELOPMENT'}}))
    .pipe(gulp.dest('./www/js/'));

    gulp.src('./www/gulp_preprocess_me/index.html')
    .pipe(preprocess({context: { NODE_ENV: 'DEVICE-DEVELOPMENT'}}))
    .pipe(gulp.dest('./www/'));
});

gulp.task('desktop-development', function() {
    gulp.src('./www/gulp_preprocess_me/*.js')
    .pipe(preprocess({context: { NODE_ENV: 'DESKTOP-DEVELOPMENT'}}))
    .pipe(gulp.dest('./www/js/'));

    gulp.src('./www/gulp_preprocess_me/index.html')
    .pipe(preprocess({context: { NODE_ENV: 'DESKTOP-DEVELOPMENT'}}))
    .pipe(gulp.dest('./www/'));
});

En une ligne de commande vous pouvez maintenant choisir d’utiliser la libriaire ngCordova pour un développement sur mobile, ou bien ngCordovaMocks pour un développement sur desktop.

gulp device-development
gulp desktop-development

Attention quand même au déploiement : ne surtout pas envoyer la librairie des mocks en production 🙂

Conclusion

Pour rapidement utiliser ngCordova et ses plugins, une phase de configuration et d’outillage est nécessaire et peut s’avérer un peu longue. Néanmoins, une fois cette étape réalisée, l’installation de nouveaux plugins, leur utilisation, et leur déboggage sont très simplifiés. Toutes les documentations sont disponibles sur http://ngcordova.com/docs/plugins/ et sur les dépôts des plugins. Maintenant, à vos claviers…

Architecturer ses applications JS à l’aide du pattern MVVM

Pourquoi imposer une architecture à nos applications ?

Nos projets en JavaScript deviennent de plus en plus complexes : par leur taille, par leur fonctionnement et par leur coût de développement.

Le nombre d’outils et de librairies accessibles aux développeurs ne cesse de grandir et ce n’est pas en 2015 que la cadence va s’arrêter. Il est donc de plus en plus facile de se perdre et de rendre notre code illisible, difficilement maintenable et évolutif.

Toute notre problématique est là : rendre nos applications scalables pour les :

  • Maintenir
    -> Facilement améliorer le code courant
    -> Rendre son code compréhensible de tous
  • Adapter
    -> Inclure de nouvelles librairies
    -> Etendre, changer des parties
  • Debugger
    -> Tester chaque partie de manière indépendante
  • Développer plus rapidement
    -> Chaque développeur peut travailler en parallèle.

Le pattern MVVM va nous aider à résoudre ces problèmes tout en nous simplifiant la vie.

Pattern MVVM

Le pattern MVVM est un dérivé du célèbre modèle de conception MVC.

L’ancêtre MVC

L’idée principale du MVC, signifiant Model-View-Controller, est de découper l’application en trois parties interconnectées :

  • Model: représente les données reçues du serveur
  • View: contient l’ensemble des vues affichées à l’utilisateur
  • Controller: contient la logique de l’application

MVC

Le Modèle définit la structure des données et communique avec le serveur.
La Vue affiche les informations du Modèle et reçoit les actions de l’utilisateur.
Le Contrôleur gère les évènements et la mise à jour de la Vue et du Modèle.

Nous avons un premier découpage de l’application qui nous permet déjà de répondre à certaines de nos problématiques. En identifiant clairement les parties logiques, nous pouvons plus facilement maintenir notre application et la tester.

Mais qu’en est-il de l’adaptation et de l’évolution de notre application ? Si le modèle de données est amené à changer, nous devons obligatoirement modifier l’ensemble de nos entités.

Qu’apporte alors le pattern MVVM et que signifie-t-il ?

Principes du MVVM

MVVM est l’acronyme de : Model-View-ViewModel.

On pourrait donc simplement croire qu’on a remplacé le nom Controller par ViewModel. Mais si nous reprenons le schéma précédent avec le pattern MVVM, nous obtenons les interactions suivantes :

MVVM-2

La Vue reçoit toujours les actions de l’utilisateur et interagit seulement avec le ViewModel.
Le Modèle communique avec le serveur et notifie le ViewModel de son changement.

Le ViewModel s’occupe de :

  • présenter les données du Model à la Vue,
  • recevoir les changements de données de la Vue,
  • demander au Model de se modifier.

Data Binding

La Vue n’a donc plus aucun lien avec le Model. Ainsi le ViewModel s’occupe entièrement du cycle de modification de ce dernier. Il réalise à la fois la réception et l’envoi des données à la Vue. On parle alors de « data binding ». Les informations affichées sont liées entre deux entités et mises à jour en temps réel.

Ce dernier mécanisme est la clef du pattern MVVM. Il nous permet de découpler les différentes parties de notre application en étant capable de la faire évoluer de manière modulaire.

Dans le schéma suivant, nous pouvons voir qu’un même Modèle peut notifier plusieurs ViewModel de son changement. Ainsi ces ViewModel vont à leur tour indiquer à leur Vue de se rafraîchir.

mvvm-sample-2

Si la Vue n°2 modifie le Model, la Vue n°1 sera automatiquement mise à jour.

Du fait de cette indépendance entre la Vue et le Modèle, nous pouvons modifier ou remplacer les différents composants visuels sans impacter le cœur de notre application. Nous pouvons également changer, améliorer la logique de notre application à travers le ViewModel sans toucher à la Vue et au Model.

Presenter

Le pattern MVVM est également cité comme une extension du MVP : Model-View-Presenter.

La structure de données que nous recevons du serveur n’est pas forcément celle à afficher à l’utilisateur. Il est néanmoins conseillé de garder cette structure initiale pour pouvoir l’exploiter à d’autres moments.  Le ViewModel peut présenter à la Vue, une autre structure ou des données formatées différemment.

Cet aspect du MVVM va nous permettre de facilement modifier nos services et donc notre Modèle sans toucher à la Vue. Seul le ViewModel devra se réadapter pour fournir à la Vue les mêmes informations.

Un autre avantage est la possibilité de mocker nos Webservices dans le ViewModel. Ainsi nous pouvons travailler sur notre IHM en parallèle de la mise en place de nos WebServices.

Implémentations

Il existe de nombreux frameworks Javascript, qui permettent de développer des applications MVVM et qui fournissent des outils pour réaliser un data-binding.

Voyons comment au travers de trois frameworks : KnockoutJS, AngularJS et ExtJS, nous pouvons créer une application TodoList. Notre exemple sera volontairement simple pour faire apparaître les différents mécanismes du MVVM.

Notre application sera composée de deux vues :

Vue 1 :
Un formulaire d’édition composé de :
– Un bouton : au clic une nouvelle tâche est ajoutée.
– Un input texte : affiche et édite la tâche en cours.
– Un label : affiche le nombre de tâches.

Vue 2 :
Une liste de label : chaque label affichera le titre d’une tâche.
Au clic sur un label, la tâche courante sera modifiée pour afficher dans le formulaire, le titre sélectionné.

Notre modèle sera composé de deux « classes » :

Une classe Task, pour définir une tâche, composée d’un titre

Une classe Setting contenant :
– La tâche courante à éditer.
– La liste de toutes les tâches crées.

class-2

KnockOutJS

KnockoutJS est une libraire Javascript, qui peut facilement s’intégrer à une architecture déjà existante. Elle propose plusieurs outils pour simplifier et automatiser  la mise à jour de l’UI lorsque les données changent.

Model

On définit les deux classes. Chaque propriété doit être attribuée à partir des méthodes « observables » de KnockOutJS. L’appel à ces méthodes va permettre d’enregistrer ces données pour écouter leur modification.

var Task = function (title, index) {
    this.title = ko.observable(title);
};

var Setting = function() {
    var task = new Task("new task");
    this.currentTask = ko.observable(task);
    this.tasks = ko.observableArray([task]);
};
var setting = new Setting();

ViewModel

Les ViewModel contiennent :

  • le modèle, également écouté par KnockOutJS via ko.observable.
  • des données calculées via la méthode ko.computed, qui vont permettre d’étendre le modèle de base.
  • la logique de la Vue.

Chaque ViewModel s’occupe d’une partie logique côté IHM.

// ViewModel associé au formulaire d’édition d’une tâche
var FormViewModel = function () {
    // charge le modèle dans le ViewModel
    this.setting = ko.observable(setting);
    
    // ajoute une nouvelle tâche dans la liste
    // et change la tâche courante avec celle-ci
    this.addNewTask = function() {
        var currentTask = new Task("new task");
        this.setting().tasks.push(currentTask);
        this.setting().currentTask(currentTask);
    }.bind(this);

    // retourne le nombre de tâches
    // propriété calculé, étendant le modèle
    this.remainingCount = ko.computed(function () {
        return this.setting().tasks().length;
    }.bind(this));
};

// crée le ViewModel et l’applique à la bonne vue
var formViewModel = new FormViewModel();
ko.applyBindings(formViewModel, document.getElementById("form"));

// ViewModel associé à la vue de la liste des tâches
var ListViewModel = function () {

    // charge le même modèle
    this.setting = ko.observable(setting);

    // change la tâche courante
    this.editTask = function(item) {
        this.setting().currentTask(item);
    }.bind(this);
}

// crée le ViewModel et l’applique à la bonne vue
var listViewModel = new ListViewModel();
ko.applyBindings(listViewModel, document.getElementById("list"));

View

La Vue, décrite en html, récupère les données et les méthodes depuis le ViewModel. A l’aide de la propriété data-bind, nous déclarons toutes nos liaisons à des données et des méthodes.

// formulaire d’édition d’une tâche
<div id="form">

    // méthode addNewTask définit dans le ViewModel
    <button class="add" data-bind="click: $root.addNewTask"></button>

    //bind la tâche courante depuis le ViewModel
    <input id="new-todo" data-bind="value: setting.currentTask.title , valueUpdate: 'afterkeydown' " autofocus>

    //bind le nombre de tâches ajoutées
    <strong data-bind="text: remainingCount">0</strong>

</div>


//liste des tâches
<div id="list">
    <ul id="todo-list" data-bind="foreach: tasks">
        <li>
            <label data-bind="text: title, event: { click: $root.editTask }"></label>
        </li>
    </ul>
</div>

Le tag « input » affiche et modifie la valeur currentTask.title.

L’objet currentTask est aussi affiché dans la liste, via la propriété tasks du Modèle Setting.

Ainsi lorsque l’utilisateur clique sur un tag « label », le titre de la tâche s’affiche dans l’input du formulaire. Si ce dernier modifie cette valeur, le label de la liste est automatiquement mis à jour.

AngularJS

AngularJS n’est plus vraiment à présenter. La framework JS made in Google n’implémente pas à proprement parlé le pattern MVVM. On utilise le $scope comme ViewModel. On parle alors plus de MVW, pour Model-View-Whatever. L’important ici est de séparer la Vue et le Modèle.

Model

Nous n’avons pas besoin de créer de structure de Model spécifique. Angular se rappelle des valeurs précédemment stockées et envoie un évènement si elles sont différentes. Il nous suffit alors d’instancier un objet avec les propriétés que nous voulons utiliser. On peut également utiliser les Services d’AngularJS pour initialiser nos modèles depuis un serveur et les fournir ensuite au ViewModel.

//initialisation de la première tâche
var task = {title : 'new task'};

//créer un objet global contenant
//un tableau de tâches
//et la tâche courante
var setting = {};
setting.currentTask = task;
setting.tasks = [task];

ViewModel

Côté ViewModel, nous nous rapprochons de ce que peut produire KnockOutJS. C’est pourquoi on compare souvent ces deux frameworks pour créer des applications MVVM. Nous déclarons deux ViewModel, qui garde une référence sur le même objet setting, qui est ajouté un $scope.

angular.module('myApp', [])

    //ViewModel utilisé par le formulaire
    .controller('FormViewModel', ['$scope', function($scope) {

        //charge le modèle dans le ViewModel
        $scope.setting = setting;

        //méthode d’ajout d’une tâche
        $scope.addTask = function() {
            var newtask = {title : "new task"}
            $scope.setting.tasks.push(newtask);
            $scope.setting.currentTask = newtask;
        };

        //retourne le nombre de tâches
        //la propriété calculée, permet d’étendre le modèle
        $scope.nbtasks = function() {
            var count = 0;
            angular.forEach($scope.setting.tasks, function(todo) {
                count += 1;
            });
            return count;
        };
    }])

    //ViewModel utilisé par la liste
    .controller('ListViewModel', ['$scope', function($scope) {

        //charge le même modèle
        $scope.setting = setting;

        //méthode permettant d’éditer une tâche
        $scope.editTask = function(task) {
            $scope.setting.currentTask = task;
        };
    }]);

View

Les Vues, décrites en HTML, déclarent leur ViewModel et indiquent quelles données et méthodes vont être liées. Encore une fois seule la syntaxe diffère de KnockoutJS. On utilise la propriété ng-model ou des doubles accolades pour lier le modèle à la vue.

//formulaire d’édition d’une tâche
<div ng-controller="FormViewModel">
    <span>nb tasks : {{nbtasks()}}</span>
    <form ng-submit="addTask()">
        <input class="btn-primary" type="submit" value="add new task"/>
    </form>
    <input type="text" ng-model="setting.currentTask.title" size="30"/>
</div>

//liste des tâches
<div ng-controller="ListViewModel">
    <ul>
        <li ng-repeat="task in setting.tasks">
            <span ng-click="editTask(task)">{{task.title}}</span>
        </li>
    </ul>
</div>

ExtJS 5

ExtJS dans sa version 5 propose une implémentation revisitée du MVVM . En plus du ViewModel, il introduit une quatrième entité : le ViewController.  Associé à une unique Vue, le ViewController va permettre de gérer le comportement et la logique de la Vue, alors que le ViewModel se cantonne au rôle de Presenter.

2.3.1  Model

Nous déclarons une tâche et un setting, avec la syntaxe ExtJS. Nous créons aussi  une collection de tâches et une collection de settings. Cette dernière va nous permettre de récupérer les settings n’importe où. De plus nous utilisons cette collection pour ajouter des méthodes de manipulation sur le Modèle.

//structure d’une tâche, composée uniquement d’un titre
Ext.define(‘Task’, {
    extend: 'Ext.data.Model',
    fields: [{
        name: 'title',
        type: 'string',
        defaultValue : 'new task'
    }]
});

//donnée globale de l’application
//contient la liste de toutes les tâches
//et la tâche courante
Ext.define(‘Settings', {
    extend: 'Ext.data.Model',
    fields: [{
        name: 'currentTicket',
    },{
        name: 'tasks',
    }]
});

//définit une collection de tâches
Ext.define(‘Tasks, {
    extend: 'Ext.data.Store',
    model : 'Task
    storeId : tasks
});

//définit une collection de settings
//cette collection ne contiendra qu’un seul élément
Ext.define('SettingsStore', {
    extend: 'Ext.data.Store',
    storeId : 'settings',
    model : 'Settings',

    //méthode appelée au chargement de l’application 
    load : function() {

        //ajoute un seul element Settings
        var settings = Ext.create('Settings');
        settings.set(tasks, Ext.create('Tasks'));
        this.add(settings);

        //ajoute une première tâche
        this.addTask();
    },

    //ajoute une nouvelle tâche
    addTask : function() {
        var newTask = Ext.create('Task');
        this.getAt(0).get('tasks').add(newTask);
        this.setCurrentTask(newTask);
    },

    //met à jour la tâche courante
    setCurrentTask : function(task) {
        this.getAt(0).set('currentTask', task);
    }
});

ViewModel et View Controller

Le ViewModel n’est utilisé que pour présenter les données.

Le ViewController est utilisé pour charger les données dans le ViewModel et gérer la logique de la Vue. Chaque ViewController récupère le même modèle Settings et le fournit à son ViewModel.

Ainsi les deux vues manipuleront le même objet sans interagir ensemble.

//ViewModel du composant graphique FormTask
Ext.define('FormTaskViewModel', {
    extend: 'Ext.app.ViewModel',
    alias: 'viewmodel.formtask',

    //data à binder
    data : {
        settings : null
    },

    //data “calculée” à binder
    formulas : {
        nbTasks : function(get) {
            return get('settings.tasks').count();
        }
    }
});

//ViewController du composant graphique FormTask
Ext.define('FormTaskViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.formtask',

    //méthode appelée automatiquement à l’initialisation
    init : function() {

        //récupère le model settings
        var setting = Ext.getStore('settings').getAt(0);

        //charge le modèle dans le ViewModel
        this.getViewModel().set('settings', setting);
    },

    onClickCreateButton : function(cmp) {
        //ajoute une nouvelle task
        this.getViewModel().get('settings').store.addTask();
    }
});

// ViewModel du composant graphique GridTicket
Ext.define('MVVM.view.GridTicketViewModel', {
    extend: 'Ext.app.ViewModel',
    alias: 'viewmodel.gridticket',
    data : {
        settings : null
    }
});

//ViewController associé à la vue GridTicket
Ext.define('MVVM.view.GridTicketViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.gridticket',

    init : function() {

        //récupère le model settings
        var setting = Ext.getStore('settings').getAt(0);

        //charge le modèle dans le ViewModel
        this.getViewModel().set('settings', setting);
    },

    onSelectRow : function(cmp, model) {
        //modifie la tâche courante par celle sélectionné
        this.getViewModel().get('settings').store.setCurrentTicket(model);
    }
});

View

Les Vues en ExtJS sont décrites dans des objets. Chaque Vue déclare son ViewModel et son ViewController, mais n’appelle jamais ses instances. Il suffit alors d’utiliser le mot clef ‘bind’ pour lier la valeur dans le ViewModel à celle de la Vue.

//Composant graphique définissant le formulaire d’édition d’une tâche
Ext.define('FormTask', {
    extend: 'Ext.form.Panel',

    //déclaration du viewcontroller associé
    controller: 'formtask',

    //declaration du viewmodel associé
    viewModel: {
        type: 'formtask'
    },

    items : [{
        xtype : 'button',
        label : 'Create new Task',
        //fonction définit dans le ViewController
        handler : 'onClickCreateButton'
    },{
        xtype : 'textfield',
        fieldLabel : 'Title',
        name: 'title',
        allowBlank: false,
        bind : {
            //affiche le titre de la tâche courante
            value : '{settings.currentTask.title}'
        }
    }, {
        xtype : 'label',
        //affiche le nombre de tâches
        bind : '{nbTasks}'
    }]
});

//Composant graphique définissant la liste des tâches
Ext.define('MVVM.view.GridTicket', {
    extend: 'Ext.grid.Panel',
    xtype : 'grid-ticket',
    controller: 'gridticket',
    viewModel: {
        type: 'gridticket'
    },

        // bind la collection de tâches
        // le composant Ext.grid.Panel s’occupe 
        // d’afficher automatiquement toutes les tâches
    bind : {
        store : '{settings.tickets}'
    },

    //affiche dans cette colonne la propriété ‘title’ de chaque tâche
    columns : [{
        text: 'Tasks',
        dataIndex : 'title'
    }],

    //au clic sur une ligne, on exécute la méthode 'onSelectRow'
    listeners : {
        select : 'onSelectRow'
    }
});

Conclusion

Chaque framework a sa propre manière et syntaxe pour implémenter le pattern MVVM. Le principe d’indépendance entre la Vue et le Modèle est à chaque fois respecté et tous proposent des outils pour mettre en place un data-binding entre la Vue et le ViewModel.

Chaque framework a ses propres avantages :

  • KnockoutJS est peu intrusif en étant une simple librairie.
  • AngularJS est le plus simple syntaxiquement et le plus éprouvé par la communauté.
  • ExtJS pousse encore plus à découper notre application et à structurer notre code.

Mais le pattern MVVM n’a pas que des avantages. Pour de petits projets, il peut être contre-productif de proposer une telle architecture.  De plus pour de très gros projets avec de nombreux ViewModel, le data-binding peut consommer la mémoire de manière considérable.

Il n’est donc pas interdit d’implémenter son propre mécanisme de liaison, tant que l’on respecte le découpage complet entre la Vue et le Modèle.

Grunt, Gulp, quel outil pour automatiser vos tâches front-end ? (3/3)

Grunt, Gulp, le match !

Comme nous en avions précédemment discuté ici et ici, Grunt et Gulp sont deux outils fantastiques, bâtis autour de la plateforme Node.js, qui nous permettent d’automatiser les tâches fastidieuses mais essentielles du développement front-web. Si la finalité de ces deux outils est la même, comment se distinguent-ils ?

La configuration

Gulp est un outil qui est tourné vers le code plutôt que vers la configuration. Les tâches Grunt sont configurées dans un objet de configuration alors que les tâches Gulp sont plutôt configurées en utilisant un style de syntaxe à la Node.js.
Le style de configuration de chaque plugin Grunt peut varier d’un plugin à un autre alors que les options de configuration des plugins sous Gulp sont bien plus standardisées.
Sachez aussi que l’on peut facilement se retrouver avec de très gros fichiers de configuration Grunt cela étant dû en partie à cause des configurations un peu trop over-kill pour des tâches qui restent simples.

L’exemple ci-dessous nous permet de constater la différence entre Grunt et Gulp pour automatiser la compilation de fichiers LESS:

Gruntfile.js

grunt.initConfig({
  less: {
      options: {
        paths: ["app/styles/css"]
      },
      files: {
        "build/styles/result.css": "app/styles/source.less"
      }
  },

  watch: {
    styles: {
      files: ['app/styles/{,*/}*.less'],
      tasks: ['less']
    }
  }
});

grunt.registerTask('default', ['less', 'watch']);

Gulpfile.js

gulp.task('less', function () {
  gulp.src('app/styles/**/*.less')
    .pipe(less())
    .pipe(gulp.dest('build/styles'));
});

gulp.task('default', function() {
  gulp.run('less');
  gulp.watch('app/styles/**/*.less', function() {
    gulp.run('less');
  });
});

Selon votre style, vous préférerez le code plutôt que la configuration ou inversement.
Personnellement, je trouve que le style de syntaxe de Gulp est plus lisible que celui de Grunt.

Les performances

J’ai pu constaté sur différents projets que Gulp était deux fois plus rapides que Grunt dans la majeure partie des cas.
Cela est du au fait que Gulp utilise le système de streams qui lui permet de monter les flux de données en mémoire, ce qui limite grandement les lectures/ecritures dans les fichiers.

Ce n’est pas le cas de Grunt, il n’utilise pas de streams mais va plutôt mulitplier les accès aux fichiers et avoir recours à des fichiers temporaires pour écrire les contenus d’une étape à une autre, ce qui peut être particulièrement coûteux en terme de performance suivant la taille du projet.

La communauté

Gulp est encore jeune et de ce fait il dispose d’une petite communauté et je trouve que la documentation est très souvent insuffisante.
Avec pratiquement 2 ans d’existence, Grunt se pose comme référence dans le domaine des task runners, il dispose d’une solide communauté et d’un peu moins de 3000 plugins (contre 650 pour Gulp).
Mais attention toutefois à Gulp qui pourrait s’imposer dans le futur car de plus en plus de developpeurs l’ont adopté pour les différentes raisons que nous avons pu voir.

Conclusion

La finalité de ces deux outils est la même : automatiser les tâches récurrentes pour nous faire gagner du temps !

Il faudra garder un oeil sur les évolutions futures de Grunt. Il se murmure que dans ses prochaines versions il pourrait prendre en charge le piping et les streams, ce qui réduirait considérablement son retard par rapport à Gulp en terme de performances. De ce fait, la configuration des plugins serait minime puisque nous pourrions nous passer totalement des fichiers temporaires.

Si vous devez faire un choix, il faut se demander quel style vous correspond le mieux. Est-ce que je prèfere un outil basé sur de la configuration ou sur du code ? À chaque style son outil, choisissez Grunt si vous êtes plus à l’aise avec de la configuration plutôt qu’avec du code, sinon choisissez Gulp.

Si au final vous avez du mal à faire un choix sachez que les deux cohabitent parfaitement !

grunt_logo gulp_logo
Grunt Gulp
3000 plugins 650 plugins
Configuration Code
Fichiers temporaires Streams
Grande communauté Communauté naissante
Rapide comme l’éclair
Documentations perfectible

Grunt, Gulp, quel outil pour automatiser vos tâches front-end ? (2/3)

Gulp

Tout comme Grunt, Gulp est un outil d’automatisation JS (javascript task runner) basé sur la plateforme NodeJS qui va nous permettre d’améliorer notre workflow et d’optimiser nos applications web. Qui dit automatisation dit plus de temps pour se concentrer sur d’autres choses plus importantes comme produire du code de qualité ! Grâce à cet outil, nous allons pouvoir automatiser des tâches telles que la compression d’images, les tests unitaires ou encore la minification.

Depuis sa sortie, Gulp a suscité un grand intérêt auprès de la communauté des developpeurs front-end du fait de la simplicité de sa mise en place et de sa configuration qui lui permette d’être très facilement lisible et maintenable.

Installation

Puisque Gulp est basé sur NodeJS, ce dernier doit être installé sur votre poste avant d’installer Gulp.

Vous pourrez installer NodeJS sur votre machine sans trop de difficulté en vous rendant ici.

Une fois NodeJS installé, vous pourrez installer facilement Gulp en utilisant npm (Node Packet Manager).
Il faut lancer la commande suivante :

$ npm install -g gulp

Cette commande aura pour effet d’aller récupérer Gulp depuis le registre npm et de l’installer globalement dans votre système.
Pour installer gulp uniquement au niveau de votre projet, il ne faut pas mettre l’option -g dans la ligne de commande.

Configuration

À ce niveau, Gulp est installé sur votre système et est prêt à être utiliser dans nos projets.

Tout comme nous l’avions vu dans notre précédent article, il faut avoir mis à la racine de notre projet un fichier package.json dans lequel on va renseigner toutes nos dépendances de développement .

À présent, il faut installer Gulp en tant que dépendance du projet, ce qui permettra aux autres membres de la team, de partir du bon pied lorsqu’ils feront un « npm install » !
La commande à exécuter est la suivante :

$ npm install --save-dev gulp gulp-util

L’option « –save-dev » dans la ligne de commandes va permettre de mettre à jour les dépendances de développement dans le fichier package.json en ajoutant une référence à gulp et gulp util. Si vous omettez cette option, le fichier package.json ne sera pas mis à jour avec la dépendance en question et vous serez obligé de la rajouter manuellement.

Maintenant nous allons pouvoir installer tous les plugins qu’il nous faut pour automatiser l’éxécution des tâches !
Étant donné que les plugins de Gulp sont des modules de node, nous allons utiliser npm pour les installer.

$ npm install --save-dev gulp-less gulp-watch

La ligne de commande ci-dessus va installer le plugin pour compiler nos fichier LESS et va nous permettre de surveiller les fichiers que l’on modifie dans le but d’y associer un traitement particulier. De plus, une entrée sera automatiquement ajoutée au niveau de devDependencies dans le fichier package.json

{
  "devDependencies": {
    "gulp": "^3.7.0"
    "gulp-less": "^1.2.3",
    "gulp-watch": "^0.6.5"
  }
}

À l’heure actuelle, Gulp dispose d’un peu plus de 600 plugins. Certains feront sûrement votre bonheur, tels que :

Gulpfile.js

De la même façon que Grunt, toute la configuration de Gulp réside dans le fichier gulpfile.js situé à la racine du projet.
Nous allons donc commencer par dire à Gulp de quels plugins nous avons besoin pour executer les tâches :

var gulp  = require('gulp'),
    util  = require('gulp-util'),
    less  = require('gulp-less'),
    watch = require('gulp-watch');

Par la suite nous allons specifier quelles tâches nous allons exécuter. Dans notre cas nous allons faire 2 choses :

  • Une tâche « less » qui va compiler les fichiers LESS et placer le résultat à un endroit donné
  • Une tâche « default » qui va exécuter la tâche « less » puis surveiller toutes modifications des fichiers de type .less, si une modification survient la tâche « less » sera rejouée

Pour définir une tâche Gulp, on utilise la méthode task() suivante :

gulp.task('less', function () {
  gulp.src('app/styles/**/*.less')
    .pipe(less())
    .pipe(gulp.dest('build/styles'));
});

La méthode task() prend en entrée un nom sémantique « less » qui définit le nom de la tâche et ensuite une fonction anonyme qui contient le traitement en question.

Nous allons décomposer cette méthode ligne par ligne :

gulp.src('app/styles/**/*.less')

La méthode src() va nous permettre de spécifier le chemin vers les fichiers avec l’extension LESS et va les transformer en un stream qui sera diffusé aux plugins pour effectuer les traitements.

.pipe(less())

La méthode pipe() va utiliser le stream provenant de src() et le passer aux plugins référencés. Ici en l’occurrence, le plugin less recevra le stream de fichier pour les traiter.

.pipe(gulp.dest('build/styles'))

La méthode dest() va nous permettre de spécifier l’emplacement où l’on souhaite écrire le résultat du traitement. Les fichiers LESS compilés seront donc placés dans le dossier styles du build.

Pour exécuter cette tâche, il suffira de se placer à la racine du projet et de lancer la commande :

$ gulp less

Si nous ne spécifions pas de tâche dans la ligne de commande, la tâche « default » sera lancée.

gulp.task('default', function() {
  gulp.run('less');
  gulp.watch('app/styles/**/*.less', function() {
    gulp.run('less');
  });
});

La tâche « default » ci-dessus a pour but de lancer la tâche « less » (compilation de LESS pour avoir en sortie une feuille de style CSS) puis surveiller toutes modifications des fichiers LESS. Si une modification survient, la tâche « less » sera re-éxecutée. De cette façon, on n’a plus à se soucier de compiler les fichiers LESS à chaque fois qu’on les modifie !

Là est toute la puissance de Gulp, une syntaxe très séduisante, claire et facilement lisible et un gain de performance grâce à un système de stream qui évite d’écrire dans un dossier temporaire.
Il n’y a pas autant de plugins disponibles pour Gulp que pour Grunt, c’est un fait, mais la communauté n’en est pas moins active et grandissante, il ne serait pas étonnant que Gulp s’impose comme le prochain standard de l’automatisation JS des tâches, mais d’ici là quel outil choisir ?

La suite, dans notre prochain article consacré au match Grunt / Gulp.

Grunt, Gulp, quel outil pour automatiser vos tâches front-end ? (1/3)

Introduction

En tant que développeurs front-end, nous sommes amenés à réaliser nos projets web en ayant à l’esprit un certain nombre de bonnes pratiques telles que :

  •  La compression des feuilles de styles CSS et la minification des scripts Javascript pour qu’ils soient le plus léger possible en production
  •  L’optimisation des images en réduisant leur poids sans altérer leur qualité
  • Auto-générer des sprites d’images
  •  S’assurer que les scripts Javascript ne contiennent pas d’erreurs de syntaxe
  •  Retirer les alertes/console log en production
  •  etc …

Si  nous voulons avoir une webapp optimisée, nous devons réaliser absolument toutes ces tâches. Des tâches répétitives, quelque soit le projet, et fastidieuses car elles ne se limitent pas seulement aux 4 citées en exemple plus haut.

Automatiser ce genre de tâches est donc devenu quasi-vital dans le but, d’une part, de gagner un temps considérable à ne plus tout faire manuellement et d’autre part, d’être sûr de l’optimisation du code livré en production (minification, obfusquation etc…). Tout cela en moins de temps qu’il ne faut pour préparer votre café préféré !

Si votre quotidien est synonyme de tâches à répétition et que vous avez cherché à un moment donné à les automatiser, vous avez forcément entendu parler de Grunt ou encore de Gulp qui s’imposent comme des références en la matière d’outils d’automatisation Javascript.

Outils Javascript d’automatisation

Les outils js d’automatisation des tâches s’appuient sur Node.js, une plateforme software écrite en Javascript orientée serveur qui fut créé il y a quelques années. Elle utilise le moteur V8 de Google Chrome qui va analyser/exécuter ultra-rapidement du code js et qui fonctionne suivant un modèle non bloquant (programmation asynchrone). Node.js a ouvert la voie, entre autres, à de nouveaux outils d’automatisation tel que Grunt et c’est une vraie chance pour nous autres développeurs front-end que d’avoir à notre disposition ce genre d’outils pour :

  • déployer rapidement un serveur de développement
  • rafraîchir sa page lorsque l’on modifie un fichier
  • compiler les templates
  • bouchonner les données renvoyées par un web service,
  • supprimer tous les commentaires dans le code
  • minifier les feuilles de styles
  • obfusquer les scripts JS
  • etc…

Il n’y a pratiquement pas de limites à ce que l’on peut faire !

Grunt

Grunt est un outil basé sur des tâches exécutées en ligne de commande qui va grandement accélérer nos workflows classiques de développement en réduisant notre effort lors de la préparation des sources pour les mises en production.

Dans la plupart des cas, sachez que lorsque vous effectuez manuellement une tâche, son équivalent automatique existe sous forme de plugin Grunt comme par exemple la minification et la concaténation de scripts.

Grunt est également capable de gérer des tâches qui ne sont pas forcément liées à Javascript comme la compilation CSS depuis SASS ou LESS.

Enfin sachez qu’il dispose d’une incroyable communauté de développeurs et d’une énorme base de plugins mise à jour très régulièrement. Vous n’êtes donc pas seul à vous lancer dans l’aventure !

Installation

Comme évoqué précédemment, Grunt est basé sur Node.js. De ce fait il doit être installé sur votre machine pour que Grunt puisse fonctionner. L’installation de Grunt se fait via le npm (Node Packet Manager), il suffit d’ouvrir votre invité de commandes et d’exécuter cette instruction pour installer Grunt :

$ npm install -g grunt-cli

Package.json

Package.json est un fichier à créer à la racine de votre projet, il va vous permettre de tracker et d’installer toutes les dépendances liées au développement. Ainsi, tous les membres de votre team disposeront des toutes dernières versions des dépendances. Ce qui en fin de compte permet de garder un environnement de développement constamment synchronisé.

{
  "name": "Example",
  "description": "Un package de test",
  "version": "0.0.1",
  "private": "true",
  "author": "Prenom Nom ",
  "devDependencies": {
    "grunt": "^0.4.5"
  }
}
$ npm install

La commande ci-dessus va dire à npm d’aller récupérer toutes les dépendances qui sont listées dans devDependencies puis de les installer dans le dossier node_modules.
 Dans cet exemple, si la commande réussie, Grunt version 0.4.5 ou supérieure sera installée localement dans le projet.

Gruntfile.js

Comme le fichier package.json, le fichier Gruntfile.js doit être placé à la racine du projet. Il contient toute la configuration des tâches automatiques à exécuter lorsque l’on exécute grunt depuis l’invité de commandes.

module.exports = function(grunt) {

    /* 1. Toute la configuration. 
     * L'ordre n'a pas d'importance.
     */
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

   /* 2. La configuration du plugin cssmin pour la minification de css.
    * Une sous tâche build qui va minifier index.css dans style.css
    */
        cssmin: {
                   build: {
                             src: 'app/css/index.css',
                             dest:'build/css/style.css'
                          } 
                }
    });
    
    /* Chargement du plugin
     * 3. On dit à Grunt que l'on compte utiliser le plugin grunt-contrib-cssmin.
     */
    grunt.loadNpmTasks('grunt-contrib-cssmin');

    /* Déclaration des tâches
     * On dit à grunt ce qu'il faut faire lorsque l'on execute "grunt" dans l'invité de commandes.
     */
    grunt.registerTask('default', ['cssmin']);

};

À partir de maintenant il n’y a plus qu’à exécuter la ligne de commande suivante dans notre invité de commandes pour minifier notre feuille de style :

$ grunt

Et voila ! Notre feuille de style a été minifiée en une fraction de seconde. grunt-command-line
Imaginez maintenant toutes les tâches que l’on peut automatiser exactement avec la même simplicité! 
Pour vous en rendre compte, je vous invite à jeter un œil à l’impressionnant listing de plugins disponible pour Grunt. Pas moins de 3000 plugins pour faciliter notre quotidien.

Grunt est donc un outil très simple d’usage, qui consiste en un fichier de configuration js facilement lisible, dans lequel on paramètre les plugins que l’on souhaite utiliser pour automatiser nos tâches.
Sachez qu’il n’est pas le seul sur le marché… 
La suite, dans la deuxième partie de ce dossier qui sera consacrée au petit nouveau, Gulp !