menu
首页文章相册工具关于
search
...

Next.js 拦截路由

2025/03/06
eye-

Next.js 有多种处理路由的方式。今天我想向你展示如何拦截路由。 让我们从一个简单的例子开始。

创建画廊页面

首先,让我们创建一个包含图片列表的画廊页面。

// app/gallery/page.tsx

export default function Gallery() {
  const images = [
    {
      id: "1",
      src: "https://placehold.co/300x150@2x.png?text=1",
      alt: "gallery image 1",
      width: 300,
      height: 150,
    },
    {
      id: "2",
      src: "https://placehold.co/300x150@2x.png?text=2",
      alt: "gallery image 2",
      width: 300,
      height: 150,
    },
    {
      id: "3",
      src: "https://placehold.co/300x150@2x.png?text=3",
      alt: "gallery image 3",
      width: 300,
      height: 150,
    },
  ]

  return (
    <div className="p-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
      {images.map((image) => (
        <Link key={image.id} href={`/gallery/${image.id}`}>
          <Image
            src={image.src}
            alt={image.alt}
            width={image.width}
            height={image.height}
          />
        </Link>
      ))}
    </div>
  )
}

现在我们有了一个包含图片列表的画廊页面。

添加动态路由

当我们访问像 /gallery/1 这样的 URL 时,它会重定向到 /gallery/1。1 代表图片的 ID。

// app/gallery/[imageId]/page.tsx

import Image from "next/image"

export default function ImagePage({ params }) {
  const { imageId } = params

  const getImageById = (id) => {
    const images = {
      1: {
        id: "1",
        src: "https://placehold.co/300x150@2x.png?text=1",
        alt: "gallery image 1",
        width: 300,
        height: 150,
      },
      2: {
        id: "2",
        src: "https://placehold.co/300x150@2x.png?text=2",
        alt: "gallery image 2",
        width: 300,
        height: 150,
      },
      3: {
        id: "3",
        src: "https://placehold.co/300x150@2x.png?text=3",
        alt: "gallery image 3",
        width: 300,
        height: 150,
      },
    }
    return images[id]
  }

  const image = getImageById(imageId)

  if (!image) {
    return <div>Image not found</div>
  }

  return (
    <div className="container mx-auto p-8">
      <h1 className="text-2xl font-bold mb-4">Image {imageId}</h1>

      <Image
        src={image.src}
        alt={image.alt}
        width={image.width}
        height={image.height}
        className="rounded-lg"
      />
    </div>
  )
}

然后我们为每张图片都有一个动态路由。我们可以直接访问 /gallery/1 来查看图片。

拦截路由

正如你在官方文档中看到的:

拦截路由允许你在当前布局内加载来自应用程序其他部分的路由。当你想要显示路由内容而不让用户切换到不同上下文时,这种路由范式很有用。

这段话有点复杂,难以理解。让我们想象这样一个场景:

我们有一个画廊页面,当我们点击图片时,我们想要显示图片。但我们不希望用户离开画廊页面。你知道在大多数情况下,当用户重定向到新页面时,他们可能不会再回来。

(.) 匹配同一级别的段
(..) 匹配上一级的段
(..)(..) 匹配上两级的段
(...) 匹配根 app 目录的段

让我们使用第一个规则 (.) 来匹配同一级别的段。

// app/(.gallery)/[imageId]/page.js
"use client"

import Image from "next/image"
import { useRouter } from "next/navigation"
import { useState, useEffect } from "react"
import { use } from "react"

export default function ImageModal({ params }) {
  const router = useRouter()

  const unwrappedParams = params instanceof Promise ? use(params) : params
  const imageId = unwrappedParams.imageId

  const [imageData, setImageData] = useState(null)
  const [error, setError] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    if (!imageId) return

    setLoading(true)

    try {
      const images = {
        1: {
          src: "https://placehold.co/300x150@2x.png?text=1",
          alt: "gallery image 1",
          width: 300,
          height: 150,
        },
        2: {
          src: "https://placehold.co/300x150@2x.png?text=2",
          alt: "gallery image 2",
          width: 300,
          height: 150,
        },
        3: {
          src: "https://placehold.co/300x150@2x.png?text=3",
          alt: "gallery image 3",
          width: 300,
          height: 150,
        },
      }

      const image = images[imageId]
      if (image) {
        setImageData(image)
      } else {
        setError(`Image with ID ${imageId} not found`)
      }
    } catch (err) {
      console.error("Error loading image:", err)
      setError("Error loading image data")
    } finally {
      setLoading(false)
    }
  }, [imageId])

  useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key === "Escape") {
        closeModal()
      }
    }

    document.addEventListener("keydown", handleKeyDown)
    document.body.style.overflow = "hidden"

    return () => {
      document.removeEventListener("keydown", handleKeyDown)
      document.body.style.overflow = "auto"
    }
  }, [])

  const closeModal = () => {
    router.back()
  }

  const handleModalClick = (e) => {
    e.stopPropagation()
  }

  return (
    <div
      className="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50"
      onClick={closeModal}
    >
      <div
        className="relative bg-white/10 rounded-lg p-4 max-w-[90%] max-h-[90%] overflow-auto"
        onClick={handleModalClick}
      >
        <div className="flex justify-end items-center mb-4">
          <button
            onClick={closeModal}
            className="text-white hover:text-gray-300 text-2xl"
            aria-label="Close modal"
          >
            &times;
          </button>
        </div>

        <div className="flex justify-center">
          {error ? (
            <div className="text-red-400 p-4">{error}</div>
          ) : loading ? (
            <div className="text-gray-300 p-4">Loading image...</div>
          ) : (
            imageData && (
              <Image
                src={imageData.src}
                alt={imageData.alt}
                width={imageData.width}
                height={imageData.height}
                className="rounded-lg max-h-[70vh] w-auto object-contain"
              />
            )
          )}
        </div>
      </div>
    </div>
  )
}

当我们点击图片时可以看到模态窗口,我们可以在浏览器中看到 URL 是 /gallery/1。然后我们可以将 URL 分享给其他人。

工作原理

在 Next.js 的拦截路由机制中:

  • 当用户通过客户端导航(点击链接)从 /gallery 导航到 /gallery/1 时,Next.js 检测到有一个匹配 (.)gallery/[imageId] 的拦截路由,所以它显示一个模态窗口。
  • 当用户直接访问 /gallery/1(例如,通过在新标签页中打开或刷新页面)时,Next.js 检查是否有匹配此路径的标准路由。如果 gallery/[imageId]/page.js 存在,它会渲染这个页面而不是模态窗口。

这两个文件通常共存:

  • gallery/[imageId]/page.js - 处理直接访问
  • (.)gallery/[imageId]/page.js - 处理拦截路由(模态窗口)

Next.js 根据以下规则决定使用哪一个:

  • 如果用户通过客户端导航从同一级别的路由导航,使用拦截路由
  • 如果用户直接访问 URL 或从其他地方导航,使用标准路由

为了完整的用户体验,应该提供两个文件,但它们可以共享大部分逻辑,仅在呈现样式上有所不同(一个作为完整页面,一个作为模态窗口)。

你可能遇到的国际化问题网页打开微信小程序
目录
  • 创建画廊页面
  • 添加动态路由
  • 拦截路由
  • 工作原理