路由链接与导航

在 Next.js 中有四种方式实现路由导航:

本文将介绍如何使用这些方法,并深入探讨导航的工作原理。

<Link> 是内置组件,它扩展了 HTML <a> 标签的功能,提供预加载和客户端路由导航。这是 Next.js 中推荐的主要路由导航方式。

你可以从 next/link 导入该组件,并通过 href 属性指定目标路由:

import Link from 'next/link'

export default function Page() {
  return <Link href="/dashboard">仪表盘</Link>
}

<Link> 还支持其他可选属性,详见 API 参考文档

使用示例

链接到动态路由段

当链接到动态路由段时,可以使用模板字符串和插值生成链接列表。例如生成博客文章列表:

app/blog/PostList.js
import Link from 'next/link'

export default function PostList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

检测当前活动链接

可以使用 usePathname() 判断链接是否处于活动状态。例如,为活动链接添加特定类名:

'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export function Links() {
  const pathname = usePathname()

  return (
    <nav>
      <ul>
        <li>
          <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
            首页
          </Link>
        </li>
        <li>
          <Link
            className={`link ${pathname === '/about' ? 'active' : ''}`}
            href="/about"
          >
            关于
          </Link>
        </li>
      </ul>
    </nav>
  )
}

滚动到指定 id

Next.js 应用路由器的默认行为是:导航到新路由时滚动到页面顶部,或在前进/后退导航时保持原有滚动位置

如需滚动到特定 id 元素,可以在 URL 后添加 # 哈希链接,或直接将哈希链接作为 href 属性值。这是因为 <Link> 最终会渲染为 <a> 元素。

<Link href="/dashboard#settings">设置</Link>

// 输出
<a href="/dashboard#settings">设置</a>

须知

  • 如果目标页面在视窗中不可见,Next.js 会在导航时自动滚动到页面顶部。

禁用滚动恢复

Next.js 应用路由器的默认行为是:导航到新路由时滚动到页面顶部,或在前进/后退导航时保持原有滚动位置。如需禁用此行为,可以向 <Link> 组件传递 scroll={false},或向 router.push()/router.replace() 传递 scroll: false

// next/link
<Link href="/dashboard" scroll={false}>
  仪表盘
</Link>
// useRouter
import { useRouter } from 'next/navigation'

const router = useRouter()

router.push('/dashboard', { scroll: false })

useRouter() 钩子

useRouter 钩子允许你在客户端组件中以编程方式更改路由。

app/page.js
'use client'

import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      仪表盘
    </button>
  )
}

完整的 useRouter 方法列表,请参阅 API 参考文档

推荐:除非有特殊需求,否则优先使用 <Link> 组件进行路由导航。

redirect 函数

对于服务端组件,请使用 redirect 函数。

import { redirect } from 'next/navigation'

async function fetchTeam(id: string) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}

export default async function Profile({ params }: { params: { id: string } }) {
  const team = await fetchTeam(params.id)
  if (!team) {
    redirect('/login')
  }

  // ...
}

须知

  • redirect 默认返回 307 (临时重定向) 状态码。在服务器动作中使用时,会返回 303 (参见其他),通常用于 POST 请求后重定向到成功页面。
  • redirect 内部会抛出错误,因此应在 try/catch 块外部调用。
  • redirect 可以在客户端组件的渲染过程中调用,但不能在事件处理程序中调用。此时应使用 useRouter 钩子
  • redirect 也支持绝对 URL,可用于重定向到外部链接。
  • 如需在渲染前重定向,可使用 next.config.js中间件

更多信息请参阅 redirect API 参考文档

使用原生 History API

Next.js 允许使用原生 window.history.pushStatewindow.history.replaceState 方法来更新浏览器历史记录栈,而无需重新加载页面。

pushStatereplaceState 调用会与 Next.js 路由器集成,可与 usePathnameuseSearchParams 同步使用。

window.history.pushState

用于向浏览器历史记录栈添加新条目。用户可以导航回之前的状态。例如实现商品排序功能:

'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder: string) {
    const params = new URLSearchParams(searchParams.toString())
    params.set('sort', sortOrder)
    window.history.pushState(null, '', `?${params.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>升序排序</button>
      <button onClick={() => updateSorting('desc')}>降序排序</button>
    </>
  )
}
'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder) {
    const params = new URLSearchParams(searchParams.toString())
    params.set('sort', sortOrder)
    window.history.pushState(null, '', `?${params.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>升序排序</button>
      <button onClick={() => updateSorting('desc')}>降序排序</button>
    </>
  )
}

