分享好友 最新动态首页 最新动态分类 切换频道
手摸手带你实现前端乞丐版的 ChatGPT
2024-12-26 11:04

本文首发于个人博客

一开始看到 chatgpt 的流式渲染,有点好奇流式渲染是如何实现的,无意之间发现 vercel 的库:ai,仔细学习了它的代码,写的小而精,把它看明白了之后,想着写一篇文章来输出一下。

所以我就根据 ai 这个库,来一步步带大家来实现前端乞丐版 chatgpt ,把这篇文章看完之后,也可以去看看 ai 这个库,代码是写的真不错(会发现代码都是抄它的,哈哈哈哈)!!!

本文会使用 Next13,不熟悉也没关系,用到的 api 不多,不懂的 api 可以查看 Next13 文档。

注意:需要申请有一个 openai 的 apiKey,不然就无法调用接口哦。

源代码在这里

效果展示:

先看看一个概念:网络流,平时可能用不到。流是一种用于访问数据的数据结构,比如说:文件、接口返回的数据等等。

使用流有两个好处:

  • 可以处理大量数据,流可以将它们分成更小的部分(chunk),可以一次处理一个(chunk)。
  • 可以使用相同的数据结构、流,同时处理不同的数据,这使得代码变得更加复用。

在网络流中,一个 chunk 通常是:

  • 文本流:string
  • 二进制流:Uint8Arrays

网络流主要有三种:

  • ReadableStream:用于从数据源中读取数据。执行此操作的代码成为消费者。
  • WritableStream:用户将数据写入。执行此操作的代码成为生产者。
  • TransformStream 由两个流组成:
    • 它从其可写端(WritableStream)接收输入。
    • 它将输出发送到它的可读端,一个 ReadableStream。

本文中只会使用到 ReadableStream 和 TransformStream。

ReadableStream 可以从各种来源读取数据块,类型声明如下:

这三个属性的作用是:

  • :返回一个 Reader,可以从 ReadableStream 读取的对象,返回的 Reader 类似于迭代器。
  • :每个 ReadableStream 一次只能有一个活动的 Reader,当 Reader 在使用时,ReadableStream 被锁定并且不能被调用。
  • :将其 ReadableStream 连接到 ReadableWritablePair(一个 TransformStream)。它返回一个新的 ReadableStream(类比一下:把它理解成一个数组的 map 方法)。

下面来看看 的返回类型:

  • :在一个活动的 Reader 中,这个方法取消关联的 ReadableStream。
  • :停用 Reader 并解锁流。
  • 返回来两个属性的 ReadableStreamReadResult 的 Promise:
    • :布尔值,false 表示可以读取,true 表示最后一个块。
    • :块(chunk)。

师傅,别念 api 了,再念人都要傻了,赶紧来一个 demo 吧。

以下是通过 getReader 方式来读取 ReadableStream 的小例子。

  • A 行:不能直接读取 readableStream,需要调用来获取 Reader。
  • B 行:在 之后,readableStream 被锁定,所以 B 行打印的是 true,如果想再次调用,必须调用(E 行)。
  • C 行:返回属性 done 和 value,如果 done 为 true,表示是最后一个块,
  • D 行:可以对返回的 value 进行操作,这里是将返回的 value 全部都加在一个字符串里。

如果想通过 ReadableStream 读取外部源,可以将其包装在适配器对象中并将该对象传递给构造函数 ReadableStream。

以下是类型声明:

  • 调用构造函数后立即调用 start 方法。

controller 的参数类型如下:

  • :添加 chunk 到 ReadableStream 的内部队列。
  • :关闭 ReadableStream,消费者仍然可以清空队列,在那之后,流结束。

自定义 ReadableStream demo

ReadableStream 是异步可迭代的,可以使用来进行迭代。

使用控制器创建一个包含两个块的流(A 和 B 行),关闭流(C 行)很重要,否则永远不会结束。

转化流:

  • 通过其可写端(WritableStream)接受输入。
  • 然后它可能会或可能不会转换输入。
  • 结果可以通过 ReadableStream 读取,它是可读的。

使用最常见的方式是。

将 readableStream 传输到 transformStream 的可写端,并进行转换返回其可读端。

换句话说: 创建了一个新的 ReadableStream,它是 ReadableStream 的转换版本,类似于数组的 map。

