零、路由系统概览

从 Next.js 13 开始,Next.js 开始使用新的路由方式——App 路由。新的路由方式支持共享 layout、嵌套路由、loading 状态、错误处理等等。

新的路由方式工作于 app 目录下,默认情况下,该目录内的所有组件都是服务端组件(React Server Component)。如果想让其变为客户端组件,则需要手动指定。

目录和文件

目录用来定义路由。从根目录开始,到每个有 page.js 文件的嵌套目录都是一个合法的路由。

文件用来构建 UI,展示。

app 目录下的每个目录表示一个路由段(Route Segment),每个路由段都可以映射到 URL 路径中的对应的段。

考虑如下目录结构:

  
/app
└── /dashboard
    └── /settings
        └── page.tsx

这会产生如下路由:

  
http://foo.com/dashbord/settings

页面展示的内容,即为 page.tsx 组件。

文件约定

Next.js 提供了一组特殊的文件用来让你更好的创建和组织组件树:

  • layout:页面布局,影响一整个路由段及其子路由。
  • page:路由展示的页面组件,可以通过路由访问到(其他自定义组件无法被公开访问)。
  • loading:加载中的 UI,影响一整个路由段及其子路由。
  • not-found:404 页面,影响一整个路由段及其子路由。
  • error:错误页,影响一整个路由段及其子路由。
  • global-error:全局的错误页。
  • route:服务端 API。
  • template:和 layout 差不多,但是路由切换时会重新创建。
  • default:并行路由的 fallback。

所有后缀为 .js.jsx.tsx 的文件都会被识别。

其他文件

可以将任意文件托管在 app 下,包括自定义的组件、样式表、测试用例等放在 app 目录下的各层目录中。

这是因为目录定义的路由中,只有 page.jsroute.js 是可以公开寻址的。

高级路由模式

Next.js 还提供了一组约定来帮助实现更高级的路由模式,包括:

  • 并行路由(Parallel Routes):允许在同一视图中同时展示多个可以独立导航的页面。常用来做子导航的分屏视图,如仪表盘。
  • 拦截路由(Intercepting Routes):允许拦截一条路由,并在另一条路由的上下文中展示它。用来保持当前页面的上下文。

一、定义一个基本的路由

通过嵌套的目录定义路由段(Route Segment),page.js 定义该路由的展示页面。

考虑如下目录结构:

  
/app
├── /dashboard
│   ├── /analytics
│   ├── /settings
│   │   └── page.tsx
│   └── page.tsx
└── page.tsx

这会定义三个路由://dashboard/dashboard/settings

至于 /dashboard/analytics,它并不是一个可访问的 URL,因为目录内没有 page.js 文件。这个目录可以用来存放其他组件、样式表图片等资源,但不能被公开寻址。

二、页面(Pages)

每个路由都有自己唯一的页面。通过 page.js 文件来定义一个页面。

我们只需要在 page.js 中,默认导出一个组件,该组件会作为对应路由的页面组件来渲染。

  
// `app/page.tsx` is the UI for the `/` URL

export default function Page() {
  return <h1>Hello, Home page!</h1>
}

Tips:

  • page 文件的扩展名可以是 .js.jsx.tsx
  • page 文件永远是一棵路由树的叶子节点。
  • 如果某个路由段想要被公共访问,就必须为其添加 page 文件。
  • page 组件默认是服务端组件,但是可以被设置为客户端组件。
  • page 组件可以获取数据(Fetch Data),后续会介绍数据获取。

三、布局(Layout & Template)

layout 和 template 文件可以允许你创建在多个组件之间共享的 UI 模板。

Layout

layout 组件负责规范 UI 的布局和展示,并在多个路由间共享。在路由跳转时,layout 的状态不会被销毁和重建。

我们通过 layout.js 文件来定义一个 layout。layout 组件需要接受 children 属性,用来指定子组件的位置和布局。

考虑如下目录结构:

  
/app
└── /dashboard
    ├── /settings
    │   └── page.tsx
    ├── layout.tsx  # 添加了 layout 文件 [!code highlight]
    └── page.tsx

我们为其添加了 layout.tsx 文件,该文件需要暴露一个布局组件:

  
// app/dashboard/layout.tsx

export default function DashboardLayout({
  children, // will be a page or nested layout
}: {
  children: React.ReactNode
}) {
  return (
    <section>
      {/* Include shared UI here e.g. a header or sidebar */}
      <nav></nav>
      {children}
    </section>
  )
}

