跳到內容

元件

Astro 元件是 Astro 專案的基石,只包含 HTML 模板,不依賴客戶端運行環境,使用 .astro 副檔名。

:::note如果你懂 HTML,你就有足夠的知識寫出你的第一個 Astro 元件了。

Astro 語法參考了解更多資訊。
:::

Astro 元件極其彈性,Astro 元件能只有一小段 HTML(例如跟 SEO 有關的 <meta> 標籤),也可以是共用的 UI 元素,像是頁首、個人資訊卡。元件甚至能包含整個網頁版面,當元件在特殊的 src/pages/ 資料夾時,元件可以就是整個網頁版面。

Astro 元件最重要的一點是:不在客戶端算繪。它們只在建置階段或在需要時轉換為 HTML。元件的 frontmatter 可放入 JavaScript 程式碼,它們不會送至使用者的瀏覽器。預設不包含任何 JavaScript 足跡,因此你的網站變得更快了。

如果需要客戶端的互動效果,可適時加入 HTML <script> 標籤 (EN),或加入 UI 框架元件 (EN)做為「客戶端群島」。

對於需要算繪個人化或動態內容的元件,你可以加入伺服器端指令 (EN)延後元件的伺服器端算繪。這些「伺服器端群島」會在有空的時候才算繪內容,不會耽誤到整個頁面的載入。

元件架構

Astro 元件由兩塊組成:元件腳本元件模板。兩者各司其職,攜手構築出既容易使用,又能滿足多樣開發需求的框架。

---
// 元件腳本(JavaScript)
---
<!-- 元件模板(HTML + JS 表達式) -->

元件腳本

Astro 使用「程式碼圍欄」(---)區別元件腳本。如果你曾寫過 Markdown,你可能知道 frontmatter,兩者是相似的概念。Astro 元件腳本的構想就是受此概念啟發。

你可在元件腳本中撰寫算繪模板時需要用到的 JavaScript 程式碼。包含:

  • 匯入其他 Astro 元件
  • 匯入其他框架元件,例如 React
  • 匯入資料,例如 JSON 檔
  • 從 API 或資料庫抓取資料
  • 建立變數供模板使用
---
import SomeAstroComponent from '../components/SomeAstroComponent.astro';
import SomeReactComponent from '../components/SomeReactComponent.jsx';
import someData from '../data/pokemon.json';

// 存取傳入元件的參數,例如 `<X title="Hello, World" />`
const { title } = Astro.props;

// 抓取外部資料,從非公開 API 或資料庫都可以
const data = await fetch('SOME_SECRET_API_URL/users').then(r => r.json());
---
<!-- 模板在這! -->

程式碼圍欄的設計理念是要「圍住」你寫的 JavaScript,確保它不會跑到前端應用程式、落入使用者手中。在圍欄裡可以安心地寫較耗資源或者敏感的程式碼(例如呼叫你的私有資料庫),不必擔心裡面的程式碼不小心出現在使用者的瀏覽器裡。

:::noteAstro 元件腳本使用 TypeScript,這讓您可以為 JavaScript 增加額外的語法,以支援編輯器工具和錯誤檢查功能。

進一步了解 Astro 內建的 TypeScript 支援 (EN)
:::

元件模板

元件模板位於程式碼圍欄下方,它決定了最終產出的 HTML。

寫在這裡的 HTML 會被元件算繪,當元件被其他 Astro 頁面匯入使用時亦同。

不只一般的 HTML,Astro 的元件模板語法 也支援 JavaScript 表達式、Astro <style> (EN)<script> (EN) 標籤、匯入的元件,以及特殊的 Astro 指令 (EN)。定義在元件腳本的資料與值可以用在元件模板以產生動態建立的 HTML。

---
// 這裡是元件腳本!
import Banner from '../components/Banner.astro';
import Avatar from '../components/Avatar.astro';
import ReactPokemonComponent from '../components/ReactPokemonComponent.jsx';
const myFavoritePokemon = [/* ... */];
const { title } = Astro.props;
---
<!-- 支援 HTML 註解! -->
{/* JS 註解也沒問題! */}

<Banner />
<h1>Hello, world!</h1>

<!-- 使用 props 和元件腳本的變數: -->
<p>{title}</p>

<!-- 延後算繪元件並提供備用的載入中內容: -->
<Avatar server:defer>
  <svg slot="fallback" class="generic-avatar" transition:name="avatar">...</svg>
</Avatar>

<!-- 透過 `client:` 指令引入其他 UI 框架元件:-->
<ReactPokemonComponent client:visible />