一个简单的 demo:

TextEncoder.encode():将字符串作为输入,并返回 Uint8Array 包含 UTF-8 编码的文本。

使用了内置 TransformStream:,作用就是将接收到的二进制流转换为可读的文本流(Uint8Array -> string)。

自定义 TransformStream

跟上面的 ReadableStream 类似,如果要自定义 TransformStream,也可以传递适配器对象给构造函数 TransformStream。

它具有以下类型:

上面属性的解释:

  • :在调用构造函数后立即被调用,可以在转换之前做一些准备。
  • 执行实际的转化 。接受一个输入块,并可以使用 controller 将一个或多个转换后的输出块排队。

该 contrller 具有以下类型:

  • :添加 chunk 到 TransformStream 的可读端(输出)。
  • :关闭 TransformStream 的可读端(输出)并在可写端(输入)出错。如果转换器对可写端(输入)的剩余块不感兴趣并想跳过他们,则可以使用它。

小 demo

说了一大堆 api,来一个简单的例子:

Fetch API 是一种用于获取和发送网络资源的现代 Web API。它提供了一种替代 XMLHttpRequest 的方式,可以更简单、更灵活地进行网络请求。

Fetch API 使用 Promise 对象来返回请求结果,可以轻松地将其与 async/await 结合使用。

简单的小例子:

这里要用的是 fetch 返回的属性,它返回的是。

用到了上面提到的 ReadableStream,也算是简单的回顾一下。

需要注意的是:返回的是二进制流,后面会再提到

讲到这里,终于把前置的知识熟悉一下,我知道你很急,但是你先别急。

我们来进入实战环节。

首先使用初始化一个 Next13 项目。

  • What is your project named? openai-stream
  • Would you like to use TypeScript with this project? Yes
  • Would you like to use ESLint with this project? Yes
  • Would you like to use Tailwind CSS with this project? Yes
  • Would you like to use directory with this project? No
  • Use App Router (recommended)? Yes
  • Would you like to customize the default import alias? No

初始化之后就会安装 TypeScript、Eslint 和 Tailwind CSS。

第一步就开始画 UI。

UI 的话主要有两个部分:

  1. 消息列表展示。
  2. input 输入框。

MessageCard 渲染信息

创建类型文件定义关于 message 的类型。

MessageCard 组件用来渲染输入和 openai 返回的信息。

基础页面 + input 输入框

下一步画基础的页面和 input 输入框。

直接在里面书写即可。

先 mock 消息列表,看看展示效果咋样。

在 app 目录下新建文件,用来处理 api 请求。

可以直接访问 http://localhost:3000/api/chat 可以看到返回的数据。

关于 Next.js 的 Route 可以查看相关文档,不过多赘述。

需要发送 POST 请求将 message 传递给 openai,通过就可以处理 POST 请求。

就是对于 Response 的简单封装,将状态码置为 200。

创建文件用来处理网络流,使用 ReadableStream 先 mock 两条数据。

下一步写一个 hook 来进行页面交互,创建文件。

装包

需要安装一些依赖包:nanoid 和 swr。

  • nanoid 是可以生成唯一 ID 的库。
  • swr 是用于数据请求和缓存的库。

类型

将 CreateMessage 和 UseChatOptions 添加到文件,定义好 use-chat 的类型声明。

use-chat 具体逻辑

下面来写对应的 use-chat 具体逻辑:

使用来声明一个状态,第二个参数(fetcher)传入 null,表示不需要进行网络请求,可以把它当成本地状态来处理,返回的 mutate 函数可以对这个状态进行更新。

小小的工具函数

新建文件用来保存两个工具函数: nanoid、createChunkDecoder。

  • nanoid:使用 customAlphabet 函数创建了自定义的 ID 生成器 nanoid,用于生成唯一 ID。
  • createChunkDecoder:用于将 Uint8Array 类型的数据块解码成字符串(Uint8Array -> string)。

请求接口

接下来就是发送网络请求到,请求成功之后将数据渲染出来。

事件处理

接下来就是将上面的 hook 逻辑和视图绑定在一起。

连通性验证

不出意外的报 bug 了。

问题在于方法返回的是一个二进制流(Uint8Array),fetch 提供了、、等方式将二进制流转化为其他数据格式。

