服务器功能允许您指定几乎可以在任何地方(甚至客户端)调用但**只**在服务器上运行的逻辑。事实上,它们与 API 路由并无太大区别,但有一些关键差异
然而,它们与常规 API 路由类似,因为
服务器功能与“React 服务器功能”有何不同?
- TanStack 服务器功能不与特定的前端框架绑定,可以与任何前端框架或完全不使用任何框架。
- TanStack 服务器功能由标准 HTTP 请求支持,可以根据需要频繁调用,而不会出现串行执行瓶颈。
服务器功能可以在应用程序中的任何地方定义,但必须在文件的顶层定义。它们可以在整个应用程序中调用,包括加载器、钩子等。传统上,这种模式被称为远程过程调用 (RPC),但由于这些函数的同构性,我们将它们称为服务器功能。
服务器功能可以使用中间件来共享逻辑、上下文、通用操作、先决条件等等。要了解有关服务器功能中间件的更多信息,请务必阅读中间件指南中的内容。
我们要感谢 tRPC 团队,感谢他们对 TanStack Start 服务器功能设计的启发和实施过程中的指导。我们非常喜欢(并推荐)将 tRPC 用于 API 路由,以至于我们坚持服务器功能也获得相同的顶级待遇和开发体验。谢谢!
服务器功能通过 @tanstack/react-start 包中的 createServerFn 函数定义。此函数接受一个可选的 options 参数,用于指定 HTTP 方法和响应类型等配置,并允许您在结果上进行链式操作,以定义服务器函数的主体、输入验证、中间件等。这是一个简单示例
// 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()
})
创建服务器功能时,您可以提供配置选项来自定义其行为
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 方法
method?: 'GET' | 'POST'
method?: 'GET' | 'POST'
默认情况下,如果未指定,服务器功能使用 GET。
response
控制响应的处理和返回方式
response?: 'data' | 'full' | 'raw'
response?: 'data' | 'full' | 'raw'
服务器功能接受一个单一参数,该参数可以是多种类型
这是一个接受简单字符串参数的服务器功能示例
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 等外部验证器,验证器也可以与它们无缝集成。
这是一个验证输入参数的服务器功能简单示例
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 等验证库可以这样使用
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 函数的返回类型将是服务器功能处理程序的输入。
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 甚至可以有自己独立的输入/输出类型,如果您的验证器对输入数据执行转换,这会很有用。
为了说明这一点,让我们看一个使用 zod 验证库的示例
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,为服务器功能提供类型信息,该标识函数将输入和/或输出类型化为正确的类型
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 序列化对象作为参数。这对于向服务器传递复杂数据结构很有用
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 对象作为参数
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 中的实用程序从任何服务器功能内部访问服务器请求上下文。在底层,我们使用 Unjs 的 h3 包来执行跨平台 HTTP 请求。
有许多可用的上下文功能可供您使用,例如
有关可用上下文功能的完整列表,请参阅所有可用的 h3 方法或检查 @tanstack/start-server-core 源代码。
首先,这里有一些示例
让我们使用 getWebRequest 函数从服务器功能内部访问请求本身
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 函数从服务器功能内部访问所有请求头
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 函数访问单个请求头
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 序列化对象,只需从服务器功能返回该值
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 函数
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 函数
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: 'raw'
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' 选项还允许流式响应等功能
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' 选项对于以下情况特别有用
除了特殊的 redirect 和 notFound 错误之外,服务器功能可以抛出任何自定义错误。这些错误将被序列化并作为 JSON 响应以及 500 状态码发送到客户端。
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 将发出通知。
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"
}
}
服务器功能可以从路由 loader、beforeLoad 或任何其他路由器控制的 API 正常调用。这些 API 能够自动处理服务器功能抛出的错误、重定向和 notFounds。
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/react-start 包导出一个 useServerFn 钩子,可用于将服务器功能绑定到组件和钩子
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(),
})
}
使用服务器功能时,请注意它们抛出的重定向和 notFounds 仅在以下情况自动处理
对于其他使用位置,您需要手动处理这些情况。
服务器功能可以抛出 redirect 错误以将用户重定向到不同的 URL。这对于处理身份验证、授权或其他需要将用户重定向到不同页面的场景非常有用。
要抛出重定向,您可以使用 @tanstack/react-router 包导出的 redirect 函数
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.navigate、useNavigate() 和 <Link> 组件相同的所有选项。因此,也可以随意传递
重定向还可以通过传递 status 选项来设置响应的状态码
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 重定向到外部目标
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/server 的 sendRedirect 函数从服务器功能内部发送软重定向。这会使用 Location 请求头发送重定向,并强制客户端进行完整的页面硬导航。
您还可以通过传递 headers 选项来设置重定向的自定义请求头
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',
},
})
})
在从 loader 或 beforeLoad 路由生命周期调用服务器功能时,可以抛出特殊的 notFound 错误,以向路由器指示请求的资源未找到。这比简单的 404 状态码更有用,因为它允许您渲染自定义 404 页面或以自定义方式处理错误。如果从路由生命周期之外使用的服务器功能抛出 notFound,它将不会自动处理。
要抛出 notFound,您可以使用 @tanstack/react-router 包导出的 notFound 函数
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,
}
},
})
未找到错误是 TanStack Router 的核心功能,
如果服务器功能抛出(非重定向/非 notFound)错误,它将被序列化并作为 JSON 响应以及 500 状态码发送到客户端。这对于调试很有用,但您可能希望以更用户友好的方式处理这些错误。您可以通过捕获错误并像往常一样在路由生命周期、组件或钩子中处理它来做到这一点。
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)"
}
},
})
在没有启用 JavaScript 的情况下,执行服务器功能的唯一方法是通过提交表单。
这是通过在页面上添加一个带有 HTML 属性 action 的 form 元素来完成的。
请注意,我们提到了 **HTML** 属性 action。此属性在 HTML 中只接受字符串,就像所有其他属性一样。
虽然 React 19 添加了对将函数传递给 action 的支持,但它是一个 React 特定的功能,不属于 HTML 标准。
action 属性告诉浏览器在提交表单时将表单数据发送到何处。在这种情况下,我们希望将表单数据发送到服务器功能。
为此,我们可以利用服务器功能的 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)
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 属性
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>
)
}
提交表单时,将执行服务器功能。
要在提交表单时将参数传递给服务器功能,您可以使用带有 name 属性的 input 元素,将参数附加到传递给服务器功能的 FormData
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,服务器功能都将向客户端发出的 HTTP 请求返回响应。
启用 JavaScript 时,此响应可以作为客户端 JavaScript 代码中服务器功能的返回值进行访问。
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 传输的新数据重新加载当前页面
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 完成。
过程如下
您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。