当前位置: 首页 > news >正文

NextJs基础

目录

一、创建项目

二、导航和路由

2.1、布局和模板

2.2、usePathname

2.3、链接和导航

2.3.1、动态路由参数

2.3.2、查询参数 (Search Params)

2.5、路由组(Route Groups)

2.6、动态路由案例

2.7、动态Metadata(generateMetadata)

2.8、并行路由(平行路由)

2.9、拦截路由

三、数据渲染

3.1、传递动态Props

3.2、集成lowdb的增删改查

3.3、GET缓存与缓存退出

3.4、Middleware中间件

3.5、Middleware的登录、退出和拦截

一、创建项目

官网文档:https://nextjs.org/docs
创建项目:npx create-next-app@latest
指定版本:npx create-next-app@14.0.0 my-next-app

安装插件:Tailwind CSS IntelliSense 提示工具

Tailwind官网:border-width - Borders - Tailwind CSS

二、导航和路由

案例:

"use client";
import Link from "next/link";
import { useState } from "react";
import { usePathname } from "next/navigation";
const linkData = [{ name: "About", path: "/dashboard/about" },{ name: "Settings", path: "/dashboard/settings" },
];
export default function DashboardLayout({children,
}: Readonly<{children: React.ReactNode;
}>) {const [count, setCount] = useState(0);const pathname = usePathname();return (<div className="border-2 border-dashed border-black p-4 w-1/2 mx-auto"><div className="flex gap-4 font-bold text-lg mb-4">{linkData.map((link) => (<Linkkey={link.path}className={pathname === link.path ? "text-purple-500" : ""}href={link.path}>{link.name}</Link>))}</div><h2>Dashboard-Layout-{count}</h2><buttonclassName="bg-black text-white my-4 p-2 rounded-md"onClick={() => setCount(count + 1)}>increment</button>{children}</div>);
}

2.1、布局和模板

特性布局(layout)模板(template)
定义方式app/layout.tsxapp/template.tsx
复用性全局共享(导航栏+页脚)针对特定页面组复用
状态保持切换路由时状态保留切换路由时重新挂载
用途全局结构(如<html><body>标签)动画过渡、强制刷新子组件

2.2、usePathname

  • 功能:获取当前URL的路径部分(不含查询参数和域名)

  • 案例上实现了高亮当前导航项根据路径动态渲染内容功能

2.3、链接和导航

特性链接(<Link>)编程式导航(useRouter)
实现方式声明式命令式(js函数调用)
预加载自动预加载手动调用router.push()
SEO优化保持SPA特性,不刷新页面

2.3.1、动态路由参数

// 传递
<Link href="/user/123">User</Link>
router.push('/user/123');// 接收参数页面:app/user/[id]/page.tsx
export default function User({ params }: { params: { id: string } }) {return <div>User ID: {params.id}</div>; // 输出: User ID: 123
}

2.3.2、查询参数 (Search Params)

// 传递
<Link href="/search?q=keyword">Search</Link>
router.push('/search?q=keyword');// 接收参数页面: app/search/page.tsx
import { useSearchParams } from 'next/navigation';
export default function Search() {const searchParams = useSearchParams();const query = searchParams.get('q'); // "keyword"return <div>Search: {query}</div>;
}

2.5、路由组(Route Groups)

路由组的作用
1、逻辑分组:将功能相关的路由组织在一起(如认证、仪表板)
2、避免URL影响:组名不会出现在最终 URL 中(如 (auth)/login → /login)
3、共享布局:可在组内创建共享的 layout.js
4、选择性嵌套:允许部分路由继承布局,部分不继承

注意事项
1、组名约定:
必须用括号包裹:(groupName);不能是路由段保留字(如 page.js, layout.js)
2、URL 影响:
组名不会出现在 URL 中;无法通过 usePathname() 获取组名
3、SEO 友好:
适合组织管理后台等无需SEO的路由;建议使用清晰的一级路径(如 /blog)

2.6、动态路由案例

