How Astro Works
module นี้มีไว้ทำอะไร
หัวข้อที่มีชื่อว่า “module นี้มีไว้ทำอะไร”ทุก module ก่อนหน้าในคอร์สนี้สอนให้คุณ ใช้ Astro: component, island, content collection, action, middleware, adapter ไม่มีอันไหนต้องรู้ว่าจริง ๆ แล้วเกิดอะไรขึ้นกับไฟล์ .astro หลังจากคุณเขียนมันเสร็จ, JavaScript อะไรถูกส่งไป browser เมื่อคุณเพิ่ม client:load, astro sync เขียนอะไรลงดิสก์จริง ๆ, request วิ่งผ่าน order ไหน หรือ astro build ทำอะไรระหว่าง “Collecting build info…” กับ “Complete!” คุณเขียน component ที่ทำงานได้, loader ที่ถูกต้อง, action ที่ผ่านได้ — โดยไม่เคยต้องถามว่า $$createComponent คืออะไร, ทำไม island ที่ซ้อนกันต้องรอ parent hydrate ก่อน หรือทำไม content entry ที่ไม่เปลี่ยนถึงไม่ถูก reparse
ช่องว่างนั้นแหละคือจุดที่ Astro code ที่ AI เขียนพังในแบบที่ดูดีตอนแรก: island ที่ซ้อนกันซึ่งสมมติว่า hydrate เป็นอิสระจาก parent, custom loader ที่ mutate data store โดยไม่มี digest ทำให้ทุก build reparse ทุกอย่างใหม่, middleware ที่อ่าน context.request.body สองครั้ง, route ที่ชนกันแล้ว “แก้” ด้วยการเปลี่ยนชื่อไฟล์แทนที่จะเข้าใจ priority ของ static/dynamic/rest ไม่มีอันไหนเป็น syntax error ไม่มีอันไหนถูกจับโดย astro check ทั้งหมดเป็นข้อเท็จจริงเกี่ยวกับ runtime และ build
module นี้มีหน้าที่เดียว: ให้ความรู้ runtime ห้าอย่างที่ทำให้คุณดู Astro code ของคนอื่น — ไม่ต้องรัน ไม่ต้อง profile แค่อ่าน — แล้วตอบได้ว่า:
- ไฟล์
.astroนี้ compile ออกมาเป็นอะไรจริง ๆ? (compiler output —$$createComponent,$$render, scoped style, slot, hydration metadata) - เกิดอะไรขึ้นตอนที่ island กลายเป็น interactive — และเรียงลำดับยังไงเมื่อ island ซ้อนกัน? (island hydration —
<astro-island>จริง, prop encoding, directive loader) astro syncเขียนอะไรลงดิสก์ และ loader รันใหม่จริง ๆ เมื่อไหร่? (content layer — data store, digest, loader แบบ live vs build-time)- request ที่เข้ามาวิ่งผ่าน order ไหนจริง ๆ? (request pipeline — adapter → middleware → routing → page/endpoint/action → response)
astro buildทำอะไร ตามลำดับไหน และทำไม build รอบสองมักเร็วกว่า? (build pipeline — Vite 8 + Rolldown, prerender pass, client bundle, content-layer cache)
ทุก claim ใน module นี้อ้างอิงจากสามอย่าง: docs.astro.build, Astro source จริงที่ shipped ใน node_modules/astro (คอร์สนี้ pin ไว้ที่ 7.3.5) อ้างด้วยชื่อไฟล์และ function; หรือการรันจริงที่ reproduce ได้ — เรียก transform() กับ @astrojs/compiler-rs ที่ติดตั้งจริง, vitest assertion, curl จริงกับ build ที่รันอยู่ หรือ log จริงจาก astro build/astro sync — วางไว้ใต้ claim นั้นเลย ไม่มีการเดา internals
module นี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “module นี้ครอบคลุมอะไรบ้าง”| บทเรียน | ตอบคำถามอะไร |
|---|---|
| Compiler Output | ไฟล์ .astro ที่มี prop, scoped style, expression และ slot compile ออกมาเป็นอะไรจริง ๆ? |
| Island Hydration | <astro-island> ทำอะไรจริง ๆ ตอน connectedCallback และทำไม island ที่ซ้อนกันถึง hydrate ตามลำดับที่แน่นอน? |
| Content Layer Internals | astro sync เขียนอะไรลงดิสก์ และ loader รู้ได้ยังไงว่า entry ไม่เปลี่ยน? |
| Request Pipeline | order จริงที่พิสูจน์ได้ของ request คืออะไร ตั้งแต่ adapter ถึง response? |
| Build Pipeline | astro build ทำอะไรจริง ๆ ตามลำดับไหน และอะไรทำให้ build รอบสองเร็วกว่า? |
module Foundations แนะนำ “Astro ส่ง JavaScript เป็นศูนย์โดย default” และ component model จากภายนอกไปแล้ว module นี้ไปไกลกว่า framing นั้นเข้าไปใน compiled output จริง, runtime จริง และ build จริง
คำถามที่รันได้จริง: scoped style กลายเป็นอะไรจริง ๆ
หัวข้อที่มีชื่อว่า “คำถามที่รันได้จริง: scoped style กลายเป็นอะไรจริง ๆ”ก่อนกลไกที่ลึกกว่านี้ นี่คือข้อเท็จจริงที่ module ทั้งหมดวนรอบ: compile source .astro เดียวกันเป๊ะสองครั้ง ภายใต้ชื่อไฟล์สองชื่อ ได้ scoped-style hash สองแบบที่ต่างกัน — พิสูจน์ว่า hash มาจาก path ของไฟล์ ไม่ใช่จาก counter หรือ content อย่างเดียว
import { transform } from '@astrojs/compiler-rs';import { expect, test } from 'vitest';
const source = `---const { name } = Astro.props;---<style> .card { color: red; }</style><div class="card">Hello {name}</div>`;
test('transform(): the scoped-style hash is derived from the filename, not the content', () => { const a = transform(source, { filename: 'src/components/Card.astro' }); const b = transform(source, { filename: 'src/components/RenamedCard.astro' });
const hashOf = (code: string) => code.match(/astro-([a-z0-9]+)/)?.[1];
expect(hashOf(a.code)).toBeTruthy(); expect(hashOf(b.code)).toBeTruthy(); expect(hashOf(a.code)).not.toBe(hashOf(b.code)); // same source, different file -> different hash});✓ transform(): the scoped-style hash is derived from the filename, not the content
Test Files 1 passed (1) Tests 1 passed (1)Compiler Output เดิน transform() call เดียวกันนี้ต่อแบบเต็ม ๆ — ทุก import, ทุก call ที่ขึ้นต้นด้วย $$ และเหตุผลที่ hash เปลี่ยนตามชื่อไฟล์
flowchart TD src["`.astro` source"] --> compile["`@astrojs/compiler-rs` `transform()`"] compile --> js["Compiled JS: `$$createComponent`/`$$render` `$$addAttribute`/scoped `astro-*` hash (Compiler Output)"] js --> island["ถ้า hydrate: `<astro-island>` ถูกส่งไป connectedCallback -> hydrate() (Island Hydration)"] content["`astro sync`: content collections -> data store digest-keyed, ข้าม entry ที่ไม่เปลี่ยน"] --> js req["Request ที่เข้ามา"] --> adapter["adapter entry (เช่น `@astrojs/node`)"] adapter --> mw["middleware `sequence()`"] mw --> route["route matching: static > dynamic > rest (Request Pipeline)"] route --> js js --> resp["Response"] build["`astro build`: Vite 8 + Rolldown prerender pass -> client bundle (Build Pipeline)"] --> js
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”อ่าน Astro ที่ไม่คุ้นเคยได้ดี คือรันเครื่องเล็ก ๆ ในหัวขณะที่ตาเห็นแค่ไฟล์: สิ่งนี้ compile เป็นอะไร, มัน ship JavaScript ไหม และถ้า ship JavaScript นั้นรันเมื่อไหร่เทียบกับทุกอย่างบนหน้าและใน request ห้าบทเรียนในนี้คือคำถามนั้นเป๊ะ ๆ ตอน compile time (Compiler Output), ตอน hydration time (Island Hydration), ตอน sync/build time สำหรับ content (Content Layer Internals), ต่อ request (Request Pipeline) และสำหรับทั้งไซต์ (Build Pipeline) ไม่มีอะไรในนี้แปลกใหม่ — compiled output เป็น JavaScript ธรรมดาที่อ่านได้ และ runtime ก็แค่ไม่กี่ร้อยบรรทัดที่อ่านตรง ๆ ได้ใน node_modules/astro/dist/
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “compiler ของ Astro คือ
@astrojs/compilerตัว WASM” พิสูจน์ว่าผิดใน module นี้: compiler จริงของ Astro 7 — ตัวที่astro check/astro buildรันจริง — คือ@astrojs/compiler-rsซึ่งเป็น native Rust NAPI addon@astrojs/compiler(WASM package รุ่นเก่าที่โหลดใน browser ได้) ยังมีอยู่และเป็นตัวที่<AstroPlayground>ในเบราว์เซอร์ของคอร์สนี้ใช้ เพราะ Rust compiler จริงไม่มี browser build — แต่มันไม่ใช่ compiler ที่ gate snippet ของคอร์สนี้เอง - “island hydrate ทันทีที่ directive ของตัวเองทำงาน ไม่เกี่ยวกับอะไรอื่นบนหน้า” Island Hydration พิสูจน์ตรงข้ามด้วย
astro-island.jsจริงที่ติดตั้งอยู่: island ที่ซ้อนกันซึ่ง parent ยัง server-rendered-only อยู่ (มี attributessr) จะ defer การ hydrate ของตัวเองจนกว่า parent จะ hydrate จริง - “
astro buildทุกครั้ง reparse ทุก content entry ใหม่หมด” Content Layer Internals พิสูจน์ ด้วยglob()loader จริงที่ไม่ได้แก้ไข และรันastro syncสองครั้งติดกัน ว่า entry ที่ content digest ไม่เปลี่ยนจะไม่ถูก reparse เลย — มีแค่ module/asset import ที่ถูก register ใหม่
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”จุดประสงค์ทั้งหมดของ module นี้คือ section นี้ ห้าครั้ง — บทเรียนละครั้ง เป็นตัวอย่างระดับภาพรวม: snippet ที่ render ถูกต้องครั้งแรกแต่ยังผิดอยู่
🐛 หา bug ในโค้ดนี้
---import Inner from './Inner.jsx';---<div class="panel"><Inner client:load /></div>
<script>// "runs once the island is interactive"document.querySelector('.panel')?.classList.add('ready');</script>เฉลย
ดูสมเหตุสมผลและ “ทำงาน” ด้วยซ้ำ — class ถูกเพิ่มจริง แต่ comment อ้างผิด: <script> ธรรมดาบนหน้ารันตาม order ของ DOMContentLoaded/module execution ซึ่งไม่เกี่ยวอะไรเลยกับตอนที่ island ของ Inner hydrate เสร็จจริง ๆ Island Hydration พิสูจน์ signal จริง: astro-island.js dispatch CustomEvent ชื่อ astro:hydrate จริงบน island element เอง เฉพาะตอนที่ hydration เสร็จจริงแล้วเท่านั้น (this.dispatchEvent(new CustomEvent('astro:hydrate')) ใน source ที่ติดตั้งจริง) วิธีแก้คือ listen event นั้นบน <astro-island> element ของ island (หรือ ancestor ที่แน่นอน) แทนที่จะสมมติว่า order ของ script tag แปลว่า hydration order — document.querySelector('astro-island')?.addEventListener('astro:hydrate', () => { ... })