根 Layout(必须)

我们可以在 app 的顶层创建根 layout,根 layout 会影响所有路由。根 layout 是必须的,且必须包含 htmlbody 标签,允许你自定义从服务端返回的 HTML 的结构。

  
// app/layout.tsx

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {/* Layout UI */}
        <main>{children}</main>
      </body>
    </html>
  )
}

嵌套 Layout

默认情况下,多个 layout 在目录层级下是嵌套的,这意味这父 layout 会通过 children 属性来包裹子 layout。你可以为不同的路由段设定不同的 layout,他们会依次嵌套着应用。

例如我们为 /dashbord 创建一个 layout.tsx 文件:

  
/app
├── /dashboard
│   ├── layout.tsx  # 为 dashboard 添加了 layout [!code highlight]
│   └── page.tsx
├── layout.tsx  # 根 layout [!code highlight]
└── page.tsx

现在当我们访问 /dashboard 时,会先应用根 layout,然后嵌套使用 dashboard 的 layout,再包裹 dashboard 的 page。

同理,如果 dashboard 目录下还有其他路由段,那么 dashboard 的 layout 也会应用到其上。

Tips:

  • 只有根 layout 可以包含 <html><body> 标签。
  • 当 layout 和 page 同时定义在一个路由段内时,layout 会包裹 page。
  • layout 组件默认是服务端组件,但是可以将其设置为客户端组件。
  • layout 组件可以获取数据(Fetch Data)。
  • 不能在父 layout 和它的 children 之间传递数据。但是你可以在一个路由内多次获取相同的数据,React 会自动删除多余的请求,不会影响性能。
  • layout 组件不能访问 pathname。但是导入的客户端组件可以通过 usePathname hook 来访问。
  • layout 组件不能访问在它下面的任何路由段。如果你想访问所有路由段,可以在一个客户端组件中,使用 useSelectedLayoutSegmentuseSelectedLayoutSegments hook。
  • 可以使用路由组(Route Groups)来将特定的路由段(Route Segment)纳入或排除在共享布局之外。
  • 可以使用路由组(Route Groups)来创建多个根布局(Root Layout)。

Template

template 和 layout 在包裹子布局或 page 时是一样的。但是不同点在于,layout 在路由跳转时不会被重建(保持其状态),而 template 会在路由跳转时重新创建。这意味着当用于的路由跳转时,所有的子组件都会被销毁重建,DOM 会重新创建挂载,客户端组件的状态会重置,effect 会重新执行同步。

通常在以下情况,你可能会想要使用 template:

  • 需要在导航时,重新执行 useEffect hook。
  • 需要在导航时,重置子客户端组件的状态。

其他例子

Metadata

可以通过 Metadata API 来修改 HTML 的 <head> 标签,比如 title 或者 meta

通过在 layout 或 page 中,导出一个命名为 metadata 的对象或一个 genetateMetadata 的函数,来指定 metadata。

  
// app/page.tsx

import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'Next.js',
}
 
export default function Page() {
  return '...'
}

不应该手动在根 layout 的 <head> 中添加类似 <title><meta> 的标签,而是应该使用 Metadata API,它可以自动处理诸如流式传输和去重 <head> 元素等高级需求。

高亮当前导航

可以使用 usePathname 钩子来检测哪个导航是当前的导航。

因为 usePathname 是一个客户端钩子,我们需要将导航栏组件单独抽出,变成客户端组件。

  
// app/ui/nav-links.tsx

'use client'

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

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

  return (
    <nav>
      <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
        Home
      </Link>

      <Link
        className={`link ${pathname === '/about' ? 'active' : ''}`}
        href="/about"
      >
        About
      </Link>
    </nav>
  )
}
  
// app/layout.tsx

import { NavLinks } from '@/app/ui/nav-links'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <NavLinks />
        <main>{children}</main>
      </body>
    </html>
  )
}

在 Next.js 中,有四种方法可以进行路由导航:

  1. 使用 <Link> 组件。
  2. 使用 useRouter hook(在客户端组件)。
  3. 使用 redirect 函数(在服务器组件)。
  4. 使用原生的 History API。

<Link> 组件是 Next.js 基于 a 标签封装的内置组件,提供了预获取(Prefetch)能力和客户端路由导航的能力。这是在 Next.js 中主要的导航方式,也是推荐的导航方式。

可以从 next/link 中导入 <Link> 组件,并传入 href 属性:

  
// app/page.tsx

import Link from 'next/link'
 
