API 路由

了解如何使用 Expo Router 创建服务器端点。


Expo Router 让你可以在 app 目录中为所有平台编写安全的服务器代码。

🌐 Expo Router enables you to write secure server code for all platforms, right in your app directory.

app/hello+api.ts
export function GET(request: Request) { return Response.json({ hello: 'world' }); }

服务器功能需要自定义服务器,可以部署到 EAS 或大多数其他托管服务提供商

🌐 Server features require a custom server, which can be deployed to EAS or most other hosting providers.

观看:Expo 路由 API 路由处理请求并流式传输数据
观看:Expo 路由 API 路由处理请求并流式传输数据

什么是 API 路由

🌐 What are API Routes

API 路由是在匹配路由时在服务器上执行的函数。它们可用于安全地处理敏感数据,例如 API 密钥,或实现自定义服务器逻辑,例如将授权码交换为访问令牌。API 路由应在符合 WinterCG 的环境中执行。

🌐 API Routes are functions that are executed on a server when a route is matched. They can be used to handle sensitive data, such as API keys securely, or implement custom server logic, such as exchanging auth codes for access tokens. API Routes should be executed in a WinterCG-compliant environment.

在 Expo 中,API 路由是通过在 app 目录中创建 +api.ts 扩展名的文件来定义的。例如,当匹配到路由 /hello 时,会执行以下 API 路由。

🌐 In Expo, API Routes are defined by creating files in the app directory with the +api.ts extension. For example, the following API route is executed when the route /hello is matched.

app
index.tsx
hello+api.tsAPI Route

创建 API 路由

🌐 Create an API route

1

确保你的项目正在使用服务器输出,这将配置导出和生产构建,同时生成服务器包和客户端包。

🌐 Ensure your project is using server output, this will configure the export and production builds to generate a server bundle as well as the client bundle.

app.json
{ "web": { "output": "server" } }

2

app 目录中创建了一个 API 路由。例如,添加以下路由处理程序。当匹配到路由 /hello 时,它将被执行。

🌐 An API route is created in the app directory. For example, add the following route handler. It is executed when the route /hello is matched.

app/hello+api.ts
export function GET(request: Request) { return Response.json({ hello: 'world' }); }

你可以从服务器路由导出以下任意函数 GETPOSTPUTPATCHDELETEHEADOPTIONS。当匹配到相应的 HTTP 方法时,该函数会执行。不支持的方法将自动返回 405: Method not allowed

🌐 You can export any of the following functions GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS from a server route. The function executes when the corresponding HTTP method is matched. Unsupported methods will automatically return 405: Method not allowed.

3

使用 Expo CLI 启动开发服务器:

🌐 Start the development server with Expo CLI:

Terminal
npx expo

4

你可以向该路由发送网络请求以访问数据。运行以下命令来测试该路由:

🌐 You can make a network request to the route to access the data. Run the following command to test the route:

Terminal
curl http://localhost:8081/hello

你也可以从客户端代码发出请求:

🌐 You can also make a request from the client code:

app/index.tsx
import { Button } from 'react-native'; async function fetchHello() { const response = await fetch('/hello'); const data = await response.json(); alert('Hello ' + data.hello); } export default function App() { return <Button onPress={() => fetchHello()} title="Fetch hello" />; }

相对 fetch 请求在开发阶段会自动相对于开发服务器的源进行获取,并且可以在生产环境中通过 app.json 中的 origin 字段进行配置:

🌐 Relative fetch requests automatically fetch relative to the dev server origin in development, and can be configured in production using the origin field in the app.json:

app.json
{ "plugins": [ [ "expo-router", { "origin": "https://evanbacon.dev/" } ] ] }

在 EAS 构建期间,可以通过设置 EXPO_UNSTABLE_DEPLOY_SERVER=1 环境变量来自动配置此 URL。这将触发一个版本化的服务器部署,并自动将源设置为预览部署 URL。

🌐 This URL can be automatically configured during EAS Builds by setting the EXPO_UNSTABLE_DEPLOY_SERVER=1 environment variable. This will trigger a versioned server deployment which sets the origin to a preview deploy URL automatically.

5

将网站和服务器部署到托管服务提供商,以便在本地和网页端访问生产环境中的路由。