<!-- 在 HTML 使用 JavaScript 表達式,類似 JSX:-->
<ul>
  {myFavoritePokemon.map((data) => <li>{data.name}</li>)}
</ul>

<!-- 使用模板指令合併 class name,字串甚至是物件都可以! -->
<p class:list={["add", "dynamic", { classNames: true }]} />

以元件為基礎的設計

元件的設計理念是要能夠重複使用組合。你可以在元件裡使用其他元件,創造出更多更複雜的 UI。舉例來說,Button 元件可以組合成 ButtonGroup 元件:

---
import Button from './Button.astro';
---
<div>
  <Button title="Button 1" />
  <Button title="Button 2" />
  <Button title="Button 3" />
</div>

元件參數

Astro 元件可以自訂和接受參數,供元件模板算繪 HTML 時使用。參數可以在 frontmatter 腳本透過全域的 Astro.props 存取。

以下範例中,元件接受傳入的 greetingname 參數。注意參數是從全域的 Astro.props 物件解構出來的。

---
// src/components/GreetingHeadline.astro
// 用法:<GreetingHeadline greeting="Howdy" name="Partner" />
const { greeting, name } = Astro.props;
---
<h2>{greeting}, {name}!</h2>

其他 Astro 元件/版面/頁面,可以傳遞參數給 GreetingCard 元件:

---
// src/components/GreetingCard.astro
import GreetingHeadline from './GreetingHeadline.astro';
const name = 'Astro';
---
<h1>歡迎卡片</h1>
<GreetingHeadline greeting="Hi" name={name} />
<p>祝你有個美好的一天!</p>

你可以用 TypeScript 自訂 Props 的型別,Astro 能自動偵測 frontmatter 的 Props,依此顯示警告/錯誤訊息。另外,從 Astro.props 解構參數時,能夠設定預設值。

---
// src/components/GreetingHeadline.astro
interface Props {
  name: string;
  greeting?: string;
}

const { greeting = "Hello", name } = Astro.props;
---
<h2>{greeting}, {name}!</h2>

未傳入參數值時,可以設定預設值。

---
// src/components/GreetingHeadline.astro
const { greeting = "Hello", name = "Astronaut" } = Astro.props;
---
<h2>{greeting}, {name}!</h2>

插槽

<slot /> 元素是為外部 HTML 內容預留的位置,供子元素從其他檔案放入元件模板用。

預設情況下,元件會算繪所有透過 <slot /> 傳入的子元素。

:::note參數 是傳遞給 Astro 元件的屬性,可以在元件中透過 Astro.props 存取,而 插槽 則是在它們被寫下的位置算繪 HTML 子元素。:::

---
// src/components/Wrapper.astro
import Header from './Header.astro';
import Logo from './Logo.astro';
import Footer from './Footer.astro';

const { title } = Astro.props;
---
<div id="content-wrapper">
  <Header />
  <Logo />
  <h1>{title}</h1>
  <slot />  <!-- 子元素會跑到這 -->
  <Footer />
</div>
---
// src/pages/fred.astro
import Wrapper from '../components/Wrapper.astro';
---
<Wrapper title="Fred's Page">
  <h2>關於 Fred</h2>
  <p>關於 Fred 的一些資訊。</p>
</Wrapper>

一整頁 HTML 內容被 <SomeLayoutComponent></SomeLayoutComponent> 標籤「包起來」,接著傳給其他元件算繪頁面所有元素——這個模式便是 Astro 版面元件的基礎。

請參閱 Astro.slots 工具函式以了解更多存取與算繪插槽內容的方式。

具名插槽

Astro 元件可以放置具名插槽,讓你只傳入名字相符的 HTML 元素到指定插槽。

具名插槽使用 name 屬性:

---
// src/components/Wrapper.astro
import Header from './Header.astro';
import Logo from './Logo.astro';
import Footer from './Footer.astro';

const { title } = Astro.props;
---
<div id="content-wrapper">
  <Header />
  <!-- 有 `slot="after-header"` 屬性的子元素會跑來這 -->
  <slot name="after-header" />
  <Logo />
  <h1>{title}</h1>
  <!-- 沒有 `slot` 屬性的子元素,以及有 `slot="default"` 屬性的子元素會跑來這 -->
  <slot />
  <Footer />
  <!-- 有 `slot="after-footer"` 屬性的子元素會跑來這 -->
  <slot name="after-footer" />
</div>

要把 HTML 內容放進特定的插槽,必須在子元素加上 slot 屬性指定對應的插槽,所有其他子元素則會被放入預設(不具名)的 <slot />

