服务器函数

什么是服务器函数?

服务器函数允许您指定几乎可以在任何地方(甚至客户端)调用的逻辑,但 **仅** 在服务器上运行。事实上,它们与 API 路由没有太大区别,但有几个关键区别

  • 它们没有稳定的公共 URL。
  • 它们可以从应用程序中的任何位置调用,包括加载器、钩子、组件、服务器路由等。

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

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

服务器函数与“Solid 服务器函数”有何不同?

  • TanStack 服务器函数不与特定前端框架绑定,并且可以与任何前端框架或不使用任何框架一起使用。
  • TanStack 服务器函数由标准的 HTTP 请求支持,并且可以随意调用,而不会遭受串行执行瓶颈。

它们是如何工作的?

服务器函数可以定义在应用程序的任何位置,但必须定义在文件的顶层。它们可以从应用程序的任何地方调用,包括加载器、钩子等。传统上,这种模式称为远程过程调用(RPC),但由于这些函数的同构性质,我们称它们为服务器函数。

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

服务器函数中间件

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

定义服务器函数

我们感谢 tRPC 团队提供的灵感,为 TanStack Start 的服务器函数设计和实现提供了指导。我们非常喜欢(并推荐)在 API 路由中使用 tRPC,以至于我们坚持让服务器函数获得同等的头等舱待遇和开发体验。谢谢!

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

tsx
// getServerTime.ts
import { createServerFn } from '@tanstack/solid-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/solid-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/solid-start'

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

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

可用选项

方法

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

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

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

响应

控制响应如何被处理和返回

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

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

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

接收参数

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

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

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

tsx
import { createServerFn } from '@tanstack/solid-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/solid-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/solid-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/solid-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/solid-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/solid-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/solid-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/solid-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` 甚至可以有自己的单独的输入/输出类型,如果您的验证器对输入数据执行转换,这将非常有用。

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

tsx
import { createServerFn } from '@tanstack/solid-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/solid-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/solid-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/solid-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/solid-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/solid-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/solid-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/solid-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/solid-start/server` 的实用程序在任何服务器函数内部访问服务器请求上下文。在底层,我们使用 Unjs 的 `h3` 包来执行跨平台 HTTP 请求。

您可以使用许多上下文函数来实现诸如以下功能:

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

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

首先,这里有一些例子

访问请求上下文

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

tsx
import { createServerFn } from '@tanstack/solid-start'
import { getWebRequest } from '@tanstack/solid-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/solid-start'
import { getWebRequest } from '@tanstack/solid-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/solid-start'
import { getHeaders } from '@tanstack/solid-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/solid-start'
import { getHeaders } from '@tanstack/solid-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/solid-start'
import { getHeader } from '@tanstack/solid-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/solid-start'
import { getHeader } from '@tanstack/solid-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` 错误(也可以抛出)
  • 原始响应对象

返回原始值和 JSON

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

tsx
import { createServerFn } from '@tanstack/solid-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/solid-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!',
    }
  },
)

默认情况下,服务器函数假定任何非响应对象返回的都是原始值或 JSON 可序列化对象。

使用自定义响应头进行响应

要使用自定义响应头进行响应,您可以使用 `setHeader` 函数

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

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    setHeader('X-Custom-Header', 'value')
    return new Date().toISOString()
  },
)
import { createServerFn } from '@tanstack/solid-start'
import { setHeader } from '@tanstack/solid-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/solid-start'
import { setResponseStatus } from '@tanstack/solid-start/server'

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

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

返回原始响应对象

要返回原始响应对象,请从服务器函数返回一个响应对象,并将 `response: 'raw'` 设置为

tsx
import { createServerFn } from '@tanstack/solid-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/solid-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/solid-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/solid-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,其中数据是增量发送的
  • 服务器发送事件
  • 长轮询响应
  • 自定义内容类型和二进制数据

抛出错误

除了特殊的 `redirect` 和 `notFound` 错误之外,服务器函数还可以抛出任何自定义错误。这些错误将被序列化并作为 JSON 响应发送到客户端,同时带有 500 状态码。这对于调试很有用,但您可能希望以更用户友好的方式处理这些错误。

tsx
import { createServerFn } from '@tanstack/solid-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/solid-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/solid-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/solid-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"
  }
}

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

服务器函数可以像往常一样从路由 `loader`、`beforeLoad` 或任何其他路由控制的 API 中调用。这些 API 能够自动处理服务器函数抛出的错误、重定向和未找到。

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

从钩子和组件调用服务器函数

服务器函数可以抛出 `redirect` 或 `notFound` 错误,虽然不是必需的,但建议捕获这些错误并进行适当处理。为了更轻松地做到这一点,`@tanstack/solid-start` 包导出一个 `useServerFn` 钩子,可用于将服务器函数绑定到组件和钩子。

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

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

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

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

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

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

使用服务器函数时,请注意,只有在从以下位置调用时,重定向和未找到错误才能自动处理:

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

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

重定向

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

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

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

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

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

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

重定向可以利用与 `router.navigate`、`useNavigate()` 和 `` 组件相同的选项。因此,请随意传递

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

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

tsx
import { redirect } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-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/solid-router'
import { createServerFn } from '@tanstack/solid-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/solid-router'
import { createServerFn } from '@tanstack/solid-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/solid-router'
import { createServerFn } from '@tanstack/solid-start'

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

⚠️ 请勿使用 `@tanstack/solid-start/server` 的 `sendRedirect` 函数从服务器函数中发送软重定向。这将使用 `Location` 头发送重定向,并强制客户端进行完全页面硬导航。

重定向头

您还可以通过传递 `headers` 选项来为重定向设置自定义请求头。

tsx
import { redirect } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-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/solid-router'
import { createServerFn } from '@tanstack/solid-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',
    },
  })
})

未找到

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

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

tsx
import { notFound } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-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/solid-router'
import { createServerFn } from '@tanstack/solid-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,
    }
  },
})

未找到错误是 TanStack Router 的核心功能。

错误处理

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

tsx
import { createServerFn } from '@tanstack/solid-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/solid-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)"
    }
  },
})

无 JavaScript 的服务器函数

在没有启用 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>
  )
}

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

无 JavaScript 的服务器函数参数

要使用表单提交向服务器函数传递参数,您可以使用带有 `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>
  )
}

当表单提交时,服务器函数将以表单数据作为参数执行。

无 JavaScript 的服务器函数返回值

无论是否启用 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/solid-router'
import { createServerFn } from '@tanstack/solid-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/solid-router'
import { createServerFn } from '@tanstack/solid-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` 指令,则会在函数顶部添加。
  • 在客户端,内部函数将从客户端捆绑包中提取到一个单独的服务器捆绑包中。
  • 客户端服务器函数被替换为一个代理函数,该函数向服务器发送请求以执行已提取的函数。
  • 在服务器上,服务器函数不会被提取,而是按原样执行。
  • 提取发生后,每个捆绑包都会应用死代码消除过程,以从每个捆绑包中删除任何未使用的代码。
我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
Prisma
订阅 Bytes

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

Bytes

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

订阅 Bytes

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

Bytes

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