DEV Community

Cover image for Sveltekit การทำงานกับ remote function [Part 1]
Nutchapon Makelai
Nutchapon Makelai

Posted on

Sveltekit การทำงานกับ remote function [Part 1]

สวัสดีครับเพื่อนๆ! 👋 วันนี้จะมาเล่าเรื่องน่าตื่นเต้นให้ฟังนะเพื่อนๆ สำหรับใครที่เป็นสาย SvelteKit เตรียมตัวอัปเดตความรู้ใหม่กันได้เลย เพราะตอนนี้เขามีของเล่นใหม่ที่กำลังอยู่ในช่วงทดลองใช้งาน แต่บอกเลยว่าว้าวมาก! เราไปดูกันดีกว่าว่ามันคืออะไร...

📡 Remote function คืออะไร

เป็น function ตัวใหม่ ✨ (ที่คาดว่าจะเป็น new way to implement สำหรับ Sveltekit 3.0) เอาไว้ใช้สื่อสารพูดคุยกันระหว่างฝั่ง client และ server ของ Sveltekit นั่นเอง 💬

ความเจ๋งคือเราสามารถเรียกใช้มันจากมุมไหนของ Sveltekit ก็ได้ 🌍 ไม่จำเป็นต้องจำกัดแค่ฝั่ง server หรือ client แต่จุดสำคัญคือ การทำงานของมันจะเกิดขึ้นที่ฝั่ง server เสมอ 👍 นั่นหมายความว่ามันสามารถทะลุทะลวงไปดึงข้อมูลหรือโมดูลที่เป็น server-only ได้สบายๆ เช่น ตัวแปร environment ที่เราประกาศไว้ หรือพวกฐานข้อมูลต่างๆ ก็ดึงมาได้ชิลๆ เลย 😎

เวลาจะใช้งาน เราจะต้องใช้ท่าการ await แบบใหม่ของ Sveltekit ⏳ ที่ช่วยให้คุณโหลดหรือดึงข้อมูลแบบ promise มาใช้ใน component ของคุณได้ทันที 🚀

⚠️ หมายเหตุ: ตอนนี้ทั้ง await และ remote function ยังอยู่ในช่วงทดลองใช้งาน 🧪 (experimental) นั่นแปลว่า syntax บางอย่างอาจจะมีการปรับเปลี่ยนหรือบินหายไปบ้างในอนาคต 🥲 แต่แกนหลัก (core functional) ของมันก็จะยังทำงานได้ตามที่เราคาดหวังแน่นอน

ถ้าใครคันไม้คันมืออยากลองของใหม่ตอนนี้ สามารถไปเปิดโหมด experimental ได้ที่ไฟล์ svelte.config.js(.ts) ตามโค้ดด้านล่างนี้เลย 👇

svelte.config.js(.ts)

/** @type {import('@sveltejs/kit').Config} */
const config = {
    kit: {
        experimental: {
            remoteFunctions: true
        }
    },
    compilerOptions: {
        experimental: {
            async: true
        }
    }
};

export default config;
Enter fullscreen mode Exit fullscreen mode

🏃‍♂️ Let get started!!

เราสามารถเริ่มใช้ remote function ได้ง่ายๆ ผ่านการสร้างไฟล์นามสกุล .remote.js หรือ .remote.ts 📝 ซึ่งตอนนี้มี function ให้เราหยิบมาเล่นทั้งหมด 4 ตัวด้วยกันคือ:

  • query (ที่เราจะมาพูดถึงกันในบทความนี้)
  • form
  • command
  • prerender

หลักการทำงานเบื้องหลังคือ เวลาที่เรา import ตัว remote function ไปใช้ในฝั่ง client มันจะถูกแอบแปลงร่างเป็นโค้ดที่หุ้มด้วย fetch ในช่วง build time 🏗️ นั่นหมายความว่าระบบจะใจดีสร้างเส้น HTTP endpoint ให้เราแบบอัตโนมัติ ✨ ด้วยเหตุนี้เราเลยเอาไฟล์ .remote.js หรือ .remote.ts ไปแปะไว้ตรงไหนก็ได้ภายใต้โฟลเดอร์ /src

