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

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完成方案”、“反向渲染方案”;这三种方案各有利弊,希望能对读者带来一些启发和帮助。

线下活动推荐:

时间:4月9日(周日)14:00-18:00

主题:得物技术沙龙-安全专场

地点:上海市杨浦区黄兴路221号互联宝地C2栋5楼 培训教室

活动亮点:本次沙龙聚焦于行业安全前沿最佳实践,将通过得物安全白皮书分享、企企业安全体系建设经验、零信任安全介绍、数据安全治理手段等多个维度,来讲述安全管理在当前企业中遇到的挑战和解决方案。

报名链接:https://www.huodongxing.com/event/4693455809600?td=5304793668787

图片

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论