中间件

什么是服务器函数中间件?

中间件允许您通过共享验证、上下文等来自定义使用 createServerFn 创建的服务器函数的行为。中间件甚至可以依赖其他中间件来创建按层次结构和顺序执行的操作链。

我可以用中间件在服务器函数中做哪些事情?

  • 身份验证:在执行服务器函数之前验证用户身份。
  • 授权:检查用户是否具有执行服务器函数所需的权限。
  • 日志记录:记录请求、响应和错误。
  • 可观察性:收集指标、跟踪和日志。
  • 提供上下文:将数据附加到请求对象,供其他中间件或服务器函数使用。
  • 错误处理:以一致的方式处理错误。
  • 还有更多!可能性由您决定!

为服务器函数定义中间件

中间件使用 createMiddleware 函数定义。此函数返回一个 Middleware 对象,可用于通过 middlewarevalidatorserverclient 等方法继续自定义中间件。

tsx
import { createMiddleware } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware({ type: 'function' }).server(
  async ({ next, data }) => {
    console.log('Request received:', data)
    const result = await next()
    console.log('Response processed:', result)
    return result
  },
)
import { createMiddleware } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware({ type: 'function' }).server(
  async ({ next, data }) => {
    console.log('Request received:', data)
    const result = await next()
    console.log('Response processed:', result)
    return result
  },
)

在服务器函数中使用中间件

定义中间件后,您可以将其与 createServerFn 函数结合使用,以自定义服务器函数的行为。

tsx
import { createServerFn } from '@tanstack/react-start'
import { loggingMiddleware } from './middleware'

const fn = createServerFn()
  .middleware([loggingMiddleware])
  .handler(async () => {
    // ...
  })
import { createServerFn } from '@tanstack/react-start'
import { loggingMiddleware } from './middleware'

const fn = createServerFn()
  .middleware([loggingMiddleware])
  .handler(async () => {
    // ...
  })

中间件方法

有几种方法可用于自定义中间件。如果您(希望如此)正在使用 TypeScript,这些方法的顺序由类型系统强制执行,以确保最大程度的类型推断和类型安全。

  • middleware:向链中添加中间件。
  • validator:在将数据对象传递给此中间件和任何嵌套中间件之前对其进行修改。
  • server:定义中间件将在任何嵌套中间件和最终服务器函数之前执行的服务器端逻辑,并向下一个中间件提供结果。
  • client:定义中间件将在任何嵌套中间件和最终客户端 RPC 函数(或服务器端函数)之前执行的客户端逻辑,并向下一个中间件提供结果。

middleware 方法

middleware 方法用于将依赖中间件添加到将在当前中间件之前执行的链中。只需使用中间件对象数组调用 middleware 方法即可。

tsx
import { createMiddleware } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware({ type: 'function' }).middleware([
  authMiddleware,
  loggingMiddleware,
])
import { createMiddleware } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware({ type: 'function' }).middleware([
  authMiddleware,
  loggingMiddleware,
])

类型安全上下文和负载验证也从父中间件继承!

validator 方法

validator 方法用于在将数据对象传递给此中间件、嵌套中间件以及最终服务器函数之前对其进行修改。此方法应接收一个函数,该函数接受数据对象并返回一个经过验证(并可选地修改)的数据对象。通常会使用像 zod 这样的验证库来完成此操作。这是一个示例:

tsx
import { createMiddleware } from '@tanstack/react-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'

const mySchema = z.object({
  workspaceId: z.string(),
})

const workspaceMiddleware = createMiddleware({ type: 'function' })
  .validator(zodValidator(mySchema))
  .server(({ next, data }) => {
    console.log('Workspace ID:', data.workspaceId)
    return next()
  })
import { createMiddleware } from '@tanstack/react-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'

const mySchema = z.object({
  workspaceId: z.string(),
})

const workspaceMiddleware = createMiddleware({ type: 'function' })
  .validator(zodValidator(mySchema))
  .server(({ next, data }) => {
    console.log('Workspace ID:', data.workspaceId)
    return next()
  })

server 方法

server 方法用于定义中间件将在任何嵌套中间件和最终服务器函数之前和之后执行的服务器端逻辑。此方法接收一个具有以下属性的对象:

  • next:一个函数,调用时将执行链中的下一个中间件。
  • data:传递给服务器函数的数据对象。
  • context:一个存储来自父中间件数据的对象。它可以扩展为将传递给子中间件的附加数据。

next 返回所需结果

next 函数用于执行链中的下一个中间件。您必须 await 并返回(或直接返回)提供给您的 next 函数的结果,以使链继续执行。

tsx
import { createMiddleware } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    console.log('Request received')
    const result = await next()
    console.log('Response processed')
    return result
  },
)
import { createMiddleware } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    console.log('Request received')
    const result = await next()
    console.log('Response processed')
    return result
  },
)

通过 next 向下一个中间件提供上下文

next 函数可以选择使用一个具有 context 属性且值为对象的对象调用。您传递给此 context 值的任何属性都将合并到父 context 中,并提供给下一个中间件。