// blog文件夹下的page.tsx
"use client";
import React from "react";
import { Avatar, List } from "antd";
import Link from "next/link";
import { data } from "@/data/index";
export default function Page() {return (<div><ListitemLayout="horizontal"dataSource={data}renderItem={(item, index) => (<List.Item><List.Item.Metaavatar={<Avatarsrc={`https://api.dicebear.com/7.x/miniavs/svg?seed=${index}`}/>}title={<Link href={`/blog/${item.id}`}>{item.title}</Link>}description={item.body}/></List.Item>)}/></div>);
}
// blog文件夹下的[id]文件夹下的page.tsx
import React from "react";
import { Card } from "antd";
import { data } from "@/data/index";
export default function page({params}:{params:{id:string}}) {const item = data.find((item) => item.id === +params.id);return (<div>{" "}<Card title={item?.title} variant="borderless" style={{ width: 300 }}><p>{item?.body}</p></Card></div>);
}

2.7、动态Metadata(generateMetadata)

// 静态元数据: 向xx路由添加标题
import type { Metadata } from "next";
export const metadata: Metadata = {title: "博客列表",description: "博客列表",
};
// 详情页面动态渲染
interface IProps {params: { id: string };
}
export async function generateMetadata({ params }: IProps) {return {title: `博客详情 - ${params.id}`,};
}

2.8、并行路由(平行路由)

并行路由:允许在同一个页面(layout)中同时渲染多个独立的路由内容,类似于 Vue/React 中的 <slot> 或命名插槽机制。

备注:app目录下,使用@前缀的文件夹定义插槽,在layout.tsx 中通过props接收并渲染。

如案例所示:Home路由下是第一张图,Visitors路由下是第二张图,,此时@analytics文件夹下的page.tsx和visitors下的page.tsx属于并行路由,刷新后为了防止404,在app文件夹和@team文件夹下建立default.tsx。

2.9、拦截路由

拦截路由:在不离开当前页面的情况下,动态加载并覆盖部分路由内容。例如:

  • 点击相册图片,在当前页弹出大图预览(URL 变化但背景保留)

  • 表单编辑弹窗(覆盖当前列表页)

"use client"
import React from 'react'
import { photos } from "@/data/index";
import Image from "next/image";
import {useRouter} from 'next/navigation'
export default function page({params}:{params:{id:string}}) {const photo = photos.find(item => item.id === params.id)!//非空断言idconst router=useRouter()return (<div className='flex justify-center items-center fixed inset-0 bg-gray-500/[.8]' onClick={router.back}><Image src={photo.src} alt={photo.alt} width={400} height={400} className='rounded-lg' onClick={(e) => e.stopPropagation()}/></div>)
}

并行路由 vs 拦截路由

特性并行路由拦截路由
用途同时渲染多个独立内容区临时覆盖当前页面部分内容
URL内容无(隐式加载)有(URL变化但UI叠加)
场景多面板布局模态框、临时编辑弹窗
语法@slot文件夹(..)或(.)文件夹    拦截语法

三、数据渲染

3.1、传递动态Props

// 不同父组件
import React from "react";
import homeSrc from "@/public/bg1.jpg";
import Cover from "@/components/cover";
export default function Page() {return (<Cover imgUrl={homeSrc} altTxt="Performance" content="Welcome to Performance" />);
}
// 子组件cover.tsx接收
import Image, { StaticImageData } from "next/image";
import React from "react";
interface CoverProps {imgUrl: StaticImageData;//静态文件图片altTxt: string;content: string;
}
export default function Cover(props: CoverProps) {return (<div className="h-screen relative"><div className="absolute inset-0 -z-10"><Imagesrc={props.imgUrl}fillstyle={{ objectFit: "cover" }}alt={props.altTxt}></Image><div className="absolute inset-0 bg-gradient-to-r from-gray-900"></div></div><div className="flex justify-center pt-48"><h1 className="text-white text-6xl">{props.content}</h1></div></div>);
}

3.2、集成lowdb的增删改查

