SSR

Vue Query 支持在服务器上预取多个查询,然后将这些查询“脱水”(dehydrate)到 queryClient。这意味着服务器可以预渲染在页面加载时即可使用的标记,并且一旦 JavaScript 可用,Vue Query 就可以用该库的全部功能来“水合”(hydrate)这些查询。这包括在客户端重新获取自服务器渲染以来已变陈旧的查询。

使用 Nuxt.js

Nuxt 3

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

ts
import type {
  DehydratedState,
  VueQueryPluginOptions,
} from '@tanstack/vue-query'
import {
  VueQueryPlugin,
  QueryClient,
  hydrate,
  dehydrate,
} from '@tanstack/vue-query'
// Nuxt 3 app aliases
import { defineNuxtPlugin, useState } from '#imports'

export default defineNuxtPlugin((nuxt) => {
  const vueQueryState = useState<DehydratedState | null>('vue-query')

  // Modify your Vue Query global settings here
  const queryClient = new QueryClient({
    defaultOptions: { queries: { staleTime: 5000 } },
  })
  const options: VueQueryPluginOptions = { queryClient }

  nuxt.vueApp.use(VueQueryPlugin, options)

  if (import.meta.server) {
    nuxt.hooks.hook('app:rendered', () => {
      vueQueryState.value = dehydrate(queryClient)
    })
  }

  if (import.meta.client) {
    hydrate(queryClient, vueQueryState.value)
  }
})
import type {
  DehydratedState,
  VueQueryPluginOptions,
} from '@tanstack/vue-query'
import {
  VueQueryPlugin,
  QueryClient,
  hydrate,
  dehydrate,
} from '@tanstack/vue-query'
// Nuxt 3 app aliases
import { defineNuxtPlugin, useState } from '#imports'

export default defineNuxtPlugin((nuxt) => {
  const vueQueryState = useState<DehydratedState | null>('vue-query')

  // Modify your Vue Query global settings here
  const queryClient = new QueryClient({
    defaultOptions: { queries: { staleTime: 5000 } },
  })
  const options: VueQueryPluginOptions = { queryClient }

  nuxt.vueApp.use(VueQueryPlugin, options)

  if (import.meta.server) {
    nuxt.hooks.hook('app:rendered', () => {
      vueQueryState.value = dehydrate(queryClient)
    })
  }

  if (import.meta.client) {
    hydrate(queryClient, vueQueryState.value)
  }
})

现在您已准备好在页面中使用 onServerPrefetch 来预取数据。

  • 使用 queryClient.prefetchQuerysuspense 预取所有需要的查询。
ts
export default defineComponent({
  setup() {
    const { data, suspense } = useQuery({
      queryKey: ['test'],
      queryFn: fetcher,
    })

    onServerPrefetch(async () => {
      await suspense()
    })

    return { data }
  },
})
export default defineComponent({
  setup() {
    const { data, suspense } = useQuery({
      queryKey: ['test'],
      queryFn: fetcher,
    })

    onServerPrefetch(async () => {
      await suspense()
    })

    return { data }
  },
})

Nuxt 2

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

js
import Vue from 'vue'
import { VueQueryPlugin, QueryClient, hydrate } from '@tanstack/vue-query'

export default (context) => {
  // Modify your Vue Query global settings here
  const queryClient = new QueryClient({
    defaultOptions: { queries: { staleTime: 5000 } },
  })

  if (process.server) {
    context.ssrContext.VueQuery = queryClient
  }

  if (process.client) {
    Vue.use(VueQueryPlugin, { queryClient })

    if (context.nuxtState && context.nuxtState.vueQueryState) {
      hydrate(queryClient, context.nuxtState.vueQueryState)
    }
  }
}
import Vue from 'vue'
import { VueQueryPlugin, QueryClient, hydrate } from '@tanstack/vue-query'

export default (context) => {
  // Modify your Vue Query global settings here
  const queryClient = new QueryClient({
    defaultOptions: { queries: { staleTime: 5000 } },
  })

  if (process.server) {
    context.ssrContext.VueQuery = queryClient
  }

  if (process.client) {
    Vue.use(VueQueryPlugin, { queryClient })

    if (context.nuxtState && context.nuxtState.vueQueryState) {
      hydrate(queryClient, context.nuxtState.vueQueryState)
    }
  }
}

