ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Next.js App Router 中实现 React Router 风格 activeClassName 导航高亮:以 active-class-name 示例为例

2026/9/7 19:31:57 拓冰建站 浏览量
Next.js App Router 中实现 React Router 风格 activeClassName 导航高亮:以 active-class-name 示例为例 Next.js App Router 中实现 React Router 风格 activeClassName 导航高亮以 active-class-name 示例为例【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js本文基于 Next.js 官方仓库中的 active-class-name 示例讲解如何在 App Router 下用next/link与usePathname复刻 React RouterLink组件的activeClassName能力——即在当前路由对应的导航链接上自动附加一个高亮类名。读完本文你将掌握完整的ActiveLink组件实现与逐行原理、动态路由as与静态路由href的匹配差异、usePathname在 Next.js 客户端组件中的底层实现以及该示例的工程结构与启动方式。背景为什么需要自己实现 activeClassNameReact Router 的Link组件提供了一个便捷的activeClassName属性当用户停留在某个链接指向的路由时该链接自动获得指定的类名便于用 CSS 高亮当前页。而 Next.js 自带的Link组件无论是 Pages Router 还是 App Router并没有内建这个能力。本示例的 README 对此的定位很明确ReactRouter has a convenience property on theLinkelement to allow an author to set theactiveclassName on a link. This example replicates that functionality using Nexts ownLinkcomponent with the new app router.也就是说示例的核心目标是在不脱离 Next.js 原生Link的前提下用 App Router 时代推荐的usePathname客户端 Hook 来等价实现当前路由高亮。整条技术路线只依赖两个 Next.js 原生 APInext/navigation的usePathname读取当前 URL 的 pathnamenext/link的Link负责客户端路由跳转与预取。项目结构与启动方式示例采用 App Router 目录结构app/下为路由完整文件布局如下examples/active-class-name/ ├── app/ │ ├── [slug]/page.tsx # 动态路由页用于验证 as 匹配 │ ├── about/page.tsx │ ├── news/page.tsx │ ├── layout.tsx # 根布局 │ └── page.tsx # 首页 ├── components/ │ ├── ActiveLink.tsx # 核心可高亮当前路由的链接组件 │ └── Nav.tsx # 演示用的导航栏 ├── next.config.js ├── package.json └── tsconfig.json根据 README 给出的方式可以用create-next-app引导一个包含该示例的独立项目分别对应 npm、Yarn、pnpm 三种包管理器npx create-next-app --example active-class-name active-class-name-appyarn create next-app --example active-class-name active-class-name-apppnpm create next-app --example active-class-name active-class-name-app生成项目后按 package.json 中定义的脚本运行npm run dev # next dev npm run build # next build npm run start # next start依赖上示例锁定react/react-dom为 18.3.1next使用latest渠道见 package.json因此整套实现针对的是 App RouterReact 18环境。核心实现ActiveLink 组件逐行剖析整个示例的灵魂是 ActiveLink.tsx不到 60 行代码值得逐段拆解。1. 客户端组件声明与类型定义use client; import Link, { LinkProps } from next/link; import React, { PropsWithChildren, useEffect, useState } from react; import { usePathname } from next/navigation;文件顶部的use client是关键usePathname是客户端 Hook因此ActiveLink必须被标记为 Client Component。它的 Props 在LinkProps基础上额外要求一个activeClassNametype ActiveLinkProps LinkProps { className?: string; activeClassName: string; };activeClassName是必填的其余属性href、as、className以及Link支持的其他透传属性全部沿用next/link的原始类型。2. 区分动态路由与静态路由的 URL 提取const getLinkUrl (href: LinkProps[href], as?: LinkProps[as]): string { // Dynamic route will be matched via props.as // Static route will be matched via props.href if (as) return as.toString(); return href.toString(); };这段函数处理了Link中一个容易踩坑的差异静态路由如href/about实际地址由href决定动态路由如href/dynamic-route配合as/posts/1用户看到的真实地址是as若拿href去和当前 pathname 比较就永远不会命中。因此只要传了as就用as参与匹配。示例中的 Nav.tsx 同时演示了这两种形态/about、/blog是静态链接/dynamic-route则对应 app/[slug]/page.tsx 动态段。3. pathname 归一化与类名拼接const ActiveLink ({ children, activeClassName, className, ...props }: PropsWithChildrenActiveLinkProps) { const pathname usePathname(); const [computedClassName, setComputedClassName] useState(className); useEffect(() { if (pathname) { const linkUrl getLinkUrl(props.href, props.as); const linkPathname new URL(linkUrl, location.href).pathname; const activePathname new URL(pathname, location.href).pathname; const newClassName linkPathname activePathname ? ${className} ${activeClassName}.trim() : className; if (newClassName ! computedClassName) { setComputedClassName(newClassName); } } }, [ pathname, props.as, props.href, activeClassName, className, computedClassName, ]); return ( Link className{computedClassName} {...props} {children} /Link ); };几个实现细节值得注意new URL(x, location.href).pathname做归一化usePathname()返回的是当前 URL 的 pathname 部分不含 query 与 hash而href/as可能写成相对路径、带前导/等不同形式。借助URL构造器以当前页面地址为基准解析只取.pathname可以把两侧统一为可直接比较的绝对路径字符串。类名拼接与去重命中时输出${className} ${activeClassName}.trim()否则只保留className。拼接前先trim保证className缺省undefined时不会出现多余空格。useEffect 状态守卫匹配逻辑放在useEffect中、依赖数组覆盖pathname、as、href、activeClassName、className与上一次计算结果且只有newClassName ! computedClassName时才setState避免在路由未变化时触发多余渲染。这也是仅在必要时更新的防御式写法。{...props}全量透传activeClassName与className被显式解构出来后剩余 props 原样交给Link所以replace、shallow、事件回调等Link能力全部保留ActiveLink是对Link的严格超集可以无缝替换。使用演示Nav 组件Nav.tsx 给出了标准用法并顺带展示如何用 styled-jsx 全局样式让当前页可视化use client; import ActiveLink from ./ActiveLink; const Nav () ( nav style jsx global{ .nav-link { text-decoration: none; } .active:after { content: (current page); } }/style ul classNamenav li ActiveLink activeClassNameactive classNamenav-link href/ Home /ActiveLink /li li ActiveLink activeClassNameactive classNameclassName classNamenav-link href/about About /ActiveLink /li ... /ul /nav );要点每个导航项都同时传classNamenav-link常驻样式与activeClassNameactive高亮样式两者职责分离高亮类名可以随项目任意自定义高亮效果通过.active:after { content: (current page); }直接渲染出文字后缀方便肉眼验证匹配是否生效。在实际项目中.active通常对应颜色/字重等视觉差异从示例的 app 目录结构看静态页面有/page.tsx、/aboutabout/page.tsx、/newsnews/page.tsx以及兜底的单段动态路由[slug]。导航中的/dynamic-route链接会被[slug]段捕获正好用来验证链接 pathname 与动态段解析后 pathname 相等的匹配路径。各页面本身都很简单——服务端/客户端组件中直接渲染Nav /加一行文案例如 app/page.tsximport Nav from ../components/Nav; const IndexPage () ( Nav / pHello, Im the index page/p / ); export default IndexPage;而动态页 app/[slug]/page.tsx 特意声明为客户端组件并再次调用usePathname()把实际访问的路径打印出来use client; import { usePathname } from next/navigation; import Nav from ../../components/Nav; const SlugPage () { const pathname usePathname(); return ( Nav / pHello, Im the {pathname} page/p / ); };这为访问任意单段路径都会命中[slug]、且usePathname返回真实 pathname提供了可观测的验证手段。底层原理usePathname 在 Next.js 中如何实现ActiveLink的全部魔法来自一个 Hook看它的源码就能理解匹配为何可靠。usePathname定义在 packages/next/src/client/components/navigation.tsexport function usePathname(): string { useDynamicRouteParams?.(usePathname()) // In the case where this is null, the compat types added in next-env.d.ts // will add a new overload that changes the return type to include null. const pathname useContext(PathnameContext) as string // During build-time instant validation, error if fallback params exist // because usePathname() cant return a sensible value without all params. if ( typeof window undefined process.env.__NEXT_CACHE_COMPONENTS pathname ) { expectCompleteParamsInClientValidation!(usePathname()) return pathname } // Instrument with Suspense DevTools (dev-only) if (process.env.NODE_ENV ! production use in React) { const navigationPromises use(NavigationPromisesContext) if (navigationPromises) { return use(navigationPromises.pathname) } } return pathname }从源码结构看有三个关键事实数据来源是 React ContextusePathname()本质是useContext(PathnameContext)pathname 由 Next.js 客户端路由运行时在导航时注入 Context。因此它是响应式的——每次客户端路由切换后 Context 更新依赖它的useEffect会重新执行这正是ActiveLink不需要监听popstate或pushState就能跟随路由变化的原因。返回值语义官方 JSDoc 给出的例子是/dashboard?foobar下返回/dashboard即不含 query 与 hash 的路径部分。这也解释了为什么ActiveLink中只需比较两个pathname字符串带 query 的同一页面天然归一为同一路径。动态路由约束useDynamicRouteParams?.(usePathname())表明该 Hook 与动态段参数体系联动构建期校验场景下若动态参数不完整会直接报错避免返回半吊子路径。类型层面compat 类型声明 中usePathname的返回类型是string | null兼容 Pages Router 等场景而 App Router 客户端组件的实际实现返回string。示例中if (pathname)的判断正是对这一类型差异的稳健处理。适用边界与可扩展方向把实现放回 README 的语境下有几条边界值得明确精确 pathname 匹配不做前缀匹配linkPathname activePathname是严格相等。若你的导航结构是分类页 列表页如/blog与/blog/1当前实现在/blog/1上不会高亮/blog链接。可以将其扩展为activePathname.startsWith(linkPathname)对非根路径加/边界判断这是社区常见的改进形态。仅客户端生效usePathname是 Client Component API首次服务端渲染阶段ActiveLink只会输出不带activeClassName的基类高亮在客户端水合/路由切换后的useEffect中才生效。对导航样式这类客户端增强而言这是合理代价但不应依赖activeClassName参与首屏 SSR 输出。as与href的优先级只要as存在就以as为准这与next/link的实际跳转地址一致若自定义封装ActiveLink时改动匹配逻辑务必保持这一语义否则动态路由下的导航高亮会全部失效。小结这个不到百行的示例完整演示了 App Router 下当前路由高亮的标准解法用usePathname读取响应式 pathname、用URL归一化后做严格比较、把计算结果通过受控的useState回灌给next/link的className同时保持对Link全部 props 的透传。它既是 Next.js 官方 examples 目录中一个可直接通过create-next-app --example active-class-name拉取的独立应用也是一份关于客户端导航状态如何与路由上下文联动的最小可读参考实现。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考