Skip to content

MessageScroller 会话滚动容器 ​

会话滚动容器:一套「滚动意图模型」——读者在底部时跟随新内容(AI 式)、IM 式始终钉底可切、跳到底按钮、上翻加载历史不跳动(含异步资源保位)、轮次锚定(AI「回到最后提问」)、首屏防跳。组件不拥有消息(只提供滚动行为),宿主接任意后端。

基础用法 ​

会话流(default-position=end 默认打开在底部)

今天 10:00客客服小 O · 10:01您好,请问有什么可以帮您?我我 · 10:02想了解一下会话滚动容器的用法。客客服小 O · 10:03往上翻可以看到更多历史消息;新消息到来时如果您在底部就会自动跟随。系统消息:会话已加密客客服小 O · 10:05还有别的问题吗?

默认 default-position="end":打开即定位到底部。viewport 为 role="region" + 可读名称(缺省走 locale「消息列表」,可用 label 属性覆盖)+ tabindex="0"(Tab 聚焦后方向键滚动);内容容器 role="log" + aria-relevant="additions"(屏幕阅读器只播报新增行)。

首屏防跳(data-pending-scroll) ​

end / last-anchor 打开前的未定位帧不闪现顶部

重演首屏挂载点击按钮动态挂载一个 scroller,观察防跳时序。

default-position="end"(及 last-anchor)需要真实定位:连接时组件即在宿主上反射 data-pending-scroll,此刻 viewport visibility: hidden(保留布局供定位测量);首帧定位与属性移除在同一帧完成——读者看不到「闪现顶部再跳到底」的未定位帧。default-position="start" 天然定位(scrollTop=0 就是初始值),不设该反射。读者若在定位前已滚动(让位守卫生效),反射同样移除,viewport 不会被永久隐藏。

AI 式底部跟随(auto-scroll) ​

auto-scroll:读者在底部才跟随流式输出

帮我写一首关于秋天的短诗。
开始流式回答先往上翻离底部,再点按钮——阅读不被打断;回到底部后才跟随。

auto-scroll 是 AI 助手的默认安全模式:仅当读者在底部(near-bottom ≤ edge-threshold,默认 8px)时跟随流式增长;上翻阅读历史时不抢位置——「绝不逆着读者意图移动」。内容高度变化由 slotchange + ResizeObserver 双通道感知(纯文本增长无 slotchange 信号时仍能跟随)。

IM 式始终钉底(pin-to-bottom) ​

pin-to-bottom:无论在哪都拉回底部

IM 群聊场景:新消息必达。
来一条新消息往上翻再点按钮——也会被拉回底部(典型客服/IM 持续接收)。

pin-to-bottom 在场即 IM 模式:新内容总是滚到底,读者上翻也会被拉回。裁决优先级:pin-to-bottom > turn-anchor > auto-scroll。

跳底按钮与状态事件 ​

跳底按钮 + oas-scroll-state 反馈

占位内容 1——滚动容器内容超过视口高度时,离开底部会出现右下角跳底按钮。

占位内容 2

占位内容 3

占位内容 4

占位内容 5

占位内容 6(最底部)

scrollToStart()scrollToEnd()滚动后这里显示 oas-scroll-state 状态。

离开底部且可向下滚时显示内置跳底按钮(part="button",可读名称走 locale;也可 slot="button" 自定义);点击滚到底并消失。滚动位置跨过判定线时派发 oas-scroll-state(detail { atBottom, atTop, canScrollStart, canScrollEnd }),并反射 data-scrollable="start end"(供边缘渐隐等 CSS 判定)。scrollToEnd()/scrollToStart() 方法可编程定位(behavior 可传 'smooth')。

上翻加载历史(异步资源保位) ​

滚到顶加载更早消息,阅读位置不跳

滚到顶部触发加载更早 5 条——画面停留位置不变(顶部插入补偿 scrollTop)。

历史内图片延迟加载撑高,锚行位置不动

在顶部插入一批历史(含延迟加载的图片)插入后图片延迟撑高——锚行(首行)位置应保持不动。