将此插件添加到您的 nuxt.config.js

js
module.exports = {
  ...
  plugins: ['~/plugins/vue-query.js'],
}
module.exports = {
  ...
  plugins: ['~/plugins/vue-query.js'],
}

现在您已准备好在页面中使用 onServerPrefetch 来预取数据。

  • 使用 useContext 获取 nuxt 上下文
  • 使用 useQueryClient 获取 queryClient 的服务器端实例
  • 使用 queryClient.prefetchQuerysuspense 预取所有需要的查询。
  • queryClient 脱水到 nuxtContext
vue
// pages/todos.vue
<template>
  <div>
    <button @click="refetch">Refetch</button>
    <p>{{ data }}</p>
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
  onServerPrefetch,
  useContext,
} from '@nuxtjs/composition-api'
import { useQuery, useQueryClient, dehydrate } from '@tanstack/vue-query'

export default defineComponent({
  setup() {
    // Get QueryClient either from SSR context, or Vue context
    const { ssrContext } = useContext()
    // Make sure to provide `queryClient` as a second parameter to `useQuery` calls
    const queryClient =
      (ssrContext != null && ssrContext.VueQuery) || useQueryClient()

    // This will be prefetched and sent from the server
    const { data, refetch, suspense } = useQuery(
      {
        queryKey: ['todos'],
        queryFn: getTodos,
      },
      queryClient,
    )
    // This won't be prefetched, it will start fetching on client side
    const { data2 } = useQuery(
      {
        queryKey: 'todos2',
        queryFn: getTodos,
      },
      queryClient,
    )

    onServerPrefetch(async () => {
      await suspense()
      ssrContext.nuxt.vueQueryState = dehydrate(queryClient)
    })

    return {
      refetch,
      data,
    }
  },
})
</script>
// pages/todos.vue
<template>
  <div>
    <button @click="refetch">Refetch</button>
    <p>{{ data }}</p>
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
  onServerPrefetch,
  useContext,
} from '@nuxtjs/composition-api'
import { useQuery, useQueryClient, dehydrate } from '@tanstack/vue-query'

export default defineComponent({
  setup() {
    // Get QueryClient either from SSR context, or Vue context
    const { ssrContext } = useContext()
    // Make sure to provide `queryClient` as a second parameter to `useQuery` calls
    const queryClient =
      (ssrContext != null && ssrContext.VueQuery) || useQueryClient()

    // This will be prefetched and sent from the server
    const { data, refetch, suspense } = useQuery(
      {
        queryKey: ['todos'],
        queryFn: getTodos,
      },
      queryClient,
    )
    // This won't be prefetched, it will start fetching on client side
    const { data2 } = useQuery(
      {
        queryKey: 'todos2',
        queryFn: getTodos,
      },
      queryClient,
    )

    onServerPrefetch(async () => {
      await suspense()
      ssrContext.nuxt.vueQueryState = dehydrate(queryClient)
    })

    return {
      refetch,
      data,
    }
  },
})
</script>

如示例所示,可以预取某些查询,让其他查询在 queryClient 上获取。这意味着您可以添加或删除特定查询的 prefetchQuerysuspense 来控制服务器渲染哪些内容。

使用 Vite SSR

同步 VueQuery 客户端状态与 vite-ssr,以便在 DOM 中序列化它。

js
// main.js (entry point)
import App from './App.vue'
import viteSSR from 'vite-ssr/vue'
import {
  QueryClient,
  VueQueryPlugin,
  hydrate,
  dehydrate,
} from '@tanstack/vue-query'

export default viteSSR(App, { routes: [] }, ({ app, initialState }) => {
  // -- This is Vite SSR main hook, which is called once per request

  // Create a fresh VueQuery client
  const queryClient = new QueryClient()

  // Sync initialState with the client state
  if (import.meta.env.SSR) {
    // Indicate how to access and serialize VueQuery state during SSR
    initialState.vueQueryState = { toJSON: () => dehydrate(queryClient) }
  } else {
    // Reuse the existing state in the browser
    hydrate(queryClient, initialState.vueQueryState)
  }

  // Mount and provide the client to the app components
  app.use(VueQueryPlugin, { queryClient })
})
// main.js (entry point)
import App from './App.vue'
import viteSSR from 'vite-ssr/vue'
import {
  QueryClient,
  VueQueryPlugin,
  hydrate,
  dehydrate,
} from '@tanstack/vue-query'

