Tính năng bình luận

10

Percentage Translated

In this chapter, you will:

  • 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.
  • 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 đó.

    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

    Đừng quen việc publish và subscribe cho collection mới:

    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

    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:

    <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

    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ó:

    <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

    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:

    Template.commentItem.helpers({
      submittedText: function() {
        return this.submitted.toString();
      }
    });
    
    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:

    <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

    Và thêm helper commentsCount vào 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

    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:

    Displaying comments
    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:

    <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

    Và sau đó tạo template cho form bình luận:

    <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

    Để đă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:

    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('');
          }
        });
      }
    });
    
    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.

    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

    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.

    The comment submit form
    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 đó):

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

    Và chúng ta sẽ thêm vào một hàm waitOn ở mức route cho route postPage:

    //...
    
    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

    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:

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

    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:

    Our comments are gone!
    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 đó):

    // 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
      });
    }
    
    server/fixtures.js

    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:

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

    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):

    //...
    
    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);
    
    //...
    
    collections/comments.js

    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 đó!