客服IM消息列表虚拟滚动技术实践

开发 前端
在IM会话消息列表体验优化事项中我们对“上拉加载”、“下拉加载”、“下拉刷新”的技术特点和使用场景做了分析,然后对于下拉加载精确回滚这个场景,提出了三种解决方案:“定时器方案”、“等待图片/视频资源onload完成方案”、“反向渲染方案”;这三种方案各有利弊,希望能对读者带来一些启发和帮助。​

1、场景分析

在IM系统中,核心事件都是围绕着“聊天”这个主题展开的,在聊天的过程中,获悉用户的需求,再通过系统集成的各种工具,帮助用户完成诉求;“聊天”在IM业务中就是“会话消息”,当客服与用户之间存在大量聊天消息的时候,如何更好的去加载用户历史消息,提升客服查看消息体验,是一个值得研究的方向。

由于聊天室的特殊布局,历史消息加载需要用到虚拟滚动的方式去实现,如果想要更好的性能,还需要使用虚拟列表技术,而虚拟滚动技术又分为“上拉加载”和“下拉加载”,在移动端领域,还需要“下拉刷新”,如何选择合适的技术方案是我们接下来需要讨论的问题。

2、虚拟滚动技术调研

虚拟滚动技术的使用场景主要是在布局空间较小,不方便添加分页器的页面,例如移动端列表页,IM系统左侧进线会话列表,会话消息列表,右侧功能区域订单/商品查询列表等。

例如:会话进线列表,商品查询列表可以用到上拉加载,会话消息列表需要用到下拉加载,在移动端,页面刷新还需要用到下拉刷新。

图片

下拉加载、上拉加载、下拉刷新方案对比:

技术方案

触发方式

应用场景

技术特点/难点

下拉加载

滚动到页面顶部触发

会话消息列表数据加载

需要解决回滚定位不准的问题,还需要关注页面图片/视频资源的对滚动定位的影响

上拉加载

滚动到页面底部触发

订单/商品列表数据加载,select下拉框,移动端列表页面

需要计算滚动到页面底部,加载滚动体验较好,更符合用户的视觉感受

下拉刷新

拖动页面顶部向下移动一定距离触发

H5页面刷新

需要处理好下拉橡皮筋效果,成功后刷新页面

上面对我们系统中需要用到的加载/刷新技术做了简单的实现和应用场景对比,其中上拉加载,下拉刷新不作为此次讨论的重点,且社区中实现的方案和博客也较多,我们此次重点讨论的是下拉加载在IM会话消息中的应用和体验优化。

3、下拉加载在会话消息的应用

3.1   会话消息历史数据下拉加载流程

历史数据拉取会经历三个过程:

  • 用户滚动消息到页顶,触发加载机制,在拉取数据的过程中,顶部展示一个“数据正在加载中”的loading文案,告知用户需要等待加载结果的完成;
  • 数据返回之后,会被置于原数据的顶部(array.unshift(newArray)),渲染后原来的内容就会被新的内容压到页面底部;
  • 为了提高用户的体验,还需要将页面滚动到滚动条最后停留的位置(加载前最后一条消息位置)

图片

3.2   如何实现下拉加载

  • 监听页面scroll事件
// 监听会话消息区域添加滚动监听事件
const listenScrollEvent = () => {
chatMsgContainer.value.addEventListener('scroll', scrolHandle)
}


// 滚动逻辑处理回调函数
const scrolHandle = throttle(event => {
const { scrollHeight, scrollTop } = chatMsgContainer.value || {}
const { target } = event || {}
// 记录下当前会话滚动位置,切换会话的时候需要回滚到最后停留的位置
userInfo.value.scrollPosition = scrollHeight - scrollTop || 0
// 超出一屏,滚动到顶部,且没有拉取完所有的数据
if (
target.scrollTop === 0 &&
target.scrollHeight > target.clientHeight &&
!userInfo.value?.isComplete
) {
handleScrollEvent(event) // 拉取历史消息
}
}, 300)
  • 监听数据变化执行回滚动作
