【人工智能】深入解析!三种实现ChatGPT打字机效果的最佳方案

999 阅读14分钟

在当今AI快速发展的时代,ChatGPT 凭借其强大的自然语言处理能力,已经成为众多开发者和企业的首选工具。然而,如何在前端页面中实现类似于ChatGPT的打字机效果,以提升用户交互体验,成为了一个广受关注的话题。今天,我将为大家深入解析三种实现ChatGPT打字机效果的最佳方案,并分享一些实战中的踩坑经验,助你轻松应对开发中的各种挑战。🎯

技术背景

随着人工智能技术的飞速发展,AI交互应用逐渐成为用户与系统之间的重要桥梁。ChatGPT 作为OpenAI推出的先进语言模型,能够生成自然、流畅的文本回复,极大地提升了用户体验。然而,仅有强大的后台逻辑是不够的,前端的表现形式同样关键,特别是那些能够模拟人类打字行为的打字机效果,不仅能增强互动性,还能提升用户的沉浸感。

本次技术探讨将聚焦于如何在Web端实现类似于ChatGPT的打字机效果。我们将介绍三种常见的实现方案,并详细分析每种方案的优缺点,帮助开发者选择最适合自己项目的方案。此外,还将分享一些在Umijs开发环境Nginx配置中遇到的常见问题及解决方案,助力项目顺利推进。🔧

本文所用示例涉及的技术栈

  • 前端框架:React + Tailwindcss
  • 后端框架:Express
  • 其他库
    • @microsoft/fetch-event-source
    • alova.js

提示:本文假设读者已具备上述技术栈的基本使用知识,重点介绍如何结合这些工具实现打字机效果。


实现方案概览

在实现ChatGPT打字机效果的过程中,我们主要探讨以下三种方案

  1. 方案一:普通请求 + 前端模拟
    • 通过常规的Ajax或Fetch请求获取数据,之后在前端通过定时器模拟打字效果。
  2. 方案二:基于SSE技术的请求实现
    • 使用Server-Sent Events (SSE) 技术,实现数据的实时流式传输与展示。
  3. 方案三(推荐):Fetch请求 + ReadableStream.getReader()流读取
    • 利用Fetch APIReadableStream结合,实现更高效的数据流处理,推荐作为主推方案。

接下来,我们将逐一详细介绍每种方案的实现方式、代码示例及其优缺点分析。🔥


方案一:普通请求 + 前端模拟

描述

方案一的核心思想是通过常规的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并提取content
        dataBlocks.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;

代码详解

  1. 状态管理

    • 使用React的useState管理显示数据。
    • 使用useRef管理定时器的引用,以便在需要时清除定时器。
  2. 发起请求

    • 用户点击按钮后,触发handleClick函数,开始数据请求。
    • 通过fetch请求后端的SSE接口,获取完整的数据流。
  3. 数据处理

    • 使用filterData函数解析响应数据,提取实际内容。
    • 将解析后的完整字符串传递给timerEffect函数,开始逐字符显示。
  4. 打字机效果实现

    • timerEffect函数利用setInterval定时逐个字符地更新显示内容。
    • 当所有字符展示完毕后,清除定时器,结束效果。

效果展示

效果如下所示,通过点击“发起请求”按钮,数据将以打字机效果逐字符展示:

在这里插入图片描述

优缺点分析

优点

  • 简单易行:无需复杂的后端改动,只需前端简单处理即可实现打字机效果。
  • 适用范围广:适用于数据量较小、响应速度快的场景,开发周期短。
  • 低改造成本:对于已有项目,只需在前端添加定时器逻辑,无需调整后端接口。

缺点

  • 用户体验较差:当数据量大或后端响应缓慢时,整个接口请求耗时较长,导致打字效果延迟,影响用户体验。
  • 资源浪费:前端通过定时器逐字符显示,存在性能开销,特别是在高频操作下,可能导致页面卡顿。
  • 无法实时更新:由于一次性获取所有数据,无法实现数据的实时流式展示,缺乏动态性。

建议

适用于问答较少、数据量较小的项目初期阶段,作为临时方案使用。一旦项目规模扩大,建议立即迁移至更高效的SSEReadableStream方案,以提升性能和用户体验。🚀


方案二:基于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;

代码详解

  1. 使用Alova.js库

    • alova.js 是一个封装了SSE请求的现代化网络请求库,简化了SSE的使用流程。
    • 通过createAlova创建实例,并使用useSSE钩子进行SSE请求的管理。
  2. 状态管理

    • 使用React的useState管理显示的数据内容。
    • useEffect监听数据变化,实时更新显示内容。
  3. 发起请求

    • 用户点击按钮后,触发handleClick函数,发送SSE请求。
    • send方法启动SSE连接,close方法关闭连接。
  4. 数据处理

    • useEffect中解析收到的数据,根据事件类型 (start, message, done) 更新显示内容。
    • 在接收到done事件后,主动关闭SSE连接,避免持续监听。

效果展示

通过点击“发起请求”按钮,数据将实时流式展示,效果如下:

在这里插入图片描述

优缺点分析

