-
Notifications
You must be signed in to change notification settings - Fork 11
/
13-voting.md.erb
703 lines (563 loc) · 24.8 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
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
---
title: 投票機能
slug: voting
date: 0013/01/01
number: 13
points: 10
photoUrl: http://www.flickr.com/photos/ikewinski/8561920811/
photoAuthor: Mike Lewinski
contents: ユーザーが投稿に投票することができるシステムを構築します。|"best"ページ上に投票によってランク付けされた投稿を一覧表示。|一般的なSpacebarsヘルパーの書き方を学びます。|データセキュリティについて少し詳細について。|MongoDBの中でいくつかの興味深いパフォーマンスに関する考慮事項。
paragraphs: 49
version: 1.7.1
---
私たちのサイトは だんだんと一般向けになってきました。
今、本サイトは、最高のリンクを見つけ、より人気になっているリンクを取得するのは厄介です。
記事を並び変えるためのなんらかのランキングシステムが必要です。
私たちは、カルマ、ポイントの時間ベースの崩壊、
そして他の多くのものを持つ複雑なランキングシステムを構築することができます。
(そのほとんどは、[Telescope](http://telesc.pe)、Microscopeの兄に実装されています)
しかしこのアプリでは、シンプルにして、投票数で投稿を格付けることにします。
ユーザーに 投稿に投票する方法を提供することから始めていきましょう。
### データモデル
私たちは、私たちがユーザーにupvoteボタンを表示するだけでなく、
2回投票しているかどうかを知ることができる各投稿毎のupvotersのリストを格納します。
<% note do %>
### データのプライバシー & パブリケーション
私たちは、すべのユーザーに投票者のリストを公開します。
これでブラウザーコンソールで自動的にそのデータを公的にアクセスできるようにします。
これはある種のデータプライバシー問題ですが、
これはコレクションを動かす方法に起因しています。
たとえば、私たちはユーザーに誰が投稿に投票したのかわかるようにしたいでしょうか?
この場合、その情報を公的に利用することは、実際のところ全く影響も及ぼしませんが、
この問題を少なくとも認識することは重要です。
<% end %>
また、それが簡単に数字を取得するために、
投稿にupvotersの合計数を非正規化します。
投稿に`upvoters`と`votes`を2つの属性を追加しましょう。
私たちのfixturesファイルにそれらを追加することから始めましょう。:
~~~js
// Fixture data
if (Posts.find().count() === 0) {
var now = new Date().getTime();
// create two users
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: 'Interesting project Sacha, can I get involved?'
});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: new Date(now - 3 * 3600 * 1000),
body: 'You sure can 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" %>
これまで通り、アプリを停止して、`meteor reset`を実行して、新しいユーザーアカウントを作ります。
それから、投稿が作成された時に2つのプロパティが初期化されるようにします。
~~~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" %>
### 投稿テンプレート
最初に、投稿セルにupvoteボタンを追加し、投稿のメタデータとして投稿数を表示します:
~~~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", "The upvote button" %>
次に、 ユーザーがボタンをクリックしたら、サーバーの upvoteメソッドを呼び出します。:
~~~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", "Added basic upvoting algorithm." %>
このメソッドは非常に簡単です。
私たちは、ユーザが、ポストが実際に存在していることを記録されていることを確認するために、
いくつかの防御的なチェックを行います。
その後、ユーザーがすでにポストに投票していないことを再度確認し、いない場合、
投票の合計スコアをインクリメントし、upvotersのセットにユーザーを追加します。
この最後のステップは、興味深い特別なMongoの演算子を使用しました。
他にも学ぶべき演算子はありますが、これら二つは非常に有用です。
`$addToSet`は、それがすでに存在していない場合に限り配列プロパティに項目を追加し、
`$inc`は、単純に整数フィールドをインクリメントします。
### ユーザーインターフェースの微調整
ユーザーがログインしていないか、既にポストをupvotedしている場合、彼らは投票することができません。
UIでこれを反映するために、条件付きでupvoteボタンに`disabled`CSSクラスを追加するために
ヘルパーを使用します。
~~~html
<template name="postItem">
<div class="post">
<a href="#" class="upvote btn btn-default {{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" %>
`.upvote`から`.upvotable`へクラスを変更しているので、クリックイベントハンドラを変更することを忘れないでください。
<%= screenshot "13-2", "Greying out upvote buttons." %>
<%= commit "13-2", "Grey out upvote link when not logged in / already voted." %>
次に、あなたが "1 vote**s**”と、単一の投票がラベル付けされていることに気づくでしょう。
適切にこれらのラベルのプロパティを調整するには時間がかかるかもしれません。
複数化は、複雑なプロセスになる可能性がありますが、今のところは、かなり単純な方法でこれを行います。
我々はどこにでも使用することができ、一般的なSpacebarsヘルパーを作ります。
~~~js
UI.registerHelper('pluralize', function(n, thing) {
// fairly stupid pluralizer
if (n === 1) {
return '1 ' + thing;
} else {
return n + ' ' + thing + 's';
}
});
~~~
<%= caption "client/helpers/spacebars.js" %>
以前に作成したヘルパーは、適用対象のテンプレートに縛られてきたました。
しかし`UI.registerHelper`を使用することによって、
任意のテンプレート内で使用することができる*グローバル*ヘルパーを作成しました:
~~~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", "Perfecting Proper Pluralization (now say that 10 times)" %>
<%= commit "13-3", "Added pluralize helper to format text better." %>
"1 vote"と見えることを確認してください。
### よりスマートな投票アルゴリズム
私たちのupvotingコードは格好良いですが、まだ良くすることができます。
upvote Methodでは、Mongoへの2つの呼び出しを行います。:
一つ目は投稿を取得するため、二つ目は更新するためです。
二つの問題があります。
第一に、それは二度データベースに行く分多少非効率的です。
しかし、もっと重要なのは、競合状態が導入されています。次のアルゴリズムを追ってください:
1. データベースから投稿を取得する。
2. ユーザーが投票したかどうかを確認する。
3. 投票していない場合は、ユーザーによる投票を行う。
同じユーザがステップ1と3の間で再びポストに投票した場合はどうなりますか?
現在のコードでは、二度同じポストに投票することができるという可能性があります。
ありがたいことに、Mongoは賢くなり、手順1〜3を組み合わせて、
単一の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', "You weren't able to upvote that post");
}
});
//...
~~~
<%= caption "collections/posts.js" %>
<%= highlight "12~21" %>
<%= commit "13-4", "Better upvoting algorithm." %>
これは「すべての投稿を`id`で見つけ、このユーザはまだに投票していなければこの方法でそれらを更新する」
という処理です。
ユーザーは*まだ*投票していない場合、当然ですが`id`を持つ投稿を見つけるでしょう。
一方ユーザーが投票した場合、問合せは書類と一致しなくなり、その結果、何も起こりません。
<% note do %>
### Latency Compensation
例えば、あなたがチートや投票のその数を微調整することにより、リストの一番上にあなたの記事のいずれかを送信しようとしたとしましょう:
~~~js
> Posts.update(postId, {$set: {votes: 10000}});
~~~
<%= caption "Browser console" %>
(ここで、 `postId`はあなたの記事の1つのidです)
システムに対する遊びとしての図々しい試みは私たちの`deny()`コールバックによってキャッチされます。
( `collections/posts.js`です。覚えてます?)、そして、すぐに打ち消されます。
しかし、慎重に見ればあなたはこの行為でlatency compensationを目撃することができるかもしれません。
それは、それは一瞬で進みますが、投稿は元の位置に戻る前に、一時的にリストの一番上にジャンプします。
何が起こったのか?ローカルの`Posts`コレクションでは、`update`は何事もなく適用されます。
これは瞬時に起こるのですが、投稿がリストの一番上にジャンプします。
一方、サーバー上で、`update`が拒否されていました。
なので、ちょっと後(自分のマシン上でMeteorを実行している場合は、ミリ秒単位です。)に、
サーバは、エラーが返され、ローカルコレクションに元に戻すように指示しました。
最終結果:サーバが応答するのを待っている間、ユーザーインターフェースの助けにはなりますが、
ローカルコレクションを信頼することはできません。
とすぐに、サーバが戻ってくると変更を拒否したように、
ユーザインタフェースは、それを反映するように適応させれます。
<% end %>
### フロントページの投稿ランキング
今のところ、投票数に基づいた、投稿ごとにスコアがあるので、
最も良い投稿のリストを表示しましょう。
そうするために、私たちはpostコレクションの、2つのサブスクリプションを管理する方法を見て、
`postList`テンプレートをさらに少しだけ一般的にします。
始めるにあたり、異なるソート順の*二つの*サブスクリプションを持ちたいと思います。
ここでのトリックは、両方のサブスクリプションが、
*同じ*`posts`パブリケーションに異なる引数でサブスクライブということです!
また、ページネーションのための二つの新しいルート`newPosts`、`bestPosts`を作成しそれぞれ
`/new`と`/best`というURLでアクセス可能にします。
(もちろんページング用は`/new/5`、`/best/5`となります)
これを行うために、我々は`PostsListController`を*拡張*し、2つの別個の`NewPostsListController`と
`BestPostsListController`を作成します。
これは、`home`と`newPosts`ルートの両方に、
単一の継承された`NewPostsListController`を与えることによって、
まったく同じルートオプションを再利用できるようになります。
そして、Iron Router がいかに柔軟なかを説明できたと思います。
それでは、`{submitted: -1}`で`PostsListController`のソートプロパティ`this.sort`を上書き
しましょう。`NewPostsListController` and `BestPostsListController`を置き換えましょう:
So let's replace the `{submitted: -1}` sort property in `PostsListController` by `this.sort`, which will be provided by `NewPostsListController` and `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" %>
注意点として、一つ以上のルートを持ち、パスはどちらの場合では異なるであろうことから、`nextPath`ロジックを`PostsListController`から出して、
`NewPostsController`、 `BestPostsController`に入れています。
加えて、`votes`によってソートする時、タイムスタンプや `_id`によって後続のソートを指定し、
順序が完全に指定されていることを確認しました。
Additionally, when we sort by `votes`, we have a subsequent sorts by submitted timestamp and then `_id` to ensure that the ordering is completely specified.
新しいコントローラでは、安全に、以前の`postsList`ルートを取り除くことができます。
ただ、次のコードを削除します:
```
Router.route('/:postsLimit?', {
name: 'postsList'
})
```
<%= caption "lib/router.js" %>
ヘッダー内にリンクも追加しましょう:
~~~html
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<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>
</div>
</nav>
</template>
~~~
<%= caption "client/templates/includes/header.html" %>
<%= highlight "11, 15~20" %>
最後に、投稿削除のイベントハンドラを更新する必要があります:
~~~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_edit.js" %>
<%= highlight "7" %>
これがすべて終わると、私たちは ベスト投稿リストを得ます:
<%= screenshot "13-4", "Ranking by points" %>
<%= commit "13-5", "Added routes for post lists, and pages to display them." %>
### A Better Header
これで2つのリストページがあるので、
どちらのリストを現在見ているのか知ることは難しいかもしれません。
そのため、もっとわかりやすくするために、ヘッダーを再検討してみましょう。
ナビゲーション項目上で`.active`クラスを設定するために
現在のパスと1つ以上の名前付きルートを使用して`header.js`マネージャとヘルパーを作成します:
複数の名前付きルートをサポートしたい理由は、
私たちの`home`と`newPosts`ルートの両方(URLはそれぞれ`/`と `/ new`)
に同じテンプレートを使いたいからです。
両方のケースで`<LI>`タグをアクティブにするので、
`activeRouteClass`は十分にスマートでなければならないことを意味します。
~~~html
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<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>
</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", "Showing the active page" %>
<% note do %>
### ヘルパーの引数
私たちは、今までこのパターンを使用していませんでした。
他のSpacebarsのタグのように、テンプレートヘルパータグは引数を取ることができます。
あなたはもちろん、あなたの関数に特定の名前付き引数を渡すことができつつ、
不特定多数の匿名のパラメータを渡し、関数内で`arguments`オブジェクトを呼び出すことで、
それらを取得することができます。
この最後のケースでは、
あなたはおそらく正規のJavaScript配列に`arguments`オブジェクトを変換したいと思うでしょうし、
それでSpacebarsによって最後に追加されたハッシュを取り除くために`pop()`を呼び出しました。
<% end %>
各ナビゲーションアイテムで、`activeRouteClass`ヘルパーはルート名のリストを取り、
その後の経路のいずれかがテスト(対応するURLが現在のパスに等しいかどうか)
に合格するかどうかを確認するために、
Underscoreの`any()`ヘルパーを使用しています。
もし現在のパスにルートがマッチするなら、`any()`は`true`を返します。
最後に、`boolean && string`というJavaScriptパターンの利点についてですが、これは
`false && myString`であれば`false`を返しますが、`true && myString`であれば `myString`を返します。
<%= commit "13-6", "Added active classes to the header." %>
これでユーザーはリアルタイムで投稿に投票することができるので、
ランキングが変化する度に項目ががページ上で上下することがわかります。
この変化時にアニメーションが滑らかできれば、素晴らしいことではないでしょうか?