export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}

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')}>
      Dashboard
    </button>
  )
}

尽量使用 <Link>,而不是 useRouter。除非你有非用不可的理由。

redirect 函数

在服务器组件中,使用 redirect 函数来代替 useRouter 钩子。

  
// app/team/[id]/page.tsx

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')
  }
 
  // ...
}

Tips:

  • redirect 默认返回 307(临时重定向)状态码。当在 Server Action 中使用时,它返回一个 303(See Other)状态码,常作为 POST 请求的结果重定向到成功页面。
  • redirect 内部抛出了一个错误,所以在使用它时,应该在 try/catch 块外面调用。
  • redirect 可以在渲染过程中在客户端组件中调用,但不能在事件处理器中使用。
  • redirect 也接受一个绝对 URL,来重定向到外部链接。
  • 如果你希望在渲染进程之前重定向,使用 next.config.js 或 中间件(Middleware)。

原生 History API

Next.js 允许你使用原生的 winodw.history.pushStatewindow.history.replaceState 方法来控制浏览器的浏览历史,而不刷新页面。

Next.js Router 兼容了 pushStatereplaceState,与 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')}>Sort Ascending</button>
      <button onClick={() => updateSorting('desc')}>Sort Descending</button>
    </>
  )
}

window.history.replaceState

使用这个方法来添加一个新的记录到浏览器访问记录中。用户不可以返回到前一个状态。

  
'use client'
 
import { usePathname } from 'next/navigation'
 
export function LocaleSwitcher() {
  const pathname = usePathname()
 
  function switchLocale(locale: string) {
    // e.g. '/en/about' or '/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }
 
  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>French</button>
    </>
  )
}

导航的工作原理(How Routing and Navigation Works)

App Router 使用混合方式(Hibird Approach)进行路由和导航。在服务器上,应用的代码会自动根据路由段(Route Segment)进行代码分割(Code Split)。在客户端,Next.js 预获取(Prefetch)并缓存路由段的代码。这意味着,当用户导航到一个新的路由上时,浏览器并不会刷新页面,仅仅是路由段重新渲染——用以提高路由的体验和性能。

1. 代码分割(Code Splitting)

代码分割允许你将整个应用切分为更小的 bundle 来让用户下载执行。这减少了每次请求时,数据传输的体积和代码执行时间。

服务端组件会自动根据路由段来进行代码分割,这意味着用户在导航时只需要加载它真正需要的那部分代码。

2. 预获取(Prefetching)

预获取是一种在用户真正访问对应路由前,在浏览器后台提前加载资源的方式。

在 Next.js 中,以下两种情况的路由会被预加载:

  • <Link> 组件:对应的路由会在进入用户视口时预加载。预加载发生在页面第一次加载好或它滚动进了用户的视口中。
  • router.prefetch()useRouter 钩子可以允许你手动预加载内容。

3. 缓存(Caching)

Nextjs 有一套基于内存的客户端缓存,称为路由缓存(Router Cache)。当用户在应用中导航时,预加载的服务端组件内容和访问过的路由会被加载到内存缓存中。

这意味着当进行导航时,缓存会被尽可能的复用,以避免重新请求服务器。

4. 部分渲染(Partial Rendering)

部分渲染是,在客户端,仅路由变化后改变的部分需要重新渲染,共享的路由段保持不变。

举例来说,当在 /dashboard/settings/dashboard/analytics 中导航时,settingsanalytics 页面都会被重新渲染,但是共享的 dashboard 不会。

如果没有部分渲染,每次路由导航时,都需要在客户端进行全页面的重渲染。仅渲染改变的路由段可以减少执行时间,提升性能。

5. 软导航(Soft Navigation)

浏览器会在多个页面之间跳转时,执行“硬导航”(Hard Navigation)。Nextjs 的路由会在页面间使用“软导航”,来保证仅改变的路由段重新渲染(部分渲染),其余页面保持其 UI 和 状态。

6. 回退和前进(Back and Forward Navigation)

默认情况下,Nextjs 会在前进和回退时,维持用户的滚动位置,并通过路由缓存来重用路由段。

五、错误处理

错误可以被分为两个类型:预期错误(Expected Error) 和 未捕获异常(Uncaught Exceptions)。

  • 将预期错误当作返回值处理(Model expected errors as return values):不要在 Server Actions 中对预期错误使用 try/catch 块。使用 useFormState 来管理这些错误并将它们返回给客户端。
  • 对未捕获异常使用 Error Boundaries(Use error boundaries for unexpected errors):使用 error.tsxglobal-error.tsx 文件来处理未捕获异常并提供对应的错误页面。

