SSE post 实践
assassin_cike 5 月 17 日 新加坡 阅读 5 分钟 1需求:对接大模型的聊天功能
疑惑:但是接口是post方法,需要传一些复杂的数据,而EventSource不支持post,那我们应该怎么办呢?
思路:SSE (Server-Sent Events) Using A POST Request Without EventSource
办法:用fetch的post
实验:sse-demo
客户端
async function fetchData() { const response = await fetch("/sse", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ user_id: 123, }), }); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); while (true) { const { value, done } = await reader.read(); if (done) break; console.log("Received:", value); } }
webpack.config.js 配置代理
const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { mode: "development", entry: "./index.js", output: { path: path.resolve(__dirname, "dist"), filename: "[name].bundle.js", clean: true, }, optimization: { runtimeChunk: "single", }, plugins: [ new HtmlWebpackPlugin({ title: "Development", template: "index.html", }), ], devtool: "inline-source-map", devServer: { port: 8345, static: "./dist", proxy: [ { context: ["/sse"], target: "http://127.0.0.1:3333", changeOrigin: true, }, ], }, };
node.js服务
// server.js const http = require("http"); // Create a HTTP server const server = http.createServer((req, res) => { // Check if the request path is /stream if (req.url === "/sse") { // Set the response headers res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-type", "Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS", Connection: "keep-alive", }); // Create a counter variable let counter = 0; // Create an interval function that sends an event every second const interval = setInterval(() => { // Increment the counter counter++; // Create an event object with name, data, and id properties const event = { name: "message", data: `Hello, this is message number ${counter}`, id: counter, }; // Convert the event object to a string const eventString = `event: ${event.name}\ndata: ${event.data}\nid: ${event.id}\n\n`; // Write the event string to the response stream res.write(eventString); // End the response stream after 10 events if (counter === 10) { clearInterval(interval); res.end(); } }, 1000); } else { // Handle other requests res.writeHead(404); res.end("Not found"); } }); server.listen(3333, () => { console.log("Server listening on port 3333"); });
启动服务,点击button触发fetchData函数,发现服务端的数据并不是流式输出到客户端的,而是等所有数据准备好后一次性返回给了客户端,这不是我想要的,排查,SSE doen't work with vue-cli devServer proxy,于是改了webpack配置
devServer: { port: 8345, static: "./dist", compress: false, proxy: [ { context: ["/sse"], target: "http://127.0.0.1:3333", changeOrigin: true, ws: true, }, ], },
新增了compress: false
跟ws: true,
,再次发送请求,数据一个个被吐出来了,但是到底是哪个参数起了作用,经过测试证明是compress: false
的作用。但是公司的项目使用的umijs@4没有这个配置项,搜,umijs4由于无法配置decServer中的compress属性 导致无法实时输出sse请求的数据,将UMI_DEV_SERVER_COMPRESS=none umi dev
配置好还是无法输出,原来我的umijs版本不够新,只好升级到最新npm update umi
,但是还是不能流式输出,问题到底在哪?还有一点后端返回的数据并没有出现在EventSream面板,而我的demo的数据跟竞品的数据都会出现在该面板,如下
从而推断是接口返回的数据不对,原来返回的数据要有固定的格式,Server-Sent Events 教程告诉后端数据要被包裹在data: json数据 \n\n
之中,从而数据流式的出现在了EventSream面板,但是我的控制台打印出来的数据的个数跟EventSream面板的数据不一致且有很多重复的数据格式出现在一个输出字符串里,又开始怀疑后端的数据有问题,仔细推敲,已经正确的流式的输出在了EventSream面板,应该不是接口的问题,而是我解析的问题,尝试eventsource-parser
export const useSendMessageWithSse = ( url: string = api.completeConversation, ) => { const [answer, setAnswer] = useState<IAnswer>({} as IAnswer); const [done, setDone] = useState(true); const send = useCallback( async (body: any) => { try { setDone(false); const response = await fetch(url, { method: 'POST', headers: { [Authorization]: getAuthorization(), 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); const reader = response?.body ?.pipeThrough(new TextDecoderStream()) .pipeThrough(new EventSourceParserStream()) .getReader(); while (true) { const x = await reader?.read(); if (x) { const { done, value } = x; try { const val = JSON.parse(value?.data || ''); const d = val?.data; if (typeof d !== 'boolean') { console.info('data:', d); setAnswer(d); } } catch (e) { console.warn(e); } if (done) { console.info('done'); break; } } } console.info('done?'); setDone(true); return response; } catch (e) { setDone(true); console.warn(e); } }, [url], ); return { send, answer, done }; };
这次终于得到了正确的结果,不容易啊。eventsource-parser/stream
作用是将sse接口返回的字符串转为对象且避免了debug断点时接口不间断返回的数据被塞到一个字符串的问题,完整的测试代码在,sse-demo
参考:
Event Streaming Made Easy with Event-Stream and JavaScript Fetch