// 消息滚动
const handleMessageScroll = (len: number, oldLen: number) => {
if (!len) return
let msgScrollTimer = null
let targetDom = null
nextTick(() => {
// 获取到加载后最后一条数据位置
const recentlyMsg = messagePools[len - 1]
// 计算新加载数据条数
const calcMsgLenDiff = len - oldLen
// 首次加载数据的时候让滚动条滚动到最底部
if (len <= LIMIT_MESSAGE) {
// msgid是会话中的唯一标识,可以用此作为唯一ID
targetDom = document.querySelector(recentlyMsg.msgid)
// true 元素的顶部将对齐到可滚动祖先的可见区域的顶部。对应于scrollIntoViewOptions: {block: "start", inline: "nearest"}
firstDom?.scrollIntoView?.(true)
} else if (calcMsgLenDiff <= 1 && !recentlyMsg?.isHistory) {
// 这里用来处理用户/客服发送消息滚动逻辑
handleUserOrCustomerMsg()
} else if (calcMsgLenDiff >= 1) {
// 拉取历史消息逻辑
// 获取到加载前最后一条数据位置
const prevLastMsg = messagePools[calcMsgLenDiff - 1]
targetDom = document.querySelector(prevLastMsg.msgid)
targetDom?.scrollIntoView?.()
}
userInfo.value.isShowLoading = false
})
}


// 监听会话消息数据变化
watch(
() => messagePools.length,
(len, oldLen) => {
handleMessageScroll(len, oldLen)
},
{
immediate: true
}
)
  • 下拉加载体验优化方案及效果

如果只是按照上面的方式去处理,当页面中存在图片/视频的情况下,由于图片/视频渲染慢于普通文本,在加载图片/视频类型的消息的时候,回滚的位置就会有偏差,不能准确的回滚到预期的位置,我们对以下三种方案进行了对比实现,最终选择了反向渲染加载的方案,如下:

3.2.1   setTimeout延时回滚方案

  • 优点:简单易实现,只需要设置一个合适的定时器时间,对于大部分场景都能回滚正确;
  • 缺点:可靠性较低,资源加载慢的情况下,也会出现回滚不准确的情况,且setTimeout会带来页面闪烁的问题;

// 消息滚动
const handleMessageScroll = (len: number, oldLen: number) => {
if (!len) return
let msgScrollTimer = null
let targetDom = null
nextTick(() => {
// 获取到加载后最后一条数据位置
const recentlyMsg = messagePools[len - 1]
// 计算新加载数据条数
const calcMsgLenDiff = len - oldLen
// 首次加载数据的时候让滚动条滚动到最底部
if (len <= LIMIT_MESSAGE) {
...
// 针对图片/视频渲染慢的场景做个补偿
msgScrollTimer = setTimeout(() => {
clearTimeout(msgScrollTimer)
firstDom?.scrollIntoView?.(true)
}, SCROLL_THRESHOLD)
} else if (calcMsgLenDiff <= 1 && !recentlyMsg?.isHistory) {
// 这里用来处理用户/客服发送消息滚动逻辑
handleUserOrCustomerMsg()
} else if (calcMsgLenDiff >= 1) {
// 拉取历史消息逻辑
// ...
// 针对图片/视频渲染慢的场景做个补偿
msgScrollTimer = setTimeout(() => {
clearTimeout(msgScrollTimer)
targetDom?.scrollIntoView?.()
}, SCROLL_THRESHOLD)
}
userInfo.value.isShowLoading = false
})
}

3.2.2   监听img/vedio的onload事件方案

  • 优点:可以回滚的精准度较高,没有页面闪烁的问题;
  • 缺点:如果不是虚拟列表,每次滚动的时候可能会有大量的DOM节点查询操作,造成页面滚动卡顿;
