前言
在Next.js中,组件主要被划分为两大类:服务端组件与客户端组件。这两类组件各自扮演着重要的角色,同时又会在页面构建过程中相互协作与引用。本文将详细阐述如何使用服务端组件与客户端组件,及使用注意点。
对Next.js14项目搭建还不熟悉的,可先参考我的另一篇文章:给上市公司从0到1搭建Next.js14项目
1. 服务端组件
1. 服务端组件的优点
-
性能提升:减少获取数据的时间;可以缓存服务器渲染的结果供后续请求复用
-
安全提升:可以将敏感数据和逻辑保留在服务器端,避免被客户端暴露
-
页面初始加载速度提升:服务端可以直接生成HTML页面,让用户立即看到内容,无需等待客户端下载、解析和执行JavaScript。并且通过流式传输技术,将渲染工作拆分成块,让用户更早看到静态页面
-
搜索引擎优化SEO
2. 如何启用服务端组件
在Next.js中,默认都是服务端组件
3. 服务端组件流式渲染
当进入一个新页面时,如果需要发送请求以获取页面数据,由于接口响应存在延迟,页面在这段时间内无法呈现。可使用流式渲染方式,先呈现页面的静态内容,动态内容使用骨架屏等方式提示加载中
- 新建app/home/page.tsx,嵌套Suspense组件
js
import { Suspense } from 'react';
import { Spin } from 'antd';
import List from './components/list';
export default function Home() {
return (
<article>
我是首页
<Suspense fallback={<Spin />}>
<List />
</Suspense>
</article>
);
}
- 新建app/home/components/list.tsx
getProjects方法模拟了一个接口,2s后返回结果
js
async function getProjects(): Promise<{ name: string; id: number }[]> {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{
name: 'zhang',
id: 1,
},
{
name: 'li',
id: 2,
},
]);
}, 2000);
});
}
export default async function List() {
const projects = await getProjects();
return (
<div className="App">
{projects.map((o: any) => (
<div key={o.id}>{o.name}</div>
))}
</div>
);
}
访问/home,浏览器将先显示loading加载状态,之后再显示页面内容
注意点:请不要在顶层组件中直接发起接口请求,因为在等待接口响应的await期间,异步组件将不会进行渲染,这会导致其他不依赖于接口数据的静态内容也无法正常显示。正确的做法应像示例中所示,将数据请求的逻辑放在如List子组件中,并在页面组件Home中使用Suspense来包裹这个子组件。如果整个页面的内容都依赖于接口数据,那么可以在布局文件layout中使用Suspense来包裹页面组件,以确保在数据加载完成前显示加载指示器或占位内容。
提示:在Suspense中显示的服务端组件,不会影响SEO,依旧能够被获取到。可将自己的网站部署后,访问:search.google.com/test/rich-r...,进行SEO测试
4. 服务端组件数据共享
在服务端组件中,不能使用React Context来创建数据共享,React Context API只能在客户端组件中使用。同样地,诸如redux或zustand这样的状态管理仓库,它们依赖于React的hook机制,而hook只能在客户端组件的渲染过程中使用,因此在服务端组件中也无法应用这些状态管理库。
在服务端组件中进行数据共享共有三种方式:
-
通过props,父组件将值传递给子组件
-
通过cache API,缓存数据获取或计算结果。常用于缓存数据库操作查询结果
-
通过fetch,Next.js拓展了原生的fetch,增加了缓存和重新验证机制。在缓存期间内多次请求,不会向后端发送请求,而是直接返回缓存的结果
5. 服务端组件调用第三方客户端组件库
在一些第三方库中,使用到了hook,例如useState,但内部又没有使用'use client'显式声明为客户端组件,如果直接在服务端组件中使用,会报错。
解决办法:在外层自定义为客户端组件
js
//app/carousel.tsx
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
服务端组件调用app/carousel.tsx,即可使用第三方库acme-carousel
2. 客户端组件
1. 客户端组件的优点
-
交互性:客户端组件可以使用state,effects和事件监听器
-
浏览器API:客户端组件可以使用浏览器API,例如localStorage
2. 如何启用客户端组件
在文件顶部,import之前,添加"use client",显式声明为客户端组件。在客户端组件中导入其他模块,都将被视为客户端组件的一部分
3. 服务端组件与客户端组件如何选择
只要用到hook的或者浏览器API的,需使用客户端组件,其余情况使用服务端组件
3. 服务端组件与客户端组件混合使用
在一个页面中,不可避免的存在客户端组件与服务端组件混合使用的情况。在使用中,尽量把服务端组件定义在外层去获取数据,把客户端组件定义在最里层去处理用户交互
注意:在服务端组件中可以使用import导入客户端组件,但在客户端组件中不能使用import导入服务端组件,只能把服务端组件作为props传递给客户端组件
在客户端组件中直接使用import导入服务端组件(错误使用示例):
js
'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 />
</>
)
}
如果服务端组件仅包含静态文件,通常不会引发报错。然而,一旦服务端组件中涉及到接口请求,就会导致接口被多次不必要地调用,并且如果接口数据频繁变动。这种情况下,服务端请求得到的接口数据可能与客户端请求的数据不一致,进而引发渲染错误。
正确做法,把服务端组件作为props传递给客户端组件
js
'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}
</>
)
}
js
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>
)
}
4. 保护敏感信息
1. 防止客户端组件调用应在服务端使用的方法
在一个项目中,会同时存在服务端组件和客户端组件。如果单人开发,可以自己注意规范,例如不用客户端组件去请求接口,防止请求路径对外暴露。但在团队协作时,口头约定显然是不保险的。
例如有一个获取接口数据的方法,为了不使请求路径暴露出去,该方法只能在服务端使用。
js
//api.ts
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
为了防止其他开发者在客户端组件中使用该接口,需使用server-only库显式声明为只能在服务端组件中使用
- 安装
js
npm install server-only
- 使用
js
//api.ts
import 'server-only'
//...省略
这样如果在客户端组件中使用api.ts文件中定义的函数,终端将报错。
2. 防止客户端组件调用私有环境变量
在上述api.ts示例中,我们使用了process.env.API_KEY获取了存在环境变量中的token信息。为了防止token信息被无意间暴露出去(在客户端组件中使用),环境变量名不应使用NEXT_PUBLIC_
前缀,例如使用API_KEY,在客户端组件中访问process.env.API_KEY
,将会得到undefined。
如果想定义服务端与客户端都能使用的环境变量,可使用NEXT_PUBLIC_
前缀,例如NEXT_PUBLIC_URL
结尾
对Next.js感兴趣的,可先关注我,后续将继续更新相关内容
参考资料: