大家好~ 今天给大家带来一篇「零基础也能上手」的 SSE 教学,核心目标是让你学会用 SSE 实现 AI 流式对话 Agent(就是 ChatGPT 那种“打字机”实时输出效果)。

整篇文章全程无晦涩概念,所有代码都附带详细注释,复制就能运行,不管你是刚入门前端的新手,还是想快速落地流式对话功能的开发者,都能看懂、会用。话不多说,直接开干!

一、先搞懂:SSE 到底是什么?

在学代码之前,我们先搞清楚核心技术 SSE 是什么,不用记复杂定义,记住一句话就够了:

SSE(Server-Sent Events,服务器发送事件)是 HTML5 原生支持的技术,作用是「客户端连接服务器后,服务器能主动、持续地向客户端推送数据」,不用客户端反复发请求。

1. 为什么 AI 流式对话首选 SSE?

我们平时用的 AI 对话(比如 ChatGPT),不是等所有内容生成完再一次性返回,而是逐字、逐句实时输出,这种效果用 SSE 实现最适合,原因有 4 个:

  • 轻量:基于 HTTP 协议,不用像 WebSocket 那样升级协议,开发成本极低

  • 原生:前端自带 EventSource API,不用装任何第三方库

  • 适配:单向通信(服务器 → 客户端),刚好匹配 AI 流式输出的场景(只需要服务器推数据给前端)

  • 省心:自带自动重连、心跳机制,不用自己手写复杂的重连逻辑

2. SSE vs 轮询 vs WebSocket

很多人会把这三个技术搞混,这里做一个直观对比,帮你快速分清适用场景:

技术

核心原理

适用场景

学习难度

SSE

服务器单向推送,基于 HTTP

AI 流式回复、系统通知、实时日志

⭐(最低)

轮询

客户端定时向服务器发请求,问“有没有新数据”

简单数据更新(如定时刷新公告)

⭐⭐

WebSocket

客户端与服务器双向通信,需升级协议

实时聊天、游戏、直播弹幕

⭐⭐⭐(最高)

总结:做 AI 流式对话,SSE 是性价比最高、最容易上手的选择,不用多学复杂的双向通信逻辑。

二、前置准备

不用准备复杂的环境,只要 3 样东西,5 分钟就能搞定:

  1. 代码编辑器:推荐 VS Code(免费、好用,新手友好)

  2. 基础储备:懂一点点 HTML、CSS、JS(不用精通,能看懂简单代码就行)

  3. Node.js 环境:用于运行我们的简易后端(模拟 AI 流式输出),官网下载安装即可(安装时一路下一步,不用额外配置)

⚠️ 注意:本文全程用「原生 JS」编写,不依赖 Vue、React 等任何前端框架,新手可以直接跟着敲代码。

三、项目结构(极简,2 个文件搞定)

我们整个项目只有 2 个文件,结构非常简单,新建一个文件夹(比如叫 sse-ai-agent),里面放这两个文件即可:

sse-ai-agent/
  ├─ index.html       # 前端页面(核心:聊天界面 + SSE 逻辑)
  └─ server.js        # 简易后端(模拟 AI 流式输出,新手可直接复制)
解释:前端负责显示聊天界面、发起 SSE 连接、接收服务器推送的流式数据;后端负责模拟 AI 生成内容,逐字推送给前端。

四、核心步骤 1:编写前端代码(完整可运行,带详细注释)

