服务器中间件

编辑页面

了解如何创建在 Expo Router 中对服务器的每个请求都运行的中间件。


For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

重要 Server middleware 处于 alpha 阶段,并且可在 SDK 54 及更高版本中使用。它在生产环境中使用时需要一个已部署的服务器

Expo Router 中的 Server middleware 允许你在请求到达路由之前运行代码,从而为每个请求启用强大的服务端功能,例如身份验证和日志记录。与处理特定端点的 API routes 不同,middleware 会对应用中的每一个请求运行,因此它应尽可能快速,以避免拖慢应用性能。在原生端进行的客户端导航,或在 Web 应用中使用 <Link /> 时,都不会经过 server middleware。

设置

1

在应用配置中启用 server middleware

首先,通过在你的 app config 中添加服务器配置,将应用配置为使用 server 输出:

app.json
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "web": { "output": "server" }, "plugins": [ [ "expo-router", { "unstable_useServerMiddleware": true } ] ] } }

2

创建你的 middleware 文件

在你的 src/app 目录中创建一个 +middleware.ts 文件,用于定义你的 server middleware 函数:

src/app/+middleware.ts
export default function middleware(request) { console.log(`Middleware executed for: ${request.url}`); // 你的 middleware 逻辑写在这里 }

middleware 函数必须作为该文件的默认导出。它接收一个不可变请求,并且可以返回一个 Response,或者什么都不返回以让请求按原样继续传递。请求是不可变的,以防止副作用;你可以读取 headers 和属性,但不能修改 headers 或消耗请求体。

3

启动你的开发服务器

运行你的开发服务器以测试 middleware:

Terminal
npx expo start

现在你的 middleware 将对应用的所有请求运行。

4

测试 middleware 功能

在浏览器中访问你的应用,或发起请求来测试 middleware 是否正常工作。检查控制台中来自 middleware 函数的日志消息。

5

配置 middleware 匹配器(可选)

默认情况下,middleware 会运行于所有服务器请求。你可以使用 unstable_settings 添加一个 matcher 来控制 middleware 何时执行:

src/app/+middleware.ts
export const unstable_settings = { matcher: { // 仅在 GET 请求上运行 methods: ['GET'], // 仅在 API 路由和特定路径上运行 patterns: ['/api', '/admin/[...path]'], }, }; export default function middleware(request) { console.log(`Middleware executed for: ${request.url}`); }

matcher 配置允许你:

  • 按 HTTP 方法过滤:指定哪些方法会触发 middleware
  • 按路径模式过滤:使用精确路径、命名参数或正则表达式定义应匹配哪些 URL 模式

工作原理

middleware 函数在任何路由处理程序之前执行,使你能够执行日志记录、身份验证或修改响应等操作。它仅在服务器上运行,并且只针对实际的 HTTP 请求运行。

请求/响应流程

当请求到达你的应用时,Expo Router 会按以下顺序处理它:

  1. middleware 函数首先使用一个不可变请求运行。
  2. 如果 middleware 返回一个 Response,则会立即发送该响应
  3. 如果 middleware 什么都不返回,请求将继续进入匹配的路由
  4. 路由处理程序处理请求并返回其响应

模式匹配

matcher 支持不同类型的模式,以控制 middleware 何时运行:

export const unstable_settings = { matcher: { patterns: [ '/api', // 精确路径 '/posts/[postId]', // 命名参数 '/blog/[...slug]', // 捕获所有参数 /^\/api\/v\d+\/users$/, // 正则表达式 ], }, };
  • 精确路径 只匹配指定路径。/api 匹配 /api,但不匹配 /api/users
  • 命名参数[postId] 会捕获任意单个段。/posts/[postId] 匹配 /posts/123/posts/my-post
  • 捕获所有参数[...slug] 会捕获一个或多个段。/blog/[...slug] 匹配 /blog/2024/blog/2024/12/post
  • 正则表达式 用于复杂模式。/^\/api\/v\d+\/users$/ 匹配 /api/v1/users,但不匹配 /api/users

如果任意一个模式匹配请求 URL,middleware 就会运行。当同时指定 methodspatterns 时,middleware 运行必须同时满足这两个条件。

Middleware 执行顺序

Expo Router 支持一个名为 +middleware.ts 的单一 middleware 文件,它会针对所有服务器请求运行。使用 matcher 时,middleware 只会对匹配指定模式和方法的请求执行,并且发生在任何路由匹配或渲染之前。

middleware 何时运行

middleware 仅针对发送到你服务器的实际 HTTP 请求执行。这意味着它会在以下情况执行:

  • 首次页面加载,例如用户第一次访问你的网站
  • 整页刷新
  • 直接 URL 导航
  • 来自任何客户端(原生/Web 应用、外部服务)的 API 路由调用
  • 服务端渲染请求

middleware 不会在以下情况运行:

  • 使用 <Link />router 的客户端导航
  • 原生应用的屏幕切换
  • 预取的路由
  • 静态资源请求,例如图片和字体

示例

身份验证

middleware 常用于在路由加载之前执行授权检查。你可以检查 headers、cookies 或查询参数,以确定用户是否有权访问某些路由:

src/app/+middleware.ts
import { jwtVerify } from 'jose'; export default function middleware(request) { const token = request.headers.get('authorization'); const decoded = jwtVerify(token, process.env.SECRET_KEY); if (!decoded.payload) { return new Response('Forbidden', { status: 403 }); } }
日志记录

你可以使用 middleware 记录请求,用于调试或分析。这有助于你跟踪用户活动或诊断应用中的问题:

src/app/+middleware.ts
export default function middleware(request) { console.log(`${request.method} ${request.url}`); }
动态重定向

middleware 也可以用于执行动态重定向。这使你能够根据特定条件控制用户导航:

src/app/+middleware.ts
export default function middleware(request) { if (request.headers.has('specific-header')) { return Response.redirect('https://expo.dev'); } }
仅 API 的 middleware

使用 matcher 仅对 API 路由运行 middleware,同时保持其他路由不受影响:

src/app/+middleware.ts
export const unstable_settings = { matcher: { patterns: ['/api'], }, }; export default function middleware(request) { // 记录所有 API 请求以用于调试 console.log(`API request: ${request.method} ${request.url}`); // 为 API 路由添加 CORS 头 const response = new Response(); response.headers.set('Access-Control-Allow-Origin', '*'); return response; }
按方法进行身份验证

保护写操作(POST、PUT、DELETE),同时允许公开读取访问:

src/app/+middleware.ts
export const unstable_settings = { matcher: { methods: ['POST', 'PUT', 'DELETE'], patterns: ['/api', '/admin/[...path]'], }, }; export default function middleware(request) { const token = request.headers.get('authorization'); if (!token || !isValidToken(token)) { return new Response('Unauthorized', { status: 401 }); } } function isValidToken(token: string): boolean { // 你的 token 验证逻辑 return token.startsWith('Bearer '); }
选择性日志记录

监控特定端点,而不是记录每个请求:

src/app/+middleware.ts
export const unstable_settings = { matcher: { patterns: ['/api/users/[userId]', '/admin', /^\/webhook/], }, }; export default function middleware(request) { const userAgent = request.headers.get('user-agent'); const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ${request.method} ${request.url} - ${userAgent}`); }

其他说明

最佳实践

  • 保持 middleware 轻量,因为它会在每个服务器请求上同步运行,并直接影响响应时间。
  • 使用 matcher 通过避免在不需要 middleware 的路由上执行它来优化性能,尤其适用于高流量应用。
  • 对于简单模式,优先使用精确路径和命名参数,而不是 regex,因为它们比复杂正则表达式更快、更易维护。
  • 结合方法和模式过滤,以精确控制 middleware 何时执行。
  • 对于原生应用,使用 API routes 进行安全的数据获取。当原生应用调用 API routes 时,这些请求会先经过 middleware。

类型化 middleware

src/app/+middleware.ts
import { MiddlewareFunction } from 'expo-router/server'; const middleware: MiddlewareFunction = request => { if (request.headers.has('specific-header')) { return Response.redirect('https://expo.dev'); } }; export default middleware;

限制

  • middleware 仅在服务器上运行,并且只针对 HTTP 请求运行。它不会在客户端导航期间执行,例如使用 <Link /> 或原生应用的屏幕切换时。
  • 传递给 middleware 的 request 对象是不可变的,以防止副作用。你不能修改 headers 或消耗请求体,从而确保它仍可供路由处理程序使用。
  • 你的应用中只能有一个根级 +middleware.ts
  • 适用于 API routes 的相同限制也适用于 middleware。

请求不可变性

为防止意外副作用并确保请求体仍可供路由处理程序使用,传递给 middleware 的 Request 是不可变的。这意味着你可以:

  • 读取所有请求属性,如 urlmethodheaders
  • 使用 request.headers.get() 读取 header 值
  • 使用 request.headers.has() 检查 header 是否存在
  • 访问 URL 参数和查询字符串

但你不能:

  • 使用 set()append()delete() 修改 headers
  • 使用 text()json()formData() 等方法消耗请求体
  • 直接访问 body 属性