创见博客
Server Component 和 Client Component 可以互相包裹吗?
七崽爱吃小饼干2026/07/22阅读 2

在 Next.js App Router 里,Server Component 和 Client Component 经常会一起出现。一个很容易混淆的问题是:

Server Component 和 Client Component 可以互相包裹吗?

答案是:可以组合,但不是任意互相 import。关键要区分两件事:

  • 谁 import 谁。
  • 谁作为 children 被传进去。

Server Component 可以渲染 Client Component

Server Component 可以 import 并渲染 Client Component。

这是最常见的页面拆分方式。

比如文章页中,文章标题、正文、作者、发布时间这些内容不需要浏览器交互,可以放在 Server Component 中;点赞、收藏、评论输入框这些需要事件和状态的部分,则放在 Client Component 中。

示例:

tsx
// app/reader/[articleId]/page.tsx
import LikeButton from './LikeButton';

export default async function ArticlePage() {
  const article = await getArticle();

  return (
    <article>
      <h1>{article.title}</h1>
      <div>{article.content}</div>
      <LikeButton articleId={article.id} />
    </article>
  );
}
tsx
// LikeButton.tsx
'use client';

import { useState } from 'react';

export default function LikeButton({ articleId }: { articleId: number }) {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '已点赞' : '点赞'}
    </button>
  );
}

这里 ArticlePage 是 Server Component,LikeButton 是 Client Component。

这个组合是合法的。

服务端负责生成文章主体 HTML,客户端只接管点赞按钮的交互。

Client Component 不能直接 import Server Component

反过来就不行。

Client Component 不能直接 import 一个 Server Component。

例如:

tsx
'use client';

import ArticleContent from './ArticleContent';

export default function ClientShell() {
  return <ArticleContent />;
}

如果 ArticleContent 是 Server Component,这种写法是不允许的。

原因是 Client Component 的代码会被打包发送到浏览器,而 Server Component 可能包含只能在服务端运行的逻辑,比如:

  • 查询数据库。
  • 读取文件系统。
  • 读取服务端环境变量。
  • 使用服务端密钥。
  • 调用内部服务。

如果允许 Client Component 直接 import Server Component,就可能把服务端代码拉进客户端 bundle,破坏服务端和客户端边界。

所以规则可以简单理解为:

text
Server Component 可以 import Client Component
Client Component 不能 import Server Component

Client Component 可以通过 children 包住 Server Component 的结果

这里还有一个容易误解的地方。

虽然 Client Component 不能直接 import Server Component,但它可以通过 children 接收 Server Component 渲染出来的结果。

示例:

tsx
// page.tsx,Server Component
import ClientShell from './ClientShell';
import ArticleContent from './ArticleContent';

export default async function Page() {
  return (
    <ClientShell>
      <ArticleContent />
    </ClientShell>
  );
}
tsx
// ClientShell.tsx
'use client';

export default function ClientShell({ children }: { children: React.ReactNode }) {
  return <div className="shell">{children}</div>;
}
tsx
// ArticleContent.tsx,Server Component
export default async function ArticleContent() {
  const article = await getArticle();

  return (
    <article>
      <h1>{article.title}</h1>
      <div>{article.content}</div>
    </article>
  );
}

这看起来像是 Client Component 包住了 Server Component。

但本质上不是 Client Component import 了 Server Component。

真正发生的是:

  1. Page 作为 Server Component 负责组织整棵树。
  2. ArticleContent 在服务端执行并生成结果。
  3. 这个结果作为 children 传给 ClientShell。
  4. ClientShell 在客户端 hydration 后获得交互能力。

所以这种模式是合法的。

它在实际项目里非常有用。

比如文章阅读页可以这样拆:

tsx
<ReaderClientShell articleId={article.id} authorId={article.author_id}>
  <ArticleReaderContent article={article} />
</ReaderClientShell>

其中:

  • ReaderClientShell 负责点赞、收藏、评论、目录滚动、阅读记录。
  • ArticleReaderContent 负责服务端输出标题和正文。

这样既能让正文进入初始 HTML,又能保留客户端交互。

props 必须可序列化

Server Component 向 Client Component 传 props 时,还有一个重要限制:props 必须可序列化。

可以传:

tsx
<LikeButton articleId={article.id} title={article.title} />

这类 number、string、boolean、普通对象、数组通常都可以。

不适合传:

tsx
<ClientComponent onSave={serverFunction} db={connection} />

函数、数据库连接、类实例、不可序列化对象不能随便从 Server Component 传给 Client Component。

如果需要触发服务端写入,应该通过 Server Action、API Route 或 Route Handler 这类明确的边界来做。

为什么要这样设计

这个限制不是 Next.js 故意增加复杂度,而是为了让边界清楚。

Server Component 的优势是:

  • 不把组件代码发送到浏览器。
  • 不需要 hydration。
  • 可以直接访问服务端资源。
  • 适合渲染内容主体和 SEO 页面。

Client Component 的优势是:

  • 可以使用 useState、useEffect。
  • 可以绑定 onClick、onChange。
  • 可以访问 window、document、localStorage。
  • 适合处理交互和浏览器 API。

如果两者可以随意互相 import,这些边界就会被打破。

正确的组合方式是:

text
服务端负责数据和内容
客户端负责交互和浏览器能力

一个实用判断方法

写组件时可以用这几个问题判断:

如果组件需要这些能力,通常应该是 Client Component:

  • useState
  • useEffect
  • 点击、输入、提交事件
  • window / document
  • localStorage
  • 浏览器-only 第三方库

如果组件主要做这些事情,通常适合 Server Component:

  • 查询服务端数据
  • 渲染文章正文
  • 渲染商品详情
  • 生成 SEO 友好的内容
  • 使用服务端密钥或内部服务
  • 不需要用户交互

然后用 Server Component 组织页面,把需要交互的最小部分拆成 Client Component。

总结

Server Component 和 Client Component 可以组合,但不是任意互相 import。

可以记住三条规则:

  1. Server Component 可以 import 并渲染 Client Component。
  2. Client Component 不能直接 import Server Component。
  3. Client Component 可以通过 children 接收 Server Component 渲染出来的结果。

所以“能不能互相包裹”的答案是:

表面上可以组合出互相包裹的结构,但代码依赖方向必须从 Server Component 组织到 Client Component,不能让 Client Component 直接 import Server Component。

这也是 App Router 推荐的页面拆分方式:让稳定内容服务端直出,让交互能力在客户端局部增强。

评论
0/100