目的
使用
SSE
+deepseekAPI
+react
+node.js
实现流式输出markdown
语法
效果速看

SSE简介
- SSE(Server-Sent Events)译为服务器推送事件,通过
EventSource
接口实现服务器推送通信。EventSource
实例会对 HTTP 服务器开启一个持久化的连接
,以text/event-stream
格式发送事件
SSE使用
服务端设置
- 请求头设置
Content-Type
为text/event-stream
来开启SSE- 连接状态
Connection
为keep-alive
- 实时推送需要设置
Cache-Control
为no-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流式输出和网站页面呈现 效果速看 - 掘金