---
// src/pages/fred.astro
import Wrapper from '../components/Wrapper.astro';
---
<Wrapper title="Fred's Page">
  <img src="https://my.photo/fred.jpg" slot="after-header" />
  <h2>關於 Fred</h2>
  <p>關於 Fred 的一些資訊。</p>
  <p slot="after-footer">版權所有 2022</p>
</Wrapper>

:::tip在子元素加上 slot="my-slot",藉此將它們傳遞到對應的 <slot name="my-slot" /> 預留位置。:::

若要一次傳遞多個 HTML 元素,但不想額外包一層 <div> 的話,可以在 Astro 的 <Fragment/> 元件使用 slot=""

---
// 客製一個 table,使用具名插槽為 header 和 body 預留兩個位置
---
<table class="bg-white">
  <thead class="sticky top-0 bg-white"><slot name="header" /></thead>
  <tbody class="[&_tr:nth-child(odd)]:bg-gray-100"><slot name="body" /></tbody>
</table>

透過 slot="" 屬性,將表格內容放置於 "header""body"。還能單獨調整 HTML 元素的樣式。

---
import CustomTable from './CustomTable.astro';
---
<CustomTable>
  <Fragment slot="header"> <!-- 表首會傳到這 -->
    <tr><th>產品名稱</th><th>庫存量</th></tr>
  </Fragment>

  <Fragment slot="body"> <!-- 內容會傳到這 -->
    <tr><td>夾腳拖</td><td>64</td></tr>
    <tr><td>靴子</td><td>32</td></tr>
    <tr><td>球鞋</td><td class="text-red-500">0</td></tr>
  </Fragment>
</CustomTable>

只能透過具名插槽傳送一層子元素,不能傳送巢狀元素,請特別留意。

:::tip具名插槽也可傳遞到 UI 框架元件 (EN)!:::

:::noteAstro 插槽名稱無法動態產生(例如透過 map 函式操作)。想在 UI 框架元件裡動態產生插槽名稱的話,最好還是讓框架本身去處理。:::

插槽的備用內容

插槽也能算繪備用內容。沒有對應的子元素傳遞過來時,<slot /> 元素會算繪自己的備用內容。

---
// src/components/Wrapper.astro
import Header from './Header.astro';
import Logo from './Logo.astro';
import Footer from './Footer.astro';

const { title } = Astro.props;
---
<div id="content-wrapper">
  <Header />
  <Logo />
  <h1>{title}</h1>
  <slot>
    <p>這是我的備用內容,如果沒有傳子元素到 slot 時會顯示。</p>
  </slot>
  <Footer />
</div>

備用內容只會在沒有符合 slot="name" 屬性的元素被傳遞到具名插槽時顯示。

當插槽元素存在,但是沒有內容可以傳遞時,Astro 會傳一個空插槽。備用內容不能當做傳遞空插槽進去時的預設值。備用內容只會在找不到插槽元素時顯示。

轉移插槽

插槽可以轉移到其他元件。以建立巢狀版面為例:

---
---
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
		<meta name="viewport" content="width=device-width" />
		<meta name="generator" content={Astro.generator} />
    <slot name="head" />
	</head>
	<body>
		<slot />
	</body>
</html>
// src/layouts/HomeLayout.astro
---
import BaseLayout from './BaseLayout.astro';
---
<BaseLayout>
  <slot name="head" slot="head" />
  <slot />
</BaseLayout>

:::notenameslot 屬性都可將具名插槽轉移至其他元件。:::

傳給 HomeLayout 的預設插槽和 head 插槽,現在會被轉移到上層的 BaseLayout

// src/pages/index.astro
---
import HomeLayout from '../layouts/HomeLayout.astro';
---
<HomeLayout>
	<title slot="head">Astro</title>
	<h1>Astro</h1>
</HomeLayout>

HTML 元件

Astro 支援將 .html 檔案匯入成元件,或放置到 src/pages 子目錄裡當作頁面。如果你想在不使用框架的前提下重複利用現有網站程式碼,或想確保元件沒有動態功能的話,可以考慮使用 HTML 元件。

HTML 元件只能包含合法的 HTML,沒有 Astro 元件的特色功能:

:::noteHTML 元件裡的 <slot /> 元素可以正常運作。如果想用 HTML Web Component 插槽 元素的話,在 <slot> 元素加上 is:inline。:::

下一步

繼續閱讀如何在 Astro 專案使用 UI 框架元件 (EN)
貢獻 社群 贊助