🚨 ยกเว้น! ห้ามเอาไปวางในโฟลเดอร์ /src/lib/server เด็ดขาดนะ เพราะทำไมนะหรอ เพราะมันเป็น folder ที่ถูกล๊อคชื่อไว้เพื่อใช้เป็น server-only module ยังไงละ❗


🔍 query

คำสั่งนี้เอาไว้สำหรับค้นหาข้อมูลแล้วดึงมาแสดงผลชิลๆ 🕵️‍♂️ อารมณ์เหมือนเวลาเราเรียก HTTP Method GET นั่นแหละ

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

มาดูตัวอย่างการสร้าง remote function ด้วยคำสั่ง query ภายใต้ไฟล์ src/routes/blog/data.remote.js กันครับ

src/routes/blog/data.remote.js

import { query } from '$app/server';
import * as db from '$lib/server/database';

export const getPosts = query(async () => {
    const posts = await db.sql`
        SELECT title, slug
        FROM post
        ORDER BY published_at
        DESC
    `;

    return posts;
});
Enter fullscreen mode Exit fullscreen mode

📌 หมายเหตุ: ในตัวอย่างต่อๆ ไป จะเห็นว่ามีการ import พวก $lib/server/database และ $lib/server/auth เข้ามาด้วย อันนี้เป็นแค่โมดูลจำลองเพื่อให้เห็นภาพการทำงานเฉยๆ นะ ของจริงคุณสามารถเชื่อมต่อไปยัง database หรือ service ตัวไหนก็ได้ตามใจชอบเลย 🔌 ส่วน db.sql ก็เป็นแค่ฟังก์ชันสมมติให้ดูว่าเรากำลังคุยกับ database อยู่เท่านั้นจ้า

ซึ่งตัวข้อมูลที่จะส่งกลับมาผ่านฟังก์ชัน getPosts จะทำงานอยู่ในรูปแบบ Promise ⏳ ซึ่งเดี๋ยวมันจะถูกแปลงร่างออกมาเป็นข้อมูล posts ให้เราใช้งานได้นั่นเอง 📄

src/routes/blog/+page.svelte

<script>
    import { getPosts } from './data.remote';
</script>

<h1>Recent posts</h1>

