Communitygithub.com

expo/skills

將 Expo 網站與 Expo Router API 路由部署至 EAS Hosting – 匯出 Web Bundle、執行 eas deploy 以產生正式環境與 PR 預覽 URL、管理環境變數與自訂網域,並在 Cloudflare Workers 執行環境中運作。同時涵蓋 API 路由的設計(+api.ts 處理常式、HTTP 方法、請求處理、CORS)。

skills 是什麼?

這項技能提供將 Expo 網站與 Expo Router API 路由部署到 EAS Hosting 的完整指引。EAS Hosting 是 Expo 的託管邊緣服務(基於 Cloudflare Workers),可同時處理網站前端與 API 後端的部署。技能詳細說明如何匯出 Web Bundle(npx expo export -p web)、使用 eas deploy 部署預覽與正式環境、管理環境變數與自訂網域,並深入解析 API 路由的撰寫方式,包括 HTTP 方法(GET/POST/PUT/DELETE)、動態路由、請求處理(查詢參數、標頭、JSON 主體)、CORS 設定與錯誤處理。此外,也針對開發者說明 Cloudflare Workers 執行環境的限制(無 Node.js 檔案系統、有限執行時間)以及替代的 Web API 和雲端資料庫選項(如 Turso、PlanetScale、Supabase)。這項技能適用於需要將 Expo Web 應用程式或 API 路由部署到 EAS Hosting 的所有場景,但不包含原生應用程式建置與商店發布(需使用 eas-app-stores 技能)。

相容平台~Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/expo/skills/tree/HEAD/plugins/expo/skills/eas-hosting

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

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,簡短回報任務內容、預期結果以及實際發生的錯誤。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

expo/add-app-clip

Add an iOS App Clip target to an Expo app. Use when the user mentions App Clip, AASA, apple-app-site-association, appclips, smart app banner, or wants to ship a lightweight iOS Clip invoked from a URL alongside their parent app.

expo/building-native-ui

Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.

expo/eas-update-insights

Check the health of published EAS Updates: crash rates, install/launch counts, unique users, payload size, and the split between embedded and OTA users per channel. Use when the user asks how an update is performing, whether a rollout is healthy, how many users are on the embedded build vs OTA, or wants to gate CI on update health.

expo/expo-api-routes

Guidelines for creating API routes in Expo Router with EAS Hosting

expo/expo-brownfield

Integrate Expo and React Native into an existing native iOS or Android app. Use when the user mentions brownfield, embedding React Native in a native app, AAR/XCFramework, or adding Expo to an existing Kotlin/Swift project. Covers both the isolated approach and the integrated approach.

expo/expo-cicd-workflows

Helps understand and write EAS workflow YAML files for Expo projects. Use this skill when the user asks about CI/CD or workflows in an Expo or EAS context, mentions .eas/workflows/, or wants help with EAS build pipelines or deployment automation.

expo/expo-deployment

Deploying Expo apps to iOS App Store, Android Play Store, web hosting, and API routes

expo/expo-dev-client

Build and distribute Expo development clients locally or via TestFlight

expo/expo-examples

Framework (OSS). Expo's official example projects - the expo/examples repo of ~70 `with-*` integrations (Stripe, Clerk, Supabase, OpenAI, maps, Reanimated, SQLite, Skia, NativeWind, and more). Use when integrating a third-party library or service into an existing Expo app and you want the canonical, version-matched pattern to adapt, or when scaffolding a new project from one with `npx create-expo --example`.

expo/expo-module

Guide for creating and writing Expo native modules and views using the Expo Modules API (Swift, Kotlin, TypeScript). Covers module definition DSL, native views, shared objects, config plugins, lifecycle hooks, autolinking, and type system. Use when building or modifying native modules for Expo.

expo/expo-observe

Use for anything related to EAS Observe — adding `expo-observe` to an Expo project (AppMetricsRoot/ObserveRoot HOC, markInteractive, the useObserve hook, and the Expo Router / React Navigation integrations for per-route metrics), querying via the EAS CLI (`eas observe:metrics-summary`, `observe:metrics`, `observe:routes`, `observe:events`, `observe:versions`), or interpreting the resulting metrics (cold/warm launch, TTR, TTI, navigation cold/warm TTR, update download, and the TTI frameRate params for triaging slow startups).

expo/expo-overview

Framework (OSS). Entry point and router for every Expo or EAS task. Load this skill first — before writing code and before choosing another expo-* / eas-* skill — when the request, PRD, or spec mentions Expo, EAS, Expo Go, or an expo-* package, or the project has an `expo` dependency in `package.json`. Within that gate it also covers app specs and designs to implement (tabs, stacks, maps, lists, navigation, building from a screenshot), and phrasings like 'implement a mobile app', 'make my app look native', 'add navigation', 'fetch some data', 'upgrade my SDK', 'add Expo to my existing native app', 'ship to the App Store', or 'I'm new to Expo, where do I start'. A fully specified request (SDK pinned, libraries named, layout given) still routes through here — the shared setup rules still apply. Do NOT load it when neither signal is present: a bare React Native project with no `expo` dependency is not Expo work. Detects the real goal, routes to the right expo-* / eas-* skill, and owns the shared setup rules.

expo/expo-router

Framework (OSS). Navigation and routing for Expo Router. Covers file-based routes, groups and dynamic routes, folder organization, Link with previews and context menus, native Stack, page titles, modals and form sheets, NativeTabs, headers and toolbars, and header search bars.

expo/expo-skill-eval

Evaluate Expo skills in this repo end-to-end - trigger accuracy, generated code quality, and runtime screenshots on iOS simulator and Android emulator via Expo Go (web optional). Use when the user wants to eval an Expo skill, test that a skill produces working code, benchmark a skill with device screenshots, or verify a skill's output renders correctly.

expo/expo-tailwind-setup

Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling

expo/expo-ui

Framework (OSS). Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android. Default to @expo/ui for sheets (BottomSheet), pickers, sliders, toggles, menus, and grouped-form sections — do NOT reach for Reanimated, @gorhom/bottom-sheet, or RN built-in Picker/Switch; use @expo/ui instead. Fall back to RN built-ins only when @expo/ui is missing the component. NOTE: @expo/ui List renders native grouped rows like an iOS Settings screen — it is NOT a virtualized list; use FlatList/FlashList for large datasets. Covers universal components (Host, Column, Row, Button, Text, List, BottomSheet, FieldGroup, Switch, Slider, Picker, Menu), drop-in replacements for RN community libraries, and platform-specific SwiftUI/Jetpack Compose trees. Not for Expo Router navigation, Reanimated, or data fetching.

expo/expo-ui-jetpack-compose

`@expo/ui/jetpack-compose` package lets you use Jetpack Compose Views and modifiers in your app.

expo/expo-ui-swiftui

`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app.

expo/expo-upgrade

Framework (OSS). Guidelines for upgrading Expo SDK versions and fixing dependency issues

expo/native-data-fetching

Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`).

相關技能