服务器函数

什么是服务器函数?

服务器函数允许您指定几乎可以在任何地方(甚至客户端)调用的逻辑,但在服务器端运行。 实际上,它们与 API 路由并无太大区别,但有一些主要区别

  • 它们没有稳定的公共 URL(但您很快就能做到这一点!)
  • 它们可以从应用程序中的任何位置调用,包括 loaders、hooks、components 等,但不能从 API 路由调用。

但是,它们与常规 API 路由类似,因为

  • 它们可以访问请求上下文,允许您读取标头、设置 Cookie 等
  • 它们可以访问敏感信息,例如环境变量,而不会将其暴露给客户端
  • 它们可用于执行任何类型的服务器端逻辑,例如从数据库获取数据、发送电子邮件或与其他服务交互
  • 它们可以返回任何值,包括原始类型、JSON 可序列化对象,甚至原始 Response 对象
  • 它们可以抛出错误,包括重定向和 notFound,这些错误可以由路由器自动处理

服务器函数与“React Server Functions”有何不同?

  • TanStack 服务器函数不绑定到特定的前端框架,可以与任何前端框架或无框架一起使用。
  • TanStack 服务器函数由标准 HTTP 请求支持,可以根据需要频繁调用,而不会受到串行执行瓶颈的影响。

它们是如何工作的?

服务器函数可以在应用程序中的任何位置定义,但必须在文件的顶层定义。 它们可以在整个应用程序中调用,包括 loaders、hooks 等。 传统上,此模式称为远程过程调用 (RPC),但由于这些函数的同构性质,我们将其称为服务器函数。

  • 在服务器包上,服务器函数逻辑保持不变。 由于它们已在正确的位置,因此无需执行任何操作。
  • 在客户端上,服务器函数将被删除;它们仅存在于服务器上。 客户端对服务器函数的任何调用都将替换为对服务器的 fetch 请求,以执行服务器函数,并将响应发送回客户端。

服务器函数中间件

服务器函数可以使用中间件来共享逻辑、上下文、通用操作、先决条件等等。 要了解有关服务器函数中间件的更多信息,请务必阅读中间件指南中的相关内容。

定义服务器函数

我们要感谢 tRPC 团队,感谢他们为 TanStack Start 的服务器函数设计提供的灵感,以及在实现过程中的指导。 我们非常喜欢(并推荐)将 tRPC 用于 API 路由,以至于我们坚持服务器函数获得相同的一流待遇和开发者体验。 谢谢!

服务器函数使用 createServerFn 函数定义,该函数来自 @tanstack/react-start 包。 此函数接受一个可选的 options 参数,用于指定 HTTP 方法和响应类型等配置,并允许您链式调用结果以定义服务器函数的主体、输入验证、中间件等。 这是一个简单的示例

tsx
// getServerTime.ts
import { createServerFn } from '@tanstack/react-start'

export const getServerTime = createServerFn().handler(async () => {
  // Wait for 1 second
  await new Promise((resolve) => setTimeout(resolve, 1000))
  // Return the current time
  return new Date().toISOString()
})
// getServerTime.ts
import { createServerFn } from '@tanstack/react-start'

export const getServerTime = createServerFn().handler(async () => {
  // Wait for 1 second
  await new Promise((resolve) => setTimeout(resolve, 1000))
  // Return the current time
  return new Date().toISOString()
})

配置选项

创建服务器函数时,您可以提供配置选项来自定义其行为

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

export const getData = createServerFn({
  method: 'GET', // HTTP method to use
  response: 'data', // Response handling mode
}).handler(async () => {
  // Function implementation
})
import { createServerFn } from '@tanstack/react-start'

export const getData = createServerFn({
  method: 'GET', // HTTP method to use
  response: 'data', // Response handling mode
}).handler(async () => {
  // Function implementation
})

可用选项

method

指定服务器函数请求的 HTTP 方法

tsx
method?: 'GET' | 'POST'
method?: 'GET' | 'POST'

默认情况下,如果未指定,服务器函数使用 GET

response

控制如何处理和返回响应

tsx
response?: 'data' | 'full' | 'raw'
response?: 'data' | 'full' | 'raw'
  • 'data'(默认):自动解析 JSON 响应并仅返回数据
  • 'full':返回包含结果数据、错误信息和上下文的 response 对象
  • 'raw':直接返回原始 Response 对象,从而启用流式响应和自定义标头

我在哪里可以调用服务器函数?

  • 来自服务器端代码
  • 来自客户端代码
  • 来自其他服务器函数

警告

