在幫我的電子報寫網站的時候,我意識到 Routing 跟同構並不是我最關注的重點,也沒有要頻繁與資料庫拿資料或是在瀏覽器端做互動的需求,所以當時在想 Next.js 這樣的 SSR 框架是否過於強大。雖然 Next.js 也支援全靜態輸出,但想到寫 React 元件的心智負擔還是讓我有點卻步。
第一次聽到 Astro 是同事介紹,想說機會難得不妨一試。Astro 提供了以 SFC(單檔案元件) 為核心的開發方式,可以很輕鬆地快速把網頁建構出來,架構上也有配合雲端的趨勢,針對 Vercel、Netlify、Github Pages 等服務做調整,只要將程式碼上傳就可以打造出一個可以用的網頁。
市場上已經有許多靜態網站產生器,或許你也在想,為何還需要另一個呢?我的部落格從 Jekyll、Hexo,一路進化到 Gatsby,曾經試過多次框架的轉換。這段經歷讓我更加明確自己在寫這種內容導向的網站時所看重的要點。
Jekyll 和 Hexo 兩者有許多相似之處。儘管它們具備了建立內容網站的基本功能,但在擴充 Markdown 語法或是實現更高度互動性的 MDX 上卻相對困難,而且必須適應它們所固有的樣板語法。
使用 Gatsby 時則需要自己寫 GraphQL 。剛開始這種彈性寫起來很愉快,但在沒有定義 Schema 的情況下,當我想要增加 Markdown 的 frontmatter 屬性時,就會變得相當麻煩。
久而久之,我常常陷入這樣的情況:明明只是想簡單編寫一個頁面,卻不得不花費大量時間去寫 GraphQL 和 React 元件。
另一個選項是 11ty,它和 Astro 很相似,但我認為最大的差異在於樣板語法。11ty 支援多種樣板語法,然而對於已經在 React 上寫了多年的我來說,JSX 似乎是最直觀的方式,而 Astro 剛好實現了這一點。下面分享一些我覺得 Astro 和其他產生器明顯不同的地方。
單一檔案元件(Single-File Components)
在 Astro 的設計當中,每一個檔案都可以當作完整的元件。
也就是 JavaScript(只會在 build time 時執行)、HTML、style 可以寫在同一個檔案,讓框架處理 class name 隔離。你如果會寫 JSX 會寫 Vue,就會寫 Astro 的元件,不需要再另外學樣板語法。
---
const variable = 'Hello';
---
<section>
<p>
{variableA}
</p>
</section>
<style>
p { font-size: 14px; }
</style>
寫網頁的時候能將 JavaScript、HTML、CSS 寫在同一個檔案是件相當舒服的事,而且 Astro 預設情況下就支援,不需要額外的設定。
雖然建構元件的方式跟 Vue 或 Svelte 一樣,但要注意的是 Astro 預設並不是 SSR,也不是前端框架,所以頁面都只會在 build time 執行,不會有額外的 JavaScript 產出。(在不開啟 SSR 模式的情況下)
如果要在瀏覽器端執行 JavaScript 做互動怎麼辦?Astro 提供兩種方式:
- 寫在
<script>
裡面,但因為沒有 DOM 綁定的功能,所以要寫像是document.querySelector()
的程式碼 - 直接用別的前端框架 import 進來
Astro Island
有時候我們會有個需求是,大部分的頁面都是靜態,但就偏偏一兩個頁面或頁面上的一部份要做大量互動,比如部落格的留言區。
瀏覽器互動在 Astro 裡可以引用其他框架的元件檔案,像是 React、Vue 等等,只要有對應的 adapter 都可以加進來。這個功能對我來說蠻特別的,算是讓靜態網站產生器又多了一份彈性。
<aside>
<ReactOrVueComponent client:load />
</aside>
在這段程式碼裡面,遇到非 Astro 的元件時,Astro 在編譯後會建立一個空的 DOM,在瀏覽器端載入 JavaScript 再把元件的 DOM 節點掛載上來。這邊的 client:load
提示了 Script 應該在什麼時機點載入。這個選項我覺得蠻貼心的一點是他有 client:media
跟 client:visible
。
有時候會遇到只有特定裝置才想要載入的元件,例如在手機螢幕上才想要做可滑動的菜單;或是只有在瀏覽到特定位置的時候才去載入元件。沒有框架的幫忙,這些都要自己額外寫 JavaScript 解決,但 Astro 都幫你搞定好了。
這邊有一個問題,不同框架(例如:Astro、React)兩者要怎麼共享狀態,Astro 這邊已經提供解法,雖然說又引入一個 Library,不免讓人疑惑付出的成本是否大於好處。
我對於在靜態網站生成器裏面加入其他前端框架支援這件事是抱持正面想法的。或許 Astro 本身就已經滿足大部分網站的需求,如果你真的要做純互動的網站,那一開始也不會選 Astro。而加入前端框架支援這件事多少滿足了少部分網站的需求,既能夠快速建立網站,必要時 Preact 裝下去寫互動也是相當輕鬆的。
Content Collections
Astro 有個功能叫做 Content Collections。簡單來說是幫助你 Query 靜態的 Markdown 檔案。在 content/
底下的檔案都會被當作 Content Collections。
import { getEntry } from "astro:content";
const entry = await getEntry("weekly", "my-first-weekly.md");
const { Content } = await entry.render();
Astro 雖然可以直接用 Markdown 當作一個頁面渲染,但如果你想保留更多頁面調整的彈性,它也提供了內建的查找函數給你做搜尋。entry.render()
會回傳一個已經處理好的 <Content />
元件,可以放在你想要的位置,就能把 Markdown 的內容以 HTML 形式渲染出來。
型別支援
Astro 支援 TypeScript,儘管是靜態頁面,你還是可以透過宣告 Props 的方式來定義該元件能夠接收哪些屬性,使用上方便很多。
// MyComponent.astro
export interface Prop {
user: string
}
// <MyComponent user="kalan" />
除了元件的屬性可以被定義之外,Content 也是可以被定義的。例如在文章的定義當中,我們通常會使用 frontmatter 來定義一些 meta 資料,像是標題、大綱、圖片等等:
---
title: 這是標題
summary: 這是大綱
image: https://image.com
---
Astro 提供 Schema 定義的功能(背後實作是 zod),只要在 content/config.ts
裡定義 Schema 型別,當你在使用 getCollection
或 getEntry
的時候就能得到對應的型別。
import { z, defineCollection } from "astro:content";
const weeklyCollection = defineCollection({
type: "content",
schema: z.object({
published_at: z.date(),
issue_num: z.number(),
title: z.string(),
description: z.string().optional(),
image: z.string().optional(),
}),
/* ... */
});
export const collections = {
weekly: weeklyCollection,
};
// 其他 Astro 元件內呼叫
import { getCollection, getEntry } from "astro:content";
const entry = await getEntry("weekly", 'my-first-weekly.md'); // entry 會解析出 weekly 型別,來自上面的 weeklyCollection
Astro 使用心得
整體而言 Astro 帶給我的感覺是相當舒適的,如果不需要什麼特別的功能,把它當成可以塞變數的 HTML 來寫也完全沒問題。
靜態網站生成器的功能都是大同小異的,上述講的功能,其他工具真的要做也找得到方法。那麼 Astro 到底好在哪裡?我覺得是舒服的開發者體驗、兼顧簡潔與彈性的語法(jsx)、對於特定場景的設計很到位(加入其他前端框架支援以及多元載入腳本的方式),還有和雲端服務的整合。
前端如何建構靜態網站,在這幾年有很大變化。從 Gatsby 開始,開發者一直在尋找怎麼更有效率地去建構網頁,不僅僅只是讓元件可以在伺服器端渲染而已,舉凡 JavaScript 的載入時機、Prefetch、透過 Service Worker 做快取、圖片優化機制、樣式管理、Edge Computing、兼顧開發速度,都是一個現代靜態網站生成器所要考慮的功能,不再只是把 Markdown 變成 HTML 然後上傳到伺服器而已。