🤔你知道AI如何通过SSE流式渲染到页面的吗(附带完整案例)

目的

使用SSE+deepseekAPI+react+node.js实现流式输出markdown语法

效果速看

SSE简介

  • SSE(Server-Sent Events)译为服务器推送事件,通过EventSource接口实现服务器推送通信。
  • EventSource 实例会对 HTTP 服务器开启一个持久化的连接,以 text/event-stream 格式发送事件

SSE使用

服务端设置

  • 请求头设置
  • Content-Typetext/event-stream来开启SSE
  • 连接状态Connectionkeep-alive
  • 实时推送需要设置Cache-Controlno-cahce停用缓存
js 复制代码
const express = require("express");
const cors = require("cors");
const app = express();

// 允许跨域
app.use(cors());

// SSE端点
app.get("/sse", (req, res) => {
  // 设置SSE需要的headers
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });
  // 定时发送消息
  let counter = 0;
  const intervalId = setInterval(() => {
    counter++;
    res.write(`event: message\n`);
    res.write(`data: 这是第${counter}次推送\n\n`);
    if (counter > 10) {
      clearInterval(intervalId);
    }
  }, 1000);

  // 客户端断开连接时清理
  req.on("close", () => {
    clearInterval(intervalId);
    console.log("客户端断开连接");
  });
});
// 启动服务器
const PORT = 8080;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

客户端设置

  • new EventSource创建实例
  • 监听message事件
js 复制代码
    <div id="root"></div>
    <script>
      const sse = new EventSource("http://localhost:8080/sse");
      sse.addEventListener("message", (e) => {
        console.log("---数据", e.data);
        root.innerHTML += e.data + " ";
        //页面渲染
      });
    </script>

- 缺点:EventSource只支持get请求

可读流实现SSE

ReadableStream

用fetch发送post请求,使用ReadableStream实现流式传输

js 复制代码
const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "text/event-stream",
    },
    body: JSON.stringify(data),
  }).catch((err) => {
    console.log("err报错了", err);
  });
  // 获取 ReadableStream 并创建读取器
  const reader = response.body.getReader();
}

reader.read()得到的value为字节流

extDecoder 解码

js 复制代码
  const decoder = new TextDecoder();
  // 持续读取流数据
  while (true) {
    const { done, value } = await reader.read();
    if (done) {
      reader.releaseLock();
      break;
    } // 流结束
    console.log("字节流", value);
    const chunk = decoder.decode(value);
    console.log("解码后数据为", chunk);
  }

markdown解析

引入markdown

js 复制代码
import { marked } from "marked";
marked.parse(value.current)

失去重连

  • 注意:自己实现SSE的话, SSE将失去重连
  • 解决方法:第三方库,@microsoft/fetch-event-source
js 复制代码
import { fetchEventSource } from '@microsoft/fetch-event-source';

fetchEventSource('http://localhost:3000/sse',{
    method:'POST',
    headers:{
        'Content-Type':'application/json'
    },
    body:JSON.stringify({message:'Hello, SSE!'}),
    onmessage:(event)=>{
        console.log(JSON.parse(event.data))
    },
    onerror:(event)=>{
        console.log(event)
    }
})

具体代码

ai调用

js 复制代码
let OpenAI = require("openai");

const openai = new OpenAI({
  baseURL: "https://api.deepseek.com/v1",
  apiKey: "XXXXXX",//这里用自己的
});

async function main(content, res) {
  try {
    const response = await openai.chat.completions.create({
      messages: [
        {
          role: "system",
          content:
            "You are a translation expert. Please format your responses in Markdown syntax.",
        },
        { role: "user", content: content },
      ],
      model: "deepseek-chat",
      stream: true,
    });

    for await (const chunk of response) {
      if (chunk.choices && chunk.choices[0] && chunk.choices[0].delta) {
        const content = chunk.choices[0].delta.content || "";
        res.write(`data: ${JSON.stringify({ text: content })}\n\n`);
      }
    }
  } catch (error) {
    console.error("Error in OpenAI request:", error);
    res.write(
      `event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`
    );
  } finally {
    res.end();
  }
}
module.exports = { main };

服务器

js 复制代码
const express = require("express");
const app = express();
const PORT = 8080;
const cors = require("cors");
const { main } = require("./ai.js");

// 允许跨域
app.use(cors());
app.use(express.json());

app.post("/sse", (req, res) => {
  try {
    let body = req.body;
    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    });

    main(body.content, res);
  } catch (error) {
    console.error("Error in handling request:", error);
    res.write(
      `event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`
    );
    res.end();
  }
});
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

前端代码

js 复制代码
import React, { useState, useRef } from "react";
import { marked } from "marked";

const App = () => {
  const value = useRef("");
  const [renderedContent, setRenderedContent] = useState("");
  const inputRef = useRef(null);

  const fetchSSE = async () => {
    const url = "http://localhost:8080/sse";
    const inputValue = inputRef.current.value;
    const data = {
      userId: 123,
      content: inputValue,
      date: new Date().toISOString(),
    };

    try {
      const response =
        inputValue &&
        (await fetch(url, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Accept: "text/event-stream",
          },
          body: JSON.stringify(data),
        }));

      // 获取 ReadableStream 并创建读取器
      const reader = response.body.getReader();
      const decoder = new TextDecoder();

      // 持续读取流数据
      while (true) {
        const { done, value } = await reader.read();
        if (done) {
          reader.releaseLock();
          break;
        } // 流结束
        const chunk = decoder.decode(value);
        const events = chunk.split("\n\n"); // SSE 事件以双换行分隔
        events.forEach((event) => {
          if (event.trim() === "") return;
          parseSSEEvent(event);
        });
      }
    } catch (err) {
      console.log("err报错了", err);
    }
  };

  // 解析单个 SSE 事件
  const parseSSEEvent = (event) => {
    const lines = event;
    let dataObj = JSON.parse(lines.split(": ")[1]);
    value.current += dataObj.text;
    setRenderedContent(marked.parse(value.current));
  };

  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={fetchSSE}>发送</button>
      <div dangerouslySetInnerHTML={{ __html: renderedContent }} />
    </div>
  );
};

export default App;

参考

SSE(Server-Sent Events),解密AI流式输出和呈现SSE,解密AI流式输出和网站页面呈现 效果速看 - 掘金

相关推荐
小着1 小时前
vue项目页面最底部出现乱码
前端·javascript·vue.js·前端框架
lichenyang4534 小时前
React ajax中的跨域以及代理服务器
前端·react.js·ajax
呆呆的小草4 小时前
Cesium距离测量、角度测量、面积测量
开发语言·前端·javascript
一 乐5 小时前
民宿|基于java的民宿推荐系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·源码
testleaf6 小时前
前端面经整理【1】
前端·面试
好了来看下一题6 小时前
使用 React+Vite+Electron 搭建桌面应用
前端·react.js·electron
啃火龙果的兔子6 小时前
前端八股文-react篇
前端·react.js·前端框架
小前端大牛马6 小时前
react中hook和高阶组件的选型
前端·javascript·vue.js
刺客-Andy6 小时前
React第六十二节 Router中 createStaticRouter 的使用详解
前端·javascript·react.js
萌萌哒草头将军8 小时前
🚀🚀🚀VSCode 发布 1.101 版本,Copilot 更全能!
前端·vue.js·react.js