一 前言
已经长时间大火,未来将会是AI的天下。人们需要更多地学习和掌握AI,而不是被AI所取代。
目前市面上已经有很多类似 的智能应用,应用有可能是 web h5 应用,也有可能是小程序或者是 应用。随着 深入,移动端也会再次火爆起来。
在 的背景下,我们今天来聊聊在小程序中怎么实现类似 的聊天打字效果,并且实现滚动效果,具体如下:
这篇文章将深入一下内容:
二 实现打字效果1.预热内容—数据请求与接收
开发者可以接入 提供的接口,实现自定义的问答流程。在聊天会话中,我们问 一句话:
介绍一下跨端开发
那么和平常的请求不同的是,数据并不是一次性返回的,而是采用 流式返回的。我们可以在 中看到 的大体结构:
如上可以看到返回的 text 是分片处理的,每次会返回一小段内容,只要前端根据返回这一小段内容就可以了,也就自然形成了打字的效果。
可能会有同学好奇,这种分片的数据结构,前端应该怎么接收呢?实际很简单,我们拿 axios 为例子,开发者可以通过监听 事件来接受服务端返回的文本片段。具体例子如下:
axios({
method: 'post',
url: 'https:xxx.xxx,
onDownloadProgress: function({ event }) {
const xhr = event.target
const { responseText } = xhr
/* 获取返回的内容,本质上是 json 字符串 */
let chunk = responseText
try{
/* 序列化返回的内容 */
const data = JSON.parse(chunk)
/* chatGPT 返回的内容 */
console.log(data.text)
}catch(e){
}
}
})
这里描述请求的流程,通过 来监听返回的内容,然后获取到返回的内容,JSON.parse 解析内容,这里有一个注意事项,就是对于 JSON.parse 应该加上 try catch ,防止解析的失败。
2.小程序中接口处理
小程序没有如上 axios 里面监听 流式响应数据的能力,也没有处理 的回调函数。简单来说 的实现,本质上是 axios 在浏览器发起 http 请求,会创建一个 XHR 对象,其用于发送请求和接收响应,在创建 XHR 对象后,axios 会注册一个 事件监听器到 XHR 对象上,用于获取下载的进度信息。
那么小程序中如何实现分片流式下载呢?在小程序中,统一收口到 中,在 中可以用 的 来接收服务端的分片数据。这个方法可以监听 – Chunk 事件。当接收到新的 chunk 时触发。
我们来看看具体怎么使用:
const requestTask = wx.request({
enableChunked:true, // 开启分片模式
...
})
requestTask.onChunkReceived((res)=>{
// 接收分片的数据
})
这样就可以通过分片来实现打字的效果。
3.打字效果实现
接下来我们看一下小程序是如何实现打字效果的,先不考虑返回的数据是 流式结构,先认为返回的数据格式是整个文本,那么应该怎么样处理文本呢。
首先我们聊天的内容如下所示:
如上, 每一个消息都是一个 -item ,所有的 保存到了 列表中,在 wxml 中如下所示:
<view wx:for="{{messageList}}" wx:key="id" id="item-{{item.id}}">
<message-item
data-index="{{index}}"
role="{{item.role}}"
content="{{item.content}}"
finished="{{item.finished}}"
bind:share="handleMessageShare"
/>
</view>
如上,可以看到 -item 保存了一条会话内容。
当我们发一条信息的时候,产生一条 -item 。接下来 返回内容后,也会产生一条 -item ,要实现打字效果就是这条 -item 。
我们只需要将这条 -item 的内容,通过 方式分片渲染就可以了。比如我们想打字实现 ‘您好GPT’,那么分五次 渲染就可以了,比如如下:
如上就是分五次渲染,每一次渲染的结果。接下来就是代码的实现。
this.handleRequestResolve(data.text)
比如 每次返回一条内容,都用 函数处理返回的内容。看一下 的核心实现。
handleRequestResolve(result){
const timestamp = Date.now();
const index = this.data.messageList.length
const newMessageList = `messageList[${index}]`
const contentCharArr = result.trim().split("")
const content_key = `messageList[${index}].content`
const finished_key = `messageList[${index}].finished`
this.setData({
thinking: false,
[newMessageList]: {
id: timestamp,
role: 'assistant',
finished: false
}
})
currentContent = ''
this.showText(0, content_key, finished_key, contentCharArr);
}
在 中会构建一条新的 -item ,然后就是 展示内容,来看一下 怎么处理内容。
showText(key = 0, content_key, finished_key, value) {
/* 所有内容展示完成 */
if (key >= value.length) {
this.setData({
loading: false,
[finished_key]: true
})
wx.vibrateShort()
return;
}
currentContent = currentContent + value[key]
/* 渲染回话内容 */
this.setData({
[content_key]: currentContent,
})
setTimeout(() => {
/* 递归渲染内容 */
this.showText(key + 1, content_key, finished_key, value);
}, 50);
},
这样用递归就实现了打字效果。我们来看一下效果:
通过上面可以看到,在文字打印的过程中,列表不能跟随一起滚动,当文字内容超出一屏幕之后,视图就停止了(本质上数据在后面追加),这是一个很不好的效果。
接下来,我们进行优化处理,让视图可以根据内容自动滚动。
三 如何实现视图跟随内容滚动3.1 实现原理
实现视图跟随内容滚动实际很简单,因为 -item 的容器本质上就是一个 -view , 那么想要 -view 视图跟随返回内容变化,只需要动态设置 -view 的 -top 值就可以了。
视图跟随内容滚动,本质上就是让 -view 一直自动滚动到底部, 如何要让 -view 一直滚动到底部呢?先看一下如下示意图:
如上可以看到,想让 -view 一直滚动到底部,只需要让 -top 等于 -view 内容高度减去 -view 容器本身高度就可以了。
所以需要我们给 -view 里面的内容,用一个 view 包裹如下:
如上 -view 的类名为 , -view 内部元素的类名为 -view-,接下来可以通过如下代码设置 -top 值了。
handleScollTop() {
return new Promise((resolve) => {
const query = wx.createSelectorQuery()
query.select('.content').boundingClientRect()
query.select('.scroll-view-content').boundingClientRect()
query.exec((res) => {
const scrollViewHeight = res[0].height
const scrollContentHeight = res[1].height
if (scrollContentHeight > scrollViewHeight) {
const scrollTop = scrollContentHeight - scrollViewHeight
this.setData({
scrollTop
}, () => {
resolve()
})
}else{
resolve()
}
})
})
},
如上通过 分别获取 -view 和 -view 内部元素的高度,两者的差值就是 -top 值。
接下里在渲染会话内容的时候,渲染之后,调用 来动态设置 -top 就可以了。
showText(key = 0, content_key, finished_key, value) {
if (key >= value.length) {
this.setData({
loading: false,
[finished_key]: true
})
wx.vibrateShort()
return;
}
currentContent = currentContent + value[key]
this.setData({
[content_key]: currentContent,
},()=>{
this.handleScollTop().then(()=>{
setTimeout(() => {
this.showText(key + 1, content_key, finished_key, value);
}, 20);
})
})
},
这里有一个小细节,就是在渲染上一次文本内容之后,需要先校验一下 -top 值,然后再次调用 来渲染会话内容。
我们来看一下效果。
后续优化: 本质上不需要在每次 之后都通过 异步获取元素 -top 并再次渲染,这无疑是性能的浪费,实际可以控制 到 设置 -top 值的频率来提升性能。
四 总结
感兴趣的同学可以自己实现一个会话打字效果,其中还有很多小细节这里就不讲了。
号外,号外,号外
移动端小册上新,助力进阶大前端工程师
目前前端日常开发的工作,已经从传统的 web 浏览器端,进入了移动端时代,移动端需求日益增多。
学好移动端开发变的非常重要,这里我写了一本《大前端跨端开发指南》的小册,本小册从小程序到 RN 再到 DSL ,全方位讲解前端跨端技术。
适合的人群如下:
为了感谢大家对我的信任,弄了几个五折码 奉上,先到先得。