window.history.replaceState

用于替换浏览器历史记录栈中的当前条目。用户无法导航回之前的状态。例如实现语言切换功能:

'use client'

import { usePathname } from 'next/navigation'

export function LocaleSwitcher() {
  const pathname = usePathname()

  function switchLocale(locale: string) {
    // 例如 '/en/about' 或 '/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }

  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>Français</button>
    </>
  )
}
'use client'

import { usePathname } from 'next/navigation'

export function LocaleSwitcher() {
  const pathname = usePathname()

  function switchLocale(locale) {
    // 例如 '/en/about' 或 '/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }

  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>Français</button>
    </>
  )
}

路由与导航工作原理

应用路由器采用混合方式实现路由和导航。在服务端,你的应用代码会按路由段自动进行代码分割。在客户端,Next.js 会预加载缓存路由段。这意味着当用户导航到新路由时,浏览器不会重新加载页面,只有发生变化的路由段会重新渲染——从而提升导航体验和性能。

1. 代码分割

代码分割允许将应用代码拆分为更小的包,由浏览器下载和执行。这减少了每次请求传输的数据量和执行时间,从而提升性能。

服务端组件使得应用代码能按路由段自动分割。这意味着导航时只会加载当前路由所需的代码。

2. 预加载

预加载是一种在用户访问前提前加载路由的机制。

Next.js 中有两种预加载方式:

  • <Link> 组件:当链接出现在用户视窗中时会自动预加载。预加载发生在页面首次加载或通过滚动进入视窗时。
  • router.prefetch():可通过 useRouter 钩子以编程方式预加载路由。

<Link> 的默认预加载行为(即不指定 prefetch 属性或设为 null 时)会根据 loading.js 的使用情况有所不同。只会预加载共享布局,直到第一个 loading.js 文件为止的组件树,并缓存 30 秒。这降低了获取整个动态路由的成本,同时可以显示即时加载状态以提供更好的视觉反馈。

通过将 prefetch 属性设为 false 可禁用预加载。或者,通过设为 true 可以预加载超出加载边界的完整页面数据。

更多信息请参阅 <Link> API 参考文档

须知

  • 预加载仅在生产环境启用,开发环境不会生效。

3. 缓存

Next.js 有一个称为路由缓存内存客户端缓存。当用户在应用中导航时,预加载的路由段和已访问路由的 React 服务端组件负载会被存储在缓存中。

这意味着导航时会尽可能复用缓存,而不是向服务器发起新请求——通过减少请求次数和数据传输量来提升性能。

详细了解路由缓存的工作原理及配置方法。

4. 部分渲染

部分渲染意味着导航时只有发生变化的路由段会在客户端重新渲染,共享的布局会保持不变。

例如,在兄弟路由 /dashboard/settings/dashboard/analytics 之间导航时,只会渲染 settingsanalytics 页面,共享的 dashboard 布局会保留。

部分渲染工作原理

如果没有部分渲染,每次导航都会导致整个页面在客户端重新渲染。只渲染变化的部分减少了数据传输量和执行时间,从而提升性能。

5. 软导航

浏览器在页面间导航时会执行"硬导航"。Next.js 应用路由器实现了页面间的"软导航",确保只有变化的路由段会重新渲染(部分渲染)。这使得客户端的 React 状态在导航过程中得以保留。

6. 前进/后退导航

默认情况下,Next.js 会在前进/后退导航时保持滚动位置,并复用路由缓存中的路由段。

7. pages/app/ 之间的路由

当从 pages/ 逐步迁移到 app/ 时,Next.js 路由器会自动处理两者之间的硬导航。为了检测从 pages/app/ 的转换,客户端路由器过滤器会通过概率检查应用路由,偶尔可能出现误判。默认情况下这种情况非常罕见,因为我们将误判概率配置为 0.01%。可以通过 next.config.js 中的 experimental.clientRouterFilterAllowedRate 选项自定义此概率。需要注意的是,降低误判率会增加客户端包中生成的过滤器大小。

或者,如果希望完全禁用此功能并手动管理 pages/app/ 之间的路由,可以在 next.config.js 中将 experimental.clientRouterFilter 设为 false。禁用此功能后,任何与应用路由重叠的 pages 动态路由默认将无法正确导航。