Ember et contre tous !

Deux mots d’introduction

Je profite de la sortie de la version 2.0 du framework Ember.js pour rédiger ce premier post de Blog sur l’environnement de développement Javascript qui me paraît le plus passionnant du moment. Car même si ce framework demande un effort d’apprentissage important (et on ne peut pas le nier…), sa philosophie et sa productivité une fois maîtrisée sont réellement phénoménales !

L’objectif de cet article (et je l’espère des suivants d’une longue série) est donc d’accélérer la prise en main des différents outils. Bien qu’excellente, la documentation disponible sur le site, précise et imposante, reste difficile à appréhender du fait même de sa quantité et de sa diversité.

Pour revenir à Ember.js et le présenter en quelques mots, il s’agit d’un framework MVC côté client, qu’on pourrait présenter comme un concurrent à Angular.js (https://angularjs.org/) pour aller dans la vulgarisation simpliste. Dans les faits, le périmètre d’Ember.js est beaucoup plus ambitieux puisqu’il offre immédiatement trois sous-projets supplémentaires :

  • l’impressionnant Ember-data, qui vise à manipuler un modèle métier directement dans la couche Javascript et à en assurer la persistence, généralement par des appels Rest,
  • Liquid Fire(http://ef4.github.io/liquid-fire/), qui permet la gestion des transitions entre pages avec un niveau d’abstraction très élevé,
  • l’environnement de développement Ember CLI (http://www.ember-cli.com/), qui structure le format des projets, intégre Node.js pour fluidifier le suivi des modifications, encapsule les tests unitaires et procure les moyens de construction et de distribution du livrable final (et bien plus encore !).

Ces différentes extensions seront également utilisées dans cette série d’articles, en commençant par Ember CLI — passage quasi obligé pour se lancer dans l’aventure.

Les grands principes d’Ember.js

Pour obtenir une introduction plus poussée des principaux concepts utilisés dans Ember.js, je vous propose de vous reporter à l’excellente traduction de l’article de Julien Knebel http://vfsvp.fr/article/une-introduction-en-profondeur-a-emberjs/ et notamment la présentation des concepts de Modèle, Routeur, Route, Contrôleur, Vue, Template, Composant et Helper.

Dans les grandes lignes, on a le schéma suivant :

Concepts

Le fichier ‘router.js’ va contenir toutes les routes (URL) de l’application et permettre l’utilisation des ‘routes’ (attention à ne pas confondre ces deux concepts).

La partie ‘Model’ va contenir la description des différents objets manipulés par le framework. Elle va prendre tout son sens avec l’utilisation d’Ember Data.

Les ‘routes’ disposent d’un hook model permettant d’aller chercher les données qui vont être mises à disposition des ‘controllers’ et des ‘templates’ associés. Ces objets ‘route’ peuvent aussi permettre de valuer les propriétés d’un ‘controller’ et disposent d’événements ou d’actions qui leur sont propres.

Enfin on dispose de deux possibilités de rendu des vues : soit par utilisation d’un objet Javascript ‘view’, soit par l’intermédiaire d’un ‘template’ Handlebar.js.

Les templates peuvent s’appuyer sur des composants (qui eux-mêmes sont constitués d’un ensemble Contrôleur / Vue / Template – vive les poupées russes ! 😉 ).

Le point EXTRÊMEMENT important à comprendre lorsqu’on débute avec Ember.js, c’est que l’enchaînement de tous ces concepts repose sur des conventions de nommage et que si l’un d’entre eux n’a pas été défini par le code applicatif, le framework va automatiquement en créer un par défaut ! Assez surprenant (et déroutant) au début, ce comportement est extrêmement puissant et satisfaisant une fois assimilé.

Un peu de concret !

Création du squelette applicatif avec Ember CLI

Avant de faire quoi que ce soit, il est maintenant fortement recommandé de passer par l’installation d’Ember CLI. Pour cela, on passe par la commande :

npm install -g ember-cli

(Je passe sur l’installation de node.js qui est lui même un préalable).

La création d’un nouveau projet se fait très simplement par les commandes suivantes :

ember new ember-strava

cd ember-strava

On obtient alors une structure de répertoire du type :

Ember CLI

J’ai décommenté la ligne

   ENV.APP.LOG_TRANSITIONS = true;

dans le fichier Environment.js afin de pouvoir visualiser dans la console Javascript le cheminement dans les routes. Ce mode de trace s’avère bien pratique pour mieux comprendre le fonctionnement du framework.

D’ores et déjà, la commande ‘ember serve’ permet de démarrer le server node.js et en allant sur la page http://localhost:4200 , on obtient une magnifique page arborant le message :

Borat

‘Welcome to Ember’

Une bien belle réussite !  🙂 

(mais qui ne démontre rien du framework…)

Si on fouille un peu le code, on constate que cet affichage est réalisé par le template ‘application.hbs’ contenant simplement :

Welcome to Ember
{{outlet}}
La première ligne est immédiate (et correspond à ce que l’on voit s’afficher à l’écran). À noter que l’on peut modifier le contenu de cette première ligne et que la sauvegarde du fichier conduit à un rechargement de la page dans le navigateur et donc à une mise à jour à chaud. Cela est vrai pour tous les fichiers modifiés, qu’ils soient js, hbs ou css… Ça, c’est du confort !La seconde correspond simplement à un ‘place holder’ qui contiendra le code issu des templates intégrés lors de la navigation vers les URLs désignées dans le fichier router.js. Pour le moment, nous n’avons pas ajouté de route dans ce fichier et {{outlet}} ne conduit à l’affichage d’aucun contenu supplémentaire. Mais ce n’est que partie remise pour la suite de cet exercice.

Ajout d’un framework

Difficile (en tout cas pour moi) de faire une application sans utiliser Bootstrap. On va donc l’installer dans notre nouvel environnement applicatif. Pour cela, on utilise classiquement bower :

bower install --save bootstrap

On doit ensuite modifier le fichier ember-cli-build.js pour y intégrer les fichiers Bootstrap.

(on aurait également pu passer par la commande ‘ember install ember-cli-boostrap’)

app.import('bower_components/bootstrap/dist/css/bootstrap.css');
app.import('bower_components/bootstrap/dist/css/bootstrap.css.map', { destDir: 'assets' });
app.import('bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot', { destDir: 'fonts' });
app.import('bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg', { destDir: 'fonts' });  app.import('bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf', { destDir: 'fonts' });
app.import('bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff', { destDir: 'fonts' });
app.import('bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2', { destDir: 'fonts' });

Il va y avoir du sport !

Pour aller vers une application un peu plus ambitieuse et démonstrative des possibilités d’Ember.js, nous allons nous interfacer avec Strava (https://www.strava.com), site dédié aux cyclistes, coureurs ou triathlètes.

Si vous n’avez pas de compte, vous pouvez en créer un gratuitement. Une fois inscrit sur le site, Strava vous offre la possibilité de récupérer un token de connexion pour pouvoir effectuer des requêtes permettant de récupérer ses données sous forme de flux JSON (voir http://strava.github.io/api/ pour l’API et http://www.strava.com/developers pour la déclaration d’une application et la récupération d’un token).

La requête que nous allons utiliser correspond à la description de l’athlète (vous !) et de son équipement :

curl -G https://www.strava.com/api/v3/athlete -d access_token=xxx

ou pour les chanceux qui ont pris le temps d‘installer jq (https://stedolan.github.io/jq/)

curl -G https://www.strava.com/api/v3/athlete -d access_token=xxx  | jq .

Afin d’éviter tout message relatif aux requêtes cross-domaines vers Strava, il est nécessaire d’ajouter les lignes suivantes dans le fichier ‘environnement.js’ :

ENV.contentSecurityPolicy = {
'default-src': "'none'",
'script-src': "'self' https://www.strava.com",
'font-src': "'self'",
'connect-src': "'self' https://www.strava.com",
'img-src': "'self'",
'style-src': "'self'",
'media-src': "'self'"
}

Dans la mise en place initiale de notre projet, nous étions resté sur le fait que le helper {{outlet}} présent dans le template application.hbs n’avait rien à afficher, puisqu’aucune route n’avait été créée… Ce n’est pas tout à fait exact. En fait, Ember.js avait lui même créé certaines routes par défaut, avec des objets ne retournant simplement aucun rendu.

Pour s’en convaincre, on peut installer l’extension Chrome Ember Inspector (essentielle, dès lors que l’on commence à travailler avec ce framework).

Cet outil nous permet ainsi de visualiser l’ensemble des routes définies :

Inspector

Comme on le constate dans cet écran, de nombreuses routes sont créées par défaut et permettent de gérer des pages de chargement ou les cas d’erreur.  

Notre objectif est donc de mettre à profit ces routes afin de mettre en lumière le fonctionnement de la balise outlet. Pour cela, nous allons créer 2 fichiers ‘index’ (correspondant à la route application / index) :

  • le premier dans le répertoire ‘routes’, nommé ‘index.js’ (pour correspondre au nommage de la route par défaut) et contenant le code suivant :
import Ember from 'ember';

export default Ember.Route.extend({

model: function() {
       let token = "<your token from Strava>";
   return Ember.$.ajax({
   url: "https://www.strava.com/api/v3/athlete?access_token="+token+"&callback=?",
   dataType: "jsonp",
   error: function(xhr, status, error) {
         console.log("Error");
         console.log(xhr.statusText);
         console.log(xhr.responseText);
         console.log(xhr.status);
         console.log(error);
     }
   });
}
});

le second dans le répertoire ‘templates’, cette fois-ci nommé ‘index.hbs’ et contenant le code suivant :

Statistiques de {{model.firstname}}</h2>
</div>
<div>
<h3>Vélos</h3>
<table id="bikes" class="mytable">
   <tr>
    <th>Name</th>
     <th>Distance</th>
     <th>Principal</th>
   </tr>
   {{#each model.bikes as |bike|}}
   <tr>
     <td>{{bike.name}}</td>
     <td>{{bike.distance}}</td>
     <td>
     {{#if bike.primary}}
     <span class="glyphicon glyphicon-star" aria-hidden="true"></span>
     {{else}}
     <span class="glyphicon glyphicon-minus" aria-hidden="true"></span>
     {{/if}}
     </td>
   </tr>
{{/each}}
</table>
</div>
<div>
<h3>Chaussures</h3>
<table id="shoes" class="mytable">
   <tr>
    <th>Name</th>
     <th>Distance</th>
     <th>Principal</th>
   </tr>
   {{#each model.shoes as |shoe|}}
   <tr>
     <td>{{shoe.name}}</td>
     <td>{{shoe.distance}}</td>
     <td>
     {{#if shoe.primary}}
     <span class="glyphicon glyphicon-star" aria-hidden="true"></span>
     {{else}}
     <span class="glyphicon glyphicon-minus" aria-hidden="true"></span>
     {{/if}}
     </td>
   </tr>
{{/each}}
</table>
</div>

Pour rendre les choses un poil plus jolie, nous allons également enrichir le fichier app.css (dans le répertoire styles) :

table.mytable {
 width:90%;
}
table.mytable, th.mytable, td.mytable {
 border: 1px solid black;
 border-collapse: collapse;
 padding: 20px;
 text-align: center;
}

table tr:nth-child(even) {
 background-color: #eee;
}
table tr:nth-child(odd) {
 background-color:#fff;
}
table th {
 background-color: black;
 color: white;
}
div {
margin-left: 10px;
 margin-right: 10px;
}
Le code contenu dans l’objet ‘route’ va donc simplement implémenter le hook model pour le charger et le mettre à disposition du contrôlleur (qui ne fait rien de particulier dans notre exemple et qui sera donc une instantiation par défaut) et surtout du template.

Dans le template, le flux JSON est donc disponible par l’intermédiaire de cet objet ‘model’ sous une syntaxe handlebar.js ; du type {{model.firstName}} pour accéder au champ firstName.

Ce template contient également quelques helpers permettant de parcourir les tableaux ou de faire de la mise en forme conditionnelle. Je pense que cela est assez auto-explicatif.

Si vous avez renseigné votre profil Strava en y ajoutant des équipements (paires de chaussures et vélos) et si vous avez effectué de nombreuses sorties, vous devriez obtenir un affichage du type :

Simple-table

Et si on mettait de beaux tableaux !

Le template précédent a le mérite de la simplicité. Mais le rendu des tableaux est bien pauvre.

Pour l’améliorer, nous allons faire appel à des composants sur étagère en utilisant le projet ember-models-table (http://onechiporenko.github.io/ember-models-table/).

À noter que les addons d’Ember.js sont référencés sur le site http://www.emberaddons.com/ .

Pour utiliser ce composant de tableau, on va commencer par l’installer dans notre environnement par la commande :

ember install ember-models-table

Le composant, que l’on va placer dans notre template, va attendre qu’on lui fournisse :

  • la liste des colonnes à afficher avec le titre à utiliser,
  • les données à afficher.

C’est typiquement le boulot d’un contrôleur que de mettre en forme pour le template les données mises à disposition par la route. Et pour le moment, nous n’avons pas de contrôleur puisque nous utilisons celui par défaut. On va donc le créer en ajoutant un fichier ‘index.js’ dans le répertoire ‘controllers’ avec le contenu suivant :

import Ember from 'ember';

export default Ember.Controller.extend({

 bikeColumns: Ember.computed(function() {
 var col = Ember.A([
 Ember.Object.create({
 propertyName: 'name',
 title: 'Name'
 }),
 Ember.Object.create({
 propertyName: 'distance',
 title: 'Distance'
 }),
 Ember.Object.create({
 propertyName: 'principal',
 title: 'Primary'
 })
 ]);
 return col;
 }),

 bikeContent: Ember.computed(function() {
 return this.get("model").bikes;
 }),

 shoeColumns: Ember.computed(function() {
 var col = Ember.A([
 Ember.Object.create({
 propertyName: 'name',
 title: 'Name'
 }),
 Ember.Object.create({
 propertyName: 'distance',
 title: 'Distance'
 }),
 Ember.Object.create({
 propertyName: 'principal',
 title: 'Primary'
 })
 ]);
 return col;
 }),

 shoeContent: Ember.computed(function() {
 return this.get("model").shoes;
 })
});

Le template index.js peut ensuite être modifié pour se résumer à :

<h2>Statistiques de {{model.firstname}}</h2>
</div>
<div>
 <h3>Vélos</h3>
 {{models-table
 data=bikeContent
 columns=bikeColumns
 useNumericPagination=true
 showColumnsDropdown=false
 pageSize=10
 }}
</div>
<div>
 <h3>Chaussures</h3>
 {{models-table
 data=shoeContent
 columns=shoeColumns
 useNumericPagination=true
 showColumnsDropdown=false
 pageSize=10
 }}
</div>

Le rendu est alors nettement plus flatteur et les possibilités offertes par les tables (pagination, filtrage, etc.), sans rapport avec ce que l’on avait préalablement :

Advanced-table

Conclusion (provisoire !)

En rédigeant ce long article, j’ai deux espoirs :

  1. que vous ayez lu ce post jusqu’au bout et réussi à suivre et à comprendre mes explications,
  2. que vous ayez pris goût à ce framework, un peu trop méconnu selon moi, du moins en France où tout le monde ne jure plus que par Angular.js (qui a de grandes qualités par ailleurs).

Une plus grande popularité lui permettrait certainement de se retrouver dans de plus nombreux projets et de casser son image de framework à forte courbe d’apprentissage.

Si mon emploi du temps me le permet, je publierai d’autres sujets très prochainement, notamment au sujet d’Ember Data et de Liquid Fire, deux pépites gravitant dans la mouvance du framework principal. Mais n’attendez pas ces articles pour vous lancer !

(L’ensemble du code est disponible sur mon github : https://github.com/bpinel/EmberStrava)