Next.js 15 实现小红书网页版查看内容的示例

596 阅读4分钟

Next.js 15 实现小红书网页版查看内容的示例

Next.js 15 引入了 Intercepting Routes 特性,这使得开发者能够创建更加丰富的用户界面和体验。在小红书网页版中,用户可以浏览内容流,并在点击某项内容时查看详细内容,而不必离开当前页面。以下是如何使用 Next.js 15 实现这一功能的示例。

Intercepting routes conventions 拦截路由惯例

Intercepting routes can be defined with the (..) convention, which is similar to relative path convention ../ but for segments.
拦截路由可以使用 (..) 约定来定义,这与相对路径约定 ../ 类似,但用于段。

  • (.) to match segments on the same level.
    (.) 匹配同一级别的段
  • (..) to match segments one level above.
    (..) 匹配上一级段
  • (..)(..) to match segments two level above.
    (..)(..) 匹配两级以上的段
  • (...) to match segments from the root directory.
    (...) 匹配 root 目录中的段

For example, you can intercept the photo segment from whithin the feed segment by creating a (..)photo directory.
例如,您可以通过创建一个 (..)photo 目录来截取 feed 节段中的 photo 节段。

folderStructure

1. 项目结构

首先,我们需要设置项目的基本结构。在 app 目录下,我们可以创建以下文件和目录:

/app
├── @modal/
│   ├── (.)photos/[id]/
│   │   ├── modal.tsx
│   │   ├── page.tsx
│   │   └── default.tsx
│   └── photos/[id]/
│       ├── page.tsx
│       ├── default.tsx
│       ├── global.css
│       ├── layout.tsx
│       └── page.tsx
└── page.tsx

2. 模态窗口组件

@modal/(.)photos/[id]/modal.tsx 中,我们创建一个 Modal 组件,用于显示模态窗口:

// @modal/(.)photos/[id]/modal.tsx
'use client';

import { type ElementRef, useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { createPortal } from 'react-dom';

export function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  const dialogRef = useRef<ElementRef<'dialog'>>(null);

  useEffect(() => {
    if (!dialogRef.current?.open) {
      dialogRef.current?.showModal();
    }
  }, []);

  function onDismiss() {
    router.back();
  }

  return createPortal(
    <div className="modal-backdrop">
      <dialog ref={dialogRef} className="modal" onClose={onDismiss}>
        {children}
        <button onClick={onDismiss} className="close-button" />
      </dialog>
    </div>,
    document.getElementById('modal-root')!
  );
}

3. 模态页面

@modal/(.)photos/[id]/page.tsx 中,我们创建一个页面,用于在模态窗口中显示照片详情:

// @modal/(.)photos/[id]/page.tsx
import { Modal } from "./modal";

export default async function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const photoId = (await params).id;
  return <Modal>{photoId}</Modal>;
}

4. 照片详情页面

@modal/photos/[id]/page.tsx 中,我们创建一个页面,用于直接通过 URL 访问照片详情:

// @modal/photos/[id]/page.tsx
export const dynamicParams = false;

export function generateStaticParams() {
  let slugs = ["1", "2", "3", "4", "5", "6"];
  return slugs.map((slug) => ({ id: slug }));
}

export default async function PhotoPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const id = (await params).id;
  return <div className="card">{id}</div>;
}

5. 首页

app/page.tsx 中,我们创建一个首页,展示照片列表,并允许用户点击照片查看详情:

// app/page.tsx
import Link from 'next/link';

export default function Page() {
  let photos = Array.from({ length: 6 }, (_, i) => i + 1);

  return (
    <section className="cards-container">
      {photos.map((id) => (
        <Link className="card" key={id} href={`/photos/${id}`} passHref>
          {id}
        </Link>
      ))}
    </section>
  );
}

6. 样式和交互

@modal/photos/[id]/global.css 中,我们可以添加全局样式,比如模态的背景遮罩、模态窗口的样式等。

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
    Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}

body,
html {
  height: 100%;
  margin: 0;
}

.modal-backdrop {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.7);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal {
  width: 80%;
  max-width: 500px;
  height: auto;
  max-height: 500px;
  border: none;
  border-radius: 12px;
  background-color: white;
  padding: 20px;
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 48px;
  font-weight: 500;
}

.close-button {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 48px;
  height: 48px;
  background-color: transparent;
  border: none;
  border-radius: 15px; /* Circular shape */
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 500;
  font-size: 24px; /* Adjust font size as needed */
}

.close-button:hover {
  background-color: #eee;
}

.close-button:after {
  content: 'x';
  color: black;
}

.cards-container {
  display: grid;
  grid-template-columns: repeat(3, 200px);
  gap: 16px;
  justify-content: center;
  align-items: center;
  padding: 16px;
}

.card {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
  background-color: #eee;
  border-radius: 8px;
  text-decoration: none;
  color: black;
  font-size: 24px;
  font-weight: 500;
  max-width: 200px;
}

@media (max-width: 600px) {
  .cards-container {
    grid-template-columns: 1fr;
    justify-items: center;
  }

  .card {
    width: 80%;
  }
}

7. Next.js 文档补充

Next.js 官方文档提供了关于 Intercepting Routes 的详细信息,这允许我们在当前布局中加载应用程序其他部分的路由。这种路由模式在想要在不切换用户上下文的情况下显示路由内容时非常有用。例如,点击内容流中的照片时,可以在模态窗口中显示照片,而不需要跳转到新页面。

8. 总结

通过 Next.js 15 的 Intercepting Routes 特性,我们能够实现一个类似于小红书的网页版应用,其中用户可以点击照片查看详情,而无需离开当前页面。这种模态窗口的实现提高了用户体验,使得内容查看更加流畅和直观。通过结合 Next.js 的官方文档,我们可以更深入地理解这一特性,并将其应用于实际项目中。

9. 链接