react-query手把手教程20-服务端渲染

634 阅读4分钟

场景

在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 天,点击查看活动详情