ข้ามไปยังเนื้อหา

How Astro Works

ทุก 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 แค่อ่าน — แล้วตอบได้ว่า:

  1. ไฟล์ .astro นี้ compile ออกมาเป็นอะไรจริง ๆ? (compiler output — $$createComponent, $$render, scoped style, slot, hydration metadata)
  2. เกิดอะไรขึ้นตอนที่ island กลายเป็น interactive — และเรียงลำดับยังไงเมื่อ island ซ้อนกัน? (island hydration — <astro-island> จริง, prop encoding, directive loader)
  3. astro sync เขียนอะไรลงดิสก์ และ loader รันใหม่จริง ๆ เมื่อไหร่? (content layer — data store, digest, loader แบบ live vs build-time)
  4. request ที่เข้ามาวิ่งผ่าน order ไหนจริง ๆ? (request pipeline — adapter → middleware → routing → page/endpoint/action → response)
  5. 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

บทเรียน ตอบคำถามอะไร
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 จริง

ก่อนกลไกที่ลึกกว่านี้ นี่คือข้อเท็จจริงที่ module ทั้งหมดวนรอบ: compile source .astro เดียวกันเป๊ะสองครั้ง ภายใต้ชื่อไฟล์สองชื่อ ได้ scoped-style hash สองแบบที่ต่างกัน — พิสูจน์ว่า hash มาจาก path ของไฟล์ ไม่ใช่จาก counter หรือ content อย่างเดียว

src/lib/scoped-hash.test.ts
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
ไฟล์ .astro หนึ่งไฟล์ จาก source ถึง response

อ่าน 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 อยู่ (มี attribute ssr) จะ defer การ hydrate ของตัวเองจนกว่า parent จะ hydrate จริง
  • “astro build ทุกครั้ง reparse ทุก content entry ใหม่หมด” Content Layer Internals พิสูจน์ ด้วย glob() loader จริงที่ไม่ได้แก้ไข และรัน astro sync สองครั้งติดกัน ว่า entry ที่ content digest ไม่เปลี่ยนจะไม่ถูก reparse เลย — มีแค่ module/asset import ที่ถูก register ใหม่

จุดประสงค์ทั้งหมดของ module นี้คือ section นี้ ห้าครั้ง — บทเรียนละครั้ง เป็นตัวอย่างระดับภาพรวม: snippet ที่ render ถูกต้องครั้งแรกแต่ยังผิดอยู่

🐛 หา bug ในโค้ดนี้

src/components/Panel.astro
---
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', () => { ... })

เป้าหมายหลักของ module นี้คืออะไร?
Astro 7.3.5 ใช้ package ไหนจริง ๆ ในการ compile ไฟล์ .astro ตอน astro check/astro build?
ถ้า Inner เป็น island ที่ซ้อนอยู่ใน Outer astro-island.js source จริงพิสูจน์อะไรเกี่ยวกับ hydration order?
<Outer client:load>
  <Inner client:load />
</Outer>
จริงหรือเท็จ: ทุก claim เกี่ยวกับ internals ใน module นี้ ระบุไว้บน docs.astro.build, อ้างจาก astro/@astrojs source ที่ติดตั้งจริงพร้อมชื่อไฟล์และ function หรือพิสูจน์ด้วยการรันจริง (compile, test, curl หรือ build log) ที่แสดง output จริง
บทเรียนไหนอธิบายว่า astro sync เขียนอะไรลงดิสก์จริง ๆ และ content loader รันการ parse ใหม่เมื่อไหร่?