1. 跨平台SSE解决方案概述
在当今AIGC(生成式AI)应用爆发的时代背景下,Server-Sent Events(SSE)作为实现文本流式传输的关键技术,已成为各类AI对话、内容生成场景的标配接口方案。然而在uni-app这样的跨平台框架中,由于各运行环境(微信小程序、H5、原生App)对网络请求的实现差异,开发者往往会遇到SSE兼容性问题。
SSE协议本质上是一个基于HTTP的长连接机制,服务器通过持续发送事件流(event stream)实现实时数据推送。与WebSocket不同,SSE是单向通信(服务端→客户端),特别适合AI生成内容这类持续输出的场景。但在uni-app中:
- 微信小程序环境:原生不支持EventSource对象,但提供了独特的HTTP分块传输能力
- H5环境:可以完整使用Fetch API的流式读取能力
- App环境:虽然语法与H5相同,但实际运行时缺少关键API支持
2. 微信小程序实现方案
2.1 核心机制解析
微信小程序通过enableChunked参数开启HTTP分块传输模式,这实际上是TCP层的数据分片机制。当服务器启用Transfer-Encoding: chunked时,响应体会被分成多个数据块传输,小程序端通过onChunkReceived回调逐个接收。
与标准SSE的区别在于:
- 数据格式需要手动处理(没有自动的event/data解析)
- 连接管理需要自行实现重连逻辑
- 消息边界需要额外处理(可能一个chunk包含多个事件或半个事件)
2.2 完整实现代码
javascript复制// #ifdef MP-WEIXIN
function createSSEConnection(url, params, callbacks) {
let buffer = '' // 用于处理不完整消息的缓冲区
const task = uni.request({
url,
method: 'POST',
data: params,
timeout: 30000, // 适当超时设置
header: {
'Accept': 'text/event-stream',
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Connection': 'keep-alive'
},
responseType: 'text',
enableChunked: true,
onChunkReceived: ({ data }) => {
// 处理可能的UTF-8多字节字符截断问题
buffer += typeof data === 'string' ? data : new TextDecoder().decode(data)
// 按SSE协议解析消息(事件以\n\n分隔)
const messages = buffer.split('\n\n')
buffer = messages.pop() || '' // 最后一段可能是不完整消息
messages.forEach(raw => {
const event = { data: '', id: '', event: 'message' }
raw.split('\n').forEach(line => {
if (line.startsWith('data:')) event.data += line.slice(5).trim()
else if (line.startsWith('id:')) event.id = line.slice(3).trim()
else if (line.startsWith('event:')) event.event = line.slice(6).trim()
})
callbacks.onMessage(event)
})
},
success: () => callbacks.onOpen(),
fail: err => callbacks.onError(err)
})
return {
close: () => task.abort(),
reconnect: () => { /* 实现重连逻辑 */ }
}
}
// #endif
2.3 关键注意事项
- 字符编码处理:小程序可能返回ArrayBuffer,需要使用TextDecoder转换
- 消息完整性:单个chunk可能截断UTF-8字符或多行消息,需要缓冲区处理
- 性能优化:避免在onChunkReceived中频繁setData,建议使用防抖
- 错误恢复:网络波动时需实现自动重连机制,建议指数退避算法
实测发现微信开发者工具与真机环境存在差异,真机环境下chunk接收更稳定但延迟略高,建议在onChunkReceived中添加设备类型判断逻辑。
3. H5端标准实现方案
3.1 Fetch API流式处理
现代浏览器通过Fetch API的ReadableStream可以实现真正的SSE兼容处理。与微信方案相比优势在于:
- 自动处理消息边界和事件解析
- 支持标准EventSource的所有功能(重连、事件类型等)
- 更高效的流处理性能
3.2 完整实现代码
javascript复制// #ifdef H5
class SSEClient {
constructor(url) {
this.url = url
this.controller = new AbortController()
this.decoder = new TextDecoder()
this.reader = null
}
connect(params, callbacks) {
fetch(this.url, {
method: 'POST',
headers: {
'Accept': 'text/event-stream',
'Content-Type': 'application/json'
},
body: JSON.stringify(params),
signal: this.controller.signal
}).then(async response => {
if (!response.ok) throw new Error(response.statusText)
callbacks.onOpen()
this.reader = response.body.getReader()
let buffer = ''
const processChunk = ({ done, value }) => {
if (done) {
callbacks.onClose()
return
}
buffer += this.decoder.decode(value, { stream: true })
const messages = buffer.split('\n\n')
buffer = messages.pop() || ''
messages.forEach(raw => {
const event = { data: '', id: '', event: 'message' }
raw.split('\n').forEach(line => {
const [key, val] = line.split(':')
if (key === 'data') event.data += val.trim()
else if (key === 'id') event.id = val.trim()
else if (key === 'event') event.event = val.trim()
})
callbacks.onMessage(event)
})
return this.reader.read().then(processChunk)
}
return this.reader.read().then(processChunk)
}).catch(err => {
callbacks.onError(err)
})
}
close() {
this.controller.abort()
if (this.reader) this.reader.cancel()
}
}
// #endif
3.3 高级功能实现
- 自定义事件类型:通过解析event字段实现不同消息类型的处理
javascript复制// 服务端发送:event: status\ndata: {...}
// 客户端处理:
if (event.event === 'status') {
updateProgress(JSON.parse(event.data))
}
- 断线重连:基于last-event-id实现续传
javascript复制headers: {
'Last-Event-ID': lastId
// 服务端应返回:id: 123\ndata:...
}
- 性能监控:通过TransformStream实现吞吐量统计
javascript复制const transformer = new TransformStream({
transform(chunk, controller) {
stats.bytesReceived += chunk.length
controller.enqueue(chunk)
}
})
response.body.pipeThrough(transformer)
4. App端特殊处理方案
4.1 技术难点解析
App端虽然使用JavaScriptCore引擎,但与浏览器环境的关键差异包括:
- 缺少原生的ReadableStream实现
- fetch返回的body对象没有getReader()方法
- 原生渲染线程与JS线程通信受限
4.2 RenderJS解决方案
通过uni-app的renderjs技术,我们可以将流处理逻辑放到视图层执行:
html复制<!-- 逻辑层 -->
<script>
export default {
methods: {
startSSE(url, params) {
this.sseUrl = url
this.sseParams = params
this.$nextTick(() => {
this.$refs.sseRenderer.dispatchEvent(
new CustomEvent('start', { detail: { url, params } })
)
})
}
}
}
</script>
<!-- 视图层(renderjs) -->
<script module="sse" lang="renderjs">
export default {
methods: {
handleStart(event) {
const { url, params } = event.detail
const decoder = new TextDecoder()
let buffer = ''
fetch(url, {
method: 'POST',
headers: {
'Accept': 'text/event-stream',
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(async res => {
const reader = res.body.getReader()
const processChunk = ({ done, value }) => {
if (done) return
buffer += decoder.decode(value, { stream: true })
const messages = buffer.split('\n\n')
buffer = messages.pop() || ''
messages.forEach(raw => {
this.$ownerInstance.callMethod('onSSEMessage', { raw })
})
return reader.read().then(processChunk)
}
return reader.read().then(processChunk)
})
}
}
}
</script>
4.3 性能优化技巧
- 批处理消息:避免频繁跨线程通信,建议积累3-5条消息后批量传递
- 数据压缩:对大文本内容先进行gzip压缩再传输
- 心跳检测:定期发送ping消息检测连接状态
- 内存管理:及时清理已处理的消息缓存,避免内存泄漏
5. 跨平台统一封装方案
5.1 抽象接口设计
javascript复制class UnifiedSSE {
constructor(options) {
this.platform = process.env.VUE_APP_PLATFORM
this.url = options.url
this.callbacks = options.callbacks
}
connect(params) {
switch(this.platform) {
case 'mp-weixin':
return this._connectMiniProgram(params)
case 'h5':
return this._connectH5(params)
case 'app':
return this._connectApp(params)
}
}
// 各平台具体实现...
}
5.2 心跳与重连机制
javascript复制class UnifiedSSE {
constructor() {
this.retryCount = 0
this.maxRetries = 5
this.heartbeatInterval = 30000
}
_startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (Date.now() - this.lastMessageTime > this.heartbeatInterval * 2) {
this._reconnect()
}
}, this.heartbeatInterval)
}
_reconnect() {
if (this.retryCount++ >= this.maxRetries) {
this.callbacks.onError(new Error('Max reconnection attempts'))
return
}
const delay = Math.min(1000 * Math.pow(2, this.retryCount), 30000)
setTimeout(() => this.connect(this.lastParams), delay)
}
}
5.3 性能对比数据
| 平台 | 首字节延迟 | 消息吞吐量 | 内存占用 |
|---|---|---|---|
| 微信小程序 | 200-300ms | 50-100msg/s | 较低 |
| H5 | 100-150ms | 200-300msg/s | 中等 |
| App | 150-200ms | 100-150msg/s | 较高 |
实测建议:对于高频更新场景(如实时股票行情),H5方案最优;对于长文本生成(如AI写作),微信小程序方案更稳定。
6. 常见问题排查指南
6.1 连接建立失败
症状:无法触发onOpen回调
- 检查点:
- 确认服务端支持CORS(H5端)
- 验证微信小程序域名白名单
- 检查App端的https证书有效性
解决方案:
javascript复制// 在uni-app配置中 manifest.json
{
"mp-weixin": {
"permission": {
"request": {
"domains": ["your-sse-domain.com"]
}
}
}
}
6.2 消息解析异常
症状:收到乱码或截断消息
- 典型原因:
- 微信小程序未正确处理ArrayBuffer
- 多字节字符被拆分到不同chunk
- 消息分隔符(\n\n)处理不当
修复方案:
javascript复制// 增强版消息解析
function parseSSE(raw) {
const event = { data: '', id: '', event: 'message' }
const lines = raw.replace(/\r\n/g, '\n').split('\n')
lines.forEach(line => {
const sepIndex = line.indexOf(':')
if (sepIndex <= 0) return
const field = line.slice(0, sepIndex).trim()
const value = line.slice(sepIndex + 1).trim()
if (field === 'data') event.data += value
else if (field in event) event[field] = value
})
return event
}
6.3 内存泄漏问题
症状:长时间运行后页面卡顿
- 排查方向:
- 未及时释放event listeners
- 消息缓存未清理
- RenderJS未正确销毁
优化代码:
javascript复制// 在组件销毁时
beforeDestroy() {
this.sse?.close()
clearInterval(this.heartbeatTimer)
// App端特殊处理
// #ifdef APP-PLUS
this.$refs.renderer.dispatchEvent(new CustomEvent('destroy'))
// #endif
}
在实际项目中,建议为SSE连接实现状态监控面板,实时显示:
- 连接状态
- 消息速率
- 错误计数
- 内存使用情况
这能帮助快速定位各类性能问题。