banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

为什么是 RSC (二)

渐进式渲染#

Important

渐进式渲染,或者称作流式渲染。这不是一个只能在 RSC 中可以享受到的特征,这种渲染模式和 SuspenserenderToPipeableStreamrenderToReadableStream 有关。

但是在 Next.js 中你需要使用 App router RSC 才能享受此特征。所以本节讨论 Next.js 的渲染模式。

由于 RSC 组件支持异步,所以组件和组件平行关系之间的渲染并没有相互依赖性,并且可被拆分。多个组件可以谁先兑现谁先渲染。这在组件之间分别获取不同数据时非常好用。

例如一个页面上,存在两个组件,A 组件获取商品列表并渲染输出,B 组件获取商品分类并输出。两者都是独立的逻辑。

在传统 SSR 模式中,页面中组件的数据需要从页面顶层获取向下传递到组件,这样就会导致 A,B 组件的渲染都要等待页面数据获取完才能喜欢渲染。

假设我们的接口为:

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const fetchGoods = async () => {
  await sleep(1000)
  return [
    {
      name: 'iPhone 15',
      variants: ['Blue'],
    },
    {
      name: 'iPad Pro',
      variant: 'Black',
    },
  ]
}
const fetchCategories = async () => {
  await sleep(3000)
  return ['Electronics', 'Books']
}
export default (props: {
  goods: { name: string; variants: string[] }[]
  categories: string[]
}) => {
  return (
    <div>
      <h1>Goods</h1>
      <ul>
        {props.goods.map((good) => (
          <li key={good.name}>
            {good.name} - {good.variants.join(', ')}
          </li>
        ))}
      </ul>
      <h1>Categories</h1>
      <ul>
        {props.categories.map((category) => (
          <li key={category}>{category}</li>
        ))}
      </ul>
    </div>
  )
}

export const getServerSideProps = async () => {
  const [goods, categories] = await Promise.all([
    fetchGoods(),
    fetchCategories(),
  ])
  return {
    props: {
      goods,
      categories,
    },
  }
}

上面的例子中,服务器响应浏览器至少需要 3s,之后才能在浏览器呈现数据。

如果在 RSC 中,两者之间可以谁先完成谁先渲染。


export default () => {
  return (
    <>
      <Suspense>
        <Goods />
      </Suspense>

      <hr className="my-8 h-1 bg-gray-100" />
      <Suspense>
        <Categories />
      </Suspense>
    </>
  )
}

const Goods = async () => {
  const goods = await fetchGoods()
  return (
    <div>
      <h1>Goods</h1>
      <ul>
        {goods.map((good) => (
          <li key={good.name}>
            {good.name} - {good.variants.join(', ')}
          </li>
        ))}
      </ul>
    </div>
  )
}
const Categories = async () => {
  const categories = await fetchCategories()
  return (
    <div>
      <h1>Categories</h1>
      <ul>
        {categories.map((category) => (
          <li key={category}>{category}</li>
        ))}
      </ul>
    </div>
  )
}

image

可以看到,等待 1s 后首先渲染出了 Goods,然后 2s 之后渲染出 Categories。这便是渐进式渲染的好处,最大程度提升了 First Meaningful Paint (FMP) 和 Largest Contentful Paint (LCP)。

Important

这种渲染方式虽然提升了首屏的性能,但是因为这个特征也会让页面布局的抖动更加明显,在开发过程中应该更加需要注意这点,尽量在 Supsense fallback 中填充一个和原始组件大小相同的占位。

灵活的服务器数据获取#

由于 RSC 中可以使用任何 Nodejs 方法,所以在数据获取上异常方便,我们不必单独编写一个 Server Api,然后只在 Client 去请求 API,也不必在 SSR 中请求接口,把数据水合到 Client 组件中。我们只需要编写到服务端获取数据的方法,然后直接在 RSC 中调用。

例如,我们现在做一个服务器的管理,其中有组件需要服务的状态信息。

在 RSC 之前我们一般这样去获取服务器的状态信息。

首先,在 SSR 时,使用 getServerSideProps 调用 getServerStatus() 把数据返回,然后在 Page 中接收这个 props。如果需要定时去刷新这个状态的话,我们还需要编写一个 API 接口包装这个方法,在 RCC 中轮询。

https://nextjs-book.innei.in/draw/12.server-status-in-ssr-and-csr.json

在 RSC 中,我们直接调用并渲染,然后使用 revalidatePath() 去做数据刷新,无需编写任何 API。

export default function Page() {
  return (
    <div className="flex gap-4">
      <ServerStatus />
      <Revalidate />
    </div>
  )
}

const ServerStatus = async () => {
  // Accessing the server status
  const status = await getServerStatus()

  return <>{/* Render */}</>
}
'use server'
export const revalidateStatus = async () => {
  revalidatePath('/status')
}

'use client'

export const Revalidate = () => {
  useEffect(() => {
    const timerId = setInterval(() => {
        revalidateStatus()
      }, 5000)
    return () => {
      clearInterval(timerId)
    }
  }, [])
  return null
}

这里的 Revalidate 组件 + revalidateStatus 就是利用了 Server Action 的特征去做了页面的数据更新。

看似这里需要写三个文件,又要区分 RSC 和 RCC 好像挺复杂的,但是比起另写 API 和还有手动写 API 的类型定义并且无法做到 End-to-End type safe 的割裂感还是好太多了。

Server Action 的优势#

上一节其实已经利用了 Server Action 完成了页面的数据更新,其实 Server Action 还有其他的用法。

我们知道 Server Action 其实一个 POST 请求,我们编写一个异步的方法,并且标记为 'use server',那么在 RCC 中调用这个方法时,会自动帮你完成:向服务器发送 POST 请求获取这个方法的响应数据,这个数据可以是流式的,并且在此方法中可以调用 revalidate 等方法去触发页面的更新。

文档中一般会告诉你 Server action 来处理表单的提交,触发对数据的更新,最后反应到 UI 上。

不仅如此,其实我们可以利用 Server Action 去获取数据。

还是以上面的例子为例,只不过这次我们全部在 RCC 中实现数据获取和轮询。

```tsx filename="app/server-action/page.tsx" {5,8} 'use client'

import useSWR from 'swr'

import { getLoadAvg } from './action'

export default function Page() {
const { data } = useSWR('get-load-avg', getLoadAvg, {
refreshInterval: 1000,
})
return <>Load Average: {data?.loadavg.join(', ')}</>
}

</Tab>
  <Tab label="app/server-action/action.tsx">
```tsx filename="app/server-action/action.tsx" {1}
'use server'

import os from 'os'

export const getLoadAvg = async () => {
  const avg_load = os.loadavg()

  return {
    loadavg: avg_load,
  }
}

这样不仅少写了一个 API 接口,同时这样的写法也做到了 End-to-End type safe。

另外,推荐阅读:Server Action & Streamable UI


为什么是 RSC - 聊点不一样的 Next.js

此文由 Mix Space 同步更新至 xLog
原始链接为 https://innei.in/posts/tech/why-react-server-component-2


加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。