การจัดเส้นทาง

5

แปลไปแล้ว

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

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

    โดยเราจะทำให้หน้าพวกนี้เรียกใช้งานได้จาก ลิงก์ถาวร (permalink) ในรูปแบบของ http://myapp.com/posts/xyz ซึ่งแต่ละข่าวที่โพสต์จะมีค่า xyz แตกต่างกันไปตามค่าตัวแปร _id ของ MongoDB

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

    เพิ่มแพ็คเกจ Iron Router เข้าไปในแอพ

    Iron Router เป็นแพ็คเกจที่ใช้เพื่อจัดเส้นทางการทำงาน ถูกสร้างขึ้นมาเพื่อใช้กับแอพของ Meteor โดยเฉพาะ

    มันไม่เพียงแต่จะช่วยในเรื่องของการจัดเส้นทาง (กำหนดชื่อเส้นทางต่างๆ) แต่ยังช่วยในเรื่องฟิลเตอร์ (กำหนดการทำงานให้กับแต่ละเส้นทาง) และจัดการเรื่องการบอกรับข้อมูลด้วย (ควบคุมว่าเส้นทางไหนเข้าถึงข้อมูลใด) (หมายเหตุ: บางส่วนของ Iron Router พัฒนาโดย Tom Coleman ผู้แต่งร่วมของหนังสือ Discover Meteor เล่มนี้)

    เราเริ่มด้วยการติดตั้งแพ็คเกจจาก Atmosphere

    meteor add iron:router
    
    Terminal

    คำสั่งนี้จะทำการดาวน์โหลดและติดตั้งแพ็คเกจ Iron Router ลงในแอพของเราให้พร้อมใช้งาน ซึ่งบางครั้งคุณอาจต้องรีสตาร์ทแอพใหม่อีกครั้ง (โดยการกด ctrl+c เพื่อปิดโปรเซส และเรียก meteor เพื่อเริ่มต้นใหม่อีกครั้ง) ก่อนที่จะสามารถใช้งานแพ็คเกจได้

    คำศัพท์เกี่ยวกับตัวจัดการเส้นทาง (Router)

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

    • ข้อมูลเส้นทาง (routes): คือข้อมูลพื้นฐานที่ใช้เพื่อจัดการเส้นทาง ซึ่งก็คือ ชุดคำสั่งที่บอกแอพว่าต้องไปที่ไหน และต้องทำอะไรเมื่อรับค่า URL เข้ามา

    • พาธ (paths): คือ URL ภายในแอพของคุณ ที่อาจจะเป็นแบบตายตัว (/terms_of_service) หรือแบบปรับเปลี่ยนได้ (/posts/xyz) และอาจเป็นแบบที่มีตัวแปรติดมาด้วยก็ได้ (/search?keyword=meteor)

    • เซกเมนต์ (segments): คือส่วนต่างๆของพาธ คั่นด้วยเครื่องหมาย /

    • ฮุค (hooks): คือการทำงานที่คุณต้องการให้มีในระหว่างก่อน, หลัง, หรือตอนที่กำลังเปลี่ยนเส้นทาง ตัวอย่างที่เห็นได้ทั่วไปคือ การตรวจสอบสิทธิของผู้ใช้งานว่าเพียงพอก่อนที่จะแสดงหน้าเว็บหรือไม่

    • ฟิลเตอร์ (filters): จริงๆ ก็คือ ฮุคที่คุณกำหนดให้ทำงานกับทุกๆเส้นทาง

    • เทมเพลทเส้นทาง (route templates): แต่ละเส้นทางต้องมีเทมเพลท ซึ่งถ้าคุณไม่ได้กำหนดไว้ ตััวจัดการเส้นทางจะมองหาเทมเพลทที่มีชื่อเดียวกับชื่อเส้นทางนั้นโดยอัตโนมัติ

    • เลย์เอาท์ (layout): คุณอาจมองเลย์เอาท์ให้เป็น “เฟรม” ของหน้าแอพก็ได้ ซึ่งมันจะประกอบไปด้วยโค้ด HTML ที่หุ้มเทมเพลทไว้อีกชั้น และจะไม่เปลี่ยนแปลงแม้ว่าเทมเพลทจะเปลี่ยนไป

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

    ถ้าคุณต้องการรายละเอียดที่มากขึ้นเกี่ยวกับ Iron Router คุณควรไปดูที่ หน้าเอกสารบน GitHub

    จัดเส้นทาง: ผูก URL เข้ากับเทมเพลท

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

    Iron Router ช่วยให้เราหลุดออกจากกรอบนี้ได้ โดยทำหน้าที่สร้างเนื้อหาภายในแท็ก <body> ให้เรา นั่นคือเราไม่ต้องสร้างแท็กเองทั้งหน้าแบบที่เราเคยทำกับ HTML ทั่วไป เราแค่บอกตัวจัดการเส้นทางว่าเราจะใช้เทมเพลทเลย์เอาท์ตัวไหนก็พอ ซึ่งเทมเพลทเลย์เอาท์นี้จะต่างจากเทมเพลทอื่นตรงที่มีตัวช่วย {{>yield}} อยู่ข้างใน

    โดยตัวช่วย {{>yield}} นี้จะสร้างพื้นที่พิเศษขึ้นในหน้าเว็บ แล้วนำเนื้อหาที่ได้จากการทำงานของเทมเพลทที่เราผูกเข้ากับเส้นทางการทำงานปัจจุบันมาใส่ให้เราโดยอัตโนมัติ (เพื่อให้เข้าใจตรงกัน จากนี้ไปเราจะเรียกเทมเพลทพิเศษตัวนี้ว่า “เทมเพลทเส้นทาง”)

    Layouts and templates.
    Layouts and templates.

    เราจะเริ่มด้วยการสร้างเลย์เอาท์ที่มีตัวช่วย {{>yield}} อยู่ข้างใน โดยแรกสุด ให้เราย้ายแท็ก <body> ทั้งชุดออกจากหน้า main.html แล้วนำเข้าไปไว้ในเทมเพลทใหม่ชื่อ layout.html ที่เราสร้างไว้ในโฟลเดอร์ client/templates/application

    ซึ่ง Iron Router จะจัดการนำแท็กในเลย์เอาท์มาใส่ใน main.html ที่ถูกลดรูปลงเป็นตามที่เห็นนี้ ให้เราโดยอัตโนมัติ :

    <head>
      <title>Microscope</title>
    </head>
    
    client/main.html

    โดยไฟล์ layout.html ก็จะประกอบด้วยเลย์เอาท์ของหน้าแอพเราดังนี้ :

    <template name="layout">
      <div class="container">
        <header class="navbar navbar-default" role="navigation">
          <div class="navbar-header">
            <a class="navbar-brand" href="/">Microscope</a>
          </div>
        </header>
        <div id="main">
          {{> yield}}
        </div>
      </div>
    </template>
    
    client/templates/application/layout.html

    ให้สังเกตุว่าเราได้เปลี่ยนจากชื่อเทมเพลท postsList มาเรียกใช้ตัวช่วย yield แล้ว

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

    เพื่อแก้ไขให้หน้าจอกลับมาใช้งานได้เหมือนเดิม เราก็จะผูกพาธที่ / เข้ากับเทมเพลท postsList โดยเราจะสร้างไฟล์ router.js ในโฟลเดอร์ /lib ภายในโปรเจกต์ของเรา ดังนี้ :

    Router.configure({
      layoutTemplate: 'layout'
    });
    
    Router.route('/', {name: 'postsList'});
    
    lib/router.js

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

    อย่างที่สอง เราได้สร้างเส้นทางใหม่ชื่อ postsList และผูกเข้ากับพาธเริ่มต้นที่ /

    โฟลเดอร์ /lib

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

    มีคำเตือนเล็กๆ ว่า เนื่องจาก /lib ไม่ได้อยู่ใน /client หรือ /server ก็หมายความว่าอะไรที่อยู่ในนั้นจะเรียกใช้ได้จากทั้งสองฝั่ง

    ชื่อเส้นทาง

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

    โดยปกติ Iron Router จะมองหาเทมเพลทที่มีชื่อเหมือนกับชื่อเส้นทาง อันที่จริงมันหาจากชื่อพาธที่เราให้ด้วยซ้ำ ซึ่งในกรณีนี้มันจะหาไม่พบ (เพราะว่าพาธเราคือ /) แต่ถ้าเราเรียกใช้พาธ http://localhost:3000/postsList แทน Iron Router ก็จะหาเทมเพลทให้เราได้

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

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

    <header class="navbar navbar-default" role="navigation">
      <div class="navbar-header">
        <a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
      </div>
    </header>
    
    //...
    
    client/templates/application/layout.html

    กว่าข้อมูลจะมา

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

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

    โชคดีที่ Iron Router ช่วยให้เราทำแบบนั้นได้ง่ายๆ เราแค่บอกให้มันรอรับข้อมูลให้เรียบร้อยเสียก่อน แล้วถึงค่อยทำงานต่อ

    โดยเราก็แค่ย้ายโค้ดการบอกรับข้อมูล posts จากใน main.js มาไว้ที่ตัวจัดการเส้นทางแทน

    Router.configure({
      layoutTemplate: 'layout',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.route('/', {name: 'postsList'});
    
    lib/router.js

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

    สิ่งที่แตกต่างกันระหว่างโค้ดตรงนี้กับที่เราเขียนก่อนหน้า (ที่การบอกรับข้อมูลเกิดขึ้นใน main.js ซึ่งตอนนี้ไม่มีแล้ว และสามารถลบไฟล์ทิ้งได้) ก็คือ ตอนนี้ Iron Router รู้ว่าเส้นทางจะ “พร้อมใช้” เมื่อได้รับข้อมูลที่บอกรับมาเรียบร้อยแล้ว

    กำลังโหลดอยู่นะ

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

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

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

    ส่วนสุดท้ายที่เราต้องทำคือเทมเพลทขณะโหลดข้อมูล เราจะใช้แพ็คเกจ spin มาสร้างภาพเคลื่อนไหวแสดงการโหลด ด้วยการใช้คำสั่ง meteor add sacha:spin และสร้างเทมเพลท loading ลงในโฟลเดอร์ client/templates/includes ดังนี้ :

    <template name="loading">
      {{>spinner}}
    </template>
    
    client/templates/includes/loading.html

    สังเกตุว่า {{>spinner}} เป็นโค้ดตัวช่วยที่อยู่ในแพ็คเกจ spin ซึ่งแม้ว่ามันจะอยู่นอกแอพ เราก็สามารถเรียกมาใช้งานได้เหมือนเทมเพลทตัวอื่นๆ

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

    คอมมิท 5-2

    Wait on the post subscription.

    แวบมาดู Reactivity กันหน่อย

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

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

    ตอนนี้ขอตอบแค่ว่า ต้องใช้วิธีการ Reactivity เข้ามาจัดการ ซึ่งเราจะได้เรียนรู้เรื่องนี้กันมากขึ้น เร็วๆ นี้ !

    จัดเส้นทางไปที่หน้าข่าว

    ตอนนี้เมื่อเรารู้วิธีจัดเส้นทางไปยังเทมเพลท postsList กันแล้ว เราจะมาลองจัดเส้นทางไปที่หน้าแสดงข่าวกันดูบ้าง

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

    ก่อนอื่นเราก็สร้างเทมเพลทขึ้นใหม่ ให้แสดงข้อมูลเหมือนกับที่เราใช้ในหน้ารายการข่าว

    <template name="postPage">
      <div class="post-page page">
        {{> postItem}}
      </div>
    </template>
    
    client/templates/posts/post_page.html

    โดยเราจะเพิ่มข้อมูลอื่นๆ เข้าไปที่เทมเพลทนี้ตอนหลัง (เช่น คอมเมนต์) แต่ตอนนี้ใช้มันเป็นแค่เปลือกหุ้มให้กับ {{> postItem}} ไปพลางๆ ก่อน

    ต่อมาเราก็สร้างเส้นทางใหม่ โดยครั้งนี้ให้ผูกพาธ /posts/<ID> เข้ากับเทมเพลท postPage

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.route('/', {name: 'postsList'});
    Router.route('/posts/:_id', {
      name: 'postPage'
    });
    
    
    lib/router.js

    การใช้ _id จะบอกให้ตัวจัดการเส้นทางทำสองอย่าง อย่างแรกคือ ให้หาเฉพาะเส้นทางที่อยู่ในรูป /posts/xyz โดยที่ “xyz” เป็นอะไรก็ได้ อย่างที่สองคือ ให้นำค่าที่ได้จาก “xyz” ไปใส่ที่พารามิเตอร์ _id ในอาร์เรย์ params ของตัวจัดการเส้นทาง

    สังเกตุด้วยว่าเราใช้ _id เพื่อความสะดวกเท่านั้น ตัวจัดการเส้นทางไม่ได้รู้ว่าเราส่งค่า _id จริงๆ หรือตัวอักษรอะไรมาให้

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

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

    The data context.
    The data context.

    ในกรณีของเรา ชุดข้อมูลที่จะส่งให้เทมเพลท จะมาจากข้อมูลข่าวตาม _id ที่ได้จาก URL ดังนี้ :

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

    โดยทุกครั้งที่ผู้ใช้เข้ามาตามเส้นทางนี้ เราก็แค่หาข้อมูลข่าวที่เหมาะสมและส่งต่อให้เทมเพลท ทั้งนี้ให้จำไว้ว่า findOne จะคืนข้อมูลข่าวที่ตรงกับการค้นหามาตัวเดียว และการใช้แค่ id เป็นค่าพารามิเตอร์ก็เหมือนกับการส่งค่า {_id: id} เข้าไปนั่นเอง

    ภายในฟังก์ชัน data ของแต่ละเส้นทาง ตัวแปร this จะหมายถึงเส้นทางปัจจุบัน เราจึงสามารถใช้ this.params เพื่อเข้าถึงส่วนที่เป็นพารามิเตอร์ของเส้นทางได้ (ตัวที่เราใส่เครื่องหมาย : ไว้ข้างหน้า)

    รู้จักชุดข้อมูลกันหน่อย

    การกำหนดชุดข้อมูลให้กับเทมเพลท ทำให้เราควบคุมค่าของ this ในตัวช่วยเทมเพลทได้

    ปกติแล้วชุดข้อมูลที่ใช้กับเทมเพลท จะถูกกำหนดโดยอัตโนมัติจากตัวดำเนินการ {{#each}} ซึ่งในทุกรอบการทำงานจะสร้างชุดข้อมูลขึ้นใหม่ด้วยค่าของตัวแปรตามรอบนั้นๆ :

    {{#each widgets}}
      {{> widgetItem}}
    {{/each}}
    

    แต่เราก็สามารถระบุได้เองว่าเราจะใช้ชุดข้อมูลจากตัวแปรอะไร ด้วยตัวดำเนินการ {{#with}} ซึ่งทำงานเหมือนกับคำพูดที่ว่า “นำค่าจากตัวแปรนี้ ไปใช้กับเทมเพลทที่ระบุ” ดังตัวอย่างนี้ :

    {{#with myWidget}}
      {{> widgetPage}}
    {{/with}}
    

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

    {{> widgetPage myWidget}}
    

    ถ้าต้องการเรียนรู้วิธีใช้งานชุดข้อมูลที่ลึกกว่านี้ เราแนะนำให้ไป อ่านต่อที่หน้าบล็อกของเราได้

    การใช้ตัวช่วยสร้างพาธ

    ในช่วงท้ายนี้เราจะสร้างปุ่ม “Discuss” เพื่อเชื่อมไปยังหน้าข่าวแต่ละข่าว ซึ่งจริงๆ เราสามารถทำได้ด้วยแท็ก <a href="/posts/{{_id}}"> อยู่แล้ว แต่ถ้าเราใช้ตัวช่วยสร้างพาธก็จะได้อะไรที่แน่นอนกว่า

    ที่เราต้องทำคือใช้ตัวช่วย {{pathFor 'postPage'}} เพื่อสร้างพาธไปที่หน้าข่าวตามเส้นทาง `postPage’ ที่เราสร้างไว้

    <template name="postItem">
      <div class="post">
        <div class="post-content">
          <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
        </div>
        <a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
      </div>
    </template>
    
    client/templates/posts/post_item.html

    คอมมิท 5-3

    Routing to a single post page.

    แต่ช้าก่อน ไม่สงสัยกันบ้างหรือว่า ตัวจัดการเส้นทางรู้ได้อย่างไรว่าจะหาค่า xyz จาก /posts/xyz มาได้ยังไง เพราะว่าเราก็ไม่ได้ส่งค่า _id ไปให้ด้วยซ้ำ

    สิ่งที่เกิดขึ้นก็คือ Iron Router ฉลาดพอที่จะรู้ได้ด้วยตัวเอง เราแค่บอกมันให้ใช้เส้นทาง postPage และมันก็รู้ว่าเส้นทางนี้ต้องใช้ค่า _id จากที่เรากำหนดไว้ใน path

    ดังนั้นมันก็มองหา _id จากข้อมูลที่มีอยู่ ซึ่งก็คือ ชุดข้อมูลที่ตัวช่วย {{pathFor 'postPage'}} ใช้อยู่ หรืออีกนัยนึงก็คือ this นั่นเอง และ this ของเราจริงๆ แล้วก็คือ หัวข้อข่าวซึ่งมีค่า _id ติดมาด้วยอย่างน่าแปลกใจ !

    ยังมีอีกวิธีที่คุณสามารถบอกตัวจัดการเส้นทางไปเลยว่าคุณต้องการให้มันหา _id ที่ไหน ด้วยการส่งค่าพารามิเตอร์ตัวที่สองให้กับตัวช่วย เช่น {{pathFor 'postPage' someOtherPost}} ซึ่งวิธีนี้มักใช้เมื่อต้องการสร้างลิงก์ไปที่ข่าวก่อนหน้าหรือที่มาทีหลังตามลำดับที่โพสต์ไว้

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

    A single post page.
    A single post page.

    HTML5 pushState

    สิ่งหนึ่งที่ควรเข้าใจคือ การเปลี่ยนแปลงค่า URL เกิดขึ้นได้จากการใช้ HTML5 pushState

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

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

    หาข่าวที่โพสต์ไม่เจอ .. ทำไงดี

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

    ขอบคุณอีกครั้งที่ Iron Router ดูแลเราในเรื่องนี้ด้วยตัวเลือก notFoundTemplate

    เริ่มด้วยการสร้างเทมเพลทขึ้นใหม่ เพื่อใช้แสดงข้อความผิดพลาดจากโค้ด 404 แบบง่ายๆ

    <template name="notFound">
      <div class="not-found page jumbotron">
        <h2>404</h2>
        <p>Sorry, we couldn't find a page at this address.</p>
      </div>
    </template>
    
    client/templates/application/not_found.html

    จากนั้นก็แค่บอก Iron Router ให้ใช้เทมเพลทใหม่นี้ :

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

    ลองมาทดสอบหน้าแสดงข้อความผิดพลาดกันดู ด้วยการลองเข้าไปที่ URL แปลกๆ เช่น http://localhost:3000/nothing-here

    แต่ช้าก่อน ถ้าบางคนป้อน URL แบบนี้ล่ะ http://localhost:3000/posts/xyz โดยใช้ค่า xyz ที่ไม่ใช่ _id จริง ซึ่งเป็นรูปแบบเส้นทางที่ถูกต้อง เพียงแต่ว่าไม่ได้ชี้ไปที่ข้อมูลจริงเท่านั้น

    Iron Router ไม่ทำให้เราผิดหวัง มันฉลาดพอที่จะรู้ตรงนี้ ถ้าเราแค่เพิ่มฮุค dataNotFound ไว้ที่ตอนท้ายของ router.js :

    //...
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    
    lib/router.js

    จะเป็นการบอก Iron Router ให้แสดงหน้า “not found” กับเส้นทางที่ผิด และกับเส้นทาง postPage ที่ฟังก์ชัน data คืนค่าผิดๆ (เช่น null, false, undefined หรือค่าว่าง) กลับมา

    คอมมิท 5-4

    Added not found template.

    ทำไมต้องเป็น “Iron”

    คุณอาจสงสัยว่าการใช้ชื่อ “Iron Router” มีที่มาอย่างไร ตามที่ Chris Mather ผู้สร้าง Iron Router บอกไว้ ชื่อนี้ได้มาจากความจริงที่ว่าส่วนประกอบหลักๆของ meteor ก็คือ iron นั่นเอง