从流中获取的数据格式如下
小程序调用SSE接口
const requestTask = wx.request({ url: `xxx`, // 需要请求的接口地址 enableChunked: true, // enableChunked必须为true method: "GET", timeout: '120000', success(res) { console.log(res.data) }, fail: function (error) { // 请求失败的操作 console.error(error); }, complete: function () { // 请求完成的操作,无论成功或失败都会执行 console.log('请求完成', str); } }) // 监听服务端返回的数据 requestTask.onChunkReceived(res => { console.log( res, res.data); })
我这边接收到的数据类型为Uint8Array,需要处理成text文本(如上图)
// 监听服务端返回的数据 requestTask.onChunkReceived(res => { console.log( res, res.data); // Uint8Array转为text格式 let arrayBuffer = res.data; let decoder = new TextDecoder('utf-8'); let text = decoder.decode(arrayBuffer); //let text = that.arrayBufferToString(arrayBuffer)//真机使用 //正则匹配上所有event:data后面的文字 const eventRegex = /event:data/ndata:"data:(.*?)"/g; const eventRegexErr = /event:600/ndata:"(.*?)"/g; let matches = []; let match; if (text.indexOf('600') != -1) {//如果获取响应失败 while ((match = eventRegexErr.exec(text)) !== null) { wx.showToast({ title: match[1], }) matches.push(match[1]); } str = str + matches.join('') } else {//如果获取响应成功 while ((match = eventRegex.exec(text)) !== null) { matches.push(match[1]); } //处理成字符串 str = str + matches.join('') console.log(text, str); } })
TextDecoder在真机上没法使用,真机上需要使用另一种
arrayBufferToString(arr) { if (typeof arr === 'string') { return arr; } var dataview = new DataView(arr); var ints = new Uint8Array(arr.byteLength); for (var i = 0; i < ints.length; i++) { ints[i] = dataview.getUint8(i); } var str = '', _arr = ints; for (var i = 0; i < _arr.length; i++) { if (_arr[i]) { var one = _arr[i].toString(2), v = one.match(/^1+?(?=0)/); if (v && one.length == 8) { var bytesLength = v[0].length; var store = _arr[i].toString(2).slice(7 - bytesLength); for (var st = 1; st < bytesLength; st++) { if (_arr[st + i]) { store += _arr[st + i].toString(2).slice(2); } } str += String.fromCharCode(parseInt(store, 2)); i += bytesLength - 1; } else { str += String.fromCharCode(_arr[i]); } } } return str; },
使对话有打字机效果
参考自:小程序实现 ChatGPT 聊天打字兼自动滚动效果
handleRequestResolve(result) { this.setData({ currentContent: '' }) const contentCharArr = result.trim().split("") this.showText(0, contentCharArr); }, showText(key = 0, value) { /* 所有内容展示完成 */ if (key >= value.length) { // wx.vibrateShort() //判断字是否展示完 this.setData({ isShowFinish: true }) return; } /* 渲染回话内容 */ this.setData({ currentContent: this.data.currentContent + value[key], }) setTimeout(() => { /* 递归渲染内容 */ this.showText(key + 1, value); }, 50); },
对话滚动到可视区域内
handleScollTop() { return new Promise((resolve) => { const query = wx.createSelectorQuery() query.select('.page-content').boundingClientRect() query.select('.scroll-view-content').boundingClientRect() query.exec((res) => { const scrollViewHeight = res[0].height const scrollContentHeight = res[1].height if (scrollContentHeight > (scrollViewHeight - 200)) { const scrollTop = scrollContentHeight - scrollViewHeight + 200 this.setData({ scrollTop }, () => { resolve() }) } else { resolve() } }) }) }, showText(key = 0, value) { /* 所有内容展示完成 */ if (key >= value.length) { // wx.vibrateShort() this.setData({ isShowFinish: true }) return; } /* 渲染回话内容 */ this.setData({ currentContent: this.data.currentContent + value[key], }, () => { this.handleScollTop().then(() => { setTimeout(() => { this.showText(key + 1, value); }, 20); }) }) },
完整代码
.wxml
<scroll-view scroll-y scroll-top="{{scrollTop}}" wx:else class="page-content {{isFirst ? '' : 'page-content-bg'}}"> <view class="scroll-view-content"> <view wx:for="{{talkArr}}" wx:key="index" class="talk-box1"> <view class="talk-box-question" wx:if="{{item.isAnswer=='0'}}"> <view class="left"> <text class="left-content">{{item.content}}</text> </view> <image class="right" src="../images/user-icon.png" mode="aspectFill" /> </view> <view class="talk-box-reply" wx:else> <image class="left" src="../images/ai-icon.png" mode="aspectFill" /> <view class="right"> <view class="right-content"> <view wx:if="{{(index!=talkArr.length-1)}}">{{item.content}}</view> <view wx:else> <view wx:if="{{loading}}"> <image class="loading" src="../images/loading-1.png" mode="aspectFill" /> </view> <view wx:else> {{currentContent}} </view> </view> </view> </view> </view> </view> </view> </scroll-view>
.wxss
.page-content { width: 100%; margin-top: 48rpx; padding-top: 150rpx;}.page-content-bg { background: #F5F6F7; height: 75%; padding-bottom: 280rpx; overflow: scroll; padding-top: 0;}.scroll-view-content { padding-top: 50rpx;}.talk-box { display: flex;}.talk-box1 { width: 90%; margin: 0 auto;}.talk-box .left { width: 80rpx; height: 80rpx;}.talk-box .right { margin-left: 30rpx; flex: 1;}.talk-item { height: 92rpx; background: #F6FFF9; border-radius: 0rpx 20rpx 20rpx 20rpx; font-family: PingFang SC, PingFang SC; font-weight: 500; font-size: 28rpx; color: rgba(51, 51, 51, 0.9); text-align: left; display: flex; align-items: center; padding: 0 38rpx;}.talk-box-question,.talk-box-reply { width: 100%; display: flex; margin-bottom: 32rpx;}.talk-box-question .left { flex: 1; display: flex; align-items: center; justify-content: flex-end;}.left-content { background: linear-gradient(273deg, #44BE35 0%, #6ECB63 100%); box-shadow: 0rpx 2rpx 8rpx 0rpx rgba(0, 0, 0, 0.05); border-radius: 24rpx 0rpx 24rpx 24rpx; padding: 24rpx; font-family: PingFang SC, PingFang SC; font-weight: 400; font-size: 28rpx; color: #FFFFFF; line-height: 44rpx; text-align: left;}.talk-box-question .right { margin-left: 30rpx; width: 80rpx; height: 80rpx;}.talk-box-reply .left { width: 80rpx; height: 80rpx;}.talk-box-reply .right { margin-left: 30rpx; flex: 1; display: flex; align-items: center; justify-content: flex-start;}.right-content { background: #FFFFFF; box-shadow: 0rpx 2rpx 8rpx 0rpx rgba(0, 0, 0, 0.05); border-radius: 0rpx 24rpx 24rpx 24rpx; border: 2rpx solid #6ECB63; padding: 24rpx; font-family: PingFang SC, PingFang SC; font-weight: 400; font-size: 28rpx; color: rgba(0, 0, 0, 0.9); line-height: 46rpx; text-align: left;}
.js
data: { isShowFinish: false, scrollTop: '', currentContent: '', loading: false, talkArr: [] }, getDataStream(data) { let str = '' let that = this this.setData({ loading: true, }) // 基础库为2.33.0 const requestTask = wx.request({ enableChunked: true, // 开启分片模式 url: `xxx`, // 需要请求的接口地址 enableChunked: true, // enableChunked必须为true method: "GET", responseType: "arraybuffer", timeout: '120000', success(res) {}, fail: function (error) { // 请求失败的操作 console.error(error); }, complete: function () { // 请求完成的操作,无论成功或失败都会执行 that.handleRequestResolve(str) let index = that.data.talkArr.length - 1 let answerContent = `talkArr[${index}].content` that.setData({ [answerContent]: str, loading: false }) } }) // 监听服务端返回的数据 requestTask.onChunkReceived(res => { // Uint8Array转为text格式 let arrayBuffer = res.data; let decoder = new TextDecoder('utf-8'); let text = decoder.decode(arrayBuffer); //let text = that.arrayBufferToString(arrayBuffer)//真机使用 //正则匹配上所有event:data后面的文字 const eventRegex = /event:data/ndata:"data:(.*?)"/g; const eventRegexErr = /event:600/ndata:"(.*?)"/g; let matches = []; let match; if (text.indexOf('600') != -1) { //如果获取响应失败 while ((match = eventRegexErr.exec(text)) !== null) { wx.showToast({ title: match[1], icon: 'none' }) matches.push(match[1]); } str = str + matches.join('') } else { //如果获取响应成功 while ((match = eventRegex.exec(text)) !== null) { matches.push(match[1]); } //处理成字符串 str = str + matches.join('') } }) requestTask.offChunkReceived(res => {}) }, handleScollTop() { return new Promise((resolve) => { const query = wx.createSelectorQuery() query.select('.page-content').boundingClientRect() query.select('.scroll-view-content').boundingClientRect() query.exec((res) => { const scrollViewHeight = res[0].height const scrollContentHeight = res[1].height if (scrollContentHeight > (scrollViewHeight - 200)) { const scrollTop = scrollContentHeight - scrollViewHeight + 200 this.setData({ scrollTop }, () => { resolve() }) } else { resolve() } }) }) }, arrayBufferToString(arr) { if (typeof arr === 'string') { return arr; } var dataview = new DataView(arr); var ints = new Uint8Array(arr.byteLength); for (var i = 0; i < ints.length; i++) { ints[i] = dataview.getUint8(i); } var str = '', _arr = ints; for (var i = 0; i < _arr.length; i++) { if (_arr[i]) { var one = _arr[i].toString(2), v = one.match(/^1+?(?=0)/); if (v && one.length == 8) { var bytesLength = v[0].length; var store = _arr[i].toString(2).slice(7 - bytesLength); for (var st = 1; st < bytesLength; st++) { if (_arr[st + i]) { store += _arr[st + i].toString(2).slice(2); } } str += String.fromCharCode(parseInt(store, 2)); i += bytesLength - 1; } else { str += String.fromCharCode(_arr[i]); } } } return str; }, handleRequestResolve(result) { this.setData({ currentContent: '' }) const contentCharArr = result.trim().split("") this.setData({ isShowFinish: false }) this.showText(0, contentCharArr); }, showText(key = 0, value) { /* 所有内容展示完成 */ if (key >= value.length) { // wx.vibrateShort() this.setData({ isShowFinish: true }) return; } /* 渲染回话内容 */ this.setData({ currentContent: this.data.currentContent + value[key], }, () => { this.handleScollTop().then(() => { setTimeout(() => { this.showText(key + 1, value); }, 20); }) }) },