NextJS不使用getInitialProps获取数据库动态菜单填充导航
针对你在Next.js Pages Router架构下,为Layout外层的全局Navigation组件拉取数据库菜单数据、且不使用_app.js中已不推荐的getInitialProps的需求,以下是3种生产可用的实现思路,按推荐优先级排序:
方案1:使用SWR/React Query做客户端缓存拉取(最推荐)
菜单属于全站通用、更新频率极低的公共数据,用轻量数据请求库做客户端拉取+缓存是性价比最高的方案,不会阻塞页面首屏静态生成,页面切换时也不会重复发起请求,还能自由配置缓存策略。
以SWR为例,实现步骤:
- 安装依赖:
npm install swr - 修改
_app.js代码如下:
import "../styles/globals.css"; import Layout from "../components/Layout/Layout"; import Navigation from "../components/Navigation/Navigation"; import FooterPanel from "../components/FooterPanel/FooterPanel"; import { settings } from "../settings/settings"; import DarkModeSwitch from "../components/DarkModeSwitch/DarkModeSwitch"; import useSWR from 'swr' // 可全局复用的请求方法 const fetcher = (url) => fetch(url).then(res => res.json()) function MyApp({ Component, pageProps, router }) { const layoutType = Component.layout || settings.layouts.standard; // 拉取菜单数据,配置长缓存+关闭焦点重验证,适配菜单低更新的特性 const { data: menu, isLoading } = useSWR('/api/menu', fetcher, { revalidateOnFocus: false, dedupingInterval: 3600000, // 1小时内重复调用直接走缓存 }) return ( <> {/* 可传入loading状态给Navigation组件,渲染骨架屏避免位置空白 */} <Navigation menu={menu} isLoading={isLoading}/> <Layout type={layoutType} router={router}> <Component {...pageProps} key={router.asPath} router={router} /> </Layout> <DarkModeSwitch classes="fixed bottom-5 md:bottom-20 right-0 z-10 mr-4" /> <FooterPanel /> </> ); } export default MyApp;
如果用React Query,逻辑完全一致,只需要把useSWR替换成useQuery即可。
这个方案的优势:
- 不需要侵入每个页面的数据获取逻辑,不用给每个页面单独传菜单props
- 自带请求去重、缓存、错误重试能力,后续菜单更新不需要重新构建整个站点
- 不会阻塞页面hydration流程,首屏加载速度更快
方案2:页面级数据获取统一注入菜单(适合要求首屏无加载闪烁的场景)
如果你要求首屏SSR/SSG输出的HTML里就已经包含完整菜单数据,不想出现客户端请求的loading态,可以封装一个高阶工具函数,统一给每个页面的getStaticProps/getServerSideProps注入菜单数据,_app.js直接从pageProps中读取即可。
实现步骤:
- 封装通用高阶函数:
// utils/withMenuData.js import { fetchMenuFromDB } from '../lib/db' // 你自己封装的数据库读菜单方法 export function withMenuData(pageDataFunc) { return async (context) => { // 先执行页面本身的数据获取逻辑 const pageResult = pageDataFunc ? await pageDataFunc(context) : { props: {} } // 拉取菜单数据 const menu = await fetchMenuFromDB() // 合并props返回 return { ...pageResult, props: { ...pageResult.props, menu } } } }
- 每个页面的数据获取方法用这个函数包裹即可,比如首页:
// pages/index.js import { withMenuData } from '../utils/withMenuData' export default function Home() { return <div>首页内容</div> } export const getServerSideProps = withMenuData(async (context) => { // 原页面本身的数据请求逻辑 return { props: { pageContent: {} } } })
- 修改
_app.js直接读取props里的menu:
function MyApp({ Component, pageProps, router }) { const layoutType = Component.layout || settings.layouts.standard; const { menu } = pageProps return ( <> <Navigation menu={menu}/> <Layout type={layoutType} router={router}> <Component {...pageProps} key={router.asPath} router={router} /> </Layout> <DarkModeSwitch classes="fixed bottom-5 md:bottom-20 right-0 z-10 mr-4" /> <FooterPanel /> </> ); }
这个方案的优势:
- 首屏没有菜单加载闪烁,SEO更友好
- 完全不依赖客户端请求,对禁用JS的环境更友好
缺点:
- 每个页面都要包裹高阶函数,有少量重复代码
- 如果是纯静态生成的站点,菜单更新后需要重新触发全量构建
方案3:全局状态存储菜单数据
如果你的项目本身已经接入了Zustand、Redux、Jotai这类全局状态管理库,可以在应用首次加载时拉取一次菜单数据存入全局状态,Navigation组件直接从全局状态读取即可,逻辑和方案1类似,只是缓存层换成了你项目里已有的状态库。
实现时只需要在_app.js里加个useEffect,首次进入应用时判断如果状态里没有菜单数据就发起请求,后续页面切换直接读缓存即可,不会重复请求。
补充说明:
_app.js中的getInitialProps并没有被完全移除,只是使用后会导致整个应用所有页面都放弃自动静态优化,强制走SSR渲染,性能损耗比较大,非极端场景不推荐使用。
内容的提问来源于stack exchange,提问作者DaveIt