这是前端的核心文件,包含「聊天界面 + 输入框 + 发送/中断按钮 + SSE 连接 + 打字机效果 + 错误处理」,所有代码都加了注释,新手能看懂每一行的作用。新建 index.html 文件,复制下面的代码,直接保存即可:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SSE 流式 AI 对话 Agent(新手版)</title>
  <style>
    /* 全局样式:重置默认样式,让界面更美观 */
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }

    body {
      max-width: 800px; /* 限制页面宽度,避免在宽屏上太分散 */
      margin: 0 auto; /* 水平居中 */
      padding: 20px;
      background: #f5f7fa; /* 浅灰色背景,更舒适 */
    }

    /* 聊天容器:显示所有消息,可滚动 */
    .chat-container {
      height: 600px; /* 固定高度,方便查看历史消息 */
      background: white;
      border-radius: 12px; /* 圆角,更美观 */
      padding: 20px;
      overflow-y: auto; /* 消息过多时可滚动 */
      margin-bottom: 20px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.05); /* 轻微阴影,增加层次感 */
    }

    /* 消息气泡通用样式 */
    .message {
      margin-bottom: 16px; /* 消息之间的间距 */
      padding: 12px 16px;
      border-radius: 8px;
      max-width: 75%; /* 消息最大宽度,避免占满整个屏幕 */
      line-height: 1.5; /* 行高,提升可读性 */
    }

    /* 用户消息:右侧显示,绿色背景 */
    .user-message {
      background: #009688;
      color: white;
      margin-left: auto; /* 右对齐 */
    }

    /* AI 消息:左侧显示,灰色背景 */
    .ai-message {
      background: #e9ecef;
      color: #333;
      margin-right: auto; /* 左对齐 */
      white-space: pre-wrap; /* 保留空白和换行,让 AI 回复格式更规范 */
    }

    /* 输入框区域:固定在底部,方便输入 */
    .input-area {
      display: flex; /* 弹性布局,让输入框和按钮在同一行 */
      gap: 10px; /* 输入框和按钮之间的间距 */
    }

    #userInput {
      flex: 1; /* 输入框占满剩余空间 */
      padding: 14px 16px;
      border: 1px solid #ddd;
      border-radius: 8px;
      font-size: 16px;
      outline: none; /* 取消默认聚焦边框 */
    }

    /* 输入框聚焦时,边框变色,提示用户当前处于输入状态 */
    #userInput:focus {
      border-color: #009688;
    }

    button {
      padding: 14px 24px;
      background: #009688;
      color: white;
      border: none;
      border-radius: 8px;
      cursor: pointer; /* 鼠标悬浮时显示手型,提示可点击 */
      font-size: 16px;
    }

    /* 按钮悬浮时,轻微变浅,提升交互体验 */
    button:hover {
      opacity: 0.9;
    }

    /* 禁用状态的按钮:灰色,不可点击 */
    button:disabled {
      background: #ccc;
      cursor: not-allowed; /* 禁用时显示“禁止”鼠标样式 */
    }

    /* 加载提示:AI 思考时显示,提示用户正在处理 */
    .loading {
      color: #666;
      font-style: italic; /* 斜体,区分普通文本 */
      padding: 8px 0;
    }
  </style>