🌐 Deploy the website and server to a hosting provider to access the routes in production on both native and web.

警告 API 路由的文件名不能包含特定平台的扩展名。例如,hello+api.web.ts 将无法使用。

要求

🌐 Requests

请求使用全局的标准 Request 对象。

🌐 Requests use the global, standard Request object.

app/blog/[post]+api.ts
export async function GET(request: Request, { post }: Record<string, string>) { // const postId = new URL(request.url).searchParams.get('post') // fetch data for 'post' return Response.json({ ... }); }

请求体

🌐 Request body

使用 request.json() 函数来访问请求体。它会自动解析请求体并返回结果。

🌐 Use the request.json() function to access the request body. It automatically parses the body and returns the result.

app/validate+api.ts
export async function POST(request: Request) { const body = await request.json(); return Response.json({ ... }); }

请求查询参数

🌐 Request query parameters

可以通过解析请求 URL 来访问查询参数:

🌐 Query parameters can be accessed by parsing the request URL:

app/endpoint+api.ts
export async function GET(request: Request) { const url = new URL(request.url); const post = url.searchParams.get('post'); // fetch data for 'post' return Response.json({ ... }); }

响应

🌐 Response

响应使用全局标准 Response 对象。

🌐 Responses use the global, standard Response object.

app/demo+api.ts
export function GET() { return Response.json({ hello: 'universe' }); }

错误

🌐 Errors

对于错误情况,你可以创建带有任意状态码和响应体的 Response

🌐 For error cases, you can create Responses with any status code and response body.

app/blog/[post]+api.ts
export async function GET(request: Request, { post }: Record<string, string>) { if (!post) { return new Response('No post found', { status: 404, headers: { 'Content-Type': 'text/plain', }, }); } // fetch data for `post` return Response.json({ ... }); }

使用未定义的方法发出请求将自动返回 405: Method not allowed。如果在请求过程中抛出错误,它将自动返回 500: Internal server error

🌐 Making requests with an undefined method will automatically return 405: Method not allowed. If an error is thrown during the request, it will automatically return 500: Internal server error.

运行时 API

🌐 Runtime API

重要 服务器运行时 API 和 expo-server 在 SDK 54 及更高版本中可用,并且在生产环境中使用需要部署服务器。

你可以使用 expo-server 库来使用一些适用于任何服务器端 Expo 代码的工具和代码模式。这包括获取请求元数据、调度任务以及处理错误的工具。

🌐 You can use the expo-server library to use several utilities and code patterns that work in any server-side Expo code. This includes utilities to get request metadata, for scheduling tasks, and for error handling.

Terminal
npx expo install expo-server

使用 expo-server 不仅限于 API 路由,它也可以用于任何其他服务器代码,例如在 服务器中间件 中。

🌐 Using expo-server is not limited to API routes and it can be used in any other server code as well, for example, in server middleware.

错误处理

🌐 Error handling

你可以中止请求,并通过抛出 StatusError 返回错误 Response。这是一个特殊的 Error 实例,它将被替换为 HTTP 响应,以替代错误本身。

🌐 You can abort a request and instead return an error Response by throwing a StatusError. This is a special Error instance that will be replaced with an HTTP response replacing the error itself.

app/blog/[post]+api.ts
import { StatusError } from 'expo-server'; export async function GET(request: Request, { post }: Record<string, string>) { if (!post) { throw new StatusError(404, 'No post found'); } // ... }

在编写自己的服务器工具和辅助程序时,使用 StatusError 是处理异常的更方便方式,因为抛出异常会中断任何 API 功能并提前返回错误。

🌐 When composing your own server utilities and helpers, the StatusError is a more convenient way to handle exceptions, since throwing them interrupts any API functions and returns an error early.

StatusError 接受一个状态码和一个错误信息,这些也可以选择性地作为 JSON 或 Error 对象传递,并且始终会返回一个带有 JSON 主体的 Response,其中 error 键被设置为它们的错误信息。

这可能会有一定的限制,并不适用于所有情况。有时,直接对一个 Response 对象进行 throw 可能更为有利,这同样会中断你的逻辑,但会直接替换 API 路由中解析得到的 Response,而无需 StatusError 封装。例如,这可以用于创建重定向响应。

🌐 This can be restrictive, and isn't suitable for all cases. Sometimes it might be beneficial to instead throw a Response object, which interrupts your logic as well, but replaces the resolved Response from your API route directly, without a StatusError wrapper. For example, this can be used to create redirect responses.

app/blog/[post]+api.ts
import { StatusError } from 'expo-server'; export async function GET(request: Request, { post }: Record<string, string>) { if (!post) { throw Response.redirect('https://expo.dev', 302); } // ... }

请求元数据

🌐 Request metadata

请求通常在其头部包含你所需的大部分元数据。不过,expo-server 提供了一些辅助函数,可以更方便地获取常用值。

🌐 Requests typically carry most metadata you'll need in their headers. However, expo-server provides some helper functions to retrieve common values more easily.

expo-server 的辅助函数返回当前 Request 范围内的值。你只能在服务器端代码中调用这些函数,并且只能在进行中的请求期间调用。

🌐 Helper functions from expo-server return values that are scoped to the current Request. You can only call these functions in server-side code and only during ongoing requests.

你可能需要访问的一个常见值是请求的来源 URL。来源 URL 通常通过请求的 Origin 头传输,表示用户用来访问你的 API 路由的 URL。这可能与你的服务器在请求被代理时看到的任何内部部署 URL 不同。你可以使用 expo-serverorigin() 辅助方法来访问此值。

🌐 A common value that you may need to access is the request's origin URL. The origin URL, typically transmitted on a request's Origin header, represents the URL that a user used to access your API route. This may differ from any internal deployment URL that your server sees when the request is being proxied. You can use expo-server's origin() helper method to access this value.

app/help+api.ts
import { origin } from 'expo-server'; export async function GET(request: Request) { const target = new URL('/help', origin() ?? request.url); return Response.redirect('https://expo.dev', 302); }

大多数部署服务器代码的运行时环境都有环境的概念,用于区分生产环境或测试环境的部署。你可以使用 expo-serverenvironment() 辅助工具来获取环境名称。这个值会根据你运行服务器代码的方式而有所不同。

🌐 Most runtimes that you deploy your server code to have a concept of environments, to differentiate between production or staging deployments. You can use expo-server's environment() helper to get an environment name. This value will differ depending on how you're running your server code.

app/env+api.ts
import { environment } from 'expo-server'; export async function GET(request: Request) { const env = environment(); if (env === 'staging') { return Response.json({ isStaging: true }); } else if (!env) { return Response.json({ isProduction: true }); } else { return Response.json({ env }); } }

任务调度

🌐 Task scheduling

在你的请求处理程序中,你可能需要并行运行与服务器逻辑同时进行的异步任务。

🌐 In your request handlers, you may need to run asynchronous tasks in parallel to your server logic.

app/tasks+api.ts
export async function GET(request: Request) { // This will delay the response: await pingAnalytics(...); const data = await fetchExampleData(...); return Response.json({ data }); }

在上述示例中,await-ed 函数调用会延迟 API 路由其余部分的执行。如果我们不想延迟 Response,那么对这个调用进行 await 并不合适。然而,不使用 await 调用该函数也无法保证此任务会保持无服务器函数运行。

🌐 In the above example, an await-ed function call delays the rest of the API route's execution. If we don't want to delay a Response, then await-ing this call isn't suitable. However, calling the function without await wouldn't guarantee that this task keeps a serverless function running.

相反,你可以使用 expo-serverrunTask() 辅助函数来运行并发任务。这等同于你在 service worker 代码或其他无服务器运行环境中看到的 waitUntil() 方法。

🌐 Instead, you can use expo-server's runTask() helper function to run concurrent tasks. This is equivalent to the waitUntil() method that you see in service worker code or other serverless runtimes.

app/tasks+api.ts
import { runTask } from 'expo-server'; export async function GET(request: Request) { // This will NOT delay the response: runTask(async () => { await pingAnalytics(...); }); const data = await fetchExampleData(...); return Response.json({ data }); }

使用 runTask,你可以在执行 await 式操作和不执行 await 式操作的异步函数之间取得折中。它们会并发运行,并且不会延迟 API 路由的响应或执行,但同时也确保运行时知道它们的存在,不会过早退出。

🌐 With runTask, you have a compromise between await-ing and not await-ing asynchronous functions. They'll be run concurrently, and don't delay the API route's response or execution, but are also making sure the runtime is aware of them, and don't quit early.

然而,有时你可能希望将任务延迟到 API 路由返回 Response 之后再执行。在这种情况下,如果 API 拒绝了任务,你可能更倾向于不执行该任务。此外,你可能希望仅在时间敏感任务完成后才运行某个函数,以防并发代码延迟 API 路由中的计算密集型任务。

🌐 However, sometimes you may want to delay a task until after the API route has returned a Response. In such cases, you might prefer not to execute the task if the API has rejected it. Additionally, you may want to run a function only after a time-sensitive task has been completed to prevent concurrent code from delaying computation-heavy tasks in your API route.

你可以使用 expo-serverdeferTask() 辅助函数来安排任务在你的 API 路由解析 Response 后执行。

🌐 You can use expo-server's deferTask() helper function to schedule tasks to run after a Response has been resolved by your API route.

app/tasks+api.ts
import { deferTask } from 'expo-server'; export async function GET(request: Request) { // This will run after this entire function resolves: deferTask(async () => { await pingAnalytics(...); }); const data = await fetchExampleData(...); return Response.json({ data }); }

响应头

🌐 Response headers

在将服务器逻辑结构化并拆分到不同的辅助函数和文件时,可能需要在创建 Response 之前修改 Response 头信息。

🌐 When structuring and splitting server logic into separate helper functions and files, it may be necessary to modify Response headers before a Response has been created.

例如,你可能需要在 API 路由代码运行之前,在 Response 中通过 服务器中间件 添加元数据。

🌐 For example, you may need to add metadata in server middleware to a Response before your API route code is running.

app/+middleware.ts
import { setResponseHeaders } from 'expo-server'; export default function middleware(request: Request) { // Rate limiters typically add a `Retry-After` header setResponseHeaders({ 'Retry-After': '3600' }); }

在上述示例中,Retry-After 头被添加到 API 路由可能创建的将来的 Response 中。这也可以扩展用于身份验证和 Cookie。

🌐 In the above example, a Retry-After header is added to a future Response that an API route may be creating. This can also be extended for authentication and cookies.

app/+middleware.ts
import { setResponseHeaders } from 'expo-server'; export default function middleware(request: Request) { // Append cookie to future response setResponseHeaders(headers => { headers.append('Set-Cookie', 'token=123; Secure'); }); }

打包

🌐 Bundling

API 路由与 Expo CLI 和 Metro bundler 打包在一起。它们可以访问与你的客户端代码相同的所有语言功能:

🌐 API Routes are bundled with Expo CLI and Metro bundler. They have access to all of the language features as your client code:

  • TypeScript — 类型和 tsconfig.json 路径
  • 环境变量 — 服务器路由可以访问所有环境变量,而不仅仅是以 EXPO_PUBLIC_ 为前缀的那些。
  • Node.js 标准库 — 确保你在本地为服务器环境使用的是正确版本的 Node.js。
  • babel.config.jsmetro.config.js 支持 —— 设置在客户端和服务器端代码中均可生效。

安全

🌐 Security

路由处理程序在与客户端代码隔离的沙箱环境中执行。这意味着你可以安全地在路由处理程序中存储敏感数据,而不会将其暴露给客户端。

🌐 Route handlers are executed in a sandboxed environment that is isolated from the client code. It means you can safely store sensitive data in the route handlers without exposing it to the client.

  • 客户端代码中导入了包含秘密的代码,这部分代码会被包含在客户端包中。即使这些文件不是路由处理文件(例如以 +api.ts 结尾),它也适用于 app 目录 中的 所有文件
  • 如果秘密存在于 <...>+api.ts 文件中,它不会被包含在客户端包中。这适用于路由处理程序中导入的所有文件。
  • 秘密剥离发生在 expo/metro-config 中,并且需要在 metro.config.js 中使用它。

部署

🌐 Deployment

当你准备好部署到生产环境时,运行以下命令将在 dist 目录中创建服务器包(更多详情请参见 Expo CLI 文档):

🌐 When you're ready to deploy to production, run the following command to create the server bundle in the dist directory (see the Expo CLI documentation for more details):

Terminal
npx expo export --platform web

这个服务器可以在本地使用 npx expo serve(在 Expo SDK 52 及更高版本中可用)进行测试,可以在网页浏览器中访问该 URL,或使用将 origin 设置为本地服务器 URL 的原生构建。你可以使用 EAS Hosting 或其他第三方服务将服务器部署到生产环境。

🌐 This server can be tested locally with npx expo serve (available in Expo SDK 52 and later), visit the URL in a web browser or create a native build with the origin set to the local server URL. You can deploy the server for production using EAS Hosting or another third-party service.

如果你想导出 API 路由并跳过生成应用的网页版本,你可以使用以下命令,这将生成一个只包含项目服务器代码的 dist 目录。

🌐 If you want to export API routes and skip generating a website version of your app, you can use the following command, which will generate a dist directory containing only the server code of your project.

Terminal
npx expo export --platform web --no-ssg
使用 EAS 立即部署

EAS Hosting 是部署你的 Expo API 路由和服务器的最佳方式。

本地部署

🌐 Native deployment

重要 这是从 SDK 52 开始的实验性功能。未来版本中,这一过程将更加自动化,并提供更好的支持。

Expo Router 中的服务器功能(API 路由和 React 服务器组件)围绕 window.locationfetch 的原生实现,这些实现指向远程服务器。在开发过程中,我们会自动指向使用 npx expo start 运行的开发服务器,但要使生产的原生构建正常工作,你需要将服务器部署到安全主机,并设置 Expo Router 配置插件的 origin 属性。

🌐 Server features (API routes, and React Server Components) in Expo Router are centered around native implementations of window.location and fetch which point to the remote server. In development, we automatically point to the dev server running with npx expo start, but for production native builds to work you'll need to deploy the server to a secure host and set the origin property of the Expo Router Config Plugin.

配置完成后,像相对获取请求 fetch('/my-endpoint') 这样的功能将自动指向服务器源。

🌐 When configured, features like relative fetch requests fetch('/my-endpoint') will automatically point to the server origin.

这个部署过程可以实验性地自动化,以确保在使用 EXPO_UNSTABLE_DEPLOY_SERVER=1 环境变量进行本地构建时版本正确。

🌐 This deployment process can experimentally be automated to ensure correct versioning during native builds with the EXPO_UNSTABLE_DEPLOY_SERVER=1 environment variable.

以下是如何配置你的本地应用,以便在构建时自动部署并链接版本化的生产服务器:

🌐 Here's how to configure your native app to automatically deploy and link a versioned production server on build:

1

确保在 app.json 中或在 expo.extra.router.origin 字段中 设置 origin 字段。同时,确保你没有使用 app.config.js,因为这暂时不支持自动链接部署。

🌐 Ensure the origin field is NOT set in the app.json or in the expo.extra.router.origin field. Also, ensure you aren't using app.config.js as this is not supported with automatically linked deployments yet.

2

通过先在本地部署一次,为项目设置 EAS Hosting

🌐 Setup EAS Hosting for the project by deploying once locally first.

Terminal
npx expo export -p web
eas deploy

3

在你的 .env 文件中设置 EXPO_UNSTABLE_DEPLOY_SERVER 环境变量。这将用于在 EAS 构建过程中启用实验性服务器部署功能。

🌐 Set the EXPO_UNSTABLE_DEPLOY_SERVER environment variable in your .env file. This will be used to enable the experimental server deployment functionality during EAS Build.

.env
EXPO_UNSTABLE_DEPLOY_SERVER=1

4

你现在可以使用自动服务器部署了!运行构建命令以开始该过程。

🌐 You're now ready to use automatic server deployment! Run the build command to start the process.

Terminal
eas build

你也可以在本地运行这个:

🌐 You can also run this locally with:

Terminal
# Android
npx expo run:android --variant release

# iOS
npx expo run:ios --configuration Release

关于本地应用自动服务器部署的说明:

🌐 Notes about automatic server deployment for native apps:

  • 如果设置不正确,在 EAS Build 的 Bundle JavaScript 阶段可能会发生服务器故障。
  • 如果你愿意,可以在构建应用之前手动部署服务器并设置 origin URL。
  • 可以通过环境变量 EXPO_NO_DEPLOY=1 强制跳过自动部署。
  • 自动部署尚不支持 动态应用配置 (app.config.jsapp.config.ts) 文件。
  • 部署的日志将写入 .expo/logs/deploy.log
  • 部署不会以 EXPO_OFFLINE 模式运行。

在本地测试原生生产应用

🌐 Testing the native production app locally

将生产版本与本地开发服务器进行测试通常非常有用,以确保一切按预期运行。这可以大大加快调试过程。

🌐 It can often be useful to test the production build against a local dev server to ensure everything is working as expected. This can speed up the debugging process substantially.

1

导出生产服务器:

🌐 Export the production server:

Terminal
npx expo export

2

在本地托管生产服务器:

🌐 Host the production server locally:

Terminal
npx expo serve

3

app.jsonorigin 字段中设置来源。确保 expo.extra.router.origin 中没有生成的值。这个应该是 http://localhost:8081(假设 npx expo serve 正在使用默认端口运行)。

🌐 Set the origin in the app.json's origin field. Ensure no generated value is in expo.extra.router.origin. This should be http://localhost:8081 (assuming npx expo serve is running on the default port).

app.json
{ "expo": { "plugins": [ [ "expo-router", { "origin": "http://localhost:8081" } ] ] } }

记得在部署到生产环境时移除这个 origin 值。

🌐 Remember to remove this origin value when deploying to production.

4

在模拟器上以发布模式构建应用:

🌐 Build the app in release mode on to a simulator:

Terminal
EXPO_NO_DEPLOY=1 npx expo run:ios --configuration Release

你现在应该能够看到发送到本地服务器的请求。使用像 Proxyman 这样的工具来检查模拟器的网络流量,并获得更深入的了解。

🌐 You should now see requests coming in to the local server. Use a tool like Proxyman to inspect network traffic for the simulator and gain better insight.

你可以通过实验性地更改 URL 并使用 --unstable-rebundle 标志快速重新构建 iOS。这将替换 app.json 和客户端资源为新的版本,从而跳过原生重建。

🌐 You can experimentally change the URL and quickly rebuild for iOS using the --unstable-rebundle flag. This will swap out the app.json and client assets for new ones, skipping the native rebuild.

例如,你可以运行 eas deploy 来获取一个新的部署 URL,将其添加到 app.json 中,然后运行 npx expo run:ios --unstable-rebundle --configuration Release 来使用新的 URL 快速重建应用。

🌐 For example, you can run eas deploy to get a new deployment URL, add it to the app.json, then run npx expo run:ios --unstable-rebundle --configuration Release to quickly rebuild the app with the new URL.

在提交到商店之前,你需要进行一次干净的构建,以确保没有暂时性的问题存在。

🌐 You will want to make a clean build before sending to the store to ensure no transient issues are present.

托管在第三方服务上

🌐 Hosting on third-party services

重要 expo-server 库已在 SDK 54 中添加。对于较早的 SDK,请改用 @expo/server

每个云托管提供商都需要一个自定义适配器来支持 Expo 服务器运行时。以下第三方提供商得到了 Expo 团队的非官方或实验性支持。

🌐 Every cloud hosting provider needs a custom adapter to support the Expo server runtime. The following third-party providers have unofficial or experimental support from the Expo team.

在部署到这些提供商之前,熟悉 npx expo export 命令的基础知识可能会有帮助:

🌐 Before deploying to these providers, it may be good to be familiar with the basics of npx expo export command:

  • dist 是 Expo CLI 的默认导出目录。
  • public 目录中的文件在导出时会被复制到 dist
  • expo-server 包是一个用于导出的 Expo Web 和 API 路由产物的服务器端运行时。
  • expo-server 不会.env 文件中加载环境变量。它们应由托管提供商或用户自行加载。
  • Metro未包含在服务器中。

expo-server 库包含针对各种提供程序和运行时的适配器。在继续下面的任何部分之前,请先安装 expo-server 库。

🌐 The expo-server library contains adapters for various providers and runtimes. Before proceeding with any of the below sections, install the expo-server library.

Terminal
npx expo install expo-server

Bun

1

将网站导出用于生产环境:

🌐 Export the website for production:

Terminal
bunx expo export -p web

2

编写一个服务器入口文件,用于提供静态文件并将请求委托给服务器路由:

🌐 Write a server entry file that serves the static files and delegates requests to the server routes:

server.ts
import { createRequestHandler } from 'expo-server/adapter/bun'; const CLIENT_BUILD_DIR = `${process.cwd()}/dist/client`; const SERVER_BUILD_DIR = `${process.cwd()}/dist/server`; const handleRequest = createRequestHandler({ build: SERVER_BUILD_DIR }); const port = process.env.PORT || 3000; Bun.serve({ port: process.env.PORT || 3000, async fetch(req) { const url = new URL(req.url); console.log('Request URL:', url.pathname); const staticPath = url.pathname === '/' ? '/index.html' : url.pathname; const file = Bun.file(CLIENT_BUILD_DIR + staticPath); if (await file.exists()) return new Response(await file.arrayBuffer()); return handleRequest(req); }, websocket, }); console.log(`Bun server running at http://localhost:${port}`);

4

使用 bun 启动服务器:

🌐 Start the server with bun:

Terminal
bun run server.ts

表达

🌐 Express

1

安装所需的依赖:

🌐 Install the required dependencies:

Terminal
npm i -D express compression morgan

2

将网站导出用于生产环境:

🌐 Export the website for production:

Terminal
npx expo export -p web

3

编写一个服务器入口文件,用于提供静态文件并将请求委托给服务器路由:

🌐 Write a server entry file that serves the static files and delegates requests to the server routes:

server.ts
#!/usr/bin/env node const path = require('path'); const { createRequestHandler } = require('expo-server/adapter/express'); const express = require('express'); const compression = require('compression'); const morgan = require('morgan'); const CLIENT_BUILD_DIR = path.join(process.cwd(), 'dist/client'); const SERVER_BUILD_DIR = path.join(process.cwd(), 'dist/server'); const app = express(); app.use(compression()); // http://express.nodejs.cn/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header app.disable('x-powered-by'); process.env.NODE_ENV = 'production'; app.use( express.static(CLIENT_BUILD_DIR, { maxAge: '1h', extensions: ['html'], }) ); app.use(morgan('tiny')); app.all( '/{*all}', createRequestHandler({ build: SERVER_BUILD_DIR, }) ); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Express server listening on port ${port}`); });

4

使用 node 命令启动服务器:

🌐 Start the server with node command:

Terminal
node server.ts

Netlify

重要 第三方适配器可能会发生破坏性更改。我们没有对它们进行持续测试。

1

创建一个服务器入口文件。所有请求都将通过此中间件进行处理。文件的确切位置很重要。

🌐 Create a server entry file. All requests will be delegated through this middleware. The exact file location is important.

netlify/functions/server.ts
import path from 'node:path'; import { createRequestHandler } from 'expo-server/adapter/netlify'; export default createRequestHandler({ build: path.join(__dirname, '../../dist/server'), });

2

在项目根目录创建一个 Netlify 配置文件,以将所有请求重定向到服务器功能。

🌐 Create a Netlify configuration file at the root of your project to redirect all requests to the server function.

netlify.toml
[build] command = "expo export -p web" functions = "netlify/functions" publish = "dist/client" [[redirects]] from = "/*" to = "/.netlify/functions/server" status = 404 [functions] # Include everything to ensure dynamic routes can be used. included_files = ["dist/server/**/*"] [[headers]] for = "/dist/server/_expo/functions/*" [headers.values] # Set to 60 seconds as an example. "Cache-Control" = "public, max-age=60, s-maxage=60"

3

在创建配置文件后,你可以使用 Expo CLI 构建网站和功能:

🌐 After you have created the configuration files, you can build the website and functions with Expo CLI:

Terminal
npx expo export -p web

4

使用 Netlify CLI 部署到 Netlify。

🌐 Deploy to Netlify with the Netlify CLI.

Terminal
# Install the Netlify CLI globally if needed.
npm install netlify-cli -g
# Deploy the website.
netlify deploy

你现在可以通过 Netlify CLI 提供的 URL 访问你的网站。运行 netlify deploy --prod 将会发布到生产环境的 URL。

🌐 You can now visit your website at the URL provided by Netlify CLI. Running netlify deploy --prod will publish to the production URL.

5

如果你使用了任何环境变量或 .env 文件,请将它们添加到 Netlify。你可以通过进入 站点设置,然后在 构建与部署 部分添加它们。

🌐 If you're using any environment variables or .env files, add them to Netlify. You can do this by going to the Site settings and adding them to the Build & deploy section.

Vercel

重要 第三方适配器可能会发生破坏性更改。我们没有对它们进行持续测试。

1

创建一个服务器入口文件。所有请求都将通过此中间件进行处理。文件的确切位置很重要。

🌐 Create a server entry file. All requests will be delegated through this middleware. The exact file location is important.

api/index.ts
const { createRequestHandler } = require('expo-server/adapter/vercel'); module.exports = createRequestHandler({ build: require('path').join(__dirname, '../dist/server'), });

2

在项目根目录下创建一个 Vercel 配置文件(vercel.json),将所有请求重定向到服务器函数。

🌐 Create a Vercel configuration file (vercel.json) at the root of your project to redirect all requests to the server function.

vercel.json
{ "buildCommand": "expo export -p web", "outputDirectory": "dist/client", "functions": { "api/index.ts": { "runtime": "@vercel/node@5.1.8", "includeFiles": "dist/server/**" } }, "rewrites": [ { "source": "/(.*)", "destination": "/api/index" } ] }

vercel.json 的新版本不再使用 routesbuilds 配置选项,并会自动从 dist/client 输出目录提供你的公共资源。

🌐 The newer version of the vercel.json does not use routes and builds configuration options anymore, and serves your public assets from the dist/client output directory automatically.

vercel.json
{ "version": 2, "outputDirectory": "dist", "builds": [ { "src": "package.json", "use": "@vercel/static-build", "config": { "distDir": "dist/client" } }, { "src": "api/index.ts", "use": "@vercel/node", "config": { "includeFiles": ["dist/server/**"] } } ], "routes": [ { "handle": "filesystem" }, { "src": "/(.*)", "dest": "/api/index.ts" } ] }

vercel.json旧版需要 @vercel/static-build 运行时才能从 dist/client 输出目录提供你的资源。

🌐 The legacy version of the vercel.json needs a @vercel/static-build runtime to serve your assets from the dist/client output directory.

3

注意: 这一步仅适用于使用 vercel.json 旧版 的用户。如果你使用的是 v3,可以跳过此步骤。

在创建配置文件后,将一个 vercel-build 脚本添加到你的 package.json 文件中,并将其设置为 expo export -p web

🌐 After you have created the configuration files, add a vercel-build script to your package.json file and set it to expo export -p web.

4

使用 Vercel CLI 部署到 Vercel。

🌐 Deploy to Vercel with the Vercel CLI.

Terminal
# Install the Vercel CLI globally if needed.
npm install vercel -g
# Build the website.
vercel build
# Deploy the website.
vercel deploy --prebuilt

现在你可以通过 Vercel CLI 提供的 URL 访问你的网站。

🌐 You can now visit your website at the URL provided by the Vercel CLI.

已知的限制

🌐 Known limitations

API Routes beta 版本当前不支持多个已知功能。

🌐 Several known features are not currently supported in the API Routes beta release.

无动态导入

🌐 No dynamic imports

API 路由目前的工作方式是将所有代码(除 Node.js 内置模块之外)打包到一个文件中。这意味着你不能使用服务器未打包的任何外部依赖。例如,像 sharp 这样的库,它包含多个平台的二进制文件,是无法使用的。这个问题将在未来的版本中解决。

🌐 API Routes currently work by bundling all code (minus the Node.js built-ins) into a single file. This means that you cannot use any external dependencies that are not bundled with the server. For example, a library such as sharp, which includes multiple platform binaries, cannot be used. This will be addressed in a future version.

不支持 ESM

🌐 ESM not supported

当前的打包实现选择更统一而非更灵活。这意味着原生不支持 ESM 的限制也会传递到 API 路由中。所有代码都会被转译为 Common JS(require/module.exports)。然而,我们仍然建议你使用 ESM 编写 API 路由。这个问题将在未来的版本中解决。

🌐 The current bundling implementation opts to be more unified than flexible. This means the limitation of native not supporting ESM is carried over to API Routes. All code will be transpiled down to Common JS (require/module.exports). However, we recommend you write API Routes using ESM regardless. This will be addressed in a future version.