-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy path10-comments.md.erb
526 lines (422 loc) · 18.4 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
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
---
title: Tính năng bình luận
slug: comments
complete: 100
date: 0010/01/01
number: 10
points: 10
photoUrl: http://www.flickr.com/photos/ikewinski/9414222270/
photoAuthor: Mike Lewinski
contents: Hiển thị bình luận.|Thêm form gửi bình luận cho bài viết.|Học cách chỉ hiển thị bình luận của bài viết hiện tại.|Thêm thuộc tính đếm số bình luận.
paragraphs: 34
---
Mục tiêu của trang tin tức mạng xã hội là để tạo một cộng đồng người dùng. Và nó sẽ trở nên khó khăn nếu như chúng ta không cung cấp một cách để mọi người giao tiếp với nhau. Vì vậy trong chương này, hãy cùng nhau thêm vào bình luận!
Chúng ta sẽ bắt đầu bằng việc tạo một collection mới để lưu bình luận vào, và cũng sẽ thêm vào một số dữ liệu cố định vào collection đó.
~~~js
Comments = new Mongo.Collection('comments');
~~~
<%= caption "lib/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: new Date(now - 7 * 3600 * 1000)
});
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)
});
Posts.insert({
title: 'The Meteor Book',
userId: tom._id,
author: tom.profile.name,
url: 'http://themeteorbook.com',
submitted: new Date(now - 12 * 3600 * 1000)
});
}
~~~
<%= caption "server/fixtures.js" %>
Đừng quen việc publish và subscribe cho collection mới:
~~~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',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
}
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "5~7" %>
<%= commit "10-1", "Added comments collection, pub/sub and fixtures." %>
Chú ý rằng để kích hoạt đoạn code này, bạn cần phải dùng `meteor reset` để làm sạch cơ sở dữ liệu. Sau khi đã thiết lập lại, cũng không được quên tạo một tài khoản mới và đăng nhập trở lại!
Đầu tiên, chúng ta đã tạo một vài người dùng (hoàn toàn là tài khoản giả), chèn vào cơ sở dữ liệu và sử dụng `id` để chọn chúng ra khỏi cơ sở dữ liệu sau đấy. Sau đó chúng ta thêm vào bình luận cho mỗi người dùng trong bài viết đầu tiên, liên kết bình luận với bài viết (bằng `postId`), và người dùng (bằng `userId`). Chúng ta cũng đã thêm ngày gửi và nội dung cho mỗi bình luận, cùng với `author`, một trường được chuẩn hoá.
Ngoài ra, chúng ta đã bổ xung thêm định tuyến để đợi cho *mảng* chứa cả những bình luận và subscription của các bài viết.
### Hiển thị bình luận
Việc lưu bình luận vào cơ sở dữ liệu đã được xong xuôi, nhưng chúng ta cũng cần phải hiển thị chúng trên trang thảo luận. Hi vọng rằng quá trình này đã trở lên quen thuộc với bạn, và bạn có được ý tưởng những bước cần phải thực hiện:
~~~html
<template name="postPage">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
</template>
~~~
<%= caption "client/templates/posts/post_page.html" %>
<%= highlight "3~7" %>
~~~js
Template.postPage.helpers({
comments: function() {
return Comments.find({postId: this._id});
}
});
~~~
<%= caption "client/templates/posts/post_page.js" %>
<%= highlight "2~4" %>
Chúng ta đặt khối `{{#each comments}}` bên trong template của bài viết, để cho đối tượng `this` là một bài viết bên trong helper `comments`. Để tìm ra bình luận tương ứng, chúng ta kiểm tra những bình luận được gắn với bài viết thông qua thuộc tính `postId`.
Bằng việc dùng những gì chúng ta đã được học về helper và Spacebar, việc dịch ra một mình luận là khá hiển nhiên. Chúng ta sẽ tạo ra một thư mục `comments` bên trong thư mục `templates` để lưu thông tin về bình luận, và một template `commentItem` bên trong nó:
~~~html
<template name="commentItem">
<li>
<h4>
<span class="author">{{author}}</span>
<span class="date">on {{submittedText}}</span>
</h4>
<p>{{body}}</p>
</li>
</template>
~~~
<%= caption "client/templates/comments/comment_item.html" %>
Hãy cùng tạo một template helper để định dạng ngày `submitted` theo một chuẩn thân thiện hơn:
~~~js
Template.commentItem.helpers({
submittedText: function() {
return this.submitted.toString();
}
});
~~~
<%= caption "client/templates/comments/comment_item.js" %>
Sau đó, chúng ta sẽ hiển thị số lượng bình luận cho mỗi bài viết:
~~~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 btn-default">Discuss</a>
</div>
</template>
~~~
<%= caption "client/templates/posts/post_item.html" %>
<%= highlight "6,7" %>
Và thêm helper `commentsCount` vào `post_item.js`:
~~~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/templates/posts/post_item.js" %>
<%= highlight "9,10,11" %>
<%= commit "10-2", "Display comments on `postPage`." %>
Bây giờ bạn có thể hiển thị được những bình luận đã thêm và sẽ thấy được như sau:
<%= screenshot "10-1", "Displaying comments" %>
### Thêm bình luận
Hãy thêm vào một cách thức để người dùng của chúng ta có thể tạo bình luận. Công đoạn để làm điều đó khá là giống với những gì chúng ta đã làm khi tạo bài viết.
Chúng ta sẽ bắt đầu bằng việc tạo một form để thêm bình luận ở cuối mỗi bài viết:
~~~html
<template name="postPage">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
{{#if currentUser}}
{{> commentSubmit}}
{{else}}
<p>Please log in to leave a comment.</p>
{{/if}}
</template>
~~~
<%= caption "client/templates/posts/post_page.html" %>
<%= highlight "10~14" %>
Và sau đó tạo template cho form bình luận:
~~~html
<template name="commentSubmit">
<form name="comment" class="comment-form form">
<div class="form-group {{errorClass 'body'}}">
<div class="controls">
<label for="body">Comment on this post</label>
<textarea name="body" id="body" class="form-control" rows="3"></textarea>
<span class="help-block">{{errorMessage 'body'}}</span>
</div>
</div>
<button type="submit" class="btn btn-primary">Add Comment</button>
</form>
</template>
~~~
<%= caption "client/templates/comments/comment_submit.html" %>
Để đăng bình luận, chúng ta sẽ gọi method `comment` trong `comment_submit.js` mà hoạt động của nó cũng giống như khi đăng bài viết:
~~~js
Template.commentSubmit.created = function() {
Session.set('commentSubmitErrors', {});
}
Template.commentSubmit.helpers({
errorMessage: function(field) {
return Session.get('commentSubmitErrors')[field];
},
errorClass: function (field) {
return !!Session.get('commentSubmitErrors')[field] ? 'has-error' : '';
}
});
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
};
var errors = {};
if (! comment.body) {
errors.body = "Please write some content";
return Session.set('commentSubmitErrors', errors);
}
Meteor.call('commentInsert', comment, function(error, commentId) {
if (error){
throwError(error.reason);
} else {
$body.val('');
}
});
}
});
~~~
<%= caption "client/templates/comments/comment_submit.js" %>
Cũng giống như việc chúng ta đã làm với Meteor method `post` ở phía server, chúng ta tạo Meteor method `commet` để thêm bình luận, kiểm tra xem mọi thứ có hợp lệ hay không, và cuối cùng là thêm bình luận mới đó vào trong collection.
~~~js
Comments = new Mongo.Collection('comments');
Meteor.methods({
commentInsert: function(commentAttributes) {
check(this.userId, String);
check(commentAttributes, {
postId: String,
body: String
});
var user = Meteor.user();
var post = Posts.findOne(commentAttributes.postId);
if (!post)
throw new Meteor.Error('invalid-comment', 'You must comment on a post');
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
return Comments.insert(comment);
}
});
~~~
<%= caption "lib/collections/comments.js" %>
<%= highlight "3~25" %>
<%= commit "10-3", "Created a form to submit comments." %>
Việc đang làm không có gì là trang hoàng, chúng ta chỉ kiểm tra xem người dùng có đang đăng nhập, bình luận có nội dung, và nó được liên kết tới một bài viết.
<%= screenshot "10-2", "The comment submit form" %>
### Điều khiển Subscription cho bình luận
Như mọi thứ đã đúng vị trí, chúng ta đang publish tất cả bình luận trên tất cả các bài viết cho tất cả client. Điều này có vẻ khá là lãng phí. Xét cho cùng, chúng ta cũng chỉ muốn dùng một tập con nhỏ dữ liệu tại một thời điểm. Vì vậy, hãy cùng cải tiến việc xuất bản và đăng theo dõi để quản lý cho đúng bình luận nào cần phải xuất bản.
Nếu chúng ta suy nghĩ về điều đó, thì lần duy nhất cần phải đăng theo dõi xuất bản của `comments` là khi một người dùng truy cập vào một trang đơn lẻ, và chúng ta chỉ muốn tải tập con những bình luận liên quan đến bài viết đó.
Bước đầu tiên sẽ thay đổi cách chúng ta subscribe tới bình luận. Cho tới bây giờ, chúng ta đã subscribe ở mức *router*, nghĩa là chúng ta tải tất cả dữ liệu một lần khi bộ định tuyến được khởi tạo.
Nhưng bây giờ chúng ta muốn việc subscribe phụ thuộc vào tham số của đường dẫn, và tham số đó hiển nhiên là sẽ thay đổi tại bất kỳ thời điểm nào. Bởi vậy chúng ta sẽ cần chuyển đoạn code subscribe từ mức *router* sang mức *route*.
Điều này có thêm một hệ quả nữa: thay vì tải dữ liệu ngay từ lúc khởi tạo ứng dụng, chúng ta sẽ tải khi mà bước vào một *route*. Điều này có nghĩa là sẽ có một khoảng thời gian đợi để trình duyệt tải, nhưng điều này là một điểm lùi không thể tránh khỏi trừ khi bạn định luôn luôn tải trước toàn bộ phần tử của dữ liệu.
Đầu tiên, chúng ta sẽ dừng việc tải trước tất cả bình luận trong khối `configure` bằng việc xoá bỏ `Meteor.subscribe('comments')` (nói cách khác, trở lại trạng thái chúng ta đã có trước đó):
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return Meteor.subscribe('posts');
}
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "5" %>
Và chúng ta sẽ thêm vào một hàm `waitOn` ở mức *route* cho route `postPage`:
~~~js
//...
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return Meteor.subscribe('comments', this.params._id);
},
data: function() { return Posts.findOne(this.params._id); }
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "5~7" %>
Chúng ta đang thêm `this.params._id` như một tham số cho việc subscribe. Vì vậy hãy dùng thông tin đó để chắc chắn rằng dữ liệu bình luận đã được bó hẹp lại chỉ trong bài viết hiện tại:
~~~js
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function(postId) {
check(postId, String);
return Comments.find({postId: postId});
});
~~~
<%= caption "server/publications.js" %>
<%= highlight "5~7" %>
<%= commit "10-4", "Made a simple publication/subscription for comments." %>
Chỉ có một vấn đề: khi chúng ta quay lại trang chủ, nó sẽ hiển thị rằng mọi bài viết đều có 0 bình luận:
<%= screenshot "10-3", "Our comments are gone!" %>
### Đếm số lượng bình luận
Lý do cho vấn đề này khá là rõ ràng: chúng ta chỉ nạp bình luận tại route `postPage`, vì vậy khi chúng ta gọi tới `Comments.find({postId: this._id})` trong helper `commentsCount`, Meteor không thể tìm ra được dữ liệu phía client cần thiết cho chúng ta.
Cách tốt nhất để giải quyết vấn đề này là *bất chuẩn hoá* số lượng bình luận trên bài viết (nếu bạn không chắc về nghĩa của từ này, cũng đừng lo lắng vì chúng ta sẽ làm rõ trong phần sidebar tiếp theo!). Mặc dù vậy như bạn thấy, đoạn code trở nên phức tạp hơn một chút, nhưng chúng ta được lợi ích là hiệu suất được tăng nên do không cần phải publish _tất cả_ bình luận.
Chúng ta sẽ đạt được điều này bằng cách thêm vào thuộc tính `commentsCount` vào cấu trúc dữ liệu của `post`. Để bắt đầu, chúng ta cập nhật dữ liệu cố định bài viết (và dùng `meteor reset` để nạp lại -- đừng quên tạo một tài khoản mới sau đó):
~~~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
});
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
});
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
});
}
~~~
<%= caption "server/fixtures.js" %>
<%= highlight "20,21,45,46,54,55" %>
Như thông thường khi cập nhật dữ liệu tĩnh của file, bạn cần phải `meteor reset` cơ sở dữ liệu để chắc chắn nó hoạt động.
Sau đó, chúng ta chắc chắn rằng tất cả bài viết đều bắt đầu với 0 bình luận:
~~~js
//...
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date(),
commentsCount: 0
});
var postId = Posts.insert(post);
//...
~~~
<%= caption "collections/posts.js" %>
<%= highlight "6,7" %>
Và sau đó, chúng ta cập nhật `commentsCount` tương ứng mỗi khi một bình luận mới được tạo, bằng việc sử dụng toán tử `$inc` của Mongo (nghĩa là tăng giá trị của trường thêm một):
~~~js
//...
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
// update the post with the number of comments
Posts.update(comment.postId, {$inc: {commentsCount: 1}});
return Comments.insert(comment);
//...
~~~
<%= caption "collections/comments.js "%>
<%= highlight "9,10" %>
Cuối cùng, chúng ta có thể đơn giản xoá bỏ helper `commentsCount` từ `client/templates/posts/post_item.js` vì trường đó giờ có thể nhập vào trực tiếp từ bài viết.
<%= commit "10-5", "Denormalized the number of comments into the post." %>
Bây giờ người dùng đã có thể nói chuyện với nhau, và sẽ thật đáng tiếc nếu như họ bị bỏ lỡ mất bình luận của người khác. Và chương tiếp theo sẽ chỉ cho bạn cách cài đặt thông báo (notification) để ngăn chặn điều đáng tiếc đó!