【Next.js 14】服务端组件与客户端组件(使用指南与注意点)

前言

在Next.js中,组件主要被划分为两大类:服务端组件与客户端组件。这两类组件各自扮演着重要的角色,同时又会在页面构建过程中相互协作与引用。本文将详细阐述如何使用服务端组件与客户端组件,及使用注意点。

对Next.js14项目搭建还不熟悉的,可先参考我的另一篇文章:给上市公司从0到1搭建Next.js14项目

1. 服务端组件

1. 服务端组件的优点

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

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

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

  4. 搜索引擎优化SEO

2. 如何启用服务端组件

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

3. 服务端组件流式渲染

当进入一个新页面时,如果需要发送请求以获取页面数据,由于接口响应存在延迟,页面在这段时间内无法呈现。可使用流式渲染方式,先呈现页面的静态内容,动态内容使用骨架屏等方式提示加载中

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

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

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

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

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

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

  2. 浏览器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库显式声明为只能在服务端组件中使用

  1. 安装
js 复制代码
npm install server-only
  1. 使用
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感兴趣的,可先关注我,后续将继续更新相关内容

参考资料:

nextjs.org/docs/app/bu...

相关推荐
小吕学编程14 分钟前
ES练习册
java·前端·elasticsearch
Asthenia041221 分钟前
Netty编解码器详解与实战
前端
袁煦丞26 分钟前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛1 小时前
vue组件间通信
前端·javascript·vue.js
一笑code2 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员2 小时前
layui时间范围
前端·javascript·layui
NoneCoder2 小时前
HTML响应式网页设计与跨平台适配
前端·html
凯哥19702 小时前
在 Uni-app 做的后台中使用 Howler.js 实现强大的音频播放功能
前端
烛阴2 小时前
面试必考!一招教你区分JavaScript静态函数和普通函数,快收藏!
前端·javascript
GetcharZp2 小时前
xterm.js 终端神器到底有多强?用了才知道!
前端·后端·go