View Transitions
ดีที่สุดของทั้งสองโลก
หัวข้อที่มีชื่อว่า “ดีที่สุดของทั้งสองโลก”เว็บ multi-page แบบดั้งเดิม reload เต็มหน้าทุกครั้งที่ navigate — เชื่อถือได้ แต่จอกระพริบขาวและ scroll position รีเซ็ต SPA เลี่ยงปัญหานี้ได้แต่ต้องแลกกับ JavaScript router ที่หนัก view transitions ของ Astro ให้ความรู้สึกลื่นแบบ SPA บน MPA ปกติ ด้วย script แค่ไม่กี่ kilobyte
เปิดใช้ด้วยการเพิ่ม component เดียว <ClientRouter /> ใน <head> ของหน้า — ปกติวางใน layout ที่ใช้ร่วมกันเพื่อให้มีผลทั้งเว็บ ตรวจสอบตรงจาก virtual-module source ที่ติดตั้งจริงใน 7.3.5 (astro/dist/transitions/vite-plugin-transitions.js): path import จริงตอนนี้คือ astro:transitions:
---import { ClientRouter } from 'astro:transitions';---<html lang="en"> <head> <title>My site</title> <ClientRouter /> </head> <body> <slot /> </body></html>เมื่อมีสิ่งนี้แล้ว การ navigate ระหว่างหน้าในเว็บของเราจะถูก intercept และจัดการฝั่ง client: Astro fetch หน้าถัดไป สลับเนื้อหาเข้าไป แล้ว animate การเปลี่ยน — ไม่มี reload เต็มหน้า browser ที่ไม่รองรับ API เบื้องหลังจะ fallback ไป navigate ปกติเฉย ๆ ไม่มีอะไรพัง
ชื่อเก่าหายไปจริง ๆ — พิสูจน์แล้ว
หัวข้อที่มีชื่อว่า “ชื่อเก่าหายไปจริง ๆ — พิสูจน์แล้ว”Astro รุ่นเก่า component นี้ชื่อ <ViewTransitions /> มันไม่ใช่แค่ deprecated หรือ alias ใน 7.3.5 — มันไม่ export ออกมาเลย พิสูจน์ด้วย build จริง:
---import { ViewTransitions } from 'astro:transitions';---<ViewTransitions />[ERROR] [vite] ✗ Build failed[MISSING_EXPORT] "ViewTransitions" is not exported by "\0astro:transitions". ╭─[ src/pages/vt-bad-import.astro:2:10 ] 2 │ import { ViewTransitions } from "astro:transitions"; │ ───────┬─────── │ ╰───────── Missing exportถ้าเจอ <ViewTransitions /> ใน tutorial เก่า ทางแก้คือเปลี่ยนชื่อตรง ๆ เป็น <ClientRouter /> — import specifier เดิม พฤติกรรมเดิม
<ClientRouter /> เทียบกับ native cross-document view transitions
หัวข้อที่มีชื่อว่า “<ClientRouter /> เทียบกับ native cross-document view transitions”<ClientRouter /> ของ Astro เป็น client-side router: มัน intercept การคลิก link ด้วย JavaScript, fetch หน้าถัดไปเอง แล้วสลับ DOM — View Transitions API คือสิ่งที่มันใช้animateการสลับนั้นให้ลื่น browser สมัยใหม่ตอนนี้รองรับ cross-document view transitions แบบ native ด้วย ไม่ต้องใช้ JavaScript เลย ล้วน ๆ ด้วย CSS:
@view-transition { navigation: auto;}declaration เดียวนี้เปิด transition แบบ full-page-navigation ของ browser เอง — ไม่มีการ intercept ไม่มีการ fetch ฝั่ง client ไม่มี <ClientRouter /> เลย; browser ยัง navigate ปกติ แค่ animate การสลับระหว่างเอกสารเก่ากับใหม่ ใช้แบบ CSS-only นี้เมื่ออยากได้แค่ความลื่นทาง visual ไม่มีอะไรมากกว่านั้น (ไม่มี shared-element transition:name morph, ไม่มี transition:persist เก็บ state, ไม่มี lifecycle event ให้ hook) และอยากให้หน้าไม่มี JavaScript เพิ่มเลย ใช้ <ClientRouter /> เมื่อต้องการ island ที่ persist, shared-element transition ที่ตั้งชื่อได้ หรือ hook เข้า lifecycle ของการ navigate (astro:page-load ฯลฯ) — สิ่งเหล่านี้ form CSS-only แบบ native ให้ไม่ได้เลย
navigation ทำงานยังไง
หัวข้อที่มีชื่อว่า “navigation ทำงานยังไง”flowchart LR click["User clicks a link"] --> intercept["ClientRouter intercepts (no full reload)"] intercept --> fetch["Fetch the next page HTML"] fetch --> swap["Swap in the new content, animate"] swap --> event["Fire astro:page-load"]
Transition directive — markup จริงที่ build ออกมา
หัวข้อที่มีชื่อว่า “Transition directive — markup จริงที่ build ออกมา”โดย default Astro cross-fade หน้าให้ เราคุม animation ต่อ element ได้ด้วย directive:
transition:name— ให้ element ชื่อเดียวกันทั้งสองหน้า Astro จะ morph ตัวหนึ่งไปเป็นอีกตัว (shared-element transition แบบ thumbnail โตขึ้นเป็น hero image)transition:animate— เลือก animation (fade,slideหรือแบบ custom) import helper จากastro:transitionstransition:persist— เก็บ DOM และ state ของ component ไว้ข้าม navigation แทนที่จะ re-render ใหม่ เหมาะกับ video ที่กำลังเล่นหรือ audio player ที่ไม่ควรเริ่มใหม่
---import { ClientRouter } from 'astro:transitions';import { slide } from 'astro:transitions';---<html><head><title>A</title><ClientRouter /></head><body><h1 transition:name="page-title" transition:animate={slide({ duration: 200 })}>Title</h1><video controls autoplay transition:name="player" transition:persist></video></body></html>output จริงที่ build ออกมา (astro build) — Astro เขียน element ที่ตั้งชื่อไว้แต่ละตัวใหม่ให้เป็น id data-astro-transition-scope มาร์คตัวที่ persist ด้วย data-astro-transition-persist และใส่ keyframe ของ slide() เป็น CSS แบบ scoped ต่อ element ผูกกับ scope เดียวกัน พร้อม fallback CSS สำหรับ browser ที่ไม่มี native View Transitions API:
<body> <h1 data-astro-transition-scope="astro-cqr5yetz-1">Title</h1> <video data-astro-transition-persist="player" data-astro-transition-scope="astro-vnhdmk2z-2" controls autoplay></video></body>/* generated once per transition:name, real fence trimmed to the essentials */[data-astro-transition-scope="astro-cqr5yetz-1"] { view-transition-name: page-title; }@layer astro { ::view-transition-old(page-title) { animation-name: astroFadeOut, astroSlideToLeft; animation-duration: 200ms, 200ms; } ::view-transition-new(page-title) { animation-name: astroFadeIn, astroSlideFromRight; animation-duration: 200ms, 200ms; }}/* [data-astro-transition-fallback="old"/"new"] rules duplicate the same animation-name/-duration for browsers without the native API */transition:persist บน <video> ไม่ได้แค่ข้าม animation — มันคือ attribute จริงแยกต่างหาก (data-astro-transition-persist) ที่บอก client router ให้เก็บ DOM node ตัวนั้นเป๊ะ ๆ (พร้อม state ที่กำลังทำงานอยู่ — ตำแหน่งเล่น, state ของ <audio>/<video>, ค่าใน form, state ในหน่วยความจำของ island) ข้าม swap แทนที่จะแทนที่ด้วย copy ของหน้าใหม่
Lifecycle event เป็นชื่อ string ไม่ใช่ constant ที่ export — พิสูจน์แล้ว
หัวข้อที่มีชื่อว่า “Lifecycle event เป็นชื่อ string ไม่ใช่ constant ที่ export — พิสูจน์แล้ว”เพราะ navigation ไม่ trigger full page load อีกต่อไป script ที่รันตอน DOMContentLoaded จะไม่รันซ้ำตอน client-side navigation Astro ยิง event ของตัวเอง และใน 7.3.5 มันเป็น string event name ธรรมดา ที่ dispatch ด้วย document.dispatchEvent(new Event(name)) — ตรวจสอบตรงจาก astro/dist/transitions/events.js และ router.js:
astro:before-preparationastro:after-preparationastro:before-swapastro:after-swap— ทันทีหลัง DOM ใหม่ถูกสลับเข้ามา ก่อน paint (เหมาะกับ restore theme/state เพื่อเลี่ยงการกระพริบ)astro:page-load— ยิงหลังทุก navigation (โหลดครั้งแรก และ การสลับฝั่ง client)
ไม่มี constant สไตล์ TRANSITION_PAGE_LOAD ให้ import เลย — มีแค่ TypeScript class export สองตัว (TransitionBeforePreparationEvent, TransitionBeforeSwapEvent) ไว้ type event object เอง ไม่ใช่ type ชื่อของมัน พิสูจน์ด้วย build จริง:
---import { TRANSITION_PAGE_LOAD } from 'astro:transitions/client';---<p>{TRANSITION_PAGE_LOAD}</p>[ERROR] [vite] ✗ Build failed[MISSING_EXPORT] "TRANSITION_PAGE_LOAD" is not exported by "\0astro:transitions/client".ฟังด้วย string ธรรมดา แล้ว re-initialize ตรงนี้แทนที่จะเป็น DOMContentLoaded:
document.addEventListener('astro:page-load', () => { // runs on first load AND after every client-side navigation initAnalytics();});นี่คือกับดักเรื่อง view-transitions ที่พบบ่อยที่สุด: init script ที่ทำงานตอนโหลดครั้งแรกแต่ “หยุดทำงาน” หลัง navigate เพราะมันผูกกับ DOMContentLoaded ย้ายไปที่ astro:page-load
navigate() และ fallback
หัวข้อที่มีชื่อว่า “navigate() และ fallback”astro:transitions/client (ตรวจสอบตรงจาก virtual-module source เดียวกัน) ยัง export function navigate(href, options) จริง — เป็นการ navigate แบบเขียน script เอง ที่ไปผ่าน client router ตัวเดียวกับที่คลิก link ปกติ จึง animate และยิง lifecycle event เดียวกัน:
---import { ClientRouter } from 'astro:transitions';---<html><head><ClientRouter /></head><body><button id="go">Go</button><script> import { navigate } from 'astro:transitions/client'; document.getElementById('go')?.addEventListener('click', () => navigate('/next-page'));</script></body></html>การเรียก navigate() ตอน render ฝั่ง server (แทนที่จะเป็นการตอบสนอง event จริงใน browser) จะ log warning ใน source ที่ติดตั้งจริง (router.js: “The view transitions client API was called during a server side render…”) — มันตั้งใจให้รันฝั่ง client เพื่อตอบสนอง user interaction
ถ้า browser ของผู้ใช้ไม่รองรับ View Transitions API เลย <ClientRouter /> ยัง intercept และสลับฝั่ง client ให้ — แค่ข้าม animation ไป (หรือใช้ CSS-fallback rule ที่เห็นข้างบน ซึ่ง browser บางตัวยังรันได้แม้ไม่มี native API) มีแต่ browser ที่ปิด JavaScript ไปเลยเท่านั้นที่ fallback ไป navigate เต็มหน้าธรรมดา ไม่มี animation
Island + transition:persist
หัวข้อที่มีชื่อว่า “Island + transition:persist”transition:persist บน island เก็บ DOM และ live component state ของ island นั้นไว้ข้าม client-side navigation — component ไม่ถูก unmount แล้ว remount ใหม่ มันคือ instance เดิมเป๊ะ ๆ ที่ถูกส่งต่อไป นี่ต่างจาก element ธรรมดาอย่าง <video>: สำหรับ island “persist” หมายถึง state ภายในของ framework component (ตัวนับ, ค่าใน form, อะไรก็ตามที่อยู่ในหน่วยความจำ) รอดไปด้วย ไม่ใช่แค่ DOM node กับตำแหน่งเล่นแบบ native
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”- snippet
import { ViewTransitions } from 'astro:transitions'หรือเปล่า? พิสูจน์ข้างบนแล้วว่า: นี่คือMISSING_EXPORTbuild error จริงใน 7.3.5 ไม่ใช่แค่ deprecation เบา ๆ — เปลี่ยนเป็นClientRouter - มันตั้ง caching header บนหน้าที่มี
<ClientRouter />ด้วย แล้วคาดหวังว่าเนื้อหาที่สลับเข้ามาจะ “สด” หรือเปล่า? shell ที่ cache จาก edge ก็ยังเป็นแค่ HTML —<ClientRouter />ไม่ได้เปลี่ยน lesson เรื่อง caching ที่ใช้กับ response ข้างใต้มันเลย - มัน import lifecycle event เป็น named constant (
TRANSITION_PAGE_LOAD,AFTER_SWAPฯลฯ) แทนที่จะใช้ string ธรรมดาหรือเปล่า? พิสูจน์ข้างบนแล้วว่า: export แบบนั้นไม่มีอยู่จริง — ใช้ string ตรง ๆ กับaddEventListener('astro:page-load', ...) client:loadถูกใช้กับ component ที่เป็นtransition:persistด้วย ทั้งที่ component ไม่ได้ต้องการ hydrate ทันทีจริง ๆ หรือเปล่า?transition:persistกับ timing ของ hydration เป็นอิสระจากกัน — persist ไม่ได้ต้องการ directive ที่หนักที่สุด- init script ยังผูกกับ
DOMContentLoadedบนเว็บที่ใช้<ClientRouter />อยู่หรือเปล่า? มันจะหยุด re-run เงียบ ๆ หลัง client-side navigation ครั้งแรก — ย้ายไปที่astro:page-load
🐛 หา bug ในโค้ดนี้
---import { ViewTransitions } from 'astro:transitions';---<html><head><ViewTransitions /></head><body><slot /></body></html>เฉลย
นี่ไม่ใช่แค่ความชอบเรื่อง style — มันคือ build error จริงใน Astro 7.3.5 ที่ติดตั้งจริง พิสูจน์ข้างบนแล้ว: [MISSING_EXPORT] "ViewTransitions" is not exported by "astro:transitions" component นี้ถูกเปลี่ยนชื่อเป็น ClientRouter ใน Astro ปัจจุบัน ทางแก้คือ import { ClientRouter } from 'astro:transitions' แล้วใช้ <ClientRouter /> ใน template — module specifier เดิม ตำแหน่งเดิมใน <head> แค่เปลี่ยนชื่อใหม่
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”<ClientRouter /> คือ client-side interceptor ที่วางซ้อนบน MPA navigation ปกติ ไม่ใช่ router ใหม่ที่มี state model ของตัวเอง — มัน fetch HTML ของหน้าจริง สลับ DOM จริง และยิง event จริงให้เรา hook เข้าไป (astro:page-load ไม่ใช่ re-render มายากล) transition:persist คือทางออกเดียวจาก “สลับทุกอย่าง”: มันเก็บ element (หรือ island พร้อม live state) ตัวเดียวไว้เป็น node เดิมเป๊ะ ๆ แทนที่จะเอา copy ของหน้าใหม่มาแทน
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”<ClientRouter /> |
Native @view-transition { navigation: auto } |
|
|---|---|---|
| JavaScript ที่ส่งไป | ไม่กี่ KB (router + swap logic) | ศูนย์ |
shared-element transition:name morph |
มี | ไม่มี |
transition:persist (เก็บ state ของ island) |
มี | ไม่มี — ทุก navigation คือ document swap เต็มรูปแบบจริง ๆ |
Lifecycle hook (astro:page-load ฯลฯ) |
มี | ไม่มี |
| Fallback สำหรับ browser ที่ไม่รองรับ | สลับฝั่ง client ธรรมดา ไม่ animate | navigate เต็มหน้าธรรมดา ไม่ animate |
| เหมาะกับ | เว็บที่ต้อง persist state หรือคุม animation ละเอียด | ความลื่นทาง visual แบบง่าย ๆ ไม่มี JS เพิ่ม |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
<ViewTransitions />ยังใช้ได้ แค่ deprecated” พิสูจน์ข้างบนแล้วด้วย build จริง: มันคือMISSING_EXPORTerror จริงใน 7.3.5 ไม่ใช่แค่ warning เบา ๆ - “lifecycle event เป็น constant ที่ import ได้ อย่าง
TRANSITION_PAGE_LOAD” พิสูจน์ข้างบนแล้วว่า: export แบบนั้นไม่มีอยู่จริง (MISSING_EXPORTบนastro:transitions/client) — มันคือ string event name ธรรมดา ('astro:page-load'ฯลฯ) ที่ dispatch บนdocument - “
transition:persistบน element ธรรมดา persist state ของหน้าได้ทุกอย่าง” มันเก็บแค่ DOM node ตัวนั้น (และสำหรับ island ก็ state ของ component) ข้าม swap — ไม่ได้ทำให้ state อื่นที่ไม่เกี่ยวข้อง (เช่นตัวแปรใน script อื่น) รอดจาก navigation ไปด้วย - “ไม่มี
<ClientRouter />แล้ว view transitions ไม่มีอยู่เลย” browser สมัยใหม่รองรับ cross-document view transitions แบบ native ผ่าน@view-transition { navigation: auto }ใน CSS ล้วน ๆ — ไม่มี component ไม่มี JavaScript เลย