服务器函数不能从 API 路由调用。 如果需要在服务器函数和 API 路由之间共享业务逻辑,请将共享逻辑提取到实用函数中,以便两者都可以导入。

接受参数

服务器函数接受单个参数,该参数可以是多种类型

  • 标准 JavaScript 类型
    • string
    • number
    • boolean
    • null
    • Array
    • Object
  • FormData
  • ReadableStream(以上任何类型)
  • Promise(以上任何类型)

这是一个服务器函数接受简单字符串参数的示例

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

export const greet = createServerFn({
  method: 'GET',
})
  .validator((data: string) => data)
  .handler(async (ctx) => {
    return `Hello, ${ctx.data}!`
  })

greet({
  data: 'John',
})
import { createServerFn } from '@tanstack/react-start'

export const greet = createServerFn({
  method: 'GET',
})
  .validator((data: string) => data)
  .handler(async (ctx) => {
    return `Hello, ${ctx.data}!`
  })

greet({
  data: 'John',
})

运行时输入验证 / 类型安全

服务器函数可以配置为在运行时验证其输入数据,同时增加类型安全。 这对于确保输入类型正确,然后再执行服务器函数,并提供更友好的错误消息非常有用。

这是通过 validator 方法完成的。 它将接受传递给服务器函数的任何输入。 您从此函数返回的值(和类型)将成为传递给实际服务器函数处理程序的输入。

如果您想使用像 Zod 这样的工具,验证器还可以与外部验证器无缝集成。

基本验证

这是一个验证输入参数的服务器函数的简单示例

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

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown): Person => {
    if (typeof person !== 'object' || person === null) {
      throw new Error('Person must be an object')
    }

    if ('name' in person && typeof person.name !== 'string') {
      throw new Error('Person.name must be a string')
    }

    return person as Person
  })
  .handler(async ({ data }) => {
    return `Hello, ${data.name}!`
  })
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown): Person => {
    if (typeof person !== 'object' || person === null) {
      throw new Error('Person must be an object')
    }

    if ('name' in person && typeof person.name !== 'string') {
      throw new Error('Person.name must be a string')
    }

    return person as Person
  })
  .handler(async ({ data }) => {
    return `Hello, ${data.name}!`
  })

使用验证库

验证库(如 Zod)可以像这样使用

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

import { z } from 'zod'

const Person = z.object({
  name: z.string(),
})

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown) => {
    return Person.parse(person)
  })
  .handler(async (ctx) => {
    return `Hello, ${ctx.data.name}!`
  })

greet({
  data: {
    name: 'John',
  },
})
import { createServerFn } from '@tanstack/react-start'

import { z } from 'zod'

const Person = z.object({
  name: z.string(),
})

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown) => {
    return Person.parse(person)
  })
  .handler(async (ctx) => {
    return `Hello, ${ctx.data.name}!`
  })

greet({
  data: {
    name: 'John',
  },
})

类型安全

由于服务器函数跨越网络边界,因此务必确保传递给它们的数据不仅类型正确,而且在运行时也经过验证。 这在处理用户输入时尤为重要,因为用户输入可能是不可预测的。 为了确保开发人员验证其 I/O 数据,类型依赖于验证。 validator 函数的返回类型将成为服务器函数处理程序的输入。

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

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown): Person => {
    if (typeof person !== 'object' || person === null) {
      throw new Error('Person must be an object')
    }

    if ('name' in person && typeof person.name !== 'string') {
      throw new Error('Person.name must be a string')
    }

    return person as Person
  })
  .handler(
    async ({
      data, // Person
    }) => {
      return `Hello, ${data.name}!`
    },
  )

function test() {
  greet({ data: { name: 'John' } }) // OK
  greet({ data: { name: 123 } }) // Error: Argument of type '{ name: number; }' is not assignable to parameter of type 'Person'.
}
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown): Person => {
    if (typeof person !== 'object' || person === null) {
      throw new Error('Person must be an object')
    }

    if ('name' in person && typeof person.name !== 'string') {
      throw new Error('Person.name must be a string')
    }

    return person as Person
  })
  .handler(
    async ({
      data, // Person
    }) => {
      return `Hello, ${data.name}!`
    },
  )

function test() {
  greet({ data: { name: 'John' } }) // OK
  greet({ data: { name: 123 } }) // Error: Argument of type '{ name: number; }' is not assignable to parameter of type 'Person'.
}

推断

服务器函数分别根据 validator 的输入和 handler 函数的返回值来推断其输入和输出类型。 实际上,您定义的 validator 甚至可以有自己的单独输入/输出类型,如果您validator 对输入数据执行转换,这将非常有用。

