社交新闻网站的目标是创建一个用户社区,如果没有提供一种方式让人们互相交流,这将是很难做到的。因此在本章中,我们添加评论!
我们首先创建一个新的集来存储评论,并在该集中添加一些初始数据。
Comments = new Mongo.Collection('comments');
// 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)
});
}
不要忘记来发布和订阅我们这个新的集合:
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function() {
return Comments.find();
});
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
}
});
请注意,为了使用新的数据,你需要命令 Meteor reset
清除数据库。不要忘了创建一个新的用户,并重新登录!
首先,我们在数据库中创建了几个(假的)用户,并从数据库中用他们的 id
选择出来。然后给第一篇添加注释,链接注释到帖子(postId
)和用户(userId
)。同时我们还添加了提交日期,评论内容和一个非规范化的作者 author
项。
此外我们在路由器中增加等待一个含有评论和帖子订阅的数组。
把评论存到数据库,同时还需要在评论页上显示。
<template name="postPage">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
</template>
Template.postPage.helpers({
comments: function() {
return Comments.find({postId: this._id});
}
});
我们把 {{#each comments}}
块放在帖子模板里面,所以在 comments
helper 里,this
指向的是当前帖子。要找到相关的评论,我们可通过 postId
属性找到链接到该帖子的评论。
我们已经了解 helper 和 Spacebars 后,显示一个评论是相当简单的。我们将在 templates
下,创建一个新的 comments
目录和一个新的 commentItem
模板,来存储所有的评论相关的信息:
<template name="commentItem">
<li>
<h4>
<span class="author">{{author}}</span>
<span class="date">on {{submittedText}}</span>
</h4>
<p>{{body}}</p>
</li>
</template>
让我们编写一个模板 helper 来帮助我们把 submitted
提交日期格式人性化的日期格式:
Template.commentItem.helpers({
submittedText: function() {
return this.submitted.toString();
}
});
然后,我们将在每个帖子中显示评论数:
<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>
添加 commentsCount
helper 到 post_item.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();
}
});
现在您应该能够显示初始的评论并看到如下的内容:
让用户创建新的评论,这个过程将是非常类似过去我们允许用户创建新的帖子。
首先我们通过在每个帖子底部增加一个提交框:
<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>
然后创建评论表单模板:
<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>
在 comment_submit.js
中调用 comment
方法,提交新的评论,这是类似于过去提交帖子的方法:
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('');
}
});
}
});
类似以前 post
服务器端的 Meteor 方法,我们将建立一个 comment
的 Meteor 方法来创建评论,检查正确性后,插入到评论集合。
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);
}
});
这里没做什么太花哨的,只是检查该用户是否已经登录,该评论有一个内容,并且链接到一个帖子。
如同以往一样,我们将发布属于所有帖子的全部评论到每个连接的客户端。这似乎有点浪费。毕竟在任何给定的时间段,实际上只使用该数据的一小部分。因此让我们提高发布和订阅评论的精度。
如果仔细想想,我们需要订阅 comments
评论发布的时间,是当用户访问一个帖子的页面,而此刻只需要加载这个帖子的评论子集。
第一步将改变我们订阅评论的方式。目前是路由器级订阅,这意味着当路由器初始化时,加载所有数据。
但是现在希望我们的订阅依赖于路径参数,并且参数可以在任何时候改变。因此需要将我们的订阅代码从路由器级改到路径级。
这样做的另一个后果:每当打开路径时加载数据,而不是初始化应用时加载它。这意味着你在程序内浏览时,会等待加载时间。除非你打算加载全部内容到客户端,这是一个不可避免的缺点。
首先,在 configure
块中通过删除 Meteor.subscribe('comments')
,停止预装全部评论(换句话说,要回以前的方法):
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return Meteor.subscribe('posts');
}
});
我们将为 postPage
添加一个新的路径级的 waitOn
函数:
//...
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return Meteor.subscribe('comments', this.params._id);
},
data: function() { return Posts.findOne(this.params._id); }
});
//...
我们将 this.params._id
作为参数传递给订阅。所以让我们用这个新信息来确保限制评论属于当前帖子:
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function(postId) {
check(postId, String);
return Comments.find({postId: postId});
});
这里有一个问题:当我们返回主页,显示所有的帖子有0条评论:
这个问题的原因是:我们只装载 postPage
路径上的评论,所以当我们调用 Comments.find({postId: this._id})
中 commentsCount
helper,Meteor 找不到必要的客户端数据为我们提供计数值。
处理这一问题的最佳办法是非规范化的评论计数加到帖子中(如果你不明白这意味着什么,不用担心,接下来的附录会详解!)。正如我们将看到的,代码中复杂性稍微增加,但从不发布帖子列表的_全部_评论中,得到的执行性能改善是值得的。
我们将通过在 post
数据结构中增加一个 commentsCount
属性来实现这一目标。首先,更新帖子的测试数据(用 meteor reset
去重载它们 —— 不要忘了之后重新创建你的帐户):
// 测试数据
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
});
}
像往常一样,更新测试数据文件时,你必须用 meteor reset
重置数据库,以确保它再次运行。
然后,我们要确保所有新帖子的评论计数从0开始:
//...
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date(),
commentsCount: 0
});
var postId = Posts.insert(post);
//...
当我们创建一个新的评论时,使用 Mongo 的 $inc
操作(给一个数字字段值加一)更新相关的 commentsCount
:
//...
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
// 更新帖子的评论数
Posts.update(comment.postId, {$inc: {commentsCount: 1}});
return Comments.insert(comment);
//...
最后只需简单地删除 client/templates/posts/post_item.js
的 commentsCount
helper,因为该值可以从帖子中得到。
现在用户可以互相对话,如果他们错过了新的评论,这将是不可原谅的。接下来的章节将告诉你如何实现通知,以防止这个情况发生!