处理预期错误(Handling Expected Errors)

预期错误是那些在操作应用时,日常抛出的错误。比如服务端表单校验或者失败的请求。这些错误需要被显示的处理并返回给客户端。

在 Server Actions 中处理预期错误

使用 useFormState 钩子来管理 Server Action,其中就包括了错误处理。这种方法避免了使用 try/catch 来处理预期错误,而是将其作为返回值。

  
// app/actions.ts

'use server'
 
import { redirect } from 'next/navigation'
 
export async function createUser(prevState: any, formData: FormData) {
  const res = await fetch('https://...')
  const json = await res.json()
 
  if (!res.ok) {
    return { message: 'Please enter a valid email' }
  }
 
  redirect('/dashboard')
}

然后,可以将自定义的 action 传递给 useFormState 钩子并使用返回值 state 来展示错误信息。

  
// app/ui/signup.tsx

'use client'
 
import { useFormState } from 'react'
import { createUser } from '@/app/actions'
 
const initialState = {
  message: '',
}
 
export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState) 
 
  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite">{state?.message}</p> {}
      <button>Sign up</button>
    </form>
  )
}

也可以使用返回的状态,来在客户端组件展示一个 toast。

在服务器组件中处理预期错误

当我们在服务器组件中获取数据(Fetch Data)时,可以使用响应来条件渲染错误信息或重定向。

  
// app/page.tsx

export default async function Page() {
  const res = await fetch(`https://...`)
  const data = await res.json()
 
  if (!res.ok) {
    return 'There was an error.'
  }
 
  return '...'
}

未捕获异常

未捕获异常,是预期之外的错误,意味着 bug 或其他问题。这会抛出错误,然后被 Error boundaries 处理。

  • 常见做法:定义 app/error.tsx,来处理错误。
  • 可选做法:通过嵌套的 error.tsx 来更细粒度地处理错误。
  • 非常规做法:定义 app/global-error.tsx 文件来处理未捕获的错误。这种方法意味着所有未捕获的错误都将通过一个全局的错误处理文件来进行处理,这可能不够灵活,但可以保证一致性。

使用 Error Boundaries

Nextjs 使用 Error Boundaries 来处理未捕获异常。Error Boundaries 会捕获子组件中的错误,并使用对应的错误页来替换崩溃的子组件树。

在路由段内添加 error.tsx 来创建一个 Error Boundary:

  
// app/dashboard/error.tsx

'use client' // Error boundaries must be Client Components
 
import { useEffect } from 'react'
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error)
  }, [error])
 
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  )
}

如果我们想要将错误冒泡到父组件去应用上层的 Error Boundary,可以在 error.js 中继续抛出错误。

在嵌套路由中处理错误

错误会被冒泡到最近的 Error Boundary。这允许我们通过 error.tsx 不同层级的放置,来对错误处理进行颗粒控制。

处理全局错误

虽然不太常见,但是我们可以添加 app/global-error.tsx 来处理错误。全局错误页需要定义自己的 <html><body> 标签,在应用全局错误时,会替换掉根布局(Root Layout)。

  
// app/global-error.tsx

'use client' // Error boundaries must be Client Components
 
export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    // global-error must include html and body tags
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

六、加载中和流(Loading UI & Streaming)

添加 loading.tsx 来创建加载中的样式(基于 React Suspense)。通过它,你可以在加载路由段时展示服务器的加载状态。当加载完成后,新的内容会自动替换加载状态。

即时加载状态

即时加载状态可以在导航后,立即展示加载中的 UI。可以为其添加类似骨架屏和 Snipper 这种状态指示器。

只需在对应目录添加一个 loading.tsx 文件。

  
// app/dashboard/loading.tsx

export default function Loading() {
  // You can add any UI inside Loading, including a Skeleton.
  return <LoadingSkeleton />
}

loading.tsx 会被包裹在 layout.tsx 中,子组件会自动包裹在 layout 下的 <Suspense> 组件中。

Tips:

  • 即使使用以服务器为中心的路由,导航也是立即完成的。
  • 路由跳转是可中断的,这意味着路由的改变并不需要等待内容完全加载。

基于 Suspense 进行流式加载(Streaming with Suspense)

除了使用 loading.tsx,也可以手动创建 Suspense。无论是 Node 服务器还是 Edge 服务器,App Router 都支持基于 Suspense 的流式加载。