<ul>
    {#each await getPosts() as { title, slug }}
        <li><a href="/blog/{slug}">{title}</a></li>
    {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

สังเกตไหมว่าเวลาเราหยิบ remote function ประเภท query มาใช้ในฝั่ง client side เราจะต้องแปะ await ไว้เสมอ ซึ่งถ้าแจ็คพอตแตก ฟังก์ชัน query เกิด error ขึ้นมา 💥 ระบบก็จะเด้งพาเราไปยังหน้า +error.svelte หรือแสดงผลตาม <svelte:boundary> ตัวที่อยู่ใกล้ที่สุดให้เอง 🛡️

แต่เดี๋ยวก่อน! ฟังก์ชัน query ที่ถูก import เข้ามาในฝั่ง client ยังมี properties ลับๆ ให้เราเรียกใช้เพื่อจัดการเรื่องเวลา (timing) ได้เนียนขึ้น แถมไม่ต้องพึ่ง await ตลอดด้วยนะ โดยมี properties ให้ใช้คือ:

  • loading
  • error
  • current

src/routes/blog/+page.svelte

<script>
    import { getPosts } from './data.remote';

    const query = getPosts();
</script>

<h1>Recent posts</h1>

{#if query.error}
    <p>จะ render ตอนเกิด error ขึ้น 🚨</p>
{:else if query.loading}
    <p>จะ render ระหว่างที่ query function นั้นทำการ fetch ข้อมูล 🔄</p>
{:else}
// จะ render เมื่อ query function ทำการ fetch เรียบร้อย และสำเร็จ ✨
    <ul>
        {#each query.current as { title, slug }}
            <li><a href="/blog/{slug}">{title}</a></li>
        {/each}
    </ul>
{/if}
Enter fullscreen mode Exit fullscreen mode

📖 ปล. แต่ในหัวข้อถัดๆ ไปของบทความนี้ เราจะเน้นใช้งานรูปแบบการเรียกผ่าน await เป็นหลักเพื่อความกระชับนะ


🔣Query arguments

แน่นอนว่าฟังก์ชัน query ยอมให้เราโยนตัวแปร หรือ arguments เข้าไปได้ด้วย 📨 เช่น ถ้าเราอยากจะหยิบข้อมูล slug จาก URL parameters แล้วส่งเข้าไปในฟังก์ชัน query ก็ทำได้สบายๆ แบบนี้เลย 👇

src/routes/blog/[slug]/+page.svelte

<script>
    import { getPost } from '../data.remote';

    let { params } = $props(); //ดึงข้อมูล parameters จาก url 🔗

        //นำ parameters ที่มี key ชื่อว่า slug ส่งเข้า remote function 📨
    const post = $derived(await getPost(params.slug));  
</script>

<h1>{post.title}</h1>
<div>{@html post.content}</div>
Enter fullscreen mode Exit fullscreen mode

ข้อควรระวังคือ เวลาเราโยนตัวแปรเข้าหา remote function เราต้องตรวจสอบ (validate) ความถูกต้องของข้อมูลด้วยนะ ว่าเป็นประเภทข้อมูลที่ถูกต้องไหม ✅ ซึ่งตรงนี้เราจำเป็นต้องพึ่งพวก library ตรวจข้อมูลที่รองรับ Standard Schema อย่างเช่น Valibot หรือ Zod มาช่วยเป็นยามเฝ้าประตูให้ 👮‍♂️

src/routes/blog/data.remote.js

import * as v from 'valibot';
import { error } from '@sveltejs/kit';
import { query } from '$app/server';
import * as db from '$lib/server/database';

export const getPosts = query(async () => { /* ... */ });
                          // vvvvvvv ใส่ schema ตรงนี้ก่อนเข้า function 🛡️
export const getPost = query(v.string(), async (slug) => {
    const [post] = await db.sql`
        SELECT * FROM post
        WHERE slug = ${slug}
    `;

    if (!post) error(404, 'Not found');
    return post;
});
Enter fullscreen mode Exit fullscreen mode

และไม่ต้องห่วงเรื่องข้อมูลซับซ้อน เพราะไม่ว่าจะเป็นตัวแปรที่รับเข้ามา หรือข้อมูลที่ส่งกลับไป ทั้งหมดจะถูกแปลงผ่านกลไกที่เรียกว่า devalue 🔄 ซึ่งเจ๋งตรงที่มันรองรับประเภทข้อมูลอย่าง Date และ Map (หรือประเภทข้อมูลอื่นๆที่เราสร้างเองผ่าน transport hook ด้วย) ได้ด้วย (รวมถึงประเภทข้อมูลแปลกๆ ที่เราสร้างเองผ่าน transport hook ด้วยนะ) ทำให้มันทรงพลังและยืดหยุ่นกว่าการใช้ JSON.stringify แบบปกติเยอะเลย 💪

🧠 เกร็ดความรู้: สำหรับคำสั่ง query หรือ prerender ถ้าเราโยนตัวแปรแบบ object, maps หรือ sets เข้าไป แล้วข้อมูลข้างในหน้าตาเหมือนกันเป๊ะ แค่สลับที่กัน ระบบจะมองว่าเป็น cache key ตัวเดียวกันเลยนะ! เช่น getPosts({ limit: 10, offset: 10 }) กับ getPosts({ offset: 10, limit: 10 }) ถือว่าเป็นตัวเดียวกัน 👯‍♀️ แต่ถ้าโปรเจกต์ของคุณมองว่า "ลำดับ" การเรียงมีความสำคัญมาก ไม่อยากให้มันมองเป็น cache key เดียวกัน แนะนำให้เปลี่ยนไปส่งข้อมูลเป็น array แทนนะ


👯Deduplication

นี่คือฟีเจอร์ "ป้องกันการขอข้อมูลซ้ำซ้อน" 🚫 ปกติแล้วพอเราเรียกใช้ฟังก์ชัน query ตัว Sveltekit จะจัดการแอบจำตัวแปรที่ส่งเข้าไป แล้วสร้างเก็บเป็น cache key 🗝️ ไว้ ซึ่งในฝั่ง server เจ้า cache key ตัวนี้จะถูกเอาไปใช้สร้างแคชแบบ request-scoped สิ่งนี้ทำให้การเรียกขอข้อมูลซ้ำๆ ผ่าน query ด้วยตัวแปรเดิม ระบบจะประมวลผลให้ แค่เพียงครั้งเดียวเท่านั้น! ⚡ ทำให้ได้ผลลัพธ์ไวขึ้นเยอะ แถมยังช่วยลดภาระไม่ให้รัว request ไปถล่มฝั่ง database อีกต่างหาก 😌

เราสามารถหยิบ await ไปวางไว้ตรงไหนก็ได้ตามสะดวก ไม่ว่าจะเป็นในระดับ components, event handlers, universal load function หรือแม้แต่ async callback 🎯 Sveltekit จะช่วยจัดการรวบตึงการทำซ้ำ (deduped) ให้ทันที ถ้าจับได้ว่ามีการเรียกใช้ query ด้วยตัวแปรเดิมใน cache key มาดูตัวอย่างกัน

src/routes/deduped/+page.svelte

<script>
  import { getData } from './data.remote.js';

  // ใช้ await ในระดับ component template -- ทำการสร้าง cache key 🗝️
  const data = getData();
</script>

<p>{await data}</p>

<!-- แต่ในส่วนนี้ถูกเรียกใช้งานอีกครั้ง ซึ่ง sveltekit จะทำการ dedupes 
ข้อมูลกับส่วน component template ด้านบนให้เอง ซึ่งจะไม่ทำให้เกิด request เพิ่มเติมวิ่งไปหา database 🛡️ -->
<button onclick={async () => console.log(await getData())}>
    click me!
</button>
Enter fullscreen mode Exit fullscreen mode

ตัว cache key นี้จะถูกส่งต่อและแชร์ให้กันตลอดตราบใดที่ query ยังมีคนเรียกใช้งานอยู่ เช่น ตอนที่ component กำลัง render, ตอนกำลังใช้ await หรือโดนอ้างอิงจากที่อื่น แต่พอหมดคนใช้งานแล้ว ข้อมูลแคชก็จะถูกลบหรือ release ทิ้งไปสวยๆ 💨 เช่น ตอนที่ผู้ใช้กด refresh หน้าเว็บ เป็นต้น


♻️ Refreshing queries

ในจังหวะที่เราอยากจะขออัปเดตดึงข้อมูลให้เป็นของใหม่จริงๆ ฟังก์ชัน query ก็เตรียม method มาให้เราไปดูดข้อมูลมาใหม่ (re-fetched) ได้ด้วยนะ 🔄 โดยเรียกใช้ method ที่ชื่อว่า refresh เจ้านี่จะวิ่งหน้าตั้งไป fetch เพื่อหอบเอาข้อมูลใหม่ล่าสุดจาก server กลับมาเสิร์ฟให้เราทันที 🏃‍♂️💨

src/routes/blog/+page.svelte

<button onclick={() => getPosts().refresh()}>
    Check for new posts 🔄
</button>
Enter fullscreen mode Exit fullscreen mode

💡 อย่างที่รู้กันว่า query มันถูกจำ (cache) ไว้ตอนเราอยู่หน้าเพจนั้นๆ แปลว่า getPosts() === getPosts() เป๊ะ! ดังนั้นคุณไม่ต้องเหนื่อยไปประกาศตัวแปรซ้ำๆ อย่าง const posts = getPosts() เป็นทอดๆ เพียงเพื่อหวังให้ query มันอัปเดตหรอกนะ สบายใจได้เลย 💓


เป็นยังไงกันบ้างครับเพื่อนๆ กับการใช้งาน query เบื้องต้น ไม่ยากอย่างที่คิดเลยใช่ไหมล่ะ? 😁 แต่อย่างที่บอกไปตอนแรกว่าฟังก์ชันมันไม่ได้มีแค่นี้นะ! สำหรับบทความนี้คงต้องขอพักไว้ตรงนี้ก่อน แล้วเดี๋ยวในบทความหน้า... เราจะไปคุยกันต่อแบบเจาะลึกในเรื่องของ query.batch() และ query.live() 🚀 รับรองว่ามีทีเด็ดรออยู่อีกเพียบ เตรียมตัวรอติดตามกันได้เลยครับ! 😉✨

Top comments (0)