微信小程序虽然不支持sse,但可通过分块传输技术模拟实现单向实时通讯功能。
技术实现现状与替代方案
1. 原生支持情况
微信小程序框架未内置SSE协议的EventSource API,官方文档中未包含相关接口说明。
2. 替代实现方案
-
WebSocket方案 。
使用WebSocket作为官方支持的双向通信协议,适用于需要高实时性的场景(如聊天室、即时通知),API文档见:wx.connectSocket
-
分块传输模拟SSE 。
通过
wx.request
的enableChunked
参数启用分块传输,结合onChunkReceived
事件监听数据流。需手动处理以下环节:-
二进制数据解析(如ArrayBuffer转字符串)。
-
数据包完整性校验。
-
事件流格式解析(按
data:
字段分割)。
-
具体实现步骤我这里使用的是uniapp,如果微信小程序原生的话只需要替换对应的api即可
javascript
// src/hooks/useStreamingRequest.js
import {
ref
} from 'vue'
import {
BASE_URL
} from '@/config/index'
import {
getStorageSync
} from '@/utils/storage'
export function useStreamingRequest() {
const isLoading = ref(false)
let requestTask = null
/**
* 发起流式请求
* @param {string} url - 基础 URL
* @param {Object} data - 请求参数
* @param {Function} callback - 接收每一块数据的回调函数
*/
function sendStreamMessage(url, data, callback) {
isLoading.value = true
requestTask = uni.request({
url: BASE_URL + url,
method: 'POST',
data,
responseType: 'arraybuffer',
enableChunked: true,
header: {
'content-type': 'application/json',
'Authorization': getStorageSync('token')
},
success: (res) => {
console.log('请求成功:', res)
},
fail: (error) => {
console.error('请求失败:', error)
},
complete: () => {
isLoading.value = false
}
})
if (requestTask && typeof requestTask.onChunkReceived === 'function') {
requestTask.onChunkReceived(res => {
try {
const textData = extractText(res.data)
const parsedDataArray = extractAndParse(textData)
if (parsedDataArray.length > 0 && callback) {
parsedDataArray.forEach(item => callback(item))
}
} catch (e) {
console.warn('分块数据处理出错', e)
}
})
} else {
console.warn('当前环境不支持 onChunkReceived')
}
}
/**
* 中断请求
*/
function abortRequest() {
if (requestTask) {
requestTask.abort()
requestTask = null
}
}
return {
sendStreamMessage,
abortRequest,
isLoading
}
}
// -------------------------
// 工具函数提取复用
// -------------------------
/* const decoder = new TextDecoder('utf-8') */
/**
* 将 ArrayBuffer 解码为 UTF-8 字符串
*/
function extractText(data) {
try {
if (typeof TextDecoder !== 'undefined') {
return decoder.decode(new Uint8Array(data));
} else {
return decodeURIComponent(escape(String.fromCharCode.apply(null, new Uint8Array(data))));
}
} catch (error) {
return decodeURIComponent(escape(String.fromCharCode.apply(null, new Uint8Array(data))));
}
}
/**
* 提取并解析 JSON 数据
*/
function extractAndParse(text) {
const jsonRegex = /\{[^{}]*\}/gs
const jsonMatches = text.match(jsonRegex) || []
const results = []
for (const match of jsonMatches) {
try {
results.push(JSON.parse(match))
} catch (e) {
console.error('JSON 解析失败:', match)
}
}
return results
}
我这里将支持流式输出的方法封装为了一个工具函数方便在组件中调用
这段代码实现了一个用于处理流式请求的 Vue 组合式函数 useStreamingRequest
,它允许逐步接收和处理服务器返回的数据块。下面是关键部分的解释:
-
核心功能
-
使用
ref
创建响应式状态isLoading
来跟踪请求状态 -
通过
uni.request
发起支持分块传输的 HTTP 请求 -
利用
onChunkReceived
监听并处理每个数据块
-
-
主要函数
-
sendStreamMessage
: 发起流式请求,接收 URL、参数和回调函数 -
abortRequest
: 中断当前正在进行的请求 -
返回包含上述函数和加载状态的对象
-
-
数据处理流程
-
使用
extractText
将二进制数据解码为字符串 -
通过
extractAndParse
提取并解析其中的 JSON 数据 -
支持兼容不同环境的文本解码方式
-
-
注意事项
-
需要确保全局变量
decoder
被正确初始化(当前被注释掉了) -
回调函数会依次处理每个解析出的 JSON 对象
-
包含错误处理机制以避免单个数据块解析失败影响整体流程
-
uni.request中的这个参数
responseType: 'arraybuffer' 表示将响应数据作为二进制数据处理接收到的数据是原始的二进制格式(ArrayBuffer)适用于处理非文本数据,如文件下载、流式数据等需要手动解码为可读文本 -
uni.request中的这个参数
enableChunked: true,true
表示启用分块接收功能允许逐步接收服务器返回的数据块不需要等待整个响应完成就可以处理部分数据
-
组件中调用
javascript
import {
useStreamingRequest
} from '@/utils/useStreamingRequest'
const {
sendStreamMessage
} = useStreamingRequest()
const requestParameters = ref({})
const aiContent = ref('') //内容
const displayContent = ref('') // 展示内容,不含第一个字符
const status = ref('') //ai生成状态
const aiMessage = ref('') //ai生成内容
const shopId = ref('') //商户id
const taskId = ref('')
const dataShows = ref(false) //ai生成内容显示隐藏
const complete = ref(false) //ai生成内容完成
/* 发送ai */
const startStream = () => {
const url = '/miniProgram/sendOnSitePhoto'
const data = {
photoUrl: requestParameters.value.photoUrl,
onSiteTaskPositionDetailId: requestParameters.value.onSiteTaskPositionDetailId,
inspectionName: requestParameters.value.checkItemsName
}
/* const data = {
photoUrl: "https://gjian-bucket.oss-cn-beijing.aliyuncs.com/zjj/min/title.png",
onSiteTaskPositionDetailId: '1926265598757675010'
} */
sendStreamMessage(url, data, handleAIResult)
}
//接收流式数据回调函数
const handleAIResult = async (event) => {
if (event.event === 'message_end') {
console.log("生成完成")
aiMessage.value = aiContent.value
console.log(aiMessage.value)
if (event.answer) {
aiContent.value += event.answer
}
if (status.value == 1) { //ai解析结果为异常时
complete.value = true
}
} else {
if (event.answer) {
dataShows.value = true
aiContent.value += event.answer;
}
}
status.value = aiContent.value[0]
// 设置展示内容(去除第一个字符)
displayContent.value = aiContent.value.slice(1)
if (status.value == 2) {
const confirm = await showModal('拍照数据有误,请从新上传!')
if (confirm) {
useRouter("/subpkg/take_pictures/take_pictures")
} else {
console.log('用户点击了取消')
}
}
}
内容展示
html
<view class="card-content" v-if="dataShows">
<rich-text :nodes="displayContent.replace(/\n/g, '<br />').replace(/[#*]/g, '')"
style="font-weight: 400;font-size: 28rpx;color: #000000;line-height: 40rpx;"></rich-text>
</view>