第五課:Astro Actions —— 安全、簡單的伺服器通訊
核心觀念:Actions 是什麼?
前你需要寫一個 src/pages/api/login.ts,然後在前端寫 fetch('/api/login', { method: 'POST', ... })。
在 Astro 5 中,Action 就像是一個可以被前端直接呼叫的「伺服器函式」。它具備:
- 自動型別檢查:前端知道後端需要什麼資料。
- 安全:直接在伺服器端執行。
- 簡單:不需要處理 URL 和 JSON 轉換。
1. 手把手實作:定義你的第一個 Action
請建立檔案 src/actions/index.ts:
import { defineAction } from 'astro:actions'
import { z } from 'astro:schema'
export const server = {
// 定義一個名為 getGreeting 的動作
getGreeting: defineAction({
input: z.object({
name: z.string(),
}),
handler: async (input) => {
// 這裡可以寫資料庫操作,例如:db.user.create(...)
return `你好 ${input.name},這是來自伺服器的回覆!`
},
}),
}
2. 手把手實作:在前端呼叫 Action
修改你的 src/pages/index.astro,我們來做一個簡單的聯絡表單:
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout pageTitle="Astro Actions 實驗">
<h2>與伺服器對話</h2>
<form id="greeting-form">
<input type="text" name="name" placeholder="輸入你的名字" required />
<button type="submit">發送</button>
</form>
<p id="result"></p>
<script>
import { actions } from 'astro:actions';
const form = document.querySelector('form');
const resultElement = document.querySelector('#result');
form?.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const name = formData.get('name') as string;
// 直接呼叫伺服器函式,像呼叫普通 function 一樣!
const { data, error } = await actions.getGreeting({ name });
if (!error) {
resultElement!.textContent = data;
} else {
resultElement!.textContent = "出錯了:" + error.message;
}
});
</script>
</BaseLayout>
📝 第五課練習題
任務目標:實作一個「留言板」功能。
- 在
src/actions/index.ts中,增加一個新的 Action 叫做postComment。 - 這個 Action 需要接收
author(字串) 和content(字串)。 - 在
handler中,將收到的資料console.log出來(模擬存入資料庫),並回傳「留言成功」。 - 在
about.astro建立一個簡單的表單,讓使用者輸入名字與內容,並在提交後用alert()顯示伺服器的回傳訊息。 - 進階挑戰: 嘗試在 Action 的
input中使用z.string().min(5),看看如果你輸入少於 5 個字的內容,前端會收到什麼樣的錯誤訊息?
第五課練習題實作放在下一篇 ( 我也是第一次學習,自己試作,不一定是對的喔👍 )
📚 額外筆記
※ resultElement!.textContent = data;
這個 ! 不是邏輯 NOT,而是 TypeScript 的「非空斷言(Non-null Assertion)」。👉 TypeScript 保證:resultElement 絕對不是 null 或 undefined
為什麼會需要 !?
假設你前面是這樣拿 DOM:
const resultElement = document.querySelector('#result');
TypeScript 看到這行,會推斷型別是:
Element | null
因為:
- 找得到 →
Element - 找不到 →
null
所以你直接寫:
resultElement.textContent = data
❌ TypeScript 會報錯:
Object is possibly 'null'.
! 在這裡做了什麼?
resultElement!.textContent = data
等於在跟 TS 說:
「你不用再檢查了,我確定它存在」
TypeScript 就會放行,不再報錯。
⚠️ 注意:這只是「讓編譯器不再提示」,執行期不會幫你檢查。
等價寫法(比較安全)
✅ 寫法 1:if 判斷(最安全)
if (resultElement) {
resultElement.textContent = data
}
✅ 寫法 2:可選鏈(不會噴錯)
resultElement?.textContent = data
找不到元素時,這行什麼都不做。
⚠️ 寫法 3:非空斷言(你現在這個)
resultElement!.textContent = data
如果 DOM 不存在,會在 runtime 直接噴錯:
Cannot set properties of null
什麼時候「可以」用 !?
✅ 合理情境:
// HTML 一定有
<div id="result"></div>
const resultElement = document.querySelector('#result')!;
resultElement.textContent = data
例如:
- 你控制 HTML 結構
- DOM 在 script 執行前就存在
- 不可能被刪除
什麼時候「不該」用?
❌ 不確定元素是否存在
❌ 動態產生 / 延遲載入的 DOM
❌ API 回傳的資料
常見誤解 ❌
!value // ❌ 這是 JS 的 NOT
value! // ✅ 這是 TS 的非空斷言
位置不同,意思完全不一樣。
※ import { actions } from 'astro:actions'
是在 Astro 4.x(含)之後 用來存取 Astro Actions(伺服器動作) 的核心 API。
astro:actions讓你在前端「直接呼叫後端函式」
不用自己寫/api/*.ts、不用手動 fetch
actions 是什麼?
actions 是一個 型別安全(type-safe)的物件,
裡面包含你在 src/actions/** 定義的所有「伺服器動作」。
概念對照

最基本範例
1️⃣ 定義一個 Action(後端)
// src/actions/serverTime.ts
import { defineAction } from 'astro:actions'
export const serverTime = defineAction({
handler: async () => {
return new Date().toISOString()
},
})
2️⃣ 在前端使用
import { actions } from 'astro:actions'
const result = await actions.serverTime()
console.log(result)
👉 這段是在瀏覽器跑,但實際執行在伺服器
背後實際發生什麼事?
// 當你呼叫:
actions.serverTime()
Astro 會自動幫你做:
- 建立隱藏 API endpoint
- 自動送 request
- 執行 server handler
- 回傳結果
- 保證型別正確
你完全不用碰 fetch。
為什麼要用 Astro Actions?
✅ 優點
- 🔐 預設 Server-only
- 🧠 TypeScript 自動推斷
- 📦 不暴露實作細節
- 🧼 程式碼乾淨
常見用途
- 表單送出
- 登入 / 註冊
- 存資料庫
- 呼叫第三方 API
- 權限驗證
與 server:defer 的關係

➡️ 顯示用 defer,動作用 actions
Action + 表單範例(超常見)
---
import { actions } from 'astro:actions'
---
<form
onSubmit={async (e) => {
e.preventDefault()
const res = await actions.login({
email: 'test@test.com',
password: '123456',
})
console.log(res)
}}
>
<button>登入</button>
</form>
跟 API Routes 的差別

※ actions 怎麼取得自訂 api ?
// src/actions/serverTime.ts
import { defineAction } from 'astro:actions'
export const serverTime = defineAction({
handler: async () => {
return new Date().toISOString()
},
})
// src/pages/index.astro
import { actions } from 'astro:actions'
actions.serverTime()
// src/actions/index.ts
export const server = {
getGreeting: defineAction({
input: z.object({
name: z.string(),
}),
handler: async (input) => {
return `你好 ${input.name},這是來自伺服器的回覆!`
},
}),
postComment: defineAction({
input: z.object({
author: z.string(),
content: z.string().min(5),
}),
handler: async (input) => {
console.log(input)
return '留言成功'
},
}),
}
// src/pages/index.astro
import { actions } from 'astro:actions'
actions.getGreeting()
actions.postComment()
不同的 api 包裝方式,卻都能在 actions 直接取得 ?
Astro Actions 的「自動展開機制」核心設計
actions 不是匯出你寫的變數名稱
而是 Astro 在 build 時,掃描所有 defineAction,把它們「攤平(flatten)」成一個 actions 物件
所以:
export const server = {
getGreeting: defineAction(...),
postComment: defineAction(...),
}
👇 在前端會變成:
actions.getGreeting()
actions.postComment()
為什麼不是 actions.server.getGreeting()?
因為 server 只是你在檔案裡用來分組的普通物件名稱
Astro 不把它當成命名空間
👉 Astro 只在乎兩件事:
- 哪些東西是
defineAction(...) - 它們的 key 名稱
Astro 實際做了什麼?
Astro 在啟動時會:
掃描 src/actions/**/*.ts
↓
找到所有 defineAction
↓
取「屬性名稱」當 action 名
↓
全部掛到 actions 物件底下
等價於 Astro 內部幫你做了這件事(概念):
actions = {
getGreeting,
postComment,
serverTime,
...
}
不管你中間包了幾層物件。
為什麼要這樣設計?(設計理由)
1️⃣ 前端呼叫要「扁平、好用」
actions.getGreeting()
// 比
actions.server.getGreeting()
actions.user.comment.post()
// 簡單太多。
2️⃣ Actions 本質是「API 端點」
API endpoint 天生就是「全域名稱」
POST /_actions/getGreeting
POST /_actions/postComment
不是巢狀物件。
3️⃣ TS 型別自動推斷更容易
扁平結構:
actions.xxx()
👉 TS inference 最穩定
👉 不會有動態 key 問題
那「server」這個物件有沒有用?
有,但只是給你自己看、自己整理用。
你也可以這樣寫(完全一樣)
export const getGreeting = defineAction(...)
export const postComment = defineAction(...)
// 或
export const commentActions = {
postComment: defineAction(...),
}
// 👉 前端 都一樣:
actions.postComment()
⚠️ 重要限制
❌ Action 名稱不能衝突
// file A
export const server = {
hello: defineAction(...)
}
// file B
export const user = {
hello: defineAction(...)
}
❌ 會衝突
👉 兩個都變成 actions.hello
※ 大型專案 Astro Actions 命名規範
🎯 設計目標
- 避免 action 名稱衝突
- 一眼就知道「做什麼 + 對誰」
- 前端好 autocomplete
- 檔案怎麼拆都不影響呼叫方式
✅ 核心原則
Action 名稱必須是「全域唯一」
因為最後都會變成:
actions.xxx()
🧠 推薦命名公式
<Domain><Action><Target>
或(更清楚):
<Domain><Verb><Object>
🧩 常用 Domain(第一段)

🛠 動詞(第二段)

📦 物件(第三段)

✅ 好範例
actions.userGetProfile()
actions.userUpdateProfile()
actions.postCreatePost()
actions.commentDeleteComment()
actions.authLogin()
actions.authLogout()
❌ 壞範例
actions.get()
actions.create()
actions.submit()
actions.send()
actions.save()
📁 檔案結構怎麼拆?
📁 推薦結構
src/actions/
├─ user.actions.ts
├─ auth.actions.ts
├─ post.actions.ts
├─ comment.actions.ts
└─ admin.actions.ts
user.actions.ts
export const userActions = {
userGetProfile: defineAction(...),
userUpdateProfile: defineAction(...),
}
comment.actions.ts
export const commentActions = {
commentCreateComment: defineAction(...),
commentDeleteComment: defineAction(...),
}
前端一律這樣用
import { actions } from 'astro:actions'
await actions.commentCreateComment()
🔐 Auth / Admin 特別規範
Auth 類(建議特殊動詞)
authLogin
authLogout
authRefreshToken
authValidateSession
Admin 類(強制 Admin 開頭)
adminGetUsers
adminDeleteUser
adminBanUser