export default viteSSR(App, { routes: [] }, ({ app, initialState }) => {
  // -- This is Vite SSR main hook, which is called once per request

  // Create a fresh VueQuery client
  const queryClient = new QueryClient()

  // Sync initialState with the client state
  if (import.meta.env.SSR) {
    // Indicate how to access and serialize VueQuery state during SSR
    initialState.vueQueryState = { toJSON: () => dehydrate(queryClient) }
  } else {
    // Reuse the existing state in the browser
    hydrate(queryClient, initialState.vueQueryState)
  }

  // Mount and provide the client to the app components
  app.use(VueQueryPlugin, { queryClient })
})

然后,在任何组件中使用 Vue 的 onServerPrefetch 调用 VueQuery。

html
<!-- MyComponent.vue -->
<template>
  <div>
    <button @click="refetch">Refetch</button>
    <p>{{ data }}</p>
  </div>
</template>

<script setup>
  import { useQuery } from '@tanstack/vue-query'
  import { onServerPrefetch } from 'vue'

  // This will be prefetched and sent from the server
  const { refetch, data, suspense } = useQuery({
    queryKey: ['todos'],
    queryFn: getTodos,
  })

  onServerPrefetch(suspense)
</script>
<!-- MyComponent.vue -->
<template>
  <div>
    <button @click="refetch">Refetch</button>
    <p>{{ data }}</p>
  </div>
</template>

<script setup>
  import { useQuery } from '@tanstack/vue-query'
  import { onServerPrefetch } from 'vue'

  // This will be prefetched and sent from the server
  const { refetch, data, suspense } = useQuery({
    queryKey: ['todos'],
    queryFn: getTodos,
  })

  onServerPrefetch(suspense)
</script>

技巧、窍门和注意事项

脱水只包含成功查询

任何有错误的查询都会被自动排除在脱水之外。这意味着默认行为是假装这些查询从未在服务器上加载过,通常会显示加载状态,并在 queryClient 上重试这些查询。无论错误是什么,都会发生这种情况。

有时这种行为并不理想,也许您希望在某些错误或查询上呈现带有正确状态码的错误页面。在那些情况下,请使用 fetchQuery 并捕获任何错误以手动处理。

陈旧度是从服务器获取查询时开始计量的

查询的陈旧度取决于它被 dataUpdatedAt 的时间。这里的一个注意事项是,服务器需要有正确的时间才能正常工作,但使用的是 UTC 时间,因此时区不影响此计算。

由于 staleTime 默认为 0,查询在页面加载时会在后台重新获取,这是默认行为。您可能希望使用更高的 staleTime 来避免这种双重获取,尤其是在不对标记进行缓存的情况下。

重新获取陈旧查询的这种行为与使用 CDN 缓存标记非常匹配!您可以将页面本身的缓存时间设置得相当长,以避免在服务器上重新渲染页面,但同时配置查询的 staleTime 设置得更低,以确保用户访问页面时数据在后台重新获取。也许您想缓存页面一周,但当数据超过一天时,在页面加载时自动重新获取数据?

服务器内存占用高

如果您为每次请求创建 QueryClient,Vue Query 会为该客户端创建隔离的缓存,该缓存会在内存中保留 gcTime 期间。如果在该期间有大量请求,这可能会导致服务器内存占用过高。

在服务器上,gcTime 默认为 Infinity,这将禁用手动垃圾回收,并在请求完成后自动清除内存。如果您显式设置了一个非 Infinity 的 gcTime,您将负责尽早清除缓存。

要在不需要缓存后清除缓存并降低内存占用,您可以在请求处理完毕并将脱水状态发送到客户端后,添加一个对 queryClient.clear() 的调用。

或者,您可以设置一个较小的 gcTime