为了说明这一点,让我们看一个使用 zod 验证库的示例

tsx
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'

const transactionSchema = z.object({
  amount: z.string().transform((val) => parseInt(val, 10)),
})

const createTransaction = createServerFn()
  .validator(transactionSchema)
  .handler(({ data }) => {
    return data.amount // Returns a number
  })

createTransaction({
  data: {
    amount: '123', // Accepts a string
  },
})
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'

const transactionSchema = z.object({
  amount: z.string().transform((val) => parseInt(val, 10)),
})

const createTransaction = createServerFn()
  .validator(transactionSchema)
  .handler(({ data }) => {
    return data.amount // Returns a number
  })

createTransaction({
  data: {
    amount: '123', // Accepts a string
  },
})

非验证推断

虽然我们强烈建议使用验证库来验证您的网络 I/O 数据,但您可能出于某种原因想验证您的数据,但仍然需要类型安全。 为此,请使用恒等函数作为 validator 为服务器函数提供类型信息,该函数为输入和/或输出设置正确的类型

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

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((d: Person) => d)
  .handler(async (ctx) => {
    return `Hello, ${ctx.data.name}!`
  })

greet({
  data: {
    name: 'John',
  },
})
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((d: Person) => d)
  .handler(async (ctx) => {
    return `Hello, ${ctx.data.name}!`
  })

greet({
  data: {
    name: 'John',
  },
})

JSON 参数

服务器函数可以接受 JSON 可序列化对象作为参数。 这对于将复杂数据结构传递到服务器非常有用

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

type Person = {
  name: string
  age: number
}

export const greet = createServerFn({ method: 'GET' })
  .validator((data: Person) => data)
  .handler(async ({ data }) => {
    return `Hello, ${data.name}! You are ${data.age} years old.`
  })

greet({
  data: {
    name: 'John',
    age: 34,
  },
})
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
  age: number
}

export const greet = createServerFn({ method: 'GET' })
  .validator((data: Person) => data)
  .handler(async ({ data }) => {
    return `Hello, ${data.name}! You are ${data.age} years old.`
  })

greet({
  data: {
    name: 'John',
    age: 34,
  },
})

FormData 参数

服务器函数可以接受 FormData 对象作为参数

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

export const greetUser = createServerFn({ method: 'POST' })
  .validator((data) => {
    if (!(data instanceof FormData)) {
      throw new Error('Invalid form data')
    }
    const name = data.get('name')
    const age = data.get('age')

    if (!name || !age) {
      throw new Error('Name and age are required')
    }

    return {
      name: name.toString(),
      age: parseInt(age.toString(), 10),
    }
  })
  .handler(async ({ data: { name, age } }) => {
    return `Hello, ${name}! You are ${age} years old.`
  })

// Usage
function Test() {
  return (
    <form
      onSubmit={async (event) => {
        event.preventDefault()
        const formData = new FormData(event.currentTarget)
        const response = await greetUser({ data: formData })
        console.log(response)
      }}
    >
      <input name="name" />
      <input name="age" />
      <button type="submit">Submit</button>
    </form>
  )
}
import { createServerFn } from '@tanstack/react-start'

export const greetUser = createServerFn({ method: 'POST' })
  .validator((data) => {
    if (!(data instanceof FormData)) {
      throw new Error('Invalid form data')
    }
    const name = data.get('name')
    const age = data.get('age')

    if (!name || !age) {
      throw new Error('Name and age are required')
    }

    return {
      name: name.toString(),
      age: parseInt(age.toString(), 10),
    }
  })
  .handler(async ({ data: { name, age } }) => {
    return `Hello, ${name}! You are ${age} years old.`
  })

// Usage
function Test() {
  return (
    <form
      onSubmit={async (event) => {
        event.preventDefault()
        const formData = new FormData(event.currentTarget)
        const response = await greetUser({ data: formData })
        console.log(response)
      }}
    >
      <input name="name" />
      <input name="age" />
      <button type="submit">Submit</button>
    </form>
  )
}

服务器函数上下文

除了服务器函数接受的单个参数外,您还可以使用来自 @tanstack/react-start/server 的实用程序从任何服务器函数中访问服务器请求上下文。 在底层,我们使用 Unjsh3 包来执行跨平台 HTTP 请求。

有许多上下文函数可用于以下操作

  • 访问请求上下文
  • 访问/设置标头
  • 访问/设置会话/Cookie
  • 设置响应状态码和状态消息
  • 处理多部分表单数据
  • 读取/设置自定义服务器上下文属性

有关可用上下文函数的完整列表,请参阅所有可用的 h3 方法或检查 @tanstack/react-start/server 源代码