刚刚在里面推到队列里面的是字符串,所以就会报错。

有两种解决方式。

  1. enqueue 推到队列的类型改成二进制形式:

创建了 TextEncoder 对象,用于将字符串编码为 Uint8Array 对象。

  1. 采用 TransformStream 可以对输入的数据进行转换处理:

通过使用 pipeThrough 方法,将 AIStream 的输出流连接到 createCallbacksTransformer 的输入流,实现了数据的转换和传递。

实现将 string 转换为 Uint8Array 对象的流处理过程。

本文后面会使用第二种方式。

我们来看看效果:

最新文章
如何搭建小程序?
随着移动互联网的普及,小程序已经成为了一种非常流行的应用方式,因为它可以在不需要下载安装的情况下提供完整的应用体验。因此,越来越多的人开始关注如何搭建小程序。下面,我们来简单介绍一下如何搭建小程序。现在,无需找app开发公司
附件上传的大小限制
IIS6 默认允许的附件最大上传大小为4M,在IIS7之前,当我们需要扩大这个上传限制时,比如需要将允许上传的附件上限设置为50M,则可以在web.config中做如下配置:system.webhttpRuntime maxRequestLength="51200" //system.web2. maxAllowed
百度怎么开户投放广告
百度怎么开户投放广告?作为中国最大的搜索引擎,百度每天都要处理数以亿计的搜索请求,这也为广告主们提供了一个广阔的广告投放平台。如何在百度上开户并投放广告,让自己的品牌和产品得到更好的曝光和推广,是许多广告主关注的问题。本文
高清美女写真生成:超简单的AI绘图工具推荐与使用指南
限时免费,点击体验最近超火的AI生图神器,坐拥3000美女的大男主就是你! https://ai.sohu.com/pc/generate/textToImg?_trans_=030001_yljdaimn 宝子们,想象一下,你在家里随便拍拍,竟能产生出超逼真的美女写真,这不是电影里的情节,而
sem竞价代运营公司主要负责哪些工作?揭秘sem代运营的核心职责!
在数字营销这片浩瀚的海洋中,SEM竞价代运营公司就像那艘稳健的航船,载着企业在波涛汹涌的市场中破浪前行。那么,这些代运营公司究竟负责哪些工作?他们又是如何助力企业实现营销目标的呢?今天,咱们就来揭秘SEM代运营的核心职责,让您对
以图搜图
点击详情进入查看更多搜图工具网站服务:常用搜索,以图搜图,识图,以图搜图,图像检索,图像识别,图片识别,图片搜索,图像搜索,识图,搜图,找图,拍图,拍照,常用推荐,常用搜索,以图搜图。百度图像搜索-领先的图像识别技术连接人
长沙NOIP信息学奥赛集训营咨询热线:   在线咨询: 点击交谈
长沙NOIP信息学奥赛信息 长沙NOIP信息学奥赛集训营是长沙青少儿教育品牌,形成了以乐高创意启蒙课程、人工智能编程、智能机器人编程、信息学奥赛编程等课程体系为核心,集国内外大型权威科技赛事、少儿资格认证考试、科技主题的国内外游学
微信小程序如何获取链接
微信小程序的链接获取是开发者在开发过程中需要注意的一个重要问题。链接是小程序中不可或缺的元素,它可以在不同页面之间进行导航和跳转,为用户提供丰富的交互体验。这篇文章将介绍如何在微信小程序中获取链接,并提供一些实用的技巧和注
阅读与写作兴趣班教学计划
阅读与写作兴趣班教学计划(通用12篇)  时间就如同白驹过隙般的流逝,前方等待着我们的是新的机遇和挑战,现在就让我们好好地规划一下吧。计划到底怎么拟定才合适呢?以下是小编为大家收集的阅读与写作兴趣班教学计划,希望对大家有所帮
【C8600360清理大师下载】华为C8600360清理大师8.4.2免费下载
清理大师:更简单,更畅快!6亿用户信赖的手机清理!清理大师是一款完全免费的手机加速与空间清理软件,强力加速使手机运行更流畅,一键清理快速解决空间不足问题。【一键清理】一键搞定无用垃圾,手机清理更省心【手机加速】瞬间释放手机
相关文章
推荐文章
发表评论
0评