Promises และ External API

เนื้อหาของคอร์สนี้ เรียบเรียงขึ้นจาก React.js Fundamental
บทนี้เราจะมาต่อกันที่การดึงข้อมูลจาก GitHub ที่ผมได้ติดค้างเอาไว้

ตามหลักของการเขียนโปรแกรมที่ติดต่อกับ third party เช่นพวก DB, remote API เป็นต้น เราควรจะแยกส่วนโปรแกรมจัดการเรื่องเหล่านี้ออกมาต่างหาก ด้วยเหตุผลสองประการ คือ

  1. ทดสอบได้ง่าย เพราะมันไม่ปนเปื้อนกับ code หลักของระบบ
  2. สับเปลี่ยนได้ง่าย เนื่องจากระบบที่เป็น third party นั้นหลายครั้งเป็นสิ่งที่อยู่นอกเหนือความควบคุมของระบบ ดังนั้น เมื่อใดที่มีเหตุจำเป็นต้องเปลี่ยน เราก็แค่เปลี่ยน logic เพียงที่เดียวเท่านั้น ส่วนอื่นๆ ภายในระบบไม่จำเป็นต้องไปแตะ ทำให้มี side effect น้อย และทำให้เราสามารถเปลี่ยนสิ่งเหล่านี้ได้โดยไม่ต้องอกสั่นขวัญผวามากนัก

ดังนั้นให้เราสร้าง folder ที่ชื่อ utils/ ภายใต้ app/ แล้วสร้างไฟล์ใหม่ชื่อว่า githubHelpers.js (app/utils/githubHelpers.js) โดยใช้โครงสร้างแบบ module (อย่าลืมว่า component และคลาสใน React Application ทั้งหมด จะต้องสร้างเป็น JavaScript Module) ดังนี้

Axios

