在当今AI快速发展的时代,ChatGPT 凭借其强大的自然语言处理能力,已经成为众多开发者和企业的首选工具。然而,如何在前端页面中实现类似于ChatGPT的打字机效果,以提升用户交互体验,成为了一个广受关注的话题。今天,我将为大家深入解析三种实现ChatGPT打字机效果的最佳方案,并分享一些实战中的踩坑经验,助你轻松应对开发中的各种挑战。🎯
文章目录
- 技术背景
- 本文所用示例涉及的技术栈
- 实现方案概览
- 方案一:普通请求 + 前端模拟
- 描述
- 核心代码解析
- 代码详解
- 效果展示
- 优缺点分析
- 方案二:基于SSE技术的请求实现
- 描述
- 核心代码解析
- 代码详解
- 效果展示
- 优缺点分析
- 方案三(推荐):Fetch请求 + ReadableStream.getReader()流读取
- 描述
- 核心代码解析
- 代码详解
- 效果展示
- 优缺点分析
- 拓展优化
- 自定义控制器示例
- 优化效果
- 实战中的踩坑分享
- Umijs开发环境配置
- Nginx配置
- 七、更多文献
- 结语
技术背景
随着人工智能技术的飞速发展,AI交互应用逐渐成为用户与系统之间的重要桥梁。ChatGPT 作为OpenAI推出的先进语言模型,能够生成自然、流畅的文本回复,极大地提升了用户体验。然而,仅有强大的后台逻辑是不够的,前端的表现形式同样关键,特别是那些能够模拟人类打字行为的打字机效果,不仅能增强互动性,还能提升用户的沉浸感。
本次技术探讨将聚焦于如何在Web端实现类似于ChatGPT的打字机效果。我们将介绍三种常见的实现方案,并详细分析每种方案的优缺点,帮助开发者选择最适合自己项目的方案。此外,还将分享一些在Umijs开发环境和Nginx配置中遇到的常见问题及解决方案,助力项目顺利推进。🔧
本文所用示例涉及的技术栈
- 前端框架:React + Tailwindcss
- 后端框架:Express
- 其他库:
@microsoft/fetch-event-source
alova.js
提示:本文假设读者已具备上述技术栈的基本使用知识,重点介绍如何结合这些工具实现打字机效果。
实现方案概览
在实现ChatGPT打字机效果的过程中,我们主要探讨以下三种方案:
-
方案一:普通请求 + 前端模拟
- 通过常规的Ajax或Fetch请求获取数据,之后在前端通过定时器模拟打字效果。
-
方案二:基于SSE技术的请求实现
- 使用Server-Sent Events (SSE) 技术,实现数据的实时流式传输与展示。
-
方案三(推荐):Fetch请求 + ReadableStream.getReader()流读取
- 利用Fetch API与ReadableStream结合,实现更高效的数据流处理,推荐作为主推方案。
接下来,我们将逐一详细介绍每种方案的实现方式、代码示例及其优缺点分析。🔥
方案一:普通请求 + 前端模拟
描述
方案一的核心思想是通过常规的Ajax或Fetch请求一次性获取所有数据,然后在前端通过定时器(如setInterval
)逐字展示,实现打字机效果。该方法无需深层次的后端改动,适用于简单的项目或初期开发阶段。🏁
核心代码解析
以下是基于React和Tailwindcss的实现示例:
import { useRef, useState } from 'react';const SSEOnlyFE = () => {const [data, setData] = useState('');const timer = useRef(null)const handleClick = () => {setData('思考中...')clearTimer()fetch('http://localhost:5000/sse').then(response => {if (!response.ok) {throw new Error('Network response was not ok');}return response.text();}).then(resData => {setData('')const rst = filterData(resData)timerEffect(rst)})}const filterData = (dataString) => {let rst = ''const dataBlocks = dataString.split('data:');// 过滤掉第一个空项(由于split()在字符串开始处不匹配)dataBlocks.shift();// 遍历每个数据块,解析JSON并提取contentdataBlocks.forEach(block => {const jsonData = JSON.parse(block);// 根据 event 来整合最终的数据if (jsonData.event === 'start' || jsonData.event === 'message') {rst += jsonData.content}// 因为当前方案的 fetch 请求获取的是 SSE 连接结束后的整体数据,因此不必在意 jsonData.event 为 done 的状态});return rst}// 用 setInterval 来实现逐个字符的输出const timerEffect = (contentStr) => {const contentList = contentStr.split('')timer.current = setInterval(() => {if (contentList.length > 0) {const content = contentList.shift()setData(prevData => prevData + content);} else {clearTimer()}}, 200)}const clearTimer = () => {if (timer.current) {clearInterval(timer.current)}timer.current = null}return <div className='m-[20px] ml-[40px]'><h2 className='mb-[10px] text-[20px] font-bold'>普通请求 + 前端模拟</h2><div className=' p-[10px] w-[400px] h-[200px] bg-slate-200'><div className='w-[80px] h-[30px] text-center rounded-[10px] bg-blue-300 cursor-pointer' onClick={handleClick}><span>发起请求</span></div><div><div>输出结果:</div><div className='text-[#333] w-[300px] '>{data}</div></div></div></div>
};export default SSEOnlyFE;
代码详解
-
状态管理:
- 使用React的
useState
管理显示数据。 - 使用
useRef
管理定时器的引用,以便在需要时清除定时器。
- 使用React的
-
发起请求:
- 用户点击按钮后,触发
handleClick
函数,开始数据请求。 - 通过
fetch
请求后端的SSE接口,获取完整的数据流。
- 用户点击按钮后,触发
-
数据处理:
- 使用
filterData
函数解析响应数据,提取实际内容。 - 将解析后的完整字符串传递给
timerEffect
函数,开始逐字符显示。
- 使用
-
打字机效果实现:
timerEffect
函数利用setInterval
定时逐个字符地更新显示内容。- 当所有字符展示完毕后,清除定时器,结束效果。
效果展示
效果如下所示,通过点击“发起请求”按钮,数据将以打字机效果逐字符展示:
优缺点分析
优点:
- 简单易行:无需复杂的后端改动,只需前端简单处理即可实现打字机效果。
- 适用范围广:适用于数据量较小、响应速度快的场景,开发周期短。
- 低改造成本:对于已有项目,只需在前端添加定时器逻辑,无需调整后端接口。
缺点:
- 用户体验较差:当数据量大或后端响应缓慢时,整个接口请求耗时较长,导致打字效果延迟,影响用户体验。
- 资源浪费:前端通过定时器逐字符显示,存在性能开销,特别是在高频操作下,可能导致页面卡顿。
- 无法实时更新:由于一次性获取所有数据,无法实现数据的实时流式展示,缺乏动态性。
建议:
适用于问答较少、数据量较小的项目初期阶段,作为临时方案使用。一旦项目规模扩大,建议立即迁移至更高效的SSE或ReadableStream方案,以提升性能和用户体验。🚀
方案二:基于SSE技术的请求实现
描述
方案二利用Server-Sent Events (SSE)技术,通过浏览器原生的EventSource
对象,实现数据的实时流式传输。与方案一相比,SSE能够更高效地处理数据流,减少前端处理负担,提升用户体验。💡
快速体验中文版GPT - ChatMoss & ChatGPT中文版
核心代码解析
以下是基于React和alova.js
库的实现示例:
import React, { useRef, useEffect, useState } from 'react';
import { useSSE } from '@alova/scene-react';
import { createAlova } from 'alova';
import GlobalFetch from 'alova/GlobalFetch';const alovaInstance = createAlova({requestAdapter: GlobalFetch()
});const SSEAlova = () => {// 此处是 alova 库的用法,详情请参考相关文档const method = (value) => alovaInstance.Get('http://localhost:5000/sse', { param: { key: value } });const { data, send, close } = useSSE(method, {immediate: false,});const [value, setValue] = useState('');useEffect(() => {if (data) {try {const jsonData = JSON.parse(data);if (jsonData.event === 'start' || jsonData.event === 'message') {setValue(prevData => prevData + jsonData.content);} else if (jsonData.event === 'done') {// 当消息发送完毕时,接收到 done 的事件,则前端主动关闭,否则会持续获取消息close()}} catch (err) {console.log(err)close()}}}, [data])const handleClick = () => {setValue('')send('begin')}return <div className='m-[20px] ml-[40px]'><h2 className='mb-[10px] text-[20px] font-bold'>SSE请求 + Alova.js</h2><div className=' p-[10px] w-[400px] h-[200px] bg-slate-200'><div className='w-[80px] h-[30px] text-center rounded-[10px] bg-blue-300 cursor-pointer' onClick={handleClick}><span>发起请求</span></div><div><div>输出结果:</div><div className='text-[#333] w-[300px] '>{value}</div></div></div></div>
};export default SSEAlova;
代码详解
-
使用Alova.js库:
alova.js
是一个封装了SSE请求的现代化网络请求库,简化了SSE的使用流程。- 通过
createAlova
创建实例,并使用useSSE
钩子进行SSE请求的管理。
-
状态管理:
- 使用React的
useState
管理显示的数据内容。 useEffect
监听数据变化,实时更新显示内容。
- 使用React的
-
发起请求:
- 用户点击按钮后,触发
handleClick
函数,发送SSE请求。 send
方法启动SSE连接,close
方法关闭连接。
- 用户点击按钮后,触发
-
数据处理:
- 在
useEffect
中解析收到的数据,根据事件类型 (start
,message
,done
) 更新显示内容。 - 在接收到
done
事件后,主动关闭SSE连接,避免持续监听。
- 在
效果展示
通过点击“发起请求”按钮,数据将实时流式展示,效果如下:
优缺点分析
优点:
- 实时性强:SSE技术能够实时推送数据,用户无需等待全部数据加载完成,即可开始浏览信息。
- 减少前端负担:通过
alova.js
等库的封装,简化了SSE的使用,减少了前端逻辑处理。 - 高效资源利用:无需频繁发起请求,减少服务器资源消耗,提升应用性能。
缺点:
- 无法设置Header参数:SSE的
EventSource
对象不支持自定义请求头,限制了在请求中添加认证信息或其他自定义头部。 - 兼容性问题:部分老旧浏览器或特殊环境下,SSE可能无法正常工作,需要考虑兼容性。
- 服务端处理复杂:需要服务端支持SSE协议,确保数据的正确流式传输,增加了后端实现的复杂度。
建议:
适用于新项目,且服务端能够灵活处理SSE请求的场景。若项目需要在请求中添加认证信息或其他自定义头部,需在服务端做额外的处理,如通过URL参数传递认证信息等。此外,建议在项目初期充分测试SSE的兼容性,确保在目标用户环境中正常运行。🌐
方案三(推荐):Fetch请求 + ReadableStream.getReader()流读取
描述
方案三结合了Fetch API和ReadableStream,通过fetch
请求获取流式数据,并利用ReadableStream.getReader()
进行逐步读取。这种方式不仅突破了SSE在请求头设置上的限制,还能更灵活地控制数据流,提升用户体验。由于其高度的可定制性和高效性,被推荐作为实现打字机效果的首选方案。🏆
快速体验中文版GPT - ChatMoss & ChatGPT中文版
核心代码解析
以下是基于React和@microsoft/fetch-event-source
库的实现示例:
# 体验中文版GPT:https://pc.aihao123.cn/index.html#/page/login?invite=1141439&fromChannel=CodeMoss_1115_dazijiimport React, { useRef, useEffect, useState } from 'react';
import { fetchEventSource } from '@microsoft/fetch-event-source';const SSEFetchEventSource = () => {const [value, setValue] = useState('');const handleClick = () => {setValue('思考中...')fetchEventSource('http://localhost:5000/sse', {headers: {'authorization': 'test sse'},onopen(res) {console.log('连接:', res)setValue('')},onmessage(res) {try {const jsonData = JSON.parse(res.data);if (jsonData.event === 'start' || jsonData.event === 'message') {setValue(prevData => prevData + jsonData.content);} else if (jsonData.event === 'done') {// 因为本质还是 fetch 接口,当消息发送完毕时,// 接收到 done 的事件,如无特殊逻辑可以不做处理,有特殊逻辑可以做其他逻辑处理}} catch (err) {console.log(err)}},onerror(err) {console.log('错误:', err)}})}return <div className='m-[20px] ml-[40px]'><h2 className='mb-[10px] text-[20px] font-bold'>Fetch 请求 + ReadableStream </h2><div className=' p-[10px] w-[400px] h-[200px] bg-slate-200'><div className='w-[80px] h-[30px] text-center rounded-[10px] bg-blue-300 cursor-pointer' onClick={handleClick}><span>发起请求</span></div><div><div>输出结果:</div><div className='text-[#333] w-[300px] '>{value}</div></div></div></div>
};export default SSEFetchEventSource;
代码详解
-
使用
@microsoft/fetch-event-source
库:- 该库扩展了原生
fetch
功能,支持SSE风格的数据流读取,同时允许自定义请求头,解决了原生SSE无法设置Header的问题。
- 该库扩展了原生
-
状态管理:
- 使用React的
useState
管理显示的数据内容。
- 使用React的
-
发起请求:
- 用户点击按钮后,触发
handleClick
函数,开始SSE请求。 - 通过
fetchEventSource
发起请求,设置自定义请求头(如authorization
)。
- 用户点击按钮后,触发
-
数据处理:
onmessage
回调实时接收数据,根据事件类型 (start
,message
,done
) 更新显示内容。onopen
回调用于处理连接建立时的逻辑。onerror
回调用于处理错误情况,确保连接的稳定性和数据的完整性。
快速体验中文版GPT - ChatMoss & ChatGPT中文版
效果展示
点击“发起请求”按钮,数据将实时流式展示,效果如下:
优缺点分析
优点:
- 灵活性高:结合
Fetch API
与ReadableStream
,实现高度自定义的数据流读取,满足复杂业务需求。 - 支持自定义请求头:解决了SSE无法设置Header的限制,便于在请求中添加认证信息或其他自定义头部。
- 实时性强:与SSE类似,能够实时接收并展示数据,提升用户体验。
- 广泛兼容:基于标准的
Fetch API
和ReadableStream
,兼容性良好,适用于大多数现代浏览器。
缺点:
- 实现复杂度较高:相比方案一和方案二,需理解并掌握
ReadableStream
的使用,增加了开发复杂度。 - 依赖第三方库:需要引入
@microsoft/fetch-event-source
等库,增加了项目依赖。 - 库维护问题:
@microsoft/fetch-event-source
库更新缓慢,遇到问题可能需自行阅读源码或寻找替代方案。
建议:
强烈推荐作为实现ChatGPT打字机效果的首选方案。其高效、灵活的特性,尤其是在需要自定义请求头及处理复杂数据流的场景下,表现尤为出色。如果团队具备相关技术背景或愿意投入时间学习ReadableStream
,则可以充分利用此方案的优势,打造流畅的用户交互体验。💯
拓展优化
为了进一步提升打字机效果的丝滑度,我们可以在现有方案的基础上,结合前端定时器进行数据流的均匀输出。由于不同大模型返回数据的速度和内容可能存在波动,合理控制数据输出的速率,可以显著提升用户的视觉体验。✨
自定义控制器示例
以下是一个简化版的MessageManager类,用于控制数据的流式输出:
/*** 打字机效果*/
export default class MessageManager {messageList: string[] = [];timer: any = null;timerDelay = 100;onFinish: () => void;onMessage: (message: string) => void;stopFlag = false; // 停止标志,如果设置了停止,但是队列没走完,就会等队列走完之后再停止constructor(messageList: string[],timerDelay: number,onMessage: (message: string) => void,onFinish: () => void,) {this.messageList = messageList;this.timerDelay = timerDelay;this.onFinish = onFinish;this.onMessage = onMessage;}start() {this.timer = setInterval(() => {if (this.messageList.length > 0) {this.consume();} else {if (this.stopFlag) {this.immediatelyStop();}}}, this.timerDelay);}consume() {if (this.messageList.length > 0) {const str = this.messageList.shift();str && this.onMessage(str);}}add(str: string) {if (!str) return;const strChars = str.split('');this.messageList = [...this.messageList, ...strChars];}stop() {this.stopFlag = true;}immediatelyStop() {// 立刻停止clearInterval(this.timer);this.timer = null;this.messageList = [];this.onFinish();}
}
优化效果
通过上述控制器,可以实现更加平滑的打字机效果,确保不同数据块的输出速率保持一致,避免因数据波动导致的展示不均匀。
实战中的踩坑分享
在实际开发过程中,难免会遇到一些意想不到的问题。以下,我将分享在Umijs开发环境配置和Nginx配置中遇到的常见问题及解决方案,希望对大家有所帮助。🔧
Umijs开发环境配置
在项目中使用Umijs脚手架,并采用上述方案三时,可能会遇到SSE请求无法实时获取数据,只能在最后一次性展示的问题。经过调试,发现是由于Umijs开发服务器默认启用了压缩中间件,导致SSE数据无法流式传输。解决方法如下:
-
关闭压缩中间件:
在启动开发服务器时,添加环境变量UMI_DEV_SERVER_COMPRESS=none
,禁用压缩功能。UMI_DEV_SERVER_COMPRESS=none umi dev
-
升级Umijs版本:
确保使用的Umijs版本支持相关配置,推荐升级至4.1.5及以上版本。
注意:此配置仅影响本地开发环境,不会影响生产环境的打包和部署。
Nginx配置
将项目部署到服务器后,可能会发现SSE接口的数据依然无法实时展示,而是等所有数据传输完成后才整体显示。这通常是由于Nginx代理默认启用了缓冲机制。解决方法如下:
-
禁用缓冲:
针对SSE接口,关闭Nginx的缓冲功能,确保数据能实时流式传输。# 配置 SSE 请求 location /sse {proxy_pass http://localhost:5000/sse;# 禁用缓冲proxy_buffering off;# 设置必要的SSE头部proxy_set_header Cache-Control 'no-cache';proxy_set_header Connection 'keep-alive'; }
-
优化代理设置:
确保仅对SSE接口进行上述配置,避免对其他接口产生不必要的影响。
提示:进行Nginx配置修改后,记得重启Nginx服务,使配置生效。
快速体验中文版GPT - ChatMoss & ChatGPT中文版
七、更多文献
【VScode】揭秘编程利器:教你如何用“万能@符”提升你的编程效率! 全面解析ChatMoss & ChatGPT中文版
【VScode】VSCode中的智能编程利器,全面揭秘ChatMoss & ChatGPT中文版
结语
通过本文的详细解析与实战指南,相信大家对实现ChatGPT打字机效果的三种方案有了更为深入的理解。总结如下:
- 方案一:普通请求 + 前端模拟,实现简单,适用于初期开发或数据量较小的场景,但用户体验和性能表现一般。
- 方案二:基于SSE技术的请求实现,实时性强,适用于新项目,但存在请求头设置限制。
- 方案三(推荐):Fetch请求 + ReadableStream.getReader()流读取,灵活高效,支持自定义请求头,适用于复杂业务场景,是当前实现打字机效果的最佳选择。
同时,分享的Umijs开发环境配置和Nginx配置的踩坑经验,希望能帮助大家在实际项目中少走弯路,快速实现预期功能。💪