在IM系统中,核心事件都是围绕着“聊天”这个主题展开的,在聊天的过程中,获悉用户的需求,再通过系统集成的各种工具,帮助用户完成诉求;“聊天”在IM业务中就是“会话消息”,当客服与用户之间存在大量聊天消息的时候,如何更好的去加载用户历史消息,提升客服查看消息体验,是一个值得研究的方向。
由于聊天室的特殊布局,历史消息加载需要用到虚拟滚动的方式去实现,如果想要更好的性能,还需要使用虚拟列表技术,而虚拟滚动技术又分为“上拉加载”和“下拉加载”,在移动端领域,还需要“下拉刷新”,如何选择合适的技术方案是我们接下来需要讨论的问题。
虚拟滚动技术的使用场景主要是在布局空间较小,不方便添加分页器的页面,例如移动端列表页,IM系统左侧进线会话列表,会话消息列表,右侧功能区域订单/商品查询列表等。
例如:会话进线列表,商品查询列表可以用到上拉加载,会话消息列表需要用到下拉加载,在移动端,页面刷新还需要用到下拉刷新。

下拉加载、上拉加载、下拉刷新方案对比:
技术方案 | 触发方式 | 应用场景 | 技术特点/难点 |
下拉加载 | 滚动到页面顶部触发 | 会话消息列表数据加载 | 需要解决回滚定位不准的问题,还需要关注页面图片/视频资源的对滚动定位的影响 |
上拉加载 | 滚动到页面底部触发 | 订单/商品列表数据加载,select下拉框,移动端列表页面 | 需要计算滚动到页面底部,加载滚动体验较好,更符合用户的视觉感受 |
下拉刷新 | 拖动页面顶部向下移动一定距离触发 | H5页面刷新 | 需要处理好下拉橡皮筋效果,成功后刷新页面 |
上面对我们系统中需要用到的加载/刷新技术做了简单的实现和应用场景对比,其中上拉加载,下拉刷新不作为此次讨论的重点,且社区中实现的方案和博客也较多,我们此次重点讨论的是下拉加载在IM会话消息中的应用和体验优化。
历史数据拉取会经历三个过程:

// 监听会话消息区域添加滚动监听事件
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
}
)如果只是按照上面的方式去处理,当页面中存在图片/视频的情况下,由于图片/视频渲染慢于普通文本,在加载图片/视频类型的消息的时候,回滚的位置就会有偏差,不能准确的回滚到预期的位置,我们对以下三种方案进行了对比实现,最终选择了反向渲染加载的方案,如下:
// 消息滚动
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
})
}
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方案下拉加载回滚流程图:

前面我们有提到过“上拉加载”,当滚动到底部加载新的一页的数据,数据从底部添加,无需执行回滚动作,整体的体验更加流畅自然。
既然“上拉加载”有这么多好处,那我们可不可以使用这样的方式来模仿我们的“下拉加载”呢?显然是可以的,我们页面布局在使用flex布局的情况下,可以反转主轴,这样我们就可以像“上拉加载”一样,触发到页面底部的时候,就去拉取新的历史数据,且反向渲染只是数据的反转,并不会带来视觉上的反转;
display: flex;
flex-direction: column-reverse;


在IM应用中,会话消息列表扮演着很重要的角色,是用户与客服沟通结果最终呈现的地方,所以想要提升页面的加载性能和用户体验,下拉加载性能和体验一直是一个重要的指标,当然对于大列表组件最好结合使用虚拟列表技术,尽量少的DOM渲染和尽量精准的滚动效果才能给客服带来最极致的体验。
最后做个总结:在IM会话消息列表体验优化事项中我们对“上拉加载”、“下拉加载”、“下拉刷新”的技术特点和使用场景做了分析,然后对于下拉加载精确回滚这个场景,提出了三种解决方案:“定时器方案”、“等待图片/视频资源onload完成方案”、“反向渲染方案”;这三种方案各有利弊,希望能对读者带来一些启发和帮助。
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
是否有类似“RVMuse1”或“RVMuselist[0]”之类的内容而不是键入整个版本号。在任何时候,我们都会看到一个可能包含5个或更多ruby的列表,我们可以轻松地键入一个数字而不是X.X.X。这也有助于rvmgemset。 最佳答案 这在RVM2.0中是可能的=>https://docs.google.com/document/d/1xW9GeEpLOWPcddDg_hOPvK4oeLxJmU3Q5FiCNT7nTAc/edit?usp=sharing-知道链接的任何人都可以发表评论
我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c
我的工作要求我为某些测试自动生成电子邮件。我一直在四处寻找,但未能找到可以快速实现的合理解决方案。它需要在outlook而不是其他邮件服务器中,因为我们有一些奇怪的身份验证规则,我们需要保存草稿而不是仅仅发送邮件的选项。显然win32ole可以做到这一点,但我找不到任何相当简单的例子。 最佳答案 假设存储了Outlook凭据并且您设置为自动登录到Outlook,WIN32OLE可以很好地完成此操作:require'win32ole'outlook=WIN32OLE.new('Outlook.Application')message=
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
我正在使用Ruby,我正在与一个网络端点通信,该端点在发送消息本身之前需要格式化“header”。header中的第一个字段必须是消息长度,它被定义为网络字节顺序中的2二进制字节消息长度。比如我的消息长度是1024。如何将1024表示为二进制双字节? 最佳答案 Ruby(以及Perl和Python等)中字节整理的标准工具是pack和unpack。ruby的packisinArray.您的长度应该是两个字节长,并且按网络字节顺序排列,这听起来像是n格式说明符的工作:n|Integer|16-bitunsigned,network(bi
?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主
如果我在模型中设置验证消息validates:name,:presence=>{:message=>'Thenamecantbeblank.'}我如何让该消息显示在闪光警报中,这是我迄今为止尝试过的方法defcreate@message=Message.new(params[:message])if@message.valid?ContactMailer.send_mail(@message).deliverredirect_to(root_path,:notice=>"Thanksforyourmessage,Iwillbeintouchsoon")elseflash[:error]