顶部插入历史时,preserve-scroll-on-prepend(缺省开启,显式 ="false" 关闭)按「插入前首节点的内容坐标位移」精确补偿 scrollTop——同一帧既 prepend 历史又 append 新消息时,只结算顶部插入量(底部追加不参与补偿,不会多补)。补偿后组件锁定保位锚(插入前首节点):历史内的图片等异步资源随后加载撑高上方内容时(ResizeObserver 通道,无 slotchange 信号),按锚点视口位移持续补偿,阅读位置在资源加载完成前不跳。给消息行声明 stable message-id 时保位锚优先按 id 重查(宿主重建行后仍能找回);无 id 用元素引用。

打开位置 last-anchor 与 scrollToMessage ​

default-position=last-anchor:打开即回到最后提问位置

scrollToMessage('m1')

宿主在消息行上标 anchor 属性即轮次锚点(AI 场景通常标在用户提问行);default-position="last-anchor" 打开时定位到最后一个锚点的顶部——长会话回到「最后提问位置」继续阅读,而不是落底或回顶;锚点靠底(下方内容不足一屏)时贴底显示(滚动上限 clamp,与「最后提问在屏幕底部」的产品直觉一致)。无锚点时回退 end。scrollToMessage(id, opts?) 按 message-id 精确滚到某条消息顶部(命中返回 true,未知 id 返回 false 且不滚动)。

轮次锚定(turn-anchor) ​

turn-anchor:回答流入时提问钉在视口顶(prev-peek 露上一条语境)

跳到当前提问流式追加一段回答锚行(提问)在视口顶约 64px 处——追加回答时位置不动。

turn-anchor 在场把「一次提问 + 回答」作为锚定单元:跟随态把最后 anchor 标记的轮次锚点钉在视口顶 prev-peek px 处(默认 64,露出上一条尾部作语境),回答在锚下方流入。位置即意图——锚点贴视口顶(或处于 peek 对齐窗口)= 锚顶跟随;读者在底部 = 贴底跟随(新轮次从底部流入视野);两者皆离 = 读者接管,静止不拉回。跟随模式按当前位置一次性定型,贴底跟随中锚点穿越顶部窗口不会被误拉回顶部。

可见性通道(track-visible) ​

当前可见消息清单 + 当前锚点

滚动容器,这里显示 visibleMessageIds / currentAnchorId。

track-visible 在场开启可见性通道(pay-for-use,未开启零计算):可见集变化时派发 oas-visible-change(detail { visibleMessageIds, currentAnchorId }),并始终提供 visibleMessageIds / currentAnchorId getter(按需计算)。visibleMessageIds 是当前与视口相交的消息 id 清单(未声明 message-id 的行不计入);currentAnchorId 是「读者阅读位置所在的轮次」——最后一个视口顶附近的 anchor 标记行。供大纲菜单、搜索定位、已读追踪等使用。

长会话虚拟化(组合 oas-virtual-list) ​

scroller 负责常规会话的锚定/跟随;数千轮超长会话把消息数据交给 oas-virtual-list 做视口窗口渲染(只渲染可见行 + 缓冲),消息行仍用 oas-message-row / oas-bubble 渲染——虚拟化归 virtual-list,滚动意图归 scroller,职责互不重写。scrollToIndex(index, { align: 'end' }) 打开到最新、追加后跟随。

2000 条消息的虚拟化会话(追加一条试试)

追加一条新消息跳到最新

边界 ​

  • 可见性通道:track-visible 未开启时不计算、不派发(pay-for-use);开启后按视口相交计算(长会话逐行 rect 的代价由宿主开启该属性时承担)。未声明 message-id 的行不进可见集。
  • 虚拟化与渲染降耗:离屏行跳过渲染(content-visibility 等)随虚拟化一起提供(slotted 元素上单独启用会卡占位高度不展开,实测撤回);数千轮长会话接虚拟列表由宿主组合。
  • 空态:无消息时容器照常渲染(空 log),宿主放空态插槽内容即可。
  • prepend 保位边界:保位锚在读者真实滚动时释放(读者位置优先);pin-to-bottom / 贴底跟随时补偿无意义(钉底后发覆盖)。同帧混合插入的结算依赖插入前首元素仍在 DOM(宿主删除该行时该帧不补偿)。
  • 首屏防跳边界:data-pending-scroll 只覆盖「连接 → 首帧定位」这一段;宿主在挂载后异步拉取首批消息的场景由 auto-scroll/pin-to-bottom 的装载行为接管。pending 期间 viewport 不可聚焦(毫秒级窗口)。
  • label 覆盖 viewport 可读名称后被组件吸收移除(原生全局属性吸收惯例)。
  • anchor / message-id 是宿主写在 slotted 行上的协作标记(scroller 只读);Vue 宿主下两属性为普通透传属性,不会被框架剥离。

