Next.js 基础,完。
创建项目 📺视频
1 npx create-next-app@latest next-stu
1 2 3 4 5 6 7 8 PS D:\> npx create-next-app@latest next-stu √ Would you like to use TypeScript? ... No / Yes √ Would you like to use ESLint? ... No / Yes √ Would you like to use Tailwind CSS? ... No / Yes √ Would you like to use `src/` directory? ... No / Yes √ Would you like to use App Router? (recommended) ... No / Yes √ Would you like to customize the default import alias (@/*)? ... No / Yes √ ...
如果控制台出现警告:Warning: Extra attributes from the server: class …,可能是安装的一些浏览器插件导致的,关闭插件或 src/app/layout.tsx 添加相关属性,参考 :
1 2 3 <html lang="en" suppressHydrationWarning={true }> ... </html>
路由定义 文档
📺视频
关于 Layout 文档
📺视频
关于 template 文档
📺视频
Layout 和 template 的区别 📺视频
通过代码演示差异 📺视频
usePathname 严格模式为什么执行两次?模拟了立即卸载和重新挂载组件,帮助开发者提前发现由于重复挂载造成的 Bug。
文档
📺视频
案例效果演示 效果展示与分析
📺视频
📺视频
src/components/header.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import Link from "next/link" ;export default function Header ( ) { return ( <div className ="absolute w-full text-white z-10" > <nav className ="container flex items-center justify-between mx-auto p-8" > <Link className ="font-bold text-3xl" href ="/" > Home </Link > <div className ="space-x-4 text-xl" > <Link href ="/performance" > Performance</Link > <Link href ="/reliability" > Reliability</Link > <Link href ="/scale" > Scale</Link > </div > </nav > </div > ); }
编写 Hero 组件 📺视频
src/components/hero.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import type { StaticImageData } from "next/image" ;import Image from "next/image" ;interface HeroProps { imgData : StaticImageData ; imgAlt : string ; title : string ; } export default function Hero (props: HeroProps ) { return ( <div className ="relative h-screen" > <div className ="absolute -z-10 inset-0" > <Image src ={props.imgData} alt ={props.imgAlt} fill style ={{ objectFit: "cover " }} /> <div className ="absolute inset-0 bg-gradient-to-r from-slate-900" > </div > </div > <div className ="pt-48 flex justify-center" > <h1 className ="text-white text-6xl" > {props.title}</h1 > </div > </div > ); }
指定页面元数据 📺视频
指定 favicon 📺视频
NotFound 的分类和触发规律 📺视频
🎉 分类:
🤠 触发:
初步实现 404 📺视频
通过 pathname 处理 404 页面的问题 📺视频
通过路由分组处理 404 页面的问题 📺视频
https://nextjs.org/docs/app/building-your-application/routing/route-groups
🎉 目的:
在不影响 url 路径的情况下,按照项目的功能、逻辑或团队来组织路由;
在不影响 url 路径的情况下,给相关页面添加公共的 layout;
甚至可以创建多个根布局,这对于,需要将同一个项目的某些某块,划分为具有完全不同 UI 或功能的需求的时候比较有用。
修改网站字体 📺视频
Next.js 中可以直接支持很多的 Google 字体,这一点会很有意思。
https://fonts.google.com/
src/app/layout.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import type { Metadata } from "next" ;import { Comforter } from "next/font/google" ;import "./globals.css" ;const cmforter = Comforter ({ weight : ["400" ], subsets : ["latin" ] });export const metadata : Metadata = { title : "Create Next App" , description : "Generated by create next app" , }; export default function RootLayout ({ children, }: Readonly<{ children: React.ReactNode; }> ) { return ( <html lang ="en" > <body className ={cmforter.className} > {children}</body > </html > ); }
Vercel 自动部署 📺视频
动态路由 📺视频
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export const data = [ { userId : 1 , id : 1 , title : "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" , body : "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" , }, { userId : 1 , id : 2 , title : "qui est esse" , body : "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" , }, { userId : 1 , id : 3 , title : "ea molestias quasi exercitationem repellat qui ipsa sit aut" , body : "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut" , }, ];
临时集成 AntD 📺视频
动态路由案例 📺视频
📺视频
平行路由 📺视频
作用:可以在同一个布局中同时或者有条件的渲染一个或者多个页面(类似于 Vue 的插槽功能)。
🪶 条件路由;
🪶 共享 layout(不影响路由地址);
🪶 可以有自己单独的 loading 和 error 处理;
🪶 …
平行路由案例 📺视频
拦截路由 📺视频
官方文档
拦截路由案例 📺视频
🕊️ 需求:预览列表中的某一张图片。
🤠 处理方式一
点击图片,跳转到一个单独的图片详情页,展示大图。
缺点:打断了用户继续浏览的体验(当然有同学可能会想到在详情页做一些其他处理,让用户继续浏览,这个不在咱的讨论范围内)。
🤠 处理方式二
点击出现弹框,展示大图。
缺点:如果期望把具体弹框中的图片分享出去,不太容易操作(因为这个时候路由地址没有发生变化,用户打开后还是会展示所有列表图片)。
🎉 期望效果
点击列表中图片当前页出现弹框,把链接分享出去后呢,单独展示弹框中的信息,这样预览的时候既不会打断用户的体验,又可以精准的把对应的图片分享出去。
🐻 效果展示
🤔 如何实现
通过 Next.js 提供的拦截路由去实现,它允许对软导航进行路由拦截(例如点击网站中的图片),可以展示我们提前准备好的拦截后的效果,分享出去后(硬导航),不会经过路由拦截,展示具体的图片详情。
如果出现报错:Application error: a client-side exception has occurred (see the browser console for more information),尝试删除 .next 文件夹,重新启动项目。
https://github.com/vercel/nextgram
https://intercepting-routes-ochre.vercel.app/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 📦src ┗ 📂app ┃ ┣ 📂@modal ┃ ┃ ┣ 📂(.)photos ┃ ┃ ┃ ┗ 📂[id ] ┃ ┃ ┃ ┃ ┗ 📜page.tsx ┃ ┃ ┗ 📜default.tsx ┃ ┣ 📂photos ┃ ┃ ┗ 📂[id ] ┃ ┃ ┃ ┗ 📜page.tsx ┃ ┣ 📜data.ts ┃ ┣ 📜default.tsx ┃ ┣ 📜favicon.ico ┃ ┣ 📜globals.css ┃ ┣ 📜layout.tsx ┃ ┗ 📜page.tsx
data.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 export const photos = [ { id : "1" , src : "https://test.zhihur.com/img/1.png" , alt : "Earthen Bottle" , price : 4 , }, { id : "2" , src : "https://test.zhihur.com/img/2.png" , alt : "Nomad Tumbler" , price : 7 , }, { id : "3" , src : "https://test.zhihur.com/img/3.png" , alt : "Focus Paper Refill" , price : 35 , }, { id : "4" , src : "https://test.zhihur.com/img/4.png" , alt : "Machined Mechanical Pencil" , price : 16 , }, { id : "5" , src : "https://test.zhihur.com/img/5.png" , alt : "Leslie Alexander" , price : 19 , }, { id : "6" , src : "https://test.zhihur.com/img/6.png" , alt : "Michael Foster" , price : 69 , }, { id : "7" , src : "https://test.zhihur.com/img/7.png" , alt : "Dries Vincent" , price : 22 , }, { id : "8" , src : "https://test.zhihur.com/img/8.png" , alt : "Lindsay Walton" , price : 87 , }, ];
next.config.mjs
1 2 3 4 5 6 7 8 9 10 11 12 const nextConfig = { images : { remotePatterns : [ { hostname : "test.zhihur.com" , }, ], }, }; export default nextConfig;
src/app/page.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import Link from "next/link" ;import { photos } from "./data" ;import Image from "next/image" ;export default function Home ( ) { return ( <div className ="bg-white" > <div className ="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-3xl lg:px-8" > <h2 className ="sr-only" > Products</h2 > <div className ="grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8" > {photos.map(({ id, src, alt }) => ( <Link href ={ `/photos /${id }`} className ="group" key ={id} > <div className ="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg bg-gray-200 xl:aspect-h-8 xl:aspect-w-7" > <Image src ={src} alt ={alt} className ="h-full w-full object-cover object-center group-hover:opacity-75" width ={200} height ={200} /> </div > <h3 className ="mt-4 text-sm text-gray-700" > Machined Mechanical Pencil </h3 > <p className ="mt-1 text-lg font-medium text-gray-900" > $35</p > </Link > ))} </div > </div > </div > ); }
src/app/photos/[id]/page.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import Image from "next/image" ;import { photos } from "../../data" ;export default function PhotoPage ({ params: { id }, }: { params: { id: string }; } ) { const photo = photos.find ((p ) => p.id === id)!; return ( <div className ="container mx-auto" > <Image className ="block w-1/4 mx-auto mt-10 rounded-lg" src ={photo.src} alt ={photo.alt} width ={300} height ={300} /> <div className ="leading-loose border-2 border-dashed p-5 mt-5 rounded-lg border-gray-500" > <p > <strong className ="font-bold" > Title:</strong > {photo.alt} </p > <p > <strong className ="font-bold" > Price:</strong > {photo.price} </p > <p > <strong className ="font-bold" > Desc:</strong > Ut non occaecat incididunt laboris. Aliquip laboris anim dolore in officia id commodo nostrud non adipisicing. Elit consectetur dolor deserunt Lorem mollit qui irure tempor. Id aute nostrud laborum pariatur incididunt duis ea. Culpa excepteur consectetur proident mollit esse excepteur in ad eiusmod dolor do amet tempor.Irure sunt aute aliquip fugiat consectetur consequat cillum ad esse in aute ipsum veniam. Dolor ut aute sit qui qui ipsum et adipisicing ex consectetur ullamco nulla. Elit magna et ex ipsum elit non ad ex culpa nostrud quis est. </p > </div > </div > ); }
src/app/@modal/(.)photos/[id]/page.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 "use client" ;import Image from "next/image" ;import { photos } from "../../../data" ;import { useRouter } from "next/navigation" ;export default function PhotoModal ({ params: { id }, }: { params: { id: string }; } ) { const photo = photos.find ((p ) => p.id === id)!; const router = useRouter (); return ( <div className ="flex justify-center items-center fixed inset-0 bg-slate-300/[.8]" onClick ={router.back} > <Image className ="rounded-lg shadow-lg" src ={photo.src} alt ={photo.alt} width ={400} height ={400} onClick ={(e) => e.stopPropagation()} /> </div > ); }
🤠 src/app/layout.tsx 中记得输出 modal。
default.tsx
1 2 3 export default function Default ( ) { return null ; }
Router Handler 📺视频
咱们常说的路由有这么两种,一种是前端路由,另一种是后端路由。前端路由咱们一直在用,它说的是页面路径和组件(或者说页面)之间的对应关系,也就是访问怎样的地址,展示对应的界面。而咱们这次说的路由处理程序中的路由说的是后端路由,什么是后端路由呢,说白了就是前端请求地址和后端处理逻辑之间的对应关系,再换一种说法就是咱们常说的写接口。
接下来看一下编写路由处理程序的约定,首先要在 app 文件夹建一个 route.ts,名字是固定的,为了方便管理呢,一般咱们习惯统一放在 api 目录下,这样我们后续访问的接口地址都是以 /api 开头的。当然这儿并不是说一定要放在 api 目录下,你如果愿意的话,可以放在 app 目录下的任意文件夹,但是有一个注意点,route 和 page 不能放在同一层级目录,因为 route 负责处理请求,page 负责渲染页面,如果放在同一目录,这样的话他们的路径又是一样的,Next.js 不知道该使用哪个逻辑去处理了。
假如说待会咱写一个案例,会涉及到文章的增删改查接口,这个时候路由啊就可以这样去定义 …
集成 lowdb 增加和删除 📺视频
src/db.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 import { JSON FilePreset } from "lowdb/node" ;interface IData { posts : { id : string ; title : string ; content : string }[]; } const defaultData = { posts : [] } as IData ;const db = await JSON FilePreset("db.json" , defaultData);export default db;
增,src/app/api/posts/route.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 interface IBody { title : string ; content : string ; } export async function POST (request: NextRequest ) { const article : IBody = await request.json (); await db.update (({ posts } ) => { posts.unshift ({ id : Math .random ().toString (36 ).slice (-8 ), ...article, }); }); return NextResponse .json ({ code : 0 , message : "添加成功" , }); }
删,src/app/api/posts/[id]/route.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { NextRequest , NextResponse } from "next/server" ;import db from "@/db" ;export const DELETE = async ( _: NextRequest, { params }: { params: { id: string } } ) => { const id = params.id ; await db.update (({ posts } ) => posts.splice ( posts.findIndex ((item ) => item.id === id), 1 ) ); return NextResponse .json ({ code : 0 , message : "删除成功" , }); };
修改和查找 📺视频
改,src/app/api/posts/[id]/route.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export const PATCH = async ( request: NextRequest, { params }: { params: { id: string } } ) => { const { title, content } = await request.json (); let idx = 0 ; await db.update (({ posts } ) => { idx = posts.findIndex ((item ) => item.id === params.id ); posts[idx].title = title; posts[idx].content = content; }); return NextResponse .json ({ code : 0 , message : "编辑成功" , data : db.data .posts [idx], }); };
查一个,src/app/api/posts/[id]/route.ts
1 2 3 4 5 6 7 8 9 10 11 export const GET = async ( _: NextRequest, { params }: { params: { id: string } } ) => { const post = db.data .posts .find ((item ) => item.id === params.id ); return NextResponse .json ({ code : 0 , message : "获取成功" , data : post, }); };
分页查询 📺视频
查一堆,src/app/api/posts/route.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { NextResponse } from "next/server" ;import type { NextRequest } from "next/server" ;import db from "@/db" ;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 { posts : data } = db.data ; let filteredData = query ? data.filter ((item ) => { const { id, ...rest } = item; return Object .values (rest).some ((value ) => String (value).toLowerCase ().includes (query.toLowerCase ()) ); }) : data; const total = filteredData.length ; const startIndex = (pagenum - 1 ) * pagesize; const endIndex = Math .min (startIndex + pagesize, total); filteredData = startIndex >= total ? [] : filteredData.slice (startIndex, endIndex); return NextResponse .json ({ code : 0 , message : "获取成功" , data : { total, list : filteredData, }, }); }
🌺 上面这种方式写接口,能隐藏 Authorization 等认证信息。
参数的传递和接口。
1 2 3 4 5 6 7 8 9 10 11 12 export async function GET (request, context ) { const pathname = request.nextUrl .pathname ; const searchParams = request.nextUrl .searchParams ; } export async function GET (request, { params } ) { const team = params.team ; }
阶段任务 📺视频
关于 GET 缓存 📺视频
默认生产环境 情况下,GET 请求返回的数据有可能被缓存(开发环境不会),例如:
app/api/time/route.ts
1 2 3 4 5 6 export function GET ( ) { console .log ("GET /api/time" ); return Response .json ({ data : new Date ().toLocaleTimeString () }); }
注意开发环境并没有缓存,例如可以测试看下…所以需要打包后进行测试。
1 pnpm build && pnpm start
根据输出的结果,你会发现 /api/time 是静态的,也就是被预渲染为了静态的内容,换言之,/api/time 的返回结果其实在构建的时候就已经确定了,而不是在后续请求的时候才确定,其实后续的每一次请求也不会走这个处理函数,咱不妨给大家印证一下。
GET 缓存的退出 📺视频
这个时候,有小伙伴肯定会担心,这会不会给我们开发带来影响,其实一般不用担心,因为产生缓存的条件是非常“严苛”的,以下这些情况都会导致退出缓存:
官方链接
1. GET 请求中使用了 Request 对象,例如从 request 中获取了前端传递的 query,由于前端传递的 query 是动态的,所以会触发缓存退出;
1 2 3 4 5 6 7 export async function GET (request ) { const searchParams = request.nextUrl .searchParams ; return Response .json ({ data : new Date ().toLocaleTimeString (), params : searchParams.toString (), }); }
此时会变成 Dynamic API Route …
src/db.ts
1 2 3 4 5 6 7 8 9 10 11 12 import { JSON FilePreset } from "lowdb/node" ;export {};const defaultData : { posts : { id : string ; title : string ; content : string }[] } = { posts : [] }; const db = await JSON FilePreset("db.json" , defaultData);export default db;
tsconfig.json
2. 添加其他 HTTP 方法,比如 POST,因为 POST 请求往往用于改变数据,表示数据会发生变化,此时再使用 GET 请求时使用缓存就不合适了;
1 2 3 4 5 6 export async function GET ( ) { console .log ("GET /api/time" ); return Response .json ({ data : new Date ().toLocaleTimeString () }); } export async function POST ( ) {}
3. 使用像 cookies、headers 这样的动态函数 ,因为 cookies、headers 这些数据只有当请求的时候才知道具体的值,所以不适合缓存;
1 2 3 4 5 6 import type { NextRequest } from "next/server" ;export async function GET (request: NextRequest ) { request.cookies .get ("token" ); return Response .json ({ data : new Date ().toLocaleTimeString () }); }
1 2 3 4 5 6 7 8 9 import { headers } from "next/headers" ;import { NextRequest } from "next/server" ;export async function GET (request: NextRequest ) { const headersList = headers (); const referer = headersList.get ("referer" ); console .log ("GET /api/time" ); return Response .json ({ data : new Date ().toLocaleTimeString (), referer }); }
4. 路由段配置 项手动声明为动态模式;
1 2 3 4 5 export const dynamic = "force-dynamic" ;export async function GET ( ) { return Response .json ({ data : new Date ().toLocaleTimeString () }); }
5. 重新验证。
除了退出缓存,也可以通过暴露出 revalidate 设置缓存的时效,适用于一些重要性低、时效性低的页面。有两种常用的方案,一种是使用路由段配置项 。
1 2 3 4 5 6 7 export const revalidate = 10 ;export async function GET ( ) { return Response .json ({ data : new Date ().toLocaleTimeString () }); }
中间件 📺视频
使用中间件可以拦截并控制应用里所有的请求和响应,可以对传入的请求,进行修改或重写。默认情况下,任何请求都会经过 middleware ,例如咱举个例子看一下。
为了看起来清爽一些呢,咱这儿基于第一个分支创建一个新分支进行演示。
1 git checkout 25_middleware 01_项目初始化
src\middleware.ts
1 2 3 4 5 import { NextRequest } from "next/server" ;export function middleware (request: NextRequest ) { console .log (request.nextUrl .pathname , "🤠" ); }
此时浏览器输入,localhost:3000,刷新一下,看一下后端的控制台。
会发现有这么多的请求都经过了 middleware,打印了小牛牛,一般情况下呢需要对这儿进行控制,例如只需要某些请求命中 middleware,执行这里面的业务逻辑。常见的有两种方式控制 middleware 对哪些路径产生效果。
一种是通过 export const config
进行控制,里面指定 matcher 匹配选项,表示只有和 matcher 匹配才执行 middleware 中的代码,另一种是通过条件语句控制,例如根据 pathname 来决定要不要走 middleware 中特定的逻辑,当然也可以两种结合起来去使用。
咱们先看第一种,export const config
这种方式, matcher 的值可以有好多种形式,例如可以是字符串
1 2 3 export const config = { matcher : "/about" , };
这就表示只有访问的地址是 /about 的时候才走 middleware 中的逻辑,matcher 的值也可以是数组
1 2 3 export const config = { matcher : ["/about" , "/dashboard" ], };
这表示访问的地址是 /about 或者 /dashboard 的时候才走 middleware 中的逻辑,也可以编写正则进行匹配,例如
1 2 3 export const config = { matcher : ["/about/:path*" , "/dashboard/:path*" ], };
数组中也可以写正则,这就表示匹配 /about、/about/xxx、/about/xxx/xxx 等。
有个比较常见的写法是这样,表示匹配所有的路径除了以这些作为开头的,这样的话我们再访问 localhost:3000 会发现就只有地址栏输入的 / 输出啦。
1 2 3 4 5 6 7 8 9 10 11 export const config = { matcher : [ "/((?!api|_next/static|_next/image|favicon.ico).*)" , ], };
第二种控制的方式是使用条件语句,例如根据 pathname 来决定要不要走 middleware 中特定的逻辑,这样比配置 matcher 看起来直观一些。
例如官方给的这个例子就表示:当访问 /about 的时候,展示的是 /about-2 的内容,这里大家注意 rewrite 的含义是重写,前端输入的网址不会变化。
1 2 3 4 5 6 7 8 9 10 import { NextRequest , NextResponse } from "next/server" ;export function middleware (request: NextRequest ) { if (request.nextUrl .pathname .startsWith ("/about" )) { return NextResponse .rewrite (new URL ("/about-2" , request.url )); } if (request.nextUrl .pathname .startsWith ("/dashboard" )) { return NextResponse .rewrite (new URL ("/dashboard/user" , request.url )); } }
中间件案例 - 登录 📺视频
最后呢,咱给大家举个例子。有两个页面,一个是登录页,一个是 /dashboard 页,登录页输入用户名密码,点击登录跳转到 /dashboard,/dashboard 点击退出清除 Cookie 返回到登录页,这里面有一个注意点,当没有登录的情况下是不能访问 /dashboard 的,这时候就需要用到咱们前面学习的中间件。
1 git checkout -b 26_登录 07_集成AntD
1. 登录页面,src\app\login\page.tsx;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 "use client" ;import React from "react" ;import type { FormProps } from "antd" ;import { Button , Form , Input } from "antd" ;import { useRouter } from "next/navigation" ;type FieldType = { login?: string ; password?: string ; }; const onFinishFailed : FormProps <FieldType >["onFinishFailed" ] = (errorInfo ) => { console .log ("Failed:" , errorInfo); }; const Page : React .FC = () => { const router = useRouter (); const onFinish : FormProps <FieldType >["onFinish" ] = async (values) => { const r = await fetch ("/api/login" , { method : "POST" , headers : { "Content-Type" : "application/json" }, body : JSON .stringify (values), }); const data = await r.json (); if (data.success === true ) { router.push ("/dashboard" ); } }; return ( <div className="container flex justify-center pt-10 mx-auto"> <Form className="w-96" name="basic" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 600 }} initialValues={{ login: "admin", password: "123123" }} onFinish={onFinish} onFinishFailed={onFinishFailed} autoComplete="off" > <Form.Item<FieldType> label="用户名" name="login" rules={[{ required: true, message: "Please input your username!" }]} > <Input /> </Form.Item> <Form.Item<FieldType> label="密码" name="password" rules={[{ required: true, message: "Please input your password!" }]} > <Input.Password /> </Form.Item> <Form.Item wrapperCol={{ offset: 4, span: 20 }}> <Button type="primary" htmlType="submit" block> 登录 </Button> </Form.Item> </Form> </div> ); }; export default Page;
2. 登录接口,src/app/api/login/route.ts;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import { NextRequest , NextResponse } from "next/server" ;export async function POST (request: NextRequest ) { const body = await request.json (); const { login, password } = body; const response = await fetch (`${process.env.DEV_API} /auth/sign_in` , { method : "POST" , headers : { "Content-Type" : "application/json" }, body : JSON .stringify ({ login, password, }), }); const data = await response.json (); if (!data.status ) { return NextResponse .json ( { success : false , msg : data.message }, { status : response.status || 500 } ); } const token = data.data .token ; const responses = NextResponse .json ({ success : true , msg : data.message }); responses.cookies .set ("token" , token, { path : "/" , maxAge : 86400 , httpOnly : true , }); return responses; }
.env.development
1 DEV_API=https://api.zhihur.com/admin
中间件案例 - 退出和拦截 📺视频
1. 退出接口,src\app\api\logout\route.ts;
1 2 3 4 5 6 7 8 9 10 11 12 13 import { NextResponse } from "next/server" ;export async function DELETE ( ) { const responses = NextResponse .json ({ success : true , msg : "登出成功" , }); responses.cookies .set ("token" , "" , { maxAge : 0 }); return responses; }
2. 退出界面,src\app\dashboard\page.tsx;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 "use client" ;import { useRouter } from "next/navigation" ;import { Button } from "antd" ;export default function Page ( ) { const router = useRouter (); const handleLogout = async ( ) => { const r = await fetch ("/api/logout" , { method : "DELETE" , }); const data = await r.json (); if (data.success === true ) { router.push ("/login" ); } }; return ( <div className ="flex justify-center items-center h-screen" > <Button type ="primary" onClick ={handleLogout} > 退出 </Button > </div > ); }
3. 中间件处理,middleware.ts。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { NextRequest , NextResponse } from "next/server" ;export function middleware (request: NextRequest ) { if (!request.nextUrl .pathname .startsWith ("/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).*)" ], };