【Next.js 入门教程系列】03-路由与跳转

原文链接

CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话, 给我的点个star,关注一下吧

上一篇【Next.js 入门教程系列】02-风格化

路由与跳转

本篇包括以下内容:

  • Define dynamic routes
  • Access route and query string parameters
  • Create layouts
  • Show loding UIs
  • Handle errors

Routing Overview

本节代码链接

前面提到过,在 Next.js 中,依据约定俗成每个文件夹中仅有 page.tsx 会被渲染为公共页面。除此之外 Next.js 中还有其他特殊命名规则文件:

  • page.tsx
  • layout.tsx
  • loading.tsx
  • route.tsx
  • not-found.tsx
  • error.tsx

随着某个 page 里包含的内容越来越多,可以将其中的部分内容封装为 component,并放在同目录下(而不需要放到 component 文件夹里),以提高结构的可读性和效率

动态路由

本节代码链接

Typescript 可以直接使用{}解构

interface Props {
  id: num;
}
// 可以像 {id}: Props 这样,直接解构出来
const UserDetailPage = ({ id }: Props) => {
  return <div>UserDetailPage</div>;
};

动态路由,即可以根据参数进行自动适配的路由。比如想要实现 user/userid 这样的路由,就可以使用下面的这种形式。使用中括号将所需的文件夹名包起来

users
  │  page.tsx
  │  UserTable.tsx
  │
  └─[id]
          page.tsx

users/[id]/page.tsx

import React from "react";

interface Props {
  params: { id: number };
}

const UserDetailPage = ({ params: { id } }: Props) => {
  return <div>UserDetailPage {id}</div>;
};

export default UserDetailPage;

这样即可使用 localhost:5050/users/1 这样的形式来访问

Dynamic Routing

嵌套动态路由

比如我们想要实现 users/userid/photos/photoid 这样的路由,其中 userid 和 photoid 为变量,也就是实现动态路由的嵌套。我们仍然可以使用中括号将两个文件夹都括起来,就像如下的文件结构。

users
│  page.tsx
│  UserTable.tsx
│
└─[id]
    │  page.tsx
    │
    └─photos
        └─[photoId]
                page.tsx

需要注意的是,变量名(文件夹名,上结构中的 id 和 photoId)不能重复,因为我们在使用时需要直接使用这个变量名来访问

# users/[id]/photos/[photoId]/page.tsx
import React from "react";

interface Props {
  // 这里的 id 和 photoId 都要和前面文件夹名对应上,才能正确获取到
  params: { id: number, photoId: number };
}

// 同样可以直接解构使用
const PhotoPage = ({ params: { id, photoId } }: Props) => {
  return (
    <div>
      User {id}'s Photo {photoId}
    </div>
  );
};

export default PhotoPage;

以此类推,不管有多少层都可以正常嵌套,注意命名别重复即可

Nested Dynamic Routing

Catch-all 路由

本节代码链接

在 Next.js 中,使用 [...slug] 来捕捉多层并生成动态路由的功能被称为 Catch-All 路由。它允许你在路径中捕捉任意数量的片段,并将它们作为参数传递给页面组件

上文说到可以将文件夹名字用 [] 包起来来生成动态路由,如果在名字前添加 ... ,比如 [...slug] 即可生成一个 Catch-All 路由。他的作用是,无论你加多少层路由都可以识别出来。比如一般来说,想要实现 products/grocery/dairy/milk 这个路由,需要一层一层创建,而使用 Catch-all 路由,只需要创建 products/[...slug]/page.tsx 即可。

products/[...slug]/page.tsx

import React from "react";

interface Props {
  // 注意这里的的 slug 类型为 string 数组
  params: { slug: string[] };
}

const ProductPage = ({ params: { slug } }: Props) => {
  return <div>ProductPage {slug && slug.map((str) => str + " ")}</div>;
};

export default ProductPage;

最终呈现的效果如下:

Catch-All Routing

在该例子中,products 文件夹下仅有 [...slug] 文件夹,并不存在 page.tsx ,即直接访问 localhost:5050/products 会报 404。如果想要这个 slug 为空也可以正常访问,则可以使用 [[...slug]] 的形式命名文件夹,即再套一层中括号。即如下文件层级:

products
└─[[...slug]]
        page.tsx

最终效果如下

Catch-All 在多层级的 tag 或者类别中导航十分好用,只需要在页面中提取出 slug 再去数据库获取,渲染即可

获取参数

本节代码链接

我们经常会在 url 中添加一些参数,比如 users?sortOrder=name 这样,用于排序之类的操作。在 Next.js 中也比较简单,这里给一个例子

import React from "react";
import UserTable from "./UserTable";

// 首先在这里添加可选的参数,比如这里添加一个sortOrder
interface Props {
  searchParams: { sortOrder: string };
}

const UserPage = async ({ searchParams: { sortOrder } }: Props) => {
  return (
    <>
      <h1>User</h1>
      <UserTable sortOrder={sortOrder} />
    </>
  );
};

export default UserPage;

这样就可以通过 localhost:5050/users?sortOrder=name 来传参。

实现排序逻辑(与本章主线无关)

接下来是实现排序的逻辑,首先安装 fast-sort npm

npm i fast-sort

然后在 users/Usertable.tsx 中添加逻辑

import Link from "next/link";
import React from "react";
// 引入 fast-sort
import { sort } from "fast-sort";

interface User {
  id: number;
  name: string;
  email: string;
}

interface Props {
  sortOrder: string;
}

