场景
在SPA应用中,由于只提供一个静态的HTML文件,所有的内容都是javascript实时渲染在页面上的,所以将会导致不能很好的被搜索引擎所收录。因此SSG(静态站点生成)以及SSR(服务器端渲染)可以解决这个问题,一个是为每个页面渲染静态HTML,另一个是在浏览器访问某个页面时动态创建HTML来解决这个问题。
!!!请注意!!!下面的内容主要基于Next.js,如果您不了解该内容,可以移步至官方站点学习NextJS,如果你不需要使用SSR或者SSG你可以关闭该文章,不用了解下面的内容了。
SSG或者SSR框架必须要在服务器上获取数据,服务器使用这些数据生成静态的HTML文件返回给客户端(可能是浏览器也可能是爬虫等)
实践
使用initialData作为服务端数据
需要先在Next.js中配置引入react-query,下面将改变_app.js的代码:
// _app.js
import { QueryClient, QueryClientProvider } from 'react-query'
const client = new QueryClient()
export default function App({ Component, pageProps }) {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
接下来,在该路径下创建一个文件/pages/issues/[org]/[repo].js,并且在该文件中编写getServerSideProps方法来获取初始化数据。你可以通过http://localhost:3000/issues/facebook/react这样的链接来访问该页面
function fetchIssues(org, repo) {
return fetch(`https://api.github.com/repos/${org}/${repo}/issues`)
.then(response => response.json())
}
export async function getServerSideProps({req, res, query}) {
const { org, repo } = query;
const issues = await fetchIssues(org, repo)
return {
props: {
issues,
org,
repo
},
};
}
现在我们可以将这些数据作为initialData传递给useQuery钩子,之后不使用服务端回传的数据,而是使用react-query的数据
export default function Issues({issues, org, repo}) {
const issuesQuery = useQuery(
["issues", org, repo],
() => fetchIssues(org, repo),
{ initialData: issues }
)
return (
<div>
<Head>
<title>
{`${issuesQuery.data?.length} Issues for ${org}/${repo}`}
</title>
<meta
name="description"
content={
`${issuesQuery.data?.length} Issues for ${org}/${repo}`
}
/>
</Head>
<h1>Issues for {org}/{repo}</h1>
<ul>
{issuesQuery.data.map(issue => (
<Issue key={issue.id} issue={issue} />
))}
</ul>
</div>
)
}
上面的代码会让我们在两个方面受益:
①:初始化数据会让服务端先生成一个页面用于搜索引擎的SEO,让页面在加载时就有了初始化数据
②:通过react-query可以始终使页面保持最新,无需重新加载页面刷新数据。
如果你需要使用getStaticProps来做SSG,可以向react-query传递一个时间戳将其作为initialDataUpdatedAt配置项的值,如果你设置了过期时间,那么在过期后,react-query将会自动获取数据,保证当前展示给用户的数据是最新的。
function fetchIssues(org, repo) {
return fetch(`https://api.github.com/repos/${org}/${repo}/issues`)
.then(response => response.json())
}
export async function getStaticProps({params}) {
const { org, repo } = params;
await fetchIssues(org, repo)
return {
props: {
issues,
org,
repo,
updatedAt: Date.now()
},
};
}
export default function Issues(
{
issues,
org,
repo,
updatedAt
}) {
const issuesQuery = useQuery(
["issues", org, repo],
() => fetchIssues(org, repo),
{
initialData: issues,
initialDataUpdatedAt: updatedAt,
staleTime: 1000 * 60 * 60 * 24// 1 day
}
)
// ...
}
但是上面的做法也有一些不方便的地方,如果在子组件中使用useQuery,你必须将该属性值传递给子组件。如果在一个页面上使用了多个useQuery,你必须要确保你的设置准确。
下面还有另一个方式实现上面的功能,那就是采用缓存注水技术(Cache Hydration)
缓存注水(Cache Hydration)
react-query提供了缓存注水及脱水的功能,想要详细了解技术内容,可以查看如何理解 SSR 中的 hydrate?文章。
首先你需要在_app.js中引入脱水(Hydrate)组件,将其作为QueryClientProvider组件的子组件传入。
并且将queryClient移动到App中,保证缓存的数据只对当前访问的人可用。你可以使用useState钩子来存储queryClient。
import { Hydrate, QueryClient, QueryClientProvider } from 'react-query'
export default function App({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.reactQueryData}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
)
}
接下来将调整getServerSideProps中获取数据的方式。在服务器上创建一个queryClient,并通过它执行所有的获取操作!
你需要为每个请求创建一个新的queryClient,这样不同的页面访问就不会相互获取数据。在getServerSideProps中创建查询客户端将确保这种情况永远不会发生。
在下面的例子中,将会使用queryClient.prefetchQuery来设置缓存,并且使用dehydrate序列化我们的queryClient作为传递值。
import { QueryClient, dehydrate } from 'react-query'
function fetchIssues({queryKey}) {
const [issues, org, repo] = queryKey;
return fetch(`https://api.github.com/repos/${org}/${repo}/issues`)
.then(response => response.json())
}
export async function getServerSideProps({req, res, query}) {
const { org, repo } = query;
const queryClient = new QueryClient()
await queryClient.prefetchQuery(
["issues", org, repo],
fetchIssues
)
return {
props: {
reactQueryData: dehydrate(queryClient),
org,
repo
},
};
}
在最终的页面中,你可以像往常SPA中一样使用useQuery钩子,无需传递上一节中介绍的initialData。因为数据已经是缓存的一部分了。
export default function Issues({org, repo}) {
const issuesQuery = useQuery(
["issues", org, repo],
fetchIssues
)
// ...
你可以在codesandbox中查看这个例子,方便你更加深入的理解上面的内容。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 28 天,点击查看活动详情