tsx
import { createMiddleware } from '@tanstack/react-start'

const awesomeMiddleware = createMiddleware({ type: 'function' }).server(
  ({ next }) => {
    return next({
      context: {
        isAwesome: Math.random() > 0.5,
      },
    })
  },
)

const loggingMiddleware = createMiddleware({ type: 'function' })
  .middleware([awesomeMiddleware])
  .server(async ({ next, context }) => {
    console.log('Is awesome?', context.isAwesome)
    return next()
  })
import { createMiddleware } from '@tanstack/react-start'

const awesomeMiddleware = createMiddleware({ type: 'function' }).server(
  ({ next }) => {
    return next({
      context: {
        isAwesome: Math.random() > 0.5,
      },
    })
  },
)

const loggingMiddleware = createMiddleware({ type: 'function' })
  .middleware([awesomeMiddleware])
  .server(async ({ next, context }) => {
    console.log('Is awesome?', context.isAwesome)
    return next()
  })

客户端逻辑

尽管服务器函数主要是服务器端绑定操作,但客户端仍有大量围绕从客户端发出的 RPC 请求的客户端逻辑。这意味着我们还可以在中间件中定义客户端逻辑,这些逻辑将在客户端围绕任何嵌套中间件以及最终的 RPC 函数及其对客户端的响应执行。

客户端负载验证

默认情况下,中间件验证仅在服务器上执行,以保持客户端包大小较小。但是,您也可以选择通过将 validateClient: true 选项传递给 createMiddleware 函数来在客户端验证数据。这将在数据发送到服务器之前在客户端对其进行验证,从而可能节省一次往返。

为什么我不能为客户端传递不同的验证模式?

客户端验证模式派生自服务器端模式。这是因为客户端验证模式用于在数据发送到服务器之前对其进行验证。如果客户端模式与服务器端模式不同,服务器将收到它不期望的数据,这可能导致意外行为。

tsx
import { createMiddleware } from '@tanstack/react-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/react-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 函数类似,它也接收一个具有以下属性的对象:

  • next:一个函数,调用时将执行链中的下一个客户端中间件。
  • data:传递给客户端函数的数据对象。
  • context:一个存储来自父中间件数据的对象。它可以扩展为将传递给子中间件的附加数据。
tsx
const loggingMiddleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    console.log('Request sent')
    const result = await next()
    console.log('Response received')
    return result
  },
)
const loggingMiddleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    console.log('Request sent')
    const result = await next()
    console.log('Response received')
    return result
  },
)

将客户端上下文发送到服务器

默认情况下,客户端上下文不会发送到服务器,因为这可能会无意中向服务器发送大量负载。如果您需要将客户端上下文发送到服务器,则必须调用 next 函数,其中包含 sendContext 属性和对象以将任何数据传输到服务器。传递给 sendContext 的任何属性都将合并、序列化并与数据一起发送到服务器,并且在任何嵌套服务器中间件的正常上下文对象上可用。

tsx
const requestLogger = createMiddleware({ type: 'function' })
  .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({ type: 'function' })
  .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()
  })

客户端发送上下文安全

您可能已经注意到,在上面的示例中,虽然客户端发送的上下文是类型安全的,但它不要求在运行时进行验证。如果您通过上下文传递动态用户生成的数据,这可能会带来安全隐患,因此如果您通过上下文从客户端向服务器发送动态数据,则应在服务器端中间件中使用之前对其进行验证。这是一个示例:

tsx
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'

const requestLogger = createMiddleware({ type: 'function' })
  .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({ type: 'function' })
  .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()
  })

将服务器上下文发送到客户端

与将客户端上下文发送到服务器类似,您还可以通过调用 next 函数,其中包含 sendContext 属性和对象,将服务器上下文发送到客户端,以将任何数据传输到客户端。传递给 sendContext 的任何属性都将合并、序列化并与响应一起发送到客户端,并且在任何嵌套客户端中间件的正常上下文对象上可用。client 中调用 next 的返回对象包含从服务器发送到客户端的上下文,并且是类型安全的。中间件能够从从 middleware 函数链接的先前中间件推断从服务器发送到客户端的上下文。

警告

clientnext 的返回类型只能从当前中间件链中已知的中间件推断。因此,next 最准确的返回类型在中间件链末端的中间件中。

tsx
const serverTimer = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    return next({
      sendContext: {
        // Send the current time to the client
        timeFromServer: new Date(),
      },
    })
  },
)

const requestLogger = createMiddleware({ type: 'function' })
  .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({ type: 'function' }).server(
  async ({ next }) => {
    return next({
      sendContext: {
        // Send the current time to the client
        timeFromServer: new Date(),
      },
    })
  },
)

const requestLogger = createMiddleware({ type: 'function' })
  .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 函数时,您仍然可以修改请求并返回附加属性。当前支持的属性是:

  • headers:一个包含要添加到请求的头的对象。

这是一个使用此中间件为任何请求添加 Authorization 头的示例:

tsx
import { getToken } from 'my-auth-library'

const authMiddleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    return next({
      headers: {
        Authorization: `Bearer ${getToken()}`,
      },
    })
  },
)
import { getToken } from 'my-auth-library'

const authMiddleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    return next({
      headers: {
        Authorization: `Bearer ${getToken()}`,
      },
    })
  },
)

使用中间件

中间件可以通过两种不同的方式使用:

  • 全局中间件:应该为每个请求执行的中间件。
  • 服务器函数中间件:应该为特定服务器函数执行的中间件。

全局中间件

全局中间件会自动为应用程序中的每个服务器函数运行。这对于身份验证、日志记录和监控等应适用于所有请求的功能非常有用。

要使用全局中间件,请在项目中创建一个 global-middleware.ts 文件(通常在 app/global-middleware.ts)。此文件在客户端和服务器环境中运行,是您注册全局中间件的地方。

以下是如何注册全局中间件:

tsx
// app/global-middleware.ts
import { registerGlobalMiddleware } from '@tanstack/react-start'
import { authMiddleware } from './middleware'

registerGlobalMiddleware({
  middleware: [authMiddleware],
})
// app/global-middleware.ts
import { registerGlobalMiddleware } from '@tanstack/react-start'
import { authMiddleware } from './middleware'

registerGlobalMiddleware({
  middleware: [authMiddleware],
})

全局中间件类型安全

全局中间件类型本质上与服务器函数本身是分离的。这意味着如果全局中间件向服务器函数或其他特定于服务器函数的中间件提供额外的上下文,则类型不会自动传递到服务器函数或其他特定于服务器函数的中间件。

tsx
// app/global-middleware.ts
registerGlobalMiddleware({
  middleware: [authMiddleware],
})
// app/global-middleware.ts
registerGlobalMiddleware({
  middleware: [authMiddleware],
})
tsx
// authMiddleware.ts
const authMiddleware = createMiddleware({ type: 'function' }).server(
  ({ next, context }) => {
    console.log(context.user) // <-- This will not be typed!
    // ...
  },
)
// authMiddleware.ts
const authMiddleware = createMiddleware({ type: 'function' }).server(
  ({ next, context }) => {
    console.log(context.user) // <-- This will not be typed!
    // ...
  },
)

要解决此问题,请将您尝试引用的全局中间件添加到服务器函数的中间件数组中。全局中间件将被去重为单个条目(全局实例),您的服务器函数将收到正确的类型。

以下是其工作原理的示例:

tsx
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)
    // ...
  })

中间件执行顺序

中间件按依赖关系优先执行,从全局中间件开始,然后是服务器函数中间件。以下示例将按此顺序记录以下内容:

  • globalMiddleware1
  • globalMiddleware2
  • a
  • b
  • c
  • d
tsx
const globalMiddleware1 = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    console.log('globalMiddleware1')
    return next()
  },
)

const globalMiddleware2 = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    console.log('globalMiddleware2')
    return next()
  },
)

registerGlobalMiddleware({
  middleware: [globalMiddleware1, globalMiddleware2],
})

const a = createMiddleware({ type: 'function' }).server(async ({ next }) => {
  console.log('a')
  return next()
})

const b = createMiddleware({ type: 'function' })
  .middleware([a])
  .server(async ({ next }) => {
    console.log('b')
    return next()
  })

const c = createMiddleware({ type: 'function' })
  .middleware()
  .server(async ({ next }) => {
    console.log('c')
    return next()
  })

const d = createMiddleware({ type: 'function' })
  .middleware([b, c])
  .server(async () => {
    console.log('d')
  })

const fn = createServerFn()
  .middleware([d])
  .server(async () => {
    console.log('fn')
  })
const globalMiddleware1 = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    console.log('globalMiddleware1')
    return next()
  },
)

const globalMiddleware2 = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    console.log('globalMiddleware2')
    return next()
  },
)

registerGlobalMiddleware({
  middleware: [globalMiddleware1, globalMiddleware2],
})

const a = createMiddleware({ type: 'function' }).server(async ({ next }) => {
  console.log('a')
  return next()
})

const b = createMiddleware({ type: 'function' })
  .middleware([a])
  .server(async ({ next }) => {
    console.log('b')
    return next()
  })

const c = createMiddleware({ type: 'function' })
  .middleware()
  .server(async ({ next }) => {
    console.log('c')
    return next()
  })

const d = createMiddleware({ type: 'function' })
  .middleware([b, c])
  .server(async () => {
    console.log('d')
  })

const fn = createServerFn()
  .middleware([d])
  .server(async () => {
    console.log('fn')
  })

环境摇树优化

中间件功能根据每个生成的捆绑包的环境进行摇树优化。

  • 在服务器上,没有进行摇树优化,因此中间件中使用的所有代码都将包含在服务器捆绑包中。
  • 在客户端,所有服务器端特定代码都从客户端捆绑包中删除。这意味着 server 方法中使用的任何代码始终从客户端捆绑包中删除。如果 validateClient 设置为 true,则客户端验证代码将包含在客户端捆绑包中,否则 data 验证代码也将被删除。
我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
Prisma
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。