likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 2

前言

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

1. 服务端组件

1. 服务端组件的优点

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

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

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

  4. 搜索引擎优化SEO

2. 如何启用服务端组件

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

3. 服务端组件流式渲染

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

  1. 新建app/home/page.tsx,嵌套Suspense组件
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后返回结果

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'显式声明为客户端组件,如果直接在服务端组件中使用,会报错。

解决办法:在外层自定义为客户端组件

//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导入服务端组件(错误使用示例):

'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传递给客户端组件

'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}
    </>
  )
}
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. 防止客户端组件调用应在服务端使用的方法

在一个项目中,会同时存在服务端组件和客户端组件。如果单人开发,可以自己注意规范,例如不用客户端组件去请求接口,防止请求路径对外暴露。但在团队协作时,口头约定显然是不保险的。

例如有一个获取接口数据的方法,为了不使请求路径暴露出去,该方法只能在服务端使用。

//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. 安装
npm install server-only
  1. 使用
//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…