Next.js 服务端组件和客户端组件区别及使用场景
一、为什么要拆分服务端组件和客户端组件?
一个React组件的功能通常可以分为两块:
- 和用户进行交互,
- 请求数据并展示。
在正常情况下,如果一个React组件需要和服务端进行交互时会有如下的过程:
- 客户端向服务端请求HTML和javascript等资源;
- 接收完成后,应用开始解析HTML,执行Javascript;
- React运行,处理组件并渲染初始页面;
- useEffect执行,发起数据请求,
- 服务端接收到请求之后,进行处理,并将对应的数据返回给客户端,
- 客户端根据返回的数据进行进一步的渲染。
如果组件不包含交互过程: 那其实完全可以把数据请求的过程在服务端进行,然后服务端直接给客户端返回组件数据,客户端利用该数据直接渲染,不用先去解析HTML,直接进行渲染,可以简化正常情况下的渲染过程,由此我们可以将其抽取为服务端组件。
我们再将需要进行交互的部分作为客户端组件,用于处理和用户的交互过程。
二、服务端组件
Next.js中默认使用服务端组件: 服务端组件可以在服务端进行渲染,并且可以对组件内容进行缓存。
服务端组件的优势:
- 数据获取:通过将数据获取移动到服务器端,可以减少获取数据所需的时间,提高性能。同时减少客户端发出的请求次数。
- 安全性:敏感数据和逻辑可以在服务器上处理,不暴露给客户端,提高安全性。
- 缓存:在服务器端呈现可以缓存和重用结果,降低渲染和数据提取成本,并提高性能。
- 性能:通过将非交互式部分移动到服务器,可以优化性能,减少客户端 JavaScript 的量,对于网络速度较慢或设备性能较弱的用户有益。
- 初始页面加载和FCP:通过在服务器上生成 HTML,可以使用户立即查看页面,而无需等待客户端 JavaScript 的下载和执行。
- 搜索引擎优化和社交网络可共享性:呈现的 HTML 可以被搜索引擎和社交网络机器人索引和预览,提高可搜索性和分享性。
- 流式处理:将渲染工作拆分为块并流式传输到客户端,使用户可以更早地查看页面的某些部分,提升用户体验。
服务端组件的渲染方式:
- 静态渲染(默认): 在构建时渲染页面,并且可以缓存(SSG);
- 动态渲染: 具有动态路由的情况下,会每隔一段时间重新验证页面数据是否过期,并重新生成并缓存(ISR);
- 流式处理:App Router中内置: 服务端在处理UI时,可以将工作拆分为多个块,当处理完成后,UI信息会以流的方式传递给客户端; 这样可以使用户更快看到页面的内容 nextjs.org/docs/app/bu…
三、客户端组件:
使用客户端组件的原因:
如果组件纯粹在服务端进行渲染,那组件交互性会很差,在具体的开发环境中我们需要使用浏览器提供给的API,使用React的状态等去实现一定的交互效果。
如何在Nextjs中使用客户端组件:
在React组件的顶部使用"use client"
客户端组件的渲染过程:
在服务端:
- React会将服务端组件处理为
RSC Payload
(其中带有服务端组件的内容和客户端组件的占位符用于指示客户端组件需要渲染的位置). - Next.js利用
RSC Payload
和客户端组件的javascript,生成一个路由html。
在客户端:
- 使用获取到的html先展示一个用于页面跳转和展示的初始页面,该页面此时没有交互功能;
- 使用获取到的
RSC Payload
处理服务端组件和客户端组件的关系,更新React元素树,更新DOM; - 调用JavaScript进行水合(
Hydrate
:将交互需要使用到的事件侦听器加载到静态页面上,使其能够响应用户的交互);
四、何时使用
五、如何更好的使用客户端组件和服务端组件:
服务端组件:
1.组件之间数据共享时:
在服务器上获取数据时,可能需要在不同组件之间共享数据,可以使用fetch获取并调用React的cache
函数(zh-hans.react.dev/reference/r…),
这样当多个组件进行相同的数据获取时,只会发出一个请求,并且返回的数据会被缓存并在各个组件之间共享。
2.将仅限服务器的代码排除在客户端环境之外:
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
process.env.API_KEY
是一个只有服务端上的私有环境变量,为了防止服务端的数据泄露到客户端:
<1>安装server-only
npm install server-only
<2>在服务端组件中引入server-only
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
这样在构建时如果客户端组件导入了getData()
,会在打包时会提示错误
3.在服务端使用第三方组件
由于目前服务端对第三方组件的支持有限,有些第三方组件直接在服务端组件中使用会产生问题,
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* 错误:`useState` 不能在服务器组件中使用 */}
<Carousel />
</div>
)
}
可以使用客户端组件的方式进行处理,将第三方组件放在客户端组件中作为服务端组件的子组件使用。
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* 有效,因为 Carousel 是客户端组件 */}
<Carousel />
</div>
)
}
4.Context Provider 在服务端组件中不受支持,需要放在客户端组件中使用
客户端组件:
1.将客户端组件在组件树中向下移动:
对于一个既有布局又有交互的组件而言,
可以将布局部分放在服务端组件中,将交互部分放在客户端组件中,这样可以减少客户端包的大小。
例如,有一个包含静态元素(logo)的布局和一个使用状态的可以交互的搜索栏。
不需要使用客户端组件实现整个布局,可以将交互逻辑移动到客户端组件SearchBar
,并使用服务端组件实现整体布局。这样就不用将所有组件的 Javascript 发送到客户端。
// SearchBar 是一个客户端组件
import SearchBar from './searchbar'
// Logo 是一个服务端组件
import Logo from './logo'
// Layout 是一个服务端组件
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
2.当需要将服务组件的数据通过props传递给客户端组件时 确保数据是可序列化的,否则需要使用客户端组件调用第三方库或者使用路由处理程序去获取数据。服务端组件具体支持的数据类型参考react.dev/reference/r…
3.混合使用的情况:
混合使用时应该将UI看作一个组件树,从根布局(一个服务端组件)开始,通过添加use client
,在子树上使用客户端组件:
在嵌套过程中有如下注意事项:
<1>. 如果需要在客户端访问服务器上的数据和资源,则需要由客户端去发起请求;
<2>. 当向服务器发起新的请求时,服务端组件会首先被渲染,然后React的`RSC payload 会将服务端组件和客户端组件整合到一个树中
<3>. 客户端组件的处理是在服务端组件之后进行的,所以不能将服务端组件导入到客户端组件之中,如下情况:
'use client'
// 不能将服务器组件导入客户端组件。
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}
如果需要在客户端组件中使用服务端组件可以使用props的方式将其传递给客户端组件
'use client'
import { useState } from 'react'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
// 可以将服务器组件作为子组件或属性传递
// 客户端组件。
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Next.js 中的页面默认是服务器组件
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
转载自:https://juejin.cn/post/7361041787380088867