服务器中间件
编辑
学习如何在 Expo Router 中创建对服务器的每个请求都运行的中间件。
服务器中间件是 SDK 54 及更高版本中的实验性功能,需要一个已部署的服务器才能在生产环境中使用。
Expo Router 中的服务器中间件允许您在请求到达路由之前运行代码,从而为每个请求启用强大的服务器端功能,如身份验证和日志记录。与处理特定端点的API 路由不同,中间件适用于您的应用中的 每个 请求,因此应该尽可能快速运行,以避免降低应用的性能。在本地应用或在使用 <Link /> 的 Web 应用中,客户端导航不会经过服务器中间件。
设置
1
在应用配置中启用服务器中间件
首先,通过将服务器配置添加到您的应用配置中来配置您的应用以使用服务器输出:
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "web": { "output": "server" }, "plugins": [ [ "expo-router", { "unstable_useServerMiddleware": true } ] ] } }
2
5
配置中间件匹配器(可选)
默认情况下,中间件在所有服务器请求上运行。您可以添加匹配器以控制中间件执行的时机,使用 unstable_settings:
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}`); }
匹配器配置允许您:
- 按 HTTP 方法过滤:指定哪些方法应触发中间件
- 按路径模式过滤:定义哪些 URL 模式应使用精确路径、命名参数或正则表达式匹配
工作原理
中间件函数在任何路由处理程序之前执行,允许您执行日志记录、身份验证或修改响应等操作。它专门在服务器上运行,仅对实际的 HTTP 请求执行。
请求/响应流
当请求进入您的应用时,Expo Router 按以下顺序处理它:
- 中间件函数首先运行,使用不可变请求。
- 如果中间件返回一个
Response,则该响应会立即发送 - 如果中间件不返回任何内容,请求将继续到匹配的路由
- 路由处理程序处理请求并返回其响应
模式匹配
匹配器支持不同的模式类型,以控制中间件的运行时机:
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 匹配,则中间件会运行。当同时指定 methods 和 patterns 时,必须满足这两个条件,以便中间件运行。
中间件执行顺序
Expo Router 支持一个名为 +middleware.ts 的单一中间件文件,适用于所有服务器请求。当使用匹配器时,中间件仅在与指定模式和方法匹配的请求上执行,在发生任何路由匹配或呈现之前。
中间件运行时机
中间件仅在对您的服务器的实际 HTTP 请求中执行。这意味着它在以下情况下执行:
- 初始页面加载,例如用户首次访问您的网站时
- 完整页面刷新
- 直接 URL 导航
- 来自任何客户端的 API 路由调用(本地/Web 应用程序、外部服务)
- 服务器端渲染请求
中间件不在以下情况下运行:
示例
身份验证
中间件通常用于在路由加载之前执行授权检查。您可以检查头部、cookie 或查询参数,以确定用户是否可以访问特定路由:
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 }); } }
日志记录
您可以使用中间件来记录请求,以便调试或分析。这可以帮助您跟踪用户活动或诊断应用中的问题:
export default function middleware(request) { console.log(`${request.method} ${request.url}`); }
动态重定向
中间件还可以用来执行动态重定向。这使您能够根据特定条件控制用户导航:
export default function middleware(request) { if (request.headers.has('specific-header')) { return Response.redirect('https://expo.dev'); } }
仅限 API 的中间件
使用匹配器仅对 API 路由运行中间件,保持其他路由不受影响:
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),同时允许公共读取访问:
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 { // 您的令牌验证逻辑 return token.startsWith('Bearer '); }
选择性日志记录
监控特定端点,而不记录每个请求:
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}`); }
其他说明
最佳实践
- 使中间件轻量,因为它在每个服务器请求上同步运行,并直接影响响应时间。
- 使用匹配器通过避免在不需要它的路由上不必要的中间件执行来优化性能,尤其是对于流量大的应用程序。
- 优先使用精确路径和命名参数,而不是正则表达式,因为简单模式比复杂正则表达式更容易评估和维护。
- 组合方法和模式过滤,以精确控制中间件执行的时机。
- 对于本地应用,使用 API 路由进行安全的数据获取。当本地应用调用 API 路由时,这些请求将首先通过中间件。
类型化中间件
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;
限制
- 中间件仅在服务器上运行,并且仅对 HTTP 请求。它不会在客户端导航期间执行,例如使用
<Link />或本地应用程序的屏幕过渡。 - 传递给中间件的请求对象是不可变的,以防止副作用。您无法修改头部或消费请求体,确保它仍可用于路由处理程序。
- 您的应用中只能有一个根级 +middleware.ts。
- 适用于 API 路由 的相同限制也适用于中间件。
请求不可变性
为了防止意外副作用并确保请求体保持可用于路由处理程序,传递给中间件的 Request 是不可变的。这意味着您可以:
- 读取所有请求属性,如
url、method、headers等 - 使用
request.headers.get()读取头部值 - 使用
request.headers.has()检查头部的存在 - 访问 URL 参数和查询字符串
但您将无法:
- 使用
set()、append()、delete()修改头部 - 使用
text()、json()、formData()等消费请求体 - 直接访问
body属性