首先,这里有一些示例

访问请求上下文

让我们使用 getWebRequest 函数从服务器函数内部访问请求本身

tsx
import { createServerFn } from '@tanstack/react-start'
import { getWebRequest } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    const request = getWebRequest()

    console.log(request.method) // GET

    console.log(request.headers.get('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
  },
)
import { createServerFn } from '@tanstack/react-start'
import { getWebRequest } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    const request = getWebRequest()

    console.log(request.method) // GET

    console.log(request.headers.get('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
  },
)

访问请求头

使用 getHeaders 函数从服务器函数内部访问所有标头

tsx
import { createServerFn } from '@tanstack/react-start'
import { getHeaders } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    console.log(getHeaders())
    // {
    //   "accept": "*/*",
    //   "accept-encoding": "gzip, deflate, br",
    //   "accept-language": "en-US,en;q=0.9",
    //   "connection": "keep-alive",
    //   "host": "localhost:3000",
    //   ...
    // }
  },
)
import { createServerFn } from '@tanstack/react-start'
import { getHeaders } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    console.log(getHeaders())
    // {
    //   "accept": "*/*",
    //   "accept-encoding": "gzip, deflate, br",
    //   "accept-language": "en-US,en;q=0.9",
    //   "connection": "keep-alive",
    //   "host": "localhost:3000",
    //   ...
    // }
  },
)

您还可以使用 getHeader 函数访问单个标头

tsx
import { createServerFn } from '@tanstack/react-start'
import { getHeader } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    console.log(getHeader('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
  },
)
import { createServerFn } from '@tanstack/react-start'
import { getHeader } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    console.log(getHeader('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
  },
)

返回值

服务器函数可以返回几种不同类型的值

  • 原始类型
  • JSON 可序列化对象
  • redirect 错误(也可以抛出)
  • notFound 错误(也可以抛出)
  • 原始 Response 对象

返回原始类型和 JSON

要返回任何原始类型或 JSON 可序列化对象,只需从服务器函数返回值即可

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

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    return new Date().toISOString()
  },
)

export const getServerData = createServerFn({ method: 'GET' }).handler(
  async () => {
    return {
      message: 'Hello, World!',
    }
  },
)
import { createServerFn } from '@tanstack/react-start'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    return new Date().toISOString()
  },
)

export const getServerData = createServerFn({ method: 'GET' }).handler(
  async () => {
    return {
      message: 'Hello, World!',
    }
  },
)

默认情况下,服务器函数假定返回的任何非 Response 对象要么是原始类型,要么是 JSON 可序列化对象。

返回自定义响应头

要返回自定义响应头,您可以使用 setHeader 函数

tsx
import { createServerFn } from '@tanstack/react-start'
import { setHeader } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    setHeader('X-Custom-Header', 'value')
    return new Date().toISOString()
  },
)
import { createServerFn } from '@tanstack/react-start'
import { setHeader } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    setHeader('X-Custom-Header', 'value')
    return new Date().toISOString()
  },
)

返回自定义状态码

要返回自定义状态码,您可以使用 setResponseStatus 函数

tsx
import { createServerFn } from '@tanstack/react-start'
import { setResponseStatus } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    setResponseStatus(201)
    return new Date().toISOString()
  },
)
import { createServerFn } from '@tanstack/react-start'
import { setResponseStatus } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    setResponseStatus(201)
    return new Date().toISOString()
  },
)

返回原始 Response 对象

要返回原始 Response 对象,请从服务器函数返回 Response 对象并设置 response: 'raw'

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

export const getServerTime = createServerFn({
  method: 'GET',
  response: 'raw',
}).handler(async () => {
  // Read a file from s3
  return fetch('https://example.com/time.txt')
})
import { createServerFn } from '@tanstack/react-start'

export const getServerTime = createServerFn({
  method: 'GET',
  response: 'raw',
}).handler(async () => {
  // Read a file from s3
  return fetch('https://example.com/time.txt')
})

response: 'raw' 选项还允许流式响应以及其他功能

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

export const streamEvents = createServerFn({
  method: 'GET',
  response: 'raw',
}).handler(async ({ signal }) => {
  // Create a ReadableStream to send chunks of data
  const stream = new ReadableStream({
    async start(controller) {
      // Send initial response immediately
      controller.enqueue(new TextEncoder().encode('Connection established\n'))

      let count = 0
      const interval = setInterval(() => {
        // Check if the client disconnected
        if (signal.aborted) {
          clearInterval(interval)
          controller.close()
          return
        }

        // Send a data chunk
        controller.enqueue(
          new TextEncoder().encode(
            `Event ${++count}: ${new Date().toISOString()}\n`,
          ),
        )

        // End after 10 events
        if (count >= 10) {
          clearInterval(interval)
          controller.close()
        }
      }, 1000)

      // Ensure we clean up if the request is aborted
      signal.addEventListener('abort', () => {
        clearInterval(interval)
        controller.close()
      })
    },
  })

  // Return a streaming response
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  })
})
import { createServerFn } from '@tanstack/react-start'

