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

รูปแบบการประกอบ (Composition Patterns)

เมื่อคุณมอง Astro เป็น shell แล้ว คำถามการออกแบบสำคัญคือ เมื่อไหร่ ที่ slice ต่างๆ จะถูกนำมารวมกัน มีสองคำตอบ และสถาปัตยกรรมที่สุกงอมมักใช้ทั้งสองอย่าง: build-time integration และ run-time integration

ใน build-time integration แต่ละทีมเผยแพร่ slice ของตัวเองเป็น versioned package monorepo ที่มี workspaces (npm, pnpm, หรือ Yarn) เป็นบ้านที่พบบ่อยที่สุดสำหรับสิ่งนี้ shell ขึ้นอยู่กับ @store/catalog, @store/cart และอื่นๆ ปักหมุดเวอร์ชัน แล้ว import เข้ามาเหมือน component อื่นๆ:

---
// The shell imports versioned team packages.
import Catalog from '@store/catalog';
import Cart from '@store/cart';
---
<Catalog client:visible />
<Cart client:load />

เนื่องจากการประกอบเกิดขึ้นระหว่างการ build ของ shell คุณจึงได้สิ่งที่ compiler ทำได้ดี: การ type check ข้ามขอบเขต slice, tree-shaking และ output เดียวที่ถูก optimize แล้ว ข้อติดขัดคือความเป็นอิสระ — การเปลี่ยนแปลงของ Catalog จะไปถึงผู้ใช้ก็ต่อเมื่อ shell เลื่อนเวอร์ชัน, rebuild และ redeploy เท่านั้น build-time integration เอนเอียงไปทางความปลอดภัยและความสอดคล้องมากกว่าการ ship แบบทันทีและเป็นอิสระ

ใน run-time integration แต่ละ slice เป็น artifact ที่ deploy แยกกัน shell ไม่ได้ import ตอน build แต่ดึง slice เข้ามาเมื่อหน้าเพจถูกเรนเดอร์ (บนเซิร์ฟเวอร์) หรือหลังจากที่โหลดแล้ว (ใน browser) ทีมต่างๆ ship ตามนาฬิกาของตัวเอง และ shell หยิบเวอร์ชันใหม่ขึ้นมาในการร้องขอครั้งถัดไป — ไม่ต้อง redeploy shell ราคาที่จ่ายคือการรับประกัน ณ เวลา compile ที่อ่อนแอลงและพื้นผิวการดำเนินงานที่มากขึ้น ซึ่งจะกล่าวถึงโดยละเอียดในบทเรียนถัดไป

flowchart TD
  subgraph BuildTime["Build-time integration"]
    BT1["Team pkg @store/cart v1.4"] --> BT2["Shell build imports it"]
    BT2 --> BT3["One optimized bundle"]
  end
  subgraph RunTime["Run-time integration"]
    RT1["Team deploys cart service"] --> RT2["Shell fetches at request / in browser"]
    RT2 --> RT3["Composed live, no shell rebuild"]
  end
Build-time integration vs run-time integration

server islands ของ Astro ให้ทางสายกลางที่ทรงพลังแก่คุณ ด้วยการเพิ่ม server:defer ให้กับ component คุณบอก Astro ให้เรนเดอร์ส่วนที่เหลือของหน้าเพจทันทีและ stream island นั้นเข้ามาทีหลังจากเซิร์ฟเวอร์ แต่ละ island สามารถมี fallback slot ของตัวเองที่แสดงในขณะที่กำลังโหลด:

---
import Recommendations from '../islands/Recommendations.astro';
---
<Recommendations server:defer>
<p slot="fallback">Loading recommendations…</p>
</Recommendations>

สำหรับ micro-frontends นี่คือสิ่งที่เหมาะอย่างยิ่ง: team fragment ที่ช้าหรือถูกปรับให้เฉพาะบุคคล — recommendations, cart badge, ราคาแบบ live — จะไม่ block ส่วนที่เหลือของหน้าเพจอีกต่อไป shell ที่เร็วและ cache ได้จะ ship ออกไปก่อน และ island ที่ถูกเลื่อนของแต่ละทีมจะมาถึงด้วยตัวของตัวเอง นี่คือความขี้เกียจแยกตามทีมโดยไม่มีต้นทุน client-side framework สำหรับ fallback เลย

หน้าเพจที่รันได้ด้านล่างเป็น composition shell ขั้นต่ำ หน้านี้ import component “ทีม” หนึ่งตัวแล้วเรนเดอร์เป็น section — รูปร่างเดียวกับที่คุณจะใช้ไม่ว่าการ import จะ resolve ไปเป็น workspace package หรือ slice ในเครื่อง

Astro
---
// src/pages/index.astro — composition shell
// In a real setup, ProductCard would come
// from a versioned package like @store/catalog.
import ProductCard from '../components/ProductCard.astro';
---
<html lang="en">
  <head><title>Composition shell</title></head>
  <body>
    <header><h1>Acme Store</h1></header>

    <main>
      <h2>Catalog team's slice</h2>
      <ProductCard
        name="Astro Mug"
        price="12.00"
      />
    </main>

    <footer>Composed by the Astro shell.</footer>
  </body>
</html>

เพื่อให้สิ่งนี้รันได้ คุณจะต้องเพิ่ม src/components/ProductCard.astro ที่อ่าน Astro.props (name, price) และเรนเดอร์การ์ดออกมา shell ไม่สนใจว่า slice ถูก build ภายในอย่างไร — สนใจแค่ว่ารับ props อะไรบ้างเท่านั้น

ใน build-time integration slice ของทีมไปถึง shell ได้อย่างไร?
การเพิ่ม `server:defer` ให้กับ component ทำอะไร?
ข้อแลกเปลี่ยนสำคัญของ build-time integration คือ: