译零 TransZero · TanStack Query 中文文档
主站

服务端渲染(SSR)

更新于 2026-11-02 Vue 查看英文原文 ↗

Vue Query 支持在服务端预取多个查询,然后将这些查询 脱水(dehydrate)到 queryClient。这意味着服务端可以预先渲染出页面标记(markup),使内容在页面加载时立即可用;而一旦 JavaScript 加载完成,Vue Query 即可对这些查询进行升级或 注水(hydrate),赋予其完整的库功能。这包括:若查询自服务端渲染后已变为过期,则在客户端重新拉取该查询。

使用 Nuxt.js

Nuxt 3

首先,在 plugins 目录下创建 vue-query.ts 文件,内容如下:

TypeScript
1import type {2  DehydratedState,3  VueQueryPluginOptions,4} from '@tanstack/vue-query'5import {6  VueQueryPlugin,7  QueryClient,8  hydrate,9  dehydrate,10} from '@tanstack/vue-query'11// Nuxt 3 app aliases12import { defineNuxtPlugin, useState } from '#imports'13 14export default defineNuxtPlugin((nuxt) => {15  const vueQueryState = useState<DehydratedState | null>('vue-query')16 17  // Modify your Vue Query global settings here18  const queryClient = new QueryClient({19    defaultOptions: { queries: { staleTime: 5 * 1000 } },20  })21  const options: VueQueryPluginOptions = { queryClient }22 23  nuxt.vueApp.use(VueQueryPlugin, options)24 25  if (import.meta.server) {26    nuxt.hooks.hook('app:rendered', () => {27      vueQueryState.value = dehydrate(queryClient)28    })29  }30 31  if (import.meta.client && vueQueryState.value !== null) {32    hydrate(queryClient, vueQueryState.value)33  }34})

现在你就可以在页面中使用 onServerPrefetch 预取数据了。

  • 使用 queryClient.query、queryClient.infiniteQuery 或 suspense 预取所有你需要的查询
TypeScript
1export default defineComponent({2  setup() {3    const { data, suspense } = useQuery({4      queryKey: ['test'],5      queryFn: fetcher,6    })7 8    onServerPrefetch(async () => {9      await suspense()10    })11 12    return { data }13  },14})

Nuxt 2

首先,在 plugins 目录下创建 vue-query.js 文件,内容如下:

