---
title: 評論
slug: comments
complete: 100
date: 0010/01/01
number: 10
points: 10
photoUrl: http://www.flickr.com/photos/ikewinski/9414222270/
photoAuthor: Mike Lewinski
contents: 顯示當前評論。|發布評論表單。|學習如何只加載當前文章的評論。|添加帖子的評論計數屬性。
paragraphs: 34
---
社交新聞網站的目標是創建一個用戶社區,如果沒有提供一種方式讓人們互相交流,這將是很難做到的。因此在本章中,我們添加評論!
我們首先創建一個新的集來存儲評論,并在該集中添加一些初始數據。
~~~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" %>
不要忘記來發布和訂閱我們這個新的集合:
~~~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", "添加評論集合、發布/訂閱,和測試數據。" %>
請注意,為了使用新的數據,你需要命令 `Meteor reset` 清除數據庫。不要忘了創建一個新的用戶,并重新登錄!
首先,我們在數據庫中創建了幾個(假的)用戶,并從數據庫中用他們的 `id` 選擇出來。然后給第一篇添加注釋,鏈接注釋到帖子(`postId`)和用戶(`userId`)。同時我們還添加了提交日期,評論內容和一個非規范化的作者 `author` 項。
此外我們在路由器中增加等待一個含有評論和帖子訂閱的*數組*。
### 顯示評論
把評論存到數據庫,同時還需要在評論頁上顯示。
~~~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" %>
我們把 `{{#each comments}}` 塊放在帖子模板里面,所以在 `comments` helper 里,`this` 指向的是當前帖子。要找到相關的評論,我們可通過 `postId` 屬性找到鏈接到該帖子的評論。
我們已經了解 helper 和 Spacebars 后,顯示一個評論是相當簡單的。我們將在 `templates` 下,創建一個新的 `comments` 目錄和一個新的 `commentItem` 模板,來存儲所有的評論相關的信息:
~~~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" %>
讓我們編寫一個模板 helper 來幫助我們用 `submitted` 提交人性化的日期格式:
~~~js
Template.commentItem.helpers({
submittedText: function() {
return this.submitted.toString();
}
});
~~~
<%= caption "client/templates/comments/comment_item.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 btn-default">Discuss</a>
</div>
</template>
~~~
<%= caption "client/templates/posts/post_item.html" %>
<%= highlight "6,7" %>
添加 `commentsCount` helper 到 `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`." %>
現在您應該能夠顯示初始的評論并看到如下的內容:
<%= screenshot "10-1", "顯示評論" %>
### 提交新評論
讓用戶創建新的評論,這個過程將是非常類似過去我們允許用戶創建新的帖子。
首先我們通過在每個帖子底部增加一個提交框:
~~~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" %>
然后創建評論表單模板:
~~~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" %>
在 `comment_submit.js` 中調用 `comment` 方法,提交新的評論,這是類似于過去提交帖子的方法:
~~~js
Template.commentSubmit.onCreated(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" %>
類似以前 `post` 服務器端的 Meteor 方法,我們將建立一個 `comment` 的 Meteor 方法來創建評論,檢查正確性后,插入到評論集合。
~~~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", "創建提交評論表單" %>
這里沒做什么太花哨的,只是檢查該用戶是否已經登錄,該評論有一個內容,并且鏈接到一個帖子。
<%= screenshot "10-2", "評論提交表單" %>
### 控制訂閱評論
如同以往一樣,我們將發布屬于所有帖子的全部評論到每個連接的客戶端。這似乎有點浪費。畢竟在任何給定的時間段,實際上只使用該數據的一小部分。因此讓我們提高發布和訂閱評論的精度。
如果仔細想想,我們需要訂閱 `comments` 評論發布的時間,是當用戶訪問一個帖子的頁面,而此刻只需要加載這個帖子的評論子集。
第一步將改變我們訂閱評論的方式。目前是*路由器*級訂閱,這意味著當路由器初始化時,加載所有數據。
但是現在希望我們的訂閱依賴于路徑參數,并且參數可以在任何時候改變。因此需要將我們的訂閱代碼從*路由器*級改到*路徑*級。
這樣做的另一個后果:每當打開*路徑*時加載數據,而不是初始化應用時加載它。這意味著你在程序內瀏覽時,會等待加載時間。除非你打算加載全部內容到客戶端,這是一個不可避免的缺點。
首先,在 `configure` 塊中通過刪除 `Meteor.subscribe('comments')`,停止預裝全部評論(換句話說,要回以前的方法):
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return Meteor.subscribe('posts');
}
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "5" %>
我們將為 `postPage` 添加一個新的*路徑*級的 `waitOn` 函數:
~~~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" %>
我們將 `this.params._id` 作為參數傳遞給訂閱。所以讓我們用這個新信息來確保限制評論屬于當前帖子:
~~~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", "設置簡單的評論發布/訂閱。" %>
這里有一個問題:當我們返回主頁,顯示所有的帖子有0條評論:
<%= screenshot "10-3", "我們的評論消失了!" %>
### 評論計數
這個問題的原因是:我們只裝載 `postPage` 路徑上的評論,所以當我們調用 `Comments.find({postId: this._id})` 中 `commentsCount` helper,Meteor 找不到必要的客戶端數據為我們提供計數值。
處理這一問題的最佳辦法是*非規范化*的評論計數加到帖子中(如果你不明白這意味著什么,不用擔心,接下來的附錄會詳解!)。正如我們將看到的,代碼中復雜性稍微增加,但從不發布帖子列表的_全部_評論中,得到的執行性能改善是值得的。
我們將通過在 `post` 數據結構中增加一個 `commentsCount` 屬性來實現這一目標。首先,更新帖子的測試數據(用 `meteor reset` 去重載它們 —— 不要忘了之后重新創建你的帳戶):
~~~js
// 測試數據
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" %>
像往常一樣,更新測試數據文件時,你必須用 `meteor reset` 重置數據庫,以確保它再次運行。
然后,我們要確保所有新帖子的評論計數從0開始:
~~~js
//...
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date(),
commentsCount: 0
});
var postId = Posts.insert(post);
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "6,7" %>
當我們創建一個新的評論時,使用 Mongo 的 `$inc` 操作(給一個數字字段值加一)更新相關的 `commentsCount`:
~~~js
//...
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
// 更新帖子的評論數
Posts.update(comment.postId, {$inc: {commentsCount: 1}});
return Comments.insert(comment);
//...
~~~
<%= caption "lib/collections/comments.js "%>
<%= highlight "9,10" %>
最后只需簡單地刪除 `client/templates/posts/post_item.js` 的 `commentsCount` helper,因為該值可以從帖子中得到。
<%= commit "10-5", "非規范化評論數量到帖子中。" %>
現在用戶可以互相對話,如果他們錯過了新的評論,這將是不可原諒的。接下來的章節將告訴你如何實現通知,以防止這個情況發生!