uni-app支持SSE
因为uni.request没有办法支持SSE,为此尝试了各种方案,着了大急。
现将尝试的各种方案进行一个整理,若有疏漏请大家帮忙补充。
H5
因为Web端运行在浏览器内核上,SSE的支持是比较完备的,可以使用axios、@microsoft/fetch-event-source 等实现,各种案例也比较完善因此不再赘述。
微信小程序
微信小程序的SSE方案参考的是《微信小程序除了WebSocket其他思路实现流传输文字(打字机)效果》
因为我们是在uniapp中实现,所以在原文方案的基础上使用的uni相关的API来实现,考虑到要实现停止和兼容H5的的接口,最后引入了abort-controller 和 @uni-helper/uni-network来进行封装。
import type { UnCancelTokenListener, UnGenericAbortSignal, UnHeaders } from '@uni-helper/uni-network'
/**
* 二进制解析成文本
* @param data 二进制数据
* @returns 文本
*/
export function decodeArrayBuffer(data: ArrayBuffer | undefined) {
if (!data) {
return ''
}
return decodeUsingURIComponent(data)
}
/**
* URIComponent解码二进制流(不用引入额外包)
* @param data 二进制流
* @returns 文本
*/
function decodeUsingURIComponent(data: ArrayBuffer) {
const uint8Array = new Uint8Array(data)
let text = String.fromCharCode(...uint8Array)
try {
text = decodeURIComponent(escape(text))
} catch (e) {
console.error('decodeUsingURIComponent: Can not decodeURI ', text)
}
return text
}
type onStreamReceivedListener = (text: string) => void
export function fetchStreamChat(
params: { prompt: string; uuid: string },
signal?: UnGenericAbortSignal,
listener?: onStreamReceivedListener
) {
const onHeadersReceived = (response?: { headers?: UnHeaders }) => {
console.log('fetchStreamChat.onHeadersReceived: ', response?.headers)
}
const onChunkReceived = (response?: { data?: ArrayBuffer }) => {
const text = decodeArrayBuffer(response?.data)
listener?.(text)
}
return post<string>({
url: '/openai/completions/stream',
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
token: 'your-token'
},
data: {
content: params.prompt,
scene: params.uuid,
source: 'gpt4',
},
responseType: 'arraybuffer',
enableChunked: true,
onHeadersReceived,
onChunkReceived,
signal: signal
})
}
特殊注意以上代码使用时对abort-controller的引入方式
import AbortController from 'abort-controller/dist/abort-controller'
let controller = new AbortController()
const onResponseListener = async (responseText: string) => {
console.log('==response==\n', responseText)
}
await fetchStreamChat({ prompt, uuid }, controller.signal, onResponseListener)
后端NGINX配置
# 注意这里只配置代理发送接口,不然其他接口也会受影响
location /openai/completions/stream {
# ...more config
proxy_set_header Transfer-Encoding "";
chunked_transfer_encoding on;
proxy_buffering off;
}
APP
App目前看到的方案最多,但是目前为止没有找到很合适的方案。有更好的方案请各位大佬补充Thanks♪(・ω・)ノ
1. plus.net.XMLHttpRequest
参考方案 《XMLHttpRequest模块管理网络请求》,具体代码如下
import type { UnCancelTokenListener, UnGenericAbortSignal, UnHeaders } from '@uni-helper/uni-network'
type onStreamReceivedListener = (text: string) => void
export class CanceledError extends Error {
constructor(message?: string) {
super(message ?? 'canceled')
}
}
export function fetchStreamChatForApp(
params: { prompt: string; uuid: string },
signal?: UnGenericAbortSignal,
listener?: onStreamReceivedListener
) {
return new Promise((resolve, reject) => {
// 梳理好请求数据
const token = 'your-token'
const data = JSON.stringify({
content: params.prompt,
scene: params.uuid,
source: 'gpt3.5',
})
// 处理资源释放
let onCanceled: UnCancelTokenListener
const done = () => {
signal?.removeEventListener?.('abort', onCanceled)
}
// 封装请求
// @ts-ignore
let xhr: plus.net.XMLHttpRequest | undefined
// @ts-ignore
xhr = new plus.net.XMLHttpRequest()
xhr.withCredentials = true
// 配置终止逻辑
if (signal) {
signal.addEventListener?.('abort', () => {
console.log('fetchStreamChatForApp signal abort')
xhr.abort()
})
}
let nLastIndex = 0
xhr.onreadystatechange = function () {
console.log(`onreadystatechange(${xhr.readyState}) → `)
if (xhr.readyState === 4) {
if (nLastIndex < xhr.responseText.length) {
const responseText = xhr.responseText as string
// 处理 HTTP 数据块
if (responseText) {
const textLen = responseText.length
const chunk = responseText.substring(nLastIndex)
nLastIndex = textLen
listener?.(chunk)
}
}
if (xhr.status === 200) {
resolve({ code: ResultCode.SUCCESS, msg: 'end' })
done()
} else {
reject(new Error(xhr.statusText))
done()
}
}
}
xhr.onprogress = function (event: any) {
const responseText = xhr.responseText
if (responseText) {
const textLen = responseText.length
const chunk = responseText.substring(nLastIndex)
nLastIndex = textLen
listener?.(chunk)
console.log('onprogress ', chunk)
}
}
xhr.onerror = function (error: any) {
console.error('Network Error:', error)
reject(error)
done()
}
// 配置请求
xhr.open('POST', 'https://your-site/api/openai/completions/stream')
xhr.setRequestHeader('Accept', 'text/event-stream')
xhr.setRequestHeader('token', token)
xhr.setRequestHeader('User-Agent', 'Mobile')
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Host', 'mapi.lawvector.cn')
xhr.setRequestHeader('Connection', 'keep-alive')
// 处理终止逻辑
if (signal) {
onCanceled = cancel => {
console.log('fetchStreamChatForApp onCanceled ', cancel)
if (!xhr) {
return
}
reject(new CanceledError('canceled'))
xhr.abort()
xhr = undefined
}
// @ts-expect-error no types
signal?.aborted ? onCanceled() : signal?.addEventListener('abort', onCanceled)
}
xhr.send(data)
})
}
当前方案经验证,可以从流式接口获取到数据,但是流式效果不太好,而且从网上汇总来的信息来看,plus.net存在较多问题,比如《plus.net.XMLHttpRequest()在苹果端移动网络环境下不能使用》等。因此 不推荐 plus.net方案
2. event-source-polyfill
参考方案《OpenAI流式请求实现方案》、《react + ts + event-source-polyfill 实现方案》、《Vue中使用eventSource处理ChatGPT聊天SSE长连接获取数据》
实际App上运行发现报错 TypeError: XMLHttpRequest is not a constructor
哪位大佬可以解决上述问题请补充,不胜感激!
3. fetch-event-source
参考方案《js调用SSE客户端》、《fetch-event-source源码解析》、ChatGPT-SSE流式响应
经过验证发现单独引入@microsoft/fetch-event-source 会抛出异常
从ChatGPT-SSE流式响应分析应该是需要结合renderjs进行使用。
目前推荐使用该方案~
4. App原生语言插件
参考《EventSource (sse)等自定义网络请求 》
因为该插件目前仅支持Android,不推荐。
理论上,原生插件是一定能够解决这个问题,期待大佬们开发更完善的原生插件。
14 个评论
要回复文章请先登录或注册
1***@qq.com
9***@qq.com
操作起来
9***@qq.com
3***@qq.com
2***@qq.com
英曼畅学
w***@qq.com
c***@haier.com
3***@qq.com