96SEO 2026-04-24 12:16 3
还记得以前我们在网页上等待服务器响应时那个转个不停的Loading圈圈吗?说实话,那种体验真的让人抓狂。如今随着ChatGPT、Claude等大模型的爆火,“打字机效果”Yi经成为了AI应用的标配。kan着文字一个接一个地蹦出来不仅缓解了等待的焦虑,geng增添了一种“智Neng正在实时思考”的科技感。

hen多刚入门Next.js的朋友问我:“这种流式输出到底是怎么搞的?是不是hen难?”其实只要拆解开来核心逻辑并没有想象中那么复杂。今天咱们就抛开那些晦涩的学术名词,用Zui接地气的方式,带你从零开始,在Next.js中实现一个高性Neng的AI打字机对话界面。
一、 揭秘“流式输出”:它到底是个什么鬼?在动手敲代码之前,咱们得先达成一个共识。传统的HTTP请求,就像是一次性快递。你得等仓库把所有货dou打包好,装上卡车,一路开到你家门口,你才Neng签收。在这个过程中,你只Neng干等。
而流式输出,完全颠覆了这个模式。它geng像是一根水管。数据源那边产生一个字,就通过水管推过来一个字。你的前端不需要等整篇文章写完,只要水管里有水,你就接住并显示出来。这就是为什么ChatGPTNeng像人说话一样,一个字一个字往外蹦的原因。
在技术实现上,我们主要依赖的是 SSE 协议。别被这个名字吓到,你只需要记住一点:它允许服务器单向不断地向客户端推送文本流。
二、 准备工作:磨刀不误砍柴工咱们这次要用的技术栈,是目前Web开发界Zui流行的“黄金组合”:Next.js + React Hooks + TypeScript。为了演示效果,我会接入DeepSeek的API。
咱们得搞定“钥匙”。在项目根目录下新建一个 .env.local 文件。这个文件是用来存放敏感信息的,千万别传到GitHub上去。
DEEPSEEK_API_KEY=这里填入你的API密钥
DEEPSEEK_MODEL=deepseek-chat
配置好这个文件后记得重启一下你的开发服务器,否则环境变量是读不到的,这可是新手Zui容易踩的坑。
三、 后端搭建:打造数据流的“源头”Next.js的Route Handler让我们不需要单独搭建Node.js服务就Neng写后端接口,简直不要太爽。我们需要创建一个API路由,专门负责把前端的请求转发给AI模型,并把模型返回的流“透传”给前端。
请在项目中建立这个文件路径:app/api/ai/stream/route.ts。注意文件夹层级,千万别建错了。
下面是完整的后端代码,我尽量把每一行dou写上了注释,你kan不懂的地方kan注释就Neng明白大概意思。
// app/api/ai/stream/route.ts
import { NextResponse } from "next/server";
// 这里引入一个模拟的用户校验函数,实际项目中你Ke以换成JWT或Session验证
import { getCurrentUser } from "@/lib/current-user";
// ⚠️ 关键点:必须指定运行时为 nodejs
// Edge Runtime虽然冷启动快,但在处理流式透传时不如Node.js稳定,且部分库不支持
export const runtime = "nodejs";
// 禁用缓存,确保每次请求dou是实时的,这对于对话场景至关重要
export const dynamic = "force-dynamic";
// 定义角色类型,防止脏数据混入
type ChatRole = "system" | "user" | "assistant";
// 定义消息结构
type ChatMessage = {
role: ChatRole;
content: string;
};
// 处理POST请求
export async function POST {
// 1. 可选步骤:校验用户登录状态
// Ru果你的应用是开放的,Ke以把这段注释掉
// const user = await getCurrentUser;
// if {
// return NextResponse.json;
// }
// 2. 检查API Key是否存在
const apiKey = process.env.DEEPSEEK_API_KEY;
if {
return NextResponse.json(
{ code: 500, msg: "服务端未配置DEEPSEEK_API_KEY,请检查.env.local文件" },
{ status: 500 }
);
}
// 3. 解析前端传来的数据
const body = await request.json;
// 获取当前问题,并Zuo个简单的去空格处理
const message = typeof body?.message === "string" ? body.message.trim : "";
// 获取历史记录,用于上下文记忆
const historyInput = Array.isArray ? body.history : ;
// 4. 兜底校验:消息不Neng为空
if {
return NextResponse.json(
{ code: 400, msg: "消息不Neng为空,请输入你的问题" },
{ status: 400 }
);
}
// 5. 清洗历史记录
// 这一步非常重要,防止前端传来的格式不对导致报错,同时限制上下文长度省钱
const history: ChatMessage = historyInput
.map => {
if return null;
const role = .role;
const content = .content;
// 严格校验角色和内容类型
if (
||
typeof content !== "string"
) {
return null;
}
return { role: role as ChatRole, content: content.trim } as ChatMessage;
})
.filter: item is ChatMessage => !!item && !!item.content)
.slice; // 只保留Zui近20条,避免Token溢出
// 6. 发起请求到DeepSeek
const upstream = await fetch("https://api.deepseek.com/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "text/event-stream", // 告诉对方:我要流式数据
},
body: JSON.stringify({
model: process.env.DEEPSEEK_MODEL || "deepseek-chat",
stream: true, // 开启流式模式
temperature: 0.7, // 控制随机性,0.7比较适中
messages: ,
}),
});
// 7. 错误处理
if {
const text = await upstream.text.catch => "");
let msg = "AI服务请求失败,请稍后重试";
try {
const parsed = JSON.parse as { error?: { message?: string } };
if msg = parsed.error.message;
} catch {}
// 余额不足的特殊提示
if ) {
msg = "API余额不足,请充值后重试。";
}
return NextResponse.json;
}
// 8. 核心中的核心:透传流
// 我们不需要在Node.js层解析内容,直接把上游的流扔给前端,性NengZui高
return new Response(upstream.body, {
headers: {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive",
// 防止Nginx等反向代理服务器缓冲数据,导致流式失效
"X-Accel-Buffering": "no",
},
});
}
后端逻辑小结
你kan,后端其实就是一个“中转站”。它Zuo三件事:鉴权、转发、透传。Zui关键的就是Zui后那个 new Response,它像接力赛一样,把AI生成的数据流直接交到了前端手里。
前端的工作量稍微大一点,因为我们要处理UI交互、状态管理以及Zui核心的流数据解析。为了代码清晰,我们把前端拆分为两个部分:服务端组件和客户端组件。
1. 页面入口:服务端组件创建文件 app/ai/page.tsx。这个文件主要负责在服务端判断用户有没有登录,没登录就踢走,登录了就渲染聊天框。
// app/ai/page.tsx
import { redirect } from "next/navigation";
import AiClient from "./AiClient"; // 引入我们待会儿要写的客户端组件
import { getCurrentUser } from "@/lib/current-user";
// 强制动态渲染,不走缓存
export const dynamic = "force-dynamic";
export default async function AiPage {
const user = await getCurrentUser;
// 简单的守卫逻辑
if redirect;
return (
{/* 这里Ke以放一些通用的头部导航 */}
你好,{user.username},今天想聊点什么?
{/* 核心交互组件 */}
);
}
2. 核心交互:客户端组件
这是今天的重头戏。创建文件 app/ai/AiClient.tsx。记得一定要在文件Zui上面加上 "use client",因为我们要用到 useState 和 useEffect 这些React的Hook。
// app/ai/AiClient.tsx
"use client";
import { useState, useRef, useEffect } from "react";
// 定义消息类型,TypeScript的好处就是让代码geng健壮
type Msg = {
id: string;
role: "user" | "assistant";
content: string;
};
export default function AIChat {
// 存储对话列表,这就是我们要渲染的数据源
const = useState();
const = useState; // 输入框的值
// 用ref来存储虚拟列表的实例,方便我们调用滚动方法
const listRef = useRef;
// 发送消息的核心函数
const sendMsg = async => {
if ) return; // 防止发空消息
// 1. 先把用户的消息上屏
const userMsg: Msg = { id: `u_${Date.now}`, role: "user", content: input };
setMessages;
const currentInput = input; // 保存一下当前的输入,因为下面要清空
setInput; // 清空输入框
// 2. 准备一个空的AI消息占位,或者等流来了再显示,这里我们选择流来了再追加
// 为了简化逻辑,我们直接在流解析中追加消息
try {
// 3. 发起请求,记得带上Accept头
const res = await fetch("/api/ai/stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "text/event-stream"
},
body: JSON.stringify({
message: currentInput,
history: messages.filter // 别把欢迎语传给AI,浪费Token
}),
});
// 4. 处理流数据
if return;
const reader = res.body.getReader;
const decoder = new TextDecoder;
let buffer = ""; // 缓冲区,处理被截断的数据
// 创建一个临时的AI消息ID,用于追加内容
const aiId = `a_${Date.now}`;
// 先插入一个空消息,或者你Ke以设置个loading状态
setMessages;
while {
const { done, value } = await reader.read;
if break;
// 解码二进制数据
buffer += decoder.decode;
// 按行分割SSE数据
const lines = buffer.split;
// Zui后一行可Neng不完整,放回buffer
buffer = lines.pop || "";
for {
if ) {
const jsonStr = line.slice; // 去掉 "data: " 前缀
if === "") break; // DeepSeek/OpenAI结束标志
try {
const data = JSON.parse;
// 注意:不同模型返回的字段可Neng不同,DeepSeek这里假设是 choices.delta.content
const content = data.choices?.?.delta?.content || "";
if {
// 追加内容到Zui后一条消息
setMessages(prev => {
const newMsgs = ;
const lastMsg = newMsgs;
if {
lastMsg.content += content;
}
return newMsgs;
});
}
} catch {
console.error;
}
}
}
}
} catch {
console.error;
// 这里Ke以加个错误提示Toast
}
};
// 自动滚动到底部
useEffect => {
if {
// 假设虚拟列表组件有scrollToEnd方法
// Ru果是原生div,Ke以用 scrollIntoView
listRef.current.scrollToEnd?.;
}
}, );
return (
{/* 消息列表区域 */}
{messages.map(msg => (
{msg.role === 'user' ? '我' : 'AI'}
{msg.content}
))}
{/* 输入框区域 */}
setInput}
onKeyDown={e => e.key === "Enter" && sendMsg}
placeholder="输入你的问题..."
/>
);
}
前端核心逻辑拆解
上面的代码虽然有点长,但逻辑其实非常清晰:
状态管理我们用 messages 数组存所有的对话。每来一条新数据,我们就geng新这个数组,React就会自动帮我们重新渲染页面。
流式读取这是Zui难理解的部分。我们通过 res.body.getReader 拿到一个读取器。然后在一个 while 循环中不断调用 reader.read。这个方法是异步的,有数据来了它就会返回,没数据它就等着,不会卡死页面。
数据清洗SSE传过来的数据是一坨文本,格式通常是 data: {...}。而且因为网络传输的原因,可Neng一次传过来半行,或者一次传过来两行。所以我们用了一个 buffer 变量来缓存不完整的数据,按行分割后再解析JSON。这是保证程序不崩的关键细节。
增量渲染解析出内容后我们不是替换整个消息,而是找到当前正在生成的Zui后一条消息,把新字拼接到它的 content 后面。这就是打字机效果的视觉来源。
Ru果你的对话非常长,成百上千条消息,直接用 map 渲染可Neng会导致页面卡顿,因为DOM节点太多了。这时候,虚拟列表 就派上用场了。
虚拟列表的原理hen简单:只渲染屏幕可见区域的那几条消息,其他的虽然存在于数据中,但不生成DOM节点。当你滚动时它再动态销毁旧的、创建新的。
你Ke以引入 react-virtualized 或者自己写一个简单的Hook。在上面的代码中,我预留了 VirtualList 的接口。Ru果你要接入,只需要把渲染 messages.map 的那部分代码换成虚拟列表组件即可。这Neng极大提升长对话的流畅度。
写代码Zui怕的就是报错。这里了几个新手Zui容易遇到的问题,kankan你有没有中招:
Q: 页面一直转圈,没有输出?
A: 检查一下 .env.local 文件是不是配置错了或者API Key余额不足。另外kankan后端控制台有没有报错,有时候是DeepSeek那边挂了。
Q: 报错 "Edge Runtime does not support..."
A: 这是因为你在API路由里用了Node.js特有的API,但文件里没声明 export const runtime = "nodejs"。加上这行代码就好。
Q: 流式输出变成了等全部输出完才显示?
A: 这通常是Nginx或者Vercel的缓存机制在作祟。确保后端返回的Header里有 Cache-Control: no-cache 和 X-Accel-Buffering: no。
好了这就是Next.js实现AI打字机效果的全过程。从后端的流式透传,到前端的增量渲染,再到性Neng优化的虚拟列表,我们一步步拆解了其中的奥秘。
其实编程hen多时候就是把复杂的问题拆解成一个个简单的小步骤。不要被那些高大上的名词吓倒,动手写一行代码,你就离成功近了一步。希望这篇文章Neng帮你搞定那个让人头疼的Loading圈圈,让你的应用也Neng拥有丝滑的AI交互体验。下次见!
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback