Files
qiming/qiming-mobile/docs/SSE-Implementation-Guide.md

1157 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SSE (Server-Sent Events) 流式请求实现指南
## 📋 目录
- [概述](#概述)
- [核心架构](#核心架构)
- [快速开始](#快速开始)
- [实现细节](#实现细节)
- [平台差异处理](#平台差异处理)
- [数据处理器](#数据处理器)
- [现有示例](#现有示例)
- [最佳实践](#最佳实践)
- [常见问题](#常见问题)
- [总结](#总结)
## 概述
本项目使用 **SSE (Server-Sent Events)** 技术实现流式数据传输,支持 H5、微信小程序和 App 多端。SSE 是一种服务器推送技术,允许服务器向客户端推送实时数据流,非常适合聊天、实时通知等场景。
### 核心优势
-**多端支持**:统一接口,自动适配 H5、微信小程序、App
-**流式传输**:实时接收数据,无需轮询
-**自动重连**H5 端支持自动重连机制
-**错误处理**:完善的错误处理和超时监控
-**UTF-8 支持**:正确处理多字节字符,避免截断问题
-**类型安全**:完整的 TypeScript 类型定义
### 技术栈
- **H5 端**`@microsoft/fetch-event-source`
- **微信小程序**`uni.request` + `enableChunked: true`
- **App 端**`uni.request` 标准请求
- **数据处理**:自定义 SSE 数据处理器
- **类型系统**UTS (UniApp TypeScript)
## 核心架构
### 架构图
```
┌─────────────────────────────────────────┐
│ 业务层 (Business Layer) │
│ - chatService.uts │
│ - AgentDetailService.uts │
└──────────────┬──────────────────────────┘
│ 调用
┌─────────────────────────────────────────┐
│ 流式请求层 (StreamRequest) │
│ - StreamRequest 类 │
│ - 统一接口: request() │
│ - 平台适配: requestH5/Weapp/App │
└──────────────┬──────────────────────────┘
├─────────────────┬─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ H5 实现 │ │ 小程序实现 │ │ App 实现 │
│ createSSE │ │ uni.request │ │ uni.request │
│ Connection │ │ + chunked │ │ + 流处理 │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└─────────────────┴─────────────────┘
┌────────────────────────┐
│ 数据处理器层 │
│ - SSEDataProcessor │
│ - ChatDataAdapter │
└────────────────────────┘
```
### 核心文件结构
```
utils/
├── streamRequest.uts # 流式请求核心类
├── sseDataProcessor.uts # SSE 数据处理器
├── chatDataAdapter.uts # 数据格式适配器
└── utf8.uts # UTF-8 编码处理
servers/
└── useRequest.uts # H5 SSE 连接创建
types/interfaces/
└── chat.uts # 类型定义
```
## 快速开始
### 第一步:导入必要的模块
```typescript
import { streamRequest } from '@/utils/streamRequest'
import { StreamRequestConfig, StreamChunk } from '@/types/interfaces/chat'
```
### 第二步:配置请求参数
```typescript
const config: StreamRequestConfig = {
url: 'https://api.example.com/chat/stream',
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: {
message: '你好',
conversationId: 'conv-123',
stream: true
},
onChunk: (chunk: StreamChunk) => {
// 处理数据块
console.log('收到数据:', chunk.data)
},
onError: (error: any) => {
// 处理错误
console.error('请求错误:', error)
},
onComplete: () => {
// 请求完成
console.log('请求完成')
},
onOpen: (response: any) => {
// 连接建立
console.log('连接已建立:', response)
},
onTimeout: () => {
// 超时处理(仅小程序)
console.log('请求超时')
}
}
```
### 第三步:发起请求
```typescript
try {
await streamRequest.request(config)
} catch (error) {
console.error('请求失败:', error)
}
```
### 第四步:中止请求(可选)
```typescript
// 在需要时中止请求
streamRequest.abort()
```
## 实现细节
### H5 端实现
H5 端使用 `@microsoft/fetch-event-source` 库实现 SSE 连接。
#### 核心代码
**文件位置**`servers/useRequest.uts`
```typescript
import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source'
export async function createSSEConnection<T = any>(options: SSEOptions<T>) {
const controller = options.abortController || new AbortController()
// H5 需要添加 fragment 参数(用于路由识别)
const fragment = (window.location.hash || "#/").replace("#/", "/")
try {
await fetchEventSource(options.url, {
method: options.method || "POST",
headers: {
"Content-Type": "application/json",
...options.headers,
...(fragment && { fragment }), // 添加 fragment
},
body: typeof options.body === "object"
? JSON.stringify(options.body)
: options.body,
signal: controller.signal,
openWhenHidden: true, // 页面不可见时保持连接
onopen: async (response) => {
if (response.status >= 400) {
throw new Error(`SSE连接失败: ${response.statusText}`)
}
options.onOpen?.(response)
},
onmessage: (event: EventSourceMessage) => {
try {
const data = event.data ? JSON.parse(event.data) : null
options.onMessage(data)
} catch (error) {
const normalizedError = error instanceof Error
? error
: new Error(String(error))
options.onError?.(normalizedError)
}
},
onclose: () => {
options.onClose?.()
},
onerror: (error) => {
// 如果是中止错误,不抛出异常
if (error.name === "AbortError") {
console.log("SSE连接已被中止")
return
}
options.onError?.(error)
throw error // 停止自动重试
},
})
} catch (error) {
// 静默处理所有 AbortError
if (error.name === "AbortError") {
console.log("SSE连接已被中止")
return
}
// 只有真正的错误才触发错误回调
const normalized = error instanceof Error
? error
: new Error(String(error))
options.onError?.(normalized)
throw normalized
}
}
```
#### 关键特性
1. **Fragment 参数**H5 端自动添加 `fragment` 请求头,用于服务端路由识别
2. **AbortController**:支持请求中止
3. **错误处理**:区分中止错误和真实错误
4. **自动重连**`fetch-event-source` 库内置自动重连机制
#### 在 StreamRequest 中的使用
**文件位置**`utils/streamRequest.uts`
```typescript
private async requestH5(config: StreamRequestConfig): Promise<void> {
try {
// 启动超时监控
this.startTimeoutMonitor()
await createSSEConnection({
url: config.url,
method: config.method,
headers: config.headers as any,
body: config.body,
abortController: this.abortController,
onMessage: (data: any) => {
if (this.isAborted) return
this.updateLastChunkTime() // 更新接收时间
try {
// 检查是否完成
if (data && data.completed) {
this.clearTimeoutMonitor()
config.onComplete?.()
return
}
// 处理数据块
const chunk: StreamChunk = {
type: "content",
data: data ? JSON.stringify(data) : "",
}
config.onChunk?.(chunk)
} catch (e) {
console.error("Parse chunk error:", e)
}
},
onError: (error: Error) => {
this.clearTimeoutMonitor()
if (error.name === "AbortError" || this.isAborted) {
return
}
config.onError?.(error)
},
onOpen: (response: any) => {
console.log("SSE连接已建立:", response)
},
onClose: () => {
console.log("SSE连接已关闭")
},
})
} catch (error) {
// 错误处理...
}
}
```
### 微信小程序端实现
微信小程序使用 `uni.request``enableChunked` 选项实现流式传输。
#### 核心代码
**文件位置**`utils/streamRequest.uts`
```typescript
private async requestWeapp(config: StreamRequestConfig): Promise<void> {
return new Promise((resolve, reject) => {
this.requestTask = uni.request({
url: config.url,
method: config.method,
header: config.headers as any,
data: config.body,
enableChunked: true, // 启用分块传输
responseType: "arraybuffer", // 响应类型为 ArrayBuffer
timeout: 1000 * 60 * 60 * 12, // 设置超时时间为 12 小时
success: (res) => {
if (res.statusCode === 200) {
config.onOpen?.(res)
resolve() // 请求成功建立连接后即可 resolve
} else {
this.clearTimeoutMonitor()
reject(new Error(`HTTP error! status: ${res.statusCode}`))
}
},
fail: (err) => {
this.clearTimeoutMonitor()
if (!this.isAborted) {
// 忽略超时错误(流式请求的正常现象)
if (err.errMsg && (err.errMsg.includes("time out") || err.errMsg.includes("timeout"))) {
console.log("请求超时警告(流式请求正常现象):", err)
return
}
config.onError?.(err)
reject(err)
}
},
})
// 启动超时监控
this.startTimeoutMonitor()
// 监听分块数据接收
this.requestTask?.onChunkReceived?.((res) => {
if (this.isAborted) return
this.updateLastChunkTime()
try {
// 使用 decodeUTF8 处理分块数据,解决字符截断问题
const str = this.decodeUTF8(res.data as ArrayBuffer)
// 将新数据添加到缓冲区
this.buffer += str
// SSE 协议:完整的消息以 \n\n 结尾
const messages = this.buffer.split("\n\n")
this.buffer = messages.pop() || "" // 保存最后一个可能不完整的消息块
// 处理所有完整的消息块
for (const message of messages) {
if (!message.trim()) continue
const lines = message.split("\n")
let dataContent = ""
// 提取所有 data: 开头的行
for (const line of lines) {
if (line.startsWith("data:")) {
const content = line.slice(5).trim()
if (dataContent.length > 0) {
dataContent += "\n" + content
} else {
dataContent = content
}
}
}
// 处理完整的 SSE 消息
if (dataContent.length > 0) {
this.processSSEMessage(dataContent, config, resolve)
}
}
} catch (error) {
console.error("处理分块数据失败:", error)
if (!this.isAborted) {
config.onError?.(error)
}
}
})
})
}
```
#### 关键特性
1. **enableChunked**:启用分块传输,支持流式数据
2. **responseType: "arraybuffer"**:接收二进制数据,需要手动解码
3. **UTF-8 解码**:使用 `decodeUTF8` 方法处理多字节字符,避免截断
4. **消息缓冲**:使用 `buffer` 缓存不完整的消息(以 `\n\n` 为完整标记)
5. **超时监控**60 秒内无数据流动时自动断开
#### UTF-8 解码处理
小程序端接收的是 `ArrayBuffer`,需要正确处理 UTF-8 编码,避免字符截断:
```typescript
private decodeUTF8(chunk: ArrayBuffer): string {
const newBytes = new Uint8Array(chunk)
let combined = newBytes
// 合并之前未完成的字节
if (this.pendingBytes.length > 0) {
combined = new Uint8Array(this.pendingBytes.length + newBytes.length)
combined.set(this.pendingBytes)
combined.set(newBytes, this.pendingBytes.length)
this.pendingBytes = new Uint8Array(0)
}
if (combined.length === 0) return ""
// 寻找最后一个安全的字节边界
let safeEnd = combined.length
let i = combined.length - 1
let backtrack = 0
// 向前回溯,检查末尾是否被截断
// UTF-8 最大字符长度为 4 字节
while (backtrack < 4 && i >= 0) {
const b = combined[i]
// ASCII (0xxxxxxx) - 字符结束
if ((b & 0x80) === 0) {
safeEnd = combined.length - backtrack
break
}
// Start byte (11xxxxxx)
if ((b & 0xc0) === 0xc0) {
let need = 2
if ((b & 0xe0) === 0xe0) need = 3
else if ((b & 0xf0) === 0xf0) need = 4
const have = combined.length - i
if (have < need) {
// 不完整,需要保留从 i 开始的所有字节
safeEnd = i
} else {
safeEnd = combined.length
}
break
}
i--
backtrack++
}
// 如果 safeEnd 小于总长度,保存剩余字节
if (safeEnd < combined.length) {
this.pendingBytes = combined.slice(safeEnd)
return utf8ArrayToString(combined.slice(0, safeEnd))
}
return utf8ArrayToString(combined)
}
```
### App 端实现
App 端使用标准的 `uni.request`,在响应完成后统一处理流式数据。
#### 核心代码
```typescript
private async requestApp(config: StreamRequestConfig): Promise<void> {
return new Promise((resolve, reject) => {
this.requestTask = uni.request({
url: config.url,
method: config.method,
header: config.headers as any,
data: config.body,
success: (res) => {
if (res.statusCode === 200) {
// 处理流式响应
this.handleStreamResponse(res.data as string, config)
resolve()
} else {
reject(new Error(`HTTP error! status: ${res.statusCode}`))
}
},
fail: (err) => {
if (!this.isAborted) {
reject(err)
}
},
})
})
}
private handleStreamResponse(data: string, config: StreamRequestConfig): void {
const lines = data.split("\n")
for (const line of lines) {
if (line.trim() === "") continue
if (line.startsWith("data:")) {
const content = line.slice(5).trim()
if (!content) continue
// 检查是否完成
if (content === "[DONE]") {
config.onComplete?.()
return
}
// 检查是否看起来像 JSON
const looksLikeJSON = content.startsWith("{") || content.startsWith("[")
if (looksLikeJSON) {
try {
const parseData = JSON.parse(content)
if (parseData && parseData.completed) {
config.onComplete?.()
return
}
} catch (e) {
// JSON 解析失败,继续处理为普通数据
}
}
// 处理数据块
const chunk: StreamChunk = {
type: "content",
data: content,
}
config.onChunk?.(chunk)
}
}
}
```
## 平台差异处理
### 条件编译
项目使用 uni-app 的条件编译功能,根据平台自动选择实现:
```typescript
// #ifdef H5 || WEB
await this.requestH5(config)
// #endif
// #ifdef MP-WEIXIN
await this.requestWeapp(config)
// #endif
// #ifdef APP-PLUS
await this.requestApp(config)
// #endif
```
### 平台特性对比
| 特性 | H5 | 微信小程序 | App |
|------|----|-----------|-----|
| 实现方式 | fetch-event-source | uni.request + enableChunked | uni.request |
| 数据格式 | JSON 对象 | ArrayBuffer (需解码) | 字符串 |
| 自动重连 | ✅ 支持 | ❌ 不支持 | ❌ 不支持 |
| 超时监控 | ✅ 60秒 | ✅ 60秒 | ❌ 不支持 |
| Fragment 参数 | ✅ 需要 | ❌ 不需要 | ❌ 不需要 |
| UTF-8 处理 | ✅ 自动 | ✅ 手动解码 | ✅ 自动 |
## 数据处理器
### SSEDataProcessor
专门用于处理 SSE 流式数据的处理器,支持消息累积和去重。
**文件位置**`utils/sseDataProcessor.uts`
#### 核心功能
```typescript
export class SSEDataProcessor {
private sseChunks: SSEChunk[] = []
private chatId: string = ''
private messageMap = new Map<string, MsgItem>()
/**
* 处理单个SSE数据块
*/
processChunk(chunk: SSEChunk): MsgItem[] {
this.sseChunks.push(chunk)
this.processMessage(chunk)
return this.getAllMessages()
}
/**
* 智能判断是否应该追加文本(去重逻辑)
*/
private shouldAppendText(existingBody: string, newText: string): boolean {
// 如果新文本为空,不追加
if (!newText || newText.trim().length === 0) {
return false
}
// 如果现有内容已经包含新文本,不追加
if (existingBody.includes(newText)) {
return false
}
// 避免重复的标点符号和空格
const lastChar = existingBody.charAt(existingBody.length - 1)
const firstChar = newText.charAt(0)
if (lastChar === firstChar && '.,!?;:'.includes(lastChar)) {
return false
}
if (lastChar === ' ' && firstChar === ' ') {
return false
}
return true
}
}
```
#### 使用示例
```typescript
import { createSSEProcessor } from '@/utils/sseDataProcessor'
const processor = createSSEProcessor('chat-123')
// 处理每个数据块
chunks.forEach(chunk => {
const messages = processor.processChunk(chunk)
// 更新 UI
messageList.value = messages
})
// 获取所有消息
const allMessages = processor.getAllMessages()
// 清除数据
processor.clear()
```
### ChatDataAdapter
数据格式适配器,用于在不同数据格式之间转换。
**文件位置**`utils/chatDataAdapter.uts`
#### 核心功能
```typescript
export class ChatDataAdapter {
/**
* 将SSE数据块转换为MsgItem
*/
static convertSSEChunkToMsgItem(chunk: SSEChunk, chatId: string): MsgItem {
return {
_id: chunk.data.id || this.generateId(),
from_uid: this.getFromUidFromRole(chunk.data.role),
body: chunk.data.text || '',
create_time: this.parseTime(chunk.data.time) || Date.now(),
state: chunk.data.finished ? 3 : 2,
chat_id: chatId,
markdownElList: [],
rendered: false
}
}
/**
* 处理SSE流式数据累积文本内容
*/
static processSSEChunks(chunks: SSEChunk[], chatId: string): MsgItem[] {
const messageMap = new Map<string, MsgItem>()
chunks.forEach(chunk => {
const messageId = chunk.data.id
if (!messageMap.has(messageId)) {
const msgItem = this.convertSSEChunkToMsgItem(chunk, chatId)
messageMap.set(messageId, msgItem)
} else {
const existingMsg = messageMap.get(messageId)!
existingMsg.body += chunk.data.text || ''
if (chunk.data.finished) {
existingMsg.state = 3
}
}
})
return Array.from(messageMap.values())
}
}
```
## 现有示例
### 示例 1聊天服务中的使用
**文件位置**`utils/chatService.uts`
```typescript
import { streamRequest } from '@/utils/streamRequest'
import { StreamRequestConfig, StreamChunk } from '@/types/interfaces/chat'
export class ChatService {
private currentRequest: StreamRequest | null = null
private isStreaming: boolean = false
async sendStreamMessage(
data: any,
onChunk: (data: string) => void,
onError: (error: any) => void,
onComplete: () => void,
onTimeout?: () => void,
isTempChat: boolean = false
) {
this.isStreaming = true
try {
const apiUrl = isTempChat ? CHAT_API.TEMP_STREAM : CHAT_API.STREAM
const token = uni.getStorageSync(ACCESS_TOKEN)
this.currentRequest = new StreamRequest()
const header = {
...API_HEADERS,
}
// #ifdef H5 || WEB
if(process.env.NODE_ENV === 'development'){
header['Authorization'] = `Bearer ${token}`
}
// #endif
// #ifdef MP-WEIXIN
header['Authorization'] = `Bearer ${token}`
// #endif
await this.currentRequest.request({
url: apiUrl,
method: 'POST',
headers: header,
body: {
...data,
stream: true
},
onChunk: (chunk: StreamChunk) => {
if (chunk.type === 'content') {
onChunk(chunk.data)
}
},
onError: (error: any) => {
this.isStreaming = false
onError(error)
},
onComplete: () => {
this.isStreaming = false
onComplete()
},
onTimeout: () => {
this.isStreaming = false
if (onTimeout) {
onTimeout()
}
}
})
} catch (error) {
this.isStreaming = false
onError(error)
}
}
abort(): void {
if (this.currentRequest) {
this.currentRequest.abort()
this.currentRequest = null
}
this.isStreaming = false
}
}
```
### 示例 2在页面中使用
```typescript
<script setup lang="uts">
import { ref } from 'vue'
import { streamRequest } from '@/utils/streamRequest'
import { StreamRequestConfig } from '@/types/interfaces/chat'
import { SSEDataProcessor } from '@/utils/sseDataProcessor'
const messageList = ref<MsgItem[]>([])
const isStreaming = ref(false)
const processor = ref<SSEDataProcessor | null>(null)
// 发送消息
async function sendMessage(content: string) {
isStreaming.value = true
processor.value = createSSEProcessor('chat-123')
const token = uni.getStorageSync(ACCESS_TOKEN)
try {
await streamRequest.request({
url: 'https://api.example.com/chat/stream',
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: {
message: content,
conversationId: 'conv-123',
stream: true
},
onChunk: (chunk) => {
try {
// 解析 SSE 数据
const sseData = JSON.parse(chunk.data)
// 处理数据块
const messages = processor.value?.processChunk(sseData) || []
messageList.value = messages
} catch (error) {
console.error('解析数据失败:', error)
}
},
onError: (error) => {
console.error('请求错误:', error)
uni.showToast({
title: '请求失败',
icon: 'none'
})
isStreaming.value = false
},
onComplete: () => {
console.log('请求完成')
isStreaming.value = false
},
onTimeout: () => {
console.log('请求超时')
uni.showToast({
title: '请求超时',
icon: 'none'
})
isStreaming.value = false
}
})
} catch (error) {
console.error('发送消息失败:', error)
isStreaming.value = false
}
}
// 中止请求
function abortRequest() {
streamRequest.abort()
isStreaming.value = false
}
</script>
```
## 最佳实践
### 1. 错误处理
**推荐方式**:区分不同类型的错误
```typescript
onError: (error: any) => {
// 网络错误
if (error.message?.includes('network')) {
uni.showToast({
title: '网络错误,请检查网络连接',
icon: 'none'
})
return
}
// 服务器错误
if (error.status >= 500) {
uni.showToast({
title: '服务器错误,请稍后重试',
icon: 'none'
})
return
}
// 其他错误
console.error('请求错误:', error)
uni.showToast({
title: error.message || '请求失败',
icon: 'none'
})
}
```
### 2. 超时处理
**推荐方式**:小程序端设置合理的超时回调
```typescript
onTimeout: () => {
// 显示提示
uni.showToast({
title: '请求超时,已自动断开',
icon: 'none',
duration: 2000
})
// 清理状态
isStreaming.value = false
// 可选:重试机制
// retryRequest()
}
```
### 3. 数据累积
**推荐方式**:使用 SSEDataProcessor 处理数据累积
```typescript
const processor = createSSEProcessor(chatId)
onChunk: (chunk) => {
try {
const sseData = JSON.parse(chunk.data)
const messages = processor.processChunk(sseData)
messageList.value = messages
} catch (error) {
console.error('处理数据失败:', error)
}
}
```
### 4. 请求中止
**推荐方式**:在组件卸载时清理请求
```typescript
import { onUnmounted } from 'vue'
onUnmounted(() => {
// 组件卸载时中止请求
streamRequest.abort()
})
```
### 5. 内存管理
**推荐方式**:及时清理处理器数据
```typescript
onComplete: () => {
// 请求完成后清理
processor.value?.clear()
processor.value = null
isStreaming.value = false
}
```
### 6. 状态管理
**推荐方式**:使用响应式状态管理请求状态
```typescript
const isStreaming = ref(false)
const errorMessage = ref<string | null>(null)
// 在请求开始时重置状态
async function startRequest() {
isStreaming.value = true
errorMessage.value = null
try {
await streamRequest.request(config)
} catch (error) {
errorMessage.value = error.message
} finally {
isStreaming.value = false
}
}
```
## 常见问题
### Q1: H5 端连接失败怎么办?
**A**: 检查以下几点:
1. **Fragment 参数**:确认服务端支持 `fragment` 请求头
2. **CORS 配置**:确认服务端已配置正确的 CORS 头
3. **Content-Type**:确认请求头包含 `Content-Type: application/json`
4. **网络连接**:检查网络连接是否正常
```typescript
// 检查连接状态
onOpen: (response) => {
console.log('连接状态:', response.status)
if (response.status >= 400) {
console.error('连接失败:', response.statusText)
}
}
```
### Q2: 小程序端数据乱码怎么办?
**A**: 确保正确使用 UTF-8 解码:
```typescript
// StreamRequest 已自动处理 UTF-8 解码
// 如果仍有问题,检查服务端编码
```
### Q3: 如何实现自动重连?
**A**: H5 端 `fetch-event-source` 已内置自动重连。小程序和 App 端需要手动实现:
```typescript
let retryCount = 0
const MAX_RETRIES = 3
async function requestWithRetry(config: StreamRequestConfig) {
try {
await streamRequest.request(config)
} catch (error) {
if (retryCount < MAX_RETRIES) {
retryCount++
console.log(`重试第 ${retryCount} 次...`)
setTimeout(() => {
requestWithRetry(config)
}, 1000 * retryCount) // 指数退避
} else {
console.error('重试次数已达上限')
}
}
}
```
### Q4: 如何处理大量数据?
**A**: 使用数据处理器进行分批处理:
```typescript
const processor = createSSEProcessor(chatId)
let batchSize = 0
onChunk: (chunk) => {
const sseData = JSON.parse(chunk.data)
processor.processChunk(sseData)
batchSize++
// 每 10 个数据块更新一次 UI
if (batchSize >= 10) {
messageList.value = processor.getAllMessages()
batchSize = 0
}
}
onComplete: () => {
// 最后更新一次
messageList.value = processor.getAllMessages()
}
```
### Q5: 如何调试 SSE 连接?
**A**: 使用以下方法:
```typescript
// 1. 启用详细日志
onOpen: (response) => {
console.log('[SSE] 连接已建立:', response)
}
onChunk: (chunk) => {
console.log('[SSE] 收到数据:', chunk)
}
onError: (error) => {
console.error('[SSE] 错误:', error)
}
onComplete: () => {
console.log('[SSE] 请求完成')
}
// 2. 检查网络请求(浏览器开发者工具)
// 3. 使用 uni.request 的调试功能
```
### Q6: 小程序端超时时间如何设置?
**A**: 在 `requestWeapp` 方法中设置:
```typescript
this.requestTask = uni.request({
// ...
timeout: 1000 * 60 * 60 * 12, // 12 小时
// ...
})
```
注意超时监控是独立的60 秒内无数据流动会触发 `onTimeout` 回调。
### Q7: 如何避免重复数据?
**A**: 使用 SSEDataProcessor 的智能去重功能:
```typescript
const processor = createSSEProcessor(chatId)
// processor 内部已实现去重逻辑
// 会自动检查并跳过重复内容
```
## 总结
通过本指南,你可以:
1.**理解 SSE 实现原理**:了解 H5、小程序、App 三端的实现差异
2.**快速上手**:学会如何使用 StreamRequest 发起流式请求
3.**深入实现**:理解数据处理器和适配器的工作原理
4.**遵循最佳实践**:编写高质量的流式请求代码
5.**解决常见问题**:快速定位和修复错误
### 核心要点
- **统一接口**`StreamRequest` 提供统一的接口,自动适配不同平台
- **错误处理**:完善的错误处理和超时监控机制
- **数据累积**:使用 `SSEDataProcessor` 处理流式数据累积
- **UTF-8 支持**:小程序端正确处理多字节字符
- **类型安全**:完整的 TypeScript 类型定义
### 参考资源
- [uni-app 官方文档](https://uniapp.dcloud.net.cn/)
- [Server-Sent Events 规范](https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events)
- [fetch-event-source 文档](https://github.com/Azure/fetch-event-source)
---
**维护者**:开发团队
如有问题或建议,请联系开发团队或提交 Issue。