const allImgOrVedioLoaded = async() => {
const imgNodes = document.querySelectorAll('.messageWrapper img') || []
const vedioNodes = document.querySelectorAll('.messageWrapper vedio') || []
const promises = [...imgNodes, ...vedioNodes]
// 等待所有的资源加载完成,无论成功还是失败
return await Promise.allSettled(
promises.map(source => {
new Promise(resolve => {
source.addEventListener('load', () => resolve(source))
})
})
)
}
// 消息滚动
const handleMessageScroll = (len: number, oldLen: number) => {
if (!len) return
let msgScrollTimer = null
let targetDom = null
nextTick(() => {
...
// 等待img/vedio所有资源加载完成,执行回滚操作
allImgOrVedioLoaded().then(() => {
firstDom.scrollIntoView(true)
})
} else if (calcMsgLenDiff <= 1 && !recentlyMsg?.isHistory) {
// 这里用来处理用户/客服发送消息滚动逻辑
handleUserOrCustomerMsg()
} else if (calcMsgLenDiff >= 1) {
// 拉取历史消息逻辑
// ...
// 等待img/vedio所有资源加载完成,执行回滚操作
allImgOrVedioLoaded().then(() => {
targetDom.scrollIntoView()
})
}
userInfo.value.isShowLoading = false
})
}

定时器/onload方案下拉加载回滚流程图:

图片

3.2.3   反向渲染加载方案

前面我们有提到过“上拉加载”,当滚动到底部加载新的一页的数据,数据从底部添加,无需执行回滚动作,整体的体验更加流畅自然。

既然“上拉加载”有这么多好处,那我们可不可以使用这样的方式来模仿我们的“下拉加载”呢?显然是可以的,我们页面布局在使用flex布局的情况下,可以反转主轴,这样我们就可以像“上拉加载”一样,触发到页面底部的时候,就去拉取新的历史数据,且反向渲染只是数据的反转,并不会带来视觉上的反转;

display: flex;
flex-direction: column-reverse;

图片

图片

3.3   带来的效果

图片

4、总结

在IM应用中,会话消息列表扮演着很重要的角色,是用户与客服沟通结果最终呈现的地方,所以想要提升页面的加载性能和用户体验,下拉加载性能和体验一直是一个重要的指标,当然对于大列表组件最好结合使用虚拟列表技术,尽量少的DOM渲染和尽量精准的滚动效果才能给客服带来最极致的体验。

最后做个总结:在IM会话消息列表体验优化事项中我们对“上拉加载”、“下拉加载”、“下拉刷新”的技术特点和使用场景做了分析,然后对于下拉加载精确回滚这个场景,提出了三种解决方案:“定时器方案”、“等待图片/视频资源onload完成方案”、“反向渲染方案”;这三种方案各有利弊,希望能对读者带来一些启发和帮助。


责任编辑:武晓燕 来源: 得物技术
相关推荐

2023-02-01 18:33:44

得物商家客服

2023-10-16 18:39:22

2015-06-10 15:36:47

环信移动客服

2023-12-07 19:48:42

2022-12-02 18:45:06

SOP机器人技术

2024-02-01 08:00:00

百川大模型角色大模型

2021-09-18 09:53:48

京东客服IM消息消息处理

2012-02-09 15:39:38

思科虚拟化技术医疗行业

2017-11-23 09:23:05

消息推送系统存储

2024-04-25 14:27:32

顺序消息事务消息

2023-07-04 15:56:08

DevOps开发测试

2014-06-24 09:47:33

虚拟运营商170

2021-12-10 10:29:07

在线客服系统

2023-12-22 10:04:34

携程负载均衡引擎

2023-12-30 13:47:48

Redis消息队列机制

2016-01-05 13:22:42

技术架构SaaS客服平台

2015-05-25 19:27:15

2023-11-28 08:49:01

短轮询WebSocket长轮询

2017-09-10 17:31:18

iOS渲染数据

2012-02-17 09:33:52

虚拟化桌面虚拟化
点赞
收藏

51CTO技术栈公众号