前端使用vue
1.逐字输出 闪动css样式
<span id="response_row" class="result-streaming">{{ item.assistantContent }}</span>
.result-streaming:after {-webkit-animation: blink 1s steps(5, start) infinite;animation: blink 1s steps(5, start) infinite;content: "▋";margin-left: 0.25rem;vertical-align: baseline;
}
2.使用fetch/eventSource/fetchEventSource进行sse流式处理
先贴最后成功使用的
使用fetchEventSource方法
参考代码:https://blog.csdn.net/cuiyuchen111/article/details/129468291
参考/下载文档:https://www.npmjs.com/package/@microsoft/fetch-event-source?activeTab=readme
以下为后端接口要求
前端代码
<p v-if="item.requestFlag" class="content robot_content"><span id="response_row" class="result-streaming">{{ item.assistantContent }}</span></p>
<p class="content robot_content"><span v-html="item.assistantContent"></span></p>
async getResponseFromAPI() {const that = this;this.sendLoading = true;// 用户提问时间let userTime = that.getNowTime();const form = JSON.parse(JSON.stringify(this.form));console.log(form, "请求里的form");//物理添加 页面that.conversations.push({userContentId: "",userContent: form.prompt,userContentDatetime: userTime,assistantContentId: "",assistantContent: "",assistantContentDatetime: userTime,requestFlag: true,});// 对话请求const abortController = new AbortController();let formData = new FormData();formData.append("chatid", this.currentChatId);formData.append("clientid", form.clientid);formData.append("prompt", form.prompt);const url = "xxxxx";const headers = new Headers();const body = formData;const eventSource = fetchEventSource(url, {method: "POST",headers,body,signal: abortController.signal,onmessage(e) {that.handleScrollBottom();that.form.prompt = "";const response_row = document.getElementById("response_row");console.log(e.data);let res = JSON.parse(e.data);let index = that.conversations.length - 1;if (res.message == "[DONE]") {res.data = JSON.parse(res.data);console.log(res.data);let obj = {userContentId: res.data.userContentId,userContent: res.data.userContent,userContentDatetime: userTime,assistantContentId: res.data.assistantContentId,assistantContent: res.data.assistantContent,assistantContentDatetime: that.getNowTime(),requestFlag: false,};console.log(obj);that.$set(that.conversations, index, obj);that.sendLoading = false;abortController.abort();eventSource.close();console.log("我是结束!!");} else {var content = res.data;response_row.innerText += content;// console.log(content)// if (content.includes("[ENTRY]")) {// content = content.replaceAll("[ENTRY]", "\n");// }}},onclose() {console.log("close");that.sendLoading = false;abortController.abort();eventSource.close();},onerror(error) {let index = that.conversations.length - 1;that.conversations.splice(index, 1);that.sendLoading = false;console.log("error", error);abortController.abort();eventSource.close();},});}
遇到的问题:
1.只调用一次事件 但fetch请求发送了两次或多次且终止失败
//按照fetchEventSource文档内的写法 请求暂停无效
const ctrl = new AbortController();
fetchEventSource('/api/sse', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({foo: 'bar'}),signal: ctrl.signal,
});//而后看到一种说法,fetchEventSource是针对EventSource API的,而不是xhr或fetch API
//因此定义EventSource存储接口所返回的数据 使用EventSource的暂停方法 =》 fetchEventSource暂停成功
const eventSource = fetchEventSource(url, {method: "POST",headers,body,signal: abortController.signal,onmessage(e) {eventSource.close();},onclose() {eventSource.close();},onerror(error) {eventSource.close();},});
以下为fetch/eventSource使用过程中遇到的问题
1.使用fetch方式进行sse流式处理
优点:可以使用post请求
缺点:获取到的数据处理困难 获取事件返回格式或有错误
参考代码:https://blog.csdn.net/betterAndBetter_/article/details/129900233
http://681314.com/A/YaHyYpjoPF
async function send() {const input = document.getElementById("input").value;const output = document.getElementById("output");output.innerText = "";const url = "/api/stream";const data = { "Prompt": input };
//直接获取 Fetch 的response, 无法使用 await的话, Promise的方式也是可以的。const response = await fetch(url, {method: "POST",body: JSON.stringify(data),headers: {"Content-Type": "application/json"}})//获取UTF8的解码const encode = new TextDecoder("utf-8");//获取body的readerconst reader = response.body.getReader();// 循环读取reponse中的内容while (true) {const { done, value } = await reader.read();if (done) {break;}// 解码内容const text = encode.decode(value);// 当获取错误token时,输出错误信息if (text === "<ERR>") {output.innerText = "Error";break;} else {// 获取正常信息时,逐字追加输出output.innerText += text;}}
}
【记得补截图】
2.使用eventSource进行sse流式处理
优点:获取到的数据格式规范 易处理
缺点:无法使用post请求
参考b站视频:https://www.bilibili.com/video/BV1QA411C7mN/?spm_id_from=333.880.my_history.page.click&vd_source=384646ea9baa6985ceb5331bff5b87b0
var rsource = (this.rsource = new EventSource(`/api/chat/repeat/${this.cid}`));rsource.addEventListener("open", function () {console.log("connect");});//如果服务器响应报文中没有指明事件,默认触发message事件rsource.addEventListener("message", function (e) {console.log(`resp:(${e.data})`);var rconv = that.conversation[that.conversation.length - 1];if (e.data == "[DONE]") {rsource.close();rconv["loading"] = false;that.convLoading = false;that.refrechConversation();that.rsource = undefined;return;}var content = e.data;if (content.includes("[ENTRY]")) {content = content.replaceAll("[ENTRY]", "\n");}// 滚动到最下面that.handleScrollBottom();var idx = rconv.idx;rconv["speeches"][idx] += content;that.refrechConversation();});//发生错误,则会触发error事件rsource.addEventListener("error", function (e) {console.log("error:" + e.data);rsource.close();that.rsource = undefined;});
由于eventSource获取到的数据比fetch流畅许多,所以研究过eventSource能否使用post请求,使用过以下代码,但失败了
3.fetch和eventSource同时使用
优点:可以很顺利的请求并且获取到数据
缺点:fetch支持post eventSource不支持post 对接口请求方式有要求 几乎不太能兼容
// 获取表单元素
const form = document.querySelector('#my-form');// 监听表单提交事件
form.addEventListener('submit', (event) => {event.preventDefault(); // 阻止默认提交行为const formData = new FormData(form); // 创建 FormData 对象// 发送 POST 请求并接收 SSE 流式输出fetch('/api/submit-form', {method: 'POST',body: formData}).then((response) => {// 如果请求成功,则创建 EventSource 对象监听 SSE 输出if (response.ok) {const eventSource = new EventSource('/api/stream');eventSource.onmessage = (event) => {const data = JSON.parse(event.data);console.log(data); // 处理接收到的数据};eventSource.onerror = (error) => { // 监听错误事件console.error(error);};}}).catch((error) => {console.error(error);});
});