中间件

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

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

我可以在服务器函数中使用中间件做什么?

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

为服务器函数定义中间件

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

tsx
import { createMiddleware } from '@tanstack/solid-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/solid-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/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:向链中添加中间件。
  • validator:在数据对象传递给此中间件和任何嵌套中间件之前修改数据对象。
  • server:定义中间件将在任何嵌套中间件和最终服务器函数之前执行的服务器端逻辑,并将结果提供给下一个中间件。
  • client:定义中间件将在任何嵌套中间件和最终客户端 RPC 函数(或服务器端函数)之前执行的客户端逻辑,并将结果提供给下一个中间件。

middleware 方法

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

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

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

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

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

validator 方法

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

tsx
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({ type: 'function' })
  .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({ type: 'function' })
  .validator(zodValidator(mySchema))
  .server(({ next, data }) => {
    console.log('Workspace ID:', data.workspaceId)
    return next()
  })

server 方法

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

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

next 返回所需的结果

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

tsx
import { createMiddleware } from '@tanstack/solid-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/solid-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/solid-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/solid-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 函数及其对客户端的响应。

客户端有效负载验证

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

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

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

tsx
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 函数类似,它也接收一个具有以下属性的对象:

  • 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
  },
)

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

默认情况下,客户端上下文不会发送到服务器,因为这可能最终无意中向服务器发送大型有效负载。 如果您需要将客户端上下文发送到服务器,您必须使用 sendContext 属性和对象调用 next 函数来向服务器传输任何数据。传递给 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()
  })

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

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

Warning

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/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],
})

全局中间件类型安全

全局中间件类型本质上与服务器函数本身分离。这意味着如果全局中间件向服务器函数或其他服务器函数特定中间件提供附加上下文,类型将不会自动传递给服务器函数或其他服务器函数特定中间件。

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 验证代码也将被删除。