<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Firebase Thailand - Medium]]></title>
        <description><![CDATA[Let you learn and share your Firebase experiences with each other. - Medium]]></description>
        <link>https://medium.com/firebasethailand?source=rss----261beda53e69---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Firebase Thailand - Medium</title>
            <link>https://medium.com/firebasethailand?source=rss----261beda53e69---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 12 Nov 2026 19:49:06 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/firebasethailand" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Firestore เปิด Region ใหม่ Bangkok แล้ว! ถึงเวลาย้ายระบบมาใกล้ผู้ใช้ไทย]]></title>
            <link>https://medium.com/firebasethailand/firestore-%E0%B9%80%E0%B8%9B%E0%B8%B4%E0%B8%94-region-%E0%B9%83%E0%B8%AB%E0%B8%A1%E0%B9%88-bangkok-%E0%B9%81%E0%B8%A5%E0%B9%89%E0%B8%A7-%E0%B8%96%E0%B8%B6%E0%B8%87%E0%B9%80%E0%B8%A7%E0%B8%A5%E0%B8%B2%E0%B8%A2%E0%B9%89%E0%B8%B2%E0%B8%A2%E0%B8%A3%E0%B8%B0%E0%B8%9A%E0%B8%9A%E0%B8%A1%E0%B8%B2%E0%B9%83%E0%B8%81%E0%B8%A5%E0%B9%89%E0%B8%9C%E0%B8%B9%E0%B9%89%E0%B9%83%E0%B8%8A%E0%B9%89%E0%B9%84%E0%B8%97%E0%B8%A2-6e38959776bf?source=rss----261beda53e69---4</link>
            <guid isPermaLink="false">https://medium.com/p/6e38959776bf</guid>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[firestore]]></category>
            <category><![CDATA[regions]]></category>
            <category><![CDATA[bangkok]]></category>
            <dc:creator><![CDATA[PamornT]]></dc:creator>
            <pubDate>Mon, 19 Jan 2026 07:37:12 GMT</pubDate>
            <atom:updated>2026-01-19T07:37:11.434Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oyp1Y9DKu-q4zGy7WOrQiw.png" /></figure><p>Google Firestore เปิดตัว <strong>region ใหม่ asia-southeast3 (Bangkok)</strong> อย่างเป็นทางการแล้ว 🎉 <br>บอกเลยว่างานนี้ถูกใจคนทำแอปในประเทศไทยสุดๆ เพราะ data อยู่ใกล้ผู้ใช้มากขึ้นกว่าเดิม</p><p>ลองไปไล่ดูรายละเอียดมา พบว่ามี <strong>ข้อดีหลักๆ อยู่ 2 เรื่องที่เห็นผลชัดมาก</strong></p><p>อย่างแรกคือ <strong>ความเร็ว</strong> — สำหรับผู้ใช้งานในไทย latency ลดลงอย่างเห็นได้ชัด แอปตอบสนองไวขึ้นแบบรู้สึกได้ทันที</p><p>อย่างที่สองคือ <strong>เรื่องราคา</strong> — ค่าใช้จ่ายถูกลงกว่าการใช้ region <strong>asia-southeast1 (Singapore)</strong> พอสมควร โดยเฉพาะสำหรับระบบที่มี traffic เยอะหรือใช้งาน Firestore หนักๆ ลองดูราคาเปรียบเทียบที่ด้านล่างนี้ได้เลยครับ</p><p><strong>asia-southeast1 (Singapore)<br></strong>Document reads² $0.0369 per 100,000 documents<br>Document writes $0.1107 per 100,000 documents<br>Document deletes $0.0123 per 100,000 documents<br>Stored data $0.1846 GiB/month</p><p><strong>asia-southeast3 (Bangkok)<br></strong>Document reads² $0.0315 per 100,000 documents<br>Document writes $0.0945 per 100,000 documents<br>Document deletes $0.0105 per 100,000 documents<br>Stored data $0.1575 GiB/month</p><p>เจอข้อดีขนาดนี้แล้ว จะรออะไรอยู่ล่ะค้าบ 😄<br>ถึงเวลาย้าย Firestore มาใช้ <strong>region ใหม่ Bangkok (asia-southeast3)</strong> กันได้แล้ว</p><p>การย้ายครั้งนี้ต้องทำผ่าน <strong>Google Cloud Console</strong> โดยตรงนะครับ เพราะ Firestore ยังไม่สามารถเปลี่ยน region ของ database เดิมได้แบบคลิกเดียว จำเป็นต้องมีขั้นตอนวางแผนและย้ายข้อมูลอย่างถูกวิธี แต่ข่าวดีคือ <strong>ทำได้ไม่ยาก</strong> และสามารถจัดการให้ downtime ต่ำมาก หรือแทบไม่มีเลย</p><h3>เริ่มกันเลย</h3><ol><li>เข้าไปที่ <a href="https://console.cloud.google.com/">Google Cloud Console</a></li><li>ก่อนอื่น เราต้องเตรียม <strong>ที่เก็บไฟล์สำหรับ export ข้อมูล</strong> ก่อนนะครับ ให้เข้าไปที่ <strong>Google Cloud Storage</strong> แล้วสร้าง <strong>Bucket ใหม่</strong> ขึ้นมา <br>ในขั้นตอนการสร้าง Bucket ให้ตั้งค่า <strong>Location type เป็นแบบ Multi-region</strong> และเลือก <strong>region เป็น Asia <br></strong>เหตุผลที่ต้องเลือกแบบนี้ เพราะเราจะทำการย้ายข้อมูล <strong>ข้าม region</strong> (จาก Singapore ไป Bangkok) ซึ่ง Cloud Storage แบบ Multi-region จะรองรับการ export/import ข้อมูลได้ไม่มีปัญหา</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/642/1*ncoJeWxlBEammtJeHqHGAA.png" /></figure><p>เมื่อสร้าง Bucket เสร็จเรียบร้อยแล้ว เราก็พร้อมสำหรับขั้นตอนถัดไปในการย้าย Firestore ข้อมูลครับ 🚀</p><p>3. ต่อมาให้เข้าไปที่ product firestore ถ้าหาเมนูไม่เจอให้ search เอาได้เลยค้าบ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s-fef-J21EEiXrC0sgRuqg.png" /></figure><p>4. เข้ามาแล้วจะเจอ database firestore ตัว default ที่เรามีอยู่แล้ว จะสามารถดูได้เลยว่า database เดิมเรา อยู่ที่ region ไหน</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xWvDyx-icS24EN9txLwzAg.png" /></figure><p>5. สร้าง Firestore database ใหม่ โดยการคลิกปุ่ม Create Firestore database ทำการกำหนด database-id ตั้งชื่อตามใจเราเลย แล้วเลือก region เป็น asia-southeast3 (Bangkok)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/938/1*NfKzHgsn6fd_TojIH7tO3g.png" /></figure><p>6. สร้าง database เสร็จแล้ว ก็จะได้ database มา 2 database แล้วค้าบ ลองตรวจสอบ region ให้เรียบร้อยว่าถูกต้องแล้ว</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7NR6JxgFz42XTy0ohcApcQ.png" /></figure><p>7. ต่อมาให้เราคลิกเลือกที่ database ตัวหลักที่เราต้องการ export data ออกมา ในที่นี้คือ database (default)</p><p>8. เมื่อเข้ามาที่ database ตัวหลักแล้วให้เข้าไปที่เมนู Import/Export</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/945/1*rvPgPqT2VgL9s04xUA6L9w.png" /></figure><p>9. คลิกที่ปุ่ม Export แล้วทำการเลือก Bucket ที่เราสร้างไว้ในขั้นตอนที่ 2 แล้วคลิกปุ่ม Select แล้วคลิก Export เพื่อเริ่มทำงานได้เลย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Fnfgsfufg914cWoOYlyFKw.png" /></figure><p>10. เมื่อ export เสร็จเรียบร้อยแล้ว ก็จะมีรายการขึ้นมาให้เห็นบนหน้า Import/Export ครับ โดยจะมีการบอกด้วยว่า export นี้มี Collection อะไรบ้าง ในที่นี้คือ All Collections</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rzRxxZu-j0jFQnkWID4-_A.png" /></figure><p>11. จากนั้นให้เรากลับไปที่ All database แล้วเข้าไปที่ database ตัวที่เราสร้างใหม่ ที่อยู่ region asia-southeast3 จะพบ database เปล่าๆ แบบนี้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i5of0Ndhtz3HEVazWQzhQw.png" /></figure><p>12. ให้คลิกเข้าไปที่เมนู Import/Export แล้วคลิกที่ปุ่ม Import</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/951/1*nOHnwbIMeW2Cos4TXN0_LQ.png" /></figure><p>13. กดปุ่ม Browse แล้วทำการเลือกไฟล์ที่เรา export ไว้จากขั้นตอนก่อนหน้า แล้วทำการกดปุ่ม Select แล้ว กดปุ่ม Import เพื่อเริ่ม Import database</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VuoBhfQRkGiT4k-BWa3HdA.png" /></figure><p>14. เมื่อ import เสร็จแล้วก็จะขึ้นหน้าตาแบบนี้ครับ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ypNyjS-CyZLxPM415-ZlwA.png" /></figure><p>15. ทดสอบเข้าที่เมนู Firestore Studio ก็จะพบข้อมูลทั้งหมดได้ถูก import เรียบร้อยแล้ววว \(^o^)/</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*I0f2ScsCowakrDPcomAi1A.png" /></figure><p>เพียงเท่านี้ก็สามารถย้าย <strong>Firestore Database</strong> มาใช้งานใน <strong>Region ใหม่ (Bangkok)</strong> ได้เรียบร้อยแล้วครับ<br>ขั้นตอนไม่ได้ยุ่งยากอย่างที่หลายคนคิด แค่เตรียม <strong>Cloud Storage Bucket</strong> สำหรับรองรับไฟล์ จากนั้นทำการ <strong>export และ import ข้อมูล</strong> ก็จบกระบวนการย้ายได้เลย</p><p>ขอฝากข้อมูลเพิ่มเติมเรื่อง <strong>ค่าใช้จ่าย</strong> นิดหนึ่งนะครับ<br>ในการ import/export ข้อมูล Google จะคิดค่าใช้จ่ายตาม <strong>rates เดียวกับการอ่านและเขียนเอกสาร (document reads &amp; writes)</strong> ดังนั้นก่อนย้าย แนะนำให้ประเมินปริมาณข้อมูลคร่าวๆ เพื่อคุมงบประมาณให้เหมาะสม</p><p>หวังว่าบทความนี้จะช่วยให้การย้าย Firestore มาใช้ <strong>Bangkok Region</strong> เป็นเรื่องที่ง่ายและตัดสินใจได้เร็วขึ้น ขอบคุณมากครับ 🙏</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6e38959776bf" width="1" height="1" alt=""><hr><p><a href="https://medium.com/firebasethailand/firestore-%E0%B9%80%E0%B8%9B%E0%B8%B4%E0%B8%94-region-%E0%B9%83%E0%B8%AB%E0%B8%A1%E0%B9%88-bangkok-%E0%B9%81%E0%B8%A5%E0%B9%89%E0%B8%A7-%E0%B8%96%E0%B8%B6%E0%B8%87%E0%B9%80%E0%B8%A7%E0%B8%A5%E0%B8%B2%E0%B8%A2%E0%B9%89%E0%B8%B2%E0%B8%A2%E0%B8%A3%E0%B8%B0%E0%B8%9A%E0%B8%9A%E0%B8%A1%E0%B8%B2%E0%B9%83%E0%B8%81%E0%B8%A5%E0%B9%89%E0%B8%9C%E0%B8%B9%E0%B9%89%E0%B9%83%E0%B8%8A%E0%B9%89%E0%B9%84%E0%B8%97%E0%B8%A2-6e38959776bf">Firestore เปิด Region ใหม่ Bangkok แล้ว! ถึงเวลาย้ายระบบมาใกล้ผู้ใช้ไทย</a> was originally published in <a href="https://medium.com/firebasethailand">Firebase Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Tuning Firebase Storage ให้เร็วขึ้น ⚡ และค่าใช้จ่ายลดลง ]]></title>
            <link>https://medium.com/firebasethailand/tuning-firebase-storage-%E0%B9%83%E0%B8%AB%E0%B9%89%E0%B9%80%E0%B8%A3%E0%B9%87%E0%B8%A7%E0%B8%82%E0%B8%B6%E0%B9%89%E0%B8%99-%E0%B9%81%E0%B8%A5%E0%B8%B0%E0%B8%84%E0%B9%88%E0%B8%B2%E0%B9%83%E0%B8%8A%E0%B9%89%E0%B8%88%E0%B9%88%E0%B8%B2%E0%B8%A2%E0%B8%A5%E0%B8%94%E0%B8%A5%E0%B8%87-29d053a85da2?source=rss----261beda53e69---4</link>
            <guid isPermaLink="false">https://medium.com/p/29d053a85da2</guid>
            <category><![CDATA[cache]]></category>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[storage]]></category>
            <dc:creator><![CDATA[PamornT]]></dc:creator>
            <pubDate>Mon, 19 Jan 2026 07:37:04 GMT</pubDate>
            <atom:updated>2026-01-19T07:37:02.959Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ga2lTT2b0M9fYYIT3R2JVA.png" /></figure><p>เมื่อเดือนก่อนจู่ๆ ก็พบว่าค่าใช้จ่าย <strong>Google Cloud เพิ่มขึ้นจากเดือนก่อนค่อนข้างมาก </strong>ทั้งที่ feature ใหม่ก็ไม่ได้ปล่อย ระบบก็ไม่ได้ scale ผู้ใช้แบบก้าวกระโดด</p><p>พอเปิด <strong>Billing Report</strong> ดูแบบจริงจัง ก็เริ่มเห็นตัวเลขแปลกๆ โผล่ขึ้นมาในหมวด <strong>Cloud Storage</strong></p><p>ไล่ breakdown ต่อไปเรื่อยๆ จนสุดท้ายเจอต้นตอว่า…</p><blockquote>ค่าใช้จ่ายส่วนใหญ่เกิดจาก <strong>Traffic การเรียกไฟล์ Video</strong><br> ที่ User upload ไว้บน <strong>Firebase Storage</strong> <em>😫</em></blockquote><p>ทั้งที่ video เหล่านี้:</p><ul><li>ไม่ได้เปลี่ยนไฟล์</li><li>ไม่ได้ถูกแก้ไข</li><li>แต่ถูกเปิดดูซ้ำๆ จากหลายหน้า หลาย session</li></ul><p>และนี่แหละครับ คือที่มาของบทความนี้ เพื่อมาเล่าสิ่งที่ผมเจอ + สิ่งที่แก้ เผื่อใครกำลังเจอสถานการณ์คล้ายๆ กันจะได้ไม่ต้องเสียค่าเรียนเหมือนเรา 😅</p><h3>มาทำความเข้าใจก่อนว่า ค่าใช้จ่ายของ Cloud Storage มาจากไหน</h3><p>หลายคน (รวมถึงผมในตอนแรก) มักจะเข้าใจว่า <strong>Cloud Storage = ค่าฝากไฟล์<br></strong>แต่จริงๆ แล้ว ค่าใช้จ่ายหลักๆ ของ Cloud Storage มีอยู่ <strong>3 ส่วน</strong></p><h3>1. Storage (ค่าฝากไฟล์)</h3><p>อันนี้เข้าใจง่ายที่สุด</p><ul><li>เก็บไฟล์กี่ GB</li><li>เก็บนานแค่ไหน</li></ul><p>ส่วนนี้มัก <strong>ไม่ใช่ตัวปัญหา </strong>เพราะไฟล์ video ต่อให้หลาย GB ถ้า user ไม่เยอะ ก็ยังไม่แรงมาก</p><h3>2. Operations (ค่าการอ่าน / เขียน)</h3><p>เช่น:</p><ul><li>Upload</li><li>Download</li><li>List file</li></ul><p>โดยปกติค่าใช้จ่ายส่วนนี้ก็ยังไม่สูงมาก ถ้าเทียบกับสิ่งถัดไป 👇</p><h3>3. Network Data Transfer (ตัวร้ายเงียบ 😈)</h3><p>นี่แหละครับ… ตัวที่ทำให้บิลพุ่งแบบไม่รู้ตัว</p><p>เพราะทุกครั้งที่ User เรียกไฟล์จากหน้าเว็บ จะเกิด:</p><blockquote>Data transfer ออกจาก Cloud โดย มัน คิดเงินตามปริมาณข้อมูลที่ส่งออกไป</blockquote><p>โดยสิ่งที่สำคัญคือมัน</p><p>⚠️ <strong>ไม่สนใจว่าไฟล์นั้นจะเคยโหลดไปแล้วหรือยัง</strong><br>⚠️ <strong>ไม่สนใจว่าเป็นไฟล์เดิมหรือไฟล์ใหม่</strong></p><p>เรียกได้ว่า โหลด = จ่าย, refresh ที ก็จ่ายที ไม่จบไม่สิ้น 😢</p><p>ลองนึกภาพง่ายๆ ถ้าเรามี:</p><ul><li>Video ขนาด 20 MB</li><li>User เปิดดู 1 video</li><li>เปิดซ้ำ 5 ครั้ง</li></ul><p>เท่ากับ โหลด ไป = 20MB x 5 = 100MB นี่แค่ 1 user ถ้าเป็น 1,000 user จะเท่ากับ 100,000MB หรือ 100GB กันเลยทีเดียว<br>โดยค่าใช้จ่ายในส่วน data transfer จะประมาณ $0.08–$0.12 / GB ถ้าเอา 100 GB มาคำนวณ จะเสียค่าใช้จ่ายประมาณ $8 เลยทีเดียว</p><blockquote>การใช้งานจริงจะมากกว่า 1,000 user ไปอีกเยอะ ทำให้ค่าใช้จ่ายบานตะไท <em>🤢</em></blockquote><h3>เริ่ม Tuning Firebase Storage</h3><p>โดยโจทย์ของผมคือ “User ยังต้องสามารถเรียกดูไฟล์ Video ใน Storage ซ้ำๆ ได้” และ “ยังไม่อยากรื้อระบบ” เพราะจะกระทบกับหลาย service</p><p>ซึ่งพอเราดูแล้ว “ปัญหาเกิดจาก User เรียกดูไฟล์ Video เดิมๆ และ ถูกเปิดซ้ำจากหลายหน้า” ถ้าเราให้ User Cache ไฟล์ Video ไว้ที่ Client เอง จะแก้ปัญหาตรงนี้ไปได้เลย เราจึงเลือกทำในสิ่งที่เรียกว่า Cache-Control</p><h3>Cache-Control</h3><p>Cache-Control คือ HTTP Header ที่ใช้บอก browser, CDN และ proxy ว่า <em>“ไฟล์นี้ cache ได้ไหม cache ได้นานแค่ไหน และต้องโหลดใหม่ตอนไหน</em>” ซึ่งไม่ได้เป็นความรู้ใหม่เลย เป็นสิ่งที่เราเคยทำมานมนานแล้ว แต่เราอาจจะลืมสิ่งนี้ไป <br>และพอไปค้นมาแล้วพบว่า <strong>Firebase Storage สามารถกำหนด Cache Control</strong> ได้หลายวิธีเลย</p><h4>วิธีที่ 1 : ปรับ Metadata บน Google Cloud Console</h4><p>วิธีเป็นการแก้ไขแบบ Manual ทีละไฟล์ วิธีการคือ</p><ol><li>เข้าไปที่ Cloud Storage</li><li>เข้าไปที่ Bucket ที่เก็บไฟล์ที่ต้องการแก้ไข</li><li>คลิกที่เมนู 3 dots ด้านหลังไฟล์ แล้วเลือก Edit metadata</li><li>แก้ไขตรงส่วน Cache-Control โดยโครงสร้างการกำหนดค่าจะเป็นประมาณนี้</li></ol><pre>Cache-Control: public,max-age=31536000,immutable</pre><p>แบ่งเป็นส่วนๆ แบบนี้</p><ol><li>public vs private</li></ol><p>คือการกำหนดสิทธในการ cache ไฟล์นั้นๆ มี 2 ประเภท<br>public : ไฟล์นี้ใช้กับทุกคนเหมือนกัน<br>private: ไฟล์นี้เฉพาะบุคคล เช่น หลังจาก login</p><p>2. max-age — อายุของ cache</p><p>ตัวนี้จะเป็นการกำหนดว่า สามารถใช้ไฟล์จาก cache ได้ โดยไม่ต้องถาม server ใหม่นานขนาดไหน โดยหน่วยที่กำหนดเป็น วินาที เช่น max-age=31536000 ก็คือเป็นนาน 1 ปีเลย</p><p>3. immutable vs must-revalidate</p><p>คือการกำหนดว่า ถ้า max-age หมดอายุแล้ว จะต้องทำอย่างไรต่อ โดย<br><strong>immutable</strong> : คือ การบอก browser ว่าไฟล์นี้ “ไม่มีทางเปลี่ยน” ประเภทนี้จะเหมาะกับไฟล์ที่ไม่เปลี่ยนแน่ๆ หรือ ถ้าเปลี่ยนก็จะเปลี่ยนชื่อไฟล์ไปเลย<br><strong>must-revalidate </strong>: คือ การบอก browser ว่าให้กลับมาโหลดไฟล์ใหม่ที่ server</p><h4>วิธีที่ 2 : ตั้งค่า Cache-Control ตอน Upload</h4><p>วิธีนี้เป็นวิธีที่แนะนำที่สุด เพราะกำหนดตั้งแต่ตอน Upload ไฟล์เลย เราสามารถกำหนดได้เลยว่าไฟล์ประเภทไหนควรจะตั้ง Cache Control แบบไหน โดยใส่ metadata ตอน upload file ได้เลยครับ ตัวอย่าง</p><pre>await bucket.upload(filePath, {<br>  destination: destinationPath,<br>  metadata: {<br>    contentType: &#39;video/mp4&#39;,<br>    cacheControl: &#39;public, max-age=86400&#39;<br>  }<br>})</pre><h4>วิธีที่ 3: แก้ไข Cache-Control ด้วย gsutil</h4><p>วิธีนี้จะเหมาะกับกรณีที่เรามีไฟล์เก่าๆ อยู่เยอะ และต้องการแก้ไขในทีเดียว โดยไม่กระทบไฟล์ และไม่ต้องอัปโหลดใหม่</p><p>โดยเจ้า gsutil คือ CLI สำหรับจัดการ Google Cloud Storage นั่นเองครับ โดยให้เราเปิด shell ใน Google Cloud Console แล้วใช้คำสั่งประมาณนี้ครับ</p><pre>gsutil -m setmeta \<br>  -h &quot;Cache-Control:public,max-age=86400&quot; \<br>  gs://your-bucket/videos/**</pre><p>อธิบาย:</p><ul><li>-m → ทำงานแบบ parallel (เร็วขึ้น)</li><li>setmeta → แก้ metadata</li><li>Cache-Control → header ที่ต้องการ</li><li>gs://your-bucket/videos/** → ครอบคลุมไฟล์ทั้งหมดใน path videos ใน bucket นี้</li></ul><h3>ส่งท้าย</h3><p>การใช้ Cloud ทำให้เราสร้างระบบได้เร็ว scale ได้ง่าย และไม่ต้องปวดหัวกับ infrastructure เหมือนสมัยก่อน แต่สิ่งหนึ่งที่ต้องระวังเสมอคือ <strong>ค่าใช้จ่าย </strong>เพราะ Cloud ส่วนใหญ่ คิดเงินตามการใช้งานจริง</p><p>หวังว่าบทความนี้จะช่วยให้ใครที่ใช้ Firebase Storage ลองกลับไปดูระบบของตัวเอง แล้วลดค่าใช้จ่ายที่ไม่จำเป็นลงได้บ้างนะครับ 🙂</p><p>ขอบคุณที่อ่านจนจบครับ 🙏</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=29d053a85da2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/firebasethailand/tuning-firebase-storage-%E0%B9%83%E0%B8%AB%E0%B9%89%E0%B9%80%E0%B8%A3%E0%B9%87%E0%B8%A7%E0%B8%82%E0%B8%B6%E0%B9%89%E0%B8%99-%E0%B9%81%E0%B8%A5%E0%B8%B0%E0%B8%84%E0%B9%88%E0%B8%B2%E0%B9%83%E0%B8%8A%E0%B9%89%E0%B8%88%E0%B9%88%E0%B8%B2%E0%B8%A2%E0%B8%A5%E0%B8%94%E0%B8%A5%E0%B8%87-29d053a85da2">Tuning Firebase Storage ให้เร็วขึ้น ⚡ และค่าใช้จ่ายลดลง 💸</a> was originally published in <a href="https://medium.com/firebasethailand">Firebase Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ทำ Vector similarity search กับ Firebase Data Connect และ Firebase Genkit]]></title>
            <link>https://medium.com/firebasethailand/%E0%B8%97%E0%B8%B3-vector-similarity-search-%E0%B8%81%E0%B8%B1%E0%B8%9A-firebase-data-connect-%E0%B9%81%E0%B8%A5%E0%B8%B0-firebase-genkit-9d3364e81cea?source=rss----261beda53e69---4</link>
            <guid isPermaLink="false">https://medium.com/p/9d3364e81cea</guid>
            <category><![CDATA[firebase-genkit]]></category>
            <category><![CDATA[firebase-database]]></category>
            <category><![CDATA[genkit]]></category>
            <category><![CDATA[firebase]]></category>
            <dc:creator><![CDATA[Nui Somjin]]></dc:creator>
            <pubDate>Tue, 03 Dec 2024 15:33:50 GMT</pubDate>
            <atom:updated>2024-12-03T15:33:50.394Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jgWDzv_-WdrESPMqJ5G8RQ.png" /></figure><p><a href="https://medium.com/firebasethailand/firebase-data-connect-%E0%B8%9A%E0%B8%A3%E0%B8%B4%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%97%E0%B8%B5%E0%B9%88%E0%B8%97%E0%B8%B3%E0%B9%83%E0%B8%AB%E0%B9%89-firebase-%E0%B9%80%E0%B8%8A%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%A1%E0%B8%95%E0%B9%88%E0%B8%AD%E0%B8%81%E0%B8%B1%E0%B8%9A-postgressql-%E0%B9%84%E0%B8%94%E0%B9%89%E0%B9%81%E0%B8%9A%E0%B8%9A%E0%B8%87%E0%B9%88%E0%B8%B2%E0%B8%A2%E0%B9%86-43a3b0ff6141">Firebase Data Connect บริการที่ทำให้ Firebase เชื่อมต่อกับ PostgresSQL ได้แบบง่ายๆ</a></p><p>จากบทความที่แล้ว เราได้พูดถึง Firebase Data Connect บริการที่รองรับ Relational Database ตัวแรกของ Firebase โดยเบื้องหลังคือ PostgresSQL บน GCP Cloud บทความนี้เราจะมาเจาะลึกในความสามารถอีกอย่างที่ Firebase Data Connect คิดมาให้แล้ว สำหรับการทำงานกับแอปในยุค AI Era</p><h3>เข้าใจ Vector embeddings</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Q_tjFQnHLQG390kIggUt0Q.png" /></figure><p><strong>Vector</strong> คือ ชุดตัวเลขที่เรียงลำดับกัน ที่เราน่าจะรู้จักกันดีคือ พิกัดทางภูมิศาสตร์ ซึ่งจะเป็นเวกเตอร์ 2 มิติ ที่ประกอบด้วย lattitude, longitude เช่น 37.751 , -97.822 แต่เวกเตอร์ไม่จำกัดแค่ 2 หรือ 3 มิติ อาจมีหลายสิบ หลายร้อย หรือหลายพันมิติก็ได้</p><p><strong>Embeddings</strong> คือ การแปลงข้อมูลให้เป็นเวกเตอร์ เพื่อให้คอมพิวเตอร์สามารถเข้าใจและประมวลผลได้ เช่น การแทนกาแฟด้วยเวกเตอร์ 3 มิติ โดยแต่ละมิติแทนปริมาณของกาแฟ นม และน้ำตาล ซึ่งอเมริกาโน่ อาจจะมีแค่กาแฟ ไม่มีน้ำตาลและนม 1.0,0.0,0.0 แต่คาราเมลมัคคิอาโต้ มีทั้ง กาแฟ นม น้ำตาล 1.0,0.8,0.5อย่างนี้ก็จะทำให้เราวิเคราะห์และเปรียบเทียบประเภทของกาแฟต่างๆ ได้ง่ายขึ้นนั่นเอง</p><p>หรืออย่างบอร์ดเกม จริงๆอาจจะมีได้หลายมิติ ไม่ว่าจะเป็น ความสนุก ความยาก ระยะเวลาการเล่น จำนวนผู้เล่น เพื่อทำให้เห็นภาพง่ายขึ้นเราจะดู ความสนุก และ ความยาก โดยเราจะนำแต่ละเกมมา plot เป็นกราฟ ซึ่งตำแหน่งของมันก็คือ vector ที่เราเรียกกันนั่นแหละ เหมือนที่เราเรียนเรื่องทฤษฏีกราฟตอนม.ปลายเลย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s4qTspb-Wj12CStDW5nQEQ.png" /></figure><p>ทีนี้เราก็สามารถเอาค่านี้ มาคำนวณว่าเหมือนหรือต่างด้วยคำนวณระยะห่างระหว่างจุดสองจุดบนกราฟ ลองจินตนาการว่า user อยากอยากได้เกมครอบครัวคล้ายๆกับ uno ที่สนุก เล่นไม่ยากมาก ทีนี้เราก็จะเอาค่า vector คำนวณว่าอันไหนใกล้เคียง uno สุด เป็นต้น</p><h3>Vector similarity search</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HbvNV_xK9irARHUEdBtcEQ.png" /></figure><p>การทำ Vector similarity search คือการนำ embeddings มาเทียบกันว่าอันไหนมีความใกล้เคียงกันมากที่สุด โดยเริ่มต้นเลยเราต้องนำข้อมูลของเรา ไม่ว่าจะอยู่ในรูปแบบ text, image, audio, file ไปแปลงเป็น vector embeddings ก่อน แล้วเก็บไว้ใน database ที่รองรับการเก็บข้อมูลในรูปแบบ vector</p><p>พอ user ส่ง query เข้ามา เราก็จะต้องเอา query ที่เป็น text แปลงเป็น vector embeddings เพื่อที่เราจะสามารถนำ vector embeddings นั้นไปเปรียบเทียบกับ vector embeddings ที่เก็บเอาไว้ใน database เพื่อหาข้อมูลที่ใกล้เคียงกัน แล้วโยนผลลัพธ์ กลับออกไปให้ user</p><p>อ่านมาถึงตรงนี้ ทุกคนอาจจะสงสัยว่า<em> แล้วเราจะแปลงข้อมูลที่อยู่ใน Database ของเราให้เป็น vector embeddings ได้ยังไง? พอแปลงแล้วเราจะเอาข้อมูลไปเก็บไว้ที่ไหนได้บ้าง? </em>บอกเลย<em> </em>Firebase Data Connect คิดมาให้แล้ว~</p><h3>ทำงานกับข้อมูลแบบ vector ใน Firebase Data Connect</h3><p>กลับไปที่แอปบอร์ดเกมของเรา สมมุติว่าเราจะเพิ่มฟีเจอร์สำหรับค้นหาแบบ vector similarity search กระบวนท่าจะถูกแบ่งเป็น 4 ขั้นตอน ดังนี้</p><ol><li>เพิ่ม schema ที่ทำให้รองรับการเก็บข้อมูลแบบ Vector</li><li>เพิ่ม mutation ในการเขียนข้อมูลที่เป็น Vector</li><li>เขียน query ให้รองรับการค้นหาด้วย Vector</li></ol><h4>1. เพิ่ม schema ที่ทำให้รองรับการเก็บข้อมูลแบบ Vector</h4><p>ข้อดีของ PostgresSQL คือ มันสามารถเก็บข้อมูล Vector ได้ โดยเราสามารถกำหนดชนิดของข้อมูลเป็น Vectorในไฟล์ schema ได้เลย</p><pre># ไฟล์ schema.gql<br>type Game @table {<br>  title: String!<br>  description: String!<br>  descriptionEmbedding: Vector<br>}</pre><h4>2. เพิ่ม mutation ในการเขียนข้อมูลที่เป็น Vector</h4><p>หลังจากกำหนด schema ก็ได้เวลาเขียนมูลลงใน database</p><p><strong>2.1 มี embedding model ที่ใช้อยู่แล้ว</strong></p><p>กรณีที่เราอาจจะมี embedding model ที่ใช้อยู่แล้ว เช่น เราอาจจะใช้ OpenAIs text embedding models หรือ model ที่บริษัททำเอง เราสามารถแปลงข้อมูลให้เป็น vector embeddings ให้เรียบร้อย แล้วเขียนไฟล์ mutation เพื่อโยน embeddings ไปเก็บไว้ใน database เฉยๆก็ได้ ตามโค้ดตัวอย่างด้านล่าง</p><pre>mutation CreateNewGame(<br>  $title: String!, <br>  $description: String!, <br>  $descriptionEmbedding: Vector!<br>) {<br>  game_insert(data: {<br>    title: $title,<br>    description: $description,<br>    descriptionEmbedding: $descriptionEmbedding<br>  })<br>}</pre><p><strong>2.2 ใช้ Vertex AI vector embeddings จาก Firebase Data Connect</strong></p><p>ถ้าไม่อยากจะไปเรียก API เพื่อทำ embedding เอง Firebase ก็มี wrapper function ที่ช่วยให้เราทำ embedding ได้แบบง่ายๆ โดยเราสามารถใช้คำสั่ง _embed และกำหนด embedding model ที่ต้องการ เพื่อให้มันสร้าง embedding ให้แบบอัตโนมัติ ซึ่งตัว model ที่รองรับก็จะเป็น <a href="https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-text-embeddings#supported-models">embedding models จาก VertexAI ทั้งหมด</a></p><pre>mutation CreateNewGame(<br>  $title: String!, <br>  $description: String!, <br>  $descriptionEmbedding: Vector!<br>) {<br>  game_insert(data: {<br>    title: $title,<br>    description: $description,<br>    descriptionEmbedding_embed: {<br>      model: &quot;textembedding-gecko@003&quot;<br>      text: $description<br>    }<br>  })<br>}</pre><p>ในตัวอย่างเราทำการ embed ตัว description ที่เป็น text ให้เป็น embeddings ด้วย textembedding-gecko@003</p><h4>3. เขียน query ให้รองรับการค้นหาด้วย Vector</h4><p>หลังจากทำการเก็บข้อมูล vector แล้ว เรามาเขียน query เพื่อทำการค้นหาแบบ vector similarity search กัน</p><pre>query SearchSimilarContent($query: String!, $limit: Int = 3)<br>@auth(level: PUBLIC) {<br>  games_descriptionEmbedding_similarity(<br>    compare_embed: {<br>      model: &quot;textembedding-gecko-multilingual@001&quot;<br>      text: $query<br>    }<br>    method: L2<br>    within: 2<br>    limit: $limit<br>  ) {<br>    ...DisplayGame<br>  }<br>}</pre><p>Firebase Data Connect จะสร้างฟังก์ชั่น similarity สำหรับทุกๆ field ที่เป็น vector โดยเราสามารถใช้ฟังก์ชั่นนี้ในการทำ similarity search ได้เลย เช่น games_descriptionEmbedding_similarity</p><p>ในส่วนของการแปลง query ที่ user พิมพ์เข้ามาให้เป็น embeddings เราสามารถเรียก compare_embed เพื่อให้ Firebase Data Connect แปลง query string เป็น embeddings ให้เราได้เลย โดยเราสามารถเลือก model จาก VertexAI ได้เช่นกัน ในที่นี้เราใช้ textembedding-gecko-multilingual@001 เพื่อให้การค้นหาของเรารองรับหลากหลายภาษา (multilingual) ได้</p><p>เราสามารถทำการทดสอบ endpoints ด้วยการใช้ VSCode Extensions (ดูวิธีในบทความที่แล้ว) โดยจะเห็นว่าเราระบุ query เป็นภาษาไทยยาวเป็นกิโล แต่ก็ยังคงได้ผลลัพธ์จาก database ที่เราเก็บไว้เป็นภาษาอังกฤษ ออกมาค่อนข้างตรงทีเดียว ✨</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IInPmlkiP2Hs-zMtz0Tjzw.png" /><figcaption>ผลลัพธ์การค้นหาข้อมูลด้วยข้อความภาษาไทย</figcaption></figure><p>ถือได้ว่า ทำมาครบมาก และสามารถนำไปปรับใช้ได้กับหลากหลาย use cases เลย</p><h3>ต่อกับ Firebase Genkit เพื่อทำ Similarity search ด้วย keyword จากรูปภาพ</h3><p>ใน demo ของเรา เราเพิ่มอีกฟีเจอร์เพื่อให้ user สามารถส่งรูปเข้ามา แล้วแอปจะไปค้นหาเกมที่เหมือนกันให้ ด้วยการใช้ Firebase Genkit เป็นตัวช่วยในการดึง keyword จากรูปออกมา ก่อนนำ keyword ที่ได้ไปค้นจาก database เพื่อหาเกมที่คล้ายกัน</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZpeS4Py2KZKr9goub4Se3A.png" /><figcaption>ตัวอย่างการค้นหาแบบ Similarity search ด้วย keyword จากรูปภาพ ใน demo</figcaption></figure><h4>รู้จัก Firebase Genkit</h4><p><a href="https://firebase.google.com/docs/genkit">Firebase Genkit</a></p><p><strong>Firebase Genkit</strong> เป็นเครื่องมือที่ช่วยให้เราสร้างแอป AI ได้ง่ายขึ้น จุดเด่นของมันคือ การทำงานได้กับ model ที่หลากหลาย ไม่จำกัดแค่ของ Google พร้อมกับเราสามารถกำหนดโครงสร้างข้อมูลได้ เพื่อให้คำตอบที่ได้จาก Generative AI มี structure มากขึ้น</p><h4>ทำ Similarity search ด้วย keyword จากรูปภาพ</h4><p>เหมือนที่บอกไว้ในตอนต้นว่า เราจะส่งรูปไปให้ Gen AI ดึง keyword ออกมาให้ (ผ่าน Firebase Genkit) ก่อนนำ keyword ที่ได้ไปค้นจาก database ของเราเพื่อหาเกมที่คล้ายกัน</p><p><strong>0. ติดตั้ง Firebase Genkit</strong></p><p>ก่อนอื่นเลย เราจะต้องติดตั้ง Firebase Genkit ก่อน ตาม Document นี้</p><p><a href="https://firebase.google.com/docs/genkit/get-started">Get started | Genkit | Firebase</a></p><p>แล้วใช้คำสั่ง เพื่อเพิ่ม Genkit ที่รันบน Cloud Functions for Firebase ลงในโปรเจคของเรา</p><pre>firebase init genkit</pre><p>หลังจากที่เราติดตั้งเรียบร้อย เราจะได้โฟลเดอร์ functions มา แบบนี้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Zk7_bR1asdt_Lxlca5c-SQ.png" /></figure><p>โดยจะมี source code ตัวอย่าง ในการใช้งานเบื้องต้นทั้งการ configGenkit รวมไปถึงตัวอย่าง flow ที่เราสามารถดูตัวอย่างการเขียนและลองทดสอบการทำงานเบื้องต้นได้</p><p><strong>Firebase Genkit</strong> ยังมี <a href="https://firebase.google.com/docs/genkit/devtools">Developer Tools</a> สำหรับการทดสอบ Flow, Model ต่างๆบน local ได้ด้วย โดยเราสามารถติดตั้งตาม Document นี้</p><p><a href="https://firebase.google.com/docs/genkit/devtools">Firebase Genkit Developer Tools</a></p><p>แล้วใช้คำสั่ง genkit start เพื่อเริ่มใช้งาน Developer UI อย่าลืมโหลด API key ที่จำเป็นก่อนรันด้วยน้า export GOOGLE_GENAI_API_KEY=&lt;your API key&gt; ถ้าเลือกใช้ GoogleAI สามารถกดรับ API key ได้ที่ <a href="https://aistudio.google.com/apikey">https://aistudio.google.com/apikey</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3XTnpESi22oV4A9BjfF_lw.png" /><figcaption>หน้าตา Firebase Genkit Developer Tools</figcaption></figure><ol><li><strong>ทำ Flow ในการดึง keyword ออกมาจากรูป</strong></li></ol><p>ติดตั้งเครื่องมือที่จำเป็นเสร็จแล้ว เรามาเริ่มเขียน flow สำหรับ ดึง keyword จากรูปกัน การสร้าง flow ด้วย Firebase Genkit</p><pre>export const getKeywordFromPictureFlow = onFlow(<br>  {<br>    // Configuration ต่างๆของ Flow นั้น<br>  },<br>  async (input) =&gt; {<br>    // step การทำงานของ Flow เช่น<br>    // 1. เช็คว่ามีข้อมูล<br>    // 2. ส่งข้อมูลให้ GenAI ดึง keyword ออกมา<br>    // 3. ส่งผลลัพธ์กลับออกไปเป็น response<br>  }<br>);</pre><p>ส่วนประกอบของ Flow จะมีอยู่ 2 ส่วนด้วยกัน คือ</p><ul><li><strong>Configuration ต่างๆของ flow นั้น</strong> ที่ประกอบด้วย ชื่อ flow, schema ของ input/output, auth policy (Firebase Genkit ทำงานร่วมกับ Firebase Authentication ได้เป็นอย่างดี สามารถเช็คสิทธิ์การเข้าถึงแต่ละ flow ได้)</li></ul><pre>export const getKeywordFromPictureFlow = onFlow(<br>  {<br>    name: &quot;getKeywordFromPictureFlow&quot;,<br>    inputSchema: z.string(), // กำหนดให้รับ input มาเป็น string<br>    outputSchema: z.object({ // กำหนดว่า output ที่ได้จะอยู่ในรูปแบบของ {keywords: [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;]}<br>      keywords: z.array(z.string()),<br>    }),<br>    // ให้ User ที่ login แล้วเท่านั้นใช้งานได้ เพื่อจำกัดการใช้งาน GenAI ที่อาจมีค่าใช้จ่าย 💸💸 (หรือเงื่อนไขอื่นๆตามต้องการ) <br>    authPolicy: firebaseAuth((user) =&gt; {}), <br>    httpsOptions: {<br>      cors: true,<br>    },<br>  },<br>...</pre><ul><li><strong>Step การทำงานของ flow </strong>เหมือนการเขียน functions ทั่วไป เพื่อระบุว่าใน flow นี้มีการทำงานอะไรบ้าง</li></ul><pre>...<br>  async (input) =&gt; {<br>    // ส่งข้อมูล Input ให้ GenAI ดึง keyword ออกมา<br>    const llmResponse = await generate({<br>      // ระบุ model ที่ต้องการใช้งาน จะเป็น claude3Sonnet, llama31, gpt4o, ollama ก็ได้<br>      model: gemini15Flash, // เราเลือกใช้ gemini15Flash เพราะเป็น multimodal ที่ใช้งานกับรูปได้<br>      output: { <br>        format: &quot;json&quot;, // บอกว่าอยากได้ structure data ในรูปแบบของ json<br>        schema: z.object({ // กำหนด structure ของ output ที่ต้องการ<br>          keywords: z.array(z.string()),<br>        }),<br>      },<br>      // กำหนด prompt<br>      prompt: [<br>        // โยน input เข้าไปเป็น media<br>        { media: { url: input } }, <br>        // prompt ที่บอกให้ GenAI ดึง keyword ออกมาจากรูปให้<br>        // พยายามลดคำซ้ำซ้อน คำสร้อย เพื่อให้ได้ keyword ที่ตรงประเด็น<br>        // พร้อมกับ brief มันอีกรอบว่าให้ส่งกลับมาเป็น array<br>        {<br>          text: `<br>            From the picture suggest 3 keywords or theme, avoid the word &#39;game&#39;, &#39;board&#39; or other decorative words. <br>            Return the keywords as an array list. e.g. [&#39;strategy&#39;, &#39;cooperative&#39;, &#39;worker placement&#39;]<br>          `,<br>        },<br>      ],<br>      config: {<br>        temperature: 1,<br>      },<br>    });<br>    return llmResponse.output() as { keywords: string[] };<br>  }<br>);</pre><p>การใช้คำสั่ง onFlow เป็นการระบุว่า Flow นี้จะถูก <a href="https://firebase.google.com/docs/genkit/flows#cloud_functions_for_firebase">Deploy flow ด้วย Cloud Functions for Firebase</a> เหมือนเป็น functions อันนึงใน Cloud functions</p><p>ลองทดสอบ flow ใน Developer tools ก่อนนำไปใช้งานกับ UI โดยเราส่ง link ของรูปเกม MouseTrap เกมโปรดของเด็กที่บ้าน เข้าไปเป็น input และได้ output ออกมาในรูปแบบ object ตามที่กำหนดไว้เลย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*T1cZGtJA6RulaVcOktrACw.png" /></figure><p><strong>2. เรียกใช้ใน client</strong></p><p>เพื่อให้การทดสอบบน local ราบรื่น เราเพิ่ม functions เข้าไปใน config ของเราก่อน พร้อมกับเชื่อมต่อไปที่ Function emulators</p><pre>/* ไฟล์ lib/firebase.ts */<br><br>export const firebaseApp = initializeApp({<br>  // your config from Firebase console<br>})<br><br>const functions = getFunctions(firebaseApp)<br><br>if (import.meta.env.DEV) {<br>  connectFunctionsEmulator(functions, &#39;localhost&#39;, 5003)<br>  // ... บริการอื่นๆที่ใช้<br>}<br>export { functions }</pre><p>จากนั้นทำการเรียกใช้ function ด้วย httpsCallable เหมือนการเรียกใช้ callable functions ทั่วๆไปจาก Cloud Functions for Firebase เลย</p><pre>export const extractKeywordsFromImage = async (<br>  input: string,<br>): Promise&lt;{ keywords: string[] }&gt; =&gt; {<br>  try {<br>    const getKeywords = httpsCallable(functions, &#39;getKeywordFromPictureFlow&#39;)<br>    const keywords = await getKeywords(input)<br>    return keywords.data as { keywords: string[] }<br>  } catch (error) {<br>    throw error<br>  }<br>}</pre><p>นำไปประกอบร่างกับ UI logic</p><pre>const searchByImage = async (imageData?: string) =&gt; {<br>    if (!imageData) return<br>    isPending.value = true<br>    try {<br><br>      // Get keywords จากรูปภาพด้วย Genkit<br>      const keywordsFromImage = (await extractKeywordsFromImage(imageData)) as {<br>        keywords: string[]<br>      }<br><br>      // นำ keyword มาต่อกันเป็น string<br>      const imgQuery = keywordsFromImage.keywords.join(&#39; &#39;)<br><br>      // ส่งไป query กับ Firebase Data Connect<br>      const response = await searchSimilarContent({<br>        query: imgQuery,<br>        limit: 10,<br>      })<br><br>      // นำค่าที่ได้ไปใช้งานใน UI<br>      games.value = response.data<br>        .games_descriptionEmbedding_similarity as Game[]<br>    } catch (err) {<br>      console.error(&#39;searchByImage&#39;, error)<br>      if (err instanceof Error) error.value = err<br>    } finally {<br>      isPending.value = false<br>    }<br>  }</pre><p>เพียงเท่านี้ก็ได้ฟีเจอร์ค้นหาแบบ Similarity search ด้วย keyword จากรูปภาพมาใช้งานแล้ว 🎉</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZpeS4Py2KZKr9goub4Se3A.png" /></figure><h4>ส่งท้าย</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*McMNlMfRTrAgBmTT4zn7lA.jpeg" /></figure><p>เนื้อหาในบทความนี้มาจากสิ่งที่เราพูดไปในงาน Google DevFest Bangkok 2024 หัวข้อ Build better AI-driven Cloud SQL apps with Firebase Data Connect สิ่งที่ยกตัวอย่างมาทั้งหมดเป็นเพียงแค่ 1 ในหลาย use cases ที่เราสามารถนำ Firebase Data Connect และ Firebase Genkit บริการใหม่ทั้ง 2 ตัวจาก Firebase ไปเป็นตัวช่วยในการเพิ่มฟีเจอร์ AI ให้แอปพลิเคชันของเรา แบบสะดวกและง่ายที่สุด ใครลองใช้แล้ว อย่าลืมมาแบ่งปันให้กันฟังที่ <a href="https://www.facebook.com/groups/FirebaseDevTh/">Firebase Developer Group Thailand</a> บ้างนะคะ</p><p>อ่านมาถึงตรงนี้ ฝากกด <em>Clap</em> กด <em>Share </em>หรือกดติดตาม <a href="https://medium.com/firebasethailand">Firebase Thailand</a> เพื่อไม่ให้พลาดบทความต่อๆไปด้วยนะคะ สำหรับวันนี้ ขอบคุณและสวัสดีค่ะ 🙏</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9d3364e81cea" width="1" height="1" alt=""><hr><p><a href="https://medium.com/firebasethailand/%E0%B8%97%E0%B8%B3-vector-similarity-search-%E0%B8%81%E0%B8%B1%E0%B8%9A-firebase-data-connect-%E0%B9%81%E0%B8%A5%E0%B8%B0-firebase-genkit-9d3364e81cea">ทำ Vector similarity search กับ Firebase Data Connect และ Firebase Genkit</a> was originally published in <a href="https://medium.com/firebasethailand">Firebase Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Firebase Data Connect บริการที่ทำให้ Firebase เชื่อมต่อกับ PostgresSQL ได้แบบง่ายๆ]]></title>
            <link>https://medium.com/firebasethailand/firebase-data-connect-%E0%B8%9A%E0%B8%A3%E0%B8%B4%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%97%E0%B8%B5%E0%B9%88%E0%B8%97%E0%B8%B3%E0%B9%83%E0%B8%AB%E0%B9%89-firebase-%E0%B9%80%E0%B8%8A%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%A1%E0%B8%95%E0%B9%88%E0%B8%AD%E0%B8%81%E0%B8%B1%E0%B8%9A-postgressql-%E0%B9%84%E0%B8%94%E0%B9%89%E0%B9%81%E0%B8%9A%E0%B8%9A%E0%B8%87%E0%B9%88%E0%B8%B2%E0%B8%A2%E0%B9%86-43a3b0ff6141?source=rss----261beda53e69---4</link>
            <guid isPermaLink="false">https://medium.com/p/43a3b0ff6141</guid>
            <category><![CDATA[graphql]]></category>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[cloud-sql]]></category>
            <category><![CDATA[firebase-database]]></category>
            <dc:creator><![CDATA[Nui Somjin]]></dc:creator>
            <pubDate>Wed, 27 Nov 2024 17:08:40 GMT</pubDate>
            <atom:updated>2024-12-03T10:50:14.018Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FbhwwrBBfdHX30QXXgI2nw.png" /></figure><p>หลายคนคงคุ้นเคยกับ Firebase ในฐานะบริการ Backend as a Service ที่โดดเด่นด้วย NoSQL database อย่าง Realtime database ที่เปิดตัวมาตั้งแต่ปี 2012 และต่อยอดด้วย Cloud Firestore ในปี 2017 แต่ทั้งสองบริการยังมีข้อจำกัดในการจัดการ Query ที่ซับซ้อน เรามักจะได้ยินเสียงเรียกร้องจาก Firebase community อยู่เสมอว่า เมื่อไหร่จะมี SQL database สักที จะได้แก้ปัญหาเรื่องข้อมูลซ้ำซ้อนและทำ complex query ได้</p><p>ในที่สุดเมื่องาน Google I/O ที่ผ่านมา Firebase ก็เปิดตัว Firebase Data Connect ถือว่าเป็นบริการตัวแรกที่รองรับ Relational database ของ Firebase เลย 🎉</p><h3>การทำงานของ Firebase Data Connect</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bLTQGzXCFGGTVuXvk-_1VQ.png" /><figcaption>App data flow</figcaption></figure><p>จะบอกว่า Firebase Data Connect เป็น database ก็ไม่ถูกซะทีเดียว เพราะจริงๆแล้วเบื้องหลังของ Firebase Data Connect ก็คือ PostgresSQL ที่รันอยู่บน GCP Cloud โดยเจ้า Firebase Data Connect เป็นเหมือนตัวเชื่อมระหว่าง SQL Database กับ Client ให้เรา ด้วย GraphQL</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*POGsEgp0V0Oti_BvrTe6Xg.png" /><figcaption>ความสามารถของ Firebase Data Connect ที่ช่วยยกระดับให้การทำงานกับ PostgresSQL ง่ายขึ้น</figcaption></figure><p>ทีม Firebase สร้าง Firebase Data Connect ภายใต้คอนเซปที่ว่า <strong><em>“You write the query, we do the rest”</em></strong> นักพัฒนาอย่างเรากำหนดเพียง 2 อย่าง ส่วนที่เหลือ Firebase จะเป็นคนจัดการให้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aOgrNje-L-rAKEMF_EKdrA.png" /><figcaption><strong><em>“You write the query, we do the rest”</em></strong></figcaption></figure><h4>สิ่งที่เรากำหนด (ในรูปแบบของ GraphQL)</h4><ul><li><strong>Data model (schema) </strong>ซึ่งเป็นการกำหนด database schema ว่าต้องมีตารางอะไรบ้าง แต่ละตารางต้องมีข้อมูลอะไร ชนิดไหนบ้าง</li><li><strong>Connector (queries &amp; mutations)</strong> ซึ่งก็คือ การ Create, Read, Update, Delete ข้อมูลใน database นั่นเอง</li></ul><h4>สิ่งที่ Firebase จะสร้างให้แบบอัตโนมัติ</h4><ul><li><strong>PostgresSQL schema</strong> คำสั่งสร้างโครงสร้างฐานข้อมูลใน PostgesSQL และ migration</li><li><strong>Typesafe SDK</strong> เพื่อใช้ใน client application ของเรา โดยรองรับทั้ง iOS, Android, Web และ Flutter</li><li><strong>API Endpoint</strong> เพื่อให้ client สามารถเชื่อมต่อ ทำ CRUD กับ Database ได้ พร้อมกับมี Authorization ที่จะคอยเช็คสิทธิ์ user ในทุกๆ endpoint อีกด้วย เช่น เราอาจจะอยากให้ user ทุกคนเห็นรายการสินค้า แต่เฉพาะ admin เท่านั้นที่เพิ่มรายการสินค้าได้ เป็นต้น</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5Ubdh2wWnIUP6F9xs3t9tw.png" /><figcaption>เรากำหนด Data Model ด้วย GraphQL ➡ Firebase สร้าง SQL schema ให้แบบอัตโนมัติ</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d6ScWLe3emKSO0X4dRPFpg.png" /><figcaption>Queries และ Mutations ที่เรากำหนดผ่าน GraphQL</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ElCnMOpSrvW1PESVNVgM9Q.png" /><figcaption>ตัวอย่าง API Endpoint ที่ Firebase สร้างให้</figcaption></figure><h3>เริ่มต้นใช้งาน Firebase Data Connect</h3><p>กระบวนท่าจะมีอยู่ 6 ขั้นตอนด้วยกัน ดังนี้</p><pre>0. ติดตั้ง<br>1. กำหนด Data model (schema)<br>2. กำหนด Connector (queries, mutations)<br>3. ทดสอบใน local<br>4. เรียกใช้ใน client<br>5. Deploy</pre><h4>0. ติดตั้ง</h4><p>เราสามารถเริ่มสร้าง Data Connect ได้ทั้งใน Firebase Console หรือ ใช้ command line ผ่าน <a href="https://firebase.google.com/docs/cli">Firebase cli</a> ด้วยคำสั่ง</p><pre>firebase init dataconnect</pre><p>ในการติดตั้ง เราต้องกำหนด ID ของ CloudSQL instance, ID ของ database, region ที่ต้องการ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lPQ1JhLV1YYJfkT-S744EQ.png" /><figcaption>init dataconnect ผ่าน command line (init โดยการสร้าง CloudSQL ใหม่ตอนนี้ ได้ใช้ฟรี 3 เดือน)</figcaption></figure><p>อย่างที่ระบุไว้ต้นบทความว่า เบื้องหลังของ Firebase Data Connect เป็น CloudSQL ดังนั้นการสร้างตรงนี้คือการสร้าง CloudSQL instance และ PostgresSQL Databaseโดยโปรเจคของเราจะต้องเป็น <strong><em>Blaze Plan (Pay as you go) </em></strong>เท่านั้น ใครจะใช้อย่าลืมไปเปิด billing กันก่อนด้วยนะคะ</p><p>ในช่วง public preview นี้ ถ้าใครสร้าง database instance ใหม่ ผ่าน Firebase Data Connect ก็จะได้ใช้งาน CloudSQL instance แบบฟรีๆไป 3 เดือนเต็ม (ซึ่งจะเป็นตัว <strong>db-f1- micro</strong> 1 vCPU, 600 MB RAM and 10 GB storage)<em> </em>หลังจากนั้นคิดค่าใช้บริการตามจริง ขึ้นอยู่กับขนาด instance และจำนวนการใช้งาน ดูรายละเอียดราคาแบบเจาะลึกที่ <a href="https://firebase.blog/posts/2024/11/dataconnect-pricing">Data Connect Pricing</a></p><p>หลังจาก setup เสร็จเราจะได้โฟลเดอร์ data connect ที่มีส่วนประกอบ 3 ส่วน</p><ol><li>โฟลเดอร์ connector ซึ่งจะเป็นส่วนที่เราเรียกข้อมูลมาใช้ (Query) หรือแก้ไขข้อมูล (Mutation)</li><li>โฟลเดอร์ schema เป็นส่วนที่เรากำหนด Data model ของข้อมูลใน Database</li><li>ไฟล์ dataconnect.yaml เป็น config เมื่อกี๊ที่เราใส่เข้าไปตอน set up</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ibh_5exyp6AYiWiOjcK_ng.png" /><figcaption>ไฟล์ dataconnect.yaml เก็บข้อมูล config ต่างๆที่เราเชื่อมต่อกับ CloudSQL</figcaption></figure><h4>1. กำหนด Data model (schema)</h4><p>สมมุติว่าเราต้องการทำแอปร้านบอร์ดเกม สิ่งที่เราต้องทำคือกำหนดรูปแบบฐานข้อมูล หน้าตาประมาณนี้ด้วย GraphQL ที่ไฟล์ schema.gqlในโฟลเดอร์ schema</p><pre># schema.gql<br><br># Users<br>type User @table {<br>  id: String! @col(name: &quot;auth_uid&quot;)<br>  username: String! @col(name: &quot;username&quot;, dataType: &quot;varchar(50)&quot;)<br>}<br><br># Games<br>type Game @table {<br>  # The below parameter values are generated by default with @table, and can be edited manually.<br>  # implies directive `@col(name: &quot;game_id&quot;)`, generating a column name<br>  id: UUID! @default(expr: &quot;uuidV4()&quot;)<br>  title: String!<br>  description: String!<br>  imageUrl: String!<br>  genre: String<br>  emoji: String<br>  minAge: Int<br>  minPlayers: Int<br>  maxPlayers: Int<br>  playTime: Int<br>  descriptionEmbedding: Vector @col(size: 768) # Enables vector search<br>}<br><br># Reservations<br>type Reservation<br>  @table(name: &quot;reservations&quot;, key: [&quot;game&quot;, &quot;user&quot;, &quot;timeSlot&quot;]) {<br>  id: UUID! @default(expr: &quot;uuidV4()&quot;)<br>  user: User!<br>  game: Game!<br>  timeSlot: Timestamp!<br>  createAt: Date! @default(expr: &quot;request.time&quot;)<br>}</pre><ul><li>ตาราง user เก็บข้อมูลของผู้ใช้งานนั้นๆเอาไว้</li><li>ตาราง game เก็บข้อมูลและรายละเอียดต่างๆของเกม</li><li>ตาราง reservation เป็นตารางที่ join ข้อมูล Game และ User เอาไว้ด้วยกัน พร้อมกับเวลาที่จอง โดยเราสามารถกำหนด foreign key หลายๆอันได้อย่างที่ระบุใน code snippet ด้านบน</li></ul><h4>2. กำหนด Connector (queries, mutations)</h4><p>เป็นการทำ CRUD (Create, Read, Update, Delete) ไม่ว่าจะเป็นการดึงข้อมูลออกมา หรือแก้ไขข้อมูล จาก database ยกตัวอย่าง ในแอปร้านบอร์ดเกมของเรา เราอาจจะอยากให้มี connector ตามนี้</p><ul><li><strong>User ทุกคนสามารถ ค้นหารายการของบอร์ดเกมด้วย keyword อายุ และ จำนวนผู้เล่น</strong></li></ul><p>คำสั่ง @auth(level: PUBLIC) เป็นการระบุว่า ให้ทุกคนเข้าถึง query นี้ได้โดยไม่ต้องมีการ login ก่อน</p><pre># queries.gql<br><br>query SearchGames($keyword: String!, $minAge: Int, $player: Int)<br>@auth(level: PUBLIC) {<br>  games(<br>    where: {<br>      _and: [<br>        { minAge: { ge: $minAge } }<br>        { minPlayers: { le: $player } }<br>        { maxPlayers: { ge: $player } }<br>        {<br>          _or: [<br>            { title: { contains: $keyword } }<br>            { description: { contains: $keyword } }<br>            { genre: { contains: $keyword } }<br>          ]<br>        }<br>      ]<br>    }<br>  ) {<br>    ...DisplayGame<br>  }<br>}</pre><ul><li><strong>User สามารถดูการจองของตัวเองเท่านั้น (My reservations)</strong></li></ul><p>คำสั่ง @auth(level: USER) เป็นการระบุว่าต้อง login ก่อนเท่านั้นถึงจะเข้าถึง query นี้ได้ ใช้คำสั่ง eq_expr: &quot;auth.uid&quot; ใน where เพื่อใช้ค่า uid จาก auth token ที่ส่งเข้ามา แทนที่จะต้องรับค่า uid เป็น parameters เพื่อความปลอดภัยไปอีกขั้น</p><pre># queries.gql<br><br>query ListMyReservations @auth(level: USER) {<br>  reservations(where: { user: { id: { eq_expr: &quot;auth.uid&quot; } } }) {<br>    id<br>    game {<br>      ...DisplayGame<br>    }<br>    timeSlot<br>  }<br>}</pre><ul><li><strong>Admin เท่านั้น ที่สามารถเพิ่มเกมใหม่ได้</strong></li></ul><p>เราสามารถเขียน expression เพื่อตรวจสอบ token ที่อยู่ใน Auth ได้ด้วย เช่น กรณีนี้ เราอาจจะเก็บค่า isAdmin ไว้ใน token แล้วก็ตรวจสอบว่าค่าต้องเป็น auth.token.isAdmin == true ก่อนจึงจะอนุญาตให้ผู้ใช้มีสิทธิ์ insert ข้อมูลเกมลงใน database</p><pre># mutations.gql<br><br>mutation CreateGame(<br>  $title: String!<br>  $description: String!<br>  ...<br>) @auth(expr: &quot;auth.token.isAdmin == true&quot;) {<br>  game_insert(<br>    data: {<br>      title: $title<br>      description: $description<br>      ...<br>    }<br>  )<br>}</pre><p>จากตัวอย่างด้านบน ทุกคนน่าจะเห็นรูปแบบการเขียน connector แบบคร่าวๆแล้ว โดยสรุปได้ 3 อย่างด้วยกัน</p><ol><li><strong>รับค่า parameters ที่ต้องการ</strong> เช่น minAge ในการ ค้นหาเกม ที่เหมาะกับอายุของผู้เล่น</li><li><strong>กำหนดสิทธิ์ในการเข้าถึงข้อมูล</strong> อย่างที่เห็นในตัวอย่างด้านบน เราสามารถกำหนดการเข้าถึงของแต่ละ endpoint ได้ สามารถเลือกเป็น @auth(level: ...) หรือเขียนเงื่อนไขโดยการใช้ @auth(expr: ...)ถ้าไม่มีการกำหนด @auth(...) จะ default เป็น NO_ACCESS คือไม่ให้ใครเข้าถึงเลย นอกจากสิทธิ์ Admin SDK เท่านั้น ดูรายละเอียดเพิ่มเติมเกี่ยวกับ Auth ได้ที่ <a href="https://firebase.google.com/docs/data-connect/authorization-and-security">Secure Data Connect with authorization and attestation</a></li><li><strong>เขียนคำสั่ง SQL </strong>ไม่ว่าจะเป็น query หรือ mutation ตามรูปแบบ GraphQL ดู Ref คำสั่งได้ที่ <a href="https://firebase.google.com/docs/reference/data-connect">Firebase Data Connect API Reference</a></li></ol><p>นอกจากนี้ ถ้าเรามีคำสั่งที่ค่อนข้างซับซ้อน เขียน GraphQL แล้วอาจจะงง Firebase Data Connect ยังรองรับการเขียน Raw SQL Query ด้วย โดยเราสามารถสร้าง view ตาม query ที่เราต้องการได้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5RIGjKi1K-FmIXOoEJw2Bg.png" /><figcaption>ตัวอย่างการใช้ Raw SQL เพื่อสร้าง view ใหม่ที่มีข้อมูล ReviewInfo (ร้านอาหารและค่าเฉลี่ยรีวิว)</figcaption></figure><h4>3.ทดสอบใน local</h4><p>Firebase Data Connect รองรับการทำงานกับ Emulators ด้วย ทำให้เราสามารถทำทุกอย่างบน local ให้มั่นใจก่อนการ deploy ไป production ได้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PuuqIgHTHfdANNrRTWhjJQ.png" /></figure><p><strong>3.1 Extensions</strong></p><p>สำหรับใครที่ใช้ VSCode หรือ IDX สามารถติดตั้ง extentions เพื่อเพิ่มความสะดวกสบายในการทำงานกับ Data Connect ทั้ง auto complete ของ GraphQL, UI สำหรับการเชื่อมต่อกับ local emulators, UI สำหรับทดสอบ การเชื่อมต่อกับ endpoint ต่างๆ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E9VdsHF23udMHa3PTG0wyg.png" /><figcaption><a href="https://marketplace.visualstudio.com/items?itemName=GoogleCloudTools.firebase-dataconnect-vscode">Firebase Data Connect extensions สำหรับ VSCode</a></figcaption></figure><p>หลังจากที่เราใช้คำสั่ง</p><pre>firebase emulators:start</pre><p>เราจะเห็นว่ามีคำสั่ง Run (local) โผล่ขึ้นมา ซึ่งเราสามารถใช้ tab DATA CONNECT EXECUTION ในการทดสอบการเชื่อมต่อกับ endpoint ที่เราสร้างไว้ได้ โดยกำหนด Parameter variables และ Authentication แล้วกด Run (local) เพื่อทำการทดสอบ endpoint ใน local หรือกด Run (Production) เพื่อเรียก endpoint ใน production เลยนั่นเอง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G2W-g6nchrwglF0U0kabiw.png" /><figcaption>วิธีการใช้ VSCode Extentions ในการทดสอบ endpoint ของ Firebase Data Connect</figcaption></figure><p>เรายังสามารถแก้ไข Authentication ในการทดสอบสิทธิ์เข้าถึงของแต่ละ endpoint ด้วย อย่างเช่น SearchGame นี้เราลองกำหนดเล่นๆว่า @auth(level: USER_EMAIL_VERIFIED) หมายความว่าต้องเป็น user ที่มีการยืนยันอีเมลล์แล้วเท่านั้นจึงจะใช้ endpoint ได้ ในช่วงของการทดสอบ เราไปตั้งค่า Authentication โดยการกำหนด Run As ให้เป็น Authenticated แต่เปลี่ยนค่า email_verified จาก true เป็น false พอกด Run (local) เพื่อทดสอบ endpoint นี้อีกครั้ง เราจะได้ error กลับมาเป็นสถานะ 401 Unauthorized ทันที เพราะไม่เข้าเงื่อนไขที่กำหนดเอาไว้นั่นเอง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7maRuoNzXbTcAwp5G72Jlg.png" /><figcaption>เราสามารถแก้ไข Authentication ในการทดสอบสิทธิ์เข้าถึงของแต่ละ endpoint ด้วย</figcaption></figure><p><strong>3.2 Seed data</strong></p><p>เวลาที่เราทำงานใน local ข้อมูลใน database จะถูก reset ใหม่ทุกรอบเวลาเรารัน emulators แต่ละครั้ง เพื่อความสะดวกสบายในการทำงานบน local เราสามารถเขียน mutation เพื่อเพิ่มข้อมูลลงใน database ได้ด้วย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*X8PxyJaom7gJ8N_p8TaFhg.png" /><figcaption>ไฟล์ seed สำหรับ seed ข้อมูลเกมต่างๆ</figcaption></figure><p>โดยหลังจากที่เรา run emulators เสร็จแต่ละครั้งเราก็สามารถมา Run (local) คำสั่งนี้ต่อได้เพื่อทำการ seed ข้อมูลเข้า database</p><p><strong>3.3 Exploring the data</strong></p><p>ความคุ้นชินของเราอย่างนึงในการทำงานกับ SQL คือการได้ดูข้อมูลในรูปแบบตาราง ซึ่งตอนนี้ Firebase Data Connect ยังไม่มีให้ ทุกอย่างที่พ่นออกมา จากการทดสอบแต่ละ endpoint ยังเป็นแค่ข้อมูลในรูปแบบ json เท่านั้น</p><p>อย่างที่บอกไปในตอนแรกว่าเบื้องหลังของมันคือ PostgresSQL และ Firebase ใช้ PGLite ในการรัน database บน local ซึ่งก็ทำให้การดูข้อมูลแบบตารางเป็นเรื่องที่ทำได้เหมือนกัน โดยเราสามารถใช้ SQL Viewer อย่าง DBeaver, DataGrip หรือเจ้าอื่นๆที่สะดวก แล้วสร้างการเชื่อมต่อไปยัง jdbc:postgressql://127.0.0.1:5432/[databaseName]?sslmode=disable</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FBG6ctudJ0NzI1zYP1ySCg.png" /><figcaption>connection configuration สำหรับการเชื่อมต่อกับ PostgresSQL บน local ด้วยโปรแกรม DBeaver</figcaption></figure><p>หลังจากนั้น เราก็สามารถ explore ข้อมูลแต่ละตาราง ดู database schema ได้เหมือน SQL อื่นๆเลย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*twtaOncidYSFofWJtF5Ukg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P5YEgqgxzwk7anl4hsk2Vg.png" /><figcaption>ER Diagram ของ Database ของเรา / ข้อมูลที่เราเพิ่มเข้าไปในตาราง Game ดูผ่าน DBeaver</figcaption></figure><p><strong>4. สร้าง SDK</strong></p><p>หลังจากที่เรากำหนดรูปแบบข้อมูลและ connector ที่ต้องการแล้ว ก็ถึงเวลาเอามาต่อกับ client หน้าบ้าน ซึ่งแทนที่เราจะไป connect กับ database ตรงๆ เราจะเชื่อมต่อกับ typesafe SDK ที่ Firebase สร้างให้ แบบเราเกริ่นเอาไว้ตอนแรก</p><p>ถ้าสังเกตตอนที่เรา firebase init dataconnect ในขั้นตอน 0.ติดตั้ง ในโฟลเดอร์ connectorมันจะมีไฟล์ connector.yaml ซึ่งไฟล์นี้แหละที่เราสามารถมากำหนด destination ของ SDK ที่เราต้องการ พร้อมกับตั้งชื่อ package เพื่อให้ Firebase ทำการสร้าง typesafe SDK ที่เราสามารถนำไปเรียกใช้ใน client app ไม่ว่าจะเป็น iOS, Android, Web, Flutter ได้เลย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rj0s1KS1oXsMjv2VBiNydg.png" /></figure><p>เข้ามาตอนแรกทุกอย่างจะถูก comment เอาไว้ ให้ทำการเอา comment ออกและเขียน config ที่ต้องการลงไปเลย อย่างในนี้ เราระบุว่า ให้ outputDir อยู่ใน folder app ซึ่งก็คือ vue app ที่เราสร้างเอาไว้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yezMy7pLURFwJhVwTmbLkA.png" /><figcaption>สิ่งที่เราได้ หลังจากกำหนดค่า connector.yaml แล้ว</figcaption></figure><h4>4. เรียกใช้ใน client</h4><p>ในบทความนี้ เราทำแอปตัวอย่างเป็น vue web application ที่แสดงรายการของเกมทั้งหมด พร้อมกับฟังก์ชันในการค้นหาเกมด้วย keyword, อายุ และ จำนวนผู้เล่น</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*is5kZ3GYXqaGH1scfJLQfA.png" /><figcaption>หน้าตาแอปหลังจากเชื่อมต่อกับ database แล้ว</figcaption></figure><p><strong>4.1 เชื่อมต่อกับ Data Connect Emulators</strong></p><pre>/* ไฟล์ lib/firebase.ts */<br><br>export const firebaseApp = initializeApp({<br>  // your config from Firebase console<br>})<br><br>const auth = getAuth(firebaseApp)<br>const dataconnect = getDataConnect(firebaseApp, connectorConfig)<br>if (import.meta.env.DEV) {<br>  connectDataConnectEmulator(dataconnect, &#39;127.0.0.1&#39;, 9398, false)<br>  connectAuthEmulator(auth, &#39;http://localhost:9098&#39;)<br>}<br>export { auth, dataconnect }</pre><p><strong>4.2 ใช้ functions และ types ใน SDK บน client</strong></p><pre>import { searchGames } from &#39;@dicely/dataconnect&#39;<br><br>const response = await searchGames({<br>  keyword: qParams.keyword,<br>  minAge: qParams.minAge ? Number(qParams.minAge) : undefined,<br>  player: qParams.player ? Number(qParams.player) : undefined,<br>})<br><br>/* ใช้ค่า response.data ใน UI <br> games.value = response.data.games as Game[]<br>*/</pre><p>ซึ่งพอเราลองรันดู ก็จะเห็นว่ามันมีการเรียก api endpoint ที่ Firebase สร้าง พร้อมกับมีการเช็คสิทธิ์ Authentication ให้ด้วย แบบที่เราไม่ต้องทำอะไรเลย นอกจากเรียก function เท่านี้เองจริงๆ 🫰</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Df8YB-JQoNvObr4AJFxDfQ.png" /></figure><h4>5. Deploy</h4><p>ถ้าเราเทสบน local แล้วแฮปปี้ เราสามารถรันคำสั่ง firebase deploy เพื่อทำการปล่อยของทั้งหมด ขึ้น production ได้ หรือจะเลือกเฉพาะ product ก็ได้เช่นเดียวกัน</p><pre>firebase deploy --only dataconnect</pre><p>ถ้าหาก database ของเรามีการเปลี่ยนแปลงที่อาจกระทบของบน production firebase ก็จะทำการแจ้งเตือนเราก่อน หรือเราสามารถใช้คำสั่ง เพื่อเรียกดูการเปลี่ยนแปลงของ database ก่อนทำการ deploy ก็ได้</p><pre>firebase dataconnect:sql:diff</pre><p>หลังจากที่ deploy เรียบร้อยแล้ว ก็สามารถเข้าไปทดสอบข้อมูลของเราผ่าน Firebase Console หรือดู Cloud SQL ที่ Google Cloud Console ได้เช่นกัน</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ht8FMB7Rhcx4vyWehu_pHA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oGk1G4YBtHC2RuDRTg46hg.png" /><figcaption>ดูข้อมูลผ่าน Firebase Console ที่เมนู Data Connect หรือจะดูผ่าน Cloud Studio ใน Google Cloud Console ก็ได้</figcaption></figure><h3>The workflow</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pwMgmORGqBnE0Wx1Z_rLDw.png" /></figure><p>ไม่ว่าจะเป็นการ dev เพื่อเอาขึ้น production หรือ บน local โฟล์วการทำงานของนักพัฒนาอย่างเราเหมือนกันเลยคือ</p><ol><li>เขียน GraphQL เพื่อกำหนด Data model และ Connector</li><li>สร้าง SDK ไปใช้กับ client</li><li>เชื่อมต่อกับ database ผ่าน client SDK</li></ol><p>สำหรับคนที่อ่านมาถึงตรงนี้ แล้วอยากจะไปลองเล่น ตามไปที่ Codelab ตัวนี้ได้เลยค่า จับมือทำตั้งแต่เริ่มจนจบเลย</p><p><a href="https://firebase.google.com/codelabs/firebase-dataconnect-web#0">Build with Firebase Data Connect</a></p><p>เนื้อหาในบทความนี้มาจากสิ่งที่เราพูดไปในงาน Google DevFest Bangkok 2024 หัวข้อ Build better AI-driven Cloud SQL apps with Firebase Data Connect เพื่อให้อร่อย ย่อยง่ายขึ้น เราขออนุญาตตัดจบไว้แค่นี้ก่อน และเดี๋ยวมีภาค 2 ที่เน้นการสร้าง Database สำหรับ AI ตามมาแบบไม่นานเกินรอค่า ฝากติดตามกันด้วยน้า</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*McMNlMfRTrAgBmTT4zn7lA.jpeg" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=43a3b0ff6141" width="1" height="1" alt=""><hr><p><a href="https://medium.com/firebasethailand/firebase-data-connect-%E0%B8%9A%E0%B8%A3%E0%B8%B4%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%97%E0%B8%B5%E0%B9%88%E0%B8%97%E0%B8%B3%E0%B9%83%E0%B8%AB%E0%B9%89-firebase-%E0%B9%80%E0%B8%8A%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%A1%E0%B8%95%E0%B9%88%E0%B8%AD%E0%B8%81%E0%B8%B1%E0%B8%9A-postgressql-%E0%B9%84%E0%B8%94%E0%B9%89%E0%B9%81%E0%B8%9A%E0%B8%9A%E0%B8%87%E0%B9%88%E0%B8%B2%E0%B8%A2%E0%B9%86-43a3b0ff6141">Firebase Data Connect บริการที่ทำให้ Firebase เชื่อมต่อกับ PostgresSQL ได้แบบง่ายๆ</a> was originally published in <a href="https://medium.com/firebasethailand">Firebase Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Google Analytics for Firebase กับ Offline Event ด้วย Measurement Protocol]]></title>
            <link>https://medium.com/firebasethailand/google-analytics-for-firebase-%E0%B8%81%E0%B8%B1%E0%B8%9A-offline-event-%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-measurement-protocol-e9afbdf6afb3?source=rss----261beda53e69---4</link>
            <guid isPermaLink="false">https://medium.com/p/e9afbdf6afb3</guid>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[measurement-protocal]]></category>
            <category><![CDATA[google-analytics]]></category>
            <category><![CDATA[offlineevents]]></category>
            <dc:creator><![CDATA[Siwawes Wongcharoen]]></dc:creator>
            <pubDate>Mon, 24 Jun 2024 02:44:35 GMT</pubDate>
            <atom:updated>2024-06-24T02:44:34.945Z</atom:updated>
            <content:encoded><![CDATA[<h4>เรื่องมันมีอยู่ว่า ในกรณีที่เรามี Frontend ที่เชื่อมต่อกับ Google Analytics แล้ว สามารถส่งข้อมูลต่าง ๆ ผ่านตัว Firebase SDK ได้อยู่แล้ว แล้วทางฝั่ง Backend ล่ะ ตัว Firebase Admin SDK มันไม่ได้มีตัว Analytic มาด้วย เราจะส่งข้อมูลได้อย่างไรบ้าง ?</h4><blockquote>TLDR — Too long, Don’t Read;</blockquote><p>แนวทางการส่ง Offline Event เข้าตัว Measurement Protocol ทำได้ดังนี้</p><ol><li>ฝั่ง Frontend ต้องเก็บ Client ID และ User ID มาให้ได้</li><li>ฝั่ง Backend ใช้ Client ID และ User ID ที่ได้มาจาก Frontend ประกอบกัน แล้วส่งข้อมูลเข้าตัว Measurement Protocol เอง</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tLjjteCEqfRE0WPfPjBYTw.png" /><figcaption>Google Analytics for Firebase กับ Offline Event ด้วย Measurement Protocol</figcaption></figure><h3>Google Analytics for Firebase ของดี ที่ใช้ง่าย แต่ดันมาไม่ครบ</h3><p>เปิดหัวมาแบบนี้ แฟน ๆ Firebase อย่าพึ่งด่าผมนะ เพราะเรื่องมันมีอยู่ว่า ตั้งแต่สมัยที่ตัว Google Analytics for Firebase ออกมาใหม่ ๆ ตอนนั้นก็จะ Support อยู่แค่ Android กับ iOS จนผ่านมาพักใหญ่ ๆ ถึงจะ Support ฝั่ง Web แต่ว่าทั้งหมดนี้มันทำงานได้กับเฉพาะฝั่ง Frontend อย่างเดียว ไม่ครอบคลุมมาถึงฝั่ง Backend ด้วย</p><p>อ้าววว แล้วถ้าฝั่ง Backend ก็ต้องการส่งข้อมูลเข้า Analytics ด้วยล่ะ จะเอายังไง อันนี้ไม่ต้องห่วง เพราะว่า Google เองก็มีตัว Measurement Protocol ให้ใช้อยู่แล้ว เป็น REST API ธรรมดา ๆ เลย เพียงแค่ว่า ตัว Measurement Protocol มันจะไม่ได้รวมอยู่ใน SDK นะ ต้องเขียนเอง</p><p>แล้วทำไมฝั่ง Backend ต้องส่งข้อมูลเข้าฝั่ง Analyitcs ด้วยล่ะ ? ก็เพื่อเติมเต็มข้อมูลต่าง ๆ ที่ไม่สามารถทำได้จากฝั่ง Frontend ไง ยกตัวอย่าง เช่น ในแง่ของ ECommerce ปกติ Metric ส่วนใหญ่ สามารถวัดได้จาก Frontend อยู่แล้ว แต่จะมี Event บางอย่างที่ไม่สามารถวัดได้จาก Frontend ตรง ๆ นั่นก็คือ Metric ที่เกียวกับการจัดส่งบางตัว เนื่องจาก ข้อมูลที่เกียวกับการจัดส่ง มันจะไม่เกิดบน Frontend ของระบบเรา แต่ฝั่ง Backend เรามีข้อมูลพวกนี้อยู่แล้ว ฉะนั้น ถ้าเราอยากทำ Conversion ที่ครอบคลุมมาถึงเรื่องการจัดส่ง เราจะต้องให้ Backend เรา ส่ง Metric พวกนี้เข้ามาใน Analyitcs ด้วย</p><p>มาถึงตรงนี้ อาจจะมีคนหัวใส อยากจะเก็บ Stat บางอย่างบน Google Analytics เลยได้มั้ย คำตอบคือ ได้นะ ถ้า Stat พวกนั้นมันสามารถผูกกับข้อมูลที่มาจาก Frontend ได้ แต่ถ้าผูกไม่ได้ ก็ยังส่งเข้าไปได้ แต่ตัว Google Analytics จะไม่แสดงข้อมูลนั้น ข้อความนี้อาจจะงง ๆ หน่อย สรุป ๆ ง่าย ๆ ก็คือ เราไม่สามารถลักไก่ใช้งาน Google Analytics ในแบบผิด ๆ</p><h3>ทำอย่างไร ?</h3><h4>เตรียมความพร้อม</h4><ol><li>Firebase Account and Firebase Project</li><li>Google Analytics Permissions</li></ol><h3>เริ่มกันที่ฝั่ง Frontend</h3><p>ในตัวอย่างนี้จะเน้นมาทาง Web นะครับ</p><p>เริ่มจากให้เอา Analytics SDK ใส่ในเว็บให้เรียบร้อย (<a href="https://firebase.google.com/docs/analytics/get-started?platform=web">วิธีทำ</a>)</p><p>จากนั้น จะมี 2 ส่วนสำคัญ ที่เราต้องทำ คือ</p><ol><li>Set User ID ให้ตัว Analytics SDK</li><li>เอาค่า Google Analytics Client ID ออกมาจากตัว Analytics SDK<br>อันนี้สำคัญที่สุด เพื่อให้ตัว Measurement Protocol ผูกข้อมูลได้อย่างถูกต้อง</li></ol><h4>1. Set User ID ให้ตัว Analytics SDK</h4><p>ขั้นตอนนี้ถึงจะไม่จำเป็นต้องทำ แต่แนะนำให้ทำเป็นอย่างยิ่ง และหากเป็น User ID ที่ Backend ใช้งานด้วยก็จะดีที่สุด</p><pre>import {<br>  getAnalytics,<br>  setUserId,<br>} from &#39;firebase/analytics&#39;<br><br>export function analyticsSetUserId(userId: string) {<br>  const analytics = getAnalytics()<br>  setUserId(analytics, userId)<br>}<br><br>// https://firebase.google.com/docs/analytics/userid#web</pre><p>รายละเอียดเต็ม ๆ แนะนำให้อ่านจาก<a href="https://firebase.google.com/docs/analytics/userid#web">ต้นทาง</a>นะครับ</p><h4>2. เอาค่า Google Analytics Client ID ออกมาจากตัว Analytics SDK</h4><p>อันนี้สำคัญที่สุด เพื่อให้ตัว Measurement Protocol ผูกข้อมูลได้อย่างถูกต้อง</p><pre>import {<br>  getAnalytics,<br>  getGoogleAnalyticsClientId as getGoogleAnalyticsClientIdFirebase<br>} from &#39;firebase/analytics&#39;<br><br>export function getGoogleAnalyticsClientId() {<br>  const analytics = getAnalytics()<br>  return getGoogleAnalyticsClientIdFirebase(analytics)<br>}<br><br>// https://firebase.google.com/docs/reference/js/analytics.md#getgoogleanalyticsclientid_b0a3b5a</pre><p>อันนี้ ใน Doc ปกติจะไม่มีเขียนไว้ แต่มี<a href="https://firebase.google.com/docs/reference/js/analytics.md#getgoogleanalyticsclientid_b0a3b5a">ในนี้</a></p><p>สำคัญมาก อย่าลืมเอาค่าที่ได้ ส่งไปเก็บที่ Backend ด้วยนะ</p><p>เอาละ ในฝั่ง Frontend เราเรียบร้อยแล้ว ง่ายนิดเดียว (ที่เหลือยากละ 😂)</p><h3>มาต่อกันที่ฝั่ง Backend</h3><p>ในตัวอย่างนี้จะเน้นเป็น Node.js แบบ TypeScript นะครับ</p><p>จะมี 3 ส่วนสำคัญ ที่เราต้องทำ คือ</p><ol><li>ทำให้ Backend เราส่งข้อมูลเข้า Measurement Protocol ได้</li><li>เตรียมค่า API Secret และ Measurement ID</li><li>เตรียมค่า User ID และ Google Analytics Client ID ที่ได้มาจาก Frontend</li></ol><h4><strong>1. ทำให้ Backend เราส่งข้อมูลเข้า Measurement Protocol ได้</strong></h4><p>จะใช้ Axios หรือ Lib ตัวอื่น อันนี้เอาตามสะดวกเลยครับ ยังไงก็ได้ กับดูให้แน่ใจว่า Backend เราส่งข้อมูลไปข้างนอกได้จริง ๆ ไม่ติดอะไร</p><h4>2. เตรียมค่า API Secret และ Measurement ID</h4><p>ค่า API Secret จะต้องไปเอามาจากตัว Google Analytics UI วิธีการคือ</p><p>ไปที่หน้า Google Analytics UI Admin &gt;&gt;&gt; Admin &gt; Data collection and modification &gt; Data Streams &gt; Web stream details &gt; Measurement Protocol API secrets &gt; Create.</p><p>ค่า Measurement ID เอามาจาก Firebase Admin UI หรือ Google Analytics UI ก็ได้</p><p>Firebase Admin UI &gt;&gt;&gt; Project settings &gt; Your apps &gt; Web apps &gt; [measurementId]</p><p>Google Analytics UI &gt;&gt;&gt; Admin &gt; Data collection and modification &gt; Data Streams &gt; Web stream details</p><h4>3. เตรียมค่า User ID และ Google Analytics Client ID ที่ได้มาจาก Frontend</h4><p>มาถึงจุดนี้ ก็ได้เวลาที่เราจะเอา User ID และ Google Analytics Client ID ที่เราเก็บมาจาก Frontend ออกมาใช้แล้ว</p><pre>type GoogleAnalyticsEvent = {<br>  name: string;<br>  params: any;<br>}<br><br>type GoogleAnalyticsPayload = {<br>  client_id: string;<br>  user_id?: string;<br>  timestamp_micros?: number;<br>  user_properties?: any;<br>  events: GoogleAnalyticsEvent[];<br>}<br><br>async function sendEventToGA(<br>  googleAnalyticsClientId: string,<br>  googleAnalyticsUserId: string,<br>  events: any[]<br>) {<br>  try {<br>    const data: GoogleAnalyticsPayload = {<br>      client_id: googleAnalyticsClientId,<br>      user_id: googleAnalyticsUserId,<br>      events,<br>    }<br>    const response = await sendEventToGoogleAnalytics(data)<br>    consola.log(&#39;payment.controller sendPaymentEventToGA ok response.status&#39;, response.status)<br>    consola.log(&#39;payment.controller sendPaymentEventToGA ok response.data&#39;, stringify(response.data))<br>    return response<br>  } catch (error) {<br>    consola.log(&#39;payment.controller sendPaymentEventToGA fail&#39;, error.message)<br>  }<br>}<br><br>export async function sendEventToGoogleAnalytics (data: GoogleAnalyticsPayload) {<br>  const endpoint = &#39;https://www.google-analytics.com/mp/collect&#39;<br>  const debugEndpoint = &#39;https://www.google-analytics.com/debug/mp/collect&#39;<br>  const config: AxiosRequestConfig = {<br>    headers: {<br>      &#39;Content-Type&#39;: &#39;application/json&#39;,<br>    },<br>    params: {<br>      measurement_id: GOOGLE_ANALYTIC_MEASUREMENT_ID,<br>      api_secret: GOOGLE_ANALYTIC_API_SECRET,<br>    },<br>  }<br><br>  consola.log(&#39;google-analytic.repo sendEventToGoogleAnalytics ok config&#39;, stringify(config))<br>  consola.log(&#39;google-analytic.repo sendEventToGoogleAnalytics ok data&#39;, stringify(data))<br>  return axios.post(endpoint, data, config)<br>}</pre><p>ใน Code ตัวอย่างนี้ จะมี 2 Function หลัก คือ</p><ol><li>sendEventToGA() เป้าหมายคือ ทำหน้าที่เตรียมประกอบข้อมูล ก่อนส่งข้อมูล</li><li>sendEventToGoogleAnalytics() เป้าหมายคือส่งข้อมูล</li></ol><p>อันที่จริงจะเขียนรวมกันก็ได้นะครับ แต่ผมชอบแยกอย่างน้อย ออกเป็น 2 Layer แบบนี้ โดยแยกตัวประกอบข้อมูล กับตัวส่งข้อมูลออกจากกัน</p><p>ใน Code ตัวสมบูรณ์ จะมีตัว Data Builder แยกออกมาอีก ก่อนจะมาประกอบร่าง และส่งข้อมูลตามลำดับ</p><h3>ดูผล</h3><p>วิธีดูผลที่สะดวกที่สุด คือดูผ่าน Realtime Event ครับ ถ้าเราใช้ User ID และ Google Analytics Client ID อย่างถูกต้อง เราจะเป็น Offline Event ที่เราส่งขึ้นเอง ไปรวมอยู่ใน User คนเดียวกันได้อย่างถูกต้อง</p><p>ตรงนี้ต้องขออภัยที่ไม่สามารถ เอารูปตัวอย่างออกมาได้ เนื่องเป็นมันเป็นงานที่ทำจริง</p><p>มาถึงตรงนี้ อาจจะมีคนคาใจว่า จะใช้ User ID ทำไม ในเมื่อ ใช้แค่ Google Analytics Client ID ก็เพียงพอแล้ว คำตอบคือ เวลาที่เราเอาข้อมูลออกมาใช้ต่อ (ไม่ว่าจะเป็น Power BI หรือ Looker Studio) เราจะได้มี Property ตัวนี้ เอาไว้เป็นตัวเชื่อมกับข้อมูลของเราเองครับ</p><h3>References</h3><ul><li><a href="https://firebase.google.com/docs/analytics/get-started?platform=web">Get started with Google Analytics | Google Analytics for Firebase</a></li><li><a href="https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag">Measurement Protocol Reference | Google Analytics | Google for Developers</a></li><li><a href="https://firebase.google.com/docs/reference/admin/node">API Reference | Firebase Admin SDK</a></li><li><a href="https://firebase.google.com/docs/reference/js">API Reference | Firebase JavaScript API reference</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e9afbdf6afb3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/firebasethailand/google-analytics-for-firebase-%E0%B8%81%E0%B8%B1%E0%B8%9A-offline-event-%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-measurement-protocol-e9afbdf6afb3">Google Analytics for Firebase กับ Offline Event ด้วย Measurement Protocol</a> was originally published in <a href="https://medium.com/firebasethailand">Firebase Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[รู้จัก Cloud Functions for Firebase 2nd Gen ตั้งแต่ Zero จนเป็น Hero]]></title>
            <link>https://medium.com/firebasethailand/cloud-functions-for-firebase-2nd-gen-cdda33bbd7dd?source=rss----261beda53e69---4</link>
            <guid isPermaLink="false">https://medium.com/p/cdda33bbd7dd</guid>
            <category><![CDATA[cloud-function-2nd-gen]]></category>
            <category><![CDATA[cloud-functions]]></category>
            <category><![CDATA[firebasethailand]]></category>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[firebase-cloud-functions]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Sun, 14 Jan 2024 11:40:15 GMT</pubDate>
            <atom:updated>2025-06-16T07:30:56.656Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KCEFu1KErT61b-WWf1Qtxg.png" /></figure><p>ในการพัฒนาแอปพลิเคชันส่วนใหญ่ ณ วันนี้ก็ต้องมี server ที่คอยทำงานอยู่เบื้องหลัง ด้วยข้อดีหลายๆอย่าง เช่น เพื่อ centralize ข้อมูลและ logic การทำงานไว้ที่จุดๆเดียว, เพื่อรองรับการใช้งานจากหลายๆแพลทฟอร์ม, เพื่อความปลอดภัยที่มากขึ้น หรือจะเพื่อให้ client ไม่ต้องทำงานหนัก</p><p>แต่การจะตั้ง server เองก็ไม่ใช่เรื่องง่าย เพราะมันมีเรื่องที่ต้องให้คำนึงอยู่หลายเรื่องเหมือนกัน ไม่ว่าจะเป็นการ scale ที่ควรจะต้องขึ้นลงได้ตาม transaction ที่เกิดขึ้น, เรื่องของความปลอดภัย, เรื่องการเตรียม API หรือ SDK ที่จะรองรับการใช้งานแบบ seamless จากหลายๆแพลทฟอร์ม, หรือค่าใช้จ่ายทั้ง ฮาร์ดแวร์, ซอฟต์แวร์ และผู้เชี่ยวชาญที่จะเข้ามาดูแลตรงส่วนนี้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XjoqoFBmZTuoB60q3oqmBg.png" /></figure><p>มือใหม่ที่กำลังคิดจะสร้างแอปพลิเคชัน ฟังดูก็คงหัวจะปวดอยู่เหมือนกัน แต่ไม่ต้องกังวลไป เพราะบทความนี้จะพาคุณไปรู้จัก <strong>Cloud Functions for Firebase</strong> อีกหนึ่งทางเลือก ที่เตรียมทุกสิ่งทุกอย่างที่กล่าวไปข้างต้นไว้ให้แล้ว พร้อมให้คุณเขียนโปรแกรมตาม business logic ที่คุณต้องการได้เลย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UmuoQwGKSvHJFnInjTIMaA.png" /></figure><p><strong>Cloud Functions for Firebase</strong> เป็นบริการแบบ Functions-as-a-Service ที่จะให้นักพัฒนาเขียนและ run โค้ดบน Google infrastructure เพื่อตอบสนอง event ต่างๆที่เกิดขึ้นจากบริการใน Firebase และ HTTP Requests</p><h4>Event Triggers</h4><p>เป็นการสร้างฟังก์ชันเพื่อเฝ้าดูความเปลี่ยนแปลง(subscribe)ที่เกิดขึ้นกับบริการต่างๆ Firebase ซึ่งเมื่อมีความเปลี่ยนแปลงเกิดขึ้น ตัวบริการที่เราเฝ้าดูจะส่งสัญญาณพร้อมกับข้อมูลมา(publish)ให้ฟังก์ชันทำงานต่อแบบอัตโนมัติ ซึ่ง ณ วันนี้ก็มีบริการมากมายที่สามารถทำงานร่วมกับ Cloud Functions ได้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VIuSrda9IYpH3iNT3vhK1Q.png" /></figure><p>เพื่อให้เห็นภาพ เรามาดูตัวอย่างการ trigger จากบริการใน Firebase ด้านล่างนี้กัน</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_FAdCi5tNd4VH76woU7iow.png" /></figure><p>จากภาพเราสามารถสร้างฟังก์ชันขึ้นมา(2) เพื่อเฝ้าดูการเปลี่ยนแปลงของฐานข้อมูลอย่าง Firestore หรือ Realtime Database ได้ เช่น เมื่อข้อมูลผู้ใช้ใหม่ถูก write เข้ามาเก็บลงฐานข้อมูล ตัวฐานข้อมูลจะส่งสัญญาณพร้อมกับข้อมูลผู้ใช้ มาให้ที่ฟังก์ชันของเรา เราก็สามารถเขียนโปรแกรมในฟังก์ชันของเราให้ส่งข้อความไปทักทายเครื่อง client ได้เป็นต้น</p><h4>HTTP Requests</h4><p>เป็นการสร้างฟังก์ชันที่เป็น API หรือ Webhook เพื่อรอรับการ request ผ่าน HTTP ให้ฟังก์ชันที่เขียนไว้ทำงานต่อ และเพื่อให้เห็นภาพ เรามาดูตัวอย่างการ trigger จาก HTTP Requests ด้านล่างนี้กัน</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*34-xmujtRH9JYQd1j913FQ.png" /></figure><p>จากภาพเราสามารถสร้าง Webhook API(Functions) ขึ้นมาเพื่อรอรับ webhook events จาก GitHub เช่น เมื่อมีสมาชิกในทีม push โค้ดขึ้นมาใน repo ตัว GitHub ก็จะส่ง request พร้อมข้อมูลมาที่ Webhook API ของเรา ซึ่งเราก็สามารถเขียนโปรแกรมเพื่อส่งต่อข้อมูลเหล่านั้นไปให้เพื่อนร่วมทีมคนอื่นๆในห้อง Slack ได้เป็นต้น</p><p><strong>Tip</strong>: เรายังสามารถสร้างฟังก์ชันที่ให้แอปเรียกหาตรง หรือที่เรียกว่า Callable Functions โดยไม่ต้องระบุ Endpoint URL ลงไปในโค้ดได้ด้วยนะ ใครสนใจก็อ่านบทความด้านล่างนี้ได้เลย</p><p><a href="https://medium.com/firebasethailand/callable-function-c423e75f6ece">รู้จัก Callable Function ใน Cloud Functions for Firebase ตั้งแต่ Zero จนเป็น Hero</a></p><h4>Schedule Functions</h4><p>นอกเหนือจาก trigger ทั้ง 2 แบบที่กล่าวไปข้างต้น Cloud Functions for Firebase ยังได้ร่วมมือกับทีม Cloud Scheduler ออก Schedule functions ที่ให้เราสร้าง Cron Job แบบง่ายๆด้วยโค้ด 2–3 บรรทัด ใครสนใจอ่านเรื่องนี้จากบทความได้เลย</p><p><a href="https://medium.com/firebasethailand/%E0%B8%AA%E0%B8%A3%E0%B9%89%E0%B8%B2%E0%B8%87-cron-job-%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-cloud-functions-for-firebase-%E0%B8%87%E0%B9%88%E0%B8%B2%E0%B8%A2%E0%B8%99%E0%B8%B4%E0%B8%94%E0%B9%80%E0%B8%94%E0%B8%B5%E0%B8%A2%E0%B8%A7-fe8cb64cec52">สร้าง Cron Job ด้วย Cloud Functions for Firebase ง่ายนิดเดียว</a></p><h3>1. รู้จักกับ Cloud Functions for Firebase 2nd Gen</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QHtgtDVmT86xxKpnHx1khQ.png" /></figure><p>ในงาน Google I/O ปีที่ผ่านมาทีม Firebase ก็ได้เปิดตัว Cloud Functions 2nd Gen ซึ่งได้ยกเครื่องเบื้องหลังใหม่ โดยเปลี่ยนจาก <strong>GAE(Google App Engine)</strong> ที่ใช้ตั้งแต่ปี 2017 มาเป็น Cloud Build, Cloud Run และ Eventarc</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dUhocrckoNzJuNUMHZBBfA.png" /></figure><p>โดยการเปลี่ยนบริการที่อยู่เบื้องหลังใหม่นี้เอง ก็มาพร้อมกับจุดเด่นที่มาปิด pain point จาก 1st Gen และฟีเจอร์ใหม่ๆที่เพิ่มเข้ามา จะมีอะไรบ้างไปดูกันเลย!</p><h4>1.1 Concurrency</h4><p>ใน 1st Gen นั้นกฎของเรื่อง concurrency คือ <strong>1 request/instance</strong> ซึ่งสมมติว่ามี 16 requests วิ่งมาหาฟังก์ชันที่เราสร้างไว้พร้อมๆกัน ตัว 1st Gen จะทำการสร้าง instance ขึ้นมา 16 ตัว คำถามคือแล้วมันมีข้อเสียตรงไหน? คำตอบคือ ตรงที่ทุกครั้งในการสร้าง instance มันจะต้องใช้เวลาในการ build ตัว instance ขึ้นมาจากทั้ง Linux, npm, Native libs และ Node.js ซึ่งทำให้การ response จะมี latency แฝงอยู่ หรือที่เราจะเรียกปัญหานี้ว่า Cold Start</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*O2ASpE8PUP1NEHfR9aRotQ.png" /></figure><p>แต่ใน 2nd Gen ปัญหานี้ได้หมดไปแล้ว เนื่องจาก 1 instance รองรับได้หลาย request โดยค่าเริ่มต้นจะอยู่ที่ 80 request/instance แต่เราก็สามารถปรับแต่งเองได้ตั้งแต่ 1–1,000 request/instance เลยทีเดียว ดังนั้นถ้าจำนวน concurrency ไม่เกินจากที่กำหนด ก็จะไม่มีการสร้าง instance ใหม่นั่นเอง</p><h4>1.2 Cost Saving</h4><p>เรื่องถัดมาคือเรื่องเงินๆทองๆ ซึ่งถือเป็นเรื่องใหญ่ของพวกเรา ^^ โดยผมจะขอเปรียบเทียบการคิดค่าบริการผ่านตัวอย่างต่อไปนี้ สมมติว่าฟังก์ชันที่สร้างจาก 1st Gen และ 2nd Gen มี 3 requests ที่จะต้องใช้เวลาในการประมวลผล 150ms วิ่งเข้าหาเหมือนๆกัน และแต่ละ request จะมีระยะห่างจากกัน 60ms ตามรูปด้านล่าง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Y-5YYpJBexa-NNJd5P0Vrw.png" /></figure><ul><li>ฟังก์ชันจาก 1st Gen จะทำการสร้าง instance ออกมา 3 ตัว โดย<strong>เวลาขั้นต่ำที่ Google จะคำนวนค่าใช้จ่ายจะอยู่ที่ 100ms</strong> ทำให้ 150ms ถูกปัดขึ้นเป็น 200ms ดังนั้น compute time จากทั้ง 3 requests จึงมีผลรวม <strong>600ms</strong></li><li>ฟังก์ชันจาก 2nd Gen จะทำการสร้าง instance ออกมาแค่ตัวเดียว โดยกว่าที่ request ตัวสุดท้ายจะทำงานเวลาก็จะผ่านไปแล้ว 120ms เมื่อมารวมกับระยะเวลาในการประมวลผลของ request ตัวสุดท้าย ก็จะเป็น <strong>120 + 150 = 270ms</strong> และ Google จะคำนวนค่า compute time ขั้นต่ำที่ 100ms ผมรวมจึงเป็น <strong>300ms</strong> ซึ่ง<strong>ประหยัดกว่า 1st Gen ถึง 2 เท่า</strong>ในตัวอย่างนี้</li></ul><h4>1.3 <a href="https://firebase.google.com/docs/functions/manage-functions?gen=2nd#override-CPU">New Instance Size</a></h4><p>ด้วยการที่ 1 instance สามารถรองรับได้หลาย request แน่นอนว่าทรัพยากรก็จะถูกใช้เพิ่มขึ้นด้วยเช่นกัน ดังนั้นใน 2nd Gen ก็ให้เราสามารถปรับแต่ง memory และ CPU ของ instance ได้เพิ่มมากขึ้นด้วย โดย</p><ul><li>Memory สามารถปรับแต่งได้ตั้งแต่ 128MB ไปจนถึง 32GB เลย</li><li>CPU สามารถปรับแต่งได้ตั้งแต่ 1 CPU ไปจนถึง 8 CPU</li></ul><p><strong>หมายเหตุ</strong>: การปรับ memory กับ CPU ที่สูงจะมีผลทำให้ค่าบริการสูงขึ้นตามไปด้วย</p><h4>1.4 <a href="https://firebase.google.com/docs/functions/manage-functions?gen=2nd#set_timeout_and_memory_allocation">Request Timeout</a></h4><p>ค่า Request Timeout จะเป็นค่าที่เราสามารถระบุเวลาสูงสุดที่จะให้ฟังก์ชันทำงาน โดยหากการทำงานของฟังก์ชันเกินเวลาไป ก็จะตีเป็น error timeout กลับไป โดยค่าเริ่มต้นของ request timeout ในแต่ละฟังก์ชันคือ 60s</p><ul><li><strong>1st Gen</strong> สามารถปรับแต่งค่า request timeout ได้ตั้งแต่ 1s ถึง <strong>9mins</strong> ทั้งฟังก์ชันแบบ Event triggers และ HTTP Requests</li><li><strong>2nd Gen</strong> สามารถปรับแต่งค่า request timeout กับฟังก์ชันแบบ <strong>HTTP Requests</strong> ได้ตั้งแต่ 1s ถึง <strong>60mins</strong> โดยที่แบบ Event triggers จะเหมือน 1st Gen</li></ul><h4>1.5 New Language Support</h4><p>ในตอนเปิดตัวในปี 2017 ตัว Cloud Functions นั้นรองรับเฉพาะ Node.js ซึ่งในปีถัดมาก็มีการรองรับ TypeScript เพิ่มเข้ามา จนกระทั่งใน 2nd Gen ที่เพิ่งเปิดตัวนี้เองที่ Cloud Functions เปิดตัวภาษาที่รองรับภาษาใหม่ นั่นก็คือ Python ภาษาที่มีคนทั่วโลกกว่า 17 ล้านเขียนกัน คราวนี้ใครอยากใช้พวก lib หรือ framework เกี่ยวกับ ML เช่น PyTorch ก็จัดไปได้เลย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C7DoY7UNfZXDkgJiuK1plQ.png" /></figure><h4>1.6 <a href="https://firebase.google.com/docs/functions/alert-events">Firebase Alert Events</a></h4><p>Cloud Functions 2nd Gen มีการ integrate ร่วมกับบริการอีก 3 ตัวใน Firebase สำหรับการแจ้งเตือนทีมนักพัฒนา</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NxrZeqMTzXwr0AnCKLRDcw.png" /></figure><ul><li><strong>Crashlytics</strong> สามารถเขียนฟังก์ชันไปเฝ้าดู crash ประเภทต่างๆที่เกิดขึ้นในแอป</li><li><strong>Performance Monitoring</strong> สามารถเขียนฟังก์ชันไปเฝ้าดูประสิทธิ์ภาพของแอปหากเกิน threshold ที่กำหนด</li><li>App Distribution สามารถเขียนฟังก์ชันไปเฝ้าดู feedback ที่ tester ส่งมา หรือดูว่ามี iOS device ใหม่ register เข้ามา</li></ul><h4>1.7 <a href="https://firebase.google.com/docs/functions/custom-events">Custom Event</a></h4><p>หนึ่งในบริการของ Firebase ที่อำนวยความสะดวกให้นักพัฒนามากที่สุดตัวหนึ่ง ก็คือ <a href="https://extensions.dev/">Firebase Extensions</a> ที่เป็น pre-package solution ให้นักพัฒนาสามารถ plug &amp; play ได้ง่าย ซึ่งใน 2nd Gen ก็มีฟีเจอร์ให้เราสามารถไปคอยเฝ้าดูการทำงานของ extension บางตัวได้ เช่น Resize images หรือ Stripe เป็นต้น เพื่อที่จะให้ฟังก์ชันของเราทำงานเมื่อเกิดเหตุการณ์ต่างๆใน extension เช่น เมื่อ resize รูปเสร็จให้ฟังก์ชันเราส่งรูปที่ resize ไปให้เพื่อนเป็นต้น</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jYHBLkghTGhhbYnUXAR9hg.png" /></figure><h4>1.8 <a href="https://firebase.google.com/docs/functions/auth-blocking-events?gen=2nd">Blocking Functions</a></h4><p>ด้วยความร่วมมือของ Cloud Functions 2nd Gen, Firebase Authentication และ Google Cloud Identity ตอนนี้เรามี Blocking functions ที่ให้เราสามารถสร้างฟังก์ชันไปคั่นในกระบวนการ sign-up และ sign-in ของ Firebase Auth ได้ โดยตัวอย่างคือก่อนที่ผู้ใช้จะ sign-up สำเร็จเราจะตรวจสอบอีเมลของเขาก่อน ถ้าไม่ใช่ domain ที่เราต้องการก็สามารถปฏิเสธการ sign-up ครั้งนี้ได้</p><pre>import { beforeUserCreated } from &quot;firebase-functions/v2/identity&quot;;<br><br>export const blockauth = beforeUserCreated((event) =&gt; {<br>  const user = event.data;<br>  // Ensure only google.com emails can log into the changelog app<br>  if (!user?.email?.includes(&#39;@google.com&#39;)) {<br>    throw new HttpsError(<br>      &quot;permission-denied&quot;,<br>      &quot;Only @google.com are allowed to register.&quot;);<br>  }<br>});</pre><h4>1.9 <a href="https://firebase.google.com/docs/functions/task-functions?gen=2nd">Task Queue Functions</a></h4><p>ใครที่มี job ที่ต้องใช้เวลา run นานๆเจอเคส timeout บ้าง memory limit บ้าง หรือใครที่ยิง API ไปแล้วติด Rate limit ต้องชอบสิ่งนี้ เพราะ Cloud Functions 2nd Gen จับมือกับ Google Cloud Tasks ออกฟีเจอร์ Task Queue Functions ที่ให้นักพัฒนาสามารถเขียนโปรแกรมเพื่อ enqueue งาน โดยสามารถกำหนด rate limit ได้กำหนดการ retry ได้อีกด้วย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iMD7Wfg9qo67jPtQCjOBig.png" /></figure><h4>1.10 New Environment Configuration</h4><p>นอกจากไฟล์ .env และ Cloud Secret Manager ที่นักพัฒนาน่าจะเคยใช้กันแล้วใน 1st Gen ใน 2nd จะมีตัว <a href="https://firebase.google.com/docs/functions/config-env?gen=2nd">Parameterized Config</a> เพิ่มเข้ามาอีกตัว</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7MaUqU3E351SgyiEqAp8jQ.png" /></figure><p>โดย Parameterized Config จะให้เราระบุ key และประเภทของ value ลงในโค้ด แต่จะไม่มีการระบุ value ลงไป โดย value จะถูกระบุครั้งแรกที่มีการ deploy ฟังก์ชันผ่าน command line เท่านั้น ซึ่งก็จะปลอดภัยมากขึ้น แถม param แต่ละตัวที่เรากำหนด เราสามารถระบุค่า default ให้ได้ แต่ทำการ validate ตัว value ที่นักพัฒนากรอกมาตอน deploy ได้อีกด้วย</p><pre>const { onRequest } = require(&quot;firebase-functions/v2/https&quot;);<br>const { defineString, defineInt, defineSecret } = require(&quot;firebase-functions/params&quot;);<br><br>const welcomeMessage = defineString(&quot;WELCOME_MESSAGE&quot;);<br>const concurrencyConfig = defineInt(&quot;CONCURRENCY&quot;, { default: 80 });<br>const apiKey = defineSecret(&quot;API_KEY&quot;);<br><br>exports.f2 = onRequest({secrets: [apiKey], concurrency: concurrencyConfig}, (req, res) =&gt; {<br>  const sampleApiKey = apiKey.value();<br>  res.send(`${welcomeMessage.value()}! I am a function.`);<br>});</pre><h4>11. Built-in CORS</h4><p>ใน 1st Gen การจะกำหนดเรื่อง CORS ว่าให้ domain ไหนเรียก API เราได้บ้างนี่จะต้องไปดึง dependency จาก third-party เข้ามา แล้วเขียนโค้ดโดยเอา cors มาคลุมโค้ดเราอีกท ซึ่งมันแอบขัดใจเล็กๆ แต่ใน 2nd Gen นี่สบายเลย เพราะเขา built-in ตัว CORS มาให้แล้ว ซึ่งวิธีเรียกใช้ก็ง่ายๆแบบด้านล่างนี้ครับ</p><pre>const { onRequest } = require(&quot;firebase-functions/v2/https&quot;);<br><br>// Allow all origins<br>exports.f1 = onRequest({ cors: true }, (req, res) =&gt; {<br>  res.send(&quot;Hello from Firebase!&quot;);<br>});<br><br>const origins = [<br>  &#39;http://localhost:5000&#39;,<br>  &#39;https://localhost:4000&#39;<br>];<br><br>// Allow specific origins<br>exports.f2 = onRequest({ cors: origins }, (req, res) =&gt; {<br>  res.send(&quot;Hello from Firebase!&quot;);<br>});</pre><h4>12. <a href="https://firebase.google.com/docs/reference/functions/2nd-gen/node/firebase-functions.globaloptions">Gloal Runtime Options</a></h4><p>สำหรับคนที่มีประสบการณ์ในการสร้างฟังก์ชันใน 1st Gen มาแล้ว น่าจะรู้จักกับ Runtime Options ที่ให้เรากำหนด option ต่างๆให้กับฟังก์ชันเรา เช่น region ที่ต้องการ deploy ไปลง, memory หรือ timeoutSeconds เป็นต้น ซึ่งใน 1st Gen หากเรามีหลายๆฟังก์ชัน และเราต้องการให้ runtime options เหมือนๆกัน เราก็ต้องไปกำหนดทุกฟังก์ชันเอง เช่น</p><pre>const functions = require(&quot;firebase-functions/v1&quot;);<br><br>exports.f1 = functions.runWith({ memory: &quot;1GB&quot; }).https.onRequest((req, res) =&gt; {<br>    res.send(&quot;Hello from Firebase!&quot;);<br>});<br><br>exports.f2 = functions.runWith({ memory: &quot;1GB&quot; }).https.onRequest((req, res) =&gt; {<br>    res.send(&quot;Hello from Firebase!&quot;);<br>});</pre><p>ซึ่งมันก็ดูเป็นงานที่ซ้ำซ้อนใช่ไหม แต่ใน 2nd Gen ปัญหานี้จะหมดไปด้วย Global Runtime Options ที่ให้เราสามารถกำหนด option ต่างๆไว้ที่ส่วนกลาง และทุกฟังก์ชันก็จะได้รับอนิสงส์ไปด้วยอัตโนมัติ คือดีย์!</p><pre>const { onRequest } = require(&quot;firebase-functions/v2/https&quot;);<br>const { setGlobalOptions } = require(&quot;firebase-functions/v2&quot;);<br><br>setGlobalOptions({<br>  region: &quot;asia-southeast1&quot;,<br>  memory: &quot;1GB&quot;,<br>  concurrency: 40<br>});<br><br>exports.f1 = onRequest((req, res) =&gt; {<br>  res.send(&quot;Hello from Firebase!&quot;);<br>})<br><br>exports.f2 = onRequest((req, res) =&gt; {<br>  res.send(&quot;Hello from Firebase!&quot;);<br>})</pre><p><strong>หมายเหตุ</strong>: กรณีที่ต้องการให้ฟังก์ชันใดมีการกำหนดค่า Runtime Options ที่แตกต่างก็ยังสามารถไปกำหนดค่าตรงๆแบบในข้อ 11.1 ได้ด้วย</p><h3>2. วิธีติดตั้ง Cloud Functions for Firebase</h3><h4>2.1 สร้างโปรเจคใน Firebase</h4><p>ใครที่มีโปรเจคกับ Firebase อยู่แล้วให้ข้ามขั้นตอนนี้ไปได้เลย ส่วนใครที่ยังไม่เคยมีหรือต้องการสร้างใหม่ให้ไปที่ <a href="http://firebase.google.com/console/">Firebase Console</a> จากนั้นกด <strong>Add project</strong> ระบุรายละเอียดต่างๆแล้วกด <strong>Create project</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yiwqAQ0hvFtJaFb6PdbpVg.png" /></figure><p>เมื่อกดเข้าไปในโปรเจคที่เราสร้าง สิ่งที่แรกที่ต้องทำคือ…ให้ไปเปลี่ยนแพลนจาก Spark เป็นเป็น <strong>Blaze(Pay as you go)</strong> ก่อน เนื่องจาก Cloud Functions for Firebase มีเงื่อนไขว่า หากต้องการ deploy ฟังก์ชันขึ้น production จำเป็นจะต้องใช้แพลนนี้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9hbOyLHZklO0e7W4evNgHQ.png" /></figure><blockquote><em>การเปลี่ยนมาใช้ </em><strong><em>Blaze plan</em></strong><em> นักพัฒนาจะได้โค้วต้าในการเรียกใช้งานฟังก์ชัน </em><strong><em>ฟรี 2,000,000 requests/เดือน</em></strong></blockquote><h4>2.2 ติดตั้ง Node.js และ npm</h4><p>ใครที่มีพร้อมทั้ง Node.js และ npm ในเครื่องแล้วให้ข้ามขั้นตอนนี้ไปได้เลย หรือตรวจสอบให้แน่ใจอีกครั้งผ่าน command line</p><pre>node --version // ปัจจุบัน Cloud Functions รองรับเวอร์ชัน 18, 20, และ 22<br>npm --version</pre><p>ส่วนใครที่ยังไม่ได้ลง หรือไม่พบเลขเวอร์ชัน ให้ไปที่ <a href="https://nodejs.org/">https://nodejs.org</a> ดาวน์โหลดและติดตั้ง Node.js ตามแต่ละ OS ที่เราใช้งานให้เรียบร้อย (การติดตั้ง Node.js จะได้ npm มาด้วย)</p><h4>2.3 ติดตั้ง Firebase CLI</h4><p>การ deploy ฟังก์ชันใน Cloud Functions for Firebase จะต้องทำผ่าน Firebase CLI ดังนั้นให้เราติดตั้งตัว CLI ใน command line ด้วยคำสั่ง</p><pre>npm install -g firebase-tools</pre><p>วิธีตรวจสอบว่าเราติดตั้ง Firebase CLI เรียบร้อยแล้ว ให้ใช้คำสั่งด้านล่างนี้ ซึ่งถ้าเห็นตัวเลขเวอร์ชันก็คือ ผ่าน!</p><pre>firebase --version</pre><h4>2.4 Initial โปรเจค</h4><p>เราจะมายืนยันตัวตน เพื่อเข้าถึงโปรเจคใน Firebase ของเรากัน โดยให้ run คำสั่งด้านล่างนี้ใน command line แล้วจะมี browser เด้งมาให้เรา login ก็ให้เรา login ด้วย account ที่ใช้สร้างโปรเจคใน Firebase ไปก่อนหน้านี้</p><pre>firebase login</pre><p>ถัดไปให้สร้างโฟลเดอร์ขึ้นมา(ในตัวอย่างนี้ชื่อ bot) แล้วให้เข้าไปในนั้น</p><pre>mkdir bot<br>cd bot</pre><p>แล้วให้ init ตัว Cloud Functions for Firebase ด้วยคำสั่ง</p><pre>firebase init functions</pre><p>จะเจอหน้าตาแบบด้านล่างนี้ ก็ให้เลือก <strong>Use an existing project</strong> แล้วเลือกโปรเจคที่เราสร้างไว้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Byb4no9J8cuHzWrUUHLGjg.png" /></figure><p>จากนั้นจะมีคำถามมาอีก 3 คำถามคือ</p><ul><li>จะใช้ภาษาอะไรในการพัฒนา โดยมี 3 ภาษาให้เลือก ได้แก่ JavaScript, TypeScript หรือ Python (ตัวอย่างในบทความจะใช้ <strong>JavaScript</strong>)</li><li>จะใช้ <strong>ESLint</strong> เพื่อตรวจสอบ bug และ code style ไหม (<strong>optional</strong>)</li><li>จะติดตั้ง <strong>dependencies</strong> ทันทีเลยไหม (จัดไปอย่าให้เสีย)</li></ul><p>เพียงเท่านี้ก็ถือว่าเสร็จสิ้นขั้นตอนการ initial ละ โดยโครงสร้างของโปรเจคที่เราได้จะมีหน้าตาแบบนี้</p><pre>/bot<br>+- .firebaserc    # Hidden file that helps you quickly switch between projects with `firebase use`<br>|<br>+- firebase.json  # Describes properties for your project<br>|<br>+- functions/     # Directory containing all your functions code<br>   |<br>   +- .eslintrc.json  # Optional file containing rules for JavaScript linting.<br>   |<br>   +- package.json  # npm package file describing your Cloud Functions code<br>   |<br>   +- index.js      # Main source file for your Cloud Functions code<br>   |<br>   +- node_modules/ # Directory where your dependencies (declared in package.json) are installed</pre><h3>3. ลองสร้างฟังก์ชัน HTTP Requests กับ Event Triggers ให้ทำงานร่วมกัน</h3><h4>3.1 สร้างฟังก์ชันสำหรับ Write และ Update ค่าใน Firestore</h4><p>เริ่มจากไปเปิดไฟล์ /functions/index.js แล้วให้ import ตัว dependency ของ Cloud Functions 2nd Gen, Firestore และ Firebase Admin เข้ามา</p><pre>const {onRequest} = require(&quot;firebase-functions/v2/https&quot;);<br>const {onDocumentCreated} = require(&quot;firebase-functions/v2/firestore&quot;);<br><br>const {initializeApp} = require(&quot;firebase-admin/app&quot;);<br>const {getFirestore} = require(&quot;firebase-admin/firestore&quot;);<br>initializeApp();</pre><p>ถัดไปให้สร้างฟังก์ชัน <strong>addmessage()</strong> ที่เป็นแบบ HTTP Requests โดยโค้ดภายในจะมีการรับ query param ชื่อ text แล้วเอาไป write ลง Firestore ที่ messages/original จากนั้นก็ให้พ่น JSON ซึ่งเป็น id ของ document ออกมา</p><pre>exports.addmessage = onRequest(async (req, res) =&gt; {<br>  const original = req.query.text;<br>  const writeResult = await getFirestore().collection(&quot;messages&quot;).add({original: original});<br>  res.json({result: `Message with ID: ${writeResult.id} added.`});<br>});</pre><p>ถัดไปให้สร้างฟังก์ชัน <strong>makeuppercase()</strong> ที่เป็นแบบ Firebase trigger โดยการทำงานของฟังก์ชันนี้คือจะเฝ้าดู document ที่ถูกสร้างใหม่ขึ้นมา ซึ่งเมื่อมี doument ถูกสร้างใหม่จะส่งข้อมูลของ document นั้นมาที่ฟังก์ชันนี้ แล้วเราก็จะเอา value ที่อยู่ใน original ออกมา เข้าฟังก์ชัน toUpperCase() เพื่อทำให้ตัวหนังสือกลายเป็นพิมพ์ใหญ่ทั้งหมด จากนั้นก็ write กลับไปที่ Firestore</p><pre>exports.makeuppercase = onDocumentCreated(&quot;/messages/{documentId}&quot;, (event) =&gt; {<br>  const original = event.data.data().original;<br>  const uppercase = original.toUpperCase();<br>  return event.data.ref.set({uppercase}, {merge: true});<br>});</pre><h4>3.2 Deploy และทดลองเรียกฟังก์ชัน</h4><p>วิธีการ deploy ให้เราเปิด command line แล้วเข้าไปยังโฟลเดอร์ /functions ในโปรเจค จากนั้นให้ใช้คำสั่ง</p><pre>firebase deploy --only functions</pre><p>ซึ่งเมื่อ deploy เสร็จแล้ว ให้เราเข้าไปที่ <a href="https://console.firebase.google.com/">Firebase console</a> แล้วเลือกโปรเจคที่เราสร้าง จากนั้นให้คลิกเลือกเมนู Build &gt; Functions เราก็จะเห็นฟังก์ชันที่เรา deploy ไปตรงนั้น</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ATyBJP8dLVCX8B4UNhIvJg.jpeg" /></figure><p>จากรูปเราจะ copy ตัว Endpoint URL จากฟังก์ชันที่เป็น HTTP Requests ออกมาเพื่อแล้วนำไปเปิดบน browser เพื่อทดสอบกัน</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mMdBRi_4B0jSkJfkXf6sMw.png" /></figure><p>ซึ่งเมื่อเราเข้าไปดูใน Firestore ก็จะพบข้อมูลทั้งแบบ oginial และ uppercase</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M1O0nHFGsroJ56YZk79QFQ.png" /></figure><p>หมายเหตุ</p><ul><li>กรณีที่นำ Endpoint URL ที่ได้จากฟังก์ชันแบบ HTTP Requests ไปเปิด แล้วเจอว่าติด permission ให้ทำตาม<a href="https://cloud.google.com/run/docs/securing/authenticating">ลิงค์นี้</a> เพื่อเปิดสิทธิ์ให้ทุกคนสามารถเปิด</li><li>การจะใช้งาน Firestore ครั้งแรก จะต้องเข้าไปเลือก location ของ Firestore ใน <a href="https://console.firebase.google.com/">Firebase console</a> ก่อน โดยคลิกเมนู Build &gt; Firestore Database</li></ul><h3>สรุป</h3><p>จากตัวอย่างในข้อ 3 น่าจะทำให้ผู้อ่านทุกท่านเห็นภาพในการสร้างฟังก์ชันทั้งแบบ HTTP Requests และ Event Triggers รวมถึงการทำงานร่วมกันได้ชัดเจนมากขึ้น</p><p>หวังว่าบทความนี้จะทำให้ผู้อ่านทุกท่านได้รู้จัก Cloud Functions for Firebase และเกิดไอเดียในนำมันไปพัฒนาบริการฝั่งหลังบ้านเพื่อเชื่อมกับ Client ในแพลทฟอร์มต่างๆนะครับ</p><p>สุดท้ายนี้ เพื่อไม่ให้พลาดข่าวสารเกี่ยวกับ Firebase ก็ฝากติดตามเพจ <a href="https://www.facebook.com/FirebaseThailand">Firebase Thailand</a> ไว้ หรือถ้าใครอยากแชร์ความรู้ สอบถามเกี่ยวกับ Firebase ก็ไปโพสถามกันได้ที่ <a href="https://www.facebook.com/groups/FirebaseDevTh">Firebase Developer Group Thailand</a> ได้นะครับ แล้วพบกันใหม่ในบทความถัดไป สวัสดีครับ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cdda33bbd7dd" width="1" height="1" alt=""><hr><p><a href="https://medium.com/firebasethailand/cloud-functions-for-firebase-2nd-gen-cdda33bbd7dd">รู้จัก Cloud Functions for Firebase 2nd Gen ตั้งแต่ Zero จนเป็น Hero</a> was originally published in <a href="https://medium.com/firebasethailand">Firebase Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[รู้จัก Callable Function ใน Cloud Functions for Firebase ตั้งแต่ Zero จนเป็น Hero]]></title>
            <link>https://medium.com/firebasethailand/callable-function-c423e75f6ece?source=rss----261beda53e69---4</link>
            <guid isPermaLink="false">https://medium.com/p/c423e75f6ece</guid>
            <category><![CDATA[firebase-emulator-suite]]></category>
            <category><![CDATA[callable-function]]></category>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[cloud-functions]]></category>
            <category><![CDATA[https-callable-function]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Mon, 14 Feb 2022 02:55:00 GMT</pubDate>
            <atom:updated>2024-12-16T09:36:46.856Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="Callable Function in Cloud Functions for Firebase" src="https://cdn-images-1.medium.com/max/1024/1*7KIeePLr5aimUq_HrJFVNA.png" /></figure><p>พูดถึงการรับส่งข้อมูลระหว่าง client และ server ในยุคนี้ เชื่อว่าหลายๆคนก็เลือกใช้บริการที่เป็น BaaS(Backend-as-a-Service) เพราะมันช่วยประหยัดเรื่องของ resource แถมลดเวลาในการพัฒนาได้เยอะ และผมเชื่อว่า Cloud Functions for Firebase ก็เป็นหนึ่งตัวเลือกที่นักพัฒนาไทยนิยมใช้กัน</p><p>โดยวิธีที่นักพัฒนาส่วนมากใช้ในการพัฒนาเพื่อรับส่งข้อมูลระหว่าง client และ server ด้วย Cloud Functions ที่ผมได้เจอมา ก็จะเป็นการสร้าง API แบบที่เรียกว่า HTTP Function ซึ่งรองรับ GET, POST, PUT, DELETE และ OPTIONS แต่…คุณรู้หรือไม่ว่า มันมีวิธีที่ง่ายกว่าในการพัฒนา ที่รู้จักกันในชื่อว่า Callable Function</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OES_AVhOOcv4Kuw_jWDPqw.png" /></figure><h3>Callable Function</h3><p>คือฟังก์ชันที่ให้ client อย่าง Web, Android, iOS, Unity และ C++ สามารถเรียกใช้งานได้ตรงๆ โดยการ deploy ฟังก์ชันแบบ Callable Function คุณจะไม่ได้ Endpoint ของ API ออกมา แต่คุณสามารถเชื่อมต่อ client กับ server ได้ง่ายกว่าเดิมผ่าน SDK โดยความพิเศษใน Callable Function มีดังนี้</p><ul><li>Firebase Auth token, FCM token และ App Check token จะแนบมากับ request ให้อัตโนมัติ(ถ้ามีการ implement บริการเหล่านี้ไว้ที่ฝั่ง client)</li><li>ถ้ามี Firebase Auth token หรือ App Check token แนบมากับ request มันจะถูก validate ให้อัตโนมัติที่ฝั่ง server ซึ่งแปลว่า ถ้า token ถูก revoke หรือ expire มา ตัว request นั้นจะถูก reject กลับไป</li></ul><p>เอาหละครับ บทความนี้ผมจะพาทุกคนไปเรียนรู้เรื่องราวของ Callable Function ตามหัวข้อดังต่อไปนี้</p><ol><li>การพัฒนาฟังก์ชันแบบ Callable Function</li><li>การเชื่อมต่อ Client เพื่อรับส่งข้อมูล</li><li>Tip สำหรับคนอยาก Run ใน Local ด้วย Firebase Emulator Suite</li></ol><h3>1. การพัฒนาฟังก์ชันแบบ Callable Function</h3><h4>1.1 เตรียมความพร้อม</h4><p>ขั้นตอนนี้นักพัฒนาจะต้องมีโปรเจค Firebase, ต้องติดตั้ง Firebase CLI และ Cloud Functions ให้เรียบร้อยก่อน โดยใครที่ยังไม่เคยพัฒนาฟังก์ชันใน Cloud Functions มาก่อน ผมแนะนำให้อ่านและทำตามข้อ 2 ของบทความด้านล่างนี้</p><p><a href="https://medium.com/firebasethailand/cloud-functions-for-firebase-2nd-gen-cdda33bbd7dd">รู้จัก Cloud Functions for Firebase 2nd Gen ตั้งแต่ Zero จนเป็น Hero</a></p><p>แต่สำหรับใครที่มีประสบการณ์การพัฒนาฟังก์ชันใน Cloud Functions มาแล้วให้ข้ามไปข้อถัดไปได้เลย</p><h4>1.2 เริ่มต้นเขียนฟังก์ชัน</h4><p>โครงสร้างของ Callable Function จะมีรูปแบบที่ขึ้นต้นด้วย https.onCall และจะมี params คืนมา 1 ตัวคือ request โดยโค้ดที่จะเขียนลงไปใน index.js ก็จะเป็นประมาณนี้</p><pre>const {onCall, HttpsError} = require(&quot;firebase-functions/v2/https&quot;);</pre><pre>exports.myCallable = onCall(async (request) =&gt; {<br>  // ...<br>})</pre><p>โดยคุณสามารใช้ <strong>request.data </strong>เพื่อดึงค่าต่างๆที่ส่งมาจากฝั่ง client ได้</p><pre>const msg = <strong>request.data</strong>.msg</pre><p>และคุณยังสามารถดึงข้อมูลจาก Firebase Auth ออกมาได้ด้วย(ข้อมูลจะแนบมาอัตโนมัติ กรณีที่มีการใช้งานที่ฝั่ง client)</p><pre><em>// Firebase Auth</em><br>const uid = <strong>request</strong><strong>.auth.uid</strong><br>const name = <strong>request</strong><strong>.auth.token.name</strong> || null<br>const picture = <strong>request</strong><strong>.auth.token.picture</strong> || null<br>const email = <strong>request</strong><strong>.auth.token.email</strong> || null</pre><h4>1.3 การตอบกลับข้อมูล</h4><p>การ return ข้อมูลกลับไปที่ client เราสามารถส่งข้อมูลกลับไปเป็นรูปแบบของ text หรือจะเป็นแบบ JSON ก็ได้ง่ายๆแบบนี้เลย</p><pre><strong>return</strong> {<br>  result: request.data.msg + &quot;, Feb 14th, 2022&quot;<br>};</pre><h4>1.4 การจัดการ Error</h4><p>เราสามารถ throw error ไปพร้อมกับข้อความ กรณีที่ต้องการ return promise reject ได้ด้วย HttpsError(<a href="https://github.com/grpc/grpc/blob/master/doc/statuscodes.md">FunctionsErrorCode</a>, ErrorMessage) เพื่อให้ฝั่ง client สามารถเข้าใจ error และนำไปแสดงผลได้ เช่น</p><pre>if (!request.auth) {<br>  throw new <strong>HttpsError</strong>(<br>    &#39;failed-precondition&#39;,<br>    &#39;The function must be called while authenticated.&#39;<br>  )<br>}</pre><h4>1.5 เลือก Location เพื่อเพิ่มความเร็ว</h4><p>เมื่อคุณ deploy ฟังก์ชัน โค้ดของคุณจะถูกส่งไปทำงานที่เมกา(us-central1) เป็นค่าเริ่มต้น ซึ่งแน่นอนว่าการรับส่งข้อมูลจากไทยไปเมกาจะมี network latency เกิดขึ้น</p><p>ดังนั้นเพื่อลด latency ที่อาจเกิดขึ้น นักพัฒนาสามารถเลือก <a href="https://firebase.google.com/docs/functions/locations">location</a> ที่จะทำการ deploy โดยคำนึงถึงระยะทาง และบริการอื่นๆที่เกี่ยวข้องในฟังก์ชันนั้น เพื่อให้ได้ประสิทธิภาพดีที่สุด ซึ่งวิธีการหลังจากเลือก location ได้แล้วก็เพียงเพิ่ม region ไปในตอนประกาศฟังก์ชันแบบนี้</p><pre>exports.myCallable = onCall({region: &quot;asia-southeast1&quot;}, async (request) =&gt; {<br>  // ...<br>})</pre><h4>1.6 Deploy</h4><p>เมื่อเขียนโค้ดได้ตามต้องการแล้ว ก็ให้เปิด Terminal แล้วเข้าไปที่โฟลเดอร์ /functions ของโปรเจคจากนั้นก็เรียกคำสั่ง deploy ได้เลย</p><pre><strong>firebase deploy</strong></pre><h3>2. การเชื่อมต่อ Client เพื่อรับส่งข้อมูล</h3><p>การเชื่อมต่อเพื่อรับส่งข้อมูลระหว่าง client กับ Callable Function เราจะใช้ Firebase client SDK เข้ามาเป็นตัวกลางในการเชื่อมต่อ โดย minimum SDK ที่รองรับมีดังนี้</p><ul><li>Firebase SDK for Apple platforms 8.11.0</li><li>Firebase SDK for Android 20.0.1</li><li>Firebase JavaScript SDK 8.10.1</li><li>Firebase Modular Web SDK v. 9.0</li></ul><p>สำหรับตัวอย่างในบทความนี้ ผมจะทำการเชื่อมต่อด้วย Firebase Modular Web SDK นะครับ</p><blockquote>สำหรับ <a href="https://firebase.google.com/docs/functions/callable#set_up_your_client_development_environment">JavaScript SDK v8, Android, iOS, Unity และ C++ ดูวิธีการเชื่อมต่อที่นี่</a></blockquote><h4>2.1 ลงทะเบียน Web app</h4><p>ให้เข้าไปที่ <a href="https://console.firebase.google.com/">Firebase console</a> เลือกโปรเจคที่เราสร้างไว้ โดยเมื่อเข้าสู่หน้า Overview ให้คลิก Add appตามรูปด้านล่าง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nmEoEvZ-UZh32kMHqzTF0A.jpeg" /></figure><p>ต่อไปให้คลิกเลือก Web</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5BvlLIjeqUzN1L4d-rP6VA.png" /></figure><p>ตั้งชื่อ Web app ของเราให้ง่ายต่อการจดจำ แล้วกดปุ่ม Register app</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CWY-NFX-AJjEPg-Cxo7Y_A.png" /></figure><p>ขั้นตอนถัดไป ให้เลือกวิธีการพัฒนาที่ถนัด โดยจะมีแบบ npm และ &lt;script&gt; (ตัวอย่างจะเลือกแบบ &lt;script&gt;) จากนั้นให้ copy ชุด JavaScript เอาไว้ แล้วกด Continue to console ได้เลย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Sgo5I5vzyJeyDGeb615_lg.png" /></figure><h4>2.2 เชื่อมต่อ Callable Function กับ SDK</h4><p>จากโค้ดที่เรา copy มาจากขั้นตอนที่ 2.1 ให้เราเพิ่มโค้ดตามขั้นตอนทั้ง 4 นี้ในไฟล์ HTML ที่ต้องการ</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/91f61485dc4c50bfe94e61d2a2d28ff2/href">https://medium.com/media/91f61485dc4c50bfe94e61d2a2d28ff2/href</a></iframe><p><strong>สำคัญมาก</strong>: กรณี Callable Function ที่สร้างในข้อ 1 มีการระบุ location คุณจำเป็นต้องกำหนด location ให้ฝั่ง client ด้วยโดยการแก้ไขโค้ดด้านบนในบรรทัดที่ 24 เป็นดังนี้</p><pre>const functions = getFunctions(app, &quot;<strong>asia-southeast1</strong>&quot;);</pre><blockquote>สำหรับ <a href="https://firebase.google.com/docs/functions/locations#client-side_location_selection_for_callable_functions">JavaScript SDK v8, Android, iOS, Unity, C++ ดูวิธีกำหนด location ที่นี่</a></blockquote><h4>2.3 Deploy และ Demo</h4><p>ถ้าโค้ดพร้อมแล้ว ก็ deploy โลด ปล.ถ้าใครยังไม่มีโฮสฟรีไว้ใช้ ผมนี่แนะนำ Firebase Hosting เลยครับ :)</p><p><a href="https://medium.com/firebasethailand/%E0%B8%A3%E0%B8%B9%E0%B9%89%E0%B8%88%E0%B8%B1%E0%B8%81-firebase-hosting-%E0%B8%95%E0%B8%B1%E0%B9%89%E0%B8%87%E0%B9%81%E0%B8%95%E0%B9%88-zero-%E0%B8%88%E0%B8%99%E0%B9%80%E0%B8%9B%E0%B9%87%E0%B8%99-hero-1f999edfe8ea">รู้จัก Firebase Hosting ตั้งแต่ Zero จนเป็น Hero</a></p><p>เสร็จแล้วมาลองเอา URL ที่ได้ไปเปิดใน browser ดูผลลัพธ์กัน</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gvwTS7OTUFg75ecHJ26vkg.png" /></figure><h3>3. Tip สำหรับคนอยาก Run ใน Local ด้วย Firebase Emulator Suite</h3><p>เชื่อว่านักพัฒนาหลายคนที่ใช้ Firebase ต่างก็ใช้ <a href="https://firebase.google.com/docs/emulator-suite">Firebase Emulator Suite</a> เพื่อทดสอบโปรเจคของตัวเองใน local เพราะมันรวดเร็ว แก้ปุ๊บเห็นผลปั๊บไม่ต้อง deploy ไม่กระทบข้อมูลใน Production</p><p>สำหรับ Callable Function ก็สามารถพัฒนาใน local ได้เช่นกัน เพียงแต่การ run ฝั่ง client ที่เชื่อมต่อ เราจะต้องมีการเขียนโค้ดเพิ่มเข้าไปนิสนึง โดยโค้ดจะอ้างอิงจากข้อ 2.2 บรรทัดที่ 13 ให้คุณเพิ่ม connectFunctionsEmulatorเข้าไป</p><pre>import { getFunctions, httpsCallable, <strong>connectFunctionsEmulator</strong> } from &quot;https://www.gstatic.com/firebasejs/9.6.6/firebase-functions.js&quot;;</pre><p>และเพิ่มคำสั่งด้านล่างนี้ ต่อจากบรรทัดที่ 24</p><pre>const functions = getFunctions(app);<br><strong>if (location.hostname === &quot;localhost&quot;) {</strong> <br>  <strong>connectFunctionsEmulator(functions, &quot;localhost&quot;, 5001);<br>}</strong></pre><blockquote>สำหรับ <a href="https://firebase.google.com/docs/emulator-suite/connect_functions#instrument_your_app_for_callable_functions">JavaScript SDK v8, Android และ iOS ดูวิธีกำหนด config ที่นี่</a></blockquote><h3>สรุป</h3><p>เป็นอย่างไรบ้างครับสำหรับ Callable Function นอกจากจะพัฒนาง่ายแล้ว ส่วนตัวผมยังชอบที่เราไม่ต้องระบุ Endpoint ของ API ลงไปในโค้ดของฝั่ง client เลย(ดูเท่ และปลอดภัยขึ้นไปอีกนิสนึง)</p><p>ก่อนจากกันไป หากคุณชอบบทความนี้ก็ฝากกด <em>Clap</em> เป็นกำลังใจให้ผม หากมันเป็นประโยชน์ก็ฝากกด <em>Share</em> ให้เพื่อนของคุณ และหากคุณไม่อยากพลาดบทความถัดๆไปของซีรี่ย์ Zero to Hero ก็ฝากกด <em>Follow</em>: <a href="https://medium.com/firebasethailand">Firebase Thailand</a> ไว้ด้วยนะครับ สำหรับวันนี้ต้องขอตัวลาไปก่อน แล้วพบกันใหม่บทความหน้าครับ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c423e75f6ece" width="1" height="1" alt=""><hr><p><a href="https://medium.com/firebasethailand/callable-function-c423e75f6ece">รู้จัก Callable Function ใน Cloud Functions for Firebase ตั้งแต่ Zero จนเป็น Hero</a> was originally published in <a href="https://medium.com/firebasethailand">Firebase Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Deploy เว็บของคุณแบบไร้รอยต่อด้วย Firebase Hosting และ GitHub Actions]]></title>
            <link>https://medium.com/firebasethailand/firebase-hosting-x-github-actions-672c05831588?source=rss----261beda53e69---4</link>
            <guid isPermaLink="false">https://medium.com/p/672c05831588</guid>
            <category><![CDATA[firebasehosting]]></category>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[continuous-delivery]]></category>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[continuous-deployment]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Sat, 15 May 2021 07:17:14 GMT</pubDate>
            <atom:updated>2021-05-15T07:16:47.464Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N1_xozPuQ53OiWtCbrmsxg.jpeg" /></figure><p>นักพัฒนาที่ใช้งาน <strong>GitHub</strong> อยู่น่าจะรู้จัก <a href="https://docs.github.com/en/actions">GitHub Actions</a> บริการที่เข้ามาช่วยจัดการ Development workflow ใน Repo ของเรา ให้มีระบบ CI/CD แบบอัตโนมัติได้</p><p>ด้วยเสียงตอบรับที่ดีของบริการดังกล่าว ทีม Firebase จึงไม่รอช้าที่จะออกฟีเจอร์ใหม่ซึ่งเป็นการ Integrate กันระหว่าง <strong>Firebase Hosting</strong> และ <strong>GitHub Actions</strong> เพื่อมาช่วยให้การ deploy เว็บของเรามีความ automate โดยที่นักพัฒนาไม่จำเป็นต้องมีความรู้ในการติดตั้ง GitHub Actions มาก่อน เพราะ Firebase CLI เค้าจะจัดให้</p><h3>การ Deploy จาก GitHub Actions ไปยัง Firebase Hosting</h3><p>การ deploy จาก GitHub Actions ที่จะแสดงในบทความนี้ จะเป็นแบบ default ที่ได้หลังจากการติดตั้ง ซึ่งจะมีด้วยกัน 2 รูปแบบ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t96P7x-eWzgORJbU3Vz36w.png" /></figure><h4>Preview Channel</h4><p>เกิดจากการเปิดหรือแก้ไข <strong>Pull Request(PR)</strong> โดยระบบจะ deploy ไปยัง Preview channel ของ Firebase Hosting ซึ่งผลลัพธ์เราจะได้ <strong>temporary URL</strong> ซึ่งแต่ละ PR จะได้ URL ที่แตกต่างกัน</p><p><strong>หมายเหตุ</strong>: การแก้ไขโค้ดใน PR เดิม โค้ดจะถูก deploy ไปยัง Preview URL เดิม</p><h4>Live Channel</h4><p>เกิดจากการ <strong>Merge PR</strong> หรือ <strong>Push</strong> โค้ดเข้าไปที่ <strong>main repo</strong> โดยระบบจะ deploy ไปยัง Live channel ของ Firebase Hosting หรือเรียกง่ายๆว่าขึ้น <strong>Production</strong> นั่นเอง</p><p>มาถึงตรงนี้ เชื่อว่าทุกคนน่าจะคันไม้คันมือกันแล้ว อย่าได้ช้าที เราไปลงมือทำแบบ Step by Step กับ 7 ขั้นตอนกันเลย!</p><h3>1. เตรียม GitHub Repository</h3><p>ให้เราเตรียม GitHub repo โดยจะมาจากการ<a href="https://github.com/new">สร้างใหม่</a> หรือจะใช้ repo ที่มีอยู่แล้วก็ได้ ซึ่งจะเป็น repo แบบส่วนตัว หรือ organization ก็ได้ จากนั้นก็ให้ clone มาไว้ที่เครื่องของเรา</p><p><strong>หมายเหตุ</strong>: สิทธิ์ของ repo ที่จะนำมาใช้จะต้องเป็นสิทธิ์ admin เท่านั้น</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PMynE2aczabR9bcCIQBNnA.png" /></figure><p>ถัดไปเราจะสร้าง branch ใหม่ใน <strong>local</strong> โดยผมจะตั้งชื่อว่า new-feature และเราจะลุยกันต่อใน branch นี้ครับ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZKREZfHWVbFeoshfKBVFvQ.png" /><figcaption>หน้าจอแสดงการสร้าง branch ใหม่ใน <a href="https://desktop.github.com/">GitHub Desktop</a></figcaption></figure><h3>2. สร้าง Firebase Project</h3><p>ให้เข้าไปที่ <a href="https://console.firebase.google.com/">Firebase console</a> และ sign in ด้วย Google account จากนั้นกด <strong>Add project</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3XdiZ1XnkxCTesukW3yXCQ.png" /></figure><p>ตั้งชื่อโปรเจคให้เรียบร้อย แล้วกดปุ่ม <strong>Continue</strong> จากนั้นให้ข้ามขั้นตอนการตั้งค่า Google Analytics ไปก่อน เนื่องจากเราไม่ได้ใช้ในโปรเจคนี้</p><h3>3. เปิดใช้งาน Firebase Hosting API</h3><p>เนื่องจากการ GitHub Actions จะทำการ deploy โค้ดของเราผ่านทาง REST API ไปยัง Firebase Hosting ดังนั้นต้องให้แน่ใจว่าบริการนี้ถูกเปิดใช้งานแล้ว โดยให้เราไปที่หน้ารวม Library ใน <a href="https://console.cloud.google.com/apis/library">Google Cloud console</a> จากนั้นให้เลือกโปรเจคเดียวกับที่เราสร้างไว้ใน <a href="https://console.firebase.google.com/">Firebase console</a> แล้วค้นห้าคำว่า Firebase Hosting API ก็จะเจอหน้าจอ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Dvgg-fcYUAK9j7PavoXQ2A.png" /></figure><p>ให้กดปุ่ม <strong>Enable</strong> เพื่อเปิดใช้บริการ โดยหากปุ่มนี้แสดงคำว่า Manage นั่นแปลว่าเราได้เปิดใช้งานไว้แล้ว</p><h3>4. ติดตั้ง Node.js และ Firebase CLI</h3><h4>4.1 ติดตั้ง Node.js</h4><p>เนื่องจาก Firebase CLI จำเป็นต้องมี <strong>Node.js</strong> และ <strong>npm</strong>(มาพร้อมกับการติดตั้ง Node.js) ดังนั้นให้เข้าไปที่เว็บไซต์ <a href="https://nodejs.org/">nodejs.org</a> แล้วดาวน์โหลด Node.js เวอร์ชันที่เป็น LTS หรือ Current มาติดตั้งให้เรียบร้อย</p><p>เมื่อติดตั้งเสร็จแล้ว ให้เปิด Terminal มาทดสอบด้วยคำสั่งด้านล่างนี้ ซึ่งถ้าสำเร็จจะเห็นตัวเลขเวอร์ชันของทั้งคู่</p><pre><strong>node --version <br>npm --version</strong></pre><h4>4.2 ติดตั้ง Firebase CLI</h4><p>ต่อกันที่ Terminal ให้เรา run คำสั่ง</p><pre><strong>npm install -g firebase-tools</strong></pre><p><strong>หมายเหตุ</strong>: หากพบปัญหาในการติดตั้งในขั้นตอนนี้ คุณจำเป็นตัองตั้งค่า <a href="https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally">npm permission</a> ในเครื่องซะก่อน</p><p>จากนั้นให้ตรวจสอบความเรียบร้อยด้วยคำสั่งด้านล่างนี้ ซึ่งถ้าผ่านจะต้องเห็นเลขเวอร์ชัน</p><pre><strong>firebase --version</strong></pre><h3>5. Initial ตัว Firebase Hosting และ GitHub Actions</h3><p>เริ่มต้นให้ run คำส่ังด้านล่างนี้ ใน Terminal โดย browser จะเปิดขึ้นมาแล้วให้คุณ sign in ด้วย account เดียวกับที่สร้างโปรเจคใน Firebase โดยหากสำเร็จจะเห็นอีเมลตัวเองปรากฎอยู่ใน Terminal</p><pre><strong>firebase login</strong></pre><p>ถัดไปให้ shell ผ่าน Terminal เข้าไปยัง root ของโฟลเดอร์ที่เรา clone มาจาก GitHub แล้ว run คำสั่ง</p><pre><strong>firebase init hosting</strong></pre><p><strong>หมายเหตุ</strong>: หากเคยติดตั้ง Firebase Hosting มาก่อนหน้านี้ ให้ใช้คำสั่ง <strong>firebase init hosting:github</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*F_5AukTG5OYdFdRoYLMq6A.png" /></figure><p>เลือก <strong>Use an existing project</strong> แล้ว Enter เบาๆ จากนั้นจะเจอชื่อโปรเจคที่เราได้สร้างไว้ ก็กดเลือกแล้ว Enter ต่อเลย แล้วจะเจอชุดคำถามตามนี้</p><ul><li>ตั้งชื่อโฟลเดอร์ที่เก็บไฟล์เว็บของเราว่า public ไหม: <strong>ถ้าเอาก็กด Enter ได้เลย</strong></li><li>จะตั้งค่า single-page เว็บไหม: <strong>ตอบ N ตามคำแนะนำ</strong></li><li>ติดตั้งการ deploy ด้วย GitHub ไหม: <strong>ตรงนี้ตอบ y นาจา</strong></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dXg0mMEIQojYbKzch0Fmgw.png" /></figure><p>หลังตอบ y ตัว browser จะเปิดขึ้นมา ก็ให้เรา sign in ด้วย GitHub account ให้เรียบร้อย เสร็จแล้วก็กลับมาที่ Terminal จะพบว่าให้เราระบุ repo ที่ต้องการเชื่อมต่อกับ GitHub Actions ในรูปแบบ <strong>user/repository</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fz2zxr7C1Kfk_qpHeWIIcw.png" /></figure><p>หลังจากกด Enter ชื่อ repo เราไป ตัว Firebase CLI ก็จะไปสร้าง service account สำหรับการอนุญาตให้ GitHub Actions ทำการ deploy ไฟล์ขึ้นไป Firebase Hosting ได้ โดยสามารถเข้าไปดูที่เมนู Settings-&gt;secrets ใน GitHub repo ของเรา</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PA8yy2bWwYRI1Uqu2s5GnQ.png" /></figure><p>กลับมาที่ Terminal จะ 2 คำถามสุดท้ายละ</p><ul><li>จะให้ run script อะไรเพิ่มเติมก่อนการ deploy ไหม เช่น script สำหรับ build หรือ test: <strong>ถ้าไม่มีก็ให้ตอบ N ไป</strong></li><li>ถ้า Merge PR จะให้ deploy ขึ้น Live channel เลยไหม: <strong>ตอบ Y</strong></li><li>ฺBranch ที่จะเอามาเชื่อมกับ Live channel ชื่ออะไร: ถ้าใช้ main ก็แค่ Enter ไป</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qph2quyL6UOLitMETsDXXg.png" /></figure><p>เสร็จแล้วก็แอบไปส่องไฟล์ในโฟลเดอร์ดูหน่อย จะสั่งเกตเห็นโฟลเดอร์ที่เป็น hidden ชื่อ <strong>.github</strong> เข้าไปเลยจะเจอโฟลเดอร์ <strong>workflows</strong> แล้วก็เข้าไปอีก จะเจอไฟล์ 2 ไฟล์ที่มีนามสกุล <strong>yml</strong> นั่นหละครับคือ script สำหรับการรัน GitHub Actions ซึ่งเราไม่ต้องเขียนเองเลย สบ๊าย!</p><h3>6. ทดลองเปิด PR</h3><p>หลังจากเราได้ Initial โปรเจคเราเรียบร้อย ใน branch ที่ชื่อ new-feature ก็ให้เรา commit โค้ดทั้งหมด และ publish ตัว branch นี้ขึ้น GitHub ไปเลย</p><p>จากนั้นก็เข้าไปที่ Repo ของเราใน GitHub เราก็จะเห็น notice ว่า branch ใหม่เรามาแว้ว เอ้า รอรัยอะ กดปุ่มเขียวๆที่ชื่อ <strong>Compare &amp; pull request</strong> โลด</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mFiPW_PTvpaUekY8X815hg.png" /></figure><p>จากนั้นก็ใส่ comment ตามสะดวกแล้วกด <strong>Creat pull request</strong> ต่อเลย ที่เหลือก็รอเวลาเพียงชั่วเคี้ยวหมากแหลก หากสำเร็จคุณก็จะเห็นลิงก์ของ <strong>Preview channel</strong> ปรากฎล้าาาว</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z1vxiH4pzxlQlLwpDhyJkw.png" /></figure><p>สุดท้ายก็เอา Preview URL มาลองเปิดหน่อยซิ…Ta Da!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z1rQZzIqDsjVY5KbCbrP3Q.png" /></figure><p><strong>หมายเหตุ</strong>: Preview URL จะสามารถใช้งานได้ 7 วัน(default) หากต้องการเพิ่มหรือลดเวลา สามารถตั้งค่า <strong>expires</strong> ได้ที่ไฟล์ <strong>firebase-hosting-pull-request.yml</strong> ในโฟลเดอร์ .github/ (ต่ำสุด 1h สูงสุด 30d)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G8VLmzAd5y_Mx778-kmTOA.png" /><figcaption>firebase-hosting-pull-request.yml</figcaption></figure><h3>7. ทดลอง Merge PR เข้า Main Repository</h3><p>ขั้นตอนสุดท้าย เมื่อเราโอเคกับ PR ที่เปิดเข้ามาแล้ว ก็ได้เวลา Merge มันเข้าไปที่ main ซึ่งเป็น branch หลักของเรา…ในหน้า PR ด้านล่างจะมีปุ่ม <strong>Merge pull request</strong> อยู่ กดมันเบาๆ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gk7W6ySimEJ3bekiPKGFoA.png" /></figure><p>จากนั้นไปติดตามสถานะการ build และ deploy มันใน tab ที่ชื่อ Actions</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XAqjVzrW3ddb6p8nH6CFLA.png" /></figure><p>ในหน้านี้จะสังเกตเห็นสถานะของงาน ตั้งแต่ตอนเปิด PR และตอน Merge โดยจะแบ่งสถานะออกเป็น 3 สี</p><ul><li>🟡 กำลังทำงาน</li><li>🔴 เกิดข้อผิดพลาด</li><li>🟢 เสร็จสมบูรณ์</li></ul><p>ซึ่งเราสามารถกดเข้าไปดูรายละเอียดภายในแต่ละงานได้ เผื่อในกรณีที่เกิดข้อผิดพลาดจะได้ทราบสาเหตุว่าเกิดจากอะไร</p><p>เอาหละครับ และนี่คือ URL ของ Live channel ของเราในบทความนี้ เย้!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FIUQa6O2MSOXLURKKYP9lw.png" /></figure><h3>สรุป</h3><p>ชอบกันไหมครับกับฟีเจอร์ใหม่ที่มาแนะนำกันในบทความนี้ ส่วนตัวผมชอบตอนที่เปิด PR แล้วได้ Preview URL มานี่เหมาะมากกับการนำไปทดสอบ หรือส่งไปให้ลูกค้า UAT เพราะเราไม่ต้องไปนั่งหา domain ใหม่ รู้ที่มาที่ไปของ URL ว่ามาจาก PR ไหน แถมควบคุมเวลาการ expire ได้ด้วย</p><p>สำหรับ GitHub Actions จะมีการคำนวนการใช้งานจากระยะเวลาที่มันทำงาน โดยแพ็คเกจ Free ก็ให้เราใช้ <strong>ฟรี</strong> เลยสำหรับ <strong>Public repo </strong>ส่วนถ้าเป็น <strong>private repo</strong> จะให้ใช้ฟรีได้ <strong>2,000 นาที/เดือน</strong> ซึ่งหากใครสนใจใช้เกินกว่านี้ก็ลองดูรายละเอียดของ <a href="https://github.com/pricing">Price plan</a> นะครับ</p><p><a href="https://medium.com/firebasethailand/%E0%B8%A3%E0%B8%B9%E0%B9%89%E0%B8%88%E0%B8%B1%E0%B8%81-firebase-hosting-%E0%B8%95%E0%B8%B1%E0%B9%89%E0%B8%87%E0%B9%81%E0%B8%95%E0%B9%88-zero-%E0%B8%88%E0%B8%99%E0%B9%80%E0%B8%9B%E0%B9%87%E0%B8%99-hero-1f999edfe8ea">รู้จัก Firebase Hosting ตั้งแต่ Zero จนเป็น Hero</a></p><p>ก่อนจากกันไป ขอฝากชาว Firebasers ให้กดติดตาม <a href="https://medium.com/firebasethailand">Firebase Thailand</a> กันด้วยนะครับ เพื่อที่ทุกท่านจะได้ไม่พลาดบทความตอนใหม่ๆ แล้วพบกันใหม่บทความหน้าราตรีสวัสดิ์พี่น้องนักพัฒนาชาวไทย</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=672c05831588" width="1" height="1" alt=""><hr><p><a href="https://medium.com/firebasethailand/firebase-hosting-x-github-actions-672c05831588">Deploy เว็บของคุณแบบไร้รอยต่อด้วย Firebase Hosting และ GitHub Actions</a> was originally published in <a href="https://medium.com/firebasethailand">Firebase Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[มาสร้าง CI บน Android โดยใช้ GitHub Actions เพื่อทำ UI Test ด้วย Firebase Test Lab แบบเท่ ๆ กัน]]></title>
            <link>https://medium.com/firebasethailand/build-android-ci-with-github-actions-and-firebase-test-labs-cf172aee192d?source=rss----261beda53e69---4</link>
            <guid isPermaLink="false">https://medium.com/p/cf172aee192d</guid>
            <category><![CDATA[continuous-integration]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[firebase-test-lab]]></category>
            <dc:creator><![CDATA[Akexorcist]]></dc:creator>
            <pubDate>Sun, 04 Apr 2021 10:37:23 GMT</pubDate>
            <atom:updated>2021-04-07T11:15:37.035Z</atom:updated>
            <content:encoded><![CDATA[<p>เพราะในโลกของการพัฒนาซอฟต์แวร์ที่ไม่ได้พัฒนาขึ้นมาจากโค้ดของคนเพียงคนเดียวนั้นมีโอกาสเกิดข้อผิดพลาดได้อยู่เสมอ รวมไปถึงการพัฒนาแอปแอนดรอยด์ก็เช่นกัน</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IgoIkjwd7aTyfj3dHHLcvw.jpeg" /></figure><p>จึงทำให้ในปัจจุบันได้มีการนำ Continuous Integration หรือ CI มาใช้ควบคู่ไปกับการเขียนเทสให้กับโค้ดในซอฟต์แวร์นั้น ๆ ด้วย เพื่อทำให้ซอฟต์แวร์สามารถทำงานได้ถูกต้องอยู่เสมอ ไม่ว่าโค้ดเหล่านั้นจะอยู่มานานเท่าไร หรือมีการเปลี่ยนคนเขียนโค้ดนั้นไปแค่ไหน</p><h3>การเขียนเทสบน Android</h3><p>นิยมแบ่งออกเป็น 2 ส่วนใหญ่ ๆ ด้วยกัน คือส่วนที่เป็น Unit Test สำหรับทดสอบ Logic ที่อยู่ในโค้ด และส่วนที่เป็น UI Test เพื่อทดสอบ​การแสดงผลภายในแอป</p><p>โดย Unit Test จะใช้ JVM ในการทำงาน จึงสามารถรันบนเครื่องที่ใช้เขียนโค้ดได้เลย แต่สำหรับ UI Test จะต้องใช้อุปกรณ์แอนดรอยด์ (Physical Device หรือ Emulator ก็ได้) เพื่อทำการทดสอบ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pDbQcsRATaTGatLUlVAErA.jpeg" /></figure><p>สำหรับ UI Test ที่ต้องใช้อุปกรณ์แอนดรอยด์เพื่อทดสอบ ก็จะมีเรื่องของ Fragmentation เข้ามาเกี่ยวข้องด้วย ทำให้การใช้อุปกรณ์แอนดรอยด์เพียงเครื่องเดียวอาจจะไม่เพียงพอต่อการทดสอบ จึงต้องมีการใช้อุปกรณ์แอนดรอยด์มากกว่าหนึ่งเครื่องที่มีสเปคที่แตกต่างกัน เพื่อให้ครอบคลุมกับเครื่องของผู้ใช้ให้หลากหลาย</p><p>แต่การหาอุปกรณ์เหล่านั้นมาทดสอบก็อาจจะไม่ใช่เรื่องง่ายสำหรับทุกคนเสมอไป จึงทำให้ทาง Firebase มีบริการที่ชื่อว่า <a href="https://firebase.google.com/docs/test-lab">Firebase Test Lab</a> เพิ่มเข้ามา โดยทำหน้าที่เป็น Device Farm เพื่อให้นักพัฒนาส่งแอปขึ้นไปทดสอบได้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kenV_uu3ta_tMDkaP0kjdw.jpeg" /></figure><p>ข้อดีคือนักพัฒนาไม่ต้องเสียเวลาไปสร้าง Device Farm เพราะ Firebase Test Lab ได้เตรียมไว้ให้หมดแล้ว ไม่ว่าจะเป็นการทำ Device Matrix, การบันทึกหน้าจอระหว่างรันเทส, การแสดง Log หรือ Stack Trace, และอื่น ๆ อีกมากมาย</p><p>และ Firebase Test Lab จะมีค่าบริการในการใช้งานด้วย ซึ่งจะคิดตามระยะเวลาที่ใช้ในการทดสอบแอป โดยแยกราคาระหว่าง Physical Device กับ Virtual Device (Emulator)</p><h3>การทำ Continuous Integration สำหรับ Android</h3><p>ในปัจจุบันสามารถทำได้หลายวิธี ไม่ว่าจะสร้างขึ้นมาบนระบบของตัวเองอย่างการใช้ Jenkins หรือการใช้ 3rd Party Service อย่าง Travis CI หรือ Circle CI รวมไปถึง Git Repository แต่จะเจ้าที่รองรับ CI/CD ในตัว ซึ่งแต่ละแบบก็จะมีข้อดี ข้อเสีย และความเหมาะสมที่แตกต่างกันออกไป</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CSgyDAkoqZe1tm4AI-Z8lw.jpeg" /></figure><p>โดยบทความนี้เลือกที่จะใช้ <a href="https://github.com/features/actions">GitHub Actions</a> ด้วยเหตุผลที่ว่า GitHub เป็น Git Repository ที่นักพัฒนาทั่วโลกนิยมใช้งานมากที่สุด จึงเหมาะกับนักพัฒนาทุก ๆ คน ไม่ว่าจะเป็นโปรเจคของบริษัท, โปรเจคที่เป็น Open Source, หรือโปรเจคส่วนตัวก็ตาม</p><h3>โปรเจคตัวอย่างที่จะใช้ในบทความนี้</h3><p>ถ้าโปรเจคของคุณมีการเขียนเทสไว้แล้ว ก็สามารถใช้โปรเจคนั้นทำไปพร้อม ๆ กับบทความนี้ได้เลย แต่ถ้ายังไม่มีก็สามารถใช้โปรเจคตัวอย่างของผมได้เช่นกัน เป็นโปรเจคที่เขียนขึ้นมาแบบง่าย ๆ เพื่อให้มีทั้ง Unit Test และ UI Test</p><p><a href="https://github.com/akexorcist/EvenOdd">akexorcist/EvenOdd</a></p><p>สามารถ Fork และ Clone เพื่อเอาไปทำตามได้เลย โดยให้ใช้ Branch ที่ชื่อ main ซึ่งเป็น Branch ที่ยังไม่ได้เพิ่ม .yml ของ GitHub Actions เข้าไปในโปรเจค</p><p>แอปตัวนี้จะให้ผู้ใช้พิมพ์ตัวเลขใด ๆ ก็ได้ แล้วแอปจะแสดงผลลัพธ์ว่าเป็นเลขคู่หรือเลขคี่ โดยที่</p><ul><li><strong>เลขคู่</strong> : แสดงคำว่า “is even” และคำว่า “even” จะเป็นสี Indigo 400 (#5C6BC0)</li><li><strong>เลขคี่</strong> : แสดงคำว่า “is odd” และคำว่า “odd” จะเป็นสี Pink 400 (#EC407A)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*5OV3DLKqZslN_aoYr4lAtg.gif" /></figure><p>โดย Unit Test จะอยู่ใน MainViewModelTest.kt และ UI Test จะอยู่ใน MainActivityUiTest.kt</p><h3>สร้าง Workflow ให้กับ GitHub Actions</h3><p>ในการสร้าง Workflow ให้กับ GitHub Actions จะกำหนดผ่าน YAML โดยเพิ่มไฟล์ .yml ไว้ใน &lt;your_project&gt;/.github/workflow/ ซึ่งผู้อ่านสามารถตั้งชื่อไฟล์ได้ตามใจชอบ</p><blockquote>ในขั้นตอนนี้จะเพิ่มไฟล์ผ่าน Editor ของ GitHub Actions หรือใน Android Studio ก็ได้</blockquote><p>โดยผมจะขอตั้งชื่อไฟล์ว่า android.yml</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/1*GTs-EygUVmid_Ftqzx06Yw.jpeg" /></figure><blockquote>และเพื่อให้ผู้อ่านเข้าใจ Script ในแต่ละส่วนมากขึ้น ผมจะค่อย ๆ เพิ่มและอธิบาย Script ในแต่ละส่วนให้นะ</blockquote><h4>เริ่มจาก Script พื้นฐานที่จำเป็นสำหรับ Workflow</h4><p>ซึ่งจะมีลักษณะแบบนี้</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/2c561ecbd72c543f7e98db8573456c15/href">https://medium.com/media/2c561ecbd72c543f7e98db8573456c15/href</a></iframe><ul><li>name : ชื่อของ Workflow ที่สามารถกำหนดได้ตามใจชอบ</li><li>on : กำหนด Trigger Event ที่จะใช้ Workflow ทำงาน โดยในตัวอย่างนี้ผมกำหนดให้ทำงานตอนที่มีการ Push หรือ Pull Request ใน Branch ที่ชื่อว่า main นั่นเอง</li><li>jobs : การทำงานต่าง ๆ ที่จะให้ทำใน Workflow ตัวนี้ โดยผมจะแบ่งเป็น 3 Job ด้วยกันคือ test, apk, และ firebase</li></ul><h4>Job แต่ละตัวทำหน้าที่อะไรกันบ้าง?</h4><ul><li>test : รัน Unit Test</li><li>apk : Build APK สำหรับ UI Test</li><li>firebase : ส่ง APK ที่ได้จาก apk ขึ้นไปรัน UI Test บน Firebase Test Lab</li></ul><p>ดังนั้นเราจะให้ firebase ทำงานหลังจาก apk ส่วน test แยกออกมาจาก Job อื่น</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DfcP19EOcgApGR5Z_4-aCg.jpeg" /></figure><p>โดยทุก Job จะกำหนดให้รันอยู่บน Ubuntu เวอร์ชันล่าสุด (Latest) และใช้ JDK เวอร์ชัน 1.8 ซึ่งจะต้องกำหนดไว้ใน Job แต่ละตัวอีกที</p><h4>กำหนด Script ให้กับ Job สำหรับ Unit Test</h4><p>ใน Job สำหรับ Unit Test จะมี Script ดังนี้</p><ul><li>Checkout</li><li>ติดตั้ง JDK 1.8</li><li>กำหนดให้ gradlew เป็น Executable File</li><li>ใช้คำสั่ง ./gradlew test เพื่อรัน Unit Test</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/20823eda089354bca65a59bf621e7632/href">https://medium.com/media/20823eda089354bca65a59bf621e7632/href</a></iframe><h4>กำหนด Script ให้กับ Job สำหรับ Build APK เพื่อใช้ใน UI Test</h4><p>การสร้างไฟล์ APK สำหรับ UI Test นั้น จะต้องสร้างไฟล์ APK ขึ้นมา 2 ไฟล์ด้วยกัน</p><ul><li>APK ของแอป</li><li>APK สำหรับ Instrumented Test (UI Test)</li></ul><p>โดยอุปกรณ์แอนดรอยด์จะต้องติดตั้ง APK ทั้ง 2 ไฟล์นี้ถึงจะรัน UI Test ได้</p><p>และใน Job สำหรับ Build APK เพื่อใช้ใน UI Test จะมี Script ดังนี้</p><ul><li>Checkout</li><li>ติดตั้ง JDK 1.8</li><li>กำหนดให้ gradlew เป็น Executable File</li><li>ใช้คำสั่ง ./gradlew assembleDebug เพื่อ Build Debug APK</li><li>Upload Debug APK ไปเก็บไว้ใน Artifact เพื่อส่งต่อให้ Job ถัดไป โดยกำหนดชื่อเป็น app-debug</li><li>ใช้คำสั่ง ./gradlew assembleDebugAndroidTest เพื่อ Build Debug Instrumented APK</li><li>Upload Debug Instrumented APK ไปเก็บไว้ใน Artifact เพื่อส่งต่อให้ Job ถัดไป โดยกำหนดชื่อเป็น app-debug-androidTest</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1e13ffa5f9a21fc9de100cb6691bd3be/href">https://medium.com/media/1e13ffa5f9a21fc9de100cb6691bd3be/href</a></iframe><h4>กำหนด Script ให้กับ Job สำหรับรัน UI Test บน Firebase Test Labs</h4><p>การเรียกใช้งาน Firebase Test Lab บน GitHub Actions จะเรียกผ่าน Google Cloud ซึ่งจะต้องใช้ข้อมูลจาก Firebase อยู่ 2 อย่างด้วยกัน</p><ul><li>Project ID ของ Firebase Project ที่ต้องการใช้งาน Firebase Test Lab</li><li>Service Key ที่เป็น Private Key จาก Firebase Project ที่ Encode ด้วย Base64</li></ul><p>โดยค่าทั้ง 2 ตัวนี้จะไม่กำหนดไว้ใน Script โดยตรง เพราะเป็นข้อมูลสำคัญในการเข้าใช้งาน Firebase Project นั้น ๆ แต่จะเรียกผ่าน Repository Secrets ที่กำหนดไว้ใน Repository Settings บน GitHub แทน ซึ่งจะพูดถึงเรื่องนี้ทีหลัง</p><p>และใน Job สำหรับรัน UI Test บน Firebase Test Lab จะมี Script ดังนี้</p><ul><li>Checkout</li><li>Download Debug APK ที่อยู่ใน Artifact ที่ใช้ชื่อว่า app-debug</li><li>Download Debug Instrumented APK ที่อยู่ใน Artifact ที่ใช้ชื่อว่า app-debug-androidTest</li><li>เข้าใช้งาน Google Cloud เวอร์ชัน 270.0.0 ด้วย Service Key ที่กำหนดไว้ใน Repository Secrets นั้น ๆ</li><li>กำหนด Project ID ของ Firebase ที่จะเรียกใช้งาน Firebase Test Lab</li><li>สั่งให้ Firebase Test Lab เริ่มทำงาน โดยใช้ไฟล์ APK ที่ Download มาจาก Artifact ในขั้นตอนก่อนหน้านี้</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/53276c2eda989469c433f37fe88c6692/href">https://medium.com/media/53276c2eda989469c433f37fe88c6692/href</a></iframe><p>จะเห็นว่าคำสั่งรัน UI Test บน Firebase Test Lab จะต้องกำหนด --type เป็น Instrumentation ด้วย และ Path ของไฟล์ APK ทั้ง 2 ไฟล์จะถูกกำหนดไว้ใน --app และ --test โดยอุปกรณ์แอนดรอยด์ที่ใช้รัน UI Test จะเป็น Pixel 2 ที่เป็น API Level 30</p><h4>อย่าเพิ่ง Push ขึ้น Repository นะ</h4><p>ยังมีขั้นตอนอื่น ๆ ที่ต้องทำให้เรียบร้อยก่อน ดังนั้นอย่าเพิ่ง Push Script ชุดนี้ขึ้น Repository ไม่เช่นนั้น Workflow จะเริ่มทำงานทันที (แล้วก็ Failed)</p><h3>การกำหนดอุปกรณ์แอนดรอยด์เพื่อใช้ทดสอบบน Firebase Test Lab</h3><p>บน Firebase Test Lab จะสามารถกำหนดอุปกรณ์แอนดรอยด์ที่ต้องการใช้ทดสอบได้อยู่ 2 แบบด้วยกันคือ</p><ul><li>กำหนดข้อมูลของเครื่องโดยตรง</li><li>กำหนดด้วย Device Matrix</li></ul><p>จาก Script ก่อนหน้าจะเห็นว่าผมได้ระบุเครื่องที่ต้องการทดสอบเป็น Pixel 2 เลย ซึ่งวิธีนี้จะต้องกำหนด Model ID ของเครื่องที่ต้องการลงไปใน --device โดย Pixel 2 จะมี Model ID เป็น Pixel2</p><blockquote>อุปกรณ์แอนดรอยด์บน Firebase Test Lab สามารถเปลี่ยนแปลงได้เสมอ</blockquote><p>ถ้าอยากจะรู้ว่ามีอุปกรณ์แอนดรอยด์รุ่นไหนให้ใช้งานได้บ้าง จะต้องใช้คำสั่ง gcloud firebase test android models list จากเครื่องของเราเอง เพื่อแสดงรายชื่ออุปกรณ์แอนดรอยด์ทั้งหมดใน Firebase Test Lab</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3iWdgT7KlAiFMUUd4cVMnA.jpeg" /></figure><p>หรือจะดู<a href="https://gist.github.com/akexorcist/c55af0f438f6ddea6a94e26962ea52ba">รายชื่ออุปกรณ์แอนดรอยด์จาก Gist ของผม</a>ก็ได้นะ</p><p>ในกรณีที่อยากจะทดสอบบนอุปกรณ์แอนดรอยด์มากกว่า 1 เครื่องก็ให้กำหนด --device เพิ่มเข้าไปเท่าที่ต้องการได้เลย</p><pre>gcloud firebase test android run<br>  --type instrumentation<br>  --app app-debug/app-debug.apk<br>  --test app-debug-androidTest/app-debug-androidTest.apk<br>  --device model=flame,version=30,locale=en,orientation=portrait<br>  --device model=x1q,version=29,locale=en,orientation=portrait<br>  --device model=mata,version=25,locale=en,orientation=portrait</pre><p>ส่วนการกำหนดอุปกรณ์แอนดรอยด์ด้วย Device Matrix จะมีขั้นตอนที่ยุ่งยากกว่านิดหน่อย เพราะจะต้องสร้างไฟล์ YAML เอาไว้อีกชุดแทน สามารถเข้าไปดูรายละเอียดเพิ่มเติมได้ที่</p><p><a href="https://cloud.google.com/sdk/gcloud/reference/topic/arg-files">gcloud topic arg-files | Cloud SDK Documentation | Google Cloud</a></p><h3>เตรียม Project ID กับ Service Key ของ Firebase Project ไว้ใน Repository Secrets</h3><p>อย่างที่บอกไปก่อนหน้านี้ว่า Project ID กับ Service Key ไม่ควรใส่ไว้ในโปรเจคโดยตรง แต่จะเก็บข้อมูลเหล่านี้ไว้ใน Repository Secrets ที่อยู่ใน Repository Settings บน GitHub</p><p>โดยข้อมูลทั้งสองนี้ สามารถเข้าไปดูได้ใน Project Settings ใน Firebase Project ของเราได้เลย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nt4fNHCU23hbXh61Mt793A.jpeg" /></figure><h4>Project ID</h4><p>จะอยู่ใน <strong>Genetal &gt; Your project &gt; Project ID</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ksTE12tSx2NZr167Bngsxw.jpeg" /></figure><h4>Service Key</h4><p>จะอยู่ใน <strong>Service accounts &gt; Firebase Admin SDK</strong> แล้วกดปุ่ม <strong>Generate new private key</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*X6HoT2Nx3YXvGJpSWqz_hg.jpeg" /></figure><p>เมื่อกดปุ่มดังกล่าวจะมี Popup แสดงขึ้นมาเพื่อยืนยันอีกครั้ง ให้กดปุ่ม <strong>Generate key</strong> แล้วหน้าเว็ปจะให้ดาวน์โหลดไฟล์ Private Key ในรูปของ JSON</p><p>โดยจะต้องนำข้อมูล JSON ดังกล่าวไป Encode แบบ Base64 ก่อน</p><pre>ewogICJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIsCiAgInByb2plY3RfaWQiOiAiYWtleG9yY2lzdC1leHBlcmltZW50IiwKICAicHJpdmF0ZV9rZXlfaWQiOiAiZjJlMjRkYjlhNGNkODIzZmU3YjE1YjUy...</pre><p>อย่างของผมจะใช้ Online Tools เพื่อความสะดวก</p><p><a href="https://www.utilities-online.info/base64">Base64 encode/decode online</a></p><h4>ใส่ข้อมูลไว้ใน Repository Secrets</h4><p>เปิด Repository บน GitHub ขึ้นมา แล้วเข้าไปที่ <strong>Settings &gt; Secrets</strong> แล้วกดปุ่ม <strong>New repository secret</strong> เพื่อกำหนดข้อมูลของ Project ID และ Service Key ลงไป โดยที่</p><ul><li><strong>Project ID</strong> : ให้กำหนดชื่อว่า FIREBASE_PROJECT_ID</li><li><strong>Service Key</strong> : ให้กำหนดชื่อว่า FIREBASE_SERVICES_KEY</li></ul><p>โดยจะเป็นชื่อที่กำหนดไว้ใน Script ของ Workflow ในตอนแรกนั่นเอง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9qu-cPgReBkkgBVM06VUFg.jpeg" /></figure><h3>เพิ่ม Editor Role ให้กับ Firebase Admin SDK Service Agent</h3><p>เนื่องจากในตอนนี้ Service Key ที่กำหนดไว้ใน Repository Secrets จะเป็นของ Firebase Admin SDK Service Agent ที่ยังไม่มี Permission ในการเรียกใช้งาน Firebase Test Lab โดยตรง</p><p>ดังนั้นจะต้องเพิ่ม Role ที่ชื่อว่า “Editor” ให้กับ Firebase Admin SDK Service Agent ใน Google Cloud เสียก่อน</p><p>โดยเข้าไปที่ <a href="https://console.cloud.google.com/iam-admin/iam">Google Cloud Console — IAM</a> แล้วเลือก Firebase Project ที่จะใช้ Firebase Test Lab</p><p><a href="https://console.cloud.google.com/iam-admin/iam">Google Cloud Platform</a></p><p>จากนั้นให้กดปุ่ม Edit (สัญลักษณ์ดินสอ) ที่อยู่ขวามือสุดของ Member ที่ชื่อว่า “firebase-adminsdk” เพื่อแก้ไข Permission</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hxTZYLOSatTrZ5SN3mSftA.jpeg" /></figure><p>จะมีหน้าต่าง Edit permissions แสดงขึ้นมาที่ฝั่งขวามือ ให้กดปุ่ม “ADD ANOTHER ROLE” เพื่อเพิ่ม Role ที่ชื่อว่า “Editor - Edit access to all resources” (สามารถพิมพ์ที่แถบค้นหาได้เลย) จากนั้นให้กดปุ่ม “Save” ให้เรียบร้อย</p><h3>เปิดใช้งาน Cloud Tool Results API บน Google Cloud</h3><p>นอกจากการเพิ่ม Role ให้กับ Firebase Admin SDK Service Agent แล้ว จะต้องเปิดใช้บริการ API ที่ชื่อว่า Cloud Tool Results API ด้วย</p><p>โดยเข้าไปที่ <a href="https://console.cloud.google.com/apis/library/toolresults.googleapis.com">Google Cloud Console — Cloud Tool Results API</a></p><p><a href="https://console.cloud.google.com/apis/library/toolresults.googleapis.com">Google Cloud Platform</a></p><p>แล้วกดปุ่ม “Enable” เพื่อเปิดใช้งาน Cloud Tool Results API ให้กับ Firebase Project ของเรา</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fBdZt7LFR7NNpP_cJLZrdQ.jpeg" /></figure><h3>ในที่สุด GitHub Action ก็พร้อมทำงานแล้ว</h3><p>ตอนนี้ผู้อ่านสามารถ Push Script ของ Workflow ขึ้น Repository ได้แล้ว และเมื่อ Push ขึ้นไปเรียบร้อย ให้ลองเข้าไปที่ “Actions” ของ Repository บน GitHub ก็จะเห็นว่า Workflow จะเริ่มทำงานโดยอัตโนมัติตามที่ได้กำหนดไว้ใน Script</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vW0KqXtitp3B87hnOeCvxQ.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Fsx15gB8CP2A9Au7QlpofQ.jpeg" /></figure><p>เมื่อ Workflow ทำงานจนถึง Job ที่รัน UI Test บน Firebase Test Lab แล้ว ให้ลองเปิด Firebase Console ดู ก็จะเห็นว่ามี Test matrix แสดงขึ้นมา ซึ่งเป็น UI Test ที่มาจาก Workflow ใน GitHub Actions นั่นเอง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZP2SGJXG7SfbAEABM0KN-g.jpeg" /></figure><p>และหลังจากนี้เมื่อมี Push หรือ Pull Request ใด ๆ เข้าไปใน Branch นี้ก็จะทำให้ Workflow เริ่มทำงานโดยอัตโนมัติเพื่อรัน Unit Test และ UI Test ของโปรเจคเราเพื่อทดสอบว่าโค้ดทั้งหมดยังสามารถทำงานได้ถูกต้องหรือไม่</p><h3>เรื่องที่ควรรู้เกี่ยวกับ Repository Secrets</h3><p>ในการกำหนดค่า Project ID และ Service Key ของ Firebase Project ไว้ใน Repository Secrets เพื่อเรียกใช้งาน Firebase Test Lab จะใช้งานได้เฉพาะ Workflow Runner ท่ีมาจาก Repository นั้น ๆ เท่านั้น</p><p>นั่นหมายความว่าในกรณีที่มีคนอื่น Fork Repository ไป แล้วเปิด Pull Request กลับเข้ามา ก็จะไม่สามารถใช้งาน Firebase Test Lab ของเราได้ เพราะเป็น Workflow Runner ที่มาจาก Forked Repository จึงไม่สามารถเข้าถึง Project ID และ Service Key ที่อยู่ใน Original Repository ได้</p><p>ยินดีด้วยครับ ตอนนี้โปรเจคของคุณมี Continuous Integration ที่สร้างด้วย GitHub Actions และ Firebase Test Lab เรียบร้อยแล้ว 🔥🔥</p><p>โดยสามารถสรุปขั้นตอนทั้งหมดได้ดังนี้</p><ul><li>เตรียม Script สำหรับ Workflow ของ GitHub Actions ไว้ในแอนดรอยด์</li><li>เตรียม Project ID และ Service Key ของ Firebase Project ไว้ใน Repository Secrets</li><li>เพิ่ม Editor Role ของ Firebase Admin SDK Service Agent ใน IAM บน Google Cloud Console</li><li>เปิดใช้งาน Cloud Tool Resuls API ใน Library บน Google Cloud Console</li><li>Push Script ของ Workflow ที่เตรียมไว้ในตอนแรกขึ้น Repository บน GitHub</li><li>รอดูผลลัพบน GitHub Actions และ Firebase Test Lab</li></ul><p>และเมื่อโปรเจคของคุณมี Continuous Integration แล้ว ก็อย่าลืมหมั่นเขียนเทสทุกครั้งเวลาที่มีการเขียนโค้ดเพิ่มเข้าไป อย่าให้สิ่งที่ทำทั้งหมดในบทความนี้ต้องไร้ค่า เพียงเพราะว่าไม่อยากเสียเวลาเขียนเทสให้กับโค้ดของเราเลยนะ</p><h3>แหล่งข้อมูลอ้างอิง</h3><ul><li><a href="https://medium.com/firebase-developers/github-actions-firebase-test-lab-4bc830685a99">GitHub Actions: Firebase Test Lab [Medium]</a></li><li><a href="https://firebase.google.com/docs/test-lab">Firebase Test Labs</a></li><li><a href="https://github.com/features/actions">GitHub Actions</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cf172aee192d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/firebasethailand/build-android-ci-with-github-actions-and-firebase-test-labs-cf172aee192d">มาสร้าง CI บน Android โดยใช้ GitHub Actions เพื่อทำ UI Test ด้วย Firebase Test Lab แบบเท่ ๆ กัน</a> was originally published in <a href="https://medium.com/firebasethailand">Firebase Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Deploy to Firebase Hosting with Github Actions]]></title>
            <link>https://medium.com/firebasethailand/deploy-to-firebase-hosting-with-github-actions-f795785fde6b?source=rss----261beda53e69---4</link>
            <guid isPermaLink="false">https://medium.com/p/f795785fde6b</guid>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[cicd]]></category>
            <category><![CDATA[firebasehosting]]></category>
            <category><![CDATA[automation]]></category>
            <dc:creator><![CDATA[Siwawes Wongcharoen]]></dc:creator>
            <pubDate>Sun, 04 Apr 2021 00:52:46 GMT</pubDate>
            <atom:updated>2019-10-03T06:04:45.417Z</atom:updated>
            <content:encoded><![CDATA[<h4>Deploy application static assets to Firebase Hosting with Github Actions. You will love automation</h4><p>To deploy application static assets to Firebase Hosting, It is not too hard if you are familiar with Firebase CLI. By the way, if you are in development environment, using Firebase CLI directly from your PC still is the most easiest way. But if you are runing in production environment, using Firebase CLI directly from your PC is too risky.</p><p>I believe that everyone love automation. So, let’s move to automation. If you are using Github, then Github Actions will be you 1st chioce. In this blog post, I will only focusing on how to deploy to Firebase Hosting with Github actions. If you want to know how to set up Firebase Hosting, you <a href="https://firebase.google.com/docs/hosting/quickstart">will found it here</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BZ_jv-xjX_FfJR5fQH_6UQ.png" /><figcaption>Github Actions</figcaption></figure><h3>Setup Workflow</h3><p>Start with breakdown the workflow into steps.</p><ol><li>Authenticate to Firebase (need token)</li><li>Deploy assests to Firebase Hosting (with firebase deploy)</li></ol><h4>Acquire Firebase token</h4><p>Now, We need the Firebase token for authentication. This token can be aquired through the firebase login:ci command. Keep it secure in Github repo setting -&gt; Secrets.</p><h3>Create Github Actions</h3><p>Go to Acions tab on your Github repo. Create workflows by yourself. Then you can use my workflow. But please make sure you understand it.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0ce9436b28f8a295c575d43464643ad1/href">https://medium.com/media/0ce9436b28f8a295c575d43464643ad1/href</a></iframe><p>At line 18. I’ve decided to use <a href="https://github.com/marketplace/actions/github-action-for-firebase">w9jds/firebase-action</a>. Instead of using this action, We will need to install Firebase CLI by ourselves and use full Firebase command firebase deploy --token [YOUR_TOKEN] --only hosting --non-interactive.</p><p>Now, try to trigger the workflow. You will see the magic is happening.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Vdgm2lJKZQvSG6gDQU4L_g.png" /><figcaption>It is magic 😏</figcaption></figure><h3>Conclusions</h3><p>Even you work solo or team, development or production, Github Actions is very helpful.</p><p>Let’s make is automation.</p><h3>References</h3><ul><li><a href="https://firebase.google.com/docs/cli">Firebase CLI reference | Firebase Documentation</a></li><li><a href="https://firebase.google.com/docs/hosting/quickstart">Get started with Firebase Hosting</a></li><li><a href="https://github.com/marketplace/actions/github-action-for-firebase">GitHub Action for Firebase - GitHub Marketplace</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f795785fde6b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/firebasethailand/deploy-to-firebase-hosting-with-github-actions-f795785fde6b">Deploy to Firebase Hosting with Github Actions</a> was originally published in <a href="https://medium.com/firebasethailand">Firebase Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>