สวัสดีครับเพื่อนๆ! 👋 วันนี้จะมาเล่าเรื่องน่าตื่นเต้นให้ฟังนะเพื่อนๆ สำหรับใครที่เป็นสาย 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;
🏃♂️ 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;
});
📌 หมายเหตุ: ในตัวอย่างต่อๆ ไป จะเห็นว่ามีการ 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>
สังเกตไหมว่าเวลาเราหยิบ 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}
📖 ปล. แต่ในหัวข้อถัดๆ ไปของบทความนี้ เราจะเน้นใช้งานรูปแบบการเรียกผ่าน
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>
ข้อควรระวังคือ เวลาเราโยนตัวแปรเข้าหา 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;
});
และไม่ต้องห่วงเรื่องข้อมูลซับซ้อน เพราะไม่ว่าจะเป็นตัวแปรที่รับเข้ามา หรือข้อมูลที่ส่งกลับไป ทั้งหมดจะถูกแปลงผ่านกลไกที่เรียกว่า 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>
ตัว 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>
💡 อย่างที่รู้กันว่า
queryมันถูกจำ (cache) ไว้ตอนเราอยู่หน้าเพจนั้นๆ แปลว่าgetPosts() === getPosts()เป๊ะ! ดังนั้นคุณไม่ต้องเหนื่อยไปประกาศตัวแปรซ้ำๆ อย่างconst posts = getPosts()เป็นทอดๆ เพียงเพื่อหวังให้queryมันอัปเดตหรอกนะ สบายใจได้เลย 💓
เป็นยังไงกันบ้างครับเพื่อนๆ กับการใช้งาน query เบื้องต้น ไม่ยากอย่างที่คิดเลยใช่ไหมล่ะ? 😁 แต่อย่างที่บอกไปตอนแรกว่าฟังก์ชันมันไม่ได้มีแค่นี้นะ! สำหรับบทความนี้คงต้องขอพักไว้ตรงนี้ก่อน แล้วเดี๋ยวในบทความหน้า... เราจะไปคุยกันต่อแบบเจาะลึกในเรื่องของ query.batch() และ query.live() 🚀 รับรองว่ามีทีเด็ดรออยู่อีกเพียบ เตรียมตัวรอติดตามกันได้เลยครับ! 😉✨
Top comments (0)