สร้างข่าวใหม่

7

แปลไปแล้ว

ในบทนี้ คุณจะได้

  • เรียนรู้วิธี submit ข่าวจากฝั่งไคลเอนต์
  • เพิ่มการตรวจสอบความปลอดภัยอย่างง่าย
  • ป้องกันการเข้าถึงฟอร์ม submit
  • เรียนรู้วิธีเพิ่มความปลอดภัยด้วยเมธอดฝั่งเซิร์ฟเวอร์
  • เราก็เห็นกันแล้วว่า การสร้างข่าวใหม่จากคอนโซลนั้นง่ายแค่ไหน ด้วยการเรียกใช้คำสั่ง Posts.insert ของฐานข้อมูล แต่เราก็คาดหวังไม่ได้ว่า ผู้ใช้จะเปิดคอนโซลแล้วสร้างข่าวใหม่เข้าไปเอง

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

    สร้างหน้าโพสต์ข่าว

    เราเริ่มต้นด้วยการสร้างเส้นทางไปที่หน้าใหม่ของเรา

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.route('/', {name: 'postsList'});
    
    Router.route('/posts/:_id', {
      name: 'postPage',
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/submit', {name: 'postSubmit'});
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    
    lib/router.js

    เพิ่มลิงก์ที่ส่วนหัว

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

    <template name="header">
      <nav class="navbar navbar-default" role="navigation">
        <div class="container-fluid">
          <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
          </div>
          <div class="collapse navbar-collapse" id="navigation">
            <ul class="nav navbar-nav">
              <li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>
            </ul>
            <ul class="nav navbar-nav navbar-right">
              {{> loginButtons}}
            </ul>
          </div>
        </div>
      </nav>
    </template>
    
    client/templates/includes/header.html

    เส้นทางที่เรากำหนดยังหมายถึงว่า ถ้าผู้ใช้เปิดเข้าไปที่พาธ /submit Meteor ก็จะแสดงเทมเพลต postSubmit ด้วย ดังนั้นเราก็จะมาเขียนเทมเพลตนี้กัน

    <template name="postSubmit">
      <form class="main form page">
        <div class="form-group">
          <label class="control-label" for="url">URL</label>
          <div class="controls">
              <input name="url" id="url" type="text" value="" placeholder="Your URL" class="form-control"/>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="title">Title</label>
          <div class="controls">
              <input name="title" id="title" type="text" value="" placeholder="Name your post" class="form-control"/>
          </div>
        </div>
        <input type="submit" value="Submit" class="btn btn-primary"/>
      </form>
    </template>
    
    client/templates/posts/post_submit.html

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

    The post submit form
    The post submit form

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

    สร้างข่าวใหม่

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

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        post._id = Posts.insert(post);
        Router.go('postPage', post);
      }
    });
    
    client/templates/posts/post_submit.js

    คอมมิท 7-1

    Added a submit post page and linked to it in the header.

    โดยฟังก์ชันนี้ใช้ jQuery เพื่อแปลงค่าที่ได้จากฟิลด์ต่างๆในฟอร์ม และสร้างอ็อบเจกต์โพสต์จากค่าเหล่านั้น ที่เราต้องเรียก preventDefault จากพารามิเตอร์ event ก็เพื่อให้แน่ใจว่า เบราว์เซอร์จะไม่พยายาม submit เอง

    ในช่วงท้ายของโค้ด เราก็สามารถสั่งให้ตัวจัดการเส้นทางส่งเราไปที่หน้าข่าวที่เพิ่งสร้างใหม่ได้ ทั้งนี้ฟังก์ชัน insert() ที่กระทำกับคอลเลคชั่นจะคืนค่า _id ของอ็อบเจกต์ใหม่ที่เพิ่งถูกแทรกเข้าไปในฐานข้อมูล ซึ่งฟังก์ชัน go() ของตัวจัดการเส้นทางจะนำไปสร้างพาธให้เราเปิดเข้าไปใช้งานได้

    ผลลัพธ์ที่ได้เมื่อผู้ใช้กดปุ่ม submit ก็คือ ข่าวใหม่ถูกสร้างขึ้น และผู้ใช้ถูกพาเข้าไปที่หน้าสนทนาของข่าวที่สร้างใหม่นั้นทันที

    เพิ่มความปลอดภัยเข้าไป

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

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

    ตอนนี้แอพเราก็ไม่ต้องการตัวช่วยพวกนี้อีกแล้ว ดังนั้นก็ปิดมันซะเลย! โดยการถอนแพ็คเกจ insecure ออกมาดังนี้

    meteor remove insecure
    
    Terminal

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

    และเราจำเป็นที่จะต้องกำหนดกฏเกณฑ์ชัดเจนให้ Meteor รู้ว่า มันโอเคที่จะยอมให้ไคลเอนต์เพิ่มข้อมูลเข้ามาได้ หรือว่าเราจะทำการเพิ่มข้อมูลที่ฝั่งเซิร์ฟเวอร์แทน

    ยอมให้เพิ่มข่าวได้

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

    Posts = new Mongo.Collection('posts');
    
    Posts.allow({
      insert: function(userId, doc) {
        // only allow posting if you are logged in
        return !! userId;
      }
    });
    
    lib/collections/posts.js

    คอมมิท 7-2

    Removed insecure, and allowed certain writes to posts.

    เราเรียกใช้ Posts.allow เพื่อบอกให้ Meteor รู้ว่า “สิ่งนี้คือชุดเหตุการณ์ซึ่งเรายอมให้ไคลเอนต์ทำอะไรกับคอลเลคชั่น Posts ได้บ้าง” โดยในกรณีนี้ เราก็บอกว่า “เรายอมให้ไคลเอนต์เพิ่มข่าวใหม่เข้าไปตราบเท่าที่พวกเค้ามี userID

    ค่า userId ของผู้ใช้ตอนที่กำลังแก้ไขข้อมูลจะถูกส่งต่อไปที่ฟังก์ชัน allow และ deny (หรือมีค่าเป็น null ถ้าผู้ใช้ยังไม่ได้ล็อกอิน) ซึ่งนำไปใช้ประโยชน์ต่อได้ และในเมื่อบัญชีผู้ใช้ก็เป็นส่วนหนึ่งของ Meteor ด้วยแล้ว เราจึงมั่นใจได้ว่า userId จะมีค่าที่ถูกต้องเสมอ

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

    Insert failed: Access denied
    Insert failed: Access denied

    แต่อย่างไรก็ตาม ยังมีเรื่องที่เราต้องจัดการอีกสองสามอย่างคือ

    • ผู้ใช้ที่ล็อกเอาท์ ยังคงเข้าไปที่หน้าฟอร์มสร้างข่าวได้
    • ข่าวที่สร้างใหม่ ก็ไม่ได้มีความเกี่ยวข้องอะไรกับผู้ใช้เลย (และยังไม่มีโค้ดฝั่งเซิร์ฟเวอร์ที่จัดการตรงนี้ด้วย)
    • ข่าวใหม่ๆ สามารถสร้างด้วย URL เดียวกันได้

    เราจะมาแก้ไขเรื่องพวกนี้กัน

    ป้องกันฟอร์มสร้างข่าว

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

    ฮุคนี้จะดักจับการจัดเส้นทาง และสามารถเปลี่ยนการทำงานของตัวจัดการเส้นทางได้ เปรียบได้กับเจ้าหน้าที่รักษาความปลอดภัยกำลังตรวจสอบข้อมูลคุณ ก่อนที่จะยอมให้คุณเข้าไปข้างใน (หรือไล่คุณกลับไป)

    สิ่งที่เราต้องทำคือ ตรวจสอบว่าผู้ใช้ล็อกอินเข้ามาแล้วหรือยัง ถ้ายังไม่ล็อกอิน ให้แสดงเทมเพลต accessDenied แทนเทมเพลต postSubmit ตามปกติ (และเราก็จะสั่งให้ตัวจัดการเส้นทางหยุดทำงานอื่นๆต่อจากนั้น) ดังนั้นเราก็มาแก้ไขไฟล์ router.js กันดังนี้

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.route('/', {name: 'postsList'});
    
    Router.route('/posts/:_id', {
      name: 'postPage',
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/submit', {name: 'postSubmit'});
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        this.render('accessDenied');
      } else {
        this.next();
      }
    }
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    และเราก็ต้องสร้างเทมเพลตของหน้า access denied นี้ด้วย

    <template name="accessDenied">
      <div class="access-denied page jumbotron">
        <h2>Access Denied</h2>
        <p>You can't get here! Please log in.</p>
      </div>
    </template>
    
    client/templates/includes/access_denied.html

    คอมมิท 7-3

    Denied access to new posts page when not logged in.

    ตอนนี้ถ้าคุณเข้าไปที่ http://localhost:3000/submit/ โดยไม่ได้ล็อกอิน คุณก็ควรจะเห็นข้อความคล้ายๆแบบนี้

    The access denied template
    The access denied template

    ข้อดีของการใช้ฮุคที่เส้นทางคือ พวกมันทำงานแบบ รีแอกทีฟ หมายความว่า เราไม่ต้องคิดเรื่องการใช้ฟังก์ชัน callback เมื่อมีผู้ใช้ล็อกอินเข้ามา เพราะเมื่อสถานะล็อกอินของผู้ใช้เปลี่ยนไป เทมเพลตที่ตัวจัดการเส้นทางใช้ในหน้านั้นก็จะเปลี่ยนจาก accessDenied เป็น postSubmit ทันที โดยเราไม่ต้องเขียนโค้ดจัดการใดๆทั้งสิ้น (และอย่าลืมว่า มันทำงานข้ามแท็บของเบราว์เซอร์ได้ด้วย)

    ลองล็อกอิน และรีเฟรชหน้านี้ดู คุณอาจจะเห็นหน้า access denied กระพริบขึ้นมาแว่บนึงก่อนเข้าหน้าป้อนข่าว เหตุผลที่เป็นอย่างนี้ก็เพราะ Meteor จะเริ่มวาดเทมเพลททันทีที่เป็นไปได้ ก่อนที่มันจะติดต่อกับเซิร์ฟเวอร์ และเช็ค (จากค่าที่เก็บในข้อมูลของเบราว์เซอร์) ว่า ผู้ใช้คนปัจจุบันมีตัวตนซะอีก

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

    จริงๆแล้วในตอนนี้เราก็ไม่รู้ว่า ผู้ใช้มีสิทธิการใช้งานที่ถูกต้องหรือไม่ ทำให้เราไม่สามารถแสดงได้ทั้งเทมเพลต accessDenied และ postSubmit จนกว่าเราจะรู้

    ดังนั้นเราก็จะแก้ไขฮุคของเราให้เปลี่ยนมาใช้เทมเพลทแสดงการโหลด เมื่อ Meteor.loggingIn() มีค่าเป็นจริง

    //...
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        if (Meteor.loggingIn()) {
          this.render(this.loadingTemplate);
        } else {
          this.render('accessDenied');
        }
      } else {
        this.next();
      }
    }
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    คอมมิท 7-4

    Show a loading screen while waiting to login.

    การซ่อนลิงก์

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

    //...
    
    <ul class="nav navbar-nav">
      {{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
    </ul>
    
    //...
    
    client/templates/includes/header.html

    คอมมิท 7-5

    Only show submit post link if logged in.

    ตัวช่วย currentUser ถูกเตรียมไว้ให้เราโดยแพ็คเกจ accounts ซึ่งเป็นโค้ดของ Spacebars ที่มีค่าเหมือนกับคำสั่ง Meteor.user() และเนื่องจากมันเป็นรีแอกทีฟ ลิงก์นี้ก็จะแสดงหรือหายไปเมื่อคุณล็อกอินเข้ามา หรือล็อกเอาท์ออกจากแอพ

    เมธอดของ Meteor : ง่ายและปลอดภัยขึ้น

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

    • บันทึกเวลาที่ป้อนข่าว
    • ทำให้แน่ใจว่า ไม่สามารถป้อนข่าวใหม่ด้วย URL เดียวกันได้
    • เพิ่มข้อมูลรายละเอียดเกี่ยวกับผู้สร้างข่าว (ไอดี, ชื่อผู้ใช้, และอื่นๆ)

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

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

    ด้วยเหตุผลเหล่านี้ มันจึงดีกว่าถ้าเราจะทำให้ฟังก์ชันจัดการเหตุการณ์ทำงานพื้นฐานง่ายๆ และถ้าต้องการทำอะไรที่มากกว่าการเพิ่มหรืออัพเดทคอลเลคชั่น เราก็ควรใช้ เมธอด (method)

    เมธอดของ Meteor เป็นฟังก์ชันบนฝั่งเซิร์ฟเวอร์ที่ถูกเรียกใช้จากฝั่งไคลเอนต์ ซึ่งไม่ใช่ว่าเราจะไม่คุ้นเคยกับมันเลย อันที่จริงแล้วฟังก์ชัน insert, update และ remove ของคอลเลคชั่น ต่างก็เป็นเมธอดที่ทำงานอยู่เบื้องหลังทั้งหมด ถึงตอนนี้เราจะลองมาสร้างของเราเองบ้าง

    ย้อนกลับไปที่ post_submit.js แทนที่เราจะเพิ่มข่าวเข้าไปตรงๆที่คอลเลคชั่น Posts เราก็จะเรียกใช้เมธอด postInsert แทน

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return alert(error.reason);
    
          Router.go('postPage', {_id: result._id});  
        });
      }
    });
    
    client/templates/posts/post_submit.js

    ฟังก์ชัน Method.call จะเรียกใช้งานเมธอดตามชื่อในพารามิเตอร์ตัวแรก ซึ่งคุณสามารถส่งพารามิเตอร์เพิ่มเติมตามไปได้ (ในกรณีนี้คือ อ็อบเจกต์ post ที่เราสร้างจากฟอร์ม) และใส่ฟังก์ชัน callback เป็นตัวสุดท้าย ซึ่งจะทำงานเมื่อเมธอดฝั่งเซิร์ฟเวอร์ทำงานแล้วเสร็จ

    ฟังก์ชัน callback ของเมธอด Meteor จะมีพารามิเตอร์สองตัวเสมอ คือ error และ result ไม่ว่าด้วยเหตุผลอะไรก็ตาม ถ้าตัวแปร error มีค่า เราจะแจ้งเตือนผู้ใช้ (ด้วยการใช้ return เพื่อยกเลิก callback) แต่ถ้าทุกอย่างทำงานตามที่ควรจะเป็น เราก็จะส่งผู้ใช้ไปที่หน้าสนทนาของข่าวที่เพิ่งสร้างนี้

    ตรวจสอบความปลอดภัย

    เราจะถือโอกาสนี้เพิ่มความปลอดภ้ยให้กับเมธอดของเราด้วยการใช้แพ็คเกจ audit-argument-checks

    โดยแพ็คเกจนี้จะตรวจสอบอ็อบเจกต์จาวาสคริปต์กับรูปแบบที่กำหนดไว้แล้ว ในกรณีของเรานั้น เราจะใช้มันตรวจสอบว่า ผู้ใช้ที่เรีียกใช้เมธอดนั้นล็อกอินเข้าระบบอย่างถูกต้อง (ด้วยการตรวจสอบว่า Meteor.userId() มีค่าเป็น string) และอ็อบเจกต์ postAttributes ที่ถูกส่งเข้ามาทางพารามิเตอร์ของเมธอด มีค่าคุณสมบัติ title และ url มาด้วย เพื่อป้องกันไม่ให้เราป้อนข้อมูลมั่วๆเข้าไปในฐานข้อมูล

    ดังนั้นเราจะสร้างเมธอด postInsert ไว้ในไฟล์ collections/posts.js ของเรา และเราจะลบบล็อก allow() ออกจาก posts.js เนื่องจากเมธอดของ Meteor จะมองข้ามมันไปอยู่ดี

    จากนั้นเราก็จะ extend อ็อบเจกต์ postAttributesโดยเพิ่มคุณสมบัติเข้าไปอีกสามตัวคือ _id ของผู้ใช้ และ username รวมทั้งเวลาที่ป้อนข่าว submitted ก่อนที่เราจะเพิ่มข้อมูลทั้งหมดนี้เข้าไปในฐานข้อมูลของเรา และส่งคืนค่า _id กลับไปให้ไคลเอนต์ (หรืออีกนัยหนึ่งคือ ผู้เรียกใช้งานเมธอดนี้) ในรูปแบบอ็อบเจกต์จาวาสคริปต์

    Posts = new Mongo.Collection('posts');
    
    Meteor.methods({
      postInsert: function(postAttributes) {
        check(Meteor.userId(), String);
        check(postAttributes, {
          title: String,
          url: String
        });
    
        var user = Meteor.user();
        var post = _.extend(postAttributes, {
          userId: user._id, 
          author: user.username, 
          submitted: new Date()
        });
    
        var postId = Posts.insert(post);
    
        return {
          _id: postId
        };
      }
    });
    
    lib/collections/posts.js

    จำไว้ว่า เมธอด _extend() เป็นส่วนหนึ่งของไลบรารี่ Underscore และช่วยให้คุณ “extend” อ็อบเจกต์ตัวนึงด้วยคุณสมบัติของอีกตัวได้

    คอมมิท 7-6

    Use a method to submit the post.

    ลาก่อน Allow/Deny

    เมธอด Meteor จะทำงานบนเซิร์ฟเวอร์ ดังนั้น Meteor จึงสมมุติว่า เมธอดเหล่านี้เชื่อถือได้ ด้วยเหตุนี้เมธอด Meteor จึงข้ามการทำงานของฟังก์ชัน allow/deny ไป

    ถ้าคุณต้องการรันโค้ดบางอย่างก่อน insert,update หรือ remove แม้จะอยู่บนเซิร์ฟเวอร์ เราก็แนะนำให้ใช้แพ็คเกจ collection-hooks

    ป้องกันไม่ให้ซ้ำ

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

    Meteor.methods({
      postInsert: function(postAttributes) {
        check(this.userId, String);
        check(postAttributes, {
          title: String,
          url: String
        });
    
        var postWithSameLink = Posts.findOne({url: postAttributes.url});
        if (postWithSameLink) {
          return {
            postExists: true,
            _id: postWithSameLink._id
          }
        }
    
        var user = Meteor.user();
        var post = _.extend(postAttributes, {
          userId: user._id, 
          author: user.username, 
          submitted: new Date()
        });
    
        var postId = Posts.insert(post);
    
        return {
          _id: postId
        };
      }
    });
    
    collections/posts.js

    เราทำได้ด้วยการค้นหาข่าวจากฐานข้อมูลด้วย URL ที่ป้อนเข้ามา ถ้าเราพบ เราจะ return ค่า _id ของข่าวพร้อมด้วยค่า postExists: true เพื่อให้ไคลเอนต์รู้ว่าเป็นสถานะการพิเศษ

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

    สิ่งที่เหลือก็คือการใช้ข้อมูล postExists ที่ตัวช่วยจัดการเหตุการณ์ในฝั่งไคลเอนต์ มาแสดงข้อความเตือน

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return alert(error.reason);
    
          // show this result but route anyway
          if (result.postExists)
            alert('This link has already been posted');
    
          Router.go('postPage', {_id: result._id});  
        });
      }
    });
    
    client/templates/posts/post_submit.js

    คอมมิท 7-7

    Enforce post URL uniqueness.

    จัดเรียงข่าว

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

    Template.postsList.helpers({
      posts: function() {
        return Posts.find({}, {sort: {submitted: -1}});
      }
    });
    
    client/templates/posts/posts_list.js

    คอมมิท 7-8

    Sort posts by submitted timestamp.

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

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