Vue3 + EventSource 实战:手把手教你打造实时AI聊天悬浮窗(附完整代码)

Vue3 + EventSource 实战:手把手教你打造实时AI聊天悬浮窗(附完整代码)

最近在重构一个内部知识库系统时,我遇到了一个挺有意思的需求:用户希望在任何页面都能随时唤起AI助手进行问答,并且回答内容要像ChatGPT那样一个字一个字“流”出来,体验要足够流畅。这让我想起了很多SaaS产品里常见的客服悬浮窗,但这次要做的更复杂——它得是一个功能完整的、支持流式输出的AI对话窗口。

市面上关于Vue3和EventSource的教程不少,但大多停留在基础连接和数据显示。真正要把这个悬浮窗做得“能用”且“好用”,你会发现有一堆细节要处理:窗口拖拽时不能跑出屏幕外、移动端上手指操作要灵敏、大量流式数据渲染时页面不能卡顿、还得考虑Markdown表格和代码块的优雅展示……这些才是决定功能成败的关键。

这篇文章,我就从一个产品化的视角,带你完整实现一个高可用的实时AI聊天悬浮窗。我会重点分享那些在文档里很少提及,但在实际开发中一定会踩到的“坑”,以及对应的解决方案和性能优化技巧。文末会提供一个可以直接克隆、运行的GitHub仓库地址,你可以基于它快速搭建自己的AI助手功能。

1. 项目架构与核心技术选型

在动手写代码之前,我们先明确一下这个悬浮窗的核心目标和实现路径。我们的目标不是一个简单的演示组件,而是一个可以无缝集成到任何Vue3项目中的、生产可用的独立模块。

核心功能清单

  • 可拖拽悬浮球:作为入口,点击后展开对话窗口。
  • 流式对话窗口:支持实时显示AI的“思考过程”和最终答案。
  • 完整的拖拽与缩放:窗口可自由移动,并能在一定范围内调整大小。
  • 多端适配:在桌面端和移动端均有良好的交互体验。
  • 性能优化:处理大量、快速的流式数据更新时,保持UI流畅。
  • 内容渲染:完美支持Markdown语法,包括代码块、表格、列表等。

基于这些需求,我的技术选型如下:

技术栈 具体库/API 选型理由
前端框架 Vue 3 (Composition API) 更好的TypeScript支持、更灵活的逻辑复用。
流式通信 EventSource (SSE) 协议简单,天然支持服务端推送,非常适合AI文本流。
拖拽与缩放 vue-draggable-resizable (v3) 社区活跃,API稳定,支持触摸事件,省去大量底层开发。
Markdown渲染 marked + DOMPurify marked性能好,DOMPurify用于安全过滤,防止XSS。
UI与样式 原生CSS / Less 保持组件独立性,不依赖特定UI库,方便集成。
构建工具 Vite 快速的冷启动和热更新,提升开发体验。

这里重点说一下为什么选择 EventSource 而不是更常见的 WebSocket。对于AI文本流这种单向、顺序、文本为主的数据流,SSE(Server-Sent Events)协议有天然优势:

  1. 更简单:基于HTTP,无需额外的握手和心跳协议。
  2. 自动重连:浏览器内置了连接断开后的重试机制。
  3. 成本更低:服务端实现更简单,连接开销通常小于WebSocket。

当然,SSE是单向的(服务端到客户端)。如果你的场景需要频繁的客户端上行(如传输文件),那WebSocket更合适。但就纯聊天问答而言,SSE是绝配。

提示:如果你的后端服务不支持SSE,或者你需要更细粒度的控制(如自定义重连、更复杂的数据格式),也可以使用 fetch API 读取 ReadableStream 来实现流式接收,这在后文会提供备选方案代码。

2. 核心实现:流式数据接收与渲染

这是整个项目的灵魂。我们要实现的效果是:用户提问后,AI的回复像打字机一样逐字呈现,中间可能还会穿插“思考中...”这样的中间状态。

2.1 建立EventSource连接与事件解析

首先,我们创建一个独立的Composables函数 useEventSource,来封装所有流式连接的逻辑。这样做的好处是逻辑与UI分离,易于测试和复用。

// composables/useEventSource.js
import { ref, onUnmounted } from 'vue';

export function useEventSource(url, options = {}) {
  const data = ref('');
  const error = ref(null);
  const isLoading = ref(false);
  const eventSource = ref(null);

  const connect = () => {
    if (eventSource.value) {
      eventSource.value.close();
    }

    isLoading.value = true;
    error.value = null;
    data.value = '';

    const es = new EventSource(url);
    eventSource.value = es;

    es.onopen = () => {
      console.log('SSE连接已建立');
      isLoading.value = false;
    };

    es.onmessage = (event) => {
      // 默认处理没有event字段的消息
      try {
        const parsed = JSON.parse(event.data);
        // 这里可以根据业务需要处理通用数据
        console.log('收到消息:', parsed);
      } catch (e) {
        // 如果是纯文本流
        data.value += event.data;
      }
    };

    // 处理自定义事件,例如 `event: think`, `event: answer`
    es.addEventListener('think', (event) => {
      try {
        const parsed = JSON.parse(event.data);
        // 触发一个自定义回调,由父组件处理
        options.onThink?.(parsed);
      } catch (e) {
        console.error('解析think事件失败:', e);
      }
    });

    es.addEventListener('answer', (event) => {
      try {
        const parsed = JSON.parse(event.data);
        options.onAnswer?.(parsed);
      } catch (e) {
        console.error('解析answer事件失败:', e);
      }
    });

    es.onerror = (err) => {
      console.error('SSE连接错误:', err);
      error.value = err;
      isLoading.value = false;
      es.close();
    };

    // 监听服务器端可能发送的“结束”事件
    es.addEventListener('close', () => {
      console.log('SSE流结束');
      isLoading.value = false;
      es.close();
    });
  };

  const disconnect = () => {
    if (eventSource
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值