关于 eas-hosting
面向 Expo 开发者的技能,介绍如何在 EAS Hosting 部署 Web 应用和 API 路由,并说明编写方式、运行限制与部署流程。导出的服务器产物也可自行托管。
主要功能
- 01创建 Expo Router API 路由
- 02在服务端保管密钥
- 03处理请求、CORS 和错误
- 04在本地测试路由
- 05部署 Web 包与路由
适用场景
- 01代理第三方 API 并隐藏密钥
- 02接收 Webhook 并验证服务端数据
- 03部署带 API 路由的 Expo 网站
- 04导出服务器产物并自行托管
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ expo/ skills --skill eas-hosting安装后在 Claude Code 中输入/eas-hosting确认已加载。
附带文件agents/openai.yaml
详细介绍
EAS Hosting
EAS 服务需付费。 EAS Hosting 是一项付费的 Expo Application Services 产品,设有免费套餐限额;生产环境部署会使用你套餐中的请求和带宽额度。请参阅 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 路由以及 Hosting 运行时;部署工作流请参阅下文的「部署」部分。
何时使用 API 路由
在以下情况下使用 API 路由:
- 服务器端密钥 — API 密钥、数据库凭据或绝不能发送到客户端的令牌
- 数据库操作 — 不应公开的直接数据库查询
- 第三方 API 代理 — 调用外部服务(OpenAI、Stripe 等)时隐藏 API 密钥
- 服务器端验证 — 在写入数据库前验证数据
- Webhook 端点 — 接收 Stripe 或 GitHub 等服务的回调
- 速率限制 — 在服务器级别控制访问
- 繁重计算 — 将在移动设备上运行缓慢的处理任务转移到服务器
何时不使用 API 路由
在以下情况下避免使用 API 路由:
- 数据已公开 — 改为直接请求公共 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();
// Process...
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 路由的后端,还是两者兼有,都会执行导出。
# Export the web bundle (includes any API routes)
npx expo export -p web
# Deploy a preview (PR-style URL)
npx eas-cli@latest deploy
# Deploy to production
npx eas-cli@latest deploy --prod
所有内容都会部署到 EAS Hosting(Cloudflare Workers)。
生产环境的环境变量
# Create a secret
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production
# Or use the Expo dashboard
自定义域名
在 eas.json 或 Expo 控制面板中配置。
使用 EAS Workflows 自动化
通过 type: deploy 工作流,在每次推送到 main 时部署网站(及 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
拉取请求的预览部署使用相同的作业类型,并将 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
// Use Web Crypto instead of Node crypto
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode("data")
);
// Use fetch instead of node-fetch
const response = await fetch("https://api.example.com");
// Use Response/Request (already available)
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 路由
// From React Native components
const response = await fetch("/api/hello");
const data = await response.json();
// With body
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" },
});
}
// Verify 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" "<actionable feedback>"
仅当你有具体且可执行的反馈时才提交。请尽可能提供相关背景信息。
如果 Agent 反复无法完成 Expo 任务,或用户不得不介入,请使用 --category evals,并将相关 Expo 软件包或命令作为 --subject,简要报告任务内容、预期结果以及问题所在。