1. SSE实现流式对话
1.1 什么是SSE?
Server-Sent Events (SSE) 是一种基于HTTP的服务器推送技术,允许服务器向客户端发送事件流。与WebSocket不同,SSE是单向的(只从服务器到客户端),专为数据流式传输设计,特别适合AI对话这类需要实时展示生成内容的场景。
SSE的主要优势:
- 基于标准HTTP,无需额外协议
- 自动重连机制
- 支持事件ID和自定义事件类型
- 轻量级,实现简单
1.2 SSE流式对话框架
下面是一个实现SSE流式对话的通用框架:
主要搭配EventSource API实现,EventSource是专门为服务器发送事件设计的API。
<template>
<div class="sse-demo">
<h2>SSE Demo</h2>
<div class="controls">
<button @click="startSSEConnection" :disabled="isConnected">
开始接收消息
</button>
<button @click="closeSSEConnection" :disabled="!isConnected">
停止接收
</button>
</div>
<div class="messages">
<h3>接收到的消息:</h3>
<div v-if="messages.length === 0" class="no-messages">
暂无消息
</div>
<ul v-else>
<li v-for="(message, index) in messages" :key="index">
{{ message }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'SSEDemo',
data() {
return {
// SSE连接对象
eventSource: null,
// 是否已连接
isConnected: false,
// 接收到的消息列表
messages: []
}
},
methods: {
// 开始SSE连接
startSSEConnection() {
// 关闭可能存在的连接
this.closeSSEConnection();
try {
// 创建EventSource连接
// 注意:这里的URL应该指向你的SSE服务端点
this.eventSource = new EventSource('/api/sse-stream');
this.isConnected = true;
// 监听消息事件
this.eventSource.onmessage = (event) => {
try {
// 解析消息数据
const data = JSON.parse(event.data);
// 添加到消息列表
this.messages.push(`${new Date().toLocaleTimeString()}: ${data.content}`);
} catch (error) {
console.error('解析SSE消息失败:', error);
this.messages.push(`[错误] 无法解析消息: ${event.data}`);
}
};
// 监听连接打开事件
this.eventSource.onopen = () => {
console.log('SSE连接已建立');
this.messages.push(`[系统] 连接已建立`);
};
// 监听错误事件
this.eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
this.messages.push(`[错误] 连接发生错误`);
// 如果连接关闭,更新状态
if (this.eventSource.readyState === EventSource.CLOSED) {
this.isConnected = false;
}
};
} catch (error) {
console.error('创建SSE连接失败:', error);
this.isConnected = false;
}
},
// 关闭SSE连接
closeSSEConnection() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
this.isConnected = false;
this.messages.push(`[系统] 连接已关闭`);
}
}
},
// 组件销毁时关闭连接
beforeDestroy() {
this.closeSSEConnection();
}
}
</script>
<style scoped>
.sse-demo {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
.controls {
margin-bottom: 20px;
}
button {
margin-right: 10px;
padding: 8px 16px;
}
.messages {
border: 1px solid #ddd;
padding: 15px;
height: 300px;
overflow-y: auto;
}
.no-messages {
color: #999;
font-style: italic;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 8px;
border-bottom: 1px solid #eee;
}
</style>
2.后端实现 (Node.js/Express)
const express = require('express');
const app = express();
// SSE端点
app.get('/api/sse-stream', (req, res) => {
// 设置SSE所需的响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 发送初始消息
sendSSEMessage(res, { content: '连接成功,开始接收消息' });
// 每2秒发送一条消息
const intervalId = setInterval(() => {
sendSSEMessage(res, {
content: `服务器时间: ${new Date().toLocaleTimeString()}`
});
}, 2000);
// 客户端断开连接时清理资源
req.on('close', () => {
clearInterval(intervalId);
res.end();
console.log('客户端断开连接');
});
});
// 发送SSE消息的辅助函数
function sendSSEMessage(res, data) {
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
// 启动服务器
const PORT = 3000;
app.listen(PORT, () => {
console.log(`SSE服务器运行在端口 ${PORT}`);
});
2. Web Worker 的优势和基本使用
2.1 什么是Worker?
Web Worker是现代Web应用中的一个强大特性,它通过在后台线程中运行JavaScript代码来提升应用性能和用户体验。
Worker的核心优势
1.1 不阻塞主线程
- 主线程负责UI渲染和用户交互
- Worker在独立线程中运行,不会影响主线程的响应性
- 用户界面保持流畅,不会出现"卡顿"现象
1.2 并行处理能力
- 可以同时运行多个Worker
- 充分利用多核CPU的优势
- 提高复杂计算的处理速度
1.3 内存隔离
- Worker有独立的内存空间
- 不会直接影响主线程的内存使用
- 降低内存相关问题的风险
Web Worker允许在后台线程中运行JavaScript代码,不会阻塞主线程的UI渲染。
2.2下面是一个简单的Web Worker示例。
<template>
<div class="worker-demo">
<h2>Web Worker Demo</h2>
<div class="controls">
<div class="input-group">
<label for="number">输入一个数字:</label>
<input
id="number"
type="number"
v-model.number="inputNumber"
min="1"
max="45"
/>
</div>
<button @click="calculateInMainThread" :disabled="isCalculating">
在主线程计算斐波那契数
</button>
<button @click="calculateInWorker" :disabled="isCalculating">
在Worker中计算斐波那契数
</button>
</div>
<div class="results">
<div class="result-item">
<strong>计算结果:</strong> {{ result }}
</div>
<div class="result-item">
<strong>计算时间:</strong> {{ calculationTime }}ms
</div>
<div class="result-item">
<strong>计算位置:</strong> {{ calculationMethod }}
</div>
</div>
<div class="animation">
<div class="animation-box" :class="{ 'smooth': !isMainThreadBlocked }"></div>
<p>这个动画应该保持流畅。如果卡顿,说明主线程被阻塞了。</p>
</div>
</div>
</template>
<script>
export default {
name: 'WorkerDemo',
data() {
return {
// 输入数字
inputNumber: 35,
// 计算结果
result: null,
// 计算时间(ms)
calculationTime: 0,
// 计算方法
calculationMethod: '-',
// 是否正在计算
isCalculating: false,
// Worker实例
worker: null,
// 主线程是否被阻塞
isMainThreadBlocked: false
}
},
created() {
// 创建Worker
this.initWorker();
},
methods: {
// 初始化Worker
initWorker() {
// 创建Worker
// 注意:Worker文件路径是相对于项目根目录的公共路径
this.worker = new Worker('/fibonacci-worker.js');
// 监听Worker消息
this.worker.onmessage = (event) => {
const { result, time } = event.data;
// 更新结果
this.result = result;
this.calculationTime = time;
this.calculationMethod = 'Web Worker (后台线程)';
this.isCalculating = false;
};
// 监听Worker错误
this.worker.onerror = (error) => {
console.error('Worker错误:', error);
this.calculationMethod = 'Worker错误';
this.isCalculating = false;
};
},
// 在主线程中计算斐波那契数
calculateInMainThread() {
this.isCalculating = true;
this.isMainThreadBlocked = true;
// 使用setTimeout让UI有机会更新
setTimeout(() => {
const startTime = performance.now();
// 计算斐波那契数
this.result = this.fibonacci(this.inputNumber);
const endTime = performance.now();
this.calculationTime = Math.round(endTime - startTime);
this.calculationMethod = '主线程 (UI可能卡顿)';
this.isCalculating = false;
this.isMainThreadBlocked = false;
}, 100);
},
// 在Worker中计算斐波那契数
calculateInWorker() {
if (!this.worker) {
this.initWorker();
}
this.isCalculating = true;
// 发送消息到Worker
this.worker.postMessage({
number: this.inputNumber
});
},
// 递归计算斐波那契数 (故意使用低效算法来演示阻塞)
fibonacci(n) {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
},
// 组件销毁时终止Worker
beforeDestroy() {
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
}
}
</script>
<style scoped>
.worker-demo {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
.controls {
margin-bottom: 20px;
}
.input-group {
margin-bottom: 15px;
}
button {
margin-right: 10px;
margin-bottom: 10px;
padding: 8px 16px;
}
.results {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 20px;
}
.result-item {
margin-bottom: 10px;
}
.animation {
padding: 15px;
border: 1px solid #ddd;
}
.animation-box {
width: 50px;
height: 50px;
background-color: #3498db;
margin-bottom: 15px;
}
.smooth {
animation: move 2s infinite alternate;
}
@keyframes move {
from { transform: translateX(0); }
to { transform: translateX(500px); }
}
</style>
Web Worker文件 (public/fibonacci-worker.js)
// fibonacci-worker.js
// 注意:这个文件需要放在public目录下,以便浏览器可以直接访问
// 监听主线程消息
self.onmessage = function(event) {
const { number } = event.data;
// 记录开始时间
const startTime = performance.now();
// 计算斐波那契数
const result = fibonacci(number);
// 记录结束时间
const endTime = performance.now();
const time = Math.round(endTime - startTime);
// 发送结果回主线程
self.postMessage({
result,
time
});
};
// 递归计算斐波那契数 (故意使用低效算法来演示Worker的优势)
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
3.SSE+Worker实现流式对话demo
安装Worker
npm install vue-worker
安装MarkdownIt
npm install markdown-it --save
<template>
<div class="stream-chat">
<h2>流式对话Demo</h2>
<!-- 消息列表 -->
<div class="messages" ref="messagesContainer">
<div
v-for="(message, index) in messages"
:key="index"
:class="['message', message.role]"
>
<div class="avatar">{{ message.role === 'user' ? '👤' : '🤖' }}</div>
<div class="content">
<!-- 使用v-html显示渲染后的内容 -->
<div v-if="message.rendered" v-html="message.rendered"></div>
<!-- 未渲染时显示原始内容 -->
<div v-else>{{ message.content }}</div>
<!-- 加载指示器 -->
<div v-if="message.isLoading" class="loading-indicator">
<span></span><span></span><span></span>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<input
type="text"
v-model="userInput"
placeholder="输入消息..."
@keyup.enter="sendMessage"
:disabled="isReceiving"
/>
<button @click="sendMessage" :disabled="isReceiving || !userInput.trim()">
发送
</button>
</div>
</div>
</template>
<script>
export default {
name: 'StreamChat',
data() {
return {
// 消息列表
messages: [],
// 用户输入
userInput: '',
// SSE连接
eventSource: null,
// 是否正在接收消息
isReceiving: false,
// Web Worker
worker: null,
// 当前接收的消息
currentMessage: '',
// 当前消息索引
currentMessageIndex: -1
}
},
created() {
// 初始化Web Worker
this.initWorker();
},
methods: {
// 初始化Web Worker
initWorker() {
// 创建Worker实例
this.worker = new Worker('/message-worker.js');
// 监听Worker消息
this.worker.onmessage = (event) => {
const { messageIndex, renderedContent } = event.data;
// 更新消息的渲染内容
if (messageIndex >= 0 && messageIndex < this.messages.length) {
this.$set(this.messages[messageIndex], 'rendered', renderedContent);
}
};
},
// 发送消息
sendMessage() {
const message = this.userInput.trim();
if (!message || this.isReceiving) return;
// 清空输入框
this.userInput = '';
// 添加用户消息
this.messages.push({
role: 'user',
content: message,
rendered: message // 用户消息不需要特殊渲染
});
// 添加AI消息占位
this.messages.push({
role: 'assistant',
content: '',
isLoading: true
});
// 记录当前消息索引
this.currentMessageIndex = this.messages.length - 1;
this.currentMessage = '';
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
// 开始接收流式响应
this.startSSEConnection(message);
},
// 建立SSE连接
startSSEConnection(message) {
// 关闭可能存在的连接
this.closeSSEConnection();
this.isReceiving = true;
try {
// 创建SSE连接
const url = `/api/chat-stream?message=${encodeURIComponent(message)}`;
this.eventSource = new EventSource(url);
// 监听消息事件
this.eventSource.onmessage = (event) => {
this.handleSSEMessage(event);
};
// 监听错误
this.eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
this.handleSSEError('连接错误,请重试');
};
} catch (error) {
console.error('创建SSE连接失败:', error);
this.handleSSEError('无法建立连接');
}
},
// 处理SSE消息
handleSSEMessage(event) {
try {
const data = JSON.parse(event.data);
switch (data.type) {
case 'content':
// 添加内容
this.currentMessage += data.content;
this.updateAssistantMessage(this.currentMessage, true);
break;
case 'end':
// 结束消息
this.updateAssistantMessage(this.currentMessage, false);
// 使用Worker渲染消息
this.renderMessageContent(this.currentMessageIndex, this.currentMessage);
this.isReceiving = false;
this.closeSSEConnection();
break;
default:
console.warn('未知的消息类型:', data.type);
}
} catch (error) {
console.error('处理SSE消息失败:', error);
this.handleSSEError('处理消息失败');
}
},
// 处理SSE错误
handleSSEError(errorMessage) {
this.updateAssistantMessage(errorMessage || '发生错误', false);
this.isReceiving = false;
this.closeSSEConnection();
},
// 更新助手消息
updateAssistantMessage(content, isLoading) {
if (this.currentMessageIndex >= 0) {
this.$set(this.messages, this.currentMessageIndex, {
role: 'assistant',
content: content,
isLoading: isLoading
});
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
}
},
// 使用Worker渲染消息内容
renderMessageContent(messageIndex, content) {
// 发送消息到Worker进行处理
this.worker.postMessage({
messageIndex,
content
});
},
// 关闭SSE连接
closeSSEConnection() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
},
// 滚动到底部
scrollToBottom() {
const container = this.$refs.messagesContainer;
if (container) {
container.scrollTop = container.scrollHeight;
}
}
},
// 组件销毁时清理资源
beforeDestroy() {
this.closeSSEConnection();
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
}
}
</script>
<style scoped>
.stream-chat {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.messages {
height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 20px;
border-radius: 5px;
}
.message {
display: flex;
margin-bottom: 15px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
font-size: 20px;
}
.content {
flex: 1;
padding: 10px;
border-radius: 10px;
max-width: calc(100% - 60px);
}
.user .content {
background-color: #e1f5fe;
}
.assistant .content {
background-color: #f1f1f1;
}
.input-area {
display: flex;
}
input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
margin-right: 10px;
}
button {
padding: 10px 20px;
background-color: #2196f3;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:disabled {
background-color: #b0bec5;
cursor: not-allowed;
}
.loading-indicator {
display: flex;
margin-top: 5px;
}
.loading-indicator span {
width: 8px;
height: 8px;
margin-right: 5px;
background-color: #999;
border-radius: 50%;
display: inline-block;
animation: bounce 1.4s infinite ease-in-out both;
}
.loading-indicator span:nth-child(1) {
animation-delay: -0.32s;
}
.loading-indicator span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1.0); }
}
</style>
2.Web Worker文件 (public/message-worker.js)
/ message-worker.js
// 这个Worker负责处理消息内容,如Markdown渲染
// 导入MarkdownIt插件
import MarkdownIt from 'markdown-it';
// 简单的Markdown渲染函数
function renderMarkdown(text) {
if (!text) return '';
// 这里是一个简化的Markdown渲染实现
// 在实际项目中,你可能会使用成熟的库如marked或markdown-it
// 处理标题
text = text.replace(/^### (.*$)/gm, '<h3>$1</h3>');
text = text.replace(/^## (.*$)/gm, '<h2>$1</h2>');
text = text.replace(/^# (.*$)/gm, '<h1>$1</h1>');
// 处理粗体
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// 处理斜体
text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
// 处理代码块
text = text.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
// 处理行内代码
text = text.replace(/`(.*?)`/g, '<code>$1</code>');
// 处理链接
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// 处理列表
text = text.replace(/^\s*-\s*(.*$)/gm, '<li>$1</li>');
text = text.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
// 处理段落
text = text.replace(/^(?!<[a-z])(.*$)/gm, '<p>$1</p>');
// 处理换行
text = text.replace(/\n/g, '<br>');
return text;
}
// 处理代码高亮
function highlightCode(html) {
// 这里可以实现代码高亮逻辑
// 在实际项目中,你可能会使用highlight.js等库
return html;
}
// 监听主线程消息
self.onmessage = function(event) {
const { messageIndex, content } = event.data;
try {
// 模拟耗时操作
const startTime = performance.now();
// 渲染Markdown
let renderedContent = renderMarkdown(content);
// 代码高亮
renderedContent = highlightCode(renderedContent);
// 模拟复杂处理耗时
if (content.length > 100) {
// 对于长内容,模拟更长的处理时间
const sleepTime = Math.min(content.length / 10, 500);
const endTime = performance.now() + sleepTime;
while (performance.now() < endTime) {
// 空循环模拟耗时操作
}
}
// 发送处理结果回主线程
self.postMessage({
messageIndex,
renderedContent,
processingTime: performance.now() - startTime
});
} catch (error)