</head>
<body>
  <!-- 聊天容器:显示用户和 AI 的消息 -->
  <div class="chat-container" id="chatContainer"></div>
  <!-- 输入区域:输入框 + 发送/中断按钮 -->
  <div class="input-area">
    <input type="text" id="userInput" placeholder="请输入你的问题..." autocomplete="off">
    <button id="sendBtn">发送</button>
    <button id="abortBtn" disabled>中断</button>
  </div>

  <script>
    // ==================== 1. 获取页面元素(DOM 操作)====================
    // 聊天容器:用于渲染消息
    const chatContainer = document.getElementById('chatContainer');
    // 用户输入框:获取用户输入的问题
    const userInput = document.getElementById('userInput');
    // 发送按钮:点击发送问题
    const sendBtn = document.getElementById('sendBtn');
    // 中断按钮:点击中断 SSE 连接(停止 AI 输出)
    const abortBtn = document.getElementById('abortBtn');

    // 存储 SSE 连接对象:全局变量,方便后续中断连接
    let eventSource = null;

    // ==================== 2. 渲染消息到页面(通用函数)====================
    // 功能:将用户/AI 的消息添加到聊天容器中
    // 参数1:message 消息内容
    // 参数2:type 消息类型(user:用户消息;ai:AI 消息)
    function addMessage(message, type) {
      // 1. 创建消息元素(div 标签)
      const messageDom = document.createElement('div');
      // 2. 给消息元素添加类名(用于区分用户/AI 消息,设置不同样式)
      messageDom.className = `message ${type}-message`;
      // 3. 设置消息内容
      messageDom.textContent = message;
      // 4. 将消息元素添加到聊天容器中
      chatContainer.appendChild(messageDom);
      // 5. 自动滚动到底部,让用户能看到最新消息
      chatContainer.scrollTop = chatContainer.scrollHeight;
      // 6. 返回创建的消息元素(后续 AI 流式输出时,需要不断追加内容)
      return messageDom;
    }

    // ==================== 3. 发送消息 + 建立 SSE 连接(核心逻辑)====================
    function sendMessage() {
      // 1. 获取用户输入的问题,去除首尾空格(避免用户输入空内容)
      const userMessage = userInput.value.trim();
      if (!userMessage) {
        alert('请输入你的问题哦~'); // 提示用户输入内容
        return;
      }

      // 2. 渲染用户消息到页面(让用户看到自己发送的内容)
      addMessage(userMessage, 'user');
      // 3. 清空输入框,方便用户输入下一个问题
      userInput.value = '';

      // 4. 禁用发送按钮、启用中断按钮(防止重复发送,同时允许中断)
      sendBtn.disabled = true;
      abortBtn.disabled = false;

      // 5. 创建 AI 消息占位(先显示“加载中”,后续用流式数据填充)
      const aiMessageDom = addMessage('', 'ai');
      aiMessageDom.innerHTML = '<span class="loading">AI 思考中...</span>';

      // ==================== 关键:建立 SSE 连接 ====================
      // 后端接口地址:携带用户的问题(encodeURIComponent 处理中文,避免乱码)
      const apiUrl = `http://localhost:3000/chat?msg=${encodeURIComponent(userMessage)}`;

      // 创建 SSE 实例(前端原生 API,不用装任何库)
      // withCredentials: false 表示不携带 cookie(跨域时常用,新手不用改)
      eventSource = new EventSource(apiUrl, { withCredentials: false });

      // ------------------- SSE 事件监听(3 个核心事件)-------------------
      // 1. onmessage:收到服务器推送的消息时触发(最核心的事件)
      eventSource.onmessage = function (event) {
        // event.data:服务器推送过来的具体数据(字符串格式)
        const pushData = event.data;

        // 约定:如果服务器推送 [DONE],表示 AI 内容生成完成
        if (pushData === '[DONE]') {
          eventSource.close(); // 关闭 SSE 连接(节省资源)
          sendBtn.disabled = false; // 恢复发送按钮(允许用户发送下一个问题)
          abortBtn.disabled = true; // 禁用中断按钮(已经完成,无需中断)
          aiMessageDom.querySelector('.loading')?.remove(); // 移除“加载中”提示
          return;
        }

        // 移除“加载中”提示,开始逐字追加 AI 内容(实现打字机效果)
        aiMessageDom.querySelector('.loading')?.remove();
        aiMessageDom.textContent += pushData; // 追加内容(关键:流式效果的核心)
        // 自动滚动到底部,让用户看到最新的 AI 回复
        chatContainer.scrollTop = chatContainer.scrollHeight;
      };

      // 2. onopen:SSE 连接建立成功时触发
      eventSource.onopen = function () {
        console.log('SSE 连接已建立,等待 AI 回复...'); // 控制台打印,方便调试
      };

      // 3. onerror:SSE 连接出错时触发(比如后端崩溃、网络中断)
      eventSource.onerror = function (error) {
        console.error('SSE 连接出错:', error); // 控制台打印错误信息,方便排查
        // 给用户提示错误信息
        aiMessageDom.textContent += '\n【连接异常,请重试】';
        eventSource.close(); // 关闭出错的连接
        sendBtn.disabled = false; // 恢复发送按钮
        abortBtn.disabled = true; // 禁用中断按钮
      };
    }

    // ==================== 4. 中断 SSE 连接(停止 AI 输出)====================
    function abortConnection() {
      if (eventSource) {
        eventSource.close(); // 关闭 SSE 连接
        eventSource = null; // 重置连接对象
        sendBtn.disabled = false; // 恢复发送按钮
        abortBtn.disabled = true; // 禁用中断按钮
        // 给 AI 消息追加“已中断”提示
        const lastAiMessage = chatContainer.querySelector('.ai-message:last-child');
        if (lastAiMessage) {
          lastAiMessage.textContent += '\n【已中断回复】';
        }
      }
    }

    // ==================== 5. 绑定按钮事件(交互逻辑)====================
    // 发送按钮:点击触发 sendMessage 函数
    sendBtn.addEventListener('click', sendMessage);
    // 输入框:按 Enter 键也能发送消息(提升用户体验)
    userInput.addEventListener('keydown', function (e) {
      if (e.key === 'Enter') {
        sendMessage();
      }
    });
    // 中断按钮:点击触发 abortConnection 函数
    abortBtn.addEventListener('click', abortConnection);
  </script>
</body>
</html>

前端代码关键说明

不用死记硬背代码,重点理解这 3 个核心点:

  1. EventSource:前端原生的 SSE 实例,只要传入后端接口地址,就能建立连接,不用额外配置。

  2. onmessage 事件:服务器每推送一次数据,这个事件就会触发一次,我们在这里实现“逐字追加”,就能做出打字机效果。

  3. 中断功能:通过 eventSource.close() 关闭 SSE 连接,就能停止接收服务器推送的数据,实现“中断回复”。

五、核心步骤 2:编写简易后端代码(模拟 AI 流式输出)

前端需要一个后端接口来建立 SSE 连接,这里我们用 Node.js 编写一个极简后端,功能是:接收用户的问题,模拟 AI 逐字生成回复,然后通过 SSE 推送给前端。

新建 server.js 文件,复制下面的代码,保存即可:

