阅读视图
Next.js第九章(AI)
AI
Vercel提供了AI SDK,可以让我们在Next.js中轻松集成AI功能。AI SDK 官网
安装AI-SDK
npm i ai @ai-sdk/deepseek @ai-sdk/react
这儿我们使用deepseek作为AI模型,@ai-sdk/react封装了流式输出和上下文管理hook,可以让我们在Next.js中轻松集成AI功能。如果你要安装其他模型,只需要将deepseek替换为其他模型即可。
例如:安装openai模型
npm i ai @ai-sdk/openai @ai-sdk/react
为什么使用
deepseek模型?因为deepseek比较便宜,充10元可以测试很久(非广告)。
获取deepSeek API Key
然后把生成的API Key复制一下保存起来。
编写API接口
src/app/api/chat/route.ts
import { NextRequest } from "next/server";
import { streamText,convertToModelMessages } from 'ai'
import { createDeepSeek } from "@ai-sdk/deepseek";
import { DEEPSEEK_API_KEY } from "./key";
const deepSeek = createDeepSeek({
apiKey: DEEPSEEK_API_KEY, //设置API密钥
});
export async function POST(req: NextRequest) {
const { messages } = await req.json(); //获取请求体
//这里为什么接受messages 因为我们使用前端的useChat 他会自动注入这个参数,所有可以直接读取
const result = streamText({
model: deepSeek('deepseek-chat'), //使用deepseek-chat模型
messages:convertToModelMessages(messages), //转换为模型消息
//前端传过来的额messages不符合sdk格式所以需要convertToModelMessages转换一下
//转换之后的格式:
//[
//{ role: 'user', content: [ [Object] ] },
//{ role: 'assistant', content: [ [Object] ] },
//{ role: 'user', content: [ [Object] ] },
//{ role: 'assistant', content: [ [Object] ] },
//{ role: 'user', content: [ [Object] ] },
//{ role: 'assistant', content: [ [Object] ] },
//{ role: 'user', content: [ [Object] ] }
//]
system: '你是一个高级程序员,请根据用户的问题给出回答', //系统提示词
});
return result.toUIMessageStreamResponse() //返回流式响应
}
src/app/page.tsx
我们在前端使用useChat组件来实现AI对话,这个组件内部封装了流式响应,默认会向/api/chat发送请求。
-
messages: 消息列表,包含用户和AI的对话内容 -
sendMessage: 发送消息的函数,参数为消息内容 -
onFinish: 消息发送完成后回调函数,可以在这里进行一些操作,例如清空输入框
messages:数据结构解析
[
{
"parts": [
{
"type": "text", //文本类型
"text": "你知道 api router 吗"
}
],
"id": "FPHwY1udRrkEoYgR", //消息ID
"role": "user" //用户角色
},
{
"id": "qno6vcWcwFM4Yc8J", //消息ID
"role": "assistant", //AI角色
"parts": [
{
"type": "step-start" //步骤开始
},
{
"type": "text", //文本类型
"text": "是的,我知道 **API Router**。", //文本内容
"state": "done" //步骤完成
}
]
}
]
'use client';
import { useState, useRef, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { useChat } from '@ai-sdk/react';
export default function HomePage() {
const [input, setInput] = useState(''); //输入框的值
const messagesEndRef = useRef<HTMLDivElement>(null); //获取消息结束的ref
//useChat 内部封装了流式响应 默认会向/api/chat 发送请求
const { messages, sendMessage } = useChat({
onFinish: () => {
setInput('');
}
});
// 自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
//回车发送消息
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (input.trim()) {
sendMessage({ text: input });
}
}
};
return (
<div className='flex flex-col h-screen bg-linear-to-br from-blue-50 via-white to-purple-50'>
{/* 头部标题 */}
<div className='bg-white/80 backdrop-blur-sm shadow-sm border-b border-gray-200'>
<div className='max-w-4xl mx-auto px-6 py-4'>
<h1 className='text-2xl font-bold bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent'>
AI 智能助手
</h1>
<p className='text-sm text-gray-500 mt-1'>随时为您解答问题</p>
</div>
</div>
{/* 消息区域 */}
<div className='flex-1 overflow-y-auto px-4 py-6'>
<div className='max-w-4xl mx-auto space-y-4'>
{messages.length === 0 ? (
<div className='flex flex-col items-center justify-center h-full text-center py-20'>
<div className='bg-linear-to-br from-blue-100 to-purple-100 rounded-full p-6 mb-4'>
<svg className='w-12 h-12 text-blue-600' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z' />
</svg>
</div>
<h2 className='text-xl font-semibold text-gray-700 mb-2'>开始对话</h2>
<p className='text-gray-500'>输入您的问题,我会尽力帮助您</p>
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} animate-in fade-in slide-in-from-bottom-4 duration-500`}
>
<div className={`flex gap-3 max-w-[80%] ${message.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}>
{/* 头像 */}
<div className={`shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-white font-semibold ${
message.role === 'user'
? 'bg-linear-to-br from-blue-500 to-blue-600'
: 'bg-linear-to-br from-purple-500 to-purple-600'
}`}>
{message.role === 'user' ? '你' : 'AI'}
</div>
{/* 消息内容 */}
<div className={`flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'}`}>
<div className={`rounded-2xl px-4 py-3 shadow-sm ${
message.role === 'user'
? 'bg-linear-to-br from-blue-500 to-blue-600 text-white'
: 'bg-white border border-gray-200 text-gray-800'
}`}>
{message.parts.map((part, index) => {
switch (part.type) {
case 'text':
return (
<div key={message.id + index} className='whitespace-pre-wrap wrap-break-word'>
{part.text}
</div>
);
}
})}
</div>
</div>
</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
</div>
{/* 输入区域 */}
<div className='bg-white/80 backdrop-blur-sm border-t border-gray-200 shadow-lg'>
<div className='max-w-4xl mx-auto px-4 py-4'>
<div className='flex gap-3 items-end'>
<div className='flex-1 relative'>
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder='请输入你的问题... (按 Enter 发送,Shift + Enter 换行)'
className='min-h-[60px] max-h-[200px] resize-none rounded-xl border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all shadow-sm'
/>
</div>
<Button
onClick={() => {
if (input.trim()) {
sendMessage({ text: input });
}
}}
disabled={!input.trim()}
className='h-[60px] px-6 rounded-xl bg-linear-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 transition-all shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed'
>
<svg className='w-5 h-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M12 19l9 2-9-18-9 18 9-2zm0 0v-8' />
</svg>
</Button>
</div>
</div>
</div>
</div>
);
}
Next.js第八章(路由处理程序)
路由处理程序(Route Handlers)
路由处理程序,可以让我们在Next.js中编写API接口,并且支持与客户端组件的交互,真正做到了什么叫前后端分离人不分离。
文件结构
定义前端路由页面我们使用的page.tsx文件,而定义API接口我们使用的route.ts文件,并且他两都不受文件夹的限制,可以放在任何地方,只需要文件的名称以route.ts结尾即可。
注意:
page.tsx文件和route.ts文件不能放在同一个文件夹下,否则会报错,因为Next.js就搞不清到底用哪一个了,所以我们最好把前后端代码分开。
为此我们可以定义一个api文件夹,然后在这个文件夹下创建一对应的模块例如user login register等。
目录结构如下
app/
├── api
│ ├── user
│ │ └── route.ts
│ ├── login
│ │ └── route.ts
│ └── register
│ └── route.ts
定义请求
Next.js是遵循RESTful API的规范,所以我们可以使用HTTP方法来定义请求。
export async function GET(request) {}
export async function HEAD(request) {}
export async function POST(request) {}
export async function PUT(request) {}
export async function DELETE(request) {}
export async function PATCH(request) {}
//如果没有定义OPTIONS方法,则Next.js会自动实现OPTIONS方法
export async function OPTIONS(request) {}
注意: 我们在定义这些请求方法的时候
不能修改方法名称而且必须是大写,否则无效。
工具准备: 打开vsCode / Cursor 找到插件市场搜索REST Client,安装完成后,我们可以使用REST Client来测试API接口。
定义GET请求
src/app/api/user/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const query = request.nextUrl.searchParams; //接受url中的参数
console.log(query.get('id'));
return NextResponse.json({ message: 'Get request successful' }); //返回json数据
}
REST client测试:
在src目录新建test.http文件,编写测试请求
src/test.http
GET http://localhost:3000/api/user?id=123 HTTP/1.1
定义Post请求
src/app/api/user/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest){
//const body = await request.formData(); //接受formData数据
//const body = await request.text(); //接受text数据
//const body = await request.arrayBuffer(); //接受arrayBuffer数据
//const body = await request.blob(); //接受blob数据
const body = await request.json(); //接受json数据
console.log(body); //打印请求体中的数据
return NextResponse.json({ message: 'Post request successful', body },{status: 201});
//返回json数据
}
REST client测试:
src/test.http
POST http://localhost:3000/api/user HTTP/1.1
Content-Type: application/json
{
"name": "张三",
"age": 18
}
动态参数
我们可以在路由中使用方括号[]来定义动态参数,例如/api/user/[id],其中[id]就是动态参数,这个参数可以在请求中传递,这个跟前端路由的动态路由类似。
src/app/api/user/[id]/route.ts
接受动态参参数,需要在第二个参数解构{ params },需注意这个参数是异步的,所以需要使用await来等待参数解析完成。
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest,
{ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
console.log(id);
return NextResponse.json({ message: `Hello, ${id}!` });
}
REST client测试:
src/test.http
GET http://localhost:3000/api/user/886 HTTP/1.1
cookie
Next.js也内置了cookie的操作可以方便让我们读写,接下来我们用一个登录的例子来演示如何使用cookie。
安装手动挡组件库shadcn/ui官网地址
npx shadcn@latest init
为什么使用这个组件库?因为这个组件库是把组件放入你项目的目录下面,这样做的好处是可以让你随时修改组件库样式,并且还能通过AI分析修改组件库
安装button,input组件
npx shadcn@latest add button
npx shadcn@latest add input
新建login接口 src/app/api/login/route.ts
import { cookies } from "next/headers"; //引入cookies
import { NextRequest, NextResponse } from "next/server"; //引入NextRequest, NextResponse
//模拟登录成功后设置cookie
export async function POST(request: NextRequest) {
const body = await request.json();
if(body.username === 'admin' && body.password === '123456'){
const cookieStore = await cookies(); //获取cookie
cookieStore.set('token', '123456',{
httpOnly: true, //只允许在服务器端访问
maxAge: 60 * 60 * 24 * 30, //30天
});
return NextResponse.json({ code: 1 }, { status: 200 });
}else{
return NextResponse.json({ code: 0 }, { status: 401 });
}
}
//检查登录状态
export async function GET(request: NextRequest) {
const cookieStore = await cookies();
const token = cookieStore.get('token');
if(token && token.value === '123456'){
return NextResponse.json({ code:1 }, { status: 200 });
}else{
return NextResponse.json({ code:0 }, { status: 401 });
}
}
src/app/page.tsx
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useRouter } from 'next/navigation';
export default function HomePage() {
const router = useRouter();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleLogin = () => {
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
}).then(res => {
return res.json();
}).then(data => {
if(data.code === 1){
router.push('/home');
}
});
}
return (
<div className='mt-10 flex flex-col items-center justify-center gap-4'>
<Input value={username} onChange={(e) => setUsername(e.target.value)} className='w-[250px]' placeholder="请输入用户名" />
<Input value={password} onChange={(e) => setPassword(e.target.value)} className='w-[250px]' placeholder="请输入密码" />
<Button onClick={handleLogin}>登录</Button>
</div>
)
}
src/app/home/page.tsx
'use client';
import { useEffect } from 'react';
import { redirect } from 'next/navigation';
const checkLogin = async () => {
const res = await fetch('/api/login');
const data = await res.json();
if (data.code === 1) {
return true;
} else {
redirect('/');
}
}
export default function HomePage() {
useEffect(() => {
checkLogin()
}, []);
return <div>你已经登录进入home页面</div>;
}
Next.js第七章(路由组)
路由组
路由组也是一种基于文件夹的约定范式,可以让我们开发者,按类别或者团队组织路由模块,并且不影响 URL 路径。
用法:只需要通过(groupName)包裹住文件夹名即可,例如(shop),(user)等,名字可以自定义。
定义多个根布局
这种一般是大型项目使用的,例如我们需要把,后台管理系统和前台的门户网站,放到一个项目就可以使用这种方法实现。
使用方法:
- 先把
app目录下的layout.tsx文件删除 - 在每组的目录下创建
layout.tsx文件,并且定义html,body标签。
Next.js第六章(平行路由)
平行路由
平行路由指的是在同一布局layout.tsx中,可以同时渲染多个页面,例如team,analytics等,这个东西跟vue的router-view类似。
基本用法
平行路由的使用方法就是通过@ + 文件夹名来定义,例如@team,@analytics等,名字可以自定义。
平行路由也不会影响
URL路径。
定义完成之后,我们就可以在layout.tsx中使用team和analytics来渲染对应的页面,他会自动注入layout的props里面
注意:例子中我们使用了解构的语法,这里面的名称
team,analytics需跟文件夹名称一致。
export default function RootLayout({children,team,analytics}:
{children: React.ReactNode,team: React.ReactNode,analytics: React.ReactNode}
) {
return (
<html>
<body>
{team}
{children}
{analytics}
</body>
</html>
)
}
独立路由
当我们使用了平行路由之后,我们为其单独定义loading,error,等组件使其拥有独立加载和错误处理的能力。
default.tsx
首先我们先认识一下子导航,每一个平行路由下面还可以接着创建对应的路由,例如@team下面可以接着创建@team/setting,@team/user等。
那我们的目录结构就是:
├── @team
│ ├── page.tsx
│ ├── setting
│ │ └── page.tsx
└── @analytics
│ └── page.tsx
└── layout.tsx
└── page.tsx
然后我们使用Link组件跳转子导航setting页面
import Link from "next/link"
export default function RootLayout({children,team,analytics}:
{children: React.ReactNode,team: React.ReactNode,analytics: React.ReactNode}) {
return (
<html>
<body>
{team}
{children}
{analytics}
<Link className="text-blue-500 block" href="/setting">Setting</Link>
</body>
</html>
)
}
观察上图我们发现,子导航使用Link组件跳转setting页面时,是没有问题的,但是我们在跳转之后刷新页面,就出现404了,这是怎么回事?
- 当使用软导航
Link组件跳转子页面的时候,这时候@analytics和children依然保持活跃,所以他只会替代@team里面的内容。 - 而当我们使用硬导航
浏览器页面刷新,此时@analytics和children已经失活,因为它的底层原理其实是同时匹配@team和@analytics,children目录下面的setting页面,但是只有@team有这个页面,其他两个没有,所以导致404。
解决方案:使用default.tsx来进行兜底,确保不会404
- @analytics/default.tsx 定义default.tsx文件
- app/default.tsx 定义default.tsx文件
Next.js第五章(动态路由)
动态路由
动态路由是指在路由中使用方括号[]来定义路由参数,例如/blog/[id],其中[id]就是动态路由参数,因为在某些需求下,我们需要根据不同的id来显示不同的页面内容,例如商品详情页,文章详情页等。
基本用法[slug]
使用动态路由只需要在文件夹名加上方括号[]即可,例如[id],[params]等,名字可以自定义。
来看demo: 我们在app/shop目录下创建一个[id]目录
//app/shop/[id]/page.tsx
export default function Page() {
return <div>Page</div>
}
访问路径为:http://localhost:3000/shop/123 其中123就是动态路由参数,这个可以是任意值。
路由片段[...slug]
我们如果需要捕获多个路由参数,例如/shop/123/456,我们可以使用路由片段来捕获多个路由参数,他的用法就是[...slug],其中slug就是路由片段,这个名字可以自定义,后面的片段有多少就捕获多少。
//app/shop/[...id]/page.tsx
export default function Page() {
return <div>Page</div>
}
访问路径为:http://localhost:3000/shop/123/456/789 其中123和456和789就是动态路由参数,后面的片段有多少就捕获多少。
可选路由[[...slug]]
可选路由指的是,我们可能会有这个路由参数,也可能会没有这个路由参数,例如/shop/123,也可能是/shop,我们可以使用可选路由来捕获这个路由参数,他的用法就是[[...slug]],其中slug就是路由片段,这个名字可以自定义,后面的片段有多少就捕获多少。
//app/shop/[[...id]]/page.tsx
export default function Page() {
return <div>Page</div>
}
- 访问路径为:
http://localhost:3000/shop,可以没有参数 - 访问路径为:
http://localhost:3000/shop/123,可以有参数 - 访问路径为:
http://localhost:3000/shop/123/456,可以有多个参数
这种方式比较灵活。
接受参数
使用useParams hook来接受参数,这个hook只能在客户端组件中使用。
'use client'
import { useParams } from "next/navigation";
export default function ShopPage() {
const params = useParams();
console.log(params); //{id: '123'} {id: ['123', '456']} 接受单个值以及多个值
return <div>ShopPage</div>;
}