1 服务器组件
默认情况下,Next.js 使用服务器组件。
1.1 服务器组件是如何呈现的?
在服务器上,Next.js 使用 React 的 API 来编排渲染。渲染工作被拆分为多个块:按单个路段和Suspense
每个区块分两个步骤呈现:
- React 将服务器组件渲染为一种称为 React Server 组件有效负载 (RSC Payload) 的特殊数据格式。
- Next.js 使用 RSC 有效负载和客户端组件 JavaScript 指令在服务器上呈现 HTML。
然后,在客户端上:
- HTML 用于立即显示路由的快速非交互式预览 - 这仅适用于初始页面加载。
- React Server 组件有效负载用于协调客户端和服务器组件树,并更新 DOM。
- JavaScript 指令用于 hydrate客户端组件,并使应用程序具有交互性。
RSC 有效负载是渲染的 React Server 组件树的紧凑二进制表示。客户端上的 React 使用它来更新浏览器的 DOM。
1.2 渲染策略
1.2.1 静态渲染(默认)
使用静态渲染时,路由在构建时渲染,或在数据重新验证后在后台渲染。结果被缓存,
1.2.2 动态渲染
当路由包含对用户个性化的数据或具有仅在请求时才能知道的信息(例如 cookie 或 URL 的搜索参数)
在渲染过程中,如果发现动态函数或未缓存的数据请求,Next.js将切换到动态渲染整个路由。下表总结了动态函数和数据缓存如何影响路由是静态呈现还是动态呈现:
动态函数 | 数据 | 路线 |
---|---|---|
不 | 缓存 | 静态渲染 |
是的 | 缓存 | 动态渲染 |
不 | 未缓存 | 动态渲染 |
是的 | 未缓存 | 动态渲染 |
注:静态渲染只有在非动态函数和数据缓存同时存在情况
1.2.3 流式
通过流式处理,可以从服务器逐步呈现 UI。工作被拆分为多个块,并在准备就绪时流式传输到客户端。这允许用户在整个内容完成呈现之前立即看到页面的某些部分。
例如:
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
children
:要呈现的实际 UI。
fallback
:如果实际 UI 尚未完成加载,则要代替实际 UI 进行渲染的备用 UI
2 客户端组件
"use client"用于声明服务器和客户端组件模块之间的边界
例如:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
3 组合模式
何时使用服务器和客户端组件?
3.1 将仅限服务器的代码排除在客户端环境之外
npm install server-only
然后导入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()
}
3.2 使用第三方软件包和提供程序
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Error: `useState` can not be used within Server Components */}
<Carousel />
</div>
)
}
由于三方组件使用了客户端组件,服务器组件不能使用useState
则需要包装一层:
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
现在,可以直接在服务器组件中使用:<Carousel />
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}
3.3 使用上下文提供程序
上下文提供者通常在应用的根附近渲染,以共享全局关注点,例如当前主题。
(1)错误例子:
import { createContext } from 'react'
// createContext is not supported in Server Components
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
(2)正确例子:(由于服务器组件不支持 React 上下文,采用children的方式)
app/theme-provider.tsx
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({
children,
}: {
children: React.ReactNode
}) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
app/layout.tsx
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
3.4 交错的服务器和客户端组件
不支持的模式:将服务器组件导入客户端组件
'use client'
// You cannot import a Server Component into a Client Component.
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 />
</>
)
}
支持的模式:将服务器组件作为道具传递给客户端组件
'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}
</>
)
}
将服务器组件作为子项导入
// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}