今天我学习了Next.js + Ollama + TypeScript,收获真的超级多!没想到一天时间就能做出一个真正能用的AI聊天应用,并且是将模型下载到本地,减少了我们平常调用API的花费,让我兴奋得简直想马上分享给大家。下面我就来详细回顾一下今天学到的内容,保证通俗易懂,就算是前端小白也能看懂!
老规矩,先来看一下具体效果:

准备工作
首先我们得下载 Ollama,下载完成后,可以通过命令行来看看是否下载成功:ollama --version

然后选择模型,这里我选择得是一个比较小得模型,太大了下载太慢了,这本来就是一个学习怎么使用,所以我就下载得是比较小得模型:

ollama pull deepseek-r1:1.5b
在命令行输入,然后下载所选模型,
ollama run deepseek-r1:1.5b
测试是否能够运行

我们页可以查看本地的11434端口查看是否运行成功

项目整体架构
首先,我得搞清楚这个项目是做什么的。简单来说,我构建了一个可以在浏览器里和AI模型聊天的应用!用户在页面上输入消息,消息会发送到我们写的API接口,然后API去调用本地运行的Ollama服务,最后把AI的回复展示给用户。
这涉及到几个关键部分:前端页面(page.tsx)、API接口(route.ts)和数据类型定义(chat.ts)。它们各司其职,配合得天衣无缝!
数据类型定义:打好基础
先从最简单的开始看------数据类型定义。在chat.ts
文件中,我定义了聊天功能需要的各种类型:
typescript
export type Message = {
role: 'user' | 'assistant' | 'system' | 'tool';
content: string
}
export type ChatResponse = {
model: string; // 使用的模型名称
created_at: string; // 响应生成的时间戳
message: Message; // 核心:模型返回的消息内容
done: boolean; // 流式传输结束标志
total_duration: number; // 整个请求的总耗时(纳秒)
load_duration: number; // 模型加载耗时(纳秒)
prompt_eval_count: number; // 提示词(prompt)处理的 token 数量
prompt_eval_duration: number; // 处理提示词的耗时(纳秒)
eval_count: number; // 生成回复的 token 数量
eval_duration: number; // 生成回复的耗时(纳秒)
}
export type ChatRequest = {
model: string;
messages: Message[]
stream?: boolean; // ? 可选的 默认是false
}
为什么要先定义类型呢?这就好比建房子要先画图纸一样。TypeScript是类型安全的,提前定义好数据结构能让代码更加可靠,减少错误。
Message
类型表示一条聊天消息,它有role
和content
两个字段。role
可以是'user'、'assistant'、'system'或'tool',表示消息的发送者身份;content
就是消息的实际内容。
ChatRequest
是我们要发送给Ollama API的请求结构,包含要使用的模型名称和消息列表。这里的stream?
后面的问号表示这个字段是可选的,如果不传,默认就是false。
ChatResponse
是Ollama API返回的响应结构,包含了很多有用的信息!不只是AI回复的消息内容,还有各种性能指标,比如处理耗时、token数量等。这些信息对于优化应用性能很有帮助。
前端页面:用户看到的界面
接下来看看用户直接交互的页面部分,在page.tsx
文件中:
typescript
'use client'; // 组件在客户端执行
import { useState } from 'react';
import type { Message } from '@/types/chat';
export default function Home() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState<string>("")
const [isLoading, setIsLoading] = useState<boolean>(false);
// ...其余代码
}
第一行的'use client'
指令很重要,它告诉Next.js这个组件是在客户端(也就是浏览器中)执行的,而不是在服务器端。这是因为我们需要处理用户的交互和状态管理。
然后我导入了useState
钩子和Message
类型。useState
是React中管理状态的神器!它让函数组件也能拥有状态管理能力。
在Home组件中,我定义了三个状态:
messages
:存储所有聊天消息的数组input
:存储用户当前输入的文本isLoading
:表示是否正在等待AI回复的标志
useState的用法是const [变量名, 设置函数名] = useState<类型>(初始值)
。比如useState<Message[]>([])
表示初始值为空数组,数组中的每个元素都是Message类型。
javascript
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim || isLoading) return;
const userMessage: Message = {
role: 'user',
content: input,
}
setMessages((prev) => [...prev, userMessage])
setInput('')
setIsLoading(true)
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ messages: [...messages, userMessage] })
})
const data = await res.json();
const assistantMessage: Message = data.message;
setMessages((prev) => [...prev, assistantMessage])
} catch (err) {
// 错误处理
} finally {
setIsLoading(false)
}
}
handleSubmit
函数处理表单提交事件。当用户点击发送按钮或按回车时,这个函数就会被调用。
首先调用e.preventDefault()
阻止表单的默认提交行为(防止页面刷新)。然后检查输入是否为空或是否正在加载中,如果是就直接返回,不做任何处理。
接着创建用户消息对象,并将其添加到消息列表中。这里用了setMessages((prev) => [...prev, userMessage])
,意思是"取之前的所有消息,再加上新的用户消息"。
[...messages, userMessage]
这种写法叫做扩展运算符(spread operator),它相当于创建了一个新数组,包含原数组的所有元素和新元素。
然后清空输入框并将加载状态设为true,这样界面上就会显示"正在加载"的提示。
最关键的部分是使用fetch
API向我们的后端接口发送请求:await fetch('/api/chat', {...})
。这里用了async/await
语法,让异步代码看起来像同步代码一样清晰。
请求成功后,从响应中获取AI回复的消息,并将其添加到消息列表中。无论成功还是失败,最后都要将加载状态设回false。
ini
return (
<div className="flex flex-col h-screen bg-gray-100">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{
messages.length === 0 ? (
<p className="text-center text-gray-500 mt-10">
开始与deepseek模型聊天吧
</p>
) : (
messages.map((msg, idx) => (
<div
key={msg.content}
className={
`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`
}
>
<div
className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg
${msg.role === 'user' ? 'bg-blue-500 text-white'
: 'bg-white text-gray-800 shadow'
}
`}
>
<p>{msg.content}</p>
</div>
</div>
))
)
}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white text-gray-800 shadow px-4 py-2
rounded-lg max-w-xs lg:max-w-md
">
<p>DeepSeek正在思考....</p>
</div>
</div>
)}
</div>
<div className="p-4 bg-white border-t">
<form onSubmit={handleSubmit} className='flex space-x-2'>
<input
type='text'
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder='输入你的消息'
className='flex-1 p-2 border rounded-lg focus:outline-none
focus:ring-2 focus:ring-blue-500
'
/>
<button
type='submit'
disabled={isLoading}
className='px-4 py-2 bg-blue-500 text-white rounded-lg
hover:bg-blue-600 disabled:bg-blue-300 disabled:cursor-not-allowed
'
>
{isLoading ? '发送中...' : '发送'}
</button>
</form>
</div>
</div>
)
页面的UI部分使用了Tailwind CSS来样式化。整个页面是一个垂直flex容器,占据整个屏幕高度。
消息列表部分会根据消息数量显示不同的内容:如果没有消息,显示提示文本;如果有消息,就用map
函数遍历所有消息并渲染出来。
每条消息根据发送者的不同有不同的样式:用户消息靠右显示,蓝色背景;AI消息靠左显示,白色背景。这种条件样式通过模板字符串和三元表达式实现。
加载状态时显示"DeepSeek正在思考...."的提示。
底部是输入表单,包含一个文本输入框和一个发送按钮。输入框的值绑定到input
状态, onChange事件更新这个状态。按钮在加载状态下会禁用,并显示不同的文本。
API路由:连接前端和Ollama的桥梁
现在来看看后端的API路由,在route.ts
文件中:
javascript
import type { NextResponse } from "next/server";
import {
Message,
ChatResponse,
ChatRequest,
} from "@/types/chat";
const oLLAMA_API_URL = "http://localhost:11434/api/chat";
const MODEL_NAME = 'deepseek-r1:1.5b';
export async function POST(request: NextResponse) {
try {
const body: { messages: Message[] } = await request.json();
const ollamaRequestBody: ChatRequest = {
model: MODEL_NAME,
messages: body.messages,
stream: false
}
const response = await fetch(oLLAMA_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(ollamaRequestBody)
})
if (!response.ok) {
const errorText = await response.text
return Response.json(
{
error: `Ollama API Error:${errorText}`
}
)
}
const ollamaData: ChatResponse = await response.json()
return Response.json(ollamaData)
} catch (err) {
console.log('Chat API Error:', err);
return Response.json(
{
status: 500
}
)
}
}
这个文件定义了一个POST接口,它充当了前端和Ollama服务之间的代理。
首先定义了Ollama API的URL和要使用的模型名称。Ollama默认在本地11434端口提供服务,所以我们连接到http://localhost:11434/api/chat
。
在POST函数中,首先从请求体中获取前端发送的消息列表。然后构建要发送给Ollama的请求体,包括模型名称、消息列表和stream选项(这里设为false,表示不使用流式传输)。
接下来使用fetch向Ollama API发送请求。如果请求失败,返回错误信息;如果成功,将Ollama的响应直接返回给前端。
整个流程用try-catch包裹,确保任何错误都能被捕获并处理,返回500状态码。
总结与思考
通过今天的学习,我深刻理解了Next.js全栈开发的魅力!前端、API路由、类型定义各司其职,构成了一个完整的应用。
前端页面负责用户交互和状态管理,使用React的useState钩子来管理聊天消息、用户输入和加载状态。
API路由作为中间层,接收前端请求,转发给Ollama服务,再将响应返回给前端。这种架构的好处是前端不需要直接连接Ollama,提高了安全性和灵活性。
类型定义确保了整个应用的数据结构一致性,减少了潜在的错误。
最让我兴奋的是,这个应用虽然代码量不大,但涵盖了现代Web开发的很多核心概念:React状态管理、API调用、异步处理、错误处理、TypeScript类型系统等。
而且这个应用是真正可用的!我可以在浏览器里和本地的AI模型聊天,看到消息一来一回,那种成就感真的无法用言语形容。