不会打断阅读体验的直播聊天滚动功能
一种React聊天滚动组件,能够跟随底部的实时消息滚动,当用户向上滚动时会暂停,同时提供便捷方式让用户重新回到下方。
beUI的MessageScroller背后的滚动机制
向上滚动可以重新阅读某句话。此时助手仍在生成内容,又有新的数据块到达,视图区域就会跳回最新的一行。
你再次向上滚动。
再精美的消息气泡也无法挽救那些让用户阅读体验不佳的聊天界面。在构建专注于动画与交互的React组件库beUI(beui.dev)时,正是针对这类交互问题进行了优化。
MessageScroller的设计核心问题是:何时让视图区域跟随流式回复移动,何时保持不动?
以下为不同状态说明
一种简单的处理方式是每次消息内容变化时都滚动到底部。这种方式在查看最新回复时尚可,但一旦需要查看之前的内容,就会显得很不方便。
应将不同场景视为不同的状态来处理:
- 跟随滚动:读者当前位于页面底部附近,因此新增内容可能会使视口向上移动。
- 阅读 earlier 的消息:读者已经离开底部区域,因此新内容不得将其再次拉向下方。
可滚动区域底部的距离决定了 beUI 内部的决策逻辑:
const distanceFromEnd =
viewport.scrollHeight -
viewport.scrollTop -
viewport.clientHeight;
const following = distanceFromEnd <= followThreshold;
默认阈值是 56 像素——这是设置在底部附近的一个可调缓冲区,避免因微小滚动就立即中断跟随功能。
刻意避免完全相等的情况。即使高度数值看似为整数,滚动偏移量也可能存在小数部分——MDN 上关于浏览器 scrollHeight 行为的文档也提到了这一点。
关注内容的动态变化
流式加载通常会修改现有消息而非添加新行。即使段落增加了多行,消息总数仍可能保持不变。
beUI 使用 ResizeObserver 监控内容容器。当尺寸发生变化时,它会在滚动到末尾之前重新检查相关操作是否仍有效。
这种方式将行为与实际渲染的几何形状绑定,而非假设每个内容块都会改变布局。图片加载或工具扩展后导致高度变化的情况也会以相同方式被检测到——ResizeObserver 的存在正是为了报告元素的尺寸变化。
限制自动滚动并不会使文档冻结。没有预留空间的前面图片仍可能挤占后面的文本。保持媒体尺寸稳定以及消息布局可预测仍是产品需要解决的问题。
需要用户手动中断
程序控制的滚动也会触发滚动事件。这些事件不应被误认为是用户选择了新位置。
MessageScroller 会监测自身是否正在滚动。滚轮输入、触摸手势以及上页键等操作会解除该限制,从而使后续事件能够根据真实的阅读者动作来更新对应标志。
当内容持续输入时,这种区分非常重要。新生成的片段应当更新显示的文本,但并不能因此抢占正在被阅读的段落的焦点。
让返回操作更有目的性
当阅读者滚动到底部后,再次返回应当是经过刻意设计的。
MessageScroller 可以在消息行上方显示简洁的导航栏。每个标记都配有文本预览,这样即使内容很长,也能无需滚动全部内容即可快速浏览。
选择更早的标记会禁用跟随功能并滚动到该消息处。选择最后一个标记则返回末尾并重新启用跟随功能。在底部阈值内手动向上滚动也能恢复跟随功能。
该组件会发出onFollowChange信号,以便应用可以显示自己的“返回最新内容”控件。该控件属于应用层面的实现,而滚动条则是内置的导航工具。
平滑滚动仍应可选
默认情况下平滑跟随功能是开启的。用户可以通过smooth属性将其关闭,而在要求降低动画效果时,该组件会回退到立即滚动的方式。
无论采用哪种模式,其逻辑都应保持简单:在底部跟随内容,允许在上方阅读,当读者选择继续时再恢复跟随。
视口滚动并不能替代无障碍提示功能。应以日志形式的实时区域展示对话内容,并通过礼貌的更新提示及忙碌状态标志来告知用户;需根据自身的渲染器和屏幕阅读器进行验证,切勿默认现有设置即可满足需求。
测试用户滚动离开时的情况
简短的对话无法充分考验这一逻辑。建议使用仍在持续生成的较长回复进行测试:
- 始终显示最新内容,确保新生成的行清晰可见。
- 在消息仍在不断出现时向上滚动,确认视口未被拖动向下。
- 通过键盘和消息栏跳转至之前的对话内容,同时保持当前位置不变。
- 返回最新消息后,检查后续对话内容是否再次正常显示。
- 在开启降低动画效果的情况下,以及使用高度会变化的文本内容(如图片、工具面板)时,重复进行上述测试。
这些设计正是让智能体用户界面显得贴心的原因之一:在用户的注意力仍停留在指定位置时,内容生成可以持续进行。
MessageScroller 的演示版本及源代码位于 beUI 网站的智能体板块下。发送一条测试消息,然后在回复逐步显示的同时向上滚动页面。