接收后端post流式接口返回,模拟打字机效果输出回答

745 阅读2分钟

1.获取流式的接口根据格式拆解出想要的数据

// 首先一个完整的data格式是
data: {"msg_id": "4365c87e-c0db-11ef-b910-0242ac12000c", "created": 1734923343, "choices": [{"delta": {"content": "xxx"}, "index": 16, "finish_reason": null, "error_info": null, "node_name": "xxx", "meta": {}}], "type": "str", "text_print": true, "params": []}

  • 要将后端不断返回的内容按照上面的格式先拆解,一开始我是用data :来分割,无法解析的就留在下一次合并后再分割,但是因为接口返回速度有时候很快,短时间很多数据都无法拆分先存到数组中,导致存储的文本太多数组超出长度报错,所以只能按照']}' 结束语来分割。
// 获取数据分段处理的核心代码
  async publicChatRoom(param) {
    const token = await this.getToken()
    const response = await fetch('url', {
      method: 'POST',
      headers: {
        'content-type': 'application/json;charset=utf-8',
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({
        param,
      }),
    })
    let generateData = '' 
    const decoder = new TextDecoder('utf-8')
    const reader = response.body.getReader() // 获取流的读取器
    while (true) {
      const processTextStream = ({ done, value }) => {
        if (done) {
          // 此处可以写流式接口返回完毕后的逻辑
          return
        }
        // 当done为false时,表示还有数据需要处理
        // 将读取到的二进制数据块value使用decoder解码为文本数据chunk
        const chunk = decoder.decode(value, { stream: true })
        // 将文本数据chunk按照换行符进行分割,得到一个包含每行文本的数组lines
        let lines = (generateData + chunk).split(']}\n\n')
        lines = lines.map((item, index) => {
          return index === lines.length - 1 ? item : item + ']}'
        })
        let repeatList = lines[0].split('data: {"msg_id"').filter(item => {
          return item
        })
        if (repeatList.length > 1) {
          lines[0] = '{"msg_id"' + repeatList[repeatList.length - 1]
        }
        for (const line of lines) {
          // 如果该行文本不为空(通过line.trim()判断)
          if (line) {
            const adopt = this.isValidJsonObject(line.replace('data: ', '')) // 尝试解析字符串
            if (adopt) {
              // curdata为完整的一个mag格式
              const curdata = JSON.parse(line.replace('data: ', ''))
              // 下面可以继续写获取到数据后的逻辑....
              generateData = ''
            } else {
              generateData += line
              // console.log('解析失败的', generateData)
            }
          }
        }
        return reader.read().then(processTextStream)
      }
      return reader.read().then(processTextStream)
    }
  }

2.页面输出要有打字机效果

  • 相关变量
      displayText: '', // 打字机输出的有效回答
      typingInterval: null,
      currentCharIndex: 0, // 记录页面输出到哪个字的位置
      typerCompleted: true, // 打印机效果是否完成
      answerCompleted // 后端返回的数据是否回答完毕
  • 开始调用打印方法typeLine
  
    startTyping() {
      // 清空显示文本,重新开始
      this.displayText = '' // 存放页面输出文字的变量
      this.typerCompleted = false // 是否完成打印输出
      if (this.typingInterval) {
        clearInterval(this.typingInterval)
      }
      this.typingInterval = setInterval(this.typeLine, 20) // 这里调整打字速度
    }
  • typeLine 核心代码

     typeLine() {
      let text = content // content是解析出来的回答,是不断在增加的
      if (text !== undefined && text.length > this.currentCharIndex) {
        this.displayText += text[this.currentCharIndex]
        this.currentCharIndex = this.currentCharIndex + 1
        setTimeout(() => {
          this.scrollToBottom() // 每次输出了内容,都要滚动到回答的最下面
        }, 500)
        if (text.length === this.currentCharIndex && this.answerCompleted) {
          clearInterval(this.typingInterval) // 清除定时器
          this.typerCompleted = true
          this.scrollToBottom()
          this.typingInterval = null
          this.currentCharIndex = 0
        }
      }
    },

3.要将输出的回答展示为markdown格式需要用到的组件(两个都可以)

  • vue-markdown

<vue-markdown :source="displayText"></vue-markdown>

  • marked.js

<span v-html="compiledMarkdown" id="answerMarkdown" ></span>

// 写在计算属性中
    compiledMarkdown() {
      return marked.parse(this.displayText)
    }