Uniapp仿ChatGPT Stream流式输出(非Websocket)-uniapp+see接收推送示例

前言#

最近写一个chagpt小程序,流式输出可以使用websocket也可以使用stream来实现,这里就不折腾websocket的了,我发现uniapp实现流式输出的方式挺多的,主要是有些小程序还不兼容,花了点时间研究了一下。

EventSource#

EventSource也称为SSE(Server-Sent Events),是服务器推送的一个网络事件接口,一个EventSource会对http服务开启一个持久化链接,它发送的事件格式是‘text/stream’,开启EventSource事件后,它会一直保持开启状态,直到被要求关闭

后端php,原生实现个流式输出即可

/**
 * @function 与客户端server send event通信方式
 * @param    $callback    callable 回调,若返回数组代表要输出json,返回null代表本次循环不进行输出
 * @param    $millisecond int      数据分发间隔,单位:毫秒
 * @return   string
 * @other    void
 */
function sse($callback, $millisecond = 1000) {
    set_time_limit(0);
    ini_set('output_buffering', 'off');
    ini_set('zlib.output_compression', false);
    while (@ob_end_flush()) {}
    header('Content-Type: text/event-stream; Charset=UTF-8');
    header('Cache-Control: no-cache');
    header('Connection: keep-alive');
    header('X-Accel-Buffering: no');
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Credentials: true");
    header('Access-Control-Allow-Methods: *');
    header('Access-Control-Allow-Headers: *');

    ob_start();
    while (true) {
        $callback_res = $callback();
        if($callback_res !== null) {
            $data = json_encode($callback_res, 320);
            echo "data:{$data}\n\n";
        }
        ob_flush();
        flush();
        usleep($millisecond * 1000);
    }
}
调用
示例
public function test_flush(){
sse(function() {
$one=Db::name('vpnlink')->where('create_time','>',time()-300)->find();
if($one) {
return $one;
}
return null;
}, 1000);
}

uniapp前端如何连接

XHR方式#

uniapp自带的uni.request()不支持stream方式,所以可以使用XHR来实现,但是小程序又不支持XHR

const requestChat = (msg)=>{
    // 在页面中创建 XMLHttpRequest 对象
    var xhr = new XMLHttpRequest()
    // 设置 XMLHttpRequest 对象
    xhr.open('GET', 'http://localhost:8090/chat?msg=' + msg)
    xhr.responseType = 'text'
    xhr.timeout = 0
    const msgNum = messageList.value.length;
    xhr.onreadystatechange = function () {
        if (xhr.readyState === XMLHttpRequest.LOADING) {
            // 处理 HTTP 数据块
            console.log(xhr.responseText)
        }
    }
    xhr.send()
}

EventSource方式#

uniapp中也可以直接使用EventSource来实现流式输出,可惜小程序也不支持(难顶)

const chatStream = () => {
    const es = new EventSource("http://localhost:8090/sse")
    es.onmessage = function(event) {
        console.log(event)
        if (event.lastEventId=="[DONE]") {
            console.log(event.data);
            return es.close()
        }
        console.log(event.data);
    };
}

Transfer-Encoding Chunk#

uniapp中使用uni.request(),配置enableChunked: true即可--推荐

const requestTask = uni.request({
    url: 'http://localhost:8090/sse',
    timeout: 15000,
    responseType: 'text',
    method: 'GET',
    enableChunked: true,  //配置这里
    data: {},
    success: response => {
        console.log(response)
    },
    fail: error => {}
})
requestTask.onHeadersReceived(function(res) {
    console.log(res.header);
});
// 这里监听消息
requestTask.onChunkReceived(function(res) {
    let decoder = new TextDecoder('utf-8');
    let text = decoder.decode(new Uint8Array(res.data));
    console.log(text)
})

由于接收到的数据是arraybuffer,所以我们要转换为字符串类型,但是编码会出现一点问题,上面这段代码使用TextDecoder来转换编码,但是小程序又不支持TextDecoder。。。

再换一种转换方式

requestTask.onChunkReceived(function(res) {
    const uint8Array = new Uint8Array(res.data);
    let text = String.fromCharCode.apply(null, uint8Array);
    text = decodeURIComponent(escape(text));
    console.log(text);
})

这样就可以在小程序中把Arraybuffer转换为字符串了

还有许多方式,比如调用第三方库来代替TextDecoder,我就不去尝试了

注意:

1.enableChunked: true流式响应开关,会自动在header中加入transfer-encoding chunked

2.arraybuffer转字符串问题,有TextDecoder就很好处理,没有也可以参照我上面的示例。

THE END