// 导入 Node.js 内置的 http 模块(用于创建服务器)
const http = require('http');
// 导入 url 模块(用于解析前端传递的参数)
const url = require('url');

// 1. 创建 HTTP 服务器
const server = http.createServer((req, res) => {
  // 解析前端请求的 URL,获取查询参数(用户输入的问题)
  const parsedUrl = url.parse(req.url, true);
  const pathname = parsedUrl.pathname; // 请求路径
  const userMsg = parsedUrl.query.msg; // 用户输入的问题(从查询参数中获取)

  // 2. 只处理 /chat 路径的请求(前端 SSE 连接的接口)
  if (pathname === '/chat') {
    // 关键:设置响应头,告诉前端这是 SSE 类型的响应
    res.writeHead(200, {
      'Content-Type': 'text/event-stream', // 必须设置为 text/event-stream,标识这是 SSE 响应
      'Cache-Control': 'no-cache', // 禁止缓存,确保实时接收数据
      'Connection': 'keep-alive', // 保持连接,让服务器能持续推送数据
      'Access-Control-Allow-Origin': '*' // 允许跨域(前端和后端端口不同,必须设置,否则会报错)
    });

    // 模拟 AI 回复内容(新手可以修改这里的内容,自定义 AI 回复)
    const aiReply = `你好!你刚才问的是:"${userMsg}"。我是用 SSE 实现的流式 AI 助手,正在逐字向你推送回复哦~ 这个效果就是 SSE 的核心能力,服务器可以持续、主动地向客户端推送数据,不用客户端反复请求。是不是很神奇?赶紧试试输入其他问题吧!`;

    // 3. 模拟 AI 逐字输出(核心:每隔 50 毫秒推送一个字符,实现打字机效果)
    let index = 0; // 字符索引,用于逐字截取回复内容
    const interval = setInterval(() => {
      // 每次截取一个字符(从 index 位置开始,截取 1 个字符)
      const char = aiReply[index];
      if (char) {
        // 向前端推送数据(SSE 格式:data: 数据\n\n,必须严格遵循这个格式)
        res.write(`data: ${char}\n\n`);
        index++; // 索引加 1,下次截取下一个字符
      } else {
        // 所有字符推送完成,推送 [DONE] 标识,告诉前端结束
        res.write(`data: [DONE]\n\n`);
        clearInterval(interval); // 清除定时器,停止推送
        res.end(); // 结束响应
      }
    }, 50); // 每隔 50 毫秒推送一个字符,速度可以调整(数值越小,打字越快)

    // 处理客户端断开连接的情况(比如用户关闭页面、点击中断)
    req.on('close', () => {
      clearInterval(interval); // 清除定时器
      res.end(); // 结束响应
      console.log('客户端断开 SSE 连接');
    });
  } else {
    // 处理其他路径的请求(返回 404)
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('404 Not Found');
  }
});

// 3. 启动服务器,监听 3000 端口(端口可以修改,但要和前端接口地址对应)
const port = 3000;
server.listen(port, () => {
  console.log(`SSE 服务器已启动,监听端口:${port}`);
  console.log(`前端访问地址:http://localhost:${port}/index.html`);
});

后端代码关键说明

后端的核心是「设置 SSE 响应头 + 逐字推送数据」,重点记住 2 点:

  1. 响应头必须设置 Content-Type: text/event-stream,否则前端无法识别这是 SSE 连接。

  2. 推送数据的格式必须是 data: 数据\n\n(data: 后面跟推送的内容,然后两个换行),这是 SSE 的标准格式,少一个换行都会报错。

⚠️ 注意:后端的端口是 3000,前端接口地址写的是 http://localhost:3000/chat,两者必须一致,否则会出现跨域错误。

六、运行项目

代码写好后,我们来运行项目,看看实际效果,步骤非常简单:

步骤 1:启动后端服务器

  1. 打开 VS Code,打开我们创建的 sse-ai-agent 文件夹。

  2. 打开 VS Code 的终端(顶部菜单:终端 → 新建终端)。

  3. 在终端中输入命令:node server.js(注意:确保已经安装了 Node.js,否则会报错)。

  4. 如果终端显示 SSE 服务器已启动,监听端口:3000,说明后端启动成功。

步骤 2:打开前端页面

  1. 打开浏览器(推荐 Chrome、Edge)。

  2. 在地址栏输入:http://localhost:3000/index.html,按下回车。

  3. 此时会看到聊天界面,输入问题(比如“什么是 SSE?”),点击发送,就能看到 AI 逐字输出的打字机效果啦!

常见问题排查(新手必看)

