-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy path13-voting.md.erb
613 lines (484 loc) · 22.6 KB
/
13-voting.md.erb
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
---
title: Le Vote
slug: voting
date: 0013/01/01
number: 13
points: 10
photoUrl: http://www.flickr.com/photos/ikewinski/8561920811/
photoAuthor: Mike Lewinski
contents: Construire un système de vote d'utilisateurs sur les articles.|Classer nos articles par vote sur une page des "meilleurs" articles.|Apprendre comment écrire un helper Spacebars général.|En apprendre un peu plus sur la sécurité des données dans Meteor.|Couvrir quelques remarques de performance intéressantes dans MongoDB.
paragraphs: 49
---
Maintenant que notre site est en train de devenir populaire, trouver les meilleurs liens va rapidement devenir délicat. Ce que nous avons besoin est un système de classement pour ordonner nos articles.
Nous pourrions construire un système de classement complexe avec karma, basé sur une perte de points dans le temps, et plein d'autres choses (la plupart sont implémentés dans [Telescope](http://telesc.pe), le grand frère de Microscope). Mais pour notre application, nous garderons les choses simples et nous noterons juste les articles par le nombre de votes qu'ils ont reçus.
Commençons par donner aux utilisateurs un moyen de voter sur les articles.
### Modèle de données
Nous stockerons une liste de votants sur chaque article afin de savoir si on doit montrer le bouton de vote aux utilisateurs et pour empêcher les personnes de voter deux fois.
<% note do %>
### Confidentialité des données et Publications
Nous publierons ces listes de votants à tous les utilisateurs, ce qui rendra automatiquement ces données accessibles publiquement via la console du navigateur.
C'est le type de problème sur la confidentialité des données qui peut subvenir de la façon dont les collections fonctionnent. Par exemple, voulons-nous que les personnes soient capables de trouver qui a voté pour leurs articles ? Dans notre cas, rendre cette information disponible publiquement n'aura pas réellement de conséquences, mais il est important de reconnaître au minimum le problème.
<% end %>
Nous allons également dénormaliser le nombre total de votants sur un article pour rendre plus facile la récupération de ce chiffre. Donc nous ajouterons deux attributs à nos articles, `upvoters` et `votes`. Commençons par les ajouter à notre fichier de pré-installation :
~~~js
// Données de préinstallation
if (Posts.find().count() === 0) {
var now = new Date().getTime();
// Créer deux utilisateurs
var tomId = Meteor.users.insert({
profile: { name: 'Tom Coleman' }
});
var tom = Meteor.users.findOne(tomId);
var sachaId = Meteor.users.insert({
profile: { name: 'Sacha Greif' }
});
var sacha = Meteor.users.findOne(sachaId);
var telescopeId = Posts.insert({
title: 'Introducing Telescope',
userId: sacha._id,
author: sacha.profile.name,
url: 'http://sachagreif.com/introducing-telescope/',
submitted: new Date(now - 7 * 3600 * 1000),
commentsCount: 2,
upvoters: [],
votes: 0
});
Comments.insert({
postId: telescopeId,
userId: tom._id,
author: tom.profile.name,
submitted: new Date(now - 5 * 3600 * 1000),
body: "C'est un projet intéressant Sacha, est-ce-que je peux y participer ?"
});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: new Date(now - 3 * 3600 * 1000),
body: 'Bien sûr Tom !'
});
Posts.insert({
title: 'Meteor',
userId: tom._id,
author: tom.profile.name,
url: 'http://meteor.com',
submitted: new Date(now - 10 * 3600 * 1000),
commentsCount: 0,
upvoters: [],
votes: 0
});
Posts.insert({
title: 'The Meteor Book',
userId: tom._id,
author: tom.profile.name,
url: 'http://themeteorbook.com',
submitted: new Date(now - 12 * 3600 * 1000),
commentsCount: 0,
upvoters: [],
votes: 0
});
for (var i = 0; i < 10; i++) {
Posts.insert({
title: 'Test post #' + i,
author: sacha.profile.name,
userId: sacha._id,
url: 'http://google.com/?q=test-' + i,
submitted: new Date(now - i * 3600 * 1000 + 1),
commentsCount: 0,
upvoters: [],
votes: 0
});
}
}
~~~
<%= caption "server/fixtures.js" %>
<%= highlight "22,23,49,50,60,61,72,73" %>
Comme d'habitude, arrêtez votre application, exécutez `meteor reset`, redémarrez votre application, et créez un nouvel utilisateur. Assurons-nous également ensuite que ces deux propriétés sont initialisées quand les articles sont créés :
~~~js
//...
var postWithSameLink = Posts.findOne({url: postAttributes.url});
if (postWithSameLink) {
return {
postExists: true,
_id: postWithSameLink._id
}
}
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date(),
commentsCount: 0,
upvoters: [],
votes: 0
});
var postId = Posts.insert(post);
return {
_id: postId
};
//...
~~~
<%= caption "collections/posts.js" %>
<%= highlight "17~18" %>
### Templates de vote
Premièrement, nous allons ajouter un bouton de vote à notre article partiel et afficher le nombre de votes dans les métadonnées de l'article :
~~~html
<template name="postItem">
<div class="post">
<a href="#" class="upvote btn btn-default">⬆</a>
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
{{votes}} Votes,
submitted by {{author}},
<a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>
~~~
<%= caption "client/templates/posts/post_item.html" %>
<%= highlight "3,7" %>
<%= screenshot "13-1", "Le bouton de vote" %>
Ensuite, nous allons appeler une méthode serveur upvote quand l'utilisateur clique sur le bouton :
~~~js
//...
Template.postItem.events({
'click .upvote': function(e) {
e.preventDefault();
Meteor.call('upvote', this._id);
}
});
~~~
<%= caption "client/templates/posts/post_item.js" %>
<%= highlight "3~8" %>
Finalement, nous allons revenir à notre fichier `lib/collections/posts.js` et ajouter une méthode Meteor côté serveur qui votera pour les articles :
~~~js
//...
Meteor.methods({
post: function(postAttributes) {
//...
},
upvote: function(postId) {
check(this.userId, String);
check(postId, String);
var post = Posts.findOne(postId);
if (!post)
throw new Meteor.Error('invalid', 'Post not found');
if (_.include(post.upvoters, this.userId))
throw new Meteor.Error('invalid', 'Already upvoted this post');
Posts.update(post._id, {
$addToSet: {upvoters: this.userId},
$inc: {votes: 1}
});
}
});
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "8~25" %>
<%= commit "13-1", "Algorithme basique de vote." %>
Cette méthode est assez directe. Nous faisons quelques vérifications de sécurité pour nous assurer que l'utilisateur est authentifié et que l'article existe réellement. Puis nous vérifions doublement que l'utilisateur n'a pas déjà voté pour cet article, et si c'est le cas nous incrémentons le score total de vote et ajoutons l'utilisateur à l'ensemble des votants.
L'étape finale est intéressante, comme nous avons utilisé deux opérateurs Mongo spéciaux. Il y en a beaucoup plus à apprendre, mais ces deux sont extrêmement pratique : `$addToSet` ajoute un item à une propriété de tableau tant qu'elle n'existe pas déjà, et `$inc` incrémente simplement un entier.
### Optimisations de l'interface utilisateur
Si l'utilisateur n'est pas authentifié, ou a déjà voté un article, il ne sera pas autorisé à voter. Pour refléter ça dans notre UI, nous utiliserons un helper pour ajouter de façon conditionnelle une classe CSS `disabled` au bouton de vote.
~~~html
<template name="postItem">
<div class="post">
<a href="#" class="upvote btn {{upvotedClass}}">⬆</a>
<div class="post-content">
//...
</div>
</template>
~~~
<%= caption "client/templates/posts/post_item.html" %>
<%= highlight "3" %>
~~~js
Template.postItem.helpers({
ownPost: function() {
//...
},
domain: function() {
//...
},
upvotedClass: function() {
var userId = Meteor.userId();
if (userId && !_.include(this.upvoters, userId)) {
return 'btn-primary upvotable';
} else {
return 'disabled';
}
}
});
Template.postItem.events({
'click .upvotable': function(e) {
e.preventDefault();
Meteor.call('upvote', this._id);
}
});
~~~
<%= caption "client/templates/posts/post_item.js" %>
<%= highlight "8~15, 19" %>
Nous changeons notre classe `.upvote` en `.upvotable`, donc n'oubliez pas de changer l'événement click également.
<%= screenshot "13-2", "Griser les boutons vote." %>
<%= commit "13-2", "Griser lien vote quand non authentifié / déjà voté." %>
Ensuite, vous pouvez noter que les articles avec un seul vote sont étiquetés "1 vote**s**", donc prenons le temps de pluraliser ces labels proprement. Pluralisation peut être un processus compliqué, mais pour l'instant nous ferons ça d'une façon assez simpliste. Nous allons créer un helper Spacebars général que nous pourrons utiliser n'importe où :
~~~js
Template.registerHelper('pluralize', function(n, thing) {
// pluraliser assez simpliste
if (n === 1) {
return '1 ' + thing;
} else {
return n + ' ' + thing + 's';
}
});
~~~
<%= caption "client/helpers/spacebars.js" %>
Les helpers que nous avons créés avant ont été relié au manager et template auxquels ils s'appliquent. Mais en utilisant `Template.registerHelper`, nous avons créé un helper *global* qui peut être utilisé à l'intérieur d'un template :
~~~html
<template name="postItem">
//...
<p>
{{pluralize votes "Vote"}},
submitted by {{author}},
<a href="{{pathFor 'postPage'}}">{{pluralize commentsCount "comment"}}</a>
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
//...
</template>
~~~
<%= caption "client/templates/posts/post_item.html" %>
<%= highlight "6, 8" %>
<%= screenshot "13-3", "Perfectionner Pluralisation Propre (maintenant dites ça 10 fois)" %>
<%= commit "13-3", "Helper pluraliser ajouté pour un meilleur format texte." %>
Maintenant vous devriez voir "1 vote".
### Algorithme de vote plus intelligent
Notre code de vote est bon, mais nous pouvons encore faire mieux. Dans la méthode upvote, nous créons deux appels vers Mongo : un pour trouver l'article, l'autre pour le mettre à jour.
Il y a deux problèmes avec ça. Premièrement, c'est un peu inefficace d'aller vers la base de données deux fois. Mais plus important, il introduit une concurrence. Nous suivons l'algorithme suivant :
1. Récupérer l'article de la base de données.
2. Vérifier si l'utilisateur a voté.
3. Sinon, faire un vote par l'utilisateur.
Que se passe-t-il si le même utilisateur vote plusieurs fois pour l'article entre les étapes 1 et 3 ? Notre code actuel ouvre la porte au utilisateur capables de voter pour le même article deux fois. Heureusement, Mongo nous permet d'être plus intelligent et combine les étapes 1-3 dans une seule commande Mongo :
~~~js
//...
Meteor.methods({
post: function(postAttributes) {
//...
},
upvote: function(postId) {
check(this.userId, String);
check(postId, String);
var affected = Posts.update({
_id: postId,
upvoters: {$ne: this.userId}
}, {
$addToSet: {upvoters: this.userId},
$inc: {votes: 1}
});
if (! affected)
throw new Meteor.Error('invalid', "Vous n'avez pas pu voter pour ce post.");
}
});
//...
~~~
<%= caption "collections/posts.js" %>
<%= highlight "12~21" %>
<%= commit "13-4", "Meilleur algorithme de vote." %>
Ce que nous sommes en train de dire c'est "trouve tous les articles avec cet `id` pour lesquels cet utilisateur n'a pas déjà voté, et mets les à jour dans ce sens". Si l'utilisateur *n'a pas* déjà voté, il trouvera bien entendu l'article avec cet `id`. D'un autre côté si l'utilisateur *a* voté, alors la requête correspondra à aucun documents, et par conséquent rien ne se passera.
<% note do %>
### Compensation de la latence
Disons que vous avez essayé de tricher et envoyé un de vos articles en haut de la lise en bidouillant son nombre de votes :
~~~js
> Posts.update(postId, {$set: {votes: 10000}});
~~~
<%= caption "Console du navigateur" %>
(Où `postId` est l'id d'un de vos articles)
Cette tentative impudente de jouer avec le système serait gérée par notre callback `deny()` (dans `collections/posts.js`, vous vous souvenez ?) et immédiatement rejetée.
Mais si vous regardez attentivement, vous pourriez voir la compensation de latence en action. Ça peut être rapide, mais l'article bondira brièvement en tête de la liste avant de revenir à sa position.
Que s'est-il passé ? Dans votre collection `Posts` en local, le `update` a été appliquée sans incident. Ça arrive instantanément, donc l'article a atteint le haut de la liste. Pendant ce temps, sur le serveur, le `update` a été rejeté. Puis un peu plus tard (mesuré en millisecondes si vous exécutez Meteor sur votre propre machine), le serveur a retourné une erreur, en disant à la collection en local de revenir en arrière.
Le résultat final : en attendant que le serveur réponde, l'interface utilisateur ne peut pas aider mais fait confiance à la collection en local. Aussitôt que le serveur revient et refuse la modification, les interfaces utilisateur s'adaptent pour refléter ça.
<% end %>
### Classer les articles de la première page
Maintenant que nous avons un score pour chaque article basé sur le nombre de votes, affichons la liste des meilleurs articles. Pour faire ça, nous allons voir comment gérer deux souscriptions séparées sur la collection article, et rendre notre template `postList` un peu plus général.
Pour commencer, nous voulons avoir *deux* souscriptions, une pour chaque ordre de tri. L'astuce ici est que les deux souscriptions souscriront à la *même* publication `posts`, seulement avec des arguments différents.
Nous allons également créer deux nouvelles routes appelées `newPosts` et `bestPosts`, accessibles respectivement aux URLs `/new` et `/best` (avec `/new/5` et `/best/5` pour notre pagination bien sur).
Pour faire cela, nous *étendrons* notre `PostsListController` dans deux contrôleurs `NewPostListController` et `BestPostsListController` distincts. Ça nous laissera réutiliser exactement les mêmes options de routes pour les deux routes `home` et `newPosts`, en nous donnant un seul `NewPostsListController` d'où hériter. De plus, c'est juste une belle illustration de la flexibilité de Iron Router.
Remplaçons donc la propriété de tri `{submitted: -1}` dans `PostsListController` par `this.sort`, qui sera fournie par `NewPostsListController` et `BestPostsListController`:
~~~js
//...
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: this.sort, limit: this.postsLimit()};
},
subscriptions: function() {
this.postsSub = Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
return Posts.find({}, this.findOptions());
},
data: function() {
var hasMore = this.posts().count() === this.postsLimit();
return {
posts: this.posts(),
ready: this.postsSub.ready,
nextPath: hasMore ? this.nextPath() : null
};
}
});
NewPostsController = PostsListController.extend({
sort: {submitted: -1, _id: -1},
nextPath: function() {
return Router.routes.newPosts.path({postsLimit: this.postsLimit() + this.increment})
}
});
BestPostsController = PostsListController.extend({
sort: {votes: -1, submitted: -1, _id: -1},
nextPath: function() {
return Router.routes.bestPosts.path({postsLimit: this.postsLimit() + this.increment})
}
});
Router.route('/', {
name: 'home',
controller: NewPostsController
});
Router.route('/new/:postsLimit?', {name: 'newPosts'});
Router.route('/best/:postsLimit?', {name: 'bestPosts'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "10,23,27~55" %>
Notez que maintenant que nous avons plus d'une route, nous prenons la logique `nextPath` hors de `PostListController` et la plaçons dans `NewPostsController` et `BestPostsController`, à partir du moment où le chemin sera différent dans les deux cas.
De plus, quand nous trions par `votes`, nous avons des tris ultérieurs par horodatage d'envoi puis par `_id` pour nous assurer que l'ordre est entièrement spécifié.
Avec nos nouveaux contrôleurs en place, nous pouvons maintenant sans problème nous débarrasser de la route `postList` précédente. Supprimez simplement le code suivant :
```
Router.route('/:postsLimit?', {
name: 'postsList'
})
```
<%= caption "lib/router.js" %>
Nous ajouterons également des liens dans l'en-tête :
~~~html
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'home'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
<li>
<a href="{{pathFor 'newPosts'}}">New</a>
</li>
<li>
<a href="{{pathFor 'bestPosts'}}">Best</a>
</li>
{{#if currentUser}}
<li>
<a href="{{pathFor 'postSubmit'}}">Submit Post</a>
</li>
<li class="dropdown">
{{> notifications}}
</li>
{{/if}}
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>
~~~
<%= caption "client/templates/includes/header.html" %>
<%= highlight "11, 15~20" %>
Et finalement, nous avons également besoin de mettre à jour notre gestionnaire de suppression d'article :
~~~html
'click .delete': function(e) {
e.preventDefault();
if (confirm("Delete this post?")) {
var currentPostId = this._id;
Posts.remove(currentPostId);
Router.go('home');
}
}
~~~
<%= caption "client/templates/posts/posts_edit.js" %>
<%= highlight "7" %>
Avec tout cela de fait, nous avons maintenant une liste des meilleurs posts.
<%= screenshot "13-4", "Classer par points" %>
<%= commit "13-5", "Routes ajoutées pour les listes d'articles, et pages pour les afficher." %>
### Une meilleur en-tête
Maintenant que nous avons deux pages de liste d'articles, il peut être difficile de savoir laquelle vous êtes en train de regarder. Donc revisitons notre en-tête pour rendre ça plus évident. Nous allons créer un gestionnaire `header.js` et créer un helper qui utilise le chemin courant et une ou plusieurs routes nommées pour ajouter une class active dans nos items de navigation :
La raison pour laquelle nous voulons supporter de multiples routes nommées est que les deux routes `home` et `newPosts` (qui correspondent respectivement aux URLs `/` et `/new`) invoque le même template. Ça signifie que notre `activeRouteClass` devrait être assez intelligente pour rendre actif le tag `<li>` dans les deux cas.
~~~html
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'home'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
<li class="{{activeRouteClass 'home' 'newPosts'}}">
<a href="{{pathFor 'newPosts'}}">New</a>
</li>
<li class="{{activeRouteClass 'bestPosts'}}">
<a href="{{pathFor 'bestPosts'}}">Best</a>
</li>
{{#if currentUser}}
<li class="{{activeRouteClass 'postSubmit'}}">
<a href="{{pathFor 'postSubmit'}}">Submit Post</a>
</li>
<li class="dropdown">
{{> notifications}}
</li>
{{/if}}
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>
~~~
<%= caption "client/templates/includes/header.html" %>
<%= highlight "15,18,22" %>
~~~js
Template.header.helpers({
activeRouteClass: function(/* route names */) {
var args = Array.prototype.slice.call(arguments, 0);
args.pop();
var active = _.any(args, function(name) {
return Router.current() && Router.current().route.getName() === name
});
return active && 'active';
}
});
~~~
<%= caption "client/templates/includes/header.js" %>
<%= screenshot "13-5", "Montrer la page active" %>
<% note do %>
### Arguments des helpers
Nous n'avons pas utilisé ce patron spécifique jusqu'à maintenant, mais tout comme d'autres tags Spacebars, les tags de template helper peuvent prendre des arguments.
Et pendant que vous pouvez bien entendu passer des arguments nommés spécifiques dans votre fonction, vous pouvez également passer un nombre non spécifié de paramètres anonymes et les récupérer en appelant l'objet `arguments` dans la fonction.
Dans ce dernier cas, vous voudrez probablement convertir l'objet `arguments` en tableau Javascript classique et ensuite appeler `pop()` dessus pour vous débarrasser du hash ajouté à la fin par Spacebars.
<% end %>
Pour chaque item de navigation, le helper `activeRouteClass` prendre une liste de noms de routes, et ensuite utilise le helper `Any()` de Underscore pour voir si certaines des routes passent le test (i.e. leur url correspondante est égale au chemin courant).
Si une de ces routes correspond avec le chemin courant, `any()` retournera `true`. Finalement, nous prenons avantage du patron Javascript `boolean && string` où `false && myString` retourne `false`, mais `true && myString` retourne `myString`.
<%= commit "13-6", "Classes active ajoutées à l'en-tête." %>
Maintenant que les utilisateurs peuvent voter sur les articles en temps réel, vous verrez les items monter et descendre la page d'accueil en fonction de leur classement. Mais ne serait-il pas joli s'il y avait un moyen d'affiner tout ça avec quelques animations bien temporisées ?