ข้อคิดเห็น

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

    Added comments collection, pub/sub and fixtures.

    จำไว้ว่า ถ้าเราต้องการให้โค้ดสร้างข้อมูลชุดนี้ทำงาน เราจำเป็นต้องสั่ง meteor reset เพื่อลบข้อมูลทั้งหมด หลังจากรีเซ็ทแล้ว อย่าลืมสร้างบัญชีผู้ใช้ใหม่แล้วล็อกอินด้วยล่ะ !

    ขั้นแรก เราสร้างข้อมูลผู้ใช้ (หลอกๆ) ขึ้นมาสองสามรายการ ใส่มันเข้าไปในฐานข้อมูล แล้วนำค่า id ที่ได้มาสร้างอ็อบเจกต์ผู้ใช้โดยค้นหาจากฐานข้อมูลอีกที จากนั้นก็ใส่ข้อคิดเห็นเข้าไปที่ข่าวแรก โดยเชื่อมข้อคิดเห็นเข้ากับข่าว (ด้วย postId) และกับผู้ใช้ (ด้วย userId) นอกจากนี้เรายังใส่วันที่ป้อนข่าว และเนื้อหาข้อคิดเห็น ตามด้วยชื่อผู้เขียน author ที่เป็นฟิลด์แบบ denormalized ด้วย

    อีกทั้งเรายังบอกตัวจัดการเส้นทางให้รอข้อมูล อาร์เรย์ ที่ได้จากการบอกรับข้อมูลข้อคิดเห็นและข้อมูลข่าวด้วย

    แสดงข้อคิดเห็น

    ข้อคิดเห็นก็ถูกใส่เข้าไปในฐานข้อมูลแล้ว ที่เราต้องทำต่อคือ ดึงมันออกมาแสดงในหน้าข่าวด้วยวิธีที่ตอนนี้คุณคงคุ้นเคยกันแล้ว และพอจะรู้ว่า ขั้นตอนต่อไปเราต้องทำอะไรกันบ้าง

    <template name="postPage">
      <div class="post-page page">
        {{> postItem}}
    
        <ul class="comments">
          {{#each comments}}
            {{> commentItem}}
          {{/each}}
        </ul>
      </div>
    </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}} ลงในเทมเพลทหน้าข่าว ดังนั้น this ในตัวช่วย comments ก็คือ ข่าวที่โพสท์ การค้นหาข้อคิดเห็นของข่าวทำได้โดยดึงข้อคิดเห็นที่เชื่อมโยงกับข่าวนั้นผ่านฟิลด์ postId

    ด้วยสิ่งที่เรารู้เกี่ยวกับตัวช่วยและ Spacebar การแสดงข้อคิดเห็นก็ทำได้ไม่ยากนัก โดยเราจะสร้างโฟลเดอร์ใหม่ชื่อ comments ข้างใน templates เพื่อไว้เก็บไฟล์เทมเพลทของข้อคิดเห็น และสร้างไฟล์เทมเพลท 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

    จากนั้นก็สร้างตัวช่วยเทมเพลทที่ใช้เปลี่ยนรูปแบบของวันที่ 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 ใน 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`.

    ตอนนี้คุณก็สามารถแสดงข้อคิดเห็นจากข้อมูลที่เราใส่ไว้ได้ และเห็นหน้าจอคล้ายๆแบบนี้

    Displaying comments
    Displaying comments

    ป้อนข้อคิดเห็น

    ตอนนี้เราจะหาทางให้ผู้ใช้สร้างข้อคิดเห็นใหม่ได้ โดยวิธีที่เราทำจะคล้ายๆกับที่เราใช้สร้างข่าวใหม่นั่นเอง

    โดยเราจะเริ่มด้วยการเพิ่มกล่องข้อคิดเห็นที่ท้ายข่าว ดังนี้

    <template name="postPage">
      <div class="post-page page">
        {{> postItem}}
    
        <ul class="comments">
          {{#each comments}}
            {{> commentItem}}
          {{/each}}
        </ul>
    
        {{#if currentUser}}
          {{> commentSubmit}}
        {{else}}
          <p>Please log in to leave a comment.</p>
        {{/if}}
      </div>
    </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 จากใน comment_submit.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('');
          }
        });
      }
    });
    
    client/templates/comments/comment_submit.js

    ก็เหมือนกับที่เราทำตอนสร้างเมธอด post ที่รันบนฝั่งเซิร์ฟเวอร์ เราจะให้เมธอด 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

    Created a form to submit comments.

    ซึ่งเราก็ไม่ได้ทำอะไรที่มากเกินไป เราแค่ตรวจสอบว่าผู้ใช้ล็อกอินแล้ว และตรวจดูว่ามีข้อคิดเห็นมาพร้อมกับข่าวหรือไม่ เท่านั้นเอง

    The comment submit form
    The comment submit form

    ควบคุมการบอกรับข้อคิดเห็น

    สิ่งที่เกิดขึ้นตอนนี้ก็คือ เรากำลังเผยแพร่ข้อคิดเห็นทั้งหมดของข่าวทุกๆข่าวส่งไปยังไคลเอนต์ทุกตัวที่เชื่อมต่ออยู่ ซึ่งดูแล้วมากเกินความจำเป็น อันที่จริงแล้วในขณะใดขณะหนึ่งเราใช้แค่ข้อมูลชุดย่อยๆเท่านั้น ดังนั้นเราจะมาปรับปรุงวิธีการเผยแพร่และบอกรับข้อมูลโดยกำหนดให้ชัดเจนไปเลยว่า จะเผยแพร่ข้อคิดเห็นตัวไหนส่งออกไปบ้าง

    ถ้าลองคิดดู ช่วงเวลาที่เราจำเป็นต้องบอกรับข้อมูลจากการเผยแพร่ของ comments จะเกิดขึ้นแค่ช่วงท่ี่ผู้ใช้งานเปิดเข้าไปดูในหน้าข่าว และเราก็แค่ต้องการโหลดข้อคิดเห็นของข่าวมาเท่านั้น

    ขั้นตอนแรก เราจะทำการเปลี่ยนแปลงวิธีบอกรับข้อมูล ซึ่งที่ผ่านมาเราเขียนโค้ดไว้ที่ระดับ ตัวจัดการเส้นทาง หมายความว่า เราโหลดข้อมูลทั้งหมดแค่ครั้งเดียวในตอนที่ตัวจัดการเส้นทางเริ่มทำงาน

    แต่ตอนนี้เราต้องการให้การบอกรับข้อมูลขึ้นอยู่กับค่าพารามิเตอร์ของเส้นทาง ซึ่งมีค่าเปลี่ยนแปลงได้ตลอดเวลา ดังนั้นเราจำเป็นต้องย้ายโค้ดบอกรับข้อมูลจากระดับของ ตัวจัดการเส้นทาง มาไว้ที่ระดับ เส้นทาง แทน

    ผลกระทบที่เกิดตามมาคือ แทนที่เราจะโหลดข้อมูลตั้งแต่ตอนแอพเริ่มทำงาน เราก็จะโหลดมันเมื่อเราเปิดเข้าไปที่ เส้นทาง นั้นแทน หมายความว่า ในตอนนี้คุณจะต้องรอให้มีการโหลดข้อมูลทุกครั้งที่เข้าดูข้อมูลจากแอพ ซึ่งเป็นข้อเสียที่หลีกเลี่ยงไม่ได้ยกเว้นว่าคุณจะโหลดข้อมูลทั้งหมดเข้ามาตั้งแต่ตอนแรก

    เราจะเริ่มจาก ยกเลิกการโหลดข้อคิดเห็นในบล็อก configure ด้วยการลบคำสั่ง Meteor.subscribe('comments') (พูดง่ายๆคือ เปลี่ยนกลับไปให้เป็นแบบเดิม)

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

    และเพิ่มฟังก์ชัน waitOn ตัวใหม่ที่ระดับ เส้นทาง ของ 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

    จะเห็นว่าเราส่งค่า 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

    Made a simple publication/subscription for comments.

    ตอนนี้เหลือแค่ปัญหาเดียว เมื่อเรากลับไปที่หน้าโฮม จะเห็นว่าข่าวทุกตัวมีจำนวนข้อคิดเห็นเป็นศูนย์

    Our comments are gone!
    Our comments are gone!

    นับจำนวนข้อคิดเห็น

    เดี๋ยวคุณก็รู้ว่าทำไมเราต้องทำ เนื่องจากเราโหลดข้อคิดเห็นเฉพาะที่เส้นทาง postPage เท่านั้น เมื่อเราเรียกใช้คำสั่ง Comments.find({postId: this._id}) ในตัวช่วย commentsCount Meteor จึงไม่สามารถหาผลลัพธ์จากข้อมูลที่ฝั่งไคลเอนต์ให้เราได้

    วิธีที่ดีที่สุดที่จะจัดการกับเรื่องนี้คือ ทำการ denormalize จำนวนข้อคิดเห็นไปไว้ที่ข่าว (ถ้าคุณไม่ค่อยแน่ใจว่ามันหมายถึงอะไร ไม่ต้องกังวลไป เราเตรียมเรื่องนี้ไว้ในบทแทรกตอนต่อไปแล้ว !) ซึ่งแม้ว่ามันจะทำให้โค้ดดูซับซ้อนขึ้นนิด แต่ประสิทธิภาพที่เราได้จากการที่ไม่ต้องเผยแพร่ข้อคิดเห็น ทั้งหมด มาที่หน้าแสดงข่าวนั้น คุ้มค่ามากทีเดียว

    โดยที่เราจะทำ คือ เพิ่มคุณสมบัติ commentsCount ให้กับโครงสร้างข้อมูล post เริ่มจากการปรับเปลี่ยนโค้ดสร้างข้อมูล (และเรียก meteor reset เพื่อโหลดมันเข้ามาใหม่ แล้วอย่าลืมสร้างบัญชีผู้ใช้ของคุณอีกครั้งด้วย)

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

    ก็เหมือนอย่างเคย เมื่อคุณเปลี่ยนแปลงโค้ดสร้างข้อมูล คุณก็ต้องเรียก meteor reset กับฐานข้อมูลคุณ เพื่อให้แน่ใจว่ามันถูกรันอีกครั้ง

    จากนั้นเราก็ทำให้แน่ใจว่า ข่าวใหม่ทุกตัวจะมีจำนวนข้อคิดเห็นเป็นศูนย์

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

    และเราก็อัพเดทค่า commentsCount อีกครั้งเมื่อเราสร้างข้อคิดเห็นใหม่ ด้วยการใช้ตัวดำเนินการ $inc ของ Mongo (ที่ใช้เพิ่มค่าฟิลด์ตัวเลขทีละหนึ่ง)

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

    สุดท้าย เราก็แค่ลบตัวช่วย commentsCount ออกจาก client/templates/posts/post_item.js เนื่องจากเรามีฟิลด์นี้แล้วที่ตัวข่าว

    คอมมิท 10-5

    Denormalized the number of comments into the post.

    ตอนนี้ผู้ใช้ก็สามารถพูดคุยกันได้ แต่มันจะน่าเสียดายแค่ไหนถ้าพวกเค้าพลาดข้อคิดเห็นใหม่ๆ และไม่ว่าคุณจะรู้อะไรมา ในบทต่อไปเราจะแสดงให้คุณเห็นวิธีใช้ การแจ้งเตือน เพื่อป้องกันเรื่องนี้ !