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: falsews: true,,再次发送请求,数据一个个被吐出来了,但是到底是哪个参数起了作用,经过测试证明是compress: false的作用。但是公司的项目使用的umijs@4没有这个配置项,搜,umijs4由于无法配置decServer中的compress属性 导致无法实时输出sse请求的数据,将UMI_DEV_SERVER_COMPRESS=none umi dev配置好还是无法输出,原来我的umijs版本不够新,只好升级到最新npm update umi,但是还是不能流式输出,问题到底在哪?还有一点后端返回的数据并没有出现在EventSream面板,而我的demo的数据跟竞品的数据都会出现在该面板,如下
image.png
从而推断是接口返回的数据不对,原来返回的数据要有固定的格式,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

4fa1aee3c4aa891123447455b1578ae.png

参考:
Event Streaming Made Easy with Event-Stream and JavaScript Fetch


assassin_cike
1.3k 声望74 粉丝

生活不是得过且过