mcpField
E
eas-hosting

eas-hosting

github.com/expo云与 DevOps3.5k19.8k 次安装今天更新
代码仓库

关于 eas-hosting

面向 Expo 开发者的技能,介绍如何在 EAS Hosting 部署 Web 应用和 API 路由,并说明编写方式、运行限制与部署流程。导出的服务器产物也可自行托管。

主要功能

  1. 01创建 Expo Router API 路由
  2. 02在服务端保管密钥
  3. 03处理请求、CORS 和错误
  4. 04在本地测试路由
  5. 05部署 Web 包与路由

适用场景

  1. 01代理第三方 API 并隐藏密钥
  2. 02接收 Webhook 并验证服务端数据
  3. 03部署带 API 路由的 Expo 网站
  4. 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,简要报告任务内容、预期结果以及问题所在。

评论

支持 Markdown