跳至主内容
版本:11.x

服务端渲染

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

只需在 createTRPCNext 配置回调中设置 ssr: true 即可启用 SSR。

信息

启用 SSR 后,tRPC 会使用 getInitialProps 在服务端预取所有查询。当您同时使用 getServerSideProps 时,这会导致此类问题,且该问题超出我们的解决范围。

 
替代方案是保持 SSR 禁用(默认状态),并使用服务端助手getStaticPropsgetServerSideProps 中预取查询。

为了在服务端渲染阶段正确执行查询,我们需要在 config 中添加额外逻辑。此外,请考虑使用 Response Caching

utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssr: true,
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
});
utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssr: true,
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
});

或者,若需根据特定请求条件启用 SSR,可向 ssr 传递回调函数。此回调可返回布尔值,或解析为布尔值的 Promise:

utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
ssr(opts) {
// only SSR if the request is coming from a bot
return opts.ctx?.req?.headers['user-agent']?.includes('bot') ?? false;
},
});
utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
ssr(opts) {
// only SSR if the request is coming from a bot
return opts.ctx?.req?.headers['user-agent']?.includes('bot') ?? false;
},
});
pages/_app.tsx
tsx
import { trpc } from '../utils/trpc';
import type { AppProps } from 'next/app';
import type { AppType } from 'next/app';
 
const MyApp: AppType = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
 
export default trpc.withTRPC(MyApp);
pages/_app.tsx
tsx
import { trpc } from '../utils/trpc';
import type { AppProps } from 'next/app';
import type { AppType } from 'next/app';
 
const MyApp: AppType = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
 
export default trpc.withTRPC(MyApp);

结合 SSR 的响应缓存

若在应用中启用 SSR,可能会发现应用在 Vercel 等平台加载缓慢,实际上无需 SSG 也能静态渲染整个应用;参阅此 Twitter 讨论串获取更多洞见。

您可以在 createTRPCNext 上使用 responseMeta 回调来为 SSR 响应设置缓存标头。另请参阅通用的响应缓存文档,了解使用 responseMeta 的框架无关缓存方案。

utils/trpc.tsx
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from '../server/routers/_app';
 
export const trpc = createTRPCNext<AppRouter>({
config() {
if (typeof window !== 'undefined') {
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: 'http://localhost:3000/api/trpc';
 
return {
links: [
httpBatchLink({
url,
}),
],
};
},
ssr: true,
ssrPrepass,
responseMeta(opts) {
const { clientErrors } = opts;
 
if (clientErrors.length) {
// propagate http first error from API calls
return {
status: clientErrors[0].data?.httpStatus ?? 500,
};
}
 
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
},
});
utils/trpc.tsx
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from '../server/routers/_app';
 
export const trpc = createTRPCNext<AppRouter>({
config() {
if (typeof window !== 'undefined') {
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: 'http://localhost:3000/api/trpc';
 
return {
links: [
httpBatchLink({
url,
}),
],
};
},
ssr: true,
ssrPrepass,
responseMeta(opts) {
const { clientErrors } = opts;
 
if (clientErrors.length) {
// propagate http first error from API calls
return {
status: clientErrors[0].data?.httpStatus ?? 500,
};
}
 
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
},
});

使用 Next.js 适配器进行 API 响应缓存

您也可以在 Next.js API 处理程序上使用 responseMeta 来直接缓存 API 响应:

pages/api/trpc/[trpc].ts
tsx
import { initTRPC } from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
 
export const createContext = async ({
req,
res,
}: trpcNext.CreateNextContextOptions) => {
return {
req,
res,
};
};
 
type Context = Awaited<ReturnType<typeof createContext>>;
 
export const t = initTRPC.context<Context>().create();
 
export const appRouter = t.router({
public: t.router({
slowQueryCached: t.procedure.query(async (opts) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
 
return {
lastUpdated: new Date().toJSON(),
};
}),
}),
});
 
export type AppRouter = typeof appRouter;
 
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
responseMeta(opts) {
const { ctx, paths, errors, type } = opts;
// assuming you have all your public routes with the keyword `public` in them
const allPublic = paths && paths.every((path) => path.includes('public'));
// checking that no procedures errored
const allOk = errors.length === 0;
// checking we're doing a query request
const isQuery = type === 'query';
 
if (ctx?.res && allPublic && allOk && isQuery) {
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
}
return {};
},
});
pages/api/trpc/[trpc].ts
tsx
import { initTRPC } from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
 
export const createContext = async ({
req,
res,
}: trpcNext.CreateNextContextOptions) => {
return {
req,
res,
};
};
 
type Context = Awaited<ReturnType<typeof createContext>>;
 
export const t = initTRPC.context<Context>().create();
 
export const appRouter = t.router({
public: t.router({
slowQueryCached: t.procedure.query(async (opts) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
 
return {
lastUpdated: new Date().toJSON(),
};
}),
}),
});
 
export type AppRouter = typeof appRouter;
 
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
responseMeta(opts) {
const { ctx, paths, errors, type } = opts;
// assuming you have all your public routes with the keyword `public` in them
const allPublic = paths && paths.every((path) => path.includes('public'));
// checking that no procedures errored
const allOk = errors.length === 0;
// checking we're doing a query request
const isQuery = type === 'query';
 
if (ctx?.res && allPublic && allOk && isQuery) {
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
}
return {};
},
});

常见问题

问:为什么需要手动将客户端 headers 转发到服务器?tRPC 不能自动处理吗?

虽然在 SSR 场景中通常都需要转发客户端 headers,但您可能需要在 headers 中动态添加内容。因此 tRPC 不负责处理 header 键名冲突等问题。

问:为什么在 Node 18 上使用 SSR 时需要删除 connection 头?

若不移除 connection 头,数据获取将失败并出现 TRPCClientError: fetch failed 错误,因为 connection 属于禁用标头名称

问:为什么在 Network 标签页仍能看到网络请求?

默认情况下,@tanstack/react-query(我们用于数据获取的 hooks)会在组件挂载和窗口重新聚焦时重新获取数据,即使已通过 SSR 获得初始数据。这确保了数据始终最新。如需禁用此行为,请参阅静态站点生成(SSG)页面。