今天要來講 trpc cache 機制,在 trpc 中有兩種方式 :
因為 getInitialProps 是早期在 getServerSideProps 還沒出現前的替代方式,trpc 為了整合這部分功能所以才加上,如果需要啟用只需要在 trpc client 中設定就好。
getInitialProps 是一種獲取頁面資料的一種方式,也別於在 client side 透過 useEffect fetch data,getInitialProps 讓你可以在 server side 中提前幫你 fetch data ,不用等到 client side 才發送 get data 的 request 而達到 preFetch 效果,而 getInitialProps 這個 function 執行時機點可以在 client side 跟 server side 。
server side: 當 user 第一次進到頁面時,會在 server side 先執行第一次,透過序列化以及緩序列化將 data 存到 window.__NEXT_DATA__.props 中,並寫入 html 的 inline script ,<script>window.__NEXT_DATA__={props:{xxx}}</script> 這樣 client side 的 component 就可以透過 inline script 吃到這個 page 中會用到的 data,而不是去發 request api 。
client side: 當 user 是透過連結跳轉過去或是點選上一頁的時候,而不是透過貼上連結的方式時, getInitialProps 會在 client side 中去觸發,但因為 user 已經訪問過第一次,所以這時在 client 端中執行的 getInitialProps 會去讀取 window.__NEXT_DATA__.props 內容然後給 component 中使用。
至於 getInitialProps 怎麼更新,則是透過設置 request 的 cache-control 決定 revalidate 的時間點,那 trpc 怎麼做到下面 demo 會解釋。
getInitialProps 目前的生態系中已經不是主流的寫法,取而代之的是透過 getServerSideProps 、getStaticProps 等方法,差別在於後者只會在 server 中拿取頁面會用到的資料以及 revalidae cache 。
getInitialProps 中還有一個致命缺點就是因為他會在 client 跟 server 都去執行,所以 getInitialProps 中會用到的 function 也會一並打包到 client 中,這會大大增加 js boundle size 的大小,所以這部分筆者不建議去用他。
透過 ssr: true 設置,如此一來 trpc 預設將透過 getInitialProps preFetch 每個頁面調用的 useQuery。
export const trpc = createTRPCNext<AppRouter>({
config(opts) {
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: {
http: httpBatchLink({
url,
}),
},
};
},
ssr: true,
如果你需要設置 revalidate , trpc 有一個 responseMeta 可以設置 cache-control 時機點,但這部分必須使用 ssr: true 的功能。
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers/_app';
export const trpc = createTRPCNext<AppRouter>({
config(opts) {
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: {
http: httpBatchLink({
url,
}),
},
};
},
ssr: true,
responseMeta(opts) {
const { clientErrors } = opts;
if (clientErrors.length) {
// propagate http first error from API calls
return {
status: clientErrors[0].data?.httpStatus ?? 500,
};
}
// 每秒進行一次 revalidate , cache 最多存一天
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: {
'cache-control': `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
},
};
},
});
這部分是本人比較推薦的用法,有個 helper 就不需要依靠 getInitialProps 幫我們做 preFetch 而是能在 getServerSideProps 、getStaticProps 中自由使用。
首先先新增 posts/[id].tsx page ,然後在 posts page 加一個 click 轉址到 post details
// ~src/posts/[id].tsx
const PostDetail = ({ id }: PostDetailProps) => {
const router = useRouter()
const { data: post, status, ...rest } = api.posts.getPost.useQuery({ post_id: id })
if (status !== 'success') {
return <>Loading...</>
}
return (
<>
<h1>Post details Page</h1>
<p>title : {post.title}</p>
<p>id : {post.id}</p>
<p>content : {post.content}</p>
<button onClick={() => router.back()}>back to home</button>
</>
)
}
添加 IoIosArchive icon 做導頁。
// ~src/page/index.tsx
import { IoIosArchive } from "react-icons/io";
export default function Home() {
// ..
return (
// ..
{posts.map((post, index) => (
<li
key={post.id}
className="flex items-center justify-between cursor-pointer"
>
//..
<div className="flex items-center gap-[1rem]">
<AiFillDelete
color="red"
className="cursor-pointer"
size={20}
onClick={async () => {
await deletePost({ id: post.id })
}}
/>
<IoIosArchive
onClick={() => router.push(`/posts/${post.id}`)}
/>
//..
);
}
然後呼叫 createServerSideHelpers 這樣你就可以在 server Side 中使用 trpc 的內容了。
export async function getStaticProps(
context: GetStaticPropsContext<{ id: string }>,
) {
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
prisma
},
});
// const id = context.params?.id as string;
const id = context.params?.id as string
// preFetch post data to PostDetail
await helpers.posts.getPost.prefetch({ post_id: id })
return {
props: {
trpcState: helpers.dehydrate(),
id,
},
revalidate: 1,
};
}
這邊 trpc 先做 prefetch 將資料送到 query cache 中。
await helpers.posts.getPost.prefetch({ post_id: id })
並將 pararms.id 一並傳到 client。
return {
props: {
trpcState: helpers.dehydrate(),
id,
},
//..
因為 trpc 已經先 preFetch 過,所以在 PostDetail 中的 api.posts.getPost.useQuery 不會再重新發送一次 request 而是直接從 query cahce 中拿資料,所以 if (status !== 'success') 這裡的 function 永遠不會去觸發,也就不會再有 loading 的畫面。
// ~src/posts/[id].tsx
const PostDetail = ({ id }: PostDetailProps) => {
const router = useRouter()
const { data: post, status, ...rest } = api.posts.getPost.useQuery({ post_id: id })
if (status !== 'success') {
return <>Loading...</>
}
//..
那因為有加 revalidate: 1 ,所以只要 user 只要重新回到頁面時每秒都會幫你重新 update data。
export async function getStaticProps(
context: GetStaticPropsContext<{ id: string }>,
) {
//..
return {
props: {
trpcState: helpers.dehydrate(),
id,
},
revalidate: 1,
};
}
但假如如果頁面中資料並不太需要太頻繁的 revalidate data ,你可以把 revalidate 的時間調高,或是把 user 觸發 revalidate 行為關閉如下:
如果一來就不會因為 WindowReFocus 就打一次 api ,但這部分還是取決於你的需求是什麼去做調整,並沒有一定的答案。
const data = trpc.example.useQuery(
// if your query takes no input, make sure that you don't
// accidentally pass the query options as the first argument
undefined,
{ refetchOnMount: false, refetchOnWindowFocus: false },
);
或是你可以在 trpc client 中設定。
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import superjson from 'superjson';
import type { AppRouter } from './api/trpc/[trpc]';
export const trpc = createTRPCNext<AppRouter>({
config(opts) {
return {
transformer: superjson,
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
// Change options globally
queryClientConfig: {
defaultOptions: {
queries: {
refetchOnMount: false,
refetchOnWindowFocus: false,
},
},
},
},
},
});
最後我們透過 npm build 來看看結果吧~
這樣 data 跟 initial html 都先被 server side build 完成了,如此一來你就完成 preLoad跟preFetch 的功能摟~
https://github.com/Danny101201/next_demo/tree/main
✅ 前端社群 :
https://lihi3.cc/kBe0Y