export const streamEvents = createServerFn({
  method: 'GET',
  response: 'raw',
}).handler(async ({ signal }) => {
  // Create a ReadableStream to send chunks of data
  const stream = new ReadableStream({
    async start(controller) {
      // Send initial response immediately
      controller.enqueue(new TextEncoder().encode('Connection established\n'))

      let count = 0
      const interval = setInterval(() => {
        // Check if the client disconnected
        if (signal.aborted) {
          clearInterval(interval)
          controller.close()
          return
        }

        // Send a data chunk
        controller.enqueue(
          new TextEncoder().encode(
            `Event ${++count}: ${new Date().toISOString()}\n`,
          ),
        )

        // End after 10 events
        if (count >= 10) {
          clearInterval(interval)
          controller.close()
        }
      }, 1000)

      // Ensure we clean up if the request is aborted
      signal.addEventListener('abort', () => {
        clearInterval(interval)
        controller.close()
      })
    },
  })

  // Return a streaming response
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  })
})

response: 'raw' 选项对于以下情况尤其有用

  • 数据以增量方式发送的流式 API
  • 服务器发送事件
  • 长轮询响应
  • 自定义内容类型和二进制数据

抛出错误

除了特殊的 redirectnotFound 错误外,服务器函数还可以抛出任何自定义错误。 这些错误将被序列化并作为 JSON 响应以及 500 状态码发送到客户端。

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

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  throw new Error('Something went wrong!')
})

// Usage
function Test() {
  try {
    await doStuff()
  } catch (error) {
    console.error(error)
    // {
    //   message: "Something went wrong!",
    //   stack: "Error: Something went wrong!\n    at doStuff (file:///path/to/file.ts:3:3)"
    // }
  }
}
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  throw new Error('Something went wrong!')
})

// Usage
function Test() {
  try {
    await doStuff()
  } catch (error) {
    console.error(error)
    // {
    //   message: "Something went wrong!",
    //   stack: "Error: Something went wrong!\n    at doStuff (file:///path/to/file.ts:3:3)"
    // }
  }
}

取消

在客户端,服务器函数调用可以通过 AbortSignal 取消。 在服务器上,如果请求在执行完成之前关闭,AbortSignal 将发出通知。

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

export const abortableServerFn = createServerFn().handler(
  async ({ signal }) => {
    return new Promise<string>((resolve, reject) => {
      if (signal.aborted) {
        return reject(new Error('Aborted before start'))
      }
      const timerId = setTimeout(() => {
        console.log('server function finished')
        resolve('server function result')
      }, 1000)
      const onAbort = () => {
        clearTimeout(timerId)
        console.log('server function aborted')
        reject(new Error('Aborted'))
      }
      signal.addEventListener('abort', onAbort, { once: true })
    })
  },
)

// Usage
function Test() {
  const controller = new AbortController()
  const serverFnPromise = abortableServerFn({
    signal: controller.signal,
  })
  await new Promise((resolve) => setTimeout(resolve, 500))
  controller.abort()
  try {
    const serverFnResult = await serverFnPromise
    console.log(serverFnResult) // should never get here
  } catch (error) {
    console.error(error) // "signal is aborted without reason"
  }
}
import { createServerFn } from '@tanstack/react-start'

export const abortableServerFn = createServerFn().handler(
  async ({ signal }) => {
    return new Promise<string>((resolve, reject) => {
      if (signal.aborted) {
        return reject(new Error('Aborted before start'))
      }
      const timerId = setTimeout(() => {
        console.log('server function finished')
        resolve('server function result')
      }, 1000)
      const onAbort = () => {
        clearTimeout(timerId)
        console.log('server function aborted')
        reject(new Error('Aborted'))
      }
      signal.addEventListener('abort', onAbort, { once: true })
    })
  },
)

// Usage
function Test() {
  const controller = new AbortController()
  const serverFnPromise = abortableServerFn({
    signal: controller.signal,
  })
  await new Promise((resolve) => setTimeout(resolve, 500))
  controller.abort()
  try {
    const serverFnResult = await serverFnPromise
    console.log(serverFnResult) // should never get here
  } catch (error) {
    console.error(error) // "signal is aborted without reason"
  }
}

