“Firebase Cloud Function” — ว่าด้วยเรื่องของ Promise

Siratee K.
Firebase Thailand
Published in
4 min readFeb 3, 2020

--

{Intro}

เคยเจอปัญหานี้กันหรือไม่? Warning: Avoid promise nesting หรือว่า Error: Each then() should return a value or throw ถ้าเป็นแค่ Warning ⚠️ ผมเคยเป็นคนนึงแหละที่จะ ………😏😏😏 (ข้ามสิ ยัง Deploy ได้ ไม่ได้พังซักหน่อย 55555 ) ซึ่งถ้าเราทำให้มันถูกต้องตามหลักการแล้ว ก็จะเป็นสิ่งที่ดีกว่าแน่นอนครับ ดังนั้นในบทความนี้มาทำความรู้จักกับสิ่งที่เรียกว่า Promise ใน Javascript และการใช้งาน Promise ใน Cloud Function for Firebase กันดีกว่า

{อะไรคือ Promise?}

ขอ Promise ไว้ก่อน ตอนนี้ทำไม่ทันแล้วววว

ธรรมชาติของภาษา JavaScript นั้น จะ Run ตั้งแต่บรรทัดแรก จน บรรทัดสุดท้ายต่อเนื่องกันเลยแบบไม่รอ (Asynchronous) ซึ่งก็จะเกิดปัญหาว่า บางคำสั่งอาจต้องใช้เวลาในการทำงาน เช่น การดึงข้อมูลจากฐานข้อมูล ทำให้ข้อมูลมาไม่ทัน ระบบจบการทำงานไปก่อน จึงมีสิ่งที่เรียกว่า Promise เกิดขึ้นมาเพื่อช่วยรอในการทำคำสั่งเหล่านั้นในเสร็จสิ้นก่อนจบการทำงาน

ถ้าเปรียบเทียบง่ายๆ Promise เปรียบได้กับ Async/Await นั้นเองครับ ภายใน Promise จะเป็นการทำงานแบบ Async รอผลลัพท์จากคำสั่ง และส่งกลับไปยัง Callback Function

ตัวอย่างของคำสั่งที่มีการ return ค่า Promise

admin.firestore().doc("/test/Hello").get().then((doc) => {
return ......
}).catch((e)=> {
return ......
})

จะสังเกตว่ามีการใช้ .then() และ .catch() เข้ามาเพื่อรอข้อมูลจากคำสั่งก่อนหน้า ในที่นี้เป็นการเอาข้อมูลจาก Firestore มานั้นเองครับ

→ State ของ Promise

สถานะการทำงานของ Promise ประกอบไปด้วย 3 สถานะคือ 1)Pending 2)Fulfilled 3)Rejected

  • Pending — กำลังทำคำสั่งอยู่ยังไม่เสร็จ
  • Fulfilled — ทำคำสั่งเสร็จแล้วได้ผลลัพท์ ไม่เกิด Error
  • Rejected — ทำคำสั่งแล้วได้ผลลัพท์เป็น Error, Exception

สถานะไม่สามารถเปลี่ยนได้เมื่อเปลี่ยนเป็น Fulfilled หรือ Rejected แล้ว

Cr: https://www.youtube.com/watch?v=7IkUgCLr5oA

{Promise ใน Cloud Function}

หากเราได้เคยใช้งาน Firebase มา จะรู้ได้ทันทีเลยว่าเกือบทั้งหมดใน Firebase Admin SDK มีการใช้ Promise ในการ Handle ข้อมูลต่างๆของเราเช่นการ ดึง,เขียน,เเก้,ลบ ข้อมูลต่างๆใน Firestore และอีกมากมาย

→ ธรรมชาติของ Cloud Function

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

* กฎการจบการทำงานของ Cloud Function มีอยู่ด้วยกัน 2 ข้อ

→ Cloud Function แบบ HTTP Trigger มีการ return ด้วย response object เพื่อตอบกลับไปยังผู้เรียก Function นี้

→ Cloud Function แบบ Background Trigger (Trigger อื่นๆที่ไม่ใช่ HTTP Trigger) ต้องมีการ return promise Cloud Function จะรอจนกว่า Promise จะทำงานเสร็จและจบการทำงานด้วยตัวเอง หรือ หากไม่มีอะไรต้องรอ สามารถ return null ได้เลย ถือเป็นการจบ Function นั้นเช่นกัน

สำคัญ!! เราต้อง return Promise ให้ถูกต้อง มิเช่นนั้น Cloud Function จะจบการทำงานไปก่อน หรือ อาจรอทำงานจนเป็น timeout ได้เลย ส่งผลให้โค๊ดของเราทำงานไม่สมบูรณ์

