You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 13.5.3导航菜单:客户端/服务端组件选择及加载策略疑问

针对你的Next.js菜单实现问题的解答

1. 客户端组件 vs 服务端组件的选择

  • 主菜单优先用服务端组件:菜单属于页面核心导航内容,用服务端组件可以在服务器端直接获取/api/menu.js的数据,不需要等到客户端 hydration 完成再发起请求,首屏加载更快,也更利于SEO(爬虫能直接抓取到导航内容)。
  • 交互部分嵌套客户端组件:因为你需要鼠标悬停加载子菜单的交互逻辑(涉及onMouseEnter这类客户端事件),可以把处理悬停和子菜单加载的部分抽成单独的客户端组件,嵌套在服务端渲染的主菜单组件里。

示例实现:

// Navbar.server.js(服务端组件)
import Link from "next/link";
import SubMenu from "./SubMenu.client"; // 客户端组件,处理悬停逻辑

async function getMenuData() {
  const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/menu.js`, { cache: 'force-cache' });
  if (!res.ok) throw new Error('Failed to fetch menu data');
  return res.json();
}

export default async function Navbar() {
  const menuData = await getMenuData();
  const mainMenu = menuData.filter(item => item.index === "menu");

  return (
    <nav>
      <ul>
        {mainMenu.map((menuItem) => (
          <li key={menuItem.sn} className="relative">
            <Link
              href={menuItem.content}
              className="px-2 py-1 transition-all hover:bg-slate-50 hover:text-stone-900 cursor-pointer"
            >
              {menuItem.heading}
            </Link>
            {/* 嵌套客户端组件处理子菜单加载 */}
            <SubMenu parentSn={menuItem.sn} />
          </li>
        ))}
      </ul>
    </nav>
  );
}

客户端组件处理悬停逻辑:

"use client";
import { useState, useEffect } from "react";

export default function SubMenu({ parentSn }) {
  const [subMenu, setSubMenu] = useState([]);
  const [isHovered, setIsHovered] = useState(false);

  // 悬停时加载子菜单
  useEffect(() => {
    if (!isHovered) return;

    async function fetchSubMenu() {
      try {
        const res = await fetch(`/api/menu.js?parent=${parentSn}`); // 假设接口支持按父级sn查询子菜单
        const data = await res.json();
        setSubMenu(data.filter(item => item.parentSn === parentSn));
      } catch (error) {
        console.error("Failed to fetch submenu:", error);
      }
    }

    fetchSubMenu();
  }, [isHovered, parentSn]);

  return (
    <>
      <div
        onMouseEnter={() => setIsHovered(true)}
        onMouseLeave={() => setIsHovered(false)}
        className="absolute left-0 top-full bg-white shadow-lg p-2"
        style={{ display: isHovered ? 'block' : 'none' }}
      >
        {subMenu.map(item => (
          <a key={item.sn} href={item.content} className="block px-2 py-1 hover:bg-slate-50">
            {item.heading}
          </a>
        ))}
      </div>
    </>
  );
}

2. 菜单数据加载策略选择

  • 如果数据量小:一次性加载所有数据
    直接在服务端组件获取完整的菜单(包括子菜单),然后在客户端组件里根据父级sn筛选子菜单,这样不需要额外的请求,用户悬停时子菜单能立刻显示,体验更流畅。

  • 如果数据量大:按需加载子菜单
    首屏只加载主菜单数据,当用户悬停时再请求对应子菜单的数据,这样能减少首屏的 payload 大小,提升首屏加载速度。这种方式需要你的API支持按父级ID查询子菜单,或者在服务端处理数据拆分。

3. 你当前客户端组件写法的合理性

你用客户端组件的写法本身没问题,但存在两个可优化点:

  • 首屏加载延迟:客户端组件需要等到页面 hydration 完成后才会发起请求,用户可能会看到短暂的空白导航。
  • SEO劣势:爬虫无法抓取到客户端渲染的菜单内容,不利于网站的SEO表现。

所以更推荐服务端组件获取主菜单数据,嵌套客户端组件处理交互的方案。

内容的提问来源于stack exchange,提问作者Trayansh Agarwal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 03:01:28