Skip to main content

服务端渲染 SSR

Vue Query 支持在服务器上预取多个查询,然后将这些查询 dehydrating 到 queryClient 上。这意味着服务器可以预取页面加载时立即可用的标记。 一旦 JS 可用,Vue Query 就可以用它的全部功能升级或 hydrate 这些查询。 这包括在客户端重新获取那些服务端渲染后变得过时的查询。

在 Nuxt.js 中

Nuxt 3

首先在你的plugins文件夹中创建一个vue-query.ts文件,并添加如下代码:

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

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 (process.server) {
nuxt.hooks.hook("app:rendered", () => {
vueQueryState.value = dehydrate(queryClient);
});
}

if (process.client) {
nuxt.hooks.hook("app:created", () => {
hydrate(queryClient, vueQueryState.value);
});
}
});

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

  • 所有你需要的查询都需要使用queryClient.prefetchQuerysuspense预取。
export default defineComponent({
setup() {
const { data, suspense } = useQuery("test", fetcher);

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

return { data };
},
});

Nuxt 2

首先在你的plugins文件夹中创建一个vue-query.ts文件,并添加如下代码:

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 } },
});
const options = { queryClient };

Vue.use(VueQueryPlugin, options);

if (process.client) {
if (context.nuxtState && context.nuxtState["vue-query"]) {
hydrate(queryClient, context.nuxtState["vue-query"]);
}
}
};

将这个文件添加到nuxt.config.js

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

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

  • 使用 useContext 获取 Nust 上下文
  • 使用 useQueryClient 获取queryClient的服务端实例
  • 使用queryClient.prefetchQuerysuspense预取所有你需要的查询
  • Dehydrate queryClientnuxtContext
// 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() {
// 这将被预取服务器发送的数据
const { refetch, data, suspense } = useQuery("todos", getTodos);
// 这不会被预取,它将在客户端js执行后开始获取
const { data2 } = useQuery("todos2", getTodos);

onServerPrefetch(async () => {
const { ssrContext } = useContext();
const queryClient = useQueryClient();
await suspense();

ssrContext.nuxt.vueQueryState = dehydrate(queryClient);
});

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

如前所述,可以预取一些查询,也可以在 queryClient 上获取别的查询。 这就意味着你可以通过为特定查询添加或删除 prefetchQuerysuspense 来控制服务器需要渲染的内容。

使用 Vite 的 SSR

将 VueQuery 客户端状态与vite-ssr同步,以便在 DOM 中进行序列化:

// 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 }) => {
// 这是Vite SSR的主钩子,每次请求都会被调用一次

// 创建一个全新的VueQuery客户端
const queryClient = new QueryClient();

// 将initialState与客户端状态同步
if (import.meta.env.SSR) {
// 说明如何在SSR期间访问和序列化VueQuery状态
initialState.vueQueryState = { toJSON: () => dehydrate(queryClient) };
} else {
// 重用浏览器中现有的状态
hydrate(queryClient, initialState.vueQueryState);
}

// 挂载并提供给客户端的应用程序组件
app.use(VueQueryPlugin, { queryClient });
});

然后,使用 Vue 的onServerPrefetch从任何组件调用 VueQuery:

<!-- 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("todos", getTodos);
onServerPrefetch(suspense);
</script>

提示、技巧及部分可能的陷阱

只有成功的查询才会 dehydration

任何有错误的查询都会自动从 dehydration 中排除。 这意味着在默认情况下,Vue Query 会假装这些查询从未在服务器上加载过,(你通常需要)在 UI 上显示一个加载状态,并在 queryClient 上重新尝试这些查询。 这种情况会发生,不管是否有错误。

有时这种行为是不可取的,也许你想在某些错误或查询中呈现一个具有对应状态码的错误页面。 在这些情况下,使用 fetchQuery 捕捉错误并手动处理这些错误。

在服务端上获取查询数据后就开始度量“数据何时过时”

查询被标记为 dataUpdatedAt 时,就代表它已经是过时的查询了。 这里需要注意的是,服务器需要有正确的时间才能正常工作。 Vue Query 使用的是 UTC 时间,所以时区不需要考虑在内。

由于 staleTime 默认为 0,因此默认情况下,在页面加载时,查询就将在后台重新获取。 你可能想使用一个较久的 staleTime 来避免这种多次没必要的获取,特别是你不缓存你的标记(don't cache your markup)的情况下。

在 CDN 中缓存标记时,这种对过时查询的重新获取是完美的选择! 你可以将页面本身的缓存时间设置得相当高,以避免在服务器上重新渲染页面,但是可以将查询的 staleTime 配置得较低,以确保用户访问该页面后立即在后台重新获取数据。 当然,或许你可以设置得将页面缓存一周,但如果数据大于一天,则在页面加载时自动重新获取数据?

服务端的内存占用过高

如果你为每个请求都创建一个 QueryClient, Vue Query 就会为每个 QueryClient 都创建一个隔离的缓存,每个都会在内存中保存一段时间(通过 cacheTime 设定*)。 如果在此期间有大量的请求,这可能会导致服务器上的内存使用过高。

* 在服务器上,`cacheTime` 默认为 `Infinity`。它禁用了手动垃圾收集,一旦请求结束就会自动清除内存。 如果你明确设置了一个非 `Infinity` 的 `cacheTime`,那么你需要手动负责提前清除缓存。

为了在不需要缓存后立即清除缓存并降低内存消耗,可以在服务器处理请求并将 dehydrated 的状态发送给客户端后,添加一句对queryClient.clear()的调用。

另外,你可以设置一个较小的 cacheTime