→ Promise ใน Cloud Function

  • 1. ทุกๆ Flow ของ คำสั่งภายใน Promise ต้องมีการ return or throwค่าออกมา
  • 2. ควรมี .catch() เพื่อดัก Error ที่อาจจะเกิดขึ้น ทุกครั้งที่มีการเรียก Promise (หากไม่มี เวลาเกิด Error อาจเป็น Timeout ได้ ซึ่งนั้นไม่ใช่เรื่องที่ดีของ UX แน่นอน)

→ ทดสอบการทำงานของ Promise ใน Cloud Function

ผมจะทดสอบให้เห็นโดยใช้ firebase-admin-sdk dependency เรียกข้อมูลต่างๆจาก Firestore Database ซึ่งอันดับแรกผมจะสร้าง Firestore Database ที่มี Collection ชื่อ Firebase ในนั้น มี Doc ชื่อ Function และมี Field value เป็น Hello World ตามนี้

ซึ่งผมจะพัฒนา Cloud Function เพื่อดึงข้อมูลนี้ออกมาแสดง

Note: การ Deploy Function ขึ้นไปยัง Firebase แต่ละครั้งมันใช้เวลานานเหลือเกิน แต่ Firebase ก็มีตัวช่วยลดเวลาดีๆให้เราโดยทำให้เราสามารถ Test Function ที่เราเขียนขึ้นมาใน Local Machine ได้ทันที โดยไม่ต้อง Deploy ไปยัง Firebase ก่อน ด้วยการใช้คำสั่งนี้

$ firebase serve --only functions

หลังจากนี้ ผมจะยกตัวอย่างการใช้ Promise ใน Cloud Function ทั้งถูกและผิดให้เพื่อนๆได้เห็นการทำงานครับ

* ตัวอย่างที่ 1

หลักการคือ ผมประกาศตัวเเปรชื่อว่า value โดยให้มีค่าเป็น “No data” และผมจะดึงค่าจาก Firestore มาเพื่อเปลี่ยนค่านั้นภายหลัง ซึ่งผมมีการ log ค่าของ value ออกมาในตอนสุดท้าย และใช้ res object ในการส่งค่า value ออกมาให้ผู้เรียก Function นี้….. สิ่งที่คาดหวังคือ จะเห็นคำว่า Hello World from Cloud Function ตอบกลับมายังผู้เรียก…. เพื่อนๆคิดว่าผลลัพท์จะเป็นยังงัยครับ?

→ คำตอบคือ…….

No data from Cloud Function

อ่าว… ทำมัยละ เราก็เขียนเรียก Data มาแล้วนิ T^T

จุดผิดพลาดของโค๊ดนี้มี 2 จุดใหญ่ๆครับ

  1. Function .get() เป็น Function ที่จะ Return Promise ออกมาครับ และอย่างที่ผมบอกไปว่า ธรรมชาติของ Javascript คือ รันตั้งแต่แรกจนจบโดยไม่รอ ซึ่งในที่นี้ Promise กำลังอยู่ในระหว่างการทำงาน หรือ Pending State ค่าที่มาจาก Firestore เลยยังยังมาไม่ถึง คำสั่งเปลี่ยนค่า value ข้างใน .then() เลยยังไม่ทำงานทำให้ value ของเรายังคงเป็น No data เหมือนเดิม
  2. จากกฏ 2 ข้อของการจบ Cloud Function เรามีการเรียก response object( res.send() ) ทันที เพราะ javascript รันตั้งแต่แรกจนจบเลย Cloud Function จึงเข้าใจว่า ฉันสามารถจบการทำงานได้แล้ว แต่จริงๆแล้วมี Promise ที่กำลังทำงานอยู่อีก ด้วยเหตุนี้ ทำให้ ข้อมูลที่มาจาก Firestore มาไม่ถึง เพราะ Function ของเราจบการทำงานไปก่อนนั้นเอง ** สำคัญมากๆในข้อนี้ เพราะการที่เราจบ Function ของเราไปก่อนที่ควรจะจบ อาจทำให้เกิด Crash ในรูปแบบที่เราไม่รู้เลยก็ได้ ดังนั้น Flow ที่ปลอดภัยที่สุดสำหรับการจบ Function คือ รอการทำงานต่างๆให้เสร็จทั้งหมดก่อนแล้วจึงจบ Function

ดังนั้น เราจะ Correct Code ชุดนี้ได้เป็นแบบนี้ครับ

ผลลัพท์คือ

Hello World from Cloud Function

* ตัวอย่างที่ 2 Promise Nesting

