I. 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 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 persistance, généralement par des appels Rest ;
- 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, 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.
II. 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 (Une introduction en profondeur à Ember.js) 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 :
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 attribuer une valeur aux 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é.
III. Un peu de concret !▲
III-A. 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 :
J'ai décommenté la ligne :
ENV.
APP.
LOG_TRANSITIONS =
true;
qui se trouve 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 :
'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 :
2.
<h2 id
=
"title"
>
Welcome to Ember</h2>
{{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 URL 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.
III-B. 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 :
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-bootstrap.)
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'
}
);
IV. 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, 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.
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 :
2.
3.
4.
5.
6.
7.
8.
9.
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és 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 :
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 deux 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
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 :
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.
41.
42.
43.
44.
45.
46.
47.
48.
49.
<div class
=
"page-header"
>
<h2>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 jolies, nous allons également enrichir le fichier app.css (dans le répertoire styles) :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
table.mytable
{
width:
90
%;
}
table.mytable
,
th.mytable
,
td.mytable
{
border:
1
px solid
black
;
border-collapse:
collapse
;
padding:
20
px;
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:
10
px;
margin-right:
10
px;
}
Le code contenu dans l'objet route va donc simplement implémenter le hook model pour le charger et le mettre à disposition du contrôleur (qui ne fait rien de particulier dans notre exemple et qui sera donc une instanciation 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 :
IV-A. 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.
À 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 :
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.
41.
42.
43.
44.
45.
46.
47.
48.
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 à :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
<div class
=
"page-header"
>
<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 :
V. Conclusion (provisoire !)▲
En rédigeant ce long article, j'ai deux espoirs :
- Que vous ayez lu ce post jusqu'au bout et réussi à suivre et à comprendre mes explications ;
- 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.)
VI. Remerciements▲
Nous tenons à remercier Bertrand Pinel et Ippon de nous avoir donné l'autorisation de publier cet article. Merci également à Claude Leloup pour sa relecture.