服务端渲染
非官方测试版翻译
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
只需在 createTRPCNext 配置回调中设置 ssr: true 即可启用 SSR。
信息
为了在服务端渲染阶段正确执行查询,我们需要在 config 中添加额外逻辑。此外,请考虑使用 Response Caching。
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssr : true,ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},});
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssr : true,ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},});
或者,若需根据特定请求条件启用 SSR,可向 ssr 传递回调函数。此回调可返回布尔值,或解析为布尔值的 Promise:
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},ssr (opts ) {// only SSR if the request is coming from a botreturnopts .ctx ?.req ?.headers ['user-agent']?.includes ('bot') ?? false;},});
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},ssr (opts ) {// only SSR if the request is coming from a botreturnopts .ctx ?.req ?.headers ['user-agent']?.includes ('bot') ?? false;},});
pages/_app.tsxtsximport {trpc } from '../utils/trpc';import type {AppProps } from 'next/app';import type {AppType } from 'next/app';constMyApp :AppType = ({Component ,pageProps }:AppProps ) => {return <Component {...pageProps } />;};export defaulttrpc .withTRPC (MyApp );
pages/_app.tsxtsximport {trpc } from '../utils/trpc';import type {AppProps } from 'next/app';import type {AppType } from 'next/app';constMyApp :AppType = ({Component ,pageProps }:AppProps ) => {return <Component {...pageProps } />;};export defaulttrpc .withTRPC (MyApp );
结合 SSR 的响应缓存
若在应用中启用 SSR,可能会发现应用在 Vercel 等平台加载缓慢,实际上无需 SSG 也能静态渲染整个应用;参阅此 Twitter 讨论串获取更多洞见。
您可以在 createTRPCNext 上使用 responseMeta 回调来为 SSR 响应设置缓存标头。另请参阅通用的响应缓存文档,了解使用 responseMeta 的框架无关缓存方案。
utils/trpc.tsxtsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from '../server/routers/_app';export consttrpc =createTRPCNext <AppRouter >({config () {if (typeofwindow !== 'undefined') {return {links : [httpBatchLink ({url : '/api/trpc',}),],};}consturl =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 callsreturn {status :clientErrors [0].data ?.httpStatus ?? 500,};}// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['cache-control',`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS }`,],]),};},});
utils/trpc.tsxtsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from '../server/routers/_app';export consttrpc =createTRPCNext <AppRouter >({config () {if (typeofwindow !== 'undefined') {return {links : [httpBatchLink ({url : '/api/trpc',}),],};}consturl =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 callsreturn {status :clientErrors [0].data ?.httpStatus ?? 500,};}// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['cache-control',`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS }`,],]),};},});
使用 Next.js 适配器进行 API 响应缓存
您也可以在 Next.js API 处理程序上使用 responseMeta 来直接缓存 API 响应:
pages/api/trpc/[trpc].tstsximport {initTRPC } from '@trpc/server';import * astrpcNext from '@trpc/server/adapters/next';export constcreateContext = async ({req ,res ,}:trpcNext .CreateNextContextOptions ) => {return {req ,res ,};};typeContext =Awaited <ReturnType <typeofcreateContext >>;export constt =initTRPC .context <Context >().create ();export constappRouter =t .router ({public :t .router ({slowQueryCached :t .procedure .query (async (opts ) => {await newPromise ((resolve ) =>setTimeout (resolve , 5000));return {lastUpdated : newDate ().toJSON (),};}),}),});export typeAppRouter = typeofappRouter ;export defaulttrpcNext .createNextApiHandler ({router :appRouter ,createContext ,responseMeta (opts ) {const {ctx ,paths ,errors ,type } =opts ;// assuming you have all your public routes with the keyword `public` in themconstallPublic =paths &&paths .every ((path ) =>path .includes ('public'));// checking that no procedures erroredconstallOk =errors .length === 0;// checking we're doing a query requestconstisQuery =type === 'query';if (ctx ?.res &&allPublic &&allOk &&isQuery ) {// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['cache-control',`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS }`,],]),};}return {};},});
pages/api/trpc/[trpc].tstsximport {initTRPC } from '@trpc/server';import * astrpcNext from '@trpc/server/adapters/next';export constcreateContext = async ({req ,res ,}:trpcNext .CreateNextContextOptions ) => {return {req ,res ,};};typeContext =Awaited <ReturnType <typeofcreateContext >>;export constt =initTRPC .context <Context >().create ();export constappRouter =t .router ({public :t .router ({slowQueryCached :t .procedure .query (async (opts ) => {await newPromise ((resolve ) =>setTimeout (resolve , 5000));return {lastUpdated : newDate ().toJSON (),};}),}),});export typeAppRouter = typeofappRouter ;export defaulttrpcNext .createNextApiHandler ({router :appRouter ,createContext ,responseMeta (opts ) {const {ctx ,paths ,errors ,type } =opts ;// assuming you have all your public routes with the keyword `public` in themconstallPublic =paths &&paths .every ((path ) =>path .includes ('public'));// checking that no procedures erroredconstallOk =errors .length === 0;// checking we're doing a query requestconstisQuery =type === 'query';if (ctx ?.res &&allPublic &&allOk &&isQuery ) {// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['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)页面。