คราวนี้มาดูตัวอย่างการใช้ Promise ที่ Complex มากขึ้นหน่อยดีกว่า ผมจะทำการดึงข้อมูลจาก 2 Collection และนำข้อมูลมาต่อกัน สุดท้ายก็ Print ข้อมูลออกมาแสดง

โดยใน Firestore Database ผมจะมี Collection อยู่ 2 อัน

  1. Test มี Doc ชื่อ Medium และใน Doc มี field ชื่อ Message มีค่าเป็น Hello
  2. Test2 มี Doc ชื่อ Function และใน Doc มี field ชื่อ Message มีค่าเป็น Test

ผลลัพท์คือ….

Hello Test

แต่!! ในตอน Deploy (รวมถึงในบาง Editor) จะต้องมี Warning ขึ้นมาเตือนเราแน่นอนว่า

Warning: Avoid Nesting Promises

Warning ⚠️ นี้เป็นการเตือนว่า เรากำลังทำคำสั่ง Promise ซ้อน ใน Promise อีกที ซึ่งมันไม่ได้หมายความว่าทั้ง Function นั้นจะมีการส่ง Promise ได้แค่ 1 ครั้ง แต่ เราต้องซ้อน Promise ให้ถูกวิธีนั้นเอง

วิธีการแก้: อย่างที่ผมบอกว่าทุกครั้งที่มีการ .then() เราจะต้อง return ค่าออกมาซึ่ง ตรงส่วนนี้เราสามารถ return ค่าที่เป็น promise ออกมาและใช้ .then() ต่อไปได้เลย ซึ่ง Cloud Function ก็จะรอเพื่อให้การทำงานของ Promise ทั้งหมดจบก่อน และสุดท้าย เราก็ return response object ออกมาเป็นการสั่งจบ Cloud Function นั้นเองครับ

ซึ่งเราจะสามารถแก้ไขโค๊ดด้านบนได้เป็น:

เราสามารถที่จะซ้อน promise เป็น chain ลงไปได้เรื่อยๆตามที่เราต้องการเลย เท่านี้ก็จะแก้ไขเรื่องของ Warning: Avoid nesting promises ได้เรียบร้อยแล้ว และโค๊ดของเราก็ดีสบายตามากขึ้นด้วย

→ เสริม:

ผมเชื่อว่าบางคนอาจมีคำถามว่า แล้วถ้าสมมุติว่า เรามี Condition ในระหว่างการทำ Promise ซ้อน Promise หละ เช่น ถ้า true ให้ทำ return promise ต่อ แต่ถ้า false ให้หยุด Promise ไว้ตรงนี้

จากที่ผมค้นคว้าและลองทำมา มีวิธี Workaround ดีๆมากฝากครับ

วิธี: ให้ใช้การ throw object ออกมาเพื่อหยุด Promise Chaining นั้น ซึ่งพอเรา throw object ออกมาแล้ว ไม่ว่าจะอยู่ใน Chain ที่เท่าไหรแล้วก็ตาม มันจะออกมาและไปเข้า case .catch() ของ Promise นั้นทันที

ในตัวอย่างนี้ ผมจะสร้าง Empty Class ขึ้นมา 1 class เพื่อไว้ใช้ดักว่ามันเป็น BreakSignal หรือ เป็น Error จริงๆกันแน่

หลักการคือ ไปเอาค่าจาก doc ที่ Firebase/Permission มาก่อนเพื่อเช็ค Permission หาก True ให้ทำต่อคือไปเอาค่าจาก doc ที่ Firebase/Function มาส่งกลับไปหา user ด้วย response object

ซึ่ง หาก Permission เป็น False แล้วละก็ มันก็จะ throw BreakSignal ออกมา และจะมาถูกคัดแยกใน .catch() อีกทีว่า สิ่งที่เข้ามาในนี้ เป็น BreakSignal หรือเป็น Exception จริงๆนั้นเอง

{Outro}

ก็จบไปแล้วนะครับสำหรับบทความแนะนำการใช้งาน Promise ใน Cloud Function for Firebase หากมีคำถามตรงส่วนไหน สามารถ response ไว้ด้านล่างได้เลยครับ และถ้าชอบบทความนี้อย่าลืมกด ปุ่มปรบมือ และ กด Follow ผมเพื่อเป็นกำลังใจให้ผมเขียนบทความออกมาเรื่อยๆด้วยนะครับ

ขอบคุณทุกคนที่เข้ามาอ่านบทความนี้ แล้วพบกันใหม่ในบทความถัดไปครับ,

ต้นน้ำ

--

--

Siratee K.
Firebase Thailand

A wild Software Engineer. 🐤 Fascinated with Low-Level Computing & Computer Networking & Computer Architecture 🧑‍💻