API ​

oas-message-scroller ​

属性 ​

属性说明类型默认值
auto-scrollAI 式底部跟随(布尔在场,默认关):仅当读者在底部(near-bottom ≤ edge-threshold)时跟随流式增长,上翻阅读不打扰boolean—
default-position打开位置:start(顶部)/ end(底部,默认;非法值回退 end)/ last-anchor(定位到最后一个 anchor 标记消息顶部——AI 场景「回到最后提问位置」;无锚点回退 end)——
edge-threshold底部判定阈值 px(默认 8)——距底 ≤ 阈值即视为在底部(near-bottom 检测)——
labelviewport 可读名称覆盖(缺省走 locale「消息列表」;读入渲染后从宿主移除——原生全局属性吸收惯例)——
pin-to-bottomIM 式始终钉底(布尔在场):新内容总是滚到底、读者上翻也拉回;与 auto-scroll 同时在场时优先boolean—
preserve-scroll-on-prepend顶部插入历史时补偿 scrollTop 保持阅读位置(缺省开启;显式 ="false" 关闭)——按插入前首节点位移精确结算(同帧 append+prepend 混合只结算顶部插入量),补偿后锁定保位锚:历史内异步资源(图片等)撑高上方内容时按锚点视口位移持续补偿(message-id 优先重查)——
prev-peek轮次锚定对齐时锚顶上方露出的语境 px(默认 64;非法/负值回退 64)——
track-visible布尔在场:开启可见性通道(oas-visible-change 事件 + visibleMessageIds / currentAnchorId getter);未开启零计算(pay-for-use)boolean—
turn-anchor轮次锚定(布尔在场):把一次提问+回答作为锚定单元——锚点贴视口顶时把最后 anchor 标记消息钉在视口顶 prev-peek 处,回答在锚下方流入;读者在底部延续贴底跟随,上翻即静止(绝不逆着读者意图移动)boolean—

事件 ​

事件说明
oas-scroll-state滚动位置跨过判定线时派发,detail { atBottom, atTop, canScrollStart, canScrollEnd };首帧(初始定位后)广播一次初始态——宿主监听晚于 upgrade 也能收到
oas-visible-changetrack-visible 开启时可见集变化派发,detail { visibleMessageIds, currentAnchorId }(供大纲/搜索定位等)

插槽 ​

名称说明
默认消息行列表(oas-message-row / oas-marker 等;行上的 anchor / message-id 属性为 scroller 协作标记)
button自定义跳底按钮内容(替换内置箭头图形;显隐与点击仍由组件裁决——缺省为内置按钮 part=button)

CSS 变量 ​

CSS 变量默认值
--oas-message-gapvar(--oas-space-4)

方法 ​

方法说明
scrollToEnd(opts?)滚到底部;opts.behavior 可传 'smooth'(缺省瞬时)
scrollToStart(opts?)滚到顶部(参数同上)
scrollToMessage(id, opts?)按 message-id 滚到指定消息行顶部;命中返回 true,未知 id 返回 false 且不滚动(参数同上)

状态反射:data-scrollable("start" / "end" / "start end" 空格分隔,无可滚为空);data-pending-scroll(首屏防跳——初始定位应用前在场,定位/读者让位后移除)。ARIA:viewport role="region" + 可读名称 + tabindex="0";内容容器 role="log" + aria-relevant="additions"。CSS 变量:--oas-message-gap(行间距,默认 var(--oas-space-4))。部件:::part(scroller) / ::part(viewport) / ::part(content) / ::part(button)。

基于 MIT OR Apache-2.0 双许可发布