💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
--- 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", "非规范化评论数量到帖子中。" %> 现在用户可以互相对话,如果他们错过了新的评论,这将是不可原谅的。接下来的章节将告诉你如何实现通知,以防止这个情况发生!