Skip to content

Next 服务器组件和客户端组件介绍

前言

服务端组件和客户端组件是 Next.js 中非常重要的概念。如果没有细致的了解过,你可能会简单的以为所谓服务端组件就是 SSR,客户端组件就是 CSR,服务端组件在服务端进行渲染,客户端组件在客户端进行渲染等等,实际上并非如此。本篇就让我们深入学习和探究 Next.js 的双组件模型吧!

服务端组件

介绍

在 Next.js 中 组件默认就是服务端组件

举个例子 新建一个pages/todo/index.js文件

ts
export default async function Page() {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos");
  const data = (await res.json()).slice(0, 10);
  console.log(data);
  return (
    <ul>
      {data.map(({ title, id }) => {
        return <li key={id}>{title}</li>;
      })}
    </ul>
  );
}

请求会在服务端执行,并将渲染后的 HTML 发送给客户端:

因为在服务端执行,console 打印的结果也只可能会出现在命令行中,而非客户端浏览器中。

优势

  • 数据获取:通常服务端环境(网络、性能等)更好,离数据源更近,在服务端获取数据会更快。通过减少数据加载时间以及客户端发出的请求数量来提高性能

  • 安全:在服务端保留敏感数据和逻辑,不用担心暴露给客户端

  • 缓存:服务端渲染的结果可以在后续的请求中复用,提高性能

  • bundle 大小:服务端组件的代码不会打包到 bundle 中,减少了 bundle 包的大小

  • 初始页面加载和 FCP:服务端渲染生成 HTML,快速展示 UI

  • Streaming:服务端组件可以将渲染工作拆分为 chunks,并在准备就绪时将它们流式传输到客户端。用户可以更早看到页面的部分内容,而不必等待整个页面渲染完毕

因为服务端组件的诸多好处,在实际项目开发的时候,能使用服务端组件就尽可能使用服务端组件

限制

虽然使用服务端组件有很多好处,但使用服务端组件也有一些限制,比如不能使用 useState 管理状态,不能使用浏览器的 API 等等。如果我们使用了 Next.js 会报错,比如我们将代码修改为:

ts
import { useState } from "react";

export default async function Page() {
  const [title, setTitle] = useState("");

  const res = await fetch("https://jsonplaceholder.typicode.com/todos");
  const data = (await res.json()).slice(0, 10);
  console.log(data);
  return (
    <ul>
      {data.map(({ title, id }) => {
        return <li key={id}>{title}</li>;
      })}
    </ul>
  );
}
  • 此时浏览器会报错

报错提示我们此时需要使用客户端组件。那么又该如何使用客户端组件呢?

客户端组件

介绍

使用客户端组件,你需要在文件顶部添加一个 use client 声明,修改 app/todo/page.js,代码如下:

ts
"use client";

import { useEffect, useState } from "react";

function getRandomInt(min, max) {
  const minCeiled = Math.ceil(min);
  const maxFloored = Math.floor(max);
  return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled);
}

export default function Page() {
  const [list, setList] = useState([]);

  const fetchData = async () => {
    const res = await fetch("https://jsonplaceholder.typicode.com/todos");
    const data = (await res.json()).slice(0, getRandomInt(1, 10));
    setList(data);
  };

  useEffect(() => {
    fetchData();
  }, []);

  return (
    <>
      <ul>
        {list.map(({ title, id }) => {
          return <li key={id}>{title}</li>;
        })}
      </ul>
      <button
        onClick={() => {
          location.reload();
        }}
      >
        换一批
      </button>
    </>
  );
}

在这个例子中,我们使用了 useEffectuseState 等 React API,也给按钮添加了点击事件、使用了浏览器的 API。无论使用哪个都需要先声明为客户端组件。

注意

注意:use client用于声明服务端和客户端组件模块之间的边界。当你在文件中定义了一个 use client,导入的其他模块包括子组件,都会被视为客户端 bundle 的一部分。

优势

  • 交互性:客户端组件可以使用 state、effects 和事件监听器,意味着用户可以与之交互

  • 浏览器 API:客户端组件可以使用浏览器 API 如地理位置、localStorage 等

服务端组件 VS 客户端组件

注意

注意

服务端组件中可以直接导入客户端组件,但客户端组件中并不能导入服务端组件

"use client"用于声明服务端和客户端组件模块之间的边界。当你在文件中定义了一个 "use client",导入的其他模块包括子组件,都会被视为客户端 bundle 的一部分。

组件默认是服务端组件,但当组件导入到客户端组件中会被认为是客户端组件。客户端组件不能导入服务端组件,其实是在告诉你,如果你在服务端组件中使用了诸如 Node API 等,该组件可千万不要导入到客户端组件中。

但你可以将服务端组件以 props 的形式传给客户端组件:

  • 举例
ts
"use client";

import { useState } from "react";

export default function ClientComponent({ children }) {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      {children}
    </>
  );
}
  • 导入服务器端组件
ts
import ClientComponent from "./client-component";
import ServerComponent from "./server-component";

export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  );
}

各自渲染各自的不影响