JavaScript
1import Vue from 'vue'2import { VueQueryPlugin, QueryClient, hydrate } from '@tanstack/vue-query'3 4export default (context) => {5  // Modify your Vue Query global settings here6  const queryClient = new QueryClient({7    defaultOptions: { queries: { staleTime: 5 * 1000 } },8  })9 10  if (process.server) {11    context.ssrContext.VueQuery = queryClient12  }13 14  if (process.client) {15    Vue.use(VueQueryPlugin, { queryClient })16 17    if (context.nuxtState && context.nuxtState.vueQueryState) {18      hydrate(queryClient, context.nuxtState.vueQueryState)19    }20  }21}

将此插件添加到你的 nuxt.config.js 中:

JavaScript
1module.exports = {2  ...3  plugins: ['~/plugins/vue-query.js'],4}

现在你就可以在页面中使用 onServerPrefetch 预取数据了。

  • 使用 useContext 获取 Nuxt 上下文
  • 使用 useQueryClient 获取服务端的 queryClient 实例
  • 使用 queryClient.query、queryClient.infiniteQuery 或 suspense 预取所有你需要的查询
  • 将 queryClient 脱水至 nuxtContext
HTML
1// pages/todos.vue2<template>3  <div>4    <button @click="refetch">Refetch</button>5    <p>{{ data }}</p>6  </div>7</template>8 9<script lang="ts">10import {11  defineComponent,12  onServerPrefetch,13  useContext,14} from '@nuxtjs/composition-api'15import { useQuery, useQueryClient, dehydrate } from '@tanstack/vue-query'1617export default defineComponent({18  setup() {19    // Get QueryClient either from SSR context, or Vue context20    const { ssrContext } = useContext()21    // Make sure to provide `queryClient` as a second parameter to `useQuery` calls22    const queryClient =23      (ssrContext != null && ssrContext.VueQuery) || useQueryClient()2425    // This will be prefetched and sent from the server26    const { data, refetch, suspense } = useQuery(27      {28        queryKey: ['todos'],29        queryFn: getTodos,30      },31      queryClient,32    )33    // This won't be prefetched, it will start fetching on client side34    const { data: data2 } = useQuery(35      {36        queryKey: ['todos2'],37        queryFn: getTodos,38      },39      queryClient,40    )4142    onServerPrefetch(async () => {43      await suspense()44      ssrContext.nuxt.vueQueryState = dehydrate(queryClient)45    })4647    return {48      refetch,49      data,50    }51  },52})53</script>

如上所示,你可以选择性地预取部分查询,其余查询则交由客户端拉取。这意味着你可以通过为特定查询添加或移除 queryClient.query 或 suspense,来控制服务端是否渲染对应内容。

使用 Vite SSR

通过 vite-ssr 同步 VueQuery 客户端状态,以便将其序列化到 DOM 中:

JavaScript
1// main.js (entry point)2import App from './App.vue'3import viteSSR from 'vite-ssr/vue'4import {5  QueryClient,6  VueQueryPlugin,7  hydrate,8  dehydrate,9} from '@tanstack/vue-query'10 11export default viteSSR(App, { routes: [] }, ({ app, initialState }) => {12  // -- This is Vite SSR main hook, which is called once per request13 14  // Create a fresh VueQuery client15  const queryClient = new QueryClient()16 17  // Sync initialState with the client state18  if (import.meta.env.SSR) {19    // Indicate how to access and serialize VueQuery state during SSR20    initialState.vueQueryState = { toJSON: () => dehydrate(queryClient) }21  } else {22    // Reuse the existing state in the browser23    hydrate(queryClient, initialState.vueQueryState)24  }25 26  // Mount and provide the client to the app components27  app.use(VueQueryPlugin, { queryClient })28})

然后,在任意组件中使用 Vue 的 onServerPrefetch 调用 VueQuery:

HTML
1<!-- MyComponent.vue -->2<template>3  <div>4    <button @click="refetch">Refetch</button>5    <p>{{ data }}</p>6  </div>7</template>8 9<script setup>10import { useQuery } from '@tanstack/vue-query'11import { onServerPrefetch } from 'vue'1213// This will be prefetched and sent from the server14const { refetch, data, suspense } = useQuery({15  queryKey: ['todos'],16  queryFn: getTodos,17})1819onServerPrefetch(suspense)20</script>

提示、技巧与注意事项

仅成功查询会被包含在脱水结果中

任何出错的查询会自动被排除在脱水过程之外。这意味着默认行为是假装这些查询从未在服务端加载过,通常表现为显示加载状态,并在 queryClient 上重试这些查询——无论错误类型如何。

有时这种行为并不理想:例如,你可能希望对某些错误或查询直接渲染带正确状态码的错误页。此时,请使用 queryClient.query 并捕获错误,手动处理。

过期性以查询在服务端拉取的时间为基准计算

一个查询是否过期,取决于它何时被 dataUpdatedAt。此处需注意:服务端时间必须准确,才能保证该机制正常工作;不过由于采用 UTC 时间,时区不会影响判断。

由于 staleTime 默认为 0,因此查询默认会在页面加载时于后台重新拉取。如果你不缓存页面标记,可能希望提高 staleTime 值以避免重复拉取。

当 CDN 缓存页面标记时,这种对过期查询的后台重新拉取尤为适用!你可以将页面自身的缓存时间设得较高,从而避免频繁服务端重渲染;同时将查询的 staleTime 设得较低,确保用户一访问页面就立即在后台刷新数据。例如,你可能希望页面缓存一周,但只要数据超过一天即在页面加载时自动刷新?

suspense() 对持续禁用的查询会阻塞服务端渲染

suspense() 会等待查询启用后才解析,因此对始终禁用的查询永远不会解析。在服务端,若在 onServerPrefetch 中 await 此类查询(例如某个依赖查询失败的从属查询),将导致渲染被阻塞。请在查询被禁用时跳过它:

HTML
1<script setup>2import { computed, onServerPrefetch } from 'vue'3import { useQuery } from '@tanstack/vue-query'45const { data: user, suspense: userSuspense } = useQuery({6  queryKey: ['user'],7  queryFn: getUser,8})910const userId = computed(() => user.value?.id)11const enabled = computed(() => !!user.value?.id)12const { data: projects, suspense: projectsSuspense } = useQuery({13  queryKey: ['projects', userId],14  queryFn: () => getProjectsByUser(userId.value),15  enabled,16})1718onServerPrefetch(async () => {19  await userSuspense()20  if (enabled.value) {21    await projectsSuspense()22  }23})24</script>

服务端内存占用过高

若你为每个请求都新建 QueryClient,Vue Query 会为该客户端创建独立缓存,并在 gcTime 期间保留在内存中。若该时间段内请求量很高,可能导致服务端内存占用激增。

在服务端,gcTime 默认为 Infinity,即禁用手动垃圾回收,并在单个请求结束后自动清空内存。若你显式设置了非 Infinity 的 gcTime,则需自行负责提前清理缓存。

为降低内存占用,可在请求处理完毕、脱水状态已发送至客户端后,调用 queryClient.clear() 清理缓存。

dehydrate/hydrate 选项

dehydrate 和 hydrate 从 @tanstack/query-core 重新导出,因此可直接从 @tanstack/vue-query 使用——本指南中即采用此方式。

dehydrate(client, options?) 接受一个 DehydrateOptions 对象:

  • shouldDehydrateMutation: (mutation) => boolean —— 是否对给定修改执行脱水。对缓存中的每个修改调用;默认仅包含已暂停的修改。若想扩展而非替换默认行为,请在返回语句中导入并调用 defaultShouldDehydrateMutation。
  • shouldDehydrateQuery: (query) => boolean —— 是否对给定查询执行脱水。对缓存中的每个查询调用;默认仅包含成功查询(参见上文)。同样可通过 defaultShouldDehydrateQuery 扩展。
  • serializeData?: (data) => any —— 在脱水过程中转换(序列化)数据。
  • shouldRedactErrors?: (error) => boolean —— 仅适用于脱水时仍处于 pending 状态的查询,其 Promise 也会被脱水:决定该 Promise 后续拒绝时是否隐藏错误。默认隐藏所有此类错误。不适用于 query.state.error 已完成的查询——该错误会原样包含在脱水状态中;若其中可能含敏感数据,请通过 shouldDehydrateQuery 自行清洗。

hydrate(client, dehydratedState, options?) 接受一个 HydrateOptions 对象:

  • defaultOptions.mutations —— 注水后修改的默认选项。
  • defaultOptions.queries —— 注水后查询的默认选项。
  • defaultOptions.deserializeData?: (data) => any —— 在数据写入缓存前转换(反序列化)数据。

hydrate 的一个注意事项:若待注水的查询已在缓存中存在,则仅当传入数据比已有数据更新时才会覆盖;否则静默跳过。

你也可以设置更小的 gcTime。

本页为 TanStack Query 文档(Vue 版)的非官方中文翻译,原文由上游团队维护,遵循 MIT 许可。 译文如有偏差,以英文原文为准。

译自上游 59d11de