中间件允许你使用诸如共享验证、上下文等功能自定义使用 createServerFn 创建的服务端函数的行为。中间件甚至可以依赖其他中间件来创建按层级和顺序执行的操作链。
中间件使用 createMiddleware 函数定义。此函数返回一个 Middleware 对象,该对象可用于使用诸如 middleware、validator、server 和 client 等方法继续自定义中间件。
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(async ({ next, data }) => {
console.log('Request received:', data)
const result = await next()
console.log('Response processed:', result)
return result
})
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(async ({ next, data }) => {
console.log('Request received:', data)
const result = await next()
console.log('Response processed:', result)
return result
})
一旦你定义了你的中间件,你就可以将它与 createServerFn 函数结合使用,以自定义你的服务端函数的行为。
import { createServerFn } from '@tanstack/solid-start'
import { loggingMiddleware } from './middleware'
const fn = createServerFn()
.middleware([loggingMiddleware])
.handler(async () => {
// ...
})
import { createServerFn } from '@tanstack/solid-start'
import { loggingMiddleware } from './middleware'
const fn = createServerFn()
.middleware([loggingMiddleware])
.handler(async () => {
// ...
})
有几种方法可用于自定义中间件。如果你(希望)使用 TypeScript,这些方法的顺序由类型系统强制执行,以确保最大的推断和类型安全。
middleware
方法middleware
方法用于将依赖中间件添加到将在当前中间件之前执行的链中。只需使用中间件对象数组调用 middleware
方法。
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().middleware([
authMiddleware,
loggingMiddleware,
])
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().middleware([
authMiddleware,
loggingMiddleware,
])
类型安全的上下文和有效负载验证也从父中间件继承!
validator
方法validator
方法用于修改数据对象,然后再将其传递给此中间件、嵌套的中间件以及最终的服务端函数。此方法应接收一个函数,该函数接受数据对象并返回验证过的(以及可选地修改过的)数据对象。通常使用诸如 zod 之类的验证库来执行此操作。这是一个例子
import { createMiddleware } from '@tanstack/solid-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const mySchema = z.object({
workspaceId: z.string(),
})
const workspaceMiddleware = createMiddleware()
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
import { createMiddleware } from '@tanstack/solid-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const mySchema = z.object({
workspaceId: z.string(),
})
const workspaceMiddleware = createMiddleware()
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
server
方法server
方法用于定义服务端逻辑,中间件将在任何嵌套的中间件以及最终的服务端函数之前和之后执行该逻辑。此方法接收具有以下属性的对象
next
返回必需的结果next
函数用于执行链中的下一个中间件。你必须等待并返回(或直接返回)提供给你的 next
函数的结果,链才能继续执行。
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(async ({ next }) => {
console.log('Request received')
const result = await next()
console.log('Response processed')
return result
})
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(async ({ next }) => {
console.log('Request received')
const result = await next()
console.log('Response processed')
return result
})
next
为下一个中间件提供上下文next
函数可以选择性地使用具有 context
属性和对象值的对象调用。你传递给此 context
值的任何属性都将合并到父 context
中,并提供给下一个中间件。
import { createMiddleware } from '@tanstack/solid-start'
const awesomeMiddleware = createMiddleware().server(({ next }) => {
return next({
context: {
isAwesome: Math.random() > 0.5,
},
})
})
const loggingMiddleware = createMiddleware().server(
async ({ next, context }) => {
console.log('Is awesome?', context.isAwesome)
return next()
},
)
import { createMiddleware } from '@tanstack/solid-start'
const awesomeMiddleware = createMiddleware().server(({ next }) => {
return next({
context: {
isAwesome: Math.random() > 0.5,
},
})
})
const loggingMiddleware = createMiddleware().server(
async ({ next, context }) => {
console.log('Is awesome?', context.isAwesome)
return next()
},
)
尽管服务端函数主要是服务端绑定的操作,但在客户端发出的 RPC 请求周围仍然存在大量的客户端逻辑。这意味着我们还可以在中间件中定义客户端逻辑,该逻辑将在任何嵌套的中间件以及最终的 RPC 函数及其对客户端的响应周围在客户端执行。
默认情况下,中间件验证仅在服务器上执行,以保持客户端包大小较小。但是,你也可以选择通过将 validateClient: true
选项传递给 createMiddleware
函数来在客户端验证数据。这将导致数据在发送到服务器之前在客户端进行验证,从而可能节省往返行程。
为什么我不能为客户端传递不同的验证模式?
客户端验证模式是从服务端模式派生的。这是因为客户端验证模式用于在数据发送到服务器之前验证数据。如果客户端模式与服务端模式不同,则服务器将收到它不期望的数据,这可能会导致意外行为。
import { createMiddleware } from '@tanstack/solid-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const workspaceMiddleware = createMiddleware({ validateClient: true })
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
import { createMiddleware } from '@tanstack/solid-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const workspaceMiddleware = createMiddleware({ validateClient: true })
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
client
方法客户端中间件逻辑使用 Middleware
对象上的 client
方法定义。此方法用于定义客户端逻辑,中间件将在任何嵌套的中间件以及最终的客户端 RPC 函数(或者,如果你正在执行 SSR 或从另一个服务端函数调用此函数,则为服务端函数)之前和之后执行该逻辑。
客户端中间件逻辑与使用 server
方法创建的逻辑共享许多相同的 API,但它在客户端执行。 这包括
next
函数以继续链。next
函数为下一个客户端中间件提供上下文。与 server
函数类似,它也接收具有以下属性的对象
const loggingMiddleware = createMiddleware().client(async ({ next }) => {
console.log('Request sent')
const result = await next()
console.log('Response received')
return result
})
const loggingMiddleware = createMiddleware().client(async ({ next }) => {
console.log('Request sent')
const result = await next()
console.log('Response received')
return result
})
默认情况下,客户端上下文不会发送到服务器,因为这最终可能会向服务器发送大型有效负载。 如果你需要将客户端上下文发送到服务器,则必须使用 sendContext
属性和对象调用 next
函数,以将任何数据传输到服务器。传递给 sendContext
的任何属性都将被合并、序列化并与数据一起发送到服务器,并且在任何嵌套服务端中间件的正常上下文对象上都可用。
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
// Send the workspace ID to the server
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Woah! We have the workspace ID from the client!
console.log('Workspace ID:', context.workspaceId)
return next()
})
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
// Send the workspace ID to the server
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Woah! We have the workspace ID from the client!
console.log('Workspace ID:', context.workspaceId)
return next()
})
你可能已经注意到,在上面的示例中,虽然客户端发送的上下文是类型安全的,但它不是必须在运行时验证的。如果你通过上下文传递动态用户生成的数据,那可能会构成安全隐患,因此,如果你正在通过上下文从客户端向服务器发送动态数据,则应在服务端中间件中验证它,然后再使用它。 这是一个例子
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Validate the workspace ID before using it
const workspaceId = zodValidator(z.number()).parse(context.workspaceId)
console.log('Workspace ID:', workspaceId)
return next()
})
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Validate the workspace ID before using it
const workspaceId = zodValidator(z.number()).parse(context.workspaceId)
console.log('Workspace ID:', workspaceId)
return next()
})
与将客户端上下文发送到服务器类似,你也可以通过使用 sendContext
属性和对象调用 next
函数来将服务端上下文发送到客户端,以将任何数据传输到客户端。传递给 sendContext
的任何属性都将被合并、序列化并与响应一起发送到客户端,并且在任何嵌套客户端中间件的正常上下文对象上都可用。调用 client
中的 next
返回的对象包含从服务器发送到客户端的上下文,并且是类型安全的。中间件能够从 middleware
函数链接的前一个中间件推断出从服务器发送到客户端的上下文。
警告
client
中 next
的返回类型只能从当前中间件链中已知的中间件推断出来。因此,next
最准确的返回类型位于中间件链末端的中间件中
const serverTimer = createMiddleware().server(async ({ next }) => {
return next({
sendContext: {
// Send the current time to the client
timeFromServer: new Date(),
},
})
})
const requestLogger = createMiddleware()
.middleware([serverTimer])
.client(async ({ next }) => {
const result = await next()
// Woah! We have the time from the server!
console.log('Time from the server:', result.context.timeFromServer)
return result
})
const serverTimer = createMiddleware().server(async ({ next }) => {
return next({
sendContext: {
// Send the current time to the client
timeFromServer: new Date(),
},
})
})
const requestLogger = createMiddleware()
.middleware([serverTimer])
.client(async ({ next }) => {
const result = await next()
// Woah! We have the time from the server!
console.log('Time from the server:', result.context.timeFromServer)
return result
})
使用 server
方法的中间件在与服务端函数相同的上下文中执行,因此你可以遵循完全相同的 服务端函数上下文实用程序 来读取和修改有关请求标头、状态代码等的任何内容。
使用 client
方法的中间件在与服务端函数完全不同的客户端上下文中执行,因此你不能使用相同的实用程序来读取和修改请求。但是,你仍然可以在调用 next
函数时修改请求返回的其他属性。当前支持的属性是
这是一个使用此中间件向任何请求添加 Authorization
标头的示例
import { getToken } from 'my-auth-library'
const authMiddleware = createMiddleware().client(async ({ next }) => {
return next({
headers: {
Authorization: `Bearer ${getToken()}`,
},
})
})
import { getToken } from 'my-auth-library'
const authMiddleware = createMiddleware().client(async ({ next }) => {
return next({
headers: {
Authorization: `Bearer ${getToken()}`,
},
})
})
中间件可以通过两种不同的方式使用
全局中间件为应用程序中的每个服务端函数自动运行。这对于应应用于所有请求的功能(如身份验证、日志记录和监控)非常有用。
要使用全局中间件,请在你的项目中创建一个 global-middleware.ts
文件(通常位于 app/global-middleware.ts
)。此文件在客户端和服务器环境以及你注册全局中间件的位置中运行。
以下是如何注册全局中间件
// app/global-middleware.ts
import { registerGlobalMiddleware } from '@tanstack/solid-start'
import { authMiddleware } from './middleware'
registerGlobalMiddleware({
middleware: [authMiddleware],
})
// app/global-middleware.ts
import { registerGlobalMiddleware } from '@tanstack/solid-start'
import { authMiddleware } from './middleware'
registerGlobalMiddleware({
middleware: [authMiddleware],
})
全局中间件类型本质上与服务端函数分离。这意味着,如果全局中间件为服务端函数或其他服务端函数特定中间件提供额外的上下文,则类型不会自动传递到服务端函数或其他服务端函数特定中间件。
// app/global-middleware.ts
registerGlobalMiddleware({
middleware: [authMiddleware],
})
// app/global-middleware.ts
registerGlobalMiddleware({
middleware: [authMiddleware],
})
// authMiddleware.ts
const authMiddleware = createMiddleware().server(({ next, context }) => {
console.log(context.user) // <-- This will not be typed!
// ...
})
// authMiddleware.ts
const authMiddleware = createMiddleware().server(({ next, context }) => {
console.log(context.user) // <-- This will not be typed!
// ...
})
为了解决这个问题,将你尝试引用的全局中间件添加到服务端函数的中间件数组中。全局中间件将被去重为单个条目(全局实例),并且你的服务端函数将接收正确的类型。
这是一个关于其工作原理的示例
import { authMiddleware } from './authMiddleware'
const fn = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
console.log(context.user)
// ...
})
import { authMiddleware } from './authMiddleware'
const fn = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
console.log(context.user)
// ...
})
中间件首先按依赖关系执行,首先是全局中间件,然后是服务端函数中间件。以下示例将按以下顺序记录
const globalMiddleware1 = createMiddleware().server(async ({ next }) => {
console.log('globalMiddleware1')
return next()
})
const globalMiddleware2 = createMiddleware().server(async ({ next }) => {
console.log('globalMiddleware2')
return next()
})
registerGlobalMiddleware({
middleware: [globalMiddleware1, globalMiddleware2],
})
const a = createMiddleware().server(async ({ next }) => {
console.log('a')
return next()
})
const b = createMiddleware()
.middleware([a])
.server(async ({ next }) => {
console.log('b')
return next()
})
const c = createMiddleware()
.middleware()
.server(async ({ next }) => {
console.log('c')
return next()
})
const d = createMiddleware()
.middleware([b, c])
.server(async () => {
console.log('d')
})
const fn = createServerFn()
.middleware([d])
.server(async () => {
console.log('fn')
})
const globalMiddleware1 = createMiddleware().server(async ({ next }) => {
console.log('globalMiddleware1')
return next()
})
const globalMiddleware2 = createMiddleware().server(async ({ next }) => {
console.log('globalMiddleware2')
return next()
})
registerGlobalMiddleware({
middleware: [globalMiddleware1, globalMiddleware2],
})
const a = createMiddleware().server(async ({ next }) => {
console.log('a')
return next()
})
const b = createMiddleware()
.middleware([a])
.server(async ({ next }) => {
console.log('b')
return next()
})
const c = createMiddleware()
.middleware()
.server(async ({ next }) => {
console.log('c')
return next()
})
const d = createMiddleware()
.middleware([b, c])
.server(async () => {
console.log('d')
})
const fn = createServerFn()
.middleware([d])
.server(async () => {
console.log('fn')
})
中间件功能根据每个生成的捆绑包的环境进行 tree-shaking。
server
方法中使用的任何代码始终从客户端捆绑包中删除。如果 validateClient
设置为 true
,则客户端验证代码将包含在客户端捆绑包中,否则 data
验证代码也将被删除。你的每周 JavaScript 新闻。每周一免费发送给超过 100,000 名开发者。