Next.js中客户端组件和服务端组件

在 Next.js 中,客户端组件和服务端组件是两种不同的渲染环境,决定了代码在哪里执行(服务器 vs 浏览器),直接影响 SEO、性能和安全性。

📊 核心区别

维度 服务端组件 (Server Component) 客户端组件 (Client Component)
执行环境 Node.js 服务器 用户浏览器
包大小 需要打包发送到浏览器
SEO ✅ 完美支持 ❌ 需要额外处理
访问浏览器 API ❌ 不可能 ✅ 可以 (window, document)
访问后端 API ✅ 直接(服务器间通信) ❌ 需要跨域
交互性 ❌ 无 ✅ 事件监听、状态、效果

🎯 场景示例

场景1:NFT 市场首页

需求:展示热门 NFT 列表,需要 SEO,无需交互

tsx 复制代码
// app/page.tsx - 服务端组件(默认)
// 这是服务端组件,因为没有 'use client'

import { db } from '@/lib/db'
import { fetchNFTsFromIPFS } from '@/lib/ipfs'

export default async function HomePage() {
  // 1. ✅ 在服务器上直接读取数据库
  const featuredNFTs = await db.nft.findMany({
    where: { featured: true },
    take: 10
  })
  
  // 2. ✅ 在服务器上获取 IPFS 数据
  const nftsWithMetadata = await Promise.all(
    featuredNFTs.map(async (nft) => {
      const metadata = await fetchNFTsFromIPFS(nft.ipfsHash)
      return { ...nft, metadata }
    })
  )
  
  // 3. ✅ 安全的 API 调用(不暴露密钥)
  const floorPrice = await fetch(
    'https://api.opensea.io/api/v1/stats',
    { headers: { 'X-API-KEY': process.env.OPENSEA_API_KEY! } }
  ).then(res => res.json())

  return (
    <div>
      <h1>热门 NFT</h1>
      <p>市场地板价: {floorPrice.eth_price} ETH</p>
      
      {/* 服务端组件内包含客户端组件 */}
      <NFTGallery nfts={nftsWithMetadata} />
      
      {/* 统计信息 - 完全静态 */}
      <Stats 
        totalVolume={floorPrice.total_volume}
        totalSales={floorPrice.total_sales}
      />
    </div>
  )
}

场景2:NFT 画廊组件

需求:需要交互(点击收藏、悬停效果)

tsx 复制代码
// components/NFTGallery.tsx
'use client' // 必须声明

import { useState } from 'react'
import { Heart, ShoppingCart } from 'lucide-react'
import { useAccount, useWriteContract } from 'wagmi'

type NFT = {
  id: number
  name: string
  image: string
  price: string
}

export function NFTGallery({ nfts }: { nfts: NFT[] }) {
  const { address, isConnected } = useAccount()
  const [favorites, setFavorites] = useState<number[]>([])
  const { writeContract, isPending } = useWriteContract()
  
  // 1. ✅ 客户端状态
  const handleFavorite = (nftId: number) => {
    setFavorites(prev => 
      prev.includes(nftId) 
        ? prev.filter(id => id !== nftId)
        : [...prev, nftId]
    )
  }
  
  // 2. ✅ 客户端交互
  const handleBuy = async (nft: NFT) => {
    if (!address) {
      // 使用浏览器 API
      alert('请先连接钱包')
      return
    }
    
    writeContract({
      address: '0x...',
      abi: [...],
      functionName: 'buyNFT',
      args: [nft.id],
      value: parseEther(nft.price)
    })
  }
  
  return (
    <div className="grid grid-cols-3 gap-4">
      {nfts.map((nft) => (
        <div 
          key={nft.id}
          className="group relative border rounded-lg p-4 hover:shadow-lg transition-shadow"
          // 3. ✅ 客户端事件
          onMouseEnter={() => console.log('hover')}
        >
          <img src={nft.image} alt={nft.name} />
          <h3>{nft.name}</h3>
          <p>{nft.price} ETH</p>
          
          {/* 4. ✅ 客户端交互按钮 */}
          <div className="flex gap-2 mt-2">
            <button
              onClick={() => handleFavorite(nft.id)}
              className="p-2 hover:bg-gray-100 rounded"
            >
              <Heart className={`w-5 h-5 ${
                favorites.includes(nft.id) ? 'fill-red-500 text-red-500' : ''
              }`} />
            </button>
            
            <button
              onClick={() => handleBuy(nft)}
              disabled={!isConnected || isPending}
              className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
            >
              {isPending ? '购买中...' : '购买'}
            </button>
          </div>
        </div>
      ))}
    </div>
  )
}

场景3:混合使用示例

需求:一个页面包含静态内容 + 动态交互

tsx 复制代码
// app/product/[id]/page.tsx
// 这个页面是服务端组件(默认)

import { db } from '@/lib/db'
import { notFound } from 'next/navigation'
import ProductImage from '@/components/ProductImage' // 客户端
import AddToCart from '@/components/AddToCart' // 客户端
import ProductReviews from '@/components/ProductReviews' // 服务端

