艾林博客 - 技术交流与经验分享的个人博客

实现微信小程序与服务端流式数据交互:打造实时打字效果

Liner51

Liner51

5个月前更新

小程序端

1.首先我们定义wxml文件的内容
<view class="container">
  <label for="textInput">Prompt:</label>
  <textarea id="textInput" placeholder="您有什么问题" bindinput="bindTextInput" class="prompt-text"></textarea>
  <van-button type="primary" bindtap="runPrompt" class="prompt-button">执行prompt</van-button>
  <div class="answer">{{answer}}</div>
</view>
2.定义简单的wxss样式
page{
  background-color: #f4f6f8;
  height: 100%;
}
.prompt-button{
  margin-top: 1rem;
}
.prompt-text{
  margin-top: 1rem;
  border: 1px solid #fcf;
  padding: 1rem;
}
.answer {
  margin-top: 1rem;
  padding:1rem ;
  white-space: pre-wrap; /* 保留换行符和空白字符 */
  overflow: hidden;
  background-color: #fff;
  border-radius: 6px;
  width: 80%;
}

3.页面

** 接下来要实现的功能是 **

  • 用户输入问题
  • 点击prompt按钮提交
  • 文本以打字机效果出现在白色区域
4.js文件

主要设置 enableChunked: true,

Page({
  data: {
    answer: '',
    textInput: '写一篇五十字的作文',
    polling: true,
    serverUrl: 'http://127.0.0.1:7661/api/chat/stream', // 替换为你的服务端地址
    typingBuffer: [],
    typingInterval: null,
    isTyping: false // 当前是否在执行打字效果
  },

  runPrompt: function () {
    const inputValue = this.data.textInput;
    this.setData({
      answer: '',
      polling: true,
      typingBuffer: [],
      isTyping: false
    });
    this.sendPollingRequest({ 'prompt': inputValue });
  },

  sendPollingRequest: function (data) {
    const that = this;
    if (!this.data.polling) return;

    const requestTask = wx.request({
      url: this.data.serverUrl,
      method: 'POST',
      enableChunked: true,// 一定设置
      header: {
        'content-type': 'application/json'
      },
      data: JSON.stringify(data),
      success: function (res) {
        console.log("Request success:", res);
      },
      fail: function (err) {
        console.error("Request error:", err);
        that.setData({ polling: false });
      }
    });


    requestTask.onChunkReceived(res => {
      const textDecoder = new TextDecoder('utf-8');
      const chunkText = textDecoder.decode(new Uint8Array(res.data));
	  
      console.log('chunkText:',chunkText);
    });
  },

  bindTextInput: function (e) {
    this.setData({
      textInput: e.detail.value
    });
  }
});

截止此处 就可以看到服务端返回的数据

  • 此处的res如果不做转化 是无法直接显示文文字的 我们使用TextDecoder 进行转化
  • 我们可以就此处对chunkText进行后续处理 打字机效果 或者直接展示于页面也好
  requestTask.onChunkReceived(res => {
      const textDecoder = new TextDecoder('utf-8');
      const chunkText = textDecoder.decode(new Uint8Array(res.data));
	  
      console.log('chunkText:',chunkText);
    });

因为服务端返回数据是类似下边的字符串,所以要进行截取转化最后打印机效果展现

{"content":"当然可以。","type":"text"}{"content":"以下是一篇约150字的文章:\n\n随着数字化时代的到来,人工智能在企业营销领域扮演着越来越重要的角色。","type":"text"}

每个服务端返回的数据不一样,我们在拿到数据以后自行处理

以下是完整的js代码

Page({
  data: {
    answer: '',
    textInput: '写一篇五十字的作文',
    polling: true,
    serverUrl: 'http://127.0.0.1:7661/api/chat/stream', // 替换为你的服务端地址
    typingBuffer: [],
    typingInterval: null,
    isTyping: false // 当前是否在执行打字效果
  },

  runPrompt: function () {
    const inputValue = this.data.textInput;
    this.setData({
      answer: '',
      polling: true,
      typingBuffer: [],
      isTyping: false
    });
    this.sendPollingRequest({ 'prompt': inputValue });
  },

  sendPollingRequest: function (data) {
    const that = this;
    if (!this.data.polling) return;

    const requestTask = wx.request({
      url: this.data.serverUrl,
      method: 'POST',
      enableChunked: true,
      header: {
        'content-type': 'application/json'
      },
      data: JSON.stringify(data),
      success: function (res) {
        console.log("Request success:", res);
      },
      fail: function (err) {
        console.error("Request error:", err);
        that.setData({ polling: false });
      }
    });

    let buffer = ''; // 用于存储不完整的JSON字符串

    requestTask.onChunkReceived(res => {
      const textDecoder = new TextDecoder('utf-8');
      const chunkText = textDecoder.decode(new Uint8Array(res.data));

      console.log('chunkText:',chunkText);
      buffer += chunkText;



      // 使用正则表达式找到所有完整的JSON对象
      const regex = /(\{.*?\})(?=\{|\s*$)/g;
      let match;
      let lastProcessedIndex = 0;

      while ((match = regex.exec(buffer)) !== null) {
        try {
          const jsonStr = match[1];
          const json = JSON.parse(jsonStr);

          if (json.type === 'text') {
            that.data.typingBuffer.push(...json.content.split('')); // 将内容逐字符添加到缓冲区
            // console.log('Updated typingBuffer:', that.data.typingBuffer);
          }

          lastProcessedIndex = regex.lastIndex;
        } catch (e) {
          console.error('JSON解析错误:', e);
        }
      }

      // 清除已经处理的部分
      buffer = buffer.slice(lastProcessedIndex);

      if (!that.data.isTyping) {
        that.typingEffect(); // 触发打字效果
      }
    });
  },

  typingEffect: function () {
    const that = this;

    if (this.data.isTyping) {
      return;
    }

    this.setData({ isTyping: true });

    this.data.typingInterval = setInterval(() => {
      if (that.data.typingBuffer.length > 0) {
        const char = that.data.typingBuffer.shift(); // 从缓冲区取出一个字符
        // console.log('Typing character:', char);
        that.setData({
          answer: that.data.answer + char
        });
      } else {
        console.log('Buffer empty, clearing interval.');
        clearInterval(that.data.typingInterval);
        that.setData({ isTyping: false });
      }
    }, 100); // 调整间隔时间以控制打字速度
  },

  bindTextInput: function (e) {
    this.setData({
      textInput: e.detail.value
    });
  }
});

服务端

服务端就不多做解释了 主要是返回头

     'Content-Type'      => 'text/event-stream',  // 必须
     'Cache-Control'     => 'no-cache',
     'Connection'        => 'keep-alive',
     'Transfer-Encoding' => 'chunked', // 必须

最终效果

此文章仅做学习参考使用,其逻辑自行修改 总结就是wx.request 开启 enableChunked: true, 配置 ,以onChunkReceived 读取数据 并实时显示在页面上

The End
案例分析

喜欢就支持一下把!

(0)
爱情是叹息吹起的一阵烟;恋人的眼中有它净化了的火星;恋人的眼泪是它激起的波涛。它又是最智慧的疯狂,哽喉的苦味,吃不到嘴的蜜糖。

莎士比亚

为您推荐