forked from DiscoverMeteor/DiscoverMeteor_De
-
Notifications
You must be signed in to change notification settings - Fork 0
/
10-comments.md.erb
434 lines (344 loc) · 14.9 KB
/
10-comments.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
---
title: Kommentare
slug: comments
complete: 0
date: 2014/03/13
number: 10
contents: Anzeigen von existierenden Kommentaren.|Hinzufügen eines Kommentarformulars.|Lerne wie du erreichst, dass nur Kommentare des aktuellen Beitrags geladen werden.|Füge eine Kommentaranzahl-Eigenschaft zu den Beiträgen hinzu.
paragraphs: 34
---
Das Ziel einer Social-Media-News-Site ist es eine aktive Benutzer-Community zu erschaffen. Damit dieses Ziel erreicht werden kann, ist es unerlässlich, dass sich die Benutzer austauschen können. Dazu lass uns in diesem Kapitel eine Kommentarfunktion hinzufügen.
~~~js
Comments = new Meteor.Collection('comments');
~~~
<%= caption "collections/comments.js" %>
~~~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: now - 7 * 3600 * 1000
});
Comments.insert({
postId: telescopeId,
userId: tom._id,
author: tom.profile.name,
submitted: now - 5 * 3600 * 1000,
body: 'Interesting project Sacha, can I get involved?'
});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: 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: now - 10 * 3600 * 1000
});
Posts.insert({
title: 'The Meteor Book',
userId: tom._id,
author: tom.profile.name,
url: 'http://themeteorbook.com',
submitted: now - 12 * 3600 * 1000
});
}
~~~
<%= caption "server/fixtures.js" %>
Wir dürfen nicht vergessen die neue Collection im Server zu veröffentlichen (publish) und im Router zu abonnieren (subscribe):
~~~js
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function() {
return Comments.find();
});
~~~
<%= caption "server/publications.js" %>
<%= highlight "5,6,7" %>
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() {
return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
}
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "4~6" %>
<%= commit "10-1", "Added comments collection, pub/sub and fixtures." %>
Damit der Fixture-Code ausgeführt wird, musst du vorher die Datenbank mit: `meteor reset` entleeren. Denk daran einen neuen Benutzer nach dem Reset anzulegen.
Im Fixture-Code legen wir als Erstes ein paar Dummy-Benutzer an, fügen sie zur Datenbank hinzu und verwenden die generierten `id`s um sie anschließend wieder aus der Datenbank abzurufen. Anschließend fügen wir für jeden Benutzer einen Kommentar am ersten Beitrag hinzu. Die Verbindung vom Kommentar zum Beitrag stellen wir mit dem Attribut `postId` her und die Verbindung zum Benutzer analog mit dem Attribut `userId`. Weiterhin bekommen unsere Komentare ein Anlagedatum (`submitted`), ein Attribut für den Kommentartext (`body`) und den Namen des Authors (ein denormalisiertes Feld).
Bleibt anzumerken, dass wir unseren Router so erweitert haben, dass nun auf die Initialisierung der `posts` als auch der `comments` gewartet wird.
### Displaying comments
Wir schreiben die Kommentare nun erfolgreich in die Datenbank, aber wir müssen sie auch auf der Diskussions-Seite zur Anzeige bringen. Die notwendigen Schritte um dies zu erreichen, sollten dir inzwischen ziemlich vertraut sein.
~~~html
<template name="postPage">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> comment}}
{{/each}}
</ul>
</template>
~~~
<%= caption "client/views/posts/post_page.html" %>
<%= highlight "3~7" %>
~~~js
Template.postPage.helpers({
comments: function() {
return Comments.find({postId: this._id});
}
});
~~~
<%= caption "client/views/posts/post_page.js" %>
<%= highlight "2~4" %>
Im Template `postPage` fügen wir den Block `{{#each comments}}` ein. Innerhald des Helpers `comments` entspricht `this` damit einem Beitrag. Um die zugehörigen Kommenentare zu finden selektieren wir die Kommentare nach der ID des Beitrags.
Berücksichtigen wir, was wir über Helpers und Handlebars gelernt haben, gestaltet sich das Rendering eines Kommentars als ziemlich unkompliziert. Wir legen ein neues Verzeichnis namens `comments` im `views` Verzeichnis an. Dort werden alle Informationen zu unseren Kommentaren gespeichert.
~~~html
<template name="comment">
<li>
<h4>
<span class="author">{{author}}</span>
<span class="date">on {{submittedText}}</span>
</h4>
<p>{{body}}</p>
</li>
</template>
~~~
<%= caption "client/views/comments/comment.html" %>
Lass uns einen einfachen Template-Helper definieren, der dazu dient das Anlagedatum in einem menschenlesbaren Format auszugeben (es sei denn, du gehörst zu denen, die UNIX-Timestamps und hexadezimale Farbencodes flüssig lesen und schreiben können.)
~~~js
Template.comment.helpers({
submittedText: function() {
return new Date(this.submitted).toString();
}
});
~~~
<%= caption "client/views/comments/comment.js" %>
////
~~~html
<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
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">Discuss</a>
</div>
</template>
~~~
<%= caption "client/views/posts/post_item.html" %>
<%= highlight "6,7" %>
////
~~~js
Template.postItem.helpers({
ownPost: function() {
return this.userId == Meteor.userId();
},
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
},
commentsCount: function() {
return Comments.find({postId: this._id}).count();
}
});
~~~
<%= caption "client/views/posts/post_item.js" %>
<%= highlight "9,10,11" %>
<%= commit "10-2", "Display comments on `postPage`." %>
Die Fixture-Kommentare solltest du nun zur Anzeige bringen können. Das Ganze sollte in etwa so aussehen:
<%= screenshot "10-1", "Displaying comments" %>
### Submitting Comments
Lass uns einen Weg bereitstellen, der es den Benutzern erlaubt neue Kommentare hinzuzufügen. Dies gestaltet sich ziemlich ähnlich zur Vorgehensweise, die wir angewandt haben, um das Anlegen neue Beiträgen zu ermöglichen.
Wir beginnen mit dem Hinzufügen eines Kommentarbereichs am Ende eines jeden Beitrags.
~~~html
<template name="postPage">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> comment}}
{{/each}}
</ul>
{{#if currentUser}}
{{> commentSubmit}}
{{else}}
<p>Please log in to leave a comment.</p>
{{/if}}
</template>
~~~
<%= caption "client/views/posts/post_page.html" %>
<%= highlight "11~15" %>
Und erzeugen dann das Formular-Template zur Anlage eines Kommentars.
~~~html
<template name="commentSubmit">
<form name="comment" class="comment-form">
<div class="control-group">
<div class="controls">
<label for="body">Comment on this post</label>
<textarea name="body"></textarea>
</div>
</div>
<div class="control-group">
<div class="controls">
<button type="submit" class="btn">Add Comment</button>
</div>
</div>
</form>
</template>
~~~
<%= caption "client/views/comments/comment_submit.html" %>
<%= screenshot "10-2", "The comment submit form" %>
Zum Übermitteln unseres Kommentars wird die Methode `comment` im Manager `commentSubmit` aufgerufen. Dieser arbeitet analog zu dem Manager `postSubmit`:
~~~js
Template.commentSubmit.events({
'submit form': function(e, template) {
e.preventDefault();
var $body = $(e.target).find('[name=body]');
var comment = {
body: $body.val(),
postId: template.data._id
};
Meteor.call('comment', comment, function(error, commentId) {
if (error){
throwError(error.reason);
} else {
$body.val('');
}
});
}
});
~~~
<%= caption "client/views/comments/comment_submit.js" %>
Genau wie wir vorher die serverseitige Meteor-Methode `post` definiert haben, legen wir nun die Meteor-Methode `comment` an, wir überprüfen die übermittelten Daten, erzeugen einen Kommentar-Objekt und fügen dieses in die Kommentar-Collection ein.
~~~js
Comments = new Meteor.Collection('comments');
Meteor.methods({
comment: function(commentAttributes) {
var user = Meteor.user();
var post = Posts.findOne(commentAttributes.postId);
// ensure the user is logged in
if (!user)
throw new Meteor.Error(401, "You need to login to make comments");
if (!commentAttributes.body)
throw new Meteor.Error(422, 'Please write some content');
if (!post)
throw new Meteor.Error(422, 'You must comment on a post');
comment = _.extend(_.pick(commentAttributes, 'postId', 'body'), {
userId: user._id,
author: user.username,
submitted: new Date().getTime()
});
return Comments.insert(comment);
}
});
~~~
<%= caption "collections/comments.js" %>
<%= highlight "3~25" %>
<%= commit "10-3", "Created a form to submit comments." %>
Es passieren nicht wirklich komplizierte Dinge: wir prüfen, dass ein Benutzer angemeldet ist, dass der Kommentar einen `body` hat und dass der Kommentar auf einen existierenden Beitrag verweist.
### Controlling the Comments Subscription
Momentan veröffentlichen wir alle Kommentare aller Beiträge an alle verbundenen Clients. Das erscheint etwas verschwenderisch. Denn eigentlich verwenden wir ja nur einen kleinen Anteil der Daten zu jedem gegebenen Zeitpunkt. Nachfolgend werden wir unsere Publication und Subscription anpassen, um genau zu kontrollieren welche Kommentare veröffentlicht werden.
Wenn wir darüber nachdenken, dann ist der einzige Zeitpunkt an dem wir überhaupt auf Kommentare zugreifen müssen, der Moment in dem wir einen individuellen Beitrag anzeigen. Und wir müssen nur die Kommentare laden, die zu diesem Beitrag gehören.
Im ersten Schritt werden wir die Art und Weise ändern, wie wir Kommentare abonnieren (subscribe). Bisher haben wir die Kommentare auf der *Router*-Ebene abonniert, was bedeutet, dass wir die gesamten Daten laden, wenn wir den Router initialisieren.
Aber jetzt wollen wir, dass die Subscription abhängig von einem Pfad-Parameter ist. Dieser Parameter kann sich offensichtlich zu jedem Zeitpunkt ändern. Also müssen wir unsere Subscription von der *Router*-Ebene auf die *Route*-Ebene verschieben.
Daraus ergibt, dass wir unsere Daten nicht mehr beim Initialisieren unserer App laden, sondern wenn die *Route* aufgerufen wird. Daraus resultieren Wartezeiten beim Browsen innerhalb unsere Anwendung. Was jedoch unvermeidlich ist, wenn wir nicht sämtliche Daten bereits beim Initialiesern unserer App laden wollen.
So sieht der Code unserer neuen *Route*-Level-Funktion `waitOn` aus:
~~~js
Router.map(function() {
//...
this.route('postPage', {
path: '/posts/:_id',
waitOn: function() {
return Meteor.subscribe('comments', this.params._id);
},
data: function() { return Posts.findOne(this.params._id); }
});
//...
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "7~9" %>
Dir ist sicher aufgefallen, dass wir `this.params._id` als Argument an die Subscription übergeben. Also lass uns nun diese Information benutzen um die Menge der Kommentare auf die einzuschränken, die zum aktuellen Beitrag gehören.
~~~js
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function(postId) {
return Comments.find({postId: postId});
});
~~~
<%= caption "server/publications.js" %>
<%= highlight "5~7" %>
<%= commit "10-4", "Made a simple publication/subscription for comments." %>
Es gibt eigentlich nur ein Problem: wenn wir wieder auf die Homepage zurückkehren, behauptet unsere App, dass alle Beiträge null Kommentare haben:
<%= screenshot "10-3", "Our comments are gone!" %>
### Counting Comments
Der Grund dafür wird schnell klar: wir haben zu jedem Zeitpunkt die Kommentare von maximal einem unserer Beiträge geladen. Wenn wir also `Comments.find({postId: this._id})` im Helper `commentsCount` im Manager `post_item` aufrufen, kann Meteor die notwendigen client-seitigen Daten nicht finden und damit auch nicht als Resultat zur Verfügung stellen.
Der beste Weg damit umzugehen ist die Anzahl der Kommentare zu *denormalisieren*, also als Attribut des Beitrags zu speichern (keine Sorge, falls Du nicht sicher bist was damit gemeint ist, die nächste Sidebar hilft Dir weiter.) Auch wenn wir, wie wir gleich sehen werden, eine geringfügig höhere Komplexität in Kauf nehmen, so gewinnen wir doch erhebliche Performancevorteile, dadurch, dass wir nicht _alle_ Kommentare veröffentlichen müssen um die Liste der Beiträge anzuzeigen.
Wir erreichen dies in dem wir das Attribut `commentsCount` zur Datenstruktur `post` hinzufügen. Wir beginnen beim Aktualisieren des Fixture-Codes unseres Beitrags (und `meteor reset` um die Beiträge erneut zu laden -- denk daran die Benutzerkennung wiederherzustellen):
~~~js
var telescopeId = Posts.insert({
title: 'Introducing Telescope',
..
commentsCount: 2
});
Posts.insert({
title: 'Meteor',
...
commentsCount: 0
});
Posts.insert({
title: 'The Meteor Book',
...
commentsCount: 0
});
~~~
<%= caption "server/fixtures.js" %>
Dann stellen wir sich, dass allen neuen Beiträge mit 0 Kommentaren erstellt werden:
~~~js
// pick out the whitelisted keys
var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
userId: user._id,
author: user.username,
submitted: new Date().getTime(),
commentsCount: 0
});
var postId = Posts.insert(post);
~~~
<%= caption "collections/posts.js" %>
Und dann aktualisieren wir den `commentsCount` wenn wir einen neuen Kommentar erstellen, indem wir Mongo's `$inc` Operator (welcher ein numerisches Feld um den Wert eins erhöht) verwenden:
~~~js
// update the post with the number of comments
Posts.update(comment.postId, {$inc: {commentsCount: 1}});
return Comments.insert(comment);
~~~
<%= caption "collections/comments.js "%>
Schlussendlich können wir den Helper `commentsCount` aus `client/views/posts/post_item.js` entfernen, da das Feld nun direkt am Beitrag verfügbar ist.
<%= commit "10-5", "Denormalized the number of comments into the post." %>
Nun, da die Benutzer miteinander sprechen können, wäre es eine Schande wenn sie neue Kommentare verpassen würden. Tja, jetzt kannst Du vielleicht den Inhalt des nächsten Kapitels erraten: wir werden Benachrichtigungen implementieren.