微信小程序-接入sse数据流并实现打字机效果( ChatGPT )

小程序 0

从流中获取的数据格式如下

图1

小程序调用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);      })    })  },

也许您对下面的内容还感兴趣: