评论

10

翻译进度

本章内容:

  • 显示当前评论。
  • 发布评论表单。
  • 学习如何只加载当前文章的评论。
  • 添加帖子的评论计数属性。
  • 社交新闻网站的目标是创建一个用户社区,如果没有提供一种方式让人们互相交流,这将是很难做到的。因此在本章中,我们添加评论!

    我们首先创建一个新的集来存储评论,并在该集中添加一些初始数据。

    Comments = new Mongo.Collection('comments');
    
    lib/collections/comments.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)
      });
    }
    
    server/fixtures.js

    不要忘记来发布和订阅我们这个新的集合:

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    Meteor.publish('comments', function() {
      return Comments.find();
    });
    
    server/publications.js
    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
      }
    });
    
    lib/router.js

    提交 10-1

    添加评论集合、发布/订阅,和测试数据。

    请注意,为了使用新的数据,你需要命令 Meteor reset 清除数据库。不要忘了创建一个新的用户,并重新登录!

    首先,我们在数据库中创建了几个(假的)用户,并从数据库中用他们的 id 选择出来。然后给第一篇添加注释,链接注释到帖子(postId)和用户(userId)。同时我们还添加了提交日期,评论内容和一个非规范化的作者 author 项。

    此外我们在路由器中增加等待一个含有评论和帖子订阅的数组

    显示评论

    把评论存到数据库,同时还需要在评论页上显示。

    <template name="postPage">
      {{> postItem}}
      <ul class="comments">
        {{#each comments}}
          {{> commentItem}}
        {{/each}}
      </ul>
    </template>
    
    client/templates/posts/post_page.html
    Template.postPage.helpers({
      comments: function() {
        return Comments.find({postId: this._id});
      }
    });
    
    client/templates/posts/post_page.js

    我们把 {{#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>
    
    client/templates/comments/comment_item.html

    让我们编写一个模板 helper 来帮助我们用 submitted 提交人性化的日期格式:

    Template.commentItem.helpers({
      submittedText: function() {
        return this.submitted.toString();
      }
    });
    
    client/templates/comments/comment_item.js

    然后,我们将在每个帖子中显示评论数:

    <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>
    
    client/templates/posts/post_item.html

    添加 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();
      }
    });
    
    client/templates/posts/post_item.js

    提交 10-2

    Display comments on `postPage`.

    现在您应该能够显示初始的评论并看到如下的内容:

    显示评论
    显示评论

    提交新评论

    让用户创建新的评论,这个过程将是非常类似过去我们允许用户创建新的帖子。

    首先我们通过在每个帖子底部增加一个提交框:

    <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>
    
    client/templates/posts/post_page.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>
    
    client/templates/comments/comment_submit.html

    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('');
          }
        });
      }
    });
    
    client/templates/comments/comment_submit.js

    类似以前 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);
      }
    });
    
    lib/collections/comments.js

    提交 10-3

    创建提交评论表单

    这里没做什么太花哨的,只是检查该用户是否已经登录,该评论有一个内容,并且链接到一个帖子。

    评论提交表单
    评论提交表单

    控制订阅评论

    如同以往一样,我们将发布属于所有帖子的全部评论到每个连接的客户端。这似乎有点浪费。毕竟在任何给定的时间段,实际上只使用该数据的一小部分。因此让我们提高发布和订阅评论的精度。

    如果仔细想想,我们需要订阅 comments 评论发布的时间,是当用户访问一个帖子的页面,而此刻只需要加载这个帖子的评论子集。

    第一步将改变我们订阅评论的方式。目前是路由器级订阅,这意味着当路由器初始化时,加载所有数据。

    但是现在希望我们的订阅依赖于路径参数,并且参数可以在任何时候改变。因此需要将我们的订阅代码从路由器级改到路径级。

    这样做的另一个后果:每当打开路径时加载数据,而不是初始化应用时加载它。这意味着你在程序内浏览时,会等待加载时间。除非你打算加载全部内容到客户端,这是一个不可避免的缺点。

    首先,在 configure 块中通过删除 Meteor.subscribe('comments'),停止预装全部评论(换句话说,要回以前的方法):

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return Meteor.subscribe('posts');
      }
    });
    
    lib/router.js

    我们将为 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); }
    });
    
    //...
    
    lib/router.js

    我们将 this.params._id 作为参数传递给订阅。所以让我们用这个新信息来确保限制评论属于当前帖子:

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    Meteor.publish('comments', function(postId) {
      check(postId, String);
      return Comments.find({postId: postId});
    });
    
    server/publications.js

    提交 10-4

    设置简单的评论发布/订阅。

    这里有一个问题:当我们返回主页,显示所有的帖子有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
      });
    }
    
    server/fixtures.js

    像往常一样,更新测试数据文件时,你必须用 meteor reset 重置数据库,以确保它再次运行。

    然后,我们要确保所有新帖子的评论计数从0开始:

    //...
    
    var post = _.extend(postAttributes, {
      userId: user._id,
      author: user.username,
      submitted: new Date(),
      commentsCount: 0
    });
    
    var postId = Posts.insert(post);
    
    //...
    
    lib/collections/posts.js

    当我们创建一个新的评论时,使用 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);
    
    //...
    
    lib/collections/comments.js

    最后只需简单地删除 client/templates/posts/post_item.jscommentsCount helper,因为该值可以从帖子中得到。

    提交 10-5

    非规范化评论数量到帖子中。

    现在用户可以互相对话,如果他们错过了新的评论,这将是不可原谅的。接下来的章节将告诉你如何实现通知,以防止这个情况发生!