Communitygithub.com

EAS Hosting 部署与 API 路由

将 Expo 网站和 Expo Router API 路由部署到 EAS Hosting。支持导出 Web 包、使用 eas deploy 进行生产和 PR 预览部署、管理环境变量和自定义域名,并可在 Cloudflare Workers 运行时中工作。

EAS Hosting 部署与 API 路由 是什么?

这项技能指导开发者使用 Expo Application Services (EAS) Hosting 部署 Expo 网站和 API 路由。EAS Hosting 是一个付费服务(有免费额度),它将你的 Web 包和 Expo Router API 路由部署到 Expo 管理的边缘计算平台(基于 Cloudflare Workers)。该技能详细介绍了 API 路由的最佳适用场景(如服务器端密钥、数据库操作、第三方 API 代理)和不适用场景(如数据已公开、无需密钥、实时更新)。它还提供了完整的文件结构、HTTP 方法示例(GET、POST、PUT、DELETE)、动态路由、请求处理(查询参数、请求头、JSON 体)、CORS 设置、错误处理、本地测试方法以及通过 eas deploy 命令部署到生产环境或创建 PR 预览的完整流程。此外,该技能还涵盖了 EAS Hosting 运行时的限制(如无 Node.js 文件系统、30 秒超时),并推荐使用 Web API 或云端数据库(如 Turso、PlanetScale、Supabase)作为替代方案。

兼容平台~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 Application Services 产品,包含免费套餐上限;生产部署会消耗你套餐中的请求和带宽配额。详情请参见 https://expo.dev/pricing。编写 API 路由和导出 Web 包是免费且开源的,你也可以选择自托管导出的服务端输出,而非使用 EAS Hosting。

EAS Hosting 将你的 Expo Web 应用和 API 路由 部署到 Expo 托管的边缘计算平台(Cloudflare Workers)。先使用 npx expo export -p web 导出 Web 包,再通过 eas deploy 发布——同一个命令会同时部署随 Web 包捆绑的任何 Expo Router API 路由。本技能涵盖网站部署、API 路由编写以及托管运行时;部署流程请参见下方的“部署到 EAS Hosting”部分。

何时使用 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 包和所有 Expo Router API 路由——eas deploy 会处理两者。无论你只有完整网站、仅 API 路由的后端,还是两者都有,都会执行导出操作。

# 导出 Web 包(包含所有 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 实现自动化

通过 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 类型但设置 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" },
    });
  }

  // 验证 token...
  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`).

相关技能