从路由生命周期内调用服务器函数

服务器函数可以从路由 loaderbeforeLoad 或任何其他路由器控制的 API 中正常调用。 这些 API 配备为自动处理服务器函数抛出的错误、重定向和 notFound。

tsx
import { getServerTime } from './getServerTime'

export const Route = createFileRoute('/time')({
  loader: async () => {
    const time = await getServerTime()

    return {
      time,
    }
  },
})
import { getServerTime } from './getServerTime'

export const Route = createFileRoute('/time')({
  loader: async () => {
    const time = await getServerTime()

    return {
      time,
    }
  },
})

从 Hook 和组件中调用服务器函数

服务器函数可以抛出 redirectnotFound,虽然不是必需的,但建议捕获这些错误并适当处理它们。 为了使这更容易,@tanstack/react-start 包导出了一个 useServerFn Hook,可用于将服务器函数绑定到组件和 Hook

tsx
import { useServerFn } from '@tanstack/react-start'
import { useQuery } from '@tanstack/react-query'
import { getServerTime } from './getServerTime'

export function Time() {
  const getTime = useServerFn(getServerTime)

  const timeQuery = useQuery({
    queryKey: 'time',
    queryFn: () => getTime(),
  })
}
import { useServerFn } from '@tanstack/react-start'
import { useQuery } from '@tanstack/react-query'
import { getServerTime } from './getServerTime'

export function Time() {
  const getTime = useServerFn(getServerTime)

  const timeQuery = useQuery({
    queryKey: 'time',
    queryFn: () => getTime(),
  })
}

在任何其他地方调用服务器函数

使用服务器函数时,请注意它们抛出的重定向和 notFound 仅在从以下位置调用时才会被自动处理

  • 路由生命周期
  • 使用 useServerFn Hook 的组件

对于其他使用位置,您需要手动处理这些情况。

重定向

服务器函数可以抛出 redirect 错误,以将用户重定向到不同的 URL。 这对于处理身份验证、授权或需要将用户重定向到不同页面的其他场景非常有用。

  • 在 SSR 期间,重定向通过向客户端发送包含新位置的 302 响应来处理
  • 在客户端,重定向由路由器从路由生命周期或使用 useServerFn Hook 的组件内自动处理。 如果您从任何其他地方调用服务器函数,则重定向将不会被自动处理。

要抛出重定向,您可以使用从 @tanstack/react-router 包导出的 redirect 函数

tsx
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page
  throw redirect({
    to: '/',
  })
})
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page
  throw redirect({
    to: '/',
  })
})

重定向可以利用与 router.navigateuseNavigate()<Link> 组件相同的所有选项。 因此,也可以随意传递

  • 路径参数
  • 搜索参数
  • 哈希值

重定向还可以通过传递 status 选项来设置响应的状态码

tsx
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page with a 301 status code
  throw redirect({
    to: '/',
    status: 301,
  })
})
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page with a 301 status code
  throw redirect({
    to: '/',
    status: 301,
  })
})

您还可以使用 href 重定向到外部目标

tsx
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const auth = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the auth provider
  throw redirect({
    href: 'https://authprovider.com/login',
  })
})
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const auth = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the auth provider
  throw redirect({
    href: 'https://authprovider.com/login',
  })
})

⚠️ 请勿使用 @tanstack/react-start/serversendRedirect 函数从服务器函数内部发送软重定向。 这将使用 Location 标头发送重定向,并将强制客户端进行完整页面硬导航。

重定向响应头

您还可以通过传递 headers 选项在重定向上设置自定义标头

tsx
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page with a custom header
  throw redirect({
    to: '/',
    headers: {
      'X-Custom-Header': 'value',
    },
  })
})
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page with a custom header
  throw redirect({
    to: '/',
    headers: {
      'X-Custom-Header': 'value',
    },
  })
})

未找到

当从 loaderbeforeLoad 路由生命周期调用服务器函数时,可以抛出一个特殊的 notFound 错误,以向路由器指示请求的资源未找到。 这比简单的 404 状态码更有用,因为它允许您渲染自定义 404 页面,或以自定义方式处理错误。 如果从路由生命周期之外使用的服务器函数中抛出 notFound,则不会自动处理。

要抛出 notFound,您可以使用从 @tanstack/react-router 包导出的 notFound 函数

tsx
import { notFound } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const getStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Randomly return a not found error
  if (Math.random() < 0.5) {
    throw notFound()
  }

  // Or return some stuff
  return {
    stuff: 'stuff',
  }
})