一些浏览器不会立即展示流式响应数据,至少需要到 1024 字节的数据才开始处理响应。这通常只会影响极小的项目(“Hello World” application),但不太可能影响到真实项目的用户体验。

什么是流式加载?

为了了解 React 和 Nextjs 中的流式加载,我们需要先理解服务端渲染(Server Side Rendering,SSR)和它的问题。

如果使用 SSR,那么用户看到页面前需要经历以下步骤:

  1. 首先,在服务器上,对应页面需要获取数据(Fetch Data)。
  2. 服务器开始渲染页面的 HTML(脱水)。
  3. 将 HTML、CSS、JS 发送到客户端。
  4. 用户可以看到一个不可交互的页面(因为 JS 没有执行)。
  5. 最后,React 会在客户端进行水合(Hydrate),用户可以对页面进行交互。
  
Time ------->
                      |TTFB         |FCP  |TTI
|:-----A-----:|:--B--:|:-----C-----:|:-D-:|

A: 从服务器获取数据(Fetch Data on Server)
B: 在服务器上渲染 HTML(Rendering HTML on Server)
C: 客户端加载资源(Loading code on The Client)
D: React 进行水合(Hydrating)

这些步骤是顺序执行且阻塞的,这意味着服务器只能在获取所有数据后,才能渲染页面的脱水 HTML。在客户端,React 也只能等页面中所有组件的代码都加载完,再开始水合。

使用 React 和 Nextjs SSR,可以通过尽快交付不可交互页面,来提高客户对加载速度的感知。

然而,这依旧可能会很慢,因为在渲染页面前需要进行数据获取(Fetch Data)。

流式加载允许我们将页面的 HTML 拆成小块(chunk),并逐步发送给客户端。这可以让页面先展示一部分,而不需要等待所有数据加载好。

流式加载在 React 的组件模型中工作的很好,因为每一个组件都可以被认为是一个小块(chunk),那些高优先级的组件和不依赖数据的组件可以被提前发送给客户端让 React 开始水合。低优先级的组件可以在获取到数据后再发送给客户端。

  
Time ------->
        |TTFB   |FCP  |TTI
|:--B--:|:--C--:|:-D-:|
|:---A---:|:-B-:|:--C--:|:-D-:|
|:----A----:|:-B-:|:--C--:|:-D-:|

A: 从服务器获取数据(Fetch Data on Server)
B: 在服务器上渲染 HTML(Rendering HTML on Server)
C: 客户端加载资源(Loading code on The Client)
D: React 进行水合(Hydrating)

流式加载对于因为加载数据慢导致的渲染阻塞有着很大提升。它可以减少首字节时间(TTFB)、首次内容绘制时间(FCP)。也有助于提升可交互时间(TTI),在慢速设备上尤其明显。

例子

<Suspense> 的工作原理是包装一个执行异步操作的组件(比如获取数据),在它加载时展示 fallback(比如骨架屏或 Spinner),然后在异步操作完成时,展示对应的组件。

  
// app/dashboard/page.tsx

import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
 
export default function Posts() {
  return (
    <section>
      <Suspense fallback={<p>Loading feed...</p>}>
        <PostFeed />
      </Suspense>
      <Suspense fallback={<p>Loading weather...</p>}>
        <Weather />
      </Suspense>
    </section>
  )
}

通过使用 <Suspense>,你可以获得如下提升:

  • 流式的服务端渲染——更快的将内容返回给客户端。
  • 选择性水合——React 根据用户交互来考虑哪些组件首先应该交互。

SEO

  • Nextjs 会等待 generateMetada 中的数据获取完,再开始流式传输。这确保流的第一部分一定包含 <head> 标签。
  • 因为流式传输是服务器渲染,所以不会影响 SEO。

状态码

当进行流式传输时,会返回一个 200 的状态码,表示请求成功。

服务器仍然可以在流内容本身中向客户端传递错误,比如重定向或 404。因为响应标头已经传输到了客户端,所以状态码并不会更新。这不会影响 SEO。

七、重定向(Redirecting)

以下是能在 Nextjs 中处理重定向的方法。

API Purpose Where Status Code
redirect 在某些操作或发生某些事件时重定向 服务器组件、Server Action、Route Handlers 307 (Temporary) or 303 (Server Action)
permanentRedirect 在某些操作或发生某些事件时重定向 服务器组件、Server Action、Route Handlers 308 (Permanent)
useRouter 在客户端进行路由跳转 客户端组件的事件处理函数 N/A
redirects in next.config.js 基于访问路径的重定向传入请求(Redirect an incoming request based on a path) next.config.js 文件 307 (Temporary) or 308 (Permanent)
NextResponse.redirect 基于条件的重定向传入请求(Redirect an incoming request based on a condition) 中间件(Middleware) 任意

