主题
服务端组件与客户端组件
一、服务端组件
1. 服务端组件的优点
性能提升:减少获取数据的时间;可以缓存服务器渲染的结果供后续请求复用
安全提升:可以将敏感数据和逻辑保留在服务器端,避免被客户端暴露
页面初始加载速度提升:服务器可以直接生成HTML页面,让用户立即看到内容,无需等待客户端下载、解析和执行JavaScript。并且通过流式传输技术,将渲染工作拆分成块,让用户更早看到静态页面
搜索引擎优化
2. 如何启用服务端组件
在Next.js中,默认都是服务端组件
使用注意点:不能将服务端组件导入到客户端组件中,可以将服务端组件作为props传递给客户端组件
3. 服务端组件流式渲染
当进入一个新页面时,如果需要发送请求以获取页面数据,由于接口响应存在延迟,页面在这段时间内无法呈现。可使用流式渲染方式,先呈现页面的静态内容,动态内容使用骨架屏的方式提示加载中
1. 新建app/home/page.tsx,嵌套Suspense组件
tsx
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>
);
}2. 新建app/home/components/list.tsx
getProjects方法模拟了一个接口,1s后返回结果
tsx
async function getProjects(): Promise<{ name: string; id: number }[]> {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{
name: "zhang",
id: 1,
},
{
name: "li",
id: 2,
},
]);
}, 1000);
});
}
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,依旧能够被获取到。可将自己的网站部署后,访问:https://search.google.com/test/rich-results/result,进行SEO测试
4. 服务端组件数据共享
在服务端组件中,不能使用React Context来创建数据共享,React Context API只能在客户端组件中使用。同样地,诸如redux或zustand这样的状态管理仓库,它们依赖于React的hook机制,而hook只能在客户端组件的渲染过程中使用,因此在服务端组件中也无法应用这些状态管理。
在服务端组件中进行数据共享共有三种方式:
通过props,父组件将值传递给子组件
通过cache API,缓存数据获取或计算结果。常用于缓存数据库操作查询结果
通过fetch,Next.js拓展了原生的fetch,增加了缓存和重新验证机制。在缓存期间内多次请求,不会向后端发送请求,而是直接返回缓存的结果
5. 服务端组件调用第三方客户端组件库
在一些第三方库中,使用到了hook,例如useState,但内部又没有使用'use client'显式声明为客户端组件,如果直接在服务端组件中使用,会报错。
解决办法:在外层自定义为客户端组件
tsx
//app/carousel.tsx
"use client";
import { Carousel } from "acme-carousel";
export default Carousel;服务端组件调用app/carousel.tsx,即可使用第三方库acme-carousel
二、客户端组件
1. 客户端组件的优点
交互性:客户端组件可以使用state,effects和事件监听器
浏览器API:客户端组件可以使用浏览器API,例如localStorage
2. 如何启用客户端组件
在文件顶部,import之前,添加"use client",显式声明为客户端组件。在客户端组件中导入其他模块,都将被视为客户端组件的一部分
三、服务端组件与客户端组件如何选择
只要用到hook的或者浏览器API的,需使用客户端组件,其余情况使用服务端组件
四、服务端组件与客户端组件混合使用
在一个页面中,不可避免的存在客户端组件与服务端组件混合使用的情况。在使用中,尽量把服务端组件定义在外层去获取数据,把客户端组件定义在最里层去处理用户交互
注意:在服务端组件中可以使用import导入客户端组件,但在客户端组件中不能使用import导入服务端组件,只能把服务端组件作为props传递给客户端组件
在客户端组件中直接使用import导入服务端组件(错误使用示例):
tsx
"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传递给客户端组件
tsx
"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}
</>
);
}tsx
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>
);
}五、保护敏感信息
1. 防止客户端组件调用应在服务端使用的方法
在一个项目中,会同时存在服务端组件和客户端组件。如果单人开发,可以自己注意规范,例如不用客户端组件去请求接口,防止请求路径对外暴露。但在团队协作时,口头约定显然是不保险的。
例如有一个获取接口数据的方法,为了不使请求路径暴露出去,该方法只能在服务端使用。
ts
//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库显式声明为只能在服务端组件中使用
1. 安装
bash
npm install server-only2. 使用
ts
//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
提示:通过类似API_KEY方式定义的环境变量不会被打包进源代码,需把.env文件移动到打包后的根目录