export default async function ProductPage({ 
  params 
}: { 
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  
  // 1. ✅ 服务端:从数据库获取数据
  const product = await db.product.findUnique({
    where: { id: parseInt(id) }
  })
  
  if (!product) {
    notFound()
  }
  
  // 2. ✅ 服务端:安全获取外部 API
  const relatedProducts = await fetch(
    `${process.env.INTERNAL_API_URL}/recommendations/${id}`,
    { headers: { 'API-Key': process.env.INTERNAL_API_KEY! } }
  ).then(res => res.json())

  return (
    <div className="max-w-6xl mx-auto p-4">
      {/* 3. ✅ 静态内容 - 服务端渲染 */}
      <h1 className="text-3xl font-bold">{product.name}</h1>
      <p className="text-gray-600 mt-2">{product.description}</p>
      
      {/* 4. ✅ 客户端组件:交互式图片 */}
      <div className="grid grid-cols-2 gap-8 mt-8">
        <ProductImage 
          images={product.images} 
          productId={product.id}
        />
        
        {/* 5. ✅ 客户端组件:购物车交互 */}
        <div>
          <p className="text-2xl font-semibold">${product.price}</p>
          <AddToCart productId={product.id} />
          
          {/* 6. ✅ 服务端组件:评论列表 */}
          <ProductReviews productId={product.id} />
        </div>
      </div>
      
      {/* 7. ✅ 静态相关内容 - 服务端渲染 */}
      <div className="mt-12">
        <h2 className="text-2xl font-semibold mb-4">相关商品</h2>
        <div className="grid grid-cols-4 gap-4">
          {relatedProducts.map((related) => (
            <a 
              key={related.id} 
              href={`/product/${related.id}`}
              className="border rounded-lg p-4 hover:shadow-md"
            >
              <h3 className="font-medium">{related.name}</h3>
              <p className="text-sm text-gray-500">${related.price}</p>
            </a>
          ))}
        </div>
      </div>
    </div>
  )
}

🎯 决策指南:什么时候用什么?

服务端组件

tsx 复制代码
// ✅ 读取数据库/API
// ✅ 访问安全环境变量
// ✅ 大型依赖(避免打包到客户端)
// ✅ 提高性能(减少客户端JS)
// ✅ 更好的SEO

客户端组件

tsx 复制代码
'use client'

// ✅ 用户交互(onClick, onChange)
// ✅ 状态和生命周期(useState, useEffect)
// ✅ 浏览器API(window, document, localStorage)
// ✅ 第三方库(需要DOM的图表、地图)
// ✅ Web3集成(wagmi, 钱包连接)

🔧 最佳实践模式

模式1:服务端获取数据,客户端交互

tsx 复制代码
// 服务端组件
export default async function Dashboard() {
  const data = await getData() // 服务端获取
  
  return (
    <div>
      <StaticContent data={data} />
      <InteractiveChart data={data} /> {/* 客户端组件 */}
    </div>
  )
}

模式2:客户端组件中获取数据

tsx 复制代码
'use client'

export function InteractiveChart({ initialData }) {
  const [data, setData] = useState(initialData)
  
  useEffect(() => {
    // 客户端获取实时数据
    fetch('/api/realtime-data')
      .then(res => res.json())
      .then(setData)
  }, [])
  
  return <Chart data={data} />
}

模式3:动态导入大型客户端组件

tsx 复制代码
import dynamic from 'next/dynamic'

// 避免重型组件影响首屏加载
const HeavyChart = dynamic(
  () => import('@/components/HeavyChart'),
  { 
    ssr: false, // 不在服务端渲染
    loading: () => <Spinner />
  }
)

⚠️ 常见错误

tsx 复制代码
// ❌ 错误:在服务端组件中使用客户端特性
export default async function Page() {
  const [state, setState] = useState(0) // 错误!
  useEffect(() => {}) // 错误!
  
  return <div onClick={() => {}}>点击</div> // 错误!
}

// ✅ 正确:提取为客户端组件
'use client'
export function InteractivePart() {
  const [state, setState] = useState(0)
  return <div onClick={() => setState(state + 1)}>{state}</div>
}

💡 总结

  1. 默认用服务端组件:更好的性能、SEO、安全性
  2. 需要交互时用客户端组件 :加 'use client'
  3. 混合使用:页面的静态部分用服务端,交互部分用客户端
  4. 数据流:服务端获取初始数据 → 传递给客户端组件

一句话决策

问自己:用户需要和它交互吗?

是 → 客户端组件

否 → 服务端组件

相关推荐
海上彼尚1 小时前
Nodejs也能写Agent - 7.基础篇 - MCP
前端·javascript·人工智能·node.js
天若有情6731 小时前
轻量级状态事件总线 eventbusx-js 开源使用教程
开发语言·javascript·npm·开源·事件·事件总线
XMYX-01 小时前
36 - Go exec 执行命令
开发语言·golang
寻道码路1 小时前
LangChain4j Java AI 应用开发实战(二):大模型参数调优实战:Temperature、TopP、MaxTokens 深度解析
java·开发语言·人工智能·aigc
ZC跨境爬虫1 小时前
跟着 MDN 学CSS day_5:掌握属性选择器的存否匹配与子字符串匹配
前端·javascript·css·ui·html
ZC跨境爬虫1 小时前
跟着 MDN 学CSS day_4:(深入理解CSS选择器的核心机制)
前端·javascript·css·交互
吃好睡好便好1 小时前
在Matlab中绘制饼状图
开发语言·学习·matlab·3d·信息可视化
weixin_6681 小时前
DGX-spark上成功部署Voxtral-Mini-4B-Realtime-2602支持realtime ws接口
开发语言·python
燐妤1 小时前
前端HTML编程6:ES6与前后端交互
前端·javascript·学习·html5