const UserTable = async ({ sortOrder }: Props) => {
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  const users: User[] = await res.json();

  // 排序
  const sortedUsers = sort(users).asc(
    sortOrder === "email" ? (user) => user.email : (user) => user.name
  );
  return (
    <>
      <table className="table table-bordered">
        <thead>
          <tr>
            <th>
              <Link href="/users?sortOrder=name">Name</Link>
            </th>
            <th>
              <Link href="/users?sortOrder=email">Email</Link>
            </th>
          </tr>
        </thead>
        <tbody>
          {sortedUsers.map((user) => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.email}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
};


布局

本节代码链接

上文说到,Next.js 中的 layout.tsx 也是特殊文件,用于布局页面,在某个文件夹下的 layout.tsx 会将其布局应用到其所有的子文件夹中。比如想要在每个页面都添加 NavBar 和 Footer,则只需要根文件夹下的 layout.tsx 中添加即可。用此方法可以减少大量的冗余代码,提升易读性,也更便于后期修改

为某个文件夹创建新的 layout 可以用如下的形式

import React, { ReactNode } from "react";

interface Props {
  // layout 固定需要一个 NeactNode 类型的 children 为参数
  children: ReactNode;
}

const AdminLayout = ({ children }: Props) => {
  return (
    <div className="flex">
      {/* 这里是 NavBar */}
      <aside className="bg-slate-200 p-5 mr-5">Admin Sidebar</aside> <div>
        {children}
      </div> {/* 渲染 children */}
      {/* 这里还可以加 Footer*/}
    </div>
  );
};

export default AdminLayout;

这里是根目录下的 layout 文件,相对会多一些 metadata 之类的内容,无伤大雅,其内容和上面差不多,只不过直接都写到了 export 里面,略掉了格式声明

import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import NavBar from "./NavBar";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  return (
    // 主要修改的还是这部分
    <html lang="en" data-theme="winter">
      <body className={inter.className}>
        <NavBar />
        <main className="p-5">{children}</main>
      </body>
    </html>
  );
}

除此之外,在 global.css 中还可以修改对应 tag 的 style

/*  tailwind 有三个不同层级的样式,作用于不同的域 */
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
  }
}

body {
  color: rgb(var(--foreground-rgb));
}

/*  可以通过@layer base来修改不同层级的样式 */
@layer base {
  h1 {
    /*  修改全局 h1 的样式 */
    @apply font-extrabold text-2xl mb-3;
  }
}

导航

本节代码链接

Link 标签有以下三个特点

  • 只下载目标页面的内容(不会重复下载 css, tsx, js 等内容)
  • 会预先下载所有 Link 的内容(仅在部署环境中)
  • 在客户端缓存页面

程序性导航

比如在需要提交表单后跳转的场景,就需要用到 Button 来跳转,这个时候就要用到程序性导航,下面是一个示例。首先需要将该组件变为客户端组件(因为要获取客户端信息),然后使用 next/navigation 库中的 router 即可实现跳转

"use client"; // 改为客户端组件
// 引入 router,注意是 next/navigation 不是 next/router
import { useRouter } from "next/navigation";
import React from "react";

const NewUserPage = () => {
  // 初始化 router
  const router = useRouter();

  return (
    <>
      <div>NewUserPage</div>
      {/* 直接使用router.push() */}
      <button className="btn btn-primary" onClick={() => router.push("/users")}>
        Create
      </button>
    </>
  );
};

export default NewUserPage;

展示一个 Loading 的 UI

本节代码链接

展示一个 loading 的 UI 很简单,如果只有部分需要加载的组件需要这个功能,只需要拿 Suspense tag 把它包起来即可,其中 fallback 则是加载中显示的内容

// 需要 import 的
import React, { Suspense } from "react";

<Suspense fallback={<p>Loading...</p>}>
  <UserTable sortOrder={sortOrder} />
</Suspense>;

上文我们说到 loading.tsx 也是特殊文件,如果创建该文件,将直接应用于所有的子文件夹,比如下面这样。并且 daisyUI Loading 也有对应的样式

import React from "react";

const Loading = () => {
  return (
    <div className="place-content-center">
      {/* 也有现成的 daisyUI 类可以使用 */}
      <span className="loading loading-infinity loading-lg"></span>
    </div>
  );
};

export default Loading;

除此之外,如下图所示使用 chorme 的 React Devtool ,先选中 suspense 标签,然后可以在这里暂停这个网页,从而看到 suspend tag 里面的内容,也就是渲染时用户看到的内容。更方便于测试

Suspending

处理 Error

本节代码链接

404 not Found

在文件夹中添加 not-found.tsx 即可(同理也是特殊文件,命名必须一字不差)。也和 layout 那些一样,某个文件夹内的会作用到所有子文件夹内,除非这个文件夹内有自己的 not-found.tsx,即可以针对不同的区域设置不同的 404。如 想访问一个不存在的用户和不存在的商品,就可以显示不同的内容

import React from "react";

const NotFoundPage = () => {
  return <div>The requested page doesn&apos;t exist</div>;
};

export default NotFoundPage;

效果如下:

404

其他 Error

其他可能出现的错误就是代码错误了,一些 502、 runtime error 这类的。同样的,添加 error.tsx 来捕捉这类问题

"use client";
import React from "react";

interface Props {
  error: Error;
  reset: () => void;
}

const ErrorPage = ({ error, reset }: Props) => {
  // 可以打印错误到本地日志,这里的是打印到用户端的 console
  console.log("Error", error);
  return (
    <>
      <div>An unexpected error has occurred</div>
      {/* 可以添加一个重试的 button */}
      <button className="btn" onClick={() => reset()}>
        Retry
      </button>
    </>
  );
};

export default ErrorPage;

效果如下:

Other Error

CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话, 给我的点个star,关注一下吧

下一篇讲 API 的构造

下一篇【Next.js 入门教程系列】04-构造 API

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值