引言
Next.js 是 React 的一个 Web 框架,本篇文章是 “学习NextJs框架”的第一篇教程。
学习之前,默认读者已经熟悉 HTML、CSS、TypeScript、React。
安装
创建新的Next.js应用并在本地运行它。
快速入门
- 创建一个Next.js项目并命名为
my-app。 cd my-app并运行程序。- 访问
http://localhost:3000。
npx create-next-app@latest my-app --yes
cd my-app
npm run dev
-
这里使用的是
npm,如果你更熟悉pnpm、yarn、bun等方式,可以按你熟悉的方式运行项目。 -
--yes使用已保存的首选项或默认值跳过提示。默认设置启用 TypeScript、Tailwind、App Router 和 Turbopack,并带有导入别名@/*。
系统要求
在开始之前,请确保您的系统满足以下要求:
- Node.js 20.9或以后。
- macOS、Windows(包括 WSL)或 Linux。
应用目录
Next.js使用文件系统路由,这意味着应用程序中的路由取决于您构建文件的方式。
文件夹app是程序路由的根目录,若想修改http://localhost:3000的内容,则只需修改app/page.tsx中的内容即可。
若是有其他页面,例如http://localhost:3000\user,则只需设置app/user/page.tsx中的内容即可。
公用文件夹
创建一个 public 文件夹用于存储静态资产,例如图像、字体等。
在代码中,可以使用根路径(/)引用这些资产,例如public/profile.png可引用为/profile.png。
import Image from 'next/image'
export default function Page() {
return <Image src="/profile.png" alt="Profile" width={100} height={100} />
}
项目结构
关于项目结构,Next.js官方文档写的很好
路由
捕获所有路由片段
使用 doc/[...slug]/page.tsx 可以知晓doc路由下所有的子路由。
export default async function Docs({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
if(slug?.length === 2) {
return (
<h1>
Viewing docs for feature {slug[0]} and concept {slug[1]}
</h1>
);
} else if (slug?.length === 1) {
return <h1>Viewing docs for feature {slug[0]}</h1>
}
return <h1>Viewing docs</h1>
}
-
在上述代码中,如果你访问
http://localhost:3000/docs/routing,那么会得到内容Viewing docs for feature routing -
同理,访问
http://localhost:3000/docs/routing/catch-all-segments,那么会得到内容Viewing docs for feature routing and concept catch-all-segments -
如果你在
docs目录中没有page.tsx,那么访问http://localhost:3000/docs是会报错的。如果你想得到内容Viewing docs而不是去单独创建一个page.tsx,那么你就需要将文件夹改名为:[[...slug]],即为doc/[[...slug]]/page.tsx

自定义404页面not-found.tsx
创建app/not-found.tsx文件,这个文件就是项目404页面。
如果你希望某些页面单独使用404,例如“未找到产品”、“没有该用户”等页面级别的404,则只需要在对应文件夹下创建页面文件即可,例:产品的评论数量不会超过100,超过的就要404,那么可以写如下代码:
这是页面代码:
// app/products/[productId]/reviews/[reviewId]/page.tsx
import { notFound } from "next/navigation";
export default async function ProductReview({
params,
}: {
params:Promise<{ productId:string;reviewId:string}>;
}) {
const { productId,reviewId }= await params;
if(parseInt(reviewId) > 1000) {
notFound();
}
return (
<h1>Review {reviewId} for product {productId}</h1>
);
}
这是404代码:
// app/products/[productId]/reviews/[reviewId]/not-found.tsx
"use client";
import { usePathname } from "next/navigation"
export default function NotFound() {
const pathname = usePathname();
const productId = pathname.split("/")[2];
const reviewId = pathname.split("/")[4];
return (
<div>
<h2>
Review {reviewId} not found for product {productId}
</h2>
</div>
);
}
-
访问
http://localhost:3000/products/1/reviews/1001就会出现Review not found,而不是app/not-found.tsx的内容了。 -
也就是说层级深的404页面就会覆盖层级浅的404页面,即
app/products/[productId]/reviews/[reviewId]/not-found.tsx覆盖了app/not-found.tsx -
"use client"代表着该组件是客户端渲染组件,因为Next.js默认所有的React组件都是服务器端组件,这里使用了import { usePathname } from "next/navigation"Next的Hook,只能在客户端使用,所以需要写上"use client"
公开访问&私有访问
一个路由只有在添加了page.tsx或者page.js才会成为公开可访问的。
-
如果你想要某些文件只在内部使用,不对外公开链接,那么可以在文件夹中添加下划线前缀“_”,这样该文件夹及其所有子文件夹都会被排除在外
-
如果你的项目确实需要用到下划线“_”,请使用“%5F”代替。
-
这个并非必须这样做,你也可以把文件夹放到
app目录之外,以确保不会公开访问。
路由组
路由组可以在不影响URL结构的同时让项目结构更有组织性
省流,文件夹使用圆括号"()“包裹即可,被包裹的将不会在路由上显示
例如:app/(auth)/login可通过http://localhost:3000/login访问。
而app/auth/login则是通过http://localhost:3000/auth/login访问
值得注意的是,路线组实际上是唯一可以在不影响URL的情况下在不同路由之间共享布局的方式
假设有需求:http://localhost:3000/register、http://localhost:3000/login为注册和登录页面。直接在app目录下创建app/register/page.tsx确实可以,但是如果是多人开发(甚至自己过个十天半个月的)就会浪费时间来寻找注册和登录页面代码的位置,这样很不好,没有组织性。当然,也有解决办法,就是app/auth/register/page.tsx和app/auth/login/page.tsx,但是这样访问链接就变了,那么有没有不更换访问链接,但同时注册和登录的代码文件又放到一起的方法呢?答案是有的,就是文件夹使用圆括号“()”包裹,app/(auth),将注册和登录放到这个文件夹下,访问链接不变,同时代码也放到了一起。
布局
与page.tsx一样,layout.tsx是Next.js约定俗成的文件命名。
基础用法
export const metadata ={
title: 'Next.js',
description:'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children:React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
app根目录下必须要有一个layout.tsx,此为根布局;如果你删除了它,那么在下次运行next dev时,它会自动重新生成文件。
嵌套多布局
除了根布局,你可以在其他页面添加layout.tsx,并编写代码,当访问对应页面时,布局将会嵌套显示。
例如在app/products/[productId]/layout.tsx添加如下代码:
export default function ProductDetailsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
{children}
<h2>Featured products</h2>
</>
);
}
运行结果,如下图显示:

多个根布局
有时候希望除了登录和注册页面,其他页面都要有页眉和页脚。如果我们在根布局app/layout.tsx中添加页眉页脚,它将应用与所有的页面,包括登录注册页面,这显然与我们需要的不符。
这时候就需要用到之前的路由组功能了。
最终目录结构可能如下:
app
├─(auth)
│ ├─login
│ ├─register
│ └─layout.tsx // 根布局
├─(dashboard)
│ ├─page1
│ ├─page2
│ ├─layout.tsx // 根布局
│ └─page.tsx // 根页面
│ ...
元数据metadata
最直观、最能体现的就是页面的标题了。Next.js提供了API允许我们为每个页面定义元数据。
规则
-
layout.tsx和page.tsx都可以导出元数据。Layout元数据适用于其所有页面,而页面元数据仅针对该特定页面。 -
元数据遵循自上而下的顺序,从根级别开始。
-
当元数据存在于路由的多个位置时,它们会合并在一起,页面元数据将覆盖对应属性的布局元数据。
权重比较:页面元数据 > 页面布局元数据 > 根页面元数据 > 根页面布局元数据,优先显示页面元数据。
- *(重要)*在使用了
"use client";的页面上无法工作。需要将元数据保留到几组件中,并且将任何客户端功能提取到单独的组件中。
静态元数据
在page.tsx或者layout.tsx中,填写如下代码:
export const metadata ={
title: 'Next.js',
description:'Generated by Next.js',
}
- 此为元数据对象。
动态元数据
这在元数据依赖于动态信息(如当前路由参数、外部数据或父段中定义的元数据)时非常有用。
import { Metadata } from "next";
type Props = {
params: Promise<{ productId: string }>;
};
export const generateMetadata = async ({
params,
}: Props): Promise<Metadata> => {
const id = (await params).productId;
const title = await new Promise((resolve) => {
setTimeout(() => {
resolve(`iPhone ${id}`);
}, 100);
});
return {
title: `Product ${title}`,
};
};
export default async function ProductDetails({ params }: Props) {
return (
<div>123123</div>
)
}
-
此为生成元数据的函数。
-
不能在同一路由中同时使用元数据对象和生成元数据的函数。
title字段
字符串形式
这个不必多说
export const metadata ={
title: 'Next.js',
description:'Generated by Next.js',
}
----------------分割线------------------
export const generateMetadata = async ({
params,
}: Props): Promise<Metadata> => {
const id = (await params).productId
return {
title: `Product ${id}`,
};
};
- 不管是静态数据还是动态数据,都是字符串形式的。
对象形式
使用对象形式的title,在某些情况下会让我们省很多事情。
import { Metadata } from "next";
export const metadata: Metadata = {
title: {
default: "",
template: "%s | caihongtu",
absolute: "",
},
}
-
如果你不知道
absolute写什么,请删除该字段,而不是写absolute: "",因为它会让标题变为空! -
default: 默认标题,供给没有指定标题的子页面使用。
-
template: 可以给子页面的标题添加前缀或者后缀,%s代表子页面标题。
-
absolute: 此项可以脱离父级title的template设置,这里写什么就是什么。

说些什么吧!