redirect 函数

redirect 函数允许我们重定向到另一个 URL。可以在 服务器组件、路由处理器(Route Handlers)和 Server Action 中使用 rediect 函数。

  
// app/actions.tsx

'use server'
 
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
 
export async function createPost(id: string) {
  try {
    // Call database
  } catch (error) {
    // Handle errors
  }
 
  revalidatePath('/posts') // Update cached posts
  redirect(`/post/${id}`) // Navigate to the new post page
}

Tips:

  • redirect 默认返回 307(临时重定向)状态码。当在 Server Action 中使用时,它返回一个 303(See Other)状态码,常作为 POST 请求的结果重定向到成功页面。
  • redirect 内部抛出了一个错误,所以在使用它时,应该在 try/catch 块外面调用。
  • redirect 可以在渲染过程中在客户端组件中调用,但不能在事件处理器中使用。
  • redirect 也接受一个绝对 URL,来重定向到外部链接。
  • 如果你希望在渲染进程之前重定向,使用 next.config.js 或 中间件(Middleware)。

permanentRedirect 函数

permanentRedirect 函数允许我们永久地将用户重定向到另一个 URL。可以在 服务器组件、路由处理器(Route Handlers)和 Server Action 中使用 permanentRedirect 函数。

permanentRedirect 通常在修改项目的 URL 后使用。

  
// app/actions.ts

'use server'
 
import { permanentRedirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
 
export async function updateUsername(username: string, formData: FormData) {
  try {
    // Call database
  } catch (error) {
    // Handle errors
  }
 
  revalidateTag('username') // Update all references to the username
  permanentRedirect(`/profile/${username}`) // Navigate to the new user profile
}

Tips:

  • permanentRedirect 默认返回 308(永久重定向)。
  • permanentRedirect 接受一个绝对 URL 来重定向到外部链接。
  • 如果你希望在渲染进程之前重定向,使用 next.config.js 或 中间件(Middleware)。

useRouter 钩子

如果你想要在客户端组件的事件处理器中重定向,可以使用 useRouter 钩子。

  
// app/page.tsx

'use client'
 
import { useRouter } from 'next/navigation'
 
export default function Page() {
  const router = useRouter()
 
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

next.config.js 中使用 redirects

next.config.js 中的 redirects 配置项允许我们对访问的 URL 进行重定向。当你重构了 URL 结构或有一个预先知道的重定向列表时,这很有用。

redirects 支持配置 地址、请求头、cookie 和 query 参数匹配,让你可以基于传入地址,弹性的配置重定向。

  
// next.config.js

module.exports = {
  async redirects() {
    return [
      // Basic redirect
      {
        source: '/about',
        destination: '/',
        permanent: true,
      },
      // Wildcard path matching
      {
        source: '/blog/:slug',
        destination: '/news/:slug',
        permanent: true,
      },
    ]
  },
}

Tips:

  • redirects 可以通过 permanent 参数来控制返回 307 或 308 状态码。
  • redirects 可能在某些平台有限制。比如在 Vercel,最多只能有 1024 个重定向配置。
  • redirects 在中间件(Middleware)之前运行。

在中间件中使用 NextResponse.redirect

中间件(Middleware)允许你在请求完成之前允许一些代码。然后,你可以基于传入的请求,使用 NextResponse.redirect 重定向到一个新的地址。这在你想要基于某些条件进行重定向或者你有大量重定向配置(超过 1024 会被 Vercel 限制)时很有用。

比如你想要在用户鉴权不通过时,重定向到 /login 页面。

  
// middleware.ts

import { NextResponse, NextRequest } from 'next/server'
import { authenticate } from 'auth-provider'
 
export function middleware(request: NextRequest) {
  const isAuthenticated = authenticate(request)
 
  // If the user is authenticated, continue as normal
  if (isAuthenticated) {
    return NextResponse.next()
  }
 
  // Redirect to login page if not authenticated
  return NextResponse.redirect(new URL('/login', request.url))
}
 
export const config = {
  matcher: '/dashboard/:path*',
}

Tips:

  • 中间件在 next.config.js 中的 redirects 之后,渲染之前运行。