服务端渲染(SSR)
Vue Query 支持在服务端预取多个查询,然后将这些查询 脱水(dehydrate)到 queryClient。这意味着服务端可以预先渲染出页面标记(markup),使内容在页面加载时立即可用;而一旦 JavaScript 加载完成,Vue Query 即可对这些查询进行升级或 注水(hydrate),赋予其完整的库功能。这包括:若查询自服务端渲染后已变为过期,则在客户端重新拉取该查询。
使用 Nuxt.js
Nuxt 3
首先,在 plugins 目录下创建 vue-query.ts 文件,内容如下:
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预取所有你需要的查询
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 文件,内容如下:
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 中:
1module.exports = {2 ...3 plugins: ['~/plugins/vue-query.js'],4}现在你就可以在页面中使用 onServerPrefetch 预取数据了。
- 使用
useContext获取 Nuxt 上下文 - 使用
useQueryClient获取服务端的queryClient实例 - 使用
queryClient.query、queryClient.infiniteQuery或suspense预取所有你需要的查询 - 将
queryClient脱水至nuxtContext
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 中:
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:
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 此类查询(例如某个依赖查询失败的从属查询),将导致渲染被阻塞。请在查询被禁用时跳过它:
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。