第一步:npm i lowdb(https://github.com/typicode/lowdb)

第二步:建立db.ts

import { JSONFilePreset } from "lowdb/node";
// 同级目录下创建 db.json
const defaultData: { posts: { id: string; title: string; content: string }[] } ={ posts: [] };
const db = await JSONFilePreset("db.json", defaultData);
export default db;

第三步:创建接口url

//============================api\articles\route.ts
import { NextRequest, NextResponse } from "next/server";
import db from "@/db";
// 分页查询  GET=> /api/articles
export async function GET(request: NextRequest) {const searchParams = request.nextUrl.searchParams;const pagenum = Number(searchParams.get("pageNum")) || 1;const pagesize = Number(searchParams.get("pageSize")) || 2;const query = searchParams.get("query") || "";const data = db.data.posts;let filterData = query? data.filter((item) => {const { id, ...rest } = item;return Object.values(rest).some((value) =>String(value).toLowerCase().includes(query.toLowerCase()));}): data;const total = filterData.length;const startIndex = (pagenum - 1) * pagesize;const endIndex = Math.min(startIndex + pagesize, total);filterData =startIndex >= total ? [] : filterData.slice(startIndex, endIndex);return NextResponse.json({code: 200,message: 'success',data: {total,list: filterData}});
}
// 添加  POST=> /api/articles
export async function POST(request: Request) {const data = await request.json();await db.update(({ posts }) =>posts.unshift({id: Math.random().toString(36).slice(-8),...data,}));return NextResponse.json({code: 200,message: "添加成功",data,});
}
//============================api\articles\[id]\route.ts
import { NextResponse } from "next/server";
import db from "@/db";
// 指定删除  DELETE=> /api/articles/:id
interface IParams {params: { id: string };
}
export async function DELETE(request: Request, { params }: IParams) {await db.update(({ posts }) => {const idx = posts.findIndex((item) => item.id === params.id);posts.splice(idx, 1);});return NextResponse.json({code: 200,message: "删除成功",});
}
// 修改  PATCH=> /api/articles/:id
export async function PATCH(request: Request, { params }: IParams) {const data = await request.json();await db.update(({ posts }) => {const idx = posts.findIndex((item) => item.id === params.id);if (idx !== -1) {posts[idx] = { ...posts[idx], ...data };}});return NextResponse.json({code: 200,message: "修改成功",data});
}
// 查找 GET=> /api/articles/:id
export async function GET(request: Request, { params }: IParams) {const data = db.data.posts.find((item) => item.id === params.id);return NextResponse.json({code: 200,message: "查询成功",data,});
}

第四步:封装接口

// lib/api.ts
export async function fetchArticles(query = "", pageNum = 1, pageSize = 5) {const res = await fetch(`/api/articles?query=${query}&pageNum=${pageNum}&pageSize=${pageSize}`,{ cache: "no-store", next: { revalidate: 0 } } // SSR 时不要缓存);return res.json();
}
export async function addArticle(data: any) {const res = await fetch("/api/articles", {method: "POST",headers: { "Content-Type": "application/json" },body: JSON.stringify(data),});return res.json();
}
export async function updateArticle(id: string, data: any) {const res = await fetch(`/api/articles/${id}`, {method: "PATCH",headers: { "Content-Type": "application/json" },body: JSON.stringify(data),});return res.json();
}
export async function deleteArticle(id: string) {const res = await fetch(`/api/articles/${id}`, {method: "DELETE",});return res.json();
}
export async function fetchArticleById(id: string) {const res = await fetch(`/api/articles/${id}`);return res.json();
}

第五步:以编辑页面的信息查询和编辑提交为例

"use client";
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { fetchArticleById, updateArticle } from "@/lib/api";
export default function EditArticlePage() {const { id } = useParams();const router = useRouter();const [form, setForm] = useState({ title: "", content: "" });useEffect(() => {const load = async () => {const res = await fetchArticleById(id as string);setForm(res.data || {});};load();}, [id]);const handleSubmit = async (e: React.FormEvent) => {e.preventDefault();await updateArticle(id as string, form);router.push("/articles");router.refresh();};return (<form onSubmit={handleSubmit} className="p-4 space-y-4"><h1 className="text-xl font-bold">编辑文章</h1><inputtype="text"value={form.title}onChange={(e) => setForm({ ...form, title: e.target.value })}className="border p-2 w-full"/><textareavalue={form.content}onChange={(e) => setForm({ ...form, content: e.target.value })}className="border p-2 w-full h-40"/><buttontype="submit"className="bg-green-500 text-white px-4 py-2 rounded">保存修改</button></form>);
}

3.3、GET缓存与缓存退出

默认生产环境下,GET请求返回的数据有可能被缓存(开发环境不会),例如在GET请求种返回该日期对象时间部分的字符串,此时在频繁请求时结果可能不会及时刷新

通过以下情况可以将其从静态渲染API变成动态API:

1、使用Request对象,从Request获取了前端传递的动态query,此时会触发缓存退出

2、用POST代替GET,因为POST请求往往用于改变数据

3、使用像cookies、headers这样的的动态函数,这些数据只有当请求的时候才知道具体的值,所以不适合缓存

4、路由段配置项手动声明为动态模式

import { headers } from "next/headers";
import { NextRequest } from "next/server";
export const dynamic = "force-dynamic";
export async function GET(request: NextRequest) {const headersList = headers();const referer = headersList.get("referer");request.cookies.get('token')return Response.json({ data: new Date().toLocaleTimeString(), referer });
}

3.4、Middleware中间件

Middleware 在请求完成前运行,可以修改请求和响应,一般请求都会经过它

用export const config或者使用条件语句控制 middleware 对哪些路径产生效果

import { NextRequest, NextResponse } from "next/server";
export default function middleware(request: NextRequest) {console.log(request.nextUrl.pathname, "@@@@");if (request.nextUrl.pathname.startsWith("/about")) {return NextResponse.rewrite(new URL("/about-2", request.url));}
}
export const config = {matcher: "/about",// matcher: ["/about", "/dashboard"],// matcher: ["/about/:path*", "/dashboard/:path*"],
};

3.5、Middleware的登录、退出和拦截

登录时接收用户名和密码,并保存token到cookies里,在退出时将其删除

import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {const { login, password } = await request.json();const r = await fetch("https://api.zhihur.com/admin/auth/sign_in", {method: "POST",headers: {"Content-Type": "application/json",},body: JSON.stringify({login,password,}),});const data = await r.json();const response = NextResponse.json({success: true,msg:data.message});response.cookies.set("token", data.data.token, {path: "/",maxAge: 60 * 60 * 24 * 7,httpOnly: true,});return response;
}

退出:

import { NextResponse } from "next/server";
export async function DELETE() {// 设置过期时间为 0 来删除 cookie// cookies().set('token', '', { maxAge: 0 });const responses = NextResponse.json({success: true,msg: "登出成功",});responses.cookies.set("token", "", { maxAge: 0 });return responses;
}

登录验证拦截:

​除了 /login页面以外,如果用户访问其他页面时没有携带有效的 token(登录凭证,存在 Cookie 中),就自动跳转到 /login登录页

import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {if (request.nextUrl.pathname !== "/login") {const token = request.cookies.get("token")?.value;if (!token) {return NextResponse.redirect(new URL("/login", request.url));}}
}
export const config = {matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)",],
};

http://www.dtcms.com/a/361453.html

相关文章:

  • 《LINUX系统编程》笔记p7
  • 1.数值分析——概述、误差
  • 【数据可视化-105】Pyecharts主题组件:让你的图表瞬间高大上
  • c++关键字
  • 首屏优化讲解
  • JavaEE 进阶第一期:开启前端入门之旅(上)
  • pip不是内部或外部命令的问题怎么解决?
  • 【数据库】Sql Server数据库中isnull、iif、case when三种方式的使用和空值判断
  • docker本地部署dify,nginx80端口占用的报错
  • Spring Ioc —— 集合类型的依赖注入
  • js语言编写科技风格博客网站-详细源码
  • LINUX驱动篇(二)驱动开发
  • 埃文科技荣获2025年“数据要素×”大赛河南分赛二等奖
  • FPGA时序约束(二)--做时序约束,本质上是在干嘛
  • 新闻资讯|基于微信小程序的经济新闻资讯系统设计与实现(源码+数据库+文档)
  • ARM-进阶汇编指令
  • 基于AI与物联网的中央空调节能调控系统
  • 原子操作(Atomic Operation) 是指不可被中断的操作——要么完整执行,要么完全不执行
  • Android修改进程优先级
  • 在IDEA里使用Google Java Format
  • UART控制器——ZYNQ学习笔记14
  • 解决Content Security Policy (CSP)问题
  • Sparse4Dv3 部署到 TensorRT-(1)
  • Gradle vs. Maven,Java 构建工具该用哪个?
  • Paimon MergeTreeWrite、Compaction 和 快照构建
  • 嵌入式解谜日志之Linux操作系统—进程间的通信(IPC):无名管道,有名管道,信号通信5
  • 单片机元件学习
  • 【stm32】定时器(超详细)
  • Git安装教程
  • 【51页PPT】智慧社区解决方案(附下载方式)