eas-hosting
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseEAS Hosting
EAS Hosting
EAS service - costs apply. EAS Hosting is a paid Expo Application Services product with free-tier limits; production deploys use your plan's request and bandwidth allowance. See https://expo.dev/pricing. Authoring API routes and exporting the web bundle are free and open source, and you can self-host the exported server output instead of EAS Hosting.
EAS Hosting deploys your Expo web app and API routes to Expo's managed edge (Cloudflare Workers). Export the web bundle with and ship it with - the same command deploys any Expo Router API routes bundled alongside it. This skill covers deploying a website, authoring API routes, and the hosting runtime; see the Deployment section below for the deploy workflow.
npx expo export -p webeas deployEAS服务 - 需付费。EAS Hosting是Expo Application Services的付费产品,包含免费层级限制;生产环境部署会占用您套餐的请求和带宽额度。详情请见https://expo.dev/pricing。编写API路由和导出Web包是免费且开源的,您也可以选择自行托管导出的服务器输出,而非使用EAS Hosting。
EAS Hosting将您的Expo Web应用和API路由部署到Expo的托管边缘节点(Cloudflare Workers)。使用导出Web包,再通过部署——该命令会同时部署与之捆绑的所有Expo Router API路由。本技能涵盖网站部署、API路由编写以及托管运行时相关内容;部署流程请见下方的「部署」章节。
npx expo export -p webeas deployWhen to Use API Routes
API路由适用场景
Use API routes when you need:
- Server-side secrets — API keys, database credentials, or tokens that must never reach the client
- Database operations — Direct database queries that shouldn't be exposed
- Third-party API proxies — Hide API keys when calling external services (OpenAI, Stripe, etc.)
- Server-side validation — Validate data before database writes
- Webhook endpoints — Receive callbacks from services like Stripe or GitHub
- Rate limiting — Control access at the server level
- Heavy computation — Offload processing that would be slow on mobile
在以下场景中适合使用API路由:
- 服务端密钥——API密钥、数据库凭证或令牌,绝对不能暴露给客户端
- 数据库操作——不应公开的直接数据库查询
- 第三方API代理——调用外部服务(如OpenAI、Stripe等)时隐藏API密钥
- 服务端验证——写入数据库前验证数据
- Webhook端点——接收来自Stripe或GitHub等服务的回调
- 速率限制——在服务端控制访问频率
- 密集型计算——将在移动端运行缓慢的处理任务转移到服务端
When NOT to Use API Routes
API路由不适用场景
Avoid API routes when:
- Data is already public — Use direct fetch to public APIs instead
- No secrets required — Static data or client-safe operations
- Real-time updates needed — Use WebSockets or services like Supabase Realtime
- Simple CRUD — Consider Firebase, Supabase, or Convex for managed backends
- File uploads — Use direct-to-storage uploads (S3 presigned URLs, Cloudflare R2)
- Authentication only — Use Clerk, Auth0, or Firebase Auth instead
在以下场景中应避免使用API路由:
- 数据已公开——直接调用公开API即可
- 无需密钥——静态数据或客户端安全操作
- 需要实时更新——使用WebSocket或Supabase Realtime等服务
- 简单CRUD操作——考虑使用Firebase、Supabase或Convex等托管后端
- 文件上传——使用直接上传到存储服务的方式(如S3预签名URL、Cloudflare R2)
- 仅需认证——使用Clerk、Auth0或Firebase Auth替代
File Structure
文件结构
API routes live in the directory with suffix:
app+api.tsapp/
api/
hello+api.ts → GET /api/hello
users+api.ts → /api/users
users/[id]+api.ts → /api/users/:id
(tabs)/
index.tsxAPI路由位于目录下,以为后缀:
app+api.tsapp/
api/
hello+api.ts → GET /api/hello
users+api.ts → /api/users
users/[id]+api.ts → /api/users/:id
(tabs)/
index.tsxBasic API Route
基础API路由
ts
// app/api/hello+api.ts
export function GET(request: Request) {
return Response.json({ message: "Hello from Expo!" });
}ts
// app/api/hello+api.ts
export function GET(request: Request) {
return Response.json({ message: "Hello from Expo!" });
}HTTP Methods
HTTP方法
Export named functions for each HTTP method:
ts
// 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 });
}为每个HTTP方法导出命名函数:
ts
// 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 });
}Dynamic Routes
动态路由
ts
// app/api/users/[id]+api.ts
export function GET(request: Request, { id }: { id: string }) {
return Response.json({ userId: id });
}ts
// app/api/users/[id]+api.ts
export function GET(request: Request, { id }: { id: string }) {
return Response.json({ userId: id });
}Request Handling
请求处理
Query Parameters
查询参数
ts
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 });
}ts
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 });
}Headers
请求头
ts
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 });
}ts
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 Body
JSON请求体
ts
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 });
}ts
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 });
}Environment Variables
环境变量
Use for server-side secrets:
process.envts
// 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);
}Set environment variables:
- Local: Create file (never commit)
.env - EAS Hosting: Use or Expo dashboard
eas env:create
使用获取服务端密钥:
process.envts
// 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:使用命令或Expo控制台
eas env:create
CORS Headers
CORS请求头
Add CORS for web clients:
ts
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 });
}为Web客户端添加CORS支持:
ts
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 });
}Error Handling
错误处理
ts
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 });
}
}ts
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 });
}
}Testing Locally
本地测试
Start the development server with API routes:
bash
npx expo serveThis starts a local server at with full API route support.
http://localhost:8081Test with curl:
bash
curl http://localhost:8081/api/hello
curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}'启动开发服务器并启用API路由:
bash
npx expo serve这会在启动本地服务器,完全支持API路由。
http://localhost:8081使用curl测试:
bash
curl http://localhost:8081/api/hello
curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}'Deployment to EAS Hosting
部署到EAS Hosting
Prerequisites
前置条件
bash
npm install -g eas-cli
eas loginbash
npm install -g eas-cli
eas loginDeploy
部署
Deploying ships your web bundle and any Expo Router API routes together - handles both. The export runs whether you have a full website, an API-routes-only backend, or both.
eas deploybash
undefined部署会将您的Web包和所有Expo Router API路由一起发布——会处理这两者。无论您是完整网站、仅API路由的后端,还是两者兼具,导出操作都会执行。
eas deploybash
undefinedExport the web bundle (includes any API routes)
导出Web包(包含所有API路由)
npx expo export -p web
npx expo export -p web
Deploy a preview (PR-style URL)
部署预览版本(PR风格URL)
npx eas-cli@latest deploy
npx eas-cli@latest deploy
Deploy to production
部署到生产环境
npx eas-cli@latest deploy --prod
Everything lands on EAS Hosting (Cloudflare Workers).npx eas-cli@latest deploy --prod
所有内容都会部署到EAS Hosting(Cloudflare Workers)。Environment Variables for Production
生产环境环境变量
bash
undefinedbash
undefinedCreate a secret
创建密钥
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production
Or use the Expo dashboard
或使用Expo控制台
undefinedundefinedCustom Domain
自定义域名
Configure in or Expo dashboard.
eas.json在或Expo控制台中配置。
eas.jsonAutomate with EAS Workflows
使用EAS工作流自动化
Deploy the website (and API routes) on every push to main with a workflow:
type: deploy.eas/workflows/deploy.ymlyaml
name: Deploy
on:
push:
branches:
- main通过工作流,在每次推送到main分支时自动部署网站(和API路由):
type: deploy.eas/workflows/deploy.ymlyaml
name: Deploy
on:
push:
branches:
- mainjobs:
deploy_web:
type: deploy
params:
prod: true
Preview deploys for pull requests use the same job type with `prod: false`:
```yaml
name: Web PR Preview
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
type: deploy
params:
prod: falseTo author or validate workflow YAML beyond these examples, use the skill.
eas-workflowsjobs:
deploy_web:
type: deploy
params:
prod: true
为拉取请求创建预览部署可使用相同的任务类型,设置`prod: false`:
```yaml
name: Web PR Preview
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
type: deploy
params:
prod: false如需编写或验证超出这些示例的工作流YAML,请使用技能。
eas-workflowsEAS Hosting Runtime (Cloudflare Workers)
EAS Hosting运行时(Cloudflare Workers)
API routes run on Cloudflare Workers. Key limitations:
API路由运行在Cloudflare Workers上。主要限制:
Missing/Limited APIs
缺失/受限的API
- No Node.js filesystem — module unavailable
fs - No native Node modules — Use Web APIs or polyfills
- Limited execution time — 30 second timeout for CPU-intensive tasks
- No persistent connections — WebSockets require Durable Objects
- fetch is available — Use standard fetch for HTTP requests
- 无Node.js文件系统——模块不可用
fs - 无原生Node模块——使用Web API或polyfill
- 有限的执行时间——CPU密集型任务超时时间为30秒
- 无持久连接——WebSocket需要Durable Objects
- fetch可用——使用标准fetch发起HTTP请求
Use Web APIs Instead
使用Web API替代
ts
// 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" },
});ts
// 使用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" },
});Database Options
数据库选项
Since filesystem is unavailable, use cloud databases:
- Cloudflare D1 — SQLite at the edge
- Turso — Distributed SQLite
- PlanetScale — Serverless MySQL
- Supabase — Postgres with REST API
- Neon — Serverless Postgres
Example with Turso:
ts
// 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);
}由于文件系统不可用,请使用云数据库:
- Cloudflare D1——边缘节点上的SQLite
- Turso——分布式SQLite
- PlanetScale——无服务器MySQL
- Supabase——带REST API的Postgres
- Neon——无服务器Postgres
Turso示例:
ts
// 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);
}Calling API Routes from Client
从客户端调用API路由
ts
// 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" }),
});ts
// 在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" }),
});Common Patterns
常见模式
Authentication Middleware
认证中间件
ts
// 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 });
}ts
// 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 });
}Proxy External API
代理外部API
ts
// 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());
}ts
// 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());
}Rules
规则
- NEVER expose API keys or secrets in client code
- ALWAYS validate and sanitize user input
- Use proper HTTP status codes (200, 201, 400, 401, 404, 500)
- Handle errors gracefully with try/catch
- Keep API routes focused — one responsibility per endpoint
- Use TypeScript for type safety
- Log errors server-side for debugging
- 切勿在客户端代码中暴露API密钥或机密信息
- 始终验证并清理用户输入
- 使用正确的HTTP状态码(200、201、400、401、404、500)
- 使用try/catch优雅处理错误
- 保持API路由聚焦——每个端点只负责一项功能
- 使用TypeScript保证类型安全
- 在服务端记录错误以便调试