优点

  • 实时性强:SSE技术能够实时推送数据,用户无需等待全部数据加载完成,即可开始浏览信息。
  • 减少前端负担:通过alova.js等库的封装,简化了SSE的使用,减少了前端逻辑处理。
  • 高效资源利用:无需频繁发起请求,减少服务器资源消耗,提升应用性能。

缺点

  • 无法设置Header参数:SSE的EventSource对象不支持自定义请求头,限制了在请求中添加认证信息或其他自定义头部。
  • 兼容性问题:部分老旧浏览器或特殊环境下,SSE可能无法正常工作,需要考虑兼容性。
  • 服务端处理复杂:需要服务端支持SSE协议,确保数据的正确流式传输,增加了后端实现的复杂度。

建议

适用于新项目,且服务端能够灵活处理SSE请求的场景。若项目需要在请求中添加认证信息或其他自定义头部,需在服务端做额外的处理,如通过URL参数传递认证信息等。此外,建议在项目初期充分测试SSE的兼容性,确保在目标用户环境中正常运行。🌐


方案三(推荐):Fetch请求 + ReadableStream.getReader()流读取

描述

方案三结合了Fetch APIReadableStream,通过fetch请求获取流式数据,并利用ReadableStream.getReader()进行逐步读取。这种方式不仅突破了SSE在请求头设置上的限制,还能更灵活地控制数据流,提升用户体验。由于其高度的可定制性和高效性,被推荐作为实现打字机效果的首选方案。🏆

快速体验中文版GPT - ChatMoss & ChatGPT中文版

核心代码解析

以下是基于React和@microsoft/fetch-event-source库的实现示例:

# 体验中文版GPThttps://pc.aihao123.cn/index.html#/page/login?invite=1141439&fromChannel=CodeMoss_1115_daziji

import 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;

代码详解

  1. 使用@microsoft/fetch-event-source
    • 该库扩展了原生fetch功能,支持SSE风格的数据流读取,同时允许自定义请求头,解决了原生SSE无法设置Header的问题。
  2. 状态管理
    • 使用React的useState管理显示的数据内容。
  3. 发起请求
    • 用户点击按钮后,触发handleClick函数,开始SSE请求。
    • 通过fetchEventSource发起请求,设置自定义请求头(如authorization)。
  4. 数据处理
    • onmessage回调实时接收数据,根据事件类型 (start, message, done) 更新显示内容。

    • onopen回调用于处理连接建立时的逻辑。

    • onerror回调用于处理错误情况,确保连接的稳定性和数据的完整性。

快速体验中文版GPT - ChatMoss & ChatGPT中文版

效果展示

点击“发起请求”按钮,数据将实时流式展示,效果如下:

在这里插入图片描述

优缺点分析

优点

  • 灵活性高:结合Fetch APIReadableStream,实现高度自定义的数据流读取,满足复杂业务需求。
  • 支持自定义请求头:解决了SSE无法设置Header的限制,便于在请求中添加认证信息或其他自定义头部。
  • 实时性强:与SSE类似,能够实时接收并展示数据,提升用户体验。
  • 广泛兼容:基于标准的Fetch APIReadableStream,兼容性良好,适用于大多数现代浏览器。

缺点

  • 实现复杂度较高:相比方案一和方案二,需理解并掌握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数据无法流式传输。解决方法如下:

  1. 关闭压缩中间件: 在启动开发服务器时,添加环境变量UMI_DEV_SERVER_COMPRESS=none,禁用压缩功能。

    UMI_DEV_SERVER_COMPRESS=none umi dev
    
  2. 升级Umijs版本: 确保使用的Umijs版本支持相关配置,推荐升级至4.1.5及以上版本。

注意:此配置仅影响本地开发环境,不会影响生产环境的打包和部署。

Nginx配置

将项目部署到服务器后,可能会发现SSE接口的数据依然无法实时展示,而是等所有数据传输完成后才整体显示。这通常是由于Nginx代理默认启用了缓冲机制。解决方法如下:

  1. 禁用缓冲: 针对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';  
    }
    
  2. 优化代理设置: 确保仅对SSE接口进行上述配置,避免对其他接口产生不必要的影响。

提示:进行Nginx配置修改后,记得重启Nginx服务,使配置生效。


快速体验中文版GPT - ChatMoss & ChatGPT中文版

七、更多文献

【VScode】揭秘编程利器:教你如何用“万能@符”提升你的编程效率! 全面解析ChatMoss & ChatGPT中文版

【VScode】VSCode中的智能编程利器,全面揭秘ChatMoss & ChatGPT中文版

结语

通过本文的详细解析与实战指南,相信大家对实现ChatGPT打字机效果的三种方案有了更为深入的理解。总结如下:

  1. 方案一:普通请求 + 前端模拟,实现简单,适用于初期开发或数据量较小的场景,但用户体验和性能表现一般。
  2. 方案二:基于SSE技术的请求实现,实时性强,适用于新项目,但存在请求头设置限制。
  3. 方案三(推荐):Fetch请求 + ReadableStream.getReader()流读取,灵活高效,支持自定义请求头,适用于复杂业务场景,是当前实现打字机效果的最佳选择。

同时,分享的Umijs开发环境配置Nginx配置踩坑经验,希望能帮助大家在实际项目中少走弯路,快速实现预期功能。💪