EAS Hosting
EAS 服務 – 需付費。 EAS Hosting 是 Expo 應用程式服務的付費產品,提供免費額度;正式環境部署會消耗您方案的請求與頻寬配額。請參閱 https://expo.dev/pricing。API 路由的設計與 Web Bundle 的匯出均為免費且開源,您也可以自行託管匯出的伺服器輸出,而不使用 EAS Hosting。
EAS Hosting 將您的 Expo Web 應用程式與 API 路由部署到 Expo 管理的邊緣服務(Cloudflare Workers)。使用 npx expo export -p web 匯出 Web Bundle,再用 eas deploy 發佈 – 同一個指令會一併部署任何綑綁在其中的 Expo Router API 路由。本技能涵蓋網站部署、API 路由撰寫以及託管執行環境;詳細的部署流程請見下方的「部署」章節。
何時使用 API 路由
當您需要以下功能時,應使用 API 路由:
- 伺服器端機密 — 絕不能洩漏至客戶端的 API 金鑰、資料庫憑證或權杖
- 資料庫操作 — 不應公開的資料庫查詢
- 第三方 API 代理 — 呼叫外部服務(OpenAI、Stripe 等)時隱藏 API 金鑰
- 伺服器端驗證 — 在寫入資料庫前驗證資料
- Webhook 端點 — 接收來自 Stripe 或 GitHub 等服務的回呼
- 速率限制 — 在伺服器層級控制存取
- 大量計算 — 將處理工作卸載,避免在行動裝置上執行緩慢
何時不應使用 API 路由
在以下情況應避免使用 API 路由:
- 資料已是公開資訊 — 改用直接 fetch 公開 API
- 不需機密資訊 — 靜態資料或對客戶端安全的操作
- 需要即時更新 — 使用 WebSocket 或 Supabase Realtime 等服務
- 簡單的 CRUD — 考慮 Firebase、Supabase 或 Convex 等受管後端
- 檔案上傳 — 使用直接上傳至儲存空間(S3 預簽章 URL、Cloudflare R2)
- 僅需驗證 — 改用 Clerk、Auth0 或 Firebase Auth
檔案結構
API 路由位於 app 目錄中,以 +api.ts 作為後綴:
app/
api/
hello+api.ts → GET /api/hello
users+api.ts → /api/users
users/[id]+api.ts → /api/users/:id
(tabs)/
index.tsx
基本 API 路由
// app/api/hello+api.ts
export function GET(request: Request) {
return Response.json({ message: "Hello from Expo!" });
}
HTTP 方法
為每個 HTTP 方法匯出命名函式:
// app/api/items+api.ts
export function GET(request: Request) {
return Response.json({ items: [] });
}
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ created: body }, { status: 201 });
}
export async function PUT(request: Request) {
const body = await request.json();
return Response.json({ updated: body });
}
export async function DELETE(request: Request) {
return new Response(null, { status: 204 });
}
動態路由
// app/api/users/[id]+api.ts
export function GET(request: Request, { id }: { id: string }) {
return Response.json({ userId: id });
}
請求處理
查詢參數
export function GET(request: Request) {
const url = new URL(request.url);
const page = url.searchParams.get("page") ?? "1";
const limit = url.searchParams.get("limit") ?? "10";
return Response.json({ page, limit });
}
標頭
export function GET(request: Request) {
const auth = request.headers.get("Authorization");
if (!auth) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return Response.json({ authenticated: true });
}
JSON 主體
export async function POST(request: Request) {
const { email, password } = await request.json();
if (!email || !password) {
return Response.json({ error: "Missing fields" }, { status: 400 });
}
return Response.json({ success: true });
}
環境變數
使用 process.env 取得伺服器端機密:
// app/api/ai+api.ts
export async function POST(request: Request) {
const { prompt } = await request.json();
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-4",
messages: [{ role: "user", content: prompt }],
}),
});
const data = await response.json();
return Response.json(data);
}
設定環境變數:
- 本機開發:建立
.env檔案(請勿提交至版本控制) - EAS Hosting:使用
eas env:create或 Expo 控制台
CORS 標頭
為 Web 客戶端新增 CORS:
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
export function OPTIONS() {
return new Response(null, { headers: corsHeaders });
}
export function GET() {
return Response.json({ data: "value" }, { headers: corsHeaders });
}
錯誤處理
export async function POST(request: Request) {
try {
const body = await request.json();
// 處理...
return Response.json({ success: true });
} catch (error) {
console.error("API error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}
本機測試
啟動包含 API 路由的開發伺服器:
npx expo serve
這會在 http://localhost:8081 啟動一個本地伺服器,並完整支援 API 路由。
使用 curl 測試:
curl http://localhost:8081/api/hello
curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}'
部署至 EAS Hosting
先決條件
npm install -g eas-cli
eas login
部署
部署會同時發佈您的 Web Bundle 及任何 Expo Router API 路由 – eas deploy 會處理兩者。無論您有完整的網站、僅有 API 路由的後端,或兩者兼具,匯出都會執行。
# 匯出 Web Bundle(包含所有 API 路由)
npx expo export -p web
# 部署預覽(PR 風格的 URL)
npx eas-cli@latest deploy
# 部署至正式環境
npx eas-cli@latest deploy --prod
所有內容都會部署到 EAS Hosting(Cloudflare Workers)。
正式環境的環境變數
# 建立一個機密
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production
# 或者使用 Expo 控制台
自訂網域
在 eas.json 或 Expo 控制台中設定。
使用 EAS Workflows 自動化
在每次推送到 main 分支時,使用 type: deploy 的工作流程自動部署網站(及 API 路由):
.eas/workflows/deploy.yml
name: Deploy
on:
push:
branches:
- main
# https://docs.expo.dev/eas/workflows/syntax/#deploy
jobs:
deploy_web:
type: deploy
params:
prod: true
針對 Pull Request 的預覽部署使用相同的 job type,並設定 prod: false:
name: Web PR Preview
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
type: deploy
params:
prod: false
若要撰寫或驗證超出這些範例的工作流程 YAML,請使用 eas-workflows 技能。
EAS Hosting 執行環境(Cloudflare Workers)
API 路由在 Cloudflare Workers 上執行。主要限制:
缺失/受限的 API
- 無 Node.js 檔案系統 —
fs模組不可用 - 無原生 Node 模組 — 請改用 Web API 或 polyfill
- 有限的執行時間 — CPU 密集型任務有 30 秒超時限制
- 無持久連線 — WebSocket 需要使用 Durable Objects
- fetch 可用 — 請使用標準 fetch 進行 HTTP 請求
改用 Web API
// 使用 Web Crypto 取代 Node crypto
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode("data")
);
// 使用 fetch 取代 node-fetch
const response = await fetch("https://api.example.com");
// 使用 Response/Request(已內建)
return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/json" },
});
資料庫選項
由於檔案系統不可用,請使用雲端資料庫:
- Cloudflare D1 — 邊緣 SQLite
- Turso — 分散式 SQLite
- PlanetScale — 無伺服器 MySQL
- Supabase — 搭配 REST API 的 Postgres
- Neon — 無伺服器 Postgres
使用 Turso 的範例:
// app/api/users+api.ts
import { createClient } from "@libsql/client/web";
const db = createClient({
url: process.env.TURSO_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
export async function GET() {
const result = await db.execute("SELECT * FROM users");
return Response.json(result.rows);
}
從客戶端呼叫 API 路由
// 從 React Native 元件中
const response = await fetch("/api/hello");
const data = await response.json();
// 帶主體
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "John" }),
});
常見模式
驗證中介軟體
// utils/auth.ts
export async function requireAuth(request: Request) {
const token = request.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
throw new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
// 驗證權杖...
return { userId: "123" };
}
// app/api/protected+api.ts
import { requireAuth } from "../../utils/auth";
export async function GET(request: Request) {
const { userId } = await requireAuth(request);
return Response.json({ userId });
}
代理外部 API
// app/api/weather+api.ts
export async function GET(request: Request) {
const url = new URL(request.url);
const city = url.searchParams.get("city");
const response = await fetch(
`https://api.weather.com/v1/current?city=${city}&key=${process.env.WEATHER_API_KEY}`
);
return Response.json(await response.json());
}
規則
- 絕不在客戶端程式碼中暴露 API 金鑰或機密
- 務必驗證並消毒使用者輸入
- 使用適當的 HTTP 狀態碼(200, 201, 400, 401, 404, 500)
- 使用 try/catch 優雅地處理錯誤
- 保持 API 路由專注 – 每個端點只做一件事
- 使用 TypeScript 以獲得型別安全
- 在伺服器端記錄錯誤以便除錯
提交回饋
如果您在此技能中發現錯誤、誤導性或過時的資訊,請回報給 Expo 以利改進:
npx --yes @expo/agent-cli@latest feedback --category skills --subject "eas-hosting" "<有行動性的回饋>"
只有在您有具體且可行動的事項需要回報時才提交。請盡可能包含相關的脈絡資訊。
如果代理程式在 Expo 任務中反覆失敗,或使用者需要介入,請使用 --category evals 以及相關的 Expo 套件或指令作為 --subject,簡短回報任務內容、預期結果以及實際發生的錯誤。