如果运行失败,大概率是以下 3 个问题,对照排查即可:

  • 问题 1:终端输入 node server.js 报错 → 检查 Node.js 是否安装成功(打开终端输入 node -v,能显示版本号就是安装成功)。

  • 问题 2:前端页面无法连接,控制台报错“跨域” → 检查后端代码中是否有 Access-Control-Allow-Origin: *(必须加上,否则跨域会被拦截)。

  • 问题 3:AI 不输出内容 → 检查后端代码中 aiReply 是否有内容,以及定时器的时间是否合理(50 毫秒是比较合适的速度)。

七、进阶优化(可选,新手可后续学习)

如果想让这个流式对话 Agent 更完善,可以尝试以下优化(本文不展开,但给出方向,新手可以逐步探索):

  1. 对接真实 AI 接口(如 OpenAI、文心一言):将后端的“模拟 AI 回复”替换成真实的 AI 接口请求,实现真正的 AI 流式对话。以下是对接 OpenAI 接口的简化代码(可直接复制替换,附详细注释):

    // 1. 先安装依赖(终端输入命令):npm install openai
    // 2. 替换原有 server.js 中“模拟 AI 回复”相关代码,保留其他逻辑不变
    const OpenAI = require('openai');
    
    // 初始化 OpenAI 实例(替换成你的 API Key,从 OpenAI 官网获取)
    const openai = new OpenAI({
      apiKey: '你的 OpenAI API Key', // 关键:替换成自己的 API Key
    });
    
    // 替换原有“模拟 AI 回复”部分,改为调用 OpenAI 流式接口
    // 注意:保留前面的响应头设置、路径判断等代码,只替换 aiReply 和定时器部分
    async function getOpenAIStream(userMsg, res) {
      try {
        // 调用 OpenAI 流式接口(gpt-3.5-turbo 模型,适合新手,成本低)
        const stream = await openai.chat.completions.create({
          model: "gpt-3.5-turbo",
          messages: [
            { role: "user", content: userMsg } // 传入用户的问题
          ],
          stream: true, // 开启流式输出(核心,实现打字机效果)
        });
    
        // 监听流数据,逐段推送给前端
        for await (const chunk of stream) {
          // 提取 AI 回复的内容(处理空数据情况,避免报错)
          const content = chunk.choices[0]?.delta?.content || '';
          if (content) {
            // 按照 SSE 标准格式,推送给前端
            res.write(`data: ${content}\n\n`);
          }
        }
    
        // 所有内容推送完成,发送结束标识
        res.write(`data: [DONE]\n\n`);
        res.end();
      } catch (error) {
        // 错误处理:推送错误信息给前端
        res.write(`data: 【AI 接口请求失败:${error.message}】\n\n`);
        res.write(`data: [DONE]\n\n`);
        res.end();
        console.error('OpenAI 接口错误:', error);
      }
    }
    
    // 调用方式(替换原有定时器部分)
    // 注释掉原来的 aiReply 和 setInterval 代码,替换为:
    getOpenAIStream(userMsg, res);

    补充说明:需先在终端输入 npm install openai 安装依赖(确保后端文件夹下执行该命令)。

  2. API Key 需从 OpenAI 官网注册获取(免费额度足够测试,注意保护好自己的 Key,不要泄露)。

  3. 代码仅替换后端 server.js 中“模拟 AI 回复”的核心逻辑,其他响应头、跨域设置等代码保持不变,确保和前端 SSE 连接兼容。

  4. 若无法访问 OpenAI 接口,可替换为国内 AI 接口(如文心一言、通义千问),逻辑类似,只需修改接口调用代码即可。。

  5. 增加错误重试:SSE 连接失败时,自动重试连接,提升用户体验。

八、总结

到这里,你已经学会了用 SSE 实现 AI 流式对话 Agent,总结一下核心知识点:

  1. SSE 是前端原生的服务器单向推送技术,适合 AI 流式回复、通知等场景,比 WebSocket 更轻量、更容易上手。

  2. 前端用 EventSource 建立 SSE 连接,通过 onmessage 接收服务器推送的数据,实现打字机效果。

  3. 后端需要设置 SSE 响应头,按照 data: 数据\n\n 的格式逐字推送数据。

  4. 整个项目只有 2 个文件,代码全注释,复制就能运行,新手也能快速上手。

其实 SSE 没有想象中复杂,核心就是“服务器持续推数据,前端持续接数据”,只要掌握了这个逻辑,就能轻松实现各种流式效果。

如果觉得这篇文章对你有帮助,欢迎点赞、收藏、转发~ 有任何问题,评论区留言,我会一一回复!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