export const Route = createFileRoute('/stuff')({
  loader: async () => {
    const stuff = await getStuff()

    return {
      stuff,
    }
  },
})
import { notFound } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const getStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Randomly return a not found error
  if (Math.random() < 0.5) {
    throw notFound()
  }

  // Or return some stuff
  return {
    stuff: 'stuff',
  }
})

export const Route = createFileRoute('/stuff')({
  loader: async () => {
    const stuff = await getStuff()

    return {
      stuff,
    }
  },
})

NotFound 错误是 TanStack Router 的核心功能,

错误处理

如果服务器函数抛出(非重定向/非 notFound)错误,它将被序列化并作为 JSON 响应以及 500 状态码发送到客户端。 这对于调试很有用,但您可能希望以更用户友好的方式处理这些错误。 您可以通过捕获错误并在您的路由生命周期、组件或 Hook 中像往常一样处理它来实现。

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

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  undefined.foo()
})

export const Route = createFileRoute('/stuff')({
  loader: async () => {
    try {
      await doStuff()
    } catch (error) {
      // Handle the error:
      // error === {
      //   message: "Cannot read property 'foo' of undefined",
      //   stack: "TypeError: Cannot read property 'foo' of undefined\n    at doStuff (file:///path/to/file.ts:3:3)"
    }
  },
})
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  undefined.foo()
})

export const Route = createFileRoute('/stuff')({
  loader: async () => {
    try {
      await doStuff()
    } catch (error) {
      // Handle the error:
      // error === {
      //   message: "Cannot read property 'foo' of undefined",
      //   stack: "TypeError: Cannot read property 'foo' of undefined\n    at doStuff (file:///path/to/file.ts:3:3)"
    }
  },
})

No-JS 服务器函数

在禁用 JavaScript 的情况下,只有一种执行服务器函数的方法:通过提交表单。

这是通过向页面添加 form 元素并使用 HTML 属性 action 完成的。

请注意,我们提到了 HTML 属性 action。 与所有其他属性一样,此属性在 HTML 中仅接受字符串。

虽然 React 19 添加了对将函数传递给 action 的支持,但这只是 React 特有的功能,而不是 HTML 标准的一部分。

action 属性告诉浏览器在提交表单时将表单数据发送到哪里。 在这种情况下,我们希望将表单数据发送到服务器函数。

为此,我们可以利用服务器函数的 url 属性

ts
const yourFn = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const name = formData.get('name')

    if (!name) {
      throw new Error('Name is required')
    }

    return name
  })
  .handler(async ({ data: name }) => {
    console.log(name) // 'John'
  })

console.info(yourFn.url)
const yourFn = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const name = formData.get('name')

    if (!name) {
      throw new Error('Name is required')
    }

    return name
  })
  .handler(async ({ data: name }) => {
    console.log(name) // 'John'
  })

console.info(yourFn.url)

并将其传递给表单的 action 属性

tsx
function Component() {
  return (
    <form action={yourFn.url} method="POST">
      <input name="name" defaultValue="John" />
      <button type="submit">Click me!</button>
    </form>
  )
}
function Component() {
  return (
    <form action={yourFn.url} method="POST">
      <input name="name" defaultValue="John" />
      <button type="submit">Click me!</button>
    </form>
  )
}

提交表单时,将执行服务器函数。

No-JS 服务器函数参数

要在提交表单时将参数传递给服务器函数,您可以使用带有 name 属性的 input 元素,将参数附加到传递给服务器函数的 FormData

tsx
const yourFn = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const age = formData.get('age')

    if (!age) {
      throw new Error('age is required')
    }

    return age.toString()
  })
  .handler(async ({ data: formData }) => {
    // `age` will be '123'
    const age = formData.get('age')
    // ...
  })

function Component() {
  return (
    //  We need to tell the server that our data type is `multipart/form-data` by setting the `encType` attribute on the form.
    <form action={yourFn.url} method="POST" encType="multipart/form-data">
      <input name="age" defaultValue="34" />
      <button type="submit">Click me!</button>
    </form>
  )
}
const yourFn = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const age = formData.get('age')

    if (!age) {
      throw new Error('age is required')
    }

    return age.toString()
  })
  .handler(async ({ data: formData }) => {
    // `age` will be '123'
    const age = formData.get('age')
    // ...
  })

function Component() {
  return (
    //  We need to tell the server that our data type is `multipart/form-data` by setting the `encType` attribute on the form.
    <form action={yourFn.url} method="POST" encType="multipart/form-data">
      <input name="age" defaultValue="34" />
      <button type="submit">Click me!</button>
    </form>
  )
}

