会话历史
一个长期使用的 AI 对话,历史消息很容易累积到几万条。这一节讲三件事: 往上翻历史、翻历史期间收到新消息、以及在会话之间切换。
基础的 loadMore 模型见 分页与无限加载, 这里只讲对话场景特有的部分。
下面这个示例就带着向上加载:滚到顶部会拉取更早的对话, 生成过程中向上翻历史时收到的新消息会记成未读数。
只实现向上加载
对话的「未来」是用户自己产生的,不需要从服务端拉取,所以只实现 top 方向, 并关掉底部方向(hasMoreBottom 置 false)。头部插入后视口内容会留在原处, 不需要任何手动的位移补偿。
向上的加载不会自动触发:偏移量为 0 是初始常态而不是用户意图, 若自动触发,任何配了 loadMore 的对话在挂载瞬间就会开始无限向上拉取。 要更早的消息,必须由「主动向上滚到顶」这个动作来表达。
翻历史期间来了新消息
用户正在向上读历史时收到新消息(自己在别的设备上发的、或者协作场景里对方发的), 视口不该被拽到底部。stickyBottom 只在原本就贴底时才跟随, 其余情况把新增量记在 loadState.pendingNew 里,用它渲染「N 条新消息」角标。
用户滚回底部后 pendingNew 自动归零,不需要自己清理。
切换会话
切换到另一个会话时,不只是数据换了——尺寸缓存、滚动位置、头部快照 都属于旧会话,必须一并丢弃:先 reset(),再灌新数据,再 scrollToBottom()。
只换数据是不够的:旧会话的实测尺寸会被当成新数据的尺寸用,首屏位置就会偏。
跳到某一条消息
引用跳转、搜索结果定位都用 scrollToIndex(index, { align: 'start' })。 目标项此刻多半还没渲染、只能按预估尺寸落位;渲染出来量到真实尺寸后位置会自动修正, 所以不定高的消息列表里也不需要自己算偏移量。
未渲染的项搜不到
浏览器的页内搜索(Ctrl+F)只能找到当前渲染的那几十项。会话内搜索需要在数据里 匹配,再跳过去并高亮,不能依赖浏览器的查找功能——具体做法见搜索与引用跳转。
Vanilla 写法
const vl = new VirtList(
container,
{
list,
itemKey: 'id',
estimatedSize: 80,
initialPosition: 'bottom',
stickyBottom: true,
hasMoreBottom: false,
loadMore: async (direction) => {
if (direction !== 'top') return false;
const older = await fetchHistory(cursor);
list = older.concat(list);
vl.setList(list);
// 返回该方向是否还有更多;返回 false 后不再触发
return older.length > 0;
},
},
{
loadStateChange: (state) => {
jumpBtn.textContent =
state.pendingNew > 0 ? `${state.pendingNew} 条新消息` : '回到底部';
},
},
);
function switchConversation(messages) {
vl.reset(); // 清空尺寸缓存与滚动状态
vl.setList(messages);
vl.scrollToBottom(); // 新会话同样从最新消息开始看
}