Skip to content

服务端组件与客户端组件 ​

一、服务端组件 ​

1. 服务端组件的优点 ​

  1. 性能提升:减少获取数据的时间;可以缓存服务器渲染的结果供后续请求复用

  2. 安全提升:可以将敏感数据和逻辑保留在服务器端,避免被客户端暴露

  3. 页面初始加载速度提升:服务器可以直接生成HTML页面,让用户立即看到内容,无需等待客户端下载、解析和执行JavaScript。并且通过流式传输技术,将渲染工作拆分成块,让用户更早看到静态页面

  4. 搜索引擎优化

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只能在客户端组件的渲染过程中使用,因此在服务端组件中也无法应用这些状态管理。

在服务端组件中进行数据共享共有三种方式:

  1. 通过props,父组件将值传递给子组件

  2. 通过cache API,缓存数据获取或计算结果。常用于缓存数据库操作查询结果

  3. 通过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. 客户端组件的优点 ​

  1. 交互性:客户端组件可以使用state,effects和事件监听器

  2. 浏览器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-only

2. 使用 ​

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文件移动到打包后的根目录