Next.js13中文指南-服务端组件和客户端组件的区别
Tags: webdev, javascript, tutorial, react
Published: 2023-09-08 15:42:12

如果想看专栏的方式可以点击进入[Next图册目实践](http://rexweb.link/categories/Next%E5%9B%BE%E5%86%8C%E7%9B%AE%E5%AE%9E%E8%B7%B5/)

在 React 18 之前，使用 React 渲染应用程序的主要方式是完全在客户端进行。Next.js 提供了一种更简单的方式，将应用程序分解为页面，并通过在服务器上生成 HTML 并将其发送到客户端由 React 进行 hydrate，从而在服务器上进行预渲染。然而，这导致了客户端需要额外的 JavaScript 来使初始 HTML 变得可交互。

现在，有了服务端组件和客户端组件，React 可以在客户端和服务器上进行渲染，这意味着你可以在组件级别选择渲染环境。默认情况下，应用路由器使用服务端组件，使你可以轻松地在服务器上渲染组件，减少发送到客户端的 JavaScript 的数量。

## 何时使用服务端组件和客户端组件？

我们建议默认使用服务端组件（在 app 目录中默认），直到你有使用客户端组件的用例为止。以下表格总结了服务端组件和客户端组件的不同用例：

| 你需要做什么？                                                    | 服务端组件 | 客户端组件 |
| ----------------------------------------------------------------- | ---------- | ---------- |
| 获取数据。                                                        | 🉐️        | ❌         |
| 直接访问后端资源                                                  | 🉐️        | ❌         |
| 在服务器上保留敏感信息（访问令牌，API 密钥等）                    | 🉐️        | ❌         |
| 在服务器上保留大型依赖项 / 减少客户端 JavaScript                  | 🉐️        | ❌         |
| 添加交互性和事件监听器（onClick(), onChange()等）                 | ❌         | 🉐️        |
| 使用状态和生命周期效果（useState(), useReducer(), useEffect()等） | ❌         | 🉐️        |
| 使用仅浏览器 API                                                  | ❌         | 🉐️        |
| 使用依赖于状态，效果或仅浏览器 API 的自定义钩子                   | ❌         | 🉐️        |
| 使用 React 类组件                                                 | ❌         | 🉐️        |

> 简单的来说就是当你要使用到 React Api 或者 客户端浏览器的 Event 事件的时候比例 useEffect onClick window.localhost 等与浏览器相关的 api

# 服务端组件

服务端和客户端组件允许开发人员构建跨服务器和客户端的应用程序，将客户端应用程序的丰富交互性与传统服务器渲染的性能提升相结合。

服务端组件让开发者能够更有效地使用服务器资源。例如，你可以把数据获取的操作放在服务器上执行，这样就能更直接地访问你的数据库。一些大型的依赖项如果放在客户端可能会因为 JavaScript 包的大小增大从而导致影响加载的问题影响了获取数据的速度。

但如果这些数据可以在服务器先处理就可以避免这个问题提升渲染的页面的响应性能。

服务端组件让编写 React 应用的体验更接近于编写 PHP 或 Ruby on Rails 应用的体验。你同时可以享受到 React 特性进行 HTML 渲染。

换个表达方式来说，服务端组件就像是一个桥梁，它连接了服务器和客户端，让你可以在服务器上执行一些操作，然后将结果发送到客户端。这样，你就可以在服务器上处理一些复杂的任务，比如数据获取和处理大型数据并行获取，而不需要将这些任务放在客户端上执行，从而提升了应用的性能。

比如上次的代码我们可以改成这样：

```ts
import React from "react";
import RenderBaidu from "../compoents/RenderBaidu";

async function page() {
  const data = fetch("https://api.uomg.com/api/rand.qinghua", {
    next: { revalidate: 60 },
  }).then((res) => res.json());
  const data1 = fetch("https://api.uomg.com/api/rand.qinghua", {
    next: { revalidate: 60 },
  }).then((res) => res.json());
  const data2 = fetch("https://api.uomg.com/api/rand.qinghua", {
    next: { revalidate: 60 },
  }).then((res) => res.json());
  const res = Promise.all([data, data1, data2]);
  const [d, d1, d2] = await res;
  return <RenderBaidu txt={d.content + d1.content + d2.content} />;
}

export default page;
```

为了使过渡到服务端组件更容易，App Router 中的所有组件默认都是服务端组件，包括特殊文件和并置组件。这使你可以自动采用它们，无需额外的工作，就可以获得开箱即用的优秀性能。你也可以选择使用'use client'指令来选择使用客户端组件

# 客户端组件

创建新的客户端组件，你需要在 app 目录中创建一个新的单独文件（例如 app/home-page.tsx 或类似），该文件导出一个客户端组件。要定义客户端组件，需要在文件的顶部（在任何导入之前）添加'use client'指令。这样你才能使用`use hooks`。

> 当然props的传递在服务端组件依然是有效的。但你不能把服务端的handle func传到props里面给`use client`使用这是不允许的！！还记得我们前面说的那句话嘛直到你的组件使用到了客户端组件的时候你就必须想清楚那部分是要在服务端进行操作，那部分是右客户端进行操作。做好组件的隔离。

## 我们什么时候需要使用到客户端组件？

### 1. 处理用户交互事件
例如，你可能有一个按钮，当用户点击时，你希望在客户端执行某些操作。这种情况下，你需要使用客户端组件。

总的来说，服务端组件和客户端组件为开发者提供了更大的灵活性，可以根据组件的用途和需求选择在哪里进行渲染。
``` ts
"use client";

export default function Button() {
  const [count, setCount] = React.useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}
```
### 2.使用浏览器 API
``` ts
"use client";

export default function BrowserComponent() {
  const [width, setWidth] = React.useState(window.innerWidth);

  React.useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>Window width: {width}</div>;
}
```
### 3.使用状态和生命周期钩子
例如，你可能需要使用 useState 或 useEffect 这样的 React 钩子来管理组件的状态和生命周期。这些也需要在客户端组件中进行。
``` ts
"use client";

export default function Counter() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    const intervalId = setInterval(() => {
      setCount((prevCount) => prevCount + 1);
    }, 1000);

    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return <div>Count: {count}</div>;
}
```

# 组件状态的处理
在浏览器器客户端中SPA应用中我们会经常使用React组件状态库进行业务逻辑上的快速开发。但是在next.js这种web应用开发中，当进行路由跳转的时候，页面会向服务端进行请求响应重写进行渲染，这意味着使用的useContext上下文状态将会被重置。

如果你希望在路由跳转后保持上下文状态，你需要将状态保存在一个更持久的地方，例如浏览器的localStorage或sessionStorage。然后，在组件挂载时，你可以从这些存储中读取状态。

这是一个简单的例子，展示了如何使用localStorage来保存和恢复上下文状态：
``` ts
import React, { createContext, useState, useEffect } from 'react';

const MyContext = createContext();

export function MyProvider({ children }) {
  const [state, setState] = useState(() => {
    // 在组件挂载时，尝试从localStorage中恢复状态
    const savedState = localStorage.getItem('myState');
    return savedState ? JSON.parse(savedState) : initialState;
  });

  useEffect(() => {
    // 当状态改变时，将新的状态保存到localStorage 
    // 这里这是一个方式而已，只要你有地方存起来让上面的savedState能initialState即可
    localStorage.setItem('myState', JSON.stringify(state));
  }, [state]);

  return (
    <MyContext.Provider value={[state, setState]}>
      {children}
    </MyContext.Provider>
  );
}

export function useMyContext() {
  return useContext(MyContext);
}
```
然后，你可以在你的组件中使用useMyContext钩子来访问和更新状态，就像你通常使用useContext一样。

请注意，这个例子使用了localStorage，这意味着状态会在浏览器的多个标签页之间共享，并且即使关闭和重新打开浏览器，状态也会被保留。如果你不希望这样，你可以使用sessionStorage代替localStorage，sessionStorage只在当前的浏览器标签页中保留状态，当标签页关闭时，状态会被清除。

另外，这个例子假设你的状态可以被序列化为JSON。如果你的状态包含不能被序列化的值，例如函数或React元素，你可能需要找到其他的解决方案。

# 结语

如果你有任何问题或者需要进一步的帮助，欢迎随时向我提问。我期待看到你使用Next.js来创建更多优秀的Web应用。记住，编程是一门技艺，而每一行代码都是你的作品。所以，无论你在哪里，无论你做什么，都要热爱编程，享受编程带来的乐趣。再次感谢你的阅读，期待在下一篇文章中再次见到你！

![截屏2023-08-17 23.44.00.png](https://x.imgs.ovh/x/2023/09/04/64f5f2c94bf2c.png)

这里是梦兽编程，本次的代码更新将会放在Github[本次项目的Github连接](https://github.com/rexleimo/rex-huaban)中的V3.0分支中

[我的B站视频号](https://space.bilibili.com/106325238?spm_id_from=333.1007.0.0)更多视频动态。

![截屏2023-08-18 00.02.24.png](https://x.imgs.ovh/x/2023/09/04/64f5f2c85195a.png)