tools ที่เราจะใช้เพื่อดึงข้อมูลจาก Github มีชื่อว่า Axios (https://github.com/mzabriskie/axios) ซึ่งเป็น tools ที่ใช้สำหรับ call http service ผ่าน node.js โดยทำงานในรูปแบบของ JavaScript Promises (เรื่อง Promises เดี๋ยวผมจะพูดให้ฟังภายในเนื้อหาตอนนี้ครับ)

ให้เราทำการ install Axios โดยเปิด terminal แล้วเข้าไปที่ project root จากนั้นใส่ command เข้าไปว่า

จากนั้นให้กลับไปที่ app/utils/githubHelpers.js แล้วเพิ่ม code เข้าไปดังนี้

ตรงส่วนของ id กับ sec นั้นให้เอาจาก account ใน github ของแต่ละคนมาเองนะครับ

วิธีนำ github client id และ client secret มาใช้งาน

ให้ login เข้าไปใน account ของตัวเองที่ github.com จากนั้นให้คลิกตรงรูป avatar ตรงมุมขวาบนของเพจ แล้วเลือก settings ดังรูป

screen-shot-2559-11-05-at-1-02-15-pm

จากนั้นให้เลือก OAuth Applications จากเมนูด้านซ้าย แล้วคลิกปุ่ม Register a new application ที่อยู่มุมขวาบน (ด้านล่าง avatar ของเรา)

screen-shot-2559-11-05-at-1-02-32-pm

ให้กรอกข้อมูลตามรูปข้างล่าง

screen-shot-2559-11-05-at-1-52-03-pm

จากนั้นกดปุ่ม Register application

ในหน้าต่างต่อมาจะมีการแสดง client id และ client secret ให้เรา copy code ทั้งสองตัวมาวางไว้ใน app/utils/githubHelpers.js ให้เรียบร้อย

จากนั้นให้เราจัดการเขียน code ใน helpers ให้สมบูรณ์ ดังนี้

 

Promises

จาก code ของ function getPlayersInfo ข้างบน จะเห็นว่า ไวยกรณ์มันดูประหลาดนิดๆ

วิธีการเขียนโปรแกรมแบบนี้เป็นการเขียนโปรแกรมที่รองรับการทำงานแบบ Asynchronous ที่ไม่สามารถรัน “ตามลำดับ” งานได้ นั่นเป็นเพราะ function ในรูปแบบนี้นั้น มักจะใช้กับการ call remote service ซึ่งเราไม่รู้ว่าการ call ครั้งนี้มันจะจบลงเมื่อไหร่ ดังนั้น เพื่อไม่ให้ระบบในส่วนอื่นๆ ต้องถูก lock จากการนั่งรอ remote service เราก็ควรจะปล่อยให้ code ในส่วนอื่นๆ ได้ทำงานไปด้วย ในขณะที่ยังรอผลลัพธ์จากภายนอก

จึงเป็นที่มาของ promises หรือการรองรับ Asynchronous calling ของ Javascript โดยเจ้า promises แท้ที่จริงแล้วก็คือ ฟังก์ชั่นที่ถูกเรียกใช้เมื่อใด จะต้องรอเวลาซักระยะหนึ่งก่อนที่มันจะ return ค่าใดๆ กลับมา (เอาประมาณนี้ก่อนครับ เดี๋ยวเรื่องรายละเอียดเชิงลึกกว่านี้ ผมจะว่ากันอีกทีในตอนที่เราจำเป็นต้องยุ่งเกี่ยวกับ promise มากกว่านี้)

หากลองไปไล่ดู code ในฟังก์ชั่น getUserInfo

axios.get() คือฟังก์ชั่นที่ใช้สำหรับเรียก http remote service โดยจะรับ argument เป็น full url ที่เราต้องการเรียกใช้บริการ ซึ่งในที่นี้ก็คือ api.github.com/users/blah blah blah นั่นเอง

เจ้าตัวฟังก์ชั่น axios.get() นี้ มีธรรมชาติเป็น promises function อยู่แล้ว เพราะมันเป็น tool สำหรับเรียกใช้ remote http service ซึ่งรับประกันไม่ได้ว่า เมื่อ call ไปแล้วจะต้องใช้เวลานานแค่ไหน ดังนั้น ฟังก์ชั่น getUserInfo จึงมีหน้าที่ในการ return ผลลัพธ์ที่ได้จากการ call remote http service ของ github ตัวนี้

คราวนี้เรามาดู code พระเอกของเรากันบ้าง getPlayersInfo

axios.all คือฟังก์ชั่นที่ใช้สำหรับ call promises array กล่าวคือ จะมีการ call function ที่เป็น promises หลายๆ ตัวพร้อมๆ กัน

ประเด็นก็คือ เราต้องการสั่งให้มีการรัน callback function ทันทีที่ promises ทั้งหมดทำงานเสร็จ (ภาษาทางเทคนิคเขาเรียกว่ามีการ resolved) คราวนี้ปัญหาคือ ในเมื่อ promises เป็นฟังก์ชั่นที่มันไม่ return (หรือ resolved) ทันที แล้วเราจะไปรู้ได้ไงว่า promises มันทำงานเสร็จทุกตัวแล้ว

axios.all คือฟังก์ชั่นที่ทำหน้าที่ตัวนี้นั่นเอง เพราะมันจะ call promises ทั้งหมด แล้วรอจนมันทำงานเสร็จทุกตัว จากนั้นจึงค่อยไปสู่ .then() ซึ่งภายใน .then นั้นก็มี callback function อยู่ภายในนั้นรอให้ถูกเรียกใช้อีกที

กลับมาที่ code ของ getPlayersInfo อีกครั้งหนึ่ง ฟังก์ชั่นตัวนี้จะรับค่า players ซึ่งก็คือ array ของ playerOne และ playerTwo นั่นเอง แล้วใช้ map() มาจัดการกับ array ตัวนี้

(สำหรับฟังก์ชั่น map() ของ Array ใน JavaScript นั้น ผมอยากให้ผู้อ่านที่ยังไม่รู้จักมัน ลองไปค้นดูในกูเกิลนะครับว่ามันทำหน้าที่อะไร แต่โดยสรุปก็คือ มันมีหน้าที่ในการนำสมาชิกภายใน array แต่ละตัวมา “แปลงสภาพ” ทีละตัวโดยใช้ callback function ที่กำหนดไว้มาจัดการให้)

จาก code ข้างบนนี้ จะมีการ call getUserInfo ซึ่งเป็น promises สองครั้ง (ตามจำนวนสมาชิกใน players parameter) และเมื่อใดก็ตามที่ getUserInfo ทั้งสองนี้ทำงานจบลงทั้งคู่ code จะวิ่งไปทำงานที่ .then() ต่อ

ภายใน .then() นั้นผมดัก console.log ไว้เพื่อสำรวจว่า axios.all() นั้นส่งค่าอะไรมาให้ .then หลังจากที่ promises ทั้งสองทำงานจบไปแล้ว

ให้เปิดไฟล์ app/containers/ConfirmBattleContainer.js ขึ้นมา แล้วเพิ่ม code เข้าไปดังนี้

แล้วจากนั้นให้เข้าไปที่ localhost:8080 (อย่าลืมเปิด server ด้วยล่ะ) จากนั้นไล่กรอกข้อมูลทีละหน้า โดยให้กรอกชื่อ user ของ github ที่คุณรู้จักใน playerOne และ playerTwo (หากไม่รู้จักใคร ก็เลือกเอาซักสอง user จากใน link นี้ก็ได้ https://github.com/facebook/react/graphs/contributors) จากนั้นในหน้า /battle ให้เปิด console ไว้ จะเห็นข้อมูลดังภาพต่อไปนี้

screen-shot-2559-11-05-at-4-27-01-pm

จากรูป เราจะเห็นได้ทันทีว่าชุดข้อมูลที่ทาง axios.all() return กลับมาที่ .then() มีอะไรบ้าง ซึ่งก็คือ array โดยภายใน array นั้นจะมีสมาชิก 2 ตัว แต่ละตัวก็จะมีข้อมูลใน property ที่ชื่อ data ที่ใช้เก็บข้อมูล github user นั่นเอง

กลับมาที่ไฟล์ app/utils/githubHelpers.js แล้วแก้ code ภายใน .then() ดังนี้

จาก code ข้างต้น เราทำการเรียกฟังก์ชั่น map กับ info ซึ่งเป็น return ของ axios.all จากนั้นก็ทำการ “แปลงสภาพ” array info ให้เก็บเฉพาะส่วนของ data (หรือข้อมูลจาก github user) เท่านั้น

โดยสรุปก็คือ เมื่อใดก็ตามที่มีการเรียกใช้ getPlayersInfo() ก็จะมีการ call ไปยัง github API เพื่อดึงข้อมูล user กลับมา แล้วทำการแปลงค่าให้กลายเป็น array ที่มีแต่ข้อมูลของ user ล้วนๆ (ข้อมูลแวดล้อมอื่นๆ ของ github ให้กำจัดทิ้งไป) แล้ว return array ดังกล่าวออกมา

สรุปให้สั้นกว่านี้ก็คือ เมื่อใดมีการเรียกใช้ getPlayerInfo() ก็จะมีการคืนค่า array ของ github users ออกมานั่นเอง

คราวนี้กลับมาที่ app/containers/ConfirmBattleContainer.js ให้แก้ code ในฟังก์ชั่น componnentDidMount ดังนี้

concept ภายในฟังก์ชั่นตัวนี้ก็เหมือนเดิม กล่าวคือ ตัว githubHelpers.getPlayersInfo นั้นมีลักษณะเป็น promises ดังนั้น การใช้งานก็ต้องทำแบบ promises นั่นคือ ต้องมี .then() มาต่อท้าย และภายใน .then() ก็ต้องมีการเรียกใช้ callback function อยู่ภายในเพื่อรอคำสั่งในขั้นถัดไปหลังจากที่ promises ทำงานเสร็จ (resolved) แล้ว

หลังจากที่ getPlayersInfo ทำงานเสร็จแล้ว จะมีการคืนค่าออกมาให้กับทาง .then() ซึ่งผมได้ทำการดัก console.log เอาไว้เพื่อดูว่า promises function ตัวนี้มันคืนค่าอะไรออกมา

ให้ลองรัน localhost:8080 ใหม่อีกครั้ง (อย่าลืมเปิด console ใน browser เอาไว้) เมื่อเล่นโปรแกรมไปถึงหน้าสุดท้าย ก็จะเห็น log บน console ตามภาพ

screen-shot-2559-11-05-at-4-52-02-pm

จะเห็นว่า มันคืนค่าเป็น array ของ players ออกมา และเราจะเอาตัว player ทั้งสองตัวไป set ไว้ใน state ที่ชื่อ playerInfo และเปลี่ยน state isLoading ให้เป็น false เพื่อแจ้งว่า มัน load ข้อมูลจาก github เสร็จแล้ว

เปิดไฟล์ app/containers/ConfirmBattleContainer.js แล้วทำการแก้ code ภายใน componentDidMount ดังนี้

หากสังเกตุดีๆ จะมี code บรรทัดที่ 24 ที่มันดูประหลาดๆ กว่าชาวบ้าน

ก่อนจะอธิบายตัวฟังก์ชั่นนี้ ขอให้เราไปดู code ดังต่อไปนี้

screen-shot-2559-11-05-at-5-15-31-pm

จะเห็นว่าใน componentDidMount มี การอ้างอิง this อยู่สองจุด (ตามรูปคือจุดที่ 1 และ 2

ในโลกของ JavaScript นั้น การใช้งาน this จะไม่เหมือนกับภาษาอื่นๆ ตรงที่ this ของภาษาอื่นๆ นั้น จะอ้างอิงที่ระดับ scope ของ class เสมอ ไม่ว่าจะอ้างอิงตรงไหนก็ตาม

แต่ Javascript นั้นต่างออกไป เนื่องจาก JavaScript นั้น ก่อนหน้านี้ไม่เคยมี concept เรื่อง class ดังนั้น this ใน Javascript นั้นจะอ้างอิงจาก scope ล่าสุดที่มันอยู่เท่านั้น อย่างใน code ข้างต้น ตัวฟังก์ชั่น componentDidMount เป็นฟังก์ชั่น life cycle ของ React ดังนั้น this ตัวแรกจะอ้างอิงถึง React ซึ่งครอบมันอยู่ ส่งผลให้มันสามารถ access ไปที่ค่า props ได้ตรงๆ ผ่าน this.props

แต่ปัญหาคือ this ตัวที่สองนั้น เกิดขึ้นอยู่ภายใต้ callback function ที่อยู่ภายใน .then() ซึ่งถือเป็นคนละ scope กับ React โดยสิ้นเชิง (จำไว้เลยว่า scope ของ callback function จะเป็นคนละ scope กับตัว React เสมอ) ทำให้ this ตัวนี้ไม่สามารถใช้งาน setState ได้ เพราะมันเป็นฟังก์ชั่นของ React

วิธีแก้ก็คือ ให้ทำการ “ป้อน” this จาก scope ของ React เข้าไปใน callback function ด้วยฟังก์ชั่น bind ซึ่งภาพข้างล่างจะแสดงให้เห็นการทำงานของ bind ฟังก์ชั่น

screen-shot-2559-11-05-at-5-29-50-pm

สรุปคือ ฟังก์ชั่น bind() จะทำการ binding this จาก scope ที่ตัวมันอยู่ (ซึ่งก็คือ React scope) ไปยัง scope ภายใน (ซึ่งก็คือ callback function ภายใน .then()) เพียงเท่านี้ เราก็สามารถทำให้ this ตัวที่สอง สามารถใช้งาน .setState ได้

เมื่อเราลองเล่นระบบดูอีกครั้ง คราวนี้พอ browse ไปถึงหน้า /battle ก็จะเห็นคำว่า LOADING อยู่พักนึง แล้วจากนั้นมันจะเปลี่ยนไปเป็น CONFIRM BATTLE ซึ่งแสดงให้เห็นว่า ตัว state ที่ชื่อว่า isLoading ได้เปลี่ยนเป็น false แล้ว

Catch Exception กับ Promises

มีงานสุดท้ายให้เก็บกันเล็กน้อยครับ สำหรับ githubHelpers.js

เนื่องจากไฟล์ดังกล่าวต้องทำงานกับ remote service ดังนั้น เรารับประกันไม่ได้ว่าจะมีข้อผิดพลาดเกิดขึ้นเมื่อใด จะเป็นการดีกว่า หากเรา catch exception เอาไว้ เพื่อทำการ log error ที่เกิดขึ้นมา

ให้เราเปิดไฟล์ app/utils/githubHelpers.js แล้วเพิ่ม code เข้าไปดังนี้

นี่คือลักษณะพิเศษของการใช้ฟังก์ชั่นแบบ promises นั่นคือ เราสามารถใส่ฟังก์ชั่น .catch() ตอนท้ายสุดได้เพื่อดักทุก error (exception) ที่เกิดขึ้นมาภายใน promises แต่ละตัวได้ และหาก error เกิดเมื่อใด มันจะวิ่งไปที่ catch ทันที ทำให้เราสามารถเขียน callback function เพื่อรอดักในกรณีที่เกิด error ได้

สำหรับกรณีที่อยากได้ code ต้นแบบ ผู้อ่านสามารถเข้าไปดูได้ที่

https://github.com/himaeng/react-fun-course/tree/master/07-promises

สรุป

เนื้อหาภายใน tutorial บทนี้ จะเน้นหนักไปยังเรื่องของการจัดการ remote service ซึ่งเราจะใช้ tool ที่ชื่อว่า axios เป็นตัวจัดการ

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

นอกจากนี้ เรายังได้พูดถึงเรื่องของ scope ของ this อีกด้วย ซึ่ง this ในโลกของ Javascript นั้นจะอ้างอิงตาม scope ที่ตัวมันอาศัยอยู่ ไม่ได้อ้างอิงที่ระดับ class scope แบบในภาษาอื่นๆ