提交表单时,将使用表单的数据作为参数来执行服务器函数。

No-JS 服务器函数返回值

无论是否启用 JavaScript,服务器函数都将向客户端发出的 HTTP 请求返回响应。

启用 JavaScript 后,此响应可以作为客户端 JavaScript 代码中服务器函数的返回值访问。

ts
const yourFn = createServerFn().handler(async () => {
  return 'Hello, world!'
})

// `.then` is not available when JavaScript is disabled
yourFn().then(console.log)
const yourFn = createServerFn().handler(async () => {
  return 'Hello, world!'
})

// `.then` is not available when JavaScript is disabled
yourFn().then(console.log)

但是,当 JavaScript 被禁用时,无法在客户端的 JavaScript 代码中访问服务器函数的返回值。

相反,服务器函数可以向客户端提供响应,告诉浏览器以某种方式导航。

当与来自 TanStack Router 的 loader 结合使用时,即使在禁用 JavaScript 的情况下,我们也能够提供类似于单页应用程序的体验;所有这些都是通过告诉浏览器使用通过 loader 管道传输的新数据重新加载当前页面来实现的

tsx
import * as fs from 'fs'
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const filePath = 'count.txt'

async function readCount() {
  return parseInt(
    await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
  )
}

const getCount = createServerFn({
  method: 'GET',
}).handler(() => {
  return readCount()
})

const updateCount = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const addBy = formData.get('addBy')

    if (!addBy) {
      throw new Error('addBy is required')
    }

    return parseInt(addBy.toString())
  })
  .handler(async ({ data: addByAmount }) => {
    const count = await readCount()
    await fs.promises.writeFile(filePath, `${count + addByAmount}`)
    // Reload the page to trigger the loader again
    return new Response('ok', { status: 301, headers: { Location: '/' } })
  })

export const Route = createFileRoute('/')({
  component: Home,
  loader: async () => await getCount(),
})

function Home() {
  const state = Route.useLoaderData()

  return (
    <div>
      <form
        action={updateCount.url}
        method="POST"
        encType="multipart/form-data"
      >
        <input type="number" name="addBy" defaultValue="1" />
        <button type="submit">Add</button>
      </form>
      <pre>{state}</pre>
    </div>
  )
}
import * as fs from 'fs'
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const filePath = 'count.txt'

async function readCount() {
  return parseInt(
    await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
  )
}

const getCount = createServerFn({
  method: 'GET',
}).handler(() => {
  return readCount()
})

const updateCount = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const addBy = formData.get('addBy')

    if (!addBy) {
      throw new Error('addBy is required')
    }

    return parseInt(addBy.toString())
  })
  .handler(async ({ data: addByAmount }) => {
    const count = await readCount()
    await fs.promises.writeFile(filePath, `${count + addByAmount}`)
    // Reload the page to trigger the loader again
    return new Response('ok', { status: 301, headers: { Location: '/' } })
  })

export const Route = createFileRoute('/')({
  component: Home,
  loader: async () => await getCount(),
})

function Home() {
  const state = Route.useLoaderData()

  return (
    <div>
      <form
        action={updateCount.url}
        method="POST"
        encType="multipart/form-data"
      >
        <input type="number" name="addBy" defaultValue="1" />
        <button type="submit">Add</button>
      </form>
      <pre>{state}</pre>
    </div>
  )
}

静态服务器函数

当使用预渲染/静态生成时,服务器函数也可以是“静态的”,这使其结果可以在构建时缓存并作为静态资源提供。

静态服务器函数页面上了解有关此模式的所有信息。

服务器函数是如何编译的?

在底层,服务器函数从客户端包中提取出来并放入单独的服务器包中。 在服务器上,它们按原样执行,结果将发送回客户端。 在客户端上,服务器函数将请求代理到服务器,服务器执行该函数并将结果发送回客户端,所有这些都通过 fetch 完成。

流程如下所示

  • 当在文件中找到 createServerFn 时,将检查内部函数是否具有 use server 指令
  • 如果缺少 use server 指令,则将其添加到函数顶部
  • 在客户端上,内部函数从客户端包中提取出来并放入单独的服务器包中
  • 客户端服务器函数将替换为代理函数,该代理函数向服务器发送请求以执行被提取的函数
  • 在服务器上,服务器函数未被提取,并按原样执行
  • 提取发生后,每个包都应用死代码消除过程,以从每个包中删除任何未使用的代码。
订阅 Bytes

您每周的 JavaScript 新闻。 每周一免费发送给超过 10 万名开发者。

Bytes

没有垃圾邮件。 随时取消订阅。