前端接收流式数据demo,并用markdown解析数据,包括EventSource和fetch两种方式
AI模型对话,前端接受流式数据效果demo
可以直接复制代码
如上视频,要实现类似deepseek和chatgpt的效果,用传统的方式接受数据根本行不通,必须使用SSE, Server-Sent Events(SSE)功能,允许服务端推送数据到客户端,通常叫数据推送。当数据源有新数据,它马上发送到客户端,不需要再等待客户端请求。
这里提供了完整的前端代码和用node.js写的后端模拟数据,分别包括了get和post方式,因为EventSource只支持get方式,后用fetch支持了post方式,两种效果都是一样的。
前端用的vue2,node版本v16.20.2,介绍一篇nvm管理node版本的文章(https://blog.csdn.net/qq_41822157/article/details/141259132)
DeepSeekChatGet.vue
<template><div class="deepseek-chat"><div class="chat-container" ref="chatContainer"><div class="messages"><divv-for="(message1, index) in messages":key="index":class="['message', message1.role]"><div class="avatar"><span v-if="message1.role === 'user'">你</span><span v-else>DS</span></div><div class="message-content"><divclass="message-body"v-for="(message, index1) in message1.arr":key="index1"><!-- 文本内容 --><div v-if="message1.role === 'user'" class="text-content"><p>{{ message.content }}</p></div><div v-else class="text-content"><p v-if="index === 0">{{ message.content }}</p><div v-else><div><pv-html="md.render(message.content)"class="chatDatas-box"></p></div></div></div><!-- 加载状态 --><divv-if="index1 === messages[messages.length - 1].arr.length - 1 &&isStreaming"class="typing-indicator"><span></span><span></span><span></span></div></div></div></div></div></div><div class="input-area"><div class="input-container"><textareav-model="userInput"placeholder="输入消息..."@keydown.enter.exact.prevent="sendMessage3"@keydown.shift.enter="newLine"rows="2"ref="textarea"></textarea><button @click="sendMessage3" :disabled="isStreaming"><span v-if="!isStreaming">发送</span><span v-else>生成中...</span></button></div></div></div>
</template><script>
class DeepSeekStream {constructor() {this.buffer = "";this.eventSource = null;}connect(url, onMessage, onComplete, onError, onStart) {this.eventSource = new EventSource(url);this.eventSource.addEventListener("message", (event) => {try {// 确保处理完整的消息if (event.data.trim()) {const message = JSON.parse(event.data);onMessage(message);}} catch (error) {onError(`消息解析错误: ${error.message}`);}});this.eventSource.addEventListener("end", () => {onComplete();this.close();});this.eventSource.addEventListener("error", (error) => {onError(`流错误: ${error.message}`);this.close();});this.eventSource.addEventListener("open", () => {//监听连接成功的时候onStart();});}close() {if (this.eventSource) {this.eventSource.close();this.eventSource = null;}}
}// 导入解析markdown语法的第三方库markdown-it
import MarkdownIt from "markdown-it";// 引入代码高亮
import hljs from "highlight.js";
// 你用到了什么语言就要引入什么语言,目前我还不知道怎么样导入所有语言
import javascript from "highlight.js/lib/languages/javascript";
// 这个是高亮的样式,有很多,我选了这个
import "highlight.js/styles/ir-black.css";hljs.registerLanguage("javascript", javascript);
import "highlight.js/styles/github-dark.css";
export default {name: "DeepSeekChat",data() {return {userInput: "js中 array.sort的用法",messages: [{role: "assistant",model: "deepseek-chat",arr: [{content: "您好!请输入您的问题",type: "text",},],},],isStreaming: false,streamHandler: null,selectedModel: "deepseek-chat",chatDatas: [],md: new MarkdownIt({html: true, // 允许HTML标签linkify: true, // 自动转换URL为链接typographer: true, // 优化排版breaks: true, // 将换行符转换为<br>highlight: function (str, lang) {// 代码高亮处理if (lang && hljs.getLanguage(lang)) {try {return ('<pre class="hljs"><code>' +hljs.highlight(lang, str, true).value +"</code></pre>");} catch (error) {console.error(error);}}return ('<pre class="hljs"><code>' +this.md.utils.escapeHtml(str) +"</code></pre>");},}),};},computed: {},methods: {sendMessage3() {if (!this.userInput.trim() || this.isStreaming) return;// 添加用户消息this.messages.push({role: "user",model: this.selectedModel,arr: [{content: this.userInput,},],});const prompt = this.userInput;// const prompt = "111";this.userInput = "";// 添加AI消息占位this.messages.push({role: "assistant",model: this.selectedModel,arr: [],});let arr1 = { content: "思考中,请稍候..." };this.messages[this.messages.length - 1].arr.push(arr1);// 创建流处理器this.streamHandler = new DeepSeekStream();const url = `http://localhost:3000/deepseek-stream?prompt=${encodeURIComponent(prompt)}&model=${this.selectedModel}`;console.log("this.messages", this.messages);this.streamHandler.connect(url,(data) => {// console.log("data--", data.content);arr1.content += data.content;// console.log("this.chatDatas---", this.chatDatas);// 滚动到底部this.scrollToBottom();},() => {console.log("结束了");this.isStreaming = false;this.scrollToBottom();},(error) => {console.error("流错误:", error);},() => {// console.log("开始---111");this.messages[this.messages.length - 1].arr[0].content = "";});this.scrollToBottom();},newLine() {this.userInput += "\n";this.$nextTick(() => {this.$refs.textarea.scrollTop = this.$refs.textarea.scrollHeight;});},copyCode(code) {navigator.clipboard.writeText(code).then(() => {// this.$toast.success("代码已复制到剪贴板");});},scrollToBottom() {this.$nextTick(() => {const container = this.$refs.chatContainer;container.scrollTop = container.scrollHeight;});},},mounted() {this.scrollToBottom();},beforeDestroy() {if (this.streamHandler) {this.streamHandler.close();}},
};
</script><style lang="less" scoped>