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

https://file-one.7k7s.com//uploads/20240604/89f56a7378e381410f4dfcfab3948775.jpg
陈杰 案例分析 发布于1年前 更新于1年前 1.1K+

小程序端

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

喜欢就支持一下吧!

版权声明:除却声明转载或特殊注明,否则均为艾林博客原创文章,分享是一种美德,转载请保留原链接,感谢您的支持和理解

忠诚是人生的本色

清·黄宗羲

推荐阅读

深度探索 PHP 8 注解:从基础概念到高级应用

本文全面深入地探讨了 PHP 8 注解,从基础概念、原理分析到自带注解详解与高级应用实践,为开发者提供了关于注解的全方位...

https://file-one.7k7s.com//uploads/20240604/89f56a7378e381410f4dfcfab3948775.jpg
陈杰 03月12日

探索 PHP 8.4 的革新:增强类型系统、惰性初始化与更多新特性

深入了解PHP 8.4带来的重大改进,包括更强大的类型系统、属性(Property Hooks)支持、非对称可见性、惰性...

https://file-one.7k7s.com//uploads/20240604/89f56a7378e381410f4dfcfab3948775.jpg
陈杰 01月08日

php获取指定日期的前一天,前一月等日期范围

php获取指定日期的前一天,前一月等日期范围

https://file-one.7k7s.com//uploads/20240604/89f56a7378e381410f4dfcfab3948775.jpg
陈杰 02月24日

一文读懂 XSS 攻击:原理、类型与防范措施

本文详细介绍了 XSS 攻击的原理、三种类型(反射型、存储型、DOM - Based),并通过示例进行说明,同时给出了输...

https://file-one.7k7s.com//uploads/20240604/89f56a7378e381410f4dfcfab3948775.jpg
陈杰 03月05日

为什么平台都不管你 key 泄露?

很多开发者疑惑:如果我的 API-Key 被盗了,为什么平台方(比如腾讯云、OpenAI)都不报警、不封禁?他们难道不负...

https://file-one.7k7s.com//uploads/20240604/89f56a7378e381410f4dfcfab3948775.jpg
陈杰 07月04日

前后端分离架构的优点、挑战与最佳实践

本文详细介绍了前后端分离架构的优点、常见挑战及最佳实践,适用于开发团队在构建现代 Web 应用时参考和借鉴。

https://file-one.7k7s.com//uploads/20240604/89f56a7378e381410f4dfcfab3948775.jpg
陈杰 01月13日

thinkphp 模型withCount方法如何指定COUNT字段

本文将详细介绍如何在 ThinkPHP 模型中使用 withCount 方法来获取关联模型的计数信息。通过指定 COUN...

https://file-one.7k7s.com//uploads/20240604/89f56a7378e381410f4dfcfab3948775.jpg
陈杰 01月03日

现代接口安全实战:从加密到防滥用的全栈策略

很多人以为接口加了个 API-Key 或 JWT 就算“安全”。其实现代 API 安全从来不靠某一种“工具”,而是靠传输...

https://file-one.7k7s.com//uploads/20240604/89f56a7378e381410f4dfcfab3948775.jpg
陈杰 07月04日