实战案例:微信小程序中实现 ChatGPT 打字机效果(上)

2,067 阅读4分钟

背景

之前分享过在 Web 端如何实现 ChatGPT 打字机效果的文章,感兴趣的同学可以前往查看:实战案例:ChatGPT 打字机效果的三种实现方式,本文主要分享在微信小程序中如何实现 ChatGPT 打字机效果的前半部分,即如何获取 SSE 通信的接口数据。

接下来的文章实战案例:微信小程序中实现 ChatGPT 打字机效果(下)中,实现了小程序中渲染服务端 SSE 返回的 Markdown 格式的内容。

技术背景

本文所使用示例涉及的技术栈:

  • Taro 3.x + React
  • Express
  • 其他库:
    • text-encoding-shim

tips: 默认你已经知道上述库或框架如何使用,本文不会介绍相关技术栈的使用教程。

目录

  • 服务端 SSE 接口示例
  • 核心实现
  • 代码示例

服务端 SSE 接口示例

此处的接口,直接复用之前 Web 端的示例,代码如下:

const express = require("express");
const cors = require("cors");

const app = express();
app.use(cors());

const PORT = 5000;
const getTime = () => new Date().toLocaleTimeString();
const contentStr = "很高兴为您服务,我是模拟的 ChatGPT 机器人。".split('')

app.get("/sse", function (req, res) {
  res.writeHead(200, {
    Connection: "keep-alive",
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
  });
  // 正常的 sse 结束,需要从客户端触发 close 事件,如果从服务端触发,客户端会收到 error 
  req.on('close', function () {
    console.log('close')
    clearInterval(interval)
  })
  let count = 0
  // 此处用计时器来模拟大模型的查询结果
  // 通过发送字符数组的长度,来模拟 SSE 服务的 start、cmpl、done 状态
  const interval = setInterval(() => {
    // 如果前端没有正确触发 SSE 的 close 事件,服务端判断如果数据已发送完成,也会主动关闭事件
    if (count > contentStr.length) {
      res.end()
      clearInterval(interval)
      return
    } else if (count === 0) {
      res.write(
        `data:${JSON.stringify({
          time: getTime(),
          event: 'start',
          content: contentStr[count]
        })}`
      );
      res.write("\n\n");
    } else if (count === contentStr.length) {
      res.write(
        `data:${JSON.stringify({
          time: getTime(),
          event: 'done',
        })}`
      );
      res.write("\n\n");
    }
    else {
      res.write(
        `data:${JSON.stringify({
          time: getTime(),
          event: 'message',
          content: contentStr[count]
        })}`
      );
      res.write("\n\n");
    }
    count++
  }, 100);
});

app.listen(PORT, function () {
  console.log(`Server is running on port ${PORT}`);
});

核心实现

  • 由于小程序中没有 Web 中的 EventSource 对象,因此无法直接使用浏览器的 eventSource 对象。
  • 在小程序中的数据请求,需要使用 wx.request 方法,对应在 Taro 中需要使用 Taro.request 方法,所以无法通过 ReadableStream.getReader() 获取流数据,也就无法使用 @microsoft/fetch-event-source
  • 我们可以从 ReadableStream 思路出发,寻找小程序中是否有和数据流相似的东西,这时候再看一遍文档,会发现 wx.request 中的 enableChunked 配置,这也是我们本文的核心实现的原理。

代码示例

数据交互

  • 本项目基于 Taro 3.6.34,使用 taro-cli 生成一个默认模板项目即可,代码如下:
// 完整代码
import { View } from "@tarojs/components";
import Taro from "@tarojs/taro";

export default function Index() {
  const handleClick = () => {
    const requestTask = Taro.request({
      url: "http://localhost:5000/sse",
      method: "GET",
      enableChunked: true,
      success: (res) => {
        console.log("---->", res);
      },
    });

    requestTask.onChunkReceived((res) => {
      console.log("chunk", res);
    });
  };

  return (
    <View className="index">
      <View onClick={handleClick}>点我发起请求</View>
    </View>
  );
}

  • 获取到的 Chunk 数据效果如下:

图1-1.png

数据处理

  • 上一步已经实现了 SSE 的数据请求,但是我们从打印中可以看到,chunk 拿到的数据是 Unit8Array 格式的,并不能像我们常用的 JSON 格式。
  • 借助 text-encoding-shim 库,做一次转换,通过 npm i text-encoding-shim 安装后使用
// 完整代码
import { View } from "@tarojs/components";
import Taro from "@tarojs/taro";
import * as TextEncoding from "text-encoding-shim";

export default function Index() {
  const handleClick = () => {
    const requestTask = Taro.request({
      url: "http://localhost:5000/sse",
      method: "GET",
      enableChunked: true,
      success: (res) => {
        console.log("---->", res);
      },
    });

    requestTask.onChunkReceived((res) => {
      const uint8Array = new Uint8Array(res.data);
      const str = new TextEncoding.TextDecoder("utf-8").decode(uint8Array);
      console.log("chunk", str);
    });
  };

  return (
    <View className="index">
      <View onClick={handleClick}>点我发起请求</View>
    </View>
  );
}

  • 处理后的数据输出

图1-2.png

  • 至此我们完成了在小程序中请求并处理服务端接口返回的 SSE 方式的数据流。

杂谈

  • 本人之前的 实战案例:ChatGPT 打字机效果的三种实现方式 文章中的第一种方案,感兴趣的同学可是在小程序中尝试这种效果,因为这种方案还属于最基础 HTTP 请求,不涉及数据流/块的传输。
  • 依然要注意在 Nginx 上的配置,不要有缓存,否则也会等到数据全部